Download code/src/preprocessing/quality_metrics.py from savastakan/hitit-cuneiform-ocr: direct link, hf CLI and curl.
- Browser
- Download file 5.52 kB
-
https://huggingface.co/savastakan/hitit-cuneiform-ocr/resolve/main/code/src/preprocessing/quality_metrics.py
- Command line
-
hf download hf://savastakan/hitit-cuneiform-ocr/code/src/preprocessing/quality_metrics.py
-
curl -L -o quality_metrics.py https://huggingface.co/savastakan/hitit-cuneiform-ocr/resolve/main/code/src/preprocessing/quality_metrics.py
5.52 kB
| #!/usr/bin/env python3 | |
| """SOTA quality metrics — blur, exposure, contrast, resolution. | |
| Her image için skor hesaplar, manifest'e yazar. Training'de filter. | |
| Reference: Pech-Pacheco 2000, PreP-OCR ACL 2025, CHURRO-DS EMNLP 2025. | |
| """ | |
| import json, os, argparse | |
| from pathlib import Path | |
| from concurrent.futures import ProcessPoolExecutor | |
| import numpy as np | |
| from PIL import Image | |
| import cv2 | |
| Image.MAX_IMAGE_PIXELS = None | |
| ROOT = Path("/arf/scratch/stakan/hitit-proje") | |
| SOURCES = ROOT / "datasets" / "sources" | |
| def compute_quality(item): | |
| """ | |
| Returns: (rid, blur_score, exposure_mean, contrast_std, width, height, mode) | |
| blur_score: Laplacian variance (higher = sharper) | |
| exposure_mean: mean brightness on grayscale | |
| contrast_std: std dev of grayscale | |
| """ | |
| rid, path = item | |
| try: | |
| with Image.open(path) as img: | |
| img_arr = np.array(img.convert('L')) | |
| if img_arr.size == 0: | |
| return (rid, None, None, None, 0, 0, None) | |
| h, w = img_arr.shape[:2] | |
| blur = float(cv2.Laplacian(img_arr, cv2.CV_64F).var()) | |
| exposure = float(img_arr.mean()) | |
| contrast = float(img_arr.std()) | |
| with Image.open(path) as img: | |
| mode = img.mode | |
| return (rid, blur, exposure, contrast, w, h, mode) | |
| except Exception: | |
| return (rid, None, None, None, 0, 0, None) | |
| def main(): | |
| ap = argparse.ArgumentParser() | |
| ap.add_argument('--workers', type=int, default=200) | |
| ap.add_argument('--limit-per-source', type=int, default=None) | |
| args = ap.parse_args() | |
| # Tüm image path'leri topla (unique) | |
| all_items = [] | |
| seen_paths = set() | |
| for d in sorted(SOURCES.iterdir()): | |
| if not d.is_dir(): continue | |
| mp = d / "manifest.jsonl" | |
| if not mp.exists(): continue | |
| src_items = [] | |
| with open(mp) as f: | |
| for line in f: | |
| r = json.loads(line) | |
| p = r.get('path') | |
| if p and r.get('storage') == 'fs' and r.get('integrity_ok') is True: | |
| if p in seen_paths: continue | |
| seen_paths.add(p) | |
| src_items.append((r['id'], p)) | |
| if args.limit_per_source and len(src_items) >= args.limit_per_source: | |
| break | |
| all_items.extend(src_items) | |
| print(f"Quality metrics: {len(all_items):,} unique images") | |
| # Paralel compute | |
| results = {} | |
| with ProcessPoolExecutor(max_workers=args.workers) as ex: | |
| for r in ex.map(compute_quality, all_items, chunksize=500): | |
| results[r[0]] = r | |
| print(f"Hesaplanan: {len(results):,}") | |
| # Manifest'lere yaz | |
| for d in sorted(SOURCES.iterdir()): | |
| if not d.is_dir(): continue | |
| for mf in ['manifest.jsonl', 'manifest_classification.jsonl', 'manifest_detection.jsonl']: | |
| mp = d / mf | |
| if not mp.exists(): continue | |
| records = [] | |
| with open(mp) as f: | |
| for line in f: | |
| r = json.loads(line) | |
| res = results.get(r.get('id')) | |
| if res: | |
| _, blur, expo, cont, w, h, mode = res | |
| r['blur_score'] = round(blur, 2) if blur is not None else None | |
| r['exposure_mean'] = round(expo, 2) if expo is not None else None | |
| r['contrast_std'] = round(cont, 2) if cont is not None else None | |
| # width/height zaten var, güvenli ekle | |
| if w and not r.get('width'): r['width'] = w | |
| if h and not r.get('height'): r['height'] = h | |
| records.append(r) | |
| with open(mp, 'w') as f: | |
| for r in records: | |
| f.write(json.dumps(r, ensure_ascii=False) + '\n') | |
| # Dataset stats | |
| blurs = [v[1] for v in results.values() if v[1] is not None] | |
| expos = [v[2] for v in results.values() if v[2] is not None] | |
| conts = [v[3] for v in results.values() if v[3] is not None] | |
| def percentiles(arr): | |
| arr = sorted(arr) | |
| n = len(arr) | |
| return {f"p{p}": round(arr[int(n*p/100)], 2) for p in [1, 5, 25, 50, 75, 95, 99]} | |
| summary = { | |
| "n_images_scored": len(results), | |
| "blur_score": percentiles(blurs) if blurs else {}, | |
| "exposure_mean": percentiles(expos) if expos else {}, | |
| "contrast_std": percentiles(conts) if conts else {}, | |
| "thresholds_applied": { | |
| "blur_min": 100.0, | |
| "exposure_min": 20.0, | |
| "exposure_max": 235.0, | |
| "contrast_min": 15.0, | |
| }, | |
| "n_failing_blur": sum(1 for b in blurs if b < 100), | |
| "n_failing_exposure": sum(1 for e in expos if e < 20 or e > 235), | |
| "n_failing_contrast": sum(1 for c in conts if c < 15), | |
| } | |
| with open(ROOT / "datasets" / "processed" / "quality_metrics_summary.json", 'w') as f: | |
| json.dump(summary, f, indent=2, ensure_ascii=False) | |
| print(f"Blur: min={min(blurs):.1f}, median={np.median(blurs):.1f}, max={max(blurs):.1f}") | |
| print(f"Exposure: min={min(expos):.1f}, median={np.median(expos):.1f}, max={max(expos):.1f}") | |
| print(f"Contrast: min={min(conts):.1f}, median={np.median(conts):.1f}, max={max(conts):.1f}") | |
| print(f"Failing blur (<100): {summary['n_failing_blur']:,}") | |
| print(f"Failing exposure: {summary['n_failing_exposure']:,}") | |
| print(f"Failing contrast (<15): {summary['n_failing_contrast']:,}") | |
| if __name__ == '__main__': | |
| main() | |