from ..IMAQ.IMAQ import IMAQFrameGrabber
from ..interface import camera
from ...core.devio import interface
from ...core.devio.comm_backend import DeviceError
from ...core.utils import funcargparse, py3
import numpy as np
import collections
import re
[docs]
class BonitoError(DeviceError):
"""Generic AlliedVision Bonito error"""
TDeviceInfo=collections.namedtuple("TDeviceInfo",["version","serial_number","grabber_info"])
[docs]
class IBonitoCamera(camera.ICamera): # pylint: disable=abstract-method
Error=DeviceError
GrabberClass=None
def __init__(self, **kwargs):
super().__init__(do_open=False,**kwargs)
self._frame_period=None
self._add_settings_variable("status_line",self.is_status_line_enabled,self.enable_status_line)
self._add_settings_variable("bl_offset",self.get_black_level_offset,self.set_black_level_offset)
self._add_settings_variable("digital_gain",self.get_digital_gain,self.set_digital_gain)
self._add_settings_variable("exposure",self.get_exposure,self.set_exposure)
self._add_settings_variable("frame_period",self.get_frame_period,self.set_frame_period)
self._add_settings_variable("exposure_control_mode",self.get_exposure_control_mode,self.set_exposure_control_mode)
self._add_status_variable("frame_timings",self.get_frame_timings)
self.open()
[docs]
def open(self):
super().open()
with self._close_on_error():
self.setup_serial_params(write_term="\r",datatype="str") # pylint: disable=no-member
self.set_roi()
self._frame_period=self.get_frame_period()
self.set_exposure(self.get_exposure())
self._max_nbuff=2**16-1
[docs]
def serial_query(self, query, timeout=3.):
self.serial_flush() # pylint: disable=no-member
self.serial_write(query) # pylint: disable=no-member
reply=self.serial_readline(timeout=timeout,maxn=2**16) # pylint: disable=no-member
if not reply.endswith("\r\n>"):
raise BonitoError("unexpected reply: {}; expect to end with '\\r\\n>'".format(reply))
if reply.startswith(query+"\r\r\n"):
reply=reply[len(query)+3:]
reply=reply[:-1].strip()
return reply
[docs]
def get_serial_parameter(self, comm, kind="int", timeout=3.):
funcargparse.check_parameter_range(kind,"kind",["int","str"])
reply=self.serial_query(comm+"=?",timeout=timeout)
if not re.match(r"=[A-Fa-f0-9]+",reply):
raise BonitoError("unexpected reply to command {}: {}".format(comm,reply))
reply=reply[1:]
if kind=="int":
return int(reply,base=16)
return reply
[docs]
def set_serial_parameter(self, comm, value):
if isinstance(value,(int,float)):
value=max(value,0)
value="{:X}".format(int(value))
return self.serial_query("{}={}".format(comm,value))
[docs]
def get_device_info(self):
"""
Get camera model data.
Return tuple ``(model, serial_number, grabber_info)``.
"""
version=self.serial_query("V")
serial=self.get_serial_parameter("a")
grabber_info=tuple(self.GrabberClass.get_device_info(self)) if self.GrabberClass else None
return TDeviceInfo(version,serial,grabber_info)
[docs]
def get_detector_size(self):
"""Get camera detector size (in pixels) as a tuple ``(width, height)``; as the camera does not provide this information, use the frame grabber parameters"""
return self.get_grabber_detector_size() # pylint: disable=no-member
def _get_cam_data_dimensions_rc(self):
return self.get_serial_parameter("N")+1,self.get_detector_size()[0]
def _update_grabber_roi(self, hstart=0, hend=None):
if self.GrabberClass:
r,c=self._get_cam_data_dimensions_rc()
if hend is None:
hend=c
if self.get_grabber_roi()!=(hstart,hend,0,r):
self.set_grabber_roi(hstart,hend,0,r)
[docs]
def get_roi(self):
"""
Get current ROI.
Return tuple ``(hstart, hend, vstart, vend)``.
"""
hstart,hend=self.get_grabber_roi()[:2] # pylint: disable=no-member
f=self.get_serial_parameter("A")
n=self.get_serial_parameter("N")
return hstart,hend,f,f+n+1
def _set_cam_roi(self, vstart, vend):
self.set_serial_parameter("D",0)
self.set_serial_parameter("I",1)
self.set_serial_parameter("A",vstart)
self.set_serial_parameter("N",vend-vstart-1)
[docs]
def set_roi(self, hstart=0, hend=None, vstart=0, vend=None):
"""
Setup camera ROI.
By default, all non-supplied parameters take extreme values.
"""
hlim,vlim=self.get_roi_limits()
hstart,hend=self._truncate_roi_axis((hstart,hend),hlim) # pylint: disable=no-member
vstart,vend=self._truncate_roi_axis((vstart,vend),vlim) # pylint: disable=no-member
self.set_serial_parameter("D",0)
self.set_serial_parameter("I",1)
self.set_serial_parameter("A",vstart)
self.set_serial_parameter("N",vend-vstart-1)
self._update_grabber_roi(hstart,hend)
return self.get_roi()
[docs]
def get_roi_limits(self, hbin=1, vbin=1): # pylint: disable=unused-argument
w,h=self.get_detector_size()
hlim,_=self.get_grabber_roi_limits(hbin=hbin,vbin=vbin) if self.GrabberClass else camera.TAxisROILimit(w,w,w,w,1)
vlim=camera.TAxisROILimit(1,min(h,2048),1,1,1)
return hlim,vlim
[docs]
def setup_acquisition(self, mode="sequence", nframes=100): # pylint: disable=arguments-differ
settings=self.get_settings(include=["status_line","bl_offset","digital_gain","exposure"])
roi=self.get_roi()
result=super().setup_acquisition(mode=mode,nframes=nframes)
self._set_cam_roi(*roi[2:4])
self.apply_settings(settings)
return result
_short_line_duration=84
_default_prescaler=_short_line_duration*2
def _get_line_duration(self):
camlink_mode=self.get_serial_parameter("S")
return self._short_line_duration*(2 if camlink_mode==0 else 1)
_p_exposure_timing_mode=interface.EnumParameterClass("exposure_timing_mode",
{"continuous":0,"iod":1,"iod_exposure":2,"iod_frame_time":3})
_p_exposure_feature_mode=interface.EnumParameterClass("exposure_feature_mode",
{"standard":0,"full_well":0x10,"permanent":0x20})
[docs]
@interface.use_parameters(_returns=("exposure_timing_mode","exposure_feature_mode"))
def get_exposure_control_mode(self):
"""
Get the exposure control mode.
Return tuple ``(timing_mode, feature_mode)``, where ``timing_mode`` determines how the exposure and
frame period are timed (continuous, external trigger control, internal control, etc.),
and ``feature_mode`` controls additional features (permanent exposure, enhanced full well mode).
See documentation for details.
"""
mode=self.get_serial_parameter("M")
timing_mode=mode&0x03
feature_mode=mode&0x30
return (timing_mode,feature_mode)
[docs]
@interface.use_parameters(timing_mode="exposure_timing_mode",feature_mode="exposure_feature_mode")
def set_exposure_control_mode(self, timing_mode=None, feature_mode=None):
"""
Set the exposure control mode.
`timing_mode` determines how the exposure and frame period are timed (continuous, external trigger control, internal control, etc.),
and `feature_mode` controls additional features (permanent exposure, enhanced full well mode).
See documentation for details.
"""
mode=self.get_serial_parameter("M")
if timing_mode is not None:
mode=(mode&0xFC)|timing_mode
if feature_mode is not None:
mode=(mode&0xCF)|feature_mode
mode&=0xF3
self.set_serial_parameter("M",mode)
return self.get_exposure_control_mode()
[docs]
def get_exposure(self):
"""
Get current exposure.
Note that the actual exposure might be different, depending on the exposure control mode.
"""
psc=self.get_serial_parameter("K")+1
exp=self.get_serial_parameter("E")
return psc*exp/56E6
[docs]
def set_exposure(self, exposure, setup_mode=True):
"""
Set current exposure.
Note that the actual exposure might be different, depending on the exposure control mode.
If ``setup_mode==True``, automatically set the exposure mode to take the given exposure value into account.
"""
self.set_serial_parameter("K",self._default_prescaler-1)
exp=max(int(exposure*56E6),self.get_serial_parameter("N")*self._get_line_duration())
self.set_serial_parameter("E",exp//self._default_prescaler)
self._set_device_frame_period(setup_mode=setup_mode)
return self.get_exposure()
[docs]
def get_frame_period(self):
"""
Get frame period (time between two consecutive frames in the internal trigger mode).
Note that the actual frame period might be different, depending on the exposure control mode.
"""
psc=self.get_serial_parameter("K")+1
per=self.get_serial_parameter("F")
return psc*per/56E6
def _set_device_frame_period(self, frame_period=None, setup_mode=True):
if frame_period is None:
frame_period=self._frame_period
self.set_serial_parameter("K",self._default_prescaler-1)
per=int(frame_period*56E6/self._default_prescaler)
exp=self.get_serial_parameter("E")
self.set_serial_parameter("F",max(per,exp+2))
if setup_mode:
self.set_exposure_control_mode(timing_mode="iod_frame_time",feature_mode="standard")
[docs]
def set_frame_period(self, frame_period, setup_mode=True):
"""
Set frame period (time between two consecutive frames in the internal trigger mode).
Note that the actual frame period might be different, depending on the exposure control mode.
If ``setup_mode==True``, automatically set the exposure mode to take the given exposure value into account.
"""
self._frame_period=frame_period
self._set_device_frame_period(setup_mode=setup_mode)
return self.get_frame_period()
_TAcqTimings=camera.TAcqTimings
[docs]
def get_frame_timings(self):
"""
Get acquisition timing.
Return tuple ``(exposure, frame_period)``.
"""
return self._TAcqTimings(self.get_exposure(),self.get_frame_period())
[docs]
def is_status_line_enabled(self):
"""Check if the status line is on"""
return bool(self.get_serial_parameter("U")&0x01)
[docs]
def enable_status_line(self, enabled=True):
"""Enable or disable status line"""
v=self.get_serial_parameter("U")
v=(v&0xFE)|(0x01 if enabled else 0x00)
self.set_serial_parameter("U",v)
return self.is_status_line_enabled()
[docs]
def get_black_level_offset(self):
"""Get the black level offset"""
return self.get_serial_parameter("W")
[docs]
def set_black_level_offset(self, offset):
"""Set the black level offset"""
offset=int(min(offset,255))
self.set_serial_parameter("W",offset)
return self.get_black_level_offset()
[docs]
def get_digital_gain(self):
"""Get the digital gain (0 for 1x, 1 for 2x, 2 for 4x)"""
return self.get_serial_parameter("G")
[docs]
def set_digital_gain(self, gain):
"""Get the digital gain (0 for 1x, 1 for 2x, 2 for 4x)"""
self.set_serial_parameter("G",int(gain))
return self.get_digital_gain()
[docs]
class BonitoIMAQCamera(IBonitoCamera,IMAQFrameGrabber):
"""
IMAQ+PFCam interface to a AlliedVision Bonito camera.
Args:
imaq_name: IMAQ interface name (can be learned by :func:`.IMAQ.list_cameras`; usually, but not always, starts with ``"img"``)
"""
Error=DeviceError
GrabberClass=IMAQFrameGrabber
def __init__(self, imaq_name="img0"):
super().__init__(imaq_name=imaq_name)
[docs]
def check_grabber_association(cam):
"""
Check if the given IMAQ frame grabber corresponds to Bonito camera.
`cam` should be an opened instance of :class:`.IMAQCamera`.
"""
try:
cam.serial_flush()
cam.serial_write("s=?\r")
res=cam.serial_read(4,timeout=0.1)
if res!=b"s=?\r":
return False
rest=cam.serial_readline(timeout=0.1)
if not rest.endswith(b"\r\n>"):
return False
if not re.match(r"\s+=[A-Za-z0-9]+\s+>",py3.as_str(rest)):
return False
return True
except cam.Error:
return False
TStatusLine=collections.namedtuple("TStatusLine",["framestamp"])
[docs]
def get_status_lines(frames):
"""
Get frame info from the binary status line.
`frames` can be 2D array (one frame), 3D array (stack of frames, first index is frame number), or list of 1D or 2D arrays.
Assume that the status line is present; if it isn't, the returned frame info will be a random noise.
Return a 1D or 2D numpy array, where the first axis (if present) is the frame number, and the last is the status line entry.
"""
if isinstance(frames,list):
return [get_status_lines(f) for f in frames]
sline=frames[...,0,:8].astype("u1").view("<i4")
if np.all(sline[...,0]==0x4C344D43): # backwards hex for "CM4L", magic for Bonito cameras status line
return sline[...,1:2]
return None
[docs]
class BonitoStatusLineChecker(camera.StatusLineChecker):
[docs]
def get_framestamp(self, frames):
slines=get_status_lines(frames)
return slines[...,0] if slines is not None else None