import numpy as np
from emva1288.camera import routines
[docs]class Camera(object):
"""Camera simulator. It creates images according to the given parameters.
"""
[docs] def __init__(self,
f_number=8, # F-number of the light source/camera setup
pixel_area=25, # um^2
bit_depth=8, # Bit depth of the image [8, 10, 12, 14]
width=640,
height=480,
temperature=22, # Sensor temperature in ^oC
temperature_ref=30, # Reference temperature
temperature_doubling=8, # Doubling temperature
qe=None, # Quantum efficiency for the given wavelength
exposure=1000000, # Exposure time in ns
exposure_min=500000, # Minimum exposure time in ns
exposure_max=500000000, # Maximum exposure time in ns
K=0.1, # Overall system gain
K_min=0.1,
K_max=17.,
K_steps=255,
blackoffset=0,
blackoffset_min=0,
blackoffset_max=None,
blackoffset_steps=255,
dark_current_ref=30,
dark_signal_0=10.,
sigma2_dark_0=10.,
u_esat=15000.,
dsnu=None,
prnu=None,
seed=None
):
"""Camera simulator init method.
Parameters
----------
f_number : float, optional
The emva1288 f_number for the camera.
pixel_area : float, optional
The area of one pixel (in um ^ 2)
bit_depth : int, optional
The number of bits allowed for one pixel value.
width : int, optional
The number of columns in the the image.
height : int, optional
The number of rows in the image.
temperature : float, optional
The camera's sensor temperature in degrees Celsius.
temperature_ref : float, optional
The reference temperature (at which the dark current
is equal to the reference dark current).
temperature_doubling: float, optional
The doubling temperature (at which the dark
current is two times the reference dark
current).
qe : float, optional
Quantum efficiency (between 0 and 1). If None, a simulated
quantum efficiency is choosen with the
:func:`~emva1288.camera.routines.qe` function.
exposure : float, optional
The camera's exposure time in ns.
exposure_min : float, optional
The camera's minimal exposure time in ns.
exposure_max : float, optional
The camera's maximal exposure time in ns.
K : float, optional
The overall system gain (in DN/e^-).
K_min : float, optional
The overall minimal system gain (in DN/e^-).
K_max : float, optional
The overall maximal system gain (in DN/e^-).
K_steps : int, optional
The number of available intermediate overall system gains
between K_min and K_max.
blackoffset : float, optional
The dark signal offset for each pixel (in DN).
blackoffset_min: float, optional
The minimal dark signal offset for each pixel (in DN).
blackoffset_max : float, optional
The maximal dark signal offset for each pixel
(in DN).
blackoffset_steps : int, optional
The number of available blackoffsets between the
mimimal and maximal blackoffsets.
dark_current_ref : float, optional
The reference dark current used for computing the
total dark current.
dark_signal_0 : float, optional
Mean number of electrons generated by the electronics (offset)
sigma2_dark_0 : float, optional
Variance of electrons generated by the electronics
u_esat : float, optional
Full well capacity
dsnu : np.array, optional
DSNU image in e^-, array with the same shape of the image
that is added to every image.
prnu : np.array, optional
PRNU image in percentages (1 = 100%), array with the same shape
of the image. Every image is multiplied by it.
seed : int, optional
A seed to initialize the random number generator.
"""
self._pixel_area = pixel_area
self._bit_depth = bit_depth
self._img_max = 2 ** int(bit_depth) - 1
if bit_depth <= 8:
self._img_type = np.uint8
elif bit_depth <= 16:
self._img_type = np.uint16
else:
self._img_type = np.uint64
self._width = width
self._height = height
self._shape = (self.height, self.width)
self._temperature_ref = temperature_ref
self._temperature_doubling = temperature_doubling
self._qe = qe
# When no specific qe is provided we simulate one
if qe is None:
self._qe = routines.Qe(height=self._height, width=self._width)
self._dark_current_ref = dark_current_ref
self._dark_signal_0 = dark_signal_0
self._sigma2_dark_0 = sigma2_dark_0
self._exposure = exposure
self._exposure_min = exposure_min
self._exposure_max = exposure_max
self._u_esat = u_esat
self.__Ks = np.linspace(K_min, K_max, num=K_steps)
self._K = None
self.K = K
# A good gestimate for maximum blackoffset is 1/16th of the full range
if not blackoffset_max:
blackoffset_max = self.img_max // 16
self.__blackoffsets = np.linspace(blackoffset_min, blackoffset_max,
num=blackoffset_steps)
self._blackoffset = None
self.blackoffset = blackoffset
self._dsnu = dsnu
self._prnu = prnu
if dsnu is None:
self._dsnu = np.zeros(self._shape)
if prnu is None:
self._prnu = np.ones(self._shape)
self.environment = {'temperature': temperature,
'f_number': f_number}
self._rng = np.random.default_rng(seed)
@property
def bit_depth(self):
"""The number of bits allowed for a gray value for one pixel."""
return self._bit_depth
@property
def pixel_area(self):
"""The area of one pixel (in um ^ 2)."""
return self._pixel_area
@property
def img_max(self):
"""The maximal value for one pixel (in DN)."""
return self._img_max
@property
def width(self):
"""The number of columns"""
return self._width
@property
def height(self):
"""The number of rows"""
return self._height
@property
def exposure(self):
"""The camera's exposure time (in ns)."""
return self._exposure
@exposure.setter
def exposure(self, value):
self._exposure = value
@property
def exposure_min(self):
"""The camera's minimal exposure time (in ns)."""
return self._exposure_min
@property
def exposure_max(self):
"""The camera's maximal exposure time (in ns)."""
return self._exposure_max
@property
def K(self):
"""The overall system gain (in DN/e^-).
:Setter: The setter uses the
:func:`~emva1288.camera.routines.nearest_value` function
to set the system gain to the nearest value given to the
setter.
This is because not all system gains are possible but rather a
linear sample between the minimal and maximal value.
"""
return self._K
@K.setter
def K(self, value):
self._K = routines.nearest_value(value, self.__Ks)
@property
def Ks(self):
"""The array of all the available system gains (in DN/e^-)."""
return self.__Ks
@property
def blackoffset(self):
"""The system dark signal offset (in DN).
:Setter: The setter uses the
:func:`~emva1288.camera.routines.nearest_value` function
to set the black signal offset to the nearest value given
to the setter. This is because not all black offsets are
possible but rather a linear sample between the minimal
and maximal value.
"""
return self._blackoffset
@blackoffset.setter
def blackoffset(self, value):
self._blackoffset = routines.nearest_value(value,
self.__blackoffsets)
@property
def blackoffsets(self):
"""The array of all blackoffsets (in DN)."""
return self.__blackoffsets
@property
def DSNU(self):
"""The Dark Signal non uniformity, DSNU (in e^-).
:Setter: The setter allow the option to set a new dsnu who have the
same shape than the image. See how it's used
:func:`~emva1288.camera.bad_images.bad_pixel`.
"""
return self._dsnu
@DSNU.setter
def DSNU(self, value):
# TODO: Conditions to be sure than the dsnu gived has the good shape.
self._dsnu = value
@property
def qe(self):
return self._qe
[docs] def grab(self, radiance, temperature=None, f_number=None):
"""
Create an image based on the mean and standard deviation from the
EMVA1288 parameters.
The image is generated using a normal distribution for each pixel.
Parameters
----------
radiance : ndarray
The sensor's illumination in W/cm^2/sr. This is the only
mandatory argument because it is frequently changed during
an test.
temperature : float, optional
The camera's temperature in degrees Celsius.
If None, the environment's
temperature will be taken.
f_number : float, optional
The optical setup f_number.
If None, the environment's f_number will be taken.
"""
clipping_point = int(self.img_max)
###################################
# Thermally induced electrons image
u_d = self._u_therm(temperature=temperature)
# Noise centred on the number of electrons thermally generated
img_e = self._rng.poisson(u_d, size=self._shape)
###############################
# Light induced electrons image
u_e = self._u_e(radiance, f_number=f_number)
# Noise centred on the number of electrons from the Light
img_e += self._rng.poisson(u_e, size=self._shape)
####################################################################
# Electronics induced electrons image and Dark Signal non uniformity
variance = np.sqrt(self._sigma2_dark_0)
dark_signal = self._dsnu + self._rng.normal(loc=self._dark_signal_0,
scale=variance,
size=self._shape)
img_e = img_e + dark_signal
###########################################
# Clip of the Full well electrons capacity
np.clip(img_e, 0, self._u_esat, img_e)
# Analog to Digital gain
img = self.K * img_e
# Quantization noise image
img_q = self._rng.uniform(-0.5, 0.5, self._shape)
img += img_q
# Offset on the dark_signal
img += self.blackoffset
np.rint(img, img)
np.clip(img, 0, clipping_point, img)
return img.astype(self._img_type)
def _u_e(self, radiance, f_number=None):
"""
Light induced electrons image for the exposure time.
"""
# number of photons for each pixel during exposure time.
photons = self.get_photons(radiance, f_number=f_number)
# copy the filter so we don't alter the camera's filter
u_e = np.sum(np.multiply(photons, self._qe.qe), axis=2)
u_e *= self._prnu
return u_e
def _u_therm(self, temperature=None):
"""
Mean number of electrons due to temperature.
"""
u_d = self._u_i(temperature=temperature) * self.exposure / (10 ** 9)
return u_d
def _u_i(self, temperature=None):
"""
Dark current (in DN/s).
"""
if temperature is None:
temperature = self.environment['temperature']
u_i = 1. * self._dark_current_ref * 2 ** (
(temperature - self._temperature_ref) / self._temperature_doubling)
return u_i
[docs] def get_radiance_for(self, mean=None, exposure=None):
"""Radiance to achieve saturation.
Calls the :func:`~emva1288.camera.routines.get_radiance` function
to get the radiance for saturation.
Parameters
----------
mean : float, optional
The saturation value of the camera. If None, this value
is set to the :attr:`img_max` attribute.
exposure : float, optional
The camera's exposure time at which the radiance
for saturation value will be computed. If None, the
exposure time taken will be the camera's actual exposure
time.
Returns
-------
float :
The radiance at which, for the given saturation value and
the given exposure time, the camera saturates.
"""
if not mean:
mean = self.img_max
if not exposure:
exposure = self.exposure
ud = self._u_therm()
ue = ((mean / self.K) - ud)/len(self._qe.w)
ue *= np.ones(len(self._qe.w))
up = np.zeros((self.height, self.width, len(self._qe.w)))
up[:, :] = ue / self._qe.qe.mean()
radiance = routines.get_radiance(exposure,
up,
self.pixel_area,
self.environment['f_number'],
self._qe.w)
return radiance
[docs] def get_photons(self, radiance, exposure=None, f_number=None):
"""Computes the number of photons received by one pixel.
Uses the :func:`~emva1288.camera.routines.get_photons` function
to compute this number.
Parameters
----------
radiance : float
The radiance exposed to the camera (in Wsr^-1cm^-2).
exposure : float, optional
The pixel's exposure time in ns.
Returns
-------
float :
The number of photons received by one pixel.
"""
if exposure is None:
exposure = self.exposure
if f_number is None:
f_number = self.environment['f_number']
return routines.get_photons(exposure,
radiance,
self.pixel_area,
self._qe.w,
f_number)