From d836647c7e659f184054ac16fe4af4eb7482b99e Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 27 Oct 2026 15:46:28 +0000 Subject: [PATCH 1/4] Spell out the CameraBase settings in the plugin class, press the checkable ROI buttons in the tests The settings of the plugin were built by a helper that copied and modified the ones of CameraBase, so the class preamble did not show them. They are now written in the class. The ROI buttons are checkable (utils 5.3.1, pymodaq 5.3.9): the tests press them and the floors follow. Co-Authored-By: Claude Sonnet 5.5 Claude-Session: https://claude.ai/code/session_01Tdiso1u1ZhikSutj2Qs5Cg --- pyproject.toml | 4 +- .../plugins_2D/daq_2Dviewer_OpenCV.py | 56 +++++++++++-------- tests/test_opencv_camera.py | 13 +++-- 3 files changed, 43 insertions(+), 30 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 1dbdfbc..26e7c91 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -12,9 +12,9 @@ package-url = 'https://github.com/PyMoDAQ/pymodaq_plugins_opencv' name = "pymodaq_plugins_opencv" description = 'plugin to use camera instrumented using the opencv library' dependencies = [ - "pymodaq>=5.3.2", + "pymodaq>=5.3.9", "pymodaq_utils>=5.3.1", - "pymodaq_gui>=5.3.0", + "pymodaq_gui>=5.3.9", "pymodaq_data>=5.3.0", "pymodaq_plugins_utils>=5.3.1", 'opencv-python', diff --git a/src/pymodaq_plugins_opencv/daq_viewer_plugins/plugins_2D/daq_2Dviewer_OpenCV.py b/src/pymodaq_plugins_opencv/daq_viewer_plugins/plugins_2D/daq_2Dviewer_OpenCV.py index 14877ae..0e06a71 100644 --- a/src/pymodaq_plugins_opencv/daq_viewer_plugins/plugins_2D/daq_2Dviewer_OpenCV.py +++ b/src/pymodaq_plugins_opencv/daq_viewer_plugins/plugins_2D/daq_2Dviewer_OpenCV.py @@ -1,33 +1,16 @@ -from copy import deepcopy - from pymodaq_utils.logger import set_logger, get_module_name from pymodaq_gui.parameter import Parameter +from pymodaq_gui.utils.status_palette import Status from pymodaq.control_modules.viewer_utility_classes import comon_parameters, main -from pymodaq_plugins_utils.hardware.camera_base import CameraBase, cam_params +from pymodaq_plugins_utils.hardware.camera_base import CameraBase, ROI_LED_STATES from pymodaq_plugins_opencv.hardware.opencv_camera import OpenCVCamera logger = set_logger(get_module_name(__file__)) -def webcam_params() -> list: - """The settings of ``CameraBase`` that suit a webcam: colour image converted to gray by default, no exposure - time nor buffer (OpenCV sets neither, see ``Cam. Settings``)""" - params = deepcopy(cam_params) - for param in params: - if param['name'] == 'color_conversion': - param['value'] = 'RGB2GRAY' - elif param['name'] == 'buffer': - param['visible'] = False - elif param['name'] == 'timing_opts': - for child in param['children']: - if child['name'] == 'exposure_time': - child['visible'] = False - return params - - class DAQ_2DViewer_OpenCV(CameraBase): """Cameras read with OpenCV (webcams, any device handled by ``cv2.VideoCapture``) @@ -38,11 +21,36 @@ class DAQ_2DViewer_OpenCV(CameraBase): ``Cam. Settings`` lists the properties of the camera that OpenCV can read (brightness, gain, exposure...), the unit of some of them depends on the backend of OpenCV. Changing the width or the height there resets the ROI. """ - params = comon_parameters + \ - [{'title': 'Camera index:', 'name': 'camera_index', 'type': 'int', 'value': 0, 'default': 0, 'min': 0}, - {'title': 'Open Settings:', 'name': 'open_settings', 'type': 'bool', 'value': False}, - {'title': 'Cam. Settings:', 'name': 'cam_settings', 'type': 'group', 'children': []}, - ] + webcam_params() + # the settings of CameraBase, spelled out here so that they can be read (the exposure time and the buffer are + # hidden: OpenCV sets neither, and the default colour conversion is RGB2GRAY) + params = comon_parameters + [ + {'title': 'Camera index:', 'name': 'camera_index', 'type': 'int', 'value': 0, 'default': 0, 'min': 0}, + {'title': 'Open Settings:', 'name': 'open_settings', 'type': 'bool', 'value': False}, + {'title': 'Cam. Settings:', 'name': 'cam_settings', 'type': 'group', 'children': []}, + {'title': 'Camera name:', 'name': 'camera_name', 'type': 'str', 'value': '', 'readonly': True}, + {'title': 'Color Conversion:', 'name': 'color_conversion', 'type': 'list', 'value': 'RGB2GRAY', + 'limits': ['None', 'RGB2GRAY', 'BAYER_BG2RGB', 'BAYER_BG2GRAY']}, + {'title': 'ROI', 'name': 'roi', 'type': 'group', 'children': [ + {'title': 'Update ROI from Viewer', 'name': 'update_roi', 'type': 'action_led', + 'value': Status.OFF.value, 'label': 'Update', 'limits': ROI_LED_STATES, 'checkable': True}, + {'title': 'Apply ROI', 'name': 'apply_roi', 'type': 'action_led', + 'value': Status.OFF.value, 'label': 'Apply', 'limits': ROI_LED_STATES, 'checkable': True}, + {'title': 'Clear ROI+Bin', 'name': 'clear_roi', 'type': 'bool_push', 'value': False}, + {'title': 'ROI:', 'name': 'roi_slices', 'type': 'str', 'value': '', 'readonly': True}, + {'title': 'X binning', 'name': 'x_binning', 'type': 'int', 'value': 1}, + {'title': 'Y binning', 'name': 'y_binning', 'type': 'int', 'value': 1}, + ], }, + {'title': 'Image width', 'name': 'hdet', 'type': 'int', 'value': 1, 'readonly': True}, + {'title': 'Image height', 'name': 'vdet', 'type': 'int', 'value': 1, 'readonly': True}, + {'title': 'Timing', 'name': 'timing_opts', 'type': 'group', 'children': [ + {'title': 'Exposure Time (ms)', 'name': 'exposure_time', 'type': 'int', 'value': 1, 'visible': False}, + {'title': 'Compute FPS', 'name': 'fps_on', 'type': 'bool', 'value': True}, + {'title': 'FPS', 'name': 'fps', 'type': 'float', 'value': 0.0, 'readonly': True}]}, + {'title': 'Buffer', 'name': 'buffer', 'type': 'group', 'visible': False, 'children': [ + {'title': 'Size:', 'name': 'size', 'type': 'int', 'value': 10}, + {'title': 'mode:', 'name': 'mode', 'type': 'list', 'value': 'now', + 'limits': ['now', 'lastread', 'lastwait', 'start']}]}, + ] def ini_detector_custom(self, controller=None): """Open the camera (master) or take the one of the master (slave)""" diff --git a/tests/test_opencv_camera.py b/tests/test_opencv_camera.py index 931a62c..7ace4a5 100644 --- a/tests/test_opencv_camera.py +++ b/tests/test_opencv_camera.py @@ -116,12 +116,17 @@ def acquire(driver: OpenCVCamera, nframes=1) -> np.ndarray: return driver.read_multiple_images() +def click_led(plugin, name: str): + """What the user does: press the (checkable) button of the parameter, which the DAQ_Viewer sends to the plugin""" + param = plugin.settings.child('roi', name) + param.opts['checked'] = True + param.activate() + + def apply_roi(plugin, rows: slice, columns: slice): - plugin.settings.child('roi', 'update_roi').setValue(True) plugin.roi_select(RoiInfo.from_slices([rows, columns])) - param = plugin.settings.child('roi', 'apply_roi') - param.setValue(True) - plugin.commit_settings(param) + click_led(plugin, 'update_roi') + click_led(plugin, 'apply_roi') class TestVideoFile: From 64b7d9a8371f93827e088191f21dd6bcd656887b Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 27 Oct 2026 15:48:13 +0000 Subject: [PATCH 2/4] Require pymodaq_plugins_utils 5.3.2 (checkable ROI buttons) Co-Authored-By: Claude Sonnet 5.5 Claude-Session: https://claude.ai/code/session_01Tdiso1u1ZhikSutj2Qs5Cg --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 26e7c91..cca9caa 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -16,7 +16,7 @@ dependencies = [ "pymodaq_utils>=5.3.1", "pymodaq_gui>=5.3.9", "pymodaq_data>=5.3.0", - "pymodaq_plugins_utils>=5.3.1", + "pymodaq_plugins_utils>=5.3.2", 'opencv-python', ] From 05118ed9945317dedda1ff72375de5a39fae6abb Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 27 Oct 2026 15:55:01 +0000 Subject: [PATCH 3/4] Use cam_params of CameraBase directly in the params, apply the webcam differences in __init__ Co-Authored-By: Claude Sonnet 5.5 Claude-Session: https://claude.ai/code/session_01Tdiso1u1ZhikSutj2Qs5Cg --- .../plugins_2D/daq_2Dviewer_OpenCV.py | 45 ++++++------------- 1 file changed, 13 insertions(+), 32 deletions(-) diff --git a/src/pymodaq_plugins_opencv/daq_viewer_plugins/plugins_2D/daq_2Dviewer_OpenCV.py b/src/pymodaq_plugins_opencv/daq_viewer_plugins/plugins_2D/daq_2Dviewer_OpenCV.py index 0e06a71..851f424 100644 --- a/src/pymodaq_plugins_opencv/daq_viewer_plugins/plugins_2D/daq_2Dviewer_OpenCV.py +++ b/src/pymodaq_plugins_opencv/daq_viewer_plugins/plugins_2D/daq_2Dviewer_OpenCV.py @@ -1,10 +1,9 @@ from pymodaq_utils.logger import set_logger, get_module_name from pymodaq_gui.parameter import Parameter -from pymodaq_gui.utils.status_palette import Status from pymodaq.control_modules.viewer_utility_classes import comon_parameters, main -from pymodaq_plugins_utils.hardware.camera_base import CameraBase, ROI_LED_STATES +from pymodaq_plugins_utils.hardware.camera_base import CameraBase, cam_params from pymodaq_plugins_opencv.hardware.opencv_camera import OpenCVCamera @@ -21,36 +20,18 @@ class DAQ_2DViewer_OpenCV(CameraBase): ``Cam. Settings`` lists the properties of the camera that OpenCV can read (brightness, gain, exposure...), the unit of some of them depends on the backend of OpenCV. Changing the width or the height there resets the ROI. """ - # the settings of CameraBase, spelled out here so that they can be read (the exposure time and the buffer are - # hidden: OpenCV sets neither, and the default colour conversion is RGB2GRAY) - params = comon_parameters + [ - {'title': 'Camera index:', 'name': 'camera_index', 'type': 'int', 'value': 0, 'default': 0, 'min': 0}, - {'title': 'Open Settings:', 'name': 'open_settings', 'type': 'bool', 'value': False}, - {'title': 'Cam. Settings:', 'name': 'cam_settings', 'type': 'group', 'children': []}, - {'title': 'Camera name:', 'name': 'camera_name', 'type': 'str', 'value': '', 'readonly': True}, - {'title': 'Color Conversion:', 'name': 'color_conversion', 'type': 'list', 'value': 'RGB2GRAY', - 'limits': ['None', 'RGB2GRAY', 'BAYER_BG2RGB', 'BAYER_BG2GRAY']}, - {'title': 'ROI', 'name': 'roi', 'type': 'group', 'children': [ - {'title': 'Update ROI from Viewer', 'name': 'update_roi', 'type': 'action_led', - 'value': Status.OFF.value, 'label': 'Update', 'limits': ROI_LED_STATES, 'checkable': True}, - {'title': 'Apply ROI', 'name': 'apply_roi', 'type': 'action_led', - 'value': Status.OFF.value, 'label': 'Apply', 'limits': ROI_LED_STATES, 'checkable': True}, - {'title': 'Clear ROI+Bin', 'name': 'clear_roi', 'type': 'bool_push', 'value': False}, - {'title': 'ROI:', 'name': 'roi_slices', 'type': 'str', 'value': '', 'readonly': True}, - {'title': 'X binning', 'name': 'x_binning', 'type': 'int', 'value': 1}, - {'title': 'Y binning', 'name': 'y_binning', 'type': 'int', 'value': 1}, - ], }, - {'title': 'Image width', 'name': 'hdet', 'type': 'int', 'value': 1, 'readonly': True}, - {'title': 'Image height', 'name': 'vdet', 'type': 'int', 'value': 1, 'readonly': True}, - {'title': 'Timing', 'name': 'timing_opts', 'type': 'group', 'children': [ - {'title': 'Exposure Time (ms)', 'name': 'exposure_time', 'type': 'int', 'value': 1, 'visible': False}, - {'title': 'Compute FPS', 'name': 'fps_on', 'type': 'bool', 'value': True}, - {'title': 'FPS', 'name': 'fps', 'type': 'float', 'value': 0.0, 'readonly': True}]}, - {'title': 'Buffer', 'name': 'buffer', 'type': 'group', 'visible': False, 'children': [ - {'title': 'Size:', 'name': 'size', 'type': 'int', 'value': 10}, - {'title': 'mode:', 'name': 'mode', 'type': 'list', 'value': 'now', - 'limits': ['now', 'lastread', 'lastwait', 'start']}]}, - ] + params = comon_parameters + \ + [{'title': 'Camera index:', 'name': 'camera_index', 'type': 'int', 'value': 0, 'default': 0, 'min': 0}, + {'title': 'Open Settings:', 'name': 'open_settings', 'type': 'bool', 'value': False}, + {'title': 'Cam. Settings:', 'name': 'cam_settings', 'type': 'group', 'children': []}, + ] + cam_params + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + # OpenCV sets neither the exposure time nor the buffer of CameraBase, and a colour image is shown in gray + self.settings.child('timing_opts', 'exposure_time').show(False) + self.settings.child('buffer').show(False) + self.settings.child('color_conversion').setOpts(value='RGB2GRAY', default='RGB2GRAY') def ini_detector_custom(self, controller=None): """Open the camera (master) or take the one of the master (slave)""" From ba567fd9539c7d6b7687d382e6a33f8cd4d600b7 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 27 Oct 2026 16:01:47 +0000 Subject: [PATCH 4/4] Keep the Qt plugins of the binding when opencv-python is imported The opencv-python wheel (not the headless one) sets QT_QPA_PLATFORM_PLUGIN_PATH to the Qt plugins it ships when cv2 is imported. The creation of a QApplication with PyQt5 then aborts (seen on CI, Python 3.10 with PyQt5). Co-Authored-By: Claude Sonnet 5.5 Claude-Session: https://claude.ai/code/session_01Tdiso1u1ZhikSutj2Qs5Cg --- .../hardware/opencv_camera.py | 13 +++++++++++++ tests/test_opencv_camera.py | 13 ++++++++++++- 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/src/pymodaq_plugins_opencv/hardware/opencv_camera.py b/src/pymodaq_plugins_opencv/hardware/opencv_camera.py index 9a97c59..dd7f73b 100644 --- a/src/pymodaq_plugins_opencv/hardware/opencv_camera.py +++ b/src/pymodaq_plugins_opencv/hardware/opencv_camera.py @@ -12,6 +12,7 @@ Frames are given as RGB, the order of OpenCV being BGR. """ +import os import sys import threading import time @@ -27,6 +28,18 @@ from pymodaq_plugins_opencv.hardware.opencv import OpenCVProp +def _keep_qt_of_the_binding(): + """The opencv-python package (not the headless one) points Qt to the platform plugins it ships, which makes the + creation of a QApplication abort with the Qt of PyQt/PySide: restore the plugins of the Qt binding""" + cv2_dir = os.path.dirname(os.path.abspath(cv2.__file__)) + for name in ('QT_QPA_PLATFORM_PLUGIN_PATH', 'QT_QPA_FONTDIR'): + if os.environ.get(name, '').startswith(cv2_dir): + del os.environ[name] + + +_keep_qt_of_the_binding() + + class OpenCVCamera(CameraDriverBase): """A camera read with ``cv2.VideoCapture`` diff --git a/tests/test_opencv_camera.py b/tests/test_opencv_camera.py index 7ace4a5..6b70491 100644 --- a/tests/test_opencv_camera.py +++ b/tests/test_opencv_camera.py @@ -1,6 +1,7 @@ """The OpenCV camera driver and its plugin, without a webcam: a video file read by the real OpenCV, and a fake of ``cv2.VideoCapture`` for what a file cannot do (properties, failing reads, change of the frame size)""" import importlib.util +import os import time import cv2 @@ -24,7 +25,7 @@ from pymodaq_plugins_utils.hardware.camera_driver import CameraDriverBase -from pymodaq_plugins_opencv.hardware.opencv_camera import OpenCVCamera +from pymodaq_plugins_opencv.hardware.opencv_camera import OpenCVCamera, _keep_qt_of_the_binding from pymodaq_plugins_opencv.daq_viewer_plugins.plugins_2D.daq_2Dviewer_OpenCV import DAQ_2DViewer_OpenCV WIDTH, HEIGHT = 64, 48 @@ -129,6 +130,16 @@ def apply_roi(plugin, rows: slice, columns: slice): click_led(plugin, 'apply_roi') +def test_the_qt_plugins_of_opencv_python_are_forgotten(monkeypatch): + cv2_qt = os.path.join(os.path.dirname(os.path.abspath(cv2.__file__)), 'qt', 'plugins') + monkeypatch.setenv('QT_QPA_PLATFORM_PLUGIN_PATH', cv2_qt) + _keep_qt_of_the_binding() + assert 'QT_QPA_PLATFORM_PLUGIN_PATH' not in os.environ + monkeypatch.setenv('QT_QPA_PLATFORM_PLUGIN_PATH', '/my/own/plugins') # not the one of OpenCV: left alone + _keep_qt_of_the_binding() + assert os.environ['QT_QPA_PLATFORM_PLUGIN_PATH'] == '/my/own/plugins' + + class TestVideoFile: """The driver with the real OpenCV""" def test_frames_are_rgb(self, video):