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
2438import time
2539from threading import Lock
26-
40+ import typing
41+ import enum
42+ import logging
2743import hid
28-
2944import microscope
3045import 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
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