Download tinydoc/pipeline/inputs.py from harryfrz/tinydoc-5m: direct link, hf CLI and curl.
- Browser
- Download file 4.77 kB
-
https://huggingface.co/harryfrz/tinydoc-5m/resolve/main/tinydoc/pipeline/inputs.py
- Command line
-
hf download hf://harryfrz/tinydoc-5m/tinydoc/pipeline/inputs.py
-
curl -L -o inputs.py https://huggingface.co/harryfrz/tinydoc-5m/resolve/main/tinydoc/pipeline/inputs.py
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"} | |
| 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] | |