Skip to content
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

Adds Docker build and GHA test of container #34

Merged
merged 35 commits into from
Mar 1, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
b0b47ae
WIP: create Dockerfile
tomaroberts Feb 20, 2024
15942d9
Update build_and_test GHA with container creation and testing
tomaroberts Feb 20, 2024
d9b05e3
Create test_dcm.py
tomaroberts Feb 20, 2024
23cf1b0
Create test_version.py
tomaroberts Feb 20, 2024
f99e4fd
Amends Exception output
tomaroberts Feb 20, 2024
e23e41b
Changes variable name to slice_index for clarity
tomaroberts Feb 20, 2024
a8a6c3e
Create test_dcm_writer.py
tomaroberts Feb 20, 2024
32aec6d
Create test_nii.py
tomaroberts Feb 20, 2024
7003bf1
Create test_run.py
tomaroberts Feb 20, 2024
b2bec42
Adds unit tests to GHA
tomaroberts Feb 21, 2024
ccb57eb
Adds pytest coverage comment/badge to GHA
tomaroberts Feb 21, 2024
54e783b
Updates README with coverage badges
tomaroberts Feb 21, 2024
765af04
Fixes some formatting issues
tomaroberts Feb 21, 2024
e4cf41d
Amends and re-tests pytest coverage badge
tomaroberts Feb 21, 2024
2deaf28
Typo – re-test
tomaroberts Feb 21, 2024
0f382d9
Test coverage badge on unit-tests branch
tomaroberts Feb 21, 2024
be589f9
Create __init__.py
tomaroberts Feb 21, 2024
2bb4058
Update .gitignore
tomaroberts Feb 21, 2024
1f97294
Adds Docker section to README
tomaroberts Feb 22, 2024
4edd9eb
Test container build GHA
tomaroberts Mar 1, 2024
a711e2e
Adds LABEL
tomaroberts Mar 1, 2024
e538735
Revert "Test container build GHA"
tomaroberts Mar 1, 2024
5539c40
Adds publish container to GHCR workflow
tomaroberts Mar 1, 2024
bd824eb
Update publish_ghcr.yml
tomaroberts Mar 1, 2024
d2e6903
Update publish_ghcr.yml
tomaroberts Mar 1, 2024
2272445
Merge branch 'unit-tests' into docker-build
tomaroberts Mar 1, 2024
0fcb428
Test container build with TestPyPI package build
tomaroberts Mar 1, 2024
c3d6e99
Update publish_testpypi.yml
tomaroberts Mar 1, 2024
975800a
Re-test
tomaroberts Mar 1, 2024
94e6c81
Adds second wait time if TestPyPI install fails
tomaroberts Mar 1, 2024
2b72406
Test updated secrets
tomaroberts Mar 1, 2024
e9a0c42
Moves container GHCR build to PyPI release GHA
tomaroberts Mar 1, 2024
c8473c7
Minor formatting changes
tomaroberts Mar 1, 2024
d46fbf4
Formatting fix
tomaroberts Mar 1, 2024
ee02e23
Update Dockerfile to build from source
tomaroberts Mar 1, 2024
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
62 changes: 58 additions & 4 deletions .github/workflows/build_and_test_cli.yml
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
# Workflow to build nii2dcm and test different command line interface (CLI) options
# Workflow to build nii2dcm, run unit tests and then execute command line interface (CLI) end-to-end

name: Build nii2dcm
name: Build & Test nii2dcm

on:
pull_request:

jobs:
build-and-test:
name: Build
venv-build-and-test:
name: venv + E2E

runs-on: ${{ matrix.os }}

Expand Down Expand Up @@ -57,6 +57,10 @@ jobs:
nii2dcm -h
nii2dcm -v

- name: Run unit tests
run: |
pytest tests/

- name: Test DicomMRISVR creation
run: |
# run nii2dcm
Expand All @@ -65,3 +69,53 @@ jobs:
ls ./output
# assert DICOM files exist
[ -f "./output/IM_0001.dcm" ] && echo "Output DICOM file exists" || exit 1

- name: Build pytest coverage file
run: |
pytest --junitxml=pytest.xml --cov-report=term-missing:skip-covered --cov=nii2dcm tests/ | tee pytest-coverage.txt ; echo $?

- name: Pytest coverage comment
id: coverageComment
uses: MishaKav/pytest-coverage-comment@main
with:
pytest-coverage-path: ./pytest-coverage.txt
junitxml-path: ./pytest.xml
default-branch: unit-tests

