import os import json import cv2 import numpy as np from collections import defaultdict from barcode_scanner import scan_all_barcodes from preprocess import preprocess_chassis BARCODE_DIR = "images/barcode" CHASSIS_DIR = "images/chassis" CONFIG_PATH = "config.json" def run_ocr_ensemble(image_path, ocr): variations = preprocess_chassis(image_path) best_text, best_score = "", -1 for var in variations: result = ocr.ocr(var, cls=True) if not result or not result[0]: continue texts = [line[1][0] for line in result[0]] confs = [line[1][1] for line in result[0]] text = "".join(texts).upper() text = "".join(c for c in text if c.isalnum()) conf = sum(confs) / len(confs) if confs else 0.0 score = conf * max(len(text), 1) if score > best_score: best_text, best_score = text, score return best_text def best_alignment(got, expected): exp_len = len(expected) if len(got) == exp_len: return got best_start, best_diffs = 0, exp_len + 1 for start in range(max(0, len(got) - exp_len) + 1): cand = got[start:start + exp_len] if len(cand) != exp_len: continue diffs = sum(1 for a, b in zip(cand, expected) if a != b) if diffs < best_diffs: best_diffs = diffs best_start = start return got[best_start:best_start + exp_len] def learn_confusion_map(ocr_results, ground_truths): counts = defaultdict(lambda: defaultdict(int)) for key, expected in ground_truths.items(): got = ocr_results.get(key, "") if not got or got == expected: continue aligned = best_alignment(got, expected) if len(aligned) != len(expected): continue for g, e in zip(aligned, expected): if g != e: counts[g][e] += 1 confusion_map = {} print("\n Learned confusions:") for char in sorted(counts.keys()): wants = sorted(counts[char], key=lambda w: counts[char][w], reverse=True) confusion_map[char] = wants print(f" '{char}' -> {wants} (counts: {dict(counts[char])})") return confusion_map def can_fix(got_str, expected_str, confusion_map, max_errors): if len(got_str) != len(expected_str): return False diffs = [(g, e) for g, e in zip(got_str, expected_str) if g != e] if len(diffs) > max_errors: return False return all( e in confusion_map.get(g, []) or g in confusion_map.get(e, []) for g, e in diffs ) def learn_thresholds(ocr_results, ground_truths, confusion_map): best_correct, best_config = 0, {"max_errors": 2, "window_size": 3} for max_err in [1, 2, 3, 4]: for win in [2, 3, 4, 5]: correct = 0 for key, expected in ground_truths.items(): got = ocr_results.get(key, "") if not got: continue if got == expected or expected in got: correct += 1 continue if abs(len(got) - len(expected)) <= win: aligned = best_alignment(got, expected) if can_fix(aligned, expected, confusion_map, max_err): correct += 1 elif len(got) < len(expected): suffix = expected[-len(got):] if can_fix(got, suffix, confusion_map, 1): correct += 1 if correct > best_correct: best_correct = correct best_config = {"max_errors": max_err, "window_size": win} print(f"\n Best thresholds: {best_config} " f"(estimated correct: {best_correct}/{len(ground_truths)})") return best_config def main(): print("=" * 60) print("LEARNING FROM DATA") print("=" * 60) print("\n[1/3] Scanning barcodes for ground truth...") ground_truths = scan_all_barcodes(BARCODE_DIR) ground_truths = {k: v for k, v in ground_truths.items() if v} print(f" Got {len(ground_truths)} ground truth labels") print("\n[2/3] Running ensemble OCR on all chassis images...") from paddleocr import PaddleOCR ocr = PaddleOCR(use_angle_cls=True, lang='en', use_gpu=False, show_log=False) chassis_files = sorted([ f for f in os.listdir(CHASSIS_DIR) if f.lower().endswith(('.jpg', '.jpeg', '.png')) ]) ocr_results = {} for fname in chassis_files: key = os.path.splitext(fname)[0] path = os.path.join(CHASSIS_DIR, fname) text = run_ocr_ensemble(path, ocr) ocr_results[key] = text expected = ground_truths.get(key, "???") match = "[OK]" if text == expected else "[--]" print(f" {match} {key}: got='{text}' expected='{expected}'") print("\n[3/3] Learning confusion map and thresholds...") confusion_map = learn_confusion_map(ocr_results, ground_truths) thresholds = learn_thresholds(ocr_results, ground_truths, confusion_map) existing = {} if os.path.exists(CONFIG_PATH): with open(CONFIG_PATH) as f: existing = json.load(f) config = { "confusion_map": confusion_map, "preprocessing": existing.get("preprocessing", { "clahe_clip": 3.0, "clahe_grid": 8, "bilateral_d": 9, "bilateral_sigma": 75, "adaptive_blocksize": 21, "adaptive_c": 8, "padding": 20 }), "error_correction": thresholds } with open(CONFIG_PATH, "w") as f: json.dump(config, f, indent=2) print(f"\n[DONE] Config saved -> {CONFIG_PATH}") print("Now run: python evaluate.py") print("=" * 60) if __name__ == "__main__": main()