Skip to content

Commit 614c8f8

Browse files
committed
[Feature] <Allow reading of DICOM images> #68
Rewrote the save dicom function still using ITK, but instead of reading the from the Array, we just convert the nifti file generated into dicom slices and reuse the bval and bvec files. add github workflow test for dicom2niix
1 parent 606b667 commit 614c8f8

File tree

6 files changed

+145
-122
lines changed

6 files changed

+145
-122
lines changed

.github/workflows/docker-build-and-run.yml

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,3 +76,73 @@ jobs:
7676
- name: Cleanup Docker
7777
run: |
7878
docker system prune -a --force
79+
build-dicom2niix-and-run-docker:
80+
runs-on: ubuntu-latest
81+
steps:
82+
- name: Checkout code
83+
uses: actions/checkout@v4
84+
85+
- name: Set up Python
86+
uses: actions/setup-python@v4
87+
with:
88+
python-version: '3.x'
89+
90+
- name: Install Python dependencies
91+
run: |
92+
python -m pip install --upgrade pip
93+
pip install -r requirements.txt
94+
95+
- name: Generate input files
96+
run: |
97+
python -m Docker.generate_signal_docker_test --dicom
98+
99+
- name: Verify input files
100+
run: |
101+
for file in ivim_simulation_0000.dcm, ivim_simulation_0001.dcm, ivim_simulation_0002.dcm ivim_simulation_0003.dcm ivim_simulation_0004.dcm; do
102+
if [ ! -f "ivim_simulation/$file" ]; then
103+
echo "Error: $file not found"
104+
exit 1
105+
fi
106+
done
107+
echo "All input files generated successfully"
108+
109+
- name: Set up Docker Buildx
110+
uses: docker/setup-buildx-action@v3
111+
112+
- name: Build Docker image
113+
run: |
114+
docker build -t tf2.4_ivim-mri_codecollection -f Docker/dicom2nifti/Dockerfile .
115+
116+
- name: Run Docker container
117+
run: |
118+
docker run --rm --name TF2.4_IVIM-MRI_CodeCollection \
119+
-v ${{ github.workspace }}:/usr/src/app \
120+
-v ${{ github.workspace }}:/usr/app/output \
121+
tf2.4_ivim-mri_codecollection \
122+
/usr/src/app/ivim_simulation
123+
124+
- name: Verify output files
125+
run: |
126+
for file in f.nii.gz dp.nii.gz d.nii.gz; do
127+
if [ ! -f "$file" ]; then
128+
echo "Error: $file not found"
129+
exit 1
130+
fi
131+
done
132+
echo "All output files generated successfully"
133+
134+
- name: Clean up artifacts and Docker image
135+
run: |
136+
docker rmi tf2.4_ivim-mri_codecollection || true
137+
rm -f tf2.4_ivim-mri_codecollection.tar.gz
138+
rm -f ${{ github.workspace }}/f.nii.gz
139+
rm -f ${{ github.workspace }}/dp.nii.gz
140+
rm -f ${{ github.workspace }}/d.nii.gz
141+
rm -f ${{ github.workspace }}/ivim_simulation.nii.gz
142+
rm -f ${{ github.workspace }}/ivim_simulation.bval
143+
rm -f ${{ github.workspace }}/ivim_simulation.bvec
144+
rm -r -f ${{ github.workspace }}/ivim_simulation || true
145+
- name: Cleanup Docker
146+
run: |
147+
docker system prune -a --force
148+

Docker/README.md

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,11 @@ This project is designed to run the `nifti_wrapper` script using a Docker contai
44

55
## Prerequisites
66

7-
- Docker must be installed on your system.
7+
- Docker must be installed on your system.
88

99
## Directory Structure
1010

