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
+22
View File
@@ -0,0 +1,22 @@
# Readme For ***Radiuma_Luigi***
This section contains research and development (R&D) projects related to the **Luigi** tool.
Luigi is a lightweight and simple workflow engine for building data and scientific pipelines that focuses on reproducibility and dependency management.
## Project Goals
- Design simple and executable pipelines in the desktop environment
- Review Luigi's capabilities for managing dependencies and parallel execution
- Test the reproducibility of results and recording artifacts for scientific analysis
- Compare Luigi's performance with other tools (such as Galaxy and Dagster)
## Folder Structure
- `tasks/` → Contains codes related to task definitions
- `examples/` → Simple examples for testing and execution
- `logs/` → Outputs and logs related to pipeline execution
- `README.md` → General description of the project and how to run
## How to run
1. Install Luigi:
```bash
pip install luigi
python run_pipeline.py RadiumaPipeline --local-scheduler
Binary file not shown.
Binary file not shown.
View File
+20
View File
@@ -0,0 +1,20 @@
import os
def flags_dir(workspace: str) -> str:
d = os.path.join(workspace, "_flags")
os.makedirs(d, exist_ok=True)
return d
def flag_path(workspace: str, name: str) -> str:
return os.path.join(flags_dir(workspace), name)
def is_set(workspace: str, name: str) -> bool:
return os.path.exists(flag_path(workspace, name))
def set_flag(workspace: str, name: str) -> None:
open(flag_path(workspace, name), "a").close()
def clear_flag(workspace: str, name: str) -> None:
p = flag_path(workspace, name)
if os.path.exists(p):
os.remove(p)
+19
View File
@@ -0,0 +1,19 @@
import luigi
import time
from engine.tasks_writer import ImageWriter
class RadiumaPipeline(luigi.WrapperTask):
artifacts_dir = luigi.Parameter(default="artifacts")
def requires(self):
# start timer at the very beginning
self.start_time = time.time()
return ImageWriter(artifacts_dir=self.artifacts_dir)
def run(self):
elapsed = time.time() - self.start_time
hours, rem = divmod(elapsed, 3600)
minutes, seconds = divmod(rem, 60)
centiseconds = int((seconds - int(seconds)) * 100)
print(f"[Pipeline] total execution time: {int(hours):02}:{int(minutes):02}:{int(seconds):02}.{centiseconds:02}")
print("=== Workflow completed successfully ===")
+26
View File
@@ -0,0 +1,26 @@
import os, time, json, hashlib
from typing import Iterable
def sha256_file(fp: str) -> str:
h = hashlib.sha256()
with open(fp, "rb") as f:
for chunk in iter(lambda: f.read(1<<20), b""):
h.update(chunk)
return h.hexdigest()
def write_sidecar(outputs: Iterable, params: dict) -> None:
outs = list(outputs)
if not outs: return
prov_path = os.path.join(os.path.dirname(outs[0].path), "_provenance.json")
meta = {
"params": params,
"tool": {"name": "Radiuma-Luigi", "version": params.get("tool_version", "0.1.0")},
"timestamps": {"finished_at": time.strftime("%Y-%m-%d %H:%M:%S")},
"checksums": {}
}
for o in outs:
p = o.path
if os.path.exists(p):
meta["checksums"][os.path.basename(p)] = sha256_file(p)
with open(prov_path, "w", encoding="utf-8") as f:
json.dump(meta, f, indent=2)
+29
View File
@@ -0,0 +1,29 @@
import os
import luigi
import SimpleITK as sitk
from pathlib import Path
class ImageConversion(luigi.Task):
artifacts_dir = luigi.Parameter(default="artifacts")
def requires(self):
from engine.tasks_fusion import ImageFusion
return ImageFusion(artifacts_dir=self.artifacts_dir)
def output(self):
return luigi.LocalTarget(os.path.join(self.artifacts_dir, "converted_index.txt"))
def run(self):
fused_index = os.path.join(self.artifacts_dir, "fused_index.txt")
with open(fused_index, "r") as f:
fused_paths = [line.strip() for line in f if line.strip()]
converted_paths = []
for img_path in fused_paths:
img = sitk.ReadImage(img_path)
out_path = Path(self.artifacts_dir) / f"converted_{Path(img_path).name}"
sitk.WriteImage(img, str(out_path))
converted_paths.append(str(out_path))
with self.output().open("w") as f:
f.write("\n".join(converted_paths))
+31
View File
@@ -0,0 +1,31 @@
import os
import luigi
import SimpleITK as sitk
from pathlib import Path
class ImageFilter(luigi.Task):
artifacts_dir = luigi.Parameter(default="artifacts")
sigma = luigi.FloatParameter(default=1.0)
def requires(self):
from engine.tasks_conversion import ImageConversion
return ImageConversion(artifacts_dir=self.artifacts_dir)
def output(self):
return luigi.LocalTarget(os.path.join(self.artifacts_dir, "filtered_index.txt"))
def run(self):
conv_index = os.path.join(self.artifacts_dir, "converted_index.txt")
with open(conv_index, "r") as f:
conv_paths = [line.strip() for line in f if line.strip()]
filtered_paths = []
for img_path in conv_paths:
img = sitk.ReadImage(img_path)
filtered_img = sitk.SmoothingRecursiveGaussian(img, sigma=self.sigma)
out_path = Path(self.artifacts_dir) / f"filtered_{Path(img_path).name}"
sitk.WriteImage(filtered_img, str(out_path))
filtered_paths.append(str(out_path))
with self.output().open("w") as f:
f.write("\n".join(filtered_paths))
+36
View File
@@ -0,0 +1,36 @@
import os
import luigi
import numpy as np
import SimpleITK as sitk
from pathlib import Path
class ImageFusion(luigi.Task):
artifacts_dir = luigi.Parameter(default="artifacts")
def requires(self):
from engine.tasks_registration import ImageRegistration
return ImageRegistration(artifacts_dir=self.artifacts_dir)
def output(self):
return luigi.LocalTarget(os.path.join(self.artifacts_dir, "fused_index.txt"))
def run(self):
reg_index = os.path.join(self.artifacts_dir, "registered_index.txt")
with open(reg_index, "r") as f:
reg_paths = [line.strip() for line in f if line.strip()]
fused_paths = []
for img_path in reg_paths:
img = sitk.ReadImage(img_path)
arr = sitk.GetArrayFromImage(img)
p5, p95 = np.percentile(arr, [5, 95])
arr = np.clip(arr, p5, p95)
arr = (arr - p5) / (p95 - p5) if p95 > p5 else arr * 0.0
fused_img = sitk.GetImageFromArray(arr)
fused_img.CopyInformation(img)
out_path = Path(self.artifacts_dir) / f"fused_{Path(img_path).name}"
sitk.WriteImage(fused_img, str(out_path))
fused_paths.append(str(out_path))
with self.output().open("w") as f:
f.write("\n".join(fused_paths))
+49
View File
@@ -0,0 +1,49 @@
import os
import luigi
import SimpleITK as sitk
from pathlib import Path
class MaskRegistration(luigi.Task):
artifacts_dir = luigi.Parameter(default="artifacts")
mask_dir = luigi.Parameter(default=os.path.join("data", "masks"))
def requires(self):
from engine.tasks_filter import ImageFilter
from engine.tasks_masks import AllMasks
return {
"filter": ImageFilter(artifacts_dir=self.artifacts_dir),
"masks": AllMasks(mask_dir=self.mask_dir)
}
def output(self):
return luigi.LocalTarget(os.path.join(self.artifacts_dir, "mask_registered_index.txt"))
def run(self):
filt_index = os.path.join(self.artifacts_dir, "filtered_index.txt")
with open(filt_index, "r") as f:
filtered_paths = [line.strip() for line in f if line.strip()]
mask_files = [os.path.join(self.mask_dir, f) for f in os.listdir(self.mask_dir) if f.endswith(".nii.gz")]
if not filtered_paths or not mask_files:
raise FileNotFoundError("Missing filtered images or masks for mask_registration.")
if len(filtered_paths) == len(mask_files):
pairs = zip(filtered_paths, mask_files)
else:
ref_path = filtered_paths[0]
pairs = [(ref_path, m) for m in mask_files]
out_paths = []
for ref_img_path, mask_path in pairs:
ref_img = sitk.ReadImage(ref_img_path)
mask_img = sitk.ReadImage(mask_path)
identity = sitk.Transform(ref_img.GetDimension(), sitk.sitkIdentity)
resampled_mask = sitk.Resample(
mask_img, ref_img, identity, sitk.sitkNearestNeighbor, 0, mask_img.GetPixelID()
)
out_path = Path(self.artifacts_dir) / f"mask_registered_{Path(mask_path).name}"
sitk.WriteImage(resampled_mask, str(out_path))
out_paths.append(str(out_path))
with self.output().open("w") as f:
f.write("\n".join(out_paths))
+17
View File
@@ -0,0 +1,17 @@
import os
import luigi
class AllMasks(luigi.Task):
mask_dir = luigi.Parameter(default=os.path.join("data", "masks"))
def output(self):
# Merely as a signal of completion
return luigi.LocalTarget(os.path.join("artifacts", "all_masks.done"))
def run(self):
files = [os.path.join(self.mask_dir, f) for f in os.listdir(self.mask_dir) if f.endswith(".nii.gz")]
if not files:
raise FileNotFoundError("No masks found in data/masks")
# Just make the done signal.
with self.output().open("w") as f:
f.write(f"{len(files)} masks discovered")
+107
View File
@@ -0,0 +1,107 @@
import luigi
import json
from pathlib import Path
import numpy as np
import SimpleITK as sitk
from engine.tasks_reader import ImageReader
from engine.utils import ensure_dir
class ImageFusion(luigi.Task):
image_file = luigi.Parameter()
mask_file = luigi.Parameter(default="")
workspace = luigi.Parameter(default="artifacts")
def requires(self):
return ImageReader(image_file=self.image_file, mask_file=self.mask_file, workspace=self.workspace)
def output(self):
out_dir = ensure_dir(Path(self.workspace) / "pipeline")
stem = Path(self.image_file).stem
return luigi.LocalTarget(str(out_dir / f"fusion_{stem}.json"))
def run(self):
# If we had the second modality, we would read here and stack the channels.
# For now, we're passing that single image along with the metadata.
payload = {"status": "fused", "modalities": 1, "image": str(self.image_file)}
with self.output().open("w") as f:
json.dump(payload, f, indent=2)
class ImageConversion(luigi.Task):
image_file = luigi.Parameter()
mask_file = luigi.Parameter(default="")
workspace = luigi.Parameter(default="artifacts")
def requires(self):
return ImageFusion(image_file=self.image_file, mask_file=self.mask_file, workspace=self.workspace)
def output(self):
out_dir = ensure_dir(Path(self.workspace) / "pipeline")
stem = Path(self.image_file).stem
return luigi.LocalTarget(str(out_dir / f"conversion_{stem}.json"))
def run(self):
# Convert to SimpleITK image for later steps
sitk_img = sitk.ReadImage(str(self.image_file))
# Type conversion/normalization
payload = {"status": "converted", "pixel_type": str(sitk_img.GetPixelIDTypeAsString())}
with self.output().open("w") as f:
json.dump(payload, f, indent=2)
class ImageFilter(luigi.Task):
image_file = luigi.Parameter()
mask_file = luigi.Parameter(default="")
workspace = luigi.Parameter(default="artifacts")
sigma = luigi.FloatParameter(default=1.0)
def requires(self):
return ImageConversion(image_file=self.image_file, mask_file=self.mask_file, workspace=self.workspace)
def output(self):
out_dir = ensure_dir(Path(self.workspace) / "pipeline")
stem = Path(self.image_file).stem
return luigi.LocalTarget(str(out_dir / f"filter_{stem}.json"))
def run(self):
img = sitk.ReadImage(str(self.image_file))
# Gaussian smoothing for ROI preparation
filtered = sitk.DiscreteGaussian(img, variance=self.sigma ** 2)
# Store Data
payload = {"status": "filtered", "sigma": self.sigma}
with self.output().open("w") as f:
json.dump(payload, f, indent=2)
class MaskRegistration(luigi.Task):
image_file = luigi.Parameter()
mask_file = luigi.Parameter(default="")
workspace = luigi.Parameter(default="artifacts")
def requires(self):
return ImageFilter(image_file=self.image_file, mask_file=self.mask_file, workspace=self.workspace)
def output(self):
out_dir = ensure_dir(Path(self.workspace) / "pipeline")
stem = Path(self.image_file).stem
return luigi.LocalTarget(str(out_dir / f"maskreg_{stem}.json"))
def run(self):
# If we have a mask, we register/resample to image space.
result = {"status": "mask_registered", "mask_available": bool(self.mask_file)}
if self.mask_file:
img = sitk.ReadImage(str(self.image_file))
msk = sitk.ReadImage(str(self.mask_file))
# Resample mask to image geometry
resampler = sitk.ResampleImageFilter()
resampler.SetReferenceImage(img)
resampler.SetInterpolator(sitk.sitkNearestNeighbor)
resampler.SetDefaultPixelValue(0)
msk_res = resampler.Execute(msk)
# Temporary storage of PySera results
tmp_dir = ensure_dir(Path(self.workspace) / "tmp")
out_mask = Path(tmp_dir) / f"regmask_{Path(self.image_file).stem}.nii.gz"
sitk.WriteImage(msk_res, str(out_mask))
result["registered_mask_path"] = str(out_mask)
with self.output().open("w") as f:
json.dump(result, f, indent=2)
+89
View File
@@ -0,0 +1,89 @@
import os
import time
import json
import luigi
from pathlib import Path
import pysera
from engine.utils import json_safe
class FeatureExtraction(luigi.Task):
artifacts_dir = luigi.Parameter(default="artifacts")
temp_dir = luigi.Parameter(default=r"C:\Users\Omen16\AppData\Local\ViSERA\res\memory\memmap\pysera_temp")
def requires(self):
from engine.tasks_filter import ImageFilter
from engine.tasks_maskreg import MaskRegistration
return {
"filter": ImageFilter(artifacts_dir=self.artifacts_dir),
"maskreg": MaskRegistration(artifacts_dir=self.artifacts_dir)
}
def output(self):
return luigi.LocalTarget(os.path.join(self.artifacts_dir, "radiomics_index.json"))
def run(self):
#
filt_index = os.path.join(self.artifacts_dir, "filtered_index.txt")
mask_index = os.path.join(self.artifacts_dir, "mask_registered_index.txt")
with open(filt_index, "r") as f:
filtered_paths = [line.strip() for line in f if line.strip()]
with open(mask_index, "r") as f:
mask_paths = [line.strip() for line in f if line.strip()]
results = []
for img, mask in zip(filtered_paths, mask_paths):
start = time.time()
result = pysera.process_batch(
image_input=img,
mask_input=mask,
output_path=self.artifacts_dir,
categories="diag,morph,glcm,glrlm,glszm,ngtdm,ngldm",
dimensions="1st,3D",
bin_size=25,
roi_num=2,
roi_selection_mode="per_region",
apply_preprocessing=True,
feature_value_mode="REAL_VALUE",
min_roi_volume=50,
enable_parallelism=True,
num_workers="4",
report="info",
temporary_files_path=str(self.temp_dir),
IBSI_based_parameters={
"radiomics_DataType": "CT",
"radiomics_DiscType": "FBS",
"radiomics_isScale": 0,
"radiomics_VoxInterp": "Nearest",
"radiomics_ROIInterp": "Nearest",
"radiomics_isotVoxSize": 2.0,
"radiomics_isotVoxSize2D": 2.0,
"radiomics_isIsot2D": 0,
"radiomics_isGLround": 0,
"radiomics_isReSegRng": 0,
"radiomics_isOutliers": 0,
"radiomics_isQuntzStat": 1,
"radiomics_ReSegIntrvl01": -1000,
"radiomics_ReSegIntrvl02": 400,
"radiomics_ROI_PV": 0.5,
"radiomics_qntz": "Uniform",
"radiomics_IVH_Type": 3,
"radiomics_IVH_DiscCont": 1,
"radiomics_IVH_binSize": 2.0,
},
)
elapsed = round(time.time() - start, 2)
safe_result = json_safe(result)
case_json = os.path.join(self.artifacts_dir, f"{Path(img).name}_radiomics.json")
with open(case_json, "w", encoding="utf-8") as f:
json.dump(safe_result, f, indent=2, ensure_ascii=False)
results.append({
"image": img,
"mask": mask,
"elapsed_seconds": elapsed,
"result_file": case_json,
})
with self.output().open("w") as f:
json.dump({"radiomics_results": results}, f, indent=2, ensure_ascii=False)
+28
View File
@@ -0,0 +1,28 @@
import os
import luigi
import SimpleITK as sitk
from pathlib import Path
from engine.utils import ensure_dir
class ImageReader(luigi.Task):
data_dir = luigi.Parameter(default=os.path.join("data", "images"))
artifacts_dir = luigi.Parameter(default="artifacts")
def output(self):
out_dir = ensure_dir(Path(self.artifacts_dir) / "reader")
return luigi.LocalTarget(str(out_dir / "reader_index.txt"))
def run(self):
reader_dir = ensure_dir(Path(self.artifacts_dir) / "reader")
files = [os.path.join(self.data_dir, f) for f in os.listdir(self.data_dir) if f.endswith(".nii.gz")]
if not files:
raise FileNotFoundError("No images found in data/images")
converted_paths = []
for path in files:
img = sitk.ReadImage(path)
img_float = sitk.Cast(img, sitk.sitkFloat32)
out_path = reader_dir / f"reader_{Path(path).name}"
sitk.WriteImage(img_float, str(out_path))
converted_paths.append(str(out_path))
with self.output().open("w") as f:
f.write("\n".join(converted_paths))
+73
View File
@@ -0,0 +1,73 @@
import os
import luigi
import SimpleITK as sitk
from pathlib import Path
from engine.utils import ensure_dir
def cast_to_float32(img: sitk.Image) -> sitk.Image:
return sitk.Cast(img, sitk.sitkFloat32)
def make_initial_transform(fixed: sitk.Image, moving: sitk.Image) -> sitk.Transform:
dim = fixed.GetDimension()
if dim == 2:
return sitk.CenteredTransformInitializer(
fixed, moving, sitk.Euler2DTransform(), sitk.CenteredTransformInitializerFilter.GEOMETRY
)
elif dim == 3:
return sitk.CenteredTransformInitializer(
fixed, moving, sitk.VersorRigid3DTransform(), sitk.CenteredTransformInitializerFilter.GEOMETRY
)
else:
raise RuntimeError(f"Unsupported image dimension: {dim}")
class ImageRegistration(luigi.Task):
artifacts_dir = luigi.Parameter(default="artifacts")
def requires(self):
from engine.tasks_reader import ImageReader
return ImageReader(artifacts_dir=self.artifacts_dir)
def output(self):
return luigi.LocalTarget(os.path.join(self.artifacts_dir, "registered_index.txt"))
def run(self):
# Reader Path Reader
reader_index = os.path.join(self.artifacts_dir, "reader", "reader_index.txt")
with open(reader_index, "r") as f:
reader_paths = [line.strip() for line in f if line.strip()]
fixed_raw = sitk.ReadImage(reader_paths[0])
fixed = cast_to_float32(fixed_raw)
R = sitk.ImageRegistrationMethod()
if fixed.GetDimension() == 2:
R.SetMetricAsMeanSquares()
R.SetInterpolator(sitk.sitkLinear)
else:
R.SetMetricAsMattesMutualInformation(numberOfHistogramBins=50)
R.SetMetricSamplingStrategy(R.RANDOM)
R.SetMetricSamplingPercentage(0.2)
R.SetInterpolator(sitk.sitkLinear)
R.SetOptimizerAsRegularStepGradientDescent(
learningRate=2.0, minStep=1e-4, numberOfIterations=200, gradientMagnitudeTolerance=1e-8
)
R.SetOptimizerScalesFromPhysicalShift()
R.SetShrinkFactorsPerLevel(shrinkFactors=[4, 2, 1])
R.SetSmoothingSigmasPerLevel(smoothingSigmas=[2, 1, 0])
R.SmoothingSigmasAreSpecifiedInPhysicalUnitsOn()
out_paths = []
for img_path in reader_paths:
moving_raw = sitk.ReadImage(img_path)
moving = cast_to_float32(moving_raw)
init_tx = make_initial_transform(fixed, moving)
R.SetInitialTransform(init_tx, inPlace=False)
final_tx = R.Execute(fixed, moving)
registered = sitk.Resample(moving, fixed, final_tx, sitk.sitkLinear, 0.0, sitk.sitkFloat32)
out_path = Path(self.artifacts_dir) / f"registered_{Path(img_path).name}"
sitk.WriteImage(registered, str(out_path))
out_paths.append(str(out_path))
with self.output().open("w") as f:
f.write("\n".join(out_paths))
+33
View File
@@ -0,0 +1,33 @@
import json
import os
from pathlib import Path
import shutil
import luigi
from engine.utils import ensure_dir
class ImageWriter(luigi.Task):
artifacts_dir = luigi.Parameter(default="artifacts")
def requires(self):
from engine.tasks_radiomics import FeatureExtraction
return FeatureExtraction(artifacts_dir=self.artifacts_dir)
def output(self):
return luigi.LocalTarget(os.path.join(self.artifacts_dir, "final_output.json"))
def run(self):
# Copy Excel file generated by PySERA to artifacts/radiomics3d
excel_src = Path(self.artifacts_dir) / "Radiomics_Results.xlsx"
if excel_src.exists():
dst_dir = ensure_dir(Path(self.artifacts_dir) / "radiomics3d")
excel_dst = dst_dir / "Radiomics_Results.xlsx"
shutil.copy2(excel_src, excel_dst)
# Load summary JSON from FeatureExtraction
with self.requires().output().open("r") as f:
summary = json.load(f)
# Write final summary JSON
with self.output().open("w") as f:
json.dump(summary, f, indent=2, ensure_ascii=False)
+27
View File
@@ -0,0 +1,27 @@
from pathlib import Path
import json
import pandas as pd
def ensure_dir(p):
p = Path(p)
p.mkdir(parents=True, exist_ok=True)
return p
def json_safe(obj):
from pathlib import Path
import numpy as np
import pandas as pd
if isinstance(obj, Path):
return str(obj)
if isinstance(obj, (str, int, float, bool)) or obj is None:
return obj
if isinstance(obj, dict):
return {str(k): json_safe(v) for k, v in obj.items()}
if isinstance(obj, (list, tuple)):
return [json_safe(v) for v in obj]
if isinstance(obj, pd.DataFrame):
return obj.to_dict(orient="records")
if isinstance(obj, np.ndarray):
return obj.tolist()
return obj # fallback
+39
View File
@@ -0,0 +1,39 @@
connected-components-3d==3.26.1
dataclasses==0.6
et_xmlfile==2.0.0
ImageIO==2.37.2
joblib==1.5.2
lazy_loader==0.4
lockfile==0.12.2
luigi==3.6.0
networkx==3.6.1
nibabel==5.3.3
numpy==2.2.6
opencv-python==4.12.0.88
openpyxl==3.1.5
packaging==25.0
pandas==2.3.3
pillow==12.0.0
psutil==7.1.3
pydicom==3.0.1
pynrrd==1.1.3
pysera==2.1.5
PySide6==6.10.1
PySide6_Addons==6.10.1
PySide6_Essentials==6.10.1
python-daemon==3.1.2
python-dateutil==2.9.0.post0
pytz==2025.2
rt-utils==1.2.7
scikit-image==0.25.2
scikit-learn==1.7.2
scipy==1.16.3
shiboken6==6.10.1
simpleitk==2.5.3
six==1.17.0
tenacity==8.5.0
threadpoolctl==3.6.0
tifffile==2025.10.16
tornado==6.5.2
typing_extensions==4.15.0
tzdata==2025.2
+14
View File
@@ -0,0 +1,14 @@
import luigi
import time
from engine.pipeline import RadiumaPipeline
if __name__ == "__main__":
start = time.time()
luigi.build([RadiumaPipeline(artifacts_dir="artifacts")], local_scheduler=True)
elapsed = time.time() - start
hours, rem = divmod(elapsed, 3600)
minutes, seconds = divmod(rem, 60)
centiseconds = int((seconds - int(seconds)) * 100)
print(f"[Pipeline] total execution time: {int(hours):02}:{int(minutes):02}:{int(seconds):02}.{centiseconds:02}")
print("=== Workflow completed successfully ===")