import os import numpy as np import matplotlib matplotlib.use("Agg") import matplotlib.pyplot as plt from matplotlib.ticker import MaxNLocator # Formal MIS palette C_PRIMARY = "#1e293b" C_ACCENT = "#334155" C_IN = "#059669" C_OUT = "#dc2626" C_FLOW = "#2563eb" C_CONG = "#d97706" C_CONF = "#7c3aed" C_BAR = "#0f766e" C_GRID = "#e2e8f0" C_BG = "#ffffff" def _style(ax, title, xlabel="", ylabel=""): ax.set_title(title, fontsize=13, fontweight="700", color=C_PRIMARY, pad=14) if xlabel: ax.set_xlabel(xlabel, fontsize=9, fontweight="600", color=C_ACCENT) if ylabel: ax.set_ylabel(ylabel, fontsize=9, fontweight="600", color=C_ACCENT) ax.tick_params(labelsize=8, colors=C_ACCENT) ax.spines["top"].set_visible(False) ax.spines["right"].set_visible(False) ax.spines["left"].set_color(C_GRID) ax.spines["bottom"].set_color(C_GRID) ax.yaxis.grid(True, color=C_GRID, linewidth=0.6, alpha=0.8) ax.set_axisbelow(True) def _save(fig, path, fmt="png"): if fmt == "pdf": path = path.rsplit(".", 1)[0] + ".pdf" fig.savefig(path, dpi=200, bbox_inches="tight", facecolor=C_BG, edgecolor="none") plt.close(fig) def direction_pie(total_in, total_out, out_dir, fmt="png"): if total_in + total_out == 0: return None fig, ax = plt.subplots(figsize=(5, 5), facecolor=C_BG) wedges, texts, autotexts = ax.pie( [total_in, total_out], labels=[f"Incoming ({total_in})", f"Outgoing ({total_out})"], autopct="%1.1f%%", startangle=90, colors=[C_IN, C_OUT], wedgeprops={"edgecolor": C_BG, "linewidth": 2.5 if (total_in > 0 and total_out > 0) else 0}, textprops={"fontsize": 10, "fontweight": "600", "color": C_PRIMARY}, ) for t in autotexts: t.set_fontsize(11) t.set_fontweight("700") t.set_color(C_BG) ax.set_title("Directional Split", fontsize=13, fontweight="700", color=C_PRIMARY, pad=16) total = total_in + total_out ax.text(0, -1.35, f"Total: {total} vehicles", ha="center", fontsize=9, color=C_ACCENT, fontweight="500") path = os.path.join(out_dir, "direction_pie.png") _save(fig, path, fmt) ext = fmt if fmt == "pdf" else "png" return f"direction_pie.{ext}" def flow_histogram(flow_times, out_dir, fmt="png"): if not flow_times: return None fig, ax = plt.subplots(figsize=(9, 4), facecolor=C_BG) bins = min(30, max(5, len(set(flow_times)))) counts, edges, patches = ax.hist(flow_times, bins=bins, color=C_FLOW, alpha=0.85, edgecolor=C_BG, linewidth=0.8) ax.yaxis.set_major_locator(MaxNLocator(integer=True)) _style(ax, "Traffic Flow Over Time", "Time (seconds)", "Vehicles Crossed") peak_idx = int(np.argmax(counts)) peak_time = (edges[peak_idx] + edges[peak_idx + 1]) / 2 ax.text(0.98, 0.95, f"Peak: {int(counts[peak_idx])} vehicles at {peak_time:.1f}s", transform=ax.transAxes, ha="right", va="top", fontsize=8, color=C_ACCENT, bbox=dict(boxstyle="round,pad=0.4", facecolor="#f8fafc", edgecolor=C_GRID)) path = os.path.join(out_dir, "flow_over_time.png") _save(fig, path, fmt) ext = fmt if fmt == "pdf" else "png" return f"flow_over_time.{ext}" def congestion_chart(congestion, out_dir, fmt="png"): if not congestion: return None fig, ax = plt.subplots(figsize=(10, 4), facecolor=C_BG) x = range(len(congestion)) ax.fill_between(x, congestion, alpha=0.08, color=C_CONG) ax.plot(x, congestion, alpha=0.25, color=C_CONG, linewidth=0.5) win = min(30, max(3, len(congestion) // 10)) smooth = np.convolve(congestion, np.ones(win) / win, mode="same") ax.plot(x, smooth, linewidth=2, color=C_CONG) ax.yaxis.set_major_locator(MaxNLocator(integer=True)) _style(ax, "Congestion Index", "Frame", "Active Vehicles") avg = np.mean(congestion) peak = max(congestion) ax.axhline(avg, color=C_ACCENT, linewidth=0.8, linestyle="--", alpha=0.5) ax.text(0.98, 0.95, f"Peak: {peak} | Avg: {avg:.1f}", transform=ax.transAxes, ha="right", va="top", fontsize=8, color=C_ACCENT, bbox=dict(boxstyle="round,pad=0.4", facecolor="#f8fafc", edgecolor=C_GRID)) path = os.path.join(out_dir, "congestion_index.png") _save(fig, path, fmt) ext = fmt if fmt == "pdf" else "png" return f"congestion_index.{ext}" def class_dominance(class_in, class_out, model_classes, out_dir, fmt="png"): totals = {} for k in set(list(class_in.keys()) + list(class_out.keys())): totals[k] = class_in.get(k, 0) + class_out.get(k, 0) if not totals or sum(totals.values()) == 0: return None sorted_items = sorted(totals.items(), key=lambda x: x[1], reverse=True) classes = [model_classes.get(int(i), f"cls_{i}") for i, _ in sorted_items] values = [v for _, v in sorted_items] fig, ax = plt.subplots(figsize=(10, 4.5), facecolor=C_BG) n = len(classes) bar_width = min(0.45, max(0.15, 0.6 / max(n, 1))) bars = ax.bar(range(n), values, width=bar_width, color=C_BAR, edgecolor=C_BG, linewidth=0.5, zorder=3) ax.set_xticks(range(n)) ax.set_xticklabels(classes, rotation=35, ha="right", fontsize=9, fontweight="500") ax.yaxis.set_major_locator(MaxNLocator(integer=True)) for bar, v in zip(bars, values): ax.text(bar.get_x() + bar.get_width() / 2, bar.get_height() + 0.15, str(v), ha="center", va="bottom", fontsize=9, fontweight="700", color=C_PRIMARY) _style(ax, "Class Dominance", "", "Vehicle Count") total = sum(values) ax.text(0.98, 0.95, f"Total: {total} vehicles | {n} classes detected", transform=ax.transAxes, ha="right", va="top", fontsize=8, color=C_ACCENT, bbox=dict(boxstyle="round,pad=0.4", facecolor="#f8fafc", edgecolor=C_GRID)) path = os.path.join(out_dir, "class_dominance.png") _save(fig, path, fmt) ext = fmt if fmt == "pdf" else "png" return f"class_dominance.{ext}" def confidence_dist(conf_scores, out_dir, fmt="png"): if not conf_scores: return None fig, ax = plt.subplots(figsize=(9, 4), facecolor=C_BG) ax.hist(conf_scores, bins=30, color=C_CONF, alpha=0.85, edgecolor=C_BG, linewidth=0.8) ax.yaxis.set_major_locator(MaxNLocator(integer=True)) _style(ax, "Detection Confidence Distribution", "Confidence Score", "Detections") mean_c = np.mean(conf_scores) median_c = np.median(conf_scores) ax.axvline(mean_c, color=C_PRIMARY, linewidth=1, linestyle="--", alpha=0.6) ax.text(0.98, 0.95, f"Mean: {mean_c:.3f} | Median: {median_c:.3f} | N={len(conf_scores)}", transform=ax.transAxes, ha="right", va="top", fontsize=8, color=C_ACCENT, bbox=dict(boxstyle="round,pad=0.4", facecolor="#f8fafc", edgecolor=C_GRID)) path = os.path.join(out_dir, "confidence_dist.png") _save(fig, path, fmt) ext = fmt if fmt == "pdf" else "png" return f"confidence_dist.{ext}" def generate_all(data, model_classes, out_dir, report_format="png"): os.makedirs(out_dir, exist_ok=True) plt.rcParams.update({ "font.family": "sans-serif", "font.sans-serif": ["DejaVu Sans", "Arial", "Helvetica"], "axes.unicode_minus": False, }) total_in = sum(data["class_in"].values()) total_out = sum(data["class_out"].values()) fmt = report_format files = [] for fn in [ lambda: direction_pie(total_in, total_out, out_dir, fmt), lambda: flow_histogram(data.get("flow_times", []), out_dir, fmt), lambda: congestion_chart(data.get("congestion", []), out_dir, fmt), lambda: class_dominance(data["class_in"], data["class_out"], model_classes, out_dir, fmt), lambda: confidence_dist(data.get("conf_scores", []), out_dir, fmt), ]: name = fn() if name: files.append(name) return files