imswitchclient 0.1.7__tar.gz → 0.2.0__tar.gz
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.
- {imswitchclient-0.1.7 → imswitchclient-0.2.0}/MANIFEST.in +0 -2
- {imswitchclient-0.1.7 → imswitchclient-0.2.0}/PKG-INFO +78 -9
- {imswitchclient-0.1.7 → imswitchclient-0.2.0}/README.md +76 -7
- imswitchclient-0.2.0/imswitchclient/ImSwitchClient.py +142 -0
- {imswitchclient-0.1.7 → imswitchclient-0.2.0}/imswitchclient/__init__.py +1 -1
- imswitchclient-0.2.0/imswitchclient/autofocusManager.py +51 -0
- imswitchclient-0.2.0/imswitchclient/client.py +6 -0
- imswitchclient-0.2.0/imswitchclient/lasersManager.py +76 -0
- imswitchclient-0.2.0/imswitchclient/liveViewManager.py +38 -0
- {imswitchclient-0.1.7 → imswitchclient-0.2.0}/imswitchclient/mdaController.py +1 -1
- imswitchclient-0.2.0/imswitchclient/positionersManager.py +153 -0
- imswitchclient-0.2.0/imswitchclient/readnoiseManager.py +106 -0
- imswitchclient-0.2.0/imswitchclient/recordingManager.py +259 -0
- imswitchclient-0.2.0/imswitchclient/settingsManager.py +146 -0
- imswitchclient-0.2.0/imswitchclient/smart.py +362 -0
- imswitchclient-0.2.0/imswitchclient/uc2ConfigManager.py +50 -0
- {imswitchclient-0.1.7 → imswitchclient-0.2.0}/imswitchclient.egg-info/PKG-INFO +78 -9
- {imswitchclient-0.1.7 → imswitchclient-0.2.0}/imswitchclient.egg-info/SOURCES.txt +8 -3
- {imswitchclient-0.1.7 → imswitchclient-0.2.0}/imswitchclient.egg-info/requires.txt +1 -1
- {imswitchclient-0.1.7 → imswitchclient-0.2.0}/setup.cfg +2 -2
- {imswitchclient-0.1.7 → imswitchclient-0.2.0}/setup.py +3 -4
- imswitchclient-0.2.0/tests/test_baseurl.py +65 -0
- imswitchclient-0.2.0/tests/test_imswitchclient.py +262 -0
- imswitchclient-0.2.0/tests/test_smart.py +213 -0
- imswitchclient-0.1.7/CONTRIBUTING.rst +0 -128
- imswitchclient-0.1.7/HISTORY.rst +0 -8
- imswitchclient-0.1.7/imswitchclient/ImSwitchClient.py +0 -74
- imswitchclient-0.1.7/imswitchclient/client.py +0 -81
- imswitchclient-0.1.7/imswitchclient/lasersManager.py +0 -30
- imswitchclient-0.1.7/imswitchclient/positionersManager.py +0 -91
- imswitchclient-0.1.7/imswitchclient/recordingManager.py +0 -142
- imswitchclient-0.1.7/imswitchclient/settingsManager.py +0 -70
- imswitchclient-0.1.7/tests/test_imswitchclient.py +0 -21
- {imswitchclient-0.1.7 → imswitchclient-0.2.0}/LICENSE +0 -0
- {imswitchclient-0.1.7 → imswitchclient-0.2.0}/imswitchclient/communicationManager.py +0 -0
- {imswitchclient-0.1.7 → imswitchclient-0.2.0}/imswitchclient/experimentController.py +0 -0
- {imswitchclient-0.1.7 → imswitchclient-0.2.0}/imswitchclient/histoscanManager.py +0 -0
- {imswitchclient-0.1.7 → imswitchclient-0.2.0}/imswitchclient/ledMatrixManager.py +0 -0
- {imswitchclient-0.1.7 → imswitchclient-0.2.0}/imswitchclient/objectiveController.py +0 -0
- {imswitchclient-0.1.7 → imswitchclient-0.2.0}/imswitchclient/socketClient.py +0 -0
- {imswitchclient-0.1.7 → imswitchclient-0.2.0}/imswitchclient/viewManager.py +0 -0
- {imswitchclient-0.1.7 → imswitchclient-0.2.0}/imswitchclient.egg-info/dependency_links.txt +0 -0
- {imswitchclient-0.1.7 → imswitchclient-0.2.0}/imswitchclient.egg-info/not-zip-safe +0 -0
- {imswitchclient-0.1.7 → imswitchclient-0.2.0}/imswitchclient.egg-info/top_level.txt +0 -0
- {imswitchclient-0.1.7 → imswitchclient-0.2.0}/tests/__init__.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: imswitchclient
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.0
|
|
4
4
|
Summary: This is a package that connects ImSwitch's REST API to the rest of the world (e.g. jupyter lab)
|
|
5
5
|
Home-page: https://github.com/openuc2/imswitchclient
|
|
6
6
|
Author: Benedict Diederich
|
|
@@ -23,7 +23,7 @@ Requires-Dist: Pillow
|
|
|
23
23
|
Requires-Dist: numpy
|
|
24
24
|
Requires-Dist: matplotlib
|
|
25
25
|
Requires-Dist: opencv-python
|
|
26
|
-
Requires-Dist: python-socketio
|
|
26
|
+
Requires-Dist: python-socketio
|
|
27
27
|
Dynamic: author
|
|
28
28
|
Dynamic: author-email
|
|
29
29
|
Dynamic: classifier
|
|
@@ -81,10 +81,23 @@ pip install imswitchclient
|
|
|
81
81
|
```python
|
|
82
82
|
import imswitchclient.ImSwitchClient as imc
|
|
83
83
|
|
|
84
|
-
#
|
|
85
|
-
client = imc.ImSwitchClient(
|
|
84
|
+
# A full URL is the least ambiguous way to say where ImSwitch is:
|
|
85
|
+
client = imc.ImSwitchClient("http://localhost:8001/imswitch/api") # local instance
|
|
86
|
+
client = imc.ImSwitchClient("http://192.168.178.76/imswitch/api") # Raspberry Pi / Docker behind Caddy (port 80)
|
|
87
|
+
client = imc.ImSwitchClient("https://imswitch.openuc2.com") # https -> port 443
|
|
88
|
+
|
|
89
|
+
# Without arguments it uses $IMSWITCH_API_URL - which ImSwitch sets for the
|
|
90
|
+
# notebooks it serves itself - and otherwise http://localhost:8001/imswitch/api.
|
|
91
|
+
client = imc.ImSwitchClient()
|
|
92
|
+
|
|
93
|
+
# The classic form still works; a bare host name keeps the old defaults (port 8001, http).
|
|
94
|
+
client = imc.ImSwitchClient(host="192.168.178.76", port=80, isHttps=False)
|
|
95
|
+
|
|
96
|
+
print(client.base_uri) # http://192.168.178.76:80/imswitch/api
|
|
86
97
|
```
|
|
87
98
|
|
|
99
|
+
Pass `socket_port=` when Socket.IO is not served on the API port.
|
|
100
|
+
|
|
88
101
|
### Example: Moving a Stage and Acquiring an Image
|
|
89
102
|
|
|
90
103
|
```python
|
|
@@ -162,18 +175,30 @@ The ImSwitch API provides access to various components:
|
|
|
162
175
|
- `getAllDeviceNames()` - Get all available positioners.
|
|
163
176
|
- `getPositionerPositions()` - Get current position.
|
|
164
177
|
- `movePositioner(name, axis, value, is_absolute, is_blocking)` - Move the stage.
|
|
178
|
+
- `moveRelativeXY(dx, dy, positioner_name, is_blocking, speed)` - Relative X/Y move in µm.
|
|
179
|
+
- `stop(name, axis)` / `stopAllAxes(name)` - Stop one axis / every axis.
|
|
165
180
|
- `homeAxis(name, axis, is_blocking)` - Home the positioner.
|
|
166
181
|
|
|
167
182
|
### Lasers Manager
|
|
168
183
|
- `getLaserNames()` - Get available lasers.
|
|
169
184
|
- `setLaserActive(name, status)` - Turn laser on/off.
|
|
170
185
|
- `setLaserValue(name, value)` - Set laser intensity.
|
|
186
|
+
- `getLaserValueRanges(name)` / `getLaserValue(name)` / `getLaserActive(name)` - Read back.
|
|
187
|
+
- `setLaserOn(name, value)` / `setLaserOff(name)` - Value and on/off in one call.
|
|
188
|
+
- `runLaserFor(name, value, duration, on_tick, stop_event)` - Run a PWM channel (pump, valve)
|
|
189
|
+
for a fixed time; always switched off afterwards, also on errors or interruption.
|
|
171
190
|
|
|
172
191
|
### Recording Manager
|
|
173
192
|
- `snapNumpyToFastAPI(resizeFactor)` - Capture an image as numpy array.
|
|
174
193
|
- `startRecording(save_format)` - Begin recording with optional save format (SaveFormat enum).
|
|
175
194
|
- `stopRecording()` - Stop recording.
|
|
176
|
-
- `snapImageToPath(file_name)` - Snap image and save to specified path.
|
|
195
|
+
- `snapImageToPath(file_name, save_format, return_preview)` - Snap image and save to specified path.
|
|
196
|
+
- `snapAndWait(file_name, save_format, poll, timeout, on_progress, stop_event)` - Snap in the
|
|
197
|
+
background on the server and wait for it. Use for long exposures (minutes to hours); an
|
|
198
|
+
interrupt, `stop_event` or timeout cancels the exposure on the server.
|
|
199
|
+
- `startSnap(...)` / `getSnapStatus(job_id)` / `cancelSnap(job_id)` - The same, step by step.
|
|
200
|
+
- `previewToNumpy(result)` - 8-bit preview of a snap result as numpy array.
|
|
201
|
+
- `downloadImage(relative_file_path)` / `downloadFile(...)` - Fetch the saved file (full bit depth).
|
|
177
202
|
- `startVideoStream()` - Start MJPEG video stream.
|
|
178
203
|
- `stopVideoStream()` - Stop MJPEG video stream.
|
|
179
204
|
- `getVideoFrame()` - Get current frame from video stream.
|
|
@@ -186,6 +211,7 @@ The recording manager supports multiple save formats:
|
|
|
186
211
|
- `SaveFormat.MP4` - MP4 video format
|
|
187
212
|
- `SaveFormat.PNG` - PNG format
|
|
188
213
|
- `SaveFormat.JPG` - JPEG format
|
|
214
|
+
- `SaveFormat.OME_TIFF`, `SaveFormat.OME_ZARR`, `SaveFormat.STITCHED_TIFF`
|
|
189
215
|
|
|
190
216
|
### Settings Manager
|
|
191
217
|
- `getDetectorNames()` - Get available detector names.
|
|
@@ -194,6 +220,25 @@ The recording manager supports multiple save formats:
|
|
|
194
220
|
- `setDetectorGain(detector_name, gain)` - Set detector gain.
|
|
195
221
|
- `setDetectorParameter(detector_name, parameter_name, value)` - Set generic detector parameter.
|
|
196
222
|
- `setDetectorROI(detector_name, x, y, w, h)` - Set detector Region of Interest.
|
|
223
|
+
- `getDetectorParameter(parameter_name, detector_name)` - Read one parameter from the camera.
|
|
224
|
+
- `setDetectorMode(detector_name, is_auto)` / `setDetectorExposureOnce(detector_name)` - Auto exposure.
|
|
225
|
+
- `getCameraTemperature(detector_name)` / `setCameraTargetTemperature(temperature_c)` - Cooled cameras.
|
|
226
|
+
- `getDetectorParameterTree(detector_name)` / `setDetectorParameterValue(name, value)` - Typed access
|
|
227
|
+
to every camera parameter.
|
|
228
|
+
|
|
229
|
+
### Live View Manager
|
|
230
|
+
- `startLiveView(detector_name, protocol, params, force)` / `stopLiveView(detector_name)`
|
|
231
|
+
- `getLiveViewActive()`, `getActiveStreams()`, `getLongExposureInfo(detector_name)`
|
|
232
|
+
|
|
233
|
+
### UC2 Config Manager
|
|
234
|
+
- `getBusStatus()` - CAN-bus power and emergency-stop state.
|
|
235
|
+
- `setBusPower(enable)` / `setBusPowerAndWait(enable, settle)` - Switch the power of all CAN
|
|
236
|
+
slave boards (motors, LED/laser boards). Raises if the board refuses.
|
|
237
|
+
- `isBoardConnected()`, `getBoardTemperature()`
|
|
238
|
+
|
|
239
|
+
### Autofocus Manager
|
|
240
|
+
- `autoFocusAndWait(rangez, resolutionz, ...)` - Server-side Z scan around the current focus,
|
|
241
|
+
waits for the result. `autoFocus`, `getAutofocusStatus`, `stopAutofocus` for manual control.
|
|
197
242
|
|
|
198
243
|
### View Manager
|
|
199
244
|
- `setLiveViewActive(active)` - Enable/disable live view.
|
|
@@ -208,6 +253,30 @@ The recording manager supports multiple save formats:
|
|
|
208
253
|
- `setLED(led_id, state)` - Set specific LED with ID and state.
|
|
209
254
|
- `setSpecial(pattern, intensity, get_return)` - Set special LED pattern.
|
|
210
255
|
|
|
256
|
+
### Smart microscopy helpers (`imswitchclient.smart`)
|
|
257
|
+
Building blocks for closed-loop experiments; they work on numpy frames and plain callables and are
|
|
258
|
+
tested without a microscope (`tests/test_smart.py`).
|
|
259
|
+
|
|
260
|
+
```python
|
|
261
|
+
from imswitchclient import smart
|
|
262
|
+
snap = smart.make_snap(client) # -> float32 frame
|
|
263
|
+
move = smart.make_relative_mover(client) # move(dx_um, dy_um), blocking
|
|
264
|
+
|
|
265
|
+
cal = smart.StageCameraCalibration.measure(snap, move, step_um=50)
|
|
266
|
+
obj = smart.find_objects(snap())[0] # largest blob
|
|
267
|
+
move(*cal.move_to_center(*obj["offset"])) # centre it - rotation and flips included
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
- `StageCameraCalibration` - pixel offsets <-> stage moves, measured on the live images.
|
|
271
|
+
- `phase_correlate(a, b)` - sub-pixel image shift.
|
|
272
|
+
- `find_objects(frame)` / `foreground_mask(frame)` - blob detection.
|
|
273
|
+
- `EmptyFieldReference(empty_frame).coverage(frame)` - how much of a field is sample (0..1).
|
|
274
|
+
- `SafetyBox` - limits closed-loop moves to a square around the start.
|
|
275
|
+
- `Mosaic(frames, positions, cal)` - quick overview of frames placed by stage position.
|
|
276
|
+
|
|
277
|
+
The ImSwitch notebooks `ImSwitch_Smart_ObjectTracking`, `ImSwitch_Smart_PathFollowing` and
|
|
278
|
+
`ImSwitch_Smart_AdaptiveImaging` show them in use.
|
|
279
|
+
|
|
211
280
|
### Communication Manager
|
|
212
281
|
- `acquireImage()` - Acquire an image through communication channel.
|
|
213
282
|
- `getImage()` - Get an image from communication channel.
|
|
@@ -246,7 +315,7 @@ import numpy as np
|
|
|
246
315
|
import matplotlib.pyplot as plt
|
|
247
316
|
|
|
248
317
|
# Initialize client
|
|
249
|
-
client = imc.ImSwitchClient(
|
|
318
|
+
client = imc.ImSwitchClient("http://192.168.1.100:8001/imswitch/api")
|
|
250
319
|
|
|
251
320
|
# XY scanning parameters
|
|
252
321
|
start_x, start_y = 0, 0 # Starting position in µm
|
|
@@ -309,7 +378,7 @@ import imswitchclient.ImSwitchClient as imc
|
|
|
309
378
|
import numpy as np
|
|
310
379
|
|
|
311
380
|
# Initialize client
|
|
312
|
-
client = imc.ImSwitchClient(
|
|
381
|
+
client = imc.ImSwitchClient("http://192.168.1.100:8001/imswitch/api")
|
|
313
382
|
|
|
314
383
|
def calculate_focus_score(image):
|
|
315
384
|
"""Calculate focus score using Laplacian variance"""
|
|
@@ -362,7 +431,7 @@ from imswitchclient.recordingManager import SaveFormat
|
|
|
362
431
|
import time
|
|
363
432
|
|
|
364
433
|
# Initialize client
|
|
365
|
-
client = imc.ImSwitchClient(
|
|
434
|
+
client = imc.ImSwitchClient("http://192.168.1.100:8001/imswitch/api")
|
|
366
435
|
|
|
367
436
|
# Setup LED illumination
|
|
368
437
|
client.ledMatrixManager.setAllLEDOff()
|
|
@@ -407,7 +476,7 @@ import imswitchclient.ImSwitchClient as imc
|
|
|
407
476
|
from imswitchclient.recordingManager import SaveFormat
|
|
408
477
|
|
|
409
478
|
# Initialize client
|
|
410
|
-
client = imc.ImSwitchClient(
|
|
479
|
+
client = imc.ImSwitchClient("http://192.168.1.100:8001/imswitch/api")
|
|
411
480
|
|
|
412
481
|
# Define multiple positions of interest
|
|
413
482
|
positions = [
|
|
@@ -42,10 +42,23 @@ pip install imswitchclient
|
|
|
42
42
|
```python
|
|
43
43
|
import imswitchclient.ImSwitchClient as imc
|
|
44
44
|
|
|
45
|
-
#
|
|
46
|
-
client = imc.ImSwitchClient(
|
|
45
|
+
# A full URL is the least ambiguous way to say where ImSwitch is:
|
|
46
|
+
client = imc.ImSwitchClient("http://localhost:8001/imswitch/api") # local instance
|
|
47
|
+
client = imc.ImSwitchClient("http://192.168.178.76/imswitch/api") # Raspberry Pi / Docker behind Caddy (port 80)
|
|
48
|
+
client = imc.ImSwitchClient("https://imswitch.openuc2.com") # https -> port 443
|
|
49
|
+
|
|
50
|
+
# Without arguments it uses $IMSWITCH_API_URL - which ImSwitch sets for the
|
|
51
|
+
# notebooks it serves itself - and otherwise http://localhost:8001/imswitch/api.
|
|
52
|
+
client = imc.ImSwitchClient()
|
|
53
|
+
|
|
54
|
+
# The classic form still works; a bare host name keeps the old defaults (port 8001, http).
|
|
55
|
+
client = imc.ImSwitchClient(host="192.168.178.76", port=80, isHttps=False)
|
|
56
|
+
|
|
57
|
+
print(client.base_uri) # http://192.168.178.76:80/imswitch/api
|
|
47
58
|
```
|
|
48
59
|
|
|
60
|
+
Pass `socket_port=` when Socket.IO is not served on the API port.
|
|
61
|
+
|
|
49
62
|
### Example: Moving a Stage and Acquiring an Image
|
|
50
63
|
|
|
51
64
|
```python
|
|
@@ -123,18 +136,30 @@ The ImSwitch API provides access to various components:
|
|
|
123
136
|
- `getAllDeviceNames()` - Get all available positioners.
|
|
124
137
|
- `getPositionerPositions()` - Get current position.
|
|
125
138
|
- `movePositioner(name, axis, value, is_absolute, is_blocking)` - Move the stage.
|
|
139
|
+
- `moveRelativeXY(dx, dy, positioner_name, is_blocking, speed)` - Relative X/Y move in µm.
|
|
140
|
+
- `stop(name, axis)` / `stopAllAxes(name)` - Stop one axis / every axis.
|
|
126
141
|
- `homeAxis(name, axis, is_blocking)` - Home the positioner.
|
|
127
142
|
|
|
128
143
|
### Lasers Manager
|
|
129
144
|
- `getLaserNames()` - Get available lasers.
|
|
130
145
|
- `setLaserActive(name, status)` - Turn laser on/off.
|
|
131
146
|
- `setLaserValue(name, value)` - Set laser intensity.
|
|
147
|
+
- `getLaserValueRanges(name)` / `getLaserValue(name)` / `getLaserActive(name)` - Read back.
|
|
148
|
+
- `setLaserOn(name, value)` / `setLaserOff(name)` - Value and on/off in one call.
|
|
149
|
+
- `runLaserFor(name, value, duration, on_tick, stop_event)` - Run a PWM channel (pump, valve)
|
|
150
|
+
for a fixed time; always switched off afterwards, also on errors or interruption.
|
|
132
151
|
|
|
133
152
|
### Recording Manager
|
|
134
153
|
- `snapNumpyToFastAPI(resizeFactor)` - Capture an image as numpy array.
|
|
135
154
|
- `startRecording(save_format)` - Begin recording with optional save format (SaveFormat enum).
|
|
136
155
|
- `stopRecording()` - Stop recording.
|
|
137
|
-
- `snapImageToPath(file_name)` - Snap image and save to specified path.
|
|
156
|
+
- `snapImageToPath(file_name, save_format, return_preview)` - Snap image and save to specified path.
|
|
157
|
+
- `snapAndWait(file_name, save_format, poll, timeout, on_progress, stop_event)` - Snap in the
|
|
158
|
+
background on the server and wait for it. Use for long exposures (minutes to hours); an
|
|
159
|
+
interrupt, `stop_event` or timeout cancels the exposure on the server.
|
|
160
|
+
- `startSnap(...)` / `getSnapStatus(job_id)` / `cancelSnap(job_id)` - The same, step by step.
|
|
161
|
+
- `previewToNumpy(result)` - 8-bit preview of a snap result as numpy array.
|
|
162
|
+
- `downloadImage(relative_file_path)` / `downloadFile(...)` - Fetch the saved file (full bit depth).
|
|
138
163
|
- `startVideoStream()` - Start MJPEG video stream.
|
|
139
164
|
- `stopVideoStream()` - Stop MJPEG video stream.
|
|
140
165
|
- `getVideoFrame()` - Get current frame from video stream.
|
|
@@ -147,6 +172,7 @@ The recording manager supports multiple save formats:
|
|
|
147
172
|
- `SaveFormat.MP4` - MP4 video format
|
|
148
173
|
- `SaveFormat.PNG` - PNG format
|
|
149
174
|
- `SaveFormat.JPG` - JPEG format
|
|
175
|
+
- `SaveFormat.OME_TIFF`, `SaveFormat.OME_ZARR`, `SaveFormat.STITCHED_TIFF`
|
|
150
176
|
|
|
151
177
|
### Settings Manager
|
|
152
178
|
- `getDetectorNames()` - Get available detector names.
|
|
@@ -155,6 +181,25 @@ The recording manager supports multiple save formats:
|
|
|
155
181
|
- `setDetectorGain(detector_name, gain)` - Set detector gain.
|
|
156
182
|
- `setDetectorParameter(detector_name, parameter_name, value)` - Set generic detector parameter.
|
|
157
183
|
- `setDetectorROI(detector_name, x, y, w, h)` - Set detector Region of Interest.
|
|
184
|
+
- `getDetectorParameter(parameter_name, detector_name)` - Read one parameter from the camera.
|
|
185
|
+
- `setDetectorMode(detector_name, is_auto)` / `setDetectorExposureOnce(detector_name)` - Auto exposure.
|
|
186
|
+
- `getCameraTemperature(detector_name)` / `setCameraTargetTemperature(temperature_c)` - Cooled cameras.
|
|
187
|
+
- `getDetectorParameterTree(detector_name)` / `setDetectorParameterValue(name, value)` - Typed access
|
|
188
|
+
to every camera parameter.
|
|
189
|
+
|
|
190
|
+
### Live View Manager
|
|
191
|
+
- `startLiveView(detector_name, protocol, params, force)` / `stopLiveView(detector_name)`
|
|
192
|
+
- `getLiveViewActive()`, `getActiveStreams()`, `getLongExposureInfo(detector_name)`
|
|
193
|
+
|
|
194
|
+
### UC2 Config Manager
|
|
195
|
+
- `getBusStatus()` - CAN-bus power and emergency-stop state.
|
|
196
|
+
- `setBusPower(enable)` / `setBusPowerAndWait(enable, settle)` - Switch the power of all CAN
|
|
197
|
+
slave boards (motors, LED/laser boards). Raises if the board refuses.
|
|
198
|
+
- `isBoardConnected()`, `getBoardTemperature()`
|
|
199
|
+
|
|
200
|
+
### Autofocus Manager
|
|
201
|
+
- `autoFocusAndWait(rangez, resolutionz, ...)` - Server-side Z scan around the current focus,
|
|
202
|
+
waits for the result. `autoFocus`, `getAutofocusStatus`, `stopAutofocus` for manual control.
|
|
158
203
|
|
|
159
204
|
### View Manager
|
|
160
205
|
- `setLiveViewActive(active)` - Enable/disable live view.
|
|
@@ -169,6 +214,30 @@ The recording manager supports multiple save formats:
|
|
|
169
214
|
- `setLED(led_id, state)` - Set specific LED with ID and state.
|
|
170
215
|
- `setSpecial(pattern, intensity, get_return)` - Set special LED pattern.
|
|
171
216
|
|
|
217
|
+
### Smart microscopy helpers (`imswitchclient.smart`)
|
|
218
|
+
Building blocks for closed-loop experiments; they work on numpy frames and plain callables and are
|
|
219
|
+
tested without a microscope (`tests/test_smart.py`).
|
|
220
|
+
|
|
221
|
+
```python
|
|
222
|
+
from imswitchclient import smart
|
|
223
|
+
snap = smart.make_snap(client) # -> float32 frame
|
|
224
|
+
move = smart.make_relative_mover(client) # move(dx_um, dy_um), blocking
|
|
225
|
+
|
|
226
|
+
cal = smart.StageCameraCalibration.measure(snap, move, step_um=50)
|
|
227
|
+
obj = smart.find_objects(snap())[0] # largest blob
|
|
228
|
+
move(*cal.move_to_center(*obj["offset"])) # centre it - rotation and flips included
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
- `StageCameraCalibration` - pixel offsets <-> stage moves, measured on the live images.
|
|
232
|
+
- `phase_correlate(a, b)` - sub-pixel image shift.
|
|
233
|
+
- `find_objects(frame)` / `foreground_mask(frame)` - blob detection.
|
|
234
|
+
- `EmptyFieldReference(empty_frame).coverage(frame)` - how much of a field is sample (0..1).
|
|
235
|
+
- `SafetyBox` - limits closed-loop moves to a square around the start.
|
|
236
|
+
- `Mosaic(frames, positions, cal)` - quick overview of frames placed by stage position.
|
|
237
|
+
|
|
238
|
+
The ImSwitch notebooks `ImSwitch_Smart_ObjectTracking`, `ImSwitch_Smart_PathFollowing` and
|
|
239
|
+
`ImSwitch_Smart_AdaptiveImaging` show them in use.
|
|
240
|
+
|
|
172
241
|
### Communication Manager
|
|
173
242
|
- `acquireImage()` - Acquire an image through communication channel.
|
|
174
243
|
- `getImage()` - Get an image from communication channel.
|
|
@@ -207,7 +276,7 @@ import numpy as np
|
|
|
207
276
|
import matplotlib.pyplot as plt
|
|
208
277
|
|
|
209
278
|
# Initialize client
|
|
210
|
-
client = imc.ImSwitchClient(
|
|
279
|
+
client = imc.ImSwitchClient("http://192.168.1.100:8001/imswitch/api")
|
|
211
280
|
|
|
212
281
|
# XY scanning parameters
|
|
213
282
|
start_x, start_y = 0, 0 # Starting position in µm
|
|
@@ -270,7 +339,7 @@ import imswitchclient.ImSwitchClient as imc
|
|
|
270
339
|
import numpy as np
|
|
271
340
|
|
|
272
341
|
# Initialize client
|
|
273
|
-
client = imc.ImSwitchClient(
|
|
342
|
+
client = imc.ImSwitchClient("http://192.168.1.100:8001/imswitch/api")
|
|
274
343
|
|
|
275
344
|
def calculate_focus_score(image):
|
|
276
345
|
"""Calculate focus score using Laplacian variance"""
|
|
@@ -323,7 +392,7 @@ from imswitchclient.recordingManager import SaveFormat
|
|
|
323
392
|
import time
|
|
324
393
|
|
|
325
394
|
# Initialize client
|
|
326
|
-
client = imc.ImSwitchClient(
|
|
395
|
+
client = imc.ImSwitchClient("http://192.168.1.100:8001/imswitch/api")
|
|
327
396
|
|
|
328
397
|
# Setup LED illumination
|
|
329
398
|
client.ledMatrixManager.setAllLEDOff()
|
|
@@ -368,7 +437,7 @@ import imswitchclient.ImSwitchClient as imc
|
|
|
368
437
|
from imswitchclient.recordingManager import SaveFormat
|
|
369
438
|
|
|
370
439
|
# Initialize client
|
|
371
|
-
client = imc.ImSwitchClient(
|
|
440
|
+
client = imc.ImSwitchClient("http://192.168.1.100:8001/imswitch/api")
|
|
372
441
|
|
|
373
442
|
# Define multiple positions of interest
|
|
374
443
|
positions = [
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import requests
|
|
3
|
+
import logging
|
|
4
|
+
from urllib.parse import urlsplit
|
|
5
|
+
from .positionersManager import positionersManager
|
|
6
|
+
from .recordingManager import recordingManager
|
|
7
|
+
from .lasersManager import lasersManager
|
|
8
|
+
from .histoscanManager import histoscanManager
|
|
9
|
+
from .experimentController import ExperimentController
|
|
10
|
+
from .mdaController import mdaController
|
|
11
|
+
from .objectiveController import objectiveController
|
|
12
|
+
from .ledMatrixManager import ledMatrixManager
|
|
13
|
+
from .settingsManager import settingsManager
|
|
14
|
+
from .readnoiseManager import readnoiseManager
|
|
15
|
+
from .viewManager import viewManager
|
|
16
|
+
from .communicationManager import communicationManager
|
|
17
|
+
from .uc2ConfigManager import uc2ConfigManager
|
|
18
|
+
from .liveViewManager import liveViewManager
|
|
19
|
+
from .autofocusManager import autofocusManager
|
|
20
|
+
from .socketClient import socketClient
|
|
21
|
+
|
|
22
|
+
DEFAULT_ROUTE = "/imswitch/api"
|
|
23
|
+
DEFAULT_PORT = 8001
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def parseBaseUrl(url, defaultRoute=DEFAULT_ROUTE):
|
|
27
|
+
"""Split a base URL into (host, port, isHttps, route).
|
|
28
|
+
|
|
29
|
+
Takes whatever you can copy out of the browser's address bar::
|
|
30
|
+
|
|
31
|
+
http://192.168.178.76/imswitch/api/docs -> ('192.168.178.76', 80, False, '/imswitch/api')
|
|
32
|
+
http://100.100.43.118:8001 -> ('100.100.43.118', 8001, False, '/imswitch/api')
|
|
33
|
+
https://imswitch.openuc2.com -> ('imswitch.openuc2.com', 443, True, '/imswitch/api')
|
|
34
|
+
"""
|
|
35
|
+
parts = urlsplit(url if "://" in url else f"http://{url}")
|
|
36
|
+
isHttps = parts.scheme == "https"
|
|
37
|
+
route = parts.path.rstrip("/")
|
|
38
|
+
for suffix in ("/docs", "/redoc", "/openapi.json"):
|
|
39
|
+
if route.endswith(suffix):
|
|
40
|
+
route = route[: -len(suffix)]
|
|
41
|
+
port = parts.port or (443 if isHttps else 80)
|
|
42
|
+
return parts.hostname, port, isHttps, route or defaultRoute
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class ImSwitchClient(object):
|
|
46
|
+
def __init__(self, host=None, isHttps=None, port=None, route=None, socket_port=None):
|
|
47
|
+
"""Connect to an ImSwitch server.
|
|
48
|
+
|
|
49
|
+
`host` may be a full URL, which is the least error-prone way to reach a
|
|
50
|
+
server that is not on the default port::
|
|
51
|
+
|
|
52
|
+
ImSwitchClient("http://192.168.178.76/imswitch/api") # Docker/Caddy on port 80
|
|
53
|
+
ImSwitchClient("http://100.100.43.118:8001") # local instance
|
|
54
|
+
ImSwitchClient("imswitch.openuc2.com") # host only -> port 8001
|
|
55
|
+
ImSwitchClient() # $IMSWITCH_API_URL, else localhost:8001
|
|
56
|
+
|
|
57
|
+
A plain host name keeps the historical defaults (port 8001, http). A URL
|
|
58
|
+
without a port uses the scheme's port (80/443). Explicit `isHttps`,
|
|
59
|
+
`port` and `route` arguments always win over what the URL says.
|
|
60
|
+
`socket_port` defaults to the API port - set it when Socket.IO is not
|
|
61
|
+
proxied on the same port.
|
|
62
|
+
"""
|
|
63
|
+
url = host if (host and "://" in host) else None
|
|
64
|
+
if url is None and host is None:
|
|
65
|
+
url = os.environ.get("IMSWITCH_API_URL") or None
|
|
66
|
+
if url:
|
|
67
|
+
urlHost, urlPort, urlHttps, urlRoute = parseBaseUrl(url)
|
|
68
|
+
else:
|
|
69
|
+
urlHost, urlPort, urlHttps, urlRoute = host, None, None, None
|
|
70
|
+
|
|
71
|
+
self.host = urlHost or "localhost"
|
|
72
|
+
self.isHttps = isHttps if isHttps is not None else bool(urlHttps)
|
|
73
|
+
self.port = port if port is not None else (urlPort or (443 if self.isHttps else DEFAULT_PORT))
|
|
74
|
+
self.route = (route or urlRoute or DEFAULT_ROUTE).rstrip("/")
|
|
75
|
+
self.get_json(self.base_swagger_uri)
|
|
76
|
+
|
|
77
|
+
logging.info(f"Connecting to microscope at {self.base_uri}")
|
|
78
|
+
|
|
79
|
+
# register managers
|
|
80
|
+
self.positionersManager = positionersManager(self)
|
|
81
|
+
self.recordingManager = recordingManager(self)
|
|
82
|
+
self.lasersManager = lasersManager(self)
|
|
83
|
+
self.histoscanManager = histoscanManager(self)
|
|
84
|
+
self.experimentController = ExperimentController(self)
|
|
85
|
+
self.mdaController = mdaController(self)
|
|
86
|
+
self.objectiveController = objectiveController(self)
|
|
87
|
+
self.ledMatrixManager = ledMatrixManager(self)
|
|
88
|
+
self.settingsManager = settingsManager(self)
|
|
89
|
+
self.readnoiseManager = readnoiseManager(self)
|
|
90
|
+
self.viewManager = viewManager(self)
|
|
91
|
+
self.communicationManager = communicationManager(self)
|
|
92
|
+
self.uc2ConfigManager = uc2ConfigManager(self)
|
|
93
|
+
self.liveViewManager = liveViewManager(self)
|
|
94
|
+
self.autofocusManager = autofocusManager(self)
|
|
95
|
+
|
|
96
|
+
# initialize Socket.IO client
|
|
97
|
+
try:
|
|
98
|
+
self.socketClient = socketClient(host=self.host,
|
|
99
|
+
port=socket_port or self.port,
|
|
100
|
+
isHttps=self.isHttps)
|
|
101
|
+
except Exception as e:
|
|
102
|
+
logging.error(f"Failed to connect Socket.IO client: {e}")
|
|
103
|
+
self.socketClient = None
|
|
104
|
+
|
|
105
|
+
@property
|
|
106
|
+
def base_uri(self):
|
|
107
|
+
scheme = "https" if self.isHttps else "http"
|
|
108
|
+
return f"{scheme}://{self.host}:{self.port}{self.route}"
|
|
109
|
+
|
|
110
|
+
@property
|
|
111
|
+
def base_root_uri(self):
|
|
112
|
+
"""Server root without the API suffix - serves /data, /jupyter, /openapi.json"""
|
|
113
|
+
base = self.base_uri
|
|
114
|
+
return base[:-4] if base.endswith("/api") else base
|
|
115
|
+
|
|
116
|
+
@property
|
|
117
|
+
def base_swagger_uri(self):
|
|
118
|
+
return self.base_root_uri + "/openapi.json"
|
|
119
|
+
|
|
120
|
+
def get_json(self, path, payload={}, headers={}, timeout=30):
|
|
121
|
+
"""Perform an HTTP GET request and return the JSON response"""
|
|
122
|
+
if not path.startswith("http"):
|
|
123
|
+
path = self.base_uri + path
|
|
124
|
+
r = requests.get(path, params=payload, headers=headers, verify=False, timeout=timeout)
|
|
125
|
+
r.raise_for_status()
|
|
126
|
+
return r.json()
|
|
127
|
+
|
|
128
|
+
def post_json(self, path, payload={}, headers={}, wait_on_task="auto", params=None, timeout=30):
|
|
129
|
+
"""Make an HTTP POST request and return the JSON response.
|
|
130
|
+
|
|
131
|
+
`params` goes into the query string: FastAPI puts scalar arguments of a
|
|
132
|
+
POST endpoint there, only pydantic-model arguments come from the body.
|
|
133
|
+
"""
|
|
134
|
+
if not path.startswith("http"):
|
|
135
|
+
path = self.base_uri + path
|
|
136
|
+
r = requests.post(path, json=payload, params=params, headers=headers,
|
|
137
|
+
verify=False, timeout=timeout)
|
|
138
|
+
r.raise_for_status()
|
|
139
|
+
r = r.json()
|
|
140
|
+
return r
|
|
141
|
+
|
|
142
|
+
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import time
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class autofocusManager(object):
|
|
5
|
+
"""Server-side autofocus (AutofocusController): a Z step scan around the
|
|
6
|
+
current position with a Gaussian fit of the focus metric. Runs on the
|
|
7
|
+
server; `autoFocusAndWait` polls it to completion."""
|
|
8
|
+
|
|
9
|
+
def __init__(self, parent):
|
|
10
|
+
self.parent = parent
|
|
11
|
+
|
|
12
|
+
def _url(self, func):
|
|
13
|
+
return f"{self.parent.base_uri}/AutofocusController/{func}"
|
|
14
|
+
|
|
15
|
+
def autoFocus(self, rangez=100, resolutionz=10, tSettle=0.1, focusAlgorithm="LAPE",
|
|
16
|
+
nCropsize=2048, nGauss=0, twoStage=False):
|
|
17
|
+
"""Start a scan of ±`rangez` µm in `resolutionz` µm steps; returns at once"""
|
|
18
|
+
payload = {'rangez': rangez, 'resolutionz': resolutionz, 'tSettle': tSettle,
|
|
19
|
+
'focusAlgorithm': focusAlgorithm, 'nCropsize': nCropsize, 'nGauss': nGauss,
|
|
20
|
+
'twoStage': twoStage}
|
|
21
|
+
return self.parent.get_json(self._url("autoFocus"), payload=payload)
|
|
22
|
+
|
|
23
|
+
def getAutofocusStatus(self):
|
|
24
|
+
"""{'state': idle|starting|scanning|fitting|moving_to_focus|finished|aborted|error, ...}"""
|
|
25
|
+
return self.parent.get_json(self._url("getAutofocusStatus"))
|
|
26
|
+
|
|
27
|
+
def stopAutofocus(self):
|
|
28
|
+
return self.parent.get_json(self._url("stopAutofocus"))
|
|
29
|
+
|
|
30
|
+
def autoFocusAndWait(self, rangez=100, resolutionz=10, poll=0.5, timeout=300, **kwargs):
|
|
31
|
+
"""`autoFocus` and wait for it; returns the final status dict.
|
|
32
|
+
|
|
33
|
+
Raises if the server refuses to start, the scan ends in error/aborted,
|
|
34
|
+
or `timeout` s pass (the scan is stopped then).
|
|
35
|
+
"""
|
|
36
|
+
started = self.autoFocus(rangez=rangez, resolutionz=resolutionz, **kwargs)
|
|
37
|
+
if isinstance(started, dict) and started.get("status") == "error":
|
|
38
|
+
raise RuntimeError(f"autofocus refused: {started.get('message')}")
|
|
39
|
+
t0 = time.time()
|
|
40
|
+
time.sleep(poll)
|
|
41
|
+
while True:
|
|
42
|
+
status = self.getAutofocusStatus()
|
|
43
|
+
state = status.get("state")
|
|
44
|
+
if state in ("finished", "idle") and not status.get("isRunning"):
|
|
45
|
+
return status
|
|
46
|
+
if state in ("error", "aborted"):
|
|
47
|
+
raise RuntimeError(f"autofocus ended with state '{state}'")
|
|
48
|
+
if time.time() - t0 > timeout:
|
|
49
|
+
self.stopAutofocus()
|
|
50
|
+
raise TimeoutError(f"autofocus exceeded {timeout} s")
|
|
51
|
+
time.sleep(poll)
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
"""Backwards-compatible alias.
|
|
2
|
+
|
|
3
|
+
The client used to exist twice, and this copy pointed at `/imswitch/api/v2`,
|
|
4
|
+
a route the server does not serve. Everything lives in `ImSwitchClient.py` now.
|
|
5
|
+
"""
|
|
6
|
+
from .ImSwitchClient import ImSwitchClient, parseBaseUrl # noqa: F401
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import time
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class lasersManager(object):
|
|
5
|
+
|
|
6
|
+
def __init__(self, parent):
|
|
7
|
+
self.parent = parent
|
|
8
|
+
|
|
9
|
+
def getLaserNames(self):
|
|
10
|
+
url = f"{self.parent.base_uri}/LaserController/getLaserNames"
|
|
11
|
+
headers = {'accept': 'application/json'}
|
|
12
|
+
|
|
13
|
+
response = self.parent.get_json(url, headers=headers)
|
|
14
|
+
return response
|
|
15
|
+
|
|
16
|
+
def getLaserValueRanges(self, laser_name):
|
|
17
|
+
"""(min, max) of the value `setLaserValue` accepts, e.g. (0, 1023) for a PWM channel"""
|
|
18
|
+
url = f"{self.parent.base_uri}/LaserController/getLaserValueRanges"
|
|
19
|
+
return self.parent.get_json(url, payload={'laserName': laser_name})
|
|
20
|
+
|
|
21
|
+
def getLaserValue(self, laser_name):
|
|
22
|
+
url = f"{self.parent.base_uri}/LaserController/getLaserValue"
|
|
23
|
+
return self.parent.get_json(url, payload={'laserName': laser_name})
|
|
24
|
+
|
|
25
|
+
def getLaserActive(self, laser_name):
|
|
26
|
+
url = f"{self.parent.base_uri}/LaserController/getLaserActive"
|
|
27
|
+
return self.parent.get_json(url, payload={'laserName': laser_name})
|
|
28
|
+
|
|
29
|
+
def setLaserValue(self, laser_name, value=0):
|
|
30
|
+
url = f"{self.parent.base_uri}/LaserController/setLaserValue"
|
|
31
|
+
payload = {
|
|
32
|
+
'laserName': laser_name,
|
|
33
|
+
'value': value
|
|
34
|
+
}
|
|
35
|
+
response = self.parent.get_json(url, payload=payload)
|
|
36
|
+
return response
|
|
37
|
+
|
|
38
|
+
def setLaserActive(self, laser_name, active=True):
|
|
39
|
+
url = f"{self.parent.base_uri}/LaserController/setLaserActive"
|
|
40
|
+
payload = {
|
|
41
|
+
"laserName": laser_name,
|
|
42
|
+
"active": active
|
|
43
|
+
}
|
|
44
|
+
response = self.parent.get_json(url, payload=payload)
|
|
45
|
+
return response
|
|
46
|
+
|
|
47
|
+
def setLaserOn(self, laser_name, value):
|
|
48
|
+
"""Set the value and switch the channel on"""
|
|
49
|
+
self.setLaserValue(laser_name, value)
|
|
50
|
+
return self.setLaserActive(laser_name, True)
|
|
51
|
+
|
|
52
|
+
def setLaserOff(self, laser_name):
|
|
53
|
+
"""Set the value to 0 and switch the channel off"""
|
|
54
|
+
self.setLaserValue(laser_name, 0)
|
|
55
|
+
return self.setLaserActive(laser_name, False)
|
|
56
|
+
|
|
57
|
+
def runLaserFor(self, laser_name, value, duration, on_tick=None, tick=1.0, stop_event=None):
|
|
58
|
+
"""Drive a channel at `value` for `duration` seconds, then switch it off.
|
|
59
|
+
|
|
60
|
+
Meant for actuators wired to a laser/PWM output (pumps, valves). The
|
|
61
|
+
channel is switched off in a `finally`, so an interrupted notebook
|
|
62
|
+
cell (KeyboardInterrupt) or a failed request does not leave a pump
|
|
63
|
+
running. `on_tick(elapsed_s)` is called about every `tick` seconds.
|
|
64
|
+
Setting the `threading.Event` `stop_event` ends the run early.
|
|
65
|
+
"""
|
|
66
|
+
self.setLaserOn(laser_name, value)
|
|
67
|
+
try:
|
|
68
|
+
t0 = time.time()
|
|
69
|
+
while (elapsed := time.time() - t0) < duration:
|
|
70
|
+
if stop_event is not None and stop_event.is_set():
|
|
71
|
+
break
|
|
72
|
+
if on_tick:
|
|
73
|
+
on_tick(elapsed)
|
|
74
|
+
time.sleep(min(tick, duration - elapsed))
|
|
75
|
+
finally:
|
|
76
|
+
self.setLaserOff(laser_name)
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
class liveViewManager(object):
|
|
2
|
+
"""Server-side live stream (LiveViewController).
|
|
3
|
+
|
|
4
|
+
Long exposures stop the stream on the server; snaps arm the camera on
|
|
5
|
+
demand, so the stream only needs to run for things that converge on
|
|
6
|
+
streamed frames, such as auto exposure.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
def __init__(self, parent):
|
|
10
|
+
self.parent = parent
|
|
11
|
+
|
|
12
|
+
def _url(self, func):
|
|
13
|
+
return f"{self.parent.base_uri}/LiveViewController/{func}"
|
|
14
|
+
|
|
15
|
+
def startLiveView(self, detector_name=None, protocol="jpeg", params=None, force=False):
|
|
16
|
+
"""Start the stream. Refused with status 'long_exposure' above the
|
|
17
|
+
server's long-exposure threshold unless `force` is set."""
|
|
18
|
+
query = {'protocol': protocol, 'force': force}
|
|
19
|
+
if detector_name is not None:
|
|
20
|
+
query['detectorName'] = detector_name
|
|
21
|
+
return self.parent.post_json(self._url("startLiveView"), payload=params, params=query)
|
|
22
|
+
|
|
23
|
+
def stopLiveView(self, detector_name=None, stop_camera=True):
|
|
24
|
+
payload = {'stopCamera': stop_camera}
|
|
25
|
+
if detector_name is not None:
|
|
26
|
+
payload['detectorName'] = detector_name
|
|
27
|
+
return self.parent.get_json(self._url("stopLiveView"), payload=payload)
|
|
28
|
+
|
|
29
|
+
def getLiveViewActive(self):
|
|
30
|
+
return self.parent.get_json(self._url("getLiveViewActive"))
|
|
31
|
+
|
|
32
|
+
def getActiveStreams(self):
|
|
33
|
+
return self.parent.get_json(self._url("getActiveStreams"))
|
|
34
|
+
|
|
35
|
+
def getLongExposureInfo(self, detector_name=None):
|
|
36
|
+
"""{'exposureMs', 'thresholdMs', 'isLongExposure'}"""
|
|
37
|
+
payload = {} if detector_name is None else {'detectorName': detector_name}
|
|
38
|
+
return self.parent.get_json(self._url("getLongExposureInfo"), payload=payload)
|