tinydoc-5m / tinydoc /pipeline /inputs.py
harryfrz's picture
tinydoc-5m v4: OmniDocBench EN text edit 0.117 (v3 0.127), inline maths as text, invoice summary tables
e24bf15 verified
Raw History Blame Contribute Delete
4.77 kB
"""Turn a file into pages: digital-PDF text where usable, otherwise rendered/loaded images.
Also page-level orientation correction and deskew."""
from dataclasses import dataclass, field
from pathlib import Path
import cv2
import numpy as np
from ..charset import normalize
from ..geometry import merge_line_cells
RENDER_DPI = 200
IMAGE_EXTS = {".png", ".jpg", ".jpeg", ".tif", ".tiff", ".bmp", ".webp", ".gif"}
@dataclass
class Page:
number: int
image: np.ndarray | None # RGB uint8, in the page coordinate frame
width: int
height: int
text_source: str # "ocr" | "pdf_text"
pdf_segments: list = field(default_factory=list) # [(x0, y0, x1, y1, text)] when text_source == "pdf_text"
def _usable_pdf_text(segs) -> bool:
text = "".join(s[4] for s in segs)
if len(text.strip()) < 25:
return False
good = sum(c.isalnum() or c in " .,:;-$€£%/()#&@'\"" for c in text)
return good / len(text) > 0.8 and "(cid:" not in text
def load_pages(path, dpi=RENDER_DPI, force_ocr=False):
path = Path(path)
suffix = path.suffix.lower()
if suffix == ".pdf":
import pypdfium2 as pdfium
doc = pdfium.PdfDocument(str(path))
scale = dpi / 72
for i in range(len(doc)):
page = doc[i]
pw, ph = page.get_size()
W, H = round(pw * scale), round(ph * scale)
segs = []
if not force_ocr:
tp = page.get_textpage()
cells = []
for r in range(tp.count_rects()):
l, b, rr, t = tp.get_rect(r)
txt = normalize(tp.get_text_bounded(l, b, rr, t))
if txt:
cells.append((l * scale, (ph - t) * scale, rr * scale, (ph - b) * scale, txt))
segs = [s for s in merge_line_cells(cells) if s[4].strip()]
tp.close()
if segs and _usable_pdf_text(segs):
yield Page(i + 1, None, W, H, "pdf_text", segs)
else:
img = page.render(scale=scale).to_numpy()[:, :, :3].copy()
yield Page(i + 1, img, img.shape[1], img.shape[0], "ocr")
page.close()
doc.close()
elif suffix in IMAGE_EXTS:
img = cv2.imread(str(path), cv2.IMREAD_COLOR)
if img is None:
from PIL import Image
img = np.array(Image.open(path).convert("RGB"))[:, :, ::-1]
img = np.ascontiguousarray(img[:, :, ::-1])
yield Page(1, img, img.shape[1], img.shape[0], "ocr")
else:
raise ValueError(f"unsupported file type: {suffix} (supported: .pdf, {', '.join(sorted(IMAGE_EXTS))})")
def estimate_skew(img_rgb, max_angle=12.0) -> float:
"""Projection-profile skew estimate in degrees (positive = text rotated counter-clockwise)."""
gray = cv2.cvtColor(img_rgb, cv2.COLOR_RGB2GRAY)
s = 1000 / max(gray.shape)
if s < 1:
gray = cv2.resize(gray, (round(gray.shape[1] * s), round(gray.shape[0] * s)), interpolation=cv2.INTER_AREA)
ink = cv2.adaptiveThreshold(gray, 1, cv2.ADAPTIVE_THRESH_MEAN_C, cv2.THRESH_BINARY_INV, 25, 15).astype(np.float32)
# the 1-degree search runs at half size (a quarter of the pixels); the 0.1-degree refinement at full size
small = cv2.resize(ink, (ink.shape[1] // 2, ink.shape[0] // 2), interpolation=cv2.INTER_AREA)
def score(a, im):
h, w = im.shape
m = cv2.getRotationMatrix2D((w / 2, h / 2), -a, 1.0)
prof = cv2.warpAffine(im, m, (w, h)).sum(1)
return float(np.var(prof))
coarse = np.arange(-max_angle, max_angle + 1e-6, 1.0)
best = coarse[int(np.argmax([score(a, small) for a in coarse]))]
fine = np.arange(best - 1, best + 1 + 1e-6, 0.1)
return float(fine[int(np.argmax([score(a, ink) for a in fine]))])
def rotate_image(img, angle):
"""Rotate by `angle` degrees (counter-clockwise), expanding the canvas. Returns (image, 2x3 matrix)."""
h, w = img.shape[:2]
m = cv2.getRotationMatrix2D((w / 2, h / 2), angle, 1.0)
cos, sin = abs(m[0, 0]), abs(m[0, 1])
nw, nh = int(h * sin + w * cos), int(h * cos + w * sin)
m[0, 2] += nw / 2 - w / 2
m[1, 2] += nh / 2 - h / 2
border = tuple(int(v) for v in np.median(img[::8, ::8].reshape(-1, 3), 0)) # page colour from a pixel sample
return cv2.warpAffine(img, m, (nw, nh), flags=cv2.INTER_LINEAR, borderValue=border), m
def rot90_matrix(k, w, h):
"""2x3 matrix mapping original coords to coords after np.rot90(img, k) (counter-clockwise k times)."""
m = np.eye(3)
for _ in range(k % 4):
step = np.array([[0, 1, 0], [-1, 0, w], [0, 0, 1]], np.float64) # (x, y) -> (y, w - x)
m = step @ m
w, h = h, w
return m[:2]