Update super resolution, add frame generation and true HDR (#42)

This commit is contained in:
Alexis Rolland
2026-10-07 15:22:04 -04:00
committed by GitHub
parent 477cb3a809
commit 28480a0d58
11 changed files with 1440 additions and 153 deletions
+149 -3
View File
@@ -1,5 +1,151 @@
# Nvidia_RTX_Nodes_ComfyUI
# NVIDIA RTX Nodes for ComfyUI
Contains the "RTX Video Super Resolution" upscaling node to upscale images and videos. Only works on Nvidia RTX GPUs.
CUDA-accelerated ComfyUI nodes powered by the NVIDIA Video Effects (VFX) SDK and its `nvidia-vfx` Python bindings.
Search RTX in the ComfyUI manager to install it.
This extension provides:
- **RTX Video Super Resolution** — upscale, denoise, deblur, or enhance images and video frames.
- **RTX TrueHDR** — convert SDR images to normalized BT.2020/PQ or BT.2020/HLG HDR image data.
- **RTX Video Frame Generation** — interpolate frames between adjacent images for frame-rate conversion or slow motion.
## Requirements
- An NVIDIA RTX GPU
- A working ComfyUI installation with CUDA-enabled PyTorch
- A Python environment supported by the [`nvidia-vfx`](https://pypi.org/project/nvidia-vfx/) package
The nodes use the CUDA device selected by ComfyUI. CPU-only execution and non-NVIDIA GPUs are not supported.
## Installation
### ComfyUI Manager
1. Open **ComfyUI Manager**.
2. Search for **ComfyUI_NVIDIA_RTX_Nodes**.
3. Install the extension and restart ComfyUI.
### Manual installation
Clone the repository into `ComfyUI/custom_nodes`, then install its dependency with the Python interpreter used by ComfyUI:
```bash
cd ComfyUI/custom_nodes
git clone https://github.com/Comfy-Org/Nvidia_RTX_Nodes_ComfyUI.git
cd Nvidia_RTX_Nodes_ComfyUI
python -m pip install -r requirements.txt
```
Restart ComfyUI after installation.
## RTX Video Super Resolution
Find the node under **image/upscaling**. It accepts a ComfyUI `IMAGE` batch and returns `upscaled_images` as another `IMAGE` batch.
### Inputs
| Input | Values | Description |
| --- | --- | --- |
| `images` | `IMAGE` batch | Images or decoded video frames to process. |
| `resize_type` | `scale by multiplier` / `target dimensions` | Selects how upscaled output dimensions are calculated. |
| `scale` | `1.00`–`4.00`, default `2.00` | Width and height multiplier. Available when scaling by multiplier. |
| `width` | `64`–`8192`, default `1920` | Requested output width. Available for target dimensions. |
| `height` | `64`–`8192`, default `1080` | Requested output height. Available for target dimensions. |
| `quality` | See below; default `ULTRA` | Selects the VFX processing model. |
| `strength` | `0.00`–`1.00`, default `1.00` | Effect strength. |
| `image_encoding` | `8-bit RGB` / `10-bit RGB (RGB10A2)` | Pixel encoding used internally by the SDK. |
The 10-bit option packs ComfyUI's normalized RGB values into RGB10A2 before inference and unpacks the result back to a normal ComfyUI `IMAGE`. It preserves more precision but does not convert SDR content to HDR.
### Quality modes
| Modes | Operation |
| --- | --- |
| `BICUBIC` | Non-AI bicubic interpolation. |
| `LOW`, `MEDIUM`, `HIGH`, `ULTRA` | Standard AI upscaling for typical compressed sources, from fastest to maximum detail preservation. |
| `DENOISE_LOW`, `DENOISE_MEDIUM`, `DENOISE_HIGH`, `DENOISE_ULTRA` | Same-resolution noise and compression-artifact removal. Higher levels remove more noise but may soften texture. |
| `DEBLUR_LOW`, `DEBLUR_MEDIUM`, `DEBLUR_HIGH`, `DEBLUR_ULTRA` | Same-resolution sharpening for soft or blurry sources. |
| `HIGHBITRATE_LOW`, `HIGHBITRATE_MEDIUM`, `HIGHBITRATE_HIGH`, `HIGHBITRATE_ULTRA` | AI upscaling for clean, high-bitrate, or lossless sources; avoids unnecessary artifact suppression. |
| `STREAMING_MEDIUM`, `STREAMING_ULTRA` | Upscaling optimized for broadcast and webcam streams. |
`DENOISE_*` and `DEBLUR_*` always keep the input dimensions, regardless of the selected resize settings. All other modes use the requested output size, rounded to the nearest multiple of 8. To limit working memory, the node chooses a chunk size targeting no more than 16 megapixels of output at once, with a minimum of one frame per chunk.
### Image workflow
Connect an image batch to **RTX Video Super Resolution**, choose an operation and settings, then connect `upscaled_images` to **Save Image** or another image node.
![RTX image upscaling workflow](example_workflows/rtx_image_upscale.jpg)
### Video workflow
Decode a video to an `IMAGE` batch, process the frames, and pass the result to video creation/encoding nodes. Super Resolution changes frame dimensions but not frame count or frame rate.
![RTX video upscaling workflow](example_workflows/rtx_video_upscale.jpg)
Importable Super Resolution examples are available in [`example_workflows`](example_workflows):
- [`rtx_image_upscale.json`](example_workflows/rtx_image_upscale.json)
- [`rtx_video_upscale.json`](example_workflows/rtx_video_upscale.json)
## RTX TrueHDR
Find the node under **image/enhancement**. It converts each SDR input image with NVIDIA RTX TrueHDR and returns `hdr_images` at the original dimensions.
### Inputs
| Input | Range/default | Description |
| --- | --- | --- |
| `images` | `IMAGE` batch | Normalized SDR RGB input images. |
| `contrast` | `0`–`200`, default `100` | Output contrast. |
| `saturation` | `0`–`200`, default `100` | Output saturation. |
| `middle_gray` | `10`–`100`, default `50` | Middle-grey reference level. |
| `luminance` | `400`–`2000`, default `650` | Target HDR display peak luminance in nits. |
| `debanding` | default enabled | Applies the SDK's DL-Debander pass. |
| `output_colorspace` | `HDR` / `HDR PQ` | Selects BT.2020/HLG or BT.2020/PQ output data. Default: `HDR` (HLG). |
`HDR PQ` returns the SDK's BT.2020/PQ signal as normalized RGB values. `HDR` converts that result to BT.2020/HLG using the selected peak luminance.
> [!IMPORTANT]
> A ComfyUI `IMAGE` does not carry HDR color-space metadata. The values this node outputs are meant for HDR workflows — downstream tools that save the video or image need to mark or encode them with the selected transfer function. If you display or save them as regular SDR data, they won't look the way HDR intends.
## RTX Video Frame Generation
Find the node under **video**. It processes every adjacent pair in an input `IMAGE` batch and returns `interpolated_images`, containing the original frames plus generated intermediate frames.
At least two input frames are required for interpolation. A one-frame batch is returned unchanged.
### Inputs
| Input | Values | Description |
| --- | --- | --- |
| `images` | `IMAGE` batch | Ordered source video frames. |
| `generation_type` | `frame rate multiplier` / `specific timestep` | Selects uniform frame multiplication or one explicit interpolation position per pair. |
| `multiplier` | `2`–`16`, default `2` | Output frame-rate multiplier. Available in multiplier mode. |
| `timestep` | `0.01`–`0.99`, default `0.50` | Position between the previous frame (`0`) and current frame (`1`). Available in timestep mode. |
| `mode` | `LOW`, `MEDIUM`, `HIGH`; default `MEDIUM` | Frame-generation quality mode. |
| `automatic_shot_change_detection` | default enabled | Lets the SDK detect shot boundaries automatically. |
| `shot_change` | default disabled | Advanced option that marks every submitted pair as a shot change; useful when processing one known cut. |
| `image_encoding` | `8-bit RGB` / `10-bit RGB (RGB10A2)` | Pixel encoding used internally by the SDK. |
### Output timing
For \(N\) input frames and multiplier \(M\), multiplier mode returns \((N - 1)M + 1\) frames. To preserve the source video's duration, multiply its frame rate by \(M\) when encoding the output.
Specific-timestep mode inserts one generated frame between each pair and returns \(2N - 1\) frames. A timestep of `0.5` creates evenly spaced 2× interpolation. Other timestep values create nonuniform temporal spacing, so use them for selecting frames or with a downstream workflow that can represent custom timestamps.
## Implementation notes
ComfyUI images use channels-last `(B, H, W, C)` tensors. The nodes move each frame to ComfyUI's selected CUDA device, convert it to the SDK's channels-first RGB8 or packed RGB10A2 representation, and pass it through DLPack. Every SDK-owned DLPack result is cloned immediately before the next inference call or before the effect closes, then converted back to a standard channels-last ComfyUI `IMAGE`.
## Troubleshooting
- **`nvidia-vfx` cannot be imported:** install `requirements.txt` with the same Python interpreter that launches ComfyUI, then restart it.
- **CUDA/device error:** confirm that ComfyUI is using CUDA-enabled PyTorch and has selected a supported NVIDIA RTX GPU.
- **VFX load or inference error:** verify the dimensions, GPU support, driver/runtime installation, and available VRAM. The bindings report SDK failures as `nvvfx.NvVFXError`.
- **Out of memory:** reduce the resolution, scale factor, quality level, or number of frames supplied in one batch.
- **Super Resolution output size differs slightly from the requested multiplier:** upscaling dimensions are rounded to the nearest multiple of 8.
- **HDR output looks incorrect:** ensure downstream tools interpret the data as BT.2020/HLG for `HDR` or BT.2020/PQ for `HDR PQ`; do not treat it as SDR/sRGB.
- **Generated video duration is wrong:** adjust the encoded output frame rate as described under [Output timing](#output-timing).
## License
Licensed under Apache License 2.0. See [`LICENSE`](LICENSE).
+341 -35
View File
@@ -1,17 +1,104 @@
import torch
import nvvfx
from enum import Enum
import math
from typing import TypedDict
import nvvfx
import torch
import torch.nn.functional as F
from typing_extensions import override
import comfy.model_management
from comfy_api.latest import ComfyExtension, io
MAX_BATCH_PIXELS = 1024 * 1024 * 16
VSR_QUALITY_OPTIONS = list(nvvfx.QualityLevel.__members__)
PQ_M1 = 2610 / 16384
PQ_M2 = 2523 / 4096 * 128
PQ_C1 = 3424 / 4096
PQ_C2 = 2413 / 4096 * 32
PQ_C3 = 2392 / 4096 * 32
HLG_A = 0.17883277
HLG_B = 1 - 4 * HLG_A
HLG_C = 0.5 - HLG_A * math.log(4 * HLG_A)
class UpscaleType(str, Enum):
SCALE_BY = "scale by multiplier"
TARGET_DIMENSIONS = "target dimensions"
class FrameGenerationType(str, Enum):
MULTIPLIER = "frame rate multiplier"
TIMESTEP = "specific timestep"
class ImageEncodingType(str, Enum):
RGB8 = "8-bit RGB"
RGB10A2 = "10-bit RGB (RGB10A2)"
class HDRColorSpace(str, Enum):
HLG = "HDR"
PQ = "HDR PQ"
def get_effect_device() -> tuple[torch.device, int]:
device = comfy.model_management.get_torch_device()
if device.type != "cuda":
raise RuntimeError("NVIDIA RTX Video Effects require a CUDA device.")
device_index = device.index if device.index is not None else torch.cuda.current_device()
return device, device_index
def pack_rgb10a2(image: torch.Tensor) -> torch.Tensor:
rgb = image.clamp(0.0, 1.0).mul(1023.0).round().to(torch.int32)
packed = rgb[..., 0] | (rgb[..., 1] << 10) | (rgb[..., 2] << 20) | -1073741824
return packed.view(torch.uint32).contiguous()
def unpack_rgb10a2(packed: torch.Tensor) -> torch.Tensor:
packed = packed.view(torch.int32)
red = packed.bitwise_and(0x3FF)
green = packed.bitwise_right_shift(10).bitwise_and(0x3FF)
blue = packed.bitwise_right_shift(20).bitwise_and(0x3FF)
return torch.stack((red, green, blue), dim=-1).to(torch.float32).div_(1023.0)
def pq_to_hlg(image: torch.Tensor, peak_luminance: float) -> torch.Tensor:
pq = image.clamp(0.0, 1.0).pow(1.0 / PQ_M2)
display_light = ((pq.sub(PQ_C1).clamp_min(0.0) / (PQ_C2 - PQ_C3 * pq)) ** (1.0 / PQ_M1)) * 10000.0
display_light.clamp_(max=peak_luminance)
display_luminance = 0.2627 * display_light[..., 0] + 0.6780 * display_light[..., 1] + 0.0593 * display_light[..., 2]
system_gamma = 1.2 + 0.42 * math.log10(peak_luminance / 1000.0)
luminance_ratio = (display_luminance / peak_luminance).clamp_min(torch.finfo(display_light.dtype).eps)
scene_light = (display_light / peak_luminance) * luminance_ratio.unsqueeze(-1).pow((1.0 - system_gamma) / system_gamma)
scene_light.clamp_(0.0, 1.0)
low = torch.sqrt(3.0 * scene_light)
high = HLG_A * torch.log((12.0 * scene_light - HLG_B).clamp_min(torch.finfo(scene_light.dtype).eps)) + HLG_C
return torch.where(scene_light <= 1.0 / 12.0, low, high)
def prepare_frame(image: torch.Tensor, device: torch.device, image_encoding: ImageEncodingType) -> torch.Tensor:
image = image.to(device=device, dtype=torch.float32)
if image_encoding == ImageEncodingType.RGB10A2:
return pack_rgb10a2(image)
return image.movedim(-1, 0).contiguous()
def copy_effect_output(dlpack_output, destination: torch.Tensor, image_encoding: ImageEncodingType) -> None:
output = torch.from_dlpack(dlpack_output).clone()
if image_encoding == ImageEncodingType.RGB10A2:
output = unpack_rgb10a2(output)
else:
output = output.movedim(0, -1)
destination.copy_(output)
class RTXVideoSuperResolution(io.ComfyNode):
class UpscaleTypedDict(TypedDict):
resize_type: UpscaleType
@@ -25,23 +112,36 @@ class RTXVideoSuperResolution(io.ComfyNode):
node_id="RTXVideoSuperResolution",
display_name="RTX Video Super Resolution",
category="image/upscaling",
search_aliases=["rtx", "nvidia", "upscale", "super resolution", "vsr"],
description="Upscale, denoise, or deblur images with NVIDIA Video Effects.",
search_aliases=["rtx", "nvidia", "upscale", "super resolution", "vsr", "denoise", "deblur"],
inputs=[
io.Image.Input("images"),
io.DynamicCombo.Input(
"resize_type",
tooltip="Choose to scale by a multiplier or to exact target dimensions.",
tooltip="Choose to scale by a multiplier or to exact target dimensions. Denoise and deblur modes keep the input dimensions.",
options=[
io.DynamicCombo.Option(UpscaleType.SCALE_BY, [
io.Float.Input("scale", default=2.0, min=1.0, max=4.0, step=0.01, tooltip="Scale factor (e.g., 2.0 doubles the size)."),
]),
io.DynamicCombo.Option(UpscaleType.TARGET_DIMENSIONS, [
io.Int.Input("width", default=1920, min=64, max=8192, step=8, tooltip="Target width in pixels."),
io.Int.Input("height", default=1080, min=64, max=8192, step=8, tooltip="Target height in pixels.")
])
io.Int.Input("height", default=1080, min=64, max=8192, step=8, tooltip="Target height in pixels."),
]),
],
),
io.Combo.Input("quality", options=["LOW", "MEDIUM", "HIGH", "ULTRA"], default="ULTRA"),
io.Combo.Input(
"quality",
options=VSR_QUALITY_OPTIONS,
default="ULTRA",
tooltip="Select the SDK model. DENOISE and DEBLUR process at the original resolution; HIGHBITRATE modes preserve clean sources.",
),
io.Float.Input("strength", default=1.0, min=0.0, max=1.0, step=0.01, tooltip="Effect strength."),
io.Combo.Input(
"image_encoding",
options=[encoding.value for encoding in ImageEncodingType],
default=ImageEncodingType.RGB8,
tooltip="SDK pixel encoding. 10-bit preserves more precision but does not by itself convert SDR content to HDR.",
),
],
outputs=[
io.Image.Output("upscaled_images"),
@@ -49,7 +149,16 @@ class RTXVideoSuperResolution(io.ComfyNode):
)
@classmethod
def execute(cls, images: torch.Tensor, resize_type: UpscaleTypedDict, quality: str) -> io.NodeOutput:
def execute(
cls,
images: torch.Tensor,
resize_type: UpscaleTypedDict,
quality: str,
strength: float = 1.0,
image_encoding: str = ImageEncodingType.RGB8,
) -> io.NodeOutput:
alpha = images[..., 3:4] if images.shape[-1] > 3 else None
images = images[..., :3]
b, h, w, c = images.shape
selected_type = resize_type["resize_type"]
@@ -63,39 +172,230 @@ class RTXVideoSuperResolution(io.ComfyNode):
else:
raise ValueError(f"Unsupported resize type: {selected_type}")
output_width = max(8, round(output_width / 8) * 8)
output_height = max(8, round(output_height / 8) * 8)
if quality.startswith(("DENOISE_", "DEBLUR_")):
output_width = w
output_height = h
else:
output_width = max(8, round(output_width / 8) * 8)
output_height = max(8, round(output_height / 8) * 8)
MAX_PIXELS = 1024 * 1024 * 16
output = torch.empty((b, output_height, output_width, c), device=images.device, dtype=images.dtype)
device, device_index = get_effect_device()
batch_size = max(1, MAX_BATCH_PIXELS // (output_width * output_height))
selected_encoding = ImageEncodingType(image_encoding)
out_pixels = output_width * output_height
batch_size = max(1, MAX_PIXELS // out_pixels)
with torch.cuda.device(device), nvvfx.VideoSuperRes(
quality=nvvfx.QualityLevel[quality],
strength=strength,
device=device_index,
image_encoding=nvvfx.ImageEncoding.RGB10A2 if selected_encoding == ImageEncodingType.RGB10A2 else nvvfx.ImageEncoding.RGB8,
) as effect:
effect.output_width = output_width
effect.output_height = output_height
effect.load()
quality_mapping = {
"LOW": nvvfx.effects.QualityLevel.LOW,
"MEDIUM": nvvfx.effects.QualityLevel.MEDIUM,
"HIGH": nvvfx.effects.QualityLevel.HIGH,
"ULTRA": nvvfx.effects.QualityLevel.ULTRA,
}
selected_quality = quality_mapping.get(quality, nvvfx.effects.QualityLevel.HIGH)
with nvvfx.VideoSuperRes(selected_quality) as sr:
sr.output_width = output_width
sr.output_height = output_height
sr.load()
out_tensor = torch.empty((images.shape[0], output_height, output_width, c), device=images.device, dtype=images.dtype)
for i in range(0, images.shape[0], batch_size):
batch = images[i:i + batch_size]
batch_cuda = batch.to(device=device, dtype=torch.float32)
if selected_encoding == ImageEncodingType.RGB10A2:
batch_cuda = pack_rgb10a2(batch_cuda)
else:
batch_cuda = batch_cuda.movedim(-1, 1).contiguous()
for j, frame in enumerate(batch_cuda):
copy_effect_output(effect.run(frame).image, output[i + j], selected_encoding)
batch_cuda = batch.cuda().permute(0, 3, 1, 2).float().contiguous()
if alpha is not None:
upscaled_alpha = F.interpolate(
alpha.movedim(-1, 1),
size=(output_height, output_width),
mode="bilinear",
align_corners=False,
).movedim(1, -1)
output = torch.cat((output, upscaled_alpha), dim=-1)
for j in range(batch_cuda.shape[0]):
input_frame = batch_cuda[j]
dlpack_out = sr.run(input_frame).image
out_tensor[i + j: i + j + 1] = torch.from_dlpack(dlpack_out).movedim(0, -1).unsqueeze(0)
return io.NodeOutput(output)
return io.NodeOutput(out_tensor)
class RTXTrueHDR(io.ComfyNode):
@classmethod
def define_schema(cls):
return io.Schema(
node_id="RTXTrueHDR",
display_name="RTX TrueHDR",
category="image/enhancement",
description="Convert SDR images to HLG or PQ HDR image data with NVIDIA RTX TrueHDR.",
search_aliases=["rtx", "nvidia", "true hdr", "hdr10", "hlg", "pq", "sdr to hdr"],
inputs=[
io.Image.Input("images"),
io.Int.Input("contrast", default=100, min=0, max=200),
io.Int.Input("saturation", default=100, min=0, max=200),
io.Int.Input("middle_gray", default=50, min=10, max=100, tooltip="Middle-grey reference level."),
io.Int.Input("luminance", default=650, min=400, max=2000, tooltip="Target HDR display peak luminance in nits."),
io.Boolean.Input("debanding", default=True, tooltip="Apply the SDK's DL-Debander pass."),
io.Combo.Input(
"output_colorspace",
options=[colorspace.value for colorspace in HDRColorSpace],
default=HDRColorSpace.HLG,
tooltip="HDR outputs BT.2020/HLG using the selected peak luminance. HDR PQ returns the SDK's BT.2020/PQ signal unchanged.",
),
],
outputs=[
io.Image.Output("hdr_images", tooltip="Normalized RGB values using the selected HDR colorspace."),
],
)
@classmethod
def execute(
cls,
images: torch.Tensor,
contrast: int,
saturation: int,
middle_gray: int,
luminance: int,
debanding: bool,
output_colorspace: str = HDRColorSpace.HLG,
) -> io.NodeOutput:
alpha = images[..., 3:4] if images.shape[-1] > 3 else None
images = images[..., :3]
device, device_index = get_effect_device()
output = torch.empty(images.shape, device=images.device, dtype=torch.float32)
selected_colorspace = HDRColorSpace(output_colorspace)
with torch.cuda.device(device), nvvfx.TrueHDR(
contrast=contrast,
saturation=saturation,
middle_gray=middle_gray,
luminance=luminance,
debanding_off=int(not debanding),
device=device_index,
) as effect:
effect.load()
for i, image in enumerate(images):
result = effect.run(prepare_frame(image, device, ImageEncodingType.RGB8))
hdr_image = unpack_rgb10a2(torch.from_dlpack(result.image).clone())
if selected_colorspace == HDRColorSpace.HLG:
hdr_image = pq_to_hlg(hdr_image, luminance)
output[i].copy_(hdr_image)
if alpha is not None:
output = torch.cat((output, alpha.to(dtype=output.dtype)), dim=-1)
return io.NodeOutput(output)
class RTXVideoFrameGeneration(io.ComfyNode):
class GenerationTypedDict(TypedDict):
generation_type: FrameGenerationType
multiplier: int
timestep: float
@classmethod
def define_schema(cls):
return io.Schema(
node_id="RTXVideoFrameGeneration",
display_name="RTX Video Frame Generation",
category="video",
description="Generate intermediate video frames with NVIDIA Video Effects.",
search_aliases=["rtx", "nvidia", "frame generation", "interpolation", "slow motion", "vfi"],
inputs=[
io.Image.Input("images"),
io.DynamicCombo.Input(
"generation_type",
options=[
io.DynamicCombo.Option(FrameGenerationType.MULTIPLIER, [
io.Int.Input("multiplier", default=2, min=2, max=16, tooltip="Output frame-rate multiplier."),
]),
io.DynamicCombo.Option(FrameGenerationType.TIMESTEP, [
io.Float.Input("timestep", default=0.5, min=0.01, max=0.99, step=0.01, tooltip="Position between each adjacent input pair."),
]),
],
),
io.Combo.Input("mode", options=["LOW", "MEDIUM", "HIGH"], default="MEDIUM", tooltip="Frame-generation quality mode."),
io.Boolean.Input("automatic_shot_change_detection", default=True),
io.Boolean.Input("shot_change", default=False, tooltip="Mark every submitted pair as a shot change. Useful when processing a single pair."),
io.Combo.Input(
"image_encoding",
options=[encoding.value for encoding in ImageEncodingType],
default=ImageEncodingType.RGB8,
tooltip="SDK pixel encoding. 10-bit preserves more precision but does not by itself convert SDR content to HDR.",
),
],
outputs=[
io.Image.Output("interpolated_images"),
],
)
@classmethod
def execute(
cls,
images: torch.Tensor,
generation_type: GenerationTypedDict,
mode: str,
automatic_shot_change_detection: bool,
shot_change: bool,
image_encoding: str = ImageEncodingType.RGB8,
) -> io.NodeOutput:
images = images[..., :3]
if images.shape[0] < 2:
return io.NodeOutput(images)
selected_type = generation_type["generation_type"]
if selected_type == FrameGenerationType.MULTIPLIER:
multiplier = generation_type["multiplier"]
generated_per_pair = multiplier - 1
elif selected_type == FrameGenerationType.TIMESTEP:
multiplier = 2
generated_per_pair = 1
else:
raise ValueError(f"Unsupported frame generation type: {selected_type}")
output_count = (images.shape[0] - 1) * (generated_per_pair + 1) + 1
output = torch.empty((output_count, *images.shape[1:]), device=images.device, dtype=images.dtype)
output[0].copy_(images[0])
_, height, width, _ = images.shape
device, device_index = get_effect_device()
selected_encoding = ImageEncodingType(image_encoding)
with torch.cuda.device(device), nvvfx.VideoFrameGeneration(
mode=nvvfx.VideoFrameGeneration.Mode[mode],
automatic_shot_change_detection_enabled=automatic_shot_change_detection,
device=device_index,
image_encoding=nvvfx.ImageEncoding.RGB10A2 if selected_encoding == ImageEncodingType.RGB10A2 else nvvfx.ImageEncoding.RGB8,
) as effect:
effect.input_width = width
effect.input_height = height
effect.frame_multiplier = multiplier
effect.load()
output_index = 1
previous_frame = prepare_frame(images[0], device, selected_encoding)
for current_image in images[1:]:
current_frame = prepare_frame(current_image, device, selected_encoding)
if selected_type == FrameGenerationType.MULTIPLIER:
for frame_index in range(1, multiplier):
result = effect.run(
previous_frame,
current_frame,
frame_index=frame_index,
shot_change=shot_change,
)
copy_effect_output(result.image, output[output_index], selected_encoding)
output_index += 1
else:
result = effect.run_at_timestep(
previous_frame,
current_frame,
generation_type["timestep"],
shot_change=shot_change,
)
copy_effect_output(result.image, output[output_index], selected_encoding)
output_index += 1
output[output_index].copy_(current_image)
output_index += 1
previous_frame = current_frame
return io.NodeOutput(output)
class NVVFXVideoExtension(ComfyExtension):
@@ -103,12 +403,18 @@ class NVVFXVideoExtension(ComfyExtension):
async def get_node_list(self) -> list[type[io.ComfyNode]]:
return [
RTXVideoSuperResolution,
RTXTrueHDR,
RTXVideoFrameGeneration,
]
async def comfy_entrypoint() -> NVVFXVideoExtension:
return NVVFXVideoExtension()
# hack so registry picks up the node name
# Registry scanner compatibility.
if False:
NODE_CLASS_MAPPINGS = {"RTXVideoSuperResolution": RTXVideoSuperResolution}
NODE_CLASS_MAPPINGS = {
"RTXVideoSuperResolution": RTXVideoSuperResolution,
"RTXTrueHDR": RTXTrueHDR,
"RTXVideoFrameGeneration": RTXVideoFrameGeneration,
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

After

Width:  |  Height:  |  Size: 10 KiB

+99 -80
View File
@@ -1,57 +1,19 @@
{
"id": "e6b92a63-6d7e-4d2b-b9e3-2e054825db02",
"revision": 0,
"last_node_id": 5,
"last_link_id": 4,
"last_node_id": 6,
"last_link_id": 7,
"nodes": [
{
"id": 3,
"type": "SaveImage",
"pos": [
-736.406353661491,
-677.7778796179039
],
"size": [
270,
270
],
"flags": {},
"order": 2,
"mode": 0,
"inputs": [
{
"name": "images",
"type": "IMAGE",
"link": 2
}
],
"outputs": [],
"properties": {
"cnr_id": "comfy-core",
"ver": "0.16.4",
"enableTabs": false,
"tabWidth": 65,
"tabXOffset": 10,
"hasSecondTab": false,
"secondTabText": "Send Back",
"secondTabOffset": 80,
"secondTabWidth": 65,
"Node name for S&R": "SaveImage"
},
"widgets_values": [
"ComfyUI"
]
},
{
"id": 1,
"type": "RTXVideoSuperResolution",
"pos": [
-1064.2136453281578,
-677.7778796179039
-1050,
-560
],
"size": [
285.8072916666667,
124.44010416666667
154
],
"flags": {},
"order": 1,
@@ -68,12 +30,12 @@
"name": "upscaled_images",
"type": "IMAGE",
"links": [
2,
4
6
]
}
],
"properties": {
"Node name for S&R": "RTXVideoSuperResolution",
"cnr_id": "comfyui_nvidia_rtx_nodes",
"ver": "0.1.0",
"enableTabs": false,
@@ -82,21 +44,29 @@
"hasSecondTab": false,
"secondTabText": "Send Back",
"secondTabOffset": 80,
"secondTabWidth": 65,
"Node name for S&R": "RTXVideoSuperResolution"
"secondTabWidth": 65
},
"widgets_values": [
"scale by multiplier",
2,
"ULTRA"
]
"ULTRA",
1,
"8-bit RGB"
],
"widgets_values_named": {
"resize_type": "scale by multiplier",
"resize_type.scale": 2,
"quality": "ULTRA",
"strength": 1,
"image_encoding": "8-bit RGB"
}
},
{
"id": 5,
"type": "ImageCompare",
"pos": [
-424.40635366149104,
-677.7778796179039
-417.90450850159755,
-653.9377806982941
],
"size": [
233.33333333333334,
@@ -116,11 +86,12 @@
"name": "image_b",
"shape": 7,
"type": "IMAGE",
"link": 4
"link": 7
}
],
"outputs": [],
"properties": {
"Node name for S&R": "ImageCompare",
"cnr_id": "comfy-core",
"ver": "0.16.4",
"enableTabs": false,
@@ -129,17 +100,17 @@
"hasSecondTab": false,
"secondTabText": "Send Back",
"secondTabOffset": 80,
"secondTabWidth": 65,
"Node name for S&R": "ImageCompare"
"secondTabWidth": 65
},
"widgets_values": []
"widgets_values": [],
"widgets_values_named": {}
},
{
"id": 2,
"type": "LoadImage",
"pos": [
-1389.000103661491,
-677.7778796179039
-1382.4982585015975,
-653.9377806982941
],
"size": [
282.78645833333337,
@@ -165,6 +136,7 @@
}
],
"properties": {
"Node name for S&R": "LoadImage",
"cnr_id": "comfy-core",
"ver": "0.16.4",
"enableTabs": false,
@@ -173,32 +145,63 @@
"hasSecondTab": false,
"secondTabText": "Send Back",
"secondTabOffset": 80,
"secondTabWidth": 65,
"Node name for S&R": "LoadImage"
"secondTabWidth": 65
},
"widgets_values": [
"example.png",
"image"
]
],
"widgets_values_named": {
"image": "example.png",
"upload": "image"
}
},
{
"id": 6,
"type": "SaveImageAdvanced",
"pos": [
-730,
-560
],
"size": [
270,
130
],
"flags": {},
"order": 2,
"mode": 0,
"inputs": [
{
"name": "images",
"type": "IMAGE",
"link": 6
}
],
"outputs": [
{
"name": "images",
"type": "IMAGE",
"links": [
7
]
}
],
"properties": {},
"widgets_values": [
"ComfyUI",
"png",
"8-bit",
"sRGB"
],
"widgets_values_named": {
"filename_prefix": "ComfyUI",
"format": "png",
"format.bit_depth": "8-bit",
"format.input_color_space": "sRGB"
}
}
],
"links": [
[
1,
2,
0,
1,
0,
"IMAGE"
],
[
2,
1,
0,
3,
0,
"IMAGE"
],
[
3,
2,
@@ -208,8 +211,24 @@
"IMAGE"
],
[
4,
1,
2,
0,
1,
0,
"IMAGE"
],
[
6,
1,
0,
6,
0,
"IMAGE"
],
[
7,
6,
0,
5,
1,
@@ -223,11 +242,11 @@
"ds": {
"scale": 0.922814962898653,
"offset": [
1714.9164154028422,
1133.1650830129668
2242.6495142142044,
1203.059918481823
]
},
"frontendVersion": "1.39.19",
"frontendVersion": "1.53.6",
"VHS_latentpreview": false,
"VHS_latentpreviewrate": 0,
"VHS_MetadataImage": true,
Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

@@ -0,0 +1,360 @@
{
"id": "e6b92a63-6d7e-4d2b-b9e3-2e054825db02",
"revision": 0,
"last_node_id": 10,
"last_link_id": 12,
"nodes": [
{
"id": 7,
"type": "GetVideoComponents",
"pos": [
-1361.201171875,
-680
],
"size": [
193.530859375,
106
],
"flags": {},
"order": 1,
"mode": 0,
"inputs": [
{
"name": "video",
"type": "VIDEO",
"link": 5
}
],
"outputs": [
{
"name": "images",
"type": "IMAGE",
"links": [
11
]
},
{
"name": "audio",
"type": "AUDIO",
"links": [
8
]
},
{
"name": "fps",
"type": "FLOAT",
"links": [
9
]
},
{
"name": "bit_depth",
"type": "COMBO",
"links": null
},
{
"name": "color_space",
"type": "COMBO",
"links": null
}
],
"properties": {
"Node name for S&R": "GetVideoComponents",
"cnr_id": "comfy-core",
"ver": "0.16.4",
"enableTabs": false,
"tabWidth": 65,
"tabXOffset": 10,
"hasSecondTab": false,
"secondTabText": "Send Back",
"secondTabOffset": 80,
"secondTabWidth": 65
}
},
{
"id": 9,
"type": "SaveVideo",
"pos": [
-370,
-680
],
"size": [
570,
520
],
"flags": {},
"order": 4,
"mode": 0,
"inputs": [
{
"name": "video",
"type": "VIDEO",
"link": 10
}
],
"outputs": [
{
"name": "video",
"type": "VIDEO",
"links": null
}
],
"properties": {
"cnr_id": "comfy-core",
"ver": "0.16.4",
"enableTabs": false,
"tabWidth": 65,
"tabXOffset": 10,
"hasSecondTab": false,
"secondTabText": "Send Back",
"secondTabOffset": 80,
"secondTabWidth": 65,
"Node name for S&R": "SaveVideo"
},
"widgets_values": [
"video/ComfyUI",
"auto",
"auto",
"auto"
],
"widgets_values_named": {
"filename_prefix": "video/ComfyUI",
"format": "auto",
"format.codec": "auto",
"codec": "auto"
}
},
{
"id": 8,
"type": "CreateVideo",
"pos": [
-690,
-680
],
"size": [
270,
150
],
"flags": {},
"order": 3,
"mode": 0,
"inputs": [
{
"name": "images",
"type": "IMAGE",
"link": 12
},
{
"name": "audio",
"shape": 7,
"type": "AUDIO",
"link": 8
},
{
"name": "fps",
"type": "FLOAT",
"widget": {
"name": "fps"
},
"link": 9
}
],
"outputs": [
{
"name": "VIDEO",
"type": "VIDEO",
"links": [
10
]
}
],
"properties": {
"Node name for S&R": "CreateVideo",
"cnr_id": "comfy-core",
"ver": "0.16.4",
"enableTabs": false,
"tabWidth": 65,
"tabXOffset": 10,
"hasSecondTab": false,
"secondTabText": "Send Back",
"secondTabOffset": 80,
"secondTabWidth": 65
},
"widgets_values": [
30,
"auto",
"sRGB",
"none"
],
"widgets_values_named": {
"fps": 30,
"bit_depth": "auto",
"color_space": "sRGB",
"codec": "none"
}
},
{
"id": 6,
"type": "LoadVideo",
"pos": [
-1790,
-680
],
"size": [
386.798828125,
510
],
"flags": {},
"order": 0,
"mode": 0,
"inputs": [],
"outputs": [
{
"name": "VIDEO",
"type": "VIDEO",
"links": [
5
]
}
],
"properties": {
"Node name for S&R": "LoadVideo",
"cnr_id": "comfy-core",
"ver": "0.16.4",
"enableTabs": false,
"tabWidth": 65,
"tabXOffset": 10,
"hasSecondTab": false,
"secondTabText": "Send Back",
"secondTabOffset": 80,
"secondTabWidth": 65
},
"widgets_values": [
"input.mp4",
"image"
],
"widgets_values_named": {
"file": "input.mp4",
"upload": "image"
}
},
{
"id": 10,
"type": "RTXVideoFrameGeneration",
"pos": [
-1090,
-680
],
"size": [
356,
178
],
"flags": {},
"order": 2,
"mode": 0,
"inputs": [
{
"name": "images",
"type": "IMAGE",
"link": 11
}
],
"outputs": [
{
"name": "interpolated_images",
"type": "IMAGE",
"links": [
12
]
}
],
"properties": {
"Node name for S&R": "RTXVideoFrameGeneration"
},
"widgets_values": [
"frame rate multiplier",
2,
"MEDIUM",
true,
false,
"8-bit RGB"
],
"widgets_values_named": {
"generation_type": "frame rate multiplier",
"generation_type.multiplier": 2,
"mode": "MEDIUM",
"automatic_shot_change_detection": true,
"shot_change": false,
"image_encoding": "8-bit RGB"
}
}
],
"links": [
[
5,
6,
0,
7,
0,
"VIDEO"
],
[
8,
7,
1,
8,
1,
"AUDIO"
],
[
9,
7,
2,
8,
2,
"FLOAT"
],
[
10,
8,
0,
9,
0,
"VIDEO"
],
[
11,
7,
0,
10,
0,
"IMAGE"
],
[
12,
10,
0,
8,
0,
"IMAGE"
]
],
"groups": [],
"config": {},
"extra": {
"workflowRendererVersion": "LG",
"ds": {
"scale": 0.8526983761385988,
"offset": [
2203.9999858436086,
1155.2227371031956
]
},
"frontendVersion": "1.53.6",
"VHS_latentpreview": false,
"VHS_latentpreviewrate": 0,
"VHS_MetadataImage": true,
"VHS_KeepIntermediate": true
},
"version": 0.4
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

+412
View File
@@ -0,0 +1,412 @@
{
"id": "e6b92a63-6d7e-4d2b-b9e3-2e054825db02",
"revision": 0,
"last_node_id": 12,
"last_link_id": 15,
"nodes": [
{
"id": 7,
"type": "GetVideoComponents",
"pos": [
-1361.201171875,
-680
],
"size": [
193.530859375,
106
],
"flags": {},
"order": 1,
"mode": 0,
"inputs": [
{
"name": "video",
"type": "VIDEO",
"link": 5
}
],
"outputs": [
{
"name": "images",
"type": "IMAGE",
"links": [
13
]
},
{
"name": "audio",
"type": "AUDIO",
"links": [
8
]
},
{
"name": "fps",
"type": "FLOAT",
"links": [
9
]
},
{
"name": "bit_depth",
"type": "COMBO",
"links": null
},
{
"name": "color_space",
"type": "COMBO",
"links": null
}
],
"properties": {
"Node name for S&R": "GetVideoComponents",
"cnr_id": "comfy-core",
"ver": "0.16.4",
"enableTabs": false,
"tabWidth": 65,
"tabXOffset": 10,
"hasSecondTab": false,
"secondTabText": "Send Back",
"secondTabOffset": 80,
"secondTabWidth": 65
}
},
{
"id": 9,
"type": "SaveVideo",
"pos": [
-430,
-680
],
"size": [
570,
520
],
"flags": {},
"order": 5,
"mode": 0,
"inputs": [
{
"name": "video",
"type": "VIDEO",
"link": 10
}
],
"outputs": [
{
"name": "video",
"type": "VIDEO",
"links": null
}
],
"properties": {
"cnr_id": "comfy-core",
"ver": "0.16.4",
"enableTabs": false,
"tabWidth": 65,
"tabXOffset": 10,
"hasSecondTab": false,
"secondTabText": "Send Back",
"secondTabOffset": 80,
"secondTabWidth": 65,
"Node name for S&R": "SaveVideo"
},
"widgets_values": [
"video/ComfyUI",
"auto",
"auto",
"auto"
],
"widgets_values_named": {
"filename_prefix": "video/ComfyUI",
"format": "auto",
"format.codec": "auto",
"codec": "auto"
}
},
{
"id": 8,
"type": "CreateVideo",
"pos": [
-750,
-680
],
"size": [
270,
150
],
"flags": {},
"order": 3,
"mode": 0,
"inputs": [
{
"name": "images",
"type": "IMAGE",
"link": 14
},
{
"name": "audio",
"shape": 7,
"type": "AUDIO",
"link": 8
},
{
"name": "fps",
"type": "FLOAT",
"widget": {
"name": "fps"
},
"link": 9
}
],
"outputs": [
{
"name": "VIDEO",
"type": "VIDEO",
"links": [
10
]
}
],
"properties": {
"Node name for S&R": "CreateVideo",
"cnr_id": "comfy-core",
"ver": "0.16.4",
"enableTabs": false,
"tabWidth": 65,
"tabXOffset": 10,
"hasSecondTab": false,
"secondTabText": "Send Back",
"secondTabOffset": 80,
"secondTabWidth": 65
},
"widgets_values": [
30,
"auto",
"sRGB",
"none"
],
"widgets_values_named": {
"fps": 30,
"bit_depth": "auto",
"color_space": "sRGB",
"codec": "none"
}
},
{
"id": 6,
"type": "LoadVideo",
"pos": [
-1790,
-680
],
"size": [
386.798828125,
510
],
"flags": {},
"order": 0,
"mode": 0,
"inputs": [],
"outputs": [
{
"name": "VIDEO",
"type": "VIDEO",
"links": [
5
]
}
],
"properties": {
"Node name for S&R": "LoadVideo",
"cnr_id": "comfy-core",
"ver": "0.16.4",
"enableTabs": false,
"tabWidth": 65,
"tabXOffset": 10,
"hasSecondTab": false,
"secondTabText": "Send Back",
"secondTabOffset": 80,
"secondTabWidth": 65
},
"widgets_values": [
"IkxCORHGJxLXORejeew86_minimax-h3.mp4",
"image"
],
"widgets_values_named": {
"file": "IkxCORHGJxLXORejeew86_minimax-h3.mp4",
"upload": "image"
}
},
{
"id": 11,
"type": "RTXTrueHDR",
"pos": [
-1070,
-680
],
"size": [
270,
178
],
"flags": {},
"order": 2,
"mode": 0,
"inputs": [
{
"name": "images",
"type": "IMAGE",
"link": 13
}
],
"outputs": [
{
"name": "hdr_images",
"type": "IMAGE",
"links": [
14,
15
]
}
],
"properties": {
"Node name for S&R": "RTXTrueHDR"
},
"widgets_values": [
100,
100,
50,
650,
true,
"HDR"
],
"widgets_values_named": {
"contrast": 100,
"saturation": 100,
"middle_gray": 50,
"luminance": 650,
"debanding": true,
"output_colorspace": "HDR"
}
},
{
"id": 12,
"type": "SaveImageAdvanced",
"pos": [
-430,
-70
],
"size": [
270,
130
],
"flags": {},
"order": 4,
"mode": 0,
"showAdvanced": true,
"inputs": [
{
"name": "images",
"type": "IMAGE",
"link": 15
}
],
"outputs": [
{
"name": "images",
"type": "IMAGE",
"links": null
}
],
"properties": {},
"widgets_values": [
"ComfyUI",
"exr",
"16-bit float",
"HDR"
],
"widgets_values_named": {
"filename_prefix": "ComfyUI",
"format": "exr",
"format.bit_depth": "16-bit float",
"format.input_color_space": "HDR"
}
}
],
"links": [
[
5,
6,
0,
7,
0,
"VIDEO"
],
[
8,
7,
1,
8,
1,
"AUDIO"
],
[
9,
7,
2,
8,
2,
"FLOAT"
],
[
13,
7,
0,
11,
0,
"IMAGE"
],
[
14,
11,
0,
8,
0,
"IMAGE"
],
[
10,
8,
0,
9,
0,
"VIDEO"
],
[
15,
11,
0,
12,
0,
"IMAGE"
]
],
"groups": [],
"config": {},
"extra": {
"workflowRendererVersion": "LG",
"ds": {
"scale": 0.9846153846153847,
"offset": [
2125,
1013.0859375
]
},
"frontendVersion": "1.53.10",
"VHS_latentpreview": false,
"VHS_latentpreviewrate": 0,
"VHS_MetadataImage": true,
"VHS_KeepIntermediate": true
},
"version": 0.4
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 38 KiB

After

Width:  |  Height:  |  Size: 12 KiB

+79 -35
View File
@@ -13,7 +13,7 @@
],
"size": [
193.530859375,
66
106
],
"flags": {},
"order": 1,
@@ -46,9 +46,20 @@
"links": [
9
]
},
{
"name": "bit_depth",
"type": "COMBO",
"links": null
},
{
"name": "color_space",
"type": "COMBO",
"links": null
}
],
"properties": {
"Node name for S&R": "GetVideoComponents",
"cnr_id": "comfy-core",
"ver": "0.16.4",
"enableTabs": false,
@@ -57,21 +68,19 @@
"hasSecondTab": false,
"secondTabText": "Send Back",
"secondTabOffset": 80,
"secondTabWidth": 65,
"Node name for S&R": "GetVideoComponents"
},
"widgets_values": []
"secondTabWidth": 65
}
},
{
"id": 1,
"type": "RTXVideoSuperResolution",
"pos": [
-1125.6703125,
-1100,
-680
],
"size": [
285.8072916666667,
106
154
],
"flags": {},
"order": 2,
@@ -93,6 +102,7 @@
}
],
"properties": {
"Node name for S&R": "RTXVideoSuperResolution",
"cnr_id": "comfyui_nvidia_rtx_nodes",
"ver": "0.1.0",
"enableTabs": false,
@@ -101,20 +111,28 @@
"hasSecondTab": false,
"secondTabText": "Send Back",
"secondTabOffset": 80,
"secondTabWidth": 65,
"Node name for S&R": "RTXVideoSuperResolution"
"secondTabWidth": 65
},
"widgets_values": [
"scale by multiplier",
2,
"ULTRA"
]
"ULTRA",
1,
"8-bit RGB"
],
"widgets_values_named": {
"resize_type": "scale by multiplier",
"resize_type.scale": 2,
"quality": "ULTRA",
"strength": 1,
"image_encoding": "8-bit RGB"
}
},
{
"id": 9,
"type": "SaveVideo",
"pos": [
-485.86302083333317,
-460,
-680
],
"size": [
@@ -131,7 +149,13 @@
"link": 10
}
],
"outputs": [],
"outputs": [
{
"name": "video",
"type": "VIDEO",
"links": null
}
],
"properties": {
"cnr_id": "comfy-core",
"ver": "0.16.4",
@@ -147,19 +171,26 @@
"widgets_values": [
"video/ComfyUI",
"auto",
"auto",
"auto"
]
],
"widgets_values_named": {
"filename_prefix": "video/ComfyUI",
"format": "auto",
"format.codec": "auto",
"codec": "auto"
}
},
{
"id": 8,
"type": "CreateVideo",
"pos": [
-797.8630208333332,
-770,
-680
],
"size": [
270,
78
150
],
"flags": {},
"order": 3,
@@ -195,6 +226,7 @@
}
],
"properties": {
"Node name for S&R": "CreateVideo",
"cnr_id": "comfy-core",
"ver": "0.16.4",
"enableTabs": false,
@@ -203,12 +235,20 @@
"hasSecondTab": false,
"secondTabText": "Send Back",
"secondTabOffset": 80,
"secondTabWidth": 65,
"Node name for S&R": "CreateVideo"
"secondTabWidth": 65
},
"widgets_values": [
30
]
30,
"auto",
"sRGB",
"none"
],
"widgets_values_named": {
"fps": 30,
"bit_depth": "auto",
"color_space": "sRGB",
"codec": "none"
}
},
{
"id": 6,
@@ -235,6 +275,7 @@
}
],
"properties": {
"Node name for S&R": "LoadVideo",
"cnr_id": "comfy-core",
"ver": "0.16.4",
"enableTabs": false,
@@ -243,13 +284,16 @@
"hasSecondTab": false,
"secondTabText": "Send Back",
"secondTabOffset": 80,
"secondTabWidth": 65,
"Node name for S&R": "LoadVideo"
"secondTabWidth": 65
},
"widgets_values": [
"input.mp4",
"image"
]
],
"widgets_values_named": {
"file": "input.mp4",
"upload": "image"
}
}
],
"links": [
@@ -261,14 +305,6 @@
0,
"VIDEO"
],
[
6,
7,
0,
1,
0,
"IMAGE"
],
[
7,
1,
@@ -293,6 +329,14 @@
2,
"FLOAT"
],
[
6,
7,
0,
1,
0,
"IMAGE"
],
[
10,
8,
@@ -307,13 +351,13 @@
"extra": {
"workflowRendererVersion": "LG",
"ds": {
"scale": 0.5824044642706089,
"scale": 0.8526983761385988,
"offset": [
2224.646676974563,
1182.1948771278435
2198.136247691975,
1166.950213406463
]
},
"frontendVersion": "1.39.19",
"frontendVersion": "1.53.6",
"VHS_latentpreview": false,
"VHS_latentpreviewrate": 0,
"VHS_MetadataImage": true,
BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 186 KiB