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,456 @@
|
|
|
1
|
+
import contextlib
|
|
2
|
+
|
|
3
|
+
import numpy as np
|
|
4
|
+
import pyqtgraph as pg
|
|
5
|
+
import pyqtgraph.opengl as gl
|
|
6
|
+
|
|
7
|
+
from microEye.analysis.rendering.cloud import normalize_intensities
|
|
8
|
+
from microEye.analysis.rendering.volumetric import (
|
|
9
|
+
create_rgba_volume,
|
|
10
|
+
normalize_volume,
|
|
11
|
+
)
|
|
12
|
+
from microEye.qt import Qt, QtGui, QtWidgets
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class VolumeViewerWindow(QtWidgets.QWidget):
|
|
16
|
+
def __init__(self, volume_data: np.ndarray = None, metadata=None):
|
|
17
|
+
super().__init__()
|
|
18
|
+
self.setWindowTitle('3D Volume Viewer')
|
|
19
|
+
|
|
20
|
+
# Store data and metadata
|
|
21
|
+
self.volume_data = volume_data
|
|
22
|
+
self.normalized_data = None
|
|
23
|
+
self.metadata = (
|
|
24
|
+
metadata
|
|
25
|
+
if metadata is not None
|
|
26
|
+
else {
|
|
27
|
+
'voxel_size': {'x': 1, 'y': 1, 'z': 1},
|
|
28
|
+
'coordinates': {'z_min': 0, 'z_max': 1},
|
|
29
|
+
}
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
# Create the main widget and layout
|
|
33
|
+
self.setLayout(QtWidgets.QHBoxLayout())
|
|
34
|
+
|
|
35
|
+
# Create 3D viewport
|
|
36
|
+
self.init_3d_viewport()
|
|
37
|
+
|
|
38
|
+
# Create control panel
|
|
39
|
+
self.init_control_panel()
|
|
40
|
+
|
|
41
|
+
# Set window size
|
|
42
|
+
self.resize(1200, 800)
|
|
43
|
+
|
|
44
|
+
# Initialize volume visualization if data is provided
|
|
45
|
+
if volume_data is not None:
|
|
46
|
+
self.set_volume_data(volume_data, metadata)
|
|
47
|
+
|
|
48
|
+
def init_3d_viewport(self):
|
|
49
|
+
'''Initialize the 3D viewport with PyQtGraph GLViewWidget.'''
|
|
50
|
+
self.view = gl.GLViewWidget()
|
|
51
|
+
self.layout().addWidget(self.view, stretch=4)
|
|
52
|
+
|
|
53
|
+
self.axis = gl.GLAxisItem()
|
|
54
|
+
self.axis.setSize(x=200, y=200, z=200)
|
|
55
|
+
self.view.addItem(self.axis)
|
|
56
|
+
|
|
57
|
+
self.grid = gl.GLGridItem()
|
|
58
|
+
self.grid.setSize(x=200, y=200)
|
|
59
|
+
self.grid.translate(dx=0, dy=0, dz=0)
|
|
60
|
+
self.view.addItem(self.grid)
|
|
61
|
+
|
|
62
|
+
self.volume_item = None
|
|
63
|
+
|
|
64
|
+
def init_control_panel(self):
|
|
65
|
+
'''Initialize the control panel with sliders and options.'''
|
|
66
|
+
control_widget = QtWidgets.QWidget()
|
|
67
|
+
control_layout = QtWidgets.QVBoxLayout(control_widget)
|
|
68
|
+
self.layout().addWidget(control_widget, stretch=1)
|
|
69
|
+
|
|
70
|
+
# Add color map gradient widget
|
|
71
|
+
colormap_label = QtWidgets.QLabel('Color Map')
|
|
72
|
+
control_layout.addWidget(colormap_label)
|
|
73
|
+
|
|
74
|
+
self.colormap_preview = pg.GradientWidget(orientation='bottom')
|
|
75
|
+
self.colormap_preview.setMinimumHeight(40)
|
|
76
|
+
self.colormap_preview.item.setColorMap(pg.colormap.get('jet', 'matplotlib'))
|
|
77
|
+
self.colormap_preview.sigGradientChanged.connect(self.update_volume)
|
|
78
|
+
control_layout.addWidget(self.colormap_preview)
|
|
79
|
+
|
|
80
|
+
# Add opacity slider
|
|
81
|
+
opacity_label = QtWidgets.QLabel('Opacity')
|
|
82
|
+
control_layout.addWidget(opacity_label)
|
|
83
|
+
|
|
84
|
+
self.opacity_slider = QtWidgets.QSlider(Qt.Orientation.Horizontal)
|
|
85
|
+
self.opacity_slider.setMinimum(0)
|
|
86
|
+
self.opacity_slider.setMaximum(100)
|
|
87
|
+
self.opacity_slider.setValue(100)
|
|
88
|
+
self.opacity_slider.valueChanged.connect(self.update_volume)
|
|
89
|
+
control_layout.addWidget(self.opacity_slider)
|
|
90
|
+
|
|
91
|
+
# Add spacer
|
|
92
|
+
control_layout.addStretch()
|
|
93
|
+
|
|
94
|
+
def update_volume(self):
|
|
95
|
+
'''
|
|
96
|
+
Update the volume visualization with current color map and opacity settings.
|
|
97
|
+
Using accelerated processing functions.
|
|
98
|
+
'''
|
|
99
|
+
if self.volume_data is None:
|
|
100
|
+
return
|
|
101
|
+
|
|
102
|
+
# Remove existing volume if any
|
|
103
|
+
if self.volume_item is not None:
|
|
104
|
+
with contextlib.suppress(Exception):
|
|
105
|
+
self.view.removeItem(self.volume_item)
|
|
106
|
+
|
|
107
|
+
# Normalize data using accelerated function
|
|
108
|
+
if self.normalized_data is None:
|
|
109
|
+
self.normalized_data = normalize_volume(self.volume_data)
|
|
110
|
+
|
|
111
|
+
# Get color lookup table from gradient widget
|
|
112
|
+
colors = self.colormap_preview.item.colorMap().getLookupTable(0.0, 1.0, 256)
|
|
113
|
+
colors = colors[:, :3].astype(np.uint8) # Extract RGB components
|
|
114
|
+
|
|
115
|
+
# Create RGBA volume with accelerated function
|
|
116
|
+
opacity = self.opacity_slider.value() / 100.0
|
|
117
|
+
|
|
118
|
+
rgba_volume = create_rgba_volume(self.normalized_data, colors, opacity)
|
|
119
|
+
|
|
120
|
+
# Create volume item with proper scaling
|
|
121
|
+
scale = (
|
|
122
|
+
self.metadata['voxel_size']['x'],
|
|
123
|
+
self.metadata['voxel_size']['y'],
|
|
124
|
+
self.metadata['voxel_size']['z'],
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
self.volume_item = gl.GLVolumeItem(rgba_volume)
|
|
128
|
+
|
|
129
|
+
# Position the volume with respect to Z zero point
|
|
130
|
+
z_offset = self.metadata['coordinates']['z_min']
|
|
131
|
+
self.volume_item.translate(0, 0, z_offset)
|
|
132
|
+
|
|
133
|
+
# Add to view
|
|
134
|
+
self.view.addItem(self.volume_item)
|
|
135
|
+
|
|
136
|
+
def set_volume_data(self, volume_data, metadata=None):
|
|
137
|
+
'''Set or update the volume data to visualize.'''
|
|
138
|
+
self.volume_data = volume_data
|
|
139
|
+
if metadata is not None:
|
|
140
|
+
self.metadata = metadata
|
|
141
|
+
|
|
142
|
+
# Update the visualization
|
|
143
|
+
self.update_volume()
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
class PointCloudViewer(QtWidgets.QWidget):
|
|
147
|
+
def __init__(
|
|
148
|
+
self, points: np.ndarray = None, intensities: np.ndarray = None, metadata=None
|
|
149
|
+
):
|
|
150
|
+
'''
|
|
151
|
+
Initialize the point cloud viewer.
|
|
152
|
+
|
|
153
|
+
Args:
|
|
154
|
+
points: Nx3 numpy array of point coordinates (x, y, z)
|
|
155
|
+
intensities: N numpy array of intensity values
|
|
156
|
+
metadata: Dictionary containing additional information
|
|
157
|
+
'''
|
|
158
|
+
super().__init__()
|
|
159
|
+
self.setWindowTitle('3D Point Cloud Viewer')
|
|
160
|
+
|
|
161
|
+
# Store data and metadata
|
|
162
|
+
self.points = points
|
|
163
|
+
self.intensities = intensities
|
|
164
|
+
self.metadata = (
|
|
165
|
+
metadata
|
|
166
|
+
if metadata is not None
|
|
167
|
+
else {
|
|
168
|
+
'point_size': 2,
|
|
169
|
+
'coordinates': {'z_min': 0, 'z_max': 1},
|
|
170
|
+
'bin_size': {
|
|
171
|
+
'x': 10,
|
|
172
|
+
'y': 10,
|
|
173
|
+
'z': 10,
|
|
174
|
+
},
|
|
175
|
+
}
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
# Create the main widget and layout
|
|
179
|
+
self.main_layout = QtWidgets.QHBoxLayout()
|
|
180
|
+
self.setLayout(self.main_layout)
|
|
181
|
+
|
|
182
|
+
# Create 3D viewport
|
|
183
|
+
self.init_3d_viewport()
|
|
184
|
+
|
|
185
|
+
# Create control panel
|
|
186
|
+
self.init_control_panel()
|
|
187
|
+
|
|
188
|
+
# Set window size
|
|
189
|
+
self.resize(1200, 800)
|
|
190
|
+
|
|
191
|
+
self.show()
|
|
192
|
+
|
|
193
|
+
# Initialize point cloud visualization if data is provided
|
|
194
|
+
# if points is not None:
|
|
195
|
+
# self.update_point_cloud()
|
|
196
|
+
|
|
197
|
+
# Initialize histogram if we have data
|
|
198
|
+
if self.intensities is not None:
|
|
199
|
+
self.update_histogram()
|
|
200
|
+
|
|
201
|
+
def set_3d_view(self, args: tuple[int]):
|
|
202
|
+
'''Set the 3D view to a specific angle.'''
|
|
203
|
+
self.view.setCameraPosition(
|
|
204
|
+
pos=QtGui.QVector3D(*(np.max(self.points, axis=0) / 2)),
|
|
205
|
+
elevation=args[0],
|
|
206
|
+
azimuth=args[1],
|
|
207
|
+
distance=1.1 * np.max(self.points),
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
def closeEvent(self, event: QtGui.QCloseEvent):
|
|
211
|
+
'''Cleanup and close the window.
|
|
212
|
+
|
|
213
|
+
Params
|
|
214
|
+
------
|
|
215
|
+
event : QCloseEvent
|
|
216
|
+
Close event object
|
|
217
|
+
'''
|
|
218
|
+
self.view.clear()
|
|
219
|
+
self.view.close()
|
|
220
|
+
event.accept()
|
|
221
|
+
|
|
222
|
+
def init_3d_viewport(self):
|
|
223
|
+
'''Initialize the 3D viewport with PyQtGraph GLViewWidget.'''
|
|
224
|
+
self.view = gl.GLViewWidget()
|
|
225
|
+
self.main_layout.addWidget(self.view, stretch=4)
|
|
226
|
+
|
|
227
|
+
# Set some reasonable defaults for camera
|
|
228
|
+
self.view.setCameraPosition(
|
|
229
|
+
pos=QtGui.QVector3D(*np.mean(self.points, axis=0)),
|
|
230
|
+
elevation=-90,
|
|
231
|
+
azimuth=-90,
|
|
232
|
+
distance=1.1 * np.max(self.points),
|
|
233
|
+
)
|
|
234
|
+
# make view ortho instead of perspective
|
|
235
|
+
self.view.opts['fov'] = 60
|
|
236
|
+
self.view.update()
|
|
237
|
+
|
|
238
|
+
def init_control_panel(self):
|
|
239
|
+
'''Initialize the control panel with point size and color options.'''
|
|
240
|
+
control_widget = QtWidgets.QWidget()
|
|
241
|
+
control_layout = QtWidgets.QVBoxLayout(control_widget)
|
|
242
|
+
self.main_layout.addWidget(control_widget, stretch=1)
|
|
243
|
+
|
|
244
|
+
# Add color map gradient widget
|
|
245
|
+
colormap_group = QtWidgets.QGroupBox('Color Map')
|
|
246
|
+
colormap_layout = QtWidgets.QVBoxLayout()
|
|
247
|
+
colormap_group.setLayout(colormap_layout)
|
|
248
|
+
|
|
249
|
+
self.colormap_preview = pg.GradientWidget(orientation='bottom')
|
|
250
|
+
self.colormap_preview.setMinimumHeight(40)
|
|
251
|
+
self.colormap_preview.item.setColorMap(pg.colormap.get('jet', 'matplotlib'))
|
|
252
|
+
self.colormap_preview.sigGradientChanged.connect(self.update_point_cloud)
|
|
253
|
+
colormap_layout.addWidget(self.colormap_preview)
|
|
254
|
+
|
|
255
|
+
# Add preset buttons
|
|
256
|
+
preset_layout = QtWidgets.QHBoxLayout()
|
|
257
|
+
presets = ['viridis', 'plasma', 'inferno', 'magma']
|
|
258
|
+
for preset in presets:
|
|
259
|
+
btn = QtWidgets.QPushButton(preset)
|
|
260
|
+
btn.setMaximumWidth(60)
|
|
261
|
+
btn.clicked.connect(
|
|
262
|
+
lambda checked, p=preset: self.colormap_preview.loadPreset(p)
|
|
263
|
+
)
|
|
264
|
+
preset_layout.addWidget(btn)
|
|
265
|
+
colormap_layout.addLayout(preset_layout)
|
|
266
|
+
control_layout.addWidget(colormap_group)
|
|
267
|
+
|
|
268
|
+
# Add point size control
|
|
269
|
+
display_options = QtWidgets.QGroupBox('Display Options')
|
|
270
|
+
display_layout = QtWidgets.QFormLayout()
|
|
271
|
+
display_options.setLayout(display_layout)
|
|
272
|
+
|
|
273
|
+
self.size_slider = QtWidgets.QSlider(Qt.Orientation.Horizontal)
|
|
274
|
+
self.size_slider.setMinimum(1)
|
|
275
|
+
self.size_slider.setMaximum(100)
|
|
276
|
+
self.size_slider.setValue(self.metadata['bin_size']['x'])
|
|
277
|
+
self.size_slider.valueChanged.connect(self.update_point_cloud)
|
|
278
|
+
|
|
279
|
+
size_display = QtWidgets.QSpinBox()
|
|
280
|
+
size_display.setMinimum(1)
|
|
281
|
+
size_display.setMaximum(100)
|
|
282
|
+
size_display.setValue(self.size_slider.value())
|
|
283
|
+
self.size_slider.valueChanged.connect(size_display.setValue)
|
|
284
|
+
size_display.valueChanged.connect(self.size_slider.setValue)
|
|
285
|
+
|
|
286
|
+
display_layout.addWidget(self.size_slider)
|
|
287
|
+
display_layout.addWidget(size_display)
|
|
288
|
+
|
|
289
|
+
# Add grid and axis to display
|
|
290
|
+
self.axis_checkbox = QtWidgets.QCheckBox('Show Axis')
|
|
291
|
+
self.axis_checkbox.setChecked(True)
|
|
292
|
+
self.axis_checkbox.stateChanged.connect(lambda: self.update_point_cloud())
|
|
293
|
+
display_layout.addWidget(self.axis_checkbox)
|
|
294
|
+
self.grid_checkbox = QtWidgets.QCheckBox('Show Grid')
|
|
295
|
+
self.grid_checkbox.setChecked(True)
|
|
296
|
+
self.grid_checkbox.stateChanged.connect(lambda: self.update_point_cloud())
|
|
297
|
+
display_layout.addWidget(self.grid_checkbox)
|
|
298
|
+
|
|
299
|
+
fov_spin = QtWidgets.QSpinBox()
|
|
300
|
+
fov_spin.setMinimum(1)
|
|
301
|
+
fov_spin.setMaximum(180)
|
|
302
|
+
fov_spin.setValue(1)
|
|
303
|
+
fov_spin.valueChanged.connect(
|
|
304
|
+
lambda: self.view.setCameraParams(fov=fov_spin.value())
|
|
305
|
+
)
|
|
306
|
+
display_layout.addWidget(fov_spin)
|
|
307
|
+
|
|
308
|
+
control_layout.addWidget(display_options)
|
|
309
|
+
|
|
310
|
+
# View controls
|
|
311
|
+
view_controls = QtWidgets.QGroupBox('View Controls')
|
|
312
|
+
view_layout = QtWidgets.QHBoxLayout()
|
|
313
|
+
|
|
314
|
+
view_buttons = {
|
|
315
|
+
'T': (-90, -90),
|
|
316
|
+
'B': (90, 90),
|
|
317
|
+
'F': (0, 0),
|
|
318
|
+
'Bck': (0, 180),
|
|
319
|
+
'L': (0, 90),
|
|
320
|
+
'R': (0, -90),
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
for label, rotation in view_buttons.items():
|
|
324
|
+
btn = QtWidgets.QPushButton(label)
|
|
325
|
+
btn.clicked.connect(lambda checked, r=rotation: self.set_3d_view(r))
|
|
326
|
+
view_layout.addWidget(btn)
|
|
327
|
+
|
|
328
|
+
view_controls.setLayout(view_layout)
|
|
329
|
+
control_layout.addWidget(view_controls)
|
|
330
|
+
|
|
331
|
+
# Add intensity histogram with range selection
|
|
332
|
+
intensity_group = QtWidgets.QGroupBox('Intensity Range')
|
|
333
|
+
intensity_layout = QtWidgets.QVBoxLayout()
|
|
334
|
+
intensity_group.setLayout(intensity_layout)
|
|
335
|
+
|
|
336
|
+
# Create histogram plot
|
|
337
|
+
self.hist_plot = pg.PlotWidget()
|
|
338
|
+
self.hist_plot.setMinimumHeight(100)
|
|
339
|
+
intensity_layout.addWidget(self.hist_plot)
|
|
340
|
+
|
|
341
|
+
# Reset range
|
|
342
|
+
reset_button = QtWidgets.QPushButton('Reset Range')
|
|
343
|
+
reset_button.clicked.connect(lambda: self.update_histogram())
|
|
344
|
+
intensity_layout.addWidget(reset_button)
|
|
345
|
+
|
|
346
|
+
# Create region selection
|
|
347
|
+
self.region = pg.LinearRegionItem([0, 100])
|
|
348
|
+
self.region.sigRegionChangeFinished.connect(self.update_point_cloud)
|
|
349
|
+
self.hist_plot.addItem(self.region)
|
|
350
|
+
|
|
351
|
+
control_layout.addWidget(intensity_group)
|
|
352
|
+
|
|
353
|
+
# Add spacer
|
|
354
|
+
control_layout.addStretch()
|
|
355
|
+
|
|
356
|
+
def update_histogram(self):
|
|
357
|
+
'''Update the intensity histogram display.'''
|
|
358
|
+
if self.intensities is None:
|
|
359
|
+
return
|
|
360
|
+
|
|
361
|
+
# Clear previous histogram
|
|
362
|
+
self.hist_plot.clear()
|
|
363
|
+
|
|
364
|
+
# Calculate histogram
|
|
365
|
+
y, x = np.histogram(self.intensities, bins=1000)
|
|
366
|
+
|
|
367
|
+
# Create histogram
|
|
368
|
+
self.hist_plot.plot(
|
|
369
|
+
x, y, stepMode='center', fillLevel=0, brush=(0, 0, 255, 150)
|
|
370
|
+
)
|
|
371
|
+
|
|
372
|
+
# Add region selection
|
|
373
|
+
if self.region not in self.hist_plot.items():
|
|
374
|
+
self.hist_plot.addItem(self.region)
|
|
375
|
+
|
|
376
|
+
# Set initial region positions to 1st and 99th percentiles
|
|
377
|
+
p1, p99 = np.percentile(self.intensities, [1, 99])
|
|
378
|
+
self.region.setRegion([p1, p99])
|
|
379
|
+
|
|
380
|
+
def map_colors(self, intensities):
|
|
381
|
+
'''Map intensity values to colors using current colormap.'''
|
|
382
|
+
# Get intensity range from region selection
|
|
383
|
+
lower, upper = self.region.getRegion()
|
|
384
|
+
|
|
385
|
+
# Clip and normalize intensities
|
|
386
|
+
norm_intensities = np.clip(intensities, lower, upper)
|
|
387
|
+
norm_intensities = (norm_intensities - lower) / (upper - lower)
|
|
388
|
+
|
|
389
|
+
# Get color lookup table from gradient widget
|
|
390
|
+
colormap = self.colormap_preview.item.colorMap()
|
|
391
|
+
colors = colormap.map(norm_intensities, mode='float')
|
|
392
|
+
|
|
393
|
+
colors[intensities < lower, 3] = 0
|
|
394
|
+
|
|
395
|
+
return colors
|
|
396
|
+
|
|
397
|
+
def update_point_cloud(self):
|
|
398
|
+
'''Update the point cloud visualization with current settings.'''
|
|
399
|
+
if self.points is None:
|
|
400
|
+
return
|
|
401
|
+
|
|
402
|
+
# Remove existing scatter if any
|
|
403
|
+
self.view.clear()
|
|
404
|
+
|
|
405
|
+
# Map intensities to colors
|
|
406
|
+
if self.intensities is not None:
|
|
407
|
+
colors = self.map_colors(self.intensities)
|
|
408
|
+
else:
|
|
409
|
+
# Use height (z-coordinate) for coloring if no intensities provided
|
|
410
|
+
z_values = self.points[:, 2]
|
|
411
|
+
colors = self.map_colors(z_values)
|
|
412
|
+
|
|
413
|
+
# Add axis and grid based on data extent
|
|
414
|
+
data_range = np.ptp(self.points, axis=0)
|
|
415
|
+
grid_size = max(data_range[0], data_range[1])
|
|
416
|
+
|
|
417
|
+
if self.grid_checkbox.isChecked():
|
|
418
|
+
grid = gl.GLGridItem(QtGui.QVector3D(grid_size, grid_size, 1))
|
|
419
|
+
grid.translate(dx=grid_size / 2, dy=grid_size / 2, dz=0)
|
|
420
|
+
grid.setSpacing(x=grid_size / 10, y=grid_size / 10)
|
|
421
|
+
self.view.addItem(grid)
|
|
422
|
+
|
|
423
|
+
if self.axis_checkbox.isChecked():
|
|
424
|
+
axis = gl.GLAxisItem()
|
|
425
|
+
axis.setSize(x=grid_size / 10, y=grid_size / 10, z=data_range[2] / 10)
|
|
426
|
+
self.view.addItem(axis)
|
|
427
|
+
|
|
428
|
+
# Create scatter plot item
|
|
429
|
+
scatter_item = gl.GLScatterPlotItem(
|
|
430
|
+
pos=self.points,
|
|
431
|
+
color=colors,
|
|
432
|
+
size=self.size_slider.value(),
|
|
433
|
+
pxMode=False,
|
|
434
|
+
)
|
|
435
|
+
|
|
436
|
+
# Add to view
|
|
437
|
+
self.view.addItem(scatter_item)
|
|
438
|
+
|
|
439
|
+
|
|
440
|
+
def show_volume(volume_data, metadata=None):
|
|
441
|
+
'''Convenience function to show volume data in a new window.'''
|
|
442
|
+
import sys
|
|
443
|
+
|
|
444
|
+
from microEye.qt import QApplication
|
|
445
|
+
|
|
446
|
+
# Create QApplication instance if it doesn't exist
|
|
447
|
+
app = QApplication.instance()
|
|
448
|
+
if app is None:
|
|
449
|
+
app = QApplication(sys.argv)
|
|
450
|
+
|
|
451
|
+
# Create and show window
|
|
452
|
+
viewer = VolumeViewerWindow(volume_data, metadata)
|
|
453
|
+
viewer.show()
|
|
454
|
+
|
|
455
|
+
# Return viewer instance
|
|
456
|
+
return viewer
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
from microEye.hardware.cams.camera_calibration import dark_calibration
|
|
2
|
+
from microEye.hardware.cams.camera_list import CameraList
|
|
3
|
+
from microEye.hardware.cams.camera_panel import Camera_Panel, CamParams
|
|
4
|
+
from microEye.hardware.cams.jobs import AcquisitionJob
|
|
5
|
+
from microEye.hardware.cams.linescan.IR_Cam import (
|
|
6
|
+
DemoLineScanner,
|
|
7
|
+
IR_Cam,
|
|
8
|
+
ParallaxLineScanner,
|
|
9
|
+
)
|
|
10
|
+
from microEye.hardware.cams.micam import miCamera, miDummy
|
|
11
|
+
from microEye.hardware.cams.thorlabs.thorlabs import CMD, thorlabs_camera
|
|
12
|
+
from microEye.hardware.cams.thorlabs.thorlabs_panel import Thorlabs_Panel
|
|
13
|
+
|
|
14
|
+
try:
|
|
15
|
+
from pyueye import ueye
|
|
16
|
+
|
|
17
|
+
from microEye.hardware.cams.ueye.ueye_camera import IDS_Camera
|
|
18
|
+
from microEye.hardware.cams.ueye.ueye_panel import IDS_Panel
|
|
19
|
+
except Exception:
|
|
20
|
+
ueye = None
|
|
21
|
+
IDS_Camera = None
|
|
22
|
+
IDS_Panel = None
|
|
23
|
+
try:
|
|
24
|
+
import vimba as vb
|
|
25
|
+
|
|
26
|
+
from microEye.hardware.cams.vimba.vimba_cam import vimba_cam
|
|
27
|
+
from microEye.hardware.cams.vimba.vimba_panel import Vimba_Panel
|
|
28
|
+
except Exception:
|
|
29
|
+
vb = None
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import os
|
|
2
|
+
|
|
3
|
+
import numpy as np
|
|
4
|
+
import tifffile as tf
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class dark_calibration:
|
|
8
|
+
'''A class holder for pixelwise average and variance.
|
|
9
|
+
|
|
10
|
+
Intended to be used for photon-free calibration:
|
|
11
|
+
https://www.biorxiv.org/lookup/doi/10.1101/2021.04.16.440125
|
|
12
|
+
http://dx.doi.org/10.1038/s41467-022-30907-2
|
|
13
|
+
|
|
14
|
+
ACCENT (ImageJ/Fiji Plugin):
|
|
15
|
+
https://github.com/ries-lab/Accent
|
|
16
|
+
'''
|
|
17
|
+
def __init__(self, shape, exposure):
|
|
18
|
+
self._counter = 0
|
|
19
|
+
self._exposure = exposure
|
|
20
|
+
self._shape = shape
|
|
21
|
+
self._accumulator = np.zeros(
|
|
22
|
+
shape=shape, dtype=np.float64)
|
|
23
|
+
self._quad_accum = np.zeros(
|
|
24
|
+
shape=shape, dtype=np.float64)
|
|
25
|
+
|
|
26
|
+
def addFrame(self, image: np.ndarray):
|
|
27
|
+
'''Adds an image frame to the mean/variance estimators.
|
|
28
|
+
|
|
29
|
+
Parameters
|
|
30
|
+
----------
|
|
31
|
+
image : np.ndarray
|
|
32
|
+
image of dark_calibration shape to be added.
|
|
33
|
+
|
|
34
|
+
Raises
|
|
35
|
+
------
|
|
36
|
+
ValueError
|
|
37
|
+
image of wrong shape.
|
|
38
|
+
'''
|
|
39
|
+
if image.shape != self._shape:
|
|
40
|
+
raise ValueError('Image of wrong shape.')
|
|
41
|
+
|
|
42
|
+
self._accumulator += image
|
|
43
|
+
self._quad_accum += np.square(image)
|
|
44
|
+
self._counter += 1
|
|
45
|
+
|
|
46
|
+
def getResults(self):
|
|
47
|
+
'''Gets the resulting mean and variance frames.
|
|
48
|
+
|
|
49
|
+
Returns
|
|
50
|
+
-------
|
|
51
|
+
tuple(ndarray, ndarray)
|
|
52
|
+
the rasults (mean, variance)
|
|
53
|
+
|
|
54
|
+
Raises
|
|
55
|
+
------
|
|
56
|
+
ValueError
|
|
57
|
+
in case zero frames are added.
|
|
58
|
+
'''
|
|
59
|
+
if self._counter < 1:
|
|
60
|
+
raise ValueError('Counter should be non-zero.')
|
|
61
|
+
|
|
62
|
+
mean = self._accumulator / self._counter
|
|
63
|
+
variance = (
|
|
64
|
+
self._quad_accum -
|
|
65
|
+
np.square(self._accumulator)/self._counter)/(self._counter - 1)
|
|
66
|
+
|
|
67
|
+
return mean, variance
|
|
68
|
+
|
|
69
|
+
def saveResults(self, path, prefix):
|
|
70
|
+
if not os.path.exists(path):
|
|
71
|
+
os.makedirs(path)
|
|
72
|
+
|
|
73
|
+
def getFilename(index: int):
|
|
74
|
+
return path + \
|
|
75
|
+
f'\\image_{index:05d}.ome.tif'
|
|
76
|
+
|
|
77
|
+
mean, variance = self.getResults()
|
|
78
|
+
|
|
79
|
+
with tf.TiffWriter(
|
|
80
|
+
path + prefix + \
|
|
81
|
+
f'_image_mean_{self._exposure:.5f}_ms'.replace('.', '_') + \
|
|
82
|
+
'.ome.tif',
|
|
83
|
+
append=False,
|
|
84
|
+
bigtiff=False,
|
|
85
|
+
ome=False) as writer:
|
|
86
|
+
writer.write(
|
|
87
|
+
data=mean,
|
|
88
|
+
photometric='minisblack')
|
|
89
|
+
|
|
90
|
+
with tf.TiffWriter(
|
|
91
|
+
path + prefix + \
|
|
92
|
+
f'_image_var_{self._exposure:.5f}_ms'.replace('.', '_') + \
|
|
93
|
+
'.ome.tif',
|
|
94
|
+
append=False,
|
|
95
|
+
bigtiff=False,
|
|
96
|
+
ome=False) as writer:
|
|
97
|
+
writer.write(
|
|
98
|
+
data=variance,
|
|
99
|
+
photometric='minisblack')
|