Skip to content

Commit

Permalink
pl-bestsurfreg-surface-resample
Browse files Browse the repository at this point in the history
  • Loading branch information
jennydaman committed Jul 27, 2023
1 parent 2387e07 commit 6087b72
Show file tree
Hide file tree
Showing 16 changed files with 164,140 additions and 147 deletions.
54 changes: 1 addition & 53 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,38 +17,9 @@ on:
branches: [ main ]

jobs:
test:
name: Unit tests
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v3
- uses: docker/setup-buildx-action@v2
- name: Cache Docker layers
uses: actions/cache@v3
with:
path: /tmp/.buildx-cache
key: ${{ runner.os }}-buildx-${{ github.sha }}
restore-keys: |
${{ runner.os }}-buildx-
- name: Build
uses: docker/build-push-action@v3
with:
build-args: extras_require=dev
context: .
load: true
push: false
tags: "localhost/local/app:dev"
cache-from: type=local,src=/tmp/.buildx-cache
cache-to: type=local,dest=/tmp/.buildx-cache
- 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'
# needs: [ test ] # uncomment to require passing tests
runs-on: ubuntu-22.04

# A local registry helps us reuse the built image between steps
Expand Down Expand Up @@ -111,24 +82,6 @@ jobs:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}

# Here, we want to do the docker build twice:
# The first build pushes to our local registry for testing.
# The second build pushes to Docker Hub and ghcr.io
- name: Build (local only)
uses: docker/build-push-action@v3
id: docker_build
with:
context: .
file: ./Dockerfile
tags: |
localhost:5000/${{ steps.determine.outputs.dock_image }}
docker.io/${{ steps.determine.outputs.dock_image }}
ghcr.io/${{ steps.determine.outputs.dock_image }}
platforms: linux/amd64
push: true
cache-from: type=local,src=/tmp/.buildx-cache
cache-to: type=local,dest=/tmp/.buildx-cache
- name: Build and push
uses: docker/build-push-action@v3
if: github.event_name == 'push' || github.event_name == 'release'
Expand All @@ -139,8 +92,7 @@ jobs:
localhost:5000/${{ steps.determine.outputs.dock_image }}
docker.io/${{ steps.determine.outputs.dock_image }}
ghcr.io/${{ steps.determine.outputs.dock_image }}
# 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: true
cache-from: type=local,src=/tmp/.buildx-cache
cache-to: type=local,dest=/tmp/.buildx-cache
Expand Down Expand Up @@ -171,7 +123,3 @@ jobs:
with:
descriptor_file: /tmp/description.json
auth: ${{ secrets.CHRIS_STORE_USER }}
## Optional ChRIS backend admin information
# chris_admin_auth: ${{ secrets.CUBE_CHRISPROJECT_ORG_ADMIN_USER }}
# chris_admin_url: https://cube.chrisproject.org/chris-admin/api/v1/
# compute_resources: host,moc # comma-separated list of names
17 changes: 6 additions & 11 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,19 +1,14 @@
# 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.11.3-slim-bullseye
FROM docker.io/fnndsc/pl-bestsurfreg-surface-resample:base-1

LABEL org.opencontainers.image.authors="FNNDSC <[email protected]>" \
org.opencontainers.image.title="Surface Data Registration" \
org.opencontainers.image.description="ChRIS plugin wrapper for bestsurfreg.pl and surface-resample"

WORKDIR /usr/local/src/pl-bestsurfreg-surface-resample
COPY ./fetal-template-29 $MNI_DATAPATH/fetal-template-29

COPY requirements.txt .
RUN pip install -r requirements.txt

COPY . .
COPY . /usr/local/src/pl-bestsurfreg-surface-resample
ARG extras_require=none
RUN pip install ".[${extras_require}]"
RUN pip install "/usr/local/src/pl-bestsurfreg-surface-resample[${extras_require}]" \
&& rm -rf /usr/local/src/pl-bestsurfreg-surface-resample

CMD ["bsr2"]
CMD ["bsrr"]
1 change: 1 addition & 0 deletions base/.dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
*
19 changes: 19 additions & 0 deletions base/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
FROM docker.io/fnndsc/microminc-builder:latest as builder

RUN microminc.sh -p '&do_cmd' bestsurfreg.pl surface-resample \
surftracc surface-stats measure_surface_area print_n_polygons \
/microminc

FROM python:3.11.2-slim-bullseye

