Reduce CER up to 20% under page skew: fix sorted_boxes' 10px threshold bug (7 langs) - #18189
Reduce CER up to 20% under page skew: fix sorted_boxes' 10px threshold bug (7 langs)#18189Vedavarshith868 wants to merge 2 commits into
Conversation
|
@Bobholamovic @TingquanGao This is a one-function fix to |
There was a problem hiding this comment.
Pull request overview
This PR improves OCR reading-order robustness on mildly skewed pages by updating sorted_boxes to sort and line-cluster boxes in a deskewed coordinate frame, avoiding the previous fixed 10px row-threshold failure mode that interleaved lines and inflated CER.
Changes:
- Update
tools/infer/predict_system.py::sorted_boxesto estimate dominant skew (median top-edge angle), deskew sort keys, and cluster lines using a height-proportional tolerance. - Add a focused pytest unit test (
tests/tools/test_sorted_boxes.py) covering upright pages and ±3–10° skew scenarios.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.
| File | Description |
|---|---|
| tools/infer/predict_system.py | Replaces fixed-threshold y-sorting with skew-aware, deskew-frame ordering and adaptive row tolerance. |
| tests/tools/test_sorted_boxes.py | Adds isolated unit tests for reading-order correctness under upright and skewed synthetic layouts. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| # Dominant skew: median angle of the box top edges (p1 - p0), | ||
| # clamped to +/-15 degrees so outliers cannot flip the page. | ||
| theta = float(np.median(np.arctan2(p1[:, 1] - p0[:, 1], p1[:, 0] - p0[:, 0]))) | ||
| theta = max(-0.2618, min(0.2618, theta)) |
There was a problem hiding this comment.
Done in 3f6643c — replaced the hard-coded 0.2618 rad with np.clip(theta, -np.deg2rad(15.0), np.deg2rad(15.0)). Thanks!
| spec = importlib.util.spec_from_file_location( | ||
| "predict_system_under_test", | ||
| REPO_ROOT / "tools" / "infer" / "predict_system.py", | ||
| ) | ||
| module = importlib.util.module_from_spec(spec) | ||
| assert spec.loader is not None | ||
| spec.loader.exec_module(module) | ||
| return module.sorted_boxes |
There was a problem hiding this comment.
Done in 3f6643c — the fixture now restores sys.path and the FLAGS_allocator_strategy env var via monkeypatch, so importing predict_system leaves no global side effects. Thanks!
|
@cuicheng01 friendly ping on this one 🙏 — it's a single, backward-compatible fix to Could a maintainer approve the workflow runs so CI can go green? And I'd be glad to port the same fix to the 3.x pipeline's ordering if that's the preferred location. Thanks for taking a look! |
|
Thanks for your contribution! |
The fixed 10 px row threshold in sorted_boxes (tools/infer/predict_system.py) assumes an upright page. Under mild skew a single text line spans far more than 10 px in y (a 1000 px-wide line at 3 degrees already spans ~52 px), so boxes from one visual line are split across several "rows" and the reading order interleaves between lines. Estimate the dominant text skew from the detected boxes, compute the sort keys in the deskewed frame, and use a row tolerance proportional to text height. On an upright page this reduces to the original top-to-bottom / left-to-right ordering, so it is backward compatible and only changes behaviour on skewed input. Pure NumPy, no new dependencies. Add tests/tools/test_sorted_boxes.py covering the upright order and the skew-robust order.
- Clamp the estimated skew with np.clip(theta, -np.deg2rad(15), np.deg2rad(15)) instead of the hard-coded 0.2618 rad magic number. - Restore sys.path and the FLAGS_allocator_strategy env var in the test fixture so importing predict_system leaves no global side effects.
3f6643c to
a3d1b19
Compare
|
Rebased onto current |

Insight
On skewed scans (real-world skew [3deg-10deg]),
sorted_boxesreads text in the wrong order — words fromdifferent lines get mixed together, which inflated CER by up to 20%. (tested across 7 languages on XFUND dataset)
This PR fixes the row-clustering logic so reading order stays correct
even when the page isn't perfectly upright.
Detailed
sorted_boxes(tools/infer/predict_system.py) clusters detected boxes intotext lines using a fixed
< 10px row threshold. That assumes an upright page.Under mild, real-world skew a single text line spans far more than 10 px in y —
a 1000 px-wide line at 3° already spans ~52 px, at 10° ~176 px — so boxes from
one visual line are split across several "rows" and the reading order
interleaves between lines.
Fix
Estimate the dominant text skew from the detected boxes (median angle of the box
top edges), compute the sort keys in the deskewed frame, and cluster lines with
a row tolerance proportional to text height instead of a fixed 10 px. On an
upright page (θ ≈ 0) this reduces to the original top-to-bottom / left-to-right
ordering, so it is backward compatible for the common case and only changes
behaviour on skewed input. Pure NumPy, no new dependencies, single function.
Evidence (detection + recognition held identical)
To isolate the ordering, I ran PP-OCRv6 detection once and recognized every
detected box once, then assembled the page text two ways — the current
sorted_boxeslogic vs. the proposed ordering — and scored CER against groundtruth. Detection and recognition are byte-for-byte identical between the two, so
any CER gap is purely reading order. Dataset: the full XFUND validation split,
7 languages × ~50 pages, each rotated by 0 / 3 / 6 / 10° (random sign).
At 0° the two orderings are within 1–3 CER (backward compatible). As skew grows,
the current
sorted_boxesdegrades by +8 to +18 CER in every language, while theproposed ordering stays flat within ±1 CER — because recognition is unchanged and
only the ordering is fixed.
Test
Added
tests/tools/test_sorted_boxes.py(loads the function in isolation, numpyonly). It asserts unchanged top-to-bottom / left-to-right order on an upright
synthetic page, and correct reading order on the same page skewed by ±3–10°. The
previous implementation interleaves lines on the skewed case.
Scope / limitations
In-plane skew of left-to-right scripts. Does not attempt multi-column
segmentation or RTL; those are out of scope and unchanged.