Source code for ClearMap.gui.pipeline_model
from __future__ import annotations
from dataclasses import dataclass, field
import uuid
import yaml
from pathlib import Path
[docs]
@dataclass(frozen=True)
class StepSpec:
"""Static definition of one binarization post-processing step."""
name: str # matches worker method suffix: binarize_channel, smooth_channel...
label: str # display label
color: str = '#2d6a4f'
description: str = ''
[docs]
@dataclass
class PipelineStep:
spec_name: str
step_id: str = field(default_factory=lambda: str(uuid.uuid4()))
enabled: bool = True
locked: bool = False # cannot be moved or displaced
keep_intermediate: bool = True # keep output for inspection
[docs]
@dataclass
class LinearPipeline:
"""
Ordered, reorderable sequence of binarization steps.
Locked steps cannot be moved and cannot be displaced by adjacent moves.
"""
steps: list[PipelineStep] = field(default_factory=list)
@property
def enabled_steps(self) -> list[str]:
"""Ordered list of spec_names for enabled steps only."""
return [s.spec_name for s in self.steps if s.enabled]
[docs]
def toggle(self, step_id: str) -> None:
"""Toggle the enabled state of a step. No-op for locked steps."""
for s in self.steps:
if s.step_id == step_id:
if not s.locked:
s.enabled = not s.enabled
return
[docs]
def move(self, step_id: str, direction: int) -> None:
"""
Move a step up (-1) or down (+1) in the pipeline.
Constraints
-----------
- Locked steps cannot move.
- No step can displace a locked step.
Parameters
----------
step_id : str
ID of the step to move.
direction : int
-1 to move up, +1 to move down.
"""
idx = next((i for i, s in enumerate(self.steps)
if s.step_id == step_id), None)
if idx is None:
return
if self.steps[idx].locked:
return
new_idx = idx + direction
if new_idx < 0 or new_idx >= len(self.steps):
return
if self.steps[new_idx].locked:
return
self.steps[idx], self.steps[new_idx] = (
self.steps[new_idx], self.steps[idx])
# # ---- serialization ----------------------------------------------------
#
# def to_dict(self) -> dict:
# return {'steps': [{'spec_name': s.spec_name,
# 'step_id': s.step_id,
# 'enabled': s.enabled,
# 'locked': s.locked,
# 'keep_intermediate': s.keep_intermediate}
# for s in self.steps]}
#
# def save(self, path: Path) -> None:
# path.write_text(yaml.safe_dump(self.to_dict())) # FIXME: move IO to ConfigHandler
#
# @classmethod
# def from_dict(cls, d: dict) -> 'LinearPipeline':
# return cls(steps=[PipelineStep(**s) for s in d.get('steps', [])])
#
# @classmethod
# def load(cls, path: Path) -> 'LinearPipeline':
# return cls.from_dict(yaml.safe_load(path.read_text())) # FIXME: move IO to ConfigHandler
# ---------------------------------------------------------------------------
# Step registry
# ---------------------------------------------------------------------------
# Steps driven by run_* flags in VesselBinarizationParams.
# Insertion order = default execution order.
BINARIZATION_STEPS: dict[str, StepSpec] = {
'binarize': StepSpec(
name='binarize',
label='Binarize',
color='#1b4332',
description='Threshold raw channel to produce a binary mask',
),
'smooth': StepSpec(
name='smooth',
label='Smooth',
color='#2d6a4f',
description='Morphological smoothing of the binary mask',
),
'binary_fill': StepSpec(
name='binary_fill',
label='Fill',
color='#40916c',
description='Fill holes in the binary mask',
),
'deep_fill': StepSpec(
name='deep_fill',
label='Deep fill',
color='#52b788',
description='CNN-based vessel lumen filling (requires GPU)',
),
}
[docs]
def default_binarization_pipeline() -> LinearPipeline:
"""
Build the default pipeline from BINARIZATION_STEPS.
The first step (binarize) is locked — it must always run first.
"""
return LinearPipeline(steps=[
PipelineStep(spec_name=name, locked=(i == 0))
for i, name in enumerate(BINARIZATION_STEPS)
])