Download src/det_data.py from lab-ii/sakha-ocr: direct link, hf CLI and curl.
- Browser
- Download file 5.37 kB
-
https://huggingface.co/lab-ii/sakha-ocr/resolve/main/src/det_data.py
- Command line
-
hf download hf://lab-ii/sakha-ocr/src/det_data.py
-
curl -L -o det_data.py https://huggingface.co/lab-ii/sakha-ocr/resolve/main/src/det_data.py
5.37 kB
| """Датасет для детектора строк из born-digital PDF. | |
| Текстовый слой даёт точные рамки строк бесплатно и в нужном домене — это | |
| разметка, которую иначе пришлось бы рисовать руками. Язык не важен: детектор | |
| ищет строку, а не буквы, поэтому берём все строки подряд, включая русские. | |
| Выпуски, попавшие в оценочную выборку распознавателя, исключаются по имени: | |
| детектор не должен видеть страницы, на которых потом меряется сквозное | |
| качество. | |
| """ | |
| import argparse, json, os, re | |
| from concurrent.futures import ProcessPoolExecutor | |
| import fitz | |
| from PIL import Image | |
| # выпуски, из которых нарезана real_eval (найдены сопоставлением текста), | |
| # плюс весь 2011 год — его выпуск в архиве не опознался однозначно | |
| HELD_OUT = re.compile(r"№ 47 \(2009\)|№ 1 \(2019\)|№ 1 \(2021\)|" | |
| r"1 № \(2013\)|№ 8 \(2017\)|№ 16 \(2015\)|\(2011\)") | |
| def page_boxes(page, scale): | |
| """Рамки строк текстового слоя в пикселях отрендеренной страницы.""" | |
| out = [] | |
| for blk in page.get_text("dict")["blocks"]: | |
| for ln in blk.get("lines", []): | |
| if not any(s["text"].strip() for s in ln["spans"]): | |
| continue | |
| x0, y0, x1, y1 = ln["bbox"] | |
| w, h = (x1 - x0) * scale, (y1 - y0) * scale | |
| # вырожденные и гигантские рамки — мусор вёрстки, не строки | |
| if w < 6 or h < 4 or h > 400 or w / h > 200: | |
| continue | |
| out.append([round(x0 * scale, 1), round(y0 * scale, 1), | |
| round(x1 * scale, 1), round(y1 * scale, 1)]) | |
| return out | |
| def one_pdf(args): | |
| path, out_dir, dpi, max_pages, quality = args | |
| tag = re.sub(r"[^0-9A-Za-z]+", "_", os.path.splitext(os.path.basename(path))[0])[:48] | |
| rows, scale = [], dpi / 72.0 | |
| try: | |
| doc = fitz.open(path) | |
| except Exception as e: | |
| return tag, 0, f"не открылся: {e}" | |
| n = len(doc) if not max_pages else min(len(doc), max_pages) | |
| for pno in range(n): | |
| page = doc[pno] | |
| boxes = page_boxes(page, scale) | |
| if len(boxes) < 20: # обложки и полосы без текстового слоя | |
| continue | |
| pm = page.get_pixmap(dpi=dpi) | |
| im = Image.frombytes("RGB", (pm.width, pm.height), pm.samples).convert("L") | |
| name = f"{tag}_p{pno + 1:03d}.jpg" | |
| im.save(os.path.join(out_dir, name), quality=quality, optimize=True) | |
| rows.append({"f": name, "w": im.width, "h": im.height, "b": boxes}) | |
| doc.close() | |
| if rows: | |
| with open(os.path.join(out_dir, f"_part_{tag}.jsonl"), "w", encoding="utf-8") as fh: | |
| for r in rows: | |
| fh.write(json.dumps(r, ensure_ascii=False) + "\n") | |
| return tag, len(rows), "" | |
| def main(): | |
| ap = argparse.ArgumentParser() | |
| ap.add_argument("--src", required=True, help="каталог с PDF") | |
| ap.add_argument("--out", required=True) | |
| ap.add_argument("--dpi", type=int, default=150) | |
| ap.add_argument("--pages", type=int, default=0, help="0 = все страницы выпуска") | |
| ap.add_argument("--quality", type=int, default=85) | |
| ap.add_argument("--workers", type=int, default=8) | |
| ap.add_argument("--keep-held-out", action="store_true", | |
| help="собрать, наоборот, только отложенные выпуски") | |
| a = ap.parse_args() | |
| os.makedirs(a.out, exist_ok=True) | |
| files = sorted(f for f in os.listdir(a.src) if f.lower().endswith(".pdf")) | |
| picked = [os.path.join(a.src, f) for f in files | |
| if bool(HELD_OUT.search(f)) == a.keep_held_out] | |
| print(f"PDF всего {len(files)}, берём {len(picked)}", flush=True) | |
| tasks = [(p, a.out, a.dpi, a.pages, a.quality) for p in picked] | |
| done = pages = 0 | |
| with ProcessPoolExecutor(max_workers=a.workers) as ex: | |
| for tag, k, err in ex.map(one_pdf, tasks): | |
| done += 1 | |
| pages += k | |
| if err: | |
| print(f" [{done}/{len(tasks)}] {tag}: {err}", flush=True) | |
| elif done % 10 == 0: | |
| print(f" [{done}/{len(tasks)}] страниц собрано {pages}", flush=True) | |
| # склеиваем части: каждый воркер пишет свой файл, общий индекс собираем в конце | |
| with open(os.path.join(a.out, "index.jsonl"), "w", encoding="utf-8") as fh: | |
| for name in sorted(os.listdir(a.out)): | |
| if not name.startswith("_part_"): | |
| continue | |
| p = os.path.join(a.out, name) | |
| with open(p, encoding="utf-8") as src: | |
| fh.write(src.read()) | |
| os.remove(p) | |
| with open(os.path.join(a.out, "index.jsonl"), encoding="utf-8") as fh: | |
| rows = sum(1 for _ in fh) | |
| print(f"готово: страниц {rows} в {a.out}") | |
| if __name__ == "__main__": | |
| main() | |