Scoring a Mesh-Stage Model Against Images
A mm error against the surfaces a registration produced says how well the
network reproduces that registration. WorkflowEvaluateMovement asks the
other question: how close are the size and shape of the inferred anatomy to the
anatomy that was actually imaged, structure by structure. See Tutorial 11 in
Tutorials.
For every gated time point it carries the reference frame’s labelmap into that time point with the network’s own deformation, and compares the result to the labelmap of the frame that was acquired: volume difference, Dice and surface RMSE per lung lobe or per heart chamber.
Per-structure scoring
- class physiotwin4d.WorkflowEvaluateMovement(movement_workflow, cohort=None, label_names=None, log_level=20)[source]
Bases:
PhysioTwin4DBaseScore inferred motion per anatomical structure against acquired frames.
- Parameters:
movement_workflow (
WorkflowInferMovement) – The displacement decoder whose predictions are scored.cohort (
Optional[EvaluateMovementBase]) – The cohort being scored —physiotwin4d.EvaluateMovementLung,physiotwin4d.EvaluateMovementDukeHeartor anotherphysiotwin4d.EvaluateMovementBase. It supplies the structures to score and this cohort’s defaults, and itsassemble_ground_truth()produces whatprocess()scores against. The workflow never learns which cohort it holds, which is what keeps it free of anatomy branches.label_names (
Optional[dict[int,str]]) – Structures to score,{label_id: name}, for a caller with no cohort. Ids the reference frame does not contain are dropped with a warning; ids a single acquired frame does not contain are skipped for that frame alone, since a structure can leave the field of view.log_level (
int|str) – Logging level. Default:logging.INFO.
- __init__(movement_workflow, cohort=None, label_names=None, log_level=20)[source]
Initialize the base class with logging configuration.
- Parameters:
class_name – Name for the class (used in log messages). If None, uses the class name. Default: None
log_level (
int|str) – Logging level. Can be an integer (logging.DEBUG, logging.INFO, logging.WARNING, logging.ERROR, logging.CRITICAL) or a string (‘DEBUG’, ‘INFO’, ‘WARNING’, ‘ERROR’, ‘CRITICAL’). Default: logging.INFOlog_to_file – Optional file path to write logs to in addition to console output. Default: None
- process(case_id, shape_parameters, fitted_reference_mesh, ground_truth, output_directory, smoothing_sigma_mm=None, evaluation_spacing_mm=None, report_dice=None, report_displacement_data=False, include_predicted_displacements=False, include_true_displacements=False, include_displacement_error=False)[source]
Score every gated time point of one case.
- Parameters:
case_id (
str) – Name of the case being scored, recorded in every output.shape_parameters (
Path) – JSON file with the case’s PCA coefficient vector.fitted_reference_mesh (
Path) – The case’s fitted reference-frame SSM surface, as produced byphysiotwin4d.WorkflowFitStatisticalModelToPatient. The predicted displacements are added to its points, and its extent defines the evaluation grid.ground_truth (
MovementGroundTruth) – What this case is scored against — the acquired labelmap per stage, the reference frame’s labelmap, and the per-stage fits the point-by-point error is measured against — as assembled byphysiotwin4d.EvaluateMovementBase.assemble_ground_truth().output_directory (
Path) – Directory the report, the CSV and the per-stage geometry are written to.smoothing_sigma_mm (
Optional[float]) – Gaussian sigma, in millimeters, that turns the network’s surface-shell deformation into a continuous field.Nonetakes the cohort’s own value.evaluation_spacing_mm (
Optional[float]) – Isotropic pitch every metric is measured on. It sets both the voxel volume the Dice and volume figures are quantized to and the resolution of the deformation fields, whose memory grows with its cube.Nonetakes the cohort’s own value.report_dice (
Optional[bool]) – Report the Dice overlap. Turn it off for a structure whose motion is small against its own size: Dice is an overlap fraction, so a lung lobe scores over 0.96 undeformed and the column says more about the organ’s bulk than about the motion. The volume and surface figures still resolve it.Nonetakes the cohort’s own value.report_displacement_data (
bool) – Writedisplacement_per_point.csv, one row per mesh point per stage carrying that point’s predicted and true displacement and the error between them.include_predicted_displacements (
bool) – Carrypredicted_displacement_mmas point data on every stage’s predicted surface.include_true_displacements (
bool) – Carrytrue_displacement_mmas point data on every stage’s predicted surface.include_displacement_error (
bool) – Carrydisplacement_error_mm, the distance between the predicted and the true position, as point data on every stage’s predicted surface, and report that error’s RMS, 95th percentile and maximum — per structure on every metric row and in the report, and pooled over every point and stage in the report and the returned dict. It is the one metric here measured point by point, so a displacement predicted in the wrong direction cannot cancel against another. A shape-model point is attributed to the structure its own per-cellSegmentationLabelIdsnames, or, on a model carrying no such ids, to the structure whose reference-frame surface is nearest — the same nearest-label partition the labelmap metrics are already scored under.
- Return type:
- Returns:
Dict with
rows(every metric row),csv_file,report_file,volume_plot_file,predicted_surfaces,warped_labelmaps,displacement_data_file,displacement_statistics(one entry per stage, empty unlessinclude_displacement_error) and that error pooled over every point and stage asdisplacement_rms_mm,displacement_95th_mmanddisplacement_max_mm.- Raises:
ValueError – If
ground_truth.labelmapsis empty, any stage it carries a labelmap for has no entry inground_truth.meshes— checked whether or not an option needing the true displacement was requested — none of the requested labels are in the reference frame, or no label survives scoring because the acquired frames contain none of them.
- static dice(truth, predicted, label)[source]
Dice overlap of one label.
nanwhen neither volume contains it.- Return type:
- static volume_mm3(labels, label, voxel_volume_mm3)[source]
Volume of one label, in cubic millimeters.
- Return type:
- static surface_rmse_mm(truth, predicted)[source]
Symmetric point-to-surface RMSE, in millimeters.
Both directions are pooled before the root-mean-square. A one-sided RMSE misses a prediction that covers the truth everywhere but also bulges somewhere the truth does not reach.
- Return type:
- static pool_displacement_error(stage_errors)[source]
RMS, 95th percentile and maximum over every point of every stage.
The errors themselves are pooled rather than their per-stage summaries: a percentile of percentiles is not the percentile of the whole. Every value is
nanwhen no stage was scored point by point.
Example
from physiotwin4d import (
WorkflowEvaluateMovement,
WorkflowInferMovement,
WorkflowInferPhysicsNeMo,
)
evaluate = WorkflowEvaluateMovement(
movement_workflow=WorkflowInferMovement(
WorkflowInferPhysicsNeMo(model_directory=model_dir)
),
label_names={28: "lung_upper_lobe_left", 29: "lung_lower_lobe_left"},
)
result = evaluate.process(
case_id="Case1Pack",
shape_parameters=pca_coefficients_file,
fitted_reference_mesh=fitted_reference_mesh_file,
reference_labelmap=reference_labelmap,
ground_truth_labelmaps={0.0: frame_00, 0.1: frame_10},
output_directory=out_dir,
)
print(result["report_file"], result["csv_file"])
Notes
Why labelmaps rather than the model’s surface. The lung shape model carries its five lobes as per-cell labels, but the heart model is a single structure — the whole heart minus its chamber cavities — so its chambers exist only in the acquired labelmaps. Warping those labelmaps scores every structure the acquisition contains, whether or not the shape model represents it separately.
The evaluation grid. Everything is measured on one isotropic grid built around the reference anatomy, so a case whose gated frames carry different slice pitches is still scored on a single, stated voxel volume. Its pitch sets both that voxel volume and the memory the per-stage deformation fields take, which grows with its cube.