From b347b6d2745570bbe6bfdfb2c3f6e0035e141a37 Mon Sep 17 00:00:00 2001 From: elerac Date: Wed, 26 May 2021 18:28:13 +0900 Subject: [PATCH 01/43] Create utils.py --- EasyPySpin/__init__.py | 1 + EasyPySpin/utils.py | 9 +++++++++ EasyPySpin/videocapture.py | 3 +++ EasyPySpin/videocaptureex.py | 1 + 4 files changed, 14 insertions(+) create mode 100644 EasyPySpin/utils.py diff --git a/EasyPySpin/__init__.py b/EasyPySpin/__init__.py index 440b6a6..b701106 100644 --- a/EasyPySpin/__init__.py +++ b/EasyPySpin/__init__.py @@ -1,3 +1,4 @@ from .videocapture import VideoCapture from .synchronizedvideocapture import SynchronizedVideoCapture from .videocaptureex import VideoCaptureEX +from .utils import EasyPySpinWarning diff --git a/EasyPySpin/utils.py b/EasyPySpin/utils.py new file mode 100644 index 0000000..3e4f653 --- /dev/null +++ b/EasyPySpin/utils.py @@ -0,0 +1,9 @@ +import warnings + +class EasyPySpinWarning(Warning): + pass + +def warn(message: str, category: Warning = EasyPySpinWarning, stacklevel: int = 2) -> None: + """Default EasyPySpin warn + """ + warnings.warn(message, category, stacklevel+1) diff --git a/EasyPySpin/videocapture.py b/EasyPySpin/videocapture.py index 58ff947..473bf1c 100644 --- a/EasyPySpin/videocapture.py +++ b/EasyPySpin/videocapture.py @@ -1,7 +1,10 @@ +import warnings import cv2 import PySpin from sys import stderr +from .utils import EasyPySpinWarning, warn + class VideoCapture: """ Open a FLIR camera for video capturing. diff --git a/EasyPySpin/videocaptureex.py b/EasyPySpin/videocaptureex.py index fb56f45..4ecdcbd 100644 --- a/EasyPySpin/videocaptureex.py +++ b/EasyPySpin/videocaptureex.py @@ -2,6 +2,7 @@ import PySpin import numpy as np from .videocapture import VideoCapture +from .utils import warn class VideoCaptureEX(VideoCapture): """ From aa397e712921775f1c089d1f2acea0d77a6ca462 Mon Sep 17 00:00:00 2001 From: elerac Date: Wed, 26 May 2021 18:54:50 +0900 Subject: [PATCH 02/43] add setExceptionMode() --- EasyPySpin/videocapture.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/EasyPySpin/videocapture.py b/EasyPySpin/videocapture.py index 473bf1c..3981dc2 100644 --- a/EasyPySpin/videocapture.py +++ b/EasyPySpin/videocapture.py @@ -268,6 +268,8 @@ def _set_Gain(self, value): gain_to_set = self.__clip(value, self.cam.Gain.GetMin(), self.cam.Gain.GetMax()) self.cam.Gain.SetValue(gain_to_set) return True + def setExceptionMode(self, enable: bool) -> None: + """Switches exceptions mode. def _set_GainAuto(self, value): self.cam.GainAuto.SetValue(value) @@ -278,12 +280,21 @@ def _set_Brightness(self, value): brightness_to_set = self.__clip(value, self.cam.AutoExposureEVCompensation.GetMin(), self.cam.AutoExposureEVCompensation.GetMax()) self.cam.AutoExposureEVCompensation.SetValue(brightness_to_set) return True + Methods raise exceptions if not successful instead of returning an error code. def _set_Gamma(self, value): if not type(value) in (int, float): return False gamma_to_set = self.__clip(value, self.cam.Gamma.GetMin(), self.cam.Gamma.GetMax()) self.cam.Gamma.SetValue(gamma_to_set) return True + Parameters + ---------- + enable : bool + """ + if enable: + warnings.simplefilter('error', EasyPySpinWarning) + else: + warnings.simplefilter('ignore', EasyPySpinWarning) def _set_FrameRate(self, value): if not type(value) in (int, float): return False From 4765c49b9be9a79e7e6652f701116b70bf60128c Mon Sep 17 00:00:00 2001 From: elerac Date: Wed, 26 May 2021 18:58:35 +0900 Subject: [PATCH 03/43] add set_pyspin_value() --- EasyPySpin/videocapture.py | 132 +++++++++++++++++++++++++++++++++++++ 1 file changed, 132 insertions(+) diff --git a/EasyPySpin/videocapture.py b/EasyPySpin/videocapture.py index 3981dc2..1a160e6 100644 --- a/EasyPySpin/videocapture.py +++ b/EasyPySpin/videocapture.py @@ -302,6 +302,8 @@ def _set_FrameRate(self, value): fps_to_set = self.__clip(value, self.cam.AcquisitionFrameRate.GetMin(), self.cam.AcquisitionFrameRate.GetMax()) self.cam.AcquisitionFrameRate.SetValue(fps_to_set) return True + def set_pyspin_value(self, node_name: str, value: any) -> bool: + """Setting PySpin value with some useful checks. def _set_BackLight(self, value): if value==True:backlight_to_set = PySpin.DeviceIndicatorMode_Active @@ -309,6 +311,16 @@ def _set_BackLight(self, value): else: return False self.cam.DeviceIndicatorMode.SetValue(backlight_to_set) return True + This function adds functions that PySpin's ``SetValue`` does not support, + such as **writable check**, **argument type check**, **value range check and auto-clipping**. + If it fails, a warning will be raised. ``EasyPySpinWarning`` can control this warning. + + Parameters + ---------- + node_name : str + Name of the node to set. + value : any + Value to set. The type is assumed to be ``int``, ``float``, ``bool``, ``str`` or ``PySpin Enumerate``. def _set_Trigger(self, value): if value==True: @@ -316,6 +328,46 @@ def _set_Trigger(self, value): elif value==False: trigger_mode_to_set = PySpin.TriggerMode_Off else: + Returns + ------- + is_success : bool + Whether success or not: True for success, False for failure. + + Examples + -------- + Success case. + + >>> set_pyspin_value("ExposureTime", 1000.0) + True + >>> set_pyspin_value("Width", 256) + True + >>> set_pyspin_value("GammaEnable", False) + True + >>> set_pyspin_value("ExposureAuto", PySpin.ExposureAuto_Off) + True + >>> set_pyspin_value("ExposureAuto", "Off") + True + + Success case, and the value is clipped. + + >>> set_pyspin_value("ExposureTime", 0.1) + EasyPySpinWarning: 'ExposureTime' value must be in the range of [20.0, 30000002.0], so 0.1 become 20.0 + True + + Failure case. + + >>> set_pyspin_value("Width", 256.0123) + EasyPySpinWarning: 'value' must be 'int', not 'float' + False + >>> set_pyspin_value("hoge", 1) + EasyPySpinWarning: 'CameraPtr' object has no attribute 'hoge' + False + >>> set_pyspin_value("ExposureAuto", "hoge") + EasyPySpinWarning: 'PySpin' object has no attribute 'ExposureAuto_hoge' + False + """ + if not self.isOpened(): + warn("Camera is not open") return False self.cam.TriggerMode.SetValue(trigger_mode_to_set) @@ -325,6 +377,86 @@ def _set_TriggerDelay(self, value): if not type(value) in (int, float): return False delay_to_set = self.__clip(value, self.cam.TriggerDelay.GetMin(), self.cam.TriggerDelay.GetMax()) self.cam.TriggerDelay.SetValue(delay_to_set) + # Check 'CameraPtr' object has attribute 'node_name' + if not hasattr(self.cam, node_name): + warn(f"'{type(self.cam).__name__}' object has no attribute '{node_name}'") + return False + + # Get attribution + node = getattr(self.cam, node_name) + + # Check 'node' object has attribute 'SetValue' + if not hasattr(node, "SetValue"): + warn(f"'{type(node).__name__}' object has no attribute 'SetValue'") + return False + + # Check node is writable + if not PySpin.IsWritable(node): + warn(f"'{node_name}' is not writable") + return False + + # Get type + node_type = type(node) + value_type = type(value) + + # Convert numpy array with one element + # into a standard Python scalar object + if value_type is np.ndarray: + if value.size == 1: + value = value.item() + value_type = type(value) + + # Check value type of Integer node case + if node_type is PySpin.IInteger: + if value_type is not int: + warn(f"'value' must be 'int', not '{value_type.__name__}'") + return False + + # Check value type of Float node case + elif node_type is PySpin.IFloat: + if value_type not in (int, float): + warn(f"'value' must be 'int' or 'float', not '{value_type.__name__}'") + return False + + # Check value type of Boolean node case + elif node_type is PySpin.IBoolean: + if value_type is not bool: + warn(f"'value' must be 'bool', not '{value_type.__name__}'") + return False + + # Check value type of Enumeration node case + elif isinstance(node, PySpin.IEnumeration): + if value_type is str: + # If the type is ``str``, + # replace the corresponding PySpin's Enumeration if it exists. + enumeration_name = f"{node_name}_{value}" + if hasattr(PySpin, enumeration_name): + value = getattr(PySpin, enumeration_name) + value_type = type(value) + else: + warn(f"'PySpin' object has no attribute '{enumeration_name}'") + return False + elif value_type is not int: + warn(f"'value' must be PySpin's Enumeration, not '{value_type.__name__}'") + return False + + # Clip the value when node type is Integer of Float + if node_type in (PySpin.IInteger, PySpin.IFloat): + v_min = node.GetMin() + v_max = node.GetMax() + value_clipped = min(max(value, v_min), v_max) + if value_clipped != value: + warn(f"'{node_name}' value must be in the range of [{v_min}, {v_max}], so {value} become {value_clipped}") + value = value_clipped + + # Finally, SetValue + try: + node.SetValue(value) + except PySpin.SpinnakerException as e: + msg_pyspin = str(e) + warn(msg_pyspin) + return False + return True def _get_ExposureTime(self): From 8e963fb620b157aae66696c4777ceb5928968ae2 Mon Sep 17 00:00:00 2001 From: elerac Date: Wed, 26 May 2021 18:59:12 +0900 Subject: [PATCH 04/43] add get_pyspin_value() --- EasyPySpin/videocapture.py | 49 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/EasyPySpin/videocapture.py b/EasyPySpin/videocapture.py index 1a160e6..59e0ff2 100644 --- a/EasyPySpin/videocapture.py +++ b/EasyPySpin/videocapture.py @@ -464,24 +464,72 @@ def _get_ExposureTime(self): def _get_Gain(self): return self.cam.Gain.GetValue() + def get_pyspin_value(self, node_name: str) -> any: + """Getting PySpin value with some useful checks. def _get_Brightness(self): return self.cam.AutoExposureEVCompensation.GetValue() + Parameters + ---------- + node_name : str + Name of the node to get. def _get_Gamma(self): return self.cam.Gamma.GetValue() + Returns + ------- + value : any + value def _get_Width(self): return self.cam.Width.GetValue() + Examples + -------- + Success case. def _get_Height(self): return self.cam.Height.GetValue() + >>> get_pyspin_value("ExposureTime") + 103.0 + >>> get_pyspin_value("GammaEnable") + True + >>> get_pyspin_value("ExposureAuto") + 0 def _get_FrameRate(self): return self.cam.AcquisitionFrameRate.GetValue() + Failure case. def _get_Temperature(self): return self.cam.DeviceTemperature.GetValue() + >>> get_pyspin_value("hoge") + EasyPySpinWarning: 'CameraPtr' object has no attribute 'hoge' + None + """ + if not self.isOpened(): + warn("Camera is not open") + return False + + # Check 'CameraPtr' object has attribute 'node_name' + if not hasattr(self.cam, node_name): + warn(f"'{type(self.cam).__name__}' object has no attribute '{node_name}'") + return None + + # Get attribution + node = getattr(self.cam, node_name) + + # Check 'node_name' object has attribute 'GetValue' + if not hasattr(node, "GetValue"): + warn(f"'{type(node).__name__}' object has no attribute 'GetValue'") + return None + + # Check node is readable + if not PySpin.IsReadable(node): + warn(f"'{node_name}' is not readable") + return None + + # Finally, GetValue + value = node.GetValue() def _get_BackLight(self): status = self.cam.DeviceIndicatorMode.GetValue() @@ -497,3 +545,4 @@ def _get_Trigger(self): def _get_TriggerDelay(self): return self.cam.TriggerDelay.GetValue() + return value From ddd361891c5a7402926307fc2baf40a934651bce Mon Sep 17 00:00:00 2001 From: elerac Date: Wed, 26 May 2021 19:00:47 +0900 Subject: [PATCH 05/43] update set() and get() --- EasyPySpin/videocapture.py | 320 ++++++++++++++++++------------------- 1 file changed, 154 insertions(+), 166 deletions(-) diff --git a/EasyPySpin/videocapture.py b/EasyPySpin/videocapture.py index 59e0ff2..04a65b3 100644 --- a/EasyPySpin/videocapture.py +++ b/EasyPySpin/videocapture.py @@ -139,14 +139,15 @@ def read(self): image.Release() return True, img_NDArray - def set(self, propId, value): """ - Sets a property in the VideoCapture. + + def set(self, propId: 'cv2.VideoCaptureProperties', value: any) -> bool: + """Sets a property in the VideoCapture. Parameters ---------- propId_id : cv2.VideoCaptureProperties - Property identifier from cv2.VideoCaptureProperties + Property identifier from cv2.VideoCaptureProperties. value : int or float or bool Value of the property. @@ -155,53 +156,89 @@ def set(self, propId, value): retval : bool True if property setting success. """ - #Exposure setting - if propId==cv2.CAP_PROP_EXPOSURE: - #Auto - if value<0: return self._set_ExposureAuto(PySpin.ExposureAuto_Continuous) - - #Manual - ret = self._set_ExposureAuto(PySpin.ExposureAuto_Off) - if ret==False: return False - return self._set_ExposureTime(value) - - #Gain setting - if propId==cv2.CAP_PROP_GAIN: - #Auto - if value<0: return self._set_GainAuto(PySpin.GainAuto_Continuous) - - #Manual - ret = self._set_GainAuto(PySpin.GainAuto_Off) - if ret==False: return False - return self._set_Gain(value) - - #Brightness(EV) setting - if propId==cv2.CAP_PROP_BRIGHTNESS: - return self._set_Brightness(value) - - #Gamma setting - if propId==cv2.CAP_PROP_GAMMA: - return self._set_Gamma(value) - - #FrameRate setting - if propId==cv2.CAP_PROP_FPS: - return self._set_FrameRate(value) - - #BackLigth setting - if propId==cv2.CAP_PROP_BACKLIGHT: - return self._set_BackLight(value) - - #Trigger Mode setting (ON/OFF) - if propId==cv2.CAP_PROP_TRIGGER: - return self._set_Trigger(value) - - #TriggerDelay setting - if propId==cv2.CAP_PROP_TRIGGER_DELAY: - return self._set_TriggerDelay(value) + # Width setting + if propId == cv2.CAP_PROP_FRAME_WIDTH: + return self.set_pyspin_value("Width", value) + + # Height setting + if propId == cv2.CAP_PROP_FRAME_HEIGHT: + return self.set_pyspin_value("Height", value) + + # FrameRate setting + if propId == cv2.CAP_PROP_FPS: + is_success1 = self.set_pyspin_value("AcquisitionFrameRateEnable", True) + is_success2 = self.set_pyspin_value("AcquisitionFrameRate", value) + return (is_success1 and is_success2) + + # Brightness (EV) setting + if propId == cv2.CAP_PROP_BRIGHTNESS: + return self.set_pyspin_value("AutoExposureEVCompensation", value) + + # Gain setting + if propId == cv2.CAP_PROP_GAIN: + if value != -1: + # Manual + is_success1 = self.set_pyspin_value("GainAuto", "Off") + is_success2 = self.set_pyspin_value("Gain", value) + return (is_success1 and is_success2) + else: + # Auto + return self.set_pyspin_value("GainAuto", "Continuous") + + # Exposure setting + if propId == cv2.CAP_PROP_EXPOSURE: + if value != -1: + # Manual + is_success1 = self.set_pyspin_value("ExposureAuto", "Off") + is_success2 = self.set_pyspin_value("ExposureTime", value) + return (is_success1 and is_success2) + else: + # Auto + return self.set_pyspin_value("ExposureAuto", "Continuous") + + # Gamma setting + if propId == cv2.CAP_PROP_GAMMA: + is_success1 = self.set_pyspin_value("GammaEnable", True) + is_success2 = self.set_pyspin_value("Gamma", value) + return (is_success1 and is_success2) + + # Trigger Mode setting + if propId == cv2.CAP_PROP_TRIGGER: + if type(value) is not bool: + warn(f"'value' must be 'bool', not '{type(value).__name__}'") + return False + + trigger_mode = "On" if value else "Off" + return self.set_pyspin_value("TriggerMode", trigger_mode) + + # TriggerDelay setting + if propId == cv2.CAP_PROP_TRIGGER_DELAY: + return self.set_pyspin_value("TriggerDelay", value) + + # BackLigth setting + if propId == cv2.CAP_PROP_BACKLIGHT: + if type(value) is not bool: + warn(f"'value' must be 'bool', not '{type(value).__name__}'") + return False + + device_indicato_mode = "Active" if value else "Inactive" + return self.set_pyspin_value("DeviceIndicatorMode", device_indicato_mode) + + # Auto White Balance setting + if propId == cv2.CAP_PROP_AUTO_WB: + if type(value) is not bool: + warn(f"'value' must be 'bool', not '{type(value).__name__}'") + return False + + balance_white_auto_mode = "Continuous" if value else "Off" + return self.set_pyspin_value("BalanceWhiteAuto", balance_white_auto_mode) + + # If none of the above conditions apply + warn(f"propID={propId} is not supported") return False - def get(self, propId): + def get(self, propId: 'cv2.VideoCaptureProperties') -> any: """ Returns the specified VideoCapture property. @@ -212,81 +249,88 @@ def get(self, propId): Returns ------- - value : int or float or bool + value : any Value for the specified property. Value Flase is returned when querying a property that is not supported. """ - if propId==cv2.CAP_PROP_EXPOSURE: - return self._get_ExposureTime() - - if propId==cv2.CAP_PROP_GAIN: - return self._get_Gain() - - if propId==cv2.CAP_PROP_BRIGHTNESS: - return self._get_Brightness() - - if propId==cv2.CAP_PROP_GAMMA: - return self._get_Gamma() - - if propId==cv2.CAP_PROP_FRAME_WIDTH: - return self._get_Width() - - if propId==cv2.CAP_PROP_FRAME_HEIGHT: - return self._get_Height() - - if propId==cv2.CAP_PROP_FPS: - return self._get_FrameRate() - - if propId==cv2.CAP_PROP_TEMPERATURE: - return self._get_Temperature() - - if propId==cv2.CAP_PROP_BACKLIGHT: - return self._get_BackLight() + # Width + if propId == cv2.CAP_PROP_FRAME_WIDTH: + return self.get_pyspin_value("Width") + + # Height + if propId == cv2.CAP_PROP_FRAME_HEIGHT: + return self.get_pyspin_value("Height") + + # Frame Rate + if propId == cv2.CAP_PROP_FPS: + # If this does not equal the AcquisitionFrameRate + # it is because the ExposureTime is greater than the frame time. + return self.get_pyspin_value("ResultingFrameRate") + + # Brightness + if propId == cv2.CAP_PROP_BRIGHTNESS: + return self.get_pyspin_value("AutoExposureEVCompensation") + + # Gain + if propId == cv2.CAP_PROP_GAIN: + return self.get_pyspin_value("Gain") + + # Exposure Time + if propId == cv2.CAP_PROP_EXPOSURE: + return self.get_pyspin_value("ExposureTime") + + # Gamma + if propId == cv2.CAP_PROP_GAMMA: + return self.get_pyspin_value("Gamma") + + # Temperature + if propId == cv2.CAP_PROP_TEMPERATURE: + return self.get_pyspin_value("DeviceTemperature") + + # Trigger Mode + if propId == cv2.CAP_PROP_TRIGGER: + trigger_mode = self.get_pyspin_value("TriggerMode") + if trigger_mode == PySpin.TriggerMode_Off: + return False + elif trigger_mode == PySpin.TriggerMode_On: + return True + else: + return trigger_mode + + # Trigger Delay + if propId == cv2.CAP_PROP_TRIGGER_DELAY: + return self.get_pyspin_value("TriggerDelay") + + # Back Light + if propId == cv2.CAP_PROP_BACKLIGHT: + device_indicator_mode = self.get_pyspin_value("DeviceIndicatorMode") + if device_indicator_mode == PySpin.DeviceIndicatorMode_Inactive: + return False + elif device_indicator_mode == PySpin.DeviceIndicatorMode_Active: + return True + else: + return device_indicator_mode + + # Auto White Balance setting + if propId == cv2.CAP_PROP_AUTO_WB: + balance_white_auto = self.get_pyspin_value("BalanceWhiteAuto") - if propId==cv2.CAP_PROP_TRIGGER: - return self._get_Trigger() + if balance_white_auto == PySpin.BalanceWhiteAuto_Off: + return False + elif balance_white_auto == PySpin.BalanceWhiteAuto_Continuous: + return True + else: + return balance_white_auto - if propId==cv2.CAP_PROP_TRIGGER_DELAY: - return self._get_TriggerDelay() + # If none of the above conditions apply + warn(f"propID={propId} is not supported") return False - - def __clip(self, a, a_min, a_max): - return min(max(a, a_min), a_max) - - def _set_ExposureTime(self, value): - if not type(value) in (int, float): return False - exposureTime_to_set = self.__clip(value, self.cam.ExposureTime.GetMin(), self.cam.ExposureTime.GetMax()) - self.cam.ExposureTime.SetValue(exposureTime_to_set) - return True - def _set_ExposureAuto(self, value): - self.cam.ExposureAuto.SetValue(value) - return True - - def _set_Gain(self, value): - if not type(value) in (int, float): return False - gain_to_set = self.__clip(value, self.cam.Gain.GetMin(), self.cam.Gain.GetMax()) - self.cam.Gain.SetValue(gain_to_set) - return True def setExceptionMode(self, enable: bool) -> None: """Switches exceptions mode. - def _set_GainAuto(self, value): - self.cam.GainAuto.SetValue(value) - return True - - def _set_Brightness(self, value): - if not type(value) in (int, float): return False - brightness_to_set = self.__clip(value, self.cam.AutoExposureEVCompensation.GetMin(), self.cam.AutoExposureEVCompensation.GetMax()) - self.cam.AutoExposureEVCompensation.SetValue(brightness_to_set) - return True Methods raise exceptions if not successful instead of returning an error code. - def _set_Gamma(self, value): - if not type(value) in (int, float): return False - gamma_to_set = self.__clip(value, self.cam.Gamma.GetMin(), self.cam.Gamma.GetMax()) - self.cam.Gamma.SetValue(gamma_to_set) - return True Parameters ---------- enable : bool @@ -296,21 +340,9 @@ def _set_Gamma(self, value): else: warnings.simplefilter('ignore', EasyPySpinWarning) - def _set_FrameRate(self, value): - if not type(value) in (int, float): return False - self.cam.AcquisitionFrameRateEnable.SetValue(True) - fps_to_set = self.__clip(value, self.cam.AcquisitionFrameRate.GetMin(), self.cam.AcquisitionFrameRate.GetMax()) - self.cam.AcquisitionFrameRate.SetValue(fps_to_set) - return True def set_pyspin_value(self, node_name: str, value: any) -> bool: """Setting PySpin value with some useful checks. - def _set_BackLight(self, value): - if value==True:backlight_to_set = PySpin.DeviceIndicatorMode_Active - elif value==False: backlight_to_set = PySpin.DeviceIndicatorMode_Inactive - else: return False - self.cam.DeviceIndicatorMode.SetValue(backlight_to_set) - return True This function adds functions that PySpin's ``SetValue`` does not support, such as **writable check**, **argument type check**, **value range check and auto-clipping**. If it fails, a warning will be raised. ``EasyPySpinWarning`` can control this warning. @@ -322,12 +354,6 @@ def _set_BackLight(self, value): value : any Value to set. The type is assumed to be ``int``, ``float``, ``bool``, ``str`` or ``PySpin Enumerate``. - def _set_Trigger(self, value): - if value==True: - trigger_mode_to_set = PySpin.TriggerMode_On - elif value==False: - trigger_mode_to_set = PySpin.TriggerMode_Off - else: Returns ------- is_success : bool @@ -370,13 +396,6 @@ def _set_Trigger(self, value): warn("Camera is not open") return False - self.cam.TriggerMode.SetValue(trigger_mode_to_set) - return True - - def _set_TriggerDelay(self, value): - if not type(value) in (int, float): return False - delay_to_set = self.__clip(value, self.cam.TriggerDelay.GetMin(), self.cam.TriggerDelay.GetMax()) - self.cam.TriggerDelay.SetValue(delay_to_set) # Check 'CameraPtr' object has attribute 'node_name' if not hasattr(self.cam, node_name): warn(f"'{type(self.cam).__name__}' object has no attribute '{node_name}'") @@ -459,36 +478,23 @@ def _set_TriggerDelay(self, value): return True - def _get_ExposureTime(self): - return self.cam.ExposureTime.GetValue() - - def _get_Gain(self): - return self.cam.Gain.GetValue() def get_pyspin_value(self, node_name: str) -> any: """Getting PySpin value with some useful checks. - def _get_Brightness(self): - return self.cam.AutoExposureEVCompensation.GetValue() Parameters ---------- node_name : str Name of the node to get. - def _get_Gamma(self): - return self.cam.Gamma.GetValue() Returns ------- value : any value - def _get_Width(self): - return self.cam.Width.GetValue() Examples -------- Success case. - def _get_Height(self): - return self.cam.Height.GetValue() >>> get_pyspin_value("ExposureTime") 103.0 >>> get_pyspin_value("GammaEnable") @@ -496,12 +502,8 @@ def _get_Height(self): >>> get_pyspin_value("ExposureAuto") 0 - def _get_FrameRate(self): - return self.cam.AcquisitionFrameRate.GetValue() Failure case. - def _get_Temperature(self): - return self.cam.DeviceTemperature.GetValue() >>> get_pyspin_value("hoge") EasyPySpinWarning: 'CameraPtr' object has no attribute 'hoge' None @@ -531,18 +533,4 @@ def _get_Temperature(self): # Finally, GetValue value = node.GetValue() - def _get_BackLight(self): - status = self.cam.DeviceIndicatorMode.GetValue() - return (True if status == PySpin.DeviceIndicatorMode_Active else - False if status == PySpin.DeviceIndicatorMode_Inactive else - status) - - def _get_Trigger(self): - status = self.cam.TriggerMode.GetValue() - return (True if status == PySpin.TriggerMode_On else - False if status == PySpin.TriggerMode_Off else - status) - - def _get_TriggerDelay(self): - return self.cam.TriggerDelay.GetValue() return value From 458bb172b09b4d1563e035e2a215527c47546b2d Mon Sep 17 00:00:00 2001 From: elerac Date: Wed, 26 May 2021 19:01:18 +0900 Subject: [PATCH 06/43] update import modules --- EasyPySpin/videocapture.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/EasyPySpin/videocapture.py b/EasyPySpin/videocapture.py index 04a65b3..d5fc7d7 100644 --- a/EasyPySpin/videocapture.py +++ b/EasyPySpin/videocapture.py @@ -1,7 +1,9 @@ import warnings +from typing import Union, Tuple + +import numpy as np import cv2 import PySpin -from sys import stderr from .utils import EasyPySpinWarning, warn From 48788a71f6478122ea8c1b9756a1d43876d1cd6a Mon Sep 17 00:00:00 2001 From: elerac Date: Wed, 26 May 2021 19:02:37 +0900 Subject: [PATCH 07/43] add grab() --- EasyPySpin/videocapture.py | 23 ++++++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/EasyPySpin/videocapture.py b/EasyPySpin/videocapture.py index d5fc7d7..323909f 100644 --- a/EasyPySpin/videocapture.py +++ b/EasyPySpin/videocapture.py @@ -116,14 +116,17 @@ def isOpened(self): def read(self): """ returns the next frame. + def grab(self) -> bool: + """Grabs the next frame from capturing device. Returns ------- retval : bool - false if no frames has been grabbed. - image : array_like - grabbed image is returned here. If no image has been grabbed the image will be None. + ``True`` the case of success. """ + if not self.isOpened(): + return False + if not self.cam.IsStreaming(): self.cam.BeginAcquisition() @@ -131,7 +134,21 @@ def read(self): if (self.cam.TriggerMode.GetValue() ==PySpin.TriggerMode_On and self.cam.TriggerSource.GetValue()==PySpin.TriggerSource_Software and self.auto_software_trigger_execute==True): + # Execute a software trigger if required + if (PySpin.IsAvailable(self.cam.TriggerSoftware) + and self.auto_software_trigger_execute): + # Software-Trigger is executed under TWO conditions. + # First, the TriggerMode is set to ``On`` + # and the TriggerSource is set to ``Software``, + # so that SoftwareTrigger is available. + # Second, the member variable ``auto_software_trigger_execute`` is set to ``True``. self.cam.TriggerSoftware.Execute() + + # Grab image + self._pyspin_image = self.cam.GetNextImage(self.grabTimeout, self.streamID) + + is_complete = not self._pyspin_image.IsIncomplete() + return is_complete image = self.cam.GetNextImage(self.grabTimeout, self.streamID) if image.IsIncomplete(): From be39898d63443b0497ee8f151c88e961b0148c3d Mon Sep 17 00:00:00 2001 From: elerac Date: Wed, 26 May 2021 19:03:16 +0900 Subject: [PATCH 08/43] add retrieve() --- EasyPySpin/videocapture.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/EasyPySpin/videocapture.py b/EasyPySpin/videocapture.py index 323909f..554ae9b 100644 --- a/EasyPySpin/videocapture.py +++ b/EasyPySpin/videocapture.py @@ -153,12 +153,27 @@ def grab(self) -> bool: image = self.cam.GetNextImage(self.grabTimeout, self.streamID) if image.IsIncomplete(): return False, None + def retrieve(self) -> Tuple[bool, Union[np.ndarray, None]]: + """Decodes and returns the grabbed video frame. img_NDArray = image.GetNDArray() image.Release() return True, img_NDArray + Returns + ------- + retval : bool + ``False`` if no frames has been grabbed. + image : np.ndarray + grabbed image is returned here. If no image has been grabbed the image will be None. """ + if hasattr(self, "_pyspin_image"): + image_array = self._pyspin_image.GetNDArray() + self._pyspin_image.Release() + del self._pyspin_image + return True, image_array + else: + return False, None def set(self, propId: 'cv2.VideoCaptureProperties', value: any) -> bool: """Sets a property in the VideoCapture. From 73cdf6c0aeb78bae967b42da7253651144279a8a Mon Sep 17 00:00:00 2001 From: elerac Date: Wed, 26 May 2021 19:04:00 +0900 Subject: [PATCH 09/43] update read() --- EasyPySpin/videocapture.py | 33 ++++++++++++++++++++------------- 1 file changed, 20 insertions(+), 13 deletions(-) diff --git a/EasyPySpin/videocapture.py b/EasyPySpin/videocapture.py index 554ae9b..9c99ca8 100644 --- a/EasyPySpin/videocapture.py +++ b/EasyPySpin/videocapture.py @@ -113,9 +113,7 @@ def isOpened(self): try: return self.cam.IsValid() except: return False - def read(self): """ - returns the next frame. def grab(self) -> bool: """Grabs the next frame from capturing device. @@ -130,10 +128,6 @@ def grab(self) -> bool: if not self.cam.IsStreaming(): self.cam.BeginAcquisition() - # Execute a software trigger if necessary - if (self.cam.TriggerMode.GetValue() ==PySpin.TriggerMode_On and - self.cam.TriggerSource.GetValue()==PySpin.TriggerSource_Software and - self.auto_software_trigger_execute==True): # Execute a software trigger if required if (PySpin.IsAvailable(self.cam.TriggerSoftware) and self.auto_software_trigger_execute): @@ -150,16 +144,9 @@ def grab(self) -> bool: is_complete = not self._pyspin_image.IsIncomplete() return is_complete - image = self.cam.GetNextImage(self.grabTimeout, self.streamID) - if image.IsIncomplete(): - return False, None def retrieve(self) -> Tuple[bool, Union[np.ndarray, None]]: """Decodes and returns the grabbed video frame. - img_NDArray = image.GetNDArray() - image.Release() - return True, img_NDArray - Returns ------- retval : bool @@ -174,6 +161,26 @@ def retrieve(self) -> Tuple[bool, Union[np.ndarray, None]]: return True, image_array else: return False, None + + def read(self) -> Tuple[bool, Union[np.ndarray, None]]: + """Grabs, decodes and returns the next video frame. + + The method combines ``grab()`` and ``retrieve()`` in one call. + This is the most convenient method for capturing data from decode and returns the just grabbed frame. + If no frames has been grabbed, the method returns ``False`` and the function returns ``None``. + + Returns + ------- + retval : bool + ``False`` if no frames has been grabbed. + image : np.ndarray + grabbed image is returned here. If no image has been grabbed the image will be ``None``. + """ + retval = self.grab() + if retval: + return self.retrieve() + else: + return False, None def set(self, propId: 'cv2.VideoCaptureProperties', value: any) -> bool: """Sets a property in the VideoCapture. From bf957e6c38ca5447e51d14c1bd316caf07f1908b Mon Sep 17 00:00:00 2001 From: elerac Date: Wed, 26 May 2021 19:04:20 +0900 Subject: [PATCH 10/43] update isOpened() --- EasyPySpin/videocapture.py | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/EasyPySpin/videocapture.py b/EasyPySpin/videocapture.py index 9c99ca8..870e80a 100644 --- a/EasyPySpin/videocapture.py +++ b/EasyPySpin/videocapture.py @@ -106,14 +106,21 @@ def release(self): """ self.__del__() - def isOpened(self): - """ - Returns true if video capturing has been initialized already. - """ - try: return self.cam.IsValid() - except: return False + def isOpened(self) -> bool: + """Returns ``True`` if video capturing has been initialized already. + Returns + ------- + retval : bool """ + if self.cam is not None: + try: + return self.cam.IsValid() + except AttributeError: + return False + else: + return False + def grab(self) -> bool: """Grabs the next frame from capturing device. From 3c87846e683b2f03eeef4b0d479b1eb0fdbb347e Mon Sep 17 00:00:00 2001 From: elerac Date: Wed, 26 May 2021 19:05:14 +0900 Subject: [PATCH 11/43] update release() --- EasyPySpin/videocapture.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/EasyPySpin/videocapture.py b/EasyPySpin/videocapture.py index 870e80a..af788d4 100644 --- a/EasyPySpin/videocapture.py +++ b/EasyPySpin/videocapture.py @@ -100,9 +100,8 @@ def __del__(self): self._system.ReleaseInstance() except: pass - def release(self): - """ - Closes capturing device. The method call VideoCapture destructor. + def release(self) -> None: + """Closes capturing device. The method call VideoCapture destructor. """ self.__del__() From f0c58442f64b33e1664f78e4bb4d2149eacc5041 Mon Sep 17 00:00:00 2001 From: elerac Date: Wed, 26 May 2021 19:05:32 +0900 Subject: [PATCH 12/43] update __del__() --- EasyPySpin/videocapture.py | 22 +++++++++++++++------- 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/EasyPySpin/videocapture.py b/EasyPySpin/videocapture.py index af788d4..dcb5c96 100644 --- a/EasyPySpin/videocapture.py +++ b/EasyPySpin/videocapture.py @@ -92,13 +92,21 @@ def __init__(self, index): def __del__(self): try: - if self.cam.IsStreaming(): - self.cam.EndAcquisition() - self.cam.DeInit() - del self.cam - self._cam_list.Clear() - self._system.ReleaseInstance() - except: pass + if hasattr(self, "_cam"): + if self._cam.IsStreaming(): + self._cam.EndAcquisition() + del self._cam + + if hasattr(self, "_cam_list"): + self._cam_list.Clear() + + if hasattr(self, "_system"): + if not self._system.IsInUse(): + self._system.ReleaseInstance() + del self._system + + except PySpin.SpinnakerException: + pass def release(self) -> None: """Closes capturing device. The method call VideoCapture destructor. From 882e11e755774edf2548e7004aa1a32dcb3bdfad Mon Sep 17 00:00:00 2001 From: elerac Date: Wed, 26 May 2021 19:06:28 +0900 Subject: [PATCH 13/43] update StreamBufferHandlingMode --- EasyPySpin/videocapture.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/EasyPySpin/videocapture.py b/EasyPySpin/videocapture.py index dcb5c96..c8e8a61 100644 --- a/EasyPySpin/videocapture.py +++ b/EasyPySpin/videocapture.py @@ -81,10 +81,10 @@ def __init__(self, index): self.cam.Init() self.nodemap = self.cam.GetNodeMap() - s_node_map = self.cam.GetTLStreamNodeMap() - handling_mode = PySpin.CEnumerationPtr(s_node_map.GetNode('StreamBufferHandlingMode')) - handling_mode_entry = handling_mode.GetEntryByName('NewestOnly') - handling_mode.SetIntValue(handling_mode_entry.GetValue()) + # Switch 'StreamBufferHandlingMode' to 'NewestOnly'. + # This setting allows acquisition of the latest image + # by ignoring old images in the buffer, just like a web cam. + self.cam.TLStream.StreamBufferHandlingMode.SetValue(PySpin.StreamBufferHandlingMode_NewestOnly) self.grabTimeout = PySpin.EVENT_TIMEOUT_INFINITE self.streamID = 0 From f3a4ee4a4a690c17223aedc2b54e2c9c16cf9275 Mon Sep 17 00:00:00 2001 From: elerac Date: Wed, 26 May 2021 19:06:48 +0900 Subject: [PATCH 14/43] add cam() --- EasyPySpin/videocapture.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/EasyPySpin/videocapture.py b/EasyPySpin/videocapture.py index c8e8a61..6de9195 100644 --- a/EasyPySpin/videocapture.py +++ b/EasyPySpin/videocapture.py @@ -44,6 +44,14 @@ def __init__(self, index): ---------- index : int id of the video capturing device to open. + @property + def cam(self) -> Union[PySpin.CameraPtr, None]: + """Provide ``PySpin.CameraPtr``. + """ + if hasattr(self, "_cam"): + return self._cam + else: + return None """ # Check for 'index' type if isinstance(index, (int, str))==False: From 95259da8957207e2ff7ae5eb34c7ba6229279d99 Mon Sep 17 00:00:00 2001 From: elerac Date: Wed, 26 May 2021 19:07:49 +0900 Subject: [PATCH 15/43] add open() --- EasyPySpin/videocapture.py | 68 +++++++++++++++++++++++--------------- 1 file changed, 42 insertions(+), 26 deletions(-) diff --git a/EasyPySpin/videocapture.py b/EasyPySpin/videocapture.py index 6de9195..ef698ae 100644 --- a/EasyPySpin/videocapture.py +++ b/EasyPySpin/videocapture.py @@ -52,10 +52,23 @@ def cam(self) -> Union[PySpin.CameraPtr, None]: return self._cam else: return None + + def open(self, index: Union[int, str]) -> bool: + """Open a capturing device for video capturing. + + Parameters + ---------- + index : int or str + ``int`` type, the index at which to retrieve the camera object. + ``str`` type, the serial number of the camera object to retrieve. + + Returns + ------- + retval : bool + ``True`` if the file has been successfully opened. """ - # Check for 'index' type - if isinstance(index, (int, str))==False: - raise TypeError("Argument 'index' is required to be an integer or a string") + # Close the already opened camera + self.release() # Cerate system instance and get camera list self._system = PySpin.System.GetInstance() @@ -63,31 +76,33 @@ def cam(self) -> Union[PySpin.CameraPtr, None]: num_cam = self._cam_list.GetSize() # Check for available cameras - if num_cam==0: - print("EasyPySpin: no camera is available", file=stderr) - self._cam_list.Clear() - self._system.ReleaseInstance() - return None + if num_cam == 0: + warn("no camera is available") + self.release() + return False - # Try to connect camera - try: - # Index case - if type(index) is int: - # Check for 'index' bound - if index<0 or num_cam-1 Union[PySpin.CameraPtr, None]: self.grabTimeout = PySpin.EVENT_TIMEOUT_INFINITE self.streamID = 0 self.auto_software_trigger_execute = False + return True def __del__(self): try: From 5bf6b48fd96f60a3fed3f2dab465c7e271fd3a06 Mon Sep 17 00:00:00 2001 From: elerac Date: Wed, 26 May 2021 19:08:25 +0900 Subject: [PATCH 16/43] add typing for initial attribute --- EasyPySpin/videocapture.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/EasyPySpin/videocapture.py b/EasyPySpin/videocapture.py index ef698ae..f02be57 100644 --- a/EasyPySpin/videocapture.py +++ b/EasyPySpin/videocapture.py @@ -39,6 +39,15 @@ class VideoCapture: Gets a property. """ def __init__(self, index): + # a 64bit value that represents a timeout in milliseconds + grabTimeout: int = PySpin.EVENT_TIMEOUT_INFINITE + + # The stream to grab the image. + streamID: int = 0 + + # Whether or not to execute a software trigger when executing ``grab()``. + auto_software_trigger_execute: bool = False + """ Parameters ---------- @@ -109,9 +118,6 @@ def open(self, index: Union[int, str]) -> bool: # by ignoring old images in the buffer, just like a web cam. self.cam.TLStream.StreamBufferHandlingMode.SetValue(PySpin.StreamBufferHandlingMode_NewestOnly) - self.grabTimeout = PySpin.EVENT_TIMEOUT_INFINITE - self.streamID = 0 - self.auto_software_trigger_execute = False return True def __del__(self): From 6395461b3ba7902d08a13bd287d2412a3976826f Mon Sep 17 00:00:00 2001 From: elerac Date: Wed, 26 May 2021 19:08:40 +0900 Subject: [PATCH 17/43] update __init__() --- EasyPySpin/videocapture.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/EasyPySpin/videocapture.py b/EasyPySpin/videocapture.py index f02be57..9f695f1 100644 --- a/EasyPySpin/videocapture.py +++ b/EasyPySpin/videocapture.py @@ -38,7 +38,7 @@ class VideoCapture: get(propId) Gets a property. """ - def __init__(self, index): + # a 64bit value that represents a timeout in milliseconds grabTimeout: int = PySpin.EVENT_TIMEOUT_INFINITE @@ -48,11 +48,17 @@ def __init__(self, index): # Whether or not to execute a software trigger when executing ``grab()``. auto_software_trigger_execute: bool = False + def __init__(self, index: Union[int, str] = None): """ Parameters ---------- - index : int - id of the video capturing device to open. + index : int or str, default=None + For ``int`` type, the index at which to retrieve the camera object. + For ``str`` type, the serial number of the camera object to retrieve. + """ + if index is not None: + self.open(index) + @property def cam(self) -> Union[PySpin.CameraPtr, None]: """Provide ``PySpin.CameraPtr``. From e14e6c8fcc924d721ec77c12d3297c50dd8c9d68 Mon Sep 17 00:00:00 2001 From: elerac Date: Wed, 26 May 2021 19:09:23 +0900 Subject: [PATCH 18/43] update docstring of VideoCapture --- EasyPySpin/videocapture.py | 51 +++++++++++++++++++++++++++----------- 1 file changed, 36 insertions(+), 15 deletions(-) diff --git a/EasyPySpin/videocapture.py b/EasyPySpin/videocapture.py index 9f695f1..4a0c8f3 100644 --- a/EasyPySpin/videocapture.py +++ b/EasyPySpin/videocapture.py @@ -8,35 +8,56 @@ from .utils import EasyPySpinWarning, warn class VideoCapture: - """ - Open a FLIR camera for video capturing. + """Open a FLIR camera for video capturing. Attributes ---------- cam : PySpin.CameraPtr - camera - nodemap : PySpin.INodeMap - nodemap represents the elements of a camera description file. - grabTimeout : uint64_t + PySpin camera pointer. + grabTimeout : int, default=PySpin.EVENT_TIMEOUT_INFINITE a 64bit value that represents a timeout in milliseconds - streamID : uint64_t + streamID : int, default=0 The stream to grab the image. - auto_software_trigger_execute : bool - Whether or not to execute a software trigger when executing "read()". - When the "TriggerMode" is "On" and the "TriggerSource" is set to "Software". (Default: False) + auto_software_trigger_execute : bool, default=False + Whether or not to execute a software trigger when executing ``grab()``. + When the SoftwareTrigger is available. Methods ------- + get(propId) + Gets a property. + grab() + Grabs the next frame from capturing device. + isOpened() + Whether a camera is open or not. + open() + Open a capturing device for video capturing. read() - returns the next frame. + Returns the next frame. release() Closes capturing device. - isOpened() - Whether a camera is open or not. + retrieve() + Decodes and returns the grabbed video frame. set(propId, value) Sets a property. - get(propId) - Gets a property. + setExceptionMode(enable) + Switches exceptions mode. + + Notes + ----- + Supported ``cv2.VideoCaptureProperties`` for ``set()`` or ``get()`` methods. + `cv2.CAP_PROP_FPS` + `cv2.CAP_PROP_FRAME_WIDTH` + `cv2.CAP_PROP_FRAME_HEIGHT` + `cv2.CAP_PROP_BRIGHTNESS` + `cv2.CAP_PROP_GAIN` + `cv2.CAP_PROP_EXPOSURE` + `cv2.CAP_PROP_GAMMA` + `cv2.CAP_PROP_TEMPERATURE` (get only) + `cv2.CAP_PROP_TRIGGER` + `cv2.CAP_PROP_TRIGGER_DELAY` + `cv2.CAP_PROP_BACKLIGHT` + `cv2.CAP_PROP_AUTO_WB` """ # a 64bit value that represents a timeout in milliseconds From 030ec90c2887790b3c2be7d41886b86589f3c780 Mon Sep 17 00:00:00 2001 From: elerac Date: Wed, 26 May 2021 19:10:27 +0900 Subject: [PATCH 19/43] update import modules --- EasyPySpin/videocaptureex.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/EasyPySpin/videocaptureex.py b/EasyPySpin/videocaptureex.py index 4ecdcbd..feee526 100644 --- a/EasyPySpin/videocaptureex.py +++ b/EasyPySpin/videocaptureex.py @@ -1,6 +1,9 @@ +from typing import Union, Tuple, List + +import numpy as np import cv2 import PySpin -import numpy as np + from .videocapture import VideoCapture from .utils import warn From c830870c44e1b9d49f5155c42bee1ae676fa6625 Mon Sep 17 00:00:00 2001 From: elerac Date: Wed, 26 May 2021 19:10:59 +0900 Subject: [PATCH 20/43] update mergeHDR --- EasyPySpin/videocaptureex.py | 38 +++++++++++++----------------------- 1 file changed, 14 insertions(+), 24 deletions(-) diff --git a/EasyPySpin/videocaptureex.py b/EasyPySpin/videocaptureex.py index feee526..028d2c0 100644 --- a/EasyPySpin/videocaptureex.py +++ b/EasyPySpin/videocaptureex.py @@ -201,49 +201,39 @@ def readExposureBracketing(self, exposures): return True, imlist - - def mergeHDR(self, imlist, times, time_ref=10000, weighting='gaussian'): + def mergeHDR(self, imlist: List[np.ndarray], times: np.ndarray, time_ref: float = 10000) -> np.ndarray: """ Merge an HDR image from LDR images. Parameters ---------- - imlist : List[array_like] + imlist : List[np.ndarray] Multiple images with different exposure. The images are a range of 0.0 to 1.0. - times : array_like + times : np.ndarray Exposure times time_ref : float, optional Reference time. Determines the brightness of the merged image based on this time. - weighting : str, {'uniform', 'tent', 'gaussian', 'photon'}, optional - Weighting scheme Returns ------- - img_hdr : array_like + img_hdr : np.ndarray merged HDR image is returned here. """ Zmin = 0.01 Zmax = 0.99 epsilon = 1e-32 - z = np.array(imlist) # (num, height, width) - t = (np.array(times) / time_ref)[:, np.newaxis, np.newaxis] # (num,1,1) + + z = np.array(imlist) # (num, height, width) or (num, height, width, ch) + + t = np.array(times) / time_ref # (num, ) + t = np.expand_dims(t, axis=tuple(range(1, z.ndim))) # (num, 1, 1) or (num, 1, 1, 1) - # Calculate weight + # Calculate gaussian weight mask = np.bitwise_and(Zmin<=z, z<=Zmax) - if weighting=='uniform': - w = 1.0 * mask - elif weighting=='tent': - w = (0.5-np.abs(z-0.5)) * mask - elif weighting=='gaussian': - w = np.exp(-4*((z-0.5)/0.5)**2) * mask - elif weighting=='photon': - w = t*np.ones_like(z) * mask - else: - raise ValueError(f"Unknown weighting scheme '{weighting}'.") - + w = np.exp(-4*((z-0.5)/0.5)**2) * mask + # Merge HDR - img_hdr = np.sum(w*z/t, axis=0) / (np.sum(w, axis=0) + epsilon) - #img_hdr = np.exp(np.sum(w*(np.log(z+epsilon)-np.log(t)), axis=0)/(np.sum(w, axis=0)+1e-32)) + img_hdr = np.average(z/t, axis=0, weights=w+epsilon) # Dealing with under-exposure and over-exposure under_exposed = np.all(Zmin>z, axis=0) @@ -251,4 +241,4 @@ def mergeHDR(self, imlist, times, time_ref=10000, weighting='gaussian'): img_hdr[under_exposed] = Zmin/np.max(t) img_hdr[over_exposed] = Zmax/np.min(t) - return img_hdr + return img_hdr \ No newline at end of file From 831926e2899ff46042e08b43bf9771bda016a2b6 Mon Sep 17 00:00:00 2001 From: elerac Date: Wed, 26 May 2021 19:11:33 +0900 Subject: [PATCH 21/43] update readExposureBracketing() --- EasyPySpin/videocaptureex.py | 51 +++++++++++++++++------------------- 1 file changed, 24 insertions(+), 27 deletions(-) diff --git a/EasyPySpin/videocaptureex.py b/EasyPySpin/videocaptureex.py index 028d2c0..21e388e 100644 --- a/EasyPySpin/videocaptureex.py +++ b/EasyPySpin/videocaptureex.py @@ -141,9 +141,8 @@ def readHDR(self, t_min, t_max, num=None, t_ref=10000): return True, img_hdr - def readExposureBracketing(self, exposures): - """ - Execute exposure bracketing. + def readExposureBracketing(self, exposures: np.ndarray) -> Tuple[bool, List[np.ndarray]]: + """Execute exposure bracketing. Parameters ---------- @@ -158,46 +157,44 @@ def readExposureBracketing(self, exposures): Captured image list """ # Original settings for triggers, exposure, gain - TriggerSelector_origin = self.cam.TriggerSelector.GetValue() - TriggerMode_origin = self.cam.TriggerMode.GetValue() - TriggerSource_origin = self.cam.TriggerSource.GetValue() + node_names_to_change = ["TriggerSelector", "TriggerMode", "TriggerSource", "ExposureTime", "ExposureAuto", "GainAuto"] + values_origin = [self.get_pyspin_value(node_name) for node_name in node_names_to_change] auto_software_trigger_execute_origin = self.auto_software_trigger_execute - ExposureAuto_origin = self.cam.ExposureAuto.GetValue() - ExposureTime_origin = self.cam.ExposureTime.GetValue() - GainAuto_origin = self.cam.GainAuto.GetValue() - Gain_origin = self.cam.Gain.GetValue() # Change the trigger setting - self.cam.TriggerSelector.SetValue(PySpin.TriggerSelector_FrameStart) - self.cam.TriggerMode.SetValue(PySpin.TriggerMode_On) - self.cam.TriggerSource.SetValue(PySpin.TriggerSource_Software) + self.set_pyspin_value("TriggerSelector", "FrameStart") + self.set_pyspin_value("TriggerMode", "On") + self.set_pyspin_value("TriggerSource", "Software") self.auto_software_trigger_execute = True # Auto gain off and fixing gain - self.cam.GainAuto.SetValue(PySpin.GainAuto_Off) - self.cam.Gain.SetValue(Gain_origin) + gain = self.get_pyspin_value("Gain") + self.set_pyspin_value("GainAuto", "Off") + self.set_pyspin_value("Gain", gain) # Capture start - imlist = [None]*exposures.shape[0] - for i , t in enumerate(exposures): + imlist = [] + for i, t in enumerate(exposures): self.set(cv2.CAP_PROP_EXPOSURE, float(t)) + # Dummy image + if i == 0: + for _ in range(3): + self.grab() + ret, frame = self.read() - if ret==False: + if not ret: return False, None - imlist[i] = frame - - # Restore the changed settings + imlist.append(frame) + self.cam.EndAcquisition() - self.cam.TriggerSelector.SetValue(TriggerSelector_origin) - self.cam.TriggerMode.SetValue(TriggerMode_origin) - self.cam.TriggerSource.SetValue(TriggerSource_origin) + + # Restore the changed settings + for node_name, value in zip(node_names_to_change, values_origin): + self.set_pyspin_value(node_name, value) self.auto_software_trigger_execute = auto_software_trigger_execute_origin - self.cam.ExposureTime.SetValue(ExposureTime_origin) - self.cam.ExposureAuto.SetValue(ExposureAuto_origin) - self.cam.GainAuto.SetValue(GainAuto_origin) return True, imlist From ca2be8261b49a98c88d22a17ca47332230264d43 Mon Sep 17 00:00:00 2001 From: elerac Date: Wed, 26 May 2021 19:13:13 +0900 Subject: [PATCH 22/43] update readHDR() --- EasyPySpin/videocaptureex.py | 70 +++++++++++++++++++----------------- 1 file changed, 37 insertions(+), 33 deletions(-) diff --git a/EasyPySpin/videocaptureex.py b/EasyPySpin/videocaptureex.py index 21e388e..1092c4b 100644 --- a/EasyPySpin/videocaptureex.py +++ b/EasyPySpin/videocaptureex.py @@ -71,75 +71,79 @@ def read(self): return True, frame - def readHDR(self, t_min, t_max, num=None, t_ref=10000): - """ - Capture multiple images with different exposure and merge into an HDR image - NOTE: A software trigger is used to capture images. In order to acquire an image reliably at the set exposure time. + def readHDR(self, t_min: float, t_max: float, t_ref: float = 10000, ratio: float = 2.0) -> Tuple[bool, np.ndarray]: + """Capture multiple images with different exposure and merge into an HDR image. Parameters ---------- t_min : float - minimum exposure time [us] + Minimum exposure time [us] t_max : float - maximum exposure time [us] - num : int - number of shots. - If 'num' is None, 'num' is automatically determined from 't_min' and 't_max'. It is set so that the ratio of neighboring exposure times is approximately 2x. - t_ref : float, optional - Reference time [us]. Determines the brightness of the merged image based on this time. + Maximum exposure time [us] + t_ref : float, default=10000 + Reference time [us]. + Determines the brightness of the merged image based on this time. + ratio : int, default=2.0 + Ratio of exposure time. + Number of shots is automatically determined from `t_min` and `t_max`. + It is set so that the `ratio` of neighboring exposure times. Returns ------- retval : bool false if no frames has been grabbed. - image_hdr : array_like + image_hdr : np.ndarray merged HDR image is returned here. If no image has been grabbed the image will be None. + + Notes + ----- + A software trigger is used to capture images. In order to acquire an image reliably at the set exposure time. """ # Set between the maximum and minimum values of the camera t_min = np.clip(t_min, self.cam.ExposureTime.GetMin(), self.cam.ExposureTime.GetMax()) - t_max = np.clip(t_max, self.cam.ExposureTime.GetMin(), self.cam.ExposureTime.GetMax()) + t_max = np.clip(t_max, self.cam.ExposureTime.GetMin(), self.cam.ExposureTime.GetMax()) + # Determine nnumber of shots + num = 2 + if ratio > 1.0: + while t_max > t_min*(ratio**num): + num += 1 + + # Exposure time to be taken + # The equality sequence from minimum (t_min) to maximum (t_max) exposure time + times = np.geomspace(t_min, t_max, num=num) + # Original settings for gamma gamma_origin = self.get(cv2.CAP_PROP_GAMMA) # To capture a linear image, the gamma value is set to 1.0 self.set(cv2.CAP_PROP_GAMMA, 1.0) - - # If 'num' is None, determine num. - if num is None: - r = 2 # Ratio of exposure time - num = 2 - while t_max>t_min*(r**num): num += 1 - - # Exposure time to be taken - # The equality sequence from minimum (t_min) to maximum (t_max) exposure time - times = np.geomspace(t_min, t_max, num=num) # Exposure bracketing ret, imlist = self.readExposureBracketing(times) - if ret==False: - return False, None - + # Restore the changed gamma self.set(cv2.CAP_PROP_GAMMA, gamma_origin) + + if not ret: + return False, None # Normalize to a value between 0 and 1 # By dividing by the maximum value dtype = imlist[0].dtype - if dtype==np.uint8: - max_value = 2**8-1 - elif dtype==np.uint16: - max_value = 2**16-1 + if dtype == np.uint8: + max_value = float(2**8-1) + elif dtype == np.uint16: + max_value = float(2**16-1) else: - max_value = 1 + max_value = 1.0 imlist_norm = [ image/max_value for image in imlist] # Merge HDR img_hdr = self.mergeHDR(imlist_norm, times, t_ref) - return True, img_hdr - + return True, img_hdr.astype(np.float32) def readExposureBracketing(self, exposures: np.ndarray) -> Tuple[bool, List[np.ndarray]]: """Execute exposure bracketing. From 3e80209ba885c2837b694d6e92c59a9fd28f7b43 Mon Sep 17 00:00:00 2001 From: elerac Date: Wed, 26 May 2021 19:13:57 +0900 Subject: [PATCH 23/43] update read() --- EasyPySpin/videocaptureex.py | 45 +++++++++++++++++++++++++++--------- 1 file changed, 34 insertions(+), 11 deletions(-) diff --git a/EasyPySpin/videocaptureex.py b/EasyPySpin/videocaptureex.py index 1092c4b..b68692d 100644 --- a/EasyPySpin/videocaptureex.py +++ b/EasyPySpin/videocaptureex.py @@ -51,26 +51,49 @@ def __init__(self, index): super(VideoCaptureEX, self).__init__(index) self.average_num = 1 - def read(self): - """ - returns the next frame. - The returned frame is the average of multiple images taken. + def read(self) -> Tuple[bool, np.ndarray]: + """Returns the next frame. + + The returned frame is the **average of multiple images**. Returns ------- retval : bool false if no frames has been grabbed. - image : array_like + image : np.ndarray grabbed image is returned here. If no image has been grabbed the image will be None. + + Examples + -------- + Noemal case + >>> cap.average_num = 1 + >>> ret, frame = cap.read() + + Average of multiple images case + + >>> cap.average_num = 10 + >>> ret, frame = cap.read() """ - if self.average_num==1: + average_num = self.average_num + + if average_num == 1: return super(VideoCaptureEX, self).read() else: - imlist = [ super(VideoCaptureEX, self).read()[1] for i in range(self.average_num) ] - frame = (cv2.merge(imlist).mean(axis=2)).astype(imlist[0].dtype) - return True, frame - - + for i in range(average_num): + ret, image = super(VideoCaptureEX, self).read() + + if i == 0: + rets = np.empty((average_num), dtype=np.bool) + images = np.empty((*image.shape, average_num), dtype=image.dtype) + + rets[i] = ret + images[..., i] = image + + if np.all(rets): + image_averaged = np.mean(images, axis=-1).astype(image.dtype) + return True, image_averaged + else: + return False, None def readHDR(self, t_min: float, t_max: float, t_ref: float = 10000, ratio: float = 2.0) -> Tuple[bool, np.ndarray]: """Capture multiple images with different exposure and merge into an HDR image. From 1cb1746aa055061e7ec9bd773df3a90e5bf64310 Mon Sep 17 00:00:00 2001 From: elerac Date: Wed, 26 May 2021 19:14:25 +0900 Subject: [PATCH 24/43] add average_num's setter and getter --- EasyPySpin/videocaptureex.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/EasyPySpin/videocaptureex.py b/EasyPySpin/videocaptureex.py index b68692d..aa4bad3 100644 --- a/EasyPySpin/videocaptureex.py +++ b/EasyPySpin/videocaptureex.py @@ -50,6 +50,19 @@ def __init__(self, index): """ super(VideoCaptureEX, self).__init__(index) self.average_num = 1 + # Number of images to average + __average_num: int = 1 + + @property + def average_num(self) -> int: + return self.__average_num + + @average_num.setter + def average_num(self, value: int): + if (type(value) is int) and (value >= 1): + self.__average_num = value + else: + warn(f"'average_num' must be natural number, {value} is invalid") def read(self) -> Tuple[bool, np.ndarray]: """Returns the next frame. From 29c3b00edb23065d117fc969d24f133e285a1732 Mon Sep 17 00:00:00 2001 From: elerac Date: Wed, 26 May 2021 19:14:43 +0900 Subject: [PATCH 25/43] delete __init__() --- EasyPySpin/videocaptureex.py | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/EasyPySpin/videocaptureex.py b/EasyPySpin/videocaptureex.py index aa4bad3..a88910f 100644 --- a/EasyPySpin/videocaptureex.py +++ b/EasyPySpin/videocaptureex.py @@ -41,15 +41,7 @@ class VideoCaptureEX(VideoCapture): get(propId) Gets a property. """ - def __init__(self, index): - """ - Parameters - ---------- - index : int - id of the video capturing device to open. - """ - super(VideoCaptureEX, self).__init__(index) - self.average_num = 1 + # Number of images to average __average_num: int = 1 From 0efc5fda94f572cd10331000ad5767e3c513e7ba Mon Sep 17 00:00:00 2001 From: elerac Date: Wed, 26 May 2021 19:15:05 +0900 Subject: [PATCH 26/43] update docstring of VideoCaptureEX --- EasyPySpin/videocaptureex.py | 62 +++++++++++++++++++++++++----------- 1 file changed, 44 insertions(+), 18 deletions(-) diff --git a/EasyPySpin/videocaptureex.py b/EasyPySpin/videocaptureex.py index a88910f..51f47e5 100644 --- a/EasyPySpin/videocaptureex.py +++ b/EasyPySpin/videocaptureex.py @@ -8,38 +8,64 @@ from .utils import warn class VideoCaptureEX(VideoCapture): - """ - VideoCaptureEX class is subclass of VideoCapture class. - It provides extensions that are not supported by OpenCV's VideoCapture. - + """Open a FLIR camera for video capturing. + + VideoCaptureEX class is subclass of VideoCapture class. + It provides EXTENSIONS that OpenCV's VideoCapture does not support. + For example, Averaged image, Exposure bracketing, and HDR image. + Attributes ---------- cam : PySpin.CameraPtr - camera - nodemap : PySpin.INodeMap - nodemap represents the elements of a camera description file. - grabTimeout : uint64_t + PySpin camera pointer. + grabTimeout : int, default=PySpin.EVENT_TIMEOUT_INFINITE a 64bit value that represents a timeout in milliseconds - streamID : uint64_t + streamID : int, default=0 The stream to grab the image. - auto_software_trigger_execute : bool - Whether or not to execute a software trigger when executing "read()". - When the "TriggerMode" is "On" and the "TriggerSource" is set to "Software". (Default: False) + auto_software_trigger_execute : bool, default=False + Whether or not to execute a software trigger when executing ``grab()``. + When the SoftwareTrigger is available. average_num : int - average number + Number of images to average. It must be natural number. Methods ------- + get(propId) + Gets a property. + grab() + Grabs the next frame from capturing device. + isOpened() + Whether a camera is open or not. + open() + Open a capturing device for video capturing. read() - returns the next frame. + Returns the next frame. release() Closes capturing device. - isOpened() - Whether a camera is open or not. + retrieve() + Decodes and returns the grabbed video frame. set(propId, value) Sets a property. - get(propId) - Gets a property. + setExceptionMode(enable) + Switches exceptions mode. + readHDR(t_min, t_max, num, t_ref) + Capture multiple images with different exposure and merge into an HDR image. + + Notes + ----- + Supported ``cv2.VideoCaptureProperties`` for ``set()`` or ``get()`` methods. + `cv2.CAP_PROP_FPS` + `cv2.CAP_PROP_FRAME_WIDTH` + `cv2.CAP_PROP_FRAME_HEIGHT` + `cv2.CAP_PROP_BRIGHTNESS` + `cv2.CAP_PROP_GAIN` + `cv2.CAP_PROP_EXPOSURE` + `cv2.CAP_PROP_GAMMA` + `cv2.CAP_PROP_TEMPERATURE` (get only) + `cv2.CAP_PROP_TRIGGER` + `cv2.CAP_PROP_TRIGGER_DELAY` + `cv2.CAP_PROP_BACKLIGHT` + `cv2.CAP_PROP_AUTO_WB` """ # Number of images to average From 2c50fb689171ba36883354cc412108c318c601ab Mon Sep 17 00:00:00 2001 From: elerac Date: Wed, 26 May 2021 19:17:39 +0900 Subject: [PATCH 27/43] add MultipleVideoCapture class --- EasyPySpin/__init__.py | 1 + EasyPySpin/multiplevideocapture.py | 96 ++++++++++++++++++++++++++++++ 2 files changed, 97 insertions(+) create mode 100644 EasyPySpin/multiplevideocapture.py diff --git a/EasyPySpin/__init__.py b/EasyPySpin/__init__.py index b701106..8c1a1c8 100644 --- a/EasyPySpin/__init__.py +++ b/EasyPySpin/__init__.py @@ -1,4 +1,5 @@ from .videocapture import VideoCapture from .synchronizedvideocapture import SynchronizedVideoCapture from .videocaptureex import VideoCaptureEX +from .multiplevideocapture import MultipleVideoCapture from .utils import EasyPySpinWarning diff --git a/EasyPySpin/multiplevideocapture.py b/EasyPySpin/multiplevideocapture.py new file mode 100644 index 0000000..79ad540 --- /dev/null +++ b/EasyPySpin/multiplevideocapture.py @@ -0,0 +1,96 @@ +from typing import List, Tuple, Union +from concurrent.futures import ThreadPoolExecutor + +import numpy as np +import PySpin + +from .videocapture import VideoCapture + +class MultipleVideoCapture: + """VideoCapture for Multiple cameras. + + Examples + -------- + >>> cap = MultipleVideoCapture(0) + >>> cap.isOpened() + [True] + >>> cap = MultipleVideoCapture(0, 1) + >>> cap.isOpened() + [True, True] + >>> cap.set(cv2.CAP_PROP_EXPOSURE, 1000) + [True, True] + >>> cap.get(cv2.CAP_PROP_EXPOSURE) + [1000.0, 1000.0] + >>> cap[0].set(cv2.CAP_PROP_EXPOSURE, 2000) + True + >>> cap.get(cv2.CAP_PROP_EXPOSURE) + [2000.0, 1000.0] + >>> (ret0, frame0), (ret1, frame1) = cap.read() + >>> cap.release() + """ + + VideoCaptureBase = VideoCapture + __caps = [None] + + def __init__(self, *indexes: Tuple[Union[int, str], ...]): + self.open(*indexes) + + def __del__(self): + return [cap.__del__() for cap in self] + + def __len__(self): + return self.__caps.__len__() + + def __getitem__(self, item): + return self.__caps.__getitem__(item) + + def __iter__(self): + return self.__caps.__iter__() + + def __next__(self): + return self.__caps.__next__() + + def __setattr__(self, key, value): + for cap in self: + if hasattr(cap, key): + setattr(cap, key, value) + + return object.__setattr__(self, key, value) + + def open(self, *indexs: Tuple[Union[int, str], ...]) -> List[bool]: + self.__caps = [self.VideoCaptureBase(index) for index in indexs] + return self.isOpened() + + def isOpened(self) -> List[bool]: + return [cap.isOpened() for cap in self] + + def grab(self) -> List[bool]: + return [cap.grab() for cap in self] + + def retrieve(self) -> List[Tuple[bool, Union[np.ndarray, None]]]: + return [cap.retrieve() for cap in self] + + def read(self) -> List[Tuple[bool, Union[np.ndarray, None]]]: + #return [cap.read() for cap in self] + executor = ThreadPoolExecutor() + futures = [executor.submit(cap.read) for cap in self] + executor.shutdown() + return [future.result() for future in futures] + + def release(self) -> List[None]: + return self.__del__() + + def set(self, propId: "cv2.VideoCaptureProperties", value: any) -> List[bool]: + return [cap.set(propId, value) for cap in self] + + def get(self, propId: "cv2.VideoCaptureProperties") -> List[any]: + return [cap.get(propId) for cap in self] + + def setExceptionMode(self, enable: bool) -> List[None]: + return [cap.setExceptionMode(enable) for cap in self] + + def set_pyspin_value(self, node_name: str, value: any) -> List[any]: + return [cap.set_pyspin_value(node_name, value) for cap in self] + + def get_pyspin_value(self, node_name: str) -> List[any]: + return [cap.get_pyspin_value(node_name) for cap in self] From af56f1ac9b1ee65da8eab8ee7b33e35f73a29636 Mon Sep 17 00:00:00 2001 From: elerac Date: Wed, 26 May 2021 19:17:57 +0900 Subject: [PATCH 28/43] Update README.md --- README.md | 66 ++++++++++++++++++++++++++++++++----------------------- 1 file changed, 38 insertions(+), 28 deletions(-) diff --git a/README.md b/README.md index 564c40b..e544d0b 100644 --- a/README.md +++ b/README.md @@ -13,13 +13,6 @@ pip install git+https://github.com/elerac/EasyPySpin ``` After installation, connect the camera and try `examples/video.py`. -## Command Line Tool -Connect the camera and execute the following commands, as shown below, then you can check the captured images. -```sh -EasyPySpin -``` -To change the camera settings, add an option to this command. Check with the `-h` option. - ## Usage ### Capture image from camera Here's an example to capture image from camera. @@ -35,43 +28,60 @@ cv2.imwrite("frame.png", frame) cap.release() ``` + ### Basic property settings You can access properties using `cap.set(propId, value)` or `cap.get(propId)`. See also [supported propId](#Supported-VideoCaptureProperties). ```python -cap.set(cv2.CAP_PROP_EXPOSURE, 100000) #us -cap.set(cv2.CAP_PROP_GAIN, 10) #dB +cap.set(cv2.CAP_PROP_EXPOSURE, 100000) # us +cap.set(cv2.CAP_PROP_GAIN, 10) # dB -print(cap.get(cv2.CAP_PROP_FRAME_WIDTH)) -print(cap.get(cv2.CAP_PROP_FRAME_HEIGHT)) +width = cap.get(cv2.CAP_PROP_FRAME_WIDTH) +height = cap.get(cv2.CAP_PROP_FRAME_HEIGHT) ``` ### Advanced property settings -`cap.set()` and `cap.get()` can only access basic properties. To access advanced properties, you should use QuickSpinAPI or GenAPI. +`cap.set()` and `cap.get()` can only access basic properties. To access advanced properties, you can use QuickSpin API, which PySpin supports. ```python -#QuickSpinAPI example cap.cam.AdcBitDepth.SetValue(PySpin.AdcBitDepth_Bit12) cap.cam.PixelFormat.SetValue(PySpin.PixelFormat_Mono16) +``` +The other way is to use `cap.set_pyspin_value()` or `cap.get_pyspin_value()`, which are supported by EasyPySpin. These methods check whether the variable is writeable or readable and check the type of the variable, etc., at the same time. +```python +cap.set_pyspin_value("AdcBitDepth", "Bit12") +cap.set_pyspin_value("PixelFormat", "Mono16") -#GenAPI example -node_exposureAuto = PySpin.CEnumerationPtr(cap.nodemap.GetNode("ExposureAuto")) -exposureAuto = PySpin.CEnumEntryPtr(node_exposureAuto.GetEntryByName("Once")).GetValue() -node_exposureAuto.SetIntValue(exposureAuto) +cap.get_pyspin_value("GammaEnable") +cap.get_pyspin_value("DeviceModelName") ``` ## Supported VideoCaptureProperties -* `cv2.CAP_PROP_EXPOSURE` -* `cv2.CAP_PROP_GAIN` -* `cv2.CAP_PROP_GAMMA` -* `cv2.CAP_PROP_FPS` -* `cv2.CAP_PROP_BRIGHTNESS` -* `cv2.CAP_PROP_FRAME_WIDTH` (get only) -* `cv2.CAP_PROP_FRAME_HEIGHT` (get only) -* `cv2.CAP_PROP_TEMPERATURE` (get only) -* `cv2.CAP_PROP_BACKLIGHT` -* `cv2.CAP_PROP_TRIGGER` -* `cv2.CAP_PROP_TRIGGER_DELAY` +Here is the list of supported VideoCaptureProperties. +In `set(propId, value)` and `get(propId)`, PySpin is used to set and get the camera's settings. The relationship between `propId` and PySpin settings is designed to be as close in meaning as possible. The table below shows the relationship between `propId` and PySpin settings in pseudo-code format. + +| propId | type | set(propId, value) | value = get(propId) | +| ---- | ---- | ---- | ---- | +| `cv2.CAP_PROP_FRAME_WIDTH` | int | `Width` = value | value = `Width` | +| `cv2.CAP_PROP_FRAME_HEIGHT` | int | `Height` = value | value = `Height` | +| `cv2.CAP_PROP_FPS` | float | `AcquisitionFrameRateEnable` = `True`
`AcquisitionFrameRate` = value | value = `ResultingFrameRate`| +| `cv2.CAP_PROP_BRIGHTNESS` | float | `AutoExposureEVCompensation` = value | value = `AutoExposureEVCompensation` | +| `cv2.CAP_PROP_GAIN` | float | if value != -1
  `GainAuto` = `Off`
  `Gain` = value