11-
```
11+
``` sh
1212
~/TF2.4_IVIM-MRI_CodeCollection/
1313
1414
├── Docker/
@@ -47,7 +47,9 @@ Before running the Docker container, here are the available options for the `Doc
4747
```sh
4848
sudo docker build -t tf2.4_ivim-mri_codecollection -f Docker/Dockerfile .
4949
```
50+
5051
OR (If you need to convert your data from DICOM TO NIfTI images)
52+
5153
```sh
5254
sudo docker build -t tf2.4_ivim-mri_codecollection -f Docker/dicom2nifti/Dockerfile .
5355
```
@@ -136,9 +138,6 @@ Run the tool in interactive mode. This launches a terminal-based wizard where yo
136138

137139
#### It is strongly recommend that users check validate the b-vector directions for their hardware and sequence as [described in a dedicated document](https://www.nitrc.org/docman/?group_id=880)
138140

139-
![DICOM2NiFTI](dicom2nifti/DICOM2NiFTI.png)
140-
141-
#### Output of NIFTI and DICOM objects generated from signal generation test
141+
- NIfTI follows the Talairach/MNI coordinate system where the X value increases as we move toward the participant's right, the Y increases as we move anteriorly. In contrast, for bipeds DICOM specifies the the X increases as we more toward the participant's left, while the Y increases as we move posteriorly. Both agree that the Z coordinate increases as we move superiorly.
142142

143-
- NIfTI follows the Talairach/MNI coordinate system where the X value increases as we move toward the participant's right, the Y increases as we move anteriorly. In contrast, for bipeds DICOM specifies the the X increases as we more toward the participant's left, while the Y increases as we move posteriorly. Both agree that the Z coordinate increases as we move superiorly.
144143
---

Docker/dicom2nifti/DICOM2NiFTI.png

-714 KB
Binary file not shown.

Docker/generate_signal_docker_test.py

Lines changed: 68 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,13 @@
1+
import datetime
2+
import os
13
from pathlib import Path
4+
import sys
5+
import uuid
26
import numpy as np
37
import nibabel as nib
8+
import itk
49
from utilities.data_simulation.GenerateData import GenerateData
510
from WrapImage.nifti_wrapper import save_nifti_file
6-
from WrapImage.dicom2niix_wrapper import save_dicom_objects
711

812
def save_bval_bvec(filename, values):
913
if filename.endswith('.bval'):
@@ -18,6 +22,8 @@ def save_bval_bvec(filename, values):
1822
with open(filename, 'w') as file:
1923
file.write(values_string)
2024

25+
26+
2127
# Set random seed for reproducibility
2228
np.random.seed(42)
2329
# Create GenerateData instance
@@ -35,28 +41,71 @@ def save_bval_bvec(filename, values):
3541
# Generate IVIM signal
3642
signals = gd.ivim_signal(D_in, Dp_in, f_in, S0, bvals_reshaped)
3743

38-
39-
# Generate 4D signal by stacking volumes for each bval
40-
signals_4d = np.stack([
41-
gd.ivim_signal(D_in, Dp_in, f_in, S0, np.full(shape, bval))
42-
for bval in bvals
43-
], axis=-1)
44-
4544
# Save the generated image as a NIfTI file
4645
save_nifti_file(signals, "ivim_simulation.nii.gz")
4746
# Save the bval in a file
4847
save_bval_bvec("ivim_simulation.bval", [0, 50, 100, 500, 1000])
4948
# Save the bvec value
5049
save_bval_bvec("ivim_simulation.bvec", [[1, 0, 0], [0, 1, 0], [0, 0, 1]])
5150

52-
bvecs = [[1, 0, 0]] * len(bvals) # Assuming all x-direction
53-
54-
save_dicom_objects(
55-
volume_4d=signals_4d,
56-
bvals=bvals.tolist(),
57-
bvecs=bvecs,
58-
out_dir=Path("ivim_simulation_dicom"),
59-
f_vals=f_in,
60-
D_vals=D_in,
61-
Dp_vals=Dp_in
62-
)
51+
52+
def save_dicom_files():
53+
os.makedirs("ivim_simulation", exist_ok=True)
54+
InputImageType = itk.Image[itk.D, 3]
55+
ReaderType = itk.ImageFileReader[InputImageType]
56+
NiftiImageIOType = itk.NiftiImageIO.New()
57+
58+
59+
reader = ReaderType.New()
60+
reader.SetImageIO(NiftiImageIOType)
61+
reader.SetFileName("ivim_simulation.nii.gz")
62+
63+
try:
64+
reader.Update()
65+
except Exception as e:
66+
print(f"Error occured while reading NIfTI in ivim_simulation: {e}")
67+
sys.exit(1)
68+
69+
OutputPixelType = itk.SS
70+
# The casting filter output image type will be 3D with the new pixel type
71+
CastedImageType = itk.Image[OutputPixelType, 3]
72+
CastFilterType = itk.CastImageFilter[InputImageType, CastedImageType]
73+
caster = CastFilterType.New()
74+
caster.SetInput(reader.GetOutput())
75+
caster.Update()
76+
77+
78+
OutputImageType = itk.Image[OutputPixelType, 2]
79+
FileWriterType = itk.ImageSeriesWriter[CastedImageType, OutputImageType]
80+
GDCMImageIOType = itk.GDCMImageIO.New()
81+
writer = FileWriterType.New()
82+
size = reader.GetOutput().GetLargestPossibleRegion().GetSize()
83+
fnames = itk.NumericSeriesFileNames.New()
84+
num_slices = size[2]
85+
86+
fnames.SetStartIndex(0)
87+
fnames.SetEndIndex(num_slices - 1) # Iterate over the Z dimension (slices)
88+
fnames.SetIncrementIndex(1)
89+
fnames.SetSeriesFormat(os.path.join("ivim_simulation", f"ivim_simulation_%04d.dcm"))
90+
91+
# meta_dict = itk.MetaDataDictionary()
92+
# include correct headers here to be tuned for Vendor
93+
# GDCMImageIOType.SetMetaDataDictionary(meta_dict)
94+
# GDCMImageIOType.KeepOriginalUIDOn()
95+
writer.SetInput(caster.GetOutput())
96+
writer.SetImageIO(GDCMImageIOType)
97+
writer.SetFileNames(fnames.GetFileNames())
98+
try:
99+
writer.Write()
100+
except Exception as e:
101+
print(f"Error occurred while writing DICOMs in ivim simulation: {e}")
102+
sys.exit(1)
103+
104+
args = sys.argv[1:]
105+
if "--dicom" in args:
106+
save_dicom_files()
107+
# Save the bval in a file
108+
save_bval_bvec(os.path.join("ivim_simulation","ivim_simulation.bval"), [0, 50, 100, 500, 1000])
109+
# Save the bvec value
110+
save_bval_bvec(os.path.join("ivim_simulation","ivim_simulation.bvec"), [[1, 0, 0], [0, 1, 0], [0, 0, 1]])
111+
# read the generated nii file to dicom files

WrapImage/dicom2niix_wrapper.py

Lines changed: 0 additions & 94 deletions
Original file line numberDiff line numberDiff line change
@@ -6,101 +6,7 @@
66
import subprocess
77
import sys
88
import inquirer
9-
import itk
109
import numpy as np
11-
import datetime
12-
import uuid
13-
14-
def save_dicom_objects(
15-
volume_4d: np.ndarray,
16-
bvals: list,
17-
bvecs: list,
18-
out_dir: Path,
19-
f_vals: np.ndarray = None,
20-
D_vals: np.ndarray = None,
21-
Dp_vals: np.ndarray = None,
22-
patient_id: str = "SIM123",
23-
modality: str = "MR",
24-
pixel_spacing=(1.0, 1.0),
25-
slice_thickness=1.0,
26-
series_description="Simulated IVIM",
27-
protocol_name="SimulatedProtocol",
28-
patient_position="HFS",
29-
manufacturer="Simulated Manufacturer",
30-
):
31-
out_dir.mkdir(parents=True, exist_ok=True)
32-
x, y, z, t = volume_4d.shape
33-
assert t == len(bvals) == len(bvecs), "Mismatch in timepoints and bvals/bvecs"
34-
35-
# Define 2D image type (DICOM expects 2D slices)
36-
ImageType = itk.Image[itk.SS, 2] # 2D image with short (int16) pixel type
37-
WriterType = itk.ImageFileWriter[ImageType]
38-
39-
for t_idx in range(t):
40-
volume_3d = volume_4d[:, :, :, t_idx].astype(np.int16)
41-
42-
# Generate common metadata for this timepoint
43-
study_uid = f"1.2.826.0.1.3680043.2.1125.{uuid.uuid4().int >> 64}"
44-
series_uid = f"1.2.826.0.1.3680043.2.1125.{uuid.uuid4().int >> 64}"
45-
now = datetime.datetime.now().strftime("%Y%m%d")
46-
series_number = str(t_idx + 1)
47-
48-
for slice_idx in range(z):
49-
# Create 2D slice
50-
slice_2d = volume_3d[:, :, slice_idx]
51-
image_2d = itk.image_view_from_array(slice_2d.T) # Transpose for proper orientation
52-
image_2d = itk.cast_image_filter(image_2d, ttype=(type(image_2d), ImageType))
53-
54-
# Set image properties
55-
image_2d.SetSpacing(pixel_spacing)
56-
image_2d.SetOrigin([0.0, 0.0])
57-
58-
# Create metadata dictionary
59-
meta_dict = itk.MetaDataDictionary()
60-
meta_dict["0010|0010"] = "Simulated^Patient"
61-
meta_dict["0010|0020"] = patient_id
62-
meta_dict["0008|0060"] = modality
63-
meta_dict["0008|0020"] = now
64-
meta_dict["0008|0030"] = "120000"
65-
meta_dict["0020|000d"] = study_uid
66-
meta_dict["0020|000e"] = series_uid
67-
meta_dict["0020|0011"] = series_number
68-
meta_dict["0008|103e"] = f"{series_description} Volume {series_number}"
69-
meta_dict["0018|1030"] = protocol_name
70-
meta_dict["0018|5100"] = patient_position
71-
meta_dict["0008|0070"] = manufacturer
72-
73-
meta_dict["0018|9087"] = str(float(bvals[t_idx]))
74-
gx, gy, gz = bvecs[t_idx]
75-
meta_dict["0018|9089"] = f"{gx}\\{gy}\\{gz}"
76-
77-
if f_vals is not None:
78-
meta_dict["0011|1001"] = f"f_in_mean={np.mean(f_vals):.6f}"
79-
if D_vals is not None:
80-
meta_dict["0011|1002"] = f"D_in_mean={np.mean(D_vals):.6e}"
81-
if Dp_vals is not None:
82-
meta_dict["0011|1003"] = f"Dp_in_mean={np.mean(Dp_vals):.6e}"
83-
84-
sop_uid = f"1.2.826.0.1.3680043.8.498.{uuid.uuid4().int >> 64}"
85-
meta_dict["0008|0018"] = sop_uid
86-
meta_dict["0020|0013"] = str(slice_idx + 1)
87-
meta_dict["0020|0032"] = f"0\\0\\{slice_idx * slice_thickness}"
88-
meta_dict["0020|0037"] = "1\\0\\0\\0\\1\\0"
89-
90-
# Create and configure writer
91-
writer = WriterType.New()
92-
writer.SetInput(image_2d)
93-
writer.SetFileName(str(out_dir / f"vol_{t_idx:03d}_slice_{slice_idx:03d}.dcm"))
94-
95-
# Use GDCMImageIO for DICOM writing
96-
gdcm_io = itk.GDCMImageIO.New()
97-
gdcm_io.SetMetaDataDictionary(meta_dict)
98-
gdcm_io.KeepOriginalUIDOn()
99-
100-
writer.SetImageIO(gdcm_io)
101-
writer.Update()
102-
103-
print(f" DICOMs written to {out_dir.resolve()}")
10410

10511
def prompt_input_directory():
10612
return inquirer.prompt([

pyproject.toml

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,9 @@
22
name = "TF2.4_IVIM-MRI-CodeCollection"
33
version = "0.1.0"
44
description = "IVIM MRI signal simulation and DICOM/NIfTI processing tools"
5-
authors = [{ name="Jackson Hardee", email="jphardee@gmail.com" }]
6-
requires-python = ">=3.11"
5+
requires-python = ">=3.9"
76
readme = "README.md"
8-
license = {text = "MIT"}
7+
license = {text = "Apache 2"}
98
dependencies = [
109
"numpy<2",
1110
"nibabel",

0 commit comments

Comments
 (0)