From cbf4c3f727ce4253b0b41b78f5766e985a2315c7 Mon Sep 17 00:00:00 2001 From: Jennings Zhang Date: Tue, 14 Nov 2023 04:59:16 -0500 Subject: [PATCH] Handle .txt and add test --- .github/workflows/ci.yml | 36 ++++++++++++++++++++++++++++++++---- Dockerfile | 29 +++++++++++++++-------------- README.md | 11 +++++------ base/Dockerfile | 4 ++-- base/build.sh | 2 +- examples/.gitignore | 2 ++ examples/download.sh | 22 ++++++++++++++++++++++ mni2common.py | 39 ++++++++++++++++++++++----------------- requirements.txt | 2 -- setup.py | 4 ++-- 10 files changed, 103 insertions(+), 48 deletions(-) create mode 100644 examples/.gitignore create mode 100755 examples/download.sh delete mode 100644 requirements.txt diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 21660e8..fa6cdb8 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -82,13 +82,41 @@ jobs: tags: ${{ steps.info.outputs.local_tag }} load: true cache-from: type=gha - # 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: examples/download.sh - name: Run examples id: run_examples run: | - echo TODO + mkdir /tmp/outgoing + docker run --rm -u "$(id -u):$(id -g)" \ + -v "${{ github.workspace }}/examples/incoming:/incoming:ro" \ + -v "/tmp/outgoing"/outgoing:rw" \ + ${{ steps.info.outputs.local_tag }} + mni2common /incoming /outgoing + - name: Assert example outputs are found + run: | + expected_outputs=( + outgoing/colin27_t1_tal_lin.mnc2nii.log + outgoing/colin27_t1_tal_lin.nii + outgoing/colin27_t1_tal_lin_headmask.mnc2nii.log + outgoing/colin27_t1_tal_lin_headmask.nii + outgoing/colin27_t1_tal_lin_mask.mnc2nii.log + outgoing/colin27_t1_tal_lin_mask.nii + outgoing/colin_white_mc_left.mz3 + outgoing/colin_white_mc_mask_left.mz3 + outgoing/colin_white_mc_mask_right.mz3 + outgoing/colin_white_mc_right.mz3 + ) + + for expected_output in "${expected_outputs[@]}"; do + if ! [ -f "$1" ]; then + echo "expected $1 to be a file, but it does not." + parent_dir="$(dirname "$1")" + set -ex + ls -lh "$parent_dir" + exit 1 + fi + done - name: Login to DockerHub if: (github.event_name == 'push' || github.event_name == 'release') && contains(steps.info.outputs.tags_csv, 'docker.io') diff --git a/Dockerfile b/Dockerfile index 205368c..04c0dd4 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,26 +1,27 @@ -FROM docker.io/fnndsc/pl-mni2common:base-1 +FROM docker.io/fnndsc/pl-mni2common:base-2 AS base -LABEL org.opencontainers.image.authors="FNNDSC " \ - org.opencontainers.image.title="pl-mnc2common" \ - org.opencontainers.image.description="A ChRIS plugin to convert MINC volume and MNI .obj surface file formats to NIFTI and Wavefront OBJ respectively." +FROM base AS mni2mz3-installer + +RUN apt-get update \ + && apt-get install -y curl -# use micromamba to install binary python dependencies for multiarch build -RUN \ - --mount=type=cache,sharing=private,target=/home/mambauser/.mamba/pkgs,uid=57439,gid=57439 \ - --mount=type=cache,sharing=private,target=/opt/conda/pkgs,uid=57439,gid=57439 \ - micromamba install -y -n base -c conda-forge python=3.11.5 numpy=1.26.0 +RUN curl --proto '=https' --tlsv1.2 -LsSf 'https://github.com/FNNDSC/mni2mz3/releases/download/v1.0.0-rc.5/installer.sh' | bash + +FROM base + +LABEL org.opencontainers.image.authors="Jennings.Zhang " \ + org.opencontainers.image.title="pl-mnc2common" \ + org.opencontainers.image.description="A ChRIS plugin to convert MINC volume, .txt surface data, and MNI .obj surface file formats to NIFTI and MZ3." ARG SRCDIR=/usr/local/src/pl-mni2common WORKDIR ${SRCDIR} -COPY --chown=57439:57439 requirements.txt . -RUN --mount=type=cache,sharing=private,target=/home/mambauser/.cache/pip,uid=57439,gid=57439 \ - pip install -r requirements.txt - -COPY --chown=mambauser:mambauser . . +COPY . . ARG extras_require=none RUN pip install ".[${extras_require}]" \ && cd / && rm -rf ${SRCDIR}/* WORKDIR / +COPY --from=mni2mz3-installer /usr/local/bin/mni2mz3 /usr/local/bin/mni2mz3 + CMD ["mni2common"] diff --git a/README.md b/README.md index 1f6e037..0ac7f50 100644 --- a/README.md +++ b/README.md @@ -5,12 +5,11 @@ [![ci](https://github.com/FNNDSC/pl-mni2common/actions/workflows/ci.yml/badge.svg)](https://github.com/FNNDSC/pl-mni2common/actions/workflows/ci.yml) `pl-mni2common` is a [_ChRIS_](https://chrisproject.org/) -_ds_ plugin which takes in ... as input files and -creates ... as output files. - -## Abstract - -... +_ds_ plugin wrapper around the programs +[mnc2nii](https://bic-mni.github.io/man-pages/man/mnc2nii.html) +and [mni2mz3](https://github.com/FNNDSC/mni2mz3). +It converts MINC volumes to NIFTI, surfaces to MZ3, and surface data to MZ3. +The output file formats NIFTI and MZ3 are convenient for visualization by [niivue](https://github.com/niivue/niivue). ## Installation diff --git a/base/Dockerfile b/base/Dockerfile index d65f20c..786f46b 100644 --- a/base/Dockerfile +++ b/base/Dockerfile @@ -2,9 +2,9 @@ FROM docker.io/fnndsc/microminc-builder:latest as builder RUN microminc.sh mnc2nii /microminc -FROM ghcr.io/mamba-org/micromamba:1.5.1-bookworm-slim +FROM python:3.12.0-slim-bullseye COPY --from=builder /microminc /opt/microminc -ENV PATH=/opt/conda/bin:/opt/microminc/bin:$PATH \ +ENV PATH=/opt/microminc/bin:$PATH \ LD_LIBRARY_PATH=/opt/microminc/lib:$LD_LIBRARY_PATH \ MINC_FORCE_V2=1 MINC_COMPRESS=4 VOLUME_CACHE_THRESHOLD=-1 \ MNI_DATAPATH=/opt/microminc/share diff --git a/base/build.sh b/base/build.sh index 78ebfa8..64c8004 100755 --- a/base/build.sh +++ b/base/build.sh @@ -2,4 +2,4 @@ exec docker buildx build --push \ --platform linux/amd64,linux/arm64,linux/ppc64le \ - -t docker.io/fnndsc/pl-mni2common:base-1 . + -t docker.io/fnndsc/pl-mni2common:base-2 . diff --git a/examples/.gitignore b/examples/.gitignore new file mode 100644 index 0000000..9485dff --- /dev/null +++ b/examples/.gitignore @@ -0,0 +1,2 @@ +/incoming +/outgoing diff --git a/examples/download.sh b/examples/download.sh new file mode 100755 index 0000000..c6eba22 --- /dev/null +++ b/examples/download.sh @@ -0,0 +1,22 @@ +#!/bin/bash + +HERE="$(dirname "$(readlink -f "$0")")" + +urls=( + https://github.com/aces/CIVET/raw/9818b3cbe8308249e7c373ef1a2a53956512143e/models/colin/colin_white_mc_mask_left.txt + https://github.com/aces/CIVET/raw/9818b3cbe8308249e7c373ef1a2a53956512143e/models/colin/colin_white_mc_mask_right.txt + https://github.com/aces/CIVET/raw/9818b3cbe8308249e7c373ef1a2a53956512143e/models/colin/colin_white_mc_left.obj + https://github.com/aces/CIVET/raw/9818b3cbe8308249e7c373ef1a2a53956512143e/models/colin/colin_white_mc_right.obj + https://github.com/aces/mni-models_colin27-lin/raw/60787485e2ed4e012bda4da8fc2b163384fc3431/colin27_t1_tal_lin.mnc.gz + https://github.com/aces/mni-models_colin27-lin/raw/60787485e2ed4e012bda4da8fc2b163384fc3431/colin27_t1_tal_lin_headmask.mnc.gz + https://github.com/aces/mni-models_colin27-lin/raw/60787485e2ed4e012bda4da8fc2b163384fc3431/colin27_t1_tal_lin_mask.mnc.gz +) + +set -ex + +cd "$HERE" +mkdir -v incoming +cd incoming + +parallel wget -q ::: ${urls[@]} +find -type f -name '*.gz' | parallel gzip -d diff --git a/mni2common.py b/mni2common.py index aa4bf09..aad353e 100644 --- a/mni2common.py +++ b/mni2common.py @@ -9,7 +9,6 @@ from argparse import ArgumentParser, Namespace, ArgumentDefaultsHelpFormatter from tqdm.contrib.concurrent import thread_map from tqdm.contrib.logging import logging_redirect_tqdm -from bicpl import PolygonObj, WavefrontObj from chris_plugin import chris_plugin, PathMapper @@ -27,7 +26,7 @@ """ -parser = ArgumentParser(description='Convert MINC and .obj to NIFTI and Wavefront respectively', +parser = ArgumentParser(description='Convert MINC and .obj to NIFTI and MZ3 respectively', formatter_class=ArgumentDefaultsHelpFormatter) parser.add_argument('-p', '--pattern', default='**/*', type=str, help='Path filter') @@ -60,12 +59,15 @@ def main(options: Namespace, inputdir: Path, outputdir: Path): def convert_file(file_pair: tuple[Path, Path]) -> bool: input_file, output_file = file_pair - if input_file.suffix == '.mnc': - return convert_minc(input_file, output_file.with_suffix('.nii')) - elif input_file.suffix == '.obj': - return convert_obj(input_file, output_file.with_suffix('.wf.obj')) - shutil.copy2(input_file, output_file) - return True + match input_file.suffix: + case '.mnc': + return convert_minc(input_file, output_file.with_suffix('.nii')) + case '.obj' | '.txt': + return mni2mz3(input_file, output_file.with_suffix('.mz3')) + case _: + if os.path.realpath(input_file) != os.path.realpath(output_file): + shutil.copy2(input_file, output_file) + return True def convert_minc(mnc, nii) -> bool: @@ -79,16 +81,19 @@ def convert_minc(mnc, nii) -> bool: return False -def convert_obj(mniobj_path, wavefront_path) -> bool: - try: - obj = PolygonObj.from_file(mniobj_path) - wf = WavefrontObj.from_mni(obj) - with wavefront_path.open('w') as f: - wf.write_to(f) +def mni2mz3(input_path, output_path) -> bool: + cmd = ('mni2mz3', str(input_path), str(output_path)) + proc = sp.run(cmd, stderr=sp.PIPE, stdout=sp.DEVNULL) + if proc.returncode == 0: + return True + # is a .txt file, but not a vertex-wise surface data file + if input_path.suffix == '.txt' and b'Could not parse 0th value as float' in proc.stderr: + output_txt = output_path.with_suffix(input_path.suffix) + if os.path.realpath(input_path) != os.path.realpath(output_txt): + shutil.copy2(input_path, output_txt) return True - except Exception as e: - LOG.error(str(e)) - return False + LOG.error(f'Command failed: {shlex.join(cmd)}') + return False if __name__ == '__main__': diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index 52710e7..0000000 --- a/requirements.txt +++ /dev/null @@ -1,2 +0,0 @@ -chris_plugin==0.3.1 -tqdm~=4.66 diff --git a/setup.py b/setup.py index f37177f..fa4bffb 100644 --- a/setup.py +++ b/setup.py @@ -21,12 +21,12 @@ def get_version(rel_path: str) -> str: setup( name='mni2common', version=get_version('mni2common.py'), - description='A ChRIS plugin to convert MINC volume and MNI .obj surface file formats to NIFTI and Wavefront OBJ respectively.', + description='A ChRIS plugin to convert MINC volume and MNI .obj surface file formats to NIFTI and MZ3 OBJ respectively.', author='FNNDSC', author_email='jennings.zhang@childrens.harvard.edu', url='https://github.com/FNNDSC/pl-mni2common', py_modules=['mni2common'], - install_requires=['chris_plugin', 'pybicpl', 'tqdm'], + install_requires=['chris_plugin==0.3.1', 'tqdm~=4.66'], license='MIT', entry_points={ 'console_scripts': [