tomio 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- tomio/__init__.py +21 -0
- tomio/__main__.py +5 -0
- tomio/cli.py +154 -0
- tomio/configfile.py +138 -0
- tomio/imagefile.py +141 -0
- tomio/interp.py +486 -0
- tomio/labelfile.py +488 -0
- tomio/volume.py +718 -0
- tomio-0.1.0.dist-info/METADATA +216 -0
- tomio-0.1.0.dist-info/RECORD +14 -0
- tomio-0.1.0.dist-info/WHEEL +5 -0
- tomio-0.1.0.dist-info/entry_points.txt +2 -0
- tomio-0.1.0.dist-info/licenses/LICENSE +21 -0
- tomio-0.1.0.dist-info/top_level.txt +1 -0
tomio/__init__.py
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
from . import volume, configfile, labelfile, interp
|
|
2
|
+
|
|
3
|
+
# backward compatible name
|
|
4
|
+
config = configfile
|
|
5
|
+
|
|
6
|
+
volread = volume.read
|
|
7
|
+
volwrite = volume.write
|
|
8
|
+
read_volume = volume.read
|
|
9
|
+
write_volume = volume.write
|
|
10
|
+
tovolume = volume.tovolume
|
|
11
|
+
asvolume = volume.asvolume
|
|
12
|
+
resample = volume.resample
|
|
13
|
+
Volume = volume.Volume
|
|
14
|
+
Geometry = volume.Geometry
|
|
15
|
+
|
|
16
|
+
read_config = configfile.read
|
|
17
|
+
write_config = configfile.write
|
|
18
|
+
|
|
19
|
+
read_labels = labelfile.read
|
|
20
|
+
write_labels = labelfile.write
|
|
21
|
+
|
tomio/__main__.py
ADDED
tomio/cli.py
ADDED
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
""" command-line interface
|
|
2
|
+
|
|
3
|
+
tomio convert IN OUT
|
|
4
|
+
tomio resample IN OUT (--like REF | --spacing SX SY SZ) [--method METHOD]
|
|
5
|
+
tomio info IN
|
|
6
|
+
"""
|
|
7
|
+
import sys
|
|
8
|
+
import argparse
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
import numpy as np
|
|
12
|
+
|
|
13
|
+
from . import volume, imagefile
|
|
14
|
+
|
|
15
|
+
METHODS = ["nearest", "linear", "spline3", "labels", "roi"]
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class UserError(Exception):
|
|
19
|
+
"""error reported to the user without traceback"""
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def main(argv=None):
|
|
23
|
+
"""run the command line, return the exit code"""
|
|
24
|
+
parser = get_parser()
|
|
25
|
+
try:
|
|
26
|
+
args = parser.parse_args(argv)
|
|
27
|
+
except SystemExit as exc:
|
|
28
|
+
# --help, or invalid arguments (argparse message already printed)
|
|
29
|
+
return exc.code
|
|
30
|
+
try:
|
|
31
|
+
args.func(args)
|
|
32
|
+
except (UserError, FileNotFoundError, ValueError, RuntimeError, ImportError, NotImplementedError) as exc:
|
|
33
|
+
print(f"tomio: error: {exc}", file=sys.stderr)
|
|
34
|
+
return 1
|
|
35
|
+
return 0
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def get_parser():
|
|
39
|
+
parser = argparse.ArgumentParser(prog="tomio", description="Convert, resample and inspect medical image volumes.")
|
|
40
|
+
commands = parser.add_subparsers(title="commands", required=True, metavar="COMMAND")
|
|
41
|
+
|
|
42
|
+
# convert
|
|
43
|
+
convert = commands.add_parser("convert", help="convert an image file to another format", description=(
|
|
44
|
+
"Convert an image file to another format (from the file extensions, e.g. .mha, .nii.gz, .nrrd). "
|
|
45
|
+
"Pixel data, geometry, metadata, vector and complex pixels are kept."))
|
|
46
|
+
convert.add_argument("input", help="input image file")
|
|
47
|
+
convert.add_argument("output", help="output image file")
|
|
48
|
+
add_output_options(convert)
|
|
49
|
+
convert.set_defaults(func=run_convert)
|
|
50
|
+
|
|
51
|
+
# resample
|
|
52
|
+
resample = commands.add_parser("resample", help="resample a volume to a new geometry", description=(
|
|
53
|
+
"Resample a volume to the geometry of another volume, or to a new spacing (same extent)."))
|
|
54
|
+
resample.add_argument("input", help="input image file")
|
|
55
|
+
resample.add_argument("output", help="output image file")
|
|
56
|
+
target = resample.add_mutually_exclusive_group(required=True)
|
|
57
|
+
target.add_argument("--like", metavar="REF", help="reference image file: use its geometry (header only)")
|
|
58
|
+
target.add_argument("--spacing", metavar=("SX", "SY", "SZ"), nargs=3, type=float, help="new spacing")
|
|
59
|
+
resample.add_argument("--method", choices=METHODS, help=(
|
|
60
|
+
"interpolation method; default: nearest for integer data, spline3 otherwise; "
|
|
61
|
+
"labels: label maps, label by label; roi: masks/ROIs"))
|
|
62
|
+
resample.add_argument("--fill", type=float, default=0.0, help="value outside the input volume (default: 0, may be nan)")
|
|
63
|
+
add_output_options(resample)
|
|
64
|
+
resample.set_defaults(func=run_resample)
|
|
65
|
+
|
|
66
|
+
# info
|
|
67
|
+
info = commands.add_parser("info", help="print the header of an image file", description=(
|
|
68
|
+
"Print shape, pixel type and geometry of an image file (the pixel data is not read)."))
|
|
69
|
+
info.add_argument("input", help="input image file")
|
|
70
|
+
info.add_argument("--metadata", action="store_true", help="also print the metadata")
|
|
71
|
+
info.set_defaults(func=run_info)
|
|
72
|
+
return parser
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def add_output_options(parser):
|
|
76
|
+
parser.add_argument("--dtype", help="cast the output pixel values (numpy dtype, e.g. float32, uint8)")
|
|
77
|
+
parser.add_argument("--compression-level", type=int, metavar="N", help="compression level (e.g. 1-9 for .nii.gz)")
|
|
78
|
+
parser.add_argument("-f", "--force", action="store_true", help="overwrite the output file if it exists")
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
# commands
|
|
82
|
+
|
|
83
|
+
def run_convert(args):
|
|
84
|
+
check_files(args)
|
|
85
|
+
imagefile.convert_image(args.input, args.output, dtype=get_dtype(args.dtype), compression_level=args.compression_level)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def run_resample(args):
|
|
89
|
+
check_files(args)
|
|
90
|
+
# complex data: 2-component vector image (as written by tomio)
|
|
91
|
+
as_complex = imagefile.read_info(args.input)["components"] == 2
|
|
92
|
+
vol = volume.read(args.input, as_complex=as_complex)
|
|
93
|
+
if args.like is not None:
|
|
94
|
+
check_input(args.like)
|
|
95
|
+
header = volume.read_header(args.like)
|
|
96
|
+
target = dict(like=volume.Geometry(shape=header["shape"][:3], spacing=header["spacing"], origin=header["origin"], transform=header["transform"]))
|
|
97
|
+
else:
|
|
98
|
+
target = dict(spacing=args.spacing)
|
|
99
|
+
kwargs = {} if args.method in ("labels", "roi") else dict(fill=args.fill)
|
|
100
|
+
out = volume.resample(vol, method=args.method, **target, **kwargs)
|
|
101
|
+
dtype = get_dtype(args.dtype)
|
|
102
|
+
if dtype is not None:
|
|
103
|
+
out = out.astype(dtype)
|
|
104
|
+
volume.write(args.output, out, compression_level=args.compression_level)
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def run_info(args):
|
|
108
|
+
check_input(args.input)
|
|
109
|
+
header = volume.read_header(args.input)
|
|
110
|
+
info = imagefile.read_info(args.input)
|
|
111
|
+
geometry = volume.Geometry(shape=header["shape"][:3], spacing=header["spacing"], origin=header["origin"], transform=header["transform"])
|
|
112
|
+
lines = [
|
|
113
|
+
("file", args.input),
|
|
114
|
+
("shape", " x ".join(str(n) for n in header["shape"])),
|
|
115
|
+
("pixel type", info["pixel_type"]),
|
|
116
|
+
("spacing", format_values(geometry.spacing)),
|
|
117
|
+
("origin", format_values(geometry.origin)),
|
|
118
|
+
("orientation", f'{volume.get_orientation(geometry)} ("from" convention), {volume.get_orientation(geometry, convention="to")} ("to" convention)'),
|
|
119
|
+
("directions", "; ".join(format_values(vec) for vec in geometry.transform)),
|
|
120
|
+
("extent", format_values(geometry.extent)),
|
|
121
|
+
("metadata", f"{len(header['metadata'])} keys"),
|
|
122
|
+
]
|
|
123
|
+
width = max(len(name) for name, _ in lines)
|
|
124
|
+
for name, value in lines:
|
|
125
|
+
print(f"{name:<{width}} {value}")
|
|
126
|
+
if args.metadata:
|
|
127
|
+
for key, value in sorted(header["metadata"].items()):
|
|
128
|
+
print(f" {key} = {value}")
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
# utilities
|
|
132
|
+
|
|
133
|
+
def check_input(filename):
|
|
134
|
+
if not Path(filename).is_file():
|
|
135
|
+
raise UserError(f"input file not found: {filename}")
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def check_files(args):
|
|
139
|
+
check_input(args.input)
|
|
140
|
+
if Path(args.output).exists() and not args.force:
|
|
141
|
+
raise UserError(f"output file exists (use --force to overwrite): {args.output}")
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def get_dtype(name):
|
|
145
|
+
if name is None:
|
|
146
|
+
return None
|
|
147
|
+
try:
|
|
148
|
+
return np.dtype(name)
|
|
149
|
+
except TypeError:
|
|
150
|
+
raise UserError(f"invalid dtype: {name}") from None
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def format_values(values):
|
|
154
|
+
return "(" + ", ".join(f"{v:.6g}" for v in values) + ")"
|
tomio/configfile.py
ADDED
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
# coding=utf-8
|
|
2
|
+
""" config file i/o (JSON, YAML; TOML: read only) """
|
|
3
|
+
import re
|
|
4
|
+
import json
|
|
5
|
+
import math
|
|
6
|
+
import numbers
|
|
7
|
+
import yaml
|
|
8
|
+
from collections import abc
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
import numpy as np
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
KINDS = {"json": "json", "yaml": "yaml", "yml": "yaml", "toml": "toml"}
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def get_kind(path):
|
|
17
|
+
"""config file type ("json", "yaml" or "toml") from path suffix"""
|
|
18
|
+
ext = path.suffix[1:].lower()
|
|
19
|
+
if ext not in KINDS:
|
|
20
|
+
raise ValueError(f"Unknown config file type: {path}")
|
|
21
|
+
return KINDS[ext]
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def read(path):
|
|
25
|
+
"""read config file (.json, .yaml, .yml or .toml; no suffix: .json)"""
|
|
26
|
+
path = Path(path)
|
|
27
|
+
if not path.suffix:
|
|
28
|
+
path = path.with_suffix(".json")
|
|
29
|
+
kind = get_kind(path)
|
|
30
|
+
|
|
31
|
+
if kind == "toml":
|
|
32
|
+
tomllib = import_tomllib()
|
|
33
|
+
with open(path, "rb") as fp:
|
|
34
|
+
return untame(tomllib.load(fp))
|
|
35
|
+
|
|
36
|
+
with open(path, "r", encoding="utf-8") as fp:
|
|
37
|
+
if kind == "json":
|
|
38
|
+
return untame(json.load(fp))
|
|
39
|
+
else:
|
|
40
|
+
# safe: no Python-specific tags
|
|
41
|
+
return untame(yaml.safe_load(fp))
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def import_tomllib():
|
|
45
|
+
"""TOML parser: tomllib (Python >= 3.11), or tomli (same API)"""
|
|
46
|
+
try:
|
|
47
|
+
import tomllib
|
|
48
|
+
except ImportError:
|
|
49
|
+
try:
|
|
50
|
+
import tomli as tomllib
|
|
51
|
+
except ImportError:
|
|
52
|
+
raise ImportError("Reading TOML files requires Python >= 3.11, or `tomli` (pip install tomli)") from None
|
|
53
|
+
return tomllib
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def write(path, config, kind="json"):
|
|
57
|
+
"""write config file
|
|
58
|
+
|
|
59
|
+
The file type is set by the path suffix (.json, .yaml or .yml; .toml is read-only).
|
|
60
|
+
If the path has no suffix, `kind` is used and added as suffix.
|
|
61
|
+
"""
|
|
62
|
+
path = Path(path)
|
|
63
|
+
if not path.suffix:
|
|
64
|
+
path = path.with_suffix(f".{kind}")
|
|
65
|
+
kind = get_kind(path)
|
|
66
|
+
if kind == "toml":
|
|
67
|
+
raise ValueError(f"Writing TOML files is not supported (read-only): {path}")
|
|
68
|
+
config = tame(config)
|
|
69
|
+
|
|
70
|
+
with open(path, "w", encoding="utf-8") as fp:
|
|
71
|
+
if kind == "json":
|
|
72
|
+
json.dump(config, fp)
|
|
73
|
+
else:
|
|
74
|
+
yaml.safe_dump(config, fp, default_flow_style=False)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def tame(element):
|
|
78
|
+
"""convert config elements to standard types
|
|
79
|
+
|
|
80
|
+
dict, list, str, None, bool, int, float
|
|
81
|
+
numpy arrays become lists, complex numbers become strings: "3 + 4i"
|
|
82
|
+
"""
|
|
83
|
+
if element is None or isinstance(element, str):
|
|
84
|
+
return element
|
|
85
|
+
elif isinstance(element, (bool, np.bool_)): # before int: bool is an int
|
|
86
|
+
return bool(element)
|
|
87
|
+
elif isinstance(element, numbers.Integral):
|
|
88
|
+
return int(element)
|
|
89
|
+
elif isinstance(element, numbers.Real):
|
|
90
|
+
return float(element)
|
|
91
|
+
elif isinstance(element, numbers.Complex):
|
|
92
|
+
return format_complex(element)
|
|
93
|
+
elif isinstance(element, np.ndarray):
|
|
94
|
+
# tolist: nested lists, or a scalar for 0-d arrays
|
|
95
|
+
return tame(element.tolist())
|
|
96
|
+
elif isinstance(element, abc.Mapping):
|
|
97
|
+
return {tame(key): tame(value) for key, value in element.items()}
|
|
98
|
+
elif isinstance(element, abc.Sequence):
|
|
99
|
+
return [tame(item) for item in element]
|
|
100
|
+
else:
|
|
101
|
+
return str(element)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def untame(element):
|
|
105
|
+
"""reverse of tame: convert complex number strings ("3 + 4i") to complex"""
|
|
106
|
+
if isinstance(element, str):
|
|
107
|
+
return parse_complex(element, default=element)
|
|
108
|
+
elif isinstance(element, dict):
|
|
109
|
+
return {key: untame(value) for key, value in element.items()}
|
|
110
|
+
elif isinstance(element, list):
|
|
111
|
+
return [untame(item) for item in element]
|
|
112
|
+
return element
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
# complex numbers
|
|
116
|
+
|
|
117
|
+
_NUMBER = r"(?:(?:\d+\.?\d*|\.\d+)(?:[eE][-+]?\d+)?|inf|nan)"
|
|
118
|
+
_RE_COMPLEX = re.compile(rf"^\s*([-+]?{_NUMBER})\s*([-+])\s*({_NUMBER})i\s*$")
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def format_complex(value):
|
|
122
|
+
"""format complex number as "3 + 4i" """
|
|
123
|
+
value = complex(value)
|
|
124
|
+
|
|
125
|
+
def fmt(x):
|
|
126
|
+
return str(int(x)) if x.is_integer() else repr(x)
|
|
127
|
+
|
|
128
|
+
sign = "-" if math.copysign(1, value.imag) < 0 else "+"
|
|
129
|
+
return f"{fmt(value.real)} {sign} {fmt(abs(value.imag))}i"
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def parse_complex(string, default=None):
|
|
133
|
+
"""parse complex number formatted as "3 + 4i" (return `default` if not one)"""
|
|
134
|
+
match = _RE_COMPLEX.match(string)
|
|
135
|
+
if not match:
|
|
136
|
+
return default
|
|
137
|
+
real, sign, imag = match.groups()
|
|
138
|
+
return complex(float(real), float(sign + imag))
|
tomio/imagefile.py
ADDED
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
""" image file i/o with SimpleITK
|
|
2
|
+
|
|
3
|
+
arrays are in ITK index order (x, y, z, ...); vector images have the components in the last axis
|
|
4
|
+
"""
|
|
5
|
+
import SimpleITK as sitk
|
|
6
|
+
import numpy as np
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def write_image(filename, array, *, use_compression=True, compression_level=None, **kwargs):
|
|
10
|
+
"""write array (x, y, z, ...) to image file
|
|
11
|
+
|
|
12
|
+
kwargs: spacing, origin, transform, metadata, is_vector (see `to_image`)
|
|
13
|
+
compression_level: if set (format-dependent, e.g. 1-9 for gzip), implies use_compression
|
|
14
|
+
"""
|
|
15
|
+
im = to_image(array, **kwargs)
|
|
16
|
+
if compression_level is None:
|
|
17
|
+
sitk.WriteImage(im, str(filename), useCompression=use_compression)
|
|
18
|
+
else:
|
|
19
|
+
sitk.WriteImage(im, str(filename), useCompression=True, compressionLevel=compression_level)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def to_image(array, *, spacing=None, origin=None, transform=None, metadata=None, is_vector=False):
|
|
23
|
+
"""convert array (x, y, z, ...) to sitk Image
|
|
24
|
+
|
|
25
|
+
is_vector: the last axis of `array` contains the components of a vector image
|
|
26
|
+
spacing, origin, transform: 3d geometry (identity for the extra dimensions)
|
|
27
|
+
"""
|
|
28
|
+
array = np.asarray(array)
|
|
29
|
+
if np.issubdtype(array.dtype, np.bool_):
|
|
30
|
+
array = array.astype('uint8')
|
|
31
|
+
if is_vector:
|
|
32
|
+
# numpy order: reversed image axes, then components
|
|
33
|
+
n = array.ndim - 1
|
|
34
|
+
im = sitk.GetImageFromArray(np.transpose(array, (*reversed(range(n)), n)), isVector=True)
|
|
35
|
+
else:
|
|
36
|
+
# isVector=False: otherwise SimpleITK may guess a vector image from the shape
|
|
37
|
+
im = sitk.GetImageFromArray(array.T, isVector=False)
|
|
38
|
+
ndim = im.GetDimension()
|
|
39
|
+
if spacing is not None:
|
|
40
|
+
im.SetSpacing(pad(spacing, ndim, 1))
|
|
41
|
+
if origin is not None:
|
|
42
|
+
im.SetOrigin(pad(origin, ndim, 0))
|
|
43
|
+
if transform is not None:
|
|
44
|
+
# transform: columns of the direction matrix D
|
|
45
|
+
direction = np.eye(max(ndim, 3))
|
|
46
|
+
direction[:3, :3] = np.asarray(transform).reshape(3, 3).T
|
|
47
|
+
im.SetDirection(direction[:ndim, :ndim].ravel())
|
|
48
|
+
if metadata is not None:
|
|
49
|
+
for key, value in metadata.items():
|
|
50
|
+
im.SetMetaData(str(key), str(value))
|
|
51
|
+
return im
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def read_image(filename):
|
|
55
|
+
"""read image file, return array (x, y, z, ...) and header (3d geometry and metadata)
|
|
56
|
+
|
|
57
|
+
vector images: components are in the last axis
|
|
58
|
+
"""
|
|
59
|
+
return from_image(sitk.ReadImage(str(filename)))
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def from_image(im):
|
|
63
|
+
"""convert sitk Image to array (x, y, z, ...) and header (3d geometry and metadata)
|
|
64
|
+
|
|
65
|
+
vector images: components are in the last axis
|
|
66
|
+
"""
|
|
67
|
+
ndim = im.GetDimension()
|
|
68
|
+
|
|
69
|
+
array = sitk.GetArrayFromImage(im)
|
|
70
|
+
if array.ndim > ndim:
|
|
71
|
+
# vector image: (reversed image axes, components)
|
|
72
|
+
array = np.transpose(array, (*reversed(range(ndim)), ndim))
|
|
73
|
+
else:
|
|
74
|
+
array = array.T
|
|
75
|
+
|
|
76
|
+
return array, get_header(im)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def read_header(filename):
|
|
80
|
+
"""read image file header only (no pixel data)
|
|
81
|
+
|
|
82
|
+
returns: shape (x, y, z, ..., [components]), 3d geometry and metadata
|
|
83
|
+
"""
|
|
84
|
+
reader = sitk.ImageFileReader()
|
|
85
|
+
reader.SetFileName(str(filename))
|
|
86
|
+
reader.ReadImageInformation()
|
|
87
|
+
|
|
88
|
+
shape = tuple(reader.GetSize())
|
|
89
|
+
if reader.GetNumberOfComponents() > 1:
|
|
90
|
+
shape += (reader.GetNumberOfComponents(),)
|
|
91
|
+
return dict(shape=shape, **get_header(reader))
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def convert_image(source, target, *, dtype=None, use_compression=True, compression_level=None):
|
|
95
|
+
"""convert image file to another format (from the file extensions)
|
|
96
|
+
|
|
97
|
+
pixel data, geometry, metadata, vector and complex pixels are kept
|
|
98
|
+
dtype: cast pixel values (numpy dtype)
|
|
99
|
+
"""
|
|
100
|
+
im = sitk.ReadImage(str(source))
|
|
101
|
+
if dtype is not None:
|
|
102
|
+
array = sitk.GetArrayFromImage(im).astype(dtype)
|
|
103
|
+
is_vector = im.GetNumberOfComponentsPerPixel() > 1 and array.ndim > im.GetDimension()
|
|
104
|
+
cast = sitk.GetImageFromArray(array, isVector=is_vector)
|
|
105
|
+
cast.CopyInformation(im)
|
|
106
|
+
for key in im.GetMetaDataKeys():
|
|
107
|
+
cast.SetMetaData(key, im.GetMetaData(key))
|
|
108
|
+
im = cast
|
|
109
|
+
if compression_level is None:
|
|
110
|
+
sitk.WriteImage(im, str(target), useCompression=use_compression)
|
|
111
|
+
else:
|
|
112
|
+
sitk.WriteImage(im, str(target), useCompression=True, compressionLevel=compression_level)
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def read_info(filename):
|
|
116
|
+
"""pixel type (e.g. "32-bit float"), number of components and image dimension (header only)"""
|
|
117
|
+
reader = sitk.ImageFileReader()
|
|
118
|
+
reader.SetFileName(str(filename))
|
|
119
|
+
reader.ReadImageInformation()
|
|
120
|
+
return dict(
|
|
121
|
+
pixel_type=sitk.GetPixelIDValueAsString(reader.GetPixelID()),
|
|
122
|
+
components=reader.GetNumberOfComponents(),
|
|
123
|
+
dimension=reader.GetDimension(),
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def get_header(src):
|
|
128
|
+
"""3d geometry and metadata of a sitk Image or ImageFileReader"""
|
|
129
|
+
ndim = src.GetDimension()
|
|
130
|
+
spacing = src.GetSpacing()[:3]
|
|
131
|
+
origin = src.GetOrigin()[:3]
|
|
132
|
+
# GetDirection: direction matrix D, row-major; transform: columns of D
|
|
133
|
+
transform = np.reshape(src.GetDirection(), (ndim, ndim))[:3, :3].T
|
|
134
|
+
metadata = {key: src.GetMetaData(key) for key in src.GetMetaDataKeys()}
|
|
135
|
+
return dict(spacing=spacing, origin=origin, transform=transform, metadata=metadata)
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def pad(values, ndim, fill):
|
|
139
|
+
"""pad 3d values for images with more than 3 dimensions"""
|
|
140
|
+
values = list(values)
|
|
141
|
+
return values + [fill] * (ndim - len(values))
|