File size: 8,010 Bytes
f211247
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
#!/usr/bin/env python3
"""5-fold WBF ensemble evaluation.

Strategy: each fold model evaluated on ITS OWN val set (out-of-fold prediction
is what CV gives us). For ensemble: each test image is predicted by ALL fold
models, predictions fused via WBF, mAP computed against ground truth.

For our 5-fold CV, we use the union of all val sets and predict each image
with all 5 models then WBF.
"""
import os, sys, json, time
from pathlib import Path
import numpy as np
import torch

ROOT = Path('/arf/scratch/stakan/hitit-proje')
FOLD_ROOT = ROOT / 'datasets/ready/detection_tablets'
WEIGHTS_ROOT = ROOT / 'runs/detect/hitit_ocr/runs/h100'

def log(m): print(f"[{time.strftime('%H:%M:%S')}] {m}", flush=True)

def main():
    from ultralytics import YOLO
    from ensemble_boxes import weighted_boxes_fusion
    from PIL import Image

    # Tüm val setlerini birleştir (full dataset, leakage olmadan: her image kendi fold'unun val'ında)
    all_imgs = []
    img_to_fold = {}
    for fold in range(5):
        for line in (FOLD_ROOT / f'fold_{fold}/val.txt').read_text().splitlines():
            line = line.strip()
            if not line: continue
            all_imgs.append(line)
            img_to_fold[line] = fold
    log(f"Total eval images: {len(all_imgs)}")

    # Modelleri yükle
    models = {}
    for fold in range(5):
        ckpt = WEIGHTS_ROOT / f'yolo_fold{fold}/weights/best.pt'
        if not ckpt.exists():
            log(f"WARN: {ckpt} eksik")
            continue
        models[fold] = YOLO(str(ckpt))
        log(f"Loaded fold {fold}: {ckpt}")

    # Her image için predict (own-fold model ve ensemble karşılaştırması)
    own_preds = {}      # img -> (boxes, scores)
    ensemble_preds = {} # img -> (boxes, scores)
    iou_thr = 0.55
    skip_box_thr = 0.001

    log("Predicting...")
    for idx, img_path in enumerate(all_imgs):
        if idx % 100 == 0: log(f"  {idx}/{len(all_imgs)}")
        own_fold = img_to_fold[img_path]
        # Image size for normalization
        try:
            with Image.open(img_path) as im:
                W, H = im.size
        except Exception:
            continue

        boxes_list, scores_list, labels_list = [], [], []
        for fold, model in models.items():
            try:
                r = model.predict(img_path, conf=0.001, iou=0.7, max_det=2000,
                                  imgsz=1280, verbose=False, device=0 if torch.cuda.is_available() else 'cpu')[0]
            except Exception as e:
                log(f"  predict skip {Path(img_path).name}: {e}"); break
            if r.boxes is None or len(r.boxes) == 0: continue
            xyxy = r.boxes.xyxy.cpu().numpy() / np.array([W, H, W, H])  # normalize 0-1
            xyxy = np.clip(xyxy, 0, 1)
            scores = r.boxes.conf.cpu().numpy().tolist()
            labels = [0] * len(scores)
            boxes_list.append(xyxy.tolist())
            scores_list.append(scores)
            labels_list.append(labels)
            if fold == own_fold:
                own_preds[img_path] = (xyxy, np.array(scores))

        if not boxes_list:
            ensemble_preds[img_path] = (np.zeros((0, 4)), np.zeros(0))
            continue
        try:
            wbf_boxes, wbf_scores, wbf_labels = weighted_boxes_fusion(
                boxes_list, scores_list, labels_list,
                weights=None, iou_thr=iou_thr, skip_box_thr=skip_box_thr)
        except Exception as e:
            log(f"  WBF skip {Path(img_path).name}: {e}")
            ensemble_preds[img_path] = (np.zeros((0, 4)), np.zeros(0))
            continue
        if len(wbf_boxes) == 0:
            ensemble_preds[img_path] = (np.zeros((0, 4)), np.zeros(0))
            continue
        wbf_boxes_px = np.asarray(wbf_boxes) * np.array([W, H, W, H])
        ensemble_preds[img_path] = (wbf_boxes_px, np.asarray(wbf_scores))

    # GT loader (YOLO normalized format)
    LBL_DIR = ROOT / 'hitit_ocr/data/detection/labels/all'
    def load_gt(img_path):
        stem = Path(img_path).stem
        lbl = LBL_DIR / f'{stem}.txt'
        if not lbl.exists(): return np.zeros((0, 4))
        boxes = []
        with Image.open(img_path) as im: W, H = im.size
        for ln in lbl.read_text().splitlines():
            p = ln.split()
            if len(p) < 5: continue
            cx, cy, w, h = float(p[1]), float(p[2]), float(p[3]), float(p[4])
            x1 = (cx - w/2) * W; y1 = (cy - h/2) * H
            x2 = (cx + w/2) * W; y2 = (cy + h/2) * H
            boxes.append([x1, y1, x2, y2])
        return np.array(boxes) if boxes else np.zeros((0, 4))

    def iou_matrix(a, b):
        if len(a) == 0 or len(b) == 0: return np.zeros((len(a), len(b)))
        ax1, ay1, ax2, ay2 = a[:,0:1], a[:,1:2], a[:,2:3], a[:,3:4]
        bx1, by1, bx2, by2 = b[:,0], b[:,1], b[:,2], b[:,3]
        inter_x1 = np.maximum(ax1, bx1); inter_y1 = np.maximum(ay1, by1)
        inter_x2 = np.minimum(ax2, bx2); inter_y2 = np.minimum(ay2, by2)
        iw = np.clip(inter_x2-inter_x1, 0, None); ih = np.clip(inter_y2-inter_y1, 0, None)
        inter = iw * ih
        a_area = (ax2-ax1) * (ay2-ay1); b_area = (bx2-bx1) * (by2-by1)
        union = a_area + b_area - inter
        return inter / np.clip(union, 1e-9, None)

    def compute_map(pred_dict, iou_threshs=None):
        if iou_threshs is None:
            iou_threshs = np.arange(0.5, 1.0, 0.05)
        # Per-class (single class). Collect TP/FP at each conf.
        per_iou_aps = []
        for iou_thr in iou_threshs:
            scores_all = []; tp_all = []; n_gt = 0
            for img_path, (boxes, scores) in pred_dict.items():
                gt = load_gt(img_path); n_gt += len(gt)
                if len(boxes) == 0: continue
                ious = iou_matrix(boxes, gt)  # (P, G)
                # sort preds by score desc
                order = np.argsort(-scores)
                used_gt = np.zeros(len(gt), dtype=bool)
                for i in order:
                    scores_all.append(scores[i])
                    if len(gt) == 0:
                        tp_all.append(0); continue
                    j = np.argmax(ious[i])
                    if ious[i, j] >= iou_thr and not used_gt[j]:
                        tp_all.append(1); used_gt[j] = True
                    else:
                        tp_all.append(0)
            if not scores_all or n_gt == 0:
                per_iou_aps.append(0.0); continue
            scores_all = np.array(scores_all); tp_all = np.array(tp_all)
            order = np.argsort(-scores_all)
            tp_sorted = tp_all[order]
            cum_tp = np.cumsum(tp_sorted); cum_fp = np.cumsum(1 - tp_sorted)
            recall = cum_tp / n_gt
            precision = cum_tp / np.clip(cum_tp + cum_fp, 1, None)
            # 101-point AP
            ap = 0.0
            for r in np.linspace(0, 1, 101):
                p = precision[recall >= r].max() if (recall >= r).any() else 0.0
                ap += p
            ap /= 101
            per_iou_aps.append(ap)
        return per_iou_aps

    log("Computing mAP for own-fold predictions (baseline)...")
    own_aps = compute_map(own_preds)
    log(f"  own  mAP50={own_aps[0]:.4f}  mAP50-95={np.mean(own_aps):.4f}")

    log("Computing mAP for WBF ensemble...")
    ens_aps = compute_map(ensemble_preds)
    log(f"  WBF  mAP50={ens_aps[0]:.4f}  mAP50-95={np.mean(ens_aps):.4f}")

    out = {
        'n_eval_images': len(all_imgs),
        'n_own_pred_images': len(own_preds),
        'n_ensemble_pred_images': len(ensemble_preds),
        'own_fold': {'mAP50': float(own_aps[0]), 'mAP50_95': float(np.mean(own_aps)),
                     'per_iou': [float(x) for x in own_aps]},
        'wbf_ensemble': {'mAP50': float(ens_aps[0]), 'mAP50_95': float(np.mean(ens_aps)),
                         'per_iou': [float(x) for x in ens_aps]},
    }
    out_path = WEIGHTS_ROOT.parent.parent.parent / 'wbf_ensemble_eval.json'
    out_path.write_text(json.dumps(out, indent=2))
    log(f"Wrote {out_path}")

if __name__ == '__main__':
    main()