With Resize on, Image Crop + Rotate + Pad and both Video Crop + Rotate +
Pad nodes resized the padded canvas as one picture. Along every seam that
mixed fill into the picture and left the mask part on, part off. A hard
paste (stitch blend 0) kept that column as a faint line, and a band under a
pixel wide after the resize came out as nothing but that column: 1 px of
padding at a 0.62 scale left a line in the stitched result whatever the
model painted.
resize_then_pad (nodes/_transform_engine.py) resizes only the picture, with
the chosen method, into the box it has on the resized canvas, and puts the
fill around it. Bands are whole pixels, the picture's edge is picture, and
a band that comes to under half a pixel is left out. The mask is 1 on the
fill; inside the box it is the transform's own mask there (the feather's
ramp, the corners of a turn, see-through parts), resized bilinear with the
picture. The prompt view is placed the same way.
The box is transform_picture_box (nodes/_inpaint_crop_helpers.py), which
the stitcher now uses for its source bbox too, so the paste and the bbox
cannot disagree. It is never empty.
The output size is unchanged. With Resize off, or with no padding, image
and mask are bit for bit what they were. With padding, the picture can sit
up to half a pixel from where it did.
Image Crop + Rotate + Pad and both Video Crop + Rotate + Pad nodes rounded
the canvas up to Divisible by and put the pixels left over on the right and
bottom, whatever was padded. A 1916 x 821 picture with 3 px of padding on
the left and Divisible by 24 came to 1920 wide, with 1 px of fill on the
right edge, which had no padding. With Resize on, that strip was smeared
into the picture's last column, and the stitched result kept it as a light
line.
The canvas now rounds one axis at a time by Load Image + Pad's rule
(_pad_helpers._axis_layout): the leftover joins a side that was padded, the
right or bottom one when both are, and an axis nobody padded has its crop
trimmed evenly to the multiple below. A picture smaller than one multiple
still grows on the far side. With no padding on an axis the size therefore
rounds down, not up: 1000 x 750 at 64 gives 960 x 704, was 1024 x 768.
Inputs and outputs are unchanged.
The editor uses the same rule (axisLayout, resolvePadding and keptCrop in
js/shared/transform_geometry.mjs), so the picture on the node, the size
line and the editor match the run. The size line names fill added on any
side and pixels trimmed, and the centre buttons count fill that sits on the
left or top. The Divisible by tooltip and help pages describe the rule, and
the Krea 2 Outpaint Model Patch warning no longer says Divisible by rounds
the other axis up.
tests/test_transform_engine.py and tests/transform_geometry.test.mjs share
one fixture table, so the editor and the run are held to the same numbers.
A part painted in the MaskEditor (see-through in the saved file) was
always handed on as flat fill. The new optional painted_area input,
"fill" by default, can be set to "keep picture": the mask is unchanged
and the image, original and prompt_image outputs carry the stored
picture there. The editor's Inpaint & Stitch section has the switch.
The same pixels as before, from integer maths over the see-through part's
bounding box only: a 17-megapixel cutout no longer spends seconds on
floating-point work across the whole canvas.
A see-through part of a picture - a cutout's missing background, an oval
photo's corners, a hole - kept its stored colour blended over the fill.
That colour is often black or the old background, so the model got a
dark or light ring round the picture and Stitch Inpaint pasted part of it
back. A pixel less than 90% as solid as the picture's most solid pixel is
now empty, like a corner a turn leaves: filled with the fill colour,
marked in the mask, new area for both seams. Kept pixels are fully solid
in their own colour.
A new last output, prompt_image, is the image with see-through parts on
white instead of the fill, for the node that writes the prompt: shown the
gray fill, a prompt writer describes a gray backdrop and the model keeps
it. original shows see-through parts on white instead of black.
A picture at least 90% solid everywhere goes through exactly as before;
the new tests hold every output and both seams to the old transform.
Resolved:
- node_image_crop_rotate_pad.py: optional inputs are the four resize
inputs, then stitch_blend, then stitch_grow; node_api.json regenerated
with test_node_api.py --update (no breaking change against main).
- test_transform_outputs.py: the optional-inputs test expects the stitch
inputs after the resize ones.
- transform_editor.mjs: the Inpaint & Stitch section and its status line
serve the image node too (feather branch) beside the size-chain status
(this branch); the Cancel code is #73's, line for line.
- LTX example note and the Clip help page carry both branches' wording.
- CHANGELOG keeps every bullet.
fix/transform-mask-only-feather stops the transform fading the picture,
so the canvas the stitcher holds is already the unfaded one. Remove
transform_pil_layers, keep_unfaded and resize_transform_to_megapixels,
their three tests and the CHANGELOG bullet; the image node keeps its
optional resize inputs and the integer aspect maths stays.
- Rotating keeps the crop's size over the same part of the picture from
the knob, slider, number box and Reset rotation (cropForRotation).
- A readout under the picture names every step that sets the output size
(crop -> pad -> align -> resize) and warns when the resize Step
stretches the picture or Align pads fill ahead of a resize; ticking
Resize shows Megapixels and Step on the face.
- Locked-aspect crops use integer maths (_ratio_box / ratioBox), so the
size shown equals the run.
- Image Crop + Rotate + Pad's four resize inputs are optional, so API
prompts saved before them validate again.
- Video Crop + Rotate + Pad -> Clip: one Length switch replaces the Fixed
frames and Limit boxes; OUT sits on the last output frame; a connected
fixed_frames / frame_load_cap takes over the length.
- The stitcher pastes the unfaded canvas when feather > 0
(transform_pil_layers / keep_unfaded). Superseded by mask-only feather
and dropped in the next commit.
Carries a copy of the editor Cancel / discard-prompt change from
feat/editor-cancel-discard (#73), which this work was built on.
The image and video transform nodes faded the picture into the fill
colour across the feather ramp. Outpaint models read that ramp as
content (the Qwen 2.1 outpaint LoRA painted a darker band along a tilted
edge, LTX's IC-LoRA left feathered bars unpainted), and the stitcher
kept the faded pixels, so Stitch's colour match measured its drift
against them and pulled the fill toward grey (d(ab) 18.5 at feather 24 on
a straight pad, 2.6 now). Feather now shapes only the mask and the stitch
blend built from it; the picture keeps a hard edge against the fill, so a
feathered canvas is the feather-0 canvas pixel for pixel.
Image Crop + Rotate + Pad gains the clip node's optional stitch_blend
(32) and stitch_grow (0) inputs, and its editor shows the same Inpaint &
Stitch section with Show blend. The editor's live fade overlay goes with
the fade.
- Optional resize of the transformed output to a pixel budget, with core
Scale Image to Total Pixels semantics: megapixels x 1024^2, aspect
preserved, each dimension rounded to resolution_steps. The image uses
the chosen filter (lanczos/area/bicubic/bilinear/nearest-exact); the
mask always resizes bilinear so feathered edges cannot ring. The math
is mirrored py+js (scale_to_megapixels / scaleToMegapixels) and
fixture-tested on both sides; the new widgets append after the stable
V1 set so saved workflows keep loading, and IS_CHANGED fingerprints
them.
- A quick row under the node's canvas: Reset (rotation, crop, padding),
a Feather toggle that remembers the amount it turns off, and the
Resize toggle with its megapixel scrub box.
- The editor gains a Resize output section (megapixels and steps as
scrub controls, method select) and its status names the exact resized
size.
- The output size readout moved off the pixels: centered below the image,
flipping above when clipped, and showing the resize target.
- CHANGELOG entries for this round and the two commits before it.
Feathering had two defects: the mask used max(original, blur), and a
blurred step edge sits at ~50% on the boundary, so the mask jumped
255->127 in one pixel - the visible seam line - and the output image was
never blended at all, ending in a hard cut against the fill. The mask
ramp is now pinned to full strength exactly at the edge (double the blur
and clip, then max with the original for thin slivers), and the visible
output fades into the fill color across the same ramp, so outpaints
blend smoothly in both the image and the mask.
The rotation knob now anchors to the outermost top-right corner of
source-union-output, so it stays visually attached when padding grows
the canvas instead of floating where the source corner used to be.
Save & close is now filled with the AusBoss accent; Reset all is styled
as a destructive red button.
22 Python + 9 JS tests pass, including new asserts that the mask ramp
holds >=200 on the first kept column (was ~127) and that the image
visibly blends into the fill. Live-browser verified: knob tracks the
outermost corner under 140/90px padding, buttons computed styles match.