From d3384fd04b6ca7bf98e270d357b7964f4b4ebc5b Mon Sep 17 00:00:00 2001 From: Jennings Zhang Date: Fri, 5 Jan 2024 15:18:56 -0500 Subject: [PATCH] :crystal_ball: --- .github/workflows/ci.yml | 56 ++++++++++++---------------------------- examples/download.sh | 13 +++++----- setup.py | 8 +----- tests/test_example.py | 21 --------------- zb.py | 54 ++++++++++++++++++++++++++------------ 5 files changed, 62 insertions(+), 90 deletions(-) delete mode 100644 tests/test_example.py diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e2eceaa..e448254 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -17,27 +17,6 @@ on: branches: [ main ] jobs: - test: - name: Unit tests - runs-on: ubuntu-22.04 - steps: - - uses: actions/checkout@v4 - - uses: docker/setup-buildx-action@v3 - - name: Build - uses: docker/build-push-action@v5 - with: - build-args: extras_require=dev - context: . - load: true - push: false - tags: "localhost/local/app:dev" - cache-from: type=gha - cache-to: type=gha,mode=max - - name: Run pytest - run: | - docker run -v "$GITHUB_WORKSPACE:/app:ro" -w /app localhost/local/app:dev \ - pytest -o cache_dir=/tmp/pytest - build: name: Build if: github.event_name == 'push' || github.event_name == 'release' @@ -106,34 +85,31 @@ jobs: # If you have a directory called examples/incoming/ and examples/outgoing/, then # run your ChRIS plugin with no parameters, and assert that it creates all the files # which are expected. File contents are not compared. + - name: Download example data + run: cd examples && ./download.sh incoming - name: Run examples id: run_examples run: | - if ! [ -d 'examples/incoming/' ] || ! [ -d 'examples/outgoing/' ]; then - echo "No examples." - exit 0 - fi - dock_image=${{ steps.info.outputs.local_tag }} output_dir=$(mktemp -d) cmd=$(docker image inspect -f '{{ (index .Config.Cmd 0) }}' $dock_image) docker run --rm -u "$(id -u):$(id -g)" \ -v "$PWD/examples/incoming:/incoming:ro" \ -v "$output_dir:/outgoing:rw" \ - $dock_image $cmd /incoming /outgoing + $dock_image $cmd --thresholds template.nii.gz:37.0 /incoming /outgoing - for expected_file in $(find examples/outgoing -type f); do - fname="${expected_file##*/}" - out_path="$output_dir/$fname" - printf "Checking output %s exists..." "$out_path" - if [ -f "$out_path" ]; then - echo "ok" - else - echo "not found" - exit 1 - fi - done - + set +e + diff -q examples/incoming/age34/template.nii.gz $output_dir/age34/template.nii.gz + if [ "$?" = "0" ]; then + echo "age34/template.nii.gz should have been changed, but it was not." + exit 1 + fi + diff -q examples/incoming/age34/mask.nii.gz $output_dir/age34/mask.nii.gz + if [ "$?" = "1" ]; then + echo "age34/template.nii.gz should have been copied, but it was changed." + exit 1 + fi + echo "tests passed" - name: Login to DockerHub if: (github.event_name == 'push' || github.event_name == 'release') && contains(steps.info.outputs.tags_csv, 'docker.io') uses: docker/login-action@v3 @@ -155,7 +131,7 @@ jobs: file: ./Dockerfile tags: ${{ steps.info.outputs.tags_csv }} # if non-x86_84 architectures are supported, add them here - platforms: linux/amd64 #,linux/arm64,linux/ppc64le + platforms: linux/amd64,linux/arm64,linux/ppc64le push: ${{ steps.info.outputs.push }} cache-to: type=gha,mode=max diff --git a/examples/download.sh b/examples/download.sh index 23b4eac..501b160 100755 --- a/examples/download.sh +++ b/examples/download.sh @@ -3,11 +3,12 @@ mkdir "$1" cd "$1" +mkdir age24 age34 -wget -O template24.nii.gz https://fetalmri-hosting-of-medical-image-analysis-platform-dcb83b.apps.shift.nerc.mghpcc.org/api/v1/files/307/template.nii.gz -wget -O ventricles24.nii.gz https://fetalmri-hosting-of-medical-image-analysis-platform-dcb83b.apps.shift.nerc.mghpcc.org/api/v1/files/331/ventricles.nii.gz -wget -O mask24.nii.gz https://fetalmri-hosting-of-medical-image-analysis-platform-dcb83b.apps.shift.nerc.mghpcc.org/api/v1/files/330/mask.nii.gz +wget -O age24/template.nii.gz https://fetalmri-hosting-of-medical-image-analysis-platform-dcb83b.apps.shift.nerc.mghpcc.org/api/v1/files/307/template.nii.gz +wget -O age24/ventricles.nii.gz https://fetalmri-hosting-of-medical-image-analysis-platform-dcb83b.apps.shift.nerc.mghpcc.org/api/v1/files/331/ventricles.nii.gz +wget -O age24/mask.nii.gz https://fetalmri-hosting-of-medical-image-analysis-platform-dcb83b.apps.shift.nerc.mghpcc.org/api/v1/files/330/mask.nii.gz -wget -O template34.nii.gz https://fetalmri-hosting-of-medical-image-analysis-platform-dcb83b.apps.shift.nerc.mghpcc.org/api/v1/files/351/template.nii.gz -wget -O ventricles34.nii.gz https://fetalmri-hosting-of-medical-image-analysis-platform-dcb83b.apps.shift.nerc.mghpcc.org/api/v1/files/350/ventricles.nii.gz -wget -O mask34.nii.gz https://fetalmri-hosting-of-medical-image-analysis-platform-dcb83b.apps.shift.nerc.mghpcc.org/api/v1/files/310/mask.nii.gz +wget -O age34/template.nii.gz https://fetalmri-hosting-of-medical-image-analysis-platform-dcb83b.apps.shift.nerc.mghpcc.org/api/v1/files/351/template.nii.gz +wget -O age34/ventricles.nii.gz https://fetalmri-hosting-of-medical-image-analysis-platform-dcb83b.apps.shift.nerc.mghpcc.org/api/v1/files/350/ventricles.nii.gz +wget -O age34/mask.nii.gz https://fetalmri-hosting-of-medical-image-analysis-platform-dcb83b.apps.shift.nerc.mghpcc.org/api/v1/files/310/mask.nii.gz diff --git a/setup.py b/setup.py index 257ac94..9ded679 100644 --- a/setup.py +++ b/setup.py @@ -38,11 +38,5 @@ def get_version(rel_path: str) -> str: 'Topic :: Scientific/Engineering', 'Topic :: Scientific/Engineering :: Bio-Informatics', 'Topic :: Scientific/Engineering :: Medical Science Apps.' - ], - extras_require={ - 'none': [], - 'dev': [ - 'pytest~=7.1' - ] - } + ] ) diff --git a/tests/test_example.py b/tests/test_example.py deleted file mode 100644 index 324e4d0..0000000 --- a/tests/test_example.py +++ /dev/null @@ -1,21 +0,0 @@ -from pathlib import Path - -from zb import parser, main - - -def test_main(tmp_path: Path): - # setup example data - inputdir = tmp_path / 'incoming' - outputdir = tmp_path / 'outgoing' - inputdir.mkdir() - outputdir.mkdir() - (inputdir / 'plaintext.txt').write_text('hello ChRIS, I am a ChRIS plugin') - - # simulate run of main function - options = parser.parse_args(['--word', 'ChRIS', '--pattern', '*.txt']) - main(options, inputdir, outputdir) - - # assert behavior is expected - expected_output_file = outputdir / 'plaintext.count.txt' - assert expected_output_file.exists() - assert expected_output_file.read_text() == '2' diff --git a/zb.py b/zb.py index 35e37da..dcf39f5 100644 --- a/zb.py +++ b/zb.py @@ -1,10 +1,17 @@ #!/usr/bin/env python - +import os +import shutil +import sys from pathlib import Path from argparse import ArgumentParser, Namespace, ArgumentDefaultsHelpFormatter -from tqdm.contrib.concurrent import process_map +from typing import Callable + +import numpy as np +import nibabel as nib -from chris_plugin import chris_plugin, PathMapper +from tqdm.contrib.concurrent import process_map, thread_map + +from chris_plugin import chris_plugin, PathMapper, helpers __version__ = '1.0.0' @@ -22,8 +29,8 @@ parser = ArgumentParser(description='Set the background intensity of MRI volumes to zero.', formatter_class=ArgumentDefaultsHelpFormatter) -parser.add_argument('-t', '--threshold', required=False, type=str, - default='.nii.gz:37', +parser.add_argument('-t', '--thresholds', required=False, type=str, + default='.nii.gz:37.0', help='A filename glob and background intensity threshold. Multiple pairs should be comma-separated,' ' i.e. GLOB1:THRESHOLD1,GLOB2:THRESHOLD2,...') parser.add_argument('-J', '--threads', type=int, default=0, @@ -36,22 +43,37 @@ @chris_plugin( parser=parser, title='Zero MRI Background', - category='', # ref. https://chrisstore.co/plugins - min_memory_limit='1Gi', # supported units: Mi, Gi - min_cpu_limit='1000m', # millicores, e.g. "1000m" = 1 CPU core + category='MRI', # ref. https://chrisstore.co/plugins + min_memory_limit='4Gi', # supported units: Mi, Gi + min_cpu_limit='4000m', # millicores, e.g. "1000m" = 1 CPU core min_gpu_limit=0 # set min_gpu_limit=1 to enable GPU ) def main(options: Namespace, inputdir: Path, outputdir: Path): print(DISPLAY_TITLE, flush=True) - mapper = PathMapper.file_mapper(inputdir, outputdir, glob=options.pattern, suffix='.count.txt') - for input_file, output_file in mapper: - # The code block below is a small and easy example of how to use a ``PathMapper``. - # It is recommended that you put your functionality in a helper function, so that - # it is more legible and can be unit tested. - data = input_file.read_text() - frequency = data.count(options.word) - output_file.write_text(str(frequency)) + mapper = PathMapper.file_mapper(inputdir, outputdir) + nproc = options.threads if options.threads else len(os.sched_getaffinity(0)) + threshold_map = {k: float(v) for k, v in helpers.parse_csv_as_dict(options.thresholds).items()} + results = thread_map(curry_zerobg(threshold_map), mapper, max_workers=nproc, total=mapper.count(), maxinterval=0.1) + if not all(results): + sys.exit(1) + + +def curry_zerobg(threshold_map: dict[str, float]) -> Callable[[tuple[Path, Path]], bool]: + return lambda t: zerobg(t[0], t[1], threshold_map) + + +def zerobg(input: Path, output: Path, threshold_map: dict[str, float]) -> bool: + threshold = next((v for k, v in threshold_map.items() if input.name.endswith(k)), None) + if threshold is None: + shutil.copy(input, output) + return True + + vol = nib.load(input) + data = vol.get_fdata() + result = np.where(data <= threshold, 0, data) + output_vol = vol.__class__(result, vol.affine, header=vol.header) + nib.save(output_vol, output) if __name__ == '__main__':