nativeres 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- nativeres/__init__.py +3 -0
- nativeres/cli/__init__.py +313 -0
- nativeres/cli/components.py +171 -0
- nativeres/cli/helpers.py +115 -0
- nativeres/constants.py +1 -0
- nativeres/funcs.py +367 -0
- nativeres/kernels.py +17 -0
- nativeres/plotting.py +772 -0
- nativeres/py.typed +0 -0
- nativeres-0.1.0.dist-info/METADATA +363 -0
- nativeres-0.1.0.dist-info/RECORD +14 -0
- nativeres-0.1.0.dist-info/WHEEL +4 -0
- nativeres-0.1.0.dist-info/entry_points.txt +2 -0
- nativeres-0.1.0.dist-info/licenses/LICENSE +21 -0
nativeres/__init__.py
ADDED
|
@@ -0,0 +1,313 @@
|
|
|
1
|
+
"""CLI module"""
|
|
2
|
+
|
|
3
|
+
import sys
|
|
4
|
+
import warnings
|
|
5
|
+
from itertools import zip_longest
|
|
6
|
+
from logging import INFO, basicConfig, captureWarnings, getLogger
|
|
7
|
+
from typing import Annotated, Any, Literal, assert_never, cast
|
|
8
|
+
|
|
9
|
+
from jetpytools import SPath
|
|
10
|
+
from rich.console import Console
|
|
11
|
+
from rich.logging import RichHandler
|
|
12
|
+
from rich.pretty import pretty_repr
|
|
13
|
+
from rich.progress import BarColumn, Progress, TextColumn
|
|
14
|
+
from rich.style import Style
|
|
15
|
+
from rich.table import Table
|
|
16
|
+
from vskernels import ComplexKernel
|
|
17
|
+
from vsmasktools import EdgeDetect
|
|
18
|
+
from vssource import Indexer
|
|
19
|
+
|
|
20
|
+
from .. import funcs
|
|
21
|
+
from ..constants import HIGH_RATE, LOW_RATE
|
|
22
|
+
from ..kernels import default_kernels
|
|
23
|
+
from .components import (
|
|
24
|
+
app,
|
|
25
|
+
base_dim_opt,
|
|
26
|
+
crop_opt,
|
|
27
|
+
cull_rate_opt,
|
|
28
|
+
debug_opt,
|
|
29
|
+
dim_mode_opt,
|
|
30
|
+
dim_opt,
|
|
31
|
+
frame_opt,
|
|
32
|
+
global_debug_opt,
|
|
33
|
+
indexer_opt,
|
|
34
|
+
input_file_arg,
|
|
35
|
+
kernel_opt,
|
|
36
|
+
mask_opt,
|
|
37
|
+
metric_mode_opt,
|
|
38
|
+
radius_opt,
|
|
39
|
+
range_dim_opt,
|
|
40
|
+
show_default_kernels_opt,
|
|
41
|
+
show_vskernels_opt,
|
|
42
|
+
step_opt,
|
|
43
|
+
)
|
|
44
|
+
from .helpers import get_progress, get_videonode_from_input
|
|
45
|
+
|
|
46
|
+
console = Console(stderr=True)
|
|
47
|
+
|
|
48
|
+
warnings.filterwarnings("always")
|
|
49
|
+
captureWarnings(True)
|
|
50
|
+
basicConfig(
|
|
51
|
+
level=INFO,
|
|
52
|
+
handlers=[RichHandler(console=console)],
|
|
53
|
+
format="{name}: {message}",
|
|
54
|
+
style="{",
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
logger = getLogger(__name__)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
@app.callback()
|
|
61
|
+
def callback(
|
|
62
|
+
show_kernels: Annotated[bool, show_default_kernels_opt] = False,
|
|
63
|
+
show_vskernels: Annotated[bool, show_vskernels_opt] = False,
|
|
64
|
+
debug: Annotated[bool, debug_opt] = False,
|
|
65
|
+
global_debug: Annotated[bool, global_debug_opt] = False,
|
|
66
|
+
) -> None:
|
|
67
|
+
"""Descale analysis tools for VapourSynth."""
|
|
68
|
+
...
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
@app.command(
|
|
72
|
+
help="[bold]Determine the native resolution of upscaled material.[/]\n\n"
|
|
73
|
+
"Analyzes a range of dimensions to find which one produces the lowest error when inverse scaled.\n"
|
|
74
|
+
"Primary use case is finding the native resolution of upscaled anime.",
|
|
75
|
+
no_args_is_help=True,
|
|
76
|
+
)
|
|
77
|
+
def getnative(
|
|
78
|
+
input_file: Annotated[SPath, input_file_arg],
|
|
79
|
+
range_dim: Annotated[tuple[int, int] | None, range_dim_opt] = None,
|
|
80
|
+
dim_mode: Annotated[Literal["height", "width"], dim_mode_opt] = "height",
|
|
81
|
+
kernel: Annotated[ComplexKernel, kernel_opt] = cast(ComplexKernel, "bilinear"),
|
|
82
|
+
frame: Annotated[int, frame_opt] = 0,
|
|
83
|
+
step: Annotated[float, step_opt] = 1,
|
|
84
|
+
crop: Annotated[tuple[int, int, int, int] | None, crop_opt] = None,
|
|
85
|
+
metric_mode: Annotated[funcs.MetricMode, metric_mode_opt] = "MAE",
|
|
86
|
+
indexer: Annotated[Indexer, indexer_opt] = cast(Indexer, "bs"),
|
|
87
|
+
) -> None:
|
|
88
|
+
import numpy as np
|
|
89
|
+
from PySide6.QtWidgets import QApplication, QMainWindow, QStyle
|
|
90
|
+
|
|
91
|
+
from ..plotting import RescalePlotWidget
|
|
92
|
+
|
|
93
|
+
clip = get_videonode_from_input(input_file, indexer)
|
|
94
|
+
|
|
95
|
+
# Resolve dimension and the range of dimensions to check
|
|
96
|
+
if range_dim:
|
|
97
|
+
start, stop = range_dim
|
|
98
|
+
else:
|
|
99
|
+
match dim_mode:
|
|
100
|
+
case "height":
|
|
101
|
+
dim = clip.height
|
|
102
|
+
case "width":
|
|
103
|
+
dim = clip.width
|
|
104
|
+
case _:
|
|
105
|
+
assert_never(dim_mode)
|
|
106
|
+
|
|
107
|
+
start, stop = int(dim * LOW_RATE), int(dim * HIGH_RATE)
|
|
108
|
+
|
|
109
|
+
# Build the list of dims (int or fractional)
|
|
110
|
+
step_f = float(step)
|
|
111
|
+
if step_f.is_integer():
|
|
112
|
+
dims = range(start, stop + 1, int(step_f))
|
|
113
|
+
x_label_fmt = "%.0f"
|
|
114
|
+
else:
|
|
115
|
+
num = int((stop - start) / step_f) + 1
|
|
116
|
+
dims = np.linspace(start, start + step_f * (num - 1), num).tolist()
|
|
117
|
+
x_label_fmt = f"%.{str(step_f)[::-1].find('.') + 1}f"
|
|
118
|
+
|
|
119
|
+
# Pair with the fixed dimension
|
|
120
|
+
match dim_mode:
|
|
121
|
+
case "height":
|
|
122
|
+
dimensions = zip_longest([clip.width], dims, fillvalue=clip.width)
|
|
123
|
+
case "width":
|
|
124
|
+
dimensions = zip_longest(dims, [clip.height], fillvalue=clip.height)
|
|
125
|
+
case _:
|
|
126
|
+
assert_never(dim_mode)
|
|
127
|
+
|
|
128
|
+
# Pretty progress
|
|
129
|
+
progress = get_progress(console)
|
|
130
|
+
gtask_id = progress.add_task("Gathering data...", total=None)
|
|
131
|
+
|
|
132
|
+
logger.debug(kernel)
|
|
133
|
+
|
|
134
|
+
with progress:
|
|
135
|
+
results = funcs.getnative(
|
|
136
|
+
clip,
|
|
137
|
+
frame,
|
|
138
|
+
dimensions,
|
|
139
|
+
kernel,
|
|
140
|
+
crop,
|
|
141
|
+
metric_mode=metric_mode,
|
|
142
|
+
progress_cb=lambda curr, total: progress.update(gtask_id, advance=1, total=total, visible=True),
|
|
143
|
+
)
|
|
144
|
+
progress.update(gtask_id, total=100, completed=100, refresh=True)
|
|
145
|
+
|
|
146
|
+
dims, errors = zip(*results)
|
|
147
|
+
|
|
148
|
+
# Show the plot window
|
|
149
|
+
app = QApplication(sys.argv)
|
|
150
|
+
win = QMainWindow()
|
|
151
|
+
win.setWindowTitle("Native Resolution Analysis")
|
|
152
|
+
win.setWindowIcon(win.style().standardIcon(QStyle.StandardPixmap.SP_FileDialogContentsView))
|
|
153
|
+
win.resize(1000, 600)
|
|
154
|
+
|
|
155
|
+
plot = RescalePlotWidget(
|
|
156
|
+
f"Error plot - {kernel.pretty_string} on {dim_mode}",
|
|
157
|
+
[getattr(d, dim_mode) for d in dims],
|
|
158
|
+
errors,
|
|
159
|
+
dim_mode.title(),
|
|
160
|
+
)
|
|
161
|
+
plot.axis_x.setLabelFormat(x_label_fmt)
|
|
162
|
+
|
|
163
|
+
win.setCentralWidget(plot)
|
|
164
|
+
|
|
165
|
+
win.show()
|
|
166
|
+
app.exec()
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
@app.command(
|
|
170
|
+
help="[bold]Identify the best inverse scaler for a given resolution.[/]\n\n"
|
|
171
|
+
"Compares multiple kernels against a specific target resolution to determine which one "
|
|
172
|
+
"was likely used for the original upscaling.",
|
|
173
|
+
epilog="""
|
|
174
|
+
[dim]Notes:
|
|
175
|
+
|
|
176
|
+
- getscaler gives heuristic results; it's not infallible.
|
|
177
|
+
|
|
178
|
+
- Always visually verify the suggested scaler and parameters on multiple frames before trusting them.[/dim]
|
|
179
|
+
""",
|
|
180
|
+
no_args_is_help=True,
|
|
181
|
+
)
|
|
182
|
+
def getscaler(
|
|
183
|
+
input_file: Annotated[SPath, input_file_arg],
|
|
184
|
+
dim: Annotated[float, dim_opt],
|
|
185
|
+
dim_mode: Annotated[Literal["height", "width"], dim_mode_opt] = "height",
|
|
186
|
+
base_dim_opt: Annotated[int | None, base_dim_opt] = None,
|
|
187
|
+
kernels: Annotated[list[ComplexKernel], kernel_opt] = [],
|
|
188
|
+
frame: Annotated[int, frame_opt] = 0,
|
|
189
|
+
crop: Annotated[tuple[int, int, int, int] | None, crop_opt] = None,
|
|
190
|
+
metric_mode: Annotated[funcs.MetricMode, metric_mode_opt] = "MAE",
|
|
191
|
+
mask: Annotated[type[EdgeDetect] | None, mask_opt] = None,
|
|
192
|
+
indexer: Annotated[Indexer, indexer_opt] = cast(Indexer, "bs"),
|
|
193
|
+
) -> None:
|
|
194
|
+
clip = get_videonode_from_input(input_file, indexer)
|
|
195
|
+
|
|
196
|
+
# Resolve dimension to check
|
|
197
|
+
scaler_args: dict[str, Any] = {
|
|
198
|
+
"width": clip.width,
|
|
199
|
+
"height": clip.height,
|
|
200
|
+
dim_mode: dim,
|
|
201
|
+
f"base_{dim_mode}": base_dim_opt,
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
progress = Progress(
|
|
205
|
+
TextColumn("[progress.description]{task.description}"),
|
|
206
|
+
BarColumn(),
|
|
207
|
+
console=console,
|
|
208
|
+
transient=True,
|
|
209
|
+
)
|
|
210
|
+
task = progress.add_task("Gathering data...", total=None)
|
|
211
|
+
|
|
212
|
+
with progress:
|
|
213
|
+
ress = funcs.getscaler(
|
|
214
|
+
clip,
|
|
215
|
+
frame,
|
|
216
|
+
kernels=(*default_kernels, *kernels),
|
|
217
|
+
crop=crop,
|
|
218
|
+
metric_mode=metric_mode,
|
|
219
|
+
mask=mask,
|
|
220
|
+
**scaler_args,
|
|
221
|
+
)
|
|
222
|
+
progress.update(task, completed=100, total=100, visible=False, refresh=True)
|
|
223
|
+
|
|
224
|
+
# Results are sorted and displayed to the CLI for the user
|
|
225
|
+
sorted_ress = sorted(ress, key=lambda r: r.error)
|
|
226
|
+
best = sorted_ress[0]
|
|
227
|
+
|
|
228
|
+
logger.debug("%s", pretty_repr(sorted_ress, max_width=200, indent_size=2))
|
|
229
|
+
|
|
230
|
+
width, height = scaler_args["width"], scaler_args["height"]
|
|
231
|
+
|
|
232
|
+
dwidth = f"{width:.0f}" if float(width).is_integer() else f"{width:.3f}"
|
|
233
|
+
dheight = f"{height:.0f}" if float(height).is_integer() else f"{height:.3f}"
|
|
234
|
+
|
|
235
|
+
table = Table(
|
|
236
|
+
title=f"Results for frame {frame} — Resolution: {dwidth}x{dheight}",
|
|
237
|
+
title_style=Style(bold=True),
|
|
238
|
+
caption=f"Smallest error archieved by {best.kernel.pretty_string}: {best.error:.13f}",
|
|
239
|
+
caption_style=Style(bold=True, dim=True),
|
|
240
|
+
caption_justify="left",
|
|
241
|
+
min_width=80,
|
|
242
|
+
)
|
|
243
|
+
table.add_column("Kernel")
|
|
244
|
+
table.add_column("Error %", justify="center")
|
|
245
|
+
table.add_column(metric_mode, justify="right")
|
|
246
|
+
|
|
247
|
+
for res in sorted_ress:
|
|
248
|
+
table.add_row(
|
|
249
|
+
res.kernel.pretty_string, f"{res.error * 100 / best.error if best.error else 0:.2f} %", f"{res.error:.13f}"
|
|
250
|
+
)
|
|
251
|
+
|
|
252
|
+
console.rule()
|
|
253
|
+
console.print(table, new_line_start=True)
|
|
254
|
+
console.rule()
|
|
255
|
+
console.print(
|
|
256
|
+
"Getfscaler is not infallible!\n"
|
|
257
|
+
"Always visually verify the suggested scaler and parameters on multiple frames before trusting them.",
|
|
258
|
+
style=Style(color="yellow", dim=True),
|
|
259
|
+
)
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
@app.command(
|
|
263
|
+
help="[bold]Visualize the frequency distribution of a frame.[/]\n\n"
|
|
264
|
+
"Calculates the Discrete Cosine Transform (DCT) of the image rows/columns "
|
|
265
|
+
"to identify spikes that may indicate the native resolution or scaling artifacts.",
|
|
266
|
+
no_args_is_help=True,
|
|
267
|
+
)
|
|
268
|
+
def getfreq(
|
|
269
|
+
input_file: Annotated[SPath, input_file_arg],
|
|
270
|
+
frame: Annotated[int, frame_opt] = 0,
|
|
271
|
+
cull_rate: Annotated[float, cull_rate_opt] = 3.0,
|
|
272
|
+
radius: Annotated[int, radius_opt] = 50,
|
|
273
|
+
indexer: Annotated[Indexer, indexer_opt] = cast(Indexer, "bs"),
|
|
274
|
+
) -> None:
|
|
275
|
+
from PySide6.QtWidgets import QApplication, QMainWindow, QStyle
|
|
276
|
+
|
|
277
|
+
from ..funcs import get_dct_distribution
|
|
278
|
+
from ..plotting import FrequencyPlotWidget
|
|
279
|
+
|
|
280
|
+
clip = get_videonode_from_input(input_file, indexer)
|
|
281
|
+
|
|
282
|
+
progress = get_progress(console)
|
|
283
|
+
task = progress.add_task("Calculating DCT distribution...", total=None)
|
|
284
|
+
|
|
285
|
+
with progress:
|
|
286
|
+
dct_h, dct_v = get_dct_distribution(clip, frame, cull_rate=cull_rate)
|
|
287
|
+
progress.update(task, completed=100, total=100, visible=False, refresh=True)
|
|
288
|
+
|
|
289
|
+
min_val_h, max_val_h = int(clip.width * LOW_RATE), int(clip.width * HIGH_RATE)
|
|
290
|
+
min_val_v, max_val_v = int(clip.height * LOW_RATE), int(clip.height * HIGH_RATE)
|
|
291
|
+
|
|
292
|
+
# Show the plot window
|
|
293
|
+
app = QApplication(sys.argv)
|
|
294
|
+
win = QMainWindow()
|
|
295
|
+
win.setWindowTitle("Frequency Analysis")
|
|
296
|
+
win.setWindowIcon(win.style().standardIcon(QStyle.StandardPixmap.SP_FileDialogContentsView))
|
|
297
|
+
win.resize(1000, 600)
|
|
298
|
+
|
|
299
|
+
plot = FrequencyPlotWidget(
|
|
300
|
+
f"DCT Frequency - {input_file.name}",
|
|
301
|
+
dct_h,
|
|
302
|
+
dct_v,
|
|
303
|
+
min_val_h,
|
|
304
|
+
max_val_h,
|
|
305
|
+
min_val_v,
|
|
306
|
+
max_val_v,
|
|
307
|
+
check_radius=radius,
|
|
308
|
+
)
|
|
309
|
+
|
|
310
|
+
win.setCentralWidget(plot)
|
|
311
|
+
|
|
312
|
+
win.show()
|
|
313
|
+
app.exec()
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
from click import BadParameter
|
|
2
|
+
from jetpytools import SPath
|
|
3
|
+
from typer import Argument, Option, Typer
|
|
4
|
+
from vsmasktools import EdgeDetect
|
|
5
|
+
|
|
6
|
+
from ..funcs import resolve_kernel
|
|
7
|
+
from .helpers import (
|
|
8
|
+
resolve_dimension,
|
|
9
|
+
resolve_dimension_mode,
|
|
10
|
+
resolve_idx,
|
|
11
|
+
set_debug,
|
|
12
|
+
set_global_debug,
|
|
13
|
+
show_default_kernels,
|
|
14
|
+
show_vskernels,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
# DEBUG
|
|
18
|
+
debug_opt = Option(
|
|
19
|
+
"--debug",
|
|
20
|
+
help="Enable debug output.",
|
|
21
|
+
hidden=True,
|
|
22
|
+
is_eager=True,
|
|
23
|
+
callback=set_debug,
|
|
24
|
+
)
|
|
25
|
+
global_debug_opt = Option(
|
|
26
|
+
"--global-debug",
|
|
27
|
+
help="Enable global debug output.",
|
|
28
|
+
hidden=True,
|
|
29
|
+
is_eager=True,
|
|
30
|
+
callback=set_global_debug,
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
# App
|
|
35
|
+
app = Typer(
|
|
36
|
+
name="nativeres",
|
|
37
|
+
rich_markup_mode="rich",
|
|
38
|
+
pretty_exceptions_enable=True,
|
|
39
|
+
add_completion=False,
|
|
40
|
+
no_args_is_help=True,
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
# Commons
|
|
44
|
+
input_file_arg = Argument(
|
|
45
|
+
help="Path to the source material to analyze. "
|
|
46
|
+
"Supports videos, images, or VapourSynth scripts. For scripts, the first output is used.",
|
|
47
|
+
metavar="INPUT",
|
|
48
|
+
resolve_path=True,
|
|
49
|
+
parser=SPath,
|
|
50
|
+
)
|
|
51
|
+
frame_opt = Option(
|
|
52
|
+
"--frame",
|
|
53
|
+
"-f",
|
|
54
|
+
help="The specific frame number to extract and analyze from video inputs. Ignored for images.",
|
|
55
|
+
metavar="INTEGER",
|
|
56
|
+
rich_help_panel="Common",
|
|
57
|
+
)
|
|
58
|
+
kernel_opt = Option(
|
|
59
|
+
"--kernel",
|
|
60
|
+
"-k",
|
|
61
|
+
help="The kernel(s) to use for inverse scaling.\n\n"
|
|
62
|
+
"Can be a kernel name or a class call with parameters (e.g., 'Bicubic(b=0, c=0.5)').\n\n"
|
|
63
|
+
"Use --show-kernels for a list of available kernels.",
|
|
64
|
+
metavar="Kernel|Kernel(arg0=..., arg1=...)",
|
|
65
|
+
parser=lambda v: resolve_kernel(v, BadParameter),
|
|
66
|
+
rich_help_panel="Common",
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
dim_mode_opt = Option(
|
|
70
|
+
"--dim-mode",
|
|
71
|
+
"-dm",
|
|
72
|
+
help="Specifies whether to analyze based on the [bold]height[/] or [bold]width[/] of the frame.",
|
|
73
|
+
metavar="height|width|h|w",
|
|
74
|
+
parser=resolve_dimension_mode,
|
|
75
|
+
rich_help_panel="Common",
|
|
76
|
+
)
|
|
77
|
+
crop_opt = Option(
|
|
78
|
+
"--crop",
|
|
79
|
+
"-c",
|
|
80
|
+
help="Crop the input frame before analysis to remove black bars.\n\n"
|
|
81
|
+
"Format: [bold]LEFT RIGHT TOP BOTTOM[/] (e.g., '0 0 240 240').",
|
|
82
|
+
metavar="L R T B",
|
|
83
|
+
rich_help_panel="Common",
|
|
84
|
+
)
|
|
85
|
+
metric_mode_opt = Option(
|
|
86
|
+
"--metric-mode",
|
|
87
|
+
"-mm",
|
|
88
|
+
help="The mathematical metric used to compare scaling results.\n\n"
|
|
89
|
+
"- [bold]MAE[/] (Mean Absolute Error)\n\n"
|
|
90
|
+
"- [bold]MSE[/] (Mean Squared Error)\n\n"
|
|
91
|
+
"- [bold]RMSE[/] (Root Mean Squared Error)",
|
|
92
|
+
metavar="MAE|MSE|RMSE",
|
|
93
|
+
parser=lambda value: value.upper(),
|
|
94
|
+
rich_help_panel="Common",
|
|
95
|
+
)
|
|
96
|
+
indexer_opt = Option(
|
|
97
|
+
"--indexer",
|
|
98
|
+
"-idx",
|
|
99
|
+
help="The VapourSynth indexer used to load files.\n\nSpecifying the plugin namespace is also allowed (e.g., 'bs').",
|
|
100
|
+
metavar="STRING",
|
|
101
|
+
parser=resolve_idx,
|
|
102
|
+
show_default="BestSource",
|
|
103
|
+
rich_help_panel="Common",
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
# Helpers
|
|
107
|
+
show_default_kernels_opt = Option(
|
|
108
|
+
"--show-kernels",
|
|
109
|
+
help="Show the default checked kernels for getscaler and exit.",
|
|
110
|
+
is_eager=True,
|
|
111
|
+
show_default=False,
|
|
112
|
+
callback=show_default_kernels,
|
|
113
|
+
rich_help_panel="Helpers",
|
|
114
|
+
)
|
|
115
|
+
show_vskernels_opt = Option(
|
|
116
|
+
"--show-vskernels",
|
|
117
|
+
help="Show the builtin supported kernels from vskernels and exit.",
|
|
118
|
+
is_eager=True,
|
|
119
|
+
show_default=False,
|
|
120
|
+
callback=show_vskernels,
|
|
121
|
+
rich_help_panel="Helpers",
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
# getnative exclusive
|
|
125
|
+
range_dim_opt = Option(
|
|
126
|
+
"--range-dim",
|
|
127
|
+
"-rd",
|
|
128
|
+
help="The inclusive range of resolutions to test.\n\nSpecify as [bold]START END[/] (e.g., '500 1080').",
|
|
129
|
+
metavar="INTEGER INTEGER",
|
|
130
|
+
show_default=False,
|
|
131
|
+
)
|
|
132
|
+
step_opt = Option("--step", "-s", help="The increment step between resolutions in the tested range.", metavar="NUMBER")
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
# getscaler exclusive
|
|
136
|
+
dim_opt = Argument(
|
|
137
|
+
help="The suspected native resolution to verify. "
|
|
138
|
+
"Use an integer for exact pixels (e.g., 720) or a float for sub-pixel dimensions (e.g., 719.8).",
|
|
139
|
+
metavar="NUMBER",
|
|
140
|
+
parser=resolve_dimension,
|
|
141
|
+
)
|
|
142
|
+
base_dim_opt = Option(
|
|
143
|
+
"--base-dim",
|
|
144
|
+
"-b",
|
|
145
|
+
help="Base integer dimension if checking for fractional resolution.",
|
|
146
|
+
)
|
|
147
|
+
mask_opt = Option(
|
|
148
|
+
"--mask",
|
|
149
|
+
"-m",
|
|
150
|
+
help="Edge-detection mask to reduce noise influence on the metric. "
|
|
151
|
+
"Pass a mask name (e.g., 'Prewitt') or a class name from vsmasktools.",
|
|
152
|
+
metavar="EDGEDETECT",
|
|
153
|
+
parser=EdgeDetect.from_param,
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
# Frequency exclusive
|
|
158
|
+
cull_rate_opt = Option(
|
|
159
|
+
"--cull-rate",
|
|
160
|
+
"-cr",
|
|
161
|
+
help="Cull the sides/top of the frame to focus on the center.",
|
|
162
|
+
metavar="NUMBER",
|
|
163
|
+
show_default=True,
|
|
164
|
+
)
|
|
165
|
+
radius_opt = Option(
|
|
166
|
+
"--radius",
|
|
167
|
+
"-r",
|
|
168
|
+
help="Radius for finding peaks/spikes in the frequency plot.",
|
|
169
|
+
metavar="INTEGER",
|
|
170
|
+
show_default=True,
|
|
171
|
+
)
|
nativeres/cli/helpers.py
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
from logging import DEBUG, getLogger
|
|
2
|
+
from typing import Any
|
|
3
|
+
|
|
4
|
+
from jetpytools import CustomValueError, SPath, get_subclasses
|
|
5
|
+
from rich.console import Console
|
|
6
|
+
from rich.progress import BarColumn, Progress, TextColumn, TimeElapsedColumn, TimeRemainingColumn
|
|
7
|
+
from typer import BadParameter, Exit
|
|
8
|
+
from vskernels import ComplexKernel
|
|
9
|
+
from vssource import BestSource, CacheIndexer, Indexer
|
|
10
|
+
from vstools import Matrix, vs
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
# Callbacks
|
|
14
|
+
def resolve_dimension(value: str) -> float:
|
|
15
|
+
nb = float(value)
|
|
16
|
+
|
|
17
|
+
if nb.is_integer():
|
|
18
|
+
return int(nb)
|
|
19
|
+
|
|
20
|
+
return nb
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def resolve_idx(idx: str) -> Indexer:
|
|
24
|
+
indexer = Indexer.from_param(idx, BadParameter)
|
|
25
|
+
|
|
26
|
+
args = dict[str, Any]()
|
|
27
|
+
|
|
28
|
+
if issubclass(indexer, CacheIndexer):
|
|
29
|
+
# Set cache path to None
|
|
30
|
+
args[indexer._cache_arg_name] = None
|
|
31
|
+
|
|
32
|
+
if issubclass(indexer, BestSource):
|
|
33
|
+
args["show_pretty_progress"] = True
|
|
34
|
+
|
|
35
|
+
return indexer(**args)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def resolve_dimension_mode(mode: str) -> str:
|
|
39
|
+
match mode:
|
|
40
|
+
case "height" | "h":
|
|
41
|
+
return "height"
|
|
42
|
+
case "width" | "w":
|
|
43
|
+
return "width"
|
|
44
|
+
case _:
|
|
45
|
+
raise BadParameter("Unknown dimension passed")
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def set_debug(value: bool) -> None:
|
|
49
|
+
if value:
|
|
50
|
+
getLogger((__package__ or "").split(".")[0]).setLevel(DEBUG)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def set_global_debug(value: bool) -> None:
|
|
54
|
+
if value:
|
|
55
|
+
getLogger().setLevel(DEBUG)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def show_default_kernels(value: bool) -> None:
|
|
59
|
+
if value:
|
|
60
|
+
from ..kernels import default_kernels
|
|
61
|
+
|
|
62
|
+
console = Console(stderr=True)
|
|
63
|
+
|
|
64
|
+
for kernel in default_kernels:
|
|
65
|
+
console.print(str(kernel))
|
|
66
|
+
|
|
67
|
+
raise Exit(0)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def show_vskernels(value: bool) -> None:
|
|
71
|
+
if value:
|
|
72
|
+
all_kernels = {k for k in get_subclasses(ComplexKernel) if not k.is_abstract}
|
|
73
|
+
|
|
74
|
+
console = Console(stderr=True)
|
|
75
|
+
|
|
76
|
+
for kernel in sorted(all_kernels, key=lambda k: k.__name__):
|
|
77
|
+
console.print(kernel.__name__)
|
|
78
|
+
|
|
79
|
+
raise Exit(0)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
# Helpers
|
|
83
|
+
def get_videonode_from_input(path: SPath, indexer: Indexer) -> vs.VideoNode:
|
|
84
|
+
if not path.exists():
|
|
85
|
+
raise BadParameter(f"{path.to_str()!r} doesn't exist.")
|
|
86
|
+
|
|
87
|
+
if path.suffix in (".py", ".vpy"):
|
|
88
|
+
from vsengine import load_script
|
|
89
|
+
|
|
90
|
+
load_script(path, module="__nativeres__").result()
|
|
91
|
+
out = next(iter(vs.get_outputs().values()))
|
|
92
|
+
|
|
93
|
+
if not isinstance(out, vs.VideoOutputTuple):
|
|
94
|
+
raise CustomValueError("Unknown VapourSynth output", get_videonode_from_input, type(out))
|
|
95
|
+
|
|
96
|
+
return out.clip
|
|
97
|
+
|
|
98
|
+
if isinstance(indexer, BestSource):
|
|
99
|
+
from signal import SIG_DFL, SIGINT, signal
|
|
100
|
+
|
|
101
|
+
signal(SIGINT, SIG_DFL)
|
|
102
|
+
|
|
103
|
+
clip = indexer.source(path, 32, idx_props=False)
|
|
104
|
+
return clip.resize.Bilinear(format=vs.GRAYS, matrix=Matrix.BT709, matrix_in=Matrix.from_video(clip))
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def get_progress(console: Console) -> Progress:
|
|
108
|
+
return Progress(
|
|
109
|
+
TextColumn("[progress.description]{task.description}"),
|
|
110
|
+
BarColumn(),
|
|
111
|
+
TextColumn("{task.percentage:>3.0f}%"),
|
|
112
|
+
TimeElapsedColumn(),
|
|
113
|
+
TimeRemainingColumn(),
|
|
114
|
+
console=console,
|
|
115
|
+
)
|
nativeres/constants.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
LOW_RATE, HIGH_RATE = 0.465, 0.925
|