Skip to content

Commit 792013f

Browse files
committed
fix: absorb near-miss vision_rect noise inside text blocks
Apple Vision's rect and text detectors have ~3-5px boundary misalignment, causing small noise rects inside text blocks to survive absorption at 73-85% containment (below the 85% threshold). Add block_padding parameter (default 5px) that expands text block bboxes during containment checks, compensating for the detector boundary divergence. Validated on 7 Draw Things screenshots: 354 → 345 detections (9 noise rects absorbed), zero false absorptions.
1 parent 1fcb651 commit 792013f

3 files changed

Lines changed: 69 additions & 8 deletions

File tree

tests/test_group.py

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -240,3 +240,49 @@ def test_rect_outside_text_block_preserved():
240240
assert groups_formed == 1
241241
assert len(result) == 2
242242
assert any(d.source == "vision_rect" for d in result)
243+
244+
245+
def test_near_miss_rect_absorbed_with_padding():
246+
"""vision_rect extending a few pixels beyond text block is absorbed.
247+
248+
Real-world case: Apple Vision's rect detector extends ~5px beyond its
249+
text detector boundary. Without padding the rect survives at ~80%
250+
containment (below the 85% threshold). With 5px block padding it's
251+
absorbed.
252+
"""
253+
from uitag.group import group_text_blocks
254+
255+
# Based on basic-settings-2: Sampler section, rect SoM 20 / block SoM 21
256+
# Block lines span y:692-803, but the rect starts at y:687 (5px above)
257+
dets = [
258+
_det("Different samplers can converge at", x=36, y=692, w=471, h=18),
259+
_det("different step counts and may result", x=36, y=716, w=471, h=18),
260+
_det("in different qualities.", x=36, y=740, w=200, h=18),
261+
# Rect starts 5px above the first text line
262+
_det("rectangle", x=36, y=687, w=87, h=24, source="vision_rect"),
263+
]
264+
result, groups_formed = group_text_blocks(dets)
265+
266+
assert groups_formed == 1
267+
# Rect should be absorbed thanks to block_padding
268+
text_blocks = [d for d in result if d.source == "vision_text_block"]
269+
rects = [d for d in result if d.source == "vision_rect"]
270+
assert len(text_blocks) == 1
271+
assert len(rects) == 0
272+
273+
274+
def test_near_miss_rect_without_padding_survives():
275+
"""Verify near-miss rect survives when padding is explicitly zero."""
276+
from uitag.group import group_text_blocks
277+
278+
dets = [
279+
_det("Different samplers can converge at", x=36, y=692, w=471, h=18),
280+
_det("different step counts and may result", x=36, y=716, w=471, h=18),
281+
_det("in different qualities.", x=36, y=740, w=200, h=18),
282+
_det("rectangle", x=36, y=687, w=87, h=24, source="vision_rect"),
283+
]
284+
result, groups_formed = group_text_blocks(dets, block_padding=0)
285+
286+
assert groups_formed == 1
287+
rects = [d for d in result if d.source == "vision_rect"]
288+
assert len(rects) == 1 # Rect survives without padding

uitag/florence.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,6 @@
3131
_LEAKED_LOC_PREFIX = re.compile(r"^(loc_\d+>\s*)+")
3232

3333

34-
3534
def _load_model():
3635
"""Lazy-load model as singleton (load once, reuse across calls)."""
3736
global _model, _processor

uitag/group.py

Lines changed: 23 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ def group_text_blocks(
1010
max_y_gap_factor: float = 1.0,
1111
x_align_tolerance: int = 20,
1212
containment_threshold: float = 0.85,
13+
block_padding: int = 5,
1314
) -> tuple[list[Detection], int]:
1415
"""Group adjacent vision_text lines into text blocks.
1516
@@ -19,7 +20,9 @@ def group_text_blocks(
1920
2021
Groups of 2+ lines become a single detection with source
2122
``"vision_text_block"`` and space-joined labels. Vision rectangles
22-
mostly contained within text blocks are absorbed.
23+
mostly contained within text blocks are absorbed (block bbox is
24+
expanded by ``block_padding`` pixels to compensate for boundary
25+
misalignment between Vision's rect and text detectors).
2326
2427
Returns:
2528
(updated_detections, groups_formed)
@@ -86,7 +89,9 @@ def group_text_blocks(
8689

8790
for det in other_dets:
8891
if det.source == "vision_rect" and text_blocks:
89-
if _is_contained_in_any(det, text_blocks, containment_threshold):
92+
if _is_contained_in_any(
93+
det, text_blocks, containment_threshold, block_padding
94+
):
9095
continue
9196
filtered_other.append(det)
9297

@@ -103,13 +108,24 @@ def _is_contained_in_any(
103108
rect: Detection,
104109
blocks: list[Detection],
105110
threshold: float,
111+
padding: int = 0,
106112
) -> bool:
107-
"""Check if rect is mostly contained within any text block."""
113+
"""Check if rect is mostly contained within any text block.
114+
115+
The block bbox is expanded by ``padding`` pixels on all sides to
116+
compensate for Apple Vision's boundary misalignment between its
117+
rectangle and text detectors.
118+
"""
108119
for block in blocks:
109-
ix1 = max(rect.x, block.x)
110-
iy1 = max(rect.y, block.y)
111-
ix2 = min(rect.x + rect.width, block.x + block.width)
112-
iy2 = min(rect.y + rect.height, block.y + block.height)
120+
bx1 = block.x - padding
121+
by1 = block.y - padding
122+
bx2 = block.x + block.width + padding
123+
by2 = block.y + block.height + padding
124+
125+
ix1 = max(rect.x, bx1)
126+
iy1 = max(rect.y, by1)
127+
ix2 = min(rect.x + rect.width, bx2)
128+
iy2 = min(rect.y + rect.height, by2)
113129

114130
if ix2 <= ix1 or iy2 <= iy1:
115131
continue

0 commit comments

Comments
 (0)