Bina-0.2-RizehPizeh / training-kit /segment_ppocr_lines.py
Reza2kn's picture
Add training-kit/segment_ppocr_lines.py
6f683f1 verified
Raw
History Blame Contribute Delete
4.11 kB
#!/usr/bin/env python3
"""Split long RTL OCR lines into CTC-safe Persian word-group crops."""
from __future__ import annotations
import argparse
from pathlib import Path
import cv2
import numpy as np
from PIL import Image
def zero_runs(mask: np.ndarray) -> list[tuple[int, int]]:
runs = []
start = None
for index, value in enumerate(mask.tolist() + [False]):
if value and start is None:
start = index
elif not value and start is not None:
runs.append((start, index))
start = None
return runs
def word_intervals(image: Image.Image, count: int) -> list[tuple[int, int]] | None:
if count == 1:
return [(0, image.width)]
gray = np.asarray(image.convert("L"))
_, ink = cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY_INV + cv2.THRESH_OTSU)
projection = (ink > 0).sum(axis=0)
active = np.flatnonzero(projection > max(1, image.height // 80))
if active.size == 0:
return None
left, right = int(active[0]), int(active[-1]) + 1
gaps = [
(a, b) for a, b in zero_runs(projection[left:right] <= max(1, image.height // 100))
if b - a >= 2 and a > 0 and b < right - left
]
if len(gaps) < count - 1:
return None
chosen = sorted(
((a + left, b + left) for a, b in sorted(gaps, key=lambda run: run[1] - run[0], reverse=True)[: count - 1]),
key=lambda run: run[0],
)
cuts = [left] + [(a + b) // 2 for a, b in chosen] + [right]
intervals = [(cuts[i], cuts[i + 1]) for i in range(len(cuts) - 1)]
return list(reversed(intervals))
def groups(words: list[str], intervals: list[tuple[int, int]], max_chars: int):
current_words: list[str] = []
current_boxes: list[tuple[int, int]] = []
for word, box in zip(words, intervals):
candidate = " ".join(current_words + [word])
if current_words and len(candidate) > max_chars:
yield " ".join(current_words), current_boxes
current_words, current_boxes = [], []
if len(word) > max_chars:
return
current_words.append(word)
current_boxes.append(box)
if current_words:
yield " ".join(current_words), current_boxes
def process_list(source: Path, destination: Path, image_dir: Path, root: Path, max_chars: int) -> tuple[int, int]:
output = []
rejected = 0
for line_index, raw in enumerate(source.read_text(encoding="utf-8").splitlines()):
relative, text = raw.split("\t", 1)
words = text.split()
with Image.open(root / relative) as opened:
image = opened.convert("RGB")
intervals = word_intervals(image, len(words))
if intervals is None:
rejected += 1
continue
for group_index, (label, boxes) in enumerate(groups(words, intervals, max_chars)):
x0 = max(0, min(x[0] for x in boxes) - 3)
x1 = min(image.width, max(x[1] for x in boxes) + 3)
crop = image.crop((x0, 0, x1, image.height))
path = image_dir / f"{line_index:07d}-{group_index:03d}.png"
crop.save(path)
output.append(f"{path.relative_to(root)}\t{label}")
destination.write_text("\n".join(output) + "\n", encoding="utf-8")
return len(output), rejected
def main() -> None:
parser = argparse.ArgumentParser()
parser.add_argument("--data-root", type=Path, required=True)
parser.add_argument("--pilot-dir", type=Path, required=True)
parser.add_argument("--max-chars", type=int, default=25)
args = parser.parse_args()
for split in ("train", "val"):
image_dir = args.pilot_dir / "images" / f"segments-{split}"
image_dir.mkdir(parents=True, exist_ok=True)
kept, rejected = process_list(
args.pilot_dir / f"{split}_list.txt",
args.pilot_dir / f"{split}_segments.txt",
image_dir,
args.data_root,
args.max_chars,
)
print(f"{split}: segments={kept} rejected_lines={rejected}")
if __name__ == "__main__":
main()