Skip to content

Commit 874adda

Browse files
author
Danail Stoychev
committed
Integrate PR python-microscope#133.
1 parent f8782a2 commit 874adda

File tree

1 file changed

+140
-17
lines changed

1 file changed

+140
-17
lines changed

microscope/filterwheels/aurox.py

Lines changed: 140 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -19,16 +19,43 @@
1919

2020
"""Adds support for Aurox devices
2121
22-
Requires package hidapi."""
22+
Requires package hidapi.
23+
24+
Config sample:
25+
26+
device(microscope.filterwheels.aurox.Clarity,
27+
{'camera': 'microscope.Cameras.cameramodule.SomeCamera',
28+
'camera.someSetting': value})
29+
30+
Deconvolving data requires:
31+
* availability of clarity_process and cv2
32+
* successful completion of a calibration step
33+
+ set_mode(Modes.calibrate)
34+
+ trigger the camera to generate an image
35+
+ when the camera returns the image, calibration is complete
36+
"""
2337

2438
import time
2539
from threading import Lock
26-
40+
import typing
41+
import enum
42+
import logging
2743
import hid
28-
2944
import microscope
3045
import microscope.devices
3146

47+
_logger = logging.getLogger(__name__)
48+
49+
try:
50+
# Currently, clarity_process is a module that is not packaged, so needs
51+
# to be put on the python path somewhere manually.
52+
from clarity_process import ClarityProcessor
53+
except Exception:
54+
_logger.warning(
55+
"Could not import clarity_process module:" "no processing available."
56+
)
57+
58+
Mode = enum.IntEnum("Mode", "difference, raw, calibrate")
3259

3360
# Clarity constants. These may differ across products, so mangle names.
3461
# USB IDs
@@ -80,7 +107,51 @@
80107
_Clarity__SETSVCMODE1 = 0xE0 # 1 byte for service mode. SLEEP activates service mode. RUN returns to normal mode.
81108

82109

83-
class Clarity(microscope.devices.FilterWheelBase):
110+
class _CameraAugmentor:
111+
def __init__(self, **kwargs):
112+
super().__init__(**kwargs)
113+
self._aurox_mode = Mode.raw
114+
self._processor = None
115+
116+
def set_aurox_mode(self, mode):
117+
self._aurox_mode = mode
118+
119+
def _process_data(self, data):
120+
"""Process data depending on state of self._aurox_mode."""
121+
if self._aurox_mode == Mode.raw:
122+
return data
123+
elif self._aurox_mode == Mode.difference:
124+
if self._processor is None:
125+
raise Exception("Not calibrated yet - can not process image")
126+
return self._processor.process(data)
127+
elif self._aurox_mode == Mode.calibrate:
128+
# This will introduce a significant delay, but returning the
129+
# image indicates that the calibration step is complete.
130+
self._processor = ClarityProcessor(data)
131+
return data
132+
else:
133+
raise Exception("Unrecognised mode: %s", self._aurox_mode)
134+
135+
def get_sensor_shape(self):
136+
"""Return image shape accounting for rotation and Aurox processing."""
137+
shape = self._get_sensor_shape()
138+
# Does current mode combine two halves into a single image?
139+
if self._aurox_mode in [Mode.difference]:
140+
shape = (shape[1] // 2, shape[0])
141+
# Does the current transform perform a 90-degree rotation?
142+
if self._transform[2]:
143+
# 90 degree rotation
144+
shape = (shape[1], shape[0])
145+
return shape
146+
147+
148+
class Clarity(
149+
microscope.devices.ControllerDevice, microscope.devices.FilterWheelBase
150+
):
151+
"""Adds support for Aurox Clarity
152+
153+
Acts as a ControllerDevice providing the camera attached to the Clarity."""
154+
84155
_slide_to_sectioning = {
85156
__SLDPOS0: "bypass",
86157
__SLDPOS1: "low",
@@ -98,17 +169,60 @@ class Clarity(microscope.devices.FilterWheelBase):
98169
__FULLSTAT: 10,
99170
}
100171

101-
def __init__(self, **kwargs):
172+
def __init__(self, camera=None, camera_kwargs={}, **kwargs) -> None:
173+
"""Create a Clarity instance controlling an optional Camera device.
174+
175+
:param camera: a class to control the connected camera
176+
:param camera_kwargs: parameters passed to camera as keyword arguments
177+
"""
102178
super().__init__(positions=Clarity._positions, **kwargs)
103179
self._lock = Lock()
104180
self._hid = None
181+
self._devices = {}
182+
if camera is None:
183+
self._cam = None
184+
_logger.warning("No camera specified.")
185+
self._can_process = False
186+
else:
187+
AugmentedCamera = type(
188+
"AuroxAugmented" + camera.__name__,
189+
(_CameraAugmentor, camera),
190+
{},
191+
)
192+
self._cam = AugmentedCamera(**camera_kwargs)
193+
self._can_process = "ClarityProcessor" in globals()
194+
# Acquisition mode
195+
self._mode = Mode.raw
196+
# Add device settings
105197
self.add_setting(
106198
"sectioning",
107199
"enum",
108200
self.get_slide_position,
109201
lambda val: self.set_slide_position(val),
110202
self._slide_to_sectioning,
111203
)
204+
self.add_setting(
205+
"mode", "enum", lambda: self._mode.name, self.set_mode, Mode
206+
)
207+
208+
@property
209+
def devices(self) -> typing.Mapping[str, microscope.devices.Device]:
210+
"""Devices property, required by ControllerDevice interface."""
211+
if self._cam:
212+
return {"camera": self._cam}
213+
else:
214+
return {}
215+
216+
def set_mode(self, mode: Mode) -> None:
217+
"""Set the operation mode"""
218+
if mode in [Mode.calibrate, Mode.difference] and not self._can_process:
219+
raise Exception("Processing not available")
220+
else:
221+
self._cam.set_aurox_mode(mode)
222+
if mode == Mode.calibrate:
223+
self._set_calibration(True)
224+
else:
225+
self._set_calibration(False)
112226

113227
def _send_command(self, command, param=0, max_length=16, timeout_ms=100):
114228
"""Send a command to the Clarity and return its response"""
@@ -172,7 +286,7 @@ def _do_enable(self):
172286
def _do_disable(self):
173287
self._send_command(__SETONOFF, __SLEEP)
174288

175-
def set_calibration(self, state):
289+
def _set_calibration(self, state):
176290
if state:
177291
result = self._send_command(__SETCAL, __CALON)
178292
else:
@@ -199,12 +313,27 @@ def get_slides(self):
199313
return self._slide_to_sectioning
200314

201315
def get_status(self):
202-
# Fetch 10 bytes VERSION[3],ONOFF,SHUTTER,SLIDE,FILT,CAL,??,??
203-
result = self._send_command(__FULLSTAT)
204-
if result is None:
205-
return
206316
# A status dict to populate and return
207-
status = {}
317+
status = dict.fromkeys(
318+
[
319+
"connected",
320+
"on",
321+
"door open",
322+
"slide",
323+
"filter",
324+
"calibration",
325+
"busy",
326+
"mode",
327+
]
328+
)
329+
status["mode"] = self._mode.name
330+
# Fetch 10 bytes VERSION[3],ONOFF,SHUTTER,SLIDE,FILT,CAL,??,??
331+
try:
332+
result = self._send_command(__FULLSTAT)
333+
status["connected"] = True
334+
except Exception:
335+
status["connected"] = False
336+
return status
208337
# A list to track states, any one of which mean the device is busy.
209338
busy = []
210339
# Disk running
@@ -279,9 +408,3 @@ def _do_set_position(self, pos, blocking=True):
279408
while blocking and self.moving():
280409
pass
281410
return result
282-
283-
def _do_shutdown(self) -> None:
284-
pass
285-
286-
def initialize(self):
287-
pass

0 commit comments

Comments
 (0)