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.
- microEye/__init__.py +47 -0
- microEye/_version.py +2 -0
- microEye/analysis/__init__.py +1 -0
- microEye/analysis/checklist_dialog.py +143 -0
- microEye/analysis/cmosMaps.py +228 -0
- microEye/analysis/filters/__init__.py +9 -0
- microEye/analysis/filters/base.py +21 -0
- microEye/analysis/filters/spatial.py +338 -0
- microEye/analysis/filters/temporal.py +76 -0
- microEye/analysis/fitting/__init__.py +0 -0
- microEye/analysis/fitting/fit.py +680 -0
- microEye/analysis/fitting/nena.py +375 -0
- microEye/analysis/fitting/phasor_fit.py +90 -0
- microEye/analysis/fitting/processing.py +317 -0
- microEye/analysis/fitting/psf/__init__.py +6 -0
- microEye/analysis/fitting/psf/extract.py +1129 -0
- microEye/analysis/fitting/psf/rubost_mean.py +150 -0
- microEye/analysis/fitting/psf/spline.py +167 -0
- microEye/analysis/fitting/psf/stats/__init__.py +12 -0
- microEye/analysis/fitting/psf/stats/core.py +295 -0
- microEye/analysis/fitting/psf/stats/curve_fit.py +708 -0
- microEye/analysis/fitting/psf/stats/io.py +104 -0
- microEye/analysis/fitting/psf/stats/slope_fit.py +171 -0
- microEye/analysis/fitting/psf/temp.py +147 -0
- microEye/analysis/fitting/psf/test.py +47 -0
- microEye/analysis/fitting/pyfit3Dcspline/CPU/CPUfunctions.py +657 -0
- microEye/analysis/fitting/pyfit3Dcspline/CPU/CPUmleFit_LM.py +1336 -0
- microEye/analysis/fitting/pyfit3Dcspline/CPU/CPUsplineLib.py +270 -0
- microEye/analysis/fitting/pyfit3Dcspline/CPU/__init__.py +1 -0
- microEye/analysis/fitting/pyfit3Dcspline/GPU/GPUfunctions.py +609 -0
- microEye/analysis/fitting/pyfit3Dcspline/GPU/GPUmleFit_LM_EMCCD.py +1396 -0
- microEye/analysis/fitting/pyfit3Dcspline/GPU/GPUmleFit_LM_sCMOS.py +1426 -0
- microEye/analysis/fitting/pyfit3Dcspline/GPU/GPUsplineLib.py +231 -0
- microEye/analysis/fitting/pyfit3Dcspline/GPU/__init__.py +2 -0
- microEye/analysis/fitting/pyfit3Dcspline/__init__.py +10 -0
- microEye/analysis/fitting/pyfit3Dcspline/constants.py +27 -0
- microEye/analysis/fitting/pyfit3Dcspline/mainfunctions.py +903 -0
- microEye/analysis/fitting/results.py +917 -0
- microEye/analysis/fitting/results_stats.py +251 -0
- microEye/analysis/fitting/tardis.py +209 -0
- microEye/analysis/multi_viewer.py +568 -0
- microEye/analysis/processing/__init__.py +1 -0
- microEye/analysis/processing/frc.py +120 -0
- microEye/analysis/rendering/__init__.py +3 -0
- microEye/analysis/rendering/base.py +452 -0
- microEye/analysis/rendering/cloud.py +215 -0
- microEye/analysis/rendering/core.py +47 -0
- microEye/analysis/rendering/volumetric.py +198 -0
- microEye/analysis/tools/__init__.py +0 -0
- microEye/analysis/tools/kymograms.py +1079 -0
- microEye/analysis/tools/roi_selectors.py +556 -0
- microEye/analysis/utils/__init__.py +3 -0
- microEye/analysis/utils/coordinates.py +18 -0
- microEye/analysis/utils/images.py +68 -0
- microEye/analysis/utils/windows.py +19 -0
- microEye/analysis/viewer/__init__.py +3 -0
- microEye/analysis/viewer/image_options_widget.py +615 -0
- microEye/analysis/viewer/images.py +1225 -0
- microEye/analysis/viewer/layers_widget.py +415 -0
- microEye/analysis/viewer/localizations.py +1304 -0
- microEye/analysis/viewer/psf.py +1003 -0
- microEye/analysis/viewer/volume.py +456 -0
- microEye/hardware/__init__.py +3 -0
- microEye/hardware/cams/__init__.py +29 -0
- microEye/hardware/cams/camera_calibration.py +99 -0
- microEye/hardware/cams/camera_list.py +528 -0
- microEye/hardware/cams/camera_options.py +694 -0
- microEye/hardware/cams/camera_panel.py +941 -0
- microEye/hardware/cams/dummy/__init__.py +1 -0
- microEye/hardware/cams/dummy/dummy_panel.py +759 -0
- microEye/hardware/cams/jobs.py +497 -0
- microEye/hardware/cams/line_profiler.py +99 -0
- microEye/hardware/cams/linescan/IR_Cam.py +474 -0
- microEye/hardware/cams/linescan/__init__.py +1 -0
- microEye/hardware/cams/micam.py +524 -0
- microEye/hardware/cams/pco/__init__.py +58 -0
- microEye/hardware/cams/pco/enums.py +382 -0
- microEye/hardware/cams/pco/pco_cam.py +761 -0
- microEye/hardware/cams/pco/pco_panel.py +477 -0
- microEye/hardware/cams/shortcuts.py +312 -0
- microEye/hardware/cams/thorlabs/__init__.py +1 -0
- microEye/hardware/cams/thorlabs/thorlabs.py +1508 -0
- microEye/hardware/cams/thorlabs/thorlabs_panel.py +850 -0
- microEye/hardware/cams/ueye/__init__.py +1 -0
- microEye/hardware/cams/ueye/ueye_camera.py +1023 -0
- microEye/hardware/cams/ueye/ueye_panel.py +861 -0
- microEye/hardware/cams/vimba/__init__.py +1 -0
- microEye/hardware/cams/vimba/vimba_cam.py +1000 -0
- microEye/hardware/cams/vimba/vimba_panel.py +813 -0
- microEye/hardware/device.py +60 -0
- microEye/hardware/lasers/__init__.py +13 -0
- microEye/hardware/lasers/io_matchbox.py +791 -0
- microEye/hardware/lasers/io_params.py +85 -0
- microEye/hardware/lasers/io_single_laser.py +742 -0
- microEye/hardware/lasers/laser_relay.py +594 -0
- microEye/hardware/mieye/__init__.py +1 -0
- microEye/hardware/mieye/acquisition_manager.py +467 -0
- microEye/hardware/mieye/devices_manager.py +533 -0
- microEye/hardware/mieye/miEye.py +659 -0
- microEye/hardware/misc/__init__.py +0 -0
- microEye/hardware/misc/acquisition_view.py +71 -0
- microEye/hardware/misc/reglo.py +761 -0
- microEye/hardware/misc/temp.py +188 -0
- microEye/hardware/port_config.py +59 -0
- microEye/hardware/protocols/__init__.py +2 -0
- microEye/hardware/protocols/actions.py +402 -0
- microEye/hardware/protocols/actions_items.py +703 -0
- microEye/hardware/protocols/designer.py +244 -0
- microEye/hardware/protocols/scene_manager.py +191 -0
- microEye/hardware/protocols/serialization.py +97 -0
- microEye/hardware/pycromanager/__init__.py +16 -0
- microEye/hardware/pycromanager/core.py +1433 -0
- microEye/hardware/pycromanager/devices.py +461 -0
- microEye/hardware/pycromanager/enums.py +107 -0
- microEye/hardware/pycromanager/headless.py +153 -0
- microEye/hardware/pycromanager/utils.py +34 -0
- microEye/hardware/pycromanager/widgets/__init__.py +5 -0
- microEye/hardware/pycromanager/widgets/bridges.py +407 -0
- microEye/hardware/pycromanager/widgets/headless_manager.py +258 -0
- microEye/hardware/pycromanager/widgets/headless_options.py +224 -0
- microEye/hardware/pycromanager/widgets/pycro_panel.py +455 -0
- microEye/hardware/stages/__init__.py +18 -0
- microEye/hardware/stages/elliptec/__init__.py +5 -0
- microEye/hardware/stages/elliptec/baseDevice.py +314 -0
- microEye/hardware/stages/elliptec/device.py +384 -0
- microEye/hardware/stages/elliptec/deviceID.py +212 -0
- microEye/hardware/stages/elliptec/devicePort.py +379 -0
- microEye/hardware/stages/elliptec/deviceStatus.py +65 -0
- microEye/hardware/stages/elliptec/devicesView.py +706 -0
- microEye/hardware/stages/elliptec/ellDevices.py +134 -0
- microEye/hardware/stages/elliptec/messageUpdater.py +34 -0
- microEye/hardware/stages/elliptec/motorInfo.py +153 -0
- microEye/hardware/stages/elliptec/stage.py +62 -0
- microEye/hardware/stages/elliptec/test.py +139 -0
- microEye/hardware/stages/kinesis/__init__.py +1 -0
- microEye/hardware/stages/kinesis/kdc101/__init__.py +1 -0
- microEye/hardware/stages/kinesis/kdc101/enums.py +1002 -0
- microEye/hardware/stages/kinesis/kdc101/factory.py +171 -0
- microEye/hardware/stages/kinesis/kdc101/kdc101.py +718 -0
- microEye/hardware/stages/kinesis/kinesis.py +776 -0
- microEye/hardware/stages/piezo_concept.py +607 -0
- microEye/hardware/stages/stabilizer.py +785 -0
- microEye/hardware/stages/stage.py +89 -0
- microEye/hardware/widgets/__init__.py +10 -0
- microEye/hardware/widgets/controller.py +246 -0
- microEye/hardware/widgets/devices.py +133 -0
- microEye/hardware/widgets/focusWidget.py +264 -0
- microEye/hardware/widgets/qlist_slider.py +113 -0
- microEye/hardware/widgets/scan_acquisition.py +424 -0
- microEye/icons/1024.png +0 -0
- microEye/icons/128.png +0 -0
- microEye/icons/16.png +0 -0
- microEye/icons/24.png +0 -0
- microEye/icons/256.png +0 -0
- microEye/icons/32.png +0 -0
- microEye/icons/48.png +0 -0
- microEye/icons/512.png +0 -0
- microEye/icons/64.png +0 -0
- microEye/icons/__init__.py +0 -0
- microEye/icons/close.svg +88 -0
- microEye/icons/mieye.png +0 -0
- microEye/icons/min.svg +83 -0
- microEye/icons/viewer.png +0 -0
- microEye/launcher.py +42 -0
- microEye/qt.py +181 -0
- microEye/utils/__init__.py +2 -0
- microEye/utils/enum_encoder.py +10 -0
- microEye/utils/expandable_groupbox.py +93 -0
- microEye/utils/gui_helper.py +457 -0
- microEye/utils/hid/__init__.py +8 -0
- microEye/utils/hid/controller.py +153 -0
- microEye/utils/hid/device.py +63 -0
- microEye/utils/hid/enums.py +140 -0
- microEye/utils/hid/utils.py +58 -0
- microEye/utils/labelled_slider.py +134 -0
- microEye/utils/metadata.py +599 -0
- microEye/utils/metadata_tree.py +718 -0
- microEye/utils/micro_launcher.py +245 -0
- microEye/utils/parameter_tree.py +325 -0
- microEye/utils/pyscripting.py +444 -0
- microEye/utils/retry_exec.py +35 -0
- microEye/utils/start_gui.py +112 -0
- microEye/utils/thread_worker.py +115 -0
- microEye/utils/uImage.py +1512 -0
- microeye-2.3.2.dist-info/METADATA +401 -0
- microeye-2.3.2.dist-info/RECORD +190 -0
- microeye-2.3.2.dist-info/WHEEL +5 -0
- microeye-2.3.2.dist-info/entry_points.txt +2 -0
- microeye-2.3.2.dist-info/licenses/LICENSE +674 -0
- 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)
|