From bce529271a79aadfe78ef1c52c9219246454975d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Pi=C5=82atowski?= Date: Sat, 8 Jan 2022 11:54:31 +0100 Subject: [PATCH 01/22] refactor qt gui placement in code --- depthai_demo.py | 439 +------------ depthai_helpers/config_manager.py | 12 + gui/.gitignore | 73 --- gui/main.py | 360 ----------- gui/{ => qt}/README.md | 0 gui/{ => qt}/__init__.py | 0 gui/{ => qt}/depthai_demo.pyproject | 0 gui/qt/main.py | 780 ++++++++++++++++++++++++ gui/{ => qt}/views/AIProperties.qml | 0 gui/{ => qt}/views/CameraPreview.qml | 0 gui/{ => qt}/views/CameraProperties.qml | 0 gui/{ => qt}/views/DepthProperties.qml | 0 gui/{ => qt}/views/MiscProperties.qml | 0 gui/{ => qt}/views/root.qml | 0 14 files changed, 799 insertions(+), 865 deletions(-) delete mode 100644 gui/.gitignore delete mode 100644 gui/main.py rename gui/{ => qt}/README.md (100%) rename gui/{ => qt}/__init__.py (100%) rename gui/{ => qt}/depthai_demo.pyproject (100%) create mode 100644 gui/qt/main.py rename gui/{ => qt}/views/AIProperties.qml (100%) rename gui/{ => qt}/views/CameraPreview.qml (100%) rename gui/{ => qt}/views/CameraProperties.qml (100%) rename gui/{ => qt}/views/DepthProperties.qml (100%) rename gui/{ => qt}/views/MiscProperties.qml (100%) rename gui/{ => qt}/views/root.qml (100%) diff --git a/depthai_demo.py b/depthai_demo.py index d9d2e45be..fff0e2a87 100755 --- a/depthai_demo.py +++ b/depthai_demo.py @@ -40,7 +40,7 @@ from log_system_information import make_sys_report from depthai_helpers.supervisor import Supervisor from depthai_helpers.arg_manager import parseArgs -from depthai_helpers.config_manager import ConfigManager, DEPTHAI_ZOO, DEPTHAI_VIDEOS +from depthai_helpers.config_manager import ConfigManager, DEPTHAI_ZOO, DEPTHAI_VIDEOS, prepareConfManager from depthai_helpers.metrics import MetricManager from depthai_helpers.version_check import checkRequirementsVersion from depthai_sdk import FPSHandler, loadModule, getDeviceInfo, downloadYTVideo, Previews, createBlankFrame @@ -544,445 +544,20 @@ def _printSysInfo(self, info): print(','.join(map(str, data.values())), file=self._reportFile) -def prepareConfManager(in_args): - confManager = ConfigManager(in_args) - confManager.linuxCheckApplyUsbRules() - if not confManager.useCamera: - if str(confManager.args.video).startswith('https'): - confManager.args.video = downloadYTVideo(confManager.args.video, DEPTHAI_VIDEOS) - print("Youtube video downloaded.") - if not Path(confManager.args.video).exists(): - raise ValueError("Path {} does not exists!".format(confManager.args.video)) - return confManager - - -def runQt(): - from gui.main import DemoQtGui - from PyQt5.QtWidgets import QMessageBox - from PyQt5.QtCore import QObject, pyqtSignal, QRunnable, QThreadPool - - - class WorkerSignals(QObject): - updateConfSignal = pyqtSignal(list) - updateDownloadProgressSignal = pyqtSignal(int, int) - updatePreviewSignal = pyqtSignal(np.ndarray) - setDataSignal = pyqtSignal(list) - exitSignal = pyqtSignal() - errorSignal = pyqtSignal(str) - - class Worker(QRunnable): - def __init__(self, instance, parent, conf, selectedPreview=None): - super(Worker, self).__init__() - self.running = False - self.selectedPreview = selectedPreview - self.instance = instance - self.parent = parent - self.conf = conf - self.callback_module = loadModule(conf.args.callback) - self.file_callbacks = { - callbackName: getattr(self.callback_module, callbackName) - for callbackName in ["shouldRun", "onNewFrame", "onShowFrame", "onNn", "onReport", "onSetup", "onTeardown", "onIter"] - if callable(getattr(self.callback_module, callbackName, None)) - } - self.instance.setCallbacks(**self.file_callbacks) - self.signals = WorkerSignals() - self.signals.exitSignal.connect(self.terminate) - self.signals.updateConfSignal.connect(self.updateConf) - - - def run(self): - self.running = True - self.signals.setDataSignal.emit(["restartRequired", False]) - self.instance.setCallbacks(shouldRun=self.shouldRun, onShowFrame=self.onShowFrame, onSetup=self.onSetup, onAppSetup=self.onAppSetup, onAppStart=self.onAppStart, showDownloadProgress=self.showDownloadProgress) - self.conf.args.bandwidth = "auto" - if self.conf.args.deviceId is None: - devices = dai.Device.getAllAvailableDevices() - if len(devices) > 0: - defaultDevice = next(map( - lambda info: info.getMxId(), - filter(lambda info: info.desc.protocol == dai.XLinkProtocol.X_LINK_USB_VSC, devices) - ), None) - if defaultDevice is None: - defaultDevice = devices[0].getMxId() - self.conf.args.deviceId = defaultDevice - if Previews.color.name not in self.conf.args.show: - self.conf.args.show.append(Previews.color.name) - if Previews.nnInput.name not in self.conf.args.show: - self.conf.args.show.append(Previews.nnInput.name) - if Previews.depth.name not in self.conf.args.show and Previews.disparityColor.name not in self.conf.args.show: - self.conf.args.show.append(Previews.depth.name) - if Previews.depthRaw.name not in self.conf.args.show and Previews.disparity.name not in self.conf.args.show: - self.conf.args.show.append(Previews.depthRaw.name) - if Previews.left.name not in self.conf.args.show: - self.conf.args.show.append(Previews.left.name) - if Previews.rectifiedLeft.name not in self.conf.args.show: - self.conf.args.show.append(Previews.rectifiedLeft.name) - if Previews.right.name not in self.conf.args.show: - self.conf.args.show.append(Previews.right.name) - if Previews.rectifiedRight.name not in self.conf.args.show: - self.conf.args.show.append(Previews.rectifiedRight.name) - try: - self.instance.run_all(self.conf) - except KeyboardInterrupt: - sys.exit(0) - except Exception as ex: - self.onError(ex) - - def terminate(self): - self.running = False - self.signals.setDataSignal.emit(["restartRequired", False]) - - - def updateConf(self, argsList): - self.conf.args = argparse.Namespace(**dict(argsList)) - - def onError(self, ex: Exception): - self.signals.errorSignal.emit(''.join(traceback.format_tb(ex.__traceback__) + [str(ex)])) - self.signals.setDataSignal.emit(["restartRequired", True]) - - def shouldRun(self): - if "shouldRun" in self.file_callbacks: - return self.running and self.file_callbacks["shouldRun"]() - return self.running - - def onShowFrame(self, frame, source): - if "onShowFrame" in self.file_callbacks: - self.file_callbacks["onShowFrame"](frame, source) - if source == self.selectedPreview: - self.signals.updatePreviewSignal.emit(frame) - - def onAppSetup(self, app): - setupFrame = createBlankFrame(500, 500) - cv2.putText(setupFrame, "Preparing {} app...".format(app.appName), (150, 250), cv2.FONT_HERSHEY_TRIPLEX, 0.5, (255, 255, 255), 4, cv2.LINE_AA) - cv2.putText(setupFrame, "Preparing {} app...".format(app.appName), (150, 250), cv2.FONT_HERSHEY_TRIPLEX, 0.5, (0, 0, 0), 1, cv2.LINE_AA) - self.signals.updatePreviewSignal.emit(setupFrame) - - def onAppStart(self, app): - setupFrame = createBlankFrame(500, 500) - cv2.putText(setupFrame, "Running {} app... (check console)".format(app.appName), (100, 250), cv2.FONT_HERSHEY_TRIPLEX, 0.5, (255, 255, 255), 4, cv2.LINE_AA) - cv2.putText(setupFrame, "Running {} app... (check console)".format(app.appName), (100, 250), cv2.FONT_HERSHEY_TRIPLEX, 0.5, (0, 0, 0), 1, cv2.LINE_AA) - self.signals.updatePreviewSignal.emit(setupFrame) - - def showDownloadProgress(self, curr, total): - self.signals.updateDownloadProgressSignal.emit(curr, total) - - def onSetup(self, instance): - if "onSetup" in self.file_callbacks: - self.file_callbacks["onSetup"](instance) - self.signals.updateConfSignal.emit(list(vars(self.conf.args).items())) - self.signals.setDataSignal.emit(["previewChoices", self.conf.args.show]) - devices = [self.instance._deviceInfo.getMxId()] + list(map(lambda info: info.getMxId(), dai.Device.getAllAvailableDevices())) - self.signals.setDataSignal.emit(["deviceChoices", devices]) - if instance._nnManager is not None: - self.signals.setDataSignal.emit(["countLabels", instance._nnManager._labels]) - else: - self.signals.setDataSignal.emit(["countLabels", []]) - self.signals.setDataSignal.emit(["depthEnabled", self.conf.useDepth]) - self.signals.setDataSignal.emit(["statisticsAccepted", self.instance.metrics is not None]) - self.signals.setDataSignal.emit(["modelChoices", sorted(self.conf.getAvailableZooModels(), key=cmp_to_key(lambda a, b: -1 if a == "mobilenet-ssd" else 1 if b == "mobilenet-ssd" else -1 if a < b else 1))]) - - - class GuiApp(DemoQtGui): - def __init__(self): - super().__init__() - self.confManager = prepareConfManager(args) - self.running = False - self.selectedPreview = self.confManager.args.show[0] if len(self.confManager.args.show) > 0 else "color" - self.useDisparity = False - self.dataInitialized = False - self.appInitialized = False - self.threadpool = QThreadPool() - self._demoInstance = Demo(displayFrames=False) - - def updateArg(self, arg_name, arg_value, shouldUpdate=True): - setattr(self.confManager.args, arg_name, arg_value) - if shouldUpdate: - self.worker.signals.setDataSignal.emit(["restartRequired", True]) - - - def showError(self, error): - print(error, file=sys.stderr) - msgBox = QMessageBox() - msgBox.setIcon(QMessageBox.Critical) - msgBox.setText(error) - msgBox.setWindowTitle("An error occured") - msgBox.setStandardButtons(QMessageBox.Ok) - msgBox.exec() - - def setupDataCollection(self): - try: - with Path(".consent").open() as f: - accepted = json.load(f)["statistics"] - except: - accepted = True - - self._demoInstance.toggleMetrics(accepted) - - def start(self): - self.setupDataCollection() - self.running = True - self.worker = Worker(self._demoInstance, parent=self, conf=self.confManager, selectedPreview=self.selectedPreview) - self.worker.signals.updatePreviewSignal.connect(self.updatePreview) - self.worker.signals.updateDownloadProgressSignal.connect(self.updateDownloadProgress) - self.worker.signals.setDataSignal.connect(self.setData) - self.worker.signals.errorSignal.connect(self.showError) - self.threadpool.start(self.worker) - if not self.appInitialized: - self.appInitialized = True - exit_code = self.startGui() - self.stop(wait=False) - sys.exit(exit_code) - - def stop(self, wait=True): - if hasattr(self._demoInstance, "_device"): - current_mxid = self._demoInstance._device.getMxId() - else: - current_mxid = self.confManager.args.deviceId - self.worker.signals.exitSignal.emit() - self.threadpool.waitForDone(10000) - - if wait and current_mxid is not None: - start = time.time() - while time.time() - start < 30: - if current_mxid in list(map(lambda info: info.getMxId(), dai.Device.getAllAvailableDevices())): - break - else: - time.sleep(0.1) - else: - print(f"[Warning] Device not available again after 30 seconds! MXID: {current_mxid}") - - def restartDemo(self): - self.stop() - self.start() - - def guiOnDepthConfigUpdate(self, median=None, dct=None, sigma=None, lrc=None, lrcThreshold=None): - self._demoInstance._pm.updateDepthConfig(self._demoInstance._device, median=median, dct=dct, sigma=sigma, lrc=lrc, lrcThreshold=lrcThreshold) - if median is not None: - if median == dai.MedianFilter.MEDIAN_OFF: - self.updateArg("stereoMedianSize", 0, False) - elif median == dai.MedianFilter.KERNEL_3x3: - self.updateArg("stereoMedianSize", 3, False) - elif median == dai.MedianFilter.KERNEL_5x5: - self.updateArg("stereoMedianSize", 5, False) - elif median == dai.MedianFilter.KERNEL_7x7: - self.updateArg("stereoMedianSize", 7, False) - if dct is not None: - self.updateArg("disparityConfidenceThreshold", dct, False) - if sigma is not None: - self.updateArg("sigma", sigma, False) - if lrc is not None: - self.updateArg("stereoLrCheck", lrc, False) - if lrcThreshold is not None: - self.updateArg("lrcThreshold", lrcThreshold, False) - - def guiOnCameraConfigUpdate(self, name, exposure=None, sensitivity=None, saturation=None, contrast=None, brightness=None, sharpness=None): - if exposure is not None: - newValue = list(filter(lambda item: item[0] == name, (self.confManager.args.cameraExposure or []))) + [(name, exposure)] - self._demoInstance._cameraConfig["exposure"] = newValue - self.updateArg("cameraExposure", newValue, False) - if sensitivity is not None: - newValue = list(filter(lambda item: item[0] == name, (self.confManager.args.cameraSensitivity or []))) + [(name, sensitivity)] - self._demoInstance._cameraConfig["sensitivity"] = newValue - self.updateArg("cameraSensitivity", newValue, False) - if saturation is not None: - newValue = list(filter(lambda item: item[0] == name, (self.confManager.args.cameraSaturation or []))) + [(name, saturation)] - self._demoInstance._cameraConfig["saturation"] = newValue - self.updateArg("cameraSaturation", newValue, False) - if contrast is not None: - newValue = list(filter(lambda item: item[0] == name, (self.confManager.args.cameraContrast or []))) + [(name, contrast)] - self._demoInstance._cameraConfig["contrast"] = newValue - self.updateArg("cameraContrast", newValue, False) - if brightness is not None: - newValue = list(filter(lambda item: item[0] == name, (self.confManager.args.cameraBrightness or []))) + [(name, brightness)] - self._demoInstance._cameraConfig["brightness"] = newValue - self.updateArg("cameraBrightness", newValue, False) - if sharpness is not None: - newValue = list(filter(lambda item: item[0] == name, (self.confManager.args.cameraSharpness or []))) + [(name, sharpness)] - self._demoInstance._cameraConfig["sharpness"] = newValue - self.updateArg("cameraSharpness", newValue, False) - - self._demoInstance._updateCameraConfigs() - - def guiOnDepthSetupUpdate(self, depthFrom=None, depthTo=None, subpixel=None, extended=None): - if depthFrom is not None: - self.updateArg("minDepth", depthFrom) - if depthTo is not None: - self.updateArg("maxDepth", depthTo) - if subpixel is not None: - self.updateArg("subpixel", subpixel) - if extended is not None: - self.updateArg("extendedDisparity", extended) - - def guiOnCameraSetupUpdate(self, name, fps=None, resolution=None): - if fps is not None: - if name == "color": - self.updateArg("rgbFps", fps) - else: - self.updateArg("monoFps", fps) - if resolution is not None: - if name == "color": - self.updateArg("rgbResolution", resolution) - else: - self.updateArg("monoResolution", resolution) - - def guiOnAiSetupUpdate(self, cnn=None, shave=None, source=None, fullFov=None, sbb=None, sbbFactor=None, ov=None, countLabel=None): - if cnn is not None: - self.updateArg("cnnModel", cnn) - if shave is not None: - self.updateArg("shaves", shave) - if source is not None: - self.updateArg("camera", source) - if fullFov is not None: - self.updateArg("disableFullFovNn", not fullFov) - if sbb is not None: - self.updateArg("spatialBoundingBox", sbb) - if sbbFactor is not None: - self.updateArg("sbbScaleFactor", sbbFactor) - if ov is not None: - self.updateArg("openvinoVersion", ov) - if countLabel is not None or cnn is not None: - self.updateArg("countLabel", countLabel) - - def guiOnPreviewChangeSelected(self, selected): - self.worker.selectedPreview = selected - self.selectedPreview = selected - - def guiOnSelectDevice(self, selected): - self.updateArg("deviceId", selected) - - def guiOnReloadDevices(self): - devices = list(map(lambda info: info.getMxId(), dai.Device.getAllAvailableDevices())) - if hasattr(self._demoInstance, "_deviceInfo"): - devices.insert(0, self._demoInstance._deviceInfo.getMxId()) - self.worker.signals.setDataSignal.emit(["deviceChoices", devices]) - if len(devices) > 0: - self.worker.signals.setDataSignal.emit(["restartRequired", True]) - - def guiOnStaticticsConsent(self, value): - try: - with Path('.consent').open('w') as f: - json.dump({"statistics": value}, f) - except: - pass - self.worker.signals.setDataSignal.emit(["restartRequired", True]) - - def guiOnToggleSync(self, value): - self.updateArg("sync", value) - - def guiOnToggleColorEncoding(self, enabled, fps): - oldConfig = self.confManager.args.encode or {} - if enabled: - oldConfig["color"] = fps - elif "color" in self.confManager.args.encode: - del oldConfig["color"] - self.updateArg("encode", oldConfig) - - def guiOnToggleLeftEncoding(self, enabled, fps): - oldConfig = self.confManager.args.encode or {} - if enabled: - oldConfig["left"] = fps - elif "color" in self.confManager.args.encode: - del oldConfig["left"] - self.updateArg("encode", oldConfig) - - def guiOnToggleRightEncoding(self, enabled, fps): - oldConfig = self.confManager.args.encode or {} - if enabled: - oldConfig["right"] = fps - elif "color" in self.confManager.args.encode: - del oldConfig["right"] - self.updateArg("encode", oldConfig) - - def guiOnSelectReportingOptions(self, temp, cpu, memory): - options = [] - if temp: - options.append("temp") - if cpu: - options.append("cpu") - if memory: - options.append("memory") - self.updateArg("report", options) - - def guiOnSelectReportingPath(self, value): - self.updateArg("reportFile", value) - - def guiOnSelectEncodingPath(self, value): - self.updateArg("encodeOutput", value) - - def guiOnToggleDepth(self, value): - self.updateArg("disableDepth", not value) - selectedPreviews = [Previews.rectifiedRight.name, Previews.rectifiedLeft.name] + ([Previews.disparity.name, Previews.disparityColor.name] if self.useDisparity else [Previews.depth.name, Previews.depthRaw.name]) - depthPreviews = [Previews.rectifiedRight.name, Previews.rectifiedLeft.name, Previews.depth.name, Previews.depthRaw.name, Previews.disparity.name, Previews.disparityColor.name] - filtered = list(filter(lambda name: name not in depthPreviews, self.confManager.args.show)) - if value: - updated = filtered + selectedPreviews - if self.selectedPreview not in updated: - self.selectedPreview = updated[0] - self.updateArg("show", updated) - else: - updated = filtered + [Previews.left.name, Previews.right.name] - if self.selectedPreview not in updated: - self.selectedPreview = updated[0] - self.updateArg("show", updated) - - def guiOnToggleNN(self, value): - self.updateArg("disableNeuralNetwork", not value) - filtered = list(filter(lambda name: name != Previews.nnInput.name, self.confManager.args.show)) - if value: - updated = filtered + [Previews.nnInput.name] - if self.selectedPreview not in updated: - self.selectedPreview = updated[0] - self.updateArg("show", filtered + [Previews.nnInput.name]) - else: - if self.selectedPreview not in filtered: - self.selectedPreview = filtered[0] - self.updateArg("show", filtered) - - def guiOnRunApp(self, appName): - self.stop() - self.updateArg("app", appName, shouldUpdate=False) - self.setData(["runningApp", appName]) - self.start() - - def guiOnTerminateApp(self, appName): - self.stop() - self.updateArg("app", None, shouldUpdate=False) - self.setData(["runningApp", ""]) - self.start() - - def guiOnToggleDisparity(self, value): - self.useDisparity = value - depthPreviews = [Previews.depth.name, Previews.depthRaw.name] - disparityPreviews = [Previews.disparity.name, Previews.disparityColor.name] - if value: - filtered = list(filter(lambda name: name not in depthPreviews, self.confManager.args.show)) - updated = filtered + disparityPreviews - if self.selectedPreview not in updated: - self.selectedPreview = updated[0] - self.updateArg("show", updated) - else: - filtered = list(filter(lambda name: name not in disparityPreviews, self.confManager.args.show)) - updated = filtered + depthPreviews - if self.selectedPreview not in updated: - self.selectedPreview = updated[0] - self.updateArg("show", updated) - GuiApp().start() - - -def runOpenCv(): - confManager = prepareConfManager(args) - demo = Demo() - demo.run_all(confManager) +def runOpenCv(in_args, instance): + confManager = prepareConfManager(in_args) + instance.run_all(confManager) if __name__ == "__main__": try: if args.noSupervisor: if args.guiType == "qt": - runQt() + from gui.qt.main import runQt + runQt(args, Demo(displayFrames=False)) else: args.guiType = "cv" - runOpenCv() + runOpenCv(args, Demo(displayFrames=True)) else: s = Supervisor() if args.guiType != "cv": diff --git a/depthai_helpers/config_manager.py b/depthai_helpers/config_manager.py index e93a91072..cd6b5677d 100644 --- a/depthai_helpers/config_manager.py +++ b/depthai_helpers/config_manager.py @@ -8,6 +8,7 @@ from depthai_helpers.cli_utils import cliPrint, PrintColors from depthai_sdk.previews import Previews +from depthai_sdk import downloadYTVideo DEPTHAI_ZOO = Path(__file__).parent.parent / Path(f"resources/nn/") @@ -271,3 +272,14 @@ def dispMultiplier(self): return val +def prepareConfManager(in_args): + confManager = ConfigManager(in_args) + confManager.linuxCheckApplyUsbRules() + if not confManager.useCamera: + if str(confManager.args.video).startswith('https'): + confManager.args.video = downloadYTVideo(confManager.args.video, DEPTHAI_VIDEOS) + print("Youtube video downloaded.") + if not Path(confManager.args.video).exists(): + raise ValueError("Path {} does not exists!".format(confManager.args.video)) + return confManager + diff --git a/gui/.gitignore b/gui/.gitignore deleted file mode 100644 index fab7372d7..000000000 --- a/gui/.gitignore +++ /dev/null @@ -1,73 +0,0 @@ -# This file is used to ignore files which are generated -# ---------------------------------------------------------------------------- - -*~ -*.autosave -*.a -*.core -*.moc -*.o -*.obj -*.orig -*.rej -*.so -*.so.* -*_pch.h.cpp -*_resource.rc -*.qm -.#* -*.*# -core -!core/ -tags -.DS_Store -.directory -*.debug -Makefile* -*.prl -*.app -moc_*.cpp -ui_*.h -qrc_*.cpp -Thumbs.db -*.res -*.rc -/.qmake.cache -/.qmake.stash - -# qtcreator generated files -*.pro.user* - -# xemacs temporary files -*.flc - -# Vim temporary files -.*.swp - -# Visual Studio generated files -*.ib_pdb_index -*.idb -*.ilk -*.pdb -*.sln -*.suo -*.vcproj -*vcproj.*.*.user -*.ncb -*.sdf -*.opensdf -*.vcxproj -*vcxproj.* - -# MinGW generated files -*.Debug -*.Release - -# Python byte code -*.pyc - -# Binaries -# -------- -*.dll -*.exe - diff --git a/gui/main.py b/gui/main.py deleted file mode 100644 index 6fc5670f4..000000000 --- a/gui/main.py +++ /dev/null @@ -1,360 +0,0 @@ -# This Python file uses the following encoding: utf-8 -import sys -from pathlib import Path - -import blobconverter -import cv2 -from PyQt5.QtQml import QQmlApplicationEngine, qmlRegisterType, qmlRegisterSingletonType, QQmlEngine -from PyQt5.QtQuick import QQuickPaintedItem -from PyQt5.QtGui import QImage -from PyQt5.QtCore import QObject, pyqtSignal, pyqtSlot, QRunnable, QThreadPool -import depthai as dai - -# To be used on the @QmlElement decorator -# (QML_IMPORT_MINOR_VERSION is optional) -from PyQt5.QtWidgets import QApplication -from depthai_sdk import Previews, resizeLetterbox, createBlankFrame - - -class Singleton(type(QQuickPaintedItem)): - _instances = {} - def __call__(cls, *args, **kwargs): - if cls not in cls._instances: - cls._instances[cls] = super(Singleton, cls).__call__(*args, **kwargs) - return cls._instances[cls] - - -instance = None - - -# @QmlElement -class ImageWriter(QQuickPaintedItem): - frame = QImage() - - def __init__(self, parent): - super().__init__(parent) - self.setRenderTarget(QQuickPaintedItem.FramebufferObject) - self.setProperty("parent", parent) - - def paint(self, painter): - painter.drawImage(0, 0, self.frame) - - def update_frame(self, image): - self.frame = image - self.update() - - -# @QmlElement -class AppBridge(QObject): - @pyqtSlot() - def applyAndRestart(self): - instance.restartDemo() - - @pyqtSlot() - def reloadDevices(self): - instance.guiOnReloadDevices() - - @pyqtSlot(bool) - def toggleStatisticsConsent(self, value): - instance.guiOnStaticticsConsent(value) - - @pyqtSlot(bool) - def toggleSync(self, value): - instance.guiOnToggleSync(value) - - @pyqtSlot(str) - def runApp(self, appName): - instance.guiOnRunApp(appName) - - @pyqtSlot(str) - def terminateApp(self, appName): - instance.guiOnTerminateApp(appName) - - @pyqtSlot(str) - def selectDevice(self, value): - instance.guiOnSelectDevice(value) - - @pyqtSlot(bool, bool, bool) - def selectReportingOptions(self, temp, cpu, memory): - instance.guiOnSelectReportingOptions(temp, cpu, memory) - - @pyqtSlot(str) - def selectReportingPath(self, value): - instance.guiOnSelectReportingPath(value) - - @pyqtSlot(str) - def selectEncodingPath(self, value): - instance.guiOnSelectEncodingPath(value) - - @pyqtSlot(bool, int) - def toggleColorEncoding(self, enabled, fps): - instance.guiOnToggleColorEncoding(enabled, fps) - - @pyqtSlot(bool, int) - def toggleLeftEncoding(self, enabled, fps): - instance.guiOnToggleLeftEncoding(enabled, fps) - - @pyqtSlot(bool, int) - def toggleRightEncoding(self, enabled, fps): - instance.guiOnToggleRightEncoding(enabled, fps) - - @pyqtSlot(bool) - def toggleDepth(self, enabled): - instance.guiOnToggleDepth(enabled) - - @pyqtSlot(bool) - def toggleNN(self, enabled): - instance.guiOnToggleNN(enabled) - - @pyqtSlot(bool) - def toggleDisparity(self, enabled): - instance.guiOnToggleDisparity(enabled) - - -# @QmlElement -class AIBridge(QObject): - @pyqtSlot(str) - def setCnnModel(self, name): - instance.guiOnAiSetupUpdate(cnn=name) - - @pyqtSlot(int) - def setShaves(self, value): - instance.guiOnAiSetupUpdate(shave=value) - - @pyqtSlot(str) - def setModelSource(self, value): - instance.guiOnAiSetupUpdate(source=value) - - @pyqtSlot(bool) - def setFullFov(self, value): - instance.guiOnAiSetupUpdate(fullFov=value) - - @pyqtSlot(bool) - def setSbb(self, value): - instance.guiOnAiSetupUpdate(sbb=value) - - @pyqtSlot(float) - def setSbbFactor(self, value): - if instance.writer is not None: - instance.guiOnAiSetupUpdate(sbbFactor=value) - - @pyqtSlot(str) - def setOvVersion(self, state): - instance.guiOnAiSetupUpdate(ov=state.replace("VERSION_", "")) - - @pyqtSlot(str) - def setCountLabel(self, state): - instance.guiOnAiSetupUpdate(countLabel=state) - - -# @QmlElement -class PreviewBridge(QObject): - @pyqtSlot(str) - def changeSelected(self, state): - instance.guiOnPreviewChangeSelected(state) - - -# @QmlElement -class DepthBridge(QObject): - @pyqtSlot(bool) - def toggleSubpixel(self, state): - instance.guiOnDepthSetupUpdate(subpixel=state) - - @pyqtSlot(bool) - def toggleExtendedDisparity(self, state): - instance.guiOnDepthSetupUpdate(extended=state) - - @pyqtSlot(bool) - def toggleLeftRightCheck(self, state): - instance.guiOnDepthConfigUpdate(lrc=state) - - @pyqtSlot(int) - def setDisparityConfidenceThreshold(self, value): - instance.guiOnDepthConfigUpdate(dct=value) - - @pyqtSlot(int) - def setLrcThreshold(self, value): - instance.guiOnDepthConfigUpdate(lrcThreshold=value) - - @pyqtSlot(int) - def setBilateralSigma(self, value): - instance.guiOnDepthConfigUpdate(sigma=value) - - @pyqtSlot(int, int) - def setDepthRange(self, valFrom, valTo): - instance.guiOnDepthSetupUpdate(depthFrom=int(valFrom * 1000), depthTo=int(valTo * 1000)) - - @pyqtSlot(str) - def setMedianFilter(self, state): - value = getattr(dai.MedianFilter, state) - instance.guiOnDepthConfigUpdate(median=value) - - -# @QmlElement -class ColorCamBridge(QObject): - name = "color" - - @pyqtSlot(int, int) - def setIsoExposure(self, iso, exposure): - if iso > 0 and exposure > 0: - instance.guiOnCameraConfigUpdate("color", sensitivity=iso, exposure=exposure) - - @pyqtSlot(int) - def setContrast(self, value): - instance.guiOnCameraConfigUpdate("color", contrast=value) - - @pyqtSlot(int) - def setBrightness(self, value): - instance.guiOnCameraConfigUpdate("color", brightness=value) - - @pyqtSlot(int) - def setSaturation(self, value): - instance.guiOnCameraConfigUpdate("color", saturation=value) - - @pyqtSlot(int) - def setSharpness(self, value): - instance.guiOnCameraConfigUpdate("color", sharpness=value) - - @pyqtSlot(int) - def setFps(self, value): - instance.guiOnCameraSetupUpdate("color", fps=value) - - @pyqtSlot(str) - def setResolution(self, state): - if state == "THE_1080_P": - instance.guiOnCameraSetupUpdate("color", resolution=1080) - elif state == "THE_4_K": - instance.guiOnCameraSetupUpdate("color", resolution=2160) - elif state == "THE_12_MP": - instance.guiOnCameraSetupUpdate("color", resolution=3040) - - -# @QmlElement -class MonoCamBridge(QObject): - - @pyqtSlot(int, int) - def setIsoExposure(self, iso, exposure): - if iso > 0 and exposure > 0: - instance.guiOnCameraConfigUpdate("left", sensitivity=iso, exposure=exposure) - instance.guiOnCameraConfigUpdate("right", sensitivity=iso, exposure=exposure) - - @pyqtSlot(int) - def setContrast(self, value): - instance.guiOnCameraConfigUpdate("left", contrast=value) - instance.guiOnCameraConfigUpdate("right", contrast=value) - - @pyqtSlot(int) - def setBrightness(self, value): - instance.guiOnCameraConfigUpdate("left", brightness=value) - instance.guiOnCameraConfigUpdate("right", brightness=value) - - @pyqtSlot(int) - def setSaturation(self, value): - instance.guiOnCameraConfigUpdate("left", saturation=value) - instance.guiOnCameraConfigUpdate("right", saturation=value) - - @pyqtSlot(int) - def setSharpness(self, value): - instance.guiOnCameraConfigUpdate("left", sharpness=value) - instance.guiOnCameraConfigUpdate("right", sharpness=value) - - @pyqtSlot(int) - def setFps(self, value): - instance.guiOnCameraSetupUpdate("left", fps=value) - instance.guiOnCameraSetupUpdate("right", fps=value) - - @pyqtSlot(str) - def setResolution(self, state): - if state == "THE_720_P": - instance.guiOnCameraSetupUpdate("left", resolution=720) - instance.guiOnCameraSetupUpdate("right", resolution=720) - elif state == "THE_800_P": - instance.guiOnCameraSetupUpdate("left", resolution=800) - instance.guiOnCameraSetupUpdate("right", resolution=800) - elif state == "THE_400_P": - instance.guiOnCameraSetupUpdate("left", resolution=400) - instance.guiOnCameraSetupUpdate("right", resolution=400) - - -class DemoQtGui: - instance = None - writer = None - window = None - progressFrame = None - - def __init__(self): - global instance - self.app = QApplication([sys.argv[0]]) - self.engine = QQmlApplicationEngine() - self.engine.quit.connect(self.app.quit) - instance = self - qmlRegisterType(ImageWriter, 'dai.gui', 1, 0, 'ImageWriter') - qmlRegisterType(AppBridge, 'dai.gui', 1, 0, 'AppBridge') - qmlRegisterType(AIBridge, 'dai.gui', 1, 0, 'AIBridge') - qmlRegisterType(PreviewBridge, 'dai.gui', 1, 0, 'PreviewBridge') - qmlRegisterType(DepthBridge, 'dai.gui', 1, 0, 'DepthBridge') - qmlRegisterType(ColorCamBridge, 'dai.gui', 1, 0, 'ColorCamBridge') - qmlRegisterType(MonoCamBridge, 'dai.gui', 1, 0, 'MonoCamBridge') - self.engine.addImportPath(str(Path(__file__).parent / "views")) - self.engine.load(str(Path(__file__).parent / "views" / "root.qml")) - self.window = self.engine.rootObjects()[0] - if not self.engine.rootObjects(): - raise RuntimeError("Unable to start GUI - no root objects!") - - def setData(self, data): - name, value = data - self.window.setProperty(name, value) - - def updatePreview(self, frame): - w, h = int(self.writer.width()), int(self.writer.height()) - scaledFrame = resizeLetterbox(frame, (w, h)) - if len(frame.shape) == 3: - img = QImage(scaledFrame.data, w, h, frame.shape[2] * w, 29) # 29 - QImage.Format_BGR888 - else: - img = QImage(scaledFrame.data, w, h, w, 24) # 24 - QImage.Format_Grayscale8 - self.writer.update_frame(img) - - def updateDownloadProgress(self, curr, total): - frame = self.createProgressFrame(curr / total) - img = QImage(frame.data, frame.shape[1], frame.shape[0], frame.shape[2] * frame.shape[1], 29) # 29 - QImage.Format_BGR888 - self.writer.update_frame(img) - - def createProgressFrame(self, donePercentage=None): - confManager = getattr(self, "confManager", None) - w, h = int(self.writer.width()), int(self.writer.height()) - if self.progressFrame is None: - self.progressFrame = createBlankFrame(w, h) - downloadText = "Downloading model blob..." - textsize = cv2.getTextSize(downloadText, cv2.FONT_HERSHEY_TRIPLEX, 0.5, 4)[0][0] - offset = int((w - textsize) / 2) - cv2.putText(self.progressFrame, downloadText, (offset, 250), cv2.FONT_HERSHEY_TRIPLEX, 0.5, (255, 255, 255), 4, cv2.LINE_AA) - cv2.putText(self.progressFrame, downloadText, (offset, 250), cv2.FONT_HERSHEY_TRIPLEX, 0.5, (0, 0, 0), 1, cv2.LINE_AA) - - newFrame = self.progressFrame.copy() - if donePercentage is not None: - cv2.rectangle(newFrame, (100, 300), (460, 350), (255, 255, 255), cv2.FILLED) - cv2.rectangle(newFrame, (110, 310), (int(110 + 340 * donePercentage), 340), (0, 0, 0), cv2.FILLED) - return newFrame - - def showSetupFrame(self, text): - w, h = int(self.writer.width()), int(self.writer.height()) - setupFrame = createBlankFrame(w, h) - cv2.putText(setupFrame, text, (200, 250), cv2.FONT_HERSHEY_TRIPLEX, 0.5, (255, 255, 255), 4, cv2.LINE_AA) - cv2.putText(setupFrame, text, (200, 250), cv2.FONT_HERSHEY_TRIPLEX, 0.5, (0, 0, 0), 1, cv2.LINE_AA) - img = QImage(setupFrame.data, w, h, setupFrame.shape[2] * w, 29) # 29 - QImage.Format_BGR888 - self.writer.update_frame(img) - - def startGui(self): - self.writer = self.window.findChild(QObject, "writer") - self.showSetupFrame("Starting demo...") - medianChoices = list(filter(lambda name: name.startswith('KERNEL_') or name.startswith('MEDIAN_'), vars(dai.MedianFilter).keys()))[::-1] - self.setData(["medianChoices", medianChoices]) - colorChoices = list(filter(lambda name: name[0].isupper(), vars(dai.ColorCameraProperties.SensorResolution).keys())) - self.setData(["colorResolutionChoices", colorChoices]) - monoChoices = list(filter(lambda name: name[0].isupper(), vars(dai.MonoCameraProperties.SensorResolution).keys())) - self.setData(["monoResolutionChoices", monoChoices]) - self.setData(["modelSourceChoices", [Previews.color.name, Previews.left.name, Previews.right.name]]) - versionChoices = sorted(filter(lambda name: name.startswith("VERSION_"), vars(dai.OpenVINO).keys()), reverse=True) - self.setData(["ovVersions", versionChoices]) - self.createProgressFrame() - return self.app.exec() diff --git a/gui/README.md b/gui/qt/README.md similarity index 100% rename from gui/README.md rename to gui/qt/README.md diff --git a/gui/__init__.py b/gui/qt/__init__.py similarity index 100% rename from gui/__init__.py rename to gui/qt/__init__.py diff --git a/gui/depthai_demo.pyproject b/gui/qt/depthai_demo.pyproject similarity index 100% rename from gui/depthai_demo.pyproject rename to gui/qt/depthai_demo.pyproject diff --git a/gui/qt/main.py b/gui/qt/main.py new file mode 100644 index 000000000..4cc00a4c7 --- /dev/null +++ b/gui/qt/main.py @@ -0,0 +1,780 @@ +# This Python file uses the following encoding: utf-8 +import argparse +import json +import sys +import time +import traceback +from functools import cmp_to_key +from pathlib import Path + +import cv2 +import numpy as np +from PyQt5.QtQml import QQmlApplicationEngine, qmlRegisterType +from PyQt5.QtQuick import QQuickPaintedItem +from PyQt5.QtGui import QImage +from PyQt5.QtWidgets import QMessageBox +from PyQt5.QtCore import QObject, pyqtSignal, pyqtSlot, QRunnable, QThreadPool +import depthai as dai + +# To be used on the @QmlElement decorator +# (QML_IMPORT_MINOR_VERSION is optional) +from PyQt5.QtWidgets import QApplication +from depthai_sdk import Previews, resizeLetterbox, createBlankFrame, loadModule + +from depthai_helpers.config_manager import prepareConfManager + + +class Singleton(type(QQuickPaintedItem)): + _instances = {} + def __call__(cls, *args, **kwargs): + if cls not in cls._instances: + cls._instances[cls] = super(Singleton, cls).__call__(*args, **kwargs) + return cls._instances[cls] + + +instance = None + + +# @QmlElement +class ImageWriter(QQuickPaintedItem): + frame = QImage() + + def __init__(self, parent): + super().__init__(parent) + self.setRenderTarget(QQuickPaintedItem.FramebufferObject) + self.setProperty("parent", parent) + + def paint(self, painter): + painter.drawImage(0, 0, self.frame) + + def update_frame(self, image): + self.frame = image + self.update() + + +# @QmlElement +class AppBridge(QObject): + @pyqtSlot() + def applyAndRestart(self): + instance.restartDemo() + + @pyqtSlot() + def reloadDevices(self): + instance.guiOnReloadDevices() + + @pyqtSlot(bool) + def toggleStatisticsConsent(self, value): + instance.guiOnStaticticsConsent(value) + + @pyqtSlot(bool) + def toggleSyncPreview(self, value): + instance.guiOnToggleSyncPreview(value) + + @pyqtSlot(str) + def runApp(self, appName): + instance.guiOnRunApp(appName) + + @pyqtSlot(str) + def terminateApp(self, appName): + instance.guiOnTerminateApp(appName) + + @pyqtSlot(str) + def selectDevice(self, value): + instance.guiOnSelectDevice(value) + + @pyqtSlot(bool, bool, bool) + def selectReportingOptions(self, temp, cpu, memory): + instance.guiOnSelectReportingOptions(temp, cpu, memory) + + @pyqtSlot(str) + def selectReportingPath(self, value): + instance.guiOnSelectReportingPath(value) + + @pyqtSlot(str) + def selectEncodingPath(self, value): + instance.guiOnSelectEncodingPath(value) + + @pyqtSlot(bool, int) + def toggleColorEncoding(self, enabled, fps): + instance.guiOnToggleColorEncoding(enabled, fps) + + @pyqtSlot(bool, int) + def toggleLeftEncoding(self, enabled, fps): + instance.guiOnToggleLeftEncoding(enabled, fps) + + @pyqtSlot(bool, int) + def toggleRightEncoding(self, enabled, fps): + instance.guiOnToggleRightEncoding(enabled, fps) + + @pyqtSlot(bool) + def toggleDepth(self, enabled): + instance.guiOnToggleDepth(enabled) + + @pyqtSlot(bool) + def toggleNN(self, enabled): + instance.guiOnToggleNN(enabled) + + @pyqtSlot(bool) + def toggleDisparity(self, enabled): + instance.guiOnToggleDisparity(enabled) + + +# @QmlElement +class AIBridge(QObject): + @pyqtSlot(str) + def setCnnModel(self, name): + instance.guiOnAiSetupUpdate(cnn=name) + + @pyqtSlot(int) + def setShaves(self, value): + instance.guiOnAiSetupUpdate(shave=value) + + @pyqtSlot(str) + def setModelSource(self, value): + instance.guiOnAiSetupUpdate(source=value) + + @pyqtSlot(bool) + def setFullFov(self, value): + instance.guiOnAiSetupUpdate(fullFov=value) + + @pyqtSlot(bool) + def setSbb(self, value): + instance.guiOnAiSetupUpdate(sbb=value) + + @pyqtSlot(float) + def setSbbFactor(self, value): + if instance.writer is not None: + instance.guiOnAiSetupUpdate(sbbFactor=value) + + @pyqtSlot(str) + def setOvVersion(self, state): + instance.guiOnAiSetupUpdate(ov=state.replace("VERSION_", "")) + + @pyqtSlot(str) + def setCountLabel(self, state): + instance.guiOnAiSetupUpdate(countLabel=state) + + +# @QmlElement +class PreviewBridge(QObject): + @pyqtSlot(str) + def changeSelected(self, state): + instance.guiOnPreviewChangeSelected(state) + + +# @QmlElement +class DepthBridge(QObject): + @pyqtSlot(bool) + def toggleSubpixel(self, state): + instance.guiOnDepthSetupUpdate(subpixel=state) + + @pyqtSlot(bool) + def toggleExtendedDisparity(self, state): + instance.guiOnDepthSetupUpdate(extended=state) + + @pyqtSlot(bool) + def toggleLeftRightCheck(self, state): + instance.guiOnDepthConfigUpdate(lrc=state) + + @pyqtSlot(int) + def setDisparityConfidenceThreshold(self, value): + instance.guiOnDepthConfigUpdate(dct=value) + + @pyqtSlot(int) + def setLrcThreshold(self, value): + instance.guiOnDepthConfigUpdate(lrcThreshold=value) + + @pyqtSlot(int) + def setBilateralSigma(self, value): + instance.guiOnDepthConfigUpdate(sigma=value) + + @pyqtSlot(int, int) + def setDepthRange(self, valFrom, valTo): + instance.guiOnDepthSetupUpdate(depthFrom=int(valFrom * 1000), depthTo=int(valTo * 1000)) + + @pyqtSlot(str) + def setMedianFilter(self, state): + value = getattr(dai.MedianFilter, state) + instance.guiOnDepthConfigUpdate(median=value) + + +# @QmlElement +class ColorCamBridge(QObject): + name = "color" + + @pyqtSlot(int, int) + def setIsoExposure(self, iso, exposure): + if iso > 0 and exposure > 0: + instance.guiOnCameraConfigUpdate("color", sensitivity=iso, exposure=exposure) + + @pyqtSlot(int) + def setContrast(self, value): + instance.guiOnCameraConfigUpdate("color", contrast=value) + + @pyqtSlot(int) + def setBrightness(self, value): + instance.guiOnCameraConfigUpdate("color", brightness=value) + + @pyqtSlot(int) + def setSaturation(self, value): + instance.guiOnCameraConfigUpdate("color", saturation=value) + + @pyqtSlot(int) + def setSharpness(self, value): + instance.guiOnCameraConfigUpdate("color", sharpness=value) + + @pyqtSlot(int) + def setFps(self, value): + instance.guiOnCameraSetupUpdate("color", fps=value) + + @pyqtSlot(str) + def setResolution(self, state): + if state == "THE_1080_P": + instance.guiOnCameraSetupUpdate("color", resolution=1080) + elif state == "THE_4_K": + instance.guiOnCameraSetupUpdate("color", resolution=2160) + elif state == "THE_12_MP": + instance.guiOnCameraSetupUpdate("color", resolution=3040) + + +# @QmlElement +class MonoCamBridge(QObject): + + @pyqtSlot(int, int) + def setIsoExposure(self, iso, exposure): + if iso > 0 and exposure > 0: + instance.guiOnCameraConfigUpdate("left", sensitivity=iso, exposure=exposure) + instance.guiOnCameraConfigUpdate("right", sensitivity=iso, exposure=exposure) + + @pyqtSlot(int) + def setContrast(self, value): + instance.guiOnCameraConfigUpdate("left", contrast=value) + instance.guiOnCameraConfigUpdate("right", contrast=value) + + @pyqtSlot(int) + def setBrightness(self, value): + instance.guiOnCameraConfigUpdate("left", brightness=value) + instance.guiOnCameraConfigUpdate("right", brightness=value) + + @pyqtSlot(int) + def setSaturation(self, value): + instance.guiOnCameraConfigUpdate("left", saturation=value) + instance.guiOnCameraConfigUpdate("right", saturation=value) + + @pyqtSlot(int) + def setSharpness(self, value): + instance.guiOnCameraConfigUpdate("left", sharpness=value) + instance.guiOnCameraConfigUpdate("right", sharpness=value) + + @pyqtSlot(int) + def setFps(self, value): + instance.guiOnCameraSetupUpdate("left", fps=value) + instance.guiOnCameraSetupUpdate("right", fps=value) + + @pyqtSlot(str) + def setResolution(self, state): + if state == "THE_720_P": + instance.guiOnCameraSetupUpdate("left", resolution=720) + instance.guiOnCameraSetupUpdate("right", resolution=720) + elif state == "THE_800_P": + instance.guiOnCameraSetupUpdate("left", resolution=800) + instance.guiOnCameraSetupUpdate("right", resolution=800) + elif state == "THE_400_P": + instance.guiOnCameraSetupUpdate("left", resolution=400) + instance.guiOnCameraSetupUpdate("right", resolution=400) + + +class DemoQtGui: + instance = None + writer = None + window = None + progressFrame = None + + def __init__(self): + global instance + self.app = QApplication([sys.argv[0]]) + self.engine = QQmlApplicationEngine() + self.engine.quit.connect(self.app.quit) + instance = self + qmlRegisterType(ImageWriter, 'dai.gui', 1, 0, 'ImageWriter') + qmlRegisterType(AppBridge, 'dai.gui', 1, 0, 'AppBridge') + qmlRegisterType(AIBridge, 'dai.gui', 1, 0, 'AIBridge') + qmlRegisterType(PreviewBridge, 'dai.gui', 1, 0, 'PreviewBridge') + qmlRegisterType(DepthBridge, 'dai.gui', 1, 0, 'DepthBridge') + qmlRegisterType(ColorCamBridge, 'dai.gui', 1, 0, 'ColorCamBridge') + qmlRegisterType(MonoCamBridge, 'dai.gui', 1, 0, 'MonoCamBridge') + self.engine.addImportPath(str(Path(__file__).parent / "views")) + self.engine.load(str(Path(__file__).parent / "views" / "root.qml")) + self.window = self.engine.rootObjects()[0] + if not self.engine.rootObjects(): + raise RuntimeError("Unable to start GUI - no root objects!") + + def setData(self, data): + name, value = data + self.window.setProperty(name, value) + + def updatePreview(self, frame): + w, h = int(self.writer.width()), int(self.writer.height()) + scaledFrame = resizeLetterbox(frame, (w, h)) + if len(frame.shape) == 3: + img = QImage(scaledFrame.data, w, h, frame.shape[2] * w, 29) # 29 - QImage.Format_BGR888 + else: + img = QImage(scaledFrame.data, w, h, w, 24) # 24 - QImage.Format_Grayscale8 + self.writer.update_frame(img) + + def updateDownloadProgress(self, curr, total): + frame = self.createProgressFrame(curr / total) + img = QImage(frame.data, frame.shape[1], frame.shape[0], frame.shape[2] * frame.shape[1], 29) # 29 - QImage.Format_BGR888 + self.writer.update_frame(img) + + def createProgressFrame(self, donePercentage=None): + confManager = getattr(self, "confManager", None) + w, h = int(self.writer.width()), int(self.writer.height()) + if self.progressFrame is None: + self.progressFrame = createBlankFrame(w, h) + if confManager is None: + downloadText = "Downloading model blob..." + else: + downloadText = f"Downloading {confManager.getModelName()} blob..." + textsize = cv2.getTextSize(downloadText, cv2.FONT_HERSHEY_TRIPLEX, 0.5, 4)[0][0] + offset = int((w - textsize) / 2) + cv2.putText(self.progressFrame, downloadText, (offset, 250), cv2.FONT_HERSHEY_TRIPLEX, 0.5, (255, 255, 255), 4, cv2.LINE_AA) + cv2.putText(self.progressFrame, downloadText, (offset, 250), cv2.FONT_HERSHEY_TRIPLEX, 0.5, (0, 0, 0), 1, cv2.LINE_AA) + + newFrame = self.progressFrame.copy() + if donePercentage is not None: + cv2.rectangle(newFrame, (100, 300), (460, 350), (255, 255, 255), cv2.FILLED) + cv2.rectangle(newFrame, (110, 310), (int(110 + 340 * donePercentage), 340), (0, 0, 0), cv2.FILLED) + return newFrame + + def showSetupFrame(self, text): + w, h = int(self.writer.width()), int(self.writer.height()) + setupFrame = createBlankFrame(w, h) + cv2.putText(setupFrame, text, (200, 250), cv2.FONT_HERSHEY_TRIPLEX, 0.5, (255, 255, 255), 4, cv2.LINE_AA) + cv2.putText(setupFrame, text, (200, 250), cv2.FONT_HERSHEY_TRIPLEX, 0.5, (0, 0, 0), 1, cv2.LINE_AA) + img = QImage(setupFrame.data, w, h, setupFrame.shape[2] * w, 29) # 29 - QImage.Format_BGR888 + self.writer.update_frame(img) + + def startGui(self): + self.writer = self.window.findChild(QObject, "writer") + self.showSetupFrame("Starting demo...") + medianChoices = list(filter(lambda name: name.startswith('KERNEL_') or name.startswith('MEDIAN_'), vars(dai.MedianFilter).keys()))[::-1] + self.setData(["medianChoices", medianChoices]) + colorChoices = list(filter(lambda name: name[0].isupper(), vars(dai.ColorCameraProperties.SensorResolution).keys())) + self.setData(["colorResolutionChoices", colorChoices]) + monoChoices = list(filter(lambda name: name[0].isupper(), vars(dai.MonoCameraProperties.SensorResolution).keys())) + self.setData(["monoResolutionChoices", monoChoices]) + self.setData(["modelSourceChoices", [Previews.color.name, Previews.left.name, Previews.right.name]]) + versionChoices = sorted(filter(lambda name: name.startswith("VERSION_"), vars(dai.OpenVINO).keys()), reverse=True) + self.setData(["ovVersions", versionChoices]) + self.createProgressFrame() + return self.app.exec() + +class WorkerSignals(QObject): + updateConfSignal = pyqtSignal(list) + updateDownloadProgressSignal = pyqtSignal(int, int) + updatePreviewSignal = pyqtSignal(np.ndarray) + setDataSignal = pyqtSignal(list) + exitSignal = pyqtSignal() + errorSignal = pyqtSignal(str) + +class Worker(QRunnable): + def __init__(self, instance, parent, conf, selectedPreview=None): + super(Worker, self).__init__() + self.running = False + self.selectedPreview = selectedPreview + self.instance = instance + self.parent = parent + self.conf = conf + self.callback_module = loadModule(conf.args.callback) + self.file_callbacks = { + callbackName: getattr(self.callback_module, callbackName) + for callbackName in ["shouldRun", "onNewFrame", "onShowFrame", "onNn", "onReport", "onSetup", "onTeardown", "onIter"] + if callable(getattr(self.callback_module, callbackName, None)) + } + self.instance.setCallbacks(**self.file_callbacks) + self.signals = WorkerSignals() + self.signals.exitSignal.connect(self.terminate) + self.signals.updateConfSignal.connect(self.updateConf) + + + def run(self): + self.running = True + self.signals.setDataSignal.emit(["restartRequired", False]) + self.instance.setCallbacks(shouldRun=self.shouldRun, onShowFrame=self.onShowFrame, onSetup=self.onSetup, onAppSetup=self.onAppSetup, onAppStart=self.onAppStart, showDownloadProgress=self.showDownloadProgress) + self.conf.args.bandwidth = "auto" + if self.conf.args.deviceId is None: + devices = dai.Device.getAllAvailableDevices() + if len(devices) > 0: + defaultDevice = next(map( + lambda info: info.getMxId(), + filter(lambda info: info.desc.protocol == dai.XLinkProtocol.X_LINK_USB_VSC, devices) + ), None) + if defaultDevice is None: + defaultDevice = devices[0].getMxId() + self.conf.args.deviceId = defaultDevice + if Previews.color.name not in self.conf.args.show: + self.conf.args.show.append(Previews.color.name) + if Previews.nnInput.name not in self.conf.args.show: + self.conf.args.show.append(Previews.nnInput.name) + if Previews.depth.name not in self.conf.args.show and Previews.disparityColor.name not in self.conf.args.show: + self.conf.args.show.append(Previews.depth.name) + if Previews.depthRaw.name not in self.conf.args.show and Previews.disparity.name not in self.conf.args.show: + self.conf.args.show.append(Previews.depthRaw.name) + if Previews.left.name not in self.conf.args.show: + self.conf.args.show.append(Previews.left.name) + if Previews.rectifiedLeft.name not in self.conf.args.show: + self.conf.args.show.append(Previews.rectifiedLeft.name) + if Previews.right.name not in self.conf.args.show: + self.conf.args.show.append(Previews.right.name) + if Previews.rectifiedRight.name not in self.conf.args.show: + self.conf.args.show.append(Previews.rectifiedRight.name) + try: + self.instance.run_all(self.conf) + except KeyboardInterrupt: + sys.exit(0) + except Exception as ex: + self.onError(ex) + + def terminate(self): + self.running = False + self.signals.setDataSignal.emit(["restartRequired", False]) + + + def updateConf(self, argsList): + self.conf.args = argparse.Namespace(**dict(argsList)) + + def onError(self, ex: Exception): + self.signals.errorSignal.emit(''.join(traceback.format_tb(ex.__traceback__) + [str(ex)])) + self.signals.setDataSignal.emit(["restartRequired", True]) + + def shouldRun(self): + if "shouldRun" in self.file_callbacks: + return self.running and self.file_callbacks["shouldRun"]() + return self.running + + def onShowFrame(self, frame, source): + if "onShowFrame" in self.file_callbacks: + self.file_callbacks["onShowFrame"](frame, source) + if source == self.selectedPreview: + self.signals.updatePreviewSignal.emit(frame) + + def onAppSetup(self, app): + setupFrame = createBlankFrame(500, 500) + cv2.putText(setupFrame, "Preparing {} app...".format(app.appName), (150, 250), cv2.FONT_HERSHEY_TRIPLEX, 0.5, (255, 255, 255), 4, cv2.LINE_AA) + cv2.putText(setupFrame, "Preparing {} app...".format(app.appName), (150, 250), cv2.FONT_HERSHEY_TRIPLEX, 0.5, (0, 0, 0), 1, cv2.LINE_AA) + self.signals.updatePreviewSignal.emit(setupFrame) + + def onAppStart(self, app): + setupFrame = createBlankFrame(500, 500) + cv2.putText(setupFrame, "Running {} app... (check console)".format(app.appName), (100, 250), cv2.FONT_HERSHEY_TRIPLEX, 0.5, (255, 255, 255), 4, cv2.LINE_AA) + cv2.putText(setupFrame, "Running {} app... (check console)".format(app.appName), (100, 250), cv2.FONT_HERSHEY_TRIPLEX, 0.5, (0, 0, 0), 1, cv2.LINE_AA) + self.signals.updatePreviewSignal.emit(setupFrame) + + def showDownloadProgress(self, curr, total): + self.signals.updateDownloadProgressSignal.emit(curr, total) + + def onSetup(self, instance): + if "onSetup" in self.file_callbacks: + self.file_callbacks["onSetup"](instance) + self.signals.updateConfSignal.emit(list(vars(self.conf.args).items())) + self.signals.setDataSignal.emit(["previewChoices", self.conf.args.show]) + devices = [self.instance._deviceInfo.getMxId()] + list(map(lambda info: info.getMxId(), dai.Device.getAllAvailableDevices())) + self.signals.setDataSignal.emit(["deviceChoices", devices]) + if instance._nnManager is not None: + self.signals.setDataSignal.emit(["countLabels", instance._nnManager._labels]) + else: + self.signals.setDataSignal.emit(["countLabels", []]) + self.signals.setDataSignal.emit(["depthEnabled", self.conf.useDepth]) + self.signals.setDataSignal.emit(["statisticsAccepted", self.instance.metrics is not None]) + self.signals.setDataSignal.emit(["modelChoices", sorted(self.conf.getAvailableZooModels(), key=cmp_to_key(lambda a, b: -1 if a == "mobilenet-ssd" else 1 if b == "mobilenet-ssd" else -1 if a < b else 1))]) + + +class GuiApp(DemoQtGui): + def __init__(self, instance, args): + super().__init__() + self.confManager = prepareConfManager(args) + self.running = False + self.selectedPreview = self.confManager.args.show[0] if len(self.confManager.args.show) > 0 else "color" + self.useDisparity = False + self.dataInitialized = False + self.appInitialized = False + self.threadpool = QThreadPool() + self._demoInstance = instance + + def updateArg(self, arg_name, arg_value, shouldUpdate=True): + setattr(self.confManager.args, arg_name, arg_value) + if shouldUpdate: + self.worker.signals.setDataSignal.emit(["restartRequired", True]) + + + def showError(self, error): + print(error, file=sys.stderr) + msgBox = QMessageBox() + msgBox.setIcon(QMessageBox.Critical) + msgBox.setText(error) + msgBox.setWindowTitle("An error occured") + msgBox.setStandardButtons(QMessageBox.Ok) + msgBox.exec() + + def setupDataCollection(self): + try: + with Path(".consent").open() as f: + accepted = json.load(f)["statistics"] + except: + accepted = True + + self._demoInstance.toggleMetrics(accepted) + + def start(self): + self.setupDataCollection() + self.running = True + self.worker = Worker(self._demoInstance, parent=self, conf=self.confManager, selectedPreview=self.selectedPreview) + self.worker.signals.updatePreviewSignal.connect(self.updatePreview) + self.worker.signals.updateDownloadProgressSignal.connect(self.updateDownloadProgress) + self.worker.signals.setDataSignal.connect(self.setData) + self.worker.signals.errorSignal.connect(self.showError) + self.threadpool.start(self.worker) + if not self.appInitialized: + self.appInitialized = True + exit_code = self.startGui() + self.stop(wait=False) + sys.exit(exit_code) + + def stop(self, wait=True): + if hasattr(self._demoInstance, "_device"): + current_mxid = self._demoInstance._device.getMxId() + else: + current_mxid = self.confManager.args.deviceId + self.worker.signals.exitSignal.emit() + self.threadpool.waitForDone(10000) + + if wait and current_mxid is not None: + start = time.time() + while time.time() - start < 30: + if current_mxid in list(map(lambda info: info.getMxId(), dai.Device.getAllAvailableDevices())): + break + else: + time.sleep(0.1) + else: + print(f"[Warning] Device not available again after 30 seconds! MXID: {current_mxid}") + + def restartDemo(self): + self.stop() + self.start() + + def guiOnDepthConfigUpdate(self, median=None, dct=None, sigma=None, lrc=None, lrcThreshold=None): + self._demoInstance._pm.updateDepthConfig(self._demoInstance._device, median=median, dct=dct, sigma=sigma, lrc=lrc, lrcThreshold=lrcThreshold) + if median is not None: + if median == dai.MedianFilter.MEDIAN_OFF: + self.updateArg("stereoMedianSize", 0, False) + elif median == dai.MedianFilter.KERNEL_3x3: + self.updateArg("stereoMedianSize", 3, False) + elif median == dai.MedianFilter.KERNEL_5x5: + self.updateArg("stereoMedianSize", 5, False) + elif median == dai.MedianFilter.KERNEL_7x7: + self.updateArg("stereoMedianSize", 7, False) + if dct is not None: + self.updateArg("disparityConfidenceThreshold", dct, False) + if sigma is not None: + self.updateArg("sigma", sigma, False) + if lrc is not None: + self.updateArg("stereoLrCheck", lrc, False) + if lrcThreshold is not None: + self.updateArg("lrcThreshold", lrcThreshold, False) + + def guiOnCameraConfigUpdate(self, name, exposure=None, sensitivity=None, saturation=None, contrast=None, brightness=None, sharpness=None): + if exposure is not None: + newValue = list(filter(lambda item: item[0] == name, (self.confManager.args.cameraExposure or []))) + [(name, exposure)] + self._demoInstance._cameraConfig["exposure"] = newValue + self.updateArg("cameraExposure", newValue, False) + if sensitivity is not None: + newValue = list(filter(lambda item: item[0] == name, (self.confManager.args.cameraSensitivity or []))) + [(name, sensitivity)] + self._demoInstance._cameraConfig["sensitivity"] = newValue + self.updateArg("cameraSensitivity", newValue, False) + if saturation is not None: + newValue = list(filter(lambda item: item[0] == name, (self.confManager.args.cameraSaturation or []))) + [(name, saturation)] + self._demoInstance._cameraConfig["saturation"] = newValue + self.updateArg("cameraSaturation", newValue, False) + if contrast is not None: + newValue = list(filter(lambda item: item[0] == name, (self.confManager.args.cameraContrast or []))) + [(name, contrast)] + self._demoInstance._cameraConfig["contrast"] = newValue + self.updateArg("cameraContrast", newValue, False) + if brightness is not None: + newValue = list(filter(lambda item: item[0] == name, (self.confManager.args.cameraBrightness or []))) + [(name, brightness)] + self._demoInstance._cameraConfig["brightness"] = newValue + self.updateArg("cameraBrightness", newValue, False) + if sharpness is not None: + newValue = list(filter(lambda item: item[0] == name, (self.confManager.args.cameraSharpness or []))) + [(name, sharpness)] + self._demoInstance._cameraConfig["sharpness"] = newValue + self.updateArg("cameraSharpness", newValue, False) + + self._demoInstance._updateCameraConfigs() + + def guiOnDepthSetupUpdate(self, depthFrom=None, depthTo=None, subpixel=None, extended=None): + if depthFrom is not None: + self.updateArg("minDepth", depthFrom) + if depthTo is not None: + self.updateArg("maxDepth", depthTo) + if subpixel is not None: + self.updateArg("subpixel", subpixel) + if extended is not None: + self.updateArg("extendedDisparity", extended) + + def guiOnCameraSetupUpdate(self, name, fps=None, resolution=None): + if fps is not None: + if name == "color": + self.updateArg("rgbFps", fps) + else: + self.updateArg("monoFps", fps) + if resolution is not None: + if name == "color": + self.updateArg("rgbResolution", resolution) + else: + self.updateArg("monoResolution", resolution) + + def guiOnAiSetupUpdate(self, cnn=None, shave=None, source=None, fullFov=None, sbb=None, sbbFactor=None, ov=None, countLabel=None): + if cnn is not None: + self.updateArg("cnnModel", cnn) + if shave is not None: + self.updateArg("shaves", shave) + if source is not None: + self.updateArg("camera", source) + if fullFov is not None: + self.updateArg("disableFullFovNn", not fullFov) + if sbb is not None: + self.updateArg("spatialBoundingBox", sbb) + if sbbFactor is not None: + self.updateArg("sbbScaleFactor", sbbFactor) + if ov is not None: + self.updateArg("openvinoVersion", ov) + if countLabel is not None or cnn is not None: + self.updateArg("countLabel", countLabel) + + def guiOnPreviewChangeSelected(self, selected): + self.worker.selectedPreview = selected + self.selectedPreview = selected + + def guiOnSelectDevice(self, selected): + self.updateArg("deviceId", selected) + + def guiOnReloadDevices(self): + devices = list(map(lambda info: info.getMxId(), dai.Device.getAllAvailableDevices())) + if hasattr(self._demoInstance, "_deviceInfo"): + devices.insert(0, self._demoInstance._deviceInfo.getMxId()) + self.worker.signals.setDataSignal.emit(["deviceChoices", devices]) + if len(devices) > 0: + self.worker.signals.setDataSignal.emit(["restartRequired", True]) + + def guiOnStaticticsConsent(self, value): + try: + with Path('.consent').open('w') as f: + json.dump({"statistics": value}, f) + except: + pass + self.worker.signals.setDataSignal.emit(["restartRequired", True]) + + def guiOnToggleSyncPreview(self, value): + self.updateArg("syncPreviews", value) + + def guiOnToggleColorEncoding(self, enabled, fps): + oldConfig = self.confManager.args.encode or {} + if enabled: + oldConfig["color"] = fps + elif "color" in self.confManager.args.encode: + del oldConfig["color"] + self.updateArg("encode", oldConfig) + + def guiOnToggleLeftEncoding(self, enabled, fps): + oldConfig = self.confManager.args.encode or {} + if enabled: + oldConfig["left"] = fps + elif "color" in self.confManager.args.encode: + del oldConfig["left"] + self.updateArg("encode", oldConfig) + + def guiOnToggleRightEncoding(self, enabled, fps): + oldConfig = self.confManager.args.encode or {} + if enabled: + oldConfig["right"] = fps + elif "color" in self.confManager.args.encode: + del oldConfig["right"] + self.updateArg("encode", oldConfig) + + def guiOnSelectReportingOptions(self, temp, cpu, memory): + options = [] + if temp: + options.append("temp") + if cpu: + options.append("cpu") + if memory: + options.append("memory") + self.updateArg("report", options) + + def guiOnSelectReportingPath(self, value): + self.updateArg("reportFile", value) + + def guiOnSelectEncodingPath(self, value): + self.updateArg("encodeOutput", value) + + def guiOnToggleDepth(self, value): + self.updateArg("disableDepth", not value) + selectedPreviews = [Previews.rectifiedRight.name, Previews.rectifiedLeft.name] + ([Previews.disparity.name, Previews.disparityColor.name] if self.useDisparity else [Previews.depth.name, Previews.depthRaw.name]) + depthPreviews = [Previews.rectifiedRight.name, Previews.rectifiedLeft.name, Previews.depth.name, Previews.depthRaw.name, Previews.disparity.name, Previews.disparityColor.name] + filtered = list(filter(lambda name: name not in depthPreviews, self.confManager.args.show)) + if value: + updated = filtered + selectedPreviews + if self.selectedPreview not in updated: + self.selectedPreview = updated[0] + self.updateArg("show", updated) + else: + updated = filtered + [Previews.left.name, Previews.right.name] + if self.selectedPreview not in updated: + self.selectedPreview = updated[0] + self.updateArg("show", updated) + + def guiOnToggleNN(self, value): + self.updateArg("disableNeuralNetwork", not value) + filtered = list(filter(lambda name: name != Previews.nnInput.name, self.confManager.args.show)) + if value: + updated = filtered + [Previews.nnInput.name] + if self.selectedPreview not in updated: + self.selectedPreview = updated[0] + self.updateArg("show", filtered + [Previews.nnInput.name]) + else: + if self.selectedPreview not in filtered: + self.selectedPreview = filtered[0] + self.updateArg("show", filtered) + + def guiOnRunApp(self, appName): + self.stop() + self.updateArg("app", appName, shouldUpdate=False) + self.setData(["runningApp", appName]) + self.start() + + def guiOnTerminateApp(self, appName): + self.stop() + self.updateArg("app", None, shouldUpdate=False) + self.setData(["runningApp", ""]) + self.start() + + def guiOnToggleDisparity(self, value): + self.useDisparity = value + depthPreviews = [Previews.depth.name, Previews.depthRaw.name] + disparityPreviews = [Previews.disparity.name, Previews.disparityColor.name] + if value: + filtered = list(filter(lambda name: name not in depthPreviews, self.confManager.args.show)) + updated = filtered + disparityPreviews + if self.selectedPreview not in updated: + self.selectedPreview = updated[0] + self.updateArg("show", updated) + else: + filtered = list(filter(lambda name: name not in disparityPreviews, self.confManager.args.show)) + updated = filtered + depthPreviews + if self.selectedPreview not in updated: + self.selectedPreview = updated[0] + self.updateArg("show", updated) + + +def runQt(args, demo_instance): + GuiApp(demo_instance, args).start() diff --git a/gui/views/AIProperties.qml b/gui/qt/views/AIProperties.qml similarity index 100% rename from gui/views/AIProperties.qml rename to gui/qt/views/AIProperties.qml diff --git a/gui/views/CameraPreview.qml b/gui/qt/views/CameraPreview.qml similarity index 100% rename from gui/views/CameraPreview.qml rename to gui/qt/views/CameraPreview.qml diff --git a/gui/views/CameraProperties.qml b/gui/qt/views/CameraProperties.qml similarity index 100% rename from gui/views/CameraProperties.qml rename to gui/qt/views/CameraProperties.qml diff --git a/gui/views/DepthProperties.qml b/gui/qt/views/DepthProperties.qml similarity index 100% rename from gui/views/DepthProperties.qml rename to gui/qt/views/DepthProperties.qml diff --git a/gui/views/MiscProperties.qml b/gui/qt/views/MiscProperties.qml similarity index 100% rename from gui/views/MiscProperties.qml rename to gui/qt/views/MiscProperties.qml diff --git a/gui/views/root.qml b/gui/qt/views/root.qml similarity index 100% rename from gui/views/root.qml rename to gui/qt/views/root.qml From bff15baf597119664dfce71af7a469c7c90a2753 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Pi=C5=82atowski?= Date: Wed, 19 Jan 2022 15:36:24 +0100 Subject: [PATCH 02/22] apply updates --- gui/qt/main.py | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/gui/qt/main.py b/gui/qt/main.py index 4cc00a4c7..696725637 100644 --- a/gui/qt/main.py +++ b/gui/qt/main.py @@ -67,8 +67,8 @@ def toggleStatisticsConsent(self, value): instance.guiOnStaticticsConsent(value) @pyqtSlot(bool) - def toggleSyncPreview(self, value): - instance.guiOnToggleSyncPreview(value) + def guiOnToggleSync(self, value): + self.updateArg("sync", value) @pyqtSlot(str) def runApp(self, appName): @@ -332,10 +332,7 @@ def createProgressFrame(self, donePercentage=None): w, h = int(self.writer.width()), int(self.writer.height()) if self.progressFrame is None: self.progressFrame = createBlankFrame(w, h) - if confManager is None: - downloadText = "Downloading model blob..." - else: - downloadText = f"Downloading {confManager.getModelName()} blob..." + downloadText = "Downloading model blob..." textsize = cv2.getTextSize(downloadText, cv2.FONT_HERSHEY_TRIPLEX, 0.5, 4)[0][0] offset = int((w - textsize) / 2) cv2.putText(self.progressFrame, downloadText, (offset, 250), cv2.FONT_HERSHEY_TRIPLEX, 0.5, (255, 255, 255), 4, cv2.LINE_AA) @@ -674,8 +671,8 @@ def guiOnStaticticsConsent(self, value): pass self.worker.signals.setDataSignal.emit(["restartRequired", True]) - def guiOnToggleSyncPreview(self, value): - self.updateArg("syncPreviews", value) + def guiOnToggleSync(self, value): + self.updateArg("sync", value) def guiOnToggleColorEncoding(self, enabled, fps): oldConfig = self.confManager.args.encode or {} From 7bdc96b009cde5b2daa325c81a16c471b26a7570 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Pi=C5=82atowski?= Date: Wed, 19 Jan 2022 19:14:05 +0100 Subject: [PATCH 03/22] add webapp template --- depthai_demo.py | 15 ++++--- depthai_helpers/arg_manager.py | 2 +- depthai_helpers/supervisor.py | 2 +- gui/web/main.py | 76 ++++++++++++++++++++++++++++++++++ 4 files changed, 88 insertions(+), 7 deletions(-) create mode 100644 gui/web/main.py diff --git a/depthai_demo.py b/depthai_demo.py index fff0e2a87..bfa3da171 100755 --- a/depthai_demo.py +++ b/depthai_demo.py @@ -555,21 +555,26 @@ def runOpenCv(in_args, instance): if args.guiType == "qt": from gui.qt.main import runQt runQt(args, Demo(displayFrames=False)) + elif args.guiType == "web": + from gui.web.main import runWeb + runWeb(args, Demo(displayFrames=False)) else: args.guiType = "cv" runOpenCv(args, Demo(displayFrames=True)) else: s = Supervisor() - if args.guiType != "cv": + if args.guiType in ("auto", "qt"): available = s.checkQtAvailability() if args.guiType == "qt" and not available: raise RuntimeError("QT backend is not available, run the script with --guiType \"cv\" to use OpenCV backend") - if args.guiType == "auto" and platform.machine() == 'aarch64': # Disable Qt by default on Jetson due to Qt issues - args.guiType = "cv" - elif available: + if available: args.guiType = "qt" - else: + if args.guiType in ("auto", "cv"): + if args.guiType == "auto" and platform.machine() == 'aarch64': # Disable Qt by default on Jetson due to Qt issues args.guiType = "cv" + args.guiType = "cv" + if args.guiType in ("auto", "web"): + args.guiType = "web" s.runDemo(args) except KeyboardInterrupt: sys.exit(0) diff --git a/depthai_helpers/arg_manager.py b/depthai_helpers/arg_manager.py index 19c6e6fb0..9c9f1a578 100644 --- a/depthai_helpers/arg_manager.py +++ b/depthai_helpers/arg_manager.py @@ -122,7 +122,7 @@ def parseArgs(): "If set to \"high\", the output streams will stay uncompressed\n" "If set to \"low\", the output streams will be MJPEG-encoded\n" "If set to \"auto\" (default), the optimal bandwidth will be selected based on your connection type and speed") - parser.add_argument('-gt', '--guiType', type=str, default="auto", choices=["auto", "qt", "cv"], help="Specify GUI type of the demo. \"cv\" uses built-in OpenCV display methods, \"qt\" uses Qt to display interactive GUI. \"auto\" will use OpenCV for Raspberry Pi and Qt for other platforms") + parser.add_argument('-gt', '--guiType', type=str, default="auto", choices=["auto", "qt", "cv", "web"], help="Specify GUI type of the demo. \"cv\" uses built-in OpenCV display methods, \"qt\" uses Qt to display interactive GUI. \"auto\" will use OpenCV for Raspberry Pi and Qt for other platforms") parser.add_argument('-usbs', '--usbSpeed', type=str, default="usb3", choices=["usb2", "usb3"], help="Force USB communication speed. Default: %(default)s") parser.add_argument('-enc', '--encode', type=_comaSeparated(default=30.0, cast=float), nargs="+", default=[], help="Define which cameras to encode (record) \n" diff --git a/depthai_helpers/supervisor.py b/depthai_helpers/supervisor.py index 4d92695a3..1eeff22cd 100644 --- a/depthai_helpers/supervisor.py +++ b/depthai_helpers/supervisor.py @@ -39,7 +39,7 @@ def runDemo(self, args): print("Waiting 5s for the device to be discoverable again...") time.sleep(5) args.guiType = "cv" - if args.guiType == "cv": + else: new_env = env.copy() new_env["DEPTHAI_INSTALL_SIGNAL_HANDLER"] = "0" new_args = createNewArgs(args) diff --git a/gui/web/main.py b/gui/web/main.py new file mode 100644 index 000000000..295acdfa3 --- /dev/null +++ b/gui/web/main.py @@ -0,0 +1,76 @@ +# This Python file uses the following encoding: utf-8 +import time +from functools import cmp_to_key + +import cv2 +import depthai as dai +from depthai_sdk import createBlankFrame + +from depthai_helpers.config_manager import prepareConfManager + +class WebApp: + def __init__(self, instance, args): + super().__init__() + self.confManager = prepareConfManager(args) + self.running = False + self.selectedPreview = self.confManager.args.show[0] if len(self.confManager.args.show) > 0 else "color" + self._demoInstance = instance + self._demoInstance.setCallbacks(shouldRun=self.shouldRun, onShowFrame=self.onShowFrame, onSetup=self.onSetup, onAppSetup=self.onAppSetup, onAppStart=self.onAppStart, showDownloadProgress=self.showDownloadProgress) + + def shouldRun(self): + return True + + def onShowFrame(self, frame, source): + if source == self.selectedPreview: + print(frame, source) + + def onSetup(self, instance): + previewChoices = self.confManager.args.show + devices = [instance._deviceInfo.getMxId()] + list(map(lambda info: info.getMxId(), dai.Device.getAllAvailableDevices())) + countLabels = instance._nnManager._labels if instance._nnManager is not None else [] + depthEnabled = self.confManager.useDepth + modelChoices = sorted(self.confManager.getAvailableZooModels(), key=cmp_to_key(lambda a, b: -1 if a == "mobilenet-ssd" else 1 if b == "mobilenet-ssd" else -1 if a < b else 1)) + + + def onAppSetup(self, app): + setupFrame = createBlankFrame(500, 500) + cv2.putText(setupFrame, "Preparing {} app...".format(app.appName), (150, 250), cv2.FONT_HERSHEY_TRIPLEX, 0.5, (255, 255, 255), 4, cv2.LINE_AA) + cv2.putText(setupFrame, "Preparing {} app...".format(app.appName), (150, 250), cv2.FONT_HERSHEY_TRIPLEX, 0.5, (0, 0, 0), 1, cv2.LINE_AA) + print(setupFrame) + + def onAppStart(self, app): + setupFrame = createBlankFrame(500, 500) + cv2.putText(setupFrame, "Running {} app... (check console)".format(app.appName), (100, 250), cv2.FONT_HERSHEY_TRIPLEX, 0.5, (255, 255, 255), 4, cv2.LINE_AA) + cv2.putText(setupFrame, "Running {} app... (check console)".format(app.appName), (100, 250), cv2.FONT_HERSHEY_TRIPLEX, 0.5, (0, 0, 0), 1, cv2.LINE_AA) + print(setupFrame) + + def showDownloadProgress(self, curr, total): + print(curr, total) + + def start(self): + self.running = True + print("Starting...") + + def stop(self, wait=True): + if hasattr(self._demoInstance, "_device"): + current_mxid = self._demoInstance._device.getMxId() + else: + current_mxid = self.confManager.args.deviceId + + if wait and current_mxid is not None: + start = time.time() + while time.time() - start < 30: + if current_mxid in list(map(lambda info: info.getMxId(), dai.Device.getAllAvailableDevices())): + break + else: + time.sleep(0.1) + else: + print(f"[Warning] Device not available again after 30 seconds! MXID: {current_mxid}") + + def restartDemo(self): + self.stop() + self.start() + + +def runWeb(args, demo_instance): + WebApp(demo_instance, args).start() From 0f7ea6518ed729007098d6e3a57cc3f8ea7baa28 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Pi=C5=82atowski?= Date: Fri, 21 Jan 2022 12:28:33 +0100 Subject: [PATCH 04/22] first working PoC --- depthai_helpers/arg_manager.py | 2 + gui/qt/main.py | 20 ++----- gui/web/main.py | 106 +++++++++++++++++++++++++++++++-- gui/web/static/404.html | 10 ++++ gui/web/static/index.html | 11 ++++ requirements.txt | 1 + 6 files changed, 130 insertions(+), 20 deletions(-) create mode 100644 gui/web/static/404.html create mode 100644 gui/web/static/index.html diff --git a/depthai_helpers/arg_manager.py b/depthai_helpers/arg_manager.py index 9c9f1a578..1756ddd27 100644 --- a/depthai_helpers/arg_manager.py +++ b/depthai_helpers/arg_manager.py @@ -147,4 +147,6 @@ def parseArgs(): parser.add_argument('--skipVersionCheck', action="store_true", help="Disable libraries version check") parser.add_argument('--noSupervisor', action="store_true", help="Disable supervisor check") parser.add_argument('--sync', action="store_true", help="Enable frame and NN synchronization. If enabled, all frames and NN results will be synced before preview (same sequence number)") + parser.add_argument('--host', default="127.0.0.1", help="Specify host address to which web server will bind (used only with guiType set to `web`)") + parser.add_argument('--port', default=8090, type=int, help="Specify port to which web server will bind (used only with guiType set to `web`)") return parser.parse_args() diff --git a/gui/qt/main.py b/gui/qt/main.py index 696725637..2c411f85b 100644 --- a/gui/qt/main.py +++ b/gui/qt/main.py @@ -410,22 +410,10 @@ def run(self): if defaultDevice is None: defaultDevice = devices[0].getMxId() self.conf.args.deviceId = defaultDevice - if Previews.color.name not in self.conf.args.show: - self.conf.args.show.append(Previews.color.name) - if Previews.nnInput.name not in self.conf.args.show: - self.conf.args.show.append(Previews.nnInput.name) - if Previews.depth.name not in self.conf.args.show and Previews.disparityColor.name not in self.conf.args.show: - self.conf.args.show.append(Previews.depth.name) - if Previews.depthRaw.name not in self.conf.args.show and Previews.disparity.name not in self.conf.args.show: - self.conf.args.show.append(Previews.depthRaw.name) - if Previews.left.name not in self.conf.args.show: - self.conf.args.show.append(Previews.left.name) - if Previews.rectifiedLeft.name not in self.conf.args.show: - self.conf.args.show.append(Previews.rectifiedLeft.name) - if Previews.right.name not in self.conf.args.show: - self.conf.args.show.append(Previews.right.name) - if Previews.rectifiedRight.name not in self.conf.args.show: - self.conf.args.show.append(Previews.rectifiedRight.name) + self.conf.args.show = [ + Previews.color.name, Previews.nnInput.name, Previews.depth.name, Previews.depthRaw.name, Previews.left.name, + Previews.rectifiedLeft.name, Previews.right.name, Previews.rectifiedRight.name + ] try: self.instance.run_all(self.conf) except KeyboardInterrupt: diff --git a/gui/web/main.py b/gui/web/main.py index 295acdfa3..68e9e2bcd 100644 --- a/gui/web/main.py +++ b/gui/web/main.py @@ -1,28 +1,79 @@ # This Python file uses the following encoding: utf-8 +import sys +import threading import time +import traceback from functools import cmp_to_key +from http.server import HTTPServer, SimpleHTTPRequestHandler +from io import BytesIO +from pathlib import Path +from PIL import Image import cv2 import depthai as dai -from depthai_sdk import createBlankFrame +from depthai_sdk import createBlankFrame, Previews from depthai_helpers.config_manager import prepareConfManager + +class HttpHandler(SimpleHTTPRequestHandler): + static_prefix = "/app" + static_path = "./static" + + def __init__(self, *args, **kwargs): + super().__init__(*args, directory=str((Path(__file__).parent / self.static_path).absolute()), **kwargs) + + def setup(self): + super().setup() + self.routes = { + "/stream": self.stream + } + + def translate_path(self, path): + if path.startswith(self.static_prefix): + return super().translate_path(path[len(self.static_prefix):]) + else: + return super().translate_path("/404.html") + + def do_GET(self): + if self.path in self.routes.keys(): + return self.routes[self.path]() + else: + return super().do_GET() + + def stream(self): + self.send_response(200) + self.send_header('Content-type', 'multipart/x-mixed-replace; boundary=--jpgboundary') + self.end_headers() + while True: + if hasattr(self.server, 'frametosend'): + image = Image.fromarray(cv2.cvtColor(self.server.frametosend, cv2.COLOR_BGR2RGB)) + stream_file = BytesIO() + image.save(stream_file, 'JPEG') + self.wfile.write("--jpgboundary".encode()) + + self.send_header('Content-type', 'image/jpeg') + self.send_header('Content-length', str(stream_file.getbuffer().nbytes)) + self.end_headers() + image.save(self.wfile, 'JPEG') + + class WebApp: def __init__(self, instance, args): super().__init__() self.confManager = prepareConfManager(args) self.running = False + self.webserver = None self.selectedPreview = self.confManager.args.show[0] if len(self.confManager.args.show) > 0 else "color" self._demoInstance = instance - self._demoInstance.setCallbacks(shouldRun=self.shouldRun, onShowFrame=self.onShowFrame, onSetup=self.onSetup, onAppSetup=self.onAppSetup, onAppStart=self.onAppStart, showDownloadProgress=self.showDownloadProgress) + self.thread = None def shouldRun(self): return True def onShowFrame(self, frame, source): if source == self.selectedPreview: - print(frame, source) + self.webserver.frametosend = frame def onSetup(self, instance): previewChoices = self.confManager.args.show @@ -47,9 +98,53 @@ def onAppStart(self, app): def showDownloadProgress(self, curr, total): print(curr, total) + def onError(self, ex: Exception): + exception_message = ''.join(traceback.format_tb(ex.__traceback__) + [str(ex)]) + print(exception_message) + + def runDemo(self): + self._demoInstance.setCallbacks( + shouldRun=self.shouldRun, onShowFrame=self.onShowFrame, onSetup=self.onSetup, onAppSetup=self.onAppSetup, + onAppStart=self.onAppStart, showDownloadProgress=self.showDownloadProgress + ) + self.confManager.args.bandwidth = "auto" + if self.confManager.args.deviceId is None: + devices = dai.Device.getAllAvailableDevices() + if len(devices) > 0: + defaultDevice = next(map( + lambda info: info.getMxId(), + filter(lambda info: info.desc.protocol == dai.XLinkProtocol.X_LINK_USB_VSC, devices) + ), None) + if defaultDevice is None: + defaultDevice = devices[0].getMxId() + self.confManager.args.deviceId = defaultDevice + self.confManager.args.show = [ + Previews.color.name, Previews.nnInput.name, Previews.depth.name, Previews.depthRaw.name, Previews.left.name, + Previews.rectifiedLeft.name, Previews.right.name, Previews.rectifiedRight.name + ] + try: + self._demoInstance.run_all(self.confManager) + except KeyboardInterrupt: + sys.exit(0) + except Exception as ex: + self.onError(ex) + def start(self): self.running = True - print("Starting...") + self.thread = threading.Thread(target=self.runDemo) + self.thread.daemon = True + self.thread.start() + + if self.webserver is None: + self.webserver = HTTPServer((self.confManager.args.host, self.confManager.args.port), HttpHandler) + print("Server started http://{}:{}/app/".format(self.confManager.args.host, self.confManager.args.port)) + + try: + self.webserver.serve_forever() + except KeyboardInterrupt: + pass + + self.webserver.server_close() def stop(self, wait=True): if hasattr(self._demoInstance, "_device"): @@ -57,6 +152,9 @@ def stop(self, wait=True): else: current_mxid = self.confManager.args.deviceId + self.running = False + self.thread.join() + if wait and current_mxid is not None: start = time.time() while time.time() - start < 30: diff --git a/gui/web/static/404.html b/gui/web/static/404.html new file mode 100644 index 000000000..74d3f21cf --- /dev/null +++ b/gui/web/static/404.html @@ -0,0 +1,10 @@ + + + + + Not Found | DepthAI Demo + + +Requested page was not found + + \ No newline at end of file diff --git a/gui/web/static/index.html b/gui/web/static/index.html new file mode 100644 index 000000000..e7b23d917 --- /dev/null +++ b/gui/web/static/index.html @@ -0,0 +1,11 @@ + + + + + DepthAI Demo + + +Welcome to DepthAI Demo + + + \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index 24774d94c..054a83558 100644 --- a/requirements.txt +++ b/requirements.txt @@ -13,3 +13,4 @@ opencv-contrib-python==4.4.0.46 ; platform_machine == "armv6l" or platform_machi pyqt5>5,<5.15.6 ; platform_machine != "armv6l" and platform_machine != "armv7l" and platform_machine != "aarch64" --extra-index-url https://artifacts.luxonis.com/artifactory/luxonis-python-snapshot-local/ depthai==2.14.1.0.dev+27fa4519f289498e84768ab5229a1a45efb7e4df +Pillow>=7.2.0 \ No newline at end of file From 6739cf73b4ecf2881a28d742cc258ef838b71197 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Pi=C5=82atowski?= Date: Fri, 21 Jan 2022 15:15:36 +0100 Subject: [PATCH 05/22] Add simple webpack config --- .gitignore | 1 - gui/web/{static => }/404.html | 0 gui/web/{static/index.html => dist/404.html} | 5 +- gui/web/dist/index.html | 1 + gui/web/dist/main.js | 1 + gui/web/dist/static/favicon.ico | Bin 0 -> 3870 bytes gui/web/index.html | 28 + gui/web/main.py | 15 +- gui/web/package.json | 28 + gui/web/src/App.js | 14 + gui/web/src/App.scss | 14 + gui/web/src/index.js | 10 + gui/web/src/index.scss | 13 + gui/web/src/logo.svg | 1 + gui/web/static/favicon.ico | Bin 0 -> 3870 bytes gui/web/webpack.config.js | 58 + gui/web/yarn.lock | 3932 ++++++++++++++++++ 17 files changed, 4109 insertions(+), 12 deletions(-) rename gui/web/{static => }/404.html (100%) rename gui/web/{static/index.html => dist/404.html} (55%) create mode 100644 gui/web/dist/index.html create mode 100644 gui/web/dist/main.js create mode 100644 gui/web/dist/static/favicon.ico create mode 100644 gui/web/index.html create mode 100644 gui/web/package.json create mode 100644 gui/web/src/App.js create mode 100644 gui/web/src/App.scss create mode 100644 gui/web/src/index.js create mode 100644 gui/web/src/index.scss create mode 100644 gui/web/src/logo.svg create mode 100644 gui/web/static/favicon.ico create mode 100644 gui/web/webpack.config.js create mode 100644 gui/web/yarn.lock diff --git a/.gitignore b/.gitignore index 4368b1570..cd18bf4c0 100644 --- a/.gitignore +++ b/.gitignore @@ -11,7 +11,6 @@ build/ package/*.tar package/*.tar.gz develop-eggs/ -dist/ downloads/ eggs/ .eggs/ diff --git a/gui/web/static/404.html b/gui/web/404.html similarity index 100% rename from gui/web/static/404.html rename to gui/web/404.html diff --git a/gui/web/static/index.html b/gui/web/dist/404.html similarity index 55% rename from gui/web/static/index.html rename to gui/web/dist/404.html index e7b23d917..74d3f21cf 100644 --- a/gui/web/static/index.html +++ b/gui/web/dist/404.html @@ -2,10 +2,9 @@ - DepthAI Demo + Not Found | DepthAI Demo -Welcome to DepthAI Demo - +Requested page was not found \ No newline at end of file diff --git a/gui/web/dist/index.html b/gui/web/dist/index.html new file mode 100644 index 000000000..b5c1d9b76 --- /dev/null +++ b/gui/web/dist/index.html @@ -0,0 +1 @@ +DepthAI Demo
\ No newline at end of file diff --git a/gui/web/dist/main.js b/gui/web/dist/main.js new file mode 100644 index 000000000..6cf5af399 --- /dev/null +++ b/gui/web/dist/main.js @@ -0,0 +1 @@ +(()=>{"use strict";var e={554:(e,t,n)=>{n.d(t,{Z:()=>u});var r=n(81),l=n.n(r),a=n(645),o=n.n(a)()(l());o.push([e.id,".App{text-align:center}.App-header{background-color:#282c34;min-height:100vh;display:flex;flex-direction:column;align-items:center;justify-content:center;font-size:calc(10px + 2vmin);color:#fff}",""]);const u=o},800:(e,t,n)=>{n.d(t,{Z:()=>u});var r=n(81),l=n.n(r),a=n(645),o=n.n(a)()(l());o.push([e.id,'body{margin:0;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI","Roboto","Oxygen","Ubuntu","Cantarell","Fira Sans","Droid Sans","Helvetica Neue",sans-serif;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}code{font-family:source-code-pro,Menlo,Monaco,Consolas,"Courier New",monospace}',""]);const u=o},645:e=>{e.exports=function(e){var t=[];return t.toString=function(){return this.map((function(t){var n="",r=void 0!==t[5];return t[4]&&(n+="@supports (".concat(t[4],") {")),t[2]&&(n+="@media ".concat(t[2]," {")),r&&(n+="@layer".concat(t[5].length>0?" ".concat(t[5]):""," {")),n+=e(t),r&&(n+="}"),t[2]&&(n+="}"),t[4]&&(n+="}"),n})).join("")},t.i=function(e,n,r,l,a){"string"==typeof e&&(e=[[null,e,void 0]]);var o={};if(r)for(var u=0;u0?" ".concat(c[5]):""," {").concat(c[1],"}")),c[5]=a),n&&(c[2]?(c[1]="@media ".concat(c[2]," {").concat(c[1],"}"),c[2]=n):c[2]=n),l&&(c[4]?(c[1]="@supports (".concat(c[4],") {").concat(c[1],"}"),c[4]=l):c[4]="".concat(l)),t.push(c))}},t}},81:e=>{e.exports=function(e){return e[1]}},418:e=>{var t=Object.getOwnPropertySymbols,n=Object.prototype.hasOwnProperty,r=Object.prototype.propertyIsEnumerable;function l(e){if(null==e)throw new TypeError("Object.assign cannot be called with null or undefined");return Object(e)}e.exports=function(){try{if(!Object.assign)return!1;var e=new String("abc");if(e[5]="de","5"===Object.getOwnPropertyNames(e)[0])return!1;for(var t={},n=0;n<10;n++)t["_"+String.fromCharCode(n)]=n;if("0123456789"!==Object.getOwnPropertyNames(t).map((function(e){return t[e]})).join(""))return!1;var r={};return"abcdefghijklmnopqrst".split("").forEach((function(e){r[e]=e})),"abcdefghijklmnopqrst"===Object.keys(Object.assign({},r)).join("")}catch(e){return!1}}()?Object.assign:function(e,a){for(var o,u,i=l(e),s=1;s{var r=n(294),l=n(418),a=n(840);function o(e){for(var t="https://reactjs.org/docs/error-decoder.html?invariant="+e,n=1;n