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
@@ -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()