else
  `GainAuto` = `Continuous` | value = `Gain` | +| `cv2.CAP_PROP_EXPOSURE` | float | if value != -1
  `ExposureAuto` = `Off`
  `ExposureTime` = value
else
  `ExposureAuto` = `Continuous` | value = `ExposureTime` | +| `cv2.CAP_PROP_GAMMA` | float | `GammaEnable` = `True`
`Gamma` = value | value = `Gamma` | +| `cv2.CAP_PROP_TEMPERATURE` | float | | value = `DeviceTemperature` | +| `cv2.CAP_PROP_TRIGGER` | bool | if value == `True`
  `TriggerMode` = `On`
else
  `TriggerMode` = `Off` | if trigger_mode == `On`
  value = `True`
elif trigger_mode == `Off`
  value = `False` | +| `cv2.CAP_PROP_TRIGGER_DELAY` | float | `TriggerDelay` = value | value = `TriggerDelay` | +| `cv2.CAP_PROP_BACKLIGHT` | bool | if value == `True`
  `DeviceIndicatorMode` = `Active`
else
  `DeviceIndicatorMode` = `Inactive` | if device_indicator_mode == `Active`
  value = `True`
elif device_indicator_mode == `Inactive`
  value = `False` | +| `cv2.CAP_PROP_AUTO_WB` | bool | if value == `True`
  `BalanceWhiteAuto` = `Continuous`
