From a557259f49b2b231010d83c72e3bb49dc86cefb3 Mon Sep 17 00:00:00 2001 From: zrezke Date: Wed, 8 Mar 2023 14:17:28 +0100 Subject: [PATCH 01/19] cam test gui --- utilities/cam_test.py | 172 ++++++++++++++++--------- utilities/cam_test_gui.py | 255 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 370 insertions(+), 57 deletions(-) create mode 100644 utilities/cam_test_gui.py diff --git a/utilities/cam_test.py b/utilities/cam_test.py index 44b4e6294..ca48429f5 100755 --- a/utilities/cam_test.py +++ b/utilities/cam_test.py @@ -37,22 +37,28 @@ import cv2 import argparse -import depthai as dai import collections import time from itertools import cycle from pathlib import Path +import sys +import cam_test_gui + def socket_type_pair(arg): socket, type = arg.split(',') - if not (socket in ['rgb', 'left', 'right', 'camd']): raise ValueError("") - if not (type in ['m', 'mono', 'c', 'color']): raise ValueError("") + if not (socket in ['rgb', 'left', 'right', 'camd']): + raise ValueError("") + if not (type in ['m', 'mono', 'c', 'color']): + raise ValueError("") is_color = True if type in ['c', 'color'] else False return [socket, is_color] + parser = argparse.ArgumentParser() parser.add_argument('-cams', '--cameras', type=socket_type_pair, nargs='+', - default=[['rgb', True], ['left', False], ['right', False], ['camd', True]], + default=[['rgb', True], ['left', False], + ['right', False], ['camd', True]], help="Which camera sockets to enable, and type: c[olor] / m[ono]. " "E.g: -cams rgb,m right,c . Default: rgb,c left,m right,m camd,c") parser.add_argument('-mres', '--mono-resolution', type=int, default=800, choices={480, 400, 720, 800}, @@ -71,8 +77,25 @@ def socket_type_pair(arg): help="Make OpenCV windows resizable. Note: may introduce some artifacts") parser.add_argument('-tun', '--camera-tuning', type=Path, help="Path to custom camera tuning database") +parser.add_argument('-d', '--device', default="", type=str, + help="Optional MX ID of the device to connect to.") + +parser.add_argument('-ctimeout', '--connection-timeout', default=30000, + help="Connection timeout in ms. Default: %(default)s (sets DEPTHAI_CONNECTION_TIMEOUT environment variable)") + +parser.add_argument('-btimeout', '--boot-timeout', default=30000, + help="Boot timeout in ms. Default: %(default)s (sets DEPTHAI_BOOT_TIMEOUT environment variable)") + args = parser.parse_args() +# Set timeouts before importing depthai +os.environ["DEPTHAI_CONNECTION_TIMEOUT"] = str(args.connection_timeout) +os.environ["DEPTHAI_BOOT_TIMEOUT"] = str(args.boot_timeout) +import depthai as dai + +if len(sys.argv) == 1: + cam_test_gui.main() + cam_list = [] cam_type_color = {} print("Enabled cameras:") @@ -85,24 +108,24 @@ def socket_type_pair(arg): print("DepthAI path:", dai.__file__) cam_socket_opts = { - 'rgb' : dai.CameraBoardSocket.RGB, # Or CAM_A - 'left' : dai.CameraBoardSocket.LEFT, # Or CAM_B - 'right': dai.CameraBoardSocket.RIGHT, # Or CAM_C - 'camd' : dai.CameraBoardSocket.CAM_D, + 'rgb': dai.CameraBoardSocket.RGB, # Or CAM_A + 'left': dai.CameraBoardSocket.LEFT, # Or CAM_B + 'right': dai.CameraBoardSocket.RIGHT, # Or CAM_C + 'camd': dai.CameraBoardSocket.CAM_D, } cam_socket_to_name = { - 'RGB' : 'rgb', - 'LEFT' : 'left', + 'RGB': 'rgb', + 'LEFT': 'left', 'RIGHT': 'right', 'CAM_D': 'camd', } rotate = { - 'rgb' : args.rotate in ['all', 'rgb'], - 'left' : args.rotate in ['all', 'mono'], + 'rgb': args.rotate in ['all', 'rgb'], + 'left': args.rotate in ['all', 'mono'], 'right': args.rotate in ['all', 'mono'], - 'camd' : args.rotate in ['all', 'rgb'], + 'camd': args.rotate in ['all', 'rgb'], } mono_res_opts = { @@ -134,9 +157,11 @@ def __init__(self, window_size=30): self.fps = 0 def update(self, timestamp=None): - if timestamp == None: timestamp = time.monotonic() + if timestamp == None: + timestamp = time.monotonic() count = len(self.dq) - if count > 0: self.fps = count / (timestamp - self.dq[0]) + if count > 0: + self.fps = count / (timestamp - self.dq[0]) self.dq.append(timestamp) def get(self): @@ -145,7 +170,7 @@ def get(self): # Start defining a pipeline pipeline = dai.Pipeline() # Uncomment to get better throughput -#pipeline.setXLinkChunkSize(0) +# pipeline.setXLinkChunkSize(0) control = pipeline.createXLinkIn() control.setStreamName('control') @@ -159,7 +184,7 @@ def get(self): cam[c] = pipeline.createColorCamera() cam[c].setResolution(color_res_opts[args.color_resolution]) cam[c].setIspScale(1, args.isp_downscale) - #cam[c].initialControl.setManualFocus(85) # TODO + # cam[c].initialControl.setManualFocus(85) # TODO cam[c].isp.link(xout[c].input) else: cam[c] = pipeline.createMonoCamera() @@ -167,13 +192,13 @@ def get(self): cam[c].out.link(xout[c].input) cam[c].setBoardSocket(cam_socket_opts[c]) # Num frames to capture on trigger, with first to be discarded (due to degraded quality) - #cam[c].initialControl.setExternalTrigger(2, 1) - #cam[c].initialControl.setStrobeExternal(48, 1) - #cam[c].initialControl.setFrameSyncMode(dai.CameraControl.FrameSyncMode.INPUT) + # cam[c].initialControl.setExternalTrigger(2, 1) + # cam[c].initialControl.setStrobeExternal(48, 1) + # cam[c].initialControl.setFrameSyncMode(dai.CameraControl.FrameSyncMode.INPUT) - #cam[c].initialControl.setManualExposure(15000, 400) # exposure [us], iso + # cam[c].initialControl.setManualExposure(15000, 400) # exposure [us], iso # When set, takes effect after the first 2 frames - #cam[c].initialControl.setManualWhiteBalance(4000) # light temperature in K, 1000..12000 + # cam[c].initialControl.setManualWhiteBalance(4000) # light temperature in K, 1000..12000 control.out.link(cam[c].inputControl) if rotate[c]: cam[c].setImageOrientation(dai.CameraImageOrientation.ROTATE_180_DEG) @@ -183,13 +208,19 @@ def get(self): if args.camera_tuning: pipeline.setCameraTuningBlobPath(str(args.camera_tuning)) + # Pipeline is defined, now we can connect to the device -with dai.Device(pipeline) as device: - #print('Connected cameras:', [c.name for c in device.getConnectedCameras()]) +device = dai.Device.getDeviceByMxId(args.device) +dai_device_args = [pipeline] +if device[0]: + dai_device_args.append(device[1]) +with dai.Device(*dai_device_args) as device: + # print('Connected cameras:', [c.name for c in device.getConnectedCameras()]) print('Connected cameras:') cam_name = {} for p in device.getConnectedCameraFeatures(): - print(f' -socket {p.socket.name:6}: {p.sensorName:6} {p.width:4} x {p.height:4} focus:', end='') + print( + f' -socket {p.socket.name:6}: {p.sensorName:6} {p.width:4} x {p.height:4} focus:', end='') print('auto ' if p.hasAutofocus else 'fixed', '- ', end='') print(*[type.name for type in p.supportedTypes]) cam_name[cam_socket_to_name[p.socket.name]] = p.sensorName @@ -237,9 +268,12 @@ def get(self): dotIntensity = 0 floodIntensity = 0 - awb_mode = cycle([item for name, item in vars(dai.CameraControl.AutoWhiteBalanceMode).items() if name.isupper()]) - anti_banding_mode = cycle([item for name, item in vars(dai.CameraControl.AntiBandingMode).items() if name.isupper()]) - effect_mode = cycle([item for name, item in vars(dai.CameraControl.EffectMode).items() if name.isupper()]) + awb_mode = cycle([item for name, item in vars( + dai.CameraControl.AutoWhiteBalanceMode).items() if name.isupper()]) + anti_banding_mode = cycle([item for name, item in vars( + dai.CameraControl.AntiBandingMode).items() if name.isupper()]) + effect_mode = cycle([item for name, item in vars( + dai.CameraControl.EffectMode).items() if name.isupper()]) ae_comp = 0 ae_lock = False @@ -252,7 +286,8 @@ def get(self): chroma_denoise = 0 control = 'none' - print("Cam:", *[' ' + c.ljust(8) for c in cam_list], "[host | capture timestamp]") + print("Cam:", *[' ' + c.ljust(8) + for c in cam_list], "[host | capture timestamp]") capture_list = [] while True: @@ -265,21 +300,25 @@ def get(self): if c in capture_list: width, height = pkt.getWidth(), pkt.getHeight() capture_file_name = ('capture_' + c + '_' + cam_name[c] - + '_' + str(width) + 'x' + str(height) - + '_exp_' + str(int(pkt.getExposureTime().total_seconds()*1e6)) - + '_iso_' + str(pkt.getSensitivity()) - + '_lens_' + str(pkt.getLensPosition()) - + '_' + capture_time - + '_' + str(pkt.getSequenceNum()) - + ".png" - ) + + '_' + str(width) + 'x' + str(height) + + '_exp_' + + str(int( + pkt.getExposureTime().total_seconds()*1e6)) + + '_iso_' + str(pkt.getSensitivity()) + + '_lens_' + + str(pkt.getLensPosition()) + + '_' + capture_time + + '_' + str(pkt.getSequenceNum()) + + ".png" + ) print("\nSaving:", capture_file_name) cv2.imwrite(capture_file_name, frame) capture_list.remove(c) cv2.imshow(c, frame) print("\rFPS:", - *["{:6.2f}|{:6.2f}".format(fps_host[c].get(), fps_capt[c].get()) for c in cam_list], + *["{:6.2f}|{:6.2f}".format(fps_host[c].get(), + fps_capt[c].get()) for c in cam_list], end='', flush=True) key = cv2.waitKey(1) @@ -297,7 +336,8 @@ def get(self): elif key == ord('f'): print("Autofocus enable, continuous") ctrl = dai.CameraControl() - ctrl.setAutoFocusMode(dai.CameraControl.AutoFocusMode.CONTINUOUS_VIDEO) + ctrl.setAutoFocusMode( + dai.CameraControl.AutoFocusMode.CONTINUOUS_VIDEO) controlQueue.send(ctrl) elif key == ord('e'): print("Autoexposure enable") @@ -305,18 +345,24 @@ def get(self): ctrl.setAutoExposureEnable() controlQueue.send(ctrl) elif key in [ord(','), ord('.')]: - if key == ord(','): lensPos -= LENS_STEP - if key == ord('.'): lensPos += LENS_STEP + if key == ord(','): + lensPos -= LENS_STEP + if key == ord('.'): + lensPos += LENS_STEP lensPos = clamp(lensPos, lensMin, lensMax) print("Setting manual focus, lens position: ", lensPos) ctrl = dai.CameraControl() ctrl.setManualFocus(lensPos) controlQueue.send(ctrl) elif key in [ord('i'), ord('o'), ord('k'), ord('l')]: - if key == ord('i'): expTime -= EXP_STEP - if key == ord('o'): expTime += EXP_STEP - if key == ord('k'): sensIso -= ISO_STEP - if key == ord('l'): sensIso += ISO_STEP + if key == ord('i'): + expTime -= EXP_STEP + if key == ord('o'): + expTime += EXP_STEP + if key == ord('k'): + sensIso -= ISO_STEP + if key == ord('l'): + sensIso += ISO_STEP expTime = clamp(expTime, expMin, expMax) sensIso = clamp(sensIso, sensMin, sensMax) print("Setting manual exposure, time: ", expTime, "iso: ", sensIso) @@ -356,21 +402,33 @@ def get(self): floodIntensity = 0 device.setIrFloodLightBrightness(floodIntensity) elif key >= 0 and chr(key) in '34567890[]': - if key == ord('3'): control = 'awb_mode' - elif key == ord('4'): control = 'ae_comp' - elif key == ord('5'): control = 'anti_banding_mode' - elif key == ord('6'): control = 'effect_mode' - elif key == ord('7'): control = 'brightness' - elif key == ord('8'): control = 'contrast' - elif key == ord('9'): control = 'saturation' - elif key == ord('0'): control = 'sharpness' - elif key == ord('['): control = 'luma_denoise' - elif key == ord(']'): control = 'chroma_denoise' + if key == ord('3'): + control = 'awb_mode' + elif key == ord('4'): + control = 'ae_comp' + elif key == ord('5'): + control = 'anti_banding_mode' + elif key == ord('6'): + control = 'effect_mode' + elif key == ord('7'): + control = 'brightness' + elif key == ord('8'): + control = 'contrast' + elif key == ord('9'): + control = 'saturation' + elif key == ord('0'): + control = 'sharpness' + elif key == ord('['): + control = 'luma_denoise' + elif key == ord(']'): + control = 'chroma_denoise' print("Selected control:", control) elif key in [ord('-'), ord('_'), ord('+'), ord('=')]: change = 0 - if key in [ord('-'), ord('_')]: change = -1 - if key in [ord('+'), ord('=')]: change = 1 + if key in [ord('-'), ord('_')]: + change = -1 + if key in [ord('+'), ord('=')]: + change = 1 ctrl = dai.CameraControl() if control == 'none': print("Please select a control first using keys 3..9 0 [ ]") diff --git a/utilities/cam_test_gui.py b/utilities/cam_test_gui.py new file mode 100644 index 000000000..8a968966d --- /dev/null +++ b/utilities/cam_test_gui.py @@ -0,0 +1,255 @@ +from typing import List +import PyQt5.sip +from PyQt5 import QtCore, QtGui, QtWidgets +import subprocess +import depthai as dai +import sys +import os +import signal + + +class CamTestGui: + + CAM_SOCKET_OPTIONS = ["rgb", "left", "right", "camd"] + CAM_TYPE_OPTIONS = ["color", "mono"] + MONO_RESOLUTION_OPTIONS = ["400", "480", "720", "800"] + COLOR_RESOLUTION_OPTIONS = ["720", "800", + "1080", "1200", "4k", "5mp", "12mp", "48mp"] + ROTATE_OPTIONS = ["disabled", "all", "rgb", "mono"] + DEPTHAI_CONNECT_TIMEOUT_DEFAULT = 30000 + DEPTHAI_BOOTUP_TIMEOUT_DEFAULT = 30000 + + def remove_camera(self, layout: QtWidgets.QHBoxLayout): + for i in reversed(range(layout.count())): + layout.itemAt(i).widget().setParent(None) + self.cameras_list.removeItem(layout) + + def add_camera(self, camera: str = None, camera_type: str = None): + layout = QtWidgets.QHBoxLayout() + cam_combo = QtWidgets.QComboBox() + cam_combo.addItems(self.CAM_SOCKET_OPTIONS) + cam_combo.setCurrentIndex( + self.CAM_SOCKET_OPTIONS.index(camera) if camera else 0) + layout.addWidget(cam_combo) + cam_type_combo = QtWidgets.QComboBox() + cam_type_combo.addItems(self.CAM_TYPE_OPTIONS) + cam_type_combo.setCurrentIndex( + self.CAM_TYPE_OPTIONS.index(camera_type) if camera_type else 0) + layout.addWidget(cam_type_combo) + self.cameras_list.addLayout(layout) + remove_button = QtWidgets.QPushButton("Remove") + remove_button.clicked.connect(lambda: self.remove_camera(layout)) + layout.addWidget(remove_button) + + def set_default_cameras(self): + self.add_camera("rgb", "color") + self.add_camera("left", "mono") + self.add_camera("right", "mono") + self.add_camera("camd", "color") + + def __init__(self, app: "Application"): + self.app = app + self.app.setWindowTitle("Camera Test") + self.main_widget = QtWidgets.QWidget() + self.main_layout = QtWidgets.QVBoxLayout() + self.main_widget.setLayout(self.main_layout) + self.app.setCentralWidget(self.main_widget) + self.label_cameras = QtWidgets.QLabel("Cameras") + self.main_layout.addWidget(self.label_cameras) + + self.cameras_list = QtWidgets.QVBoxLayout() + self.main_layout.addLayout(self.cameras_list) + self.set_default_cameras() + + self.add_cam_button = QtWidgets.QPushButton("Add Camera") + self.add_cam_button.clicked.connect(self.add_camera) + self.main_layout.addWidget(self.add_cam_button) + + self.mono_resolution_label = QtWidgets.QLabel("Mono Resolution") + self.main_layout.addWidget(self.mono_resolution_label) + self.mono_resolution_combo = QtWidgets.QComboBox() + self.mono_resolution_combo.addItems(self.MONO_RESOLUTION_OPTIONS) + self.main_layout.addWidget(self.mono_resolution_combo) + self.mono_resolution_combo.setCurrentIndex(3) + + self.label_color_resolution = QtWidgets.QLabel("Color Resolution") + self.main_layout.addWidget(self.label_color_resolution) + self.combo_color_resolution = QtWidgets.QComboBox() + self.combo_color_resolution.addItems(self.COLOR_RESOLUTION_OPTIONS) + self.main_layout.addWidget(self.combo_color_resolution) + self.combo_color_resolution.setCurrentIndex(2) + + self.label_rotate = QtWidgets.QLabel("Rotate") + self.main_layout.addWidget(self.label_rotate) + self.combo_rotate = QtWidgets.QComboBox() + self.combo_rotate.addItems(self.ROTATE_OPTIONS) + self.main_layout.addWidget(self.combo_rotate) + + self.label_fps = QtWidgets.QLabel("FPS") + self.main_layout.addWidget(self.label_fps) + self.spin_fps = QtWidgets.QSpinBox() + self.spin_fps.setMinimum(1) + self.spin_fps.setMaximum(120) + self.spin_fps.setValue(30) + self.main_layout.addWidget(self.spin_fps) + + self.label_isp_downscale = QtWidgets.QLabel("ISP Downscale") + self.main_layout.addWidget(self.label_isp_downscale) + self.spin_isp_downscale = QtWidgets.QSpinBox() + self.spin_isp_downscale.setMinimum(1) + self.spin_isp_downscale.setMaximum(4) + self.spin_isp_downscale.setValue(1) + self.main_layout.addWidget(self.spin_isp_downscale) + + self.label_resizable_windows = QtWidgets.QLabel("Resizable Windows") + self.main_layout.addWidget(self.label_resizable_windows) + self.check_resizable_windows = QtWidgets.QCheckBox() + self.main_layout.addWidget(self.check_resizable_windows) + + self.label_camera_tuning = QtWidgets.QLabel("Camera Tuning") + self.main_layout.addWidget(self.label_camera_tuning) + self.camera_tuning_path = QtWidgets.QLineEdit() + self.main_layout.addWidget(self.camera_tuning_path) + + self.label_connect_timeout = QtWidgets.QLabel("Connect Timeout (ms)") + self.main_layout.addWidget(self.label_connect_timeout) + self.spin_connect_timeout = QtWidgets.QSpinBox() + self.spin_connect_timeout.setMinimum(1) + self.spin_connect_timeout.setMaximum(60000) + self.spin_connect_timeout.setValue(self.DEPTHAI_CONNECT_TIMEOUT_DEFAULT) + self.main_layout.addWidget(self.spin_connect_timeout) + + self.label_boot_timeout = QtWidgets.QLabel("Bootup Timeout (ms)") + self.main_layout.addWidget(self.label_boot_timeout) + self.spin_boot_timeout = QtWidgets.QSpinBox() + self.spin_boot_timeout.setMinimum(1) + self.spin_boot_timeout.setMaximum(60000) + self.spin_boot_timeout.setValue(self.DEPTHAI_BOOTUP_TIMEOUT_DEFAULT) + self.main_layout.addWidget(self.spin_boot_timeout) + + self.label_available_devices = QtWidgets.QLabel("Available Devices") + self.main_layout.addWidget(self.label_available_devices) + self.available_devices_combo = QtWidgets.QComboBox() + self.main_layout.addWidget(self.available_devices_combo) + + self.connect_button = QtWidgets.QPushButton("Connect") + self.connect_button.clicked.connect(self.app.connect) + self.main_layout.addWidget(self.connect_button) + + self.disconnect_button = QtWidgets.QPushButton("Disconnect") + self.disconnect_button.clicked.connect(self.app.disconnect) + self.main_layout.addWidget(self.disconnect_button) + self.disconnect_button.setHidden(True) + + + + def handle_disconnect(self): + self.connect_button.setDisabled(False) + self.disconnect_button.setDisabled(True) + self.disconnect_button.setHidden(True) + self.connect_button.setHidden(False) + self.add_cam_button.setDisabled(False) + self.mono_resolution_combo.setDisabled(False) + self.combo_color_resolution.setDisabled(False) + self.combo_rotate.setDisabled(False) + self.spin_fps.setDisabled(False) + self.spin_isp_downscale.setDisabled(False) + self.check_resizable_windows.setDisabled(False) + self.camera_tuning_path.setDisabled(False) + self.available_devices_combo.setDisabled(False) + for i in range(self.cameras_list.count()): + self.cameras_list.itemAt(i).itemAt(2).widget().setDisabled(False) + + def handle_connect(self): + self.connect_button.setDisabled(True) + self.disconnect_button.setDisabled(False) + self.disconnect_button.setHidden(False) + self.connect_button.setHidden(True) + self.add_cam_button.setDisabled(True) + self.mono_resolution_combo.setDisabled(True) + self.combo_color_resolution.setDisabled(True) + self.combo_rotate.setDisabled(True) + self.spin_fps.setDisabled(True) + self.spin_isp_downscale.setDisabled(True) + self.check_resizable_windows.setDisabled(True) + self.camera_tuning_path.setDisabled(True) + self.available_devices_combo.setDisabled(True) + for i in range(self.cameras_list.count()): + self.cameras_list.itemAt(i).itemAt(2).widget().setDisabled(True) + +class Application(QtWidgets.QMainWindow): + + def __init__(self): + super().__init__() + self.available_devices: List[dai.DeviceInfo] = [] + self.ui = CamTestGui(self) + self.query_devices() + self.query_devices_timer = QtCore.QTimer() + self.query_devices_timer.timeout.connect(self.query_devices) + self.query_devices_timer.start(1000) + self.test_process = None + + def construct_args_from_gui(self) -> List[str]: + if not self.available_devices: + return [] + self.device = self.available_devices[self.ui.available_devices_combo.currentIndex()].mxid + cmd = [f"{os.path.dirname(__file__)}/cam_test.py"] + cmd.append("--cameras") + for i in range(self.ui.cameras_list.count()): + hbox = self.ui.cameras_list.itemAt(i) + cam_combo = hbox.itemAt(0) + cam_type_combo = hbox.itemAt(1) + cam = cam_combo.widget().currentText() + cam_type = cam_type_combo.widget().currentText() + cmd.append(f"{cam},{cam_type[0]}") + cmd.append("-mres") + cmd.append(self.ui.mono_resolution_combo.currentText()) + cmd.append("-cres") + cmd.append(self.ui.combo_color_resolution.currentText()) + if self.ui.combo_rotate.currentText() != "disabled": + cmd.append("-rot") + cmd.append(self.ui.combo_rotate.currentText()) + cmd.append("-fps") + cmd.append(str(self.ui.spin_fps.value())) + cmd.append("-ds") + cmd.append(str(self.ui.spin_isp_downscale.value())) + if self.ui.check_resizable_windows.isChecked(): + cmd.append("-rs") + if self.ui.camera_tuning_path.text(): + cmd.append("-tun") + cmd.append(self.ui.camera_tuning_path.text()) + cmd.append("--device") + cmd.append(self.device) + cmd.append("--connection-timeout") + cmd.append(str(self.ui.spin_connect_timeout.value())) + cmd.append("--boot-timeout") + cmd.append(str(self.ui.spin_boot_timeout.value())) + return cmd + + def connect(self): + args = self.construct_args_from_gui() + if not args: + return + self.test_process = QtCore.QProcess() + self.test_process.finished.connect(self.disconnect) + self.test_process.start(sys.executable, args) + self.query_devices_timer.stop() + self.ui.handle_connect() + + def disconnect(self): + self.query_devices_timer.start() + self.ui.handle_disconnect() + if self.test_process.state() == QtCore.QProcess.Running: + self.test_process.terminate() + + def query_devices(self): + self.ui.available_devices_combo.clear() + self.available_devices = dai.Device.getAllAvailableDevices() + self.ui.available_devices_combo.addItems(list(map(lambda d: f"{d.name} ({d.getMxId()})", self.available_devices))) + +def main(): + signal.signal(signal.SIGINT, signal.SIG_DFL) + app = QtWidgets.QApplication(sys.argv) + application = Application() + application.show() + sys.exit(app.exec_()) From e646d7079dbc339bdf614d18d3189ead4170f020 Mon Sep 17 00:00:00 2001 From: zrezke Date: Wed, 8 Mar 2023 14:29:13 +0100 Subject: [PATCH 02/19] use sys.executable properly, remove unused imports --- utilities/cam_test_gui.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/utilities/cam_test_gui.py b/utilities/cam_test_gui.py index 8a968966d..844645534 100644 --- a/utilities/cam_test_gui.py +++ b/utilities/cam_test_gui.py @@ -1,10 +1,7 @@ from typing import List -import PyQt5.sip from PyQt5 import QtCore, QtGui, QtWidgets -import subprocess import depthai as dai import sys -import os import signal @@ -193,7 +190,7 @@ def construct_args_from_gui(self) -> List[str]: if not self.available_devices: return [] self.device = self.available_devices[self.ui.available_devices_combo.currentIndex()].mxid - cmd = [f"{os.path.dirname(__file__)}/cam_test.py"] + cmd = [] cmd.append("--cameras") for i in range(self.ui.cameras_list.count()): hbox = self.ui.cameras_list.itemAt(i) From 5b36ec170aaec70a834b171898bf0e8cc0aea938 Mon Sep 17 00:00:00 2001 From: zrezke Date: Wed, 8 Mar 2023 14:54:23 +0100 Subject: [PATCH 03/19] add isp3afps support --- utilities/cam_test_gui.py | 27 +++++++++++++++++++++------ 1 file changed, 21 insertions(+), 6 deletions(-) diff --git a/utilities/cam_test_gui.py b/utilities/cam_test_gui.py index 844645534..4e310914c 100644 --- a/utilities/cam_test_gui.py +++ b/utilities/cam_test_gui.py @@ -90,6 +90,14 @@ def __init__(self, app: "Application"): self.spin_fps.setValue(30) self.main_layout.addWidget(self.spin_fps) + self.label_isp3afps = QtWidgets.QLabel("ISP3 AFPS") + self.main_layout.addWidget(self.label_isp3afps) + self.spin_isp3afps = QtWidgets.QSpinBox() + self.spin_isp3afps.setMinimum(1) + self.spin_isp3afps.setMaximum(120) + self.spin_isp3afps.setValue(0) + self.main_layout.addWidget(self.spin_isp3afps) + self.label_isp_downscale = QtWidgets.QLabel("ISP Downscale") self.main_layout.addWidget(self.label_isp_downscale) self.spin_isp_downscale = QtWidgets.QSpinBox() @@ -102,7 +110,7 @@ def __init__(self, app: "Application"): self.main_layout.addWidget(self.label_resizable_windows) self.check_resizable_windows = QtWidgets.QCheckBox() self.main_layout.addWidget(self.check_resizable_windows) - + self.label_camera_tuning = QtWidgets.QLabel("Camera Tuning") self.main_layout.addWidget(self.label_camera_tuning) self.camera_tuning_path = QtWidgets.QLineEdit() @@ -113,7 +121,8 @@ def __init__(self, app: "Application"): self.spin_connect_timeout = QtWidgets.QSpinBox() self.spin_connect_timeout.setMinimum(1) self.spin_connect_timeout.setMaximum(60000) - self.spin_connect_timeout.setValue(self.DEPTHAI_CONNECT_TIMEOUT_DEFAULT) + self.spin_connect_timeout.setValue( + self.DEPTHAI_CONNECT_TIMEOUT_DEFAULT) self.main_layout.addWidget(self.spin_connect_timeout) self.label_boot_timeout = QtWidgets.QLabel("Bootup Timeout (ms)") @@ -138,8 +147,6 @@ def __init__(self, app: "Application"): self.main_layout.addWidget(self.disconnect_button) self.disconnect_button.setHidden(True) - - def handle_disconnect(self): self.connect_button.setDisabled(False) self.disconnect_button.setDisabled(True) @@ -150,6 +157,7 @@ def handle_disconnect(self): self.combo_color_resolution.setDisabled(False) self.combo_rotate.setDisabled(False) self.spin_fps.setDisabled(False) + self.spin_isp3afps.setDisabled(False) self.spin_isp_downscale.setDisabled(False) self.check_resizable_windows.setDisabled(False) self.camera_tuning_path.setDisabled(False) @@ -167,6 +175,7 @@ def handle_connect(self): self.combo_color_resolution.setDisabled(True) self.combo_rotate.setDisabled(True) self.spin_fps.setDisabled(True) + self.spin_isp3afps.setDisabled(True) self.spin_isp_downscale.setDisabled(True) self.check_resizable_windows.setDisabled(True) self.camera_tuning_path.setDisabled(True) @@ -174,6 +183,7 @@ def handle_connect(self): for i in range(self.cameras_list.count()): self.cameras_list.itemAt(i).itemAt(2).widget().setDisabled(True) + class Application(QtWidgets.QMainWindow): def __init__(self): @@ -189,7 +199,8 @@ def __init__(self): def construct_args_from_gui(self) -> List[str]: if not self.available_devices: return [] - self.device = self.available_devices[self.ui.available_devices_combo.currentIndex()].mxid + self.device = self.available_devices[self.ui.available_devices_combo.currentIndex( + )].mxid cmd = [] cmd.append("--cameras") for i in range(self.ui.cameras_list.count()): @@ -208,6 +219,8 @@ def construct_args_from_gui(self) -> List[str]: cmd.append(self.ui.combo_rotate.currentText()) cmd.append("-fps") cmd.append(str(self.ui.spin_fps.value())) + cmd.append("-isp3afps") + cmd.append(str(self.ui.spin_isp3afps.value())) cmd.append("-ds") cmd.append(str(self.ui.spin_isp_downscale.value())) if self.ui.check_resizable_windows.isChecked(): @@ -242,7 +255,9 @@ def disconnect(self): def query_devices(self): self.ui.available_devices_combo.clear() self.available_devices = dai.Device.getAllAvailableDevices() - self.ui.available_devices_combo.addItems(list(map(lambda d: f"{d.name} ({d.getMxId()})", self.available_devices))) + self.ui.available_devices_combo.addItems( + list(map(lambda d: f"{d.name} ({d.getMxId()})", self.available_devices))) + def main(): signal.signal(signal.SIGINT, signal.SIG_DFL) From 898d04fa2427160e60dfde8aa72e41895c87490e Mon Sep 17 00:00:00 2001 From: zrezke Date: Wed, 8 Mar 2023 15:04:35 +0100 Subject: [PATCH 04/19] minor fixes --- utilities/cam_test_gui.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/utilities/cam_test_gui.py b/utilities/cam_test_gui.py index 4e310914c..b8debc61d 100644 --- a/utilities/cam_test_gui.py +++ b/utilities/cam_test_gui.py @@ -93,7 +93,7 @@ def __init__(self, app: "Application"): self.label_isp3afps = QtWidgets.QLabel("ISP3 AFPS") self.main_layout.addWidget(self.label_isp3afps) self.spin_isp3afps = QtWidgets.QSpinBox() - self.spin_isp3afps.setMinimum(1) + self.spin_isp3afps.setMinimum(0) self.spin_isp3afps.setMaximum(120) self.spin_isp3afps.setValue(0) self.main_layout.addWidget(self.spin_isp3afps) @@ -242,7 +242,7 @@ def connect(self): return self.test_process = QtCore.QProcess() self.test_process.finished.connect(self.disconnect) - self.test_process.start(sys.executable, args) + self.test_process.start(sys.executable, sys.argv + args) self.query_devices_timer.stop() self.ui.handle_connect() From 63cd8b7b7ad3da90ede6905a4768c8a9b2e624a7 Mon Sep 17 00:00:00 2001 From: zrezke Date: Wed, 8 Mar 2023 15:19:55 +0100 Subject: [PATCH 05/19] forward channels and detect pyinstaller env to run cam_test.py correctly --- utilities/cam_test_gui.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/utilities/cam_test_gui.py b/utilities/cam_test_gui.py index b8debc61d..46eb7a008 100644 --- a/utilities/cam_test_gui.py +++ b/utilities/cam_test_gui.py @@ -241,8 +241,13 @@ def connect(self): if not args: return self.test_process = QtCore.QProcess() + # Forward stdout + self.test_process.setProcessChannelMode(QtCore.QProcess.ProcessChannelMode.ForwardedChannels) self.test_process.finished.connect(self.disconnect) - self.test_process.start(sys.executable, sys.argv + args) + if getattr(sys, 'frozen', False): + self.test_process.start(sys.executable, args) + else: + self.test_process.start(sys.executable, sys.argv + args) self.query_devices_timer.stop() self.ui.handle_connect() From 50fbc3a398288dbc532845d201c41ebacc9ac5fc Mon Sep 17 00:00:00 2001 From: zrezke Date: Wed, 8 Mar 2023 15:50:28 +0100 Subject: [PATCH 06/19] windows support --- utilities/README.md | 16 ++++++++++++++++ utilities/cam_test_gui.py | 34 ++++++++++++++++++++++++++++++---- 2 files changed, 46 insertions(+), 4 deletions(-) diff --git a/utilities/README.md b/utilities/README.md index 0f87c2e34..2c039f025 100644 --- a/utilities/README.md +++ b/utilities/README.md @@ -24,3 +24,19 @@ pyinstaller --onefile -w --icon=assets/icon.ico --add-data="assets/icon.ico;asse ``` Optionally, append `--runtime-tmpdir [path or .]` to modify where the temporary directory should be created when launched. + + +## Cam Test +### Standalone executable +Requirements: +``` +# Linux/macOS +python3 -m pip install pyinstaller +# Windows +python -m pip install pyinstaller +``` + +To build standalone executable issue the following command: +```sh +pyinstaller --onefile -w cam_test.py --hidden-import PyQt5.sip +``` diff --git a/utilities/cam_test_gui.py b/utilities/cam_test_gui.py index 46eb7a008..9e9f51960 100644 --- a/utilities/cam_test_gui.py +++ b/utilities/cam_test_gui.py @@ -184,16 +184,34 @@ def handle_connect(self): self.cameras_list.itemAt(i).itemAt(2).widget().setDisabled(True) +class WorkerSignals(QtCore.QObject): + finished = QtCore.pyqtSignal(list) + +class Worker(QtCore.QRunnable): + + def __init__(self, fn, *args, **kwargs): + super().__init__() + self.fn = fn + self.args = args + self.kwargs = kwargs + self.signals = WorkerSignals() + + @QtCore.pyqtSlot() + def run(self): + result = self.fn(*self.args, **self.kwargs) + self.signals.finished.emit(result) + + class Application(QtWidgets.QMainWindow): def __init__(self): super().__init__() self.available_devices: List[dai.DeviceInfo] = [] self.ui = CamTestGui(self) - self.query_devices() self.query_devices_timer = QtCore.QTimer() self.query_devices_timer.timeout.connect(self.query_devices) - self.query_devices_timer.start(1000) + self.query_devices_timer.start(2000) + self.query_devices() self.test_process = None def construct_args_from_gui(self) -> List[str]: @@ -255,13 +273,21 @@ def disconnect(self): self.query_devices_timer.start() self.ui.handle_disconnect() if self.test_process.state() == QtCore.QProcess.Running: - self.test_process.terminate() + self.test_process.kill() def query_devices(self): + self.query_devices_timer.stop() + pool = QtCore.QThreadPool.globalInstance() + query_devices_worker = Worker(dai.Device.getAllAvailableDevices) + query_devices_worker.signals.finished.connect(self.on_finish_query_devices) + pool.start(query_devices_worker) + + def on_finish_query_devices(self, result): self.ui.available_devices_combo.clear() - self.available_devices = dai.Device.getAllAvailableDevices() + self.available_devices = result self.ui.available_devices_combo.addItems( list(map(lambda d: f"{d.name} ({d.getMxId()})", self.available_devices))) + self.query_devices_timer.start() def main(): From e02e0b947f92d97153677923a16c769acdee52de Mon Sep 17 00:00:00 2001 From: zrezke Date: Thu, 9 Mar 2023 13:20:16 +0100 Subject: [PATCH 07/19] tried to start detached, the problem is pyinstaller launches two instances of the program --- utilities/cam_test.py | 8 ++++++++ utilities/cam_test_gui.py | 31 +++++++++++++++++++++++++++---- 2 files changed, 35 insertions(+), 4 deletions(-) diff --git a/utilities/cam_test.py b/utilities/cam_test.py index ca48429f5..fb8bbf999 100755 --- a/utilities/cam_test.py +++ b/utilities/cam_test.py @@ -43,6 +43,7 @@ from pathlib import Path import sys import cam_test_gui +import signal def socket_type_pair(arg): @@ -208,6 +209,13 @@ def get(self): if args.camera_tuning: pipeline.setCameraTuningBlobPath(str(args.camera_tuning)) +def exit_cleanly(signum, frame): + print("Exiting cleanly") + cv2.destroyAllWindows() + sys.exit(0) + +signal.signal(signal.SIGINT, exit_cleanly) + # Pipeline is defined, now we can connect to the device device = dai.Device.getDeviceByMxId(args.device) diff --git a/utilities/cam_test_gui.py b/utilities/cam_test_gui.py index 9e9f51960..9a0fe89f4 100644 --- a/utilities/cam_test_gui.py +++ b/utilities/cam_test_gui.py @@ -3,6 +3,7 @@ import depthai as dai import sys import signal +import os class CamTestGui: @@ -147,6 +148,11 @@ def __init__(self, app: "Application"): self.main_layout.addWidget(self.disconnect_button) self.disconnect_button.setHidden(True) + self.process_label = QtWidgets.QLabel("Process") + self.pid_label = QtWidgets.QLabel("") + self.main_layout.addWidget(self.process_label) + self.main_layout.addWidget(self.pid_label) + def handle_disconnect(self): self.connect_button.setDisabled(False) self.disconnect_button.setDisabled(True) @@ -163,9 +169,16 @@ def handle_disconnect(self): self.camera_tuning_path.setDisabled(False) self.available_devices_combo.setDisabled(False) for i in range(self.cameras_list.count()): + self.cameras_list.itemAt(i).itemAt(0).widget().setDisabled(False) + self.cameras_list.itemAt(i).itemAt(1).widget().setDisabled(False) self.cameras_list.itemAt(i).itemAt(2).widget().setDisabled(False) + self.spin_connect_timeout.setDisabled(False) + self.spin_boot_timeout.setDisabled(False) + def handle_connect(self): + self.spin_boot_timeout.setDisabled(True) + self.spin_connect_timeout.setDisabled(True) self.connect_button.setDisabled(True) self.disconnect_button.setDisabled(False) self.disconnect_button.setHidden(False) @@ -181,6 +194,8 @@ def handle_connect(self): self.camera_tuning_path.setDisabled(True) self.available_devices_combo.setDisabled(True) for i in range(self.cameras_list.count()): + self.cameras_list.itemAt(i).itemAt(0).widget().setDisabled(True) + self.cameras_list.itemAt(i).itemAt(1).widget().setDisabled(True) self.cameras_list.itemAt(i).itemAt(2).widget().setDisabled(True) @@ -262,18 +277,26 @@ def connect(self): # Forward stdout self.test_process.setProcessChannelMode(QtCore.QProcess.ProcessChannelMode.ForwardedChannels) self.test_process.finished.connect(self.disconnect) + started_successfully = False + # Start detached process with the function that also returns the PID if getattr(sys, 'frozen', False): - self.test_process.start(sys.executable, args) + started_successfully, self.test_process_pid = self.test_process.startDetached(sys.executable, args, "") else: - self.test_process.start(sys.executable, sys.argv + args) + started_successfully, self.test_process_pid = self.test_process.startDetached(sys.executable, sys.argv + args, "") + if not started_successfully: + self.test_process_pid = None + self.disconnect() + return + self.ui.pid_label.setText(f"PID: {self.test_process_pid}") self.query_devices_timer.stop() self.ui.handle_connect() def disconnect(self): + if self.test_process_pid: + os.kill(self.test_process_pid, signal.SIGINT) + self.test_process_pid = None self.query_devices_timer.start() self.ui.handle_disconnect() - if self.test_process.state() == QtCore.QProcess.Running: - self.test_process.kill() def query_devices(self): self.query_devices_timer.stop() From d99a49e289ae0a167e608682d49b83d9d667cb7c Mon Sep 17 00:00:00 2001 From: zrezke Date: Thu, 9 Mar 2023 13:56:47 +0100 Subject: [PATCH 08/19] small ui fix and readme fix --- utilities/README.md | 8 +++++--- utilities/cam_test_gui.py | 13 ++++++------- 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/utilities/README.md b/utilities/README.md index 2c039f025..2209b1d0f 100644 --- a/utilities/README.md +++ b/utilities/README.md @@ -27,7 +27,7 @@ Optionally, append `--runtime-tmpdir [path or .]` to modify where the temporary ## Cam Test -### Standalone executable +### Bundled executable Requirements: ``` # Linux/macOS @@ -36,7 +36,9 @@ python3 -m pip install pyinstaller python -m pip install pyinstaller ``` -To build standalone executable issue the following command: +To build a bundled executable issue the following command: ```sh -pyinstaller --onefile -w cam_test.py --hidden-import PyQt5.sip +pyinstaller -w cam_test.py --hidden-import PyQt5.sip ``` + +The executable will be located in `dist/cam_test` folder. diff --git a/utilities/cam_test_gui.py b/utilities/cam_test_gui.py index 9a0fe89f4..27cbe9b3d 100644 --- a/utilities/cam_test_gui.py +++ b/utilities/cam_test_gui.py @@ -148,13 +148,9 @@ def __init__(self, app: "Application"): self.main_layout.addWidget(self.disconnect_button) self.disconnect_button.setHidden(True) - self.process_label = QtWidgets.QLabel("Process") - self.pid_label = QtWidgets.QLabel("") - self.main_layout.addWidget(self.process_label) - self.main_layout.addWidget(self.pid_label) - def handle_disconnect(self): - self.connect_button.setDisabled(False) + self.available_devices_combo.clear() + self.connect_button.setDisabled(True) self.disconnect_button.setDisabled(True) self.disconnect_button.setHidden(True) self.connect_button.setHidden(False) @@ -287,7 +283,6 @@ def connect(self): self.test_process_pid = None self.disconnect() return - self.ui.pid_label.setText(f"PID: {self.test_process_pid}") self.query_devices_timer.stop() self.ui.handle_connect() @@ -311,6 +306,10 @@ def on_finish_query_devices(self, result): self.ui.available_devices_combo.addItems( list(map(lambda d: f"{d.name} ({d.getMxId()})", self.available_devices))) self.query_devices_timer.start() + if self.available_devices: + self.ui.connect_button.setDisabled(False) + else: + self.ui.connect_button.setDisabled(True) def main(): From b399434a372192eb9f594af08e6b2f933d790e76 Mon Sep 17 00:00:00 2001 From: zrezke Date: Thu, 9 Mar 2023 20:11:45 +0100 Subject: [PATCH 09/19] added automode and made it a bit more failproof --- utilities/cam_test_gui.py | 99 +++++++++++++++++++++++++++++++-------- 1 file changed, 79 insertions(+), 20 deletions(-) diff --git a/utilities/cam_test_gui.py b/utilities/cam_test_gui.py index 27cbe9b3d..77c91d0c6 100644 --- a/utilities/cam_test_gui.py +++ b/utilities/cam_test_gui.py @@ -49,9 +49,13 @@ def __init__(self, app: "Application"): self.app = app self.app.setWindowTitle("Camera Test") self.main_widget = QtWidgets.QWidget() + self.scroll_widget = QtWidgets.QScrollArea() + self.scroll_widget.setWidget(self.main_widget) + self.scroll_widget.setWidgetResizable(True) + self.scroll_widget.setMinimumHeight(500) self.main_layout = QtWidgets.QVBoxLayout() self.main_widget.setLayout(self.main_layout) - self.app.setCentralWidget(self.main_widget) + self.app.setCentralWidget(self.scroll_widget) self.label_cameras = QtWidgets.QLabel("Cameras") self.main_layout.addWidget(self.label_cameras) @@ -139,21 +143,33 @@ def __init__(self, app: "Application"): self.available_devices_combo = QtWidgets.QComboBox() self.main_layout.addWidget(self.available_devices_combo) + self.connect_layout = QtWidgets.QHBoxLayout() self.connect_button = QtWidgets.QPushButton("Connect") self.connect_button.clicked.connect(self.app.connect) - self.main_layout.addWidget(self.connect_button) + self.connect_layout.addWidget(self.connect_button) + self.check_auto_mode = QtWidgets.QCheckBox("Auto Mode") + self.check_auto_mode.setToolTip( + "Whenever a device is available, connect to it automatically") + self.connect_layout.addWidget(self.check_auto_mode) + self.check_auto_mode.setChecked(False) + self.main_layout.addLayout(self.connect_layout) self.disconnect_button = QtWidgets.QPushButton("Disconnect") self.disconnect_button.clicked.connect(self.app.disconnect) self.main_layout.addWidget(self.disconnect_button) self.disconnect_button.setHidden(True) + def handle_automode_changed(self, state): + self.disconnect_button.setHidden(bool(state)) + self.connect_button.setHidden(bool(state)) + def handle_disconnect(self): self.available_devices_combo.clear() - self.connect_button.setDisabled(True) - self.disconnect_button.setDisabled(True) - self.disconnect_button.setHidden(True) - self.connect_button.setHidden(False) + if not self.check_auto_mode.isChecked(): + self.connect_button.setDisabled(True) + self.disconnect_button.setDisabled(True) + self.disconnect_button.setHidden(True) + self.connect_button.setHidden(False) self.add_cam_button.setDisabled(False) self.mono_resolution_combo.setDisabled(False) self.combo_color_resolution.setDisabled(False) @@ -171,14 +187,14 @@ def handle_disconnect(self): self.spin_connect_timeout.setDisabled(False) self.spin_boot_timeout.setDisabled(False) - def handle_connect(self): self.spin_boot_timeout.setDisabled(True) self.spin_connect_timeout.setDisabled(True) - self.connect_button.setDisabled(True) - self.disconnect_button.setDisabled(False) - self.disconnect_button.setHidden(False) - self.connect_button.setHidden(True) + if not self.check_auto_mode.isChecked(): + self.connect_button.setDisabled(True) + self.disconnect_button.setDisabled(False) + self.disconnect_button.setHidden(False) + self.connect_button.setHidden(True) self.add_cam_button.setDisabled(True) self.mono_resolution_combo.setDisabled(True) self.combo_color_resolution.setDisabled(True) @@ -198,6 +214,7 @@ def handle_connect(self): class WorkerSignals(QtCore.QObject): finished = QtCore.pyqtSignal(list) + class Worker(QtCore.QRunnable): def __init__(self, fn, *args, **kwargs): @@ -206,7 +223,7 @@ def __init__(self, fn, *args, **kwargs): self.args = args self.kwargs = kwargs self.signals = WorkerSignals() - + @QtCore.pyqtSlot() def run(self): result = self.fn(*self.args, **self.kwargs) @@ -223,7 +240,24 @@ def __init__(self): self.query_devices_timer.timeout.connect(self.query_devices) self.query_devices_timer.start(2000) self.query_devices() - self.test_process = None + + self.test_process_pid = None + + # Once the test process is started, periodically check if it's still running (catches eg. camera unplugged) + self.check_test_process_timer = QtCore.QTimer() + self.check_test_process_timer.timeout.connect(self.check_test_process) + + self.ui.check_auto_mode.stateChanged.connect(self.automode_changed) + + def closeEvent(self, a0: QtGui.QCloseEvent) -> None: + if self.test_process_pid: + os.kill(self.test_process_pid, signal.SIGINT) + return super().closeEvent(a0) + + def automode_changed(self, state): + self.ui.handle_automode_changed(state) + if not state: + self.disconnect() def construct_args_from_gui(self) -> List[str]: if not self.available_devices: @@ -265,30 +299,46 @@ def construct_args_from_gui(self) -> List[str]: cmd.append(str(self.ui.spin_boot_timeout.value())) return cmd + def check_test_process(self): + # Raises OSError if a process with the given PID doesn't exist + try: + os.kill(self.test_process_pid, 0) + except (OSError, TypeError): + self.test_process_pid = None + self.disconnect() + self.check_test_process_timer.stop() + def connect(self): args = self.construct_args_from_gui() if not args: return + started_successfully = False + self.test_process = QtCore.QProcess() # Forward stdout - self.test_process.setProcessChannelMode(QtCore.QProcess.ProcessChannelMode.ForwardedChannels) - self.test_process.finished.connect(self.disconnect) - started_successfully = False + self.test_process.setProcessChannelMode( + QtCore.QProcess.ProcessChannelMode.ForwardedChannels) # Start detached process with the function that also returns the PID if getattr(sys, 'frozen', False): - started_successfully, self.test_process_pid = self.test_process.startDetached(sys.executable, args, "") + started_successfully, self.test_process_pid = self.test_process.startDetached( + sys.executable, args, "") else: - started_successfully, self.test_process_pid = self.test_process.startDetached(sys.executable, sys.argv + args, "") + started_successfully, self.test_process_pid = self.test_process.startDetached( + sys.executable, sys.argv + args, "") if not started_successfully: self.test_process_pid = None self.disconnect() return self.query_devices_timer.stop() + self.check_test_process_timer.start(1000) self.ui.handle_connect() def disconnect(self): if self.test_process_pid: - os.kill(self.test_process_pid, signal.SIGINT) + try: + os.kill(self.test_process_pid, signal.SIGINT) + except OSError: + self.test_process_pid = None self.test_process_pid = None self.query_devices_timer.start() self.ui.handle_disconnect() @@ -297,16 +347,25 @@ def query_devices(self): self.query_devices_timer.stop() pool = QtCore.QThreadPool.globalInstance() query_devices_worker = Worker(dai.Device.getAllAvailableDevices) - query_devices_worker.signals.finished.connect(self.on_finish_query_devices) + query_devices_worker.signals.finished.connect( + self.on_finish_query_devices) pool.start(query_devices_worker) def on_finish_query_devices(self, result): + current_device = self.ui.available_devices_combo.currentText() self.ui.available_devices_combo.clear() self.available_devices = result self.ui.available_devices_combo.addItems( list(map(lambda d: f"{d.name} ({d.getMxId()})", self.available_devices))) self.query_devices_timer.start() if self.available_devices: + if current_device: + index = self.ui.available_devices_combo.findText( + current_device) + if index != -1: + self.ui.available_devices_combo.setCurrentIndex(index) + if self.ui.check_auto_mode.isChecked(): + self.connect() self.ui.connect_button.setDisabled(False) else: self.ui.connect_button.setDisabled(True) From 43a20306cc4400ebbc1dd0364319569f76d191e9 Mon Sep 17 00:00:00 2001 From: zrezke Date: Thu, 9 Mar 2023 19:39:51 +0100 Subject: [PATCH 10/19] fix for windows --- utilities/cam_test.py | 6 +++++- utilities/cam_test_gui.py | 18 +++++++----------- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/utilities/cam_test.py b/utilities/cam_test.py index fb8bbf999..18e6c3fc7 100755 --- a/utilities/cam_test.py +++ b/utilities/cam_test.py @@ -300,7 +300,11 @@ def exit_cleanly(signum, frame): capture_list = [] while True: for c in cam_list: - pkt = q[c].tryGet() + try: + pkt = q[c].tryGet() + except Exception as e: + print(e) + exit_cleanly(0, 0) if pkt is not None: fps_host[c].update() fps_capt[c].update(pkt.getTimestamp().total_seconds()) diff --git a/utilities/cam_test_gui.py b/utilities/cam_test_gui.py index 77c91d0c6..0617d8b90 100644 --- a/utilities/cam_test_gui.py +++ b/utilities/cam_test_gui.py @@ -4,6 +4,7 @@ import sys import signal import os +import psutil class CamTestGui: @@ -300,24 +301,19 @@ def construct_args_from_gui(self) -> List[str]: return cmd def check_test_process(self): - # Raises OSError if a process with the given PID doesn't exist - try: - os.kill(self.test_process_pid, 0) - except (OSError, TypeError): - self.test_process_pid = None - self.disconnect() - self.check_test_process_timer.stop() + if self.test_process_pid and psutil.pid_exists(self.test_process_pid): + return + self.test_process_pid = None + self.disconnect() + self.check_test_process_timer.stop() def connect(self): args = self.construct_args_from_gui() if not args: return + started_successfully = False - self.test_process = QtCore.QProcess() - # Forward stdout - self.test_process.setProcessChannelMode( - QtCore.QProcess.ProcessChannelMode.ForwardedChannels) # Start detached process with the function that also returns the PID if getattr(sys, 'frozen', False): started_successfully, self.test_process_pid = self.test_process.startDetached( From 0bb766523fe05b2918b4d5d7f557ba653b69cf65 Mon Sep 17 00:00:00 2001 From: zrezke Date: Sat, 1 Apr 2023 16:55:57 +0200 Subject: [PATCH 11/19] add requirements --- utilities/requirements.txt | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/utilities/requirements.txt b/utilities/requirements.txt index dfdbfd329..965875a35 100644 --- a/utilities/requirements.txt +++ b/utilities/requirements.txt @@ -1,2 +1,6 @@ PySimpleGUI==4.60.3 Pillow==9.3.0 +psutil==5.9.3 +pyqt5==5.15.9 +opencv-python==4.7.* +numpy==1.24.1 From d475353b882c7a93d22b400a2a3a3427dcb0dc20 Mon Sep 17 00:00:00 2001 From: zrezke Date: Sat, 1 Apr 2023 16:58:04 +0200 Subject: [PATCH 12/19] Clarify how to use cam_test in readme --- utilities/README.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/utilities/README.md b/utilities/README.md index 2209b1d0f..3b7259bd1 100644 --- a/utilities/README.md +++ b/utilities/README.md @@ -27,6 +27,13 @@ Optionally, append `--runtime-tmpdir [path or .]` to modify where the temporary ## Cam Test +Run: +```sh +python3 cam_test.py +``` +To start cam test with GUI. +Run cam_test.py with args to start cam test without GUI: + ### Bundled executable Requirements: ``` From e914e27603118d383b524efd31475e813cb12609 Mon Sep 17 00:00:00 2001 From: zrezke Date: Mon, 3 Apr 2023 14:26:29 +0200 Subject: [PATCH 13/19] update opencv, numpy, pyqt requirements to match luxonis/depthai --- utilities/requirements.txt | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/utilities/requirements.txt b/utilities/requirements.txt index 965875a35..ade6e25c8 100644 --- a/utilities/requirements.txt +++ b/utilities/requirements.txt @@ -1,6 +1,7 @@ PySimpleGUI==4.60.3 Pillow==9.3.0 psutil==5.9.3 -pyqt5==5.15.9 -opencv-python==4.7.* -numpy==1.24.1 +numpy>=1.21.4 # For RPi Buster (last successful build) and macOS M1 (first build). But allow for higher versions, to support Python3.11 (not available in 1.21.4 yet) +opencv-contrib-python==4.5.5.62 # Last successful RPi build, also covers M1 with above pinned numpy (otherwise 4.6.0.62 would be required, but that has a bug with charuco boards). Python version not important, abi3 wheels +pyqt5>5,<5.15.6 ; platform_machine != "armv6l" and platform_machine != "armv7l" and platform_machine != "aarch64" and platform_machine != "arm64" +--extra-index-url https://artifacts.luxonis.com/artifactory/luxonis-python-snapshot-local/ From 2bd7899a7676c5db247884f773c86d9daaeff172 Mon Sep 17 00:00:00 2001 From: saching13 Date: Mon, 3 Apr 2023 13:16:08 -0700 Subject: [PATCH 14/19] added distortion model getter --- src/CalibrationHandlerBindings.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/src/CalibrationHandlerBindings.cpp b/src/CalibrationHandlerBindings.cpp index ee7bb9e1b..734692c07 100644 --- a/src/CalibrationHandlerBindings.cpp +++ b/src/CalibrationHandlerBindings.cpp @@ -42,6 +42,7 @@ void CalibrationHandlerBindings::bind(pybind11::module& m, void* pCallstack){ .def("getFov", &CalibrationHandler::getFov, py::arg("cameraId"), py::arg("useSpec") = true, DOC(dai, CalibrationHandler, getFov)) .def("getLensPosition", &CalibrationHandler::getLensPosition, py::arg("cameraId"), DOC(dai, CalibrationHandler, getLensPosition)) + .def("getDistortionModel", &CalibrationHandler::getDistortionModel, py::arg("cameraId"), DOC(dai, CalibrationHandler, getDistortionModel)) .def("getCameraExtrinsics", &CalibrationHandler::getCameraExtrinsics, py::arg("srcCamera"), py::arg("dstCamera"), py::arg("useSpecTranslation") = false, DOC(dai, CalibrationHandler, getCameraExtrinsics)) From cad11b29849d86c64aba5fa4ff9261f1e634c98c Mon Sep 17 00:00:00 2001 From: SzabolcsGergely Date: Tue, 4 Apr 2023 15:41:27 +0300 Subject: [PATCH 15/19] Fix device destructor --- depthai-core | 2 +- src/device_bindings.cpp | 133 ---------------------------------------- src/device_bindings.hpp | 9 --- 3 files changed, 1 insertion(+), 143 deletions(-) delete mode 100644 src/device_bindings.cpp delete mode 100644 src/device_bindings.hpp diff --git a/depthai-core b/depthai-core index f8c459eaa..4b3c687dc 160000 --- a/depthai-core +++ b/depthai-core @@ -1 +1 @@ -Subproject commit f8c459eaa56a814974a62975926be871419c2d5b +Subproject commit 4b3c687dc3058bdc3d55166276ee811d4076f4c3 diff --git a/src/device_bindings.cpp b/src/device_bindings.cpp deleted file mode 100644 index 50a855399..000000000 --- a/src/device_bindings.cpp +++ /dev/null @@ -1,133 +0,0 @@ -#include "device_bindings.hpp" - -//std -#include - -//depthai-core -#include "depthai/device.hpp" -//#include "depthai/host_capture_command.hpp" - -//depthai-shared -//#include "depthai-shared/metadata/capture_metadata.hpp" - -//project -#include "pybind11_common.hpp" - - - - -// Binding for HostDataPacket -namespace py = pybind11; - -void init_binding_device(pybind11::module& m){ - - using namespace dai; - - py::class_(m, "Device") - .def(py::init<>()) - .def(py::init()) - .def(py::init()) - .def( - "create_pipeline", - [](Device& device, py::dict config) - { - - - // str(dict) for string representation uses ['] , but JSON requires ["] - // fast & dirty solution: - std::string str = py::str(config); - boost::replace_all(str, "\'", "\""); - boost::replace_all(str, "None", "null"); - boost::replace_all(str, "True", "true"); - boost::replace_all(str, "False", "false"); - // TODO: make better json serialization - - return device.create_pipeline(str); - }, - "Function for pipeline creation", - py::arg("config") = py::dict() - ) - .def( - "get_available_streams", - &Device::get_available_streams, - "Returns available streams, that possible to retreive from the device." - ) - .def( - "request_jpeg", - &Device::request_jpeg, - "Function to request a still JPEG encoded image ('jpeg' stream must be enabled)" - ) - .def( - "request_af_trigger", - &Device::request_af_trigger, - "Function to request autofocus trigger" - ) - .def( - "request_af_mode", - &Device::request_af_mode, - "Function to request a certain autofocus mode (Check 'AutofocusMode.__members__')" - ) - .def( - "send_disparity_confidence_threshold", - &Device::send_disparity_confidence_threshold, - "Function to send disparity confidence threshold for SGBM" - ) - - .def( - "get_nn_to_depth_bbox_mapping", - &Device::get_nn_to_depth_bbox_mapping, - "Returns NN bounding-box to depth mapping as a dict of coords: off_x, off_y, max_w, max_h." - ) - - // calibration data bindings - .def( - "get_left_intrinsic", - &Device::get_left_intrinsic, - "Returns 3x3 matrix defining the intrinsic parameters of the left camera of the stereo setup." - ) - - .def( - "get_left_homography", - &Device::get_left_homography, - "Returns 3x3 matrix defining the homography to rectify the left camera of the stereo setup." - ) - - .def( - "get_right_intrinsic", - &Device::get_right_intrinsic, - "Returns 3x3 matrix defining the intrinsic parameters of the right camera of the stereo setup." - ) - - .def( - "get_right_homography", - &Device::get_right_homography, - "Returns 3x3 matrix defining the homography to rectify the right camera of the stereo setup." - ) - - .def( - "get_rotation", - &Device::get_rotation, - "Returns 3x3 matrix defining how much the right camera is rotated w.r.t left camera." - ) - - .def( - "get_translation", - &Device::get_translation, - "Returns a vector defining how much the right camera is translated w.r.t left camera." - ) - - - - ; - - - py::enum_(m, "AutofocusMode") - .value("AF_MODE_AUTO", CaptureMetadata::AutofocusMode::AF_MODE_AUTO) - .value("AF_MODE_MACRO", CaptureMetadata::AutofocusMode::AF_MODE_MACRO) - .value("AF_MODE_CONTINUOUS_VIDEO", CaptureMetadata::AutofocusMode::AF_MODE_CONTINUOUS_VIDEO) - .value("AF_MODE_CONTINUOUS_PICTURE", CaptureMetadata::AutofocusMode::AF_MODE_CONTINUOUS_PICTURE) - .value("AF_MODE_EDOF", CaptureMetadata::AutofocusMode::AF_MODE_EDOF) - ; - -} - diff --git a/src/device_bindings.hpp b/src/device_bindings.hpp deleted file mode 100644 index 78a7d43f0..000000000 --- a/src/device_bindings.hpp +++ /dev/null @@ -1,9 +0,0 @@ -#pragma once - -//pybind11 -#include "pybind11_common.hpp" - -// depthai-api -#include "depthai/device.hpp" - -void init_binding_device(pybind11::module& m); \ No newline at end of file From a740e29958482a503968f35fee5b73c10c6d6b85 Mon Sep 17 00:00:00 2001 From: SzabolcsGergely Date: Tue, 4 Apr 2023 16:24:50 +0300 Subject: [PATCH 16/19] Update core --- depthai-core | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/depthai-core b/depthai-core index 4b3c687dc..3e4152a8e 160000 --- a/depthai-core +++ b/depthai-core @@ -1 +1 @@ -Subproject commit 4b3c687dc3058bdc3d55166276ee811d4076f4c3 +Subproject commit 3e4152a8e15989958031b37a7e4c314d14a3754b From e8c50bbdc8c962efdcd05dbb61d57bccbbffaa7c Mon Sep 17 00:00:00 2001 From: SzabolcsGergely Date: Tue, 4 Apr 2023 17:05:43 +0300 Subject: [PATCH 17/19] Revert to clang 14.0 pypi package --- .github/workflows/main.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index d002b5c4c..030265f29 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -49,6 +49,7 @@ jobs: run: | python -m pip install --upgrade pip sudo apt install libusb-1.0-0-dev + python -m pip install clang==14.0 --force-reinstall python -m pip install -r docs/requirements_mkdoc.txt - name: Configure project run: cmake -S . -B build -DDEPTHAI_PYTHON_FORCE_DOCSTRINGS=ON -DDEPTHAI_PYTHON_DOCSTRINGS_OUTPUT="$PWD/docstrings/depthai_python_docstring.hpp" From 2a1d8a101401eacb0a578fcfde3bc8b664a31d97 Mon Sep 17 00:00:00 2001 From: SzabolcsGergely Date: Wed, 5 Apr 2023 01:11:09 +0300 Subject: [PATCH 18/19] Update FW: fix spatial location calculator for 400p/480p resolution --- depthai-core | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/depthai-core b/depthai-core index 3e4152a8e..b21dafae1 160000 --- a/depthai-core +++ b/depthai-core @@ -1 +1 @@ -Subproject commit 3e4152a8e15989958031b37a7e4c314d14a3754b +Subproject commit b21dafae1de6c48c9927f3d56b41c28d1ec10c1c From 96590c6a45dff736e579f4969e947e83b59ade8e Mon Sep 17 00:00:00 2001 From: SzabolcsGergely Date: Wed, 5 Apr 2023 01:23:13 +0300 Subject: [PATCH 19/19] Release v2.21.1.0 --- depthai-core | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/depthai-core b/depthai-core index b21dafae1..0cd6c14ec 160000 --- a/depthai-core +++ b/depthai-core @@ -1 +1 @@ -Subproject commit b21dafae1de6c48c9927f3d56b41c28d1ec10c1c +Subproject commit 0cd6c14ecdf205006b362edc8115b910531d5f08