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.
Paint everything except a person or a pet, turn on fill holes, and it
filled the unpainted subject too, so the part meant to stay was repainted.
The new optional max_hole_size input (a percent of the picture, 0 to 100)
sets the biggest hole fill holes fills. The default, 0, fills every hole
exactly as before, so saved workflows and API prompts run the same; at 2
the gaps a quick brush leaves still fill and a subject left unpainted
stays. Areas touching the picture's edge are never holes, as before.
- Hole sizes come from scipy's labels, the same 4-connected holes its
fill finds; the torch fallback labels the same 8-connected holes as its
own fill. Each picture in a batch is measured on its own, after expand.
- The card gets a Max hole size row with a % unit, dimmed while Fill holes
is off, and a resetUnknown fallback of 0 for 2.x saves that hold the
card's empty value in its slot.
- Tests: a hole above the limit stays, holes up to it fill, 0 matches the
old fill, edge-touching areas never fill, the torch fallback agrees,
batches, and older saves opening with 0.
- Node API snapshot, help page and changelog.
A dark or colourful picture padded with the #808080 gray came back from
Krea 2 AnyPaint with the gray still there: a gray frame around the picture,
or gray corners on a turned one. AnyPaint was trained with the new area of
its reference filled with the median colour of the picture, so a gray patch
next to a dark photo reads to it as part of the scene and it paints it back.
It did the same with every Krea 2 model file tried.
Krea 2 Encode gets an optional mask input, appended last. When it is wired,
the new area of the reference (mask above 0.5) is refilled with the median
colour of the rest before it reaches the VAE and the vision tower. The
canvas the rest of the graph uses keeps its gray. Without the link nothing
changes.
Seam is a new optional choice, appended after color_match. "classic"
is the default and the old stitch, bit for bit. "blend in" hands the
model's picture over to the source by depth into the picture: colour
and tone change over slowly, fine detail changes over in a short ramp
where the two line up, the new area keeps the model's pixels, and the
deep picture keeps its own. There is no global tone shift, so
color_match and fix_edge_halo do not apply and the card hides them.
The depth map comes from the generated-area mask of a Crop + Rotate
+ Pad stitcher, or from the source rectangle of a Load Image + Pad
stitcher, with sides flush with the frame left out. It is computed
once per stitcher and shared by every frame, with scipy when present
and an exact torch fallback otherwise. Crop For Inpaint stitchers
keep the classic stitch. With blend in, blend_mask is the hand-over
weight.
An older workflow can carry the card's empty value in the slot Seam
now takes; the card resets it to classic on load and the node accepts
it as classic. Classic output is pinned against the previous code for
Load Image + Pad, Crop + Rotate + Pad, Crop For Inpaint and clip
stitchers.
When the grown context box was bigger than the picture on an axis (a wide
or tall mask, or a small picture), fit_rect centred it and the overflow was
replicate-padded: the sampler got stretched copies of the edge pixels, and
a megapixel target spent its budget on them. On a 1184x2112 portrait with
the coat painted, only 42% of the canvas was the picture and the new part
was drawn at 0.52x.
New optional input keep_inside (card row "Stay in picture", on by
default) clamps the box to the picture on the overflowing axis before it
is fitted, so the crop holds only real pixels (0.80x on that portrait).
The helper keeps the old padding by default for existing callers; off on
the node is the old crop. Appended as the last optional input, so saved
workflows keep their values; ones whose box ran past the picture now
render a little differently.
An edit made at the picture's own size loses whatever the model pushed
past the canvas edge, so realigning it left an empty strip on 13 of 16
held-out edits. Realign to Source now takes an optional stitcher: given
Load Image + Pad's stitcher for the padded canvas the edit was made on,
it measures inside the picture's area only and returns just that area,
so what the model drew into the margin fills the edges.
- A drift within half a pixel of a whole-pixel shift is cut straight
out of the edit with no resampling; an edit that already lines up
comes back untouched.
- The coarse pass also tries the edit at 0.86x-1.28x, so a large zoom
is found; models often reframe a padded canvas by zooming in to fill
it, and that zoom does not count toward Max zoom.
- The report says whether the frame was cut out or warped and names the
margin each side needed when an edit ran out of picture.
Load Image + Pad gains a mirror fill, which models read as more of the
scene: over 16 edits it provoked a reframe on 2, against 7 for edge
pixels and 5 for flat gray. The Qwen Image 2.1 Edit + Realign example
now edits on a 64 px mirror margin with the stitcher wired.
Qwen Image 2.1 draws broad edits (watercolor, anime, oil) a few percent
taller than the source, up to about 12%, differently for every seed, and
snapping sizes does not change it. Realign to Source measures that zoom
and shift against the source and warps the edit back onto the source's
frame, at the source's size. empty_mask marks the strip the model pushed
out of view; report gives the measured zoom, shift and worst corner, or
why a frame was left as it is. The display name carries EXPERIMENTAL.
Promoted from the lab node. The measurement is unchanged (the same
matrices on 166 real Qwen 2.1 renders). The warp now samples the edit
with its edge pixels extended for every fill, so the gray and source
fills no longer draw a dark line beside the strip.
New example: Qwen Image 2.1 Edit + Realign, the Qwen 2.1 Edit graph with
the realign step after decode, its report, and before and after compares.
An optional source_image socket pads an image from another node instead
of the chosen file, so a step before the pad (flattening a transparent
picture onto white, say) no longer has to be saved and loaded back.
- The wire wins over the file; validation skips the file check while it
is connected and runs core's link-type check itself, since asking for
input_types turns core's off for the node.
- A wired run hands the canvas a small temp preview (one file per node,
overwritten each run) and the true source size, so the stage draws the
last image it padded; before the first run it shows an outline.
- Card rows gain linkedBy, so the Source row dims while a wire replaces it.
- Appended and optional with no widget: saved workflows and API prompts
load unchanged. Node API snapshot refreshed.
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.
- 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.
Save Image joins the shared input_preview panel with the PREVIEW switch,
backed by a new optional preview input (display only: the file is saved
and listed in the queue either way). The embed row reads
Embed workflow: no | yes.
Video Crop + Rotate + Pad -> Frame had gained its four resize inputs as
required, so an API prompt saved before them stopped validating. They
are optional now; the widget order is unchanged.
tests/test_node_api.py loads the pack the way ComfyUI does and holds every
node to tests/fixtures/node_api.json: inputs and outputs may only be
appended, never renamed, reordered, removed or newly required. Its rule
tests need only the standard library and run in CI.