fix(word_box): score each word from its own characters (issue #749) (#750)

cal_ocr_word_box returned the line's per-character probabilities, spaces
included, as the word scores, so word n got the probability of character n
of the line. Keep each word's character probabilities in
WordInfo.char_confs and score a grouped word with their mean, like the line
score; a single character keeps its own probability.

Co-authored-by: Paul Faucher <186776257+pfauch741@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
authored and GitHub committed 2026-10-25 21:41:21 +08:00
1 parent cee004f994
commit b40c0f84b8
4 files changed
+61 -4

No files matched your search

+13 -3
View File
@@ -107,14 +107,24 @@ class CalRecBoxes:
is_all_en_num = all(v is WordType.EN_NUM for v in word_info.word_types)
line_cols, char_widths, word_contents = [], [], []
for word, word_col in zip(word_info.words, word_info.word_cols):
# A WordInfo built without char_confs gives every word the line's score.
char_confs_list = word_info.char_confs or [
[np.mean(word_info.confs).round(5).tolist()] * len(word)
for word in word_info.words
]
line_cols, char_widths, word_contents, word_confs = [], [], [], []
for word, word_col, char_confs in zip(
word_info.words, word_info.word_cols, char_confs_list
):
if is_all_en_num and not return_single_char_box:
line_cols.append(word_col)
word_contents.append("".join(word))
word_confs.append(np.mean(char_confs).round(5).tolist())
else:
line_cols.extend(word_col)
word_contents.extend(word)
word_confs.extend(char_confs)
if len(word_col) == 1:
continue
@@ -134,7 +144,7 @@ class CalRecBoxes:
word_boxes = self.calc_box(
line_cols, avg_char_width, avg_col_width, bbox_points
)
return word_contents, word_boxes, word_info.confs
return word_contents, word_boxes, word_confs
def calc_en_num_box(
self,
+1
View File
@@ -75,3 +75,4 @@ class WordInfo:
word_types: List[WordType] = field(default_factory=list)
line_txt_len: float = 0.0
confs: List[float] = field(default_factory=list)
char_confs: List[List[float]] = field(default_factory=list)
+8
View File
@@ -123,6 +123,14 @@ class CTCLabelDecode:
len(token_indices) * wh_ratio_list[batch_idx] / max_wh_ratio
)
rec_word_info.confs = conf_list
# words holds every character of text except spaces, in order.
char_confs = iter(
conf for char, conf in zip(text, conf_list) if not char.isspace()
)
rec_word_info.char_confs = [
[next(char_confs) for _ in word] for word in rec_word_info.words
]
result_words_list.append(rec_word_info)
return result_list, result_words_list
+39 -1
View File
@@ -3,8 +3,9 @@
# @Contact: liekkaskono@163.com
import sys
from pathlib import Path
from typing import List
from typing import List, Tuple
import numpy as np
import pytest
from pytest import mark
@@ -12,6 +13,9 @@ root_dir = Path(__file__).resolve().parent.parent
sys.path.append(str(root_dir))
from rapidocr import EngineType, LangRec, ModelType, OCRVersion, RapidOCR
from rapidocr.cal_rec_boxes import CalRecBoxes
from rapidocr.ch_ppocr_rec import TextRecOutput
from rapidocr.ch_ppocr_rec.utils import CTCLabelDecode
test_dir = root_dir / "tests" / "test_files"
@@ -78,3 +82,37 @@ def test_en_return_single_char_box(engine):
result = engine(img_path, return_word_box=True, return_single_char_box=True)
txts, _, _ = list(zip(*result.word_results[0]))
assert txts[0] == "3"
@mark.parametrize(
"chars,return_single_char_box,expected",
[
("ab cd", False, [("ab", 0.75), ("cd", 0.65)]),
("ab cd", True, [("a", 0.9), ("b", 0.6), ("c", 0.8), ("d", 0.5)]),
("中 文", False, [("中", 0.9), ("文", 0.6)]),
],
)
def test_word_conf_from_own_chars(
chars: str, return_single_char_box: bool, expected: List[Tuple[str, float]]
):
decoder = CTCLabelDecode(character=["a", "b", "c", "d", "中", "文"])
# One character every other step, blanks between. Spaces score 0.99.
probs = iter([0.9, 0.6, 0.8, 0.5])
steps = [(decoder.dict[c], 0.99 if c == " " else next(probs)) for c in chars]
preds = np.zeros((1, 2 * len(steps), len(decoder.character)), dtype=np.float32)
preds[0, :, 0] = 0.95
for i, (char_idx, prob) in enumerate(steps):
preds[0, 2 * i, 0] = 0.0
preds[0, 2 * i, char_idx] = prob
line_results, word_infos = decoder(preds, return_word_box=True)
assert line_results[0][0] == chars
img = np.zeros((48, 320, 3), dtype=np.uint8)
box = np.array([[0, 0], [320, 0], [320, 48], [0, 48]], dtype=np.float32)
rec_res = TextRecOutput(imgs=[img], txts=(chars,), word_results=tuple(word_infos))
rec_res = CalRecBoxes()([img], [box], rec_res, return_single_char_box)
words = [(txt, conf) for txt, conf, _ in rec_res.word_results[0]]
assert words == expected