Make quantitative charts data-auditable

This commit is contained in:
admin
2026-06-03 10:05:07 +08:00
parent 20a033cedd
commit 689660c8bc
2 changed files with 132 additions and 30 deletions

141
app.py
View File

@@ -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: 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) 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) metrics = registration_metrics(fixed_xyz, moving_xyz, warped_xyz)
metric_rows = build_metric_table(metrics)
c1, c2, c3 = st.columns(3) 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( c2.metric(
"MSE", "MSE",
f"{metrics['after_mse']:.5f}", format_metric(metrics["after_mse"]),
delta=f"{metrics['after_mse'] - metrics['before_mse']:+.5f}", delta=f"{metrics['after_mse'] - metrics['before_mse']:+.6f}",
delta_color="inverse", delta_color="inverse",
) )
c3.metric( c3.metric(
"MAE", "MAE",
f"{metrics['after_mae']:.5f}", format_metric(metrics["after_mae"]),
delta=f"{metrics['after_mae'] - metrics['before_mae']:+.5f}", delta=f"{metrics['after_mae'] - metrics['before_mae']:+.6f}",
delta_color="inverse", delta_color="inverse",
) )
labels = ["NCC", "MSE", "MAE"] max_improvement_rate = max(abs(float(row["改善率"].rstrip("%"))) for row in metric_rows)
before = [metrics["before_ncc"], metrics["before_mse"], metrics["before_mae"]] if max_improvement_rate < 0.1:
after = [metrics["after_ncc"], metrics["after_mse"], metrics["after_mae"]] st.info("当前配准前后指标变化低于 0.1%,曲线和柱子高度接近是正常的;这更像是 smoke 权重的验证结果,不是充分训练后的配准效果。")
fig, axes = plt.subplots(1, 2, figsize=(10, 3.6), dpi=130) st.caption("量化结果由当前加载的 Fixed、Moving、Warped 体数据现场计算;逐切片曲线未平滑、未随机采样。")
x = np.arange(len(labels)) st.table(metric_rows)
width = 0.36 render_metric_value_panels(metrics)
axes[0].bar(x - width / 2, before, width, label="配准前", color="#64748b")
axes[0].bar(x + width / 2, after, width, label="配准后", color="#0f766e") controls = st.columns(2)
axes[0].set_xticks(x, labels) with controls[0]:
axes[0].set_title("配准前后指标对比") 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].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].axhline(0.0, color="#9ca3af", linewidth=0.9)
axes[1].plot(curve["slice_index"], curve["before_mse"], label="配准前 MSE", color="#64748b", linewidth=1.8) axes[1].plot(slice_index, improvement_curve, color="#b45309", linewidth=1.4)
axes[1].plot(curve["slice_index"], curve["after_mse"], label="配准后 MSE", color="#b45309", linewidth=1.8) axes[1].set_title(f"{metric_label}逐切片改善量")
axes[1].set_title("轴状面逐切片误差") axes[1].set_xlabel("切片序号")
axes[1].set_xlabel("Slice") axes[1].set_ylabel("改善量")
axes[1].grid(alpha=0.22) axes[1].grid(alpha=0.22)
axes[1].legend(frameon=False)
st.pyplot(fig, width="stretch") st.pyplot(fig, width="stretch")
plt.close(fig) plt.close(fig)
if ddf_xyz is not None: if ddf_xyz is not None:
mag = ddf_magnitude(ddf_xyz) 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) 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_title("DDF 位移强度分布")
ax.set_xlabel("mm") ax.set_xlabel("mm")
ax.set_ylabel("Voxel count") ax.set_ylabel("体素数")
ax.grid(axis="y", alpha=0.2) ax.grid(axis="y", alpha=0.2)
ax.legend(frameon=False)
st.pyplot(fig2, width="stretch") st.pyplot(fig2, width="stretch")
plt.close(fig2) plt.close(fig2)

View File

@@ -86,10 +86,20 @@ def slice_metric_curve(
moving: np.ndarray, moving: np.ndarray,
warped: np.ndarray, warped: np.ndarray,
axis: int = 2, axis: int = 2,
metric: str = "mse",
) -> Dict[str, Iterable[float]]: ) -> Dict[str, Iterable[float]]:
"""逐切片计算 MSE适合生成“配准前后误差曲线""" """逐切片计算配准前后指标曲线。"""
fixed, moving, warped = crop_to_common_shape(fixed, moving, warped) 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 = [] before = []
after = [] after = []
@@ -97,13 +107,13 @@ def slice_metric_curve(
selector = [slice(None)] * 3 selector = [slice(None)] * 3
selector[axis] = index selector[axis] = index
selector = tuple(selector) selector = tuple(selector)
before.append(mse(fixed[selector], moving[selector])) before.append(metric_fn(fixed[selector], moving[selector]))
after.append(mse(fixed[selector], warped[selector])) after.append(metric_fn(fixed[selector], warped[selector]))
return { return {
"slice_index": list(range(fixed.shape[axis])), "slice_index": list(range(fixed.shape[axis])),
"before_mse": before, f"before_{metric}": before,
"after_mse": after, 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_p95": float(np.percentile(mag, 95)),
"ddf_max": float(np.max(mag)), "ddf_max": float(np.max(mag)),
} }