Nodes/PortraitUtils/Detect & Split Photo Panels
ComfyUI Node

Detect & Split Photo Panels

Splitting a Composite Into Real Panels

By heyburns·Created about a year ago·Updated 23 days ago· 3
Detect & Split Photo Panels
  • image
  • panels
  • analysis
◄detection_modeGutters only►
◄sensitivityBalanced►
◄min_panel_area_percent3.0►
◄max_panels16►
◄vertical_splits►
◄horizontal_splits►
◄column_horizontal_splits►

You scanned a page with four prints lying on the glass, and now you have one JPEG with four photographs on it. The obvious move is to crop it four times by hand, which is fine until you have forty pages. Detect & Split Photo Panels finds the dividers, cuts on them, and hands you four separate images - at their original pixels, with nothing resized.

It's a small, honest node that does one job. Worth knowing about because in the PortraitUtils workflow it's the front door: everything downstream (border cleanup, subject masks, editor sizing) assumes it's looking at one photograph, and this is how you get from "a page of photos" to "one photograph."

What it actually detects

This is a divider detector, not scene understanding. In the default mode it looks for nearly uniform strips (black, white, gray, or colored gutters) with corroborated photo boundaries on both sides, then recurses inside each panel it finds - which is how it handles grids, mixed horizontal/vertical layouts, and the guillotine-style cuts you get when someone collages prints of different sizes. Final crops are literal slices of the input tensor. No resampling, no color conversion, no quantization. Each panel keeps its own width, height, dtype, alpha, and device.

Only tiny analysis samples get pulled to the CPU; the pixels stay where they are.

The output is the important design decision: panels is an IMAGE list, not a same-size tensor batch. Four photos of four different sizes come out as four differently sized images, not padded into a 1024×1024 brick. That's the whole reason this works.

The inputs you'll actually touch

detection_mode is the one to think about. Gutters only is the default and by far the safest. Gutters + strong seams also accepts abrupt straight boundaries between panels that touch, and Collage (partial seams) goes further into thin, interrupted dividers - both are opt-in for a good reason, since a hard scene edge in a photograph looks a lot like a collage seam. If you turn them on, check the thumbnails.

sensitivity (Conservative, Balanced, Sensitive) moves how much variation a divider is allowed. min_panel_area_percent defaults to 3 - drop it if you're splitting a page of small thumbnails, because anything smaller than that percentage is ignored as noise. max_panels default 16, max 64; when it hits the cap it keeps the remaining region unsplit rather than throwing it away.

The three optional string fields are a manual escape hatch: vertical_splits, horizontal_splits, and column_horizontal_splits. They take 38.9%, whole pixel positions, or 357:364 to explicitly omit a gutter, and column_horizontal_splits lets you give each column its own y-cuts with |, e.g. 258,520 | 205,581 | 258,520 for staggered rows. Fill in any of them and automatic detection is off entirely - that's the trade.

Wiring it up

The intended partner is Image Chooser Classic (same author, separate install - this node doesn't depend on it). It accepts a list of differently sized images, so the ragged output goes straight in:

Load Image → Detect & Split Photo Panels.image
                 panels → Image Chooser Classic.images   (count = 1)
                              → AutoCrop → masks → Smart Photo Prepare

Set the chooser to "Only pause if batch" and ordinary single-photo runs sail through untouched; it only stops to ask when there's actually more than one panel. analysis is the other output, a JSON string with the panel count, source coordinates and dimensions, the divider evidence, and any warnings. Its number is one-based (for thumbnail labels) and index is zero-based (for the chooser's selection string) - a small thing that will save you twenty minutes of off-by-one confusion.

Install

ComfyUI Manager → search PortraitUtils → install → restart the ComfyUI server (a browser refresh does not reload Python nodes). Manually:

cd ComfyUI/custom_nodes
git clone https://github.com/heyburns/PortraitUtils
/path/to/ComfyUI/venv/bin/python -m pip install -r PortraitUtils/requirements.txt

Windows portable installs should use python_embeded instead of a system Python. The dependency list is short - numpy, Pillow, opencv-python-headless, tifffile - and OpenCV is the one that bites in practice: wheel variants share the same cv2 namespace, so install exactly one variant in that environment. No models, no JavaScript, no web routes.

Where it goes wrong

A photograph with a large blank margin or a busy page of text may simply not split. Non-rectangular, rotated, or overlapping prints need the manual fields - this node doesn't deskew, and for that you want Process Scanned Photo instead. Uniform empty grid cells can be dropped after automatic detection, which is usually what you want and occasionally isn't.

Two traps worth naming. First, outer print borders are not removed, so don't expect clean edges out of it - follow with Intelligent AutoCrop. Second, wiring panels directly into an ordinary processing node will generally run that node once per panel rather than failing loudly, which looks like a mystery batch job. Put the chooser (or another list-aware selector) in between, and generate masks after selection so they're built on the chosen photo's grid. Divider scores are heuristic evidence, not probabilities; the numbers in analysis tell you what it saw, not how confident it is.

CategoryPortraitUtils/Photo Edit V2

Inputs (8)

NameTypeDefaultDescription
imageIMAGE—
detection_modeCOMBOGutters onlyGutters only is safest. Collage mode also accepts interrupted straight seams; verify the thumbnails.
sensitivityCOMBOBalanced3 options: Conservative, Balanced, Sensitive
min_panel_area_percentFLOAT3.00.1–40Minimum automatic panel area as a percentage of the original input. Manual splits bypass this limit.
max_panelsINT162–64Automatic detection stops at this count without discarding unsplit regions.
vertical_splitsoptSTRINGOptional override: x positions such as '38.9%' or '357:364' to exclude a gutter. Separate multiple positions with commas. Any override replaces automatic detection.
horizontal_splitsoptSTRINGOptional override: y positions such as '50%' or '760:768'. Positions refer to original source pixels, not editor resolution.
column_horizontal_splitsoptSTRINGWith vertical_splits, give different y cuts per column separated by |, e.g. '258,520 | 205,581 | 258,520'. Do not also set horizontal_splits.

Outputs (2)

NameTypeDescription
panelsIMAGE—
analysisSTRING—