MCPcopy Create free account
hub / github.com/AggieSportsAnalytics/CourtCheck / build_report

Function build_report

backend/eval/run_eval.py:93–215  ·  view source on GitHub ↗
(video_path: Path, gt: dict, pred: dict)

Source from the content-addressed store, hash-verified

91
92
93def build_report(video_path: Path, gt: dict, pred: dict) -> str:
94 correct, misclass, miss_strokes, fp_strokes = match_strokes(gt["strokes"], pred["strokes"])
95 bounce_matches, miss_bounces, fp_bounces = match_bounces(gt["bounces"], pred["bounces"])
96
97 n_gt = len(gt["strokes"])
98 n_pred = len(pred["strokes"])
99 n_matched = len(correct) + len(misclass)
100 det_recall = n_matched / n_gt if n_gt else 0
101 det_precision = n_matched / n_pred if n_pred else 0
102 det_f1 = f1(det_precision, det_recall)
103 cls_acc = len(correct) / n_matched if n_matched else 0
104
105 classes = sorted({s["type"] for s in gt["strokes"]} | {normalize_pred_label(s["label"]) for s in pred["strokes"]})
106 confusion = {c: {c2: 0 for c2 in classes} for c in classes}
107 for record in correct + misclass:
108 gt_c = record["gt"]["type"]
109 pred_c = normalize_pred_label(record["pred"]["label"])
110 confusion[gt_c][pred_c] = confusion[gt_c].get(pred_c, 0) + 1
111
112 per_class = {}
113 for c in classes:
114 tp = confusion[c].get(c, 0)
115 fn = sum(v for k, v in confusion[c].items() if k != c)
116 fp = sum(confusion[c2].get(c, 0) for c2 in classes if c2 != c)
117 rec = tp / (tp + fn) if (tp + fn) else 0
118 prec = tp / (tp + fp) if (tp + fp) else 0
119 per_class[c] = {"tp": tp, "fn": fn, "fp": fp, "precision": prec, "recall": rec}
120
121 bn_gt = len(gt["bounces"])
122 bn_pred = len(pred["bounces"])
123 bn_matched = len(bounce_matches)
124 b_recall = bn_matched / bn_gt if bn_gt else 0
125 b_precision = bn_matched / bn_pred if bn_pred else 0
126 b_f1 = f1(b_precision, b_recall)
127
128 in_correct = sum(1 for m in bounce_matches if m["gt"]["in_bounds"] == m["pred"]["in_bounds"])
129 in_acc = in_correct / bn_matched if bn_matched else 0
130
131 fps = gt.get("fps", pred.get("fps", 30.0))
132
133 def t(frame: int) -> str:
134 return f"{frame / fps:.2f}s"
135
136 L = []
137 L.append(f"# Eval Report")
138 L.append("")
139 L.append(f"**Video:** `{video_path}`")
140 L.append(f"**FPS:** {fps} **Frames:** {gt.get('total_frames')}")
141 L.append(f"**Tolerances:** stroke = ±{STROKE_TOLERANCE_FRAMES} frames, bounce = ±{BOUNCE_TOLERANCE_FRAMES} frames")
142 L.append("")
143 L.append("## Headline")
144 L.append("")
145 L.append(f"- Strokes: P={det_precision:.2f} R={det_recall:.2f} F1={det_f1:.2f} Class-acc={cls_acc:.2f} ({len(correct)}/{n_matched} matched events classified correctly)")
146 L.append(f"- Bounces: P={b_precision:.2f} R={b_recall:.2f} F1={b_f1:.2f} In/Out-acc={in_acc:.2f}")
147 L.append("")
148 L.append("## Strokes")
149 L.append("")
150 L.append(f"- GT events: {n_gt} Pred events: {n_pred} Matched: {n_matched}")

Callers 1

mainFunction · 0.85

Calls 5

match_strokesFunction · 0.85
match_bouncesFunction · 0.85
f1Function · 0.85
normalize_pred_labelFunction · 0.85
tFunction · 0.85

Tested by

no test coverage detected