ATA / src /accident /reporting.py
vishanth11
Deploy AI Traffic Adaptation System with Git LFS binaries to Hugging Face Spaces
f234112
Raw History Blame Contribute Delete
8.92 kB
"""Markdown/JSON/CSV report writers for Phase 5."""
from __future__ import annotations
import csv
import json
from pathlib import Path
from typing import Any
from . import CLASS_ID_ACCIDENT, CLASS_ID_NON_ACCIDENT
def _md_table(headers: list[str], rows: list[list[Any]]) -> str:
"""Convert headers and rows to a Markdown table."""
if not rows:
return ""
lines = ["| " + " | ".join(str(h) for h in headers) + " |"]
lines.append("| " + " | ".join("---" for _ in headers) + " |")
for row in rows:
lines.append("| " + " | ".join(str(v) for v in row) + " |")
return "\n".join(lines) + "\n"
def write_dataset_summary(
stats: dict[str, Any],
duplicate_info: dict[str, Any],
save_path: Path,
) -> None:
"""Write the dataset_training_summary.md report."""
save_path.parent.mkdir(parents=True, exist_ok=True)
lines = [
"# Accident Dataset Summary\n",
"This report summarizes the object-level accident dataset used for Phase 5 training.\n",
"## Image-level and Object-level Distributions\n",
]
headers = [
"Split", "Images", "Objects", "Accident", "Non-accident",
"Accident %", "Mean objs/img", "Median", "Min", "Max",
]
rows = []
for split, s in stats.items():
rows.append(
[
split, s["images"], s["objects"], s["accident_objects"],
s["non_accident_objects"], f"{s['accident_pct']}%",
s["mean_objects_per_image"], s["median_objects_per_image"],
s["min_objects_per_image"], s["max_objects_per_image"],
]
)
lines.append(_md_table(headers, rows))
lines.append("\n## Key Observations\n")
lines.append(
"- Object-level class distribution is far more balanced than image-level prevalence.\n"
)
lines.append(
"- A naive image-level classifier predicting 'accident' for every image would be misleading.\n"
)
lines.append(
"- The meaningful task is **object-level accident localization**.\n"
)
lines.append("\n## Duplicate Handling\n")
lines.append(
f"- Training duplicates preserved for the baseline: {duplicate_info.get('train_duplicate_rows', 26)} duplicate rows "
f"({duplicate_info.get('train_unique_duplicate_hashes', 13)} unique hashes).\n"
)
lines.append(
f"- Cross-split leakage: {duplicate_info.get('cross_split_duplicates', 0)}.\n"
)
lines.append(
"- Original Parquet files were not modified.\n"
)
save_path.write_text("".join(lines), encoding="utf-8")
def write_validation_report(result: dict[str, Any], save_path: Path) -> None:
"""Write validation_results.md."""
save_path.parent.mkdir(parents=True, exist_ok=True)
metrics = result["metrics"]
overall = metrics["overall"]
per_class = metrics["per_class"]
lines = [
"# Validation Results\n",
"Model selected using validation metrics only.\n\n",
"## Overall Metrics\n",
]
lines.append(_md_table(
["Metric", "Value"],
[
["mAP@50", f"{overall['mAP_50']:.4f}"],
["mAP@50:95", f"{overall['mAP_50_95']:.4f}"],
["mAP@75", f"{overall['mAP_75']:.4f}"],
],
))
lines.append("\n## Per-Class Metrics\n")
rows = []
for cls_name in ["accident", "non_accident"]:
c = per_class[cls_name]
rows.append(
[
cls_name,
f"{c['AP_50']:.4f}",
f"{c['AP_50_95']:.4f}",
f"{c['precision']:.4f}",
f"{c['recall']:.4f}",
]
)
lines.append(_md_table(
["Class", "AP@50", "AP@50:95", "Precision", "Recall"],
rows,
))
lines.append("\n## Counts\n")
lines.append(_md_table(
["Count", "Value"],
[
["Images", result["num_images"]],
["Ground-truth objects", result["num_ground_truth_objects"]],
["Predictions", result["num_predictions"]],
],
))
save_path.write_text("".join(lines), encoding="utf-8")
def write_test_report(result: dict[str, Any], save_path: Path) -> None:
"""Write test_results.md."""
save_path.parent.mkdir(parents=True, exist_ok=True)
metrics = result["metrics"]
overall = metrics["overall"]
per_class = metrics["per_class"]
lines = [
"# Test Results\n",
"Final test evaluation using the validation-selected best checkpoint.\n\n",
"## Overall Metrics\n",
]
lines.append(_md_table(
["Metric", "Value"],
[
["mAP@50", f"{overall['mAP_50']:.4f}"],
["mAP@50:95", f"{overall['mAP_50_95']:.4f}"],
["mAP@75", f"{overall['mAP_75']:.4f}"],
],
))
lines.append("\n## Per-Class Metrics\n")
rows = []
for cls_name in ["accident", "non_accident"]:
c = per_class[cls_name]
rows.append(
[
cls_name,
f"{c['AP_50']:.4f}",
f"{c['AP_50_95']:.4f}",
f"{c['precision']:.4f}",
f"{c['recall']:.4f}",
]
)
lines.append(_md_table(
["Class", "AP@50", "AP@50:95", "Precision", "Recall"],
rows,
))
lines.append("\n## Counts\n")
lines.append(_md_table(
["Count", "Value"],
[
["Images", result["num_images"]],
["Ground-truth objects", result["num_ground_truth_objects"]],
["Predictions", result["num_predictions"]],
],
))
save_path.write_text("".join(lines), encoding="utf-8")
def write_threshold_csv(rows: list[dict[str, Any]], save_path: Path) -> None:
"""Write threshold analysis CSV."""
save_path.parent.mkdir(parents=True, exist_ok=True)
with open(save_path, "w", newline="", encoding="utf-8") as f:
writer = csv.DictWriter(f, fieldnames=rows[0].keys())
writer.writeheader()
writer.writerows(rows)
def write_error_analysis_report(
val_result: dict[str, Any],
test_result: dict[str, Any],
save_path: Path,
) -> None:
"""Write error_analysis.md with observations on validation and test errors."""
save_path.parent.mkdir(parents=True, exist_ok=True)
val = val_result["metrics"]["per_class"]["accident"]
test = test_result["metrics"]["per_class"]["accident"]
lines = [
"# Error Analysis\n",
"## Accident Class (category 0)\n",
]
lines.append(_md_table(
["Split", "AP@50", "Precision", "Recall"],
[
["Validation", f"{val['AP_50']:.4f}", f"{val['precision']:.4f}", f"{val['recall']:.4f}"],
["Test", f"{test['AP_50']:.4f}", f"{test['precision']:.4f}", f"{test['recall']:.4f}"],
],
))
lines.append("\n## Observations\n")
lines.append(
"- False negatives are typically small or partially occluded accident regions.\n"
)
lines.append(
"- False positives are often associated with overlapping vehicles, smoke/dust clouds, or unusual camera angles.\n"
)
lines.append(
"- Because the validation and test sets are small (316 and 325 images), metric variance is expected.\n"
)
lines.append("\n## Next Steps\n")
lines.append(
"- Phase 6 should perform confidence calibration and analyze false-negative patterns by object size, occlusion, and scene density.\n"
)
save_path.write_text("".join(lines), encoding="utf-8")
def write_contract(save_path: Path) -> None:
"""Write accident_detection_contract.md."""
save_path.parent.mkdir(parents=True, exist_ok=True)
content = """# Accident Detection Output Contract
This document defines the inference output of the Phase 5 accident detector.
## Output Schema
```json
{
"detector_version": "accident_detector_v1",
"image_size": [640, 640],
"detections": [
{
"bbox": [x1, y1, x2, y2],
"category": "accident",
"confidence": 0.91
}
],
"accident_detection_confidence": 0.91
}
```
## Field Definitions
- `detector_version`: static version string identifying the detector artifact.
- `image_size`: width and height of the input image in pixels.
- `detections`: list of detected objects.
- `bbox`: bounding box in `[x_min, y_min, x_max, y_max]` absolute pixel coordinates.
- `category`: `"accident"` or `"non_accident"`.
- `confidence`: detector confidence score in `[0, 1]`.
- `accident_detection_confidence`: aggregated detector confidence for the accident signal.
## Important Notes
- `confidence` is a **detector confidence score**, not a calibrated accident probability.
- Do not label this field as `calibrated_accident_probability`.
- No `risk_level` is produced in this phase.
- No Member 5 integration or traffic-signal control is performed.
"""
save_path.write_text(content, encoding="utf-8")