else
  `BalanceWhiteAuto` = `Off` | if balance_white_auto == `Continuous`
  value = `True`
elif balance_white_auto == `Off`
  value = `False` | + +## Command-Line Tool +EasyPySpin provides a command-line tool. Connect the camera and execute the following commands, as shown below, then you can view the captured images. +```sh +EasyPySpin [-h] [-i INDEX] [-e EXPOSURE] [-g GAIN] [-G GAMMA] + [-b BRIGHTNESS] [-f FPS] [-s SCALE] +``` ## External Links +Here are some external links that are useful for using Spinnaker SDK. * [Spinnaker® SDK Programmer's Guide and API Reference (C++)](http://softwareservices.ptgrey.com/Spinnaker/latest/index.html) * [Getting Started with Spinnaker SDK on MacOS Applicable products](https://www.flir.com/support-center/iis/machine-vision/application-note/getting-started-with-spinnaker-sdk-on-macos/) * [Spinnaker Nodes](https://www.flir.com/support-center/iis/machine-vision/application-note/spinnaker-nodes/) From b81aee2bb0947edb385f3692dceb60cc987659c3 Mon Sep 17 00:00:00 2001 From: elerac Date: Sat, 7 Aug 2021 20:31:56 +0900 Subject: [PATCH 29/43] Disable releasing pyspin-image --- EasyPySpin/videocapture.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/EasyPySpin/videocapture.py b/EasyPySpin/videocapture.py index 4a0c8f3..581da52 100644 --- a/EasyPySpin/videocapture.py +++ b/EasyPySpin/videocapture.py @@ -227,8 +227,6 @@ def retrieve(self) -> Tuple[bool, Union[np.ndarray, None]]: """ if hasattr(self, "_pyspin_image"): image_array = self._pyspin_image.GetNDArray() - self._pyspin_image.Release() - del self._pyspin_image return True, image_array else: return False, None From 3541e822450fb9e32e93652f17d1a6c5a0c76279 Mon Sep 17 00:00:00 2001 From: elerac Date: Sat, 7 Aug 2021 20:34:15 +0900 Subject: [PATCH 30/43] Set `mergeHDR` method as `staticmethod` --- EasyPySpin/videocaptureex.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/EasyPySpin/videocaptureex.py b/EasyPySpin/videocaptureex.py index 51f47e5..9fbc6d8 100644 --- a/EasyPySpin/videocaptureex.py +++ b/EasyPySpin/videocaptureex.py @@ -255,8 +255,9 @@ def readExposureBracketing(self, exposures: np.ndarray) -> Tuple[bool, List[np.n self.auto_software_trigger_execute = auto_software_trigger_execute_origin return True, imlist - - def mergeHDR(self, imlist: List[np.ndarray], times: np.ndarray, time_ref: float = 10000) -> np.ndarray: + + @staticmethod + def mergeHDR(imlist: List[np.ndarray], times: np.ndarray, time_ref: float = 10000) -> np.ndarray: """ Merge an HDR image from LDR images. @@ -296,4 +297,4 @@ def mergeHDR(self, imlist: List[np.ndarray], times: np.ndarray, time_ref: float img_hdr[under_exposed] = Zmin/np.max(t) img_hdr[over_exposed] = Zmax/np.min(t) - return img_hdr \ No newline at end of file + return img_hdr return img_hdr From 1f50b7297572ffb7a82ab250e3cd0290c1ebf59d Mon Sep 17 00:00:00 2001 From: elerac Date: Sat, 7 Aug 2021 22:33:20 +0900 Subject: [PATCH 31/43] Disable `grab` and `retrieve` methods of `VideoCaptureEX` --- EasyPySpin/videocaptureex.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/EasyPySpin/videocaptureex.py b/EasyPySpin/videocaptureex.py index 9fbc6d8..95c0827 100644 --- a/EasyPySpin/videocaptureex.py +++ b/EasyPySpin/videocaptureex.py @@ -82,6 +82,12 @@ def average_num(self, value: int): else: warn(f"'average_num' must be natural number, {value} is invalid") + def grab(self): + raise Exception("VideoCaptureEX does not support `grab` module") + + def retrieve(self): + raise Exception("VideoCaptureEX does not support `retrieve` module") + def read(self) -> Tuple[bool, np.ndarray]: """Returns the next frame. From 244b5a1367b2f4641772a8aac04e9067afc50e16 Mon Sep 17 00:00:00 2001 From: elerac Date: Sat, 7 Aug 2021 22:45:57 +0900 Subject: [PATCH 32/43] Clean up! --- EasyPySpin/utils.py | 11 ++- EasyPySpin/videocapture.py | 167 ++++++++++++++++++----------------- EasyPySpin/videocaptureex.py | 115 +++++++++++++++--------- 3 files changed, 167 insertions(+), 126 deletions(-) diff --git a/EasyPySpin/utils.py b/EasyPySpin/utils.py index 3e4f653..b934fe6 100644 --- a/EasyPySpin/utils.py +++ b/EasyPySpin/utils.py @@ -1,9 +1,12 @@ import warnings + class EasyPySpinWarning(Warning): pass -def warn(message: str, category: Warning = EasyPySpinWarning, stacklevel: int = 2) -> None: - """Default EasyPySpin warn - """ - warnings.warn(message, category, stacklevel+1) + +def warn( + message: str, category: Warning = EasyPySpinWarning, stacklevel: int = 2 +) -> None: + """Default EasyPySpin warn""" + warnings.warn(message, category, stacklevel + 1) diff --git a/EasyPySpin/videocapture.py b/EasyPySpin/videocapture.py index 581da52..4f728da 100644 --- a/EasyPySpin/videocapture.py +++ b/EasyPySpin/videocapture.py @@ -7,6 +7,7 @@ from .utils import EasyPySpinWarning, warn + class VideoCapture: """Open a FLIR camera for video capturing. @@ -62,13 +63,13 @@ class VideoCapture: # a 64bit value that represents a timeout in milliseconds grabTimeout: int = PySpin.EVENT_TIMEOUT_INFINITE - + # The stream to grab the image. streamID: int = 0 - + # Whether or not to execute a software trigger when executing ``grab()``. auto_software_trigger_execute: bool = False - + def __init__(self, index: Union[int, str] = None): """ Parameters @@ -79,11 +80,10 @@ def __init__(self, index: Union[int, str] = None): """ if index is not None: self.open(index) - + @property def cam(self) -> Union[PySpin.CameraPtr, None]: - """Provide ``PySpin.CameraPtr``. - """ + """Provide ``PySpin.CameraPtr``.""" if hasattr(self, "_cam"): return self._cam else: @@ -106,7 +106,7 @@ def open(self, index: Union[int, str]) -> bool: # Close the already opened camera self.release() - # Cerate system instance and get camera list + # Cerate system instance and get camera list self._system = PySpin.System.GetInstance() self._cam_list = self._system.GetCameras() num_cam = self._cam_list.GetSize() @@ -116,7 +116,7 @@ def open(self, index: Union[int, str]) -> bool: warn("no camera is available") self.release() return False - + # Get CameraPtr if type(index) is int: if index in range(num_cam): @@ -131,7 +131,7 @@ def open(self, index: Union[int, str]) -> bool: warn(f"'index' must be 'int' or 'str', not '{type(index).__name__}'") self.release() return False - + if not self._cam.IsValid(): self.release() return False @@ -139,24 +139,26 @@ def open(self, index: Union[int, str]) -> bool: # Initialize camera if not self.cam.IsInitialized(): self.cam.Init() - + # Switch 'StreamBufferHandlingMode' to 'NewestOnly'. - # This setting allows acquisition of the latest image + # This setting allows acquisition of the latest image # by ignoring old images in the buffer, just like a web cam. - self.cam.TLStream.StreamBufferHandlingMode.SetValue(PySpin.StreamBufferHandlingMode_NewestOnly) + self.cam.TLStream.StreamBufferHandlingMode.SetValue( + PySpin.StreamBufferHandlingMode_NewestOnly + ) return True - + def __del__(self): try: if hasattr(self, "_cam"): if self._cam.IsStreaming(): self._cam.EndAcquisition() del self._cam - + if hasattr(self, "_cam_list"): self._cam_list.Clear() - + if hasattr(self, "_system"): if not self._system.IsInUse(): self._system.ReleaseInstance() @@ -166,8 +168,7 @@ def __del__(self): pass def release(self) -> None: - """Closes capturing device. The method call VideoCapture destructor. - """ + """Closes capturing device. The method call VideoCapture destructor.""" self.__del__() def isOpened(self) -> bool: @@ -198,26 +199,28 @@ def grab(self) -> bool: if not self.cam.IsStreaming(): self.cam.BeginAcquisition() - + # Execute a software trigger if required - if (PySpin.IsAvailable(self.cam.TriggerSoftware) - and self.auto_software_trigger_execute): - # Software-Trigger is executed under TWO conditions. + if ( + PySpin.IsAvailable(self.cam.TriggerSoftware) + and self.auto_software_trigger_execute + ): + # Software-Trigger is executed under TWO conditions. # First, the TriggerMode is set to ``On`` - # and the TriggerSource is set to ``Software``, - # so that SoftwareTrigger is available. + # and the TriggerSource is set to ``Software``, + # so that SoftwareTrigger is available. # Second, the member variable ``auto_software_trigger_execute`` is set to ``True``. self.cam.TriggerSoftware.Execute() - + # Grab image self._pyspin_image = self.cam.GetNextImage(self.grabTimeout, self.streamID) - + is_complete = not self._pyspin_image.IsIncomplete() return is_complete def retrieve(self) -> Tuple[bool, Union[np.ndarray, None]]: """Decodes and returns the grabbed video frame. - + Returns ------- retval : bool @@ -234,15 +237,15 @@ def retrieve(self) -> Tuple[bool, Union[np.ndarray, None]]: def read(self) -> Tuple[bool, Union[np.ndarray, None]]: """Grabs, decodes and returns the next video frame. - The method combines ``grab()`` and ``retrieve()`` in one call. - This is the most convenient method for capturing data from decode and returns the just grabbed frame. + The method combines ``grab()`` and ``retrieve()`` in one call. + This is the most convenient method for capturing data from decode and returns the just grabbed frame. If no frames has been grabbed, the method returns ``False`` and the function returns ``None``. Returns ------- retval : bool ``False`` if no frames has been grabbed. - image : np.ndarray + image : np.ndarray grabbed image is returned here. If no image has been grabbed the image will be ``None``. """ retval = self.grab() @@ -250,8 +253,8 @@ def read(self) -> Tuple[bool, Union[np.ndarray, None]]: return self.retrieve() else: return False, None - - def set(self, propId: 'cv2.VideoCaptureProperties', value: any) -> bool: + + def set(self, propId: "cv2.VideoCaptureProperties", value: any) -> bool: """Sets a property in the VideoCapture. Parameters @@ -260,7 +263,7 @@ def set(self, propId: 'cv2.VideoCaptureProperties', value: any) -> bool: Property identifier from cv2.VideoCaptureProperties. value : int or float or bool Value of the property. - + Returns ------- retval : bool @@ -269,48 +272,48 @@ def set(self, propId: 'cv2.VideoCaptureProperties', value: any) -> bool: # Width setting if propId == cv2.CAP_PROP_FRAME_WIDTH: return self.set_pyspin_value("Width", value) - + # Height setting if propId == cv2.CAP_PROP_FRAME_HEIGHT: return self.set_pyspin_value("Height", value) - + # FrameRate setting if propId == cv2.CAP_PROP_FPS: is_success1 = self.set_pyspin_value("AcquisitionFrameRateEnable", True) is_success2 = self.set_pyspin_value("AcquisitionFrameRate", value) - return (is_success1 and is_success2) + return is_success1 and is_success2 # Brightness (EV) setting if propId == cv2.CAP_PROP_BRIGHTNESS: return self.set_pyspin_value("AutoExposureEVCompensation", value) - + # Gain setting if propId == cv2.CAP_PROP_GAIN: if value != -1: # Manual is_success1 = self.set_pyspin_value("GainAuto", "Off") is_success2 = self.set_pyspin_value("Gain", value) - return (is_success1 and is_success2) + return is_success1 and is_success2 else: # Auto return self.set_pyspin_value("GainAuto", "Continuous") - + # Exposure setting if propId == cv2.CAP_PROP_EXPOSURE: if value != -1: # Manual is_success1 = self.set_pyspin_value("ExposureAuto", "Off") is_success2 = self.set_pyspin_value("ExposureTime", value) - return (is_success1 and is_success2) + return is_success1 and is_success2 else: # Auto return self.set_pyspin_value("ExposureAuto", "Continuous") - + # Gamma setting if propId == cv2.CAP_PROP_GAMMA: is_success1 = self.set_pyspin_value("GammaEnable", True) is_success2 = self.set_pyspin_value("Gamma", value) - return (is_success1 and is_success2) + return is_success1 and is_success2 # Trigger Mode setting if propId == cv2.CAP_PROP_TRIGGER: @@ -324,7 +327,7 @@ def set(self, propId: 'cv2.VideoCaptureProperties', value: any) -> bool: # TriggerDelay setting if propId == cv2.CAP_PROP_TRIGGER_DELAY: return self.set_pyspin_value("TriggerDelay", value) - + # BackLigth setting if propId == cv2.CAP_PROP_BACKLIGHT: if type(value) is not bool: @@ -347,16 +350,16 @@ def set(self, propId: 'cv2.VideoCaptureProperties', value: any) -> bool: warn(f"propID={propId} is not supported") return False - - def get(self, propId: 'cv2.VideoCaptureProperties') -> any: + + def get(self, propId: "cv2.VideoCaptureProperties") -> any: """ Returns the specified VideoCapture property. - + Parameters ---------- propId_id : cv2.VideoCaptureProperties Property identifier from cv2.VideoCaptureProperties - + Returns ------- value : any @@ -365,37 +368,37 @@ def get(self, propId: 'cv2.VideoCaptureProperties') -> any: # Width if propId == cv2.CAP_PROP_FRAME_WIDTH: return self.get_pyspin_value("Width") - + # Height if propId == cv2.CAP_PROP_FRAME_HEIGHT: return self.get_pyspin_value("Height") - + # Frame Rate if propId == cv2.CAP_PROP_FPS: - # If this does not equal the AcquisitionFrameRate + # If this does not equal the AcquisitionFrameRate # it is because the ExposureTime is greater than the frame time. return self.get_pyspin_value("ResultingFrameRate") # Brightness if propId == cv2.CAP_PROP_BRIGHTNESS: return self.get_pyspin_value("AutoExposureEVCompensation") - + # Gain if propId == cv2.CAP_PROP_GAIN: return self.get_pyspin_value("Gain") - + # Exposure Time if propId == cv2.CAP_PROP_EXPOSURE: return self.get_pyspin_value("ExposureTime") - + # Gamma if propId == cv2.CAP_PROP_GAMMA: return self.get_pyspin_value("Gamma") - + # Temperature if propId == cv2.CAP_PROP_TEMPERATURE: return self.get_pyspin_value("DeviceTemperature") - + # Trigger Mode if propId == cv2.CAP_PROP_TRIGGER: trigger_mode = self.get_pyspin_value("TriggerMode") @@ -405,7 +408,7 @@ def get(self, propId: 'cv2.VideoCaptureProperties') -> any: return True else: return trigger_mode - + # Trigger Delay if propId == cv2.CAP_PROP_TRIGGER_DELAY: return self.get_pyspin_value("TriggerDelay") @@ -419,7 +422,7 @@ def get(self, propId: 'cv2.VideoCaptureProperties') -> any: return True else: return device_indicator_mode - + # Auto White Balance setting if propId == cv2.CAP_PROP_AUTO_WB: balance_white_auto = self.get_pyspin_value("BalanceWhiteAuto") @@ -446,9 +449,9 @@ def setExceptionMode(self, enable: bool) -> None: enable : bool """ if enable: - warnings.simplefilter('error', EasyPySpinWarning) + warnings.simplefilter("error", EasyPySpinWarning) else: - warnings.simplefilter('ignore', EasyPySpinWarning) + warnings.simplefilter("ignore", EasyPySpinWarning) def set_pyspin_value(self, node_name: str, value: any) -> bool: """Setting PySpin value with some useful checks. @@ -456,7 +459,7 @@ def set_pyspin_value(self, node_name: str, value: any) -> bool: This function adds functions that PySpin's ``SetValue`` does not support, such as **writable check**, **argument type check**, **value range check and auto-clipping**. If it fails, a warning will be raised. ``EasyPySpinWarning`` can control this warning. - + Parameters ---------- node_name : str @@ -485,7 +488,7 @@ def set_pyspin_value(self, node_name: str, value: any) -> bool: True Success case, and the value is clipped. - + >>> set_pyspin_value("ExposureTime", 0.1) EasyPySpinWarning: 'ExposureTime' value must be in the range of [20.0, 30000002.0], so 0.1 become 20.0 True @@ -510,43 +513,43 @@ def set_pyspin_value(self, node_name: str, value: any) -> bool: if not hasattr(self.cam, node_name): warn(f"'{type(self.cam).__name__}' object has no attribute '{node_name}'") return False - + # Get attribution node = getattr(self.cam, node_name) - + # Check 'node' object has attribute 'SetValue' if not hasattr(node, "SetValue"): warn(f"'{type(node).__name__}' object has no attribute 'SetValue'") return False - + # Check node is writable if not PySpin.IsWritable(node): warn(f"'{node_name}' is not writable") return False - + # Get type - node_type = type(node) + node_type = type(node) value_type = type(value) - - # Convert numpy array with one element + + # Convert numpy array with one element # into a standard Python scalar object if value_type is np.ndarray: if value.size == 1: value = value.item() value_type = type(value) - + # Check value type of Integer node case if node_type is PySpin.IInteger: if value_type is not int: warn(f"'value' must be 'int', not '{value_type.__name__}'") return False - + # Check value type of Float node case elif node_type is PySpin.IFloat: if value_type not in (int, float): warn(f"'value' must be 'int' or 'float', not '{value_type.__name__}'") return False - + # Check value type of Boolean node case elif node_type is PySpin.IBoolean: if value_type is not bool: @@ -556,7 +559,7 @@ def set_pyspin_value(self, node_name: str, value: any) -> bool: # Check value type of Enumeration node case elif isinstance(node, PySpin.IEnumeration): if value_type is str: - # If the type is ``str``, + # If the type is ``str``, # replace the corresponding PySpin's Enumeration if it exists. enumeration_name = f"{node_name}_{value}" if hasattr(PySpin, enumeration_name): @@ -566,16 +569,20 @@ def set_pyspin_value(self, node_name: str, value: any) -> bool: warn(f"'PySpin' object has no attribute '{enumeration_name}'") return False elif value_type is not int: - warn(f"'value' must be PySpin's Enumeration, not '{value_type.__name__}'") + warn( + f"'value' must be PySpin's Enumeration, not '{value_type.__name__}'" + ) return False - + # Clip the value when node type is Integer of Float if node_type in (PySpin.IInteger, PySpin.IFloat): v_min = node.GetMin() v_max = node.GetMax() value_clipped = min(max(value, v_min), v_max) if value_clipped != value: - warn(f"'{node_name}' value must be in the range of [{v_min}, {v_max}], so {value} become {value_clipped}") + warn( + f"'{node_name}' value must be in the range of [{v_min}, {v_max}], so {value} become {value_clipped}" + ) value = value_clipped # Finally, SetValue @@ -585,7 +592,7 @@ def set_pyspin_value(self, node_name: str, value: any) -> bool: msg_pyspin = str(e) warn(msg_pyspin) return False - + return True def get_pyspin_value(self, node_name: str) -> any: @@ -621,25 +628,25 @@ def get_pyspin_value(self, node_name: str) -> any: if not self.isOpened(): warn("Camera is not open") return False - + # Check 'CameraPtr' object has attribute 'node_name' if not hasattr(self.cam, node_name): warn(f"'{type(self.cam).__name__}' object has no attribute '{node_name}'") return None - + # Get attribution node = getattr(self.cam, node_name) - + # Check 'node_name' object has attribute 'GetValue' if not hasattr(node, "GetValue"): warn(f"'{type(node).__name__}' object has no attribute 'GetValue'") return None - + # Check node is readable if not PySpin.IsReadable(node): warn(f"'{node_name}' is not readable") return None - + # Finally, GetValue value = node.GetValue() diff --git a/EasyPySpin/videocaptureex.py b/EasyPySpin/videocaptureex.py index 95c0827..4aab203 100644 --- a/EasyPySpin/videocaptureex.py +++ b/EasyPySpin/videocaptureex.py @@ -7,11 +7,12 @@ from .videocapture import VideoCapture from .utils import warn + class VideoCaptureEX(VideoCapture): """Open a FLIR camera for video capturing. - VideoCaptureEX class is subclass of VideoCapture class. - It provides EXTENSIONS that OpenCV's VideoCapture does not support. + VideoCaptureEX class is subclass of VideoCapture class. + It provides EXTENSIONS that OpenCV's VideoCapture does not support. For example, Averaged image, Exposure bracketing, and HDR image. Attributes @@ -105,34 +106,45 @@ def read(self) -> Tuple[bool, np.ndarray]: Noemal case >>> cap.average_num = 1 >>> ret, frame = cap.read() - + Average of multiple images case - + >>> cap.average_num = 10 >>> ret, frame = cap.read() """ average_num = self.average_num if average_num == 1: - return super(VideoCaptureEX, self).read() + ret = super().grab() + + if not ret: + return False, None + + return super().retrieve() else: for i in range(average_num): - ret, image = super(VideoCaptureEX, self).read() + ret = super().grab() + if not ret: + return False, None + + ret, image = super().retrieve() if i == 0: rets = np.empty((average_num), dtype=np.bool) images = np.empty((*image.shape, average_num), dtype=image.dtype) - + rets[i] = ret images[..., i] = image - + if np.all(rets): image_averaged = np.mean(images, axis=-1).astype(image.dtype) return True, image_averaged else: return False, None - def readHDR(self, t_min: float, t_max: float, t_ref: float = 10000, ratio: float = 2.0) -> Tuple[bool, np.ndarray]: + def readHDR( + self, t_min: float, t_max: float, t_ref: float = 10000, ratio: float = 2.0 + ) -> Tuple[bool, np.ndarray]: """Capture multiple images with different exposure and merge into an HDR image. Parameters @@ -142,11 +154,11 @@ def readHDR(self, t_min: float, t_max: float, t_ref: float = 10000, ratio: float t_max : float Maximum exposure time [us] t_ref : float, default=10000 - Reference time [us]. + Reference time [us]. Determines the brightness of the merged image based on this time. ratio : int, default=2.0 Ratio of exposure time. - Number of shots is automatically determined from `t_min` and `t_max`. + Number of shots is automatically determined from `t_min` and `t_max`. It is set so that the `ratio` of neighboring exposure times. Returns @@ -161,16 +173,20 @@ def readHDR(self, t_min: float, t_max: float, t_ref: float = 10000, ratio: float A software trigger is used to capture images. In order to acquire an image reliably at the set exposure time. """ # Set between the maximum and minimum values of the camera - t_min = np.clip(t_min, self.cam.ExposureTime.GetMin(), self.cam.ExposureTime.GetMax()) - t_max = np.clip(t_max, self.cam.ExposureTime.GetMin(), self.cam.ExposureTime.GetMax()) - + t_min = np.clip( + t_min, self.cam.ExposureTime.GetMin(), self.cam.ExposureTime.GetMax() + ) + t_max = np.clip( + t_max, self.cam.ExposureTime.GetMin(), self.cam.ExposureTime.GetMax() + ) + # Determine nnumber of shots num = 2 if ratio > 1.0: - while t_max > t_min*(ratio**num): + while t_max > t_min * (ratio ** num): num += 1 - # Exposure time to be taken + # Exposure time to be taken # The equality sequence from minimum (t_min) to maximum (t_max) exposure time times = np.geomspace(t_min, t_max, num=num) @@ -179,7 +195,7 @@ def readHDR(self, t_min: float, t_max: float, t_ref: float = 10000, ratio: float # To capture a linear image, the gamma value is set to 1.0 self.set(cv2.CAP_PROP_GAMMA, 1.0) - + # Exposure bracketing ret, imlist = self.readExposureBracketing(times) @@ -188,24 +204,26 @@ def readHDR(self, t_min: float, t_max: float, t_ref: float = 10000, ratio: float if not ret: return False, None - + # Normalize to a value between 0 and 1 # By dividing by the maximum value dtype = imlist[0].dtype if dtype == np.uint8: - max_value = float(2**8-1) + max_value = 2.0 ** 8 - 1 elif dtype == np.uint16: - max_value = float(2**16-1) + max_value = 2.0 ** 16 - 1 else: max_value = 1.0 - imlist_norm = [ image/max_value for image in imlist] - + imlist_norm = [image / max_value for image in imlist] + # Merge HDR img_hdr = self.mergeHDR(imlist_norm, times, t_ref) return True, img_hdr.astype(np.float32) - def readExposureBracketing(self, exposures: np.ndarray) -> Tuple[bool, List[np.ndarray]]: + def readExposureBracketing( + self, exposures: np.ndarray + ) -> Tuple[bool, List[np.ndarray]]: """Execute exposure bracketing. Parameters @@ -221,10 +239,19 @@ def readExposureBracketing(self, exposures: np.ndarray) -> Tuple[bool, List[np.n Captured image list """ # Original settings for triggers, exposure, gain - node_names_to_change = ["TriggerSelector", "TriggerMode", "TriggerSource", "ExposureTime", "ExposureAuto", "GainAuto"] - values_origin = [self.get_pyspin_value(node_name) for node_name in node_names_to_change] + node_names_to_change = [ + "TriggerSelector", + "TriggerMode", + "TriggerSource", + "ExposureTime", + "ExposureAuto", + "GainAuto", + ] + values_origin = [ + self.get_pyspin_value(node_name) for node_name in node_names_to_change + ] auto_software_trigger_execute_origin = self.auto_software_trigger_execute - + # Change the trigger setting self.set_pyspin_value("TriggerSelector", "FrameStart") self.set_pyspin_value("TriggerMode", "On") @@ -235,7 +262,7 @@ def readExposureBracketing(self, exposures: np.ndarray) -> Tuple[bool, List[np.n gain = self.get_pyspin_value("Gain") self.set_pyspin_value("GainAuto", "Off") self.set_pyspin_value("Gain", gain) - + # Capture start imlist = [] for i, t in enumerate(exposures): @@ -261,9 +288,11 @@ def readExposureBracketing(self, exposures: np.ndarray) -> Tuple[bool, List[np.n self.auto_software_trigger_execute = auto_software_trigger_execute_origin return True, imlist - + @staticmethod - def mergeHDR(imlist: List[np.ndarray], times: np.ndarray, time_ref: float = 10000) -> np.ndarray: + def mergeHDR( + imlist: List[np.ndarray], times: np.ndarray, time_ref: float = 10000 + ) -> np.ndarray: """ Merge an HDR image from LDR images. @@ -275,7 +304,7 @@ def mergeHDR(imlist: List[np.ndarray], times: np.ndarray, time_ref: float = 1000 Exposure times time_ref : float, optional Reference time. Determines the brightness of the merged image based on this time. - + Returns ------- img_hdr : np.ndarray @@ -284,23 +313,25 @@ def mergeHDR(imlist: List[np.ndarray], times: np.ndarray, time_ref: float = 1000 Zmin = 0.01 Zmax = 0.99 epsilon = 1e-32 - - z = np.array(imlist) # (num, height, width) or (num, height, width, ch) - t = np.array(times) / time_ref # (num, ) - t = np.expand_dims(t, axis=tuple(range(1, z.ndim))) # (num, 1, 1) or (num, 1, 1, 1) + z = np.array(imlist) # (num, height, width) or (num, height, width, ch) + + t = np.array(times) / time_ref # (num, ) + t = np.expand_dims( + t, axis=tuple(range(1, z.ndim)) + ) # (num, 1, 1) or (num, 1, 1, 1) # Calculate gaussian weight - mask = np.bitwise_and(Zmin<=z, z<=Zmax) - w = np.exp(-4*((z-0.5)/0.5)**2) * mask + mask = np.bitwise_and(Zmin <= z, z <= Zmax) + w = np.exp(-4 * ((z - 0.5) / 0.5) ** 2) * mask # Merge HDR - img_hdr = np.average(z/t, axis=0, weights=w+epsilon) + img_hdr = np.average(z / t, axis=0, weights=w + epsilon) # Dealing with under-exposure and over-exposure - under_exposed = np.all(Zmin>z, axis=0) - over_exposed = np.all(z>Zmax, axis=0) - img_hdr[under_exposed] = Zmin/np.max(t) - img_hdr[over_exposed] = Zmax/np.min(t) + under_exposed = np.all(Zmin > z, axis=0) + over_exposed = np.all(z > Zmax, axis=0) + img_hdr[under_exposed] = Zmin / np.max(t) + img_hdr[over_exposed] = Zmax / np.min(t) - return img_hdr return img_hdr + return img_hdr From 3d6533b8856568149b7fb8d286212dba4c760b0b Mon Sep 17 00:00:00 2001 From: elerac Date: Sat, 7 Aug 2021 22:48:32 +0900 Subject: [PATCH 33/43] Use `__getattr__` insted of various method of `MultipleVideoCapture` --- EasyPySpin/multiplevideocapture.py | 55 ++++++++---------------------- 1 file changed, 15 insertions(+), 40 deletions(-) diff --git a/EasyPySpin/multiplevideocapture.py b/EasyPySpin/multiplevideocapture.py index 79ad540..72d6562 100644 --- a/EasyPySpin/multiplevideocapture.py +++ b/EasyPySpin/multiplevideocapture.py @@ -1,4 +1,4 @@ -from typing import List, Tuple, Union +from typing import List, Tuple, Union, Any from concurrent.futures import ThreadPoolExecutor import numpy as np @@ -6,6 +6,7 @@ from .videocapture import VideoCapture + class MultipleVideoCapture: """VideoCapture for Multiple cameras. @@ -31,13 +32,11 @@ class MultipleVideoCapture: VideoCaptureBase = VideoCapture __caps = [None] - + __executor = ThreadPoolExecutor() + def __init__(self, *indexes: Tuple[Union[int, str], ...]): self.open(*indexes) - def __del__(self): - return [cap.__del__() for cap in self] - def __len__(self): return self.__caps.__len__() @@ -54,43 +53,19 @@ def __setattr__(self, key, value): for cap in self: if hasattr(cap, key): setattr(cap, key, value) - + return object.__setattr__(self, key, value) + def __getattr__(self, name): + def method(*args, **kwargs) -> List[Any]: + futures = [ + self.__executor.submit(getattr(cap, name), *args, **kwargs) + for cap in self + ] + return [future.result() for future in futures] + + return method + def open(self, *indexs: Tuple[Union[int, str], ...]) -> List[bool]: self.__caps = [self.VideoCaptureBase(index) for index in indexs] return self.isOpened() - - def isOpened(self) -> List[bool]: - return [cap.isOpened() for cap in self] - - def grab(self) -> List[bool]: - return [cap.grab() for cap in self] - - def retrieve(self) -> List[Tuple[bool, Union[np.ndarray, None]]]: - return [cap.retrieve() for cap in self] - - def read(self) -> List[Tuple[bool, Union[np.ndarray, None]]]: - #return [cap.read() for cap in self] - executor = ThreadPoolExecutor() - futures = [executor.submit(cap.read) for cap in self] - executor.shutdown() - return [future.result() for future in futures] - - def release(self) -> List[None]: - return self.__del__() - - def set(self, propId: "cv2.VideoCaptureProperties", value: any) -> List[bool]: - return [cap.set(propId, value) for cap in self] - - def get(self, propId: "cv2.VideoCaptureProperties") -> List[any]: - return [cap.get(propId) for cap in self] - - def setExceptionMode(self, enable: bool) -> List[None]: - return [cap.setExceptionMode(enable) for cap in self] - - def set_pyspin_value(self, node_name: str, value: any) -> List[any]: - return [cap.set_pyspin_value(node_name, value) for cap in self] - - def get_pyspin_value(self, node_name: str) -> List[any]: - return [cap.get_pyspin_value(node_name) for cap in self] From 2e6f558e4a60516150292de7d069f5f6f211faf7 Mon Sep 17 00:00:00 2001 From: elerac Date: Sun, 8 Aug 2021 12:13:13 +0900 Subject: [PATCH 34/43] Fix open method of `MultipleVideoCapture` to add cameras --- EasyPySpin/multiplevideocapture.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/EasyPySpin/multiplevideocapture.py b/EasyPySpin/multiplevideocapture.py index 72d6562..e1078e6 100644 --- a/EasyPySpin/multiplevideocapture.py +++ b/EasyPySpin/multiplevideocapture.py @@ -4,7 +4,7 @@ import numpy as np import PySpin -from .videocapture import VideoCapture +from .videocapture import VideoCapture as EasyPySpinVideoCapture class MultipleVideoCapture: @@ -30,8 +30,7 @@ class MultipleVideoCapture: >>> cap.release() """ - VideoCaptureBase = VideoCapture - __caps = [None] + __caps = list() __executor = ThreadPoolExecutor() def __init__(self, *indexes: Tuple[Union[int, str], ...]): @@ -66,6 +65,11 @@ def method(*args, **kwargs) -> List[Any]: return method - def open(self, *indexs: Tuple[Union[int, str], ...]) -> List[bool]: - self.__caps = [self.VideoCaptureBase(index) for index in indexs] + def open( + self, *indexs: Tuple[Union[int, str], ...], VideoCapture=EasyPySpinVideoCapture + ) -> List[bool]: + for index in indexs: + cap = VideoCapture(index) + self.__caps.append(cap) + return self.isOpened() From 4be5b36db3e7698d3fe6cc3d6ac79f4a8768d0c1 Mon Sep 17 00:00:00 2001 From: elerac Date: Sun, 8 Aug 2021 12:14:02 +0900 Subject: [PATCH 35/43] Fix docstring of `MultipleVideoCapture` --- EasyPySpin/multiplevideocapture.py | 25 +++++++++++++++++++++---- 1 file changed, 21 insertions(+), 4 deletions(-) diff --git a/EasyPySpin/multiplevideocapture.py b/EasyPySpin/multiplevideocapture.py index e1078e6..64344a2 100644 --- a/EasyPySpin/multiplevideocapture.py +++ b/EasyPySpin/multiplevideocapture.py @@ -8,13 +8,10 @@ class MultipleVideoCapture: - """VideoCapture for Multiple cameras. + """VideoCapture for multiple cameras. Examples -------- - >>> cap = MultipleVideoCapture(0) - >>> cap.isOpened() - [True] >>> cap = MultipleVideoCapture(0, 1) >>> cap.isOpened() [True, True] @@ -28,6 +25,26 @@ class MultipleVideoCapture: [2000.0, 1000.0] >>> (ret0, frame0), (ret1, frame1) = cap.read() >>> cap.release() + + Add camera after initialization + + >>> cap = MultipleVideoCapture(0, 1) # open two cameras + >>> cap.isOpened() + [True, True] + >>> cap.open(2) # add a camera + >>> cap.isOpened() + [True, True, True] + + Open camera as arbitrary VideoCapture + + >>> cap = MultipleVideoCapture() + >>> cap.open(0, 1, VideoCapture=EasyPySpin.VideoCaptureEX) + >>> cap.isOpened() + [True, True] + >>> cap.average_num = 5 # Set attribute of VideoCaptureEX + >>> cap.open(0, VideoCapture=cv2.VideoCapture) + >>> cap.isOpened() + [True, True, True] """ __caps = list() From 19be6555e85ee2e4d53e4d909f565f58339feb02 Mon Sep 17 00:00:00 2001 From: elerac Date: Sun, 8 Aug 2021 12:15:27 +0900 Subject: [PATCH 36/43] Add configure methods for synchronized capture --- EasyPySpin/videocapture.py | 82 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 82 insertions(+) diff --git a/EasyPySpin/videocapture.py b/EasyPySpin/videocapture.py index 4f728da..e95561a 100644 --- a/EasyPySpin/videocapture.py +++ b/EasyPySpin/videocapture.py @@ -651,3 +651,85 @@ def get_pyspin_value(self, node_name: str) -> any: value = node.GetValue() return value + + def _get_camera_series_name(self) -> str: + """Get camera series name""" + model_name = self.get_pyspin_value("DeviceModelName") + + series_names = ["BFS", "BFLY", "CM3", "FL3", "GS3", "ORX", "FFY-DL"] + for name in series_names: + if name in model_name: + return name + + def _configure_as_primary(self): + """Configure as primary camera for synchronized capture + + Notes + ----- + https://www.flir.com/support-center/iis/machine-vision/application-note/configuring-synchronized-capture-with-multiple-cameras/ + + 4. Set the output line + 1. For CM3, FL3, GS3, FFY-DL, and ORX cameras, select Line2 from the Line Selection dropdown and set Line Mode to Output. + 2. For BFS cameras, select Line1 from the Line Selection dropdown and set Line Mode to Output. + 5. For BFS and BFLY cameras enable the 3.3V line + 1. For BFS cameras from the line selection drop-down select Line2 and check the checkbox for 3.3V Enable. + 2. For BFLY cameras, set 3.3V Enable to true + """ + series_name = self._get_camera_series_name() + + # Set the output line + if series_name in ["CM3", "FL3", "GS3", "FFY-DL", "ORX"]: + # For CM3, FL3, GS3, FFY-DL, and ORX cameras, + # select Line2 from the Line Selection dropdown and set Line Mode to Output. + self.set_pyspin_value("LineSelector", "Line2") + self.set_pyspin_value("LineMode", "Output") + elif series_name in ["BFS"]: + # For BFS cameras, select Line1 from the Line Selection dropdown + # and set Line Mode to Output. + self.set_pyspin_value("LineSelector", "Line1") + self.set_pyspin_value("LineMode", "Output") + + # For BFS and BFLY cameras enable the 3.3V line + if series_name in ["BFS"]: + # For BFS cameras from the line selection drop-down select Line2 + # and check the checkbox for 3.3V Enable. + self.set_pyspin_value("LineSelector", "Line2") + self.set_pyspin_value("V3_3Enable", True) + elif series_name in ["BFLY"]: + # For BFLY cameras, set 3.3V Enable to true + self.set_pyspin_value("V3_3Enable", True) + + def _configure_as_secondary(self): + """Configure as secondary camera for synchronized capture + + Notes + ----- + https://www.flir.com/support-center/iis/machine-vision/application-note/configuring-synchronized-capture-with-multiple-cameras/ + + 2. Select the GPIO tab. + 1. Set the trigger source + 2. For BFS, CM3, FL3, FFY-DL, and GS3 cameras, from the Trigger Source drop-down, select Line 3. + 3. For ORX cameras, from the Trigger Source drop-down, select Line 5. + 4. For BFLY cameras, from the Trigger Source drop-down, select Line 0 + 3. From the Trigger Overlap drop-down, select Read Out. + 4. From the Trigger Mode drop-down, select On. + """ + series_name = self._get_camera_series_name() + + self.set_pyspin_value("TriggerMode", "Off") + self.set_pyspin_value("TriggerSelector", "FrameStart") + + # Set the trigger source + if series_name in ["BFS", "CM3", "FL3", "FFY-DL", "GS3"]: + # For BFS, CM3, FL3, FFY-DL, and GS3 cameras, + # from the Trigger Source drop-down, select Line 3. + self.set_pyspin_value("TriggerSource", "Line3") + elif series_name in ["ORX"]: + # For ORX cameras, from the Trigger Source drop-down, select Line 5. + self.set_pyspin_value("TriggerSource", "Line5") + + # From the Trigger Overlap drop-down, select Read Out. + self.set_pyspin_value("TriggerOverlap", "ReadOut") + + # From the Trigger Mode drop-down, select On. + self.set_pyspin_value("TriggerMode", "On") From adc42e40e6165231b6f325a6415d5ecc2d9e5a22 Mon Sep 17 00:00:00 2001 From: elerac Date: Sun, 8 Aug 2021 12:16:49 +0900 Subject: [PATCH 37/43] New `SynchronizedVideoCapture` class --- EasyPySpin/synchronizedvideocapture.py | 174 +++++++++---------------- 1 file changed, 62 insertions(+), 112 deletions(-) diff --git a/EasyPySpin/synchronizedvideocapture.py b/EasyPySpin/synchronizedvideocapture.py index ff3fdd0..62bd4af 100644 --- a/EasyPySpin/synchronizedvideocapture.py +++ b/EasyPySpin/synchronizedvideocapture.py @@ -1,118 +1,68 @@ -import PySpin +from typing import Tuple, Union -class SynchronizedVideoCapture: - """ - Hardware synchronized video capturing. - It can be handled in the same way as the "VideoCapture" class and the return value is stored in the list. - - You can find instructions on how to connect the camera in FLIR official page. - [https://www.flir.com/support-center/iis/machine-vision/application-note/configuring-synchronized-capture-with-multiple-cameras] - - NOTE : Currently, only two cameras (primary and secondary) are supported, but I would like to support multiple secondary cameras in the future. - NOTE : I only have the "BFS" camera, so I haven't tested it with any other camera ("BFLY", "CM3", etc...). So, if you have a problem, please send me an issue or PR. - """ - def __init__(self, cap_primary, cap_secondary): - self.cap_primary = cap_primary - self.cap_secondary = cap_secondary - - self.cap_primary = self._configure_as_primary(self.cap_primary) - self.cap_secondary = self._configure_as_secondary(self.cap_secondary) - - self.cap_primary.auto_software_trigger_execute = True - - def __del__(self): - self.cap_primary.release() - self.cap_secondary.release() - - def release(self): - self.__del__() - - def isOpened(self): - return [self.cap_primary.isOpened(), self.cap_secondary.isOpened()] - - def read(self): - if not self.cap_primary.cam.IsStreaming(): - self.cap_primary.cam.BeginAcquisition() - - if not self.cap_secondary.cam.IsStreaming(): - self.cap_secondary.cam.BeginAcquisition() - - if (self.cap_primary.cam.TriggerMode.GetValue()==PySpin.TriggerMode_On and - self.cap_primary.cam.TriggerSource.GetValue()==PySpin.TriggerSource_Software and - self.cap_primary.auto_software_trigger_execute==True): - self.cap_primary.cam.TriggerSoftware.Execute() +from .multiplevideocapture import MultipleVideoCapture - ret_p, frame_p = self.cap_primary.read() - ret_s, frame_s = self.cap_secondary.read() - ret = [ret_p, ret_s] - frame = [frame_p, frame_s] - return ret, frame - def set(self, propId, value): - ret_p = self.cap_primary.set(propId, value) - ret_s = self.cap_secondary.set(propId, value) - return [ret_p, ret_s] +class SynchronizedVideoCapture(MultipleVideoCapture): + """VideoCapture for hardware synchronized cameras. - def get(self, propId): - value_p = self.cap_primary.get(propId) - value_s = self.cap_secondary.get(propId) - return [value_p, value_s] + I only have the "BFS" camera, so I haven't tested it with any other camera ("BFLY", "CM3", etc...). So, if you have a problem, please send me an issue or PR. - def _configure_as_primary(self, cap): - series_name = self._which_camera_series(cap) - - # Set the output line - if series_name in ["CM3", "FL3", "GS3", "FFY-DL", "ORX"]: - # For CM3, FL3, GS3, FFY-DL, and ORX cameras, - # select Line2 from the Line Selection dropdown and set Line Mode to Output. - cap.cam.LineSelector.SetValue(PySpin.LineSelector_Line2) - cap.cam.LineMode.SetValue(PySpin.LineMode_Output) - elif series_name in ["BFS"]: - # For BFS cameras, select Line1 from the Line Selection dropdown - # and set Line Mode to Output. - cap.cam.LineSelector.SetValue(PySpin.LineSelector_Line1) - cap.cam.LineMode.SetValue(PySpin.LineMode_Output) - - # For BFS and BFLY cameras enable the 3.3V line - if series_name in ["BFS"]: - # For BFS cameras from the line selection drop-down select Line2 - # and check the checkbox for 3.3V Enable. - cap.cam.LineSelector.SetValue(PySpin.LineSelector_Line2) - cap.cam.V3_3Enable.SetValue(True) - elif series_name in ["BFLY"]: - # For BFLY cameras, set 3.3V Enable to true - cap.cam.V3_3Enable.SetValue(True) - - return cap - - def _configure_as_secondary(self, cap): - series_name = self._which_camera_series(cap) - - cap.cam.TriggerMode.SetValue(PySpin.TriggerMode_Off) - cap.cam.TriggerSelector.SetValue(PySpin.TriggerSelector_FrameStart) - - # Set the trigger source - if series_name in ["BFS", "CM3", "FL3", "FFY-DL", "GS3"]: - # For BFS, CM3, FL3, FFY-DL, and GS3 cameras, - # from the Trigger Source drop-down, select Line 3. - cap.cam.TriggerSource.SetValue(PySpin.TriggerSource_Line3) - elif series_name in ["ORX"]: - # For ORX cameras, from the Trigger Source drop-down, select Line 5. - cap.cam.TriggerSource.SetValue(PySpin.TriggerSource_Line5) - - # From the Trigger Overlap drop-down, select Read Out. - cap.cam.TriggerOverlap.SetValue(PySpin.TriggerOverlap_ReadOut) - - # From the Trigger Mode drop-down, select On. - cap.cam.TriggerMode.SetValue(PySpin.TriggerMode_On) - - return cap - - def _which_camera_series(self, cap): - model_name = cap.cam.DeviceModelName.GetValue() + Notes + ----- + You can find instructions on how to connect the camera in FLIR official page. + https://www.flir.com/support-center/iis/machine-vision/application-note/configuring-synchronized-capture-with-multiple-cameras + + Examples + -------- + Case1: The pair of primary and secondary cameras. + + >>> serial_number_1 = "20541712" # primary camera + >>> serial_number_2 = "19412150" # secondary camera + >>> cap = EasyPySpin.SynchronizedVideoCapture(serial_number_1, serial_number_2) + >>> cap.isOpened() + [True, True] + >>> cap.set(cv2.CAP_PROP_EXPOSURE, 1000) + [True, True] + >>> cap.get(cv2.CAP_PROP_EXPOSURE) + [1000.0, 1000.0] + >>> cap[0].set(cv2.CAP_PROP_EXPOSURE, 2000) + True + >>> cap.get(cv2.CAP_PROP_EXPOSURE) + [2000.0, 1000.0] + >>> (ret0, frame0), (ret1, frame1) = cap.read() + + Case2: The secondary camera and external trigger. + + >>> serial_number = "19412150" # secondary camera + >>> cap = EasyPySpin.SynchronizedVideoCapture(None, serial_number_2) + + Case3: The two (or more) secondary cameras and external trigger. + + >>> serial_number_1 = "20541712" # secondary camera 1 + >>> serial_number_2 = "19412150" # secondary camera 2 + >>> cap = EasyPySpin.SynchronizedVideoCapture(None, serial_number_1, serial_number_2) + """ - series_names = ["BFS", "BFLY", "CM3", "FL3", "GS3", "ORX", "FFY-DL"] - for name in series_names: - if name in model_name: - return name - return None + def __init__( + self, + index_primary: Union[int, str], + *indexes_secondary: Tuple[Union[int, str], ...] + ): + if index_primary is not None: + self.open_as_primary(index_primary) + + for index_secondary in indexes_secondary: + self.open_as_secondary(index_secondary) + + def open_as_primary(self, index: Union[int, str]) -> bool: + self.open(index) + cap = self[-1] + cap._configure_as_primary() + return cap.isOpened() + + def open_as_secondary(self, index: Union[int, str]) -> bool: + self.open(index) + cap = self[-1] + cap._configure_as_secondary() + return cap.isOpened() From ff5a6b2106e9da6ee6ed68053ceafeb407802d44 Mon Sep 17 00:00:00 2001 From: elerac Date: Sun, 8 Aug 2021 12:17:11 +0900 Subject: [PATCH 38/43] Minor fix --- EasyPySpin/__init__.py | 2 +- EasyPySpin/multiplevideocapture.py | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/EasyPySpin/__init__.py b/EasyPySpin/__init__.py index 8c1a1c8..00244ba 100644 --- a/EasyPySpin/__init__.py +++ b/EasyPySpin/__init__.py @@ -1,5 +1,5 @@ from .videocapture import VideoCapture -from .synchronizedvideocapture import SynchronizedVideoCapture from .videocaptureex import VideoCaptureEX from .multiplevideocapture import MultipleVideoCapture +from .synchronizedvideocapture import SynchronizedVideoCapture from .utils import EasyPySpinWarning diff --git a/EasyPySpin/multiplevideocapture.py b/EasyPySpin/multiplevideocapture.py index 64344a2..72d96b7 100644 --- a/EasyPySpin/multiplevideocapture.py +++ b/EasyPySpin/multiplevideocapture.py @@ -2,7 +2,6 @@ from concurrent.futures import ThreadPoolExecutor import numpy as np -import PySpin from .videocapture import VideoCapture as EasyPySpinVideoCapture From 3417ac8c1ad23c5d974b73373dae98dc13c7e531 Mon Sep 17 00:00:00 2001 From: elerac Date: Sun, 8 Aug 2021 13:45:19 +0900 Subject: [PATCH 39/43] Fix --- EasyPySpin/videocaptureex.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/EasyPySpin/videocaptureex.py b/EasyPySpin/videocaptureex.py index 4aab203..b7c005e 100644 --- a/EasyPySpin/videocaptureex.py +++ b/EasyPySpin/videocaptureex.py @@ -271,7 +271,7 @@ def readExposureBracketing( # Dummy image if i == 0: for _ in range(3): - self.grab() + self.read() ret, frame = self.read() From fee6c23252636572c42f9309e94a5ec1327aefe5 Mon Sep 17 00:00:00 2001 From: elerac Date: Sun, 8 Aug 2021 13:55:09 +0900 Subject: [PATCH 40/43] Update example codes --- examples/capture_average.py | 23 ++++++++-------- examples/capture_hdr.py | 34 +++++++++++++++-------- examples/multiple.py | 49 +++++++++++++++++++-------------- examples/synchronized.py | 55 +++++++++++++++++-------------------- examples/video.py | 25 ++++++++--------- 5 files changed, 99 insertions(+), 87 deletions(-) diff --git a/examples/capture_average.py b/examples/capture_average.py index 20bc0f7..6ecb57e 100644 --- a/examples/capture_average.py +++ b/examples/capture_average.py @@ -1,39 +1,40 @@ """ Example of capturing the average image width VideoCaptureEX class. -Noise can be reduced by capturing multiple images and computing the average of each pixel. +The averaged image can reduce random noise. """ import EasyPySpin import cv2 + def main(): cap = EasyPySpin.VideoCaptureEX(0) - + cap.average_num = 3 + print("Press key to change average number") print("k : average_num += 1") print("j : average_num -= 1") print("--------------------") print("average num: ", cap.average_num) - + while True: ret, frame = cap.read() img_show = cv2.resize(frame, None, fx=0.25, fy=0.25) cv2.imshow("press q to quit", img_show) - + key = cv2.waitKey(30) - if key==ord("q"): + if key == ord("q"): break - elif key==ord("k"): + elif key == ord("k"): cap.average_num += 1 print("average num: ", cap.average_num) - elif key==ord("j"): + elif key == ord("j"): cap.average_num -= 1 - if cap.average_num<1: - cap.average_num = 1 print("average num: ", cap.average_num) - + cv2.destroyAllWindows() cap.release() -if __name__=="__main__": + +if __name__ == "__main__": main() diff --git a/examples/capture_hdr.py b/examples/capture_hdr.py index d99318c..ca360c1 100644 --- a/examples/capture_hdr.py +++ b/examples/capture_hdr.py @@ -1,31 +1,41 @@ -""" -Example of capturing the HDR image width VideoCaptureEX class +"""Example of capturing the HDR image width VideoCaptureEX class """ import EasyPySpin import cv2 import numpy as np import argparse + def main(): parser = argparse.ArgumentParser() parser.add_argument("-i", "--index", type=int, default=0, help="Camera index (Default: 0)") parser.add_argument("-g", "--gain", type=float, default=0, help="Gain [dB] (Default: 0)") parser.add_argument("--min", type=float, default=5000, help="Minimum exposure time [us]") parser.add_argument("--max", type=float, default=500000, help="Maximum exposure time [us]") - parser.add_argument("--num", type=int, default=8, help="Number of images to capture") - parser.add_argument("-o", "--output", type=str, default="capture_hdr.exr", help="Output file name (*.exr)") + parser.add_argument("-o", "--output", type=str, default="hdr", help="Output file name") args = parser.parse_args() cap = EasyPySpin.VideoCaptureEX(args.index) - - cap.set(cv2.CAP_PROP_GAMMA, 1.0) cap.set(cv2.CAP_PROP_GAIN, args.gain) - + print("Start capturing HDR image") - ret, img_hdr = cap.readHDR(args.min, args.max, args.num) - - print("Write {}".format(args.output)) - cv2.imwrite(args.output, img_hdr.astype(np.float32)) + ret, img_hdr = cap.readHDR(args.min, args.max) + + filename_exr = f"{args.output}.exr" + print(f"Write {filename_exr}") + cv2.imwrite(filename_exr, img_hdr.astype(np.float32)) + + for ev in [-2, -1, 0, 1, 2]: + sign = "+" if ev >= 0 else "-" + filename_png = f"{args.output}_{sign}{abs(ev)}EV.png" + ratio = 2.0 ** ev + img_hdr_u8 = np.clip(img_hdr * ratio * 255, 0, 255).astype(np.uint8) + + print(f"Write {filename_png}") + cv2.imwrite(filename_png, img_hdr_u8) + + cap.release() + -if __name__=="__main__": +if __name__ == "__main__": main() diff --git a/examples/multiple.py b/examples/multiple.py index 085b9f8..0ea7a4c 100644 --- a/examples/multiple.py +++ b/examples/multiple.py @@ -1,26 +1,35 @@ +"""Example of capture with multiple camera. +""" import EasyPySpin import cv2 -NUM_IMAGES = 10 def main(): - cap0 = EasyPySpin.VideoCapture(0) - cap1 = EasyPySpin.VideoCapture(1) - - for n in range(NUM_IMAGES): - ret0, frame0 = cap0.read() - ret1, frame1 = cap1.read() - - filename0 = "multiple-{0}-{1}.png".format(n, 0) - filename1 = "multiple-{0}-{1}.png".format(n, 1) - cv2.imwrite(filename0, frame0) - cv2.imwrite(filename1, frame1) - print("Image saved at {}".format(filename0)) - print("Image saved at {}".format(filename1)) - print() - - cap0.release() - cap1.release() - -if __name__=="__main__": + # cap = EasyPySpin.MultipleVideoCapture(0) + cap = EasyPySpin.MultipleVideoCapture(0, 1) + # cap = EasyPySpin.MultipleVideoCapture(0, 1, 2) + + if not all(cap.isOpened()): + print("All cameras can't open\nexit") + return -1 + + while True: + read_values = cap.read() + + for i, (ret, frame) in enumerate(read_values): + if not ret: + continue + + frame = cv2.resize(frame, None, fx=0.25, fy=0.25) + cv2.imshow(f"frame-{i}", frame) + + key = cv2.waitKey(30) + if key == ord("q"): + break + + cv2.destroyAllWindows() + cap.release() + + +if __name__ == "__main__": main() diff --git a/examples/synchronized.py b/examples/synchronized.py index 36bb445..4c7f894 100644 --- a/examples/synchronized.py +++ b/examples/synchronized.py @@ -1,43 +1,38 @@ +"""Example of synchronized capture with multiple cameras. + +You need to create a physical connection between the cameras by linking their GPIO pins, as follows: +https://www.flir.com/support-center/iis/machine-vision/application-note/configuring-synchronized-capture-with-multiple-cameras/ +""" import EasyPySpin import cv2 -SCALE = 0.5 def main(): - cap_primary = EasyPySpin.VideoCapture(0) - cap_secondary = EasyPySpin.VideoCapture(1) - - cap_primary.set(cv2.CAP_PROP_TRIGGER, True) #TriggerMode -> On - #import PySpin - #cap_primary.cam.TriggerSource.SetValue(PySpin.TriggerSource_Software) + serial_number_1 = "20541712" # primary camera (set your camera's serial number) + serial_number_2 = "19412150" # secondary camera (set your camera's serial number) + cap = EasyPySpin.SynchronizedVideoCapture(serial_number_1, serial_number_2) - cap_sync = EasyPySpin.SynchronizedVideoCapture(cap_primary, cap_secondary) + if not all(cap.isOpened()): + print("All cameras can't open\nexit") + return -1 while True: - ret, frame = cap_sync.read() - frame_primary = frame[0] - frame_secondary = frame[1] - - img_show_primary = cv2.resize(frame_primary, None, fx=SCALE, fy=SCALE) - img_show_secondary = cv2.resize(frame_secondary, None, fx=SCALE, fy=SCALE) - cv2.imshow("primary", img_show_primary) - cv2.imshow("secondary", img_show_secondary) - key = cv2.waitKey(1) - if key==ord("q"): + read_values = cap.read() + + for i, (ret, frame) in enumerate(read_values): + if not ret: + continue + + frame = cv2.resize(frame, None, fx=0.25, fy=0.25) + cv2.imshow(f"frame-{i}", frame) + + key = cv2.waitKey(30) + if key == ord("q"): break - elif key==ord("c"): - import datetime - time_stamp = datetime.datetime.now().strftime("%Y%m%d%H%M%S") - filename0 = "synchronized-{0}-{1}.png".format(time_stamp, 0) - filename1 = "synchronized-{0}-{1}.png".format(time_stamp, 1) - cv2.imwrite(filename0, frame_primary) - cv2.imwrite(filename1, frame_secondary) - print("Image saved at {}".format(filename0)) - print("Image saved at {}".format(filename1)) - print() cv2.destroyAllWindows() - cap_sync.release() + cap.release() + -if __name__=="__main__": +if __name__ == "__main__": main() diff --git a/examples/video.py b/examples/video.py index ec2ab27..4da6c87 100644 --- a/examples/video.py +++ b/examples/video.py @@ -1,35 +1,32 @@ -""" -A simple example of capturing and displaying an image +"""A simple example of capturing and displaying an image """ import EasyPySpin import cv2 + def main(): - # Instance creation cap = EasyPySpin.VideoCapture(0) - # Checking if it's connected to the camera if not cap.isOpened(): print("Camera can't open\nexit") return -1 - - # Set the camera parameters - cap.set(cv2.CAP_PROP_EXPOSURE, -1) #-1 sets exposure_time to auto - cap.set(cv2.CAP_PROP_GAIN, -1) #-1 sets gain to auto - # Start capturing + cap.set(cv2.CAP_PROP_EXPOSURE, -1) # -1 sets exposure_time to auto + cap.set(cv2.CAP_PROP_GAIN, -1) # -1 sets gain to auto + while True: ret, frame = cap.read() - #frame = cv2.cvtColor(frame, cv2.COLOR_BayerBG2BGR) #for RGB camera demosaicing + # frame = cv2.cvtColor(frame, cv2.COLOR_BayerBG2BGR) # for RGB camera demosaicing img_show = cv2.resize(frame, None, fx=0.25, fy=0.25) cv2.imshow("press q to quit", img_show) key = cv2.waitKey(30) - if key==ord("q"): + if key == ord("q"): break - - cv2.destroyAllWindows() + cap.release() + cv2.destroyAllWindows() + -if __name__=="__main__": +if __name__ == "__main__": main() From 3f5e85c79b4cae8f616ff3ef64da1ddf4f901064 Mon Sep 17 00:00:00 2001 From: elerac Date: Sun, 8 Aug 2021 13:55:42 +0900 Subject: [PATCH 41/43] Clean up `command_line.py` --- EasyPySpin/command_line.py | 44 ++++++++++++++++++++++---------------- 1 file changed, 25 insertions(+), 19 deletions(-) diff --git a/EasyPySpin/command_line.py b/EasyPySpin/command_line.py index 3c24cc7..ae0420c 100644 --- a/EasyPySpin/command_line.py +++ b/EasyPySpin/command_line.py @@ -5,23 +5,25 @@ import cv2 import argparse + def print_xy(event, x, y, flags, param): """ Export xy coordinates in csv format by clicking """ - if event==cv2.EVENT_LBUTTONDOWN: + if event == cv2.EVENT_LBUTTONDOWN: scale = param print(f"{int(x/scale)}, {int(y/scale)}") + def main(): parser = argparse.ArgumentParser() - parser.add_argument("-i", "--index", type=int, default=0, help="Camera index (Default: 0)") - parser.add_argument("-e", "--exposure",type=float, default=-1, help="Exposure time [us] (Default: Auto)") - parser.add_argument("-g", "--gain", type=float, default=-1, help="Gain [dB] (Default: Auto)") - parser.add_argument("-G", "--gamma", type=float, help="Gamma value") - parser.add_argument("-b", "--brightness", type=float, help="Brightness [EV]") - parser.add_argument("-f", "--fps", type=float, help="FrameRate [fps]") - parser.add_argument("-s", "--scale", type=float, default=0.25, help="Image scale to show (>0) (Default: 0.25)") + parser.add_argument("-i", "--index", type=int, default=0, help="Camera index (Default: 0)") + parser.add_argument("-e", "--exposure", type=float, default=-1, help="Exposure time [us] (Default: Auto)") + parser.add_argument("-g", "--gain", type=float, default=-1, help="Gain [dB] (Default: Auto)") + parser.add_argument("-G", "--gamma", type=float, help="Gamma value") + parser.add_argument("-b", "--brightness", type=float, help="Brightness [EV]") + parser.add_argument("-f", "--fps", type=float, help="FrameRate [fps]") + parser.add_argument("-s", "--scale", type=float, default=.25, help="Image scale to show (>0) (Default: 0.25)") args = parser.parse_args() # Instance creation @@ -31,14 +33,17 @@ def main(): if not cap.isOpened(): print("Camera can't open\nexit") return -1 - + # Set the camera parameters - cap.set(cv2.CAP_PROP_EXPOSURE, args.exposure) #-1 sets exposure_time to auto - cap.set(cv2.CAP_PROP_GAIN, args.gain) #-1 sets gain to auto - if args.gamma is not None: cap.set(cv2.CAP_PROP_GAMMA, args.gamma) - if args.fps is not None: cap.set(cv2.CAP_PROP_FPS, args.fps) - if args.brightness is not None: cap.set(cv2.CAP_PROP_BRIGHTNESS, args.brightness) - + cap.set(cv2.CAP_PROP_EXPOSURE, args.exposure) # -1 sets exposure_time to auto + cap.set(cv2.CAP_PROP_GAIN, args.gain) # -1 sets gain to auto + if args.gamma is not None: + cap.set(cv2.CAP_PROP_GAMMA, args.gamma) + if args.fps is not None: + cap.set(cv2.CAP_PROP_FPS, args.fps) + if args.brightness is not None: + cap.set(cv2.CAP_PROP_BRIGHTNESS, args.brightness) + # Window setting winname = "press q to quit" cv2.namedWindow(winname) @@ -48,16 +53,17 @@ def main(): # Start capturing while True: ret, frame = cap.read() - #frame = cv2.cvtColor(frame, cv2.COLOR_BayerBG2BGR) #for RGB camera demosaicing + # frame = cv2.cvtColor(frame, cv2.COLOR_BayerBG2BGR) #for RGB camera demosaicing img_show = cv2.resize(frame, None, fx=args.scale, fy=args.scale) cv2.imshow(winname, img_show) key = cv2.waitKey(30) - if key==ord("q"): + if key == ord("q"): break - + cv2.destroyAllWindows() cap.release() -if __name__=="__main__": + +if __name__ == "__main__": main() From f481c3fa6af90a9a0120a08b94e1afb6dfabde65 Mon Sep 17 00:00:00 2001 From: elerac Date: Sun, 8 Aug 2021 13:55:47 +0900 Subject: [PATCH 42/43] Update README.md --- README.md | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index e544d0b..9e49c6c 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ EasyPySpin is an unofficial wrapper for FLIR [Spinnaker SDK](https://www.flir.co ```sh pip install git+https://github.com/elerac/EasyPySpin ``` -After installation, connect the camera and try `examples/video.py`. +After installation, connect the camera and try [examples/video.py](examples/video.py). ## Usage ### Capture image from camera @@ -58,20 +58,20 @@ cap.get_pyspin_value("DeviceModelName") Here is the list of supported VideoCaptureProperties. In `set(propId, value)` and `get(propId)`, PySpin is used to set and get the camera's settings. The relationship between `propId` and PySpin settings is designed to be as close in meaning as possible. The table below shows the relationship between `propId` and PySpin settings in pseudo-code format. -| propId | type | set(propId, value) | value = get(propId) | -| ---- | ---- | ---- | ---- | -| `cv2.CAP_PROP_FRAME_WIDTH` | int | `Width` = value | value = `Width` | -| `cv2.CAP_PROP_FRAME_HEIGHT` | int | `Height` = value | value = `Height` | -| `cv2.CAP_PROP_FPS` | float | `AcquisitionFrameRateEnable` = `True`
