diff --git a/Dockerfile b/Dockerfile index 3c76768..5b9a65e 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,13 +1,15 @@ -# Python version can be changed, e.g. -# FROM python:3.8 -# FROM docker.io/fnndsc/conda:python3.10.2-cuda11.6.0 -FROM docker.io/python:3.10.2-slim-buster +FROM docker.io/fnndsc/mni-conda-base:civet2.1.1-python3.10.2 LABEL org.opencontainers.image.authors="FNNDSC " \ - org.opencontainers.image.title="ChRIS Plugin Title" \ - org.opencontainers.image.description="A ChRIS ds plugin that..." + org.opencontainers.image.title="ep-skimage-mcubes-mni" \ + org.opencontainers.image.description="A ChRIS ds plugin wrapper around scikit-image's implementation of marching-cubes" -WORKDIR /usr/local/src/app +WORKDIR /usr/local/src/ep-skimage-mcubes-mni + +# Install binary Python packages using conda, but install +# nibabel using pip for ppc64le support + +RUN conda install -y -c conda-forge h5py=3.6.0 scikit-image=0.19.1 COPY requirements.txt . RUN pip install -r requirements.txt @@ -15,4 +17,4 @@ RUN pip install -r requirements.txt COPY . . RUN pip install . -CMD ["commandname", "--help"] +CMD ["skimc", "--help"] diff --git a/README.md b/README.md index e3f99e9..009cb96 100644 --- a/README.md +++ b/README.md @@ -1,82 +1,9 @@ -# _ChRIS_ ds Plugin Template +# ep-skimage-mcubes-mni - +[![Version](https://img.shields.io/docker/v/fnndsc/ep-skimage-mcubes-mni?sort=semver)](https://hub.docker.com/r/fnndsc/ep-skimage-mcubes-mni) +[![MIT License](https://img.shields.io/github/license/fnndsc/ep-skimage-mcubes-mni)](https://github.com/FNNDSC/ep-skimage-mcubes-mni/blob/main/LICENSE) +[![Build](https://github.com/FNNDSC/ep-skimage-mcubes-mni/actions/workflows/ci.yml/badge.svg)](https://github.com/FNNDSC/ep-skimage-mcubes-mni/actions) - -This is a minimal template repository for _ChRIS_ _ds_ plugin applications. -For a more comprehensive boilerplate, use - -https://github.com/fnndsc/cookiecutter-chrisapp - -## How to Use This Template - -1. Click "Use this template" -2. Clone the newly created repository -3. Replace placeholder text - -```shell -function replace () { - find . -type f -not -path '*/\.*/*' -not -path '*/\venv/*' -exec sed -i -e "s/$1/$2/" '{}' \; -} - -replace commandname my_command_name -replace pl-appname pl-my-plugin-name -replace fnndsc my_username -``` - -### Template Examples - -Here are some good, complete examples of _ChRIS_ plugins created from this template. - -- https://github.com/FNNDSC/pl-nums2mask -- https://github.com/FNNDSC/pl-nii2mnc-u8 - -Advanced users can `cp -rv .github/workflows` into their own repositories to enable -automatic builds. - -## Abstract - -PROGRAMNAME is a [_ChRIS_](https://chrisproject.org/) -_ds_ plugin which takes in ... as input files and -creates ... as output files. - -## Usage - -```shell -singularity exec docker://fnndsc/pl-appname commandname [--args values...] input/ output/ -``` - -## Examples - -```shell -mkdir incoming/ outgoing/ -mv some.dat other.dat incoming/ -singularity exec docker://fnndsc/pl-appname:latest commandname [--args] incoming/ outgoing/ -``` - -## Development - -### Building - -```shell -docker build -t localhost/fnndsc/pl-appname . -``` - -### Get JSON Representation - -```shell -docker run --rm localhost/fnndsc/pl-appname chris_plugin_info > MyProgram.json -``` - -### Local Test Run - -```shell -docker run --rm -it --userns=host -u $(id -u):$(id -g) \ - -v $PWD/app.py:/usr/local/lib/python3.10/site-packages/app.py:ro \ - -v $PWD/in:/incoming:ro -v $PWD/out:/outgoing:rw -w /outgoing \ - localhost/fnndsc/pl-appname commandname /incoming /outgoing -``` +`ep-skimage-mcubes-mni` is a _ChRIS_ _ds_ plugin that performs the +[marching-cubes](https://scikit-image.org/docs/stable/auto_examples/edges/plot_marching_cubes.html) +algorithm on binary `.mnc` masks, producing surfaces in the `.obj` file format. diff --git a/app.py b/app.py deleted file mode 100755 index 25b25b0..0000000 --- a/app.py +++ /dev/null @@ -1,26 +0,0 @@ -#!/usr/bin/env python - -from pathlib import Path -from argparse import ArgumentParser, Namespace -from chris_plugin import chris_plugin - -parser = ArgumentParser(description='cli description') -parser.add_argument('-e', '--example', default='jelly', - help='argument which does not do anything') - - -# documentation: https://fnndsc.github.io/chris_plugin/ -@chris_plugin( - parser=parser, - title='My ChRIS plugin', - category='', # ref. https://chrisstore.co/plugins - min_memory_limit='100Mi', # supported units: Mi, Gi - min_cpu_limit='1000m', # 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(f'Option: {options.example}') - - -if __name__ == '__main__': - main() diff --git a/requirements.txt b/requirements.txt index 56a39c7..11cad27 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1 +1,5 @@ chris_plugin~=0.0.10 +pybicpl~=0.3.0 +numpy~=1.22.2 +nibabel~=3.2.2 +loguru~=0.6.0 diff --git a/setup.py b/setup.py index f4da0fb..6bd15da 100644 --- a/setup.py +++ b/setup.py @@ -1,19 +1,19 @@ from setuptools import setup setup( - name = 'chris-plugin-template', + name = 'ep-skimage-mcubes-mni', version = '1.0.0', - description = 'A ChRIS DS plugin template', - author = 'FNNDSC', - author_email = 'dev@babyMRI.org', - url = 'https://github.com/FNNDSC/python-chrisapp-template', - py_modules = ['app'], - install_requires = ['chris_plugin'], + description = 'Marching-cubes implementation from scikit-image', + author = 'Jennings Zhang', + author_email = 'Jennings.Zhang@childrens.harvard.edu', + url = 'https://github.com/jennydaman/ep-skimage-mcubes-mni', + py_modules = ['skimc'], + install_requires = ['chris_plugin', 'pybicpl', 'nibabel', 'scikit-image', 'h5py', 'loguru'], license = 'MIT', - python_requires = '>=3.8.2', + python_requires = '>=3.10.2', entry_points = { 'console_scripts': [ - 'commandname = app:main' + 'skimc = skimc:main' ] }, classifiers = [ diff --git a/skimc.py b/skimc.py new file mode 100755 index 0000000..5ddfd6e --- /dev/null +++ b/skimc.py @@ -0,0 +1,70 @@ +#!/usr/bin/env python + +import os +import nibabel as nib +from nibabel.affines import apply_affine +from skimage import measure +from bicpl import PolygonObj + +from pathlib import Path +from argparse import ArgumentParser, Namespace +from concurrent.futures import ThreadPoolExecutor +from loguru import logger +from chris_plugin import chris_plugin, PathMapper + +parser = ArgumentParser(description='cli description') +parser.add_argument('--spacing', default='1,1,1', + help='Voxel spacing in spatial dimensions corresponding' + ' to numpy array indexing dimensions (M, N, P).') +parser.add_argument('-s', '--step-size', default=1, type=int, + help='Step size in voxels. ' + 'Larger steps yield faster but coarser results. ' + 'The result will always be topologically correct though.') +parser.add_argument('-m', '--method', default='lewiner', + choices=('lewiner', 'lorensen'), + help='Specify which of Lewiner et al. or Lorensen et al.' + ' method will be used.') +parser.add_argument('-p', '--pattern', default='**/*.mnc', + help='pattern for file names to include') + + +def mcubes(mask_path: Path, surface_path: Path, + spacing: tuple[float, float, float], + step_size: int, + method: str): + mask = nib.load(mask_path) + data = mask.get_fdata() + verts, faces, normals, values = measure.marching_cubes( + data, spacing=spacing, step_size=step_size, method=method, allow_degenerate=False + ) + transformed_verts = apply_affine(mask.affine, verts) + obj = PolygonObj.from_data(transformed_verts, faces, normals) + obj.save(surface_path) + logger.info('Completed: {} => {}', mask_path, surface_path) + + +@chris_plugin( + parser=parser, + title='Scikit-Image Marching Cubes', + category='Surface Extraction', + min_memory_limit='200Mi', + min_cpu_limit='1000m', +) +def main(options: Namespace, inputdir: Path, outputdir: Path): + spacing = tuple(float(n) for n in options.spacing.split(',')) + + logger.info('scikit-image marching_cubes: spacing={} step_size={} method={}', + spacing, options.step_size, options.method) + + results = [] + with ThreadPoolExecutor(max_workers=len(os.sched_getaffinity(0))) as pool: + mapper = PathMapper(inputdir, outputdir, glob=options.pattern, suffix='.obj') + for mnc, obj in mapper: + results.append(pool.submit(mcubes, mnc, obj, spacing, options.step_size, options.method)) + + for future in results: + future.exception() + + +if __name__ == '__main__': + main()