Skip to content

Reduce CER up to 20% under page skew: fix sorted_boxes' 10px threshold bug (7 langs) - #18189

Open
Vedavarshith868 wants to merge 2 commits into
PaddlePaddle:mainfrom
Vedavarshith868:fix/sorted-boxes-skew-robust
Open

Reduce CER up to 20% under page skew: fix sorted_boxes' 10px threshold bug (7 langs)#18189
Vedavarshith868 wants to merge 2 commits into
PaddlePaddle:mainfrom
Vedavarshith868:fix/sorted-boxes-skew-robust

Conversation

@Vedavarshith868

@Vedavarshith868 Vedavarshith868 commented Jun 21, 2026

Copy link
Copy Markdown

Insight

On skewed scans (real-world skew [3deg-10deg]), sorted_boxes reads text in the wrong order — words from
different 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 into
text lines using a fixed < 10 px 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_boxes logic vs. the proposed ordering — and scored CER against ground
truth. 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).

lang CER @0° (current → proposed) CER @10° (current → proposed)
zh 43.6% → 40.5% 61.0% → 41.5%
ja 32.8% → 31.5% 44.7% → 31.8%
de 11.6% → 9.8% 27.9% → 10.5%
es 14.4% → 12.1% 32.2% → 12.6%
fr 32.8% → 30.9% 40.8% → 30.8%
it 23.8% → 22.7% 34.2% → 22.9%
pt 31.5% → 30.4% 40.1% → 30.4%

At 0° the two orderings are within 1–3 CER (backward compatible). As skew grows,
the current sorted_boxes degrades by +8 to +18 CER in every language, while the
proposed 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, numpy
only). 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.

@CLAassistant

CLAassistant commented Jun 21, 2026

Copy link
Copy Markdown

CLA assistant check
All committers have signed the CLA.

@Vedavarshith868 Vedavarshith868 changed the title Fix reading order under mild page skew in sorted_boxes Fix reading order under mild (real-world) page skew in sorted_boxes Jun 21, 2026
@Vedavarshith868

Copy link
Copy Markdown
Author

![Reading-order CER vs page skew]skew_sortfix (1)

PP-OCRv6, full XFUND validation split (7 languages × ~50 pages). Detection
and recognition are identical between the two curves — only the box ordering
differs. The current sorted_boxes (orange) climbs steeply with skew; the
proposed ordering (blue) stays flat. At 0° the two coincide (backward compatible).

Full per-angle results (CER, PP-OCRv6, XFUND val, 7 langs × ~50 pages)
lang  skew   sorted_boxes   skew-robust   Δ
zh     0°       43.6%          40.5%      +3.1
zh     3°       51.4%          41.3%     +10.1
zh     6°       56.6%          41.3%     +15.2
zh    10°       61.0%          41.5%     +19.5
ja     0°       32.8%          31.5%      +1.3
ja     3°       35.5%          32.0%      +3.5
ja     6°       40.4%          31.8%      +8.6
ja    10°       44.7%          31.8%     +12.8
de     0°       11.6%           9.8%      +1.8
de     3°       19.3%          10.7%      +8.6
de     6°       23.6%          10.7%     +12.8
de    10°       27.9%          10.5%     +17.4
es     0°       14.4%          12.1%      +2.3
es     3°       22.5%          12.3%     +10.2
es     6°       27.9%          12.1%     +15.8
es    10°       32.2%          12.6%     +19.6
fr     0°       32.8%          30.9%      +1.9
fr     3°       35.9%          31.2%      +4.7
fr     6°       37.7%          30.6%      +7.0
fr    10°       40.8%          30.8%     +10.0
it     0°       23.8%          22.7%      +1.1
it     3°       27.3%          22.8%      +4.5
it     6°       30.4%          22.8%      +7.6
it    10°       34.2%          22.9%     +11.3
pt     0°       31.5%          30.4%      +1.1
pt     3°       34.2%          30.2%      +3.9
pt     6°       37.8%          30.2%      +7.6
pt    10°       40.1%          30.4%      +9.6

@Vedavarshith868 Vedavarshith868 changed the title Fix reading order under mild (real-world) page skew in sorted_boxes Reduce CER up to 20% under page skew: fix sorted_boxes' 10px threshold bug (7 langs) Jun 21, 2026
@Vedavarshith868

Copy link
Copy Markdown
Author

@Bobholamovic @TingquanGao This is a one-function fix to sorted_boxes — the hardcoded 10 px row threshold breaks reading order under mild page skew (+10–20% CER (character error) tested across 7 XFUND languages). Evidence and figure in the description. Would appreciate a look when you have a moment.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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_boxes to 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.

Comment thread tools/infer/predict_system.py Outdated
Comment on lines +180 to +183
# 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))

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in 3f6643c — replaced the hard-coded 0.2618 rad with np.clip(theta, -np.deg2rad(15.0), np.deg2rad(15.0)). Thanks!

Comment on lines +67 to +74
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

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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!

@Vedavarshith868

Copy link
Copy Markdown
Author

@cuicheng01 friendly ping on this one 🙏 — it's a single, backward-compatible fix to sorted_boxes: the hardcoded 10 px row threshold scrambles reading order under mild page skew (a 1000 px-wide line at 3° already spans ~52 px), which costs +10–20% CER across all 7 XFUND languages (evidence table + figure above; detection and recognition are held identical, so the gap is purely ordering). On upright pages the output is unchanged, and tests/tools/test_sorted_boxes.py covers both the upright and skewed cases. The Copilot review comments are already addressed.

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!

@paddle-bot

paddle-bot Bot commented Jul 14, 2026

Copy link
Copy Markdown

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.
@Vedavarshith868
Vedavarshith868 force-pushed the fix/sorted-boxes-skew-robust branch from 3f6643c to a3d1b19 Compare August 9, 2026 09:32
@Vedavarshith868

Copy link
Copy Markdown
Author

Rebased onto current main — no conflicts, all checks (black, flake8 select, unit tests) still pass. @Bobholamovic saw you're actively merging this week 🙂 — this is a small, backward-compatible, tested fix whenever you get a chance to take a look. Happy to make any changes requested.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants