├── getnative ├── __init__.py ├── __main__.py ├── utils.py └── app.py ├── requirements.txt ├── setup.py ├── LICENSE └── README.md /getnative/__init__.py: -------------------------------------------------------------------------------- 1 | __all__ = ['app', 'utils'] 2 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | matplotlib>=2.0.0 2 | VapourSynth>=45 -------------------------------------------------------------------------------- /getnative/__main__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | # Make sure the CLI-Parser does not print out __main__.py 4 | import sys, os 5 | sys.argv[0] = os.path.dirname(sys.argv[0]) 6 | 7 | # Run getnatvie. 8 | from getnative import app 9 | app.main() 10 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | from setuptools import setup, find_packages 4 | 5 | with open("README.md") as fh: 6 | long_description = fh.read() 7 | 8 | with open("requirements.txt") as fh: 9 | install_requires = fh.read() 10 | 11 | setup( 12 | name="getnative", 13 | version='3.0.0', 14 | description='Find the native resolution(s) of upscaled material (mostly anime)', 15 | long_description=long_description, 16 | long_description_content_type="text/markdown", 17 | author='Infi, Kageru', 18 | author_email='infiziert@protonmail.ch, kageru@encode.moe', 19 | url='https://github.com/Infiziert90/getnative', 20 | install_requires=install_requires, 21 | python_requires='>=3.6', 22 | packages=find_packages(), 23 | classifiers=[ 24 | "Programming Language :: Python :: 3", 25 | "License :: OSI Approved :: MIT License", 26 | "Operating System :: OS Independent", 27 | ], 28 | entry_points={ 29 | 'console_scripts': ['getnative=getnative.app:main'], 30 | } 31 | ) 32 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 kageru and Infi 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /getnative/utils.py: -------------------------------------------------------------------------------- 1 | import os 2 | import argparse 3 | import vapoursynth 4 | from typing import Union, Callable 5 | 6 | 7 | class GetnativeException(BaseException): 8 | pass 9 | 10 | 11 | def plugin_from_identifier(core: vapoursynth.Core, identifier: str) -> Union[Callable, None]: 12 | """ 13 | Get a plugin from vapoursynth with only the plugin identifier 14 | 15 | :param core: the core from vapoursynth 16 | :param identifier: plugin identifier. Example "tegaf.asi.xe" 17 | :return Plugin or None 18 | """ 19 | 20 | return getattr( 21 | core, 22 | core.get_plugins().get(identifier, {}).get("namespace", ""), 23 | None 24 | ) 25 | 26 | 27 | def vpy_source_filter(path): 28 | import runpy 29 | runpy.run_path(path, {}, "__vapoursynth__") 30 | return vapoursynth.get_output(0) 31 | 32 | 33 | def get_source_filter(core, imwri, args): 34 | ext = os.path.splitext(args.input_file)[1].lower() 35 | if imwri and (args.img or ext in {".png", ".tif", ".tiff", ".bmp", ".jpg", ".jpeg", ".webp", ".tga", ".jp2"}): 36 | print("Using imwri as source filter") 37 | return imwri.Read 38 | if ext in {".py", ".pyw", ".vpy"}: 39 | print("Using custom VapourSynth script as a source. This may cause garbage results. Only do this if you know what you are doing.") 40 | return vpy_source_filter 41 | source_filter = get_attr(core, 'lsmas.LWLibavSource') 42 | if source_filter: 43 | print("Using lsmas.LWLibavSource as source filter") 44 | return source_filter 45 | source_filter = get_attr(core, 'ffms2.Source') 46 | if source_filter: 47 | print("Using ffms2 as source filter") 48 | return lambda input_file: source_filter(input_file, alpha=False) 49 | source_filter = get_attr(core, 'lsmas.LSMASHVideoSource') 50 | if source_filter: 51 | print("Using lsmas.LSMASHVideoSource as source filter") 52 | return source_filter 53 | raise GetnativeException("No source filter found.") 54 | 55 | 56 | def get_attr(obj, attr, default=None): 57 | for ele in attr.split('.'): 58 | obj = getattr(obj, ele, default) 59 | if obj == default: 60 | return default 61 | return obj 62 | 63 | 64 | def to_float(str_value): 65 | if set(str_value) - set("0123456789./"): 66 | raise argparse.ArgumentTypeError("Invalid characters in float parameter") 67 | try: 68 | return eval(str_value) if "/" in str_value else float(str_value) 69 | except (SyntaxError, ZeroDivisionError, TypeError, ValueError): 70 | raise argparse.ArgumentTypeError("Exception while parsing float") from None 71 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Getnative 2 | Find the native resolution(s) of upscaled material (mostly anime) 3 | 4 | # Usage 5 | Start by executing: 6 | 7 | $ getnative [--args] inputFile 8 | 9 | If `getnative` could not be found, try executing this: 10 | 11 | # Linux 12 | $ python -m getnative [--args] inputFile 13 | 14 | # Windows 15 | $ py -3 -m getnative [--args] inputFile 16 | 17 | ***or*** 18 | 19 | Start by executing: 20 | 21 | $ python -m getnative [--args] inputFile 22 | 23 | That's it. 24 | 25 | # Requirements 26 | 27 | To run this script you will need: 28 | 29 | * Python 3.6 30 | * [matplotlib](http://matplotlib.org/users/installing.html) 31 | * [Vapoursynth](http://www.vapoursynth.com) R45+ 32 | * [descale](https://github.com/Irrational-Encoding-Wizardry/vapoursynth-descale) 33 | * [ffms2](https://github.com/FFMS/ffms2) **or** [lsmash](https://github.com/VFR-maniac/L-SMASH-Works) **or** [imwri](https://forum.doom9.org/showthread.php?t=170981) 34 | \*imwri: ImageMagick7 is required for macOS and Linux 35 | 36 | #### Installation 37 | Install it via: 38 | 39 | $ pip install getnative 40 | 41 | **or** 42 | 43 | * Download all files from github 44 | * Install all dependencies through `pip install -r requirements.txt` 45 | 46 | #### Recommended Windows Installation 47 | 48 | Install these programs from their websites: 49 | 50 | * [Vapoursynth](http://www.vapoursynth.com) (includes imwri) 51 | 52 | Once Vapoursynth is installed: 53 | 54 | vsrepo.py install descale 55 | vsrepo.py install ffms2 56 | vsrepo.py install lsmas 57 | 58 | Install getnative (and Python dependencies): 59 | 60 | pip install getnative 61 | 62 | # Example Output 63 | Input Command: 64 | 65 | $ getnative -k bicubic -b 0.11 -c 0.51 -dir "../../Downloads" "../../Downloads/unknown.png" 66 | 67 | Terminal Output: 68 | ``` 69 | Using imwri as source filter 70 | 71 | 500/500 72 | 73 | Output Path: /Users/infi/Downloads/results 74 | 75 | Bicubic b 0.11 c 0.51 AR: 1.78 Steps: 1 76 | Native resolution(s) (best guess): 720p 77 | 78 | done in 13.56s 79 | ``` 80 | 81 | Output Graph: 82 | 83 | ![alt text](https://nayu.moe/rIimgA) 84 | 85 | Output TXT (summary): 86 | ``` 87 | 715 | 0.0000501392 | 1.07 88 | 716 | 0.0000523991 | 0.96 89 | 717 | 0.0000413640 | 1.27 90 | 718 | 0.0000593276 | 0.70 91 | 719 | 0.0000617733 | 0.96 92 | 720 | 0.0000000342 | 1805.60 93 | 721 | 0.0000599182 | 0.00 94 | 722 | 0.0000554626 | 1.08 95 | 723 | 0.0000413679 | 1.34 96 | 724 | 0.0000448137 | 0.92 97 | 725 | 0.0000455203 | 0.98 98 | ``` 99 | 100 | # Args 101 | 102 | | Property | Description | Default value | Type | 103 | | -------- | ----------- | ------------------ | ---- | 104 | | frame | Specify a frame for the analysis. | num_frames//3 | Int | 105 | | kernel | Resize kernel to be used | bicubic | String | 106 | | bicubic-b | B parameter of bicubic resize | 1/3 | Float | 107 | | bicubic-c | C parameter of bicubic resize | 1/3 | Float | 108 | | lanczos-taps | Taps parameter of lanczos resize | 3 | Int | 109 | | aspect-ratio | Force aspect ratio. Only useful for anamorphic input| w/h | Float | 110 | | min-height | Minimum height to consider | 500 | Int | 111 | | max-height | Maximum height to consider | 1000 | Int | 112 | | is-image | Force image input | False | Action | 113 | | generate-images | Save detail mask as png | False | Action | 114 | | plot-scaling | Scaling of the y axis. Can be "linear" or "log" | log | String | 115 | | plot-format | Format of the output image. Specify multiple formats separated by commas. Can be svg, png, tif(f), and more | svg | String | 116 | | show-plot-gui | Show an interactive plot gui window. | False | Action | 117 | | no-save | Do not save files to disk. | False | Action | 118 | | stepping | This changes the way getnative will handle resolutions. Example steps=3 [500p, 503p, 506p ...] | 1 | Int | 119 | | output-dir | Sets the path of the output dir where you want all results to be saved. (/results will always be added as last folder) | (CWD)/results | String | 120 | 121 | # CLI Args 122 | 123 | | Property | Description | Default value | Type | 124 | | -------- | ----------- | ------------------ | ---- | 125 | | help | Automatically render the usage information when running `-h` or `--help` | False | Action | 126 | | | Absolute or relative path to the input file | Required | String | 127 | | mode | Choose a predefined mode \["bilinear", "bicubic", "bl-bc", "all"\] | None | String | 128 | | use | Use specified source filter (e.g. "lsmas.LWLibavSource") | None | String | 129 | 130 | # Warning 131 | This script's success rate is far from perfect. 132 | If possible, do multiple tests on different frames from the same source. 133 | Bright scenes generally yield the most accurate results. 134 | Graphs tend to have multiple notches, so the script's assumed resolution may be incorrect. 135 | Also, due to the current implementation of the autoguess, it is not possible for the script 136 | to automatically recognize 1080p productions. 137 | Use your eyes or anibin if necessary. 138 | 139 | # Thanks 140 | BluBb_mADe, kageru, FichteFoll, stux!, LittlePox 141 | 142 | # Help? 143 | 144 | Join https://discord.gg/3DYaV47 (Ask in #encode-autism for help) 145 | -------------------------------------------------------------------------------- /getnative/app.py: -------------------------------------------------------------------------------- 1 | import gc 2 | import os 3 | import time 4 | import asyncio 5 | import argparse 6 | import vapoursynth 7 | from pathlib import Path 8 | from functools import partial 9 | from typing import Union, List, Tuple 10 | from getnative.utils import GetnativeException, plugin_from_identifier, get_attr, get_source_filter, to_float 11 | try: 12 | import matplotlib as mpl 13 | import matplotlib.pyplot as pyplot 14 | except BaseException: 15 | import matplotlib as mpl 16 | mpl.use('Agg') 17 | import matplotlib.pyplot as pyplot 18 | 19 | """ 20 | Rework by Infi 21 | Original Author: kageru https://gist.github.com/kageru/549e059335d6efbae709e567ed081799 22 | Thanks: BluBb_mADe, FichteFoll, stux!, Frechdachs, LittlePox 23 | """ 24 | 25 | core = vapoursynth.core 26 | core.add_cache = False 27 | imwri = getattr(core, "imwri", getattr(core, "imwrif", None)) 28 | _modes = ["bilinear", "bicubic", "bl-bc", "all"] 29 | 30 | 31 | class _DefineScaler: 32 | def __init__(self, kernel: str, b: Union[float, int] = 0, c: Union[float, int] = 0, taps: int = 0): 33 | """ 34 | Get a scaler for getnative from descale 35 | 36 | :param kernel: kernel for descale 37 | :param b: b value for kernel "bicubic" (default 0) 38 | :param c: c value for kernel "bicubic" (default 0) 39 | :param taps: taps value for kernel "lanczos" (default 0) 40 | """ 41 | 42 | self.kernel = kernel 43 | self.b = b 44 | self.c = c 45 | self.taps = taps 46 | _d = plugin_from_identifier(core, "tegaf.asi.xe") 47 | self.plugin = _d if _d is not None else get_attr(core, 'descale', None) 48 | if self.plugin is None: 49 | return 50 | 51 | self.descaler = getattr(self.plugin, f'De{self.kernel}', None) 52 | self.upscaler = getattr(core.resize, self.kernel.title()) 53 | 54 | self.check_input() 55 | self.check_for_extra_paras() 56 | 57 | def check_for_extra_paras(self): 58 | if self.kernel == 'bicubic': 59 | self.descaler = partial(self.descaler, b=self.b, c=self.c) 60 | self.upscaler = partial(self.upscaler, filter_param_a=self.b, filter_param_b=self.c) 61 | elif self.kernel == 'lanczos': 62 | self.descaler = partial(self.descaler, taps=self.taps) 63 | self.upscaler = partial(self.upscaler, filter_param_a=self.taps) 64 | 65 | def check_input(self): 66 | if self.descaler is None and self.kernel == "spline64": 67 | raise GetnativeException(f'descale: spline64 support is missing, update descale (>r3).') 68 | elif self.descaler is None: 69 | raise GetnativeException(f'descale: {self.kernel} is not a supported kernel.') 70 | 71 | def __str__(self): 72 | return ( 73 | f"{self.kernel.capitalize()}" 74 | f"{'' if self.kernel != 'bicubic' else f' b {self.b:.2f} c {self.c:.2f}'}" 75 | f"{'' if self.kernel != 'lanczos' else f' taps {self.taps}'}" 76 | ) 77 | 78 | def __repr__(self): 79 | return ( 80 | f"ScalerObject: " 81 | f"{self.kernel.capitalize()}" 82 | f"{'' if self.kernel != 'bicubic' else f' b {self.b:.2f} c {self.c:.2f}'}" 83 | f"{'' if self.kernel != 'lanczos' else f' taps {self.taps}'}" 84 | ) 85 | 86 | 87 | common_scaler = { 88 | "bilinear": [_DefineScaler("bilinear")], 89 | "bicubic": [ 90 | _DefineScaler("bicubic", b=1 / 3, c=1 / 3), 91 | _DefineScaler("bicubic", b=.5, c=0), 92 | _DefineScaler("bicubic", b=0, c=.5), 93 | _DefineScaler("bicubic", b=0, c=.75), 94 | _DefineScaler("bicubic", b=1, c=0), 95 | _DefineScaler("bicubic", b=0, c=1), 96 | _DefineScaler("bicubic", b=.2, c=.5), 97 | _DefineScaler("bicubic", b=.5, c=.5), 98 | ], 99 | "lanczos": [ 100 | _DefineScaler("lanczos", taps=2), 101 | _DefineScaler("lanczos", taps=3), 102 | _DefineScaler("lanczos", taps=4), 103 | _DefineScaler("lanczos", taps=5), 104 | ], 105 | "spline": [ 106 | _DefineScaler("spline16"), 107 | _DefineScaler("spline36"), 108 | _DefineScaler("spline64"), 109 | ] 110 | } 111 | 112 | 113 | class GetNative: 114 | def __init__(self, src, scaler, ar, min_h, max_h, frame, mask_out, plot_scaling, plot_format, show_plot, no_save, 115 | steps, output_dir): 116 | self.plot_format = plot_format 117 | self.plot_scaling = plot_scaling 118 | self.src = src 119 | self.min_h = min_h 120 | self.max_h = max_h 121 | self.ar = ar 122 | self.scaler = scaler 123 | self.frame = frame 124 | self.mask_out = mask_out 125 | self.show_plot = show_plot 126 | self.no_save = no_save 127 | self.steps = steps 128 | self.output_dir = output_dir 129 | self.txt_output = "" 130 | self.resolutions = [] 131 | self.filename = self.get_filename() 132 | 133 | async def run(self): 134 | # change format to GrayS with bitdepth 32 for descale 135 | src = self.src[self.frame] 136 | matrix_s = '709' if src.format.color_family == vapoursynth.RGB else None 137 | src_luma32 = core.resize.Point(src, format=vapoursynth.YUV444PS, matrix_s=matrix_s) 138 | src_luma32 = core.std.ShufflePlanes(src_luma32, 0, vapoursynth.GRAY) 139 | src_luma32 = core.std.Cache(src_luma32) 140 | 141 | # descale each individual frame 142 | clip_list = [self.scaler.descaler(src_luma32, self.getw(h), h) 143 | for h in range(self.min_h, self.max_h + 1, self.steps)] 144 | full_clip = core.std.Splice(clip_list, mismatch=True) 145 | full_clip = self.scaler.upscaler(full_clip, self.getw(src.height), src.height) 146 | if self.ar != src.width / src.height: 147 | src_luma32 = self.scaler.upscaler(src_luma32, self.getw(src.height), src.height) 148 | expr_full = core.std.Expr([src_luma32 * full_clip.num_frames, full_clip], 'x y - abs dup 0.015 > swap 0 ?') 149 | full_clip = core.std.CropRel(expr_full, 5, 5, 5, 5) 150 | full_clip = core.std.PlaneStats(full_clip) 151 | full_clip = core.std.Cache(full_clip) 152 | 153 | tasks_pending = set() 154 | futures = {} 155 | vals = [] 156 | full_clip_len = len(full_clip) 157 | for frame_index in range(len(full_clip)): 158 | print(f"\r{frame_index}/{full_clip_len-1}", end="") 159 | fut = asyncio.ensure_future(asyncio.wrap_future(full_clip.get_frame_async(frame_index))) 160 | tasks_pending.add(fut) 161 | futures[fut] = frame_index 162 | while len(tasks_pending) >= core.num_threads + 2: 163 | tasks_done, tasks_pending = await asyncio.wait(tasks_pending, return_when=asyncio.FIRST_COMPLETED) 164 | vals += [(futures.pop(task), task.result().props.PlaneStatsAverage) for task in tasks_done] 165 | 166 | tasks_done, _ = await asyncio.wait(tasks_pending) 167 | vals += [(futures.pop(task), task.result().props.PlaneStatsAverage) for task in tasks_done] 168 | vals = [v for _, v in sorted(vals)] 169 | ratios, vals, best_value = self.analyze_results(vals) 170 | print("\n") # move the cursor, so that you not start at the end of the progress bar 171 | 172 | self.txt_output += 'Raw data:\nResolution\t | Relative Error\t | Relative difference from last\n' 173 | self.txt_output += '\n'.join([ 174 | f'{i * self.steps + self.min_h:4d}\t\t | {error:.10f}\t\t | {ratios[i]:.2f}' 175 | for i, error in enumerate(vals) 176 | ]) 177 | 178 | plot, fig = self.save_plot(vals) 179 | if not self.no_save: 180 | if not os.path.isdir(self.output_dir): 181 | os.mkdir(self.output_dir) 182 | 183 | print(f"Output Path: {self.output_dir}") 184 | for fmt in self.plot_format.replace(" ", "").split(','): 185 | fig.savefig(f'{self.output_dir}/{self.filename}.{fmt}') 186 | 187 | with open(f"{self.output_dir}/{self.filename}.txt", "w") as stream: 188 | stream.writelines(self.txt_output) 189 | 190 | if self.mask_out: 191 | self.save_images(src_luma32) 192 | 193 | return best_value, plot, self.resolutions 194 | 195 | def getw(self, h, only_even=True): 196 | w = h * self.ar 197 | w = int(round(w)) 198 | if only_even: 199 | w = w // 2 * 2 200 | 201 | return w 202 | 203 | def analyze_results(self, vals): 204 | ratios = [0.0] 205 | for i in range(1, len(vals)): 206 | last = vals[i - 1] 207 | current = vals[i] 208 | ratios.append(current and last / current) 209 | sorted_array = sorted(ratios, reverse=True) # make a copy of the array because we need the unsorted array later 210 | max_difference = sorted_array[0] 211 | 212 | differences = [s for s in sorted_array if s - 1 > (max_difference - 1) * 0.33][:5] 213 | 214 | for diff in differences: 215 | current = ratios.index(diff) 216 | # don't allow results within 20px of each other 217 | for res in self.resolutions: 218 | if res - 20 < current < res + 20: 219 | break 220 | else: 221 | self.resolutions.append(current) 222 | 223 | best_values = ( 224 | f"Native resolution(s) (best guess): " 225 | f"{'p, '.join([str(r * self.steps + self.min_h) for r in self.resolutions])}p" 226 | ) 227 | self.txt_output = ( 228 | f"Resize Kernel: {self.scaler}\n" 229 | f"{best_values}\n" 230 | f"Please check the graph manually for more accurate results\n\n" 231 | ) 232 | 233 | return ratios, vals, best_values 234 | 235 | # Modified from: 236 | # https://github.com/WolframRhodium/muvsfunc/blob/d5b2c499d1b71b7689f086cd992d9fb1ccb0219e/muvsfunc.py#L5807 237 | def save_plot(self, vals): 238 | plot = pyplot 239 | plot.close('all') 240 | plot.style.use('dark_background') 241 | fig, ax = plot.subplots(figsize=(12, 8)) 242 | ax.plot(range(self.min_h, self.max_h + 1, self.steps), vals, '.w-') 243 | dh_sequence = tuple(range(self.min_h, self.max_h + 1, self.steps)) 244 | ticks = tuple(dh for i, dh in enumerate(dh_sequence) if i % ((self.max_h - self.min_h + 10 * self.steps - 1) // (10 * self.steps)) == 0) 245 | ax.set(xlabel="Height", xticks=ticks, ylabel="Relative error", title=self.filename, yscale="log") 246 | if self.show_plot: 247 | plot.show() 248 | 249 | return plot, fig 250 | 251 | # Original idea by Chibi_goku http://recensubshq.forumfree.it/?t=64839203 252 | # Vapoursynth port by MonoS @github: https://github.com/MonoS/VS-MaskDetail 253 | def mask_detail(self, clip, final_width, final_height): 254 | temp = self.scaler.descaler(clip, final_width, final_height) 255 | temp = self.scaler.upscaler(temp, clip.width, clip.height) 256 | mask = core.std.Expr([clip, temp], 'x y - abs dup 0.015 > swap 16 * 0 ?').std.Inflate() 257 | mask = _DefineScaler(kernel="spline36").upscaler(mask, final_width, final_height) 258 | 259 | return mask 260 | 261 | def save_images(self, src_luma32): 262 | src = src_luma32 263 | first_out = imwri.Write(src, 'png', f'{self.output_dir}/{self.filename}_source%d.png') 264 | first_out.get_frame(0) # trick vapoursynth into rendering the frame 265 | for r in self.resolutions: 266 | r = r * self.steps + self.min_h 267 | image = self.mask_detail(src, self.getw(r), r) 268 | mask_out = imwri.Write(image, 'png', f'{self.output_dir}/{self.filename}_mask_{r:d}p%d.png') 269 | mask_out.get_frame(0) 270 | descale_out = self.scaler.descaler(src, self.getw(r), r) 271 | descale_out = imwri.Write(descale_out, 'png', f'{self.output_dir}/{self.filename}_{r:d}p%d.png') 272 | descale_out.get_frame(0) 273 | 274 | def get_filename(self): 275 | return ( 276 | f"f_{self.frame}" 277 | f"_{str(self.scaler).replace(' ', '_')}" 278 | f"_ar_{self.ar:.2f}" 279 | f"_steps_{self.steps}" 280 | ) 281 | 282 | 283 | async def getnative(args: Union[List, argparse.Namespace], src: vapoursynth.VideoNode, scaler: Union[_DefineScaler, None], 284 | first_time: bool = True) -> Tuple[str, pyplot.plot, GetNative]: 285 | """ 286 | Process your VideoNode with the getnative algorithm and return the result and a plot object 287 | 288 | :param args: List of all arguments for argparse or Namespace object from argparse 289 | :param src: VideoNode from vapoursynth 290 | :param scaler: DefineScaler object or None 291 | :param first_time: prevents posting warnings multiple times 292 | :return: best resolutions string, plot matplotlib.pyplot and GetNative class object 293 | """ 294 | 295 | if type(args) == list: 296 | args = parser.parse_args(args) 297 | 298 | output_dir = Path(args.dir).resolve() 299 | if not os.access(output_dir, os.W_OK): 300 | raise PermissionError(f"Missing write permissions: {output_dir}") 301 | output_dir = output_dir.joinpath("results") 302 | 303 | if (args.img or args.mask_out) and imwri is None: 304 | raise GetnativeException("imwri not found.") 305 | 306 | if scaler is None: 307 | scaler = _DefineScaler(args.kernel, b=args.b, c=args.c, taps=args.taps) 308 | else: 309 | scaler = scaler 310 | 311 | if scaler.plugin is None: 312 | if "toggaf.asi.xe" in core.get_plugins(): 313 | print("Error: descale_getnative support ended, pls use https://github.com/Irrational-Encoding-Wizardry/vapoursynth-descale") 314 | raise GetnativeException('No descale found!') 315 | 316 | if args.steps != 1 and first_time: 317 | print( 318 | "Warning for -steps/--stepping: " 319 | "If you are not completely sure what this parameter does, use the default step size.\n" 320 | ) 321 | 322 | if args.frame is None: 323 | args.frame = src.num_frames // 3 324 | elif args.frame < 0: 325 | args.frame = src.num_frames // -args.frame 326 | elif args.frame > src.num_frames - 1: 327 | raise GetnativeException(f"Last frame is {src.num_frames - 1}, but you want {args.frame}") 328 | 329 | if args.ar == 0: 330 | args.ar = src.width / src.height 331 | 332 | if args.min_h >= src.height: 333 | raise GetnativeException(f"Input image {src.height} is smaller min_h {args.min_h}") 334 | elif args.min_h >= args.max_h: 335 | raise GetnativeException(f"min_h {args.min_h} > max_h {args.max_h}? Not processable") 336 | elif args.max_h > src.height: 337 | print(f"The image height is {src.height}, going higher is stupid! New max_h {src.height}") 338 | args.max_h = src.height 339 | 340 | getn = GetNative(src, scaler, args.ar, args.min_h, args.max_h, args.frame, args.mask_out, args.plot_scaling, 341 | args.plot_format, args.show_plot, args.no_save, args.steps, output_dir) 342 | try: 343 | best_value, plot, resolutions = await getn.run() 344 | except ValueError as err: 345 | raise GetnativeException(f"Error in getnative: {err}") 346 | 347 | gc.collect() 348 | print( 349 | f"\n{scaler} AR: {args.ar:.2f} Steps: {args.steps}\n" 350 | f"{best_value}\n" 351 | ) 352 | 353 | return best_value, plot, getn 354 | 355 | 356 | def _getnative(): 357 | args = parser.parse_args() 358 | 359 | if args.use: 360 | source_filter = get_attr(core, args.use) 361 | if not source_filter: 362 | raise GetnativeException(f"{args.use} is not available.") 363 | print(f"Using {args.use} as source filter") 364 | else: 365 | source_filter = get_source_filter(core, imwri, args) 366 | 367 | src = source_filter(args.input_file) 368 | 369 | mode = [None] # default 370 | if args.mode == "bilinear": 371 | mode = [common_scaler["bilinear"][0]] 372 | elif args.mode == "bicubic": 373 | mode = [scaler for scaler in common_scaler["bicubic"]] 374 | elif args.mode == "bl-bc": 375 | mode = [scaler for scaler in common_scaler["bicubic"]] 376 | mode.append(common_scaler["bilinear"][0]) 377 | elif args.mode == "all": 378 | mode = [s for scaler in common_scaler.values() for s in scaler] 379 | 380 | loop = asyncio.get_event_loop() 381 | for i, scaler in enumerate(mode): 382 | if scaler is not None and scaler.plugin is None: 383 | print(f"Warning: No correct descale version found for {scaler}, continuing with next scaler when available.") 384 | continue 385 | loop.run_until_complete( 386 | getnative(args, src, scaler, first_time=True if i == 0 else False) 387 | ) 388 | 389 | 390 | parser = argparse.ArgumentParser(description='Find the native resolution(s) of upscaled material (mostly anime)') 391 | parser.add_argument('--frame', '-f', dest='frame', type=int, default=None, help='Specify a frame for the analysis. Random if unspecified. Negative frame numbers for a frame like this: src.num_frames // -args.frame') 392 | parser.add_argument('--kernel', '-k', dest='kernel', type=str.lower, default="bicubic", help='Resize kernel to be used') 393 | parser.add_argument('--bicubic-b', '-b', dest='b', type=to_float, default="1/3", help='B parameter of bicubic resize') 394 | parser.add_argument('--bicubic-c', '-c', dest='c', type=to_float, default="1/3", help='C parameter of bicubic resize') 395 | parser.add_argument('--lanczos-taps', '-t', dest='taps', type=int, default=3, help='Taps parameter of lanczos resize') 396 | parser.add_argument('--aspect-ratio', '-ar', dest='ar', type=to_float, default=0, help='Force aspect ratio. Only useful for anamorphic input') 397 | parser.add_argument('--min-height', '-min', dest="min_h", type=int, default=500, help='Minimum height to consider') 398 | parser.add_argument('--max-height', '-max', dest="max_h", type=int, default=1000, help='Maximum height to consider') 399 | parser.add_argument('--output-mask', '-mask', dest='mask_out', action="store_true", default=False, help='Save detail mask as png') 400 | parser.add_argument('--plot-scaling', '-ps', dest='plot_scaling', type=str.lower, default='log', help='Scaling of the y axis. Can be "linear" or "log"') 401 | parser.add_argument('--plot-format', '-pf', dest='plot_format', type=str.lower, default='svg', help='Format of the output image. Specify multiple formats separated by commas. Can be svg, png, pdf, rgba, jp(e)g, tif(f), and probably more') 402 | parser.add_argument('--show-plot-gui', '-pg', dest='show_plot', action="store_true", default=False, help='Show an interactive plot gui window.') 403 | parser.add_argument('--no-save', '-ns', dest='no_save', action="store_true", default=False, help='Do not save files to disk. Disables all output arguments!') 404 | parser.add_argument('--is-image', '-img', dest='img', action="store_true", default=False, help='Force image input') 405 | parser.add_argument('--stepping', '-steps', dest='steps', type=int, default=1, help='This changes the way getnative will handle resolutions. Example steps=3 [500p, 503p, 506p ...]') 406 | parser.add_argument('--output-dir', '-dir', dest='dir', type=str, default="", help='Sets the path of the output dir where you want all results to be saved. (/results will always be added as last folder)') 407 | def main(): 408 | parser.add_argument(dest='input_file', type=str, help='Absolute or relative path to the input file') 409 | parser.add_argument('--use', '-u', default=None, help='Use specified source filter e.g. (lsmas.LWLibavSource)') 410 | parser.add_argument('--mode', '-m', dest='mode', type=str, choices=_modes, default=None, help='Choose a predefined mode ["bilinear", "bicubic", "bl-bc", "all"]') 411 | starttime = time.time() 412 | _getnative() 413 | print(f'done in {time.time() - starttime:.2f}s') 414 | --------------------------------------------------------------------------------