docforensics-layout / detect.py
bonzai2carn's picture
Upload folder using huggingface_hub
dd07e44 verified
Raw History Blame Contribute Delete
3.64 kB
#!/usr/bin/env python
"""
detect.py -- run docforensics on a page image and draw the regions.
python detect.py page.png [out.png]
Needs: onnxruntime, opencv-python, numpy.
"""
import sys
import cv2
import numpy as np
import onnxruntime as ort
CLASSES = ["text", "heading", "list", "table", "picture", "caption", "formula",
"header", "footer", "footnote", "seal", "form", "field", "checkbox",
"signature"]
SIGNALS = ["skew", "blur", "noise", "bleed", "warp", "tears", "handwriting", "native"]
COLOURS = {"text": (75, 180, 60), "heading": (75, 25, 230), "list": (25, 225, 255),
"table": (200, 130, 0), "picture": (48, 130, 245), "caption": (180, 30, 145),
"field": (90, 220, 60), "checkbox": (230, 0, 0), "form": (0, 140, 255)}
CONF, IOU = 0.18, 0.5
def letterbox(img, side=640):
"""Match training preprocessing exactly: fit long side, pad with 114."""
h, w = img.shape[:2]
s = side / max(h, w)
small = cv2.resize(img, (int(w * s), int(h * s)))
canvas = np.full((side, side, 3), 114, np.uint8)
canvas[:small.shape[0], :small.shape[1]] = small
return canvas
def nms(boxes, scores, thr):
order = scores.argsort()[::-1]
keep = []
while order.size:
i = order[0]
keep.append(i)
if order.size == 1:
break
xx1 = np.maximum(boxes[i, 0], boxes[order[1:], 0])
yy1 = np.maximum(boxes[i, 1], boxes[order[1:], 1])
xx2 = np.minimum(boxes[i, 2], boxes[order[1:], 2])
yy2 = np.minimum(boxes[i, 3], boxes[order[1:], 3])
inter = np.maximum(0, xx2 - xx1) * np.maximum(0, yy2 - yy1)
a = (boxes[:, 2] - boxes[:, 0]) * (boxes[:, 3] - boxes[:, 1])
order = order[1:][inter / (a[i] + a[order[1:]] - inter) <= thr]
return keep
def detect(path, model="docforensics-layout-s.onnx"):
raw = cv2.imread(path)
if raw is None:
sys.exit(f"cannot read {path}")
img = letterbox(raw)
x = cv2.cvtColor(img, cv2.COLOR_BGR2RGB).astype(np.float32) / 255.0
x = ((x - [0.485, 0.456, 0.406]) / [0.229, 0.224, 0.225])
x = x.transpose(2, 0, 1)[None].astype(np.float32)
sess = ort.InferenceSession(model, providers=["CPUExecutionProvider"])
det, signals, _maps = sess.run(
None, {"images": x, "evidence": np.zeros((1, 4), np.float32)})
# det is [1, 4+15, 8500] in YOLOv8 row order: cx, cy, w, h, then per-class
# scores. Any YOLOv8 decode works unchanged.
d = det[0].T
cx, cy, bw, bh = d[:, 0], d[:, 1], d[:, 2], d[:, 3]
xyxy = np.stack([cx - bw / 2, cy - bh / 2, cx + bw / 2, cy + bh / 2], 1)
scores, labels = d[:, 4:].max(1), d[:, 4:].argmax(1)
out = []
for c in np.unique(labels[scores > CONF]):
m = (labels == c) & (scores > CONF)
for i in nms(xyxy[m], scores[m], IOU):
out.append((xyxy[m][i], scores[m][i], CLASSES[c]))
return img, out, dict(zip(SIGNALS, signals[0].round(3).tolist()))
if __name__ == "__main__":
if len(sys.argv) < 2:
sys.exit(__doc__)
img, regions, signals = detect(sys.argv[1])
for box, score, name in regions:
c = COLOURS.get(name, (150, 150, 150))
cv2.rectangle(img, (int(box[0]), int(box[1])),
(int(box[2]), int(box[3])), c, 2)
cv2.putText(img, f"{name} {score:.2f}", (int(box[0]), max(10, int(box[1]) - 4)),
cv2.FONT_HERSHEY_SIMPLEX, 0.35, c, 1)
print(f"{len(regions)} regions")
for k, v in signals.items():
print(f" {k:12s} {v:.3f}")
dst = sys.argv[2] if len(sys.argv) > 2 else "out.png"
cv2.imwrite(dst, img)
print(f"wrote {dst}")