Make quantitative charts data-auditable
This commit is contained in:
141
app.py
141
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)
|
||||
|
||||
|
||||
21
metrics.py
21
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)),
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user