BioImager  3.9.1
A .NET microscopy imaging library. Supports various microscopes by using imported libraries & GUI automation. Supported libraries include PriorĀ® & ZeissĀ® & all devices supported by Micromanager 2.0 and python-microscope.
Loading...
Searching...
No Matches
andorsdk3.py
1#!/usr/bin/env python3
2
3## Copyright (C) 2020 Mick Phillips <mick.phillips@gmail.com>
4##
5## This file is part of Microscope.
6##
7## Microscope is free software: you can redistribute it and/or modify
8## it under the terms of the GNU General Public License as published by
9## the Free Software Foundation, either version 3 of the License, or
10## (at your option) any later version.
11##
12## Microscope is distributed in the hope that it will be useful,
13## but WITHOUT ANY WARRANTY; without even the implied warranty of
14## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15## GNU General Public License for more details.
16##
17## You should have received a copy of the GNU General Public License
18## along with Microscope. If not, see <http://www.gnu.org/licenses/>.
19
20"""AndorSDK3 camera device.
21
22This class provides a wrapper for PYME's SDK3 interface that allows
23a camera and all its settings to be exposed over Pyro.
24"""
25
26import logging
27import queue
28import time
29
30import numpy as np
31
32import microscope
33import microscope.abc
34from microscope.cameras import _SDK3 as SDK3
36 ATBool,
37 ATCommand,
38 ATEnum,
39 ATFloat,
40 ATInt,
41 ATProperty,
42 ATString,
43)
44
45
46_logger = logging.getLogger(__name__)
47
48
49# SDK data pointer type
50DPTR_TYPE = SDK3.POINTER(SDK3.AT_U8)
51
52# Convert from SDK3 trigger mode names to Microscope trigger type and
53# mode.
54SDK3_STRING_TO_TRIGGER = {
55 "external": (
56 microscope.TriggerType.RISING_EDGE,
57 microscope.TriggerMode.ONCE,
58 ),
59 "external exposure": (
60 microscope.TriggerType.RISING_EDGE,
61 microscope.TriggerMode.BULB,
62 ),
63 "software": (microscope.TriggerType.SOFTWARE, microscope.TriggerMode.ONCE),
64}
65
66SDK_NAMES = {
67 "_accumulate_count": "AccumulateCount",
68 "_acquisition_start": "AcquisitionStart",
69 "_acquisition_stop": "AcquisitionStop",
70 "_alternating_readout_direction": "AlternatingReadoutDirection",
71 "_aoi_binning": "AOIBinning",
72 "_aoi_height": "AOIHeight",
73 "_aoi_left": "AOILeft",
74 "_aoi_top": "AOITop",
75 "_aoi_width": "AOIWidth",
76 "_aoi_stride": "AOIStride",
77 "_auxiliary_out_source": "AuxiliaryOutSource",
78 "_aux_out_source_two": "AuxOutSourceTwo",
79 "_baseline_level": "BaselineLevel",
80 "_bit_depth": "BitDepth",
81 "_buffer_overflow_event": "BufferOverflowEvent",
82 "_bytes_per_pixel": "BytesPerPixel",
83 "_camera_acquiring": "CameraAcquiring",
84 "_camera_dump": "CameraDump",
85 "_camera_model": "CameraModel",
86 "_camera_name": "CameraName",
87 "_camera_present": "CameraPresent",
88 "_controller_id": "ControllerId",
89 "_frame_count": "FrameCount",
90 "_cycle_mode": "CycleMode",
91 "_electronic_shuttering_mode": "ElectronicShutteringMode",
92 "_event_enable": "EventEnable",
93 "_events_missed_event": "EventsMissedEvent",
94 "_event_selector": "EventSelector",
95 "_exposed_pixel_height": "ExposedPixelHeight",
96 "_exposure_time": "ExposureTime",
97 "_exposure_end_event": "ExposureEndEvent",
98 "_exposure_start_event": "ExposureStartEvent",
99 "_external_trigger_delay": "ExternalTriggerDelay",
100 "_fan_speed": "FanSpeed",
101 "_firmware_version": "FirmwareVersion",
102 "_frame_rate": "FrameRate",
103 "_full_aoi_control": "FullAOIControl",
104 "_image_size_bytes": "ImageSizeBytes",
105 "_interface_type": "InterfaceType",
106 "_io_invert": "IoInvert",
107 "_io_selector": "IoSelector",
108 "_line_scan_speed": "LineScanSpeed",
109 "_lut_index": "LutIndex",
110 "_lut_value": "LutValue",
111 "_max_interface_transfer_rate": "MaxInterfaceTransferRate",
112 "_metadata_enable": "MetadataEnable",
113 "_metadata_timestamp": "MetadataTimestamp",
114 "_metadata_frame": "MetadataFrame",
115 "_overlap": "Overlap",
116 "_pixel_encoding": "PixelEncoding",
117 "_pixel_readout_rate": "PixelReadoutRate",
118 "_pre_amp_gain_control": "PreAmpGainControl",
119 "_readout_time": "ReadoutTime",
120 "_rolling_shutter_global_clear": "RollingShutterGlobalClear",
121 "_row_n_exposure_end_event": "RowNExposureEndEvent",
122 "_row_n_exposure_start_event": "RowNExposureStartEvent",
123 "_row_read_time": "RowReadTime",
124 "_scan_speed_control_enable": "ScanSpeedControlEnable",
125 "_sensor_cooling": "SensorCooling",
126 "_sensor_height": "SensorHeight",
127 "_sensor_readout_mode": "SensorReadoutMode",
128 "_sensor_temperature": "SensorTemperature",
129 "_sensor_width": "SensorWidth",
130 "_serial_number": "SerialNumber",
131 "_simple_pre_amp_gain_control": "SimplePreAmpGainControl",
132 "_software_trigger": "SoftwareTrigger",
133 "_static_blemish_correction": "StaticBlemishCorrection",
134 "_spurious_noise_filter": "SpuriousNoiseFilter",
135 "_target_sensor_temperature": "TargetSensorTemperature",
136 "_temperature_control": "TemperatureControl",
137 "_temperature_status": "TemperatureStatus",
138 "_timestamp_clock": "TimestampClock",
139 "_timestamp_clock_frequency": "TimestampClockFrequency",
140 "_timestamp_clock_reset": "TimestampClockReset",
141 "_trigger_mode": "TriggerMode",
142 "_vertically_centre_aoi": "VerticallyCentreAOI",
143}
144
145# Wrapper to ensure feature is readable.
146def readable_wrapper(func):
147 def wrapper(self, *args, **kwargs):
148 if SDK3.IsReadable(self.handle, self.propertyName):
149 return func(self, *args, **kwargs)
150 else:
151 return None # Warning('%s not currently readable.' % self.propertyName)
152
153 return wrapper
154
155
156# Wrapper to ensure feature is writable.
157def writable_wrapper(func):
158 def wrapper(self, *args, **kwargs):
159 if SDK3.IsWritable(self.handle, self.propertyName):
160 return func(self, *args, **kwargs)
161 else:
162 return False # Warning('%s not currently writable.' % self.propertyName)
163
164 return wrapper
165
166
167# Overrides for local style and error handling.
168ATInt.get_value = readable_wrapper(ATInt.getValue)
169ATInt.set_value = writable_wrapper(ATInt.setValue)
170ATInt.min = readable_wrapper(ATInt.min)
171ATInt.max = readable_wrapper(ATInt.max)
172ATBool.get_value = readable_wrapper(ATBool.getValue)
173ATBool.set_value = writable_wrapper(ATBool.setValue)
174ATFloat.get_value = readable_wrapper(ATFloat.getValue)
175ATFloat.set_value = writable_wrapper(ATFloat.setValue)
176ATString.get_value = readable_wrapper(ATString.getValue)
177ATString.set_value = writable_wrapper(ATString.setValue)
178ATString.max_length = readable_wrapper(ATString.maxLength)
179ATEnum.get_index = readable_wrapper(ATEnum.getIndex)
180ATEnum.set_index = writable_wrapper(ATEnum.setIndex)
181ATEnum.get_string = readable_wrapper(ATEnum.getString)
182ATEnum.set_string = writable_wrapper(ATEnum.setString)
183ATEnum.get_available_values = readable_wrapper(ATEnum.getAvailableValueMap)
184ATProperty.is_readonly = lambda self: not SDK3.IsWritable(
185 self.handle, self.propertyName
186)
187
188# Mapping of AT type to microscope Setting type.
189PROPERTY_TYPES = {
190 ATInt: "int",
191 ATBool: "bool",
192 ATFloat: "float",
193 ATString: "str",
194 ATEnum: "enum",
195}
196
197INVALIDATES_BUFFERS = [
198 "_simple_pre_amp_gain_control",
199 "_pre_amp_gain_control",
200 "_aoi_binning",
201 "_aoi_left",
202 "_aoi_top",
203 "_aoi_width",
204 "_aoi_height",
205]
206
207
211):
212 SDK_INITIALIZED = False
213
214 def __init__(self, index=0, **kwargs):
215 super().__init__(index=index, **kwargs)
216 if not AndorSDK3.SDK_INITIALIZED:
217 SDK3.InitialiseLibrary()
218 self.handle = None
219 # self._sdk3cam = SDK3Camera(self._index)
220 # SDK3Camera.__init__(self, self._index)
221 self.add_setting(
222 "use_callback",
223 "bool",
225 self._enable_callback,
226 None,
227 )
228 # Define features with local style. The SDK treats parameter names
229 # without regard to case, so we just need to remove the underscores
230 # when connecting properties to SDK calls. We define all possible
231 # features here; they will be removed if they are not implemented
232 # on the camera.
233 self._accumulate_count = ATInt()
237 self._aoi_binning = ATEnum()
238 self._aoi_height = ATInt()
239 self._aoi_left = ATInt()
240 self._aoi_top = ATInt()
241 self._aoi_width = ATInt()
242 self._aoi_stride = ATInt()
245 self._baseline_level = ATInt()
246 self._bit_depth = ATEnum()
250 self._camera_dump = ATCommand()
251 self._camera_model = ATString()
252 self._camera_name = ATString()
253 self._camera_present = ATBool()
254 self._controller_id = ATString()
255 self._frame_count = ATInt()
256 self._cycle_mode = ATEnum()
258 self._event_enable = ATBool()
260 self._event_selector = ATEnum()
262 self._exposure_time = ATFloat()
266 self._fan_speed = ATEnum()
268 self._frame_rate = ATFloat()
270 self._image_size_bytes = ATInt()
272 self._io_invert = ATBool()
273 self._io_selector = ATEnum()
275 self._lut_index = ATInt()
276 self._lut_value = ATInt()
278 self._metadata_enable = ATBool()
280 self._metadata_frame = ATBool()
281 self._overlap = ATBool()
282 self._pixel_encoding = ATEnum()
285 self._readout_time = ATFloat()
289 self._row_read_time = ATFloat()
291 self._sensor_cooling = ATBool()
292 self._sensor_height = ATInt()
295 self._sensor_width = ATInt()
296 self._serial_number = ATString()
304 self._timestamp_clock = ATInt()
307 self._trigger_mode = ATEnum()
309
310 # Software buffers and parameters for data conversion.
311 self.num_buffers = 32
312 self.add_setting(
313 "num_buffers",
314 "int",
315 lambda: self.num_buffers,
316 lambda val: self.set_num_buffers(val),
317 lambda: (1, 100),
318 )
319 self.buffers = queue.Queue()
320 self._buffer_size = None
321 self._img_stride = None
322 self._img_width = None
323 self._img_height = None
324 self._img_encoding = None
325 self._buffers_valid = False
326 self._exposure_callback = None
327
329
330 @property
331 def _acquiring(self):
332 return self._camera_acquiring.get_value()
333
334 @microscope.abc.keep_acquiring
335 def _enable_callback(self, use=False):
336 self.disabledisable()
337 if use:
338 SDK3.RegisterFeatureCallback(
339 self.handle, "ExposureEndEvent", self._exposure_callback, None
340 )
341 self._event_selector.set_string("ExposureEndEvent")
342 self._event_enable.set_value(True)
344 else:
345 SDK3.UnregisterFeatureCallback(
346 self.handle, "ExposureEndEvent", self._exposure_callback, None
347 )
348 self._event_enable.set_value(False)
350 self.enableenable()
351
352 @_acquiring.setter
353 def _acquiring(self, value):
354 # Here to prevent an error when super.__init__ intializes
355 # self._acquiring. Doesn't do anything, because the DLL keeps
356 # track of acquisition state.
357 pass
358
359 def set_num_buffers(self, num):
360 self.num_buffers = num
361 self._buffers_valid = False
362
363 def _purge_buffers(self):
364 """Purge buffers on both camera and PC."""
365 _logger.debug("Purging buffers.")
366 self._buffers_valid = False
367 if self._acquiring:
369 "Can not modify buffers while camera acquiring."
370 )
371 SDK3.Flush(self.handle)
372 while True:
373 try:
374 self.buffers.get(block=False)
375 except queue.Empty:
376 break
377
378 def _create_buffers(self, num=None):
379 """Create buffers and store values needed to remove padding later."""
380 if self._buffers_valid:
381 return
382 if num is None:
383 num = self.num_buffers
384 self._purge_buffers()
385 _logger.debug("Creating %d buffers.", num)
386 self._img_stride = self._aoi_stride.get_value()
387 self._img_width = self._aoi_width.get_value()
388 self._img_height = self._aoi_height.get_value()
389 self._img_encoding = self._pixel_encoding.get_string()
390 img_size = self._image_size_bytes.get_value()
391 self._buffer_size = img_size
392 for i in range(num):
393 buf = np.require(
394 np.empty(img_size),
395 dtype="uint8",
396 requirements=["C_CONTIGUOUS", "ALIGNED", "OWNDATA"],
397 )
398 self.buffers.put(buf)
399 SDK3.QueueBuffer(
400 self.handle, buf.ctypes.data_as(DPTR_TYPE), img_size
401 )
402 self._buffers_valid = True
403
404 def invalidate_buffers(self, func):
405 """Wrap functions that invalidate buffers so buffers are recreated."""
406 outerself = self
407
408 def wrapper(self, *args, **kwargs):
409 func(self, *args, **kwargs)
410 outerself._buffers_valid = False
411
412 return wrapper
413
414 def _fetch_data(self, timeout=5, debug=False):
415 """Fetch data and recycle buffers."""
416 try:
417 ptr, length = SDK3.WaitBuffer(self.handle, timeout)
418 except SDK3.TimeoutError as e:
419 if debug:
420 _logger.debug(e)
421 return None
422
423 raw = self.buffers.get()
424 width = self._img_width
425 height = self._img_height
426 data = raw # .reshape((-1, bytes_per_row))[:, 0:width].copy()
427 data = np.empty((height, width), dtype="uint16")
428 SDK3.ConvertBuffer(
429 ptr,
430 data.ctypes.data_as(DPTR_TYPE),
431 width,
432 height,
433 self._img_stride,
434 self._img_encoding,
435 "Mono16",
436 )
437 # Requeue the buffer if buffer size has not been changed elsewhere.
438 if raw.size == self._buffer_size:
439 self.buffers.put(raw)
440 SDK3.QueueBuffer(self.handle, ptr, length)
441 else:
442 del raw
443
444 return data
445
446 def abort(self):
447 """Abort acquisition."""
448 _logger.debug("Disabling acquisition.")
450 self._acquisition_stop()
451
452 def initialize(self):
453 """Initialise the camera.
454
455 Open the connection, connect properties and populate settings
456 dict.
457
458 """
459 try:
460 self.handle = SDK3.Open(self._index)
461 except Exception as e:
462 raise microscope.InitialiseError("Problem opening camera.") from e
463 if self.handle == None:
464 raise microscope.InitialiseError("No camera opened.")
465 for name, var in sorted(self.__dict__.items()):
466 if isinstance(var, ATProperty):
467 sdk_name = SDK_NAMES[name]
468 if not SDK3.IsImplemented(self.handle, sdk_name):
469 delattr(self, name)
470 continue
471 var.connect(self.handle, sdk_name)
472
473 if type(var) is ATCommand:
474 continue
475
476 is_readonly_func = var.is_readonly
477 if type(var) is ATEnum:
478 set_func = var.set_index
479 get_func = var.get_index
480 vals_func = var.get_available_values
481 else:
482 set_func = var.set_value
483 get_func = var.get_value
484 if type(var) is ATString:
485 vals_func = var.max_length
486 elif type(var) in (ATFloat, ATInt):
487 vals_func = lambda v=var: (v.min(), v.max())
488 else:
489 vals_func = None
490
491 if name in INVALIDATES_BUFFERS:
492 set_func = self.invalidate_buffers(set_func)
493
494 self.add_setting(
495 name.lstrip("_"),
496 PROPERTY_TYPES[type(var)],
497 get_func,
498 set_func,
499 vals_func,
500 is_readonly_func,
501 )
502 # Default setup.
503 self.set_cooling(True)
504 if not self._camera_model.getValue().startswith("SIMCAM"):
505 self._trigger_mode.set_string("Software")
506 self._cycle_mode.set_string("Continuous")
507 else:
508 _logger.warn("No hardware found - using SIMCAM")
509
510 def callback(*args):
511 data = self._fetch_data_fetch_data(timeout=500)
512 timestamp = time.time()
513 if data is not None:
514 self._put(data, timestamp)
515 return 0
516 else:
517 return -1
518
519 self._exposure_callback = SDK3.CALLBACKTYPE(callback)
520
521 def set_cooling(self, value):
522 try:
523 self._sensor_cooling.set_value(value)
524 except AttributeError:
525 pass
526
527 def get_id(self):
528 return self._serial_number.get_value()
529
530 def _do_shutdown(self) -> None:
531 self.set_cooling(False)
532 SDK3.Close(self.handle)
533
534 def _do_disable(self):
535 self.abortabort()
536 self._buffers_valid = False
537
538 def _do_enable(self):
539 _logger.debug("Preparing for acquisition.")
541 self._acquisition_stop()
542 self._create_buffers()
543 self._acquisition_start()
544 _logger.debug("Acquisition enabled: %s.", self._acquiring_acquiring_acquiring)
545 return True
546
547 @microscope.abc.keep_acquiring
548 def set_exposure_time(self, value):
549 bounded_value = sorted(
550 (self._exposure_time.min(), self._exposure_time.max(), value)
551 )[1]
552 self._exposure_time.set_value(bounded_value)
553 self._frame_rate.set_value(self._frame_rate.max())
554 _logger.debug(
555 "Set exposure time to %f, resulting framerate %f.",
556 bounded_value,
557 self._frame_rate.get_value(),
558 )
559
561 return self._exposure_time.get_value()
562
563 def get_cycle_time(self):
564 return 1.0 / self._frame_rate.get_value()
565
566 def _get_sensor_shape(self):
567 return (
568 self._sensor_width.get_value(),
569 self._sensor_height.get_value(),
570 )
571
572 def soft_trigger(self):
573 # deprecated, use triger()
574 return self._software_trigger()
575
576 @property
577 def trigger_mode(self) -> microscope.TriggerMode:
578 sdk3_string = self._trigger_mode.get_string().lower()
579 return SDK3_STRING_TO_TRIGGER[sdk3_string][1]
580
581 @property
582 def trigger_type(self) -> microscope.TriggerType:
583 sdk3_string = self._trigger_mode.get_string().lower()
584 return SDK3_STRING_TO_TRIGGER[sdk3_string][0]
585
587 self, ttype: microscope.TriggerType, tmode: microscope.TriggerMode
588 ) -> None:
589 for available_mode in self._trigger_mode.get_available_values():
590 trigger = SDK3_STRING_TO_TRIGGER[available_mode.lower()]
591 if trigger == (ttype, tmode):
592 self._trigger_mode.set_string(available_mode)
593 break
594 else:
596 "no SDK3 mode for %s and %s" % (ttype, tmode)
597 )
598
599 def _do_trigger(self) -> None:
600 self._software_trigger()
601
602 def _get_binning(self):
603 as_text = self._aoi_binning.get_string().split("x")
604 return tuple(int(t) for t in as_text)
605
606 @microscope.abc.keep_acquiring
607 def _set_binning(self, binning):
608 modes = self._aoi_binning.get_available_values()
609 as_text = "%dx%d" % (binning.h, binning.v)
610 if as_text in modes:
611 self._aoi_binning.set_string(as_text)
612 self._create_buffers()
613 return True
614 else:
615 return False
616
617 def _get_roi(self):
618 return microscope.ROI(
619 self._aoi_left.get_value(),
620 self._aoi_top.get_value(),
621 self._aoi_width.get_value(),
622 self._aoi_height.get_value(),
623 )
624
625 @microscope.abc.keep_acquiring
626 def _set_roi(self, roi):
627 current = self.get_roi()
629 self.abortabort()
630 try:
631 self._aoi_width.set_value(roi.width)
632 self._aoi_height.set_value(roi.height)
633 self._aoi_left.set_value(roi.left)
634 self._aoi_top.set_value(roi.top)
635 except:
636 self._aoi_width.set_value(current.width)
637 self._aoi_height.set_value(current.height)
638 self._aoi_left.set_value(current.left)
639 self._aoi_top.set_value(current.top)
640 return False
641 return True
642
643 def get_gain(self):
644 if hasattr(self, "_preampgain"):
645 return self._preampgain.get_value()
646 else:
647 return None
microscope.ROI get_roi(self)
Definition: abc.py:961
None disable(self)
Definition: abc.py:629
None abort(self)
Definition: abc.py:588
None _put(self, data, timestamp)
Definition: abc.py:745
None enable(self)
Definition: abc.py:592
None _fetch_data(self)
Definition: abc.py:643
None add_setting(self, name, dtype, get_func, set_func, values, typing.Optional[typing.Callable[[], bool]] readonly=None)
Definition: abc.py:407
None initialize(self)
Definition: abc.py:339
None enable(self)
Definition: abc.py:321
None disable(self)
Definition: abc.py:307
None set_trigger(self, microscope.TriggerType ttype, microscope.TriggerMode tmode)
Definition: andorsdk3.py:588
def _fetch_data(self, timeout=5, debug=False)
Definition: andorsdk3.py:414
def _create_buffers(self, num=None)
Definition: andorsdk3.py:378
def _enable_callback(self, use=False)
Definition: andorsdk3.py:335