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
+104
View File
@@ -0,0 +1,104 @@
from dagster import job, graph, op
from pathlib import Path
from .ops import (
read_image_and_mask,
read_image_and_mask_from_case,
filter_image,
extract_features,
write_outputs,
enumerate_cases_auto
)
# Extract image file name from config for single file mode
@op(config_schema={"image_path": str})
def get_case_name_from_config(context) -> str:
image_path = context.op_config["image_path"]
return Path(image_path).stem
# Extract case_name from case dictionary for batch mode
@op
def get_case_name_from_case(context, case: dict) -> str:
return case["case_name"]
# Single-case workflow job
@job(config={
"ops": {
"read_image_and_mask": {
"config": {
"image_path": "C:/Users/Omen16/Documents/Radiuma_Mini/data/images/CT_pitch.nii.gz",
"mask_path": "C:/Users/Omen16/Documents/Radiuma_Mini/data/masks/CT_pitch_mask.nii.gz",
}
},
"get_case_name_from_config": {
"config": {
"image_path": "C:/Users/Omen16/Documents/Radiuma_Mini/data/images/CT_pitch.nii.gz"
}
}
}
})
def radiomics_job():
image, mask = read_image_and_mask()
filtered = filter_image(image)
feats = extract_features(image=filtered, mask=mask)
case_name = get_case_name_from_config()
write_outputs(features=feats, case_name=case_name)
# Graph that processes one case dict (used for batch mapping)
@graph
def process_case(case):
image, mask = read_image_and_mask_from_case(case)
filtered = filter_image(image)
feats = extract_features(image=filtered, mask=mask)
case_name = get_case_name_from_case(case)
write_outputs(features=feats, case_name=case_name)
# Batch workflow job (auto-discovery + dynamic mapping)
@job
def radiomics_batch_job():
cases = enumerate_cases_auto()
cases.map(process_case)
# Preprocessing-only workflow job
@job(config={
"ops": {
"read_image_and_mask": {
"config": {
"image_path": "C:/Users/Omen16/Documents/Radiuma_Mini/data/images/CT_pitch.nii.gz",
"mask_path": "C:/Users/Omen16/Documents/Radiuma_Mini/data/masks/CT_pitch_mask.nii.gz",
}
},
"get_case_name_from_config": {
"config": {
"image_path": "C:/Users/Omen16/Documents/Radiuma_Mini/data/images/CT_pitch.nii.gz"
}
}
}
})
def preprocessing_job():
image, mask = read_image_and_mask()
filtered = filter_image(image)
feats = extract_features(image=filtered, mask=mask)
case_name = get_case_name_from_config()
write_outputs(features=feats, case_name=case_name)
# Feature-extraction-only workflow job
@job(config={
"ops": {
"read_image_and_mask": {
"config": {
"image_path": "C:/Users/Omen16/Documents/Radiuma_Mini/data/images/CT_pitch.nii.gz",
"mask_path": "C:/Users/Omen16/Documents/Radiuma_Mini/data/masks/CT_pitch_mask.nii.gz",
}
},
"get_case_name_from_config": {
"config": {
"image_path": "C:/Users/Omen16/Documents/Radiuma_Mini/data/images/CT_pitch.nii.gz"
}
}
}
})
def feature_extraction_job():
image, mask = read_image_and_mask()
feats = extract_features(image=image, mask=mask)
case_name = get_case_name_from_config()
write_outputs(features=feats, case_name=case_name)
+223
View File
@@ -0,0 +1,223 @@
import os
import numpy as np
import pandas as pd
from pathlib import Path
import SimpleITK as sitk
from dagster import op, In, Out, DynamicOut, DynamicOutput
# Utility functions
def remove_ext(filename: str, exts):
fname = filename
for ext in exts:
if fname.lower().endswith(ext.lower()):
return fname[:-len(ext)]
return fname
def strip_suffixes(name_no_ext: str, suffixes):
for s in suffixes:
if name_no_ext.endswith(s):
return name_no_ext[:-len(s)]
return name_no_ext
def levenshtein(a: str, b: str) -> int:
if a == b:
return 0
m, n = len(a), len(b)
dp = list(range(n + 1))
for i in range(1, m + 1):
prev, dp[0] = dp[0], i
for j in range(1, n + 1):
cur = dp[j]
cost = 0 if a[i - 1] == b[j - 1] else 1
dp[j] = min(dp[j] + 1, dp[j - 1] + 1, prev + cost)
prev = cur
return dp[n]
def list_files(root: Path, exts):
files = []
for p in Path(root).rglob("*"):
if p.is_file() and any(str(p).lower().endswith(ext.lower()) for ext in exts):
files.append(p)
return sorted(files)
# Single-case readers
@op(
config_schema={
"image_path": str,
"mask_path": str,
},
out={"image": Out(), "mask": Out()}
)
def read_image_and_mask(context):
image_path = context.op_config["image_path"]
mask_path = context.op_config["mask_path"]
if not os.path.exists(image_path):
raise FileNotFoundError(f"Image not found: {image_path}")
if not os.path.exists(mask_path):
raise FileNotFoundError(f"Mask not found: {mask_path}")
image = sitk.ReadImage(image_path)
mask = sitk.ReadImage(mask_path)
img_arr = sitk.GetArrayFromImage(image)
msk_arr = sitk.GetArrayFromImage(mask)
context.log.info(f"[single] image:{img_arr.shape} mask:{msk_arr.shape}")
return img_arr, msk_arr
@op(
ins={"case": In()},
out={"image": Out(), "mask": Out()}
)
def read_image_and_mask_from_case(context, case: dict):
image_path = case["image_path"]
mask_path = case["mask_path"]
if not os.path.exists(image_path):
raise FileNotFoundError(f"Image not found: {image_path}")
if not os.path.exists(mask_path):
raise FileNotFoundError(f"Mask not found: {mask_path}")
image = sitk.ReadImage(image_path)
mask = sitk.ReadImage(mask_path)
img_arr = sitk.GetArrayFromImage(image)
msk_arr = sitk.GetArrayFromImage(mask)
context.log.info(f"[case] image:{img_arr.shape} mask:{msk_arr.shape}")
return img_arr, msk_arr
# Preprocess, features, writer
@op(out=Out())
def filter_image(context, image: np.ndarray):
context.log.info(f"[filter] image:{image.shape}")
return image
@op(ins={"image": In(), "mask": In()}, out=Out())
def extract_features(context, image: np.ndarray, mask: np.ndarray):
masked = image[mask > 0]
mean_val = float(masked.mean()) if masked.size > 0 else 0.0
features = {"mean_intensity": mean_val}
context.log.info(f"[features] {features}")
return features
RESULTS_DIR = Path("results")
RESULTS_DIR.mkdir(exist_ok=True)
@op(ins={"features": In(), "case_name": In()}, out=Out())
def write_outputs(context, features: dict, case_name: str):
# Define output CSV file path
csv_path = RESULTS_DIR / "features.csv"
# Prepare row with case name and features
row = {"case_name": case_name}
row.update(features)
# If CSV does not exist, create new file with header
if not csv_path.exists():
df = pd.DataFrame([row])
df.to_csv(csv_path, index=False)
else:
# Append new row to existing CSV
df = pd.DataFrame([row])
df.to_csv(csv_path, mode="a", header=False, index=False)
context.log.info(f"[write] features appended to {csv_path}")
# Optional Excel export (currently disabled)
# excel_path = RESULTS_DIR / "features.xlsx"
# df.to_excel(excel_path, index=False)
return str(csv_path)
# Batch case discovery
@op(
config_schema={
"images_dir": str,
"masks_dir": str,
"image_exts": list,
"mask_exts": list,
"mask_suffixes": list,
"allow_fuzzy_match": bool,
"fuzzy_max_distance": int,
"verbose": bool,
},
out=DynamicOut()
)
def enumerate_cases_auto(context):
cfg = context.op_config
images_dir = Path(cfg["images_dir"]).expanduser().resolve()
masks_dir = Path(cfg["masks_dir"]).expanduser().resolve()
image_exts = [ext.lower() for ext in cfg["image_exts"]]
mask_exts = [ext.lower() for ext in cfg["mask_exts"]]
mask_suffixes = cfg["mask_suffixes"]
allow_fuzzy = cfg["allow_fuzzy_match"]
max_dist = int(cfg["fuzzy_max_distance"])
verbose = bool(cfg.get("verbose", True))
img_files = list_files(images_dir, image_exts)
msk_files = list_files(masks_dir, mask_exts)
context.log.info(f"[discover] images_dir={images_dir} masks_dir={masks_dir}")
context.log.info(f"[discover] found images: {len(img_files)}")
for p in img_files:
context.log.info(f" - image: {p.name}")
context.log.info(f"[discover] found masks: {len(msk_files)}")
for p in msk_files:
context.log.info(f" - mask: {p.name}")
msk_index = {}
for m in msk_files:
base = strip_suffixes(remove_ext(m.name, mask_exts), mask_suffixes).lower()
msk_index.setdefault(base, []).append(m)
yielded = 0
skipped = 0
for img in img_files:
base = remove_ext(img.name, image_exts).lower()
candidates = msk_index.get(base, [])
chosen = None
reason = None
if candidates:
chosen = candidates[0]
else:
if allow_fuzzy and msk_files:
best = None
best_d = 10**9
for m in msk_files:
mbase = strip_suffixes(remove_ext(m.name, mask_exts), mask_suffixes).lower()
d = levenshtein(base, mbase)
if d < best_d:
best, best_d = m, d
if best is not None and best_d <= max_dist:
chosen = best
else:
reason = f"no exact or fuzzy match (best distance {best_d})"
if chosen is None:
skipped += 1
context.log.warning(f"[skip] image:{img.name} → no mask found ({reason}) | base(image)='{base}'")
continue
case = {
"image_path": str(img),
"mask_path": str(chosen),
"case_name": remove_ext(img.name, image_exts),
}
context.log.info(f"[pair] {case['case_name']} → image:{img.name} mask:{Path(chosen).name}")
yield DynamicOutput(case, mapping_key=case["case_name"])
yielded += 1
context.log.info(f"[summary] yielded={yielded} skipped={skipped}")
if yielded == 0:
context.log.error("No valid cases discovered. See [skip] messages above for reasons.")
raise Exception("No valid cases discovered. See logs for details.")