feat: render-column (GT top + algos stacked), fix hour60 parsing, vivid anomaly markers

This commit is contained in:
张宗平
2026-06-11 22:11:48 +08:00
parent 7866a6b629
commit 404555ac0e
5 changed files with 198 additions and 41 deletions
+138
View File
@@ -233,6 +233,144 @@ def render_algo(
return output_path
def render_column(
timestamps: list[int],
values: list[float],
gt_labels: list[int],
det_results: dict[str, dict],
output_path: str,
title: str = "",
) -> str:
"""单列纵向对比图:GT 顶部 + 各算法向下排布,共享 x 轴。
异常区域用鲜明颜色 + 竖线边框标记。
Args:
timestamps: 毫秒时间戳列表
values: 数值列表
gt_labels: GT 标签(0/1
det_results: {algo: {"windows": [[start,end],...], "error": str|None}}
output_path: PNG 输出路径
title: 图表总标题
Returns:
output_path
"""
font_name = _find_cjk_font()
plt.rcParams["font.family"] = font_name
plt.rcParams["font.size"] = 9
# 过滤有效算法
valid_algos = [(a, r) for a, r in det_results.items() if not r.get("error")]
n_panels = 1 + len(valid_algos) # GT + 各算法
fig, axes = plt.subplots(n_panels, 1, sharex=True, figsize=(16, 3 * n_panels + 1))
if n_panels == 1:
axes = [axes]
t0 = timestamps[0]
hours = [(t - t0) / 3_600_000 for t in timestamps]
# ── 提取 GT windows ──
gt_windows_h = []
in_anom = False
anom_start = None
for i, label in enumerate(gt_labels):
if label == 1 and not in_anom:
in_anom = True
anom_start = hours[i]
elif label == 0 and in_anom:
in_anom = False
gt_windows_h.append((anom_start, hours[i]))
if in_anom:
gt_windows_h.append((anom_start, hours[-1]))
# ── Panel 0: Ground Truth ──
ax = axes[0]
has_gt = any(l == 1 for l in gt_labels)
ax.plot(hours, values, color="#333333", linewidth=0.8, alpha=0.9)
if has_gt:
for start_h, end_h in gt_windows_h:
ax.axvspan(start_h, end_h, alpha=0.35, color="#FF4444",
edgecolor="#CC0000", linewidth=1.5)
# 异常点散点标记
anom_hours = [hours[i] for i, l in enumerate(gt_labels) if l == 1]
anom_vals = [values[i] for i, l in enumerate(gt_labels) if l == 1]
ax.scatter(anom_hours, anom_vals, color="red", s=8, zorder=5, label="GT 异常点")
ax.legend(loc="upper right", fontsize=7)
ax.set_ylabel("Value", fontsize=9)
gt_title = "Ground Truth"
if has_gt:
gt_title += f"{len(gt_windows_h)} 个异常区间)"
else:
gt_title += "(无异常标注)"
ax.set_title(gt_title, fontsize=10, fontweight="bold")
ax.grid(True, alpha=0.3)
# ── Panels 1..N: 各算法 ──
for idx, (algo, result) in enumerate(valid_algos):
ax = axes[idx + 1]
color = ALGO_COLORS.get(algo, "#888888")
# 原始序列(浅色背景)
ax.plot(hours, values, color="#AAAAAA", linewidth=0.5, alpha=0.6)
# GT 红色竖线边框参考
for start_h, end_h in gt_windows_h:
ax.axvspan(start_h, end_h, alpha=0.12, color="#FF4444",
linestyle="--", edgecolor="#FF6666", linewidth=0.8)
# 算法检测窗口 — 鲜明颜色 + 边框
windows = result.get("windows", [])
det_hours_list = []
for wstart, wend in windows:
ws_h = (wstart - t0) / 3_600_000
we_h = (wend - t0) / 3_600_000
ax.axvspan(ws_h, we_h, alpha=0.4, color=color,
edgecolor=color, linewidth=1.5)
det_hours_list.append((ws_h, we_h))
# 在检测区间内的高亮数据点
det_points_h = set()
for ws_h, we_h in det_hours_list:
for i, h in enumerate(hours):
if ws_h <= h <= we_h:
det_points_h.add(i)
if det_points_h:
dp_hours = [hours[i] for i in det_points_h]
dp_vals = [values[i] for i in det_points_h]
ax.scatter(dp_hours, dp_vals, color=color, s=6, zorder=5, alpha=0.7)
# IoU 计算
iou = _compute_iou(gt_windows_h, det_hours_list)
stats = f"检测窗口: {len(windows)} | IoU: {iou:.3f}"
# 图例方块
from matplotlib.patches import Patch
handles = [Patch(color=color, alpha=0.6, label=algo)]
if has_gt:
handles.insert(0, Patch(color="#FF4444", alpha=0.3, label="GT"))
ax.legend(handles=handles, loc="upper right", fontsize=7)
ax.set_title(f"{algo}{stats}", fontsize=10)
ax.set_ylabel("Value", fontsize=9)
ax.grid(True, alpha=0.3)
# 底部 x 轴标签
axes[-1].set_xlabel("Time (hours)", fontsize=9)
# 总标题
if title:
fig.suptitle(title, fontsize=13, fontweight="bold", y=0.995)
plt.tight_layout(rect=[0, 0, 1, 0.98] if title else [0, 0, 1, 1])
Path(output_path).parent.mkdir(parents=True, exist_ok=True)
fig.savefig(output_path, dpi=150, bbox_inches="tight")
plt.close(fig)
logger.info("render_column saved: %s (%d panels)", output_path, n_panels)
return output_path
def montage_pngs(
image_paths: list[str],
output_path: str,