sakha-ocr / src /eval_det.py
loalkota's picture
Детектор строк и распознаватель якутского текста с рецептом обучения
d4d4e7b verified
Raw History Blame Contribute Delete
7.18 kB
"""Оценка детектора строк на отложенных выпусках.
Меряем то, что важно продукту: сколько строк найдено (полнота), сколько
выдумано (точность) и на скольких страницах порядок чтения совпал с вёрсткой.
"""
import argparse, json, os, sys, time
import numpy as np
import torch
from PIL import Image, ImageFilter
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from linedet import LineDet
from detect import lines_from_prob, iou_match
@torch.no_grad()
def run_page(model, im, dev, stride, max_side=2600):
"""Карта вероятностей для целой страницы; длинная сторона ограничена."""
s = min(1.0, max_side / max(im.width, im.height))
if s < 1.0:
im = im.resize((int(im.width * s), int(im.height * s)), Image.BILINEAR)
w, h = im.width, im.height
pw, ph = (w + 31) // 32 * 32, (h + 31) // 32 * 32
a = np.zeros((ph, pw), np.float32)
a[:h, :w] = 1.0 - np.asarray(im, np.float32) / 255.0
x = torch.from_numpy(a)[None, None].to(dev)
with torch.amp.autocast("cuda", enabled=(dev == "cuda")):
prob, _ = model(x)
prob = torch.sigmoid(prob.float())[0, 0].cpu().numpy()
prob = prob[:h // stride, :w // stride]
return prob, s
def main():
ap = argparse.ArgumentParser()
ap.add_argument("--ckpt", required=True)
ap.add_argument("--data", required=True)
ap.add_argument("--pages", type=int, default=0)
ap.add_argument("--skip", type=int, default=0,
help="пропустить первые N страниц: по ним отбирался чекпоинт")
ap.add_argument("--iou", type=float, default=0.5)
ap.add_argument("--max-side", type=int, default=2000)
ap.add_argument("--unclip", type=float, default=1.6)
ap.add_argument("--book-scale", type=float, default=4.0)
ap.add_argument("--book", action="store_true",
help="книжно-экранное условие: та же разметка, крупнее и бледнее")
ap.add_argument("--max-gap", type=float, default=2.0,
help="порог склейки кусков строки в долях высоты строки")
a = ap.parse_args()
dev = "cuda" if torch.cuda.is_available() else "cpu"
ck = torch.load(a.ckpt, map_location="cpu", weights_only=False)
model = LineDet(ck.get("width", 32)).to(dev).eval()
model.load_state_dict(ck["model"])
stride = ck.get("stride", 2)
rows = [json.loads(l) for l in
open(os.path.join(a.data, "index.jsonl"), encoding="utf-8")]
rows = rows[a.skip:]
if a.pages:
rows = rows[:a.pages]
tot_rec = tot_prec = 0.0
n_gt = n_pred = n_hit = 0
ok_pairs = all_pairs = 0
t0 = time.time()
for r in rows:
im = Image.open(os.path.join(a.data, r["f"])).convert("L")
pre, off = 1.0, (0, 0)
gt_src = r["b"]
if a.book:
# Книжно-экранное условие. Просто увеличить страницу мало:
# run_page тут же ужмёт её обратно под max_side, и высота строки
# не изменится. Поэтому после увеличения вырезаем окно размером
# с обычную страницу — получается маленький лист с крупным
# бледным текстом, как на скриншоте книги.
pre = a.book_scale
big = im.resize((int(im.width * pre), int(im.height * pre)), Image.BILINEAR)
cw = min(big.width, a.max_side)
ch = min(big.height, a.max_side)
# окно центрируем по массе текста: середина увеличенной полосы
# часто приходится на фотографию, и в окне не оказывается строк
cx = float(np.mean([(b[0] + b[2]) / 2 for b in r["b"]])) * pre
cy = float(np.mean([(b[1] + b[3]) / 2 for b in r["b"]])) * pre
ox = int(min(max(0.0, cx - cw / 2), big.width - cw))
oy = int(min(max(0.0, cy - ch / 2), big.height - ch))
im = big.crop((ox, oy, ox + cw, oy + ch))
arr = np.asarray(im, np.float32) / 255.0
im = Image.fromarray(((0.30 + arr * 0.60) * 255).astype(np.uint8)).filter(
ImageFilter.GaussianBlur(0.6))
off = (ox, oy)
gt_src = [[b[0] * pre - ox, b[1] * pre - oy, b[2] * pre - ox, b[3] * pre - oy]
for b in r["b"]]
# оставляем строки, попавшие в окно хотя бы наполовину, и
# обрезаем их по окну: иначе точность падает на строках,
# которые видны у края лишь частично
keep = []
for b in gt_src:
x0, y0 = max(0.0, b[0]), max(0.0, b[1])
x1, y1 = min(float(cw), b[2]), min(float(ch), b[3])
if x1 <= x0 or y1 <= y0:
continue
if (x1 - x0) * (y1 - y0) < 0.5 * (b[2] - b[0]) * (b[3] - b[1]):
continue
keep.append([x0, y0, x1, y1])
gt_src = keep
pre = 1.0
prob, s = run_page(model, im, dev, stride, a.max_side)
w_s, h_s = int(im.width * s), int(im.height * s)
pred, lh = lines_from_prob(prob, stride, w_s, h_s, a.max_gap, a.unclip)
gt = [tuple(int(v * pre * s) for v in b) for b in gt_src]
rec, prec, hit = iou_match(pred, gt, a.iou)
n_gt += len(gt); n_pred += len(pred); n_hit += hit
# порядок чтения: сравниваем с порядком строк в вёрстке исходника
ordered = pred
idx = []
for b in ordered:
best, bi = 0.0, -1
ba = max(1, (b[2]-b[0]) * (b[3]-b[1]))
for i, g in enumerate(gt):
ox = min(b[2], g[2]) - max(b[0], g[0])
oy = min(b[3], g[3]) - max(b[1], g[1])
if ox <= 0 or oy <= 0:
continue
v = ox * oy / ba
if v > best:
best, bi = v, i
if best > 0.25:
idx.append(bi)
ok_pairs += sum(1 for p, q in zip(idx, idx[1:]) if q >= p)
all_pairs += max(1, len(idx) - 1)
print(f"страниц {len(rows)}, строк в эталоне {n_gt}, найдено рамок {n_pred}")
print(f"IoU {a.iou}: полнота {n_hit/max(1,n_gt)*100:.1f}%, "
f"точность {n_hit/max(1,n_pred)*100:.1f}%")
print(f"порядок чтения: {ok_pairs/all_pairs*100:.1f}% соседних пар верно")
print(f"{(time.time()-t0)/len(rows)*1000:.0f} мс на страницу ({dev})")
if __name__ == "__main__":
main()