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 6 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
2 changes: 1 addition & 1 deletion Docker/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,4 @@ RUN pip install --no-cache-dir -r requirements.txt

COPY .. .

ENTRYPOINT ["python3", "-m", "WrapImage.nifti_wrapper"]
ENTRYPOINT ["python3", "-m", "WrapImage.nifti_wrapper"]
26 changes: 25 additions & 1 deletion Docker/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ This project is designed to run the `nifti_wrapper` script using a Docker contai

Before running the Docker container, here are the available options for the `Docker image` script:

- `input_file`: Path to the input 4D NIfTI file.
- `input_file`: Path to the input 4D NIfTI file or dicom files 4D images.
- `bvec_file`: Path to the b-vector file.
- `bval_file`: Path to the b-value file.
- `--affine`: Affine matrix for NIfTI image (optional).
Expand All @@ -44,6 +44,10 @@ 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 .
```

## Running the Docker Container

Expand All @@ -58,4 +62,24 @@ Before running the Docker container, here are the available options for the `Doc

Replace `brain.nii.gz`, `brain.bvec`, and `brain.bval` with the actual file names you want to use.

## Running the Docker container for reading in DICOM Images

1. you can run the same Docker container using the `docker run` command. This command runs the Docker image with the specified input files:

```sh
sudo docker run -it --rm --name TF2.4_IVIM-MRI_CodeCollection \
-v ~/TF2.4_IVIM-MRI_CodeCollection:/usr/src/app \
-v ~/TF2.4_IVIM-MRI_CodeCollection:/usr/app/output \
tf2.4_ivim-mri_codecollection
```

Then answer the prompts for inquiring information of DICOM Images in your terminal.

