Skip to content

[WIP] Group OCR line detections by mutual gap, not absolute pixel buckets#2570

Draft
PawelPeczek-Roboflow wants to merge 2 commits into
mainfrom
fix/inference-bugs-finding-26
Draft

[WIP] Group OCR line detections by mutual gap, not absolute pixel buckets#2570
PawelPeczek-Roboflow wants to merge 2 commits into
mainfrom
fix/inference-bugs-finding-26

Conversation

@PawelPeczek-Roboflow

Copy link
Copy Markdown
Collaborator

🚧 Work in progress — not ready for review

Draft PR from a batch addressing findings from an in-depth engineering review. Fixes review finding 26 (Medium, Correctness).

The bug

group_detections_by_line in stitch_ocr_detections/v1.py:315 (and the byte-identical v2.py:400) assigns each OCR detection to a line via np.round(y1 / tolerance) * tolerance, snapping the top-y coordinate to the nearest absolute multiple of tolerance. Two boxes on the same physical text line whose y1 values straddle a bucket boundary land on different lines even though they are far closer than tolerance — directly contradicting the documented contract "Detections within this tolerance distance are grouped into the same line."

Root cause

Grouping was done by fixed pixel-grid bucket membership, not by mutual distance. With tolerance=10, y1=14 -> round(1.4)*10 = 10 and y1=16 -> round(1.6)*10 = 20, so two boxes 2px apart are placed on separate lines and the output becomes A\nB instead of AB.

Fix

Rewrote group_detections_by_line in both v1.py and v2.py to group by mutual gap (the same greedy approach v2.adaptive_word_grouping already uses for lines): sort detections by the primary coordinate, then start a new line only when the gap to the previous detection exceeds tolerance. Each line is keyed by its first detection's primary coordinate, so the caller's sorted(boxes_by_line.keys(), reverse=...) top-to-bottom / bottom-to-top ordering is preserved unchanged. Both blocks' tolerance stitching mode are affected (v2's default stitching_algorithm="tolerance" routes to the same function).

Verification

Standalone before/after on the review's exact repro (Box A xyxy=[0,14,10,30] 'A', Box B xyxy=[20,16,30,32] 'B', left_to_right, tolerance=10):

  • straddle-boundary y1=14/16: OLD 'A\nB' -> NEW 'AB' (fixed)
  • genuine two lines y1=14/40: NEW 'A\nB' (still splits correctly)
  • same-bucket y1=11/13: OLD 'AB' == NEW 'AB' (unchanged)
  • within-line ordering (A right of B, left_to_right): NEW 'BA' (correct)

Scope

Focused change; one of a coordinated batch (one PR per finding).


🤖 Generated with Claude Code

PawelPeczek-Roboflow and others added 2 commits July 6, 2026 15:49
…kets

group_detections_by_line snapped top-y to fixed multiples of tolerance, so two boxes on the same line whose y1 straddled a bucket boundary (e.g. 14 and 16 at tolerance=10) landed on different lines, producing a spurious line break. Sort by primary coordinate and start a new line only when the gap to the previous detection exceeds tolerance, honoring the documented 'within this tolerance distance are grouped into the same line' contract. Applied to both v1 and v2 (identical logic).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@PawelPeczek-Roboflow PawelPeczek-Roboflow added the skip-claude-review Opt this PR out of automated Claude review label Jul 6, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

skip-claude-review Opt this PR out of automated Claude review

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant