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.
Files changed (47) hide show
  1. simind_python_connector/__init__.py +72 -0
  2. simind_python_connector/backends/__init__.py +480 -0
  3. simind_python_connector/backends/base.py +387 -0
  4. simind_python_connector/backends/sirf_backend.py +309 -0
  5. simind_python_connector/backends/stir_backend.py +395 -0
  6. simind_python_connector/builders/__init__.py +19 -0
  7. simind_python_connector/builders/acquisition_builder.py +526 -0
  8. simind_python_connector/builders/image_builder.py +217 -0
  9. simind_python_connector/configs/AnyScan.yaml +420 -0
  10. simind_python_connector/configs/Discovery670.yaml +412 -0
  11. simind_python_connector/configs/Example.yaml +420 -0
  12. simind_python_connector/configs/MLD001_SCAN0.yaml +426 -0
  13. simind_python_connector/configs/__init__.py +41 -0
  14. simind_python_connector/configs/input.smc +51 -0
  15. simind_python_connector/connectors/__init__.py +24 -0
  16. simind_python_connector/connectors/_spacing.py +69 -0
  17. simind_python_connector/connectors/base.py +40 -0
  18. simind_python_connector/connectors/python_connector.py +355 -0
  19. simind_python_connector/connectors/pytomography_adaptor.py +263 -0
  20. simind_python_connector/connectors/sirf_adaptor.py +164 -0
  21. simind_python_connector/connectors/stir_adaptor.py +164 -0
  22. simind_python_connector/converters/__init__.py +16 -0
  23. simind_python_connector/converters/attenuation.py +367 -0
  24. simind_python_connector/converters/dicom_to_stir.py +3 -0
  25. simind_python_connector/converters/simind_to_stir.py +769 -0
  26. simind_python_connector/core/__init__.py +7 -0
  27. simind_python_connector/core/config.py +939 -0
  28. simind_python_connector/core/executor.py +96 -0
  29. simind_python_connector/core/types.py +203 -0
  30. simind_python_connector/data/Schneider2000.json +222 -0
  31. simind_python_connector/data/__init__.py +25 -0
  32. simind_python_connector/data/bone.atn +187 -0
  33. simind_python_connector/data/h2o.atn +92 -0
  34. simind_python_connector/utils/__init__.py +120 -0
  35. simind_python_connector/utils/backend_access.py +121 -0
  36. simind_python_connector/utils/import_helpers.py +74 -0
  37. simind_python_connector/utils/interfile_numpy.py +195 -0
  38. simind_python_connector/utils/interfile_parser.py +175 -0
  39. simind_python_connector/utils/io_utils.py +14 -0
  40. simind_python_connector/utils/simind_utils.py +70 -0
  41. simind_python_connector/utils/sirf_stir_utils.py +194 -0
  42. simind_python_connector/utils/stir_utils.py +485 -0
  43. simind_python_connector-1.0.0.dist-info/METADATA +274 -0
  44. simind_python_connector-1.0.0.dist-info/RECORD +47 -0
  45. simind_python_connector-1.0.0.dist-info/WHEEL +5 -0
  46. simind_python_connector-1.0.0.dist-info/licenses/LICENSE +195 -0
  47. simind_python_connector-1.0.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,367 @@
