simind-python-connector 1.0.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.
- simind_python_connector/__init__.py +72 -0
- simind_python_connector/backends/__init__.py +480 -0
- simind_python_connector/backends/base.py +387 -0
- simind_python_connector/backends/sirf_backend.py +309 -0
- simind_python_connector/backends/stir_backend.py +395 -0
- simind_python_connector/builders/__init__.py +19 -0
- simind_python_connector/builders/acquisition_builder.py +526 -0
- simind_python_connector/builders/image_builder.py +217 -0
- simind_python_connector/configs/AnyScan.yaml +420 -0
- simind_python_connector/configs/Discovery670.yaml +412 -0
- simind_python_connector/configs/Example.yaml +420 -0
- simind_python_connector/configs/MLD001_SCAN0.yaml +426 -0
- simind_python_connector/configs/__init__.py +41 -0
- simind_python_connector/configs/input.smc +51 -0
- simind_python_connector/connectors/__init__.py +24 -0
- simind_python_connector/connectors/_spacing.py +69 -0
- simind_python_connector/connectors/base.py +40 -0
- simind_python_connector/connectors/python_connector.py +355 -0
- simind_python_connector/connectors/pytomography_adaptor.py +263 -0
- simind_python_connector/connectors/sirf_adaptor.py +164 -0
- simind_python_connector/connectors/stir_adaptor.py +164 -0
- simind_python_connector/converters/__init__.py +16 -0
- simind_python_connector/converters/attenuation.py +367 -0
- simind_python_connector/converters/dicom_to_stir.py +3 -0
- simind_python_connector/converters/simind_to_stir.py +769 -0
- simind_python_connector/core/__init__.py +7 -0
- simind_python_connector/core/config.py +939 -0
- simind_python_connector/core/executor.py +96 -0
- simind_python_connector/core/types.py +203 -0
- simind_python_connector/data/Schneider2000.json +222 -0
- simind_python_connector/data/__init__.py +25 -0
- simind_python_connector/data/bone.atn +187 -0
- simind_python_connector/data/h2o.atn +92 -0
- simind_python_connector/utils/__init__.py +120 -0
- simind_python_connector/utils/backend_access.py +121 -0
- simind_python_connector/utils/import_helpers.py +74 -0
- simind_python_connector/utils/interfile_numpy.py +195 -0
- simind_python_connector/utils/interfile_parser.py +175 -0
- simind_python_connector/utils/io_utils.py +14 -0
- simind_python_connector/utils/simind_utils.py +70 -0
- simind_python_connector/utils/sirf_stir_utils.py +194 -0
- simind_python_connector/utils/stir_utils.py +485 -0
- simind_python_connector-1.0.0.dist-info/METADATA +274 -0
- simind_python_connector-1.0.0.dist-info/RECORD +47 -0
- simind_python_connector-1.0.0.dist-info/WHEEL +5 -0
- simind_python_connector-1.0.0.dist-info/licenses/LICENSE +195 -0
- simind_python_connector-1.0.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,387 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Base interfaces for SIRF and STIR Python backends.
|
|
3
|
+
|
|
4
|
+
This module defines abstract base classes that provide a unified interface
|
|
5
|
+
for working with both SIRF and STIR Python libraries.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from abc import ABC, abstractmethod
|
|
9
|
+
from typing import Tuple, Union
|
|
10
|
+
|
|
11
|
+
import numpy as np
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class ImageDataInterface(ABC):
|
|
15
|
+
"""Abstract interface for image data objects.
|
|
16
|
+
|
|
17
|
+
Includes SIRF ``ImageData`` and STIR ``FloatVoxelsOnCartesianGrid`` types.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
@classmethod
|
|
21
|
+
@abstractmethod
|
|
22
|
+
def read_from_file(cls, filepath: str) -> "ImageDataInterface":
|
|
23
|
+
"""Read image data from file.
|
|
24
|
+
|
|
25
|
+
Args:
|
|
26
|
+
filepath: Path to image file (.hv)
|
|
27
|
+
|
|
28
|
+
Returns:
|
|
29
|
+
ImageDataInterface: Wrapped image object
|
|
30
|
+
"""
|
|
31
|
+
pass
|
|
32
|
+
|
|
33
|
+
@abstractmethod
|
|
34
|
+
def write(self, filepath: str) -> None:
|
|
35
|
+
"""Write image data to file.
|
|
36
|
+
|
|
37
|
+
Args:
|
|
38
|
+
filepath: Output file path
|
|
39
|
+
"""
|
|
40
|
+
pass
|
|
41
|
+
|
|
42
|
+
@abstractmethod
|
|
43
|
+
def fill(self, data: Union[np.ndarray, float]) -> None:
|
|
44
|
+
"""Fill image with data.
|
|
45
|
+
|
|
46
|
+
Args:
|
|
47
|
+
data: Numpy array or scalar value
|
|
48
|
+
"""
|
|
49
|
+
pass
|
|
50
|
+
|
|
51
|
+
@abstractmethod
|
|
52
|
+
def clone(self) -> "ImageDataInterface":
|
|
53
|
+
"""Create a deep copy of the image.
|
|
54
|
+
|
|
55
|
+
Returns:
|
|
56
|
+
ImageDataInterface: Cloned image object
|
|
57
|
+
"""
|
|
58
|
+
pass
|
|
59
|
+
|
|
60
|
+
@abstractmethod
|
|
61
|
+
def get_uniform_copy(self, value: float) -> "ImageDataInterface":
|
|
62
|
+
"""Create a uniform copy with specified value.
|
|
63
|
+
|
|
64
|
+
Args:
|
|
65
|
+
value: Value to fill the image with
|
|
66
|
+
|
|
67
|
+
Returns:
|
|
68
|
+
ImageDataInterface: New uniform image
|
|
69
|
+
"""
|
|
70
|
+
pass
|
|
71
|
+
|
|
72
|
+
@abstractmethod
|
|
73
|
+
def dimensions(self) -> Tuple[int, int, int]:
|
|
74
|
+
"""Get image dimensions.
|
|
75
|
+
|
|
76
|
+
Returns:
|
|
77
|
+
Tuple[int, int, int]: (z, y, x) dimensions
|
|
78
|
+
"""
|
|
79
|
+
pass
|
|
80
|
+
|
|
81
|
+
@abstractmethod
|
|
82
|
+
def voxel_sizes(self) -> Tuple[float, float, float]:
|
|
83
|
+
"""Get voxel sizes in mm.
|
|
84
|
+
|
|
85
|
+
Returns:
|
|
86
|
+
Tuple[float, float, float]: (z, y, x) voxel sizes
|
|
87
|
+
"""
|
|
88
|
+
pass
|
|
89
|
+
|
|
90
|
+
@abstractmethod
|
|
91
|
+
def as_array(self) -> np.ndarray:
|
|
92
|
+
"""Convert to numpy array.
|
|
93
|
+
|
|
94
|
+
Returns:
|
|
95
|
+
np.ndarray: Image data as numpy array
|
|
96
|
+
"""
|
|
97
|
+
pass
|
|
98
|
+
|
|
99
|
+
@abstractmethod
|
|
100
|
+
def sum(self) -> float:
|
|
101
|
+
"""Sum all values in the image.
|
|
102
|
+
|
|
103
|
+
Returns:
|
|
104
|
+
float: Sum of all voxel values
|
|
105
|
+
"""
|
|
106
|
+
pass
|
|
107
|
+
|
|
108
|
+
@abstractmethod
|
|
109
|
+
def max(self) -> float:
|
|
110
|
+
"""Return the maximum voxel value.
|
|
111
|
+
|
|
112
|
+
Returns:
|
|
113
|
+
float: Maximum voxel value
|
|
114
|
+
"""
|
|
115
|
+
pass
|
|
116
|
+
|
|
117
|
+
@abstractmethod
|
|
118
|
+
def maximum(self, value: float) -> None:
|
|
119
|
+
"""Apply element-wise maximum (clipping).
|
|
120
|
+
|
|
121
|
+
Args:
|
|
122
|
+
value: Minimum value to clip to
|
|
123
|
+
"""
|
|
124
|
+
pass
|
|
125
|
+
|
|
126
|
+
@property
|
|
127
|
+
@abstractmethod
|
|
128
|
+
def native_object(self):
|
|
129
|
+
"""Get the underlying native object.
|
|
130
|
+
|
|
131
|
+
Returns either SIRF ``ImageData`` or STIR
|
|
132
|
+
``FloatVoxelsOnCartesianGrid``.
|
|
133
|
+
"""
|
|
134
|
+
pass
|
|
135
|
+
|
|
136
|
+
# Arithmetic operations
|
|
137
|
+
@abstractmethod
|
|
138
|
+
def __add__(self, other):
|
|
139
|
+
"""Add two images or image and scalar."""
|
|
140
|
+
pass
|
|
141
|
+
|
|
142
|
+
@abstractmethod
|
|
143
|
+
def __sub__(self, other):
|
|
144
|
+
"""Subtract two images or image and scalar."""
|
|
145
|
+
pass
|
|
146
|
+
|
|
147
|
+
@abstractmethod
|
|
148
|
+
def __mul__(self, other):
|
|
149
|
+
"""Multiply image by scalar or element-wise."""
|
|
150
|
+
pass
|
|
151
|
+
|
|
152
|
+
@abstractmethod
|
|
153
|
+
def __truediv__(self, other):
|
|
154
|
+
"""Divide image by scalar or element-wise."""
|
|
155
|
+
pass
|
|
156
|
+
|
|
157
|
+
@abstractmethod
|
|
158
|
+
def __radd__(self, other):
|
|
159
|
+
"""Right-hand addition."""
|
|
160
|
+
pass
|
|
161
|
+
|
|
162
|
+
@abstractmethod
|
|
163
|
+
def __rsub__(self, other):
|
|
164
|
+
"""Right-hand subtraction."""
|
|
165
|
+
pass
|
|
166
|
+
|
|
167
|
+
@abstractmethod
|
|
168
|
+
def __rmul__(self, other):
|
|
169
|
+
"""Right-hand multiplication."""
|
|
170
|
+
pass
|
|
171
|
+
|
|
172
|
+
@abstractmethod
|
|
173
|
+
def __rtruediv__(self, other):
|
|
174
|
+
"""Right-hand division."""
|
|
175
|
+
pass
|
|
176
|
+
|
|
177
|
+
@abstractmethod
|
|
178
|
+
def __neg__(self):
|
|
179
|
+
"""Negate image."""
|
|
180
|
+
pass
|
|
181
|
+
|
|
182
|
+
@abstractmethod
|
|
183
|
+
def sapyb(
|
|
184
|
+
self, a: float, y: "ImageDataInterface", b: float
|
|
185
|
+
) -> "ImageDataInterface":
|
|
186
|
+
"""Compute self * a + y * b (element-wise), modifying self in place.
|
|
187
|
+
|
|
188
|
+
This method modifies self in place: self = self * a + y * b
|
|
189
|
+
|
|
190
|
+
Args:
|
|
191
|
+
a: Scalar multiplier for self
|
|
192
|
+
y: Other image
|
|
193
|
+
b: Scalar multiplier for y
|
|
194
|
+
|
|
195
|
+
Returns:
|
|
196
|
+
ImageDataInterface: self after modification (for chaining)
|
|
197
|
+
"""
|
|
198
|
+
pass
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
class AcquisitionDataInterface(ABC):
|
|
202
|
+
"""Abstract interface for acquisition data (AcquisitionData or ProjData)."""
|
|
203
|
+
|
|
204
|
+
@classmethod
|
|
205
|
+
@abstractmethod
|
|
206
|
+
def read_from_file(cls, filepath: str) -> "AcquisitionDataInterface":
|
|
207
|
+
"""Read acquisition data from file.
|
|
208
|
+
|
|
209
|
+
Args:
|
|
210
|
+
filepath: Path to header file (.hs)
|
|
211
|
+
|
|
212
|
+
Returns:
|
|
213
|
+
AcquisitionDataInterface: Wrapped acquisition object
|
|
214
|
+
"""
|
|
215
|
+
pass
|
|
216
|
+
|
|
217
|
+
@abstractmethod
|
|
218
|
+
def write(self, filepath: str) -> None:
|
|
219
|
+
"""Write acquisition data to file.
|
|
220
|
+
|
|
221
|
+
Args:
|
|
222
|
+
filepath: Output file path
|
|
223
|
+
"""
|
|
224
|
+
pass
|
|
225
|
+
|
|
226
|
+
@abstractmethod
|
|
227
|
+
def fill(self, data: Union[np.ndarray, float]) -> None:
|
|
228
|
+
"""Fill acquisition data.
|
|
229
|
+
|
|
230
|
+
Args:
|
|
231
|
+
data: Numpy array or scalar value
|
|
232
|
+
"""
|
|
233
|
+
pass
|
|
234
|
+
|
|
235
|
+
@abstractmethod
|
|
236
|
+
def clone(self) -> "AcquisitionDataInterface":
|
|
237
|
+
"""Create a deep copy.
|
|
238
|
+
|
|
239
|
+
Returns:
|
|
240
|
+
AcquisitionDataInterface: Cloned object
|
|
241
|
+
"""
|
|
242
|
+
pass
|
|
243
|
+
|
|
244
|
+
@abstractmethod
|
|
245
|
+
def get_uniform_copy(self, value: float) -> "AcquisitionDataInterface":
|
|
246
|
+
"""Create a uniform copy with specified value.
|
|
247
|
+
|
|
248
|
+
Args:
|
|
249
|
+
value: Value to fill the acquisition data with
|
|
250
|
+
|
|
251
|
+
Returns:
|
|
252
|
+
AcquisitionDataInterface: New uniform acquisition data
|
|
253
|
+
"""
|
|
254
|
+
pass
|
|
255
|
+
|
|
256
|
+
@abstractmethod
|
|
257
|
+
def dimensions(self) -> Tuple:
|
|
258
|
+
"""Get acquisition data dimensions.
|
|
259
|
+
|
|
260
|
+
Returns:
|
|
261
|
+
Tuple: Dimensions (varies by format)
|
|
262
|
+
"""
|
|
263
|
+
pass
|
|
264
|
+
|
|
265
|
+
@property
|
|
266
|
+
def shape(self) -> Tuple:
|
|
267
|
+
"""Get acquisition data shape (alias for dimensions()).
|
|
268
|
+
|
|
269
|
+
Returns:
|
|
270
|
+
Tuple: Dimensions (varies by format)
|
|
271
|
+
"""
|
|
272
|
+
return self.dimensions()
|
|
273
|
+
|
|
274
|
+
@abstractmethod
|
|
275
|
+
def as_array(self) -> np.ndarray:
|
|
276
|
+
"""Convert to numpy array.
|
|
277
|
+
|
|
278
|
+
Returns:
|
|
279
|
+
np.ndarray: Acquisition data as numpy array
|
|
280
|
+
"""
|
|
281
|
+
pass
|
|
282
|
+
|
|
283
|
+
@abstractmethod
|
|
284
|
+
def sum(self) -> float:
|
|
285
|
+
"""Sum all values.
|
|
286
|
+
|
|
287
|
+
Returns:
|
|
288
|
+
float: Sum of all counts
|
|
289
|
+
"""
|
|
290
|
+
pass
|
|
291
|
+
|
|
292
|
+
@abstractmethod
|
|
293
|
+
def max(self) -> float:
|
|
294
|
+
"""Return the maximum bin value.
|
|
295
|
+
|
|
296
|
+
Returns:
|
|
297
|
+
float: Maximum bin value
|
|
298
|
+
"""
|
|
299
|
+
pass
|
|
300
|
+
|
|
301
|
+
@abstractmethod
|
|
302
|
+
def get_info(self) -> str:
|
|
303
|
+
"""Get metadata information.
|
|
304
|
+
|
|
305
|
+
Returns:
|
|
306
|
+
str: Metadata string
|
|
307
|
+
"""
|
|
308
|
+
pass
|
|
309
|
+
|
|
310
|
+
@abstractmethod
|
|
311
|
+
def get_energy_window_bounds(self) -> Tuple[float, float]:
|
|
312
|
+
"""Get the energy window bounds for the acquisition.
|
|
313
|
+
|
|
314
|
+
Returns:
|
|
315
|
+
Tuple[float, float]: (lower_threshold, upper_threshold) in keV
|
|
316
|
+
"""
|
|
317
|
+
pass
|
|
318
|
+
|
|
319
|
+
@property
|
|
320
|
+
@abstractmethod
|
|
321
|
+
def native_object(self):
|
|
322
|
+
"""Get the underlying native object (AcquisitionData or ProjData)."""
|
|
323
|
+
pass
|
|
324
|
+
|
|
325
|
+
# Arithmetic operations
|
|
326
|
+
@abstractmethod
|
|
327
|
+
def __add__(self, other):
|
|
328
|
+
"""Add two acquisition data or acquisition data and scalar."""
|
|
329
|
+
pass
|
|
330
|
+
|
|
331
|
+
@abstractmethod
|
|
332
|
+
def __sub__(self, other):
|
|
333
|
+
"""Subtract two acquisition data or acquisition data and scalar."""
|
|
334
|
+
pass
|
|
335
|
+
|
|
336
|
+
@abstractmethod
|
|
337
|
+
def __mul__(self, other):
|
|
338
|
+
"""Multiply acquisition data by scalar or element-wise."""
|
|
339
|
+
pass
|
|
340
|
+
|
|
341
|
+
@abstractmethod
|
|
342
|
+
def __truediv__(self, other):
|
|
343
|
+
"""Divide acquisition data by scalar or element-wise."""
|
|
344
|
+
pass
|
|
345
|
+
|
|
346
|
+
@abstractmethod
|
|
347
|
+
def __radd__(self, other):
|
|
348
|
+
"""Right-hand addition."""
|
|
349
|
+
pass
|
|
350
|
+
|
|
351
|
+
@abstractmethod
|
|
352
|
+
def __rsub__(self, other):
|
|
353
|
+
"""Right-hand subtraction."""
|
|
354
|
+
pass
|
|
355
|
+
|
|
356
|
+
@abstractmethod
|
|
357
|
+
def __rmul__(self, other):
|
|
358
|
+
"""Right-hand multiplication."""
|
|
359
|
+
pass
|
|
360
|
+
|
|
361
|
+
@abstractmethod
|
|
362
|
+
def __rtruediv__(self, other):
|
|
363
|
+
"""Right-hand division."""
|
|
364
|
+
pass
|
|
365
|
+
|
|
366
|
+
@abstractmethod
|
|
367
|
+
def __neg__(self):
|
|
368
|
+
"""Negate acquisition data."""
|
|
369
|
+
pass
|
|
370
|
+
|
|
371
|
+
@abstractmethod
|
|
372
|
+
def sapyb(
|
|
373
|
+
self, a: float, y: "AcquisitionDataInterface", b: float
|
|
374
|
+
) -> "AcquisitionDataInterface":
|
|
375
|
+
"""Compute self * a + y * b (element-wise), modifying self in place.
|
|
376
|
+
|
|
377
|
+
This method modifies self in place: self = self * a + y * b
|
|
378
|
+
|
|
379
|
+
Args:
|
|
380
|
+
a: Scalar multiplier for self
|
|
381
|
+
y: Other acquisition data
|
|
382
|
+
b: Scalar multiplier for y
|
|
383
|
+
|
|
384
|
+
Returns:
|
|
385
|
+
AcquisitionDataInterface: self after modification (for chaining)
|
|
386
|
+
"""
|
|
387
|
+
pass
|
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
"""
|
|
2
|
+
SIRF backend implementation.
|
|
3
|
+
|
|
4
|
+
Provides wrappers around sirf.STIR.ImageData and sirf.STIR.AcquisitionData
|
|
5
|
+
that conform to the backend interface.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from typing import Tuple, Union
|
|
9
|
+
|
|
10
|
+
import numpy as np
|
|
11
|
+
|
|
12
|
+
from simind_python_connector.utils.import_helpers import get_sirf_types
|
|
13
|
+
|
|
14
|
+
from .base import AcquisitionDataInterface, ImageDataInterface
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
ImageData, AcquisitionData, SIRF_AVAILABLE = get_sirf_types()
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class SirfImageData(ImageDataInterface):
|
|
21
|
+
"""Wrapper for sirf.STIR.ImageData."""
|
|
22
|
+
|
|
23
|
+
def __init__(self, sirf_obj: ImageData):
|
|
24
|
+
"""Initialize wrapper.
|
|
25
|
+
|
|
26
|
+
Args:
|
|
27
|
+
sirf_obj: SIRF ImageData object
|
|
28
|
+
"""
|
|
29
|
+
if not SIRF_AVAILABLE:
|
|
30
|
+
raise ImportError("SIRF is not available")
|
|
31
|
+
self._obj = sirf_obj
|
|
32
|
+
|
|
33
|
+
@classmethod
|
|
34
|
+
def read_from_file(cls, filepath: str) -> "SirfImageData":
|
|
35
|
+
"""Read image from file."""
|
|
36
|
+
return cls(ImageData(filepath))
|
|
37
|
+
|
|
38
|
+
@classmethod
|
|
39
|
+
def create_empty(cls) -> "SirfImageData":
|
|
40
|
+
"""Create empty image."""
|
|
41
|
+
return cls(ImageData())
|
|
42
|
+
|
|
43
|
+
def write(self, filepath: str) -> None:
|
|
44
|
+
"""Write to file."""
|
|
45
|
+
self._obj.write(filepath)
|
|
46
|
+
|
|
47
|
+
def fill(self, data: Union[np.ndarray, float, "SirfImageData"]) -> None:
|
|
48
|
+
"""Fill with data."""
|
|
49
|
+
# Unwrap if data is a wrapped object
|
|
50
|
+
if isinstance(data, SirfImageData):
|
|
51
|
+
self._obj.fill(data._obj)
|
|
52
|
+
else:
|
|
53
|
+
self._obj.fill(data)
|
|
54
|
+
|
|
55
|
+
def clone(self) -> "SirfImageData":
|
|
56
|
+
"""Clone the image."""
|
|
57
|
+
return SirfImageData(self._obj.clone())
|
|
58
|
+
|
|
59
|
+
def get_uniform_copy(self, value: float) -> "SirfImageData":
|
|
60
|
+
"""Create uniform copy."""
|
|
61
|
+
return SirfImageData(self._obj.get_uniform_copy(value))
|
|
62
|
+
|
|
63
|
+
def dimensions(self) -> Tuple[int, int, int]:
|
|
64
|
+
"""Get dimensions as (z, y, x)."""
|
|
65
|
+
return self._obj.dimensions()
|
|
66
|
+
|
|
67
|
+
def voxel_sizes(self) -> Tuple[float, float, float]:
|
|
68
|
+
"""Get voxel sizes as (z, y, x) in mm."""
|
|
69
|
+
return self._obj.voxel_sizes()
|
|
70
|
+
|
|
71
|
+
def as_array(self) -> np.ndarray:
|
|
72
|
+
"""Convert to numpy array."""
|
|
73
|
+
# Try asarray first (newer SIRF), fall back to as_array
|
|
74
|
+
if hasattr(self._obj, "asarray"):
|
|
75
|
+
return self._obj.asarray()
|
|
76
|
+
else:
|
|
77
|
+
return self._obj.as_array()
|
|
78
|
+
|
|
79
|
+
def sum(self) -> float:
|
|
80
|
+
"""Sum all values."""
|
|
81
|
+
return float(self._obj.sum())
|
|
82
|
+
|
|
83
|
+
def max(self) -> float:
|
|
84
|
+
"""Return the maximum voxel value."""
|
|
85
|
+
if hasattr(self._obj, "max"):
|
|
86
|
+
return float(self._obj.max())
|
|
87
|
+
return float(np.max(self.as_array()))
|
|
88
|
+
|
|
89
|
+
def maximum(self, value: float) -> None:
|
|
90
|
+
"""Apply element-wise maximum."""
|
|
91
|
+
self._obj.maximum(value)
|
|
92
|
+
|
|
93
|
+
@property
|
|
94
|
+
def native_object(self):
|
|
95
|
+
"""Get underlying SIRF object."""
|
|
96
|
+
return self._obj
|
|
97
|
+
|
|
98
|
+
def __repr__(self) -> str:
|
|
99
|
+
return f"SirfImageData(dims={self.dimensions()})"
|
|
100
|
+
|
|
101
|
+
# Arithmetic operations - delegate to native SIRF object
|
|
102
|
+
def __add__(self, other):
|
|
103
|
+
if isinstance(other, SirfImageData):
|
|
104
|
+
return SirfImageData(self._obj + other._obj)
|
|
105
|
+
else:
|
|
106
|
+
return SirfImageData(self._obj + other)
|
|
107
|
+
|
|
108
|
+
def __sub__(self, other):
|
|
109
|
+
if isinstance(other, SirfImageData):
|
|
110
|
+
return SirfImageData(self._obj - other._obj)
|
|
111
|
+
else:
|
|
112
|
+
return SirfImageData(self._obj - other)
|
|
113
|
+
|
|
114
|
+
def __mul__(self, other):
|
|
115
|
+
if isinstance(other, SirfImageData):
|
|
116
|
+
return SirfImageData(self._obj * other._obj)
|
|
117
|
+
else:
|
|
118
|
+
return SirfImageData(self._obj * other)
|
|
119
|
+
|
|
120
|
+
def __truediv__(self, other):
|
|
121
|
+
if isinstance(other, SirfImageData):
|
|
122
|
+
return SirfImageData(self._obj / other._obj)
|
|
123
|
+
else:
|
|
124
|
+
return SirfImageData(self._obj / other)
|
|
125
|
+
|
|
126
|
+
def __radd__(self, other):
|
|
127
|
+
return SirfImageData(other + self._obj)
|
|
128
|
+
|
|
129
|
+
def __rsub__(self, other):
|
|
130
|
+
return SirfImageData(other - self._obj)
|
|
131
|
+
|
|
132
|
+
def __rmul__(self, other):
|
|
133
|
+
return SirfImageData(other * self._obj)
|
|
134
|
+
|
|
135
|
+
def __rtruediv__(self, other):
|
|
136
|
+
return SirfImageData(other / self._obj)
|
|
137
|
+
|
|
138
|
+
def __neg__(self):
|
|
139
|
+
return SirfImageData(-self._obj)
|
|
140
|
+
|
|
141
|
+
def sapyb(self, a: float, y: "SirfImageData", b: float) -> "SirfImageData":
|
|
142
|
+
"""Compute self * a + y * b (element-wise), modifying self in place.
|
|
143
|
+
|
|
144
|
+
Args:
|
|
145
|
+
a: Scalar multiplier for self
|
|
146
|
+
y: Other image
|
|
147
|
+
b: Scalar multiplier for y
|
|
148
|
+
|
|
149
|
+
Returns:
|
|
150
|
+
SirfImageData: self after modification (for chaining)
|
|
151
|
+
"""
|
|
152
|
+
y_obj = y._obj if isinstance(y, SirfImageData) else y
|
|
153
|
+
self._obj.sapyb(a, y_obj, b)
|
|
154
|
+
return self
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
class SirfAcquisitionData(AcquisitionDataInterface):
|
|
158
|
+
"""Wrapper for sirf.STIR.AcquisitionData."""
|
|
159
|
+
|
|
160
|
+
def __init__(self, sirf_obj: AcquisitionData):
|
|
161
|
+
"""Initialize wrapper.
|
|
162
|
+
|
|
163
|
+
Args:
|
|
164
|
+
sirf_obj: SIRF AcquisitionData object
|
|
165
|
+
"""
|
|
166
|
+
if not SIRF_AVAILABLE:
|
|
167
|
+
raise ImportError("SIRF is not available")
|
|
168
|
+
self._obj = sirf_obj
|
|
169
|
+
|
|
170
|
+
@classmethod
|
|
171
|
+
def read_from_file(cls, filepath: str) -> "SirfAcquisitionData":
|
|
172
|
+
"""Read acquisition data from file."""
|
|
173
|
+
return cls(AcquisitionData(filepath))
|
|
174
|
+
|
|
175
|
+
@classmethod
|
|
176
|
+
def create_empty(cls) -> "SirfAcquisitionData":
|
|
177
|
+
"""Create empty acquisition data."""
|
|
178
|
+
return cls(AcquisitionData())
|
|
179
|
+
|
|
180
|
+
def write(self, filepath: str) -> None:
|
|
181
|
+
"""Write to file."""
|
|
182
|
+
self._obj.write(filepath)
|
|
183
|
+
|
|
184
|
+
def fill(self, data: Union[np.ndarray, float, "SirfAcquisitionData"]) -> None:
|
|
185
|
+
"""Fill with data."""
|
|
186
|
+
# Unwrap if data is a wrapped object
|
|
187
|
+
if isinstance(data, SirfAcquisitionData):
|
|
188
|
+
self._obj.fill(data._obj)
|
|
189
|
+
else:
|
|
190
|
+
self._obj.fill(data)
|
|
191
|
+
|
|
192
|
+
def clone(self) -> "SirfAcquisitionData":
|
|
193
|
+
"""Clone the acquisition data."""
|
|
194
|
+
return SirfAcquisitionData(self._obj.clone())
|
|
195
|
+
|
|
196
|
+
def get_uniform_copy(self, value: float) -> "SirfAcquisitionData":
|
|
197
|
+
"""Create uniform copy."""
|
|
198
|
+
return SirfAcquisitionData(self._obj.get_uniform_copy(value))
|
|
199
|
+
|
|
200
|
+
def dimensions(self) -> Tuple:
|
|
201
|
+
"""Get dimensions."""
|
|
202
|
+
return self._obj.dimensions()
|
|
203
|
+
|
|
204
|
+
def as_array(self) -> np.ndarray:
|
|
205
|
+
"""Convert to numpy array."""
|
|
206
|
+
# Try asarray first (newer SIRF), fall back to as_array
|
|
207
|
+
if hasattr(self._obj, "asarray"):
|
|
208
|
+
return self._obj.asarray()
|
|
209
|
+
else:
|
|
210
|
+
return self._obj.as_array()
|
|
211
|
+
|
|
212
|
+
def sum(self) -> float:
|
|
213
|
+
"""Sum all values."""
|
|
214
|
+
return float(self._obj.sum())
|
|
215
|
+
|
|
216
|
+
def max(self) -> float:
|
|
217
|
+
"""Return the maximum bin value."""
|
|
218
|
+
if hasattr(self._obj, "max"):
|
|
219
|
+
return float(self._obj.max())
|
|
220
|
+
return float(np.max(self.as_array()))
|
|
221
|
+
|
|
222
|
+
def get_info(self) -> str:
|
|
223
|
+
"""Get metadata info."""
|
|
224
|
+
return self._obj.get_info()
|
|
225
|
+
|
|
226
|
+
def get_energy_window_bounds(self) -> Tuple[float, float]:
|
|
227
|
+
"""Get the energy window bounds for the acquisition.
|
|
228
|
+
|
|
229
|
+
Returns:
|
|
230
|
+
Tuple[float, float]: (lower_threshold, upper_threshold) in keV
|
|
231
|
+
|
|
232
|
+
Raises:
|
|
233
|
+
NotImplementedError: SIRF backend does not currently support
|
|
234
|
+
accessing energy window bounds.
|
|
235
|
+
"""
|
|
236
|
+
raise NotImplementedError(
|
|
237
|
+
"SIRF backend does not currently support accessing energy window bounds. "
|
|
238
|
+
"Use STIR backend for this functionality."
|
|
239
|
+
)
|
|
240
|
+
|
|
241
|
+
def create_uniform_image(self, value: float = 0.0) -> SirfImageData:
|
|
242
|
+
"""Create compatible uniform image."""
|
|
243
|
+
img = self._obj.create_uniform_image(value)
|
|
244
|
+
return SirfImageData(img)
|
|
245
|
+
|
|
246
|
+
@property
|
|
247
|
+
def native_object(self):
|
|
248
|
+
"""Get underlying SIRF object."""
|
|
249
|
+
return self._obj
|
|
250
|
+
|
|
251
|
+
def __repr__(self) -> str:
|
|
252
|
+
return f"SirfAcquisitionData(dims={self.dimensions()})"
|
|
253
|
+
|
|
254
|
+
# Arithmetic operations - delegate to native SIRF object
|
|
255
|
+
def __add__(self, other):
|
|
256
|
+
if isinstance(other, SirfAcquisitionData):
|
|
257
|
+
return SirfAcquisitionData(self._obj + other._obj)
|
|
258
|
+
else:
|
|
259
|
+
return SirfAcquisitionData(self._obj + other)
|
|
260
|
+
|
|
261
|
+
def __sub__(self, other):
|
|
262
|
+
if isinstance(other, SirfAcquisitionData):
|
|
263
|
+
return SirfAcquisitionData(self._obj - other._obj)
|
|
264
|
+
else:
|
|
265
|
+
return SirfAcquisitionData(self._obj - other)
|
|
266
|
+
|
|
267
|
+
def __mul__(self, other):
|
|
268
|
+
if isinstance(other, SirfAcquisitionData):
|
|
269
|
+
return SirfAcquisitionData(self._obj * other._obj)
|
|
270
|
+
else:
|
|
271
|
+
return SirfAcquisitionData(self._obj * other)
|
|
272
|
+
|
|
273
|
+
def __truediv__(self, other):
|
|
274
|
+
if isinstance(other, SirfAcquisitionData):
|
|
275
|
+
return SirfAcquisitionData(self._obj / other._obj)
|
|
276
|
+
else:
|
|
277
|
+
return SirfAcquisitionData(self._obj / other)
|
|
278
|
+
|
|
279
|
+
def __radd__(self, other):
|
|
280
|
+
return SirfAcquisitionData(other + self._obj)
|
|
281
|
+
|
|
282
|
+
def __rsub__(self, other):
|
|
283
|
+
return SirfAcquisitionData(other - self._obj)
|
|
284
|
+
|
|
285
|
+
def __rmul__(self, other):
|
|
286
|
+
return SirfAcquisitionData(other * self._obj)
|
|
287
|
+
|
|
288
|
+
def __rtruediv__(self, other):
|
|
289
|
+
return SirfAcquisitionData(other / self._obj)
|
|
290
|
+
|
|
291
|
+
def __neg__(self):
|
|
292
|
+
return SirfAcquisitionData(-self._obj)
|
|
293
|
+
|
|
294
|
+
def sapyb(
|
|
295
|
+
self, a: float, y: "SirfAcquisitionData", b: float
|
|
296
|
+
) -> "SirfAcquisitionData":
|
|
297
|
+
"""Compute self * a + y * b (element-wise), modifying self in place.
|
|
298
|
+
|
|
299
|
+
Args:
|
|
300
|
+
a: Scalar multiplier for self
|
|
301
|
+
y: Other acquisition data
|
|
302
|
+
b: Scalar multiplier for y
|
|
303
|
+
|
|
304
|
+
Returns:
|
|
305
|
+
SirfAcquisitionData: self after modification (for chaining)
|
|
306
|
+
"""
|
|
307
|
+
y_obj = y._obj if isinstance(y, SirfAcquisitionData) else y
|
|
308
|
+
self._obj.sapyb(a, y_obj, b)
|
|
309
|
+
return self
|