๐ Aioli Mask BBox
The crop step that stops your inpaint from drifting
- image
- mask
- image_cropped
- mask_cropped
- x
- y
- orig_width
- orig_height
- width
- height
- target_size
The problem this node exists for
Hand an edit model the whole frame and it hands you back a whole new frame. The pixels you didn't mask come back close to what you had - not identical - and across three edits, "close" compounds into mush.
The answer the community converged on is crop, edit, stitch: cut a rectangle around the mask, run the model on just that rectangle, paste the result back at the same coordinates. Your unmasked pixels never touch the VAE, and a small face gets 1024px of generation budget instead of 70.
๐ Aioli Mask BBox is the "crop" half of that pattern, in one node. Feed it the full image and the full mask; it finds the bounding box itself, pads it, snaps the crop to a VAE-friendly multiple, optionally scales it toward a target resolution, and hands back the crop plus every number you need to paste it home again.
It replaces the old two-node chain of Mask Bounding Box (ComfyUI Essentials) โ BBox Multiple Fix. Essentials went maintenance-only in April 2025 - the author's argument being that the first step of every inpaint graph shouldn't depend on it.
How it works
The bbox comes from torch.nonzero(mask > 0) and min/max on the index tensors; padding is applied in source coordinates and clamped to the image. If your mask is a different resolution than your image, the mask gets resized onto the image - the inverse of what Mask Bounding Box did.
The interesting part is compute_crop() in bbox_core.py. It measures the space available around the bbox centre and constrains the crop to it, then fits the ratio with a GCD-based multiplier so it stays exact - no rounding drift, even when the mask touches the image border. Resizing is Lanczos via PIL, with a hard 2048px ceiling. The legacy node calls the same function, so both give bit-identical crops.
The inputs you actually touch
Five of the seven are set-and-forget. Two aren't:
padding- margin in pixels around the bbox, before rounding. Default0; for anything but a tightly-drawn mask you want 24โ64. It replaces bothpaddingandblurfrom Mask Bounding Box, which only ever widened the box.target-noneยท512ยท768ยท1024ยท1536ยท2048. This is the "render the crop at the model's native size" knob:1024for SDXL/Klein/Qwen,512for SD 1.5,noneto just crop.
multiple (8, 16, 32, 64) should match your VAE - 16 (Flux) is the sane default. force_square is off by default, but the pack's own ComfyCloud subgraph ships it on, because most edit models quietly recrop or letterbox non-square input, and that internal recrop is what shifts your pixels. force_target_downscale only matters when the bbox is bigger than target and you want it shrunk.
Wiring the nine outputs
image_cropped and mask_cropped go into VAE Encode - or, on a modern edit model, serve as the reference latent. x and y go into ImageCompositeMasked. orig_width/orig_height are the crop's size in the source, before scaling - that's what you resize the decoded crop back to. width/height are what you sent to the model, and target_size is the numeric target (0 for none), so it plugs straight into ImageResize+.
Plain path: Aioli Mask BBox โ VAE Encode โ KSampler โ VAEDecode โ ImageCompositeMasked (x, y).
Scale path: same, but VAEDecode โ ImageResize+ (โ orig_width/orig_height) โ ImageCompositeMasked (x, y).
Install
ComfyUI Manager โ search "Aioli Nodes" โ Install โ restart. Or:
cd ComfyUI/custom_nodes
git clone https://github.com/aiolicollective/aioli-nodes
No dependency step: the package declares none, and the source only imports torch, numpy and Pillow, all of which ComfyUI already ships.
Where people get bitten
An all-black mask no longer crashes - the bbox falls back to the whole image and logs a warning, so if your crop came back as the entire frame, that's why. The node handles one mask per run: a list of masks makes ComfyUI execute it once per item and collect the results, which is how the pack's multi-region examples work.
force_square when the box is bigger than your image. If max(bbox_w, bbox_h) exceeds the smallest source dimension, the square won't fit: the crop is clamped to a rectangle and then stretched to square. The node warns and still returns orig_width/orig_height pre-stretch. Set your downstream ImageResize+ to stretch mode (keep_proportion = False) or the paste-back comes home squashed.
Your target silently doing nothing. When the bbox is larger than target and force_target_downscale is off, the node ignores the target and rounds to multiple with a 2048 cap instead. By design, but it reads as a broken widget.
Should you switch to it?
If Inpaint Crop and Stitch already works in your graph, don't rewrite anything. What you get here is the numbers as first-class outputs - coordinates, source crop size, final size - which is exactly what multi-region work needs. The pack has almost no Reddit footprint, so judge it on the source, which is short and readable.
Inputs (7)
| Name | Type | Default | Description |
|---|---|---|---|
| image | IMAGE | โ | |
| mask | MASK | โ | |
| padding | INT | 00โ4096 | โ |
| multiple | COMBO | 4 options: 8 (VAE minimum), 16 (Flux), 32 (SD1.5), 64 (SDXL) | |
| target | COMBO | none | 6 options: none, 512, 768, 1024, 1536, 2048 |
| force_square | BOOLEAN | false | โ |
| force_target_downscale | BOOLEAN | false | โ |
Outputs (9)
| Name | Type | Description |
|---|---|---|
| image_cropped | IMAGE | โ |
| mask_cropped | MASK | โ |
| x | INT | โ |
| y | INT | โ |
| orig_width | INT | โ |
| orig_height | INT | โ |
| width | INT | โ |
| height | INT | โ |
| target_size | INT | โ |