Add All Folders

This commit is contained in:
soorena62
2026-02-08 04:38:10 +03:30
commit 9bb57e4799
191 changed files with 8469 additions and 0 deletions
View File
+72
View File
@@ -0,0 +1,72 @@
import json
import threading
from pathlib import Path
from loguru import logger
class WorkflowState:
def __init__(self, state_dir: Path):
self.state_dir = state_dir
self.state_file = state_dir / "state.json"
self._lock = threading.Lock()
self._state = {"steps": {}, "cancelled": False}
self.state_dir.mkdir(parents=True, exist_ok=True)
if self.state_file.exists():
self._state = json.loads(self.state_file.read_text())
def mark_success(self, step_id: str, outputs: dict):
with self._lock:
self._state["steps"][step_id] = {"status": "success", "outputs": outputs}
self._write()
def mark_failed(self, step_id: str, error: str):
with self._lock:
self._state["steps"][step_id] = {"status": "failed", "error": error}
self._write()
def get_status(self, step_id: str):
return self._state["steps"].get(step_id, {}).get("status", "pending")
def get_outputs(self, step_id: str):
return self._state["steps"].get(step_id, {}).get("outputs", {})
def set_cancelled(self, flag: bool):
with self._lock:
self._state["cancelled"] = flag
self._write()
def is_cancelled(self) -> bool:
return self._state.get("cancelled", False)
def _write(self):
self.state_file.write_text(json.dumps(self._state, indent=2))
class Node:
def __init__(self, step_id: str, run_fn, inputs: list, outputs: list):
self.step_id = step_id
self.run_fn = run_fn
self.inputs = inputs
self.outputs = outputs
class Workflow:
def __init__(self, nodes: list[Node], state: WorkflowState):
self.nodes = nodes
self.state = state
def execute(self):
for node in self.nodes:
status = self.state.get_status(node.step_id)
if status == "success":
logger.info(f"Skip {node.step_id}: already successful.")
continue
if self.state.is_cancelled():
logger.warning("Execution cancelled. Stopping workflow.")
break
try:
logger.info(f"Run {node.step_id}")
outputs = node.run_fn(self.state)
self.state.mark_success(node.step_id, outputs)
logger.info(f"Success {node.step_id}")
except Exception as e:
logger.exception(f"Failed {node.step_id}: {e}")
self.state.mark_failed(node.step_id, str(e))
break # Stop on failure, allow resume later
@@ -0,0 +1,39 @@
from dagster import op, job, In
import os, json
from app.features.pysera_node import run_pysera
DATA_DIR = os.path.join("data", "images")
MASK_DIR = os.path.join("data", "masks")
OUTPUT_DIR = os.path.join("app", "storage", "artifacts")
os.makedirs(OUTPUT_DIR, exist_ok=True)
@op(ins={"filename": In(str)})
def load_image(filename: str) -> str:
# Return normalized image file path (do not load arrays/objects)
return os.path.normpath(os.path.join(DATA_DIR, filename))
@op(ins={"filename": In(str)})
def load_mask(filename: str) -> str:
# Return normalized mask file path (do not load arrays/objects)
return os.path.normpath(os.path.join(MASK_DIR, filename))
@op
def extract_features(img_path: str, mask_path: str) -> str:
# Validate file paths and call PySERA
if not os.path.exists(img_path):
raise FileNotFoundError(f"Image path not found: {img_path}")
if not os.path.exists(mask_path):
raise FileNotFoundError(f"Mask path not found: {mask_path}")
features = run_pysera(img_path, mask_path)
out_path = os.path.join(OUTPUT_DIR, "CT_pitch_features.json")
with open(out_path, "w", encoding="utf-8") as f:
json.dump(features, f, indent=2)
return out_path
@job
def radiuma_job():
# Compose ops: produce file paths, then extract features
img = load_image()
mask = load_mask()
extract_features(img, mask)
@@ -0,0 +1,79 @@
import os
import pysera
from app.engine import radiuma_assets
from app.engine.dagster_builder import radiuma_job
from dagster import materialize, FilesystemIOManager
# Main workflow runner for Radiuma using PySERA and Dagster
def run_workflow():
# Define input image and mask paths
image_path = os.path.normpath("data/images/CT_pitch.nii.gz")
mask_path = os.path.normpath("data/masks/CT_pitch_mask.nii.gz")
output_dir = os.path.normpath("./results")
# Run PySERA radiomics workflow
# Changed dimensions from "3d" to "1st,2D"
result = pysera.process_batch(
image_input=image_path,
mask_input=mask_path,
output_path=output_dir,
categories="all",
dimensions="1st,2D", # <-- only change applied here
apply_preprocessing=True,
)
print("✅ PySERA workflow finished." if result.get("success") else "❌ PySERA workflow failed.")
def run_pipeline():
# Run Dagster job with sample configuration
result = radiuma_job.execute_in_process(
run_config={
"ops": {
"load_image": {"inputs": {"filename": {"value": "CT_pitch.nii.gz"}}},
"load_mask": {"inputs": {"filename": {"value": "CT_pitch_mask.nii.gz"}}},
}
}
)
print("✅ Dagster job finished.")
print(result)
def run_assets():
# Ensure artifacts directory exists
base_dir = os.path.normpath("app/storage/artifacts")
os.makedirs(base_dir, exist_ok=True)
# Use FilesystemIOManager for asset storage
io_manager = FilesystemIOManager(base_dir=base_dir)
# Materialize all assets defined in radiuma_assets
result = materialize(
[
radiuma_assets.raw_image,
radiuma_assets.raw_mask,
radiuma_assets.registered_image,
radiuma_assets.fused_image,
radiuma_assets.features,
],
resources={"io_manager": io_manager},
)
if result.success:
print("✅ Assets materialized successfully.")
print(f"Storage base_dir: {base_dir}")
# Print all materialized asset keys
for event in result.get_asset_materialization_events():
print(f"Asset built: {event.asset_key}")
else:
print("❌ Asset execution failed.")
print(result)
if __name__ == "__main__":
print("🚀 Starting Radiuma unified run...\n")
run_workflow()
run_pipeline()
run_assets()
print("\n🎯 All workflows completed in one run.")
@@ -0,0 +1,57 @@
import os
import SimpleITK as sitk
from dagster import asset
DATA_DIR = os.path.join("data", "images")
MASK_DIR = os.path.join("data", "masks")
ARTIFACTS_DIR = os.path.join("app", "storage", "artifacts")
os.makedirs(ARTIFACTS_DIR, exist_ok=True)
@asset
def raw_image() -> str:
# Return existing image path
path = os.path.normpath(os.path.join(DATA_DIR, "CT_pitch.nii.gz"))
if not os.path.exists(path):
raise FileNotFoundError(f"raw_image: file not found at {path}")
return path
@asset
def raw_mask() -> str:
path = os.path.normpath(os.path.join(MASK_DIR, "CT_pitch_mask.nii.gz"))
if not os.path.exists(path):
raise FileNotFoundError(f"raw_mask: file not found at {path}")
return path
@asset
def registered_image(raw_image: str) -> str:
# Read the raw image and write a registered copy
img = sitk.ReadImage(raw_image)
registered = sitk.Cast(img, img.GetPixelID()) # identity registration
out_path = os.path.normpath(os.path.join(ARTIFACTS_DIR, "CT_pitch_registered.nii.gz"))
sitk.WriteImage(registered, out_path)
return out_path
@asset
def fused_image(registered_image: str, raw_mask: str) -> str:
img = sitk.ReadImage(registered_image)
mask = sitk.ReadImage(raw_mask)
mask_uint8 = sitk.Cast(mask, sitk.sitkUInt8)
fused = sitk.Mask(img, mask_uint8)
out_path = os.path.normpath(os.path.join(ARTIFACTS_DIR, "CT_pitch_fused.nii.gz"))
sitk.WriteImage(fused, out_path)
return out_path
@asset
def features(fused_image: str) -> str:
import json, numpy as np
img = sitk.ReadImage(fused_image)
arr = sitk.GetArrayFromImage(img)
feats = {
"shape": list(arr.shape),
"spacing": list(img.GetSpacing()),
"intensity_sum": float(np.sum(arr)),
}
out_path = os.path.normpath(os.path.join(ARTIFACTS_DIR, "CT_pitch_asset_features.json"))
with open(out_path, "w", encoding="utf-8") as f:
json.dump(feats, f, indent=2)
return out_path
@@ -0,0 +1,69 @@
import json
import numpy as np
from dagster import op
import SimpleITK as sitk
from pathlib import Path
# Write Your codes Here:
ARTIFACTS = Path("artifacts")
ARTIFACTS.mkdir(exist_ok=True)
@op
def image_reader(path: str):
"""Medical image reading (MRI, CT, PET)."""
img = sitk.ReadImage(path)
# Save the processed version for display in the GUI (optional)
sitk.WriteImage(img, str(ARTIFACTS / "processed_image.nii.gz"))
return img
@op
def image_registration(fixed_img, moving_img):
"""Aligning two medical images."""
reg = sitk.ImageRegistrationMethod()
reg.SetMetricAsMeanSquares()
reg.SetOptimizerAsGradientDescent(learningRate=1.0, numberOfIterations=100)
reg.SetInterpolator(sitk.sitkLinear)
transform = reg.Execute(fixed_img, moving_img)
registered = sitk.Resample(
moving_img, fixed_img, transform, sitk.sitkLinear, 0.0, moving_img.GetPixelID()
)
sitk.WriteImage(registered, str(ARTIFACTS / "registered_image.nii.gz"))
return registered
@op
def image_fusion(img1, img2):
"""Combining two medical images (simple fusion)."""
fused = sitk.Cast((img1 + img2) / 2, sitk.sitkFloat32)
sitk.WriteImage(fused, str(ARTIFACTS / "fused_image.nii.gz"))
return fused
@op
def image_extraction(img):
"""Extracting simple features from images."""
arr = sitk.GetArrayFromImage(img)
features = {
"mean_intensity": float(np.mean(arr)),
"std_intensity": float(np.std(arr)),
"min_intensity": float(np.min(arr)),
"max_intensity": float(np.max(arr)),
}
(ARTIFACTS / "features.json").write_text(json.dumps(features, indent=2))
return features
@op
def image_writer(img):
"""Save final output."""
out_path = ARTIFACTS / "output.nii.gz"
sitk.WriteImage(img, str(out_path))
return str(out_path)
OPS = {
"reader": image_reader,
"registration": image_registration,
"fusion": image_fusion,
"extraction": image_extraction,
"writer": image_writer,
}
@@ -0,0 +1,39 @@
import os
import json
import numpy as np
import SimpleITK as sitk
def run_pysera(image_path: str, mask_path: str):
# Read image and mask from file paths
image = sitk.ReadImage(str(image_path))
mask = sitk.ReadImage(str(mask_path))
# Ensure mask is binary {0,1} and integer typed
mask = sitk.Cast(mask, sitk.sitkUInt8)
# Treat any non-zero voxel as 1
mask = sitk.BinaryThreshold(
mask,
lowerThreshold=1,
upperThreshold=1_000_000,
insideValue=1,
outsideValue=0,
)
# Use proper radius vector for BinaryErode based on image dimension
dim = image.GetDimension()
radius = [1] * dim # e.g., [1,1,1] for 3D or [1,1] for 2D
# Safe morphological operation on the binary mask
eroded = sitk.BinaryErode(mask, radius)
mask_array = sitk.GetArrayFromImage(mask)
eroded_array = sitk.GetArrayFromImage(eroded)
mask_border = np.logical_xor(mask_array, eroded_array)
# Example features (replace with your real extraction pipeline)
features = {
"voxel_spacing": tuple(image.GetSpacing()),
"mask_voxels": int(np.sum(mask_array)),
"mask_border_voxels": int(np.sum(mask_border)),
}
return features
@@ -0,0 +1,24 @@
from PySide6.QtWidgets import QGraphicsLineItem, QMessageBox
from PySide6.QtGui import QPen, QColor
class ConnectionLine(QGraphicsLineItem):
def __init__(self, source_port, target_port):
super().__init__()
self.source_port = source_port
self.target_port = target_port
self.update_position()
# Validate connection types
if self.source_port.port_type != self.target_port.port_type:
# Invalid connection → red line + warning
self.setPen(QPen(QColor("red"), 2))
QMessageBox.warning(None, "Invalid Connection",
f"Cannot connect {self.source_port.port_type}{self.target_port.port_type}")
else:
# Valid connection → black line
self.setPen(QPen(QColor("blue"), 2))
def update_position(self):
src_pos = self.source_port.scenePos()
tgt_pos = self.target_port.scenePos()
self.setLine(src_pos.x(), src_pos.y(), tgt_pos.x(), tgt_pos.y())
+214
View File
@@ -0,0 +1,214 @@
from PySide6.QtWidgets import QGraphicsScene, QGraphicsEllipseItem, QGraphicsLineItem
from PySide6.QtGui import QBrush, QColor, QPen
from PySide6.QtCore import QPointF
from app.gui.node_widget import NodeWidget
class Port(QGraphicsEllipseItem):
"""An input/output port that is placed next to each node."""
def __init__(self, parent_node: NodeWidget, kind: str, offset_x: float, offset_y: float):
super().__init__(-5, -5, 10, 10, parent_node)
self.parent_node = parent_node
self.kind = kind # "input" Or "output"
self.setBrush(QBrush(QColor("#2e7d32") if kind == "input" else QColor("#1565c0")))
self.setPos(parent_node.rect().x() + offset_x, parent_node.rect().y() + offset_y)
self.setZValue(1.0)
class ConnectionLine(QGraphicsLineItem):
"""Connection between two ports."""
def __init__(self, source_port: Port, target_port: Port):
super().__init__()
self.source_port = source_port
self.target_port = target_port
pen = QPen(QColor("#555"))
pen.setWidth(2)
self.setPen(pen)
self.update_positions()
def update_positions(self):
src = self.source_port.mapToScene(QPointF(0, 0))
tgt = self.target_port.mapToScene(QPointF(0, 0))
self.setLine(src.x(), src.y(), tgt.x(), tgt.y())
class GraphScene(QGraphicsScene):
"""Graph scene with nodes, ports, and connections. Only connected nodes are exported."""
def __init__(self):
super().__init__()
self.connections: list[ConnectionLine] = []
# Manual connection mode: You click on an outgoing port, then click on the destination incoming port.
self._pending_source_port: Port | None = None
# ---------- Helper tool for adding default nodes ----------
def add_default_medical_nodes(self):
"""Adds five medical nodes and installs their ports."""
# Create nodes
reader = NodeWidget("Image Reader", [], ["output"])
registration = NodeWidget("Image Registration", ["input"], ["output"])
fusion = NodeWidget("Image Fusion", ["input1", "input2"], ["output"])
extraction = NodeWidget("Image Extraction", ["input"], ["output"])
writer = NodeWidget("Image Writer", ["input"], [])
# Initial placement
reader.setPos(50, 50)
registration.setPos(250, 50)
fusion.setPos(450, 50)
extraction.setPos(650, 50)
writer.setPos(850, 50)
# Add to scene
for n in (reader, registration, fusion, extraction, writer):
self.addItem(n)
# Install ports for each node (a simple I/O is enough)
# Reader: Output only
reader.output_port = Port(reader, "output", 150, 45)
# Registration: Input + Output
registration.input_port = Port(registration, "input", 0, 45)
registration.output_port = Port(registration, "output", 150, 45)
# Fusion: Two inputs + one output
fusion.input_port_1 = Port(fusion, "input", 0, 30)
fusion.input_port_2 = Port(fusion, "input", 0, 60)
fusion.output_port = Port(fusion, "output", 150, 45)
# Extraction: Input + Output
extraction.input_port = Port(extraction, "input", 0, 45)
extraction.output_port = Port(extraction, "output", 150, 45)
# Writer: Input only
writer.input_port = Port(writer, "input", 0, 45)
# Register the port click handle for manual connection
self._install_port_handlers([reader, registration, fusion, extraction, writer])
def _install_port_handlers(self, nodes: list[NodeWidget]):
"""Mouse handles for ports so the user can make connections."""
all_ports: list[Port] = []
for n in nodes:
for attr in dir(n):
if attr.endswith("port"):
p = getattr(n, attr)
if isinstance(p, Port):
all_ports.append(p)
for port in all_ports:
port.mousePressEvent = lambda event, p=port: self._on_port_clicked(p)
def _on_port_clicked(self, port: Port):
"""Ports click logic: output first, then input; connection is made."""
if port.kind == "output":
# Start connection
self._pending_source_port = port
elif port.kind == "input" and self._pending_source_port is not None:
# Connection completion
line = ConnectionLine(self._pending_source_port, port)
self.addItem(line)
self.connections.append(line)
# Update positions when nodes move
self._pending_source_port = None
# ----------Export/Load Graph ----------
def export_graph(self):
"""Exports only nodes involved in connections."""
data = {"nodes": [], "connections": []}
used_nodes = set()
for conn in self.connections:
src_node = conn.source_port.parent_node
tgt_node = conn.target_port.parent_node
used_nodes.add(src_node)
used_nodes.add(tgt_node)
data["connections"].append({
"source": src_node.title.toPlainText(),
"target": tgt_node.title.toPlainText()
})
for node in used_nodes:
data["nodes"].append({
"id": node.title.toPlainText(), # Persistent ID based on title
"type": node.title.toPlainText(),
"pos": [node.pos().x(), node.pos().y()]
})
return data
def save_graph(self, filename: str):
import json
from pathlib import Path
Path(filename).write_text(json.dumps(self.export_graph(), indent=2))
def load_graph(self, filename: str):
"""Rebuild ports and connections from saved file.
Note: Here we assume that the default nodes are present on the scene; we only draw the connections.
"""
import json
from pathlib import Path
if not Path(filename).exists():
return
data = json.loads(Path(filename).read_text())
# Create a mapping from title → node in the scene
title_to_node = {}
for item in self.items():
if isinstance(item, NodeWidget):
title_to_node[item.title.toPlainText()] = item
# Rebuild connections
self._rebuild_ports_if_missing(title_to_node)
for conn in data.get("connections", []):
src = title_to_node.get(conn["source"])
tgt = title_to_node.get(conn["target"])
if not src or not tgt:
continue
# Select default input/output port based on node name
src_port = getattr(src, "output_port", None)
tgt_port = getattr(tgt, "input_port", None)
# Fusion has two inputs; if the destination is Fusion and the first input is busy, take the second input
if tgt.title.toPlainText() == "Image Fusion":
# If there is no previous connection to input_port_1, connect to it; otherwise connect to input_port_2
candidates = [tgt.input_port_1, tgt.input_port_2]
tgt_port = candidates[0]
for c in self.connections:
if c.target_port is candidates[0]:
tgt_port = candidates[1]
break
if src_port and tgt_port:
line = ConnectionLine(src_port, tgt_port)
self.addItem(line)
self.connections.append(line)
def _rebuild_ports_if_missing(self, title_to_node: dict):
"""If the node ports haven't been created yet for some reason, we'll create them here."""
for title, n in title_to_node.items():
# Reader
if title == "Image Reader" and not hasattr(n, "output_port"):
n.output_port = Port(n, "output", 150, 45)
# Registration
if title == "Image Registration":
if not hasattr(n, "input_port"):
n.input_port = Port(n, "input", 0, 45)
if not hasattr(n, "output_port"):
n.output_port = Port(n, "output", 150, 45)
# Fusion
if title == "Image Fusion":
if not hasattr(n, "input_port_1"):
n.input_port_1 = Port(n, "input", 0, 30)
if not hasattr(n, "input_port_2"):
n.input_port_2 = Port(n, "input", 0, 60)
if not hasattr(n, "output_port"):
n.output_port = Port(n, "output", 150, 45)
# Extraction
if title == "Image Extraction":
if not hasattr(n, "input_port"):
n.input_port = Port(n, "input", 0, 45)
if not hasattr(n, "output_port"):
n.output_port = Port(n, "output", 150, 45)
# Writer
if title == "Image Writer" and not hasattr(n, "input_port"):
n.input_port = Port(n, "input", 0, 45)
@@ -0,0 +1,60 @@
from PySide6.QtWidgets import QPushButton, QHBoxLayout, QWidget
from dagster_radiuma.jobs import radiomics_job, radiomics_batch_job
class WorkflowControls(QWidget):
def __init__(self, log_panel, history_panel, parent=None):
super().__init__(parent)
self.log_panel = log_panel
self.history_panel = history_panel
# دکمه‌ها
self.start_btn = QPushButton("Start")
self.stop_btn = QPushButton("Stop")
self.resume_btn = QPushButton("Resume")
self.cancel_btn = QPushButton("Cancel")
layout = QHBoxLayout()
layout.addWidget(self.start_btn)
layout.addWidget(self.stop_btn)
layout.addWidget(self.resume_btn)
layout.addWidget(self.cancel_btn)
self.setLayout(layout)
# اتصال
self.start_btn.clicked.connect(self.start_workflow)
self.stop_btn.clicked.connect(self.stop_workflow)
self.resume_btn.clicked.connect(self.resume_workflow)
self.cancel_btn.clicked.connect(self.cancel_workflow)
self.current_run_id = None
def log(self, message):
if hasattr(self.log_panel, "append"):
self.log_panel.append(message)
if hasattr(self.history_panel, "append"):
self.history_panel.append(message)
def start_workflow(self):
self.log("[GUI] Start → Dagster job starting...")
result = radiomics_job.execute_in_process()
self.current_run_id = result.run_id
self.log(f"[GUI] Workflow started. Success={result.success}")
def stop_workflow(self):
if self.current_run_id:
self.log(f"[GUI] Stop → Terminating run {self.current_run_id}")
# dagster_api.terminate_run(self.current_run_id)
self.log("[GUI] Workflow stopped.")
def resume_workflow(self):
if self.current_run_id:
self.log(f"[GUI] Resume → Restarting run {self.current_run_id}")
# dagster_api.resume_run(self.current_run_id)
self.log("[GUI] Workflow resumed. [Recovered]")
def cancel_workflow(self):
if self.current_run_id:
self.log(f"[GUI] Cancel → Cancelling run {self.current_run_id}")
# dagster_api.cancel_run(self.current_run_id)
self.log("[GUI] Workflow cancelled.")
+24
View File
@@ -0,0 +1,24 @@
# app/gui/napari_main.py
import napari
from pathlib import Path
import SimpleITK as sitk
import numpy as np
def sitk_to_numpy(img):
return sitk.GetArrayFromImage(img) # z, y, x
def run_napari_viewer():
viewer = napari.Viewer(title="Radiuma Mini - Viewer")
# Load outputs if they exist
filtered = Path("app/storage/artifacts/filtered.nii.gz")
mask = Path("data/masks/CT_AVM_mask.nii.gz")
if filtered.exists():
arr = sitk_to_numpy(sitk.ReadImage(str(filtered)))
viewer.add_image(arr, name="filtered", blending="translucent")
if mask.exists():
marr = sitk_to_numpy(sitk.ReadImage(str(mask)))
viewer.add_labels(marr, name="mask")
napari.run()
if __name__ == "__main__":
run_napari_viewer()
+368
View File
@@ -0,0 +1,368 @@
import SimpleITK as sitk
import numpy as np
import pandas as pd
import pysera
from NodeGraphQt import BaseNode
from PySide6.QtWidgets import QWidget, QVBoxLayout, QLineEdit, QTreeWidget, QTreeWidgetItem
from PySide6.QtCore import Qt
from PySide6.QtGui import QIcon
# -------------------------------
# Helpers for safe input resolution
# -------------------------------
def _resolve_array_from_input(node: BaseNode, port_index: int, upstream_attr_name: str):
"""
Try to get a NumPy array from node.get_input(port_index).
If it's a Port (NodeGraphQt Port), traverse to the connected upstream node
and read the given upstream_attr_name as a fallback payload.
"""
val = node.get_input(port_index)
# direct array
if isinstance(val, np.ndarray):
return val
# try to traverse port connections
try:
if hasattr(val, 'connected_ports'):
ports = val.connected_ports()
if ports:
upstream_node = ports[0].node()
upstream_val = getattr(upstream_node, upstream_attr_name, None)
if isinstance(upstream_val, np.ndarray):
return upstream_val
except Exception:
pass
return None
def _resolve_features_from_input(node: BaseNode, port_index: int, upstream_attr_name: str):
"""
Resolve features payload from input. Accepts pandas.DataFrame, dict, list, or str.
If input returns a Port, read upstream node attribute.
"""
val = node.get_input(port_index)
# direct usable types
if hasattr(val, "to_csv"): # pandas DataFrame
return val
if isinstance(val, (dict, list, str)):
return val
# fallback via upstream
try:
if hasattr(val, 'connected_ports'):
ports = val.connected_ports()
if ports:
upstream_node = ports[0].node()
upstream_val = getattr(upstream_node, upstream_attr_name, None)
if hasattr(upstream_val, "to_csv") or isinstance(upstream_val, (dict, list, str)):
return upstream_val
except Exception:
pass
return None
# -------------------------------
# Node Definitions
# -------------------------------
class ImageReaderNode(BaseNode):
__identifier__ = 'image.io'
NODE_NAME = 'Image Reader'
def __init__(self):
super().__init__()
self.add_output('image')
self.add_output('mask')
# store payloads for upstream fallback resolution
self._image_array = None
self._mask_array = None
def run(self):
try:
image_path = r"C:/Users/Omen16/Documents/Radiuma_Mini/data/images/CT_AVM.nii.gz"
mask_path = r"C:/Users/Omen16/Documents/Radiuma_Mini/data/masks/CT_AVM_mask.nii.gz"
image = sitk.ReadImage(image_path)
mask = sitk.ReadImage(mask_path)
img_arr = sitk.GetArrayFromImage(image)
msk_arr = sitk.GetArrayFromImage(mask)
# save locally and on ports
self._image_array = img_arr
self._mask_array = msk_arr
print("Reader: loaded image & mask.")
self.set_output(0, img_arr)
self.set_output(1, msk_arr)
except Exception as e:
print("Reader error:", e)
class ImageWriterNode(BaseNode):
__identifier__ = 'image.io'
NODE_NAME = 'Image Writer'
def __init__(self):
super().__init__()
self.add_input('image')
self.add_input('features')
def run(self):
# resolve image array robustly (if input is a Port, traverse upstream)
image_array = _resolve_array_from_input(self, 0, '_image_array')
features = _resolve_features_from_input(self, 1, '_features')
# image save
if isinstance(image_array, np.ndarray) and image_array.size > 0:
try:
sitk_image = sitk.GetImageFromArray(image_array)
sitk.WriteImage(sitk_image, "output_image.nii.gz")
print("Writer: image saved.")
except Exception as e:
print("Writer image save error:", e)
else:
print("Writer: no image to save.")
# features save
if features is not None:
try:
if hasattr(features, "to_csv"):
features.to_csv("features.csv", index=False)
elif isinstance(features, dict):
pd.DataFrame([features]).to_csv("features.csv", index=False)
elif isinstance(features, list):
pd.DataFrame(features).to_csv("features.csv", index=False)
else:
pd.DataFrame([{"features": str(features)}]).to_csv("features.csv", index=False)
print("Writer: features saved.")
except Exception as e:
print("Writer features save error:", e)
else:
print("Writer: no features to save.")
class ImageFilterNode(BaseNode):
__identifier__ = 'image.proc'
NODE_NAME = 'Image Filter'
def __init__(self):
super().__init__()
self.add_input('image')
self.add_output('filtered_image')
self._filtered_array = None
def run(self):
# resolve input image array robustly
image_array = _resolve_array_from_input(self, 0, '_image_array')
if not isinstance(image_array, np.ndarray) or image_array.size == 0:
print("Filter: no valid image.")
return
try:
image = sitk.GetImageFromArray(image_array)
filtered = sitk.SmoothingRecursiveGaussian(image, sigma=2.0)
filtered_arr = sitk.GetArrayFromImage(filtered)
self._filtered_array = filtered_arr
self.set_output(0, filtered_arr)
print("Filter: applied Gaussian smoothing.")
except Exception as e:
print("Filter error:", e)
class ImageRegistrationNode(BaseNode):
__identifier__ = 'image.proc'
NODE_NAME = 'Image Registration'
def __init__(self):
super().__init__()
self.add_input('fixed_image')
self.add_input('moving_image')
self.add_output('registered_image')
self._registered_array = None
def run(self):
fixed_array = _resolve_array_from_input(self, 0, '_image_array')
moving_array = _resolve_array_from_input(self, 1, '_image_array')
if not isinstance(fixed_array, np.ndarray) or not isinstance(moving_array, np.ndarray):
print("Registration: invalid inputs.")
return
try:
fixed = sitk.GetImageFromArray(fixed_array)
moving = sitk.GetImageFromArray(moving_array)
registration = sitk.ImageRegistrationMethod()
registration.SetMetricAsMeanSquares()
registration.SetOptimizerAsGradientDescent(
learningRate=1.0,
numberOfIterations=50
)
registration.SetInterpolator(sitk.sitkLinear)
initial_transform = sitk.CenteredTransformInitializer(
fixed,
moving,
sitk.Euler3DTransform(),
sitk.CenteredTransformInitializerFilter.GEOMETRY
)
registration.SetInitialTransform(initial_transform, inPlace=False)
final_transform = registration.Execute(fixed, moving)
registered = sitk.Resample(
moving, fixed, final_transform,
sitk.sitkLinear, 0.0, moving.GetPixelID()
)
reg_arr = sitk.GetArrayFromImage(registered)
self._registered_array = reg_arr
self.set_output(0, reg_arr)
print("Registration: complete.")
except Exception as e:
print("Registration error:", e)
class ImageFusionNode(BaseNode):
__identifier__ = 'image.proc'
NODE_NAME = 'Image Fusion'
def __init__(self):
super().__init__()
self.add_input('image_A')
self.add_input('image_B')
self.add_output('fused_image')
self._fused_array = None
def run(self):
img_a = _resolve_array_from_input(self, 0, '_image_array')
img_b = _resolve_array_from_input(self, 1, '_image_array')
if not isinstance(img_a, np.ndarray) or not isinstance(img_b, np.ndarray):
print("Fusion: invalid inputs.")
return
try:
fused = (img_a.astype(np.float32) * 0.5 + img_b.astype(np.float32) * 0.5)
self._fused_array = fused
self.set_output(0, fused)
print("Fusion: complete.")
except Exception as e:
print("Fusion error:", e)
class FeatureExtractionNode(BaseNode):
__identifier__ = 'analysis'
NODE_NAME = 'Feature Extraction'
def __init__(self):
super().__init__()
self.add_input('image')
self.add_input('mask')
self.add_output('features')
self._features = None
def run(self):
image_array = _resolve_array_from_input(self, 0, '_image_array')
mask_array = _resolve_array_from_input(self, 1, '_mask_array')
if not isinstance(image_array, np.ndarray) or not isinstance(mask_array, np.ndarray):
print("Extraction: invalid image/mask.")
return
try:
result = pysera.process_batch(
image_input=image_array,
mask_input=mask_array,
output_path="./results",
categories="all",
dimensions="3d",
apply_preprocessing=True
)
features = result.get("features_extracted")
self._features = features
self.set_output(0, features)
# End of processing report
print(f"Extraction finished. Success={result.get('success')}, "
f"Features={len(features) if features is not None else 0}, "
f"Time={result.get('processing_time')}s")
except Exception as e:
print("PySERA error:", e)
# -------------------------------
# Node Catalog Widget
# -------------------------------
class NodeCatalog(QWidget):
def __init__(self, graph):
super().__init__()
self.graph = graph
self.node_count = 0
layout = QVBoxLayout(self)
self.search_bar = QLineEdit()
self.search_bar.setPlaceholderText("Search nodes...")
layout.addWidget(self.search_bar)
self.tree = QTreeWidget()
self.tree.setHeaderHidden(True)
layout.addWidget(self.tree)
self.categories = {
"Image I/O": [
(ImageReaderNode, "icons/camera.png"),
(ImageWriterNode, "icons/save.png")
],
"Image Processing": [
(ImageFilterNode, "icons/filter.png"),
(ImageRegistrationNode, "icons/transform.png"),
(ImageFusionNode, "icons/fusion.png")
],
"Feature Extraction": [
(FeatureExtractionNode, "icons/chart.png")
]
}
self.populate_tree()
self.search_bar.textChanged.connect(self.filter_nodes)
self.tree.itemDoubleClicked.connect(self.add_node)
def populate_tree(self):
self.tree.clear()
for category, nodes in self.categories.items():
cat_item = QTreeWidgetItem([category])
self.tree.addTopLevelItem(cat_item)
for node_cls, icon_path in nodes:
node_item = QTreeWidgetItem([node_cls.NODE_NAME])
node_item.setData(0, Qt.UserRole, node_cls)
node_item.setIcon(0, QIcon(icon_path))
cat_item.addChild(node_item)
def filter_nodes(self, text):
text = text.lower()
for i in range(self.tree.topLevelItemCount()):
cat_item = self.tree.topLevelItem(i)
visible_cat = False
for j in range(cat_item.childCount()):
node_item = cat_item.child(j)
node_name = node_item.text(0).lower()
is_match = text in node_name
node_item.setHidden(not is_match)
if is_match:
visible_cat = True
cat_item.setHidden(not visible_cat)
def add_node(self, item, column):
node_cls = item.data(0, Qt.UserRole)
if node_cls:
node = node_cls()
# grid placement to avoid overlap
spacing_x, spacing_y = 200, 120
x = spacing_x * (self.node_count % 4)
y = spacing_y * (self.node_count // 4)
self.graph.add_node(node, pos=(x, y))
self.node_count += 1
+133
View File
@@ -0,0 +1,133 @@
from PySide6.QtWidgets import (
QMessageBox, QGraphicsRectItem, QGraphicsTextItem,
QPushButton, QGraphicsProxyWidget, QTableWidget,
QTableWidgetItem, QDialog, QVBoxLayout
)
from PySide6.QtGui import QBrush, QColor
from pathlib import Path
import napari
import json
from .port_item import PortItem # Import PortItem with type support
class NodeWidget(QGraphicsRectItem):
def __init__(self, title: str, inputs: list[tuple[str, str]], outputs: list[tuple[str, str]]):
"""
inputs/outputs: list of (label, port_type)
Example:
inputs=[("Image", "Image"), ("Mask", "Mask")]
outputs=[("Features", "Features")]
"""
super().__init__(0, 0, 160, 90)
self.setBrush(QBrush(QColor("#e0e0e0")))
self.title = QGraphicsTextItem(title, self)
self.title.setPos(10, 5)
self.status_text = QGraphicsTextItem("", self)
self.status_text.setPos(10, 40)
# Create input ports
self.input_ports = []
y_offset = 20
for label, port_type in inputs:
port = PortItem(label, port_type, is_input=True, parent=self)
port.setPos(0, y_offset)
self.input_ports.append(port)
y_offset += 20
# Create output ports
self.output_ports = []
y_offset = 20
for label, port_type in outputs:
port = PortItem(label, port_type, is_input=False, parent=self)
port.setPos(150, y_offset)
self.output_ports.append(port)
y_offset += 20
# Output Display Button
self.btn_view = QPushButton("View Output")
proxy = QGraphicsProxyWidget(self)
proxy.setWidget(self.btn_view)
proxy.setPos(10, 60)
self.btn_view.clicked.connect(self.view_output)
def set_status(self, status: str):
# Change node color and status text based on execution state
if status == "running":
self.setBrush(QBrush(QColor("yellow")))
self.status_text.setPlainText("⏳ Running")
elif status == "success":
self.setBrush(QBrush(QColor("lightgreen")))
self.status_text.setPlainText("✔ Success")
elif status == "failed":
self.setBrush(QBrush(QColor("red")))
self.status_text.setPlainText("❌ Failed")
else:
self.setBrush(QBrush(QColor("#e0e0e0")))
self.status_text.setPlainText("")
def view_output(self):
node_type = self.title.toPlainText()
# Image Reader Or Image Filter → Processed Image
if node_type in ["Image Reader", "Image Filter"]:
file_path = Path("artifacts/processed_image.nii.gz")
if file_path.exists():
viewer = napari.Viewer()
viewer.open(str(file_path))
else:
QMessageBox.warning(None, "Output not found", f"No output image for {node_type}")
# Mask Reader → Mask
elif node_type == "Mask Reader":
file_path = Path("artifacts/mask.nii.gz")
if file_path.exists():
viewer = napari.Viewer()
viewer.open(str(file_path))
else:
QMessageBox.warning(None, "Output not found", "No mask file found")
# Image Registration → Registered image
elif node_type == "Image Registration":
file_path = Path("artifacts/registered_image.nii.gz")
if file_path.exists():
viewer = napari.Viewer()
viewer.open(str(file_path))
else:
QMessageBox.warning(None, "Output not found", "No registered image found")
# Image Fusion → Composite Image
elif node_type == "Image Fusion":
file_path = Path("artifacts/fused_image.nii.gz")
if file_path.exists():
viewer = napari.Viewer()
viewer.open(str(file_path))
else:
QMessageBox.warning(None, "Output not found", "No fused image found")
# Image Extraction Or PySERA Extract → Features table
elif node_type in ["Image Extraction", "PySERA Extract"]:
file_path = Path("artifacts/features.json")
if file_path.exists():
features = json.loads(file_path.read_text())
dialog = QDialog()
dialog.setWindowTitle("Radiomics Features")
layout = QVBoxLayout(dialog)
table = QTableWidget()
table.setRowCount(len(features))
table.setColumnCount(2)
table.setHorizontalHeaderLabels(["Feature", "Value"])
for i, (key, value) in enumerate(features.items()):
table.setItem(i, 0, QTableWidgetItem(key))
table.setItem(i, 1, QTableWidgetItem(str(value)))
layout.addWidget(table)
dialog.exec()
else:
QMessageBox.warning(None, "Output not found", "No features.json found")
# Writer → Final Output File
elif node_type in ["Writer", "Image Writer"]:
file_path = Path("artifacts/output.nii.gz")
if file_path.exists():
QMessageBox.information(None, "Writer Output", f"Output file saved: {file_path}")
else:
QMessageBox.warning(None, "Output not found", "No output file found")
+14
View File
@@ -0,0 +1,14 @@
from PySide6.QtWidgets import QGraphicsEllipseItem, QGraphicsTextItem
from PySide6.QtGui import QBrush, QColor
class PortItem(QGraphicsEllipseItem):
def __init__(self, label: str, port_type: str, is_input: bool, parent=None):
super().__init__(-5, -5, 10, 10, parent)
# Color based on input/output
self.setBrush(QBrush(QColor("green") if is_input else QColor("blue")))
self.label = QGraphicsTextItem(label, parent)
self.label.setDefaultTextColor(QColor("black"))
self.label.setPos(10 if is_input else -50, -7)
self.is_input = is_input
# Port type (e.g. "Image", "Mask", "Features")
self.port_type = port_type
@@ -0,0 +1,53 @@
import sys
import json
from pathlib import Path
from PySide6.QtWidgets import QApplication, QWidget, QVBoxLayout, QPushButton, QLabel, QTableWidget, QTableWidgetItem
class FeatureViewer(QWidget):
def __init__(self):
super().__init__()
self.setWindowTitle("Radiomics Features Viewer")
layout = QVBoxLayout(self)
self.status = QLabel("Load features.json to view results")
self.table = QTableWidget()
self.btn_load = QPushButton("Load Features")
layout.addWidget(self.status)
layout.addWidget(self.table)
layout.addWidget(self.btn_load)
self.btn_load.clicked.connect(self.load_features)
def load_features(self):
features_path = Path("artifacts/features.json")
if not features_path.exists():
self.status.setText("No features.json found! Run workflow first.")
return
try:
features = json.loads(features_path.read_text())
except Exception as e:
self.status.setText(f"Error reading features.json: {e}")
return
# Displaying features in a table
self.table.setRowCount(len(features))
self.table.setColumnCount(2)
self.table.setHorizontalHeaderLabels(["Feature", "Value"])
for i, (key, value) in enumerate(features.items()):
self.table.setItem(i, 0, QTableWidgetItem(key))
self.table.setItem(i, 1, QTableWidgetItem(str(value)))
self.status.setText("Features loaded successfully!")
def run_gui():
app = QApplication(sys.argv)
viewer = FeatureViewer()
viewer.resize(500, 400)
viewer.show()
sys.exit(app.exec())
if __name__ == "__main__":
run_gui()
+61
View File
@@ -0,0 +1,61 @@
import sys
from pathlib import Path
from PySide6.QtWidgets import QApplication, QWidget, QVBoxLayout, QPushButton, QLabel, QFileDialog
from app.engine.dagster_runner import run_workflow_with_config
# Write Your Code Here:
class MainWindow(QWidget):
def __init__(self):
super().__init__()
self.setWindowTitle("Radiuma Mini - Workflow Runner")
layout = QVBoxLayout(self)
self.status = QLabel("Idle")
self.btn_select_image = QPushButton("Select Image File")
self.btn_select_mask = QPushButton("Select Mask File")
self.btn_run = QPushButton("Run Workflow")
layout.addWidget(self.status)
layout.addWidget(self.btn_select_image)
layout.addWidget(self.btn_select_mask)
layout.addWidget(self.btn_run)
self.image_file = None
self.mask_file = None
self.btn_select_image.clicked.connect(self.select_image)
self.btn_select_mask.clicked.connect(self.select_mask)
self.btn_run.clicked.connect(self.run_workflow)
def select_image(self):
file_path, _ = QFileDialog.getOpenFileName(self, "Select Image", "data/images", "NIfTI files (*.nii.gz)")
if file_path:
self.image_file = Path(file_path).name
self.status.setText(f"Selected image: {self.image_file}")
def select_mask(self):
file_path, _ = QFileDialog.getOpenFileName(self, "Select Mask", "data/masks", "NIfTI files (*.nii.gz)")
if file_path:
self.mask_file = Path(file_path).name
self.status.setText(f"Selected mask: {self.mask_file}")
def run_workflow(self):
if not self.image_file or not self.mask_file:
self.status.setText("Please select both image and mask files!")
return
self.status.setText("Running workflow...")
result = run_workflow_with_config(self.image_file, self.mask_file)
if result.success:
self.status.setText("Workflow completed successfully!")
else:
self.status.setText("Workflow failed!")
def run_gui():
app = QApplication(sys.argv)
w = MainWindow()
w.resize(400, 200)
w.show()
sys.exit(app.exec())
if __name__ == "__main__":
run_gui()
+179
View File
@@ -0,0 +1,179 @@
import sys
import datetime
from PySide6.QtWidgets import (
QApplication, QMainWindow, QSplitter, QTextEdit, QPushButton, QWidget, QVBoxLayout
)
from PySide6.QtCore import Qt
from PySide6.QtGui import QColor, QTextCharFormat, QTextCursor
from NodeGraphQt import NodeGraph
from NodeGraphQt.errors import NodeRegistrationError
from dagster import DagsterInstance, execute_job
import yaml
from dagster_radiuma.jobs import radiomics_job, radiomics_batch_job
from app.gui.node_catalog import (
NodeCatalog,
ImageReaderNode,
ImageWriterNode,
ImageRegistrationNode,
ImageFilterNode,
ImageFusionNode,
FeatureExtractionNode
)
from app.gui.gui_controls import WorkflowControls
class VisualEditor(QMainWindow):
def __init__(self):
super().__init__()
self.setWindowTitle("Radiuma Workflow Editor")
self.resize(1200, 800)
# Graph
self.graph = NodeGraph()
# Register nodes with unique alias: identifier + ClassName
def reg(cls):
key = f"{cls.__identifier__}.{cls.__name__}"
try:
self.graph.register_node(cls, alias=key)
except NodeRegistrationError:
pass
reg(ImageReaderNode)
reg(ImageWriterNode)
reg(ImageRegistrationNode)
reg(ImageFilterNode)
reg(ImageFusionNode)
reg(FeatureExtractionNode)
# Viewer
self.viewer = self.graph.widget
# Node catalog
self.catalog = NodeCatalog(self.graph)
# Log panel
self.log_panel = QTextEdit()
self.log_panel.setReadOnly(True)
self.log_panel.setPlaceholderText("Logs will appear here...")
# Workflow controls (Start/Stop/Resume/Cancel)
self.controls = WorkflowControls(self.log_panel, self.log_panel)
# اتصال متدهای کنترل به Dagster
self.controls.start_btn.clicked.connect(self.start_workflow)
self.controls.stop_btn.clicked.connect(self.stop_workflow)
self.controls.resume_btn.clicked.connect(self.resume_workflow)
self.controls.cancel_btn.clicked.connect(self.cancel_workflow)
# Left panel layout
left_panel = QWidget()
left_layout = QVBoxLayout(left_panel)
left_layout.addWidget(self.catalog)
left_layout.addWidget(self.controls)
# Splitters
top_splitter = QSplitter(Qt.Horizontal)
top_splitter.addWidget(left_panel)
top_splitter.addWidget(self.viewer)
main_splitter = QSplitter(Qt.Vertical)
main_splitter.addWidget(top_splitter)
main_splitter.addWidget(self.log_panel)
main_splitter.setSizes([600, 200])
self.setCentralWidget(main_splitter)
# Dagster instance
self.instance = DagsterInstance.ephemeral()
self.current_run_id = None
# Initial log
self.log("Radiuma Workflow Editor started.", "INFO")
self.log("Custom nodes registered with alias: identifier + ClassName.", "INFO")
def log(self, message, level="INFO"):
fmt = QTextCharFormat()
if level == "INFO":
fmt.setForeground(QColor("green"))
elif level == "WARNING":
fmt.setForeground(QColor("orange"))
elif level == "ERROR":
fmt.setForeground(QColor("red"))
else:
fmt.setForeground(QColor("black"))
timestamp = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
cursor = self.log_panel.textCursor()
cursor.movePosition(QTextCursor.End)
cursor.insertText(f"[{timestamp}] [{level}] {message}\n", fmt)
self.log_panel.setTextCursor(cursor)
# --- کنترل اجرای Dagster ---
import yaml
def start_workflow(self):
# Log start of workflow from GUI
self.log("[GUI] Start → Dagster job starting...", "INFO")
try:
# Load configuration from configs.yaml
with open("configs.yaml", "r", encoding="utf-8") as f:
run_config = yaml.safe_load(f)
# Check which job type is defined in the config
if "radiomics_batch_job" in run_config:
# Batch mode → run multiple cases automatically
self.log("[GUI] Batch mode detected → Running Batch Workflow", "INFO")
result = radiomics_batch_job.execute_in_process(
run_config=run_config["radiomics_batch_job"],
instance=self.instance
)
elif "radiomics_job" in run_config:
# Single case mode → run one case only
self.log("[GUI] Single case detected → Running Single Workflow", "INFO")
result = radiomics_job.execute_in_process(
run_config=run_config["radiomics_job"],
instance=self.instance
)
else:
# Config file does not contain required job section
self.log("[GUI] Config file missing required job section.", "ERROR")
return
# Save run_id and log success/failure
self.current_run_id = result.run_id
self.log(f"[GUI] Workflow finished. run_id={self.current_run_id} Success={result.success}", "INFO")
except Exception as e:
# Log any unexpected errors
self.log(f"[GUI] Workflow error: {e}", "ERROR")
def stop_workflow(self):
if self.current_run_id:
self.instance.cancel_run(self.current_run_id)
self.log(f"[GUI] Workflow stopped. run_id={self.current_run_id}", "WARNING")
def resume_workflow(self):
if self.current_run_id:
self.instance.resume_run(self.current_run_id)
self.log(f"[GUI] Workflow resumed. run_id={self.current_run_id}", "INFO")
def cancel_workflow(self):
if self.current_run_id:
self.instance.cancel_run(self.current_run_id)
self.log(f"[GUI] Workflow cancelled. run_id={self.current_run_id}", "ERROR")
def main():
app = QApplication(sys.argv)
window = VisualEditor()
window.show()
sys.exit(app.exec())
if __name__ == '__main__':
main()
+42
View File
@@ -0,0 +1,42 @@
import yaml
from dagster_radiuma.jobs import radiomics_job, radiomics_batch_job
import app.gui.visual_editor as visual_editor
def run_radiomics_pipeline(config_file: str):
# Load YAML config
with open(config_file, "r", encoding="utf-8") as f:
run_config = yaml.safe_load(f)
# Check which job to run
if "radiomics_batch_job" in run_config:
print("[INFO] Batch mode detected → Running Batch Workflow")
result = radiomics_batch_job.execute_in_process(
run_config=run_config["radiomics_batch_job"]
)
print(f"[RESULT] Batch workflow success = {result.success}")
elif "radiomics_job" in run_config:
print("[INFO] Single case detected → Running Single Workflow")
result = radiomics_job.execute_in_process(
run_config=run_config["radiomics_job"]
)
print(f"[RESULT] Single workflow success = {result.success}")
else:
raise ValueError(
"Config file must contain either 'radiomics_job' or 'radiomics_batch_job' section."
)
if __name__ == "__main__":
print("[INFO] Radiuma Workflow Editor started.")
use_gui = True #
if use_gui:
visual_editor.main()
else:
run_radiomics_pipeline("configs.yaml")
+27
View File
@@ -0,0 +1,27 @@
from app.engine.core import Node
from app.nodes.imaging import image_reader_fn, image_registration_fn, image_filter_fn, image_writer_fn
def build_nodes():
return [
Node("image_reader", image_reader_fn, inputs=[], outputs=["image_path"]),
Node("image_registration", image_registration_fn, inputs=["image_path"], outputs=["registered_path"]),
Node("image_filter", image_filter_fn, inputs=["registered_path"], outputs=["filtered_path"]),
Node("image_writer", image_writer_fn, inputs=["filtered_path"], outputs=["final_output_path"]),
]
# from app.engine.core import Node
# from app.nodes.imaging import image_reader_fn, image_registration_fn, image_filter_fn, image_writer_fn
# from app.features.pysera_node import pysera_extract_fn
# def build_nodes():
# return [
# Node("image_reader", image_reader_fn, [], ["image_path"]),
# Node("image_registration", image_registration_fn, ["image_path"], ["registered_path"]),
# Node("image_filter", image_filter_fn, ["registered_path"], ["filtered_path"]),
# Node("pysera_extract", pysera_extract_fn, ["filtered_path"], ["features_path"]),
# Node("image_writer", image_writer_fn, ["filtered_path"], ["final_output_path"]),
# ]
+54
View File
@@ -0,0 +1,54 @@
from pathlib import Path
import numpy as np
import SimpleITK as sitk
ARTIFACTS = Path("app/storage/artifacts")
ARTIFACTS.mkdir(parents=True, exist_ok=True)
def image_reader_fn(state):
# Decide input file (DICOM series or NIfTI)
input_image_path = Path("data/images/CT_AVM.nii.gz") # adapt as needed
img = sitk.ReadImage(str(input_image_path))
out_path = ARTIFACTS / "image_reader.nii.gz"
sitk.WriteImage(img, str(out_path))
return {"image_path": str(out_path)}
def image_registration_fn(state):
fixed_path = state.get_outputs("image_reader").get("image_path")
moving_path = fixed_path # demo: register image to itself; replace with real moving image
fixed = sitk.ReadImage(fixed_path)
moving = sitk.ReadImage(moving_path)
# Simple rigid registration (demo)
registration_method = sitk.ImageRegistrationMethod()
registration_method.SetMetricAsMeanSquares()
registration_method.SetOptimizerAsRegularStepGradientDescent(
learningRate=1.0, minStep=1e-4, numberOfIterations=100
)
registration_method.SetInterpolator(sitk.sitkLinear)
initial_transform = sitk.CenteredTransformInitializer(
fixed, moving, sitk.Euler3DTransform(), sitk.CenteredTransformInitializerFilter.GEOMETRY
)
registration_method.SetInitialTransform(initial_transform, inPlace=False)
final_transform = registration_method.Execute(fixed, moving)
resampled = sitk.Resample(moving, fixed, final_transform, sitk.sitkLinear, 0.0, moving.GetPixelID())
out_path = ARTIFACTS / "registered.nii.gz"
sitk.WriteImage(resampled, str(out_path))
return {"registered_path": str(out_path), "transform": "euler3d"}
def image_filter_fn(state):
reg_path = state.get_outputs("image_registration").get("registered_path")
img = sitk.ReadImage(reg_path)
# Example filter: Gaussian smoothing
filtered = sitk.DiscreteGaussian(img, variance=1.5)
out_path = ARTIFACTS / "filtered.nii.gz"
sitk.WriteImage(filtered, str(out_path))
return {"filtered_path": str(out_path)}
def image_writer_fn(state):
# Writer here is just saving; already done by previous steps; simulate final export
filtered_path = state.get_outputs("image_filter").get("filtered_path")
final_path = ARTIFACTS / "final_output.nii.gz"
Path(final_path).write_bytes(Path(filtered_path).read_bytes())
return {"final_output_path": str(final_path)}
@@ -0,0 +1,13 @@
{
"shape": [
58,
248,
175
],
"spacing": [
0.8125,
0.8125,
2.3970494270324707
],
"intensity_sum": 95678796.0
}
@@ -0,0 +1,9 @@
{
"voxel_spacing": [
0.8125,
0.8125,
2.3970494270324707
],
"mask_voxels": 2517200,
"mask_border_voxels": 0
}
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,30 @@
{
"steps": {
"image_reader": {
"status": "success",
"outputs": {
"image_path": "app\\storage\\artifacts\\image_reader.nii.gz"
}
},
"image_registration": {
"status": "success",
"outputs": {
"registered_path": "app\\storage\\artifacts\\registered.nii.gz",
"transform": "euler3d"
}
},
"image_filter": {
"status": "success",
"outputs": {
"filtered_path": "app\\storage\\artifacts\\filtered.nii.gz"
}
},
"image_writer": {
"status": "success",
"outputs": {
"final_output_path": "app\\storage\\artifacts\\final_output.nii.gz"
}
}
},
"cancelled": true
}