[Note that NIfTI and DICOM encode space differently](https://www.nitrc.org/plugins/mwiki/index.php/dcm2nii:MainPage#Spatial_Coordinates)

![image](https://github.com/user-attachments/assets/8ea21692-36ac-4773-aec7-6cb3a6838055)

##### The goal of dcm2niix is to create FSL format bvec/bval files for processing. A crucial concern is ensuring that the gradient directions are reported in the frame of reference expected by the software you use to fit your tractography. [dicom2niix should generate a ".bvec" file that reports the tensors as expected](https://www.nitrc.org/plugins/mwiki/index.php/dcm2nii:MainPage#Diffusion_Tensor_Imaging) by FSL's dtifit, where vectors are reported relative to image frame of reference (rather than relative to the scanner bore).

#### 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)
---
41 changes: 41 additions & 0 deletions Docker/dicom2nifti/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
FROM ubuntu:jammy AS build

RUN apt-get update && apt-get install -y --no-install-recommends \
build-essential \
cmake \
git \
libssl-dev \
wget \
pigz \
ca-certificates \
&& update-ca-certificates \
&& wget https://github.com/rordenlab/dcm2niix/archive/refs/tags/v1.0.20241211.tar.gz -O /tmp/dcm2niix.tar.gz \
&& mkdir -p /tmp/dcm2niix && tar -xzf /tmp/dcm2niix.tar.gz -C /tmp/dcm2niix --strip-components=1 \
&& mkdir /tmp/dcm2niix/build && cd /tmp/dcm2niix/build \
&& cmake -DBATCH_VERSION=ON -DZLIB_IMPLEMENTATION=Cloudflare -DUSE_JPEGLS=ON -DUSE_OPENJPEG=ON .. \
&& make && make install \
&& rm -rf /tmp/dcm2niix* \
&& apt-get remove -y wget git cmake \
&& apt-get autoremove -y \
&& apt-get clean \
&& rm -rf /var/lib/apt/lists/*


FROM python:3.11-slim

WORKDIR /usr/src/app

RUN apt-get update && apt-get install -y --no-install-recommends \
pigz \
&& apt-get clean \
&& rm -rf /var/lib/apt/lists/*

COPY --from=build /usr/local/bin/dcm2niix /usr/local/bin/dcm2niix

COPY ../../requirements.txt ./

RUN pip install --no-cache-dir -r requirements.txt

COPY ../.. .

ENTRYPOINT ["python3", "-m", "WrapImage.inquire_dicom2niix"]
233 changes: 233 additions & 0 deletions WrapImage/inquire_dicom2niix.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,233 @@
import os
from pathlib import Path
import subprocess
import yaml
import inquirer

def prompt_input_directory():
return inquirer.prompt([
inquirer.Path(
"path",
message="📂 Select an input directory containing DICOM image files:",
path_type=inquirer.Path.DIRECTORY,
exists=True,
)
])["path"]


def prompt_output_directory(input_dir):
# List subfolders of input_dir
subdirs = [
name for name in os.listdir(input_dir)
if os.path.isdir(os.path.join(input_dir, name))
]

choices = [f"./{name}" for name in subdirs]
choices.append("📥 Enter a custom output path...")

answer = inquirer.prompt([
inquirer.List(
"choice",
message=f"📁 Choose an output directory for NIfTI files from:\n → {input_dir}",
choices=choices
)
])["choice"]

if answer == "📥 Enter a custom output path...":
return inquirer.prompt([
inquirer.Path(
"custom_path",
message="📥 Enter custom output directory path:",
path_type=inquirer.Path.DIRECTORY,
exists=True
)
])["custom_path"]
else:
return os.path.abspath(os.path.join(input_dir, answer.strip("./")))


def generate_batch_config(
input_dirs,
output_dirs,
config_path="batch_config.yaml"
):
"""
input_dirs: List of input directory paths
output_dirs: Single output directory (string) OR list of output directories
"""

# Normalize inputs
if isinstance(output_dirs, str):
output_dirs = [output_dirs] * len(input_dirs)
elif isinstance(output_dirs, list):
if len(input_dirs) != 1 and len(output_dirs) != len(input_dirs):
raise ValueError("Number of output directories must match number of input directories, unless only one output directory is provided.")

config = {
"Options": {
"isGz": True, # compressed nii.gz
"isFlipY": False, # flip Y-axis of images
"isVerbose": False, # by default is verbose; this value does not change anything
"isCreateBIDS": True, # create BIDS-compatible NIfTI and JSON files
"isOnlySingleFile": False
},
"Files": []
}

for in_dir, out_dir in zip(input_dirs, output_dirs):
if not os.path.isdir(in_dir):
print(f"Warning: {in_dir} is not a valid directory.")
continue

files = os.listdir(in_dir)
if not files:
print(f"No files found in {in_dir}")
continue

files = [f for f in os.listdir(in_dir) if os.path.isfile(os.path.join(in_dir, f))]
if not files:
print(f"No files found in {in_dir}")
continue

for f in files:
filename = os.path.splitext(f)[0].replace(" ", "_").lower()

config["Files"].append({
"in_dir": os.path.abspath(in_dir),
"out_dir": os.path.abspath(out_dir),
"filename": filename
})

with open(config_path, 'w') as f:
yaml.dump(config, f, sort_keys=False, default_flow_style=False)

print(f"Config written to {config_path}")

def dicom_to_niix(vol_dir: Path, out_dir: Path, merge_2d: bool = False):
"""
For converting DICOM images to a (compresssed) 4d nifti image
"""
os.makedirs(out_dir, exist_ok=True)

try:
res = subprocess.run(
[
"dcm2niix",
"-f", "%s_%p", # dcm2niix attempts to provide a sensible file naming scheme
"-o", out_dir, # output directory
"-z", "y", #specifying compressed nii.gz file
"-m", "y" if merge_2d else "n", # Add merge option
# https://www.nitrc.org/plugins/mwiki/index.php/dcm2nii:MainPage
# for further configuration for general usage see page above
vol_dir # input directory
],
capture_output=True,
text=True,
check=True
)

nifti_files = list(Path(out_dir).glob("*.nii.gz"))
if not nifti_files:
raise RuntimeError("No NIfTI (.nii.gz) files were generated.")

bval_files = list(out_dir.glob("*.bval"))
bvec_files = list(out_dir.glob("*.bvec"))
bval_path = str(bval_files[0]) if bval_files else None
bvec_path = str(bvec_files[0]) if bvec_files else None


return nifti_files[0], bval_path, bvec_path

except subprocess.CalledProcessError as e:
raise RuntimeError(f"dcm2niix failed: {e.stderr}")

if __name__ == "__main__":
input_dirs = []
output_dirs = []

print("Inquiring to convert your DICOM Images to NIfTI Image(s) \n")

while True:
input_dir = prompt_input_directory()
output_dir = prompt_output_directory(input_dir)

input_dirs.append(input_dir)
output_dirs.append(output_dir)

add_more = inquirer.prompt([
inquirer.Confirm(
"more",
message="➕ Do you want to add another input/output pair?",
default=False
)
])["more"]

if not add_more:
break

if len(input_dirs) == 1:
print("📥 Single input/output pair detected — using `dcm2niix`...")
vol_dir = Path(input_dirs[0])
out_dir = Path(output_dirs[0])

merge_answer = inquirer.prompt([
inquirer.Confirm(
"merge",
message="🧩 Do you want to merge 2D slices into a single NIfTI (-m y)?",
default=True
)
])
merge_2d = merge_answer["merge"]

try:
nifti, bval, bvec = dicom_to_niix(vol_dir, out_dir, merge_2d=merge_2d)
print(f"\n✅ NIfTI file created: {nifti}")
if bval:
print(f" 📈 bval: {bval}")
if bvec:
print(f" 📊 bvec: {bvec}")

post_process = inquirer.prompt([
inquirer.Confirm(
"run_post",
message="🧩 Do you want to run IVIM fit algorithm on the NIfTI file now?",
default=True
)
])["run_post"]

if bvec and bval and post_process:
print("\n🧩 Running post-processing: OSIPI IVIM fitting...\n")
subprocess.run([
"python3", "-m", "WrapImage.nifti_wrapper",
str(nifti),
str(bvec),
str(bval),
], check=True)
print("✅ NIfTI post-processing completed.")
except RuntimeError as err:
print(f"❌ Conversion failed: {err}")

else:
print("📥📥📥 Multiple inputs detected — generating batch config and using `dcm2niibatch`...")
config_path = "batch_config.yaml"
generate_batch_config(input_dirs, output_dirs, config_path=config_path)

try:
with subprocess.Popen(
["dcm2niibatch", config_path],
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
text=True,
bufsize=1
) as proc:
# for line in proc.stdout: # uncomment this to see verbose logs
# print(line.strip())
proc.wait()
if proc.returncode != 0:
raise subprocess.CalledProcessError(proc.returncode, proc.args)

print("✅ Batch conversion completed successfully.")

except subprocess.CalledProcessError as err:
print(f"❌ Batch conversion failed:\n{err}")

8 changes: 4 additions & 4 deletions WrapImage/nifti_wrapper.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import argparse
import json
import os
from pathlib import Path
import nibabel as nib
from src.wrappers.OsipiBase import OsipiBase
import numpy as np
from tqdm import tqdm


def read_nifti_file(input_file):
"""
For reading the 4d nifti image
Expand Down Expand Up @@ -81,9 +81,9 @@ def loop_over_first_n_minus_1_dimensions(arr):

if __name__ == "__main__":
parser = argparse.ArgumentParser(description="Read a 4D NIfTI phantom file along with BIDS JSON, b-vector, and b-value files.")
parser.add_argument("input_file", type=str, help="Path to the input 4D NIfTI file.")
parser.add_argument("bvec_file", type=str, help="Path to the b-vector file.")
parser.add_argument("bval_file", type=str, help="Path to the b-value file.")
parser.add_argument("input_file", type=Path, help="Path to the input 4D NIfTI file")
parser.add_argument("bvec_file", type=Path, help="Path to the b-vector file.")
parser.add_argument("bval_file", type=Path, help="Path to the b-value file.")
parser.add_argument("--affine", type=float, nargs="+", help="Affine matrix for NIfTI image.")
parser.add_argument("--algorithm", type=str, default="OJ_GU_seg", help="Select the algorithm to use.")
parser.add_argument("--algorithm_args", nargs=argparse.REMAINDER, help="Additional arguments for the algorithm.")
Expand Down
2 changes: 2 additions & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
numpy<2
nibabel
inquirer
pyyaml
scipy
torchio
torch
Expand Down