`AcquisitionFrameRate` = value | value = `ResultingFrameRate`| -| `cv2.CAP_PROP_BRIGHTNESS` | float | `AutoExposureEVCompensation` = value | value = `AutoExposureEVCompensation` | -| `cv2.CAP_PROP_GAIN` | float | if value != -1
  `GainAuto` = `Off`
  `Gain` = value
else
  `GainAuto` = `Continuous` | value = `Gain` | -| `cv2.CAP_PROP_EXPOSURE` | float | if value != -1
  `ExposureAuto` = `Off`
  `ExposureTime` = value
else
  `ExposureAuto` = `Continuous` | value = `ExposureTime` | -| `cv2.CAP_PROP_GAMMA` | float | `GammaEnable` = `True`
`Gamma` = value | value = `Gamma` | -| `cv2.CAP_PROP_TEMPERATURE` | float | | value = `DeviceTemperature` | -| `cv2.CAP_PROP_TRIGGER` | bool | if value == `True`
  `TriggerMode` = `On`
else
  `TriggerMode` = `Off` | if trigger_mode == `On`
  value = `True`
elif trigger_mode == `Off`
  value = `False` | -| `cv2.CAP_PROP_TRIGGER_DELAY` | float | `TriggerDelay` = value | value = `TriggerDelay` | -| `cv2.CAP_PROP_BACKLIGHT` | bool | if value == `True`
  `DeviceIndicatorMode` = `Active`
