microeye 2.3.2__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 (190) hide show
  1. microEye/__init__.py +47 -0
  2. microEye/_version.py +2 -0
  3. microEye/analysis/__init__.py +1 -0
  4. microEye/analysis/checklist_dialog.py +143 -0
  5. microEye/analysis/cmosMaps.py +228 -0
  6. microEye/analysis/filters/__init__.py +9 -0
  7. microEye/analysis/filters/base.py +21 -0
  8. microEye/analysis/filters/spatial.py +338 -0
  9. microEye/analysis/filters/temporal.py +76 -0
  10. microEye/analysis/fitting/__init__.py +0 -0
  11. microEye/analysis/fitting/fit.py +680 -0
  12. microEye/analysis/fitting/nena.py +375 -0
  13. microEye/analysis/fitting/phasor_fit.py +90 -0
  14. microEye/analysis/fitting/processing.py +317 -0
  15. microEye/analysis/fitting/psf/__init__.py +6 -0
  16. microEye/analysis/fitting/psf/extract.py +1129 -0
  17. microEye/analysis/fitting/psf/rubost_mean.py +150 -0
  18. microEye/analysis/fitting/psf/spline.py +167 -0
  19. microEye/analysis/fitting/psf/stats/__init__.py +12 -0
  20. microEye/analysis/fitting/psf/stats/core.py +295 -0
  21. microEye/analysis/fitting/psf/stats/curve_fit.py +708 -0
  22. microEye/analysis/fitting/psf/stats/io.py +104 -0
  23. microEye/analysis/fitting/psf/stats/slope_fit.py +171 -0
  24. microEye/analysis/fitting/psf/temp.py +147 -0
  25. microEye/analysis/fitting/psf/test.py +47 -0
  26. microEye/analysis/fitting/pyfit3Dcspline/CPU/CPUfunctions.py +657 -0
  27. microEye/analysis/fitting/pyfit3Dcspline/CPU/CPUmleFit_LM.py +1336 -0
  28. microEye/analysis/fitting/pyfit3Dcspline/CPU/CPUsplineLib.py +270 -0
  29. microEye/analysis/fitting/pyfit3Dcspline/CPU/__init__.py +1 -0
  30. microEye/analysis/fitting/pyfit3Dcspline/GPU/GPUfunctions.py +609 -0
  31. microEye/analysis/fitting/pyfit3Dcspline/GPU/GPUmleFit_LM_EMCCD.py +1396 -0
  32. microEye/analysis/fitting/pyfit3Dcspline/GPU/GPUmleFit_LM_sCMOS.py +1426 -0
  33. microEye/analysis/fitting/pyfit3Dcspline/GPU/GPUsplineLib.py +231 -0
  34. microEye/analysis/fitting/pyfit3Dcspline/GPU/__init__.py +2 -0
  35. microEye/analysis/fitting/pyfit3Dcspline/__init__.py +10 -0
  36. microEye/analysis/fitting/pyfit3Dcspline/constants.py +27 -0
  37. microEye/analysis/fitting/pyfit3Dcspline/mainfunctions.py +903 -0
  38. microEye/analysis/fitting/results.py +917 -0
  39. microEye/analysis/fitting/results_stats.py +251 -0
  40. microEye/analysis/fitting/tardis.py +209 -0
  41. microEye/analysis/multi_viewer.py +568 -0
  42. microEye/analysis/processing/__init__.py +1 -0
  43. microEye/analysis/processing/frc.py +120 -0
  44. microEye/analysis/rendering/__init__.py +3 -0
  45. microEye/analysis/rendering/base.py +452 -0
  46. microEye/analysis/rendering/cloud.py +215 -0
  47. microEye/analysis/rendering/core.py +47 -0
  48. microEye/analysis/rendering/volumetric.py +198 -0
  49. microEye/analysis/tools/__init__.py +0 -0
  50. microEye/analysis/tools/kymograms.py +1079 -0
  51. microEye/analysis/tools/roi_selectors.py +556 -0
  52. microEye/analysis/utils/__init__.py +3 -0
  53. microEye/analysis/utils/coordinates.py +18 -0
  54. microEye/analysis/utils/images.py +68 -0
  55. microEye/analysis/utils/windows.py +19 -0
  56. microEye/analysis/viewer/__init__.py +3 -0
  57. microEye/analysis/viewer/image_options_widget.py +615 -0
  58. microEye/analysis/viewer/images.py +1225 -0
  59. microEye/analysis/viewer/layers_widget.py +415 -0
  60. microEye/analysis/viewer/localizations.py +1304 -0
  61. microEye/analysis/viewer/psf.py +1003 -0
  62. microEye/analysis/viewer/volume.py +456 -0
  63. microEye/hardware/__init__.py +3 -0
  64. microEye/hardware/cams/__init__.py +29 -0
  65. microEye/hardware/cams/camera_calibration.py +99 -0
  66. microEye/hardware/cams/camera_list.py +528 -0
  67. microEye/hardware/cams/camera_options.py +694 -0
  68. microEye/hardware/cams/camera_panel.py +941 -0
  69. microEye/hardware/cams/dummy/__init__.py +1 -0
  70. microEye/hardware/cams/dummy/dummy_panel.py +759 -0
  71. microEye/hardware/cams/jobs.py +497 -0
  72. microEye/hardware/cams/line_profiler.py +99 -0
  73. microEye/hardware/cams/linescan/IR_Cam.py +474 -0
  74. microEye/hardware/cams/linescan/__init__.py +1 -0
  75. microEye/hardware/cams/micam.py +524 -0
  76. microEye/hardware/cams/pco/__init__.py +58 -0
  77. microEye/hardware/cams/pco/enums.py +382 -0
  78. microEye/hardware/cams/pco/pco_cam.py +761 -0
  79. microEye/hardware/cams/pco/pco_panel.py +477 -0
  80. microEye/hardware/cams/shortcuts.py +312 -0
  81. microEye/hardware/cams/thorlabs/__init__.py +1 -0
  82. microEye/hardware/cams/thorlabs/thorlabs.py +1508 -0
  83. microEye/hardware/cams/thorlabs/thorlabs_panel.py +850 -0
  84. microEye/hardware/cams/ueye/__init__.py +1 -0
  85. microEye/hardware/cams/ueye/ueye_camera.py +1023 -0
  86. microEye/hardware/cams/ueye/ueye_panel.py +861 -0
  87. microEye/hardware/cams/vimba/__init__.py +1 -0
  88. microEye/hardware/cams/vimba/vimba_cam.py +1000 -0
  89. microEye/hardware/cams/vimba/vimba_panel.py +813 -0
  90. microEye/hardware/device.py +60 -0
  91. microEye/hardware/lasers/__init__.py +13 -0
  92. microEye/hardware/lasers/io_matchbox.py +791 -0
  93. microEye/hardware/lasers/io_params.py +85 -0
  94. microEye/hardware/lasers/io_single_laser.py +742 -0
  95. microEye/hardware/lasers/laser_relay.py +594 -0
  96. microEye/hardware/mieye/__init__.py +1 -0
  97. microEye/hardware/mieye/acquisition_manager.py +467 -0
  98. microEye/hardware/mieye/devices_manager.py +533 -0
  99. microEye/hardware/mieye/miEye.py +659 -0
  100. microEye/hardware/misc/__init__.py +0 -0
  101. microEye/hardware/misc/acquisition_view.py +71 -0
  102. microEye/hardware/misc/reglo.py +761 -0
  103. microEye/hardware/misc/temp.py +188 -0
  104. microEye/hardware/port_config.py +59 -0
  105. microEye/hardware/protocols/__init__.py +2 -0
  106. microEye/hardware/protocols/actions.py +402 -0
  107. microEye/hardware/protocols/actions_items.py +703 -0
  108. microEye/hardware/protocols/designer.py +244 -0
  109. microEye/hardware/protocols/scene_manager.py +191 -0
  110. microEye/hardware/protocols/serialization.py +97 -0
  111. microEye/hardware/pycromanager/__init__.py +16 -0
  112. microEye/hardware/pycromanager/core.py +1433 -0
  113. microEye/hardware/pycromanager/devices.py +461 -0
  114. microEye/hardware/pycromanager/enums.py +107 -0
  115. microEye/hardware/pycromanager/headless.py +153 -0
  116. microEye/hardware/pycromanager/utils.py +34 -0
  117. microEye/hardware/pycromanager/widgets/__init__.py +5 -0
  118. microEye/hardware/pycromanager/widgets/bridges.py +407 -0
  119. microEye/hardware/pycromanager/widgets/headless_manager.py +258 -0
  120. microEye/hardware/pycromanager/widgets/headless_options.py +224 -0
  121. microEye/hardware/pycromanager/widgets/pycro_panel.py +455 -0
  122. microEye/hardware/stages/__init__.py +18 -0
  123. microEye/hardware/stages/elliptec/__init__.py +5 -0
  124. microEye/hardware/stages/elliptec/baseDevice.py +314 -0
  125. microEye/hardware/stages/elliptec/device.py +384 -0
  126. microEye/hardware/stages/elliptec/deviceID.py +212 -0
  127. microEye/hardware/stages/elliptec/devicePort.py +379 -0
  128. microEye/hardware/stages/elliptec/deviceStatus.py +65 -0
  129. microEye/hardware/stages/elliptec/devicesView.py +706 -0
  130. microEye/hardware/stages/elliptec/ellDevices.py +134 -0
  131. microEye/hardware/stages/elliptec/messageUpdater.py +34 -0
  132. microEye/hardware/stages/elliptec/motorInfo.py +153 -0
  133. microEye/hardware/stages/elliptec/stage.py +62 -0
  134. microEye/hardware/stages/elliptec/test.py +139 -0
  135. microEye/hardware/stages/kinesis/__init__.py +1 -0
  136. microEye/hardware/stages/kinesis/kdc101/__init__.py +1 -0
  137. microEye/hardware/stages/kinesis/kdc101/enums.py +1002 -0
  138. microEye/hardware/stages/kinesis/kdc101/factory.py +171 -0
  139. microEye/hardware/stages/kinesis/kdc101/kdc101.py +718 -0
  140. microEye/hardware/stages/kinesis/kinesis.py +776 -0
  141. microEye/hardware/stages/piezo_concept.py +607 -0
  142. microEye/hardware/stages/stabilizer.py +785 -0
  143. microEye/hardware/stages/stage.py +89 -0
  144. microEye/hardware/widgets/__init__.py +10 -0
  145. microEye/hardware/widgets/controller.py +246 -0
  146. microEye/hardware/widgets/devices.py +133 -0
  147. microEye/hardware/widgets/focusWidget.py +264 -0
  148. microEye/hardware/widgets/qlist_slider.py +113 -0
  149. microEye/hardware/widgets/scan_acquisition.py +424 -0
  150. microEye/icons/1024.png +0 -0
  151. microEye/icons/128.png +0 -0
  152. microEye/icons/16.png +0 -0
  153. microEye/icons/24.png +0 -0
  154. microEye/icons/256.png +0 -0
  155. microEye/icons/32.png +0 -0
  156. microEye/icons/48.png +0 -0
  157. microEye/icons/512.png +0 -0
  158. microEye/icons/64.png +0 -0
  159. microEye/icons/__init__.py +0 -0
  160. microEye/icons/close.svg +88 -0
  161. microEye/icons/mieye.png +0 -0
  162. microEye/icons/min.svg +83 -0
  163. microEye/icons/viewer.png +0 -0
  164. microEye/launcher.py +42 -0
  165. microEye/qt.py +181 -0
  166. microEye/utils/__init__.py +2 -0
  167. microEye/utils/enum_encoder.py +10 -0
  168. microEye/utils/expandable_groupbox.py +93 -0
  169. microEye/utils/gui_helper.py +457 -0
  170. microEye/utils/hid/__init__.py +8 -0
  171. microEye/utils/hid/controller.py +153 -0
  172. microEye/utils/hid/device.py +63 -0
  173. microEye/utils/hid/enums.py +140 -0
  174. microEye/utils/hid/utils.py +58 -0
  175. microEye/utils/labelled_slider.py +134 -0
  176. microEye/utils/metadata.py +599 -0
  177. microEye/utils/metadata_tree.py +718 -0
  178. microEye/utils/micro_launcher.py +245 -0
  179. microEye/utils/parameter_tree.py +325 -0
  180. microEye/utils/pyscripting.py +444 -0
  181. microEye/utils/retry_exec.py +35 -0
  182. microEye/utils/start_gui.py +112 -0
  183. microEye/utils/thread_worker.py +115 -0
  184. microEye/utils/uImage.py +1512 -0
  185. microeye-2.3.2.dist-info/METADATA +401 -0
  186. microeye-2.3.2.dist-info/RECORD +190 -0
  187. microeye-2.3.2.dist-info/WHEEL +5 -0
  188. microeye-2.3.2.dist-info/entry_points.txt +2 -0
  189. microeye-2.3.2.dist-info/licenses/LICENSE +674 -0
  190. microeye-2.3.2.dist-info/top_level.txt +1 -0
