Skip to content

Added the feature to read DICOM images into a 4D Nifti image #99

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 28 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 4 commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
4bc40f2
Added the feature to read DICOM images into a 4D Nifti image utilizin…
jph6366 Apr 20, 2025
1e060cd
Update README.md
jph6366 Apr 21, 2025
a4ce220
[Feature] <Allow reading of DICOM images> #68
jph6366 Apr 21, 2025
e331dfc
Merge branch 'dicom2niix' of https://github.com/jph6366/TF2.4_IVIM-MR…
jph6366 Apr 21, 2025
1247c2c
Update README.md
jph6366 Apr 21, 2025
a5f63e6
[Feature] <Allow reading of DICOM images> #68
jph6366 Apr 21, 2025
0e496b6
[Feature] <Allow reading of DICOM images> #68
jph6366 Apr 22, 2025
ae56831
Update README.md
jph6366 Apr 22, 2025
317d72d
[Feature] <Allow reading of DICOM images> #68
jph6366 Apr 22, 2025
783e2f3
Merge branch 'dicom2niix' of https://github.com/jph6366/TF2.4_IVIM-MR…
jph6366 Apr 22, 2025
5347a31
[Feature] <Allow reading of DICOM images> #68
jph6366 Apr 22, 2025
22df73a
[Feature] <Allow reading of DICOM images> #68
jph6366 Apr 22, 2025
d18c2e6
Update README.md
jph6366 Apr 23, 2025
3dbb64c
[Feature] <Allow reading of DICOM images> #68
jph6366 Apr 25, 2025
606b667
Merge branch 'dicom2niix' of https://github.com/jph6366/TF2.4_IVIM-MR…
jph6366 Apr 25, 2025
9a17fb1
Update README.md
jph6366 Apr 25, 2025
614c8f8
[Feature] <Allow reading of DICOM images> #68
jph6366 Apr 28, 2025
dda0e88
Merge branch 'dicom2niix' of https://github.com/jph6366/TF2.4_IVIM-MR…
jph6366 Apr 28, 2025
ac752cb
Update pyproject.toml
jph6366 Apr 28, 2025
92abf87
Update generate_signal_docker_test.py
jph6366 Apr 28, 2025
201f495
[Feature] <Allow reading of DICOM images> #68
jph6366 Apr 30, 2025
d32460b
Merge branch 'dicom2niix' of https://github.com/jph6366/TF2.4_IVIM-MR…
jph6366 Apr 30, 2025
9cd090f
Update pyproject.toml
jph6366 Apr 30, 2025
30a0603
Update README.md
jph6366 May 9, 2025
64dc1bc
updated job to be fixed or otherwise log error to help fix
jph6366 May 9, 2025
682d655
Merge branch 'dicom2niix' of https://github.com/jph6366/TF2.4_IVIM-MR…
jph6366 May 9, 2025
814cdc9
#68
jph6366 May 24, 2025
015f7dc
tested locally successfully
jph6366 May 27, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
70 changes: 70 additions & 0 deletions .github/workflows/docker-build-and-run.yml
Original file line number Diff line number Diff line change
Expand Up @@ -76,3 +76,73 @@ jobs:
- name: Cleanup Docker
run: |
docker system prune -a --force
build-dicom2niix-and-run-docker:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4

- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: '3.x'

- name: Install Python dependencies
run: |
python -m pip install --upgrade pip
pip install -r requirements.txt

- name: Generate input files
run: |
python -m Docker.generate_signal_docker_test --dicom

- name: Verify input files
run: |
for file in ivim_simulation_0000.dcm, ivim_simulation_0001.dcm, ivim_simulation_0002.dcm ivim_simulation_0003.dcm ivim_simulation_0004.dcm; do
if [ ! -f "ivim_simulation/$file" ]; then
echo "Error: $file not found"
exit 1
fi
done
echo "All input files generated successfully"

- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3

- name: Build Docker image
run: |
docker build -t tf2.4_ivim-mri_codecollection -f Docker/dicom2nifti/Dockerfile .