else
  `DeviceIndicatorMode` = `Inactive` | if device_indicator_mode == `Active`
  value = `True`
elif device_indicator_mode == `Inactive`
  value = `False` | -| `cv2.CAP_PROP_AUTO_WB` | bool | if value == `True`
  `BalanceWhiteAuto` = `Continuous`
else
  `BalanceWhiteAuto` = `Off` | if balance_white_auto == `Continuous`
  value = `True`
elif balance_white_auto == `Off`
  value = `False` | +| propId | type | set(propId, value) | value = get(propId) | +| ---- | ---- | ---- | ---- | +| cv2.CAP_PROP_FRAME_WIDTH | int | `Width` = value | value = `Width` | +| cv2.CAP_PROP_FRAME_HEIGHT | int | `Height` = value | value = `Height` | +| cv2.CAP_PROP_FPS | float | `AcquisitionFrameRateEnable` = `True`
`AcquisitionFrameRate` = value | value = `ResultingFrameRate`| +| cv2.CAP_PROP_BRIGHTNESS | float | `AutoExposureEVCompensation` = value | value = `AutoExposureEVCompensation` | +| cv2.CAP_PROP_GAIN | float | if value != -1
  `GainAuto` = `Off`
  `Gain` = value
else
  `GainAuto` = `Continuous` | value = `Gain` | +| cv2.CAP_PROP_EXPOSURE | float | if value != -1
  `ExposureAuto` = `Off`
  `ExposureTime` = value
