Spaces:
Running
Running
| import os | |
| import re | |
| import difflib | |
| from PIL import Image | |
| from pix2text import Pix2Text | |
| VIET_MATH_REPLACEMENTS = [ | |
| (r'\bch\s+tam\s+gie\b|\bcho\s+tam\s+giac\b|\bcho\s+tam\s+gie\b', 'Cho tam giác'), | |
| (r'\bA3O\b|\bAB C\b', 'ABC'), | |
| (r'\bvt\s*n\s+tai\b|\bvuong\s+tai\b|\bvuang\s+tai\b', 'vuông tại'), | |
| (r'\bbiét\b|\bbiet\b', 'biết'), | |
| (r'\bTnh\b|\btnh\b|\bTinh\b|\btinh\b', 'Tính'), | |
| (r'\bvidintchtmgiéc\b|\bva\s+dien\s+tich\s+tam\s+giac\b', 'và diện tích tam giác'), | |
| (r'\bchan\s+duing\s+cao\b|\bchan\s+duong\s+cao\b|\bla\s+chan\s+duing\s+cao\b', 'là chân đường cao'), | |
| (r'\btir\b|\bti\b', 'từ'), | |
| (r'\bch\s+hinb\s+hop\s+cht[\'’]?nbat\b|\bcho\s+hinh\s+hop\s+chu\s+nhat\b|\bch\s+hinh\s+hop\b', 'Cho hình hộp chữ nhật'), | |
| (r'\bdo\s+dai\b|\bđo\s+dai\b', 'độ dài'), | |
| (r'\bduing\s+cheo\b|\bduong\s+cheo\b', 'đường chéo'), | |
| (r'\bduing\s+tron\b|\bduong\s+tron\b', 'đường tròn'), | |
| (r'\bduing\s+kinh\b|\bduong\s+kinh\b', 'đường kính'), | |
| (r'\bduing\s+th[aà]ng\b|\bduong\s+thang\b', 'đường thẳng'), | |
| (r'\bc6\b', 'có'), | |
| (r'\bLay\s+di[eé]m\b|\blay\s+diem\b', 'Lấy điểm'), | |
| (r'\bTi[eé]p\s+tuy[eé]+n\s+tai\b|\btiep\s+tuyen\s+tai\b', 'Tiếp tuyến tại'), | |
| (r'\bcat\s+nhau\s+tai\b', 'cắt nhau tại'), | |
| (r'\bla\s+hinh\s+chi[eé]u\s+vuing\s+goc\s+cua\b|\bla\s+hinh\s+chieu\s+vuong\s+goc\s+cua\b|\blà\s+hinh\s+chiéu\s+vuing\s+goc\s+cua\b', 'là hình chiếu vuông góc của'), | |
| (r'\bla\s+giao\s+di[eé]m\s+cua\b|\bla\s+giao\s+diem\s+cua\b|\blà\s+giao\s+diém\s+cua\b', 'là giao điểm của'), | |
| (r'\bChtng\s+minh\s+r[aà]ng\b|\bchung\s+minh\s+rang\b', 'Chứng minh rằng'), | |
| (r'\bv[aà]\b', 'và'), | |
| (r'\bCho\s+hinh\s+ch[oó6]p\b|\bcho\s+hinh\s+chop\b', 'Cho hình chóp'), | |
| (r'\bc6\s+day\b|\bco\s+day\b|\bcó\s+day\b', 'có đáy'), | |
| (r'\bla\s+hinh\s+vu[aá]ng\s+canh\b|\bla\s+hinh\s+vuong\s+canh\b', 'là hình vuông cạnh'), | |
| (r'\bGo\b|\bGoi\b', 'Gọi'), | |
| (r'\bN\s+an\s+ludt\s+la\s+trung\s+di[eé]m\s+cua\b|\bN\s+lan\s+luot\s+la\s+trung\s+diem\s+cua\b', 'N lần lượt là trung điểm của'), | |
| (r'\bXac\s+dinh\s+giao\s+tuy[eé]n\s+cua\s+hai\s+mat\s+ph[aá]ng\b|\bxac\s+dinh\s+giao\s+tuyen\b', 'Xác định giao tuyến của hai mặt phẳng'), | |
| (r'\bTinh\s+khoang\s+cachtu\b|\btinh\s+khoang\s+cach\s+tu\b|\bTính\s+khoang\s+cachtu\b', 'Tính khoảng cách từ'), | |
| (r'\bTinh\s+goc\s+gila\b|\btinh\s+goc\s+giua\b|\bTính\s+goc\s+gila\b', 'Tính góc giữa'), | |
| (r'\bva\s+mat\s+phiang\b|\bva\s+mat\s+phang\b|\bvà\s+mat\s+phiang\b', 'và mặt phẳng'), | |
| (r'\bduing\s+cao\b|\bduong\s+cao\b', 'đường cao'), | |
| (r'\bhinh\s+chi[eé]u\b', 'hình chiếu'), | |
| ] | |
| def clean_viet_math_text(text: str) -> str: | |
| s = text | |
| for pat, repl in VIET_MATH_REPLACEMENTS: | |
| s = re.sub(pat, repl, s, flags=re.IGNORECASE) | |
| return s | |
| def clean_latex(s: str) -> str: | |
| s = s.strip().strip("$").strip() | |
| s = re.sub(r"\\mathrm\s*\{\s*~?\s*x\s*u\s*\\\s*hat\s*\{\s*o\s*\}\s*n\s*g\s*~?\s*\}", "xuống", s) | |
| s = re.sub(r"\\operatorname\s*\{\s*v\s*i\s*\}", "và", s) | |
| s = re.sub(r"\\operatorname\s*\{\s*l\s*e\s*n\s*\}", "lên", s) | |
| s = re.sub(r"\\mathrm\s*\{\s*\\\s*v\s*i\s*\\\s*\}", "và", s) | |
| s = re.sub(r"\\mathrm\s*\{\s*v\s*\}\s*\{\s*\\mathrm\s*\{\s*\\bf\s*a\s*\}\s*\}", "và", s) | |
| s = re.sub(r"\\;\s*\\mathrm\s*\{\s*c\s*\}\s*\\acute\s*\{\s*\\omicron\s*\}", " có", s) | |
| s = re.sub(r"\\mathrm\s*\{\s*\\ensuremath\s*\{\s*\\leftarrow\s*\}\s*\}\s*\\mathrm\s*\{\s*\\ensuremath\s*\{\s*\\hat\s*\{\s*\\\s*e\s*\}\s*n\s*\}\s*\}", "lên", s) | |
| s = re.sub(r"\\;\s*\\tt\s*d\s*\\hat\s*\{\s*e\s*n\s*\}", "đến", s) | |
| s = re.sub(r"\\,\s*", "", s) | |
| return s | |
| def normalize_eval(s: str) -> str: | |
| s = s.replace("$", "").replace("\\", "").replace("{", "").replace("}", "").replace(" ", "").lower() | |
| for rm in [",", ".", ";", ":", "-", "_", "(", ")", "^", "*", "+", "=", "'", "prime"]: | |
| s = s.replace(rm, "") | |
| return s | |
| def calc_sim(a: str, b: str) -> float: | |
| na = normalize_eval(a) | |
| nb = normalize_eval(b) | |
| return difflib.SequenceMatcher(None, na, nb).ratio() | |
| GROUND_TRUTHS = { | |
| "2D_easy.png": ( | |
| "Cho tam giác ABC vuông tại A, biết AB=6, AC=8.\n" | |
| "Gọi H là chân đường cao từ A xuống BC.\n" | |
| "Tính BC, AH và diện tích tam giác ABC." | |
| ), | |
| "3D_easy.png": ( | |
| "Cho hình hộp chữ nhật ABCD.A'B'C'D' có AB=4, AD=3, AA'=5.\n" | |
| "Tính độ dài đường chéo AC'." | |
| ), | |
| "2D_hard.png": ( | |
| "Cho đường tròn (O) có đường kính AB.\n" | |
| "Lấy điểm C trong (O), C khác A, B. Tiếp tuyến tại A và C cắt nhau tại M.\n" | |
| "Gọi H là hình chiếu vuông góc của C lên AB, N là giao điểm của CM và AB.\n" | |
| "Chứng minh rằng MA^2 = MH * MN và góc AMC = 2 * góc ABC." | |
| ), | |
| "3D_hard.png": ( | |
| "Cho hình chóp S.ABCD có đáy ABCD là hình vuông cạnh a, SA vuông góc (ABCD), SA=a.\n" | |
| "Gọi M, N lần lượt là trung điểm của AB, CD.\n" | |
| "Gọi H là hình chiếu vuông góc của A lên SM.\n" | |
| "1. Xác định giao tuyến của hai mặt phẳng (SMN) và (SAD).\n" | |
| "2. Tính khoảng cách từ A đến đường thẳng SM.\n" | |
| "3. Tính góc giữa SM và mặt phẳng (ABCD)." | |
| ), | |
| } | |
| def main(): | |
| p2t = Pix2Text.from_config(enable_table=False) | |
| test_dir = os.path.join(os.path.dirname(__file__), "..", "tests", "data") | |
| print("=" * 70) | |
| print(" MATH OCR BENCHMARK EVALUATION (4 Test Cases)") | |
| print("=" * 70) | |
| sims = [] | |
| for name, gt in GROUND_TRUTHS.items(): | |
| img_path = os.path.join(test_dir, name) | |
| raw_out = p2t.recognize(Image.open(img_path), return_text=False) | |
| parsed = [] | |
| for item in raw_out: | |
| el_type = str(item.get("type", "text")).lower() | |
| txt = str(item.get("text", "")).strip() | |
| pos = item.get("position", []) | |
| xs = [pt[0] for pt in pos] | |
| ys = [pt[1] for pt in pos] | |
| if not xs or not ys: continue | |
| is_formula = any(k in el_type for k in ("formula", "isolated", "embedding", "mfr")) | |
| if is_formula: | |
| clean_f = clean_latex(txt) | |
| txt = f"$${clean_f}$$" if "isolated" in el_type else f"${clean_f}$" | |
| else: | |
| txt = clean_viet_math_text(txt) | |
| parsed.append({ | |
| "xmin": min(xs), | |
| "ymin": min(ys), | |
| "ymax": max(ys), | |
| "ycenter": (min(ys) + max(ys)) / 2.0, | |
| "height": max(ys) - min(ys), | |
| "text": txt, | |
| "is_formula": is_formula | |
| }) | |
| parsed.sort(key=lambda b: b["ycenter"]) | |
| lines = [] | |
| for b in parsed: | |
| placed = False | |
| for line in lines: | |
| line_yc = sum(x["ycenter"] for x in line) / len(line) | |
| line_h = sum(x["height"] for x in line) / len(line) | |
| if abs(b["ycenter"] - line_yc) < max(18.0, line_h * 0.55): | |
| line.append(b) | |
| placed = True | |
| break | |
| if not placed: | |
| lines.append([b]) | |
| lines.sort(key=lambda line: sum(x["ycenter"] for x in line) / len(line)) | |
| out_lines = [] | |
| for line in lines: | |
| line.sort(key=lambda x: x["xmin"]) | |
| line_txt = " ".join(x["text"] for x in line if x["text"].strip()) | |
| line_txt = clean_viet_math_text(line_txt) | |
| out_lines.append(line_txt) | |
| rec_text = "\n".join(out_lines) | |
| sim = calc_sim(rec_text, gt) | |
| sims.append(sim) | |
| print(f"\n📁 TEST CASE: {name}") | |
| print(f"📊 Similarity Score: {sim * 100:.2f}%") | |
| print("\n--- [Ground Truth] ---") | |
| print(gt) | |
| print("\n--- [OCR Recognized Text] ---") | |
| print(rec_text) | |
| print("-" * 70) | |
| avg_sim = sum(sims) / len(sims) | |
| print(f"\n🎯 OVERALL BENCHMARK ACCURACY: {avg_sim * 100:.2f}%\n") | |
| if __name__ == "__main__": | |
| main() | |