Files
rapidocr/python/tests/test_word_box_cls_rotation.py
Mohammad Hijjawi cee004f994 fix(word_box): place word boxes correctly on 180-degree rotated lines (issue #328) (#747)
When the text classifier turns a crop by 180 degrees, recognition runs on
the rotated crop, but CalRecBoxes mapped the word boxes back as if the crop
had never been rotated. Every word/char box on an upside-down line therefore
landed on its mirror position: the first character got the box of the last.

Pass the classifier's rotation decision (same "180" label and cls_thresh
rule TextClassifier uses) to CalRecBoxes, and rotate the word boxes back
by 180 degrees within the crop before mapping them to the original image.
2026-10-25 08:17:53 +08:00

111 lines
3.6 KiB
Python

# -*- encoding: utf-8 -*-
# @Author: Mohammad Hijjawi
from typing import List, Optional, Tuple
import numpy as np
import pytest
from rapidocr import RapidOCR
from rapidocr.ch_ppocr_cls import TextClsOutput
from rapidocr.ch_ppocr_det import TextDetOutput
from rapidocr.ch_ppocr_rec import TextRecOutput
from rapidocr.ch_ppocr_rec.typings import WordInfo, WordType
LINE_TXT = "我是人"
# A 200x20 horizontal line and a 20x200 vertical line in a 400x400 image.
HORIZONTAL_BOX = [[100, 50], [300, 50], [300, 70], [100, 70]]
VERTICAL_BOX = [[100, 100], [120, 100], [120, 300], [100, 300]]
def make_word_info() -> WordInfo:
return WordInfo(
words=[list(LINE_TXT)],
word_cols=[[2, 15, 28]],
word_types=[WordType.CN],
line_txt_len=30,
confs=[0.9, 0.9, 0.9],
)
def run_word_boxes(
boxes: List[List[List[int]]],
txts: Tuple[str, ...],
cls_res: Optional[List[Tuple[str, float]]],
):
engine = RapidOCR(params={"Global.return_word_box": True})
# The crop handed to recognition is always laid out horizontally.
crops = [np.zeros((20, 200, 3), dtype=np.uint8) for _ in boxes]
det_res = TextDetOutput(
boxes=np.array(boxes, dtype=np.float32), scores=[0.9] * len(boxes)
)
cls_out = TextClsOutput(img_list=crops, cls_res=cls_res)
rec_res = TextRecOutput(
imgs=crops,
txts=txts,
scores=tuple(0.9 for _ in txts),
word_results=tuple(make_word_info() for _ in txts),
)
return engine.build_final_output(
np.zeros((400, 400, 3), dtype=np.uint8),
det_res,
cls_out,
rec_res,
crops,
{"preprocess": {"ratio_h": 1.0, "ratio_w": 1.0}},
)
def word_centers(word_line) -> List[Tuple[float, float]]:
return [tuple(np.array(points).mean(axis=0)) for _, _, points in word_line]
@pytest.mark.parametrize(
"box,cls_res,axis,reading_direction",
[
(HORIZONTAL_BOX, None, 0, 1),
(HORIZONTAL_BOX, [("0", 0.99)], 0, 1),
# below cls_thresh the crop is not rotated, so neither are the boxes
(HORIZONTAL_BOX, [("180", 0.5)], 0, 1),
# issue #328: upside-down text is read right to left in the image
(HORIZONTAL_BOX, [("180", 0.99)], 0, -1),
(VERTICAL_BOX, [("0", 0.99)], 1, 1),
(VERTICAL_BOX, [("180", 0.99)], 1, -1),
],
)
def test_word_boxes_follow_cls_rotation(box, cls_res, axis, reading_direction):
result = run_word_boxes([box], (LINE_TXT,), cls_res)
word_line = result.word_results[0]
assert "".join(txt for txt, _, _ in word_line) == LINE_TXT
centers = [center[axis] for center in word_centers(word_line)]
assert np.all(reading_direction * np.diff(centers) > 0)
def test_rotated_word_boxes_are_mirrored_within_line():
upright = run_word_boxes([HORIZONTAL_BOX], (LINE_TXT,), [("0", 0.99)])
flipped = run_word_boxes([HORIZONTAL_BOX], (LINE_TXT,), [("180", 0.99)])
upright_xs = [
sorted({p[0] for p in points}) for _, _, points in upright.word_results[0]
]
flipped_xs = [
sorted({p[0] for p in points}) for _, _, points in flipped.word_results[0]
]
# Each character's box is mirrored about the centre of the line.
for (x0, x1), (fx0, fx1) in zip(upright_xs, flipped_xs):
assert (fx0, fx1) == (400 - x1, 400 - x0)
def test_rotation_flags_stay_aligned_after_empty_lines_are_dropped():
result = run_word_boxes(
[HORIZONTAL_BOX, VERTICAL_BOX],
("", LINE_TXT),
[("0", 0.99), ("180", 0.99)],
)
assert result.txts == (LINE_TXT,)
ys = [center[1] for center in word_centers(result.word_results[0])]
assert np.all(np.diff(ys) < 0)