else
  `ExposureAuto` = `Continuous` | value = `ExposureTime` | +| cv2.CAP_PROP_GAMMA | float | `GammaEnable` = `True`
`Gamma` = value | value = `Gamma` | +| cv2.CAP_PROP_TEMPERATURE | float | | value = `DeviceTemperature` | +| cv2.CAP_PROP_TRIGGER | bool | if value == `True`
  `TriggerMode` = `On`
else
  `TriggerMode` = `Off` | if trigger_mode == `On`
  value = `True`
elif trigger_mode == `Off`
  value = `False` | +| cv2.CAP_PROP_TRIGGER_DELAY | float | `TriggerDelay` = value | value = `TriggerDelay` | +| cv2.CAP_PROP_BACKLIGHT | bool | if value == `True`
  `DeviceIndicatorMode` = `Active`
else
  `DeviceIndicatorMode` = `Inactive` | if device_indicator_mode == `Active`
  value = `True`
elif device_indicator_mode == `Inactive`
  value = `False` | +| cv2.CAP_PROP_AUTO_WB | bool | if value == `True`
  `BalanceWhiteAuto` = `Continuous`
else
  `BalanceWhiteAuto` = `Off` | if balance_white_auto == `Continuous`
  value = `True`
elif balance_white_auto == `Off`
  value = `False` | ## Command-Line Tool EasyPySpin provides a command-line tool. Connect the camera and execute the following commands, as shown below, then you can view the captured images. From ee2e1ccd5b7749d1e2adf8cff11ab79e281488d4 Mon Sep 17 00:00:00 2001 From: elerac Date: Sun, 8 Aug 2021 14:33:52 +0900 Subject: [PATCH 43/43] version 1.2.1 -> 2.0.0 --- setup.py | 23 ++++++++++++++--------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/setup.py b/setup.py index ad9dc9e..5954154 100644 --- a/setup.py +++ b/setup.py @@ -1,13 +1,18 @@ from setuptools import setup, find_packages +with open("README.md", "r") as fh: + long_description = fh.read() + setup( - name='EasyPySpin', - version='1.2.1', - description='cv2.VideoCapture like wrapper for FLIR Spinnaker SDK', - url='https://github.com/elerac/EasyPySpin', - author='Ryota Maeda', - author_email='maeda.ryota.elerac@gmail.com', - license='MIT', - entry_points={'console_scripts': ['EasyPySpin= EasyPySpin.command_line:main']}, - packages=find_packages() + name="EasyPySpin", + version="2.0.0", + description="cv2.VideoCapture like wrapper for FLIR Spinnaker SDK", + long_description=long_description, + long_description_content_type="text/markdown", + url="https://github.com/elerac/EasyPySpin", + author="Ryota Maeda", + author_email="maeda.ryota.elerac@gmail.com", + license="MIT", + entry_points={"console_scripts": ["EasyPySpin= EasyPySpin.command_line:main"]}, + packages=find_packages(), )