Round frame canvas dimensions up when scaling
This commit is contained in:
@@ -25,6 +25,8 @@ Main controls:
|
||||
- `background_r/g/b`: background color visible where the frame does not cover the canvas
|
||||
- `resize_algorithm`: resize method
|
||||
|
||||
When canvas or frame sizes are recalculated from scaling, multiples, dragging, or zooming, fractional sizes are rounded up so the result does not undershoot the requested size. Hard `max_width` and `max_height` limits can still force the processing layout smaller.
|
||||
|
||||
Frontend preview:
|
||||
- Blue outline: requested content canvas inside the larger processing canvas
|
||||
- White outline: frame/image bounding box
|
||||
@@ -37,8 +39,9 @@ Outputs:
|
||||
- `recut_frames`: full recut frame batch on the processing canvas
|
||||
- `preview_first_frame`: first frame only, useful for connecting to a normal ComfyUI Preview Image node
|
||||
- `extended_area_mask`: mask frame sequence, repeated to match the input batch
|
||||
- `actual_canvas_width`, `actual_canvas_height`: final processing canvas size
|
||||
- `content_x`, `content_y`, `content_width`, `content_height`: position and size of the requested content canvas inside the processing canvas. Use this metadata later to crop away padding.
|
||||
- `final_canvas_width`, `final_canvas_height`: exact requested canvas size
|
||||
- `processing_canvas_width`, `processing_canvas_height`: final processing canvas size
|
||||
- `crop_x`, `crop_y`, `crop_width`, `crop_height`: position and size of the requested content canvas inside the processing canvas. Use this metadata later to crop away padding.
|
||||
|
||||
## Install
|
||||
|
||||
|
||||
@@ -2,9 +2,13 @@ import { app } from "../../../scripts/app.js";
|
||||
import { HANDLE_SIZE } from "./constants.js";
|
||||
import { getValue } from "./utils.js";
|
||||
|
||||
function ceilDimension(value) {
|
||||
return Math.max(1, Math.ceil(Number(value)));
|
||||
}
|
||||
|
||||
function ceilToMultiple(value, multiple) {
|
||||
value = Math.max(1, Math.round(Number(value)));
|
||||
multiple = Math.max(1, Math.round(Number(multiple)));
|
||||
value = ceilDimension(value);
|
||||
multiple = ceilDimension(multiple);
|
||||
return Math.ceil(value / multiple) * multiple;
|
||||
}
|
||||
|
||||
@@ -17,7 +21,7 @@ function floorToMultiple(value, multiple) {
|
||||
export function getTargetSize(node, canvasW, canvasH) {
|
||||
const targetW = Number(getValue(node, "target_width", canvasW));
|
||||
const targetH = Number(getValue(node, "target_height", canvasH));
|
||||
return { w: Math.max(1, Math.round(targetW)), h: Math.max(1, Math.round(targetH)) };
|
||||
return { w: ceilDimension(targetW), h: ceilDimension(targetH) };
|
||||
}
|
||||
|
||||
export function getAnchorBase(node, canvasW, canvasH, frameW, frameH) {
|
||||
@@ -32,15 +36,15 @@ export function getAnchorBase(node, canvasW, canvasH, frameW, frameH) {
|
||||
|
||||
export function getProcessingLayout(node, requestedCanvasW, requestedCanvasH) {
|
||||
const anchor = getValue(node, "anchor", "top_left");
|
||||
const multiple = Math.max(1, Math.round(Number(getValue(node, "processing_multiple", 32))));
|
||||
const minW = Math.max(1, Math.round(Number(getValue(node, "min_width", 512))));
|
||||
const minH = Math.max(1, Math.round(Number(getValue(node, "min_height", 512))));
|
||||
const maxW = Math.max(1, Math.round(Number(getValue(node, "max_width", 2048))));
|
||||
const maxH = Math.max(1, Math.round(Number(getValue(node, "max_height", 2048))));
|
||||
const multiple = ceilDimension(getValue(node, "processing_multiple", 32));
|
||||
const minW = ceilDimension(getValue(node, "min_width", 512));
|
||||
const minH = ceilDimension(getValue(node, "min_height", 512));
|
||||
const maxW = ceilDimension(getValue(node, "max_width", 2048));
|
||||
const maxH = ceilDimension(getValue(node, "max_height", 2048));
|
||||
const paddingStrategy = getValue(node, "padding_strategy", "centered");
|
||||
|
||||
let contentW = Math.max(1, Math.round(requestedCanvasW));
|
||||
let contentH = Math.max(1, Math.round(requestedCanvasH));
|
||||
let contentW = ceilDimension(requestedCanvasW);
|
||||
let contentH = ceilDimension(requestedCanvasH);
|
||||
let target = getTargetSize(node, contentW, contentH);
|
||||
let xOffset = Math.round(Number(getValue(node, "x_offset", 0)));
|
||||
let yOffset = Math.round(Number(getValue(node, "y_offset", 0)));
|
||||
@@ -49,9 +53,9 @@ export function getProcessingLayout(node, requestedCanvasW, requestedCanvasH) {
|
||||
const maxAlignedH = floorToMultiple(maxH, multiple);
|
||||
let scale = Math.min(1.0, maxAlignedW / contentW, maxAlignedH / contentH);
|
||||
|
||||
contentW = Math.max(1, Math.round(contentW * scale));
|
||||
contentH = Math.max(1, Math.round(contentH * scale));
|
||||
target = { w: Math.max(1, Math.round(target.w * scale)), h: Math.max(1, Math.round(target.h * scale)) };
|
||||
contentW = ceilDimension(contentW * scale);
|
||||
contentH = ceilDimension(contentH * scale);
|
||||
target = { w: ceilDimension(target.w * scale), h: ceilDimension(target.h * scale) };
|
||||
xOffset = Math.round(xOffset * scale);
|
||||
yOffset = Math.round(yOffset * scale);
|
||||
|
||||
@@ -62,9 +66,9 @@ export function getProcessingLayout(node, requestedCanvasW, requestedCanvasH) {
|
||||
|
||||
if (contentW > processingW || contentH > processingH) {
|
||||
const scale2 = Math.min(processingW / contentW, processingH / contentH);
|
||||
contentW = Math.max(1, Math.round(contentW * scale2));
|
||||
contentH = Math.max(1, Math.round(contentH * scale2));
|
||||
target = { w: Math.max(1, Math.round(target.w * scale2)), h: Math.max(1, Math.round(target.h * scale2)) };
|
||||
contentW = ceilDimension(contentW * scale2);
|
||||
contentH = ceilDimension(contentH * scale2);
|
||||
target = { w: ceilDimension(target.w * scale2), h: ceilDimension(target.h * scale2) };
|
||||
xOffset = Math.round(xOffset * scale2);
|
||||
yOffset = Math.round(yOffset * scale2);
|
||||
scale *= scale2;
|
||||
@@ -100,8 +104,8 @@ export function getProcessingLayout(node, requestedCanvasW, requestedCanvasH) {
|
||||
}
|
||||
|
||||
export function getCanvasInfo(node, image) {
|
||||
const requestedCanvasW = Math.max(1, Math.round(Number(getValue(node, "canvas_width", 1280))));
|
||||
const requestedCanvasH = Math.max(1, Math.round(Number(getValue(node, "canvas_height", 720))));
|
||||
const requestedCanvasW = ceilDimension(getValue(node, "canvas_width", 1280));
|
||||
const requestedCanvasH = ceilDimension(getValue(node, "canvas_height", 720));
|
||||
const layout = getProcessingLayout(node, requestedCanvasW, requestedCanvasH);
|
||||
|
||||
const canvasW = layout.processingW;
|
||||
|
||||
@@ -2,17 +2,21 @@ import { MIN_FRAME_SIZE } from "./constants.js";
|
||||
import { clamp, getPoint, getValue, setValue } from "./utils.js";
|
||||
import { findHandle, getAnchorBase, getCanvasInfo, pointInsideRect, previewPointToCanvas, rectsIntersect } from "./geometry.js";
|
||||
|
||||
function ceilFrameSize(value) {
|
||||
return Math.max(MIN_FRAME_SIZE, Math.ceil(Number(value)));
|
||||
}
|
||||
|
||||
function applyTargetTransform(node, info, newPasteX, newPasteY, newW, newH) {
|
||||
newW = Math.max(MIN_FRAME_SIZE, Math.round(newW));
|
||||
newH = Math.max(MIN_FRAME_SIZE, Math.round(newH));
|
||||
newW = ceilFrameSize(newW);
|
||||
newH = ceilFrameSize(newH);
|
||||
|
||||
const safeScale = info.scaleFactor || 1;
|
||||
const localPasteX = newPasteX - (info.contentX || 0);
|
||||
const localPasteY = newPasteY - (info.contentY || 0);
|
||||
const base = getAnchorBase(node, info.contentW, info.contentH, newW, newH);
|
||||
|
||||
setValue(node, "target_width", Math.max(MIN_FRAME_SIZE, Math.round(newW / safeScale)));
|
||||
setValue(node, "target_height", Math.max(MIN_FRAME_SIZE, Math.round(newH / safeScale)));
|
||||
setValue(node, "target_width", ceilFrameSize(newW / safeScale));
|
||||
setValue(node, "target_height", ceilFrameSize(newH / safeScale));
|
||||
setValue(node, "x_offset", Math.round((localPasteX - base.x) / safeScale));
|
||||
setValue(node, "y_offset", Math.round((localPasteY - base.y) / safeScale));
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import math
|
||||
import os
|
||||
import uuid
|
||||
import numpy as np
|
||||
@@ -196,8 +197,11 @@ class MAIFrameCanvasRecut:
|
||||
CATEGORY = "mAI/video"
|
||||
OUTPUT_NODE = True
|
||||
|
||||
def _ceil_dimension(self, value):
|
||||
return max(1, int(math.ceil(float(value))))
|
||||
|
||||
def _ceil_to_multiple(self, value, multiple):
|
||||
value = max(1, int(round(value)))
|
||||
value = self._ceil_dimension(value)
|
||||
multiple = max(1, int(multiple))
|
||||
return ((value + multiple - 1) // multiple) * multiple
|
||||
|
||||
@@ -222,15 +226,15 @@ class MAIFrameCanvasRecut:
|
||||
max_height,
|
||||
padding_strategy,
|
||||
):
|
||||
canvas_w = max(1, int(canvas_w))
|
||||
canvas_h = max(1, int(canvas_h))
|
||||
target_w = max(1, int(target_w))
|
||||
target_h = max(1, int(target_h))
|
||||
processing_multiple = max(1, int(processing_multiple))
|
||||
min_width = max(1, int(min_width))
|
||||
min_height = max(1, int(min_height))
|
||||
max_width = max(1, int(max_width))
|
||||
max_height = max(1, int(max_height))
|
||||
canvas_w = self._ceil_dimension(canvas_w)
|
||||
canvas_h = self._ceil_dimension(canvas_h)
|
||||
target_w = self._ceil_dimension(target_w)
|
||||
target_h = self._ceil_dimension(target_h)
|
||||
processing_multiple = self._ceil_dimension(processing_multiple)
|
||||
min_width = self._ceil_dimension(min_width)
|
||||
min_height = self._ceil_dimension(min_height)
|
||||
max_width = self._ceil_dimension(max_width)
|
||||
max_height = self._ceil_dimension(max_height)
|
||||
|
||||
max_aligned_w = self._floor_to_multiple(max_width, processing_multiple)
|
||||
max_aligned_h = self._floor_to_multiple(max_height, processing_multiple)
|
||||
@@ -241,10 +245,10 @@ class MAIFrameCanvasRecut:
|
||||
max_aligned_h / max(1, canvas_h),
|
||||
)
|
||||
|
||||
safe_canvas_w = max(1, int(round(canvas_w * scale)))
|
||||
safe_canvas_h = max(1, int(round(canvas_h * scale)))
|
||||
safe_target_w = max(1, int(round(target_w * scale)))
|
||||
safe_target_h = max(1, int(round(target_h * scale)))
|
||||
safe_canvas_w = self._ceil_dimension(canvas_w * scale)
|
||||
safe_canvas_h = self._ceil_dimension(canvas_h * scale)
|
||||
safe_target_w = self._ceil_dimension(target_w * scale)
|
||||
safe_target_h = self._ceil_dimension(target_h * scale)
|
||||
safe_x_offset = int(round(x_offset * scale))
|
||||
safe_y_offset = int(round(y_offset * scale))
|
||||
|
||||
@@ -256,10 +260,10 @@ class MAIFrameCanvasRecut:
|
||||
# If min/max are contradictory, keep the aligned max and scale the composition down again.
|
||||
if safe_canvas_w > processing_w or safe_canvas_h > processing_h:
|
||||
scale2 = min(processing_w / safe_canvas_w, processing_h / safe_canvas_h)
|
||||
safe_canvas_w = max(1, int(round(safe_canvas_w * scale2)))
|
||||
safe_canvas_h = max(1, int(round(safe_canvas_h * scale2)))
|
||||
safe_target_w = max(1, int(round(safe_target_w * scale2)))
|
||||
safe_target_h = max(1, int(round(safe_target_h * scale2)))
|
||||
safe_canvas_w = self._ceil_dimension(safe_canvas_w * scale2)
|
||||
safe_canvas_h = self._ceil_dimension(safe_canvas_h * scale2)
|
||||
safe_target_w = self._ceil_dimension(safe_target_w * scale2)
|
||||
safe_target_h = self._ceil_dimension(safe_target_h * scale2)
|
||||
safe_x_offset = int(round(safe_x_offset * scale2))
|
||||
safe_y_offset = int(round(safe_y_offset * scale2))
|
||||
scale *= scale2
|
||||
@@ -306,7 +310,7 @@ class MAIFrameCanvasRecut:
|
||||
}
|
||||
|
||||
def _get_resized_size(self, target_w, target_h):
|
||||
return max(1, int(target_w)), max(1, int(target_h))
|
||||
return self._ceil_dimension(target_w), self._ceil_dimension(target_h)
|
||||
|
||||
def _anchor_position(
|
||||
self,
|
||||
@@ -607,18 +611,18 @@ class MAIFrameCanvasRecut:
|
||||
batch, src_h, src_w, channels = frames.shape
|
||||
|
||||
layout = self._get_processing_layout(
|
||||
canvas_w=int(canvas_width),
|
||||
canvas_h=int(canvas_height),
|
||||
target_w=int(target_width),
|
||||
target_h=int(target_height),
|
||||
canvas_w=canvas_width,
|
||||
canvas_h=canvas_height,
|
||||
target_w=target_width,
|
||||
target_h=target_height,
|
||||
x_offset=int(x_offset),
|
||||
y_offset=int(y_offset),
|
||||
anchor=anchor,
|
||||
processing_multiple=int(processing_multiple),
|
||||
min_width=int(min_width),
|
||||
min_height=int(min_height),
|
||||
max_width=int(max_width),
|
||||
max_height=int(max_height),
|
||||
processing_multiple=processing_multiple,
|
||||
min_width=min_width,
|
||||
min_height=min_height,
|
||||
max_width=max_width,
|
||||
max_height=max_height,
|
||||
padding_strategy=padding_strategy,
|
||||
)
|
||||
|
||||
@@ -732,8 +736,8 @@ class MAIFrameCanvasRecut:
|
||||
canvas,
|
||||
preview,
|
||||
mask,
|
||||
int(canvas_width),
|
||||
int(canvas_height),
|
||||
self._ceil_dimension(canvas_width),
|
||||
self._ceil_dimension(canvas_height),
|
||||
int(canvas_w),
|
||||
int(canvas_h),
|
||||
int(content_x),
|
||||
@@ -750,4 +754,4 @@ NODE_CLASS_MAPPINGS = {
|
||||
|
||||
NODE_DISPLAY_NAME_MAPPINGS = {
|
||||
"MAIFrameCanvasRecut": "mAI Frame Canvas Recut",
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
import unittest
|
||||
|
||||
from nodes import MAIFrameCanvasRecut
|
||||
|
||||
|
||||
class FrameCanvasRecutLayoutTests(unittest.TestCase):
|
||||
def test_scaled_layout_dimensions_round_up(self):
|
||||
node = MAIFrameCanvasRecut()
|
||||
|
||||
layout = node._get_processing_layout(
|
||||
canvas_w=999,
|
||||
canvas_h=1000,
|
||||
target_w=999,
|
||||
target_h=1000,
|
||||
x_offset=0,
|
||||
y_offset=0,
|
||||
anchor="top_left",
|
||||
processing_multiple=1,
|
||||
min_width=1,
|
||||
min_height=1,
|
||||
max_width=512,
|
||||
max_height=512,
|
||||
padding_strategy="centered",
|
||||
)
|
||||
|
||||
self.assertEqual(layout["content_w"], 512)
|
||||
self.assertEqual(layout["content_h"], 512)
|
||||
self.assertEqual(layout["target_w"], 512)
|
||||
self.assertEqual(layout["target_h"], 512)
|
||||
|
||||
def test_ceil_to_multiple_uses_upper_integer(self):
|
||||
node = MAIFrameCanvasRecut()
|
||||
|
||||
self.assertEqual(node._ceil_to_multiple(32.1, 32), 64)
|
||||
|
||||
def test_size_inputs_are_not_truncated(self):
|
||||
node = MAIFrameCanvasRecut()
|
||||
|
||||
layout = node._get_processing_layout(
|
||||
canvas_w=728.1,
|
||||
canvas_h=90.1,
|
||||
target_w=512.1,
|
||||
target_h=256.1,
|
||||
x_offset=0,
|
||||
y_offset=0,
|
||||
anchor="top_left",
|
||||
processing_multiple=32,
|
||||
min_width=1,
|
||||
min_height=1,
|
||||
max_width=2048,
|
||||
max_height=2048,
|
||||
padding_strategy="pad_right_bottom",
|
||||
)
|
||||
|
||||
self.assertEqual(layout["content_w"], 729)
|
||||
self.assertEqual(layout["content_h"], 91)
|
||||
self.assertEqual(layout["target_w"], 513)
|
||||
self.assertEqual(layout["target_h"], 257)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user