1
+ """
2
+ Attenuation coefficient conversion utilities.
3
+
4
+ This module provides functions to convert Hounsfield Units (HU) to attenuation
5
+ coefficients and densities based on both bilinear and Schneider piecewise models.
6
+ """
7
+
8
+ import importlib.resources as pkg_resources
9
+ import json
10
+ import warnings
11
+ from pathlib import Path
12
+
13
+ import numpy as np
14
+
15
+ from simind_python_connector.data import data_path
16
+
17
+
18
+ def get_package_data_path(filename):
19
+ """Get the path to a data file in the package."""
20
+ try:
21
+ # Python 3.9+
22
+ files = pkg_resources.files("simind_python_connector.data")
23
+ return files / filename
24
+ except AttributeError:
25
+ # Python 3.8
26
+ with pkg_resources.path("simind_python_connector.data", filename) as path:
27
+ return path
28
+
29
+
30
+ def interpolate_attenuation_coefficient(filename, energy):
31
+ """Interpolate attenuation coefficient from tabulated data."""
32
+ # Skip the header lines
33
+ energies, coeffs = np.loadtxt(filename, unpack=True, skiprows=12)
34
+ return np.interp(energy, energies, coeffs)
35
+
36
+
37
+ def get_attenuation_coefficient(material, energy, file_path=None):
38
+ """
39
+ Get attenuation coefficient for a given material and energy.
40
+
41
+ Args:
42
+ material (str): 'water' or 'bone'
43
+ energy (float): Photon energy in keV
44
+ file_path (str, optional): Override default data file path
45
+
46
+ Returns:
47
+ float: Linear attenuation coefficient in cm^-1
48
+ """
49
+ density_water = 1.0 # g/cm^3
50
+ density_bone = 1.85 # g/cm^3 for cortical bone
51
+
52
+ if material == "water":
53
+ filename = data_path("h2o.atn")
54
+ elif material == "bone":
55
+ filename = data_path("bone.atn")
56
+ else:
57
+ raise ValueError("Unknown material. Accepted values are 'water' or 'bone'.")
58
+
59
+ if file_path:
60
+ filepath = Path(file_path) / filename
61
+ else:
62
+ filepath = get_package_data_path(filename)
63
+
64
+ if not filepath.exists():
65
+ raise FileNotFoundError(f"Attenuation data file not found: {filepath}")
66
+
67
+ mass_attn_coeffs = interpolate_attenuation_coefficient(filepath, energy)
68
+
69
+ if material == "water":
70
+ return mass_attn_coeffs * density_water
71
+ elif material == "bone":
72
+ return mass_attn_coeffs * density_bone
73
+
74
+
75
+ def hu_to_attenuation(image_array, photon_energy, file_path=None):
76
+ """
77
+ Convert Hounsfield Units to attenuation coefficients.
78
+
79
+ Args:
80
+ image_array (np.ndarray): Array of HU values
81
+ photon_energy (float): Photon energy in keV
82
+ file_path (str, optional): Override default data file path
83
+
84
+ Returns:
85
+ np.ndarray: Attenuation map in cm^-1
86
+ """
87
+ # Constants
88
+ HU_water = 0
89
+ HU_bone = 1000
90
+
91
+ # Convert photon_energy to MeV from keV
92
+ photon_energy_mev = photon_energy / 1000
93
+
94
+ # Get attenuation coefficients
95
+ mu_water = get_attenuation_coefficient("water", photon_energy_mev, file_path)
96
+ mu_bone = get_attenuation_coefficient("bone", photon_energy_mev, file_path)
97
+
98
+ # For air, we assume negligible attenuation
99
+ mu_air = 0.0
100
+
101
+ # Bilinear model slopes
102
+ slope_soft = (mu_water - mu_air) / (
103
+ HU_water - (-1000)
104
+ ) # from -1000 HU (air) to 0 HU (water)
105
+ slope_bone = (mu_bone - mu_water) / (
106
+ HU_bone - HU_water
107
+ ) # from 0 HU (water) to 1000 HU (bone)
108
+
109
+ # Compute attenuation map using the bilinear model
110
+ attenuation_map = np.where(
111
+ image_array <= HU_water,
112
+ mu_air + slope_soft * (image_array + 1000),
113
+ mu_water + slope_bone * (image_array - HU_water),
114
+ )
115
+
116
+ # Ensure non-negative values
117
+ attenuation_map = np.maximum(attenuation_map, 0)
118
+
119
+ return attenuation_map
120
+
121
+
122
+ def hu_to_density(image_array):
123
+ """
124
+ Convert Hounsfield Units to density values.
125
+
126
+ Args:
127
+ image_array (np.ndarray): Array of HU values
128
+
129
+ Returns:
130
+ np.ndarray: Density map in g/cm^3
131
+ """
132
+ # Constants
133
+ density_air = 0.001225 # g/cm^3
134
+ density_water = 1.0 # g/cm^3
135
+ density_bone = 1.85 # g/cm^3
136
+
137
+ HU_air = -1000
138
+ HU_water = 0
139
+ HU_bone = 1000
140
+
141
+ slope_soft = (density_water - density_air) / (HU_water - HU_air)
142
+ slope_bone = (density_bone - density_water) / (HU_bone - HU_water)
143
+
144
+ density_map = np.where(
145
+ image_array <= HU_water,
146
+ density_air + slope_soft * (image_array - HU_air),
147
+ density_water + slope_bone * (image_array - HU_water),
148
+ )
149
+
150
+ # Ensure reasonable density bounds
151
+ density_map = np.clip(density_map, 0, 3.0) # Max density ~3 g/cm^3
152
+
153
+ return density_map
154
+
155
+
156
+ def attenuation_to_density(attenuation_array, photon_energy, file_path=None):
157
+ """
158
+ Convert attenuation coefficients to density values.
159
+
160
+ This is an approximate inverse of the attenuation calculation.
161
+
162
+ Args:
163
+ attenuation_array (np.ndarray): Array of attenuation coefficients in cm^-1
164
+ photon_energy (float): Photon energy in keV
165
+ file_path (str, optional): Override default data file path
166
+
167
+ Returns:
168
+ np.ndarray: Density map in g/cm^3
169
+ """
170
+ # Convert photon_energy to MeV
171
+ photon_energy_mev = photon_energy / 1000
172
+
173
+ # Get attenuation coefficients
174
+ mu_water = get_attenuation_coefficient("water", photon_energy_mev, file_path)
175
+ mu_bone = get_attenuation_coefficient("bone", photon_energy_mev, file_path)
176
+
177
+ # Densities
178
+ # density_air = 0.001225 # g/cm^3 # Not used in this calculation
179
+ density_water = 1.0 # g/cm^3
180
+ density_bone = 1.85 # g/cm^3
181
+
182
+ # Bilinear model inverse
183
+ if mu_water > 0:
184
+ slope_soft = density_water / mu_water
185
+ else:
186
+ warnings.warn("Water attenuation coefficient is zero or negative")
187
+ slope_soft = 1.0
188
+
189
+ if mu_bone > mu_water:
190
+ slope_bone = (density_bone - density_water) / (mu_bone - mu_water)
191
+ else:
192
+ warnings.warn("Bone attenuation not greater than water")
193
+ slope_bone = 1.0
194
+
195
+ density_map = np.where(
196
+ attenuation_array <= mu_water,
197
+ slope_soft * attenuation_array,
198
+ density_water + slope_bone * (attenuation_array - mu_water),
199
+ )
200
+
201
+ # Ensure reasonable bounds
202
+ density_map = np.clip(density_map, 0, 3.0)
203
+
204
+ return density_map
205
+
206
+
207
+ def load_schneider_data():
208
+ """
209
+ Load Schneider2000 tissue data from JSON file.
210
+
211
+ Returns:
212
+ dict: Schneider tissue data with HU ranges and densities
213
+ """
214
+ try:
215
+ schneider_path = get_package_data_path("Schneider2000.json")
216
+ with open(schneider_path, "r") as f:
217
+ return json.load(f)
218
+ except FileNotFoundError:
219
+ raise FileNotFoundError(
220
+ "Schneider2000.json data file not found in package data"
221
+ )
222
+
223
+
224
+ def hu_to_density_schneider_piecewise(image_array):
225
+ """
226
+ Convert HU to density using exact Schneider2000 piecewise segments.
227
+
228
+ Simple step function: any HU value between HU_lo and HU_hi gets the
229
+ exact density for that tissue segment.
230
+
231
+ Args:
232
+ image_array (np.ndarray): Array of HU values
233
+
234
+ Returns:
235
+ np.ndarray: Density map in g/cm^3
236
+ """
237
+ schneider_data = load_schneider_data()
238
+
239
+ # Initialize output array with air density (default for unmapped values)
240
+ density_map = np.full_like(image_array, 0.001225, dtype=np.float32)
241
+
242
+ # Sort tissues by HU_lo for processing
243
+ tissues = sorted(schneider_data.items(), key=lambda x: x[1]["HU_lo (HU)"])
244
+
245
+ for tissue_name, tissue_data in tissues:
246
+ hu_lo = tissue_data["HU_lo (HU)"]
247
+ hu_hi = tissue_data["HU_hi (HU)"]
248
+ density_mg_cm3 = tissue_data["density (mg/cm3)"]
249
+ density_g_cm3 = density_mg_cm3 / 1000.0 # Convert to g/cm³
250
+
251
+ # Simple assignment: any HU in range gets this density
252
+ mask = (image_array >= hu_lo) & (image_array <= hu_hi)
253
+ density_map[mask] = density_g_cm3
254
+
255
+ return density_map
256
+
257
+
258
+ def hu_to_density_schneider(image_array):
259
+ """
260
+ Convert Hounsfield Units to density using Schneider2000 interpolated lookup table.
261
+
262
+ Uses the center point of each HU range for intelligent interpolation,
263
+ providing smooth transitions between tissue segments.
264
+
265
+ Args:
266
+ image_array (np.ndarray): Array of HU values
267
+
268
+ Returns:
269
+ np.ndarray: Density map in g/cm^3
270
+ """
271
+ schneider_data = load_schneider_data()
272
+
273
+ # Create arrays for HU center points and densities
274
+ hu_centers = []
275
+ densities = []
276
+
277
+ # Sort tissues by HU_lo to ensure proper ordering
278
+ tissues = sorted(schneider_data.items(), key=lambda x: x[1]["HU_lo (HU)"])
279
+
280
+ for tissue_name, tissue_data in tissues:
281
+ hu_lo = tissue_data["HU_lo (HU)"]
282
+ hu_hi = tissue_data["HU_hi (HU)"]
283
+ density_mg_cm3 = tissue_data["density (mg/cm3)"]
284
+ density_g_cm3 = density_mg_cm3 / 1000.0 # Convert to g/cm³
285
+
286
+ # Use center point of HU range for interpolation
287
+ hu_center = (hu_lo + hu_hi) / 2.0
288
+ hu_centers.append(hu_center)
289
+ densities.append(density_g_cm3)
290
+
291
+ # Convert to numpy arrays
292
+ hu_points = np.array(hu_centers)
293
+ density_points = np.array(densities)
294
+
295
+ # Interpolate densities for input HU values
296
+ density_map = np.interp(image_array, hu_points, density_points)
297
+
298
+ # Ensure reasonable bounds (safety check)
299
+ density_map = np.clip(density_map, 0.001, 3.0)
300
+
301
+ return density_map
302
+
303
+
304
+ def get_schneider_tissue_info(hu_value):
305
+ """
306
+ Get tissue information for a specific HU value using Schneider data.
307
+
308
+ Args:
309
+ hu_value (float): Hounsfield Unit value
310
+
311
+ Returns:
312
+ dict: Tissue information including name, density, and HU range
313
+
314
+ Example:
315
+ >>> info = get_schneider_tissue_info(50)
316
+ >>> print(f"Tissue: {info['name']}, Density: {info['density_g_cm3']:.3f} g/cm³")
317
+ """
318
+ schneider_data = load_schneider_data()
319
+
320
+ for tissue_name, tissue_data in schneider_data.items():
321
+ hu_lo = tissue_data["HU_lo (HU)"]
322
+ hu_hi = tissue_data["HU_hi (HU)"]
323
+
324
+ if hu_lo <= hu_value < hu_hi or (
325
+ hu_value == hu_hi and tissue_name.startswith("MetallImplants_43")
326
+ ):
327
+ return {
328
+ "name": tissue_name,
329
+ "density_mg_cm3": tissue_data["density (mg/cm3)"],
330
+ "density_g_cm3": tissue_data["density (mg/cm3)"] / 1000.0,
331
+ "hu_range": (hu_lo, hu_hi),
332
+ }
333
+
334
+ # If not found, return None
335
+ return None
336
+
337
+
338
+ def compare_density_methods(image_array):
339
+ """
340
+ Compare density conversion results between bilinear and Schneider methods.
341
+
342
+ Args:
343
+ image_array (np.ndarray): Array of HU values
344
+
345
+ Returns:
346
+ dict: Comparison results including density maps and statistics
347
+ """
348
+ # Compute densities using different methods
349
+ density_bilinear = hu_to_density(image_array)
350
+ density_schneider_interp = hu_to_density_schneider(image_array)
351
+ density_schneider_piecewise = hu_to_density_schneider_piecewise(image_array)
352
+
353
+ # Compute differences
354
+ diff_interp = density_schneider_interp - density_bilinear
355
+ diff_piecewise = density_schneider_piecewise - density_bilinear
356
+
357
+ return {
358
+ "bilinear": density_bilinear,
359
+ "schneider_interpolated": density_schneider_interp,
360
+ "schneider_piecewise": density_schneider_piecewise,
361
+ "difference_interpolated": diff_interp,
362
+ "difference_piecewise": diff_piecewise,
363
+ "max_diff_interp": np.max(np.abs(diff_interp)),
364
+ "max_diff_piecewise": np.max(np.abs(diff_piecewise)),
365
+ "mean_diff_interp": np.mean(np.abs(diff_interp)),
366
+ "mean_diff_piecewise": np.mean(np.abs(diff_piecewise)),
367
+ }
@@ -0,0 +1,3 @@
1
+ """
2
+ DICOM to STIR conversion is handles in the AcquisitionBuilder Class
3
+ """