Skip to content

Commit 8c632cf

Browse files
Merge pull request #112 from Unique-Usman/usman/kaapana_ivim_osipi
feat: add IVIM workflow DAG with MinIO integration and platform docum…
2 parents 2585b8c + 10f9c57 commit 8c632cf

26 files changed

+1364
-0
lines changed

WrapImage/nifti_wrapper_kaapana.py

Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
#!/usr/bin/env python3
2+
import os
3+
import glob
4+
import json
5+
import nibabel as nib
6+
import shutil
7+
import numpy as np
8+
from tqdm import tqdm
9+
from src.wrappers.OsipiBase import OsipiBase
10+
11+
# --------------------- Helper Functions --------------------- #
12+
13+
def read_nifti_file(input_file):
14+
nifti_img = nib.load(input_file)
15+
return nifti_img.get_fdata(), nifti_img.affine, nifti_img.header
16+
17+
def read_bval_file(bval_file):
18+
return np.genfromtxt(bval_file, dtype=float)
19+
20+
def read_bvec_file(bvec_file):
21+
bvec_data = np.genfromtxt(bvec_file)
22+
return np.transpose(bvec_data)
23+
24+
def save_nifti_file(data, affine, output_file):
25+
output_img = nib.Nifti1Image(data, affine)
26+
nib.save(output_img, output_file)
27+
28+
def loop_over_first_n_minus_1_dimensions(arr):
29+
n = arr.ndim
30+
for idx in np.ndindex(*arr.shape[:n - 1]):
31+
yield idx, arr[idx].flatten()
32+
33+
def load_config():
34+
workflow_dir = os.environ["WORKFLOW_DIR"]
35+
config_path = os.path.join(workflow_dir, "conf", "conf.json")
36+
with open(config_path, "r") as f:
37+
return json.load(f)
38+
39+
# --------------------- Main Execution --------------------- #
40+
if __name__ == "__main__":
41+
# Load config
42+
config = load_config()
43+
config = config["workflow_form"]
44+
45+
# Kaapana environment variables
46+
WORKFLOW_DIR = os.environ["WORKFLOW_DIR"]
47+
OPERATOR_IN_DIR = os.environ["OPERATOR_IN_DIR"]
48+
OPERATOR_OUT_DIR = os.environ["OPERATOR_OUT_DIR"]
49+
50+
element_input_dir = os.path.join(WORKFLOW_DIR, OPERATOR_IN_DIR)
51+
element_output_dir = os.path.join(WORKFLOW_DIR, OPERATOR_OUT_DIR)
52+
os.makedirs(element_output_dir, exist_ok=True)
53+
54+
# Initialize input_file, bvec_file, bval_file to None
55+
input_file = None
56+
bvec_file = None
57+
bval_file = None
58+
59+
# Check upload type
60+
dicom_or_nifti = config.get("upload_type", "nifti").lower()
61+
62+
if dicom_or_nifti == "nifti":
63+
# Parse and validate source files
64+
source_files_kaapana = [f.strip() for f in config.get("source_files").split(",")]
65+
input_file, bvec_file, bval_file = source_files_kaapana
66+
67+
# Detect by extension
68+
input_file = next((f for f in source_files_kaapana if f.endswith((".nii", ".nii.gz"))), None)
69+
bvec_file = next((f for f in source_files_kaapana if f.endswith(".bvec")), None)
70+
bval_file = next((f for f in source_files_kaapana if f.endswith(".bval")), None)
71+
72+
# Validate presence
73+
if not input_file or not bvec_file or not bval_file:
74+
raise ValueError(
75+
f"Expected files to include one NIfTI (.nii/.nii.gz), one .bvec, and one .bval, "
76+
f"but got: {source_files_kaapana}"
77+
)
78+
79+
input_file = os.path.join(element_input_dir, input_file)
80+
bvec_file = os.path.join(element_input_dir, bvec_file)
81+
bval_file = os.path.join(element_input_dir, bval_file)
82+
83+
else:
84+
# DICOM case → follow batch structure
85+
BATCH_DIR = os.path.join(WORKFLOW_DIR, "batch")
86+
batch_folders = sorted([f for f in glob.glob(os.path.join(BATCH_DIR, "*"))])
87+
print(f"batch-folders - {batch_folders}")
88+
89+
if not batch_folders:
90+
raise FileNotFoundError(f"No batch folders found in {BATCH_DIR}")
91+
92+
# Pick the first batch folder (usually one per patient/series)
93+
batch_input_dir = os.path.join(batch_folders[0], "dicom_to_nifti")
94+
95+
nifti_files = sorted(glob.glob(os.path.join(batch_input_dir, "*.nii.gz")))
96+
if not nifti_files:
97+
raise FileNotFoundError(f"No NIfTI files found in {batch_input_dir}")
98+
99+
paired_files = []
100+
for nifti in nifti_files:
101+
base = os.path.splitext(os.path.splitext(os.path.basename(nifti))[0])[0] # remove .nii.gz
102+
bvec_file = os.path.join(batch_input_dir, f"{base}.bvec")
103+
bval_file = os.path.join(batch_input_dir, f"{base}.bval")
104+
if os.path.exists(bvec_file) and os.path.exists(bval_file):
105+
paired_files.append((nifti, bvec_file, bval_file))
106+
else:
107+
raise FileNotFoundError(f"Missing bvec/bval for {nifti}")
108+
109+
# Use the first matched trio
110+
input_file, bvec_file, bval_file = paired_files[0]
111+
112+
print(f"Using DICOM-converted files from {batch_input_dir}:\n"
113+
f" {input_file}\n {bvec_file}\n {bval_file}")
114+
115+
# Optional config values
116+
affine_override = config.get("affine", None)
117+
algorithm = config.get("algorithm", "OJ_GU_seg")
118+
algorithm_args = config.get("algorithm_args", None)
119+
120+
# Load input data
121+
data, affine, _ = read_nifti_file(input_file)
122+
bvecs = read_bvec_file(bvec_file)
123+
bvals = read_bval_file(bval_file)
124+
125+
# Override affine if provided
126+
if affine_override:
127+
affine = np.array(affine_override).reshape(4, 4)
128+
129+
# Initialize model
130+
fit = OsipiBase(algorithm=algorithm)
131+
132+
# Preallocate output arrays
133+
shape = data.shape[:data.ndim - 1]
134+
f_image = np.zeros(shape, dtype=np.float32)
135+
Dp_image = np.zeros(shape, dtype=np.float32)
136+
D_image = np.zeros(shape, dtype=np.float32)
137+
138+
total_iteration = np.prod(shape)
139+
140+
# Fit IVIM model voxel by voxel
141+
for idx, view in tqdm(
142+
loop_over_first_n_minus_1_dimensions(data),
143+
desc="Fitting IVIM model", dynamic_ncols=True, total=total_iteration
144+
):
145+
fit_result = fit.osipi_fit(view, bvals)
146+
f_image[idx] = fit_result["f"]
147+
Dp_image[idx] = fit_result["Dp"]
148+
D_image[idx] = fit_result["D"]
149+
150+
# Save outputs
151+
save_nifti_file(f_image, affine, os.path.join(element_output_dir, "f.nii.gz"))
152+
save_nifti_file(Dp_image, affine, os.path.join(element_output_dir, "dp.nii.gz"))
153+
save_nifti_file(D_image, affine, os.path.join(element_output_dir, "d.nii.gz"))
154+
155+
# Copy all .nii.gz from input directory to WORKFLOW_DIR (Kaapana workaround)
156+
nii_files = glob.glob(os.path.join(element_input_dir, "*.nii.gz"))
157+
for nii_file in nii_files:
158+
shutil.copy(nii_file, WORKFLOW_DIR)
159+
print(f"Copied {nii_file} to {WORKFLOW_DIR}")
160+
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
# Kaapana Installation Guide
2+
3+
## Overview
4+
5+
The Kaapana installation process consists of three main steps:
6+
7+
1. **Building** all the container images required for Kaapana and pushing them to a remote registry, local registry, or local tarball
8+
2. **Installation** of Kaapana after building the images
9+
3. **Deployment** of Kaapana after installation
10+
11+
For my setup, I used the remote registry approach, which is also the recommended format.
12+
13+
## Machine Requirements
14+
15+
Refer to the Kaapana Hardware and Network Requirement Docs:
16+
https://kaapana.readthedocs.io/en/stable/installation_guide/requirements.html#requirements
17+
18+
## Step 1: Building Container Images
19+
20+
### Registry Setup (GitLab)
21+
22+
For the build process, I used GitLab registry. You'll need to configure three key parameters in your build-configuration file:
23+
24+
#### 1. `default_registry`
25+
26+
- Create a repository on GitLab
27+
- Navigate to **Deploy****Container Registry**
28+
- Copy the registry link (format: `registry.gitlab.com/your-username/repository-name`)
29+
- Example: `registry.gitlab.com/unique-usman/kaapana-tutorial`
30+
31+
#### 2. `registry_password`
32+
33+
- Go to your repository **Settings****Access Tokens**
34+
- Create a new token with read and write registry permissions
35+
- _Note: I granted all permissions for simplicity_
36+
37+
#### 3. `registry_username`
38+
39+
- Use the GitLab username under which you created the repository
40+
- Since I created my repository under my personal GitLab account, I used my GitLab username
41+
42+
### Build Process
43+
44+
Follow the comprehensive build guide available at:
45+
https://kaapana.readthedocs.io/en/stable/installation_guide/build.html
46+
47+
## Step 2: Server Installation
48+
49+
### Installation Process
50+
51+
Refer to the server installation documentation:
52+
https://kaapana.readthedocs.io/en/stable/installation_guide/server_installation.html
53+
54+
Since I didn't use any proxy configuration, I simply executed the server installation script as provided in the documentation.
55+
56+
## Step 3: Platform Deployment
57+
58+
### Deployment Process
59+
60+
Follow the deployment guide:
61+
https://kaapana.readthedocs.io/en/stable/installation_guide/deployment.html
62+
63+
I ran the deployment script directly, and it worked perfectly without any additional configuration.
64+
65+
## Post-Deployment Configuration
66+
67+
### Accessing Your Kaapana Instance
68+
69+
After successful deployment, you'll receive a web link to access your Kaapana instance.
70+
71+
### DNS Configuration
72+
73+
To simplify access, I mapped the deployment link to the server's IP address in `/etc/hosts`:
74+
75+
```bash
76+
# Example entry in /etc/hosts
77+
your-server-ip your-kaapana-domain
78+
```
79+
80+
## Architecture Notes
81+
82+
- **Single Machine Setup**: I used only one GCP machine for the entire Kaapana deployment
83+
- **All-in-One Configuration**: The build, installation, and deployment processes were all executed on the same machine
84+
85+
## Key Resources
86+
87+
- [Build Guide](https://kaapana.readthedocs.io/en/stable/installation_guide/build.html)
88+
- [Server Installation](https://kaapana.readthedocs.io/en/stable/installation_guide/server_installation.html)
89+
- [Deployment Guide](https://kaapana.readthedocs.io/en/stable/installation_guide/deployment.html)
90+
91+
---
92+
93+
_This guide reflects a successful single-machine Kaapana deployment using GCP and GitLab registry._

0 commit comments

Comments
 (0)