Upload infer.py with huggingface_hub
Browse files
infer.py
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""Run the Tibetan modern-book layout detector (RF-DETR-L fine-tune) on one or
|
| 3 |
+
more page images, applying the recommended *per-class* confidence thresholds.
|
| 4 |
+
|
| 5 |
+
The model is a 4-class RF-DETR-L (header, text-area, footnote, footer),
|
| 6 |
+
fine-tuned on the same `tam2col` labels as BDRC's primary RT-DETR-l release
|
| 7 |
+
(see the model card / blog post). Like that model, the detector is
|
| 8 |
+
deliberately recall-happy on the small marginal header/footer boxes, so the
|
| 9 |
+
single best operating point differs by class. The thresholds below are each
|
| 10 |
+
class's own max-F1 confidence from a native per-class sweep on the held-out
|
| 11 |
+
test set (same methodology as the primary RT-DETR-l release); a single global
|
| 12 |
+
0.30 is the best compromise if you need one number for all classes.
|
| 13 |
+
|
| 14 |
+
Usage:
|
| 15 |
+
python infer.py --checkpoint rfdetr_tibetan_book_layout.pth --source page.jpg
|
| 16 |
+
python infer.py --checkpoint rfdetr_tibetan_book_layout.pth --source pages/ --out preds
|
| 17 |
+
"""
|
| 18 |
+
from __future__ import annotations
|
| 19 |
+
|
| 20 |
+
import argparse
|
| 21 |
+
from pathlib import Path
|
| 22 |
+
|
| 23 |
+
IMG_EXTS = {".jpg", ".jpeg", ".png", ".webp", ".bmp", ".tif", ".tiff"}
|
| 24 |
+
|
| 25 |
+
# Per-class max-F1 operating points (see model card). Use --global-conf 0.30
|
| 26 |
+
# instead if you prefer one number for all classes.
|
| 27 |
+
CLASS_THRESHOLDS = {0: 0.46, 1: 0.32, 2: 0.26, 3: 0.52}
|
| 28 |
+
CONF_FLOOR = min(CLASS_THRESHOLDS.values())
|
| 29 |
+
|
| 30 |
+
|
| 31 |
+
def main() -> int:
|
| 32 |
+
ap = argparse.ArgumentParser(description=__doc__,
|
| 33 |
+
formatter_class=argparse.RawDescriptionHelpFormatter)
|
| 34 |
+
ap.add_argument("--checkpoint", required=True, help="path to the .pth checkpoint")
|
| 35 |
+
ap.add_argument("--source", required=True, help="image file or folder")
|
| 36 |
+
ap.add_argument("--out", default=None,
|
| 37 |
+
help="optional folder to write YOLO-format .txt labels")
|
| 38 |
+
ap.add_argument("--shape", type=int, default=1024)
|
| 39 |
+
ap.add_argument("--global-conf", type=float, default=None,
|
| 40 |
+
help="use ONE threshold for all classes instead of per-class")
|
| 41 |
+
args = ap.parse_args()
|
| 42 |
+
|
| 43 |
+
from rfdetr import RFDETRLarge
|
| 44 |
+
from PIL import Image
|
| 45 |
+
|
| 46 |
+
thresholds = ({c: args.global_conf for c in CLASS_THRESHOLDS}
|
| 47 |
+
if args.global_conf is not None else CLASS_THRESHOLDS)
|
| 48 |
+
floor = min(thresholds.values())
|
| 49 |
+
|
| 50 |
+
model = RFDETRLarge.from_checkpoint(args.checkpoint)
|
| 51 |
+
# class_names on the checkpoint: ['none', 'header', 'text-area', 'footnote', 'footer']
|
| 52 |
+
names = {0: "header", 1: "text-area", 2: "footnote", 3: "footer"}
|
| 53 |
+
|
| 54 |
+
src = Path(args.source)
|
| 55 |
+
imgs = sorted(p for p in src.iterdir() if p.suffix.lower() in IMG_EXTS) \
|
| 56 |
+
if src.is_dir() else [src]
|
| 57 |
+
|
| 58 |
+
out_dir = Path(args.out) if args.out else None
|
| 59 |
+
if out_dir:
|
| 60 |
+
out_dir.mkdir(parents=True, exist_ok=True)
|
| 61 |
+
|
| 62 |
+
n_img = n_kept = 0
|
| 63 |
+
for ip in imgs:
|
| 64 |
+
n_img += 1
|
| 65 |
+
with Image.open(ip) as im:
|
| 66 |
+
W, H = im.size
|
| 67 |
+
det = model.predict(str(ip), threshold=floor, shape=(args.shape, args.shape))
|
| 68 |
+
lines = []
|
| 69 |
+
if det is not None and len(det) > 0:
|
| 70 |
+
for box, cls_id, score in zip(det.xyxy, det.class_id, det.confidence):
|
| 71 |
+
cls = int(cls_id) - 1 # class 0 on the checkpoint is background
|
| 72 |
+
if cls < 0 or cls > 3 or score < thresholds.get(cls, floor):
|
| 73 |
+
continue
|
| 74 |
+
x1, y1, x2, y2 = box.tolist()
|
| 75 |
+
cx, cy = ((x1 + x2) / 2) / W, ((y1 + y2) / 2) / H
|
| 76 |
+
w, h = (x2 - x1) / W, (y2 - y1) / H
|
| 77 |
+
lines.append((cls, float(score), cx, cy, w, h))
|
| 78 |
+
n_kept += len(lines)
|
| 79 |
+
print(f"{ip.stem}: {len(lines)} boxes")
|
| 80 |
+
for cls, score, cx, cy, w, h in lines:
|
| 81 |
+
print(f" {names[cls]:10} conf={score:.3f} "
|
| 82 |
+
f"cx={cx:.3f} cy={cy:.3f} w={w:.3f} h={h:.3f}")
|
| 83 |
+
if out_dir:
|
| 84 |
+
(out_dir / f"{ip.stem}.txt").write_text(
|
| 85 |
+
"".join(f"{c} {cx:.6f} {cy:.6f} {w:.6f} {h:.6f}\n"
|
| 86 |
+
for c, _, cx, cy, w, h in lines))
|
| 87 |
+
|
| 88 |
+
print(f"\n{n_img} images, {n_kept} boxes kept (thresholds: {thresholds})")
|
| 89 |
+
return 0
|
| 90 |
+
|
| 91 |
+
|
| 92 |
+
if __name__ == "__main__":
|
| 93 |
+
raise SystemExit(main())
|