- name: Update Coverage Badge
uses: schneegans/dynamic-badges-action@v1.7.0
with:
auth: ${{ secrets.PYTEST_COVERAGE_COMMENT }}
gistID: 57ef8057d04f67dbe6e64df410b83079
filename: nii2dcm-pytest-coverage-comment.json
label: Coverage Report
message: ${{ steps.coverageComment.outputs.coverage }}
color: ${{ steps.coverageComment.outputs.color }}
namedLogo: python

container-build-and-test:
name: Container

runs-on: ${{ matrix.os }}

strategy:
fail-fast: false
matrix:
os: [ ubuntu-latest ]
python-version: [ '3.9' ]

steps:
- uses: actions/checkout@v3
with:
fetch-depth: 0

- name: Build container
run: |
docker build -t nii2dcm --progress=plain --no-cache .
docker ps

- name: Test nii2dcm container
run: |
docker run nii2dcm -h
echo "nii2dcm version:"
docker run nii2dcm -v
58 changes: 57 additions & 1 deletion .github/workflows/publish_pypi.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ permissions:
actions: write

jobs:
testpypi-publish:
pypi-publish:

name: Publish to PyPI
runs-on: ubuntu-latest
Expand Down Expand Up @@ -86,6 +86,7 @@ jobs:
time: '150' # seconds

- name: Install latest PyPI version in fresh venv
id: attempt1
run: |
NII2DCM_VERSION=`echo "$(nii2dcm -v)"`
echo $NII2DCM_VERSION
Expand All @@ -97,3 +98,58 @@ jobs:
nii2dcm -h
echo "nii2dcm version:"
nii2dcm -v
continue-on-error: true

- name: Wait longer
if: steps.attempt1.outcome != 'success'
uses: GuillaumeFalourd/wait-sleep-action@v1
with:
time: '150' # seconds

- name: Re-attempt PyPI install
if: steps.attempt1.outcome != 'success'
run: |
NII2DCM_VERSION=`echo "$(nii2dcm -v)"`
echo $NII2DCM_VERSION
python -m venv nii2dcm-temp
source nii2dcm-temp/bin/activate
pip install --upgrade pip
pip install setuptools wheel
pip install nii2dcm==$NII2DCM_VERSION
nii2dcm -h
echo "nii2dcm version:"
nii2dcm -v

ghcr-publish:
needs: pypi-publish
runs-on: ubuntu-latest

permissions:
contents: read
packages: write

steps:
- name: Checkout repository
uses: actions/checkout@v4

- name: Log in to GHCR
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ secrets.GHCR_USERNAME }}
password: ${{ secrets.GHCR_TOKEN }}

- name: Extract metadata (tags, labels) for Docker
id: meta
uses: docker/metadata-action@v5
with:
images: |
ghcr.io/tomaroberts/nii2dcm

- name: Build and push Docker image
uses: docker/build-push-action@v5
with:
context: .
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
23 changes: 23 additions & 0 deletions .github/workflows/publish_testpypi.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
# They are provided by a third-party and are governed by
# separate terms of service, privacy policy, and support
# documentation.
#

name: Publish package to TestPyPI

Expand Down Expand Up @@ -85,6 +86,28 @@ jobs:
time: '150' # seconds

- name: Install latest TestPyPI version in fresh venv
id: attempt1
run: |
NII2DCM_VERSION=`echo "$(nii2dcm -v)"`
echo $NII2DCM_VERSION
python -m venv nii2dcm-temp
source nii2dcm-temp/bin/activate
pip install --upgrade pip
pip install setuptools wheel
pip install --index-url https://test.pypi.org/simple/ --extra-index-url https://pypi.org/simple/ nii2dcm==$NII2DCM_VERSION
nii2dcm -h
echo "nii2dcm version:"
nii2dcm -v
continue-on-error: true

- name: Wait longer
if: steps.attempt1.outcome != 'success'
uses: GuillaumeFalourd/wait-sleep-action@v1
with:
time: '150' # seconds

- name: Re-attempt TestPyPI install
if: steps.attempt1.outcome != 'success'
run: |
NII2DCM_VERSION=`echo "$(nii2dcm -v)"`
echo $NII2DCM_VERSION
Expand Down
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,8 @@ coverage.xml
.hypothesis/
.pytest_cache/
cover/
pytest-coverage.txt
pytest.xml

