working
This commit is contained in:
parent
f3945d71a2
commit
58ecd66545
18 changed files with 2934 additions and 0 deletions
2
.gitignore
vendored
2
.gitignore
vendored
|
@ -162,3 +162,5 @@ cython_debug/
|
|||
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
||||
#.idea/
|
||||
|
||||
*.h5
|
||||
*.gz
|
||||
|
|
22
mri_synthmorph/CMakeLists.txt
Normal file
22
mri_synthmorph/CMakeLists.txt
Normal file
|
@ -0,0 +1,22 @@
|
|||
project(mri_synthmorph)
|
||||
|
||||
file(GLOB WEIGHTS "synthmorph.*.h5")
|
||||
|
||||
if (FSPYTHON_INSTALL_TREE)
|
||||
install_pyscript_fspython_tree(mri_synthmorph)
|
||||
install_symlinks_fspython_tree(TYPE files DESTINATION models ${WEIGHTS})
|
||||
install_directories_fspython_tree(synthmorph DESTINATION python/packages)
|
||||
else()
|
||||
install_pyscript(mri_synthmorph)
|
||||
install_symlinks(TYPE files DESTINATION models ${WEIGHTS})
|
||||
install_directories(synthmorph DESTINATION python/packages)
|
||||
endif()
|
||||
|
||||
install_configured(fs-synthmorph-reg DESTINATION bin)
|
||||
|
||||
## 08/2024 - currently failing on Intel Mac
|
||||
|
||||
if(NOT APPLE)
|
||||
add_test_script(NAME mri_synthmorph_test_register SCRIPT test_register.sh)
|
||||
add_test_script(NAME mri_synthmorph_test_apply SCRIPT test_apply.sh)
|
||||
endif()
|
46
mri_synthmorph/Dockerfile
Normal file
46
mri_synthmorph/Dockerfile
Normal file
|
@ -0,0 +1,46 @@
|
|||
# Define base image. Set HOME to avoid Matplotlib warning about non-writable
|
||||
# MPLCONFIGDIR on Neurite import when running as non-root user.
|
||||
FROM tensorflow/tensorflow:2.17.0-gpu AS base
|
||||
ENV FREESURFER_HOME=/freesurfer
|
||||
ENV PYTHONUSERBASE="$FREESURFER_HOME/env"
|
||||
ENV PATH="$FREESURFER_HOME:$PATH"
|
||||
ENV HOME=/tmp
|
||||
|
||||
|
||||
# Intermediate build stage. Install Python packages to user base for easy COPY.
|
||||
FROM base AS copy
|
||||
|
||||
COPY --chmod=0775 mri_synthmorph $FREESURFER_HOME/
|
||||
COPY --chmod=0664 synthmorph/*.py $FREESURFER_HOME/synthmorph/
|
||||
COPY --chmod=0664 synthmorph.*.h5 $FREESURFER_HOME/models/
|
||||
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends git
|
||||
RUN python3 -m pip install -U pip
|
||||
RUN python3 -m pip install --user \
|
||||
'numpy<2.0' \
|
||||
git+https://github.com/adalca/pystrum.git@ba35d4b357f54e5ed577cbd413076a07ef810a21 \
|
||||
git+https://github.com/adalca/neurite.git@9ae2f5cec2201eedbcc6929cecf852193cef7646 \
|
||||
git+https://github.com/freesurfer/surfa.git@041905fca717447780e0cc211197669e3218de2f \
|
||||
git+https://github.com/voxelmorph/voxelmorph.git@53d1b95fa734648c92fd8af4f3807b09cb56c342
|
||||
|
||||
WORKDIR /artifacts
|
||||
RUN python3 -V >python.txt
|
||||
RUN python3 -m pip freeze >requirements.txt
|
||||
RUN mri_synthmorph -h >help.general.txt
|
||||
RUN mri_synthmorph register -h >help.register.txt
|
||||
RUN mri_synthmorph apply -h >help.apply.txt
|
||||
|
||||
|
||||
# Export Python requirements for reference. Build artifacts will only exist in
|
||||
# in the target stage `export`.
|
||||
FROM scratch AS export
|
||||
COPY --from=copy /artifacts/*.txt /
|
||||
|
||||
|
||||
# Exclude Git and caches from final image to save space. Copy only once to
|
||||
# avoid unnecessary container layers. Set working directory to /mnt for data
|
||||
# exchange with the host without having to specify the full path.
|
||||
FROM base
|
||||
COPY --from=copy $FREESURFER_HOME $FREESURFER_HOME
|
||||
WORKDIR /mnt
|
||||
ENTRYPOINT ["mri_synthmorph"]
|
106
mri_synthmorph/README.md
Normal file
106
mri_synthmorph/README.md
Normal file
|
@ -0,0 +1,106 @@
|
|||
# SynthMorph
|
||||
|
||||
This guide explains how to build SynthMorph container images.
|
||||
It assumes you execute commands in the `mri_synthmorph` directory.
|
||||
For general information about SynthMorph, visit [synthmorph.io](https://synthmorph.io).
|
||||
|
||||
|
||||
## Managing weight files with git-annex
|
||||
|
||||
Weight files are large and therefore managed with `git-annex`.
|
||||
Instructions with examples are available elsewhere:
|
||||
|
||||
* https://surfer.nmr.mgh.harvard.edu/fswiki/GitAnnex
|
||||
* https://git-annex.branchable.com/walkthrough
|
||||
|
||||
|
||||
## Building SynthMorph images with Docker
|
||||
|
||||
FreeSurfer automatically ships the most recent `mri_synthmorph` and weight files.
|
||||
Building a standalone container image requires fetching and unlocking the model files with `git-annex`, replacing the symbolic links with the actual files.
|
||||
|
||||
```sh
|
||||
git fetch datasrc
|
||||
git annex get .
|
||||
git annex unlock synthmorph.*.h5
|
||||
```
|
||||
|
||||
Build a new image with the appropriate version tag:
|
||||
|
||||
```sh
|
||||
tag=X
|
||||
docker build -t freesurfer/synthmorph:$tag .
|
||||
```
|
||||
|
||||
|
||||
## Testing the local Docker image
|
||||
|
||||
Update the version reference in the wrapper script and run it to test the local image with Docker.
|
||||
|
||||
```sh
|
||||
sed -i "s/^\(version = \).*/\1$tag/" synthmorph
|
||||
./synthmorph -h
|
||||
```
|
||||
|
||||
|
||||
## Testing with Apptainer
|
||||
|
||||
Testing the image with Apptainer (Singularity) before making it public requires conversion.
|
||||
If your home directory has a low quota, set up a cache elsewhere:
|
||||
|
||||
```sh
|
||||
d=$(mktemp -d)
|
||||
export APPTAINER_CACHEDIR="$d"
|
||||
export APPTAINER_TMPDIR="$d"
|
||||
```
|
||||
|
||||
On the machine running Docker, convert the image with:
|
||||
|
||||
```sh
|
||||
apptainer build -f synthmorph_$tag.sif docker-daemon://freesurfer/synthmorph:$tag
|
||||
```
|
||||
|
||||
If you want to test the image on another machine, save it first.
|
||||
After transfer to the target machine, build a SIF file as a non-root user using the fakeroot feature.
|
||||
This relies on namespace mappings set up in /etc/subuid and /etc/subgid (likely by Help).
|
||||
|
||||
```sh
|
||||
docker save synthmorph:$tag | gzip >synthmorph_$tag.tar.gz
|
||||
apptainer build -f synthmorph_$tag.sif docker-archive://synthmorph_$tag.tar.gz
|
||||
```
|
||||
|
||||
Finally, run the image.
|
||||
|
||||
```sh
|
||||
apptainer run --nv -e -B /autofs synthmorph_$tag.sif
|
||||
```
|
||||
|
||||
|
||||
## Pushing to the Docker Hub
|
||||
|
||||
Push the new image to the Docker Hub to make it public.
|
||||
Update the default "latest" tag, so that `docker pull freesurfer/synthmorph` without a tag will fetch the most recent image.
|
||||
|
||||
```sh
|
||||
docker push freesurfer/synthmorph:$tag
|
||||
docker tag freesurfer/synthmorph:$tag freesurfer/synthmorph:latest
|
||||
docker push freesurfer/synthmorph:latest
|
||||
```
|
||||
|
||||
|
||||
## Exporting Python requirements
|
||||
|
||||
Export build artifacts for users who wish to create a custom Python environment.
|
||||
|
||||
```sh
|
||||
docker build --target export --output env .
|
||||
```
|
||||
|
||||
|
||||
## Final steps
|
||||
|
||||
Lock the annexed weight files again to prevent modification.
|
||||
|
||||
```sh
|
||||
git annex lock synthmorph.*.h5
|
||||
```
|
96
mri_synthmorph/container-script
Normal file
96
mri_synthmorph/container-script
Normal file
|
@ -0,0 +1,96 @@
|
|||
#!/usr/bin/env python3
|
||||
|
||||
# This wrapper script facilitates setup and use of SynthMorph containers by
|
||||
# pulling them from the Docker Hub and mounting the host directory defined by
|
||||
# environment variable SUBJECTS_DIR to /mnt in the container. Invoke the script
|
||||
# just like `mri_synthmorph` in FreeSurfer, with one exception: you can only
|
||||
# read and write data under SUBJECTS_DIR, which will be the working directory
|
||||
# in the container. If unset, SUBJECTS_DIR defaults to your current directory.
|
||||
# This means you can access relative paths under your working directory without
|
||||
# setting SUBJECTS_DIR. In other words, SUBJECTS_DIR sets the working directory
|
||||
# for SynthMorph, and you can specify paths relative to it.
|
||||
|
||||
# Update the version to pull a different image, unless you already have it.
|
||||
version = 4
|
||||
|
||||
# Local image location for Apptainer/Singularity. Set an absolute path to avoid
|
||||
# pulling new images when you change the folder. Ignored for Docker and Podman.
|
||||
sif_file = f'synthmorph_{version}.sif'
|
||||
|
||||
# We will use the first of the below container systems found in your PATH, from
|
||||
# left to right. You may wish to reorder them, If you have several installed.
|
||||
tools = ('docker', 'apptainer', 'singularity', 'podman')
|
||||
|
||||
|
||||
import os
|
||||
import sys
|
||||
import signal
|
||||
import shutil
|
||||
import subprocess
|
||||
|
||||
|
||||
# Report version. Avoid errors when piping, for example to `head`.
|
||||
signal.signal(signal.SIGPIPE, handler=signal.SIG_DFL)
|
||||
hub = 'https://hub.docker.com/u/freesurfer'
|
||||
print(f'Running SynthMorph version {version} from {hub}')
|
||||
|
||||
|
||||
# Find a container system.
|
||||
for tool in tools:
|
||||
path = shutil.which(tool)
|
||||
if path:
|
||||
print(f'Using {path} to manage containers')
|
||||
break
|
||||
|
||||
if not path:
|
||||
print(f'Cannot find container tools {tools} in PATH', file=sys.stderr)
|
||||
exit(1)
|
||||
|
||||
|
||||
# Prepare bind path and URL. Mount SUBJECTS_DIR as /mnt inside the container,
|
||||
# which we made the working directory when building the image. While Docker
|
||||
# and Podman will respect it, they require absolute paths for bind mounts.
|
||||
host = os.environ.get('SUBJECTS_DIR', os.getcwd())
|
||||
host = os.path.abspath(host)
|
||||
print(f'Will bind /mnt in image to SUBJECTS_DIR="{host}"')
|
||||
|
||||
image = f'freesurfer/synthmorph:{version}'
|
||||
if tool != 'docker':
|
||||
image = f'docker://{image}'
|
||||
|
||||
|
||||
# Run Docker containers with the UID and GID of the host user. This user will
|
||||
# own bind mounts inside the container, preventing output files owned by root.
|
||||
# Root inside a rootless Podman container maps to the non-root host user, which
|
||||
# is what we want. If we set UID and GID inside the container to the non-root
|
||||
# host user as we do for Docker, then these would get remapped according to
|
||||
# /etc/subuid outside, causing problems with read and write permissions.
|
||||
if tool in ('docker', 'podman'):
|
||||
arg = ('run', '--rm', '-v', f'{host}:/mnt')
|
||||
|
||||
# Pretty-print help text.
|
||||
if sys.stdout.isatty():
|
||||
arg = (*arg, '-t')
|
||||
if tool == 'docker':
|
||||
arg = (*arg, '-u', f'{os.getuid()}:{os.getgid()}')
|
||||
|
||||
arg = (*arg, image)
|
||||
|
||||
|
||||
# For Apptainer/Singularity, the user inside and outside the container is the
|
||||
# same. The working directory is also the same, unless we explicitly set it.
|
||||
if tool in ('apptainer', 'singularity'):
|
||||
arg = ('run', '--nv', '--pwd', '/mnt', '-e', '-B', f'{host}:/mnt', sif_file)
|
||||
|
||||
if not os.path.isfile(sif_file):
|
||||
print(f'Cannot find image {sif_file}, pulling it', file=sys.stderr)
|
||||
proc = subprocess.run((tool, 'pull', sif_file, image))
|
||||
if proc.returncode:
|
||||
exit(proc.returncode)
|
||||
|
||||
|
||||
# Summarize and launch container.
|
||||
print('Command:', ' '.join((tool, *arg)))
|
||||
print('SynthMorph arguments:', *sys.argv[1:])
|
||||
proc = subprocess.run((tool, *arg, *sys.argv[1:]))
|
||||
exit(proc.returncode)
|
1001
mri_synthmorph/fs-synthmorph-reg
Normal file
1001
mri_synthmorph/fs-synthmorph-reg
Normal file
File diff suppressed because it is too large
Load diff
419
mri_synthmorph/mri_synthmorph
Normal file
419
mri_synthmorph/mri_synthmorph
Normal file
|
@ -0,0 +1,419 @@
|
|||
#!/usr/bin/env python3
|
||||
|
||||
import os
|
||||
import sys
|
||||
import pathlib
|
||||
import argparse
|
||||
import textwrap
|
||||
import surfa as sf
|
||||
from synthmorph import utils
|
||||
|
||||
|
||||
# Argument settings.
|
||||
default = {
|
||||
'model': 'joint',
|
||||
'hyper': 0.5,
|
||||
'extent': 256,
|
||||
'steps': 7,
|
||||
'method': 'linear',
|
||||
'type': 'float32',
|
||||
'fill': 0,
|
||||
}
|
||||
choices = {
|
||||
'model': ('joint', 'deform', 'affine', 'rigid'),
|
||||
'extent': (192, 256),
|
||||
'method': ('linear', 'nearest'),
|
||||
'type': ('uint8', 'uint16', 'int16', 'int32', 'float32'),
|
||||
}
|
||||
limits = {
|
||||
'steps': 5,
|
||||
}
|
||||
resolve = ('model', 'method')
|
||||
|
||||
|
||||
# Documentation.
|
||||
n = '\033[0m' if sys.stdout.isatty() else ''
|
||||
b = '\033[1m' if sys.stdout.isatty() else ''
|
||||
u = '\033[4m' if sys.stdout.isatty() else ''
|
||||
prog = os.path.basename(sys.argv[0])
|
||||
|
||||
|
||||
# References.
|
||||
ref = f'''
|
||||
SynthMorph: learning contrast-invariant registration without acquired images\t
|
||||
Hoffmann M, Billot B, Greve DN, Iglesias JE, Fischl B, Dalca AV\t
|
||||
IEEE Transactions on Medical Imaging, 41 (3), 543-558, 2022\t
|
||||
https://doi.org/10.1109/TMI.2021.3116879
|
||||
|
||||
Anatomy-specific acquisition-agnostic {u}affine{n} registration learned from fictitious images\t
|
||||
Hoffmann M, Hoopes A, Fischl B*, Dalca AV* (*equal contribution)\t
|
||||
SPIE Medical Imaging: Image Processing, 12464, 1246402, 2023\t
|
||||
https://doi.org/10.1117/12.2653251\t
|
||||
https://synthmorph.io/#papers (PDF)
|
||||
|
||||
Anatomy-aware and acquisition-agnostic {u}joint{n} registration with SynthMorph\t
|
||||
Hoffmann M, Hoopes A, Greve DN, Fischl B*, Dalca AV* (*equal contribution)\t
|
||||
Imaging Neuroscience, 2, 1-33, 2024\t
|
||||
https://doi.org/10.1162/imag_a_00197
|
||||
|
||||
Website: https://synthmorph.io
|
||||
'''
|
||||
|
||||
help_general = f'''{prog}
|
||||
|
||||
{b}NAME{n}
|
||||
{b}{prog}{n} - register 3D brain images without preprocessing
|
||||
|
||||
{b}SYNOPSIS{n}
|
||||
{b}{prog}{n} [-h] {u}command{n} [options]
|
||||
|
||||
{b}DESCRIPTION{n}
|
||||
SynthMorph is a deep-learning tool for symmetric, acquisition-agnostic
|
||||
registration of single-frame brain MRI of any geometry. The
|
||||
registration is anatomy-aware, removing the need for skull-stripping,
|
||||
and you can control the warp smoothness.
|
||||
|
||||
Pass an option or {u}command{n} from the following list. You can omit
|
||||
trailing characters, as long as there is no ambiguity.
|
||||
|
||||
{b}register{n}
|
||||
Register 3D brain images without preprocessing.
|
||||
|
||||
{b}apply{n}
|
||||
Apply an existing transform to another 3D image or label map.
|
||||
|
||||
{b}-h{n}
|
||||
Print this help text and exit.
|
||||
|
||||
{b}IMAGE FORMAT{n}
|
||||
The registration supports single-frame image volumes of any size,
|
||||
resolution, and orientation. The moving and the fixed image geometries
|
||||
can differ. The accepted image file formats are: MGH (.mgz) and NIfTI
|
||||
(.nii.gz, .nii).
|
||||
|
||||
Internally, the registration converts image buffers to: isotropic 1-mm
|
||||
voxels, intensities min-max normalized into the interval [0, 1], and
|
||||
left-inferior-anterior (LIA) axes. This conversion requires intact
|
||||
image-to-world matrices. That is, the head must have the correct
|
||||
anatomical orientation in a viewer like {b}freeview{n}.
|
||||
|
||||
{b}TRANSFORM FORMAT{n}
|
||||
SynthMorph transformations operate in physical RAS space. We save
|
||||
matrix transforms as text in LTA format (.lta) and displacement fields
|
||||
as images with three frames indicating shifts in RAS direction.
|
||||
|
||||
{b}ENVIRONMENT{n}
|
||||
The following environment variables affect {b}{prog}{n}:
|
||||
|
||||
SUBJECTS_DIR
|
||||
Ignored unless {b}{prog}{n} runs inside a container. Mounts the
|
||||
host directory SUBJECTS_DIR to {u}/mnt{n} inside the container.
|
||||
Defaults to the current working directory.
|
||||
|
||||
{b}SEE ALSO{n}
|
||||
For converting, composing, and applying transforms, consider FreeSurfer
|
||||
tools {b}lta_convert{n}, {b}mri_warp_convert{n},
|
||||
{b}mri_concatenate_lta{n}, {b}mri_concatenate_gcam{n}, and
|
||||
{b}mri_convert{n}.
|
||||
|
||||
{b}CONTACT{n}
|
||||
Reach out to freesurfer@nmr.mgh.harvard.edu or at
|
||||
https://voxelmorph.net.
|
||||
|
||||
{b}REFERENCES{n}
|
||||
If you use SynthMorph in a publication, please cite us!
|
||||
''' + textwrap.indent(ref, prefix=' ' * 8)
|
||||
|
||||
help_register = f'''{prog}-register
|
||||
|
||||
{b}NAME{n}
|
||||
{b}{prog}-register{n} - register 3D brain images without preprocessing
|
||||
|
||||
{b}SYNOPSIS{n}
|
||||
{b}{prog} register{n} [options] {u}moving{n} {u}fixed{n}
|
||||
|
||||
{b}DESCRIPTION{n}
|
||||
SynthMorph is a deep-learning tool for symmetric, acquisition-agnostic
|
||||
registration of brain MRI with any volume size, resolution, and
|
||||
orientation. The registration is anatomy-aware, removing the need for
|
||||
skull-stripping, and you can control the warp smoothness.
|
||||
|
||||
SynthMorph registers a {u}moving{n} (source) image to a {u}fixed{n}
|
||||
(target) image. Their geometries can differ. The options are as
|
||||
follows:
|
||||
|
||||
{b}-m{n} {u}model{n}
|
||||
Transformation model ({', '.join(choices['model'])}). Defaults
|
||||
to {default['model']}. Joint includes affine and deformable but
|
||||
differs from running both in sequence in that it applies the
|
||||
deformable step in an affine mid-space to guarantee symmetric
|
||||
joint transforms. Deformable assumes prior affine alignment or
|
||||
initialization with {b}-i{n}.
|
||||
|
||||
{b}-o{n} {u}image{n}
|
||||
Save {u}moving{n} registered to {u}fixed{n}.
|
||||
|
||||
{b}-O{n} {u}image{n}
|
||||
Save {u}fixed{n} registered to {u}moving{n}.
|
||||
|
||||
{b}-H{n}
|
||||
Update the voxel-to-world matrix instead of resampling when
|
||||
saving images with {b}-o{n} and {b}-O{n}. For matrix transforms
|
||||
only. Not all software supports headers with shear from affine
|
||||
registration.
|
||||
|
||||
{b}-t{n} {u}trans{n}
|
||||
Save the transform from {u}moving{n} to {u}fixed{n}, including
|
||||
any initialization.
|
||||
|
||||
{b}-T{n} {u}trans{n}
|
||||
Save the transform from {u}fixed{n} to {u}moving{n}, including
|
||||
any initialization.
|
||||
|
||||
{b}-i{n} {u}trans{n}
|
||||
Apply an initial matrix transform to {u}moving{n} before the
|
||||
registration.
|
||||
|
||||
{b}-M{n}
|
||||
Apply half the initial matrix transform to {u}moving{n} and
|
||||
(the inverse of) the other half to {u}fixed{n}, for symmetry.
|
||||
This will make running the deformable after an affine step
|
||||
equivalent to joint registration. Requires {b}-i{n}.
|
||||
|
||||
{b}-j{n} {u}threads{n}
|
||||
Number of TensorFlow threads. System default if unspecified.
|
||||
|
||||
{b}-g{n}
|
||||
Use the GPU in environment variable CUDA_VISIBLE_DEVICES or GPU
|
||||
0 if the variable is unset or empty.
|
||||
|
||||
{b}-r{n} {u}lambda{n}
|
||||
Regularization parameter in the open interval (0, 1) for
|
||||
deformable registration. Higher values lead to smoother warps.
|
||||
Defaults to {default['hyper']}.
|
||||
|
||||
{b}-n{n} {u}steps{n}
|
||||
Integration steps for deformable registration. Lower numbers
|
||||
improve speed and memory use but can lead to inaccuracies and
|
||||
folding voxels. Defaults to {default['steps']}. Should not be
|
||||
less than {limits['steps']}.
|
||||
|
||||
{b}-e{n} {u}extent{n}
|
||||
Isotropic extent of the registration space in unit voxels
|
||||
{choices['extent']}. Lower values improve speed and memory use
|
||||
but may crop the anatomy of interest. Defaults to
|
||||
{default['extent']}.
|
||||
|
||||
{b}-w{n} {u}weights{n}
|
||||
Use alternative model weights, exclusively. Repeat the flag
|
||||
to set affine and deformable weights for joint registration,
|
||||
or the result will disappoint.
|
||||
|
||||
{b}-h{n}
|
||||
Print this help text and exit.
|
||||
|
||||
{b}ENVIRONMENT{n}
|
||||
The following environment variables affect {b}{prog}-register{n}:
|
||||
|
||||
CUDA_VISIBLE_DEVICES
|
||||
Use a specific GPU. If unset or empty, passing {b}-g{n} will
|
||||
select GPU 0. Ignored without {b}-g{n}.
|
||||
|
||||
FREESURFER_HOME
|
||||
Load model weights from directory {u}FREESURFER_HOME/models{n}.
|
||||
Ignored when specifying weights with {b}-w{n}.
|
||||
|
||||
{b}EXAMPLES{n}
|
||||
Joint affine-deformable registration, saving the moved image:
|
||||
# {prog} register -o out.nii mov.nii fix.nii
|
||||
|
||||
Joint registration at 25% warp smoothness:
|
||||
# {prog} register -r 0.25 -o out.nii mov.nii fix.nii
|
||||
|
||||
Affine registration saving the transform:
|
||||
# {prog} register -m affine -t aff.lta mov.nii.gz fix.nii.gz
|
||||
|
||||
Deformable registration only, assuming prior affine alignment:
|
||||
# {prog} register -m deform -t def.mgz mov.mgz fix.mgz
|
||||
|
||||
Deformable step initialized with an affine transform:
|
||||
# {prog} reg -m def -i aff.lta -o out.mgz mov.mgz fix.mgz
|
||||
|
||||
Rigid registration, setting the output image header (no resampling):
|
||||
# {prog} register -m rigid -Ho out.mgz mov.mgz fix.mgz
|
||||
'''
|
||||
|
||||
help_apply = f'''{prog}-apply
|
||||
|
||||
{b}NAME{n}
|
||||
{b}{prog}-apply{n} - apply an existing SynthMorph transform
|
||||
|
||||
{b}SYNOPSIS{n}
|
||||
{b}{prog} apply{n} [options] {u}trans{n} {u}image{n} {u}output{n}
|
||||
[{u}image{n} {u}output{n} ...]
|
||||
|
||||
{b}DESCRIPTION{n}
|
||||
Apply a spatial transform {u}trans{n} estimated by SynthMorph to a 3D
|
||||
{u}image{n} and write the result to {u}output{n}. You can pass any
|
||||
number of image-output pairs to be processed in the same way.
|
||||
|
||||
The following options identically affect all input-output pairs.
|
||||
|
||||
{b}-H{n}
|
||||
Update the voxel-to-world matrix of {u}output{n} instead of
|
||||
resampling. For matrix transforms only. Not all software and
|
||||
file formats support headers with shear from affine
|
||||
registration.
|
||||
|
||||
{b}-m{n} {u}method{n}
|
||||
Interpolation method ({', '.join(choices['method'])}). Defaults
|
||||
to {default['method']}. Choose linear for images and nearest
|
||||
for label (segmentation) maps.
|
||||
|
||||
{b}-t{n} {u}type{n}
|
||||
Output data type ({', '.join(choices['type'])}). Defaults to
|
||||
{default['type']}. Casting to a type other than
|
||||
{default['type']} after linear interpolation may result in
|
||||
information loss.
|
||||
|
||||
{b}-f{n} {u}fill{n}
|
||||
Extrapolation fill value for areas outside the field-of-view of
|
||||
{u}image{n}. Defaults to {default['fill']}.
|
||||
|
||||
{b}-h{n}
|
||||
Print this help text and exit.
|
||||
|
||||
{b}EXAMPLES{n}
|
||||
Apply an affine transform to an image:
|
||||
# {prog} apply affine.lta image.nii out.nii.gz
|
||||
|
||||
Apply a warp to an image, saving the output in floating-point format:
|
||||
# {prog} apply -t float32 warp.nii image.nii out.nii
|
||||
|
||||
Apply the same transform to two images:
|
||||
# {prog} app warp.mgz im_1.mgz out_1.mgz im_2.mgz out_2.mgz
|
||||
|
||||
Transform a label map:
|
||||
# {prog} apply -m nearest warp.nii labels.nii out.nii
|
||||
'''
|
||||
|
||||
|
||||
# Command-line parsing.
|
||||
p = argparse.ArgumentParser()
|
||||
p.format_help = lambda: utils.rewrap_text(help_general, end='\n\n')
|
||||
|
||||
sub = p.add_subparsers(dest='command')
|
||||
commands = {f: sub.add_parser(f) for f in ('register', 'apply')}
|
||||
|
||||
|
||||
def add_flags(f):
|
||||
out = {}
|
||||
|
||||
if f in choices:
|
||||
out.update(choices=choices[f])
|
||||
if f in resolve:
|
||||
out.update(type=lambda x: utils.resolve_abbrev(x, strings=choices[f]))
|
||||
|
||||
if f in default:
|
||||
out.update(default=default[f])
|
||||
|
||||
return out
|
||||
|
||||
|
||||
# Registration arguments.
|
||||
r = commands['register']
|
||||
r.add_argument('moving')
|
||||
r.add_argument('fixed')
|
||||
r.add_argument('-m', dest='model', **add_flags('model'))
|
||||
r.add_argument('-o', dest='out_moving', metavar='image')
|
||||
r.add_argument('-O', dest='out_fixed', metavar='image')
|
||||
r.add_argument('-H', dest='header_only', action='store_true')
|
||||
r.add_argument('-t', dest='trans', metavar='trans')
|
||||
r.add_argument('-T', dest='inverse', metavar='trans')
|
||||
r.add_argument('-i', dest='init', metavar='trans')
|
||||
r.add_argument('-M', dest='mid_space', action='store_true')
|
||||
r.add_argument('-j', dest='threads', metavar='threads', type=int)
|
||||
r.add_argument('-g', dest='gpu', action='store_true')
|
||||
r.add_argument('-r', dest='hyper', metavar='lambda', type=float, **add_flags('hyper'))
|
||||
r.add_argument('-n', dest='steps', metavar='steps', type=int, **add_flags('steps'))
|
||||
r.add_argument('-e', dest='extent', type=int, **add_flags('extent'))
|
||||
r.add_argument('-w', dest='weights', metavar='weights', action='append')
|
||||
r.add_argument('-v', dest='verbose', action='store_true')
|
||||
r.add_argument('-d', dest='out_dir', metavar='dir', type=pathlib.Path)
|
||||
r.format_help = lambda: utils.rewrap_text(help_register, end='\n\n')
|
||||
|
||||
# Transformation arguments.
|
||||
a = commands['apply']
|
||||
a.add_argument('trans')
|
||||
a.add_argument('pairs', metavar='image output', nargs='+')
|
||||
a.add_argument('-H', dest='header_only', action='store_true')
|
||||
a.add_argument('-m', dest='method', **add_flags('method'))
|
||||
a.add_argument('-t', dest='type', **add_flags('type'))
|
||||
a.add_argument('-f', dest='fill', metavar='fill', type=float, **add_flags('fill'))
|
||||
a.format_help = lambda: utils.rewrap_text(help_apply, end='\n\n')
|
||||
|
||||
|
||||
# Parse arguments.
|
||||
if len(sys.argv) == 1:
|
||||
p.print_usage()
|
||||
exit(0)
|
||||
|
||||
# Command resolution.
|
||||
c = sys.argv[1] = utils.resolve_abbrev(sys.argv[1], commands)
|
||||
if len(sys.argv) == 2 and c in commands:
|
||||
commands[c].print_usage()
|
||||
exit(0)
|
||||
|
||||
# Default command.
|
||||
if c not in (*commands, '-h'):
|
||||
sys.argv.insert(1, 'register')
|
||||
|
||||
arg = p.parse_args()
|
||||
|
||||
|
||||
# Command.
|
||||
if arg.command == 'register':
|
||||
|
||||
# Argument checking.
|
||||
if arg.header_only and not arg.model in ('affine', 'rigid'):
|
||||
sf.system.fatal('-H is not compatible with deformable registration')
|
||||
|
||||
if arg.mid_space and not arg.init:
|
||||
sf.system.fatal('-M requires matrix initialization')
|
||||
|
||||
if not 0 < arg.hyper < 1:
|
||||
sf.system.fatal('regularization strength not in open interval (0, 1)')
|
||||
|
||||
if arg.steps < limits['steps']:
|
||||
sf.system.fatal('too few integration steps')
|
||||
|
||||
# TensorFlow setup.
|
||||
gpu = os.environ.get('CUDA_VISIBLE_DEVICES', '0')
|
||||
os.environ['CUDA_VISIBLE_DEVICES'] = gpu if arg.gpu else ''
|
||||
os.environ['TF_CPP_MIN_LOG_LEVEL'] = '0' if arg.verbose else '3'
|
||||
os.environ['NEURITE_BACKEND'] = 'tensorflow'
|
||||
os.environ['VXM_BACKEND'] = 'tensorflow'
|
||||
from synthmorph import registration
|
||||
|
||||
registration.register(arg)
|
||||
|
||||
|
||||
if arg.command == 'apply':
|
||||
|
||||
# Argument checking.
|
||||
which = 'affine' if arg.trans.endswith('.lta') else 'warp'
|
||||
if arg.header_only and which == 'warp':
|
||||
sf.system.fatal('-H is not compatible with deformable transforms')
|
||||
|
||||
if len(arg.pairs) % 2:
|
||||
sf.system.fatal('list of input-output pairs not of even length')
|
||||
|
||||
# Transform.
|
||||
trans = getattr(sf, f'load_{which}')(arg.trans)
|
||||
prop = dict(method=arg.method, resample=not arg.header_only, fill=arg.fill)
|
||||
for inp, out in zip(arg.pairs[::2], arg.pairs[1::2]):
|
||||
sf.load_volume(inp).transform(trans, **prop).astype(arg.type).save(out)
|
||||
|
||||
|
||||
print('Thank you for choosing SynthMorph. Please cite us!')
|
||||
print(utils.rewrap_text(ref))
|
237
mri_synthmorph/mri_synthmorph_apply
Normal file
237
mri_synthmorph/mri_synthmorph_apply
Normal file
|
@ -0,0 +1,237 @@
|
|||
#!/usr/bin/env python3
|
||||
|
||||
import os
|
||||
import sys
|
||||
import shutil
|
||||
import textwrap
|
||||
import argparse
|
||||
import surfa as sf
|
||||
|
||||
|
||||
# Settings.
|
||||
default = {
|
||||
'method': 'linear',
|
||||
'fill': 0,
|
||||
'type': 'float32',
|
||||
}
|
||||
choices = {
|
||||
'method': ('linear', 'nearest'),
|
||||
'type': ('uint8', 'uint16', 'int16', 'int32', 'float32'),
|
||||
}
|
||||
|
||||
|
||||
def rewrap(text, width=None, hard='\t\n', hard_indent=0):
|
||||
"""Rewrap text such that lines fill the available horizontal space.
|
||||
|
||||
Reformats individual paragraphs of a text body, considering subsequent
|
||||
lines with identical indentation as paragraphs. For unspecified width, the
|
||||
function will attempt to determine the extent of the terminal.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
text : str
|
||||
Text to rewrap.
|
||||
width : int, optional
|
||||
Maximum line width. None means the width of the terminal as determined
|
||||
by `textwrap`, defaulting to 80 characters for background processes.
|
||||
hard : str, optional
|
||||
String interpreted as a hard break when terminating a line. Useful for
|
||||
inserting a line break without changing the indentation level. Must end
|
||||
with a line break and will be removed from the output text.
|
||||
hard_indent : int, optional
|
||||
Number of additional whitespace characters by which to indent the lines
|
||||
following a hard break. See `hard`.
|
||||
|
||||
Returns
|
||||
-------
|
||||
out : str
|
||||
Reformatted text.
|
||||
|
||||
"""
|
||||
# Inputs.
|
||||
if width is None:
|
||||
width = shutil.get_terminal_size().columns
|
||||
lines = text.splitlines(keepends=True)
|
||||
|
||||
# Merge lines to paragraphs.
|
||||
pad = []
|
||||
pad_hard = []
|
||||
par = []
|
||||
for i, line in enumerate(lines):
|
||||
ind = len(line) - len(line.lstrip())
|
||||
if i == 0 or ind != pad[-1] or lines[i - 1].endswith(hard):
|
||||
par.append('')
|
||||
pad.append(ind)
|
||||
pad_hard.append(ind)
|
||||
|
||||
if line.endswith(hard):
|
||||
line = line.replace(hard, '\n')
|
||||
pad_hard[-1] += hard_indent
|
||||
par[-1] += line[ind:]
|
||||
|
||||
# Reformat paragraphs.
|
||||
for i, _ in enumerate(par):
|
||||
par[i] = textwrap.fill(
|
||||
par[i], width,
|
||||
initial_indent=' ' * pad[i], subsequent_indent=' ' * pad_hard[i],
|
||||
)
|
||||
|
||||
return '\n'.join(par)
|
||||
|
||||
|
||||
# Documentation.
|
||||
n = '\033[0m' if sys.stdout.isatty() else ''
|
||||
b = '\033[1m' if sys.stdout.isatty() else ''
|
||||
u = '\033[4m' if sys.stdout.isatty() else ''
|
||||
prog = os.path.basename(sys.argv[0])
|
||||
doc = f'''{prog}
|
||||
|
||||
{b}NAME{n}
|
||||
{b}{prog}{n} - apply a SynthMorph transform to 3D images
|
||||
|
||||
{b}SYNOPSIS{n}
|
||||
{b}{prog}{n} [options] {u}trans{n} {u}image{n} {u}output{n}
|
||||
[{u}image{n} {u}output{n} ...]
|
||||
|
||||
{b}DESCRIPTION{n}
|
||||
Apply a spatial transform {u}trans{n} estimated by SynthMorph to a 3D
|
||||
{u}image{n} and write the result to {u}output{n}. You can pass any
|
||||
number of image-output pairs to be processed in the same way.
|
||||
|
||||
The following options identically affect all image-output pairs.
|
||||
|
||||
{b}-H{n}
|
||||
Update the voxel-to-world matrix of the output image instead of
|
||||
resampling. For matrix transforms only. Not all software and
|
||||
file formats support headers with shear from affine
|
||||
registration.
|
||||
|
||||
{b}-i{n} {u}method{n}
|
||||
Interpolation method ({', '.join(choices['method'])}). Defaults
|
||||
to {default['method']}. Choose linear for images and nearest
|
||||
for label (segmentation) maps.
|
||||
|
||||
{b}-t{n} {u}type{n}
|
||||
Output data type ({', '.join(choices['type'])}). Defaults to
|
||||
{default['type']}. Casting to a narrower type can result in
|
||||
information loss.
|
||||
|
||||
{b}-f{n} {u}fill{n}
|
||||
Extrapolation fill value for areas outside the field-of-view of
|
||||
the input image. Defaults to {default['fill']}.
|
||||
|
||||
{b}-h{n}
|
||||
Print this help text and exit.
|
||||
|
||||
{b}IMAGE FORMAT{n}
|
||||
Accepted file formats include: MGH (.mgz) and NIfTI (.nii.gz, .nii).
|
||||
|
||||
{b}TRANSFORMS{n}
|
||||
Refer to the help text of the registration utility for information on
|
||||
transform file formats.
|
||||
|
||||
For converting, composing, and applying transforms, consider the
|
||||
FreeSurfer tools lta_convert, mri_warp_convert, mri_concatenate_lta,
|
||||
mri_concatenate_gcam, mri_convert, mri_info.
|
||||
|
||||
{b}ENVIRONMENT{n}
|
||||
The following environment variables affect {b}{prog}{n}:
|
||||
|
||||
SUBJECTS_DIR
|
||||
Ignored unless {b}{prog}{n} runs inside a container. Mount the
|
||||
host directory SUBJECTS_DIR to {u}/mnt{n} inside the container.
|
||||
Defaults to the current working directory.
|
||||
|
||||
{b}EXAMPLES{n}
|
||||
Apply an affine transform to an image:
|
||||
# {prog} affine.lta image.nii out.nii.gz
|
||||
|
||||
Apply a warp to an image, saving the output in floating-point format:
|
||||
# {prog} -t float32 warp.mgz image.mgz out.mgz
|
||||
|
||||
Apply a transform to each of two images:
|
||||
# {prog} warp.mgz image_1.mgz out_1.mgz image_2.mgz out_2.mgz
|
||||
|
||||
Transform a label map:
|
||||
# {prog} -i nearest warp.mgz labels.mgz out.mgz
|
||||
|
||||
{b}CONTACT{n}
|
||||
Reach out to freesurfer@nmr.mgh.harvard.edu or at
|
||||
https://github.com/voxelmorph/voxelmorph.
|
||||
|
||||
{b}REFERENCES{n}
|
||||
If you use SynthMorph in a publication, please cite us!
|
||||
'''
|
||||
|
||||
|
||||
# References.
|
||||
ref = '''
|
||||
SynthMorph: learning contrast-invariant registration without acquired images\t
|
||||
Hoffmann M, Billot B, Greve DN, Iglesias JE, Fischl B, Dalca AV\t
|
||||
IEEE Transactions on Medical Imaging, 41 (3), 543-558, 2022\t
|
||||
https://doi.org/10.1109/TMI.2021.3116879
|
||||
|
||||
Anatomy-specific acquisition-agnostic affine registration learned from fictitious images\t
|
||||
Hoffmann M, Hoopes A, Fischl B*, Dalca AV* (*equal contribution)\t
|
||||
SPIE Medical Imaging: Image Processing, 12464, 1246402, 2023\t
|
||||
https://doi.org/10.1117/12.2653251\t
|
||||
https://synthmorph.io/#papers (PDF)
|
||||
|
||||
Anatomy-aware and acquisition-agnostic joint registration with SynthMorph\t
|
||||
Hoffmann M, Hoopes A, Greve DN, Fischl B*, Dalca AV* (*equal contribution)\t
|
||||
Imaging Neuroscience, 2, 1-33, 2024\t
|
||||
https://doi.org/10.1162/imag_a_00197
|
||||
|
||||
Website: https://synthmorph.io
|
||||
'''
|
||||
doc += textwrap.indent(ref, prefix=' ' * 8)
|
||||
|
||||
|
||||
print(rewrap((
|
||||
f'Warning: {prog} is deprecated in favor of `mri_synthmorph apply` and '
|
||||
'will be removed in the future.'
|
||||
)))
|
||||
|
||||
|
||||
# Arguments.
|
||||
p = argparse.ArgumentParser(add_help=False)
|
||||
p.add_argument('trans')
|
||||
p.add_argument('pairs', metavar='image output', nargs='+')
|
||||
p.add_argument('-H', dest='header_only', action='store_true')
|
||||
p.add_argument('-i', dest='method', choices=choices['method'], default=default['method'])
|
||||
p.add_argument('-t', dest='type', choices=choices['type'], default=default['type'])
|
||||
p.add_argument('-f', dest='fill', metavar='fill', type=float, default=default['fill'])
|
||||
p.add_argument('-h', action='store_true')
|
||||
|
||||
|
||||
# Help.
|
||||
if len(sys.argv) == 1:
|
||||
p.print_usage()
|
||||
exit(0)
|
||||
|
||||
if any(f[0] == '-' and 'h' in f for f in sys.argv):
|
||||
print(rewrap(doc), end='\n\n')
|
||||
exit(0)
|
||||
|
||||
|
||||
# Parsing.
|
||||
arg = p.parse_args()
|
||||
|
||||
if len(arg.pairs) % 2:
|
||||
sf.system.fatal('did not receive even-length list of input-output pairs')
|
||||
|
||||
|
||||
# Transform.
|
||||
f = 'affine' if arg.trans.endswith('.lta') else 'warp'
|
||||
trans = getattr(sf, f'load_{f}')(arg.trans)
|
||||
|
||||
|
||||
# Application.
|
||||
pairs = zip(arg.pairs[::2], arg.pairs[1::2])
|
||||
prop = dict(method=arg.method, resample=not arg.header_only, fill=arg.fill)
|
||||
for inp, out in pairs:
|
||||
sf.load_volume(inp).transform(trans, **prop).astype(arg.type).save(out)
|
||||
|
||||
|
||||
print('Thank you for choosing SynthMorph. Please cite us!')
|
||||
print(rewrap(ref))
|
313
mri_synthmorph/synthmorph/registration.py
Normal file
313
mri_synthmorph/synthmorph/registration.py
Normal file
|
@ -0,0 +1,313 @@
|
|||
import os
|
||||
import h5py
|
||||
import numpy as np
|
||||
import surfa as sf
|
||||
import tensorflow as tf
|
||||
import voxelmorph as vxm
|
||||
|
||||
|
||||
# Settings.
|
||||
weights = {
|
||||
'joint': ('synthmorph.affine.2.h5', 'synthmorph.deform.3.h5',),
|
||||
'deform': ('synthmorph.deform.3.h5',),
|
||||
'affine': ('synthmorph.affine.2.h5',),
|
||||
'rigid': ('synthmorph.rigid.1.h5',),
|
||||
}
|
||||
|
||||
|
||||
def network_space(im, shape, center=None):
|
||||
"""Construct transform from network space to the voxel space of an image.
|
||||
|
||||
Constructs a coordinate transform from the space the network will operate
|
||||
in to the zero-based image index space. The network space has isotropic
|
||||
1-mm voxels, left-inferior-anterior (LIA) orientation, and no shear. It is
|
||||
centered on the field of view, or that of a reference image. This space is
|
||||
an indexed voxel space, not world space.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
im : surfa.Volume
|
||||
Input image to construct the transform for.
|
||||
shape : (3,) array-like
|
||||
Spatial shape of the network space.
|
||||
center : surfa.Volume, optional
|
||||
Center the network space on the center of a reference image.
|
||||
|
||||
Returns
|
||||
-------
|
||||
out : tuple of (3, 4) NumPy arrays
|
||||
Transform from network to input-image space and its inverse, thinking
|
||||
coordinates.
|
||||
|
||||
"""
|
||||
old = im.geom
|
||||
new = sf.ImageGeometry(
|
||||
shape=shape,
|
||||
voxsize=1,
|
||||
rotation='LIA',
|
||||
center=old.center if center is None else center.geom.center,
|
||||
shear=None,
|
||||
)
|
||||
|
||||
net_to_vox = old.world2vox @ new.vox2world
|
||||
vox_to_net = new.world2vox @ old.vox2world
|
||||
return net_to_vox.matrix, vox_to_net.matrix
|
||||
|
||||
|
||||
def transform(im, trans, shape=None, normalize=False, batch=False):
|
||||
"""Apply a spatial transform to 3D image voxel data in dimensions.
|
||||
|
||||
Applies a transformation matrix operating in zero-based index space or a
|
||||
displacement field to an image buffer.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
im : surfa.Volume or NumPy array or TensorFlow tensor
|
||||
Input image to transform, without batch dimension.
|
||||
trans : array-like
|
||||
Transform to apply to the image. A matrix of shape (3, 4), a matrix
|
||||
of shape (4, 4), or a displacement field of shape (*space, 3),
|
||||
without batch dimension.
|
||||
shape : (3,) array-like, optional
|
||||
Output shape used for converting matrices to dense transforms. None
|
||||
means the shape of the input image will be used.
|
||||
normalize : bool, optional
|
||||
Min-max normalize the image intensities into the interval [0, 1].
|
||||
batch : bool, optional
|
||||
Prepend a singleton batch dimension to the output tensor.
|
||||
|
||||
Returns
|
||||
-------
|
||||
out : float TensorFlow tensor
|
||||
Transformed image with a trailing feature dimension.
|
||||
|
||||
"""
|
||||
# Add singleton feature dimension if needed.
|
||||
if tf.rank(im) == 3:
|
||||
im = im[..., tf.newaxis]
|
||||
|
||||
out = vxm.utils.transform(
|
||||
im, trans, fill_value=0, shift_center=False, shape=shape,
|
||||
)
|
||||
|
||||
if normalize:
|
||||
out -= tf.reduce_min(out)
|
||||
out /= tf.reduce_max(out)
|
||||
|
||||
if batch:
|
||||
out = out[tf.newaxis, ...]
|
||||
|
||||
return out
|
||||
|
||||
|
||||
def load_weights(model, weights):
|
||||
"""Load weights into model or submodel.
|
||||
|
||||
Attempts to load (all) weights into a model or one of its submodels. If
|
||||
that fails, `model` may be a submodel of what we got weights for, and we
|
||||
attempt to load the weights of a submodel (layer) into `model`.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
model : TensorFlow model
|
||||
Model to initialize.
|
||||
weights : str or pathlib.Path
|
||||
Path to weights file.
|
||||
|
||||
Raises
|
||||
------
|
||||
ValueError
|
||||
If unsuccessful at loading any weights.
|
||||
|
||||
"""
|
||||
# Extract submodels.
|
||||
models = [model]
|
||||
i = 0
|
||||
while i < len(models):
|
||||
layers = [f for f in models[i].layers if isinstance(f, tf.keras.Model)]
|
||||
models.extend(layers)
|
||||
i += 1
|
||||
|
||||
# Add models wrapping a single model in case this was done in training.
|
||||
# Requires list expansion or Python will get stuck.
|
||||
models.extend([tf.keras.Model(m.inputs, m(m.inputs)) for m in models])
|
||||
|
||||
# Attempt to load all weights into one of the models.
|
||||
for mod in models:
|
||||
try:
|
||||
mod.load_weights(weights)
|
||||
return
|
||||
except ValueError as e:
|
||||
pass
|
||||
|
||||
# Assume `model` is a submodel of what we got weights for.
|
||||
with h5py.File(weights, mode='r') as h5:
|
||||
layers = h5.attrs['layer_names']
|
||||
weights = [list(h5[lay].attrs['weight_names']) for lay in layers]
|
||||
|
||||
# Layers with weights. Attempt loading.
|
||||
layers, weights = zip(*filter(lambda f: f[1], zip(layers, weights)))
|
||||
for lay, wei in zip(layers, weights):
|
||||
try:
|
||||
model.set_weights([h5[lay][w] for w in wei])
|
||||
return
|
||||
except ValueError as e:
|
||||
if lay is layers[-1]:
|
||||
raise e
|
||||
|
||||
|
||||
def register(arg):
|
||||
|
||||
# Parse arguments.
|
||||
in_shape = (arg.extent,) * 3
|
||||
is_mat = arg.model in ('affine', 'rigid')
|
||||
|
||||
# Threading.
|
||||
if arg.threads:
|
||||
tf.config.threading.set_inter_op_parallelism_threads(arg.threads)
|
||||
tf.config.threading.set_intra_op_parallelism_threads(arg.threads)
|
||||
|
||||
# Input data.
|
||||
mov = sf.load_volume(arg.moving)
|
||||
fix = sf.load_volume(arg.fixed)
|
||||
if not len(mov.shape) == len(fix.shape) == 3:
|
||||
sf.system.fatal('input images are not single-frame volumes')
|
||||
|
||||
# Transforms between native voxel and network coordinates. Voxel and
|
||||
# network spaces differ for each image. The networks expect isotropic 1-mm
|
||||
# LIA spaces. Center these on the original images, except in the deformable
|
||||
# case: it assumes prior affine registration, so we center the moving
|
||||
# network space on the fixed image, to take into account affine transforms
|
||||
# via resampling, updating the header, or passed on the command line alike.
|
||||
center = fix if arg.model == 'deform' else None
|
||||
net_to_mov, mov_to_net = network_space(mov, shape=in_shape, center=center)
|
||||
net_to_fix, fix_to_net = network_space(fix, shape=in_shape)
|
||||
|
||||
# Coordinate transforms from and to world space. There is only one world.
|
||||
mov_to_ras = mov.geom.vox2world.matrix
|
||||
fix_to_ras = fix.geom.vox2world.matrix
|
||||
ras_to_mov = mov.geom.world2vox.matrix
|
||||
ras_to_fix = fix.geom.world2vox.matrix
|
||||
|
||||
# Incorporate an initial matrix transform from moving to fixed coordinates,
|
||||
# as LTAs store the inverse. For mid-space initialization, compute the
|
||||
# square root of the transform between fixed and moving network space.
|
||||
if arg.init:
|
||||
init = sf.load_affine(arg.init).convert(space='voxel')
|
||||
if init.ndim != 3 \
|
||||
or not sf.transform.image_geometry_equal(mov.geom, init.source, tol=1e-3) \
|
||||
or not sf.transform.image_geometry_equal(fix.geom, init.target, tol=1e-3):
|
||||
sf.system.fatal('initial transform geometry does not match images')
|
||||
|
||||
init = fix_to_net @ init @ net_to_mov
|
||||
if arg.mid_space:
|
||||
init = tf.linalg.sqrtm(init)
|
||||
if np.any(np.isnan(init)):
|
||||
sf.system.fatal(f'cannot compute matrix square root of {arg.init}')
|
||||
net_to_fix = net_to_fix @ init
|
||||
fix_to_net = np.linalg.inv(net_to_fix)
|
||||
|
||||
net_to_mov = net_to_mov @ tf.linalg.inv(init)
|
||||
mov_to_net = np.linalg.inv(net_to_mov)
|
||||
|
||||
# Take the input images to network space. When saving the moving image with
|
||||
# the correct voxel-to-RAS matrix after incorporating an initial transform,
|
||||
# an image viewer taking this matrix into account will show an unchanged
|
||||
# image. The networks only see the voxel data, which have been moved.
|
||||
inputs = (
|
||||
transform(mov, net_to_mov, shape=in_shape, normalize=True, batch=True),
|
||||
transform(fix, net_to_fix, shape=in_shape, normalize=True, batch=True),
|
||||
)
|
||||
|
||||
# Network. For deformable-only registration, `HyperVxmJoint` ignores the
|
||||
# `mid_space` argument, and the initialization will determine the space.
|
||||
prop = dict(in_shape=in_shape, bidir=True)
|
||||
if is_mat:
|
||||
prop.update(make_dense=False, rigid=arg.model == 'rigid')
|
||||
model = vxm.networks.VxmAffineFeatureDetector(**prop)
|
||||
|
||||
else:
|
||||
prop.update(mid_space=True, int_steps=arg.steps, skip_affine=arg.model == 'deform')
|
||||
model = vxm.networks.HyperVxmJoint(**prop)
|
||||
inputs = (tf.constant([arg.hyper]), *inputs)
|
||||
|
||||
# Weights.
|
||||
if not arg.weights:
|
||||
fs = os.environ.get('FREESURFER_HOME')
|
||||
if not fs:
|
||||
sf.system.fatal('set environment variable FREESURFER_HOME or weights')
|
||||
arg.weights = [os.path.join(fs, 'models', f) for f in weights[arg.model]]
|
||||
|
||||
for f in arg.weights:
|
||||
load_weights(model, weights=f)
|
||||
|
||||
# Inference. The first transform maps from the moving to the fixed image,
|
||||
# or equivalently, from fixed to moving coordinates. The second is the
|
||||
# inverse. Convert transforms between moving and fixed network spaces to
|
||||
# transforms between the original voxel spaces.
|
||||
pred = tuple(map(tf.squeeze, model(inputs)))
|
||||
fw, bw = pred
|
||||
fw = vxm.utils.compose((net_to_mov, fw, fix_to_net), shift_center=False, shape=fix.shape)
|
||||
bw = vxm.utils.compose((net_to_fix, bw, mov_to_net), shift_center=False, shape=mov.shape)
|
||||
|
||||
# Associate image geometries with the transforms. LTAs store the inverse.
|
||||
if is_mat:
|
||||
fw, bw = bw, fw
|
||||
fw = sf.Affine(fw, source=mov, target=fix, space='voxel')
|
||||
bw = sf.Affine(bw, source=fix, target=mov, space='voxel')
|
||||
format = dict(space='world')
|
||||
|
||||
else:
|
||||
fw = sf.Warp(fw, source=mov, target=fix, format=sf.Warp.Format.disp_crs)
|
||||
bw = sf.Warp(bw, source=fix, target=mov, format=sf.Warp.Format.disp_crs)
|
||||
format = dict(format=sf.Warp.Format.disp_ras)
|
||||
|
||||
# Output transforms.
|
||||
if arg.trans:
|
||||
fw.convert(**format).save(arg.trans)
|
||||
|
||||
if arg.inverse:
|
||||
bw.convert(**format).save(arg.inverse)
|
||||
|
||||
# Moved images.
|
||||
if arg.out_moving:
|
||||
mov.transform(fw, resample=not arg.header_only).save(arg.out_moving)
|
||||
|
||||
if arg.out_fixed:
|
||||
fix.transform(bw, resample=not arg.header_only).save(arg.out_fixed)
|
||||
|
||||
# Outputs in network space.
|
||||
if arg.out_dir:
|
||||
arg.out_dir.mkdir(exist_ok=True)
|
||||
|
||||
# Input images.
|
||||
mov = sf.ImageGeometry(in_shape, vox2world=mov_to_ras @ net_to_mov)
|
||||
fix = sf.ImageGeometry(in_shape, vox2world=fix_to_ras @ net_to_fix)
|
||||
mov = sf.Volume(inputs[-2][0], geometry=fix if arg.init else mov)
|
||||
fix = sf.Volume(inputs[-1][0], geometry=fix)
|
||||
mov.save(filename=arg.out_dir / 'inp_1.nii.gz')
|
||||
fix.save(filename=arg.out_dir / 'inp_2.nii.gz')
|
||||
|
||||
fw, bw = pred
|
||||
if is_mat:
|
||||
fw, bw = bw, fw
|
||||
fw = sf.Affine(fw, source=mov, target=fix, space='voxel')
|
||||
bw = sf.Affine(bw, source=fix, target=mov, space='voxel')
|
||||
ext = 'lta'
|
||||
|
||||
else:
|
||||
fw = sf.Warp(fw, source=mov, target=fix, format=sf.Warp.Format.disp_crs)
|
||||
bw = sf.Warp(bw, source=fix, target=mov, format=sf.Warp.Format.disp_crs)
|
||||
ext = 'nii.gz'
|
||||
|
||||
# Transforms.
|
||||
fw.convert(**format).save(filename=arg.out_dir / f'tra_1.{ext}')
|
||||
bw.convert(**format).save(filename=arg.out_dir / f'tra_2.{ext}')
|
||||
|
||||
# Moved images.
|
||||
mov.transform(fw).save(filename=arg.out_dir / 'out_1.nii.gz')
|
||||
fix.transform(bw).save(filename=arg.out_dir / 'out_2.nii.gz')
|
||||
|
||||
vmpeak = sf.system.vmpeak()
|
||||
if vmpeak is not None:
|
||||
print(f'#@# mri_synthmorph: {arg.model}, threads: {arg.threads}, VmPeak: {vmpeak}')
|
93
mri_synthmorph/synthmorph/utils.py
Normal file
93
mri_synthmorph/synthmorph/utils.py
Normal file
|
@ -0,0 +1,93 @@
|
|||
import shutil
|
||||
import textwrap
|
||||
|
||||
|
||||
def resolve_abbrev(needle, strings, lower=False):
|
||||
"""Return a full-length string matching a substring from the beginning.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
needle : str
|
||||
Substring of one of several `strings`.
|
||||
strings : str or iterable of str
|
||||
Full-length strings, one of which should begin with `needle`.
|
||||
lower : bool, optional
|
||||
Convert needle to lowercase before matching.
|
||||
|
||||
Returns
|
||||
-------
|
||||
str
|
||||
String in `strings` that begins with `needle` if there is no ambiguity.
|
||||
If there is not exactly one match, the function will return `needle`.
|
||||
|
||||
"""
|
||||
if isinstance(strings, str):
|
||||
strings = [strings]
|
||||
strings = tuple(strings)
|
||||
|
||||
if lower:
|
||||
needle = needle.lower()
|
||||
|
||||
matches = [f for f in strings if f.startswith(needle)]
|
||||
return matches[0] if len(matches) == 1 else needle
|
||||
|
||||
|
||||
def rewrap_text(text, width=None, hard='\t\n', hard_indent=0, end=''):
|
||||
"""Rewrap text such that lines fill the available horizontal space.
|
||||
|
||||
Reformats individual paragraphs of a text body, considering subsequent
|
||||
lines with identical indentation as paragraphs. For unspecified width, the
|
||||
function will attempt to determine the extent of the terminal.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
text : str
|
||||
Text to rewrap.
|
||||
width : int, optional
|
||||
Maximum line width. None means the width of the terminal as determined
|
||||
by `textwrap`, defaulting to 80 characters for background processes.
|
||||
hard : str, optional
|
||||
String interpreted as a hard break when terminating a line. Useful for
|
||||
inserting a line break without changing the indentation level. Must end
|
||||
with a line break and will be removed from the output text.
|
||||
hard_indent : int, optional
|
||||
Number of additional whitespace characters by which to indent the lines
|
||||
following a hard break. See `hard`.
|
||||
end : str, optional
|
||||
Append to the reformatted text.
|
||||
|
||||
Returns
|
||||
-------
|
||||
out : str
|
||||
Reformatted text.
|
||||
|
||||
"""
|
||||
# Inputs.
|
||||
if width is None:
|
||||
width = shutil.get_terminal_size().columns
|
||||
lines = text.splitlines(keepends=True)
|
||||
|
||||
# Merge lines to paragraphs.
|
||||
pad = []
|
||||
pad_hard = []
|
||||
par = []
|
||||
for i, line in enumerate(lines):
|
||||
ind = len(line) - len(line.lstrip())
|
||||
if i == 0 or ind != pad[-1] or lines[i - 1].endswith(hard):
|
||||
par.append('')
|
||||
pad.append(ind)
|
||||
pad_hard.append(ind)
|
||||
|
||||
if line.endswith(hard):
|
||||
line = line.replace(hard, '\n')
|
||||
pad_hard[-1] += hard_indent
|
||||
par[-1] += line[ind:]
|
||||
|
||||
# Reformat paragraphs.
|
||||
for i, _ in enumerate(par):
|
||||
par[i] = textwrap.fill(
|
||||
par[i], width,
|
||||
initial_indent=' ' * pad[i], subsequent_indent=' ' * pad_hard[i],
|
||||
)
|
||||
|
||||
return '\n'.join(par) + end
|
69
mri_synthmorph/test_apply.sh
Normal file
69
mri_synthmorph/test_apply.sh
Normal file
|
@ -0,0 +1,69 @@
|
|||
#!/usr/bin/env bash
|
||||
. "$(dirname "$0")/../test.sh"
|
||||
|
||||
t() { test_command mri_synthmorph "$@" ; }
|
||||
|
||||
# image interpolation
|
||||
t apply affine.lta moving.mgz out.mgz
|
||||
compare_vol out.mgz affine.mgz --thresh 0.02 --res-thresh 1e-3 --geo-thresh 1e-3
|
||||
|
||||
# NIfTI format
|
||||
t apply affine.lta moving.mgz out.nii.gz
|
||||
compare_vol out.nii.gz affine.mgz --thresh 0.02 --res-thresh 1e-3 --geo-thresh 1e-3
|
||||
|
||||
# dense transform
|
||||
t apply identity.mgz moving.mgz out.mgz -t uint8
|
||||
compare_vol out.mgz moving.mgz --res-thresh 1e-3 --geo-thresh 1e-3
|
||||
|
||||
# matrix update
|
||||
t apply -H rigid.lta -t uint8 moving.mgz out.mgz
|
||||
compare_vol out.mgz header.mgz --res-thresh 1e-3 --geo-thresh 1e-3
|
||||
|
||||
# label interpolation
|
||||
t apply -m nearest -t uint8 affine.lta labels.moving.mgz out.mgz
|
||||
compare_vol out.mgz labels.affine.mgz --res-thresh 1e-3 --geo-thresh 1e-3
|
||||
|
||||
# multiple input pairs
|
||||
t apply rigid.lta moving.mgz out_1.mgz moving.mgz out_2.mgz
|
||||
compare_vol out_1.mgz rigid.mgz --thresh 0.02 --res-thresh 1e-3 --geo-thresh 1e-3
|
||||
compare_vol out_2.mgz rigid.mgz --thresh 0.02 --res-thresh 1e-3 --geo-thresh 1e-3
|
||||
|
||||
# data types, command abbreviation
|
||||
t a -t uint8 rigid.lta moving.mgz out.mgz
|
||||
run_comparison mri_info --type out.mgz | grep -Fx uchar
|
||||
|
||||
t ap -t uint16 rigid.lta moving.mgz out.mgz
|
||||
run_comparison mri_info --type out.mgz | grep -Fx ushrt
|
||||
|
||||
t app -t int16 rigid.lta moving.mgz out.mgz
|
||||
run_comparison mri_info --type out.mgz | grep -Fx short
|
||||
|
||||
t appl -t int32 rigid.lta moving.mgz out.mgz
|
||||
run_comparison mri_info --type out.mgz | grep -Fx int
|
||||
|
||||
t apply -t float32 rigid.lta moving.mgz out.mgz
|
||||
run_comparison mri_info --type out.mgz | grep -Fx float
|
||||
|
||||
# method abbreviation
|
||||
FSTEST_NO_DATA_RESET=1
|
||||
for method in l li lin line linea linear n ne nea near neare neares nearest; do
|
||||
t apply -m "$method" rigid.lta moving.mgz out.mgz
|
||||
done
|
||||
|
||||
# usage, help
|
||||
t
|
||||
t -h
|
||||
t apply
|
||||
t apply -h
|
||||
|
||||
# NIfTI warp
|
||||
FSTEST_NO_DATA_RESET=1
|
||||
mri_convert identity.mgz identity.nii.gz
|
||||
t apply identity.nii.gz moving.mgz out.mgz -t uint8
|
||||
compare_vol out.mgz moving.mgz --res-thresh 1e-3 --geo-thresh 1e-3
|
||||
|
||||
# illegal arguments
|
||||
EXPECT_FAILURE=1
|
||||
t slice
|
||||
t apply -H identity.mgz moving.mgz fixed.mgz
|
||||
t apply affine.lta moving.mgz out.mgz odd-number-of-io-pairs.mgz
|
83
mri_synthmorph/test_register.sh
Normal file
83
mri_synthmorph/test_register.sh
Normal file
|
@ -0,0 +1,83 @@
|
|||
#!/usr/bin/env bash
|
||||
. "$(dirname "$0")/../test.sh"
|
||||
|
||||
t() { test_command mri_synthmorph "$@" ; }
|
||||
|
||||
# affine registration
|
||||
t -m affine -o out.mgz moving.mgz fixed.mgz
|
||||
compare_vol out.mgz affine.mgz --thresh 0.02
|
||||
|
||||
# affine symmetry
|
||||
t -m aff -O out.mgz fixed.mgz moving.mgz
|
||||
compare_vol out.mgz affine.mgz --thresh 0.02
|
||||
|
||||
# affine inverse consistency
|
||||
t -m a -t out.lta -T inv.lta moving.mgz fixed.mgz
|
||||
lta_diff out.lta inv.lta --invert2 | awk 'END {print $0; exit !($0<1e-3)}'
|
||||
|
||||
# output directory creation
|
||||
t -ma -d outputs fixed.mgz moving.mgz
|
||||
[ -d outputs ]
|
||||
|
||||
# rigid inverse consistency
|
||||
t -m rigid -t out.lta -T inv.lta moving.mgz fixed.mgz
|
||||
lta_diff out.lta inv.lta --invert2 | awk 'END {print $0; exit !($0<1e-3)}'
|
||||
|
||||
# rigid registration
|
||||
t -m rig -o out.mgz moving.mgz fixed.mgz
|
||||
compare_vol out.mgz rigid.mgz --thresh 0.02
|
||||
|
||||
# geometry update
|
||||
t -m r -Ho out.mgz moving.mgz fixed.mgz
|
||||
compare_vol out.mgz header.mgz --thresh 0.02 --res-thresh 1e-3 --geo-thresh 1e-3
|
||||
|
||||
# deformable registration with initialization
|
||||
t -m deform -i affine.lta -o out_1.mgz -O out_2.mgz moving.mgz fixed.mgz
|
||||
compare_vol out_1.mgz deform_1.mgz --thresh 0.02
|
||||
compare_vol out_2.mgz deform_2.mgz --thresh 0.02
|
||||
|
||||
# deformable registration with mid-space initialization
|
||||
t -m def -Mi affine.lta -o out.nii.gz moving.mgz fixed.mgz
|
||||
compare_vol out.nii.gz deform_mid.nii.gz --thresh 0.02 --geo-thresh 1e-4
|
||||
|
||||
# joint registration
|
||||
t -m joint -o out.mgz moving.mgz fixed.mgz
|
||||
compare_vol out.mgz joint.mgz --thresh 0.02
|
||||
|
||||
# default model
|
||||
t -o out.mgz moving.mgz fixed.mgz
|
||||
compare_vol out.mgz joint.mgz --thresh 0.02
|
||||
|
||||
# help, usage, command abbreviation
|
||||
FSTEST_NO_DATA_RESET=1
|
||||
t register -h
|
||||
for cmd in r re reg regi regis registe register; do
|
||||
t "$cmd"
|
||||
done
|
||||
|
||||
# deformable flags, explicit command
|
||||
t register moving.mgz fixed.mgz -md -j16 -e256 -n7 -r0.5
|
||||
t register moving.mgz fixed.mgz -m j -j 16 -e 192 -n 5 -r 0.7
|
||||
|
||||
# NIfTI warps
|
||||
FSTEST_NO_DATA_RESET=1
|
||||
mri_convert=$(find_path $FSTEST_CWD mri_convert/mri_convert)
|
||||
t -t out.nii.gz moving.mgz fixed.mgz
|
||||
test_command $mri_convert -odt float -at out.nii.gz moving.mgz out.mgz
|
||||
compare_vol out.mgz joint.mgz --thresh 1
|
||||
|
||||
# displacements in RAS space
|
||||
mri_warp_convert=$(find_path $FSTEST_CWD mri_warp_convert/mri_warp_convert)
|
||||
test_command $mri_warp_convert -g moving.mgz --inras out.nii.gz --outmgzwarp out.mgz
|
||||
test_command $mri_convert -odt float -at out.mgz moving.mgz moved.mgz
|
||||
compare_vol moved.mgz joint.mgz --thresh 1 --geo-thresh 1e-4
|
||||
|
||||
# illegal arguments
|
||||
EXPECT_FAILURE=1
|
||||
t moving.mgz fixed.mgz -m banana
|
||||
t moving.mgz fixed.mgz -e 1
|
||||
t moving.mgz fixed.mgz -n 4
|
||||
t moving.mgz fixed.mgz -r 0
|
||||
t moving.mgz fixed.mgz -r 1
|
||||
t moving.mgz fixed.mgz -Hm deform
|
||||
t moving.mgz fixed.mgz -Hm joint
|
4
src/conda-create.sh
Normal file
4
src/conda-create.sh
Normal file
|
@ -0,0 +1,4 @@
|
|||
conda deactivate
|
||||
conda create -y -n 25reg -c conda-forge simpleitk tensorflow-gpu
|
||||
conda activate 25reg
|
||||
pip install -r requirements.txt
|
376
src/g4synthmorph.py
Normal file
376
src/g4synthmorph.py
Normal file
|
@ -0,0 +1,376 @@
|
|||
|
||||
'''
|
||||
Use SynthMorph to register G4 images
|
||||
|
||||
https://download-directory.github.io/
|
||||
https://github.com/freesurfer/freesurfer/tree/dev/mri_synthmorph
|
||||
|
||||
|
||||
|
||||
XLA_FLAGS=--xla_gpu_cuda_data_dir=/home/xfr/.conda/envs/25reg time ./mri_synthmorph -m affine -o ../test.nii.gz -g '/mnt/1218/Public/dataset2/M6/ZYRGTRKJ/20230728/MR/3D_SAG_T1_MPRAGE_+C_MPR_Tra_20230728143005_14.nii.gz' '/mnt/1218/Public/dataset2/M6/ZYRGTRKJ/20230728/CT/1.1_CyberKnife_head(MAR)_20230728111920_3.nii.gz'
|
||||
|
||||
XLA_FLAGS=--xla_gpu_cuda_data_dir=/home/xfr/.conda/envs/25reg time mri_synthmorph/mri_synthmorph -m affine -o affine.nii.gz -g moving.nii.gz clipped.nii.gz
|
||||
|
||||
|
||||
'''
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
import argparse
|
||||
import logging
|
||||
import json
|
||||
import os
|
||||
# import pathlib
|
||||
import shelve
|
||||
import shutil
|
||||
import time
|
||||
|
||||
from skimage.metrics import normalized_mutual_information
|
||||
|
||||
import filelock
|
||||
import matplotlib.pyplot as plt
|
||||
import numpy as np
|
||||
import SimpleITK as sitk
|
||||
|
||||
from mri_synthmorph.synthmorph import registration
|
||||
# from synthmorph import registration
|
||||
|
||||
import surfa as sf
|
||||
|
||||
PATIENTS_ROOT = '/mnt/1220/Public/dataset2/G4'
|
||||
OUT_ROOT = '/mnt/1220/Public/dataset2/G4-synthmorph'
|
||||
SHELVE = os.path.join(OUT_ROOT, '0shelve')
|
||||
|
||||
MAX_Y = 256
|
||||
|
||||
SIZE_X = 249
|
||||
SIZE_Y = 249
|
||||
SIZE_Z = 192
|
||||
# SIZE_Z = 256
|
||||
|
||||
MIN_OVERLAP = 0.50
|
||||
MIN_METRIC = -0.50
|
||||
|
||||
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
format='%(asctime)s - %(levelname)s - %(message)s',
|
||||
handlers=[
|
||||
logging.StreamHandler(),
|
||||
logging.FileHandler('g4synthmorph.log')
|
||||
]
|
||||
)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# def resize_with_crop_or_pad(image, tx = SIZE_X, ty = SIZE_Y, tz = SIZE_Z):
|
||||
def resize_with_pad(image, tx = SIZE_X, ty = SIZE_Y, tz = SIZE_Z):
|
||||
sx, sy, sz = image.GetSize()
|
||||
l = [(tx-sx)//2,
|
||||
(ty-sy)//2,
|
||||
(tz-sz)//2,]
|
||||
u = [tx-sx-l[0],
|
||||
ty-sy-l[1],
|
||||
tz-sz-l[2],
|
||||
]
|
||||
# print (l, u)
|
||||
return sitk.ConstantPad(image, l, u)
|
||||
|
||||
|
||||
def draw_sitk(image, d, post):
|
||||
a = sitk.GetArrayFromImage(image)
|
||||
s = a.shape
|
||||
|
||||
fig, axs = plt.subplots(1, 3)
|
||||
# fig.suptitle('%dx%dx%d'%(s[2], s[1], s[0]))
|
||||
axs.flat[0].imshow(a[s[0]//2,:,:], cmap='gray')
|
||||
axs.flat[1].imshow(a[:,s[1]//2,:], cmap='gray')
|
||||
axs.flat[2].imshow(a[:,:,s[2]//2], cmap='gray')
|
||||
axs.flat[0].axis('off')
|
||||
axs.flat[1].axis('off')
|
||||
axs.flat[2].axis('off')
|
||||
axs.flat[1].invert_yaxis()
|
||||
axs.flat[2].invert_yaxis()
|
||||
plt.tight_layout()
|
||||
os.makedirs(d, exist_ok=True)
|
||||
plt.savefig(os.path.join(d, '%dx%dx%d-%s'%(s[2],s[1],s[0],post)))
|
||||
plt.close()
|
||||
# exit()
|
||||
|
||||
def bbox2_3D(img):
|
||||
|
||||
r = np.any(img, axis=(1, 2))
|
||||
c = np.any(img, axis=(0, 2))
|
||||
z = np.any(img, axis=(0, 1))
|
||||
|
||||
if not np.any(r):
|
||||
return -1, -1, -1, -1, -1, -1
|
||||
|
||||
rmin, rmax = np.where(r)[0][[0, -1]]
|
||||
cmin, cmax = np.where(c)[0][[0, -1]]
|
||||
zmin, zmax = np.where(z)[0][[0, -1]]
|
||||
|
||||
return rmin, rmax, cmin, cmax, zmin, zmax
|
||||
|
||||
'''
|
||||
Namespace(command='register', moving='/nn/7295866/20250127/nii/7_3D_SAG_T1_MPRAGE_+C_20250127132612_100.nii.gz', fixed='/123/onlylian/0/tmpgp96622o/clipped.nii.gz',
|
||||
model='joint', out_moving='/123/onlylian/0/tmpgp96622o/joint.nii.gz', out_fixed='/123/onlylian/0/tmpgp96622o/out_fixed-joint.nii.gz',
|
||||
header_only=False, trans='/123/onlylian/0/tmpgp96622o/moving_to_fixed-joint.nii.gz', inverse='/123/onlylian/0/tmpgp96622o/fixed_to_moving-joint.nii.gz',
|
||||
init=None, mid_space=False, threads=None, gpu=True, hyper=0.5, steps=7, extent=256, weights=None, verbose=False, out_dir=None)
|
||||
'''
|
||||
def register(ct0, ct1, moving, out_root):
|
||||
FREESURFER_HOME = '/mnt/1218/Public/packages/freesurfer-8.0.0-beta/'
|
||||
# out_root = Path(ct0).resolve().parent/os.path.basename(mr).replace('.nii.gz','')
|
||||
|
||||
# print(out_root)
|
||||
modality = os.path.basename(out_root)
|
||||
# exit()
|
||||
|
||||
out_root = Path(out_root)/os.path.basename(moving).replace('.nii.gz','')
|
||||
out_root.mkdir(exist_ok=True)
|
||||
|
||||
logger.info(' '.join((ct0, ct1, moving, str(out_root))))
|
||||
|
||||
orig = sf.load_volume(moving)
|
||||
if modality == 'CT':
|
||||
clipped = out_root/'clipped.nii.gz'
|
||||
|
||||
cl = orig.clip(0, 80)
|
||||
cl.save(clipped)
|
||||
|
||||
MODELS = [
|
||||
'rigid',
|
||||
# 'affine',
|
||||
# 'joint',
|
||||
]
|
||||
|
||||
else:
|
||||
clipped = moving
|
||||
MODELS = [
|
||||
'rigid',
|
||||
'affine',
|
||||
'joint',
|
||||
]
|
||||
|
||||
|
||||
|
||||
# exit()
|
||||
|
||||
|
||||
|
||||
default = {
|
||||
|
||||
'command': 'register',
|
||||
'header_only': False,
|
||||
'init': None,
|
||||
'mid_space': False,
|
||||
'threads': None,
|
||||
|
||||
# 'gpu': False,
|
||||
'gpu': True,
|
||||
|
||||
'verbose': False,
|
||||
# 'verbose': True,
|
||||
|
||||
'hyper': 0.5,
|
||||
'steps': 7,
|
||||
'extent': 256,
|
||||
|
||||
'weights': None,
|
||||
|
||||
# 'model': 'affine',
|
||||
|
||||
# 'out_dir': None,
|
||||
# 'out_fixed': 'out_fixed.nii.gz',
|
||||
# 'out_moving': 'out_moving.nii.gz',
|
||||
# 'trans': None,
|
||||
# 'inverse': None,
|
||||
|
||||
'out_fixed': None,
|
||||
'out_moving': None,
|
||||
'trans': None,
|
||||
'inverse': None,
|
||||
|
||||
'moving' : clipped,
|
||||
'fixed' : ct1,
|
||||
|
||||
# 'weights': str(Path(__file__).resolve().parent/'mri_synthmorph/models/synthmorph.affine.2.h5'),
|
||||
|
||||
}
|
||||
|
||||
os.environ["FREESURFER_HOME"] = FREESURFER_HOME
|
||||
os.environ["XLA_FLAGS"] = '--xla_gpu_cuda_data_dir=%s'% os.environ["CONDA_PREFIX"]
|
||||
|
||||
for m in MODELS:
|
||||
default['model'] = m
|
||||
default['out_dir'] = out_root/m
|
||||
|
||||
# if m in ('affine', 'rigid'):
|
||||
# default['trans'] = 'trans.lta'
|
||||
# default['inverse'] = 'inverse.lta'
|
||||
# else:
|
||||
# default['trans'] = 'trans.nii.gz'
|
||||
# default['inverse'] = 'inverse.nii.gz'
|
||||
|
||||
arg=argparse.Namespace(**default)
|
||||
|
||||
# CONDA_PREFIX=/home/xfr/.conda/envs/25reg
|
||||
# XLA_FLAGS=--xla_gpu_cuda_data_dir=/path/to/cuda
|
||||
|
||||
logger.info('registering %s'%m)
|
||||
registration.register(arg)
|
||||
logger.info('registered %s'%m)
|
||||
|
||||
if m in (
|
||||
'rigid',
|
||||
'affine',
|
||||
'joint',
|
||||
):
|
||||
# which = 'affine' if arg.trans.endswith('.lta') else 'warp'
|
||||
|
||||
out = out_root/('%s.nii.gz'%m)
|
||||
if m in ['affine', 'rigid']:
|
||||
trans = sf.load_affine(default['out_dir']/'tra_1.lta')
|
||||
prop = dict(method='linear', resample=True, fill=0)
|
||||
orig.transform(trans, **prop).save(out)
|
||||
logger.info('transformed %s'%out)
|
||||
else:
|
||||
# need to resample before transform in warp, too complicated, just copy it
|
||||
# trans1 = default['out_dir']/'tra_1.nii.gz'
|
||||
# trans = sf.load_warp(trans1)
|
||||
|
||||
shutil.copy(default['out_dir']/'out_1.nii.gz', out)
|
||||
logger.info('copied %s'% out)
|
||||
|
||||
with open(out_root/'metric.txt', 'w') as f_metrics:
|
||||
for m in MODELS:
|
||||
out1 = sf.load_volume(out_root/m/'out_1.nii.gz').data
|
||||
inp2 = sf.load_volume(out_root/m/'inp_2.nii.gz').data
|
||||
met = normalized_mutual_information(out1, inp2)
|
||||
f_metrics.write('%s\t%f\n'%(m, met))
|
||||
|
||||
return out_root
|
||||
|
||||
def check(epath):
|
||||
registered = 0
|
||||
for root, dirs, files in os.walk(epath):
|
||||
dirs.sort()
|
||||
|
||||
RT_DIR = os.path.join(root, 'RT')
|
||||
|
||||
ORGAN_DIR = os.path.join(RT_DIR, 'ORGAN')
|
||||
if not os.path.isdir(ORGAN_DIR):
|
||||
continue
|
||||
|
||||
# if there is no eye, it's no a brain image
|
||||
eye = None
|
||||
organs = sorted(os.scandir(ORGAN_DIR), key=lambda e: e.name)
|
||||
for o in organs:
|
||||
if 'eye' in o.name.lower():
|
||||
eye = o
|
||||
if eye is None:
|
||||
logger.info('no eye... skip ' + root)
|
||||
# exit()
|
||||
return None
|
||||
|
||||
ct_image = os.path.join(RT_DIR, 'ct_image.nii.gz')
|
||||
|
||||
|
||||
|
||||
outdir = os.path.join(OUT_ROOT, os.path.relpath(root, PATIENTS_ROOT))
|
||||
logger.info(outdir)
|
||||
|
||||
os.makedirs(outdir, exist_ok=True)
|
||||
# ct0_nii = os.path.join(outdir, 'ct0.nii.gz')
|
||||
ct1_nii = os.path.join(outdir, 'clipped.nii.gz')
|
||||
# shutil.copy(ct_image, ct0_nii)
|
||||
|
||||
ct = sf.load_volume(ct_image)
|
||||
clipped = ct.clip(0, 80)
|
||||
clipped.save(ct1_nii)
|
||||
|
||||
for root2, dirs2, files2 in os.walk(root):
|
||||
dirs2.sort()
|
||||
outdir = os.path.join(OUT_ROOT, os.path.relpath(root2, PATIENTS_ROOT))
|
||||
if root2.endswith('RT'):
|
||||
modality = 'RT'
|
||||
logger.info('copying %s %s' %(root2, outdir))
|
||||
shutil.copytree(root2, outdir)
|
||||
# exit()
|
||||
continue
|
||||
skip = (root2==root) or ('RT' in root2.split('/'))
|
||||
if skip:
|
||||
continue
|
||||
if root2.endswith('CT'):
|
||||
modality = 'CT'
|
||||
else:
|
||||
modality = 'other'
|
||||
logger.info(' '.join([str(skip), root2, modality]))
|
||||
outdir = os.path.join(OUT_ROOT, os.path.relpath(root2, PATIENTS_ROOT))
|
||||
os.makedirs(outdir, exist_ok=True)
|
||||
for e in sorted(os.scandir(root2), key=lambda e: e.name):
|
||||
if not e.name.endswith('.nii.gz'):
|
||||
continue
|
||||
if '_RTDOSE_' in e.name:
|
||||
continue
|
||||
if '_DTI_' in e.name:
|
||||
continue
|
||||
if '_ROI1.' in e.name:
|
||||
continue
|
||||
|
||||
OUT_IMG = os.path.join(outdir, e.name)
|
||||
if os.path.isfile(OUT_IMG):
|
||||
logger.info('skip '+ OUT_IMG)
|
||||
continue
|
||||
|
||||
logger.info(' '.join([e.name, e.path]))
|
||||
|
||||
moving = e.path
|
||||
register(ct_image, ct1_nii, moving, outdir)
|
||||
registered += 1
|
||||
# exit()
|
||||
|
||||
|
||||
|
||||
# exit()
|
||||
return registered
|
||||
|
||||
|
||||
|
||||
def main():
|
||||
|
||||
# check('/mnt/1220/Public/dataset2/G4/3L6LOEER') # bad registration
|
||||
# exit()
|
||||
|
||||
EXCLUDE = (
|
||||
# 'LLUQJUY4', #cervical
|
||||
)
|
||||
|
||||
os.makedirs(OUT_ROOT, exist_ok=True)
|
||||
|
||||
LOCK_DIR = os.path.join(OUT_ROOT, '0lock')
|
||||
os.makedirs(LOCK_DIR, exist_ok=True)
|
||||
for e in sorted(os.scandir(PATIENTS_ROOT), key=lambda e: e.name):
|
||||
if e.is_dir():
|
||||
d = shelve.open(SHELVE)
|
||||
if e.name in d or e.name in EXCLUDE:
|
||||
logger.info('skip '+ e.name)
|
||||
d.close()
|
||||
continue
|
||||
d.close()
|
||||
lock_path = os.path.join(LOCK_DIR, '%s.lock'%e.name)
|
||||
lock = filelock.FileLock(lock_path, timeout=1)
|
||||
try:
|
||||
lock.acquire()
|
||||
except:
|
||||
logger.info(lock_path + ' locked')
|
||||
continue
|
||||
ret = check(e.path)
|
||||
lock.release()
|
||||
# exit()
|
||||
d = shelve.open(SHELVE)
|
||||
d[e.name] = ret
|
||||
d.close()
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
1
src/mri_synthmorph
Symbolic link
1
src/mri_synthmorph
Symbolic link
|
@ -0,0 +1 @@
|
|||
../mri_synthmorph/
|
6
src/requirements.txt
Normal file
6
src/requirements.txt
Normal file
|
@ -0,0 +1,6 @@
|
|||
|
||||
filelock
|
||||
|
||||
git+https://github.com/adalca/neurite.git
|
||||
git+https://github.com/freesurfer/surfa.git
|
||||
git+https://github.com/voxelmorph/voxelmorph.git
|
1
src/synthmorph
Symbolic link
1
src/synthmorph
Symbolic link
|
@ -0,0 +1 @@
|
|||
../mri_synthmorph/synthmorph
|
59
src/synthmorph-ct.py
Normal file
59
src/synthmorph-ct.py
Normal file
|
@ -0,0 +1,59 @@
|
|||
'''
|
||||
conda create -n voxelmorph -c conda-forge python simpleitk tensorflow-gpu
|
||||
conda create -n voxelmorph -c conda-forge "python<3.11" simpleitk "tensorflow-gpu<2.16"
|
||||
conda activate voxelmorph
|
||||
pip install git+https://github.com/adalca/neurite.git git+https://github.com/freesurfer/surfa.git git+https://github.com/voxelmorph/voxelmorph.git
|
||||
|
||||
wget https://raw.githubusercontent.com/freesurfer/freesurfer/dev/mri_synthmorph/mri_synthmorph
|
||||
wget https://surfer.nmr.mgh.harvard.edu/docs/synthmorph/synthmorph.affine.2.h5
|
||||
|
||||
export FREESURFER_HOME=/home/xfr/git9/Taipei-1/trials
|
||||
time ./mri_synthmorph -m affine -t trans.lta '/mnt/1218/Public/dataset2/M6/ZYRGTRKJ/20230728/MR/3D_SAG_T1_MPRAGE_+C_MPR_Tra_20230728143005_14.nii.gz' clipped.nii.gz
|
||||
|
||||
|
||||
#### https://github.com/freesurfer/freesurfer/tree/dev/mri_synthmorph
|
||||
|
||||
conda create -n synthmorph -c conda-forge -c nvidia python=3.11 simpleitk tensorflow-gpu=2.17 cuda-nvcc
|
||||
conda create -y -n synthmorph -c conda-forge -c nvidia python simpleitk tensorflow-gpu cuda-nvcc
|
||||
conda activate synthmorph
|
||||
pip install git+https://github.com/adalca/neurite.git git+https://github.com/freesurfer/surfa.git git+https://github.com/voxelmorph/voxelmorph.git
|
||||
XLA_FLAGS=--xla_gpu_cuda_data_dir=/home/xfr/.conda/envs/synthmorph time ./mri_synthmorph -m affine -t trans.lta -o out-aff.nii.gz moving.nii.gz clipped.nii.gz -g
|
||||
XLA_FLAGS=--xla_gpu_cuda_data_dir=/home/xfr/.conda/envs/synthmorph time ./mri_synthmorph -o out.nii.gz moving.nii.gz clipped.nii.gz -g
|
||||
|
||||
export FREESURFER_HOME=/mnt/1218/Public/packages/freesurfer-7.4.1
|
||||
export FREESURFER_HOME=/mnt/1218/Public/packages/freesurfer-8.0.0-beta
|
||||
source $FREESURFER_HOME/SetUpFreeSurfer.sh
|
||||
time mri_synthmorph -m affine -t trans.lta -o out-aff.nii.gz moving.nii.gz clipped.nii.gz -g
|
||||
|
||||
time mri_easyreg --ref clipped.nii.gz --flo moving.nii.gz
|
||||
|
||||
|
||||
|
||||
./cuda_sdk_lib
|
||||
/home/conda/feedstock_root/build_artifacts/tensorflow-split_1729095706337/_build_env/targets/x86_64-linux
|
||||
/usr/local/cuda
|
||||
/home/xfr/.conda/envs/synthmorph/lib/python3.11/site-packages/tensorflow/python/platform/../../../nvidia/cuda_nvcc
|
||||
/home/xfr/.conda/envs/synthmorph/lib/python3.11/site-packages/tensorflow/python/platform/../../../../nvidia/cuda_nvcc
|
||||
|
||||
|
||||
Less than 10 seconds!!!
|
||||
'''
|
||||
|
||||
fi = '/nn/7295866/20250127/nii/a_1.1_CyberKnife_head(MAR)_20250127111447_5.nii.gz'
|
||||
mv = '/nn/7295866/20250127/nii/7_3D_SAG_T1_MPRAGE_+C_20250127132612_100.nii.gz'
|
||||
|
||||
fi = '/mnt/1218/Public/dataset2/M6/ZYRGTRKJ/20230728/CT/1.1_CyberKnife_head(MAR)_20230728111920_3.nii.gz'
|
||||
mv = '/mnt/1218/Public/dataset2/M6/ZYRGTRKJ/20230728/MR/3D_SAG_T1_MPRAGE_+C_MPR_Tra_20230728143005_14.nii.gz'
|
||||
|
||||
import shutil
|
||||
|
||||
import surfa as sf
|
||||
|
||||
|
||||
ct = sf.load_volume(fi)
|
||||
clipped = ct.clip(0, 80)
|
||||
clipped.save('../clipped.nii.gz')
|
||||
|
||||
shutil.copy(mv, '../moving.nii.gz')
|
||||
|
||||
|
Loading…
Reference in a new issue