Spaces:
Paused
Paused
File size: 9,563 Bytes
f66643d | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 | """Đánh giá BẢNG theo hướng "coi cả bảng là một vùng OCR" (không đo cấu trúc HTML).
Lý do: GT OmniDocBench chỉ có ``html`` (không có bbox từng cell) và parser xuất
``cells`` phẳng (bbox + text, không có hàng/cột). Nên KHÔNG dựng HTML/TEDS; thay
vào đó nối text các cell theo thứ tự đọc (row-major) rồi so với text bóc từ GT html.
* GT text : nối ``text_content`` của từng ``<td>/<th>`` (thứ tự tài liệu = row-major).
Nếu bảng có nhiều bản html hợp lệ (html/html_2/html_3) -> lấy bản cho
edit distance THẤP nhất (giống OmniDocBench chấp nhận đa đáp án).
* pred text: gom cell của các Table phủ bởi GT, cụm thành hàng theo y, sort x,
nối row-major.
* Chuẩn hoá bằng norm_ocr (bóc thẻ, hợp nhất inline math ``$..$``/``<math>``).
Metric: Edit distance (/max, kiểu OmniDocBench) + CER, coverage, edit_matched /
edit_all. ĐO NỘI DUNG, KHÔNG đo cấu trúc hàng/cột (hạn chế đã biết).
Ví dụ
-----
# chạy từ benchmark/parser/
python evaluation/eval_table.py --gt data/OmniDocBench.json --pred parser_results \
--mapping parser_results/mapping.json --out eval_results/eval_report_table.json
"""
from __future__ import annotations
import argparse
import json
from collections import defaultdict
import sys
from pathlib import Path
from lxml import html as LH
sys.path.insert(0, str(Path(__file__).resolve().parent)) # import module cạnh bên
import eval_layout as E
SLICE_KEYS = ("language", "layout", "subset", "data_source")
def html_to_text(html_str: str) -> str:
"""Nối text từng cell <td>/<th> theo thứ tự tài liệu (row-major)."""
try:
tree = LH.fromstring(html_str)
except Exception:
return ""
cells = tree.xpath("//td | //th")
if cells:
return " ".join(c.text_content() for c in cells)
return tree.text_content()
def cluster_rows(cells: list[dict]) -> list[dict]:
"""Sắp cell theo thứ tự đọc row-major (cụm hàng theo y, rồi sort x)."""
if not cells:
return []
boxes = [(c, c["bbox_pdf"]) for c in cells]
boxes.sort(key=lambda cb: (cb[1][1] + cb[1][3]) / 2.0) # theo y-center
heights = sorted((b[3] - b[1]) for _, b in boxes)
med_h = heights[len(heights) // 2] or 1.0
rows, cur, cur_y = [], [], None
for c, b in boxes:
yc = (b[1] + b[3]) / 2.0
if cur_y is None or abs(yc - cur_y) <= med_h * 0.6:
cur.append((c, b))
cur_y = yc if cur_y is None else (cur_y + yc) / 2.0
else:
rows.append(cur)
cur, cur_y = [(c, b)], yc
if cur:
rows.append(cur)
ordered = []
for row in rows:
row.sort(key=lambda cb: cb[1][0]) # trong hàng: theo x
ordered.extend(c for c, _ in row)
return ordered
def gt_tables(page: dict, drop_ignore: bool) -> list[dict]:
out = []
for d in page["dets"]:
if d.get("category_type") != "table":
continue
if drop_ignore and d.get("ignore"):
continue
variants = [d[k] for k in ("html", "html_2", "html_3") if d.get(k)]
out.append({
"box": E.norm_box(E.poly_to_xyxy(d["poly"]), page["w"], page["h"]),
"texts": [E.norm_ocr(html_to_text(h)) for h in variants] or [""],
})
return out
def pred_tables(page: dict) -> list[dict]:
W, H = page.get("page_width"), page.get("page_height")
out = []
for e in page["elements"]:
if e.get("label") != "Table":
continue
out.append({
"box": E.norm_box(e["bbox_pdf"], W, H),
"cells": [c for c in e.get("cells", []) if c.get("bbox_pdf")],
})
return out
class Acc:
def __init__(self):
self.n_gt = self.n_matched = self.n_pred = self.n_pred_unmatched = 0
self.num_all = self.den_all = 0.0
self.num_m = self.den_m = 0.0
self.cer_num = self.cer_den = 0.0
def summary(self):
return {
"gt_tables": self.n_gt,
"coverage": round(self.n_matched / self.n_gt, 4) if self.n_gt else None,
"pred_tables": self.n_pred, "pred_unmatched": self.n_pred_unmatched,
"edit_all_micro": round(self.num_all / self.den_all, 4) if self.den_all else None,
"edit_matched_micro": round(self.num_m / self.den_m, 4) if self.den_m else None,
"score_matched": round(1 - self.num_m / self.den_m, 4) if self.den_m else None,
"CER_matched": round(self.cer_num / self.cer_den, 4) if self.cer_den else None,
}
def best_pair(pred_text: str, gt_texts: list[str]):
"""Lấy (dist, maxlen, gtlen) theo bản GT html cho edit distance nhỏ nhất."""
best = None
for gt in gt_texts:
d = E._dist(pred_text, gt)
if best is None or d < best[0]:
best = (d, max(len(pred_text), len(gt)), len(gt))
return best
def score_page(gts, preds, member_thr):
used = [False] * len(preds)
ps = {"n_gt": len(gts), "n_matched": 0, "n_pred": len(preds), "pairs": []}
for g in gts:
members, idxs = [], []
for i, p in enumerate(preds):
if E.contain_ratio(p["box"], g["box"]) >= member_thr:
members.append(p)
idxs.append(i)
if members:
for i in idxs:
used[i] = True
cells = [c for m in members for c in m["cells"]]
pred_text = E.norm_ocr(" ".join(
c.get("source_text", "") for c in cluster_rows(cells)))
ps["n_matched"] += 1
matched = True
else:
pred_text = ""
matched = False
d, mlen, gtlen = best_pair(pred_text, g["texts"])
if mlen:
ps["pairs"].append((d, mlen, gtlen, matched))
ps["n_pred_unmatched"] = sum(1 for u in used if not u)
return ps
def evaluate(gt_pages, pred_index, member_thr, drop_ignore):
slices = defaultdict(Acc)
for img_name, pred_page in pred_index.items():
gt_page = gt_pages.get(img_name)
if gt_page is None:
continue
gts = gt_tables(gt_page, drop_ignore)
preds = pred_tables(pred_page)
if not gts and not preds:
continue
ps = score_page(gts, preds, member_thr)
keys = ["all"]
for k in SLICE_KEYS:
v = gt_page["attr"].get(k)
if isinstance(v, list):
keys += [f"{k}={x}" for x in v]
elif v is not None:
keys.append(f"{k}={v}")
for key in keys:
a = slices[key]
a.n_gt += ps["n_gt"]
a.n_matched += ps["n_matched"]
a.n_pred += ps["n_pred"]
a.n_pred_unmatched += ps["n_pred_unmatched"]
for d, mlen, gtlen, matched in ps["pairs"]:
a.num_all += d
a.den_all += mlen
if matched:
a.num_m += d
a.den_m += mlen
a.cer_num += d
a.cer_den += gtlen
return slices
def parse_args():
ap = argparse.ArgumentParser(description=__doc__,
formatter_class=argparse.RawDescriptionHelpFormatter)
ap.add_argument("--gt", type=Path, required=True)
ap.add_argument("--pred", type=Path, required=True)
ap.add_argument("--mapping", type=Path, required=True)
ap.add_argument("--out", type=Path, default=None)
ap.add_argument("--member-thr", type=float, default=0.5)
ap.add_argument("--keep-ignore", action="store_true")
ap.add_argument("--min-slice", type=int, default=10)
return ap.parse_args()
def main() -> int:
args = parse_args()
gt_pages = E.load_gt(args.gt, "merged")
mapping = json.load(open(args.mapping, encoding="utf-8"))
pred_index = E.build_pred_index(args.pred, mapping)
print(f"[table] GT trang={len(gt_pages)} pred trang={len(pred_index)} "
f"member_thr={args.member_thr}")
slices = evaluate(gt_pages, pred_index, args.member_thr, not args.keep_ignore)
report = {"config": {"member_thr": args.member_thr, "note": "OCR-only, no structure"},
"slices": {k: a.summary() for k, a in slices.items()}}
if args.out:
args.out.write_text(json.dumps(report, indent=2, ensure_ascii=False), encoding="utf-8")
a = slices["all"].summary()
print("\n===== TABLE (all) — đo NỘI DUNG, không đo cấu trúc =====")
print(f" GT bảng = {a['gt_tables']} coverage = {a['coverage']}")
print(f" pred Table = {a['pred_tables']} không khớp = {a['pred_unmatched']}")
print(f" edit_matched micro = {a['edit_matched_micro']} -> score = {a['score_matched']}")
print(f" CER_matched = {a['CER_matched']}")
print(f" edit_all micro = {a['edit_all_micro']} (gồm cả sót detect)")
print("\n===== THEO LÁT CẮT (coverage / edit_matched / CER) =====")
for key in sorted(slices):
if key == "all":
continue
s = slices[key].summary()
if s["gt_tables"] < args.min_slice:
continue
print(f" {key:28s} cov={str(s['coverage']):>6} "
f"editM={str(s['edit_matched_micro']):>6} "
f"CER={str(s['CER_matched']):>6} (gt={s['gt_tables']})")
if args.out:
print(f"\n[table] report -> {args.out}")
return 0
if __name__ == "__main__":
raise SystemExit(main())
|