@@ -0,0 +1,452 @@
1
+ from enum import Enum
2
+
3
+ import numba
4
+ import numpy as np
5
+
6
+ from microEye.analysis.rendering.core import model
7
+
8
+
9
+ @numba.njit()
10
+ def render_compute(data, step, gauss_2d, out_img):
11
+ for x, y, Intensity in data:
12
+ out_img[y - step : y + step + 1, x - step : x + step + 1] += (
13
+ Intensity * gauss_2d
14
+ )
15
+
16
+
17
+ @numba.njit()
18
+ def render_compute_projection(data, step_lateral, step_axial, gauss_2d, out_img):
19
+ for lateral, axial, Intensity in data:
20
+ out_img[
21
+ axial - step_axial : axial + step_axial + 1,
22
+ lateral - step_lateral : lateral + step_lateral + 1,
23
+ ] += Intensity * gauss_2d
24
+
25
+
26
+ @numba.njit()
27
+ def filter_points_in_range(coords, values, center, half_width):
28
+ '''Filter points within a range along one dimension.'''
29
+ mask = (coords >= center - half_width) & (coords <= center + half_width)
30
+ return mask
31
+
32
+
33
+ class RenderModes(Enum):
34
+ '''
35
+ Enum class for the rendering modes.
36
+ '''
37
+
38
+ HISTOGRAM = 0
39
+ '''Intensity Histogram'''
40
+ EVENT_HISTOGRAM = 1
41
+ '''Event Histogram'''
42
+ GAUSSIAN = 2
43
+ '''Gaussian Rendering'''
44
+
45
+
46
+ class Projection(Enum):
47
+ '''
48
+ Enum class for different projections.
49
+ '''
50
+
51
+ XY = 0
52
+ '''XY Projection'''
53
+ XZ = 1
54
+ '''XZ Projection'''
55
+ YZ = 2
56
+ '''YZ Projection'''
57
+
58
+
59
+ class BaseRenderer:
60
+ '''
61
+ Base class for rendering super resolution images
62
+ from single molecule localizations.
63
+ '''
64
+
65
+ def __init__(self, pixel_size=10, z_pixel_size=None, mode=RenderModes.HISTOGRAM):
66
+ '''
67
+ Initializes the renderer.
68
+
69
+ Parameters:
70
+ -----------
71
+ pixel_size : float
72
+ Pixel size of the rendered image in lateral dimensions (XY).
73
+ z_pixel_size : float, optional
74
+ Pixel size for axial dimension (Z). If None, uses pixel_size.
75
+ mode : RenderModes
76
+ Rendering mode.
77
+ '''
78
+ self._pixel_size = pixel_size
79
+ self._z_pixel_size = z_pixel_size if z_pixel_size is not None else pixel_size
80
+ self._mode = mode
81
+ self._image = None
82
+ self._origin = None
83
+
84
+ if self._mode == RenderModes.GAUSSIAN:
85
+ self.generate_gaussian_kernels()
86
+
87
+ def generate_gaussian_kernels(self):
88
+ '''
89
+ Generates Gaussian kernels for rendering both lateral and axial projections.
90
+ '''
91
+ # Lateral kernel (XY)
92
+ self._std = self._pixel_size # nm
93
+ self._gauss_std = self._std / self._pixel_size
94
+ self._gauss_len = 1 + np.ceil(self._gauss_std * 6)
95
+ if self._gauss_len % 2 == 0:
96
+ self._gauss_len += 1
97
+ self._gauss_shape = [int(self._gauss_len)] * 2
98
+
99
+ # Axial kernel (Z)
100
+ self._z_std = self._z_pixel_size # nm
101
+ self._z_gauss_std = self._z_std / self._z_pixel_size
102
+ self._z_gauss_len = 1 + np.ceil(self._z_gauss_std * 6)
103
+ if self._z_gauss_len % 2 == 0:
104
+ self._z_gauss_len += 1
105
+
106
+ # Generate kernels for different projections
107
+ xy_len = np.arange(0, self._gauss_shape[0])
108
+ X, Y = np.meshgrid(xy_len, xy_len)
109
+
110
+ # Standard XY kernel
111
+ self._gauss_2d = model(
112
+ (self._gauss_len - 1) / 2,
113
+ (self._gauss_len - 1) / 2,
114
+ self._gauss_std,
115
+ self._gauss_std,
116
+ 1,
117
+ 0,
118
+ X,
119
+ Y,
120
+ )
121
+
122
+ # XZ/YZ projection kernel (asymmetric)
123
+ xz_len_lateral = np.arange(0, self._gauss_len)
124
+ xz_len_axial = np.arange(0, self._z_gauss_len)
125
+ X_proj, Z_proj = np.meshgrid(xz_len_lateral, xz_len_axial)
126
+
127
+ self._gauss_2d_projection = model(
128
+ (self._gauss_len - 1) / 2,
129
+ (self._z_gauss_len - 1) / 2,
130
+ self._gauss_std,
131
+ self._z_gauss_std,
132
+ 1,
133
+ 0,
134
+ X_proj,
135
+ Z_proj,
136
+ )
137
+
138
+ def _validate_inputs(self, X, Y, Intensity):
139
+ '''
140
+ Validate the inputs for rendering.
141
+ '''
142
+ if any([X is None, Y is None, Intensity is None]):
143
+ raise Exception('One or more of the inputs are None.')
144
+ if not len(X) == len(Y) == len(Intensity):
145
+ raise Exception('The supplied arguments are of different lengths.')
146
+
147
+ def _zero_origin(self, X, Y):
148
+ '''
149
+ Zero the origin of the coordinates.
150
+ '''
151
+ x_min = np.min(X)
152
+ y_min = np.min(Y)
153
+
154
+ if x_min < 0:
155
+ X = X - x_min
156
+ if y_min < 0:
157
+ Y = Y - y_min
158
+
159
+ return X, Y
160
+
161
+ def render(
162
+ self,
163
+ projection: Projection,
164
+ X,
165
+ Y,
166
+ Z,
167
+ Intensity,
168
+ shape=None,
169
+ ):
170
+ '''
171
+ Renders super resolution image from single
172
+ molecule localizations.
173
+
174
+ Parameters
175
+ ----------
176
+ projection : Projection
177
+ Projection type
178
+ X, Y, Z : np.ndarray
179
+ Coordinates of localizations
180
+ Intensity : np.ndarray
181
+ Intensity values
182
+ shape : tuple[int, int], optional
183
+ Output image shape (height, width)
184
+
185
+ Returns
186
+ -------
187
+ np.ndarray
188
+ Rendered image
189
+ '''
190
+ if projection == Projection.XY:
191
+ return self.render_xy(X, Y, Intensity, shape)
192
+ elif projection == Projection.XZ:
193
+ return self.render_xz(X, Z, Intensity, shape)
194
+ elif projection == Projection.YZ:
195
+ return self.render_yz(Y, Z, Intensity, shape)
196
+ else:
197
+ raise ValueError('Invalid projection type.')
198
+
199
+ def render_xy(self, X, Y, Intensity, shape=None):
200
+ '''
201
+ Renders super resolution image (XY Projection) from single
202
+ molecule localizations.
203
+
204
+
205
+ Params
206
+ -------
207
+ X (np.ndarray)
208
+ Sub-pixel localized points X coordinates
209
+ Y (np.ndarray)
210
+ Sub-pixel localized points Y coordinates
211
+ Intensity (np.ndarray)
212
+ Sub-pixel localized points intensity estimate
213
+ shape (tuple[int, int], optional)
214
+ Super-res image (height, width), by default None
215
+
216
+ Returns
217
+ -------
218
+ Image (np.ndarray)
219
+ the rendered 2D super-res image array
220
+ '''
221
+ self._validate_inputs(X, Y, Intensity)
222
+ X, Y = self._zero_origin(X, Y)
223
+
224
+ if self._mode in [RenderModes.HISTOGRAM, RenderModes.EVENT_HISTOGRAM]:
225
+ if shape is None:
226
+ x_max = int((np.max(X) / self._pixel_size) + 4)
227
+ y_max = int((np.max(Y) / self._pixel_size) + 4)
228
+ else:
229
+ x_max, y_max = shape[1], shape[0]
230
+ n_max = max(x_max, y_max)
231
+
232
+ self._image = np.zeros([n_max, n_max])
233
+ X = np.round(X / self._pixel_size) + 2
234
+ Y = np.round(Y / self._pixel_size) + 2
235
+
236
+ if self._mode == RenderModes.EVENT_HISTOGRAM:
237
+ Intensity = np.ones_like(Intensity)
238
+ render_compute(np.c_[X, Y, Intensity], 0, 1, self._image)
239
+ elif self._mode == RenderModes.GAUSSIAN:
240
+ if shape is None:
241
+ x_max = int((np.max(X) / self._pixel_size) + 4 * self._gauss_len)
242
+ y_max = int((np.max(Y) / self._pixel_size) + 4 * self._gauss_len)
243
+ else:
244
+ x_max, y_max = shape[1], shape[0]
245
+ n_max = max(x_max, y_max)
246
+
247
+ step = int((self._gauss_len - 1) // 2)
248
+ self._image = np.zeros([n_max, n_max])
249
+ X = np.round(X / self._pixel_size) + 4 * step
250
+ Y = np.round(Y / self._pixel_size) + 4 * step
251
+
252
+ render_compute(np.c_[X, Y, Intensity], step, self._gauss_2d, self._image)
253
+
254
+ return self._image
255
+
256
+ def render_xz(self, X, Z, Intensity, shape=None):
257
+ '''
258
+ Renders XZ projection of super resolution image.
259
+
260
+ Parameters
261
+ ----------
262
+ X : np.ndarray
263
+ X coordinates
264
+ Z : np.ndarray
265
+ Z coordinates
266
+ Intensity : np.ndarray
267
+ Intensity values
268
+ shape : tuple[int, int], optional
269
+ Output image shape (z_height, x_width)
270
+
271
+ Returns
272
+ -------
273
+ np.ndarray
274
+ Rendered XZ projection
275
+ '''
276
+ self._validate_inputs(X, Z, Intensity)
277
+ X, Z = self._zero_origin(X, Z)
278
+
279
+ if self._mode in [RenderModes.HISTOGRAM, RenderModes.EVENT_HISTOGRAM]:
280
+ if shape is None:
281
+ x_max = int((np.max(X) / self._pixel_size) + 4)
282
+ z_max = int((np.max(Z) / self._z_pixel_size) + 4)
283
+ else:
284
+ x_max, z_max = shape[1], shape[0]
285
+
286
+ self._image = np.zeros([z_max, x_max])
287
+ X = np.round(X / self._pixel_size) + 2
288
+ Z = np.round(Z / self._z_pixel_size) + 2
289
+
290
+ if self._mode == RenderModes.EVENT_HISTOGRAM:
291
+ Intensity = np.ones_like(Intensity)
292
+ render_compute(np.c_[X, Z, Intensity], 0, 1, self._image)
293
+
294
+ elif self._mode == RenderModes.GAUSSIAN:
295
+ if shape is None:
296
+ x_max = int((np.max(X) / self._pixel_size) + 4 * self._gauss_len)
297
+ z_max = int((np.max(Z) / self._z_pixel_size) + 4 * self._z_gauss_len)
298
+ else:
299
+ x_max, z_max = shape[1], shape[0]
300
+
301
+ step_lateral = int((self._gauss_len - 1) // 2)
302
+ step_axial = int((self._z_gauss_len - 1) // 2)
303
+
304
+ self._image = np.zeros([z_max, x_max])
305
+ X = np.round(X / self._pixel_size) + 4 * step_lateral
306
+ Z = np.round(Z / self._z_pixel_size) + 4 * step_axial
307
+
308
+ render_compute_projection(
309
+ np.c_[X, Z, Intensity],
310
+ step_lateral,
311
+ step_axial,
312
+ self._gauss_2d_projection,
313
+ self._image,
314
+ )
315
+
316
+ return self._image
317
+
318
+ def render_yz(self, Y, Z, Intensity, shape=None):
319
+ '''
320
+ Renders YZ projection of super resolution image.
321
+
322
+ Parameters
323
+ ----------
324
+ Y : np.ndarray
325
+ Y coordinates
326
+ Z : np.ndarray
327
+ Z coordinates
328
+ Intensity : np.ndarray
329
+ Intensity values
330
+ shape : tuple[int, int], optional
331
+ Output image shape (z_height, y_width)
332
+
333
+ Returns
334
+ -------
335
+ np.ndarray
336
+ Rendered YZ projection
337
+ '''
338
+ return self.render_xz(Y, Z, Intensity, shape)
339
+
340
+ def render_slice(
341
+ self,
342
+ projection: Projection,
343
+ X,
344
+ Y,
345
+ Z,
346
+ Intensity,
347
+ position,
348
+ width=None,
349
+ shape=None,
350
+ ):
351
+ '''
352
+ Renders selected projection image at specific othogonal position
353
+ with given bin width.
354
+
355
+ Parameters
356
+ ----------
357
+ projection : Projection
358
+ Projection type
359
+ X, Y, Z : np.ndarray
360
+ Coordinates of localizations
361
+ Intensity : np.ndarray
362
+ Intensity values
363
+ position : float
364
+ Position of the slice in the orthogonal axis
365
+ width : float, optional
366
+ Width of the slice, by default None
367
+ shape : tuple[int, int], optional
368
+ Output image shape (height, width)
369
+
370
+ Returns
371
+ -------
372
+ np.ndarray
373
+ Rendered XY slice at specified Z position
374
+ '''
375
+ # Validate inputs
376
+ if not len(X) == len(Y) == len(Z) == len(Intensity):
377
+ raise Exception('The supplied arguments are of different lengths.')
378
+
379
+ # Set slice width
380
+ if width is None:
381
+ if projection == Projection.XY:
382
+ width = self._z_pixel_size
383
+ else:
384
+ width = self._pixel_size
385
+
386
+ # Filter points within Z range
387
+ half_width = width / 2
388
+ if projection == Projection.XY:
389
+ mask = filter_points_in_range(Z, Intensity, position, half_width)
390
+
391
+ if not np.any(mask):
392
+ return None
393
+
394
+ # Only render points within the Z range
395
+ X_filtered = X[mask]
396
+ Y_filtered = Y[mask]
397
+ I_filtered = Intensity[mask]
398
+
399
+ # Render the filtered points using existing render method
400
+ return self.render_xy(X_filtered, Y_filtered, I_filtered, shape)
401
+ elif projection == Projection.XZ:
402
+ mask = filter_points_in_range(Y, Intensity, position, half_width)
403
+
404
+ if not np.any(mask):
405
+ return None
406
+
407
+ # Filter points within Y range
408
+ half_width = width / 2
409
+ y_mask = filter_points_in_range(Y, Intensity, position, half_width)
410
+
411
+ # Only render points within the Y range
412
+ X_filtered = X[y_mask]
413
+ Z_filtered = Z[y_mask]
414
+ I_filtered = Intensity[y_mask]
415
+
416
+ return self.render_xz(X_filtered, Z_filtered, I_filtered, shape)
417
+ elif projection == Projection.YZ:
418
+ mask = filter_points_in_range(X, Intensity, position, half_width)
419
+
420
+ if not np.any(mask):
421
+ return None
422
+
423
+ # Filter points within X range
424
+ half_width = width / 2
425
+ x_mask = filter_points_in_range(X, Intensity, position, half_width)
426
+
427
+ # Only render points within the X range
428
+ Y_filtered = Y[x_mask]
429
+ Z_filtered = Z[x_mask]
430
+ I_filtered = Intensity[x_mask]
431
+
432
+ return self.render_yz(Y_filtered, Z_filtered, I_filtered, shape)
433
+ else:
434
+ raise ValueError('Invalid projection type.')
435
+
436
+ def from_array(self, data: np.ndarray, shape=None):
437
+ '''Renders as super resolution image from
438
+ single molecule localizations.
439
+
440
+ Params
441
+ -------
442
+ data (np.ndarray)
443
+ Array with sub-pixel localization data columns (X, Y, Intensity)
444
+ shape (tuple[int, int], optional)
445
+ Super-res image (height, width), by default None
446
+
447
+ Returns
448
+ -------
449
+ Image (np.ndarray)
450
+ the rendered 2D super-res image array
451
+ '''
452
+ return self.render_xy(data[:, 0], data[:, 1], data[:, 2], shape)
@@ -0,0 +1,215 @@
1
+ import numba
2
+ import numpy as np
3
+
4
+
5
+ @numba.jit(nopython=True)
6
+ def compute_point_cloud_histogram(data, bin_edges_x, bin_edges_y, bin_edges_z):
7
+ '''
8
+ Compute the 3D histogram using numba for acceleration.
9
+ Returns coordinates and intensities of non-zero bins.
10
+ '''
11
+ # Initialize arrays to store results
12
+ max_points = len(data) # Maximum possible number of points
13
+ points = np.zeros((max_points, 3), dtype=np.float64)
14
+ intensities = np.zeros(max_points, dtype=np.float64)
15
+ point_count = 0
16
+
17
+ # Create temporary volume for binning
18
+ hist = np.zeros((len(bin_edges_z) - 1, len(bin_edges_y) - 1, len(bin_edges_x) - 1))
19
+
20
+ # Accumulate points into bins
21
+ for x, y, z, intensity in data:
22
+ # Find bin indices
23
+ x_idx = np.searchsorted(bin_edges_x, x) - 1
24
+ y_idx = np.searchsorted(bin_edges_y, y) - 1
25
+ z_idx = np.searchsorted(bin_edges_z, z) - 1
26
+
27
+ # Check if point is within bounds
28
+ if (
29
+ 0 <= x_idx < len(bin_edges_x) - 1
30
+ and 0 <= y_idx < len(bin_edges_y) - 1
31
+ and 0 <= z_idx < len(bin_edges_z) - 1
32
+ ):
33
+ hist[z_idx, y_idx, x_idx] += intensity
34
+
35
+ # Extract non-zero bins
36
+ for z_idx in range(len(bin_edges_z) - 1):
37
+ for y_idx in range(len(bin_edges_y) - 1):
38
+ for x_idx in range(len(bin_edges_x) - 1):
39
+ if hist[z_idx, y_idx, x_idx] > 0:
40
+ # Calculate center coordinates of the bin
41
+ x_center = (bin_edges_x[x_idx] + bin_edges_x[x_idx + 1]) / 2
42
+ y_center = (bin_edges_y[y_idx] + bin_edges_y[y_idx + 1]) / 2
43
+ z_center = (bin_edges_z[z_idx] + bin_edges_z[z_idx + 1]) / 2
44
+
45
+ points[point_count] = np.array([x_center, y_center, z_center])
46
+ intensities[point_count] = hist[z_idx, y_idx, x_idx]
47
+ point_count += 1
48
+
49
+ return points[:point_count], intensities[:point_count]
50
+
51
+
52
+ class PointCloudRenderer:
53
+ '''
54
+ Point cloud renderer for rendering 3D super resolution images as binned point clouds
55
+ '''
56
+
57
+ def __init__(self, xy_bin_size=10, z_bin_size=50):
58
+ '''
59
+ Initialize the point cloud renderer.
60
+
61
+ Parameters:
62
+ -----------
63
+ xy_bin_size : float
64
+ Size of each bin in the lateral (XY) dimensions
65
+ z_bin_size : float
66
+ Size of each bin in the axial (Z) dimension
67
+ '''
68
+ self._xy_bin_size = xy_bin_size
69
+ self._z_bin_size = z_bin_size
70
+ self._points = None
71
+ self._intensities = None
72
+
73
+ def _validate_inputs(self, X, Y, Z, Intensity):
74
+ '''
75
+ Validate the inputs for rendering.
76
+ '''
77
+ if not len(X) == len(Y) == len(Z) == len(Intensity):
78
+ raise ValueError('The supplied coordinate arrays are of different lengths.')
79
+
80
+ def _normalize_xy(self, X, Y):
81
+ '''
82
+ Normalize XY coordinates to start from zero.
83
+ '''
84
+ x_min, y_min = np.min(X), np.min(Y)
85
+
86
+ if x_min < 0:
87
+ X = X - x_min
88
+ if y_min < 0:
89
+ Y = Y - y_min
90
+
91
+ return X, Y
92
+
93
+ def render(self, X, Y, Z, Intensity, shape=None):
94
+ '''
95
+ Generates a point cloud from 3D single-molecule localizations using binning.
96
+
97
+ Parameters:
98
+ -----------
99
+ X : np.ndarray
100
+ Array of X coordinates for the localizations.
101
+ Y : np.ndarray
102
+ Array of Y coordinates for the localizations.
103
+ Z : np.ndarray
104
+ Array of Z coordinates for the localizations (preserving the zero reference)
105
+ Intensity : np.ndarray
106
+ Array of intensity values corresponding to each localization.
107
+ shape : tuple[int, int, int], optional
108
+ Number of bins in each dimension as (depth, height, width).
109
+ Defaults to None.
110
+
111
+ Returns:
112
+ --------
113
+ points : np.ndarray
114
+ Nx3 array of point coordinates representing non-zero bins
115
+ intensities : np.ndarray
116
+ N array of intensity values for each point
117
+ metadata : dict
118
+ Dictionary containing bin sizes and coordinate ranges
119
+ '''
120
+ self._validate_inputs(X, Y, Z, Intensity)
121
+ X, Y = self._normalize_xy(X, Y)
122
+
123
+ # Calculate bin edges
124
+ if shape is None:
125
+ x_max = np.max(X) + 2 * self._xy_bin_size
126
+ y_max = np.max(Y) + 2 * self._xy_bin_size
127
+
128
+ # For Z, calculate number of bins needed on each side of zero
129
+ z_min, z_max = np.min(Z), np.max(Z)
130
+
131
+ # Handle cases where all values fall within one bin centered at zero
132
+ if -self._z_bin_size / 2 <= z_min and z_max <= self._z_bin_size / 2:
133
+ n_bins_below = 1
134
+ n_bins_above = (
135
+ 1 # Create one bin centered at zero [-bin_size/2, +bin_size/2]
136
+ )
137
+ else:
138
+ # Calculate bins needed on each side, accounting for half bins
139
+ n_bins_below = int(
140
+ np.ceil((-z_min + self._z_bin_size / 2) / self._z_bin_size)
141
+ )
142
+ n_bins_above = int(
143
+ np.ceil((z_max + self._z_bin_size / 2) / self._z_bin_size)
144
+ )
145
+
146
+ n_bins_x = int(np.ceil(x_max / self._xy_bin_size))
147
+ n_bins_y = int(np.ceil(y_max / self._xy_bin_size))
148
+ n_bins_z = n_bins_below + n_bins_above
149
+ shape = (n_bins_z, n_bins_y, n_bins_x)
150
+
151
+ # Create bin edges
152
+ bin_edges_x = np.linspace(0, shape[2] * self._xy_bin_size, shape[2] + 1)
153
+ bin_edges_y = np.linspace(0, shape[1] * self._xy_bin_size, shape[1] + 1)
154
+ bin_edges_z = np.arange(
155
+ -n_bins_below * self._z_bin_size
156
+ - self._z_bin_size / 2, # start at first bin edge
157
+ (n_bins_above + 1) * self._z_bin_size
158
+ - self._z_bin_size / 2, # end at last bin edge
159
+ self._z_bin_size,
160
+ )
161
+
162
+ # Compute point cloud histogram
163
+ points, intensities = compute_point_cloud_histogram(
164
+ np.c_[X, Y, Z, Intensity], bin_edges_x, bin_edges_y, bin_edges_z
165
+ )
166
+
167
+ self._points = points
168
+ self._intensities = intensities
169
+
170
+ metadata = {
171
+ 'bin_size': {
172
+ 'x': self._xy_bin_size,
173
+ 'y': self._xy_bin_size,
174
+ 'z': self._z_bin_size,
175
+ },
176
+ 'coordinates': {
177
+ 'z_min': bin_edges_z[0],
178
+ 'z_max': bin_edges_z[-1],
179
+ },
180
+ 'point_count': len(points),
181
+ }
182
+
183
+ return points, intensities, metadata
184
+
185
+ def from_array(self, data: np.ndarray, shape=None):
186
+ '''
187
+ Renders a point cloud from an array of localizations.
188
+
189
+ Parameters:
190
+ -----------
191
+ data : np.ndarray
192
+ Array with columns (X, Y, Z, Intensity)
193
+ shape : tuple[int, int, int], optional
194
+ Number of bins in each dimension (depth, height, width), by default None
195
+
196
+ Returns:
197
+ --------
198
+ points : np.ndarray
199
+ Nx3 array of point coordinates
200
+ intensities : np.ndarray
201
+ N array of intensity values
202
+ metadata : dict
203
+ Dictionary containing bin sizes and coordinate ranges
204
+ '''
205
+ return self.render(data[:, 0], data[:, 1], data[:, 2], data[:, 3], shape)
206
+
207
+
208
+ @numba.jit(nopython=True)
209
+ def normalize_intensities(intensities):
210
+ '''
211
+ Normalize intensity values to range [0,1] using Numba acceleration.
212
+ '''
213
+ min_val = np.min(intensities)
214
+ max_val = np.max(intensities)
215
+ return (intensities - min_val) / (max_val - min_val)
@@ -0,0 +1,47 @@
1
+ import numba
2
+ import numpy as np
3
+
4
+
5
+ def model(xc, yc, sigma_x, sigma_y, flux, offset, X, Y):
6
+ '''
7
+ 2D Gaussian model function.
8
+
9
+ Parameters
10
+ ----------
11
+ xc : float
12
+ x-coordinate of the center
13
+ yc : float
14
+ y-coordinate of the center
15
+ sigma_x : float
16
+ Standard deviation in x-direction
17
+ sigma_y : float
18
+ Standard deviation in y-direction
19
+ flux : float
20
+ Total flux
21
+ offset : float
22
+ Offset
23
+ X : np.ndarray
24
+ X-coordinate grid
25
+ Y : np.ndarray
26
+ Y-coordinate grid
27
+ '''
28
+ y_gauss = gauss_1d(Y[:, 0], yc, sigma_y)
29
+ x_gauss = gauss_1d(X[0, :], xc, sigma_x)
30
+ return flux * np.einsum('i,j->ij', y_gauss, x_gauss) + offset
31
+
32
+ @numba.njit(cache=True)
33
+ def gauss_1d(x: np.ndarray, mu: np.ndarray, sigma: np.ndarray):
34
+ '''
35
+ 1D Gaussian function.
36
+
37
+ Parameters
38
+ ----------
39
+ x : np.ndarray
40
+ x-coordinate
41
+ mu : float
42
+ Mean
43
+ sigma : float
44
+ Standard deviation
45
+ '''
46
+ return 1 / (np.sqrt(2 * np.pi) * sigma) * \
47
+ np.exp(-0.5 * (x - mu)**2 / sigma**2)