237 lines
7.4 KiB
Python
Executable file
237 lines
7.4 KiB
Python
Executable file
#!/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))
|