# Sphinx documentation
docs/_build/
Expand Down
29 changes: 29 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# Use the official Python image as the base image
FROM python:3.9-slim

LABEL org.opencontainers.image.source https://github.com/tomaroberts/nii2dcm

# Setup
COPY . /home/nii2dcm
WORKDIR /home/nii2dcm

# Install system dependencies
RUN apt-get update && apt-get install -y \
bash git \
&& apt-get clean

# Update base packages
RUN pip install --upgrade pip && \
pip install setuptools wheel

# Install nii2dcm requirements
RUN pip install -r requirements.txt

# Build package from source
RUN pip install .

# Test nii2dcm install
# To see output locally during build process: docker build -t nii2dcm --progress=plain .
RUN nii2dcm -h

ENTRYPOINT ["nii2dcm"]
24 changes: 24 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,10 @@
·
<a href="https://github.com/tomaroberts/nii2dcm/issues">Request Feature</a>
</p>
<p align="center">
<img src="https://github.com/tomaroberts/nii2dcm/actions/workflows/build_and_test_cli.yml/badge.svg?branch=unit-tests">
<img src="https://img.shields.io/endpoint?url=https://gist.githubusercontent.com/tomaroberts/57ef8057d04f67dbe6e64df410b83079/raw/nii2dcm-pytest-coverage-comment.json">
</p>
</div>


Expand Down Expand Up @@ -138,6 +142,26 @@ Currently, attributes to transfer are [listed here in the DicomMRI class](https:
<p align="right">(<a href="#readme-top">back to top</a>)</p>


<!-- Docker -->
## Docker
nii2dcm is also available as a Docker container.

Pull the latest container with:
```shell
docker pull ghcr.io/tomaroberts/nii2dcm/nii2dcm:latest
```

Run the containerised nii2dcm:
```shell
# display nii2dcm version
docker run nii2dcm -v

# perform nii2dcm conversion
docker run nii2dcm nifti-file.nii.gz dicom-output-directory/ -d MR
```

<p align="right">(<a href="#readme-top">back to top</a>)</p>

<!-- ROADMAP -->
## Roadmap

Expand Down
2 changes: 1 addition & 1 deletion nii2dcm/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ def cli(args=None):
parser.add_argument("input_file", type=str, help="[.nii/.nii.gz] input NIfTI file")
parser.add_argument("output_dir", type=str, help="[directory] output DICOM path")
parser.add_argument(
"-d","--dicom_type",
"-d", "--dicom_type",
type=str,
help="[string] type of DICOM. Available types: MR, SVR."
)
Expand Down
2 changes: 1 addition & 1 deletion nii2dcm/_version.py
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
import dunamai as _dunamai
__version__ = _dunamai.get_version("nii2dcm", third_choice=_dunamai.Version.from_any_vcs).serialize()
__version__ = _dunamai.get_version("nii2dcm", third_choice=_dunamai.Version.from_any_vcs).serialize()
3 changes: 1 addition & 2 deletions nii2dcm/dcm.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@
import pydicom as pyd
from pydicom.dataset import FileDataset, FileMetaDataset

from nii2dcm.utils import dcm_dictionary_update
from nii2dcm.modules.patient import Patient
from nii2dcm.modules.general_study import GeneralStudy
from nii2dcm.modules.patient_study import PatientStudy
Expand Down Expand Up @@ -67,7 +66,7 @@

"""
Set Dicom Date/Time
Important: doing this once sets all Instances/Series/Study creation dates and times to the same values. Whereas,
Important: doing this once sets all Instances/Series/Study creation dates and times to the same values. Whereas,
doing this within the Modules would every so slightly offset the times
"""
# TODO shift to utils.py and propagate to Modules, or, create method within this Dicom class
Expand Down Expand Up @@ -209,7 +208,7 @@
super().__init__(filename)

"""
Set DICOM attributes which are located outside of the MR Image Module to MR-specific values

Check warning on line 211 in nii2dcm/dcm.py

View workflow job for this annotation

GitHub Actions / venv + E2E (ubuntu-latest, 3.9)

W291 trailing whitespace
"""
self.ds.Modality = 'MR'

Expand Down Expand Up @@ -302,4 +301,4 @@
'RequestingService',
]


Check warning on line 304 in nii2dcm/dcm.py

View workflow job for this annotation

GitHub Actions / venv + E2E (ubuntu-latest, 3.9)

