Add official VoxelMorph CT registration pipeline

This commit is contained in:
admin
2026-06-03 00:30:32 +08:00
parent e8d8f2c468
commit 2dba05ae4a
16 changed files with 2130 additions and 0 deletions

123
metrics.py Normal file
View File

@@ -0,0 +1,123 @@
"""配准质量评估工具。
这里刻意只依赖 NumPy便于在训练、推理和 Streamlit 三处复用。
"""
from __future__ import annotations
from typing import Dict, Iterable, Tuple
import numpy as np
def crop_to_common_shape(*arrays: np.ndarray) -> Tuple[np.ndarray, ...]:
"""把多个 3D/4D 数组中心裁剪到共同尺寸,避免形状不一致导致评估失败。"""
if not arrays:
return tuple()
spatial_shapes = [arr.shape[:3] for arr in arrays]
common = tuple(min(shape[axis] for shape in spatial_shapes) for axis in range(3))
cropped = []
for arr in arrays:
slices = []
for axis, target in enumerate(common):
start = max((arr.shape[axis] - target) // 2, 0)
slices.append(slice(start, start + target))
if arr.ndim > 3:
slices.append(slice(None))
cropped.append(arr[tuple(slices)])
return tuple(cropped)
def global_ncc(a: np.ndarray, b: np.ndarray, eps: float = 1e-8) -> float:
"""计算全局 NCC。训练时使用局部 NCC这里用于快速量化展示。"""
a = np.asarray(a, dtype=np.float32)
b = np.asarray(b, dtype=np.float32)
a = a - float(np.mean(a))
b = b - float(np.mean(b))
denom = float(np.sqrt(np.sum(a * a) * np.sum(b * b)) + eps)
return float(np.sum(a * b) / denom)
def mse(a: np.ndarray, b: np.ndarray) -> float:
diff = np.asarray(a, dtype=np.float32) - np.asarray(b, dtype=np.float32)
return float(np.mean(diff * diff))
def mae(a: np.ndarray, b: np.ndarray) -> float:
diff = np.abs(np.asarray(a, dtype=np.float32) - np.asarray(b, dtype=np.float32))
return float(np.mean(diff))
def registration_metrics(
fixed: np.ndarray,
moving: np.ndarray,
warped: np.ndarray,
) -> Dict[str, float]:
"""输出配准前后可比较的常用指标。"""
fixed, moving, warped = crop_to_common_shape(fixed, moving, warped)
before_mse = mse(fixed, moving)
after_mse = mse(fixed, warped)
before_mae = mae(fixed, moving)
after_mae = mae(fixed, warped)
before_ncc = global_ncc(fixed, moving)
after_ncc = global_ncc(fixed, warped)
return {
"before_mse": before_mse,
"after_mse": after_mse,
"before_mae": before_mae,
"after_mae": after_mae,
"before_ncc": before_ncc,
"after_ncc": after_ncc,
"mse_improvement": before_mse - after_mse,
"mae_improvement": before_mae - after_mae,
"ncc_improvement": after_ncc - before_ncc,
}
def slice_metric_curve(
fixed: np.ndarray,
moving: np.ndarray,
warped: np.ndarray,
axis: int = 2,
) -> Dict[str, Iterable[float]]:
"""逐切片计算 MSE适合生成“配准前后误差曲线”。"""
fixed, moving, warped = crop_to_common_shape(fixed, moving, warped)
before = []
after = []
for index in range(fixed.shape[axis]):
selector = [slice(None)] * 3
selector[axis] = index
selector = tuple(selector)
before.append(mse(fixed[selector], moving[selector]))
after.append(mse(fixed[selector], warped[selector]))
return {
"slice_index": list(range(fixed.shape[axis])),
"before_mse": before,
"after_mse": after,
}
def ddf_summary(ddf_xyz: np.ndarray) -> Dict[str, float]:
"""统计形变场向量大小。输入应为 X/Y/Z/3单位可为 voxel 或 mm。"""
if ddf_xyz.ndim != 4 or ddf_xyz.shape[-1] != 3:
raise ValueError("DDF 必须是形状为 (X, Y, Z, 3) 的 4D 数组。")
mag = np.linalg.norm(ddf_xyz.astype(np.float32), axis=-1)
return {
"ddf_mean": float(np.mean(mag)),
"ddf_std": float(np.std(mag)),
"ddf_p95": float(np.percentile(mag, 95)),
"ddf_max": float(np.max(mag)),
}