diff --git a/app.py b/app.py index 8fa6112..588c626 100644 --- a/app.py +++ b/app.py @@ -384,57 +384,150 @@ def render_ddf_views(fixed_xyz: np.ndarray, ddf_xyz: np.ndarray) -> None: ) +METRIC_SPECS = [ + ("NCC", "before_ncc", "after_ncc", "越高越好", True), + ("MSE", "before_mse", "after_mse", "越低越好", False), + ("MAE", "before_mae", "after_mae", "越低越好", False), +] + + +def metric_improvement(before: float, after: float, higher_is_better: bool) -> float: + return after - before if higher_is_better else before - after + + +def format_metric(value: float) -> str: + return f"{float(value):.6f}" + + +def build_metric_table(metrics: Dict[str, float]) -> List[Dict[str, str]]: + rows = [] + for label, before_key, after_key, direction, higher_is_better in METRIC_SPECS: + before = float(metrics[before_key]) + after = float(metrics[after_key]) + improvement = metric_improvement(before, after, higher_is_better) + improvement_rate = improvement / (abs(before) + 1e-8) * 100.0 + rows.append( + { + "指标": label, + "方向": direction, + "配准前": format_metric(before), + "配准后": format_metric(after), + "改善量": f"{improvement:+.6f}", + "改善率": f"{improvement_rate:+.4f}%", + } + ) + return rows + + +def render_metric_value_panels(metrics: Dict[str, float]) -> None: + fig, axes = plt.subplots(1, 3, figsize=(11.5, 3.1), dpi=130) + for ax, (label, before_key, after_key, direction, higher_is_better) in zip(axes, METRIC_SPECS): + before = float(metrics[before_key]) + after = float(metrics[after_key]) + improvement = metric_improvement(before, after, higher_is_better) + values = [before, after] + x = np.arange(2) + + ax.bar(x, values, width=0.5, color=["#64748b", "#0f766e"]) + ax.plot(x, values, color="#111827", linewidth=1.2, marker="o", markersize=4) + ax.set_xticks(x, ["配准前", "配准后"]) + ax.set_title(f"{label}({direction})") + ymin = min(0.0, min(values) * 1.18) + ymax = max(0.0, max(values) * 1.18, 1e-5) + ax.set_ylim(ymin, ymax) + ax.grid(axis="y", alpha=0.22) + + for index, value in enumerate(values): + ax.text(index, value, format_metric(value), ha="center", va="bottom", fontsize=8) + ax.text(0.5, max(values) * 1.06, f"改善 {improvement:+.6f}", ha="center", fontsize=8, color="#374151") + + fig.tight_layout() + st.pyplot(fig, width="stretch") + plt.close(fig) + + def render_metric_charts(fixed_xyz: np.ndarray, moving_xyz: np.ndarray, warped_xyz: np.ndarray, ddf_xyz: np.ndarray | None) -> None: fixed_xyz, moving_xyz, warped_xyz = crop_to_common_shape(fixed_xyz, moving_xyz, warped_xyz) metrics = registration_metrics(fixed_xyz, moving_xyz, warped_xyz) + metric_rows = build_metric_table(metrics) c1, c2, c3 = st.columns(3) - c1.metric("NCC", f"{metrics['after_ncc']:.4f}", delta=f"{metrics['ncc_improvement']:+.4f}") + c1.metric("NCC", format_metric(metrics["after_ncc"]), delta=f"{metrics['ncc_improvement']:+.6f}") c2.metric( "MSE", - f"{metrics['after_mse']:.5f}", - delta=f"{metrics['after_mse'] - metrics['before_mse']:+.5f}", + format_metric(metrics["after_mse"]), + delta=f"{metrics['after_mse'] - metrics['before_mse']:+.6f}", delta_color="inverse", ) c3.metric( "MAE", - f"{metrics['after_mae']:.5f}", - delta=f"{metrics['after_mae'] - metrics['before_mae']:+.5f}", + format_metric(metrics["after_mae"]), + delta=f"{metrics['after_mae'] - metrics['before_mae']:+.6f}", delta_color="inverse", ) - labels = ["NCC", "MSE", "MAE"] - before = [metrics["before_ncc"], metrics["before_mse"], metrics["before_mae"]] - after = [metrics["after_ncc"], metrics["after_mse"], metrics["after_mae"]] + max_improvement_rate = max(abs(float(row["改善率"].rstrip("%"))) for row in metric_rows) + if max_improvement_rate < 0.1: + st.info("当前配准前后指标变化低于 0.1%,曲线和柱子高度接近是正常的;这更像是 smoke 权重的验证结果,不是充分训练后的配准效果。") - fig, axes = plt.subplots(1, 2, figsize=(10, 3.6), dpi=130) - x = np.arange(len(labels)) - width = 0.36 - axes[0].bar(x - width / 2, before, width, label="配准前", color="#64748b") - axes[0].bar(x + width / 2, after, width, label="配准后", color="#0f766e") - axes[0].set_xticks(x, labels) - axes[0].set_title("配准前后指标对比") + st.caption("量化结果由当前加载的 Fixed、Moving、Warped 体数据现场计算;逐切片曲线未平滑、未随机采样。") + st.table(metric_rows) + render_metric_value_panels(metrics) + + controls = st.columns(2) + with controls[0]: + plane_label = st.selectbox("逐切片平面", ["轴状面", "冠状面", "矢状面"]) + with controls[1]: + metric_label = st.selectbox("逐切片指标", ["MSE", "MAE", "NCC"]) + + axis = {"矢状面": 0, "冠状面": 1, "轴状面": 2}[plane_label] + metric_key = metric_label.lower() + higher_is_better = metric_key == "ncc" + curve = slice_metric_curve(fixed_xyz, moving_xyz, warped_xyz, axis=axis, metric=metric_key) + slice_index = np.asarray(curve["slice_index"], dtype=np.int32) + before_curve = np.asarray(curve[f"before_{metric_key}"], dtype=np.float32) + after_curve = np.asarray(curve[f"after_{metric_key}"], dtype=np.float32) + improvement_curve = after_curve - before_curve if higher_is_better else before_curve - after_curve + + fig, axes = plt.subplots( + 2, + 1, + figsize=(10.5, 5.4), + dpi=130, + sharex=True, + gridspec_kw={"height_ratios": [2.0, 1.0]}, + ) + axes[0].plot(slice_index, before_curve, label=f"配准前 {metric_label}", color="#64748b", linewidth=1.5) + axes[0].plot(slice_index, after_curve, label=f"配准后 {metric_label}", color="#0f766e", linewidth=1.5) + axes[0].set_title(f"{plane_label}逐切片{metric_label}") + axes[0].set_ylabel(metric_label) + axes[0].grid(alpha=0.22) axes[0].legend(frameon=False) - axes[0].grid(axis="y", alpha=0.22) - curve = slice_metric_curve(fixed_xyz, moving_xyz, warped_xyz, axis=2) - axes[1].plot(curve["slice_index"], curve["before_mse"], label="配准前 MSE", color="#64748b", linewidth=1.8) - axes[1].plot(curve["slice_index"], curve["after_mse"], label="配准后 MSE", color="#b45309", linewidth=1.8) - axes[1].set_title("轴状面逐切片误差") - axes[1].set_xlabel("Slice") + axes[1].axhline(0.0, color="#9ca3af", linewidth=0.9) + axes[1].plot(slice_index, improvement_curve, color="#b45309", linewidth=1.4) + axes[1].set_title(f"{metric_label}逐切片改善量") + axes[1].set_xlabel("切片序号") + axes[1].set_ylabel("改善量") axes[1].grid(alpha=0.22) - axes[1].legend(frameon=False) st.pyplot(fig, width="stretch") plt.close(fig) if ddf_xyz is not None: mag = ddf_magnitude(ddf_xyz) + values = mag[np.isfinite(mag)] + if values.size == 0: + return fig2, ax = plt.subplots(figsize=(10, 3.2), dpi=130) - ax.hist(mag.ravel(), bins=80, color="#7c3f00", alpha=0.82) + ax.hist(values, bins=80, color="#7c3f00", alpha=0.82) + for percentile, color in [(50, "#111827"), (95, "#0f766e"), (99, "#dc2626")]: + marker = float(np.percentile(values, percentile)) + ax.axvline(marker, color=color, linewidth=1.2, label=f"P{percentile}={format_mm(marker)} mm") ax.set_title("DDF 位移强度分布") ax.set_xlabel("mm") - ax.set_ylabel("Voxel count") + ax.set_ylabel("体素数") ax.grid(axis="y", alpha=0.2) + ax.legend(frameon=False) st.pyplot(fig2, width="stretch") plt.close(fig2) diff --git a/metrics.py b/metrics.py index 9e8c94a..e829b32 100644 --- a/metrics.py +++ b/metrics.py @@ -86,10 +86,20 @@ def slice_metric_curve( moving: np.ndarray, warped: np.ndarray, axis: int = 2, + metric: str = "mse", ) -> Dict[str, Iterable[float]]: - """逐切片计算 MSE,适合生成“配准前后误差曲线”。""" + """逐切片计算配准前后指标曲线。""" fixed, moving, warped = crop_to_common_shape(fixed, moving, warped) + metric_funcs = { + "mse": mse, + "mae": mae, + "ncc": global_ncc, + } + if metric not in metric_funcs: + raise ValueError(f"不支持的逐切片指标: {metric}") + metric_fn = metric_funcs[metric] + before = [] after = [] @@ -97,13 +107,13 @@ def slice_metric_curve( selector = [slice(None)] * 3 selector[axis] = index selector = tuple(selector) - before.append(mse(fixed[selector], moving[selector])) - after.append(mse(fixed[selector], warped[selector])) + before.append(metric_fn(fixed[selector], moving[selector])) + after.append(metric_fn(fixed[selector], warped[selector])) return { "slice_index": list(range(fixed.shape[axis])), - "before_mse": before, - "after_mse": after, + f"before_{metric}": before, + f"after_{metric}": after, } @@ -120,4 +130,3 @@ def ddf_summary(ddf_xyz: np.ndarray) -> Dict[str, float]: "ddf_p95": float(np.percentile(mag, 95)), "ddf_max": float(np.max(mag)), } -