"""
Basler Camera Model
===================
Model to adapt PyPylon to the needs of PyNTA. PyPylon is only a wrapper for Pylon, thus the documentation
has to be found in the folder where Pylon was installed. It refers only to the C++ documentation, which is
very extensive, but not necessarily clear.
Some assumptions
----------------
The program forces software trigger during :meth:`~pynta.model.cameras.basler.Camera.initialize`.
"""
import logging
import warnings
from typing import Tuple
from pypylon import pylon
from pynta.model.cameras.base_camera import BaseCamera
from pynta.model.cameras.exceptions import CameraNotFound, WrongCameraState, CameraException
from pynta.util import get_logger
from pynta import Q_
logger = get_logger(__name__)
[docs]class Camera(BaseCamera):
def __init__(self, camera):
super().__init__(camera)
self.cam_num = camera
self.max_width = 0
self.max_height = 0
self.width = None
self.height = None
self.mode = None
self.X = None
self.Y = None
self.friendly_name = None
[docs] def initialize(self):
""" Initializes the communication with the camera. Get's the maximum and minimum width. It also forces
the camera to work on Software Trigger.
.. warning:: It may be useful to integrate other types of triggers in applications that need to
synchronize with other hardware.
"""
logger.debug('Initializing Basler Camera')
tl_factory = pylon.TlFactory.GetInstance()
devices = tl_factory.EnumerateDevices()
if len(devices) == 0:
raise CameraNotFound('No camera found')
for device in devices:
if self.cam_num in device.GetFriendlyName():
self.camera = pylon.InstantCamera()
self.camera.Attach(tl_factory.CreateDevice(device))
self.camera.Open()
self.friendly_name = device.GetFriendlyName()
if not self.camera:
msg = f'{self.cam_num} not found. Please check your config file and cameras connected'
logger.error(msg)
raise CameraNotFound(msg)
logger.info(f'Loaded camera {self.camera.GetDeviceInfo().GetModelName()}')
self.max_width = self.camera.Width.Max
self.max_height = self.camera.Height.Max
offsetX = self.camera.OffsetX.Value
offsetY = self.camera.OffsetY.Value
width = self.camera.Width.Value
height = self.camera.Height.Value
self.X = (offsetX, offsetX+width)
self.Y = (offsetY, offsetY+height)
self.camera.RegisterConfiguration(pylon.SoftwareTriggerConfiguration(), pylon.RegistrationMode_ReplaceAll,
pylon.Cleanup_Delete)
self.set_acquisition_mode(self.MODE_SINGLE_SHOT)
[docs] def set_acquisition_mode(self, mode):
logger.info(f'Setting acquisition mode to {mode}')
if mode == self.MODE_CONTINUOUS:
logger.debug(f'Setting buffer to {self.camera.MaxNumBuffer.Value}')
self.camera.OutputQueueSize = self.camera.MaxNumBuffer.Value
self.camera.AcquisitionMode.SetValue('Continuous')
self.mode = mode
elif mode == self.MODE_SINGLE_SHOT:
self.camera.AcquisitionMode.SetValue('SingleFrame')
self.mode = mode
self.camera.AcquisitionStart.Execute()
[docs] def set_ROI(self, X: Tuple[int, int], Y: Tuple[int, int]) -> Tuple[int, int]:
""" Set up the region of interest of the camera. Basler calls this the
Area of Interest (AOI) in their manuals. Beware that not all cameras allow
to set the ROI (especially if they are not area sensors).
Both the corner positions and the width/height need to be multiple of 4.
Compared to Hamamatsu, Baslers provides a very descriptive error warning.
:param tuple X: Horizontal limits for the pixels, 0-indexed and including the extremes. You can also check
:mod:`Base Camera <pynta.model.cameras.base_camera>`
To select, for example, the first 100 horizontal pixels, you would supply the following: (0, 99)
:param tuple Y: Vertical limits for the pixels.
"""
width = abs(X[1]-X[0])+1
width = int(width-width%4)
x_pos = int(X[0]-X[0]%4)
height = int(abs(Y[1]-Y[0])+1)
y_pos = int(Y[0]-Y[0]%2)
logger.info(f'Updating ROI: (x, y, width, height) = ({x_pos}, {y_pos}, {width}, {height})')
if x_pos+width > self.max_width:
raise CameraException('ROI width bigger than camera area')
if y_pos+height > self.max_height:
raise CameraException('ROI height bigger than camera area')
# First set offset to minimum, to avoid problems when going to a bigger size
self.clear_ROI()
logger.debug(f'Setting width to {width}')
self.camera.Width.SetValue(width)
logger.debug(f'Setting X offset to {x_pos}')
self.camera.OffsetX.SetValue(x_pos)
logger.debug(f'Setting Height to {height}')
self.camera.Height.SetValue(height)
logger.debug(f'Setting Y offset to {y_pos}')
self.camera.OffsetY.SetValue(y_pos)
self.X = (x_pos, x_pos+width)
self.Y = (y_pos, y_pos+width)
self.width = self.camera.Width.Value
self.height = self.camera.Height.Value
return self.width, self.height
[docs] def clear_ROI(self):
""" Resets the ROI to the maximum area of the camera"""
self.camera.OffsetX.SetValue(self.camera.OffsetX.Min)
self.camera.OffsetY.SetValue(self.camera.OffsetY.Min)
self.camera.Width.SetValue(self.camera.Width.Max)
self.camera.Height.SetValue(self.camera.Height.Max)
[docs] def GetCCDWidth(self) -> int:
""" Get the full width of the camera sensor.
:return int: Maximum width
.. deprecated:: 0.1.3
Use self.max_width instead
"""
warnings.warn("This method will be removed in a future release. Use cls.max_width instead", DeprecationWarning)
return self.max_width
[docs] def GetCCDHeight(self) -> int:
""" Get the full height (in pixels) of the camera sensor.
:return int: Maximum height
.. deprecated:: 0.1.3
Use self.max_height instead
"""
warnings.warn("This method will be removed in a future release. Use cls.max_height instead", DeprecationWarning)
return self.max_height
[docs] def get_size(self) -> Tuple[int, int]:
""" Get the size of the current Region of Interest (ROI). Remember that the actual size may be different from
the size that the user requests, given that not all cameras accept any pixel. For example, Basler has some
restrictions regarding corner pixels and possible widths.
:return tuple: (Width, Height)
"""
return self.camera.Width.Value, self.camera.Height.Value
[docs] def trigger_camera(self):
if self.camera.IsGrabbing():
logger.warning('Triggering an already grabbing camera')
else:
if self.mode == self.MODE_CONTINUOUS:
self.camera.StartGrabbing(pylon.GrabStrategy_OneByOne)
elif self.mode == self.MODE_SINGLE_SHOT:
self.camera.StartGrabbing(1)
self.camera.ExecuteSoftwareTrigger()
[docs] def set_exposure(self, exposure: Q_) -> Q_:
self.camera.ExposureTime.SetValue(exposure.m_as('us'))
self.exposure = exposure
return self.get_exposure()
[docs] def get_exposure(self) -> Q_:
self.exposure = float(self.camera.ExposureTime.ToString()) * Q_('us')
return self.exposure
[docs] def read_camera(self):
if not self.camera.IsGrabbing():
raise WrongCameraState('You need to trigger the camera before reading from it')
if self.mode == self.MODE_SINGLE_SHOT:
grab = self.camera.RetrieveResult(int(self.exposure.m_as('ms')) + 100, pylon.TimeoutHandling_Return)
img = [grab.Array]
grab.Release()
self.camera.StopGrabbing()
else:
img = []
num_buffers = self.camera.NumReadyBuffers.Value
logger.debug(f'{self.camera.NumQueuedBuffers.Value} frames available')
if num_buffers:
img = [None] * num_buffers
for i in range(num_buffers):
grab = self.camera.RetrieveResult(int(self.exposure.m_as('ms')) + 100, pylon.TimeoutHandling_Return)
if grab:
img[i] = grab.Array
grab.Release()
return [i.T for i in img] # Transpose to have the correct size
[docs] def stop_camera(self):
logger.info('Stopping camera')
self.camera.StopGrabbing()
self.camera.AcquisitionStop.Execute()
def __str__(self):
return self.friendly_name
def __del__(self):
self.camera.Close()
if __name__ == '__main__':
from time import sleep
logger.setLevel(logging.DEBUG)
ch = logging.StreamHandler()
ch.setLevel(logging.DEBUG)
formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
ch.setFormatter(formatter)
logger.addHandler(ch)
logger.info('Starting Basler')
basler = Camera(0)
basler.initialize()
basler.set_acquisition_mode(basler.MODE_SINGLE_SHOT)
basler.set_exposure(Q_('.02s'))
basler.trigger_camera()
print(len(basler.read_camera()))
basler.set_acquisition_mode(basler.MODE_CONTINUOUS)
basler.trigger_camera()
sleep(1)
imgs = basler.read_camera()
print(len(imgs))