Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
8 changes: 7 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,10 @@
.virtualenv
*.tar
*.zip
data/
data/
.venv/
uv.lock
*egg-info/
# make sentinel files
.test
.format
31 changes: 31 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
.SUFFIXES:
# phony runners, but actual sentinel files so only rerun when needed
.PHONY: test format
test: .test
format: .format

SHELL := /usr/bin/bash
PYTHON ?= python3
PYTHON_FILES=$(wildcard *.py tests/*py tests/helpers/*.py)

.ONESHELL: # source in same shell as pytest
.test: $(PYTHON_FILES) single-shot.sh | .venv/
source .venv/bin/activate
$(PYTHON) -m pytest tests | tee $@

# don't format all. Would be a big git revision
.format: $(wildcard test/*py tests/helpers/*.py) #$(PYTHON_FILES)
isort $? | tee $@
black $? | tee -a $@

# if we don't have .venv, make it and install this package
# use 'uv' if we have it
.ONESHELL:
.venv/:
if command -v uv; then
uv venv .venv;
uv pip install -e .[dev];
else
$(PYTHON) -m venv .venv/;
$(PYTHON) -m pip install -e .[dev];
fi
29 changes: 29 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
[project]
name = "python-ismrmrd-server"
version = "0.1.0"
description = "MRD client/server pair and MRD format convertion utilities"
readme = "readme.md"
requires-python = ">=3.12"
dependencies = [
"ismrmrd>=1.14.2",
"matplotlib>=3.10.7",
"numpy>=2.3.4",
"pydicom>=3.0.1",
]
[project.optional-dependencies]
dev = [
"pytest",
"black",
"isort",
]
[tool.setuptools.packages.find]
# Include all packages found under "src"
include = ["*py"]
# Exclude tests and docs directories
exclude = ["docker", "*sh", "tests/"]

[pytest]
tmp_path_retention_count = 1

[tool.pytest.ini_options]
norecursedirs = "tests/helpers"
25 changes: 25 additions & 0 deletions single-shot.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
#!/usr/bin/env bash
#
# fork the server for a single shot client call
# use enviornment to adjust parameters
#


PORT=${PORT:-9002}
INFILE=${INFILE:-in/example.h5}
OUTFILE=${OUTFILE:-/tmp/examle.h5}
CONFIG=${CONFIG:-invertcontrast}
OUTGROUP=${OUTGROUP:-dataset} # ismrmr's default

scriptdir="$(cd $(dirname "$0"); pwd)"

# Make output directory if needed
mkdir -p "$(dirname "$OUTFILE")"

python $scriptdir/main.py -p $PORT &
pid_server=$!
python $scriptdir/client.py "$INFILE" -p $PORT -o "$OUTFILE" -c $CONFIG -G "$OUTGROUP"

kill $pid_server
wait $pid_server

66 changes: 66 additions & 0 deletions tests/helpers/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import sys

import ismrmrd
import numpy as np
import pydicom
import pydicom.data

import dicom2mrd

MAX = 4095 # 12-bit data


def checkers(nz, ny, nx):
checker_data = np.zeros((nz,ny, nx), dtype=np.uint16)
for z in range(nz):
for y in range(ny):
for x in range(nx):
if (x + y + z) % 2 == 0:
checker_data[z, y, x] = MAX
else:
checker_data[z, y, x] = 0
return checker_data


def write_example(data, outdir, series_number=1):
"""
Write example data files in mdr.h5 and dicom/ formats
:param outdir: where to save, likely `tmp_path` from pytest
:param series_number: dicom header information
:returns: dict with 'mrd' and 'dcmdir' keys
"""
mrd_h5 = outdir / "checker.h5"

ds = pydicom.dcmread(pydicom.data.get_testdata_file("MR_small.dcm"))

#ds.PixelData = checkers(ds.NumberOfFrames,ds.Columns,ds.Rows).tobytes()
ds.PixelData = data.tobytes()
(ds.NumberOfFrames, ds.Columns, ds.Rows) = data.shape
ds.SeriesDescription = "Test Checkerboard"
ds.MagneticFieldStrength = 1.5
ds.AcquisitionTime = "120000.000000"
ds.SeriesNumber = series_number

# Create temporary DICOM folder and file
temp_dicom_dir = outdir / "temp_dicoms"
temp_dicom_dir.mkdir()
temp_dicom = temp_dicom_dir / "temp.dcm"
ds.save_as(temp_dicom)

# 3. use dicom2mrd to make a mrd h5 file
args = dicom2mrd.argparse.Namespace(
folder=temp_dicom_dir, outFile=mrd_h5, outGroup="dataset"
)
dicom2mrd.main(args)

return {'mrd': mrd_h5, 'dcmdir': temp_dicom_dir}


def mrd_data(filename, group="dataset"):
"""Read MRD image data from file"""
dataset = ismrmrd.Dataset(filename, group, False)

# Check what image groups are available
dataset_list = dataset.list()
image_groups = [name for name in dataset_list if name.startswith("image")]
return np.squeeze(dataset.read_image(image_groups[0], 0).data)
23 changes: 23 additions & 0 deletions tests/test_invert.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import os

import numpy as np
from helpers import MAX, mrd_data, checkers, write_example


def test_invert(tmp_path):
data = checkers(4,4,4)
examples = write_example(data, tmp_path)
in_file = examples["mrd"]
out_file = tmp_path / "out.h5"

os.environ["INFILE"] = str(in_file)
os.environ["OUTFILE"] = str(out_file)
os.system("./single-shot.sh")

# Read mrd files and extract numpy matrix
in_data = mrd_data(in_file)
out_data = mrd_data(out_file)
abs_diff = np.abs(out_data - in_data)
assert in_data[0, 0, 0] == MAX, "input as expected is 0"
assert out_data[0, 0, 0] == 0, "output is inverted"
assert np.all(abs_diff == MAX), "all voxels inverted"