End-to-end tumor segmentation, quantification, and reproducible reporting workflow for 3D Slicer.
SmartTumorQuant is a Python-scripted 3D Slicer extension that takes an operator from a raw volumetric scan to a quantitative, exportable lesion report in a single panel. The codebase is shaped the way a production imaging-research lab would expect: clean separation of UI and logic, real input validation, a NumPy fallback for the SegmentStatistics module, and full reproducibility metadata baked into every export.
⚠️ Research / portfolio use only. SmartTumorQuant is not a medical device and must not be used for diagnosis or treatment decisions.
Quantitative imaging workflows in research and clinical labs share a recurring shape:
- Load a scan.
- Segment a region of interest.
- Pull biomarkers out of that segment.
- Ship the numbers somewhere downstream with enough context that another human (or pipeline) can trust them.
Every step has well-known failure modes — operators trust mocked stats,
physical units drift from voxel counts, reports leave the lab without
spacing information attached, IJK/RAS conversions silently flip handedness.
SmartTumorQuant addresses each of those concretely: it respects voxel spacing
for every physical measurement, uses Slicer's official SegmentStatistics
plugin where possible with a tested NumPy fallback, converts IJK→RAS through
the volume's transform matrix, and embeds a ReproducibilityMetadata block
plus a QualityChecklist in every export.
| Area | What's in the box |
|---|---|
| Inputs | Modality-agnostic scalar volume selection, segmentation node creation with reference geometry alignment, named target segment with deterministic color |
| Segmentation | Threshold seeding (NumPy, works headlessly), median smoothing, largest-island cleanup, ± margin expansion/shrink — all via a transient Segment Editor that doesn't disturb the user's own editor session |
| Quantification | Voxel count, volume in mm³ and mL, mean / median / min / max / std-dev intensity, axis-aligned bounding box in mm, centroid in RAS |
| Visualization | One-click 3D closed-surface model with default opacity, slice-jump to the segment centroid, refreshable metrics table |
| Exports | CSV (long format), JSON (round-trippable), human-readable Markdown report with a quality checklist |
| Reproducibility | Timestamp, Slicer version, module version, volume/segmentation/segment names, voxel spacing, image dimensions, every refinement parameter, and the statistics backend that produced the numbers |
| Extensibility | registerSegmentationBackend(name, fn) hook so future AI segmenters (nnU-Net, MONAI Label, SAM2-Med, etc.) can drop in without UI changes |
| Testing | A synthetic phantom test asserting computed volume is within 10 % of the analytical sphere volume, plus round-trip JSON checks |
End-to-end run on the built-in MRHead MR volume, captured by driving the module's own logic programmatically and then grabbing the live Qt widgets.
The four sections — Inputs, Segmentation, Quantitative analysis, Reproducibility and export — with the metrics table populated by the live analysis (voxel count, volume in mm³ and mL, intensity statistics, bounding box) and the status bar reporting the last action.
Four-up layout: axial / sagittal / coronal slices with the segmentation overlay plus the rendered 3D model. The slice views were re-centered on the segment centroid by the Jump slices to centroid action.
Closed-surface model exported from the target segment, rendered with the module's default opacity and color so the geometry reads clearly against the slice plane.
Axial slice at the segment centroid showing the segmentation overlay - the same view a clinician/researcher would scrub through to validate the boundary before exporting.
- Clone or download this repository.
- Launch 3D Slicer (5.4 or newer recommended; built and tested against the current stable scripted-module API).
- Open Edit → Application Settings → Modules.
- Add the local
SmartTumorQuant/(the inner directory containingSmartTumorQuant.py) to Additional module paths. - Restart Slicer. The module appears under Quantification → Smart Tumor Quant.
For installation alongside other Slicer extensions, the repository ships a
top-level CMakeLists.txt that wraps the module in the standard Slicer
extension build, exactly as the Extensions Index expects:
mkdir build && cd build
cmake -DSlicer_DIR=/path/to/Slicer-build/Slicer-build ..
cmake --build . --config ReleasePoint Slicer at the resulting bundle via Application Settings → Modules →
Additional module paths, or package with cpack and install through the
Extensions Manager UI.
- Inputs. Select a scalar volume. Either pick an existing segmentation or leave the field empty and let the module create one. Name the target segment (default: Target Tumor). Click Setup target segment — the segmentation node is created (if needed), aligned to the volume's reference geometry, and gets a colored empty segment to fill in.
- Segmentation. Click Suggest from volume to auto-pick a threshold range from the upper quartile of intensities (modality-agnostic). Tune the slider, then Apply threshold (seed segment). Optionally enable median smoothing, largest-island cleanup, and a positive/negative margin, then Apply refinements.
- Analysis. Compute metrics populates the table with voxel count, physical volume, intensity statistics, bounding box, and centroid. Generate 3D model materializes a closed-surface model node ready for the 3D view. Jump slices to centroid re-centers all slice views on the segment.
- Report. Tick the operator quality gates (segmentation reviewed, spacing validated). Pick an output directory and a base file name. Export writes CSV / JSON / Markdown side by side.
The status row at the bottom reports progress and the most recent action; the buttons disable themselves whenever prerequisites are missing, so the workflow is obvious from the UI state alone.
SmartTumorQuant/
├── SmartTumorQuant/
│ ├── SmartTumorQuant.py ← manifest, widget, logic, test
│ ├── CMakeLists.txt
│ ├── Testing/
│ │ └── CMakeLists.txt
│ └── Resources/
│ └── Icons/ ← place SmartTumorQuant.png here
├── SampleData/ ← how to load DICOM / NRRD samples
├── Screenshots/ ← drop README screenshots here
├── Outputs/ ← default export target (gitignored)
├── CMakeLists.txt ← extension-level build
├── LICENSE
└── README.md
Inside SmartTumorQuant.py, four concerns are kept strictly separated so the
codebase scales:
SmartTumorQuant— module manifest (categories, dependencies, help text).SmartTumorQuantWidget— presentation only. Builds the four collapsible sections (Inputs, Segmentation, Analysis, Reproducibility/Export), wires signals, manages button-enabled state, refreshes the metrics table, and delegates everything substantive to the logic class.SmartTumorQuantLogic— UI-free processing core. This is the part you would import from a notebook, a pipeline, or another module.SmartTumorQuantTest— aScriptedLoadableModuleTestthat builds a synthetic spherical phantom, runs the full segmentation → analysis → export pipeline, and asserts physical-volume correctness within 10 % of the analytical sphere volume.
Three small dataclasses formalize the result contract:
| Dataclass | Purpose |
|---|---|
QuantitativeMetrics |
The numeric biomarkers themselves, with a as_rows() helper for table rendering |
ReproducibilityMetadata |
Provenance: timestamp, Slicer version, module version, geometry, every refinement parameter, the statistics backend used |
QualityChecklist |
Four operator-facing gates: segmentation reviewed, spacing validated, non-empty segment, export completed |
A fourth, AnalysisResult, bundles the three together and round-trips through
JSON via dataclasses.asdict.
These are the parts I would point at in a code review:
_writeMaskToSegment— threshold seeding bypasses the Segment Editor's Threshold effect (which needs a real Qt widget) and writes a NumPy mask through a transientvtkMRMLLabelMapVolumeNode, copying the source volume'sIJKToRASMatrixso geometry is preserved exactly. This is the reason the test can run headless in CI.applyRefinements— for the refinement effects (Smoothing / Islands / Margin) we do use the Segment Editor, but through a transientqMRMLSegmentEditorWidgetplus a temporaryvtkMRMLSegmentEditorNodethat is cleaned up in afinallyblock. The user's own editor session is untouched.computeMetrics— prefers Slicer'sSegmentStatisticsplugin (which is battle-tested across modalities), and falls back to a NumPy implementation that the test exercises explicitly. Provenance — which backend produced the numbers — is recorded in the metadata block, so a downstream reviewer can always trace a result back to its math._bboxAndCentroidRasFromMask— IJK→RAS conversion goes through the volume'sIJKToRASMatrix(vectorized with NumPy), so oblique scans don't silently produce wrong physical extents. Bounding box dimensions get an extra voxel of physical thickness so the reported bbox represents the outer extent rather than centers of corner voxels.suggestThresholdRange— the auto-threshold heuristic picks the upper quartile of finite intensities rather than hardcoding a CT window. The module never assumes modality.registerSegmentationBackend— an explicit hook so an AI segmenter can drop in tomorrow without touching the widget. The contract is small:(volumeNode, segmentationNode, segmentId, **kwargs) -> None.- Reproducibility & quality — every export carries the metadata and the
checklist. The checklist's
export_completedflag is only flipped toTrueafter the report files are on disk, so a partial export is visible to the reviewer.
After exporting, the chosen directory contains three files:
smart_tumor_quant_report.csv
smart_tumor_quant_report.json
smart_tumor_quant_report.md
The Markdown report is structured for human review:
# SmartTumorQuant Report
_Generated 2026-05-12T14:11:09Z - Slicer 5.6.2 - SmartTumorQuant v1.0.0_
## Acquisition
| Field | Value |
|---|---|
| Volume | `CTChest` |
| Segmentation | `CTChest_Segmentation` |
| Segment | `Target Tumor` |
| Spacing (mm) | 0.703 × 0.703 × 1.250 |
| Dimensions (voxels) | 512 × 512 × 320 |
| Statistics backend | `SegmentStatistics` |
## Segmentation parameters
| Step | Value |
|---|---|
| Threshold range | 150.0 - 3000.0 |
| Smoothing | yes (2.0 mm median) |
| Largest-island cleanup | yes |
| Margin | 1.0 mm |
## Quantitative metrics
| Metric | Value | Units |
|---|---|---|
| Voxel count | 12842 | voxels |
| Volume | 7935.27 | mm³ |
| Volume | 7.9353 | mL |
| Mean intensity | 132.451 | a.u. |
...
## Quality checklist
- [x] Segmentation reviewed by operator
- [x] Voxel spacing validated against source DICOM
- [x] Segment is non-empty after refinement
- [x] Export completed successfullyThe CSV is in long format (section, key, value, units) so it ingests
cleanly into a pandas/SQL pipeline, and the JSON round-trips through
json.load directly into the AnalysisResult shape used by the module.
From the Python Interactor inside Slicer:
import SmartTumorQuant
test = SmartTumorQuant.SmartTumorQuantTest()
test.runTest()The test creates a synthetic 64³ spherical phantom (no network required), runs the entire pipeline including export, and asserts:
- segment is non-empty,
- computed volume is within 10 % of the analytical sphere volume,
- intensity statistics are internally consistent,
- bounding box has positive extent on every axis,
- CSV / JSON / Markdown files are written and non-empty,
- the JSON file round-trips back to a
dictwhosemetrics.voxel_countmatches the live result.
These are honest, not toy. Things I'd ship next, in order:
- Histogram / shape features. Skewness, kurtosis, sphericity, surface
area, elongation —
SegmentStatisticsalready exposes most of them; the missing piece is plumbing them intoQuantitativeMetricsand the report template. - Per-slice scrubbing view. A second tab with a sortable per-slice table (slice index, voxel count, mean intensity) helps spot heterogeneity that a single set of aggregate stats hides.
- DICOM SR export. Convert the report into a DICOM Structured Report (TID 1500) so it can flow back into a PACS for storage alongside the source study.
- AI segmentation integration. Wire the existing
registerSegmentationBackendhook to MONAI Label / nnU-Net Inference for first-pass segmentation, keeping the manual refinement pipeline as a reviewer step. - Multi-segment / multi-lesion mode. Currently the workflow is
single-target by design. Extending it to a longitudinal multi-lesion
tracker is straightforward once a segment-id loop replaces the single
_lastSegmentIdfield on the widget. - Unit & integration test split. The current end-to-end test is the right shape for a portfolio sample; production deployment would split it into fast logic-only unit tests plus a slower Slicer-integrated suite.
Concrete capabilities a reviewer can verify in the source:
- Real Slicer fluency. Correct use of
vtkMRMLScalarVolumeNode,vtkMRMLSegmentationNode,qMRMLSegmentEditorWidget,vtkMRMLSegmentEditorNode,SegmentStatistics,slicer.util.arrayFromVolume,slicer.util.arrayFromSegmentBinaryLabelmap, the segmentations logic, the reference-geometry mechanism, and the scripted module test framework. - Imaging engineering judgement. Physical units throughout. Modality
agnostic. IJK→RAS through the matrix, not by accident. Bounding box reported
as outer extent. Auto-threshold by quantile, not by a CT-specific constant.
A real fallback for
SegmentStatisticsrather than failing loudly. - Software engineering discipline. UI/logic separation, dataclasses for
the result contract, type hints, structured error paths that disable bad
actions rather than crash, transient Slicer nodes always cleaned up in
finallyblocks, headless-runnable tests, reproducibility metadata baked into every export. - Operational thinking. A quality checklist that intentionally leaves some gates to the operator, a status row that surfaces progress, a default output directory under the user's home, and an export that is safe to ingest into a downstream pipeline (long-format CSV + round-trippable JSON).
That mix — imaging math correct, Slicer APIs idiomatic, code shaped for a team to extend — is what the role actually requires.
MIT. See LICENSE.