- name: Run Docker container
run: |
docker run --rm --name TF2.4_IVIM-MRI_CodeCollection \
-v ${{ github.workspace }}:/usr/src/app \
-v ${{ github.workspace }}:/usr/app/output \
tf2.4_ivim-mri_codecollection \
/usr/src/app/ivim_simulation

- name: Verify output files
run: |
for file in f.nii.gz dp.nii.gz d.nii.gz; do
if [ ! -f "$file" ]; then
echo "Error: $file not found"
exit 1
fi
done
echo "All output files generated successfully"

- name: Clean up artifacts and Docker image
run: |
docker rmi tf2.4_ivim-mri_codecollection || true
rm -f tf2.4_ivim-mri_codecollection.tar.gz
rm -f ${{ github.workspace }}/f.nii.gz
rm -f ${{ github.workspace }}/dp.nii.gz
rm -f ${{ github.workspace }}/d.nii.gz
rm -f ${{ github.workspace }}/ivim_simulation.nii.gz
rm -f ${{ github.workspace }}/ivim_simulation.bval
rm -f ${{ github.workspace }}/ivim_simulation.bvec
rm -r -f ${{ github.workspace }}/ivim_simulation || true
- name: Cleanup Docker
run: |
docker system prune -a --force

11 changes: 5 additions & 6 deletions Docker/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,11 @@ This project is designed to run the `nifti_wrapper` script using a Docker contai

## Prerequisites

- Docker must be installed on your system.
- Docker must be installed on your system.

## Directory Structure

