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
ximea.py
1#!/usr/bin/env python3
2
3## Copyright (C) 2020 David Miguel Susano Pinto <carandraug@gmail.com>
4## Copyright (C) 2020 Ian Dobbie <ian.dobbie@bioch.ox.ac.uk>
5## Copyright (C) 2020 Mick Phillips <mick.phillips@gmail.com>
6##
7## This file is part of Microscope.
8##
9## Microscope is free software: you can redistribute it and/or modify
10## it under the terms of the GNU General Public License as published by
11## the Free Software Foundation, either version 3 of the License, or
12## (at your option) any later version.
13##
14## Microscope is distributed in the hope that it will be useful,
15## but WITHOUT ANY WARRANTY; without even the implied warranty of
16## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17## GNU General Public License for more details.
18##
19## You should have received a copy of the GNU General Public License
20## along with Microscope. If not, see <http://www.gnu.org/licenses/>.
21
22"""Ximea cameras.
23
24Changing settings flushes the buffer
25------------------------------------
26
27It is not possible to set some parameters during image acquisition.
28In such cases, acquisition is stopped (camera is disabled) and the
29restarted (camera is enabled). However, stopping acquisition discards
30any image in the camera memory that have not yet been read.
31
32Modifying the following settings require acquisition to be stopped:
33
34- ROIs
35- binning
36- trigger type (trigger source)
37
38For more details, see the [XiAPI manual](https://www.ximea.com/support/wiki/apis/XiAPI_Manual#Flushing-the-queue).
39
40Hardware trigger
41----------------
42
43Ximea cameras in the MQ family accept software triggers even if set
44for hardware triggers (see `vendor issues
45#3`<https://github.com/python-microscope/vendor-issues/issues/3>).
46However, `XimeaCamera.trigger()` checks the trigger type and will
47raise an exception unless the camera is set for software triggers.
48
49Requirements
50------------
51
52Support for Ximea cameras requires Ximea's API Python (xiApiPython).
53This is only available via Ximea's website and is not available on
54PyPI. See Ximea's website for `install instructions
55<https://www.ximea.com/support/wiki/apis/Python>`__.
56
57"""
58
59import contextlib
60import enum
61import logging
62import typing
63
64import numpy as np
65from ximea import xiapi
66
67import microscope
68import microscope.abc
69
70
71_logger = logging.getLogger(__name__)
72
73
74# The ximea package does not provide an enum for the error codes.
75# There is ximea.xidefs.ERROR_CODES which maps the error code to an
76# error message but what we need is a symbol that maps to the error
77# code so we can use while handling exceptions.
78_XI_TIMEOUT = 10
79_XI_NOT_SUPPORTED = 12
80_XI_NOT_IMPLEMENTED = 26
81_XI_ACQUISITION_STOPED = 45
82_XI_UNKNOWN_PARAM = 100
83
84
85# During acquisition, we rely on catching timeout errors which then
86# get discarded. However, with debug level set to warning (XiApi
87# default log level), we get XiApi messages on stderr for each timeout
88# making logging impossible. So change this to error.
89#
90# Debug level is a xiapi global setting but we need a Camera instance.
91xiapi.Camera().set_debug_level("XI_DL_ERROR")
92
93
94@contextlib.contextmanager
95def _disabled_camera(camera):
96 """Context manager to temporarily disable camera."""
97 if camera.enabled:
98 try:
99 camera.disable()
100 yield camera
101 finally:
102 camera.enable()
103 else:
104 yield camera
105
106
107@contextlib.contextmanager
108def _enabled_camera(camera):
109 """Context manager to temporarily enable camera."""
110 if not camera.enabled:
111 try:
112 camera.enable()
113 yield camera
114 finally:
115 camera.disable()
116 else:
117 yield camera
118
119
120@enum.unique
121class TrgSourceMap(enum.Enum):
122 # The complete list is the XI_TRG_SOURCE enum (C code) or in the
123 # xidefs module (Python code).
124
125 XI_TRG_SOFTWARE = microscope.TriggerType.SOFTWARE
126 XI_TRG_EDGE_RISING = microscope.TriggerType.RISING_EDGE
127 XI_TRG_EDGE_FALLING = microscope.TriggerType.FALLING_EDGE
128
129 # Not all XI_TRG_SOURCE values are defined:
130 #
131 # XI_TRG_OFF: Capture of next image is automatically started after
132 # previous.
133 #
134 # XI_TRG_LEVEL_HIGH: Specifies that the trigger is considered
135 # valid as long as the level of the source signal is high.
136 #
137 # XI_TRG_LEVEL_LOW: Specifies that the trigger is considered valid
138 # as long as the level of the source signal is low.
139
140
141@enum.unique
142class TrgSelectorMap(enum.Enum):
143 # The complete list is the XI_TRG_SELECTOR enum (C code) or in the
144 # xidefs module (Python code).
145
146 # Trigger starts the capture of one frame.
147 XI_TRG_SEL_FRAME_START = microscope.TriggerMode.ONCE
148
149 # There are other modes/selector which look like they have matches
150 # on TriggerMode but we never got to test them:
151 #
152 # XI_TRG_SEL_EXPOSURE_ACTIVE: Trigger controls the start and
153 # length of the exposure.
154 #
155 # XI_TRG_SEL_FRAME_BURST_START: Trigger starts the capture of the
156 # bursts of frames in an acquisition.
157 #
158 # XI_TRG_SEL_FRAME_BURST_ACTIVE: Trigger controls the duration of
159 # the capture of the bursts of frames in an acquisition.
160 #
161 # XI_TRG_SEL_MULTIPLE_EXPOSURES: Trigger which when first trigger
162 # starts exposure and consequent pulses are gating
163 # exposure(active HI)
164 #
165 # XI_TRG_SEL_EXPOSURE_START: Trigger controls the start of the
166 # exposure of one Frame.
167 #
168 # XI_TRG_SEL_MULTI_SLOPE_PHASE_CHANGE: Trigger controls the multi
169 # slope phase in one Frame (phase0 -> phase1) or (phase1 ->
170 # phase2).
171 #
172 # XI_TRG_SEL_ACQUISITION_START: Trigger starts acquisition of
173 # first frame.
174
175
177 """Ximea cameras
178
179 Args:
180 serial_number: the serial number of the camera to connect to.
181 It can be set to `None` if there is only camera on the
182 system.
183
184 """
185
187 self, serial_number: typing.Optional[str] = None, **kwargs
188 ) -> None:
189 super().__init__(**kwargs)
190 self._acquiring_acquiring = False
191 self._handle = xiapi.Camera()
192 self._img = xiapi.Image()
193 self._serial_number = serial_number
194 self._sensor_shape = (0, 0)
195 self._roi = microscope.ROI(None, None, None, None)
196 self._binning = microscope.Binning(1, 1)
197
198 # When using the Settings system, enums are not really enums
199 # and even when using lists we get indices sent back and forth
200 # (works fine only when using EnumInt. The gymnastic here
201 # makes it work with the rest of enums which are there to make
202 # it work with TriggerTargetMixin.
203 trg_source_names = [x.name for x in TrgSourceMap]
204
205 def _trigger_source_setter(index: int) -> None:
206 trigger_mode = TrgSourceMap[trg_source_names[index]].value
207 self.set_triggerset_trigger(trigger_mode, self.trigger_modetrigger_mode)
208
209 self.add_setting(
210 "trigger source",
211 "enum",
212 lambda: TrgSourceMap(self.trigger_typetrigger_type).name,
213 _trigger_source_setter,
214 trg_source_names,
215 )
216
218
219 def _fetch_data(self) -> typing.Optional[np.ndarray]:
220 if not self._acquiring_acquiring:
221 return None
222
223 try:
224 self._handle.get_image(self._img, timeout=1)
225 except xiapi.Xi_error as err:
226 # err.status may not exist so use getattr (see
227 # https://github.com/python-microscope/vendor-issues/issues/2)
228 if getattr(err, "status", None) == _XI_TIMEOUT:
229 return None
230 elif (
231 getattr(err, "status", None) == _XI_ACQUISITION_STOPED
232 and not self._acquiring_acquiring
233 ):
234 # We can end up here during disable if self._acquiring
235 # was True but is now False.
236 return None
237 else:
238 raise
239
240 data: np.ndarray = self._img.get_image_data_numpy()
241 _logger.info(
242 "Fetched imaged with dims %s and size %s.", data.shape, data.size
243 )
244 return data
245
246 def abort(self):
247 _logger.info("Disabling acquisition.")
248 if self._acquiring_acquiring:
249 # We set acquiring before calling stop_acquisition because
250 # the fetch loop is still running and will raise errors 45
251 # otherwise.
252 self._acquiring_acquiring = False
253 try:
254 self._handle.stop_acquisition()
255 except Exception:
256 self._acquiring_acquiring = True
257 raise
258
259 def initialize(self) -> None:
260 """Initialise the camera.
261
262 Open the connection, connect properties and populate settings dict.
263 """
264 n_cameras = self._handle.get_number_devices()
265
266 if self._serial_number is None:
267 if n_cameras > 1:
268 raise TypeError(
269 "more than one Ximea camera found but the"
270 " serial_number argument was not specified"
271 )
272 _logger.info(
273 "serial_number is not specified but there is only one"
274 " camera on the system"
275 )
276 self._handle.open_device()
277 else:
278 _logger.info(
279 "opening camera with serial number '%s'", self._serial_number
280 )
281 self._handle.open_device_by_SN(self._serial_number)
282
283 self._sensor_shape = (
284 self._handle.get_width_maximum()
285 + self._handle.get_offsetX_maximum(),
286 self._handle.get_height_maximum()
287 + self._handle.get_offsetY_maximum(),
288 )
289 self._roi = microscope.ROI(
290 left=0,
291 top=0,
292 width=self._sensor_shape[0],
293 height=self._sensor_shape[1],
294 )
295 self.set_roi(self._roi)
296
298 microscope.TriggerType.SOFTWARE, microscope.TriggerMode.ONCE
299 )
300
301 # Add settings for the different temperature sensors.
302 for temp_param_name in [
303 "chip_temp",
304 "hous_temp",
305 "hous_back_side_temp",
306 "sensor_board_temp",
307 ]:
308 get_temp_method = getattr(self._handle, "get_" + temp_param_name)
309 # Not all cameras have temperature sensors in all
310 # locations. We can't query if the sensor is there, we
311 # can only try to read the temperature and skip that
312 # temperature sensor if we get an exception.
313 try:
314 get_temp_method()
315 except xiapi.Xi_error as err:
316 # Depending on XiAPI version, camera model, and
317 # selected sensor, we might get any of these errors as
318 # meaning that it's not available. See
319 # https://github.com/python-microscope/vendor-issues/issues/6
320 if err.status not in [
321 _XI_NOT_SUPPORTED,
322 _XI_NOT_IMPLEMENTED,
323 _XI_UNKNOWN_PARAM,
324 ]:
325 raise
326 else:
327 self.add_setting(
328 temp_param_name,
329 "float",
330 get_temp_method,
331 None,
332 values=tuple(),
333 )
334
335 def _do_disable(self):
336 self.abortabort()
337
338 def _do_enable(self):
339 _logger.info("Preparing for acquisition.")
340 if self._acquiring_acquiring:
341 self.abortabort()
342 # actually start camera
343 self._handle.start_acquisition()
344 self._acquiring_acquiring = True
345 _logger.info("Acquisition enabled.")
346 return True
347
348 def set_exposure_time(self, value: float) -> None:
349 # exposure times are set in us.
350 try:
351 self._handle.set_exposure_direct(int(value * 1000000))
352 except Exception as err:
353 _logger.debug("set_exposure_time exception: %s", err)
354
355 def get_exposure_time(self) -> float:
356 # exposure times are in us, so multiple by 1E-6 to get seconds.
357 return self._handle.get_exposure() * 1.0e-6
358
359 def get_cycle_time(self):
360 return 1.0 / self._handle.get_framerate()
361
362 def _get_sensor_shape(self) -> typing.Tuple[int, int]:
363 return self._sensor_shape
364
365 def soft_trigger(self) -> None:
366 self.trigger()
367
368 def _do_trigger(self) -> None:
369 # Value for set_trigger_software() has no meaning. See
370 # https://github.com/python-microscope/vendor-issues/issues/3
371 self._handle.set_trigger_software(1)
372
373 def _get_binning(self) -> microscope.Binning:
374 return self._binning
375
376 def _set_binning(self, binning: microscope.Binning) -> bool:
377 if binning == self._binning:
378 return True
379 # We don't have a ximea camera that supports binning so we
380 # can't write support for this (a camera without this feature
381 # will raise error 100). When writing this, careful and check
382 # what XiAPI does when mixing ROI and binning.
383 raise NotImplementedError()
384
385 def _get_roi(self) -> microscope.ROI:
386 assert self._roi == microscope.ROI(
387 self._handle.get_offsetX(),
388 self._handle.get_offsetY(),
389 self._handle.get_width(),
390 self._handle.get_height(),
391 ), "ROI attribute is out of sync with internal camera setting"
392 return self._roi
393
394 def _set_roi(self, roi: microscope.ROI) -> bool:
395 if (
396 roi.width + roi.left > self._sensor_shape[0]
397 or roi.height + roi.top > self._sensor_shape[1]
398 ):
399 raise ValueError(
400 "ROI %s does not fit in sensor shape %s"
401 % (roi, self._sensor_shape)
402 )
403 try:
404 # These methods will fail if the width/height plus their
405 # corresponding offsets are higher than the sensor size.
406 # So we start by setting the offset to zero. Cases to
407 # think off: 1) shrinking ROI size, 2) increasing ROI
408 # size, 3) resetting ROI and so can't trust self._roi as
409 # the current state (see this exception handling).
410 with _disabled_camera(self):
411 self._handle.set_offsetX(0)
412 self._handle.set_offsetY(0)
413 self._handle.set_width(roi.width)
414 self._handle.set_height(roi.height)
415 self._handle.set_offsetX(roi.left)
416 self._handle.set_offsetY(roi.top)
417 except Exception:
418 self._set_roi_set_roi(self._roi) # set it back to whatever was before
419 raise
420 self._roi = roi
421 return True
422
423 def _do_shutdown(self) -> None:
424 if self._acquiring_acquiring:
425 self._handle.stop_acquisition()
426 if self._handle.CAM_OPEN:
427 # We check CAM_OPEN instead of try/catch an exception
428 # because if the camera failed initialisation, XiApi fails
429 # hard with error code -1009 (unknown) since the internal
430 # device handler is NULL.
431 self._handle.close_device()
432
433 @property
434 def trigger_mode(self) -> microscope.TriggerMode:
435 trg_selector = self._handle.get_trigger_selector()
436 try:
437 tmode = TrgSelectorMap[trg_selector]
438 except KeyError:
439 raise Exception(
440 "somehow set to unsupported trigger mode %s" % trg_selector
441 )
442 return tmode.value
443
444 @property
445 def trigger_type(self) -> microscope.TriggerType:
446 trg_source = self._handle.get_trigger_source()
447 try:
448 ttype = TrgSourceMap[trg_source]
449 except KeyError:
450 raise Exception(
451 "somehow set to unsupported trigger type %s" % trg_source
452 )
453 return ttype.value
454
456 self, ttype: microscope.TriggerType, tmode: microscope.TriggerMode
457 ) -> None:
458 if tmode is not microscope.TriggerMode.ONCE:
460 "%s not supported (only TriggerMode.ONCE)" % tmode
461 )
462
463 try:
464 trg_source = TrgSourceMap(ttype)
465 except ValueError:
467 "no support for trigger type %s" % ttype
468 )
469
470 if trg_source.name != self._handle.get_trigger_source():
471 # Changing trigger source requires stopping acquisition.
472 with _disabled_camera(self):
473 self._handle.set_trigger_source(trg_source.name)
def _set_roi(self, microscope.ROI roi)
Definition: abc.py:970
None set_roi(self, microscope.ROI roi)
Definition: abc.py:974
None abort(self)
Definition: abc.py:588
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
microscope.TriggerType trigger_type(self)
Definition: abc.py:243
None set_trigger(self, microscope.TriggerType ttype, microscope.TriggerMode tmode)
Definition: abc.py:249
microscope.TriggerMode trigger_mode(self)
Definition: abc.py:238
None __init__(self, typing.Optional[str] serial_number=None, **kwargs)
Definition: ximea.py:188
microscope.TriggerType trigger_type(self)
Definition: ximea.py:445
None set_exposure_time(self, float value)
Definition: ximea.py:348
microscope.TriggerMode trigger_mode(self)
Definition: ximea.py:434
None set_trigger(self, microscope.TriggerType ttype, microscope.TriggerMode tmode)
Definition: ximea.py:457
bool _set_roi(self, microscope.ROI roi)
Definition: ximea.py:394