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.
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: CHANGELOG keeps both bullets. The feather branch's
perfect-continuation test left rotated-and-padded out because of this
bug; with both fixes in, it covers that layout too.
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.
On a Crop + Rotate + Pad canvas that was rotated and padded (a canvas
multiple's few pixels count), the source rectangle is the crop, which
holds the rotation's empty corners. color_match read their fill as
picture and shifted the whole fill toward it: up to 33 dE on a flat
test, 57 with a resize. The transform stitcher now carries its
generated-area mask; the seam reading skips void pixels (2 px clear of
them), and a line with no picture takes the band's global offset.
Load Image + Pad and Crop For Inpaint carry no mask and stitch exactly
as before; a pin test holds Load Image + Pad's colour-matched bands to
their old values.
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.
The image and clip transform nodes could resize their output to a pixel
budget; the frame picker, whose single frame often feeds an image or
video model, could not. It takes the same appended resize inputs, off by
default, and its face and editor show them like the other two.
Load Image + Pad, Image Resize, Align Image and the Clip node all hand
their width and height to the next node; Image Crop + Rotate + Pad and
Video Crop + Rotate + Pad → Frame did not, so sizing a latent from them
took an extra Image Size node. Both append width and height outputs.