```
``` sh
~/TF2.4_IVIM-MRI_CodeCollection/
├── Docker/
Expand Down Expand Up @@ -47,7 +47,9 @@ Before running the Docker container, here are the available options for the `Doc
```sh
sudo docker build -t tf2.4_ivim-mri_codecollection -f Docker/Dockerfile .
```

OR (If you need to convert your data from DICOM TO NIfTI images)

```sh
sudo docker build -t tf2.4_ivim-mri_codecollection -f Docker/dicom2nifti/Dockerfile .
```
Expand Down Expand Up @@ -124,9 +126,6 @@ sudo docker run -it --rm --name TF2.4_IVIM-MRI_CodeCollection \

#### 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)

![DICOM2NiFTI](dicom2nifti/DICOM2NiFTI.png)

#### Output of NIFTI and DICOM objects generated from signal generation test
- 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.

- 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.
---
Binary file removed Docker/dicom2nifti/DICOM2NiFTI.png
Binary file not shown.
87 changes: 68 additions & 19 deletions Docker/generate_signal_docker_test.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
import datetime
import os
from pathlib import Path
import sys
import uuid
import numpy as np
import nibabel as nib
import itk
from utilities.data_simulation.GenerateData import GenerateData
from WrapImage.nifti_wrapper import save_nifti_file
from WrapImage.dicom2niix_wrapper import save_dicom_objects

def save_bval_bvec(filename, values):
if filename.endswith('.bval'):
Expand All @@ -18,6 +22,8 @@ def save_bval_bvec(filename, values):
with open(filename, 'w') as file:
file.write(values_string)



# Set random seed for reproducibility
np.random.seed(42)
# Create GenerateData instance
Expand All @@ -35,28 +41,71 @@ def save_bval_bvec(filename, values):
# Generate IVIM signal
signals = gd.ivim_signal(D_in, Dp_in, f_in, S0, bvals_reshaped)


# Generate 4D signal by stacking volumes for each bval
signals_4d = np.stack([
gd.ivim_signal(D_in, Dp_in, f_in, S0, np.full(shape, bval))
for bval in bvals
], axis=-1)

# Save the generated image as a NIfTI file
save_nifti_file(signals, "ivim_simulation.nii.gz")
# Save the bval in a file
save_bval_bvec("ivim_simulation.bval", [0, 50, 100, 500, 1000])
# Save the bvec value
save_bval_bvec("ivim_simulation.bvec", [[1, 0, 0], [0, 1, 0], [0, 0, 1]])

bvecs = [[1, 0, 0]] * len(bvals) # Assuming all x-direction

save_dicom_objects(
volume_4d=signals_4d,
bvals=bvals.tolist(),
bvecs=bvecs,
out_dir=Path("ivim_simulation_dicom"),
f_vals=f_in,
D_vals=D_in,
Dp_vals=Dp_in
)

def save_dicom_files():
os.makedirs("ivim_simulation", exist_ok=True)
InputImageType = itk.Image[itk.D, 3]
ReaderType = itk.ImageFileReader[InputImageType]
NiftiImageIOType = itk.NiftiImageIO.New()


reader = ReaderType.New()
reader.SetImageIO(NiftiImageIOType)
reader.SetFileName("ivim_simulation.nii.gz")

try:
reader.Update()
except Exception as e:
print(f"Error occured while reading NIfTI in ivim_simulation: {e}")
sys.exit(1)

OutputPixelType = itk.SS
# The casting filter output image type will be 3D with the new pixel type
CastedImageType = itk.Image[OutputPixelType, 3]
CastFilterType = itk.CastImageFilter[InputImageType, CastedImageType]
caster = CastFilterType.New()
caster.SetInput(reader.GetOutput())
caster.Update()


OutputImageType = itk.Image[OutputPixelType, 2]
FileWriterType = itk.ImageSeriesWriter[CastedImageType, OutputImageType]
GDCMImageIOType = itk.GDCMImageIO.New()
writer = FileWriterType.New()
size = reader.GetOutput().GetLargestPossibleRegion().GetSize()
fnames = itk.NumericSeriesFileNames.New()
num_slices = size[2]

fnames.SetStartIndex(0)
fnames.SetEndIndex(num_slices - 1) # Iterate over the Z dimension (slices)
fnames.SetIncrementIndex(1)
fnames.SetSeriesFormat(os.path.join("ivim_simulation", f"ivim_simulation_%04d.dcm"))

# meta_dict = itk.MetaDataDictionary()
# include correct headers here to be tuned for Vendor
# GDCMImageIOType.SetMetaDataDictionary(meta_dict)
# GDCMImageIOType.KeepOriginalUIDOn()
writer.SetInput(caster.GetOutput())
writer.SetImageIO(GDCMImageIOType)
writer.SetFileNames(fnames.GetFileNames())
try:
writer.Write()
except Exception as e:
print(f"Error occurred while writing DICOMs in ivim simulation: {e}")
sys.exit(1)

args = sys.argv[1:]
if "--dicom" in args:
# read the generated nii file to dicom files
save_dicom_files()
# Save the bval in a file
save_bval_bvec(os.path.join("ivim_simulation","ivim_simulation.bval"), [0, 50, 100, 500, 1000])
# Save the bvec value
save_bval_bvec(os.path.join("ivim_simulation","ivim_simulation.bvec"), [[1, 0, 0], [0, 1, 0], [0, 0, 1]])
94 changes: 0 additions & 94 deletions WrapImage/dicom2niix_wrapper.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,101 +6,7 @@
import subprocess
import sys
import inquirer
import itk
import numpy as np
import datetime
import uuid

def save_dicom_objects(
volume_4d: np.ndarray,
bvals: list,
bvecs: list,
out_dir: Path,
f_vals: np.ndarray = None,
D_vals: np.ndarray = None,
Dp_vals: np.ndarray = None,
patient_id: str = "SIM123",
modality: str = "MR",
pixel_spacing=(1.0, 1.0),
slice_thickness=1.0,
series_description="Simulated IVIM",
protocol_name="SimulatedProtocol",
patient_position="HFS",
manufacturer="Simulated Manufacturer",
):
out_dir.mkdir(parents=True, exist_ok=True)
x, y, z, t = volume_4d.shape
assert t == len(bvals) == len(bvecs), "Mismatch in timepoints and bvals/bvecs"

# Define 2D image type (DICOM expects 2D slices)
ImageType = itk.Image[itk.SS, 2] # 2D image with short (int16) pixel type
WriterType = itk.ImageFileWriter[ImageType]

for t_idx in range(t):
volume_3d = volume_4d[:, :, :, t_idx].astype(np.int16)

# Generate common metadata for this timepoint
study_uid = f"1.2.826.0.1.3680043.2.1125.{uuid.uuid4().int >> 64}"
series_uid = f"1.2.826.0.1.3680043.2.1125.{uuid.uuid4().int >> 64}"
now = datetime.datetime.now().strftime("%Y%m%d")
series_number = str(t_idx + 1)

for slice_idx in range(z):
# Create 2D slice
slice_2d = volume_3d[:, :, slice_idx]
image_2d = itk.image_view_from_array(slice_2d.T) # Transpose for proper orientation
image_2d = itk.cast_image_filter(image_2d, ttype=(type(image_2d), ImageType))

# Set image properties
image_2d.SetSpacing(pixel_spacing)
image_2d.SetOrigin([0.0, 0.0])

# Create metadata dictionary
meta_dict = itk.MetaDataDictionary()
meta_dict["0010|0010"] = "Simulated^Patient"
meta_dict["0010|0020"] = patient_id
meta_dict["0008|0060"] = modality
meta_dict["0008|0020"] = now
meta_dict["0008|0030"] = "120000"
meta_dict["0020|000d"] = study_uid
meta_dict["0020|000e"] = series_uid
meta_dict["0020|0011"] = series_number
meta_dict["0008|103e"] = f"{series_description} Volume {series_number}"
meta_dict["0018|1030"] = protocol_name
meta_dict["0018|5100"] = patient_position
meta_dict["0008|0070"] = manufacturer

meta_dict["0018|9087"] = str(float(bvals[t_idx]))
gx, gy, gz = bvecs[t_idx]
meta_dict["0018|9089"] = f"{gx}\\{gy}\\{gz}"

if f_vals is not None:
meta_dict["0011|1001"] = f"f_in_mean={np.mean(f_vals):.6f}"
if D_vals is not None:
meta_dict["0011|1002"] = f"D_in_mean={np.mean(D_vals):.6e}"
if Dp_vals is not None:
meta_dict["0011|1003"] = f"Dp_in_mean={np.mean(Dp_vals):.6e}"

sop_uid = f"1.2.826.0.1.3680043.8.498.{uuid.uuid4().int >> 64}"
meta_dict["0008|0018"] = sop_uid
meta_dict["0020|0013"] = str(slice_idx + 1)
meta_dict["0020|0032"] = f"0\\0\\{slice_idx * slice_thickness}"
meta_dict["0020|0037"] = "1\\0\\0\\0\\1\\0"

# Create and configure writer
writer = WriterType.New()
writer.SetInput(image_2d)
writer.SetFileName(str(out_dir / f"vol_{t_idx:03d}_slice_{slice_idx:03d}.dcm"))

# Use GDCMImageIO for DICOM writing
gdcm_io = itk.GDCMImageIO.New()
gdcm_io.SetMetaDataDictionary(meta_dict)
gdcm_io.KeepOriginalUIDOn()

writer.SetImageIO(gdcm_io)
writer.Update()

print(f" DICOMs written to {out_dir.resolve()}")

def prompt_input_directory():
return inquirer.prompt([
Expand Down
7 changes: 3 additions & 4 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
[project]
name = "TF2.4_IVIM-MRI-CodeCollection"
version = "0.1.0"
description = "IVIM MRI signal simulation and DICOM/NIfTI processing tools"
authors = [{ name="Jackson Hardee", email="jphardee@gmail.com" }]
requires-python = ">=3.11"
description = "IVIM MRI signal processing and NIfTI/DICOM processing tools"
requires-python = ">=3.9"
readme = "README.md"
license = {text = "MIT"}
license = {text = "Apache 2"}
dependencies = [
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm noticing these dependencies don't completely match with those in requirements.txt. Is that intentional?

"numpy<2",
"nibabel",
Expand Down
Loading