JerryX's picture
Publish all-view cine ONNX models and reference assets
d5d23f9 verified
Raw
History Blame Contribute Delete
3.15 kB
"""Quality metrics for dense and post-processed contours."""
from __future__ import annotations
import math
import numpy as np
from .geometry import rasterize_contour, strip_duplicate_endpoint, xy_to_physical
from .types import ContourQualityMetrics, PixelSpacing
def symmetric_boundary_distances(reference: np.ndarray, candidate: np.ndarray) -> np.ndarray:
reference = strip_duplicate_endpoint(reference)
candidate = strip_duplicate_endpoint(candidate)
if len(reference) == 0 or len(candidate) == 0:
return np.asarray([math.inf], dtype=np.float64)
try:
from scipy.spatial import cKDTree
forward = cKDTree(candidate).query(reference)[0]
reverse = cKDTree(reference).query(candidate)[0]
except Exception:
forward = np.linalg.norm(
reference[:, None, :] - candidate[None, :, :], axis=2
).min(axis=1)
reverse = np.linalg.norm(
candidate[:, None, :] - reference[None, :, :], axis=2
).min(axis=1)
return np.concatenate([forward, reverse]).astype(np.float64)
def contour_roughness(points: np.ndarray) -> float:
points = strip_duplicate_endpoint(points)
if len(points) < 4:
return 0.0
previous = np.roll(points, 1, axis=0)
following = np.roll(points, -1, axis=0)
second_difference = following - 2.0 * points + previous
perimeter = float(np.linalg.norm(following - points, axis=1).sum())
if perimeter <= 1e-12:
return 0.0
return float(np.linalg.norm(second_difference, axis=1).sum() / perimeter)
def contour_quality_metrics(
dense_xy: np.ndarray,
smooth_xy: np.ndarray,
shape: tuple[int, int],
pixel_spacing: PixelSpacing,
control_point_count: int,
) -> tuple[ContourQualityMetrics, np.ndarray]:
dense_mask = rasterize_contour(dense_xy, shape)
rendered_mask = rasterize_contour(smooth_xy, shape)
intersection = int(np.logical_and(dense_mask, rendered_mask).sum())
union = int(np.logical_or(dense_mask, rendered_mask).sum())
source_area = int(dense_mask.sum())
rendered_area = int(rendered_mask.sum())
distances = symmetric_boundary_distances(
xy_to_physical(dense_xy, pixel_spacing),
xy_to_physical(smooth_xy, pixel_spacing),
)
source_roughness = contour_roughness(xy_to_physical(dense_xy, pixel_spacing))
rendered_roughness = contour_roughness(xy_to_physical(smooth_xy, pixel_spacing))
metrics = ContourQualityMetrics(
control_point_count=int(control_point_count),
boundary_mean_mm=float(np.mean(distances)),
boundary_p95_mm=float(np.percentile(distances, 95)),
boundary_max_mm=float(np.max(distances)),
mask_iou=float(intersection / union) if union else 0.0,
source_area_px=source_area,
rendered_area_px=rendered_area,
area_change_pct=(
float(100.0 * (rendered_area - source_area) / source_area)
if source_area
else math.inf
),
roughness_ratio=(
float(rendered_roughness / source_roughness) if source_roughness > 0 else 0.0
),
)
return metrics, rendered_mask