tomio 0.1.0__tar.gz

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-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Pierre-Yves Baudin
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
tomio-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,216 @@
1
+ Metadata-Version: 2.4
2
+ Name: tomio
3
+ Version: 0.1.0
4
+ Summary: I/O utilities for medical images: image volumes with geometry, interpolation, label and config files
5
+ Author: Pierre-Yves Baudin
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/py-baudin/tomio
8
+ Project-URL: Repository, https://github.com/py-baudin/tomio
9
+ Project-URL: Issues, https://github.com/py-baudin/tomio/issues
10
+ Keywords: medical imaging,nifti,itk,simpleitk,volume,interpolation,itk-snap,labels
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: Intended Audience :: Science/Research
13
+ Classifier: Operating System :: OS Independent
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3 :: Only
16
+ Classifier: Programming Language :: Python :: 3.10
17
+ Classifier: Programming Language :: Python :: 3.11
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Classifier: Programming Language :: Python :: 3.13
20
+ Classifier: Topic :: Scientific/Engineering :: Medical Science Apps.
21
+ Classifier: Topic :: Scientific/Engineering :: Image Processing
22
+ Requires-Python: >=3.10
23
+ Description-Content-Type: text/markdown
24
+ License-File: LICENSE
25
+ Requires-Dist: numpy>=1.24
26
+ Requires-Dist: pyyaml
27
+ Requires-Dist: SimpleITK
28
+ Provides-Extra: interp
29
+ Requires-Dist: scipy; extra == "interp"
30
+ Provides-Extra: nibabel
31
+ Requires-Dist: nibabel; extra == "nibabel"
32
+ Provides-Extra: test
33
+ Requires-Dist: pytest; extra == "test"
34
+ Requires-Dist: scipy; extra == "test"
35
+ Requires-Dist: nibabel; extra == "test"
36
+ Dynamic: license-file
37
+
38
+ # tomio
39
+
40
+ I/O utilities for medical images: image volumes (thin wrapper over SimpleITK), interpolation, label files (ITK-SNAP, FreeSurfer, 3D Slicer) and config files (JSON, YAML, TOML).
41
+
42
+ ## Installation
43
+
44
+ ```bash
45
+ pip install tomio
46
+ pip install tomio[interp] # optional: scipy interpolation backend
47
+ pip install tomio[nibabel] # optional: conversion to/from nibabel images
48
+ ```
49
+
50
+ Requires Python >= 3.10. Reading TOML files on Python 3.10 requires `tomli`.
51
+
52
+ ## Conventions
53
+
54
+ - Arrays are indexed `(x, y, z, ...)`, in ITK index order (not the `(z, y, x)` order of `sitk.GetArrayFromImage`).
55
+ - World coordinates are LPS (ITK, DICOM). Conversion to nibabel (RAS) is done by `to_nibabel`/`from_nibabel`.
56
+ - `vol.transform` is a tuple of axis directions: `transform[i]` is the world direction of array axis `i`, i.e. column `i` of the ITK direction matrix.
57
+ - Orientation codes use the ITK "from" convention by default (identity: `RAI`); the "to" convention (identity: `LPS`) is available with `convention="to"`.
58
+
59
+ ## Volumes
60
+
61
+ ```python
62
+ import numpy as np
63
+ from tomio import volume
64
+
65
+ # read / write (formats: .nii, .nii.gz, .mha, .mhd, .nrrd...)
66
+ vol = volume.read("vol.nii.gz")
67
+ volume.write("vol2.mha", vol)
68
+
69
+ # options
70
+ vol = volume.read("vol.nii.gz", dtype=np.float32) # cast
71
+ vol = volume.read("complex.mha", as_complex=True) # complex volume (written by volume.write)
72
+ volume.write("vol2.nii.gz", vol, compression_level=9)
73
+ header = volume.read_header("vol.nii.gz") # shape, spacing, origin, transform, metadata (no pixel data)
74
+
75
+ # Volume is a numpy ndarray
76
+ isinstance(vol, np.ndarray) # True
77
+ vol.A # plain np.ndarray
78
+
79
+ # header
80
+ vol.spacing # (sx, sy, sz)
81
+ vol.origin # world coordinates of voxel (0, 0, 0)
82
+ vol.transform # axis directions
83
+ vol.metadata # dict[str, str], written to the file
84
+ vol.header # dict of spacing, origin, transform and metadata
85
+ vol.affine # 4x4 affine transform (indices -> LPS coordinates)
86
+
87
+ # world coordinates <-> indices
88
+ coords = vol.get_coords([1, 2, 3])
89
+ indices = vol.get_indices(coords)
90
+
91
+ # create volumes
92
+ vol2 = volume.Volume(np.zeros((10, 20, 30)), spacing=(1, 1, 2), origin=(0, 0, 0))
93
+ vol2 = volume.asvolume(array, ref=vol) # copy the header of `vol`
94
+ vol2 = volume.tovolume(values, mask=mask, ref=vol) # fill the voxels of a mask
95
+ ```
96
+
97
+ Volumes with more than 3 dimensions (e.g. time series `(x, y, z, t)`) and complex volumes can be read and written; the geometry applies to the first 3 axes.
98
+
99
+ ### Geometry
100
+
101
+ ```python
102
+ geom = vol.geometry # volume.Geometry: shape, spacing, origin, transform (immutable)
103
+ geom.affine, geom.extent, geom.center, geom.voxel_volume
104
+ geom.same_as(other_vol, tol=1e-4)
105
+ vol2 = volume.Volume(array, geometry=geom)
106
+ ```
107
+
108
+ ### Slicing and numpy operations
109
+
110
+ The geometry follows numpy operations when it makes sense:
111
+
112
+ ```python
113
+ vol[2:].origin # world coordinates of the first voxel of the slice
114
+ vol[::2].spacing # spacing x 2 in the first axis
115
+ vol[::-1] # axis direction flipped (also np.flip)
116
+ vol.T, np.moveaxis(vol, 0, -1) # spacing and directions permuted
117
+ vol + 1, np.sqrt(vol) # same geometry
118
+
119
+ # no geometry: plain np.ndarray
120
+ vol[0], vol[mask], vol.sum(axis=0), vol.mean()
121
+ # time series: geometry kept
122
+ vol4d[..., 0], vol4d.mean(axis=3)
123
+ ```
124
+
125
+ ### Orientation
126
+
127
+ ```python
128
+ vol.anatomical_orientation # e.g. "RAI" (ITK "from" convention)
129
+ vol.get_orientation(convention="to") # e.g. "LPS"
130
+ vol2 = vol.reorient("RAS", convention="to") # flip/permute the axes
131
+ ```
132
+
133
+ ### Interoperability
134
+
135
+ ```python
136
+ image = vol.to_sitk() # SimpleITK Image
137
+ vol = volume.Volume.from_sitk(image)
138
+ nifti = vol.to_nibabel() # nibabel Nifti1Image (requires nibabel)
139
+ vol = volume.Volume.from_nibabel(nifti)
140
+ ```
141
+
142
+ ## Interpolation
143
+
144
+ ```python
145
+ import numpy as np
146
+ from tomio import volume
147
+
148
+ vol2 = volume.resample(vol, like=ref) # geometry of another volume (or a Geometry)
149
+ vol2 = volume.resample(vol, spacing=(1, 1, 1)) # new spacing, same extent
150
+ vol2 = volume.resample(vol, geometry={"spacing": (1, 1, 1), "shape": (64, 64, 64)})
151
+ vol2 = volume.resample(vol, like=ref, method="linear", fill=np.nan) # value outside `vol`
152
+
153
+ # label maps and masks
154
+ labels2 = volume.resample(labelmap, like=ref, method="labels") # label by label (smooth boundaries)
155
+ roi2 = volume.resample(roi, like=ref, method="roi") # no label lost when downsampling
156
+ ```
157
+
158
+ - Methods: `nearest`, `linear`, `spline3` (and `splineN`), `labels`, `roi`. Default: `nearest` for integer and bool data (label maps), `spline3` otherwise. The output has the dtype of the input.
159
+ - Backends: SimpleITK (default, multithreaded) or scipy (`backend="scipy"`, requires `tomio[interp]`). SimpleITK considers voxels inside the source up to half a voxel beyond the border voxel centers.
160
+ - Lower-level functions: `interp.interpolate`, `interpolate_like`, `interpolate_to`, `interpolate_roi`, `interpolate_labels`.
161
+
162
+ ## Label files
163
+
164
+ ```python
165
+ from tomio import labelfile
166
+
167
+ labels = labelfile.read("labels.txt") # ITK-SNAP label description file
168
+ labels = labelfile.read("FreeSurferColorLUT.txt", format="freesurfer")
169
+ labels = labelfile.read("labels.ctbl", format="slicer") # 3D Slicer color table
170
+ labelfile.write("labels.txt", labels) # or labels.save("labels.txt"), ITK-SNAP format
171
+
172
+ # Labels: mapping {index: description}, sorted by index
173
+ labels = labelfile.Labels({1: "white matter", 2: "grey matter"}) # automatic colors
174
+ labels = labelfile.Labels({1: {"LABEL": "white matter", "RGBA": (255, 255, 255, 1.0)}})
175
+ labels = labelfile.Labels(["background", "white matter"]) # indices 0, 1
176
+
177
+ labels[1] # description
178
+ labels[3] = "csf" # add or rename (keeps the color)
179
+ del labels[3]
180
+ labels.getindex("grey matter") # index of a description
181
+ labels.indices, labels.descriptions # lists, sorted by index (can be assigned)
182
+ labels.colors # {index: (R, G, B, A)}, RGB in 0..255, alpha in 0..1 (can be assigned)
183
+
184
+ # label maps
185
+ labels = labelfile.Labels.from_volume(labelmap) # one label per value
186
+ rgba = labels.lut[labelmap] # RGBA image (uint8)
187
+ labels2, mapping = labels.reindex() # indices 1..N
188
+ labelmap2 = labelfile.apply_mapping(labelmap, mapping)
189
+ ```
190
+
191
+ Labels are validated: non-negative integer indices, unique descriptions without `"` or line breaks (`allow_duplicates=True` to allow duplicates), RGB in 0..255, alpha in 0..1. Label files are UTF-8.
192
+
193
+ ## Config files
194
+
195
+ ```python
196
+ from tomio import configfile
197
+
198
+ cfg = configfile.read("config.yml") # .json, .yaml, .yml, .toml -> dict
199
+ configfile.write("config.yml", cfg) # .json, .yaml, .yml
200
+ ```
201
+
202
+ - The format is set by the file extension (no extension: `.json`). TOML files are read-only.
203
+ - Values are written as standard types: numpy arrays as lists, numpy scalars as numbers, complex numbers as strings `"3 + 4i"` (read back as complex numbers).
204
+ - YAML files are loaded safely (no Python-specific tags).
205
+
206
+ ## Command line
207
+
208
+ ```bash
209
+ tomio convert vol.mha vol.nii.gz # convert (format from the extensions)
210
+ tomio convert vol.mha vol.nii.gz --dtype float32 --compression-level 9
211
+ tomio resample vol.nii.gz out.nii.gz --spacing 1 1 1 # new spacing
212
+ tomio resample labels.nii.gz out.nii.gz --like ref.nii.gz --method labels
213
+ tomio info vol.nii.gz --metadata # header: shape, pixel type, geometry
214
+ ```
215
+
216
+ Existing output files are not overwritten unless `--force` is given. See `tomio COMMAND --help`.
tomio-0.1.0/README.md ADDED
@@ -0,0 +1,179 @@
1
+ # tomio
2
+
3
+ I/O utilities for medical images: image volumes (thin wrapper over SimpleITK), interpolation, label files (ITK-SNAP, FreeSurfer, 3D Slicer) and config files (JSON, YAML, TOML).
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ pip install tomio
9
+ pip install tomio[interp] # optional: scipy interpolation backend
10
+ pip install tomio[nibabel] # optional: conversion to/from nibabel images
11
+ ```
12
+
13
+ Requires Python >= 3.10. Reading TOML files on Python 3.10 requires `tomli`.
14
+
15
+ ## Conventions
16
+
17
+ - Arrays are indexed `(x, y, z, ...)`, in ITK index order (not the `(z, y, x)` order of `sitk.GetArrayFromImage`).
18
+ - World coordinates are LPS (ITK, DICOM). Conversion to nibabel (RAS) is done by `to_nibabel`/`from_nibabel`.
19
+ - `vol.transform` is a tuple of axis directions: `transform[i]` is the world direction of array axis `i`, i.e. column `i` of the ITK direction matrix.
20
+ - Orientation codes use the ITK "from" convention by default (identity: `RAI`); the "to" convention (identity: `LPS`) is available with `convention="to"`.
21
+
22
+ ## Volumes
23
+
24
+ ```python
25
+ import numpy as np
26
+ from tomio import volume
27
+
28
+ # read / write (formats: .nii, .nii.gz, .mha, .mhd, .nrrd...)
29
+ vol = volume.read("vol.nii.gz")
30
+ volume.write("vol2.mha", vol)
31
+
32
+ # options
33
+ vol = volume.read("vol.nii.gz", dtype=np.float32) # cast
34
+ vol = volume.read("complex.mha", as_complex=True) # complex volume (written by volume.write)
35
+ volume.write("vol2.nii.gz", vol, compression_level=9)
36
+ header = volume.read_header("vol.nii.gz") # shape, spacing, origin, transform, metadata (no pixel data)
37
+
38
+ # Volume is a numpy ndarray
39
+ isinstance(vol, np.ndarray) # True
40
+ vol.A # plain np.ndarray
41
+
42
+ # header
43
+ vol.spacing # (sx, sy, sz)
44
+ vol.origin # world coordinates of voxel (0, 0, 0)
45
+ vol.transform # axis directions
46
+ vol.metadata # dict[str, str], written to the file
47
+ vol.header # dict of spacing, origin, transform and metadata
48
+ vol.affine # 4x4 affine transform (indices -> LPS coordinates)
49
+
50
+ # world coordinates <-> indices
51
+ coords = vol.get_coords([1, 2, 3])
52
+ indices = vol.get_indices(coords)
53
+
54
+ # create volumes
55
+ vol2 = volume.Volume(np.zeros((10, 20, 30)), spacing=(1, 1, 2), origin=(0, 0, 0))
56
+ vol2 = volume.asvolume(array, ref=vol) # copy the header of `vol`
57
+ vol2 = volume.tovolume(values, mask=mask, ref=vol) # fill the voxels of a mask
58
+ ```
59
+
60
+ Volumes with more than 3 dimensions (e.g. time series `(x, y, z, t)`) and complex volumes can be read and written; the geometry applies to the first 3 axes.
61
+
62
+ ### Geometry
63
+
64
+ ```python
65
+ geom = vol.geometry # volume.Geometry: shape, spacing, origin, transform (immutable)
66
+ geom.affine, geom.extent, geom.center, geom.voxel_volume
67
+ geom.same_as(other_vol, tol=1e-4)
68
+ vol2 = volume.Volume(array, geometry=geom)
69
+ ```
70
+
71
+ ### Slicing and numpy operations
72
+
73
+ The geometry follows numpy operations when it makes sense:
74
+
75
+ ```python
76
+ vol[2:].origin # world coordinates of the first voxel of the slice
77
+ vol[::2].spacing # spacing x 2 in the first axis
78
+ vol[::-1] # axis direction flipped (also np.flip)
79
+ vol.T, np.moveaxis(vol, 0, -1) # spacing and directions permuted
80
+ vol + 1, np.sqrt(vol) # same geometry
81
+
82
+ # no geometry: plain np.ndarray
83
+ vol[0], vol[mask], vol.sum(axis=0), vol.mean()
84
+ # time series: geometry kept
85
+ vol4d[..., 0], vol4d.mean(axis=3)
86
+ ```
87
+
88
+ ### Orientation
89
+
90
+ ```python
91
+ vol.anatomical_orientation # e.g. "RAI" (ITK "from" convention)
92
+ vol.get_orientation(convention="to") # e.g. "LPS"
93
+ vol2 = vol.reorient("RAS", convention="to") # flip/permute the axes
94
+ ```
95
+
96
+ ### Interoperability
97
+
98
+ ```python
99
+ image = vol.to_sitk() # SimpleITK Image
100
+ vol = volume.Volume.from_sitk(image)
101
+ nifti = vol.to_nibabel() # nibabel Nifti1Image (requires nibabel)
102
+ vol = volume.Volume.from_nibabel(nifti)
103
+ ```
104
+
105
+ ## Interpolation
106
+
107
+ ```python
108
+ import numpy as np
109
+ from tomio import volume
110
+
111
+ vol2 = volume.resample(vol, like=ref) # geometry of another volume (or a Geometry)
112
+ vol2 = volume.resample(vol, spacing=(1, 1, 1)) # new spacing, same extent
113
+ vol2 = volume.resample(vol, geometry={"spacing": (1, 1, 1), "shape": (64, 64, 64)})
114
+ vol2 = volume.resample(vol, like=ref, method="linear", fill=np.nan) # value outside `vol`
115
+
116
+ # label maps and masks
117
+ labels2 = volume.resample(labelmap, like=ref, method="labels") # label by label (smooth boundaries)
118
+ roi2 = volume.resample(roi, like=ref, method="roi") # no label lost when downsampling
119
+ ```
120
+
121
+ - Methods: `nearest`, `linear`, `spline3` (and `splineN`), `labels`, `roi`. Default: `nearest` for integer and bool data (label maps), `spline3` otherwise. The output has the dtype of the input.
122
+ - Backends: SimpleITK (default, multithreaded) or scipy (`backend="scipy"`, requires `tomio[interp]`). SimpleITK considers voxels inside the source up to half a voxel beyond the border voxel centers.
123
+ - Lower-level functions: `interp.interpolate`, `interpolate_like`, `interpolate_to`, `interpolate_roi`, `interpolate_labels`.
124
+
125
+ ## Label files
126
+
127
+ ```python
128
+ from tomio import labelfile
129
+
130
+ labels = labelfile.read("labels.txt") # ITK-SNAP label description file
131
+ labels = labelfile.read("FreeSurferColorLUT.txt", format="freesurfer")
132
+ labels = labelfile.read("labels.ctbl", format="slicer") # 3D Slicer color table
133
+ labelfile.write("labels.txt", labels) # or labels.save("labels.txt"), ITK-SNAP format
134
+
135
+ # Labels: mapping {index: description}, sorted by index
136
+ labels = labelfile.Labels({1: "white matter", 2: "grey matter"}) # automatic colors
137
+ labels = labelfile.Labels({1: {"LABEL": "white matter", "RGBA": (255, 255, 255, 1.0)}})
138
+ labels = labelfile.Labels(["background", "white matter"]) # indices 0, 1
139
+
140
+ labels[1] # description
141
+ labels[3] = "csf" # add or rename (keeps the color)
142
+ del labels[3]
143
+ labels.getindex("grey matter") # index of a description
144
+ labels.indices, labels.descriptions # lists, sorted by index (can be assigned)
145
+ labels.colors # {index: (R, G, B, A)}, RGB in 0..255, alpha in 0..1 (can be assigned)
146
+
147
+ # label maps
148
+ labels = labelfile.Labels.from_volume(labelmap) # one label per value
149
+ rgba = labels.lut[labelmap] # RGBA image (uint8)
150
+ labels2, mapping = labels.reindex() # indices 1..N
151
+ labelmap2 = labelfile.apply_mapping(labelmap, mapping)
152
+ ```
153
+
154
+ Labels are validated: non-negative integer indices, unique descriptions without `"` or line breaks (`allow_duplicates=True` to allow duplicates), RGB in 0..255, alpha in 0..1. Label files are UTF-8.
155
+
156
+ ## Config files
157
+
158
+ ```python
159
+ from tomio import configfile
160
+
161
+ cfg = configfile.read("config.yml") # .json, .yaml, .yml, .toml -> dict
162
+ configfile.write("config.yml", cfg) # .json, .yaml, .yml
163
+ ```
164
+
165
+ - The format is set by the file extension (no extension: `.json`). TOML files are read-only.
166
+ - Values are written as standard types: numpy arrays as lists, numpy scalars as numbers, complex numbers as strings `"3 + 4i"` (read back as complex numbers).
167
+ - YAML files are loaded safely (no Python-specific tags).
168
+
169
+ ## Command line
170
+
171
+ ```bash
172
+ tomio convert vol.mha vol.nii.gz # convert (format from the extensions)
173
+ tomio convert vol.mha vol.nii.gz --dtype float32 --compression-level 9
174
+ tomio resample vol.nii.gz out.nii.gz --spacing 1 1 1 # new spacing
175
+ tomio resample labels.nii.gz out.nii.gz --like ref.nii.gz --method labels
176
+ tomio info vol.nii.gz --metadata # header: shape, pixel type, geometry
177
+ ```
178
+
179
+ Existing output files are not overwritten unless `--force` is given. See `tomio COMMAND --help`.
@@ -0,0 +1,51 @@
1
+ [build-system]
2
+ requires = ["setuptools>=77"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "tomio"
7
+ version = "0.1.0"
8
+ description = "I/O utilities for medical images: image volumes with geometry, interpolation, label and config files"
9
+ readme = "README.md"
10
+ license = "MIT"
11
+ license-files = ["LICENSE"]
12
+ authors = [{ name = "Pierre-Yves Baudin" }]
13
+ requires-python = ">=3.10"
14
+ dependencies = [
15
+ "numpy>=1.24",
16
+ "pyyaml",
17
+ "SimpleITK",
18
+ ]
19
+ keywords = ["medical imaging", "nifti", "itk", "simpleitk", "volume", "interpolation", "itk-snap", "labels"]
20
+ classifiers = [
21
+ "Development Status :: 4 - Beta",
22
+ "Intended Audience :: Science/Research",
23
+ "Operating System :: OS Independent",
24
+ "Programming Language :: Python :: 3",
25
+ "Programming Language :: Python :: 3 :: Only",
26
+ "Programming Language :: Python :: 3.10",
27
+ "Programming Language :: Python :: 3.11",
28
+ "Programming Language :: Python :: 3.12",
29
+ "Programming Language :: Python :: 3.13",
30
+ "Topic :: Scientific/Engineering :: Medical Science Apps.",
31
+ "Topic :: Scientific/Engineering :: Image Processing",
32
+ ]
33
+
34
+ [project.optional-dependencies]
35
+ interp = ["scipy"]
36
+ nibabel = ["nibabel"]
37
+ test = ["pytest", "scipy", "nibabel"]
38
+
39
+ [project.scripts]
40
+ tomio = "tomio.cli:main"
41
+
42
+ [project.urls]
43
+ Homepage = "https://github.com/py-baudin/tomio"
44
+ Repository = "https://github.com/py-baudin/tomio"
45
+ Issues = "https://github.com/py-baudin/tomio/issues"
46
+
47
+ [tool.setuptools.packages.find]
48
+ where = ["src"]
49
+
50
+ [tool.pytest.ini_options]
51
+ testpaths = ["test"]
tomio-0.1.0/setup.cfg ADDED
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -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
+
@@ -0,0 +1,5 @@
1
+ import sys
2
+
3
+ from .cli import main
4
+
5
+ sys.exit(main())
@@ -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) + ")"