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,474 @@
1
+ import struct
2
+ import weakref
3
+ from os import name
4
+ from queue import LifoQueue, Queue
5
+ from typing import Union
6
+
7
+ import numpy as np
8
+
9
+ from microEye.hardware.port_config import *
10
+ from microEye.qt import QtCore, QtSerialPort, QtWidgets
11
+
12
+
13
+ class IR_Cam:
14
+ '''An abstract class for IR cameras.'''
15
+
16
+ def __init__(self, buffer: Union[Queue, LifoQueue]) -> None:
17
+ self.name = 'Dummy'
18
+ if buffer is None:
19
+ self._buffer = LifoQueue()
20
+ else:
21
+ # add buffer in weakref to avoid circular reference
22
+ self._buffer = weakref.ref(buffer)
23
+ self.buffer.put(np.array([0 for i in range(128)]))
24
+ self._connect_btn = QtWidgets.QPushButton()
25
+
26
+ def isDummy(self) -> bool:
27
+ return True
28
+
29
+ @property
30
+ def buffer(self):
31
+ if isinstance(self._buffer, weakref.ref):
32
+ # dereference the weakref to get the actual buffer
33
+ return self._buffer()
34
+
35
+ return self._buffer
36
+
37
+ @property
38
+ def isEmpty(self) -> bool:
39
+ return self.buffer.empty()
40
+
41
+ def get(self) -> np.ndarray:
42
+ return self.buffer.get()
43
+
44
+ @property
45
+ def isOpen(self) -> bool:
46
+ return False
47
+
48
+
49
+ class ParallaxLineScanner(IR_Cam):
50
+ '''A class for Parallax CCD Array (TSL1401)
51
+ connected via the Arduino LineScanner.'''
52
+
53
+ def __init__(self, buffer: Union[Queue, LifoQueue]) -> None:
54
+ super().__init__(buffer=buffer)
55
+
56
+ self.name = 'Parallax CCD Array (TSL1401) LineScanner'
57
+ self.buffer.put(np.array([0 for i in range(128)]))
58
+ self.serial = QtSerialPort.QSerialPort(None, readyRead=self.receive)
59
+ self.serial.setBaudRate(115200)
60
+ self.serial.setPortName('COM4')
61
+
62
+ def isDummy(self) -> bool:
63
+ return False
64
+
65
+ def open(self):
66
+ '''Opens the serial port.'''
67
+ self.serial.open(QtCore.QIODevice.OpenModeFlag.ReadWrite)
68
+
69
+ @property
70
+ def isOpen(self) -> bool:
71
+ '''Returns True if connected.'''
72
+ return self.serial.isOpen()
73
+
74
+ def close(self):
75
+ '''Closes the supplied serial port.'''
76
+ self.serial.close()
77
+
78
+ def setPortName(self, name: str):
79
+ '''Sets the serial port name.'''
80
+ self.serial.setPortName(name)
81
+
82
+ def setBaudRate(self, baudRate: int):
83
+ '''Sets the serial port baudrate.'''
84
+ self.serial.setBaudRate(baudRate)
85
+
86
+ def receive(self):
87
+ '''IR CCD array serial port data ready signal'''
88
+ if self.serial.bytesAvailable() >= 260:
89
+ barray = self.serial.read(260)
90
+ temp = np.array(
91
+ np.array(struct.unpack('h' * (len(barray) // 2), barray)) * 5.0 / 1023.0
92
+ )
93
+ # array realignment
94
+ if (temp[0] != 0 or temp[-1] != 0) and self.serial.bytesAvailable() >= 2:
95
+ self.serial.read(2)
96
+ # byte-wise realignment
97
+ if temp.max() > 5:
98
+ self.serial.read(1)
99
+ self.buffer.put(temp[1:129])
100
+
101
+ def open_dialog(self):
102
+ '''Opens a port config dialog
103
+ for the serial port.
104
+ '''
105
+ dialog = port_config()
106
+ if not self.isOpen:
107
+ if dialog.exec():
108
+ portname, baudrate = dialog.get_results()
109
+ self.setPortName(portname)
110
+ self.setBaudRate(baudrate)
111
+
112
+ def getQWidget(self, parent=None) -> QtWidgets.QGroupBox:
113
+ '''Generates a QGroupBox with
114
+ connect/disconnect/config buttons.'''
115
+ group = QtWidgets.QGroupBox('Parallax CCD Array')
116
+ layout = QtWidgets.QVBoxLayout()
117
+ group.setLayout(layout)
118
+
119
+ # IR CCD array arduino buttons
120
+ self._connect_btn = QtWidgets.QPushButton(
121
+ 'Connect', parent, clicked=lambda: self.open()
122
+ )
123
+ disconnect_btn = QtWidgets.QPushButton(
124
+ 'Disconnect', clicked=lambda: self.close()
125
+ )
126
+ config_btn = QtWidgets.QPushButton(
127
+ 'Port Config.', clicked=lambda: self.open_dialog()
128
+ )
129
+
130
+ btns = QtWidgets.QHBoxLayout()
131
+ btns.addWidget(self._connect_btn)
132
+ btns.addWidget(disconnect_btn)
133
+ btns.addWidget(config_btn)
134
+ layout.addLayout(btns)
135
+
136
+ return group
137
+
138
+
139
+ class DemoLineScanner(IR_Cam):
140
+ '''A demo camera class that generates random frames every 50 ms.
141
+ Useful for testing and demonstrations without hardware.'''
142
+
143
+ def __init__(self, buffer: Union[Queue, LifoQueue], array_length=128) -> None:
144
+ super().__init__(buffer=buffer)
145
+
146
+ self.name = 'Demo Line Scanner'
147
+ self._array_length = array_length # Adjustable array length
148
+ self.buffer.put(np.array([0 for i in range(self._array_length)]))
149
+ self._isOpen = False
150
+ self._timer = QtCore.QTimer()
151
+ self._timer.timeout.connect(self.generate_frame)
152
+ self._timer.setInterval(50) # 50 ms interval
153
+ self._noise_level = 0.1 # Default noise level
154
+
155
+ # Signal pattern options
156
+ self.PATTERN_NOISE = 0
157
+ self.PATTERN_STATIC_PEAK = 1
158
+ self.PATTERN_MOVING_PEAK = 2
159
+ self.PATTERN_DRIFT = 3
160
+ self._pattern_type = self.PATTERN_NOISE
161
+
162
+ # Parameters for peaks
163
+ self._peak_width = self._array_length // 12 # Width of Gaussian peak (sigma)
164
+ self._peak_amplitude = 0.8 # Height of peak (0-1)
165
+ self._peak_position = (
166
+ self._array_length // 2
167
+ ) # Current position for moving peak
168
+ self._peak_direction = 1 # Direction of movement (1 or -1)
169
+ self._peak_speed = 2 # Pixels per frame
170
+
171
+ # Microscope drift simulation parameters
172
+ self._drift_speed = 0.1 # Base drift speed (pixels per frame)
173
+ self._drift_variation = 0.3 # Random variation in drift
174
+ self._drift_momentum = (
175
+ 0.95 # How much previous movement affects current (inertia)
176
+ )
177
+ self._current_drift_velocity = 0.0 # Current drift velocity
178
+ self._vibration_amplitude = 0.3 # Amplitude of high-frequency vibrations
179
+ self._drift_time = 0 # Time counter for drift simulation
180
+ self._random_walk_factor = 0.1 # Factor for random walk component
181
+
182
+ # Initialize fake drift with some variation
183
+ self._drift_center = self._array_length // 2
184
+ self._drift_period = np.random.uniform(
185
+ 50, 120
186
+ ) # Period in frames for slow drift cycle
187
+ self._drift_phase = np.random.uniform(0, 2 * np.pi) # Random starting phase
188
+
189
+ def isDummy(self) -> bool:
190
+ return True
191
+
192
+ def open(self):
193
+ '''Starts the frame generation timer.'''
194
+ self._isOpen = True
195
+ self._timer.start()
196
+ if self._connect_btn is not None:
197
+ self._connect_btn.setEnabled(False)
198
+
199
+ @property
200
+ def isOpen(self) -> bool:
201
+ '''Returns True if the demo camera is running.'''
202
+ return self._isOpen
203
+
204
+ def close(self):
205
+ '''Stops the frame generation timer.'''
206
+ self._timer.stop()
207
+ self._isOpen = False
208
+ if self._connect_btn is not None:
209
+ self._connect_btn.setEnabled(True)
210
+
211
+ @property
212
+ def array_length(self) -> int:
213
+ '''Get the current array length'''
214
+ return self._array_length
215
+
216
+ def set_array_length(self, length: int) -> None:
217
+ '''Set a new array length'''
218
+
219
+ self._array_length = length if length > 32 else 32
220
+
221
+ # Adjust peak position and width to match new array length
222
+ self._peak_position = self._array_length // 2
223
+ self._peak_width = self._array_length // 12
224
+
225
+ def generate_frame(self):
226
+ '''Generates a frame based on the selected pattern type.'''
227
+ # Base array with zeros
228
+ frame = np.zeros(self._array_length)
229
+ x = np.arange(self._array_length)
230
+
231
+ # Add noise to all patterns
232
+ if self._noise_level > 0:
233
+ noise = np.random.random(self._array_length) * self._noise_level
234
+ frame += noise
235
+
236
+ # Add pattern based on selection
237
+ if self._pattern_type == self.PATTERN_NOISE:
238
+ # Just noise, already added
239
+ pass
240
+
241
+ elif self._pattern_type == self.PATTERN_STATIC_PEAK:
242
+ # Add static Gaussian peak at center
243
+ center = self._array_length // 2
244
+ frame += self._peak_amplitude * np.exp(
245
+ -((x - center) ** 2) / (2 * self._peak_width**2)
246
+ )
247
+
248
+ elif self._pattern_type == self.PATTERN_MOVING_PEAK:
249
+ # Add moving Gaussian peak
250
+ frame += self._peak_amplitude * np.exp(
251
+ -((x - self._peak_position) ** 2) / (2 * self._peak_width**2)
252
+ )
253
+
254
+ # Update peak position for next frame
255
+ self._peak_position += self._peak_speed * self._peak_direction
256
+
257
+ # Reverse direction when reaching edges (with margin)
258
+ edge_margin = max(8, self._array_length // 16)
259
+ if (
260
+ self._peak_position >= self._array_length - edge_margin
261
+ or self._peak_position <= edge_margin
262
+ ):
263
+ self._peak_direction *= -1
264
+
265
+ elif self._pattern_type == self.PATTERN_DRIFT:
266
+ # Simulate microscope stage drift
267
+ self._drift_time += 1
268
+
269
+ # Combine multiple drift components:
270
+
271
+ # 1. Slow sinusoidal drift (thermal/mechanical creep)
272
+ slow_drift = np.sin(
273
+ 2 * np.pi * self._drift_time / self._drift_period + self._drift_phase
274
+ )
275
+
276
+ # 2. Brownian motion / random walk component
277
+ # (use momentum to avoid jerky movement)
278
+ random_component = np.random.normal(0, self._random_walk_factor)
279
+ self._current_drift_velocity = (
280
+ self._drift_momentum * self._current_drift_velocity
281
+ + (1 - self._drift_momentum) * random_component
282
+ )
283
+
284
+ # 3. Small high-frequency vibrations (building vibrations, etc)
285
+ vibration = (
286
+ self._vibration_amplitude
287
+ * np.sin(0.1 * self._drift_time)
288
+ * np.random.random()
289
+ )
290
+
291
+ # Combine all drift components
292
+ drift_amount = (
293
+ slow_drift * self._drift_speed
294
+ + self._current_drift_velocity
295
+ + vibration
296
+ )
297
+
298
+ # Update drift center with bounds checking
299
+ self._drift_center += drift_amount
300
+ edge_margin = max(10, self._array_length // 10)
301
+ if self._drift_center < edge_margin:
302
+ self._drift_center = edge_margin
303
+ self._current_drift_velocity *= -0.5 # Bounce back with damping
304
+ elif self._drift_center > self._array_length - edge_margin:
305
+ self._drift_center = self._array_length - edge_margin
306
+ self._current_drift_velocity *= -0.5 # Bounce back with damping
307
+
308
+ # Add Gaussian peak at current drift position
309
+ frame += self._peak_amplitude * np.exp(
310
+ -((x - self._drift_center) ** 2) / (2 * self._peak_width**2)
311
+ )
312
+
313
+ # Scale to 0-5V range
314
+ frame = frame * 5.0
315
+
316
+ # Add to buffer
317
+ self.buffer.put(frame)
318
+
319
+ def set_pattern_type(self, pattern_type):
320
+ '''Set the pattern type for the generated frames'''
321
+ self._pattern_type = pattern_type
322
+
323
+ def set_peak_width(self, width):
324
+ '''Set the width (sigma) of the Gaussian peak'''
325
+ self._peak_width = max(1, min(self._array_length // 4, width))
326
+
327
+ def set_peak_amplitude(self, amplitude):
328
+ '''Set the amplitude of the Gaussian peak'''
329
+ self._peak_amplitude = max(0.0, min(1.0, amplitude))
330
+
331
+ def set_peak_speed(self, speed):
332
+ '''Set the speed of the moving peak'''
333
+ self._peak_speed = max(1, min(10, speed))
334
+
335
+ def set_noise_level(self, level):
336
+ '''Set the noise level (0.0 to 1.0)'''
337
+ self._noise_level = max(0.0, min(1.0, level)) # Clamp between 0 and 1
338
+
339
+ def set_drift_speed(self, speed):
340
+ '''Set the base speed of the drift simulation'''
341
+ self._drift_speed = max(0.01, min(1.0, speed))
342
+
343
+ def set_drift_variation(self, variation):
344
+ '''Set the random variation amount in drift simulation'''
345
+ self._random_walk_factor = max(0.01, min(1.0, variation))
346
+
347
+ def set_vibration_amplitude(self, amplitude):
348
+ '''Set the amplitude of high-frequency vibrations'''
349
+ self._vibration_amplitude = max(0.0, min(1.0, amplitude))
350
+
351
+ def getQWidget(self, parent=None) -> QtWidgets.QGroupBox:
352
+ '''Generates a QGroupBox with controls for the demo camera.'''
353
+ group = QtWidgets.QGroupBox('Demo Line Scanner')
354
+ layout = QtWidgets.QVBoxLayout()
355
+ group.setLayout(layout)
356
+
357
+ # Connect/Disconnect buttons
358
+ self._connect_btn = QtWidgets.QPushButton(
359
+ 'Start Demo', parent, clicked=lambda: self.open()
360
+ )
361
+ disconnect_btn = QtWidgets.QPushButton(
362
+ 'Stop Demo', clicked=lambda: self.close()
363
+ )
364
+
365
+ # Array length control
366
+ array_length_layout = QtWidgets.QHBoxLayout()
367
+ array_length_label = QtWidgets.QLabel('Array Length:')
368
+ array_length_spinbox = QtWidgets.QSpinBox()
369
+ array_length_spinbox.setRange(16, 2048) # Reasonable range
370
+ array_length_spinbox.setValue(self._array_length)
371
+ array_length_spinbox.valueChanged.connect(self.set_array_length)
372
+ array_length_layout.addWidget(array_length_label)
373
+ array_length_layout.addWidget(array_length_spinbox)
374
+
375
+ # Pattern selection
376
+ pattern_layout = QtWidgets.QHBoxLayout()
377
+ pattern_label = QtWidgets.QLabel('Pattern:')
378
+ pattern_combo = QtWidgets.QComboBox()
379
+ pattern_combo.addItems(
380
+ ['Noise Only', 'Static Peak', 'Moving Peak', 'Microscope Drift']
381
+ )
382
+ pattern_combo.currentIndexChanged.connect(self.set_pattern_type)
383
+ pattern_layout.addWidget(pattern_label)
384
+ pattern_layout.addWidget(pattern_combo)
385
+
386
+ # Noise level slider
387
+ noise_layout = QtWidgets.QHBoxLayout()
388
+ noise_label = QtWidgets.QLabel('Noise Level:')
389
+ noise_slider = QtWidgets.QSlider(QtCore.Qt.Orientation.Horizontal)
390
+ noise_slider.setMinimum(0)
391
+ noise_slider.setMaximum(100)
392
+ noise_slider.setValue(int(self._noise_level * 100))
393
+ noise_slider.valueChanged.connect(lambda v: self.set_noise_level(v / 100.0))
394
+ noise_layout.addWidget(noise_label)
395
+ noise_layout.addWidget(noise_slider)
396
+
397
+ # Peak width slider
398
+ width_layout = QtWidgets.QHBoxLayout()
399
+ width_label = QtWidgets.QLabel('Peak Width:')
400
+ width_slider = QtWidgets.QSlider(QtCore.Qt.Orientation.Horizontal)
401
+ width_slider.setMinimum(1)
402
+ width_slider.setMaximum(self._array_length // 4)
403
+ width_slider.setValue(self._peak_width)
404
+ width_slider.valueChanged.connect(self.set_peak_width)
405
+ width_layout.addWidget(width_label)
406
+ width_layout.addWidget(width_slider)
407
+
408
+ # Peak amplitude slider
409
+ amp_layout = QtWidgets.QHBoxLayout()
410
+ amp_label = QtWidgets.QLabel('Peak Amplitude:')
411
+ amp_slider = QtWidgets.QSlider(QtCore.Qt.Orientation.Horizontal)
412
+ amp_slider.setMinimum(0)
413
+ amp_slider.setMaximum(100)
414
+ amp_slider.setValue(int(self._peak_amplitude * 100))
415
+ amp_slider.valueChanged.connect(lambda v: self.set_peak_amplitude(v / 100.0))
416
+ amp_layout.addWidget(amp_label)
417
+ amp_layout.addWidget(amp_slider)
418
+
419
+ # Drift speed slider (for microscope drift simulation)
420
+ drift_speed_layout = QtWidgets.QHBoxLayout()
421
+ drift_speed_label = QtWidgets.QLabel('Drift Speed:')
422
+ drift_speed_slider = QtWidgets.QSlider(QtCore.Qt.Orientation.Horizontal)
423
+ drift_speed_slider.setMinimum(1)
424
+ drift_speed_slider.setMaximum(100)
425
+ drift_speed_slider.setValue(int(self._drift_speed * 100))
426
+ drift_speed_slider.valueChanged.connect(
427
+ lambda v: self.set_drift_speed(v / 100.0)
428
+ )
429
+ drift_speed_layout.addWidget(drift_speed_label)
430
+ drift_speed_layout.addWidget(drift_speed_slider)
431
+
432
+ # Drift variation slider
433
+ drift_var_layout = QtWidgets.QHBoxLayout()
434
+ drift_var_label = QtWidgets.QLabel('Drift Randomness:')
435
+ drift_var_slider = QtWidgets.QSlider(QtCore.Qt.Orientation.Horizontal)
436
+ drift_var_slider.setMinimum(1)
437
+ drift_var_slider.setMaximum(100)
438
+ drift_var_slider.setValue(int(self._random_walk_factor * 100))
439
+ drift_var_slider.valueChanged.connect(
440
+ lambda v: self.set_drift_variation(v / 100.0)
441
+ )
442
+ drift_var_layout.addWidget(drift_var_label)
443
+ drift_var_layout.addWidget(drift_var_slider)
444
+
445
+ # Vibration amplitude slider
446
+ vib_layout = QtWidgets.QHBoxLayout()
447
+ vib_label = QtWidgets.QLabel('Vibration:')
448
+ vib_slider = QtWidgets.QSlider(QtCore.Qt.Orientation.Horizontal)
449
+ vib_slider.setMinimum(0)
450
+ vib_slider.setMaximum(100)
451
+ vib_slider.setValue(int(self._vibration_amplitude * 100))
452
+ vib_slider.valueChanged.connect(
453
+ lambda v: self.set_vibration_amplitude(v / 100.0)
454
+ )
455
+ vib_layout.addWidget(vib_label)
456
+ vib_layout.addWidget(vib_slider)
457
+
458
+ # Arrange buttons
459
+ btns = QtWidgets.QHBoxLayout()
460
+ btns.addWidget(self._connect_btn)
461
+ btns.addWidget(disconnect_btn)
462
+
463
+ # Add all layouts
464
+ layout.addLayout(btns)
465
+ layout.addLayout(array_length_layout)
466
+ layout.addLayout(pattern_layout)
467
+ layout.addLayout(noise_layout)
468
+ layout.addLayout(width_layout)
469
+ layout.addLayout(amp_layout)
470
+ layout.addLayout(drift_speed_layout)
471
+ layout.addLayout(drift_var_layout)
472
+ layout.addLayout(vib_layout)
473
+
474
+ return group