RUN apt-get update && apt-get install -y perl \
&& rm -rf /var/lib/apt/lists/*

COPY --from=builder /microminc /opt/microminc
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 \
CIVET_JOB_SCHEDULER=DEFAULT \
MNIBASEPATH=/opt/microminc \
MNI_DATAPATH=/opt/microminc/share \
PERL5LIB=/opt/microminc/perl
5 changes: 5 additions & 0 deletions base/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
Uses [microminc](https://github.com/FNNDSC/microminc)
to create a minimized base image for `pl-bestsurfreg-surface-resample`,
which copies the necessary minc-tool binaries to a multi-arch conda image.

This image is built and pushed manually.
5 changes: 5 additions & 0 deletions base/build.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
#!/bin/bash -ex

exec docker buildx build --push \
--platform linux/amd64,linux/arm64,linux/ppc64le \
-t docker.io/fnndsc/pl-bestsurfreg-surface-resample:base-1 .
16 changes: 16 additions & 0 deletions bestsurfreg/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
"""
Python *ChRIS* plugin module for running ``bestsurfreg.pl`` followed by ``surface-resample``.
"""

__version__ = '1.0.0'

DISPLAY_TITLE = r"""
_ _ _ __ __ _
| | | | | | / _| / _| | |
_ __ | |______| |__ ___ ___| |_ ___ _ _ _ __| |_ _ __ ___ __ _ ______ ___ _ _ _ __| |_ __ _ ___ ___ ______ _ __ ___ ___ __ _ _ __ ___ _ __ | | ___
| '_ \| |______| '_ \ / _ \/ __| __/ __| | | | '__| _| '__/ _ \/ _` |______/ __| | | | '__| _/ _` |/ __/ _ \______| '__/ _ \/ __|/ _` | '_ ` _ \| '_ \| |/ _ \
| |_) | | | |_) | __/\__ \ |_\__ \ |_| | | | | | | | __/ (_| | \__ \ |_| | | | || (_| | (_| __/ | | | __/\__ \ (_| | | | | | | |_) | | __/
| .__/|_| |_.__/ \___||___/\__|___/\__,_|_| |_| |_| \___|\__, | |___/\__,_|_| |_| \__,_|\___\___| |_| \___||___/\__,_|_| |_| |_| .__/|_|\___|
| | __/ | | |
|_| |___/ |_|
"""
70 changes: 70 additions & 0 deletions bestsurfreg/__main__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
#!/usr/bin/env python
import itertools
import os
import sys
import shutil
from concurrent.futures import ThreadPoolExecutor
from pathlib import Path

from chris_plugin import chris_plugin, PathMapper, curry_name_mapper

from bestsurfreg import DISPLAY_TITLE
from bestsurfreg.cmd import register_surface_data
from bestsurfreg.find_template import find_template
from bestsurfreg.params import parser


@chris_plugin(
parser=parser,
title='Surface Data Registration',
category='MRI', # ref. https://chrisstore.co/plugins
min_memory_limit='1Gi', # 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, inputdir: Path, outputdir: Path):
print(DISPLAY_TITLE, flush=True)
if options.copy:
shutil.copytree(inputdir, outputdir)

try:
template_surface = find_template(options.target, inputdir)
file_to_register = find_template(options.data, inputdir)
except FileNotFoundError as e:
logger.error(str(e))
sys.exit(1)

proc = len(os.sched_getaffinity(0))
print(f'Using {proc} threads', flush=True)

mapper = PathMapper.file_mapper(inputdir, outputdir, glob=options.pattern,
name_mapper=curry_name_mapper(options.output_fname))

with ThreadPoolExecutor(max_workers=proc) as pool:
results = pool.map(
call_register_surface_data,
mapper,
itertools.repeat(template_surface),
itertools.repeat(file_to_register),
itertools.repeat(options)
)

rc = next(filter(lambda r: r != 0, results), 0)
sys.exit(rc)


def call_register_surface_data(t: tuple[Path, Path], template_surface: Path, file_to_register: Path, options) -> int:
input_surface, output_registered_data = t
sm_file = output_registered_data.with_suffix('.sm')

return register_surface_data(
input_surface, template_surface, file_to_register, sm_file, output_registered_data,
options.min_control_mesh, options.max_control_mesh, options.blur_coef,
options.neighbourhood_radius, options.maximum_blur,
output_registered_data.with_suffix('.bestsurfreg.log'),
output_registered_data.with_suffix('.surface-resample.log')
)


if __name__ == '__main__':
main()
103 changes: 103 additions & 0 deletions bestsurfreg/cmd.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
"""
Wrapper functions for external commands.
"""
import shlex
from pathlib import Path
import subprocess as sp
from colorama import Fore, Style
from datetime import datetime

FAILED = Style.BRIGHT + Fore.RED + 'FAILED' + Fore.RESET + Style.RESET_ALL


def register_surface_data(
surface: Path,
template_surface: Path,
file_to_register: Path,
sm_file: Path,
registered_data: Path,
min_control_mesh,
max_control_mesh,
blur_coef,
neighborhood_radius,
maximum_blur,
bestsurfreg_log_file: Path,
surface_resample_log_file: Path
) -> int:
cmds: list[tuple[list[str | Path], Path]] = [
(
bestsurfreg(
surface, template_surface, sm_file,
min_control_mesh, max_control_mesh,
blur_coef, neighborhood_radius, maximum_blur,
),
bestsurfreg_log_file,
),
(
surface_resample(surface, template_surface, file_to_register, sm_file, registered_data),
surface_resample_log_file
)
]
subject = _subj_log_name(surface)
for cmd, log_file in cmds:
str_cmd = shlex.join(map(str, cmd))
print(f'{log_prefix(subject)}$> {str_cmd}', flush=True)
with log_file.open('wb') as f:
proc = sp.run(cmd, stdout=f, stderr=sp.STDOUT)
if proc.returncode != 0:
print(f'{log_prefix(subject)} {FAILED}, see {log_file}', flush=True)
return proc.returncode
return 0


def log_prefix(subj) -> str:
now = datetime.now().isoformat()
return f'{Style.DIM }[{now}{Style.RESET_ALL} {subj}{Style.DIM}]{Style.RESET_ALL}'


def bestsurfreg(
surface: Path,
target: Path,
sm_file: Path,
min_control_mesh,
max_control_mesh,
blur_coef,
neighborhood_radius,
maximum_blur,
):
return [
'bestsurfreg.pl',
'-clobber',
'-min_control_mesh', str(min_control_mesh),
'-max_control_mesh', str(max_control_mesh),
'-blur_coef', str(blur_coef),
'-neighbourhood_radius', str(neighborhood_radius),
'-maximum_blur', str(maximum_blur),
surface,
target,
sm_file
]


def surface_resample(
surface: Path,
target: Path,
data: Path,
sm_file: Path,
registered_data: Path,
):
return ['surface-resample', '-nearest', surface, target, data, sm_file, registered_data]


def _subj_log_name(surface: Path) -> str:
"""Color coding of a subject identifier for terminal log output."""
s = f'{surface.parent.name}/{surface.name}'
color = _COLORS[hash(s) % len(_COLORS)]
return color + s + Fore.RESET


_COLORS = (
Fore.GREEN, Fore.YELLOW, Fore.BLUE, Fore.MAGENTA, Fore.CYAN,
Fore.LIGHTGREEN_EX, Fore.LIGHTYELLOW_EX, Fore.LIGHTBLUE_EX,
Fore.LIGHTMAGENTA_EX, Fore.LIGHTCYAN_EX,
)
21 changes: 21 additions & 0 deletions bestsurfreg/find_template.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import os
from pathlib import Path

MNI_DATAPATH = Path(os.getenv('MNI_DATAPATH', None))
FILE_RESOLUTION_DESCRIPTION = 'Should be any of an absolute path, a relative path, ' \
'a path relative to the input directory, or a relative path to ' \
f'MNI_DATAPATH(={MNI_DATAPATH}).'
"""
An English description of how this program will resolve paths to data files.
"""


def find_template(p: str | os.PathLike, input_dir: Path) -> Path:
"""
A function which implements the functionality described by the value of ``FILE_RESOLUTION_DESCRIPTION``.
"""
to_check = (Path(p), input_dir / p, MNI_DATAPATH / p)
for resolved_path in to_check:
if resolved_path.is_file():
return resolved_path
raise FileNotFoundError(f"None of the following paths are a file: {to_check}")
35 changes: 35 additions & 0 deletions bestsurfreg/params.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
from argparse import ArgumentDefaultsHelpFormatter, ArgumentParser

from bestsurfreg import __version__
from bestsurfreg.find_template import FILE_RESOLUTION_DESCRIPTION

parser = ArgumentParser(description='ChRIS plugin wrapper for bestsurfreg.pl and surface-resample',
formatter_class=ArgumentDefaultsHelpFormatter)
parser.add_argument('-c', '--copy', action='store_true',
help='Copy all input files to output dir.')
parser.add_argument('-p', '--pattern', default='**/*.obj', type=str,
help='input surface file filter glob')
parser.add_argument('-o', '--output_fname', type=str,
default='{}.to_individual.txt',
help='Name for output registered surface data. '
'{} represents the basename of the input file.')
parser.add_argument('-t', '--target', type=str,
default='fetal-template-29/bh.smoothwm.mni.obj',
help=f'Registration target. {FILE_RESOLUTION_DESCRIPTION}')
parser.add_argument('-d', '--data', type=str,
default='fetal-template-29/bh.mask.1D.dset',
help='Vertex-wise data (which can be a mask) to be registered from the target '
f'to each input surface. {FILE_RESOLUTION_DESCRIPTION}')
parser.add_argument('--min_control_mesh', type=int, default=80,
help='control mesh must be no less than X nodes...')
parser.add_argument('--max_control_mesh', type=int, default=81920,
help='control mesh must be no greater than X nodes...')
parser.add_argument('-blur_coef', type=float, default=1.25,
help='factor to increase/decrease blurring')
parser.add_argument('--neighbourhood_radius', type=float, default=2.8,
help='neighbourhood radius')
parser.add_argument('--maximum_blur', type=float, default=1.9,
help='specify target spacing')

parser.add_argument('-V', '--version', action='version',
version=f'%(prog)s {__version__}')
Loading

0 comments on commit 6087b72

Please sign in to comment.