Suppress rendering of text lines with improbable aspect ratios
When Tesseract completely fails to detect text rotation (no textangle, slope=0), it produces garbage text in tall-narrow bounding boxes. Add an aspect ratio plausibility check that compares the OCR bounding box shape to the expected shape of the rendered text (accounting for baseline slope). Lines where the ratio of aspect ratios is < 0.1 are suppressed. Uses a fast path (width >= height) to skip the expensive font measurement for the common case of normal horizontal text.
This commit is contained in:
@@ -12,7 +12,7 @@ from __future__ import annotations
|
||||
import logging
|
||||
from dataclasses import dataclass
|
||||
from itertools import pairwise
|
||||
from math import atan, degrees
|
||||
from math import atan, cos, degrees, radians, sin
|
||||
from pathlib import Path
|
||||
|
||||
from fpdf import FPDF
|
||||
@@ -163,6 +163,8 @@ class Fpdf2PdfRenderer:
|
||||
|
||||
# Registered fonts: font_path -> fpdf_family_name
|
||||
self._registered_fonts: dict[str, str] = {}
|
||||
# Track whether we've already logged the info-level suppression message
|
||||
self._logged_aspect_ratio_suppression = False
|
||||
|
||||
def render(self, output_path: Path) -> None:
|
||||
"""Render page to PDF file.
|
||||
@@ -401,6 +403,16 @@ class Fpdf2PdfRenderer:
|
||||
w for w in line.children if w.ocr_class == OcrClass.WORD and w.text
|
||||
]
|
||||
|
||||
# Suppress lines where the text aspect ratio is implausible.
|
||||
# This catches cases where Tesseract failed to detect rotation
|
||||
# entirely (slope=0, no textangle) and produced garbage text in a
|
||||
# bounding box whose shape doesn't match the text content at all.
|
||||
if not self._check_aspect_ratio_plausible(
|
||||
pdf, words, font_size, slope_angle_deg,
|
||||
line_size_width, line_size_height, line_language,
|
||||
):
|
||||
return
|
||||
|
||||
# Render each word followed by space (except last)
|
||||
# Use pairwise to iterate over consecutive word pairs, pairing the last
|
||||
# word with a None to signal the end of the line.
|
||||
@@ -429,6 +441,83 @@ class Fpdf2PdfRenderer:
|
||||
line.direction,
|
||||
)
|
||||
|
||||
def _check_aspect_ratio_plausible(
|
||||
self,
|
||||
pdf: FPDF,
|
||||
words: list[OcrElement | None],
|
||||
font_size: float,
|
||||
slope_angle_deg: float,
|
||||
line_size_width: float,
|
||||
line_size_height: float,
|
||||
line_language: str | None,
|
||||
) -> bool:
|
||||
"""Check whether the line's aspect ratio is plausible for its text.
|
||||
|
||||
Compares the aspect ratio of the OCR bounding box to the aspect ratio
|
||||
the text would have if rendered normally (accounting for baseline
|
||||
slope). A large mismatch indicates Tesseract misread rotated text
|
||||
without detecting the rotation.
|
||||
|
||||
Returns:
|
||||
True if plausible (rendering should proceed), False to suppress.
|
||||
"""
|
||||
if line_size_width <= 0 or line_size_height <= 0 or font_size <= 0:
|
||||
return True
|
||||
|
||||
# Fast path: most lines are wider than they are tall, which is
|
||||
# the normal shape for horizontal text. Only tall-narrow boxes
|
||||
# (height > width) need the expensive font measurement check.
|
||||
if line_size_width >= line_size_height:
|
||||
return True
|
||||
|
||||
line_text = ' '.join(
|
||||
w.text for w in words if w is not None and w.text
|
||||
)
|
||||
if not line_text:
|
||||
return True
|
||||
|
||||
# Measure the natural rendered width of the line text
|
||||
font_manager = self.multi_font_manager.select_font_for_word(
|
||||
line_text, line_language
|
||||
)
|
||||
font_family = self._register_font(pdf, font_manager)
|
||||
pdf.set_font(font_family, size=round(font_size))
|
||||
natural_width = pdf.get_string_width(line_text)
|
||||
|
||||
if natural_width <= 0:
|
||||
return True
|
||||
|
||||
# Compute the AABB the text would occupy considering baseline slope
|
||||
theta = radians(abs(slope_angle_deg))
|
||||
expected_w = natural_width * cos(theta) + font_size * sin(theta)
|
||||
expected_h = natural_width * sin(theta) + font_size * cos(theta)
|
||||
|
||||
if expected_h <= 0:
|
||||
return True
|
||||
|
||||
actual_aspect = line_size_width / line_size_height
|
||||
expected_aspect = expected_w / expected_h
|
||||
ratio = actual_aspect / expected_aspect
|
||||
|
||||
if ratio >= 0.1:
|
||||
return True
|
||||
|
||||
# Implausible aspect ratio — suppress this line
|
||||
log.debug(
|
||||
"Suppressing text with improbable aspect ratio: "
|
||||
"actual=%.3f expected=%.3f ratio=%.4f text=%r",
|
||||
actual_aspect,
|
||||
expected_aspect,
|
||||
ratio,
|
||||
line_text[:80],
|
||||
)
|
||||
if not self._logged_aspect_ratio_suppression:
|
||||
log.info(
|
||||
"Suppressing OCR output text with improbable aspect ratio"
|
||||
)
|
||||
self._logged_aspect_ratio_suppression = True
|
||||
return False
|
||||
|
||||
def _render_word(
|
||||
self,
|
||||
pdf: FPDF,
|
||||
|
||||
Reference in New Issue
Block a user