W391 blank line at end of file
8 changes: 4 additions & 4 deletions nii2dcm/dcm_writer.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,19 +6,19 @@
import pydicom as pyd


def write_slice(dcm, img_data, instance_index, output_dir):
def write_slice(dcm, img_data, slice_index, output_dir):
"""
write a single DICOM slice

dcm – nii2dcm DICOM object
img_data - [nX, nY, nSlice] image pixel data, such as from NIfTI file
instance_indexinstance index (important: counts from 0)
slice_indexslice index in nibabel img_data array (important: counts from 0, whereas DICOM instances count from 1)
output_dir – output DICOM file save location
"""

output_filename = r'IM_%04d.dcm' % (instance_index + 1) # begin filename from 1, e.g. IM_0001.dcm
output_filename = r'IM_%04d.dcm' % (slice_index + 1) # begin filename from 1, e.g. IM_0001.dcm

img_slice = img_data[:, :, instance_index]
img_slice = img_data[:, :, slice_index]

# Instance UID – unique to current slice
dcm.ds.SOPInstanceUID = pyd.uid.generate_uid(None)
Expand Down
4 changes: 2 additions & 2 deletions nii2dcm/modules/mr_image.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,28 +24,28 @@
# https://dicom.nema.org/medical/Dicom/current/output/chtml/part03/sect_C.8.3.html#sect_C.8.3.1.1.1
# For now, will omit thereby inheriting parent value
# self.ds.ImageType = ''

self.ds.SamplesPerPixel = 1

# PhotometricInterpretation
# TODO: decide MONOCHROME1 or MONOCHROME2 as default
# https://dicom.nema.org/medical/Dicom/current/output/chtml/part03/sect_C.7.6.3.html#sect_C.7.6.3.1.2
self.ds.PhotometricInterpretation = 'MONOCHROME2'

Check warning on line 34 in nii2dcm/modules/mr_image.py

View workflow job for this annotation

GitHub Actions / venv + E2E (ubuntu-latest, 3.9)

W293 blank line contains whitespace
# PresentationLUTShape
# depends on PhotometricInterpretation: https://dicom.innolitics.com/ciods/mr-image/general-image/20500020
if self.ds.PhotometricInterpretation == 'MONOCHROME2':
self.ds.PresentationLUTShape = 'IDENTITY'
elif self.ds.PhotometricInterpretation == 'MONOCHROME1':
self.ds.PresentationLUTShape = 'INVERSE'

Check warning on line 41 in nii2dcm/modules/mr_image.py

View workflow job for this annotation

GitHub Actions / venv + E2E (ubuntu-latest, 3.9)

W293 blank line contains whitespace
# Bits Allocated
# defined to equal 16 for MR Image Module
# https://dicom.nema.org/medical/Dicom/current/output/chtml/part03/sect_C.8.3.html#sect_C.8.3.1.1.4
self.ds.BitsAllocated = 16
self.ds.BitsStored = 12
self.ds.HighBit = self.ds.BitsStored - 1

Check warning on line 48 in nii2dcm/modules/mr_image.py

View workflow job for this annotation

GitHub Actions / venv + E2E (ubuntu-latest, 3.9)

W293 blank line contains whitespace
self.ds.ScanningSequence = 'RM' # :missing:, 'RM' = Research Mode
self.ds.SequenceVariant = '' # :missing:
self.ds.ScanOptions = '' # :missing:
Expand Down Expand Up @@ -89,10 +89,10 @@
self.ds.TemporalPositionIdentifier = '' # :missing:
self.ds.NumberOfTemporalPositions = ''
self.ds.TemporalResolution = '' # :missing:

Check warning on line 92 in nii2dcm/modules/mr_image.py

View workflow job for this annotation

GitHub Actions / venv + E2E (ubuntu-latest, 3.9)

W293 blank line contains whitespace
# Currently omitting, but part of NEMA MR Image module:
# NEMA Table 10-7 “General Anatomy Optional Macro Attributes”

Check warning on line 95 in nii2dcm/modules/mr_image.py

View workflow job for this annotation

GitHub Actions / venv + E2E (ubuntu-latest, 3.9)

W293 blank line contains whitespace
# Currently omitting, but part of NEMA MR Image module:
# NEMA Table 10-25 “Optional View and Slice Progression Direction Macro Attributes”

Expand Down
Empty file added tests/__init__.py
Empty file.
Loading
Loading