Sign Split Lines
One word smeared across a shopfront, and how to stop it
- sign_data
- image
- sign_data
- region_count
- report
Promptable segmentation is wonderful right up to the moment it hands you a shopfront. SAM3's text grounding finds the lettered area in one go - and returns the whole legend as a single region: "CAFÉ · BOULANGERIE · PÂTISSERIE" as one blob. The pipeline's next step gives each region one word, so that word gets stretched over the entire block and every line of the sign ends up reading the same nonsense. Sign Split Lines goes between the selector and the text proposer and turns one region into one region per line.
How it works
It looks at the picture, not the mask. That's not a stylistic choice - SAM3's mask is a solid blob over the whole lettered area with no internal gaps to find, so the only place line breaks exist is the actual pixels. Inside each region it finds horizontal bands of lettering separated by gaps larger than min_gap × the region's height, merges or discards bands that are too thin to hold readable text, and hands the proposer a region per line so each one gets asked for its own word.
Inputs
sign_data- straight from Sign Selector SAM3, before the proposer. Order matters: if the proposer has already assigned one word to the whole block, splitting afterwards buys you nothing.image- the picture the selector scanned. The lines are found here.min_gap(0.055, i.e. 5.5% of the region's height) - the one dial you'll actually turn. Too small and an ascender on a lowercase letter splits one line in two; too large and two lines stay welded together.min_line_px(8) - bands thinner than this get merged into their neighbour rather than becoming regions. It's a floor for "is this even a line", not for "could a human read it".max_lines(4) - if a region looks like it holds more lines than this, it's left whole. Past a few lines you're looking at a paragraph, and a paragraph isn't signage.pad_px(2) - rows added above and below each band. Err on the generous side: whatever the mask misses is restored unchanged and stands there later as leftover scribble at the edge of the new lettering.enabled- off passes everything through untouched, so you can measure the difference splitting makes on its own instead of guessing.
Outputs: sign_data (into the proposer), region_count (watch this - it should go up), and report.
Install
cd ComfyUI/custom_nodes
git clone https://github.com/ping1979ping/comfyui-FVMtools
Restart, or install via ComfyUI Manager by searching comfyui-fvmtools. The sign nodes are cheap: this one is numpy and the pack's own detection code, no model downloads. The selector upstream of it is the expensive part - SAM3 needs its weights and, depending on which SAM3 nodes you have installed, Triton. The Sign Selector in this pack is built around one vision encode per image shared across all nine text classes, which is why selecting nine classes costs barely more than one.
Where people get burned
Right node, wrong place in the chain. Split Lines is before the proposer, full stop. The selector's SIGN_DATA goes in, the proposer's SIGN_DATA comes out the other end.
A guardrail mismatch worth knowing. The selector's own minimum-height check runs before the split, on the region as a whole. A 90 px sign that passes that check can arrive at the caption node as four 20 px strips, because the splitter only tests bands against its own 8 px "is this a line at all" bar. That's fine - but it's why the Ideogram bridge downstream has its own min_word_px, and why you should leave it on.
Ascenders and descenders. If region_count looks absurd, min_gap is too small. This is the one setting where you tune by looking at the report and the crop previews rather than by reasoning about it - letterforms vary more than intuition suggests.
Inputs (7)
| Name | Type | Default | Description |
|---|---|---|---|
| sign_data | SIGN_DATA | Straight from Sign Selector SAM3, BEFORE the proposer — each new line needs its own word. | |
| image | IMAGE | The picture the selector scanned. Lines are found in the PICTURE, not in the mask: SAM3's mask is a solid blob over the whole lettered area and has no gaps to find. | |
| enabled | BOOLEAN | true | Off: pass everything through untouched, so the effect of splitting can be measured on its own. |
| min_gap | FLOAT | 0.0550.01–0.4 | How large a horizontal gap must be, as a share of the region's height, before it counts as a line break. Too small and an ascender splits a line in two; too large and two lines stay welded. |
| min_line_px | INT | 83–200 | A band thinner than this cannot hold readable text and is merged into its neighbour instead of becoming a region of its own. |
| max_lines | INT | 41–12 | Upper bound per region. A region that seems to hold more lines than this is left whole — beyond a few lines it is a paragraph, and a paragraph is not signage. |
| pad_px | INT | 20–32 | Rows added above and below each band. The mask must cover every stroke: what it misses is restored unchanged and stands there as leftover scribble. |
Outputs (3)
| Name | Type | Description |
|---|---|---|
| sign_data | SIGN_DATA | — |
| region_count | INT | — |
| report | STRING | — |