Add All Folders
This commit is contained in:
@@ -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)
|
||||
@@ -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 ===")
|
||||
@@ -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)
|
||||
@@ -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))
|
||||
@@ -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))
|
||||
@@ -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))
|
||||
@@ -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))
|
||||
@@ -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")
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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))
|
||||
@@ -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))
|
||||
@@ -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)
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user