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,395 @@
|
|
|
1
|
+
"""
|
|
2
|
+
STIR Python backend implementation.
|
|
3
|
+
|
|
4
|
+
Provides wrappers around stir.FloatVoxelsOnCartesianGrid and stir.ProjData
|
|
5
|
+
that conform to the backend interface.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from typing import Tuple, Union
|
|
9
|
+
|
|
10
|
+
import numpy as np
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
try:
|
|
14
|
+
import stir
|
|
15
|
+
import stirextra
|
|
16
|
+
|
|
17
|
+
STIR_AVAILABLE = True
|
|
18
|
+
except ImportError:
|
|
19
|
+
STIR_AVAILABLE = False
|
|
20
|
+
|
|
21
|
+
from .base import AcquisitionDataInterface, ImageDataInterface
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class StirImageData(ImageDataInterface):
|
|
25
|
+
"""Wrapper for stir.FloatVoxelsOnCartesianGrid."""
|
|
26
|
+
|
|
27
|
+
def __init__(self, stir_obj):
|
|
28
|
+
"""Initialize wrapper.
|
|
29
|
+
|
|
30
|
+
Args:
|
|
31
|
+
stir_obj: STIR FloatVoxelsOnCartesianGrid object
|
|
32
|
+
"""
|
|
33
|
+
if not STIR_AVAILABLE:
|
|
34
|
+
raise ImportError("STIR Python is not available")
|
|
35
|
+
self._obj = stir_obj
|
|
36
|
+
|
|
37
|
+
@classmethod
|
|
38
|
+
def read_from_file(cls, filepath: str) -> "StirImageData":
|
|
39
|
+
"""Read image from file."""
|
|
40
|
+
obj = stir.FloatVoxelsOnCartesianGrid.read_from_file(filepath)
|
|
41
|
+
return cls(obj)
|
|
42
|
+
|
|
43
|
+
@classmethod
|
|
44
|
+
def create_empty(
|
|
45
|
+
cls, exam_info=None, index_range=None, origin=None
|
|
46
|
+
) -> "StirImageData":
|
|
47
|
+
"""Create empty image.
|
|
48
|
+
|
|
49
|
+
Args:
|
|
50
|
+
exam_info: ExamInfo object (optional)
|
|
51
|
+
index_range: IndexRange object (optional)
|
|
52
|
+
origin: CartesianCoordinate3D for origin (optional)
|
|
53
|
+
|
|
54
|
+
Returns:
|
|
55
|
+
StirImageData: Empty image
|
|
56
|
+
"""
|
|
57
|
+
# This is complex - STIR requires geometry info to create images
|
|
58
|
+
# For now, raise NotImplementedError
|
|
59
|
+
raise NotImplementedError(
|
|
60
|
+
"Creating empty STIR images requires geometry information. "
|
|
61
|
+
"Use read_from_file or create from template."
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
def write(self, filepath: str) -> None:
|
|
65
|
+
"""Write to file.
|
|
66
|
+
|
|
67
|
+
Note: STIR uses write_to_file instead of write.
|
|
68
|
+
"""
|
|
69
|
+
self._obj.write_to_file(filepath)
|
|
70
|
+
|
|
71
|
+
def fill(self, data: Union[np.ndarray, float]) -> None:
|
|
72
|
+
"""Fill with data."""
|
|
73
|
+
if isinstance(data, (int, float)) or not isinstance(data, np.ndarray):
|
|
74
|
+
self._obj.fill(float(data))
|
|
75
|
+
else:
|
|
76
|
+
self._obj.fill(data.flat)
|
|
77
|
+
|
|
78
|
+
def clone(self) -> "StirImageData":
|
|
79
|
+
"""Clone the image."""
|
|
80
|
+
# STIR doesn't have a direct clone method
|
|
81
|
+
# We need to create a new object and copy data
|
|
82
|
+
cloned = self._obj.get_empty_copy()
|
|
83
|
+
cloned.fill(self.as_array().flat)
|
|
84
|
+
return StirImageData(cloned)
|
|
85
|
+
|
|
86
|
+
def get_uniform_copy(self, value: float) -> "StirImageData":
|
|
87
|
+
"""Create uniform copy."""
|
|
88
|
+
cloned = self.clone()
|
|
89
|
+
cloned._obj.fill(value)
|
|
90
|
+
return cloned
|
|
91
|
+
|
|
92
|
+
def dimensions(self) -> Tuple[int, int, int]:
|
|
93
|
+
"""Get dimensions as (z, y, x)."""
|
|
94
|
+
# Extract from numpy array shape
|
|
95
|
+
arr = stirextra.to_numpy(self._obj)
|
|
96
|
+
# STIR arrays are typically (z, y, x)
|
|
97
|
+
return arr.shape
|
|
98
|
+
|
|
99
|
+
def voxel_sizes(self) -> Tuple[float, float, float]:
|
|
100
|
+
"""Get voxel sizes as (z, y, x) in mm."""
|
|
101
|
+
# Access STIR's grid spacing
|
|
102
|
+
grid_spacing = self._obj.get_grid_spacing()
|
|
103
|
+
# grid_spacing is a CartesianCoordinate3D with [z, y, x] ordering
|
|
104
|
+
return (grid_spacing[1], grid_spacing[2], grid_spacing[3])
|
|
105
|
+
|
|
106
|
+
def as_array(self) -> np.ndarray:
|
|
107
|
+
"""Convert to numpy array using stirextra."""
|
|
108
|
+
return stirextra.to_numpy(self._obj)
|
|
109
|
+
|
|
110
|
+
def sum(self) -> float:
|
|
111
|
+
"""Sum all values."""
|
|
112
|
+
return float(self._obj.sum())
|
|
113
|
+
|
|
114
|
+
def max(self) -> float:
|
|
115
|
+
"""Return the maximum voxel value."""
|
|
116
|
+
return float(np.max(self.as_array()))
|
|
117
|
+
|
|
118
|
+
def maximum(self, value: float) -> None:
|
|
119
|
+
"""Apply element-wise maximum.
|
|
120
|
+
|
|
121
|
+
Note: STIR may not have this method directly.
|
|
122
|
+
"""
|
|
123
|
+
# STIR doesn't have a direct maximum method so we need to extract array
|
|
124
|
+
# and fill a clone
|
|
125
|
+
arr = self.as_array()
|
|
126
|
+
arr = np.maximum(arr, value)
|
|
127
|
+
self.fill(arr)
|
|
128
|
+
|
|
129
|
+
def minimum(self, value: float) -> None:
|
|
130
|
+
"""Apply element-wise minimum.
|
|
131
|
+
|
|
132
|
+
Note: STIR may not have this method directly.
|
|
133
|
+
"""
|
|
134
|
+
arr = self.as_array()
|
|
135
|
+
arr = np.minimum(arr, value)
|
|
136
|
+
self.fill(arr)
|
|
137
|
+
|
|
138
|
+
@property
|
|
139
|
+
def native_object(self):
|
|
140
|
+
"""Get underlying STIR object."""
|
|
141
|
+
return self._obj
|
|
142
|
+
|
|
143
|
+
def __repr__(self) -> str:
|
|
144
|
+
return f"StirImageData(dims={self.dimensions()})"
|
|
145
|
+
|
|
146
|
+
# Arithmetic operations - delegate to underlying STIR object
|
|
147
|
+
def __add__(self, other):
|
|
148
|
+
if isinstance(other, StirImageData):
|
|
149
|
+
return StirImageData(self._obj + other._obj)
|
|
150
|
+
else:
|
|
151
|
+
return StirImageData(self._obj + float(other))
|
|
152
|
+
|
|
153
|
+
def __sub__(self, other):
|
|
154
|
+
if isinstance(other, StirImageData):
|
|
155
|
+
return StirImageData(self._obj - other._obj)
|
|
156
|
+
else:
|
|
157
|
+
return StirImageData(self._obj - float(other))
|
|
158
|
+
|
|
159
|
+
def __mul__(self, other):
|
|
160
|
+
if isinstance(other, StirImageData):
|
|
161
|
+
return StirImageData(self._obj * other._obj)
|
|
162
|
+
else:
|
|
163
|
+
return StirImageData(self._obj * float(other))
|
|
164
|
+
|
|
165
|
+
def __truediv__(self, other):
|
|
166
|
+
if isinstance(other, StirImageData):
|
|
167
|
+
return StirImageData(self._obj / other._obj)
|
|
168
|
+
else:
|
|
169
|
+
return StirImageData(self._obj / float(other))
|
|
170
|
+
|
|
171
|
+
def __radd__(self, other):
|
|
172
|
+
return StirImageData(float(other) + self._obj)
|
|
173
|
+
|
|
174
|
+
def __rsub__(self, other):
|
|
175
|
+
return StirImageData(float(other) - self._obj)
|
|
176
|
+
|
|
177
|
+
def __rmul__(self, other):
|
|
178
|
+
return StirImageData(float(other) * self._obj)
|
|
179
|
+
|
|
180
|
+
def __rtruediv__(self, other):
|
|
181
|
+
return StirImageData(float(other) / self._obj)
|
|
182
|
+
|
|
183
|
+
def __neg__(self):
|
|
184
|
+
return StirImageData(-self._obj)
|
|
185
|
+
|
|
186
|
+
def sapyb(self, a: float, y: "StirImageData", b: float) -> "StirImageData":
|
|
187
|
+
"""Compute self * a + y * b (element-wise), modifying self in place.
|
|
188
|
+
|
|
189
|
+
Args:
|
|
190
|
+
a: Scalar multiplier for self
|
|
191
|
+
y: Other image
|
|
192
|
+
b: Scalar multiplier for y
|
|
193
|
+
|
|
194
|
+
Returns:
|
|
195
|
+
StirImageData: self after modification (for chaining)
|
|
196
|
+
"""
|
|
197
|
+
y_obj = y._obj if isinstance(y, StirImageData) else y
|
|
198
|
+
self._obj.sapyb(a, y_obj, b)
|
|
199
|
+
return self
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
class StirAcquisitionData(AcquisitionDataInterface):
|
|
203
|
+
"""Wrapper for stir.ProjDataInMemory.
|
|
204
|
+
|
|
205
|
+
Always uses ProjDataInMemory to ensure arithmetic operations are available.
|
|
206
|
+
"""
|
|
207
|
+
|
|
208
|
+
def __init__(self, stir_obj):
|
|
209
|
+
"""Initialize wrapper.
|
|
210
|
+
|
|
211
|
+
Args:
|
|
212
|
+
stir_obj: STIR ProjData or ProjDataInMemory object
|
|
213
|
+
"""
|
|
214
|
+
if not STIR_AVAILABLE:
|
|
215
|
+
raise ImportError("STIR Python is not available")
|
|
216
|
+
|
|
217
|
+
# Always convert to ProjDataInMemory for arithmetic support
|
|
218
|
+
if isinstance(stir_obj, stir.ProjDataInMemory):
|
|
219
|
+
self._obj = stir_obj
|
|
220
|
+
else:
|
|
221
|
+
self._obj = stir.ProjDataInMemory(stir_obj)
|
|
222
|
+
|
|
223
|
+
@classmethod
|
|
224
|
+
def read_from_file(cls, filepath: str) -> "StirAcquisitionData":
|
|
225
|
+
"""Read projection data from file and convert to ProjDataInMemory."""
|
|
226
|
+
obj = stir.ProjData.read_from_file(filepath)
|
|
227
|
+
# Constructor will convert to ProjDataInMemory
|
|
228
|
+
return cls(obj)
|
|
229
|
+
|
|
230
|
+
@classmethod
|
|
231
|
+
def create_empty(cls, exam_info=None, proj_data_info=None) -> "StirAcquisitionData":
|
|
232
|
+
"""Create empty projection data.
|
|
233
|
+
|
|
234
|
+
Args:
|
|
235
|
+
exam_info: ExamInfo object (required)
|
|
236
|
+
proj_data_info: ProjDataInfo object (required)
|
|
237
|
+
|
|
238
|
+
Returns:
|
|
239
|
+
StirAcquisitionData: Empty projection data
|
|
240
|
+
"""
|
|
241
|
+
if exam_info is None or proj_data_info is None:
|
|
242
|
+
raise ValueError(
|
|
243
|
+
"Creating empty STIR ProjData requires exam_info and proj_data_info"
|
|
244
|
+
)
|
|
245
|
+
obj = stir.ProjDataInMemory(exam_info, proj_data_info)
|
|
246
|
+
return cls(obj)
|
|
247
|
+
|
|
248
|
+
def write(self, filepath: str) -> None:
|
|
249
|
+
"""Write to file.
|
|
250
|
+
|
|
251
|
+
Note: STIR uses write_to_file instead of write.
|
|
252
|
+
"""
|
|
253
|
+
self._obj.write_to_file(filepath)
|
|
254
|
+
|
|
255
|
+
def fill(self, data: Union[np.ndarray, float]) -> None:
|
|
256
|
+
"""Fill with data."""
|
|
257
|
+
if isinstance(data, (int, float)):
|
|
258
|
+
self._obj.fill(float(data))
|
|
259
|
+
elif isinstance(data, np.ndarray):
|
|
260
|
+
self._obj.fill(data.flat)
|
|
261
|
+
else:
|
|
262
|
+
try:
|
|
263
|
+
self._obj.fill(data)
|
|
264
|
+
except Exception as e:
|
|
265
|
+
raise ValueError(
|
|
266
|
+
"Filling STIR ProjData with arbitrary data is not supported"
|
|
267
|
+
) from e
|
|
268
|
+
|
|
269
|
+
def clone(self) -> "StirAcquisitionData":
|
|
270
|
+
"""Clone the projection data."""
|
|
271
|
+
# ProjDataInMemory copy constructor creates a deep copy
|
|
272
|
+
cloned = stir.ProjDataInMemory(self._obj)
|
|
273
|
+
return StirAcquisitionData(cloned)
|
|
274
|
+
|
|
275
|
+
def get_uniform_copy(self, value: float) -> "StirAcquisitionData":
|
|
276
|
+
"""Create uniform copy."""
|
|
277
|
+
cloned = self.clone()
|
|
278
|
+
cloned._obj.fill(value)
|
|
279
|
+
return cloned
|
|
280
|
+
|
|
281
|
+
def dimensions(self) -> Tuple:
|
|
282
|
+
"""Get dimensions."""
|
|
283
|
+
# Extract from numpy array
|
|
284
|
+
arr = stirextra.to_numpy(self._obj)
|
|
285
|
+
return arr.shape
|
|
286
|
+
|
|
287
|
+
def as_array(self) -> np.ndarray:
|
|
288
|
+
"""Convert to numpy array using stirextra."""
|
|
289
|
+
return stirextra.to_numpy(self._obj)
|
|
290
|
+
|
|
291
|
+
def sum(self) -> float:
|
|
292
|
+
"""Sum all values."""
|
|
293
|
+
return float(self._obj.sum())
|
|
294
|
+
|
|
295
|
+
def max(self) -> float:
|
|
296
|
+
"""Return the maximum bin value."""
|
|
297
|
+
return float(np.max(self.as_array()))
|
|
298
|
+
|
|
299
|
+
def maximum(self, value: float) -> None:
|
|
300
|
+
"""Apply element-wise maximum."""
|
|
301
|
+
arr = self.as_array()
|
|
302
|
+
arr = np.maximum(arr, value)
|
|
303
|
+
self.fill(arr)
|
|
304
|
+
|
|
305
|
+
def minimum(self, value: float) -> None:
|
|
306
|
+
"""Apply element-wise minimum.
|
|
307
|
+
|
|
308
|
+
Note: STIR may not have this method directly.
|
|
309
|
+
"""
|
|
310
|
+
arr = self.as_array()
|
|
311
|
+
arr = np.minimum(arr, value)
|
|
312
|
+
self.fill(arr)
|
|
313
|
+
|
|
314
|
+
def get_info(self) -> str:
|
|
315
|
+
"""Get metadata info."""
|
|
316
|
+
# STIR doesn't have a direct get_info() string method
|
|
317
|
+
# Construct info from proj_data_info
|
|
318
|
+
info = self._obj.get_proj_data_info()
|
|
319
|
+
return str(info)
|
|
320
|
+
|
|
321
|
+
def get_energy_window_bounds(self) -> Tuple[float, float]:
|
|
322
|
+
"""Get the energy window bounds for the acquisition.
|
|
323
|
+
|
|
324
|
+
Returns:
|
|
325
|
+
Tuple[float, float]: (lower_threshold, upper_threshold) in keV
|
|
326
|
+
"""
|
|
327
|
+
exam_info = self._obj.get_exam_info()
|
|
328
|
+
lower_thres = float(exam_info.get_low_energy_thres())
|
|
329
|
+
upper_thres = float(exam_info.get_high_energy_thres())
|
|
330
|
+
return (lower_thres, upper_thres)
|
|
331
|
+
|
|
332
|
+
@property
|
|
333
|
+
def native_object(self):
|
|
334
|
+
"""Get underlying STIR object."""
|
|
335
|
+
return self._obj
|
|
336
|
+
|
|
337
|
+
def __repr__(self) -> str:
|
|
338
|
+
return f"StirAcquisitionData(dims={self.dimensions()})"
|
|
339
|
+
|
|
340
|
+
# Arithmetic operations - delegate to underlying ProjDataInMemory object
|
|
341
|
+
def __add__(self, other):
|
|
342
|
+
if isinstance(other, StirAcquisitionData):
|
|
343
|
+
return StirAcquisitionData(self._obj + other._obj)
|
|
344
|
+
else:
|
|
345
|
+
return StirAcquisitionData(self._obj + float(other))
|
|
346
|
+
|
|
347
|
+
def __sub__(self, other):
|
|
348
|
+
if isinstance(other, StirAcquisitionData):
|
|
349
|
+
return StirAcquisitionData(self._obj - other._obj)
|
|
350
|
+
else:
|
|
351
|
+
return StirAcquisitionData(self._obj - float(other))
|
|
352
|
+
|
|
353
|
+
def __mul__(self, other):
|
|
354
|
+
if isinstance(other, StirAcquisitionData):
|
|
355
|
+
return StirAcquisitionData(self._obj * other._obj)
|
|
356
|
+
else:
|
|
357
|
+
return StirAcquisitionData(self._obj * float(other))
|
|
358
|
+
|
|
359
|
+
def __truediv__(self, other):
|
|
360
|
+
if isinstance(other, StirAcquisitionData):
|
|
361
|
+
return StirAcquisitionData(self._obj / other._obj)
|
|
362
|
+
else:
|
|
363
|
+
return StirAcquisitionData(self._obj / float(other))
|
|
364
|
+
|
|
365
|
+
def __radd__(self, other):
|
|
366
|
+
return StirAcquisitionData(float(other) + self._obj)
|
|
367
|
+
|
|
368
|
+
def __rsub__(self, other):
|
|
369
|
+
return StirAcquisitionData(float(other) - self._obj)
|
|
370
|
+
|
|
371
|
+
def __rmul__(self, other):
|
|
372
|
+
return StirAcquisitionData(float(other) * self._obj)
|
|
373
|
+
|
|
374
|
+
def __rtruediv__(self, other):
|
|
375
|
+
return StirAcquisitionData(float(other) / self._obj)
|
|
376
|
+
|
|
377
|
+
def __neg__(self):
|
|
378
|
+
return StirAcquisitionData(-self._obj)
|
|
379
|
+
|
|
380
|
+
def sapyb(
|
|
381
|
+
self, a: float, y: "StirAcquisitionData", b: float
|
|
382
|
+
) -> "StirAcquisitionData":
|
|
383
|
+
"""Compute self * a + y * b (element-wise), modifying self in place.
|
|
384
|
+
|
|
385
|
+
Args:
|
|
386
|
+
a: Scalar multiplier for self
|
|
387
|
+
y: Other acquisition data
|
|
388
|
+
b: Scalar multiplier for y
|
|
389
|
+
|
|
390
|
+
Returns:
|
|
391
|
+
StirAcquisitionData: self after modification (for chaining)
|
|
392
|
+
"""
|
|
393
|
+
y_obj = y._obj if isinstance(y, StirAcquisitionData) else y
|
|
394
|
+
self._obj.sapyb(a, y_obj, b)
|
|
395
|
+
return self
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Numerical core: configuration acquisition data and image builders for SPECT.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import importlib
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
# Lazy imports to avoid SIRF dependencies in CI
|
|
9
|
+
def __getattr__(name):
|
|
10
|
+
if name == "STIRSPECTAcquisitionDataBuilder":
|
|
11
|
+
mod = importlib.import_module(".acquisition_builder", __name__)
|
|
12
|
+
return getattr(mod, "STIRSPECTAcquisitionDataBuilder")
|
|
13
|
+
elif name == "STIRSPECTImageDataBuilder":
|
|
14
|
+
mod = importlib.import_module(".image_builder", __name__)
|
|
15
|
+
return getattr(mod, "STIRSPECTImageDataBuilder")
|
|
16
|
+
raise AttributeError(f"module '{__name__}' has no attribute '{name}'")
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
__all__ = ["STIRSPECTAcquisitionDataBuilder", "STIRSPECTImageDataBuilder"]
|