diff --git a/.gitignore b/.gitignore index 7b0803a..7d778ad 100644 --- a/.gitignore +++ b/.gitignore @@ -1,26 +1,27 @@ -# Python -*.pyc -__pycache__/ -*.pyo -*.pyd -*.pyw -*.pyz -*.pyzw -*.pycachefile -*.egg-info/ -dist/ -build/ - -# macOS -.DS_Store -.AppleDouble -.LSOverride -Icon -._* -.Spotlight-V100 -.Trashes -__MACOSX/ - -.env -output/ -.vscode +# Python +*.pyc +__pycache__/ +*.pyo +*.pyd +*.pyw +*.pyz +*.pyzw +*.pycachefile +*.egg-info/ +dist/ +build/ + +# macOS +.DS_Store +.AppleDouble +.LSOverride +Icon +._* +.Spotlight-V100 +.Trashes +__MACOSX/ + +.env +output/ +.vscode +.aider* diff --git a/README.md b/README.md index c0ceaa6..8fec912 100644 --- a/README.md +++ b/README.md @@ -33,17 +33,17 @@ If you'd like to donate to help support the project, you can do so on [GitHub](h - Perspective correction - Image processing and binarization techniques, local, global etc. - Output to text files (.txt, .csv, .xml) -- HTTP output via local server: HTML, JSON, XML and CSV endpoints +- [HTTP output via local server](docs/http_server.md): HTML, JSON, XML and CSV endpoints - Call external HTTP services with the OCR data - Import & Export configuration profiles -- Integrations: OBS (websocket), vMix (API), NewBlue FX Titler (API) +- Integrations: [OBS](https://obsproject.com/) (websocket), [vMix](docs/vmix.md) (API), [NewBlue FX Titler](https://newbluefx.com/titler-live) (API), [UNO](https://www.overlays.uno/) (API), [generic HTTP APIs](docs/out_api.md) - Up to 30 updates/s - Unlimited detection boxes - Camera bump and drift correction with stabilization algorithm - Unlimited devices or open instances on the same device - Detect any scoreboard fonts, general fonts and even "dot" indicators - Translated to 12 languages (English, German, Spanish, French, Italian, Japanese, Korean, Dutch, Polish, Portugese, Russian, Chinese) -- Collect OCR training data and annotate it with a built-in tool +- [Collect OCR training data](docs/data_annotation.md) and annotate it with a built-in tool Price: FREE. @@ -101,7 +101,7 @@ For Mac and Windows there are further dependencies in `requirements-mac.txt` and There are some extra steps for installation on Windows: - Download and install https://visualstudio.microsoft.com/visual-cpp-build-tools/ C++ Build Tools - - Build the win32DeviceEnum pyd by `$ cd win32DeviceEnum && python.exe setup.py build_ext --inplace` + - Build the win32DeviceEnum pyd by `$ cd src/win32DeviceEnum && python.exe setup.py build_ext --inplace` #### MacOS diff --git a/docs/README.md b/docs/README.md index c67d148..1e2ddb3 100644 --- a/docs/README.md +++ b/docs/README.md @@ -30,10 +30,10 @@ If you'd like to donate to help support the project, you can do so on [GitHub](h - Perspective correction - Image processing and binarization techniques, local, global etc. - Output to text files (.txt, .csv, .xml) -- HTTP output via local server: HTML, JSON, XML and CSV endpoints +- [HTTP output via local server](http_server.md): HTML, JSON, XML and CSV endpoints - Call external HTTP services with the OCR data - Import & Export configuration profiles -- Integrations: OBS (websocket), vMix (API), NewBlue FX Titler (API) +- Integrations: [OBS](https://obsproject.com/) (websocket), [vMix](vmix.md) (API), [NewBlue FX Titler](https://newbluefx.com/titler-live) (API), [UNO](https://www.overlays.uno/) (API), generic HTTP APIs - Up to 30 updates/s - Unlimited detection boxes - Template fields: Derived from other fields and optional extra text diff --git a/requirements.txt b/requirements.txt index 3561271..2ba9b7f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -6,7 +6,7 @@ opencv-python==4.10.0.84 pillow platformdirs pyinstaller==6.10.0 -pyside6 +pyside6==6.7.3 python-dotenv requests tesserocr diff --git a/scoresight.spec b/scoresight.spec index 5e55d9c..b0ac0fc 100644 --- a/scoresight.spec +++ b/scoresight.spec @@ -51,9 +51,10 @@ datas = [ sources = [ 'src/api_output.py', 'src/base_video_capture.py', + 'src/box_settings_ui_handler.py', 'src/camera_info.py', - 'src/camera_view.py', 'src/camera_thread.py', + 'src/camera_view.py', 'src/defaults.py', 'src/file_output.py', 'src/frame_stabilizer.py', @@ -63,14 +64,15 @@ sources = [ 'src/main.py', 'src/mainwindow.py', 'src/ndi.py', - 'src/ocr_training_data.py', 'src/obs_websocket.py', + 'src/ocr_training_data.py', 'src/resizable_rect.py', 'src/resource_path.py', 'src/sc_logging.py', 'src/screen_capture_source.py', 'src/source_view.py', 'src/storage.py', + 'src/template_fields.py', 'src/tesseract.py', 'src/text_detection_target.py', 'src/training_dojo.py', @@ -85,8 +87,12 @@ sources = [ 'src/ui_update_available.py', 'src/ui_url_source.py', 'src/ui_video_settings.py', + 'src/uno_output.py', + 'src/uno_ui_handler.py', 'src/update_check.py', + 'src/video_settings.py', 'src/vmix_output.py', + 'src/vmix_ui_handler.py', ] if args.win: diff --git a/scripts/compile_ui.sh b/scripts/compile_ui.sh new file mode 100755 index 0000000..b1d3e1b --- /dev/null +++ b/scripts/compile_ui.sh @@ -0,0 +1,20 @@ +#!/bin/bash + +# Directory containing the .ui files +UI_DIR="$(dirname "$0")/../src" +# Directory to output the compiled .py files +OUTPUT_DIR=$UI_DIR + +# Ensure the output directory exists +mkdir -p "$OUTPUT_DIR" + +# Compile each .ui file in the UI_DIR +for ui_file in "$UI_DIR"/*.ui; do + # Get the base name of the file (without extension) + base_name=$(basename "$ui_file" .ui) + echo "Compiling $ui_file to $OUTPUT_DIR/ui_${base_name}.py" + # Compile the .ui file to a .py file + pyside6-uic "$ui_file" -o "$OUTPUT_DIR/ui_${base_name}.py" +done + +echo "UI files compiled successfully." diff --git a/src/box_settings_ui_handler.py b/src/box_settings_ui_handler.py new file mode 100644 index 0000000..0ef5fca --- /dev/null +++ b/src/box_settings_ui_handler.py @@ -0,0 +1,245 @@ +from functools import partial + +from defaults import ( + default_info_for_box_name, + normalize_settings_dict, + format_prefixes, +) +from storage import TextDetectionTargetMemoryStorage +from ui_mainwindow import Ui_MainWindow +from sc_logging import logger + + +class BoxSettingsUIHandler: + def __init__(self, ui: Ui_MainWindow): + self.ui = ui + self.boxSettingsUiSetup() + self.detectionTargetsStorage = TextDetectionTargetMemoryStorage() + + def editSettings(self, settingsMutatorCallback): + # update the selected item's settings in the detectionTargetsStorage + item = self.ui.tableWidget_boxes.currentItem() + if item is None: + logger.info("no item selected") + return + item_name = item.text() + item_obj = self.detectionTargetsStorage.find_item_by_name(item_name) + if item_obj is None: + logger.info("item not found: %s", item_name) + return + item_obj = settingsMutatorCallback(item_obj) + self.detectionTargetsStorage.edit_item(item_name, item_obj) + + def restoreDefaults(self): + # restore the default settings for the selected item + def restoreDefaultsSettings(item_obj): + info = default_info_for_box_name(item_obj.name) + item_obj.settings = normalize_settings_dict({}, info) + return item_obj + + self.editSettings(restoreDefaultsSettings) + self.populateSettings(self.ui.tableWidget_boxes.currentItem().text()) + + def confThreshChanged(self): + self.genericSettingsChanged( + "conf_thresh", float(self.ui.horizontalSlider_conf_thresh.value()) / 100.0 + ) + + def cleanupThreshChanged(self): + self.genericSettingsChanged( + "cleanup_thresh", float(self.ui.horizontalSlider_cleanup.value()) / 100.0 + ) + + def formatPrefixChanged(self, index): + if index == 12: + return # do nothing if "Select Preset" is selected + # based on the selected index, set the format prefix + # change lineEdit_format to the selected format prefix + self.ui.lineEdit_format.setText(format_prefixes[index]) + + def genericSettingsChanged(self, settingName, value): + def editGenericSettings(item_obj): + item_obj.settings[settingName] = value + return item_obj + + self.editSettings(editGenericSettings) + + def boxSettingsUiSetup(self): + self.ui.pushButton_restoreDefaults.clicked.connect(self.restoreDefaults) + self.ui.checkBox_smoothing.toggled.connect( + partial(self.genericSettingsChanged, "smoothing") + ) + self.ui.checkBox_skip_empty.toggled.connect( + partial(self.genericSettingsChanged, "skip_empty") + ) + self.ui.horizontalSlider_conf_thresh.valueChanged.connect( + self.confThreshChanged + ) + self.ui.lineEdit_format.textChanged.connect( + partial(self.genericSettingsChanged, "format_regex") + ) + self.ui.comboBox_fieldType.currentIndexChanged.connect( + partial(self.genericSettingsChanged, "type") + ) + self.ui.checkBox_skip_similar_image.toggled.connect( + partial(self.genericSettingsChanged, "skip_similar_image") + ) + self.ui.checkBox_autocrop.toggled.connect( + partial(self.genericSettingsChanged, "autocrop") + ) + self.ui.horizontalSlider_cleanup.valueChanged.connect(self.cleanupThreshChanged) + self.ui.horizontalSlider_dilate.valueChanged.connect( + partial(self.genericSettingsChanged, "dilate") + ) + self.ui.horizontalSlider_skew.valueChanged.connect( + partial(self.genericSettingsChanged, "skew") + ) + self.ui.horizontalSlider_vscale.valueChanged.connect( + partial(self.genericSettingsChanged, "vscale") + ) + self.ui.checkBox_removeLeadingZeros.toggled.connect( + partial(self.genericSettingsChanged, "remove_leading_zeros") + ) + self.ui.checkBox_rescalePatch.toggled.connect( + partial(self.genericSettingsChanged, "rescale_patch") + ) + self.ui.checkBox_normWHRatio.toggled.connect( + partial(self.genericSettingsChanged, "normalize_wh_ratio") + ) + self.ui.checkBox_invertPatch.toggled.connect( + partial(self.genericSettingsChanged, "invert_patch") + ) + self.ui.checkBox_dotDetector.toggled.connect( + partial(self.genericSettingsChanged, "dot_detector") + ) + self.ui.checkBox_ordinalIndicator.toggled.connect( + partial(self.genericSettingsChanged, "ordinal_indicator") + ) + self.ui.comboBox_binarizationMethod.currentIndexChanged.connect( + partial(self.genericSettingsChanged, "binarization_method") + ) + self.ui.lineEdit_templatefield.textChanged.connect( + partial(self.genericSettingsChanged, "templatefield_text") + ) + self.ui.checkBox_compositeBox.toggled.connect( + partial(self.genericSettingsChanged, "composite_box") + ) + self.ui.comboBox_formatPrefix.currentIndexChanged.connect( + self.formatPrefixChanged + ) + + def populateSettings(self, name): + self.ui.lineEdit_format.blockSignals(True) + self.ui.comboBox_fieldType.blockSignals(True) + self.ui.checkBox_smoothing.blockSignals(True) + self.ui.checkBox_skip_empty.blockSignals(True) + self.ui.horizontalSlider_conf_thresh.blockSignals(True) + self.ui.checkBox_autocrop.blockSignals(True) + self.ui.checkBox_skip_similar_image.blockSignals(True) + self.ui.horizontalSlider_cleanup.blockSignals(True) + self.ui.horizontalSlider_dilate.blockSignals(True) + self.ui.horizontalSlider_skew.blockSignals(True) + self.ui.horizontalSlider_vscale.blockSignals(True) + self.ui.checkBox_removeLeadingZeros.blockSignals(True) + self.ui.checkBox_rescalePatch.blockSignals(True) + self.ui.checkBox_normWHRatio.blockSignals(True) + self.ui.checkBox_invertPatch.blockSignals(True) + self.ui.checkBox_ordinalIndicator.blockSignals(True) + self.ui.comboBox_binarizationMethod.blockSignals(True) + self.ui.comboBox_formatPrefix.blockSignals(True) + self.ui.checkBox_templatefield.blockSignals(True) + self.ui.lineEdit_templatefield.blockSignals(True) + self.ui.checkBox_compositeBox.blockSignals(True) + + # populate the settings from the detectionTargetsStorage + item_obj = self.detectionTargetsStorage.find_item_by_name(name) + if item_obj is None: + self.ui.lineEdit_format.setText("") + self.ui.comboBox_fieldType.setCurrentIndex(0) + self.ui.checkBox_smoothing.setChecked(True) + self.ui.checkBox_skip_empty.setChecked(True) + self.ui.horizontalSlider_conf_thresh.setValue(50) + self.ui.checkBox_autocrop.setChecked(False) + self.ui.checkBox_skip_similar_image.setChecked(False) + self.ui.horizontalSlider_cleanup.setValue(0) + self.ui.horizontalSlider_dilate.setValue(1) + self.ui.horizontalSlider_skew.setValue(0) + self.ui.horizontalSlider_vscale.setValue(10) + self.ui.label_selectedInfo.setText("") + self.ui.checkBox_removeLeadingZeros.setChecked(False) + self.ui.checkBox_rescalePatch.setChecked(False) + self.ui.checkBox_normWHRatio.setChecked(False) + self.ui.checkBox_invertPatch.setChecked(False) + self.ui.checkBox_ordinalIndicator.setChecked(False) + self.ui.comboBox_binarizationMethod.setCurrentIndex(0) + self.ui.checkBox_templatefield.setChecked(False) + self.ui.lineEdit_templatefield.setText("") + self.ui.checkBox_compositeBox.setChecked(False) + else: + item_obj.settings = normalize_settings_dict( + item_obj.settings, default_info_for_box_name(item_obj.name) + ) + self.ui.label_selectedInfo.setText(f"{item_obj.name}") + self.ui.lineEdit_format.setText(item_obj.settings["format_regex"]) + self.ui.comboBox_fieldType.setCurrentIndex(item_obj.settings["type"]) + self.ui.checkBox_smoothing.setChecked(item_obj.settings["smoothing"]) + self.ui.checkBox_skip_empty.setChecked(item_obj.settings["skip_empty"]) + self.ui.horizontalSlider_conf_thresh.setValue( + int(item_obj.settings["conf_thresh"] * 100) + ) + self.ui.checkBox_autocrop.setChecked(item_obj.settings["autocrop"]) + self.ui.checkBox_skip_similar_image.setChecked( + item_obj.settings["skip_similar_image"] + ) + self.ui.horizontalSlider_cleanup.setValue( + int(item_obj.settings["cleanup_thresh"] * 100) + ) + self.ui.horizontalSlider_dilate.setValue(item_obj.settings["dilate"]) + self.ui.horizontalSlider_skew.setValue(item_obj.settings["skew"]) + self.ui.horizontalSlider_vscale.setValue(item_obj.settings["vscale"]) + self.ui.checkBox_removeLeadingZeros.setChecked( + item_obj.settings["remove_leading_zeros"] + ) + self.ui.checkBox_rescalePatch.setChecked(item_obj.settings["rescale_patch"]) + self.ui.checkBox_normWHRatio.setChecked( + item_obj.settings["normalize_wh_ratio"] + ) + self.ui.checkBox_invertPatch.setChecked(item_obj.settings["invert_patch"]) + self.ui.checkBox_dotDetector.setChecked(item_obj.settings["dot_detector"]) + self.ui.checkBox_ordinalIndicator.setChecked( + item_obj.settings["ordinal_indicator"] + ) + self.ui.comboBox_binarizationMethod.setCurrentIndex( + item_obj.settings["binarization_method"] + ) + self.ui.checkBox_templatefield.setChecked( + item_obj.settings["templatefield"] + ) + self.ui.lineEdit_templatefield.setText( + item_obj.settings["templatefield_text"] + ) + self.ui.checkBox_compositeBox.setChecked(item_obj.settings["composite_box"]) + + self.ui.comboBox_formatPrefix.setCurrentIndex(12) + + self.ui.lineEdit_format.blockSignals(False) + self.ui.comboBox_fieldType.blockSignals(False) + self.ui.checkBox_smoothing.blockSignals(False) + self.ui.checkBox_skip_empty.blockSignals(False) + self.ui.horizontalSlider_conf_thresh.blockSignals(False) + self.ui.checkBox_autocrop.blockSignals(False) + self.ui.checkBox_skip_similar_image.blockSignals(False) + self.ui.horizontalSlider_cleanup.blockSignals(False) + self.ui.horizontalSlider_dilate.blockSignals(False) + self.ui.horizontalSlider_skew.blockSignals(False) + self.ui.horizontalSlider_vscale.blockSignals(False) + self.ui.checkBox_removeLeadingZeros.blockSignals(False) + self.ui.checkBox_rescalePatch.blockSignals(False) + self.ui.checkBox_normWHRatio.blockSignals(False) + self.ui.checkBox_invertPatch.blockSignals(False) + self.ui.checkBox_ordinalIndicator.blockSignals(False) + self.ui.comboBox_binarizationMethod.blockSignals(False) + self.ui.comboBox_formatPrefix.blockSignals(False) + self.ui.checkBox_templatefield.blockSignals(False) + self.ui.lineEdit_templatefield.blockSignals(False) + self.ui.checkBox_compositeBox.blockSignals(False) diff --git a/src/defaults.py b/src/defaults.py index 15f2115..65be08f 100644 --- a/src/defaults.py +++ b/src/defaults.py @@ -218,4 +218,7 @@ def normalize_settings_dict(settings, box_info): "templatefield_text": ( settings["templatefield_text"] if "templatefield_text" in settings else "" ), + "composite_box": ( + settings["composite_box"] if "composite_box" in settings else False + ), } diff --git a/src/http_server.py b/src/http_server.py index 578374e..38c0561 100644 --- a/src/http_server.py +++ b/src/http_server.py @@ -4,6 +4,7 @@ import logging import os import signal +import socket import threading from fastapi import FastAPI, Query from fastapi.responses import HTMLResponse, JSONResponse, Response @@ -196,8 +197,17 @@ async def get_csv(): return Response(content=output.getvalue(), media_type="text/csv") +def is_port_in_use(port: int) -> bool: + with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: + return s.connect_ex(("localhost", port)) == 0 + + def start_http_server(): def run_uvicorn(): + if is_port_in_use(PORT): + logger.error(f"Port {PORT} is already in use") + return + config = uvicorn.Config( app=app, host="0.0.0.0", @@ -212,7 +222,10 @@ def run_uvicorn(): asyncio.set_event_loop(loop) logger.info(f"Server starting at port {PORT}") - loop.run_until_complete(server.serve()) + try: + loop.run_until_complete(server.serve()) + except Exception as e: + logger.error(f"Error running server: {e}") loop.close() logger.info("Server thread stopped") diff --git a/src/mainwindow.py b/src/mainwindow.py index 14f7ca9..b15462e 100644 --- a/src/mainwindow.py +++ b/src/mainwindow.py @@ -13,7 +13,7 @@ QMessageBox, QTableWidgetItem, ) -from PySide6.QtGui import QIcon, QStandardItemModel, QStandardItem, QDesktopServices +from PySide6.QtGui import QIcon, QDesktopServices from PySide6.QtCore import ( Qt, Signal, @@ -30,6 +30,7 @@ from platformdirs import user_data_dir from api_output import update_out_api +from box_settings_ui_handler import BoxSettingsUIHandler from camera_info import CameraInfo from get_camera_info import get_camera_info from http_server import start_http_server, update_http_server @@ -41,7 +42,6 @@ default_boxes, default_info_for_box_name, normalize_settings_dict, - format_prefixes, ) from storage import ( @@ -63,16 +63,17 @@ from template_fields import evaluate_template_field from text_detection_target import TextDetectionTarget, TextDetectionTargetWithResult from sc_logging import logger +from uno_ui_handler import UNOUIHandler from update_check import check_for_updates from log_view import LogViewerDialog import file_output from video_settings import VideoSettingsDialog -from vmix_output import VMixAPI from ui_mainwindow import Ui_MainWindow from ui_about import Ui_Dialog as Ui_About from ui_connect_obs import Ui_Dialog as Ui_ConnectObs from ui_url_source import Ui_Dialog as Ui_UrlSource from ui_screen_capture import Ui_Dialog as Ui_ScreenCapture +from vmix_ui_handler import VMixUIHanlder def clear_layout(layout): @@ -144,7 +145,11 @@ def __init__(self, translator: QTranslator, parent: QObject): self.ui.pushButton_connectObs.clicked.connect(self.openOBSConnectModal) - self.vmixUiSetup() + self.vmixUiHandler = VMixUIHanlder(self.ui) + self.unoUiHandler = UNOUIHandler(self.ui) + self.boxSettingsUiHandler = BoxSettingsUIHandler(self.ui) + + self.ui.checkBox_templatefield.toggled.connect(self.makeTemplateField) start_http_server() @@ -261,69 +266,15 @@ def __init__(self, translator: QTranslator, parent: QObject): self.ui.toolButton_trashFolder.clicked.connect(self.clearOutputFolder) self.ui.pushButton_stopUpdates.toggled.connect(self.toggleStopUpdates) self.ui.comboBox_ocrModel.currentIndexChanged.connect(self.ocrModelChanged) - self.ui.pushButton_restoreDefaults.clicked.connect(self.restoreDefaults) self.ui.toolButton_zoomReset.clicked.connect(self.resetZoom) self.ui.toolButton_osd.toggled.connect(self.toggleOSD) - self.ui.toolButton_showOCRrects.toggled.connect(self.toggleOCRRects) - self.ui.checkBox_smoothing.toggled.connect( - partial(self.genericSettingsChanged, "smoothing") - ) - self.ui.checkBox_skip_empty.toggled.connect( - partial(self.genericSettingsChanged, "skip_empty") - ) - self.ui.horizontalSlider_conf_thresh.valueChanged.connect( - self.confThreshChanged - ) - self.ui.lineEdit_format.textChanged.connect( - partial(self.genericSettingsChanged, "format_regex") - ) - self.ui.comboBox_fieldType.currentIndexChanged.connect( - partial(self.genericSettingsChanged, "type") - ) - self.ui.checkBox_skip_similar_image.toggled.connect( - partial(self.genericSettingsChanged, "skip_similar_image") - ) - self.ui.checkBox_autocrop.toggled.connect( - partial(self.genericSettingsChanged, "autocrop") - ) - self.ui.horizontalSlider_cleanup.valueChanged.connect(self.cleanupThreshChanged) - self.ui.horizontalSlider_dilate.valueChanged.connect( - partial(self.genericSettingsChanged, "dilate") - ) - self.ui.horizontalSlider_skew.valueChanged.connect( - partial(self.genericSettingsChanged, "skew") - ) - self.ui.horizontalSlider_vscale.valueChanged.connect( - partial(self.genericSettingsChanged, "vscale") - ) - self.ui.checkBox_removeLeadingZeros.toggled.connect( - partial(self.genericSettingsChanged, "remove_leading_zeros") - ) - self.ui.checkBox_rescalePatch.toggled.connect( - partial(self.genericSettingsChanged, "rescale_patch") - ) - self.ui.checkBox_normWHRatio.toggled.connect( - partial(self.genericSettingsChanged, "normalize_wh_ratio") - ) - self.ui.checkBox_invertPatch.toggled.connect( - partial(self.genericSettingsChanged, "invert_patch") - ) - self.ui.checkBox_dotDetector.toggled.connect( - partial(self.genericSettingsChanged, "dot_detector") - ) - self.ui.checkBox_ordinalIndicator.toggled.connect( - partial(self.genericSettingsChanged, "ordinal_indicator") - ) - self.ui.comboBox_binarizationMethod.currentIndexChanged.connect( - partial(self.genericSettingsChanged, "binarization_method") - ) - self.ui.checkBox_templatefield.toggled.connect(self.makeTemplateField) - self.ui.lineEdit_templatefield.textChanged.connect( - partial(self.genericSettingsChanged, "templatefield_text") + self.ui.comboBox_boxDisplayStyle.currentIndexChanged.connect( + partial(self.globalSettingsChanged, "box_display_style") ) - self.ui.comboBox_formatPrefix.currentIndexChanged.connect( - self.formatPrefixChanged + self.ui.comboBox_boxDisplayStyle.setCurrentIndex( + fetch_data("scoresight.json", "box_display_style", 3) ) + self.ui.checkBox_updateOnchange.toggled.connect(self.toggleUpdateOnChange) # populate the tableWidget_boxes with the default and custom boxes @@ -417,7 +368,7 @@ def toggleSpeed(self): # possible speeds are x2, x4, x8, x16, x32 and back to x1 # change the button text to the current speed # change the speed of the image viewer - if self.image_viewer: + if self.image_viewer and self.image_viewer.timerThread is not None: speed = self.image_viewer.timerThread.getSpeed() if speed == 1: speed = 2 @@ -480,7 +431,11 @@ def globalSettingsChanged(self, settingName, value): store_data("scoresight.json", settingName, value) def eventFilter(self, obj, event): - if event.type() == QEvent.KeyPress and event.key() == Qt.Key_Alt: + if ( + event.type() == QEvent.KeyPress + and isinstance(event, QKeyEvent) + and event.key() == Qt.Key_Alt + ): self.menubar.setVisible(True) elif event.type() == QEvent.FocusOut and self.menubar.isVisible(): self.menubar.setVisible(False) @@ -521,13 +476,6 @@ def toggleUpdateOnChange(self, value): if self.image_viewer: self.image_viewer.setUpdateOnChange(value) - def formatPrefixChanged(self, index): - if index == 12: - return # do nothing if "Select Preset" is selected - # based on the selected index, set the format prefix - # change lineEdit_format to the selected format prefix - self.ui.lineEdit_format.setText(format_prefixes[index]) - def importConfiguration(self): # open a file dialog to select a configuration file file, _ = QFileDialog.getOpenFileName( @@ -575,10 +523,6 @@ def toggleOSD(self, value): if self.image_viewer: self.image_viewer.toggleOSD(value) - def toggleOCRRects(self, value): - if self.image_viewer: - self.image_viewer.toggleOCRRects(value) - def resetZoom(self): if self.image_viewer: self.image_viewer.resetZoom() @@ -652,165 +596,7 @@ def clearOutputFolder(self): self.out_folder = None remove_data("scoresight.json", "output_folder") - def editSettings(self, settingsMutatorCallback): - # update the selected item's settings in the detectionTargetsStorage - item = self.ui.tableWidget_boxes.currentItem() - if item is None: - logger.info("no item selected") - return - item_name = item.text() - item_obj = self.detectionTargetsStorage.find_item_by_name(item_name) - if item_obj is None: - logger.info("item not found: %s", item_name) - return - item_obj = settingsMutatorCallback(item_obj) - self.detectionTargetsStorage.edit_item(item_name, item_obj) - - def restoreDefaults(self): - # restore the default settings for the selected item - def restoreDefaultsSettings(item_obj): - info = default_info_for_box_name(item_obj.name) - item_obj.settings = normalize_settings_dict({}, info) - return item_obj - - self.editSettings(restoreDefaultsSettings) - self.populateSettings(self.ui.tableWidget_boxes.currentItem().text()) - - def confThreshChanged(self): - def editConfThreshSettings(item_obj): - item_obj.settings["conf_thresh"] = ( - float(self.ui.horizontalSlider_conf_thresh.value()) / 100.0 - ) - return item_obj - - self.editSettings(editConfThreshSettings) - - def cleanupThreshChanged(self): - def editCleanupThreshSettings(item_obj): - item_obj.settings["cleanup_thresh"] = ( - float(self.ui.horizontalSlider_cleanup.value()) / 100.0 - ) - return item_obj - - self.editSettings(editCleanupThreshSettings) - - def genericSettingsChanged(self, settingName, value): - def editGenericSettings(item_obj): - item_obj.settings[settingName] = value - return item_obj - - self.editSettings(editGenericSettings) - - def vmixConnectionChanged(self): - self.vmixUpdater = VMixAPI( - self.ui.lineEdit_vmixHost.text(), - self.ui.lineEdit_vmixPort.text(), - self.ui.inputLineEdit_vmix.text(), - {}, - ) - self.globalSettingsChanged("vmix_host", self.ui.lineEdit_vmixHost.text()) - self.globalSettingsChanged("vmix_port", self.ui.lineEdit_vmixPort.text()) - self.globalSettingsChanged("vmix_input", self.ui.inputLineEdit_vmix.text()) - - def vmixMappingChanged(self, _): - # store entire mapping data in scoresight.json - mapping = {} - model = self.ui.tableView_vmixMapping.model() - if isinstance(model, QStandardItemModel): - for i in range(model.rowCount()): - item = model.item(i, 0) - value = model.item(i, 1) - if item and value: - mapping[item.text()] = value.text() - self.globalSettingsChanged("vmix_mapping", mapping) - self.vmixUpdater.set_field_mapping(mapping) - else: - logger.error("vmixMappingChanged: model is not a QStandardItemModel") - - def vmixUiSetup(self): - # populate the vmix connection from storage - self.ui.lineEdit_vmixHost.setText( - fetch_data("scoresight.json", "vmix_host", "localhost") - ) - self.ui.lineEdit_vmixPort.setText( - fetch_data("scoresight.json", "vmix_port", "8099") - ) - self.ui.inputLineEdit_vmix.setText( - fetch_data("scoresight.json", "vmix_input", "1") - ) - # connect the lineEdits to vmixConnectionChanged - self.ui.lineEdit_vmixHost.textChanged.connect(self.vmixConnectionChanged) - self.ui.lineEdit_vmixPort.textChanged.connect(self.vmixConnectionChanged) - self.ui.inputLineEdit_vmix.textChanged.connect(self.vmixConnectionChanged) - - # create the vmixUpdater - self.vmixUpdater = VMixAPI( - self.ui.lineEdit_vmixHost.text(), - self.ui.lineEdit_vmixPort.text(), - self.ui.inputLineEdit_vmix.text(), - {}, - ) - # add standard item model to the tableView_vmixMapping - self.ui.tableView_vmixMapping.setModel(QStandardItemModel()) - mapping = fetch_data("scoresight.json", "vmix_mapping", {}) - if mapping: - self.vmixUpdater.set_field_mapping(mapping) - - self.ui.tableView_vmixMapping.model().dataChanged.connect( - self.vmixMappingChanged - ) - - self.ui.pushButton_startvmix.toggled.connect(self.togglevMix) - - def togglevMix(self, value): - if not self.vmixUpdater: - return - if value: - self.ui.pushButton_startvmix.setText("🛑 Stop vMix") - self.vmixUpdater.running = True - else: - self.ui.pushButton_startvmix.setText("▶️ Start vMix") - self.vmixUpdater.running = False - - def updatevMixTable(self, detectionTargets): - mapping_storage = fetch_data("scoresight.json", "vmix_mapping") - model = QStandardItemModel() - model.blockSignals(True) - - for box in detectionTargets: - # add the detection to the vmix output mapping: tableView_vmixMapping - # check if the table already has the detectionTarget - items = model.findItems(box.name, Qt.MatchFlag.MatchExactly) - if len(items) == 0: - # add the item to the list - row = model.rowCount() - model.insertRow(row) - model.setItem(row, 0, QStandardItem(box.name)) - # the first item shouldn't be editable - model.item(row, 0).setFlags(Qt.ItemFlag.NoItemFlags) - else: - # update the item in the list - item = items[0] - row = item.row() - - # get value from storage or use the box name - if mapping_storage and box.name in mapping_storage: - model.setItem(row, 1, QStandardItem(mapping_storage[box.name])) - else: - model.setItem(row, 1, QStandardItem(box.name)) - # remove the items that are not in the detectionTargets - for i in range(model.rowCount()): - item = model.item(i, 0) - if not any([box.name == item.text() for box in detectionTargets]): - model.removeRow(i) - - model.blockSignals(False) - self.ui.tableView_vmixMapping.setModel(model) - self.ui.tableView_vmixMapping.model().dataChanged.connect( - self.vmixMappingChanged - ) - - def detectionTargetsChanged(self, detectionTargets): + def detectionTargetsChanged(self, detectionTargets: list[TextDetectionTarget]): for box in detectionTargets: logger.debug(f"Change: Detection target: {box.name}") # change the list icon to green checkmark @@ -835,7 +621,7 @@ def detectionTargetsChanged(self, detectionTargets): else: item = items[0] - if not box.settings["templatefield"]: + if box.settings is None or not box.settings["templatefield"]: # this is a detection target item.setIcon(QIcon(resource_path("icons", "circle-check.svg"))) item.setData(Qt.ItemDataRole.UserRole, "checked") @@ -844,7 +630,8 @@ def detectionTargetsChanged(self, detectionTargets): item.setIcon(QIcon(resource_path("icons", "template-field.svg"))) item.setData(Qt.ItemDataRole.UserRole, "templatefield") - self.updatevMixTable(detectionTargets) + self.vmixUiHandler.updatevMixTable(detectionTargets) + self.unoUiHandler.updateUNOTable(detectionTargets) # if save_csv is enabled, truncate the aggregate file if self.ui.checkBox_saveCsv.isChecked() and self.out_folder: @@ -859,118 +646,6 @@ def detectionTargetsChanged(self, detectionTargets): except Exception as e: logger.error(f"Error truncating aggregate file: {e}") - def populateSettings(self, name): - self.ui.lineEdit_format.blockSignals(True) - self.ui.comboBox_fieldType.blockSignals(True) - self.ui.checkBox_smoothing.blockSignals(True) - self.ui.checkBox_skip_empty.blockSignals(True) - self.ui.horizontalSlider_conf_thresh.blockSignals(True) - self.ui.checkBox_autocrop.blockSignals(True) - self.ui.checkBox_skip_similar_image.blockSignals(True) - self.ui.horizontalSlider_cleanup.blockSignals(True) - self.ui.horizontalSlider_dilate.blockSignals(True) - self.ui.horizontalSlider_skew.blockSignals(True) - self.ui.horizontalSlider_vscale.blockSignals(True) - self.ui.checkBox_removeLeadingZeros.blockSignals(True) - self.ui.checkBox_rescalePatch.blockSignals(True) - self.ui.checkBox_normWHRatio.blockSignals(True) - self.ui.checkBox_invertPatch.blockSignals(True) - self.ui.checkBox_ordinalIndicator.blockSignals(True) - self.ui.comboBox_binarizationMethod.blockSignals(True) - self.ui.comboBox_formatPrefix.blockSignals(True) - self.ui.checkBox_templatefield.blockSignals(True) - self.ui.lineEdit_templatefield.blockSignals(True) - - # populate the settings from the detectionTargetsStorage - item_obj = self.detectionTargetsStorage.find_item_by_name(name) - if item_obj is None: - self.ui.lineEdit_format.setText("") - self.ui.comboBox_fieldType.setCurrentIndex(0) - self.ui.checkBox_smoothing.setChecked(True) - self.ui.checkBox_skip_empty.setChecked(True) - self.ui.horizontalSlider_conf_thresh.setValue(50) - self.ui.checkBox_autocrop.setChecked(False) - self.ui.checkBox_skip_similar_image.setChecked(False) - self.ui.horizontalSlider_cleanup.setValue(0) - self.ui.horizontalSlider_dilate.setValue(1) - self.ui.horizontalSlider_skew.setValue(0) - self.ui.horizontalSlider_vscale.setValue(10) - self.ui.label_selectedInfo.setText("") - self.ui.checkBox_removeLeadingZeros.setChecked(False) - self.ui.checkBox_rescalePatch.setChecked(False) - self.ui.checkBox_normWHRatio.setChecked(False) - self.ui.checkBox_invertPatch.setChecked(False) - self.ui.checkBox_ordinalIndicator.setChecked(False) - self.ui.comboBox_binarizationMethod.setCurrentIndex(0) - self.ui.checkBox_templatefield.setChecked(False) - self.ui.lineEdit_templatefield.setText("") - else: - item_obj.settings = normalize_settings_dict( - item_obj.settings, default_info_for_box_name(item_obj.name) - ) - self.ui.label_selectedInfo.setText(f"{item_obj.name}") - self.ui.lineEdit_format.setText(item_obj.settings["format_regex"]) - self.ui.comboBox_fieldType.setCurrentIndex(item_obj.settings["type"]) - self.ui.checkBox_smoothing.setChecked(item_obj.settings["smoothing"]) - self.ui.checkBox_skip_empty.setChecked(item_obj.settings["skip_empty"]) - self.ui.horizontalSlider_conf_thresh.setValue( - int(item_obj.settings["conf_thresh"] * 100) - ) - self.ui.checkBox_autocrop.setChecked(item_obj.settings["autocrop"]) - self.ui.checkBox_skip_similar_image.setChecked( - item_obj.settings["skip_similar_image"] - ) - self.ui.horizontalSlider_cleanup.setValue( - int(item_obj.settings["cleanup_thresh"] * 100) - ) - self.ui.horizontalSlider_dilate.setValue(item_obj.settings["dilate"]) - self.ui.horizontalSlider_skew.setValue(item_obj.settings["skew"]) - self.ui.horizontalSlider_vscale.setValue(item_obj.settings["vscale"]) - self.ui.checkBox_removeLeadingZeros.setChecked( - item_obj.settings["remove_leading_zeros"] - ) - self.ui.checkBox_rescalePatch.setChecked(item_obj.settings["rescale_patch"]) - self.ui.checkBox_normWHRatio.setChecked( - item_obj.settings["normalize_wh_ratio"] - ) - self.ui.checkBox_invertPatch.setChecked(item_obj.settings["invert_patch"]) - self.ui.checkBox_dotDetector.setChecked(item_obj.settings["dot_detector"]) - self.ui.checkBox_ordinalIndicator.setChecked( - item_obj.settings["ordinal_indicator"] - ) - self.ui.comboBox_binarizationMethod.setCurrentIndex( - item_obj.settings["binarization_method"] - ) - self.ui.checkBox_templatefield.setChecked( - item_obj.settings["templatefield"] - ) - self.ui.lineEdit_templatefield.setText( - item_obj.settings["templatefield_text"] - ) - - self.ui.comboBox_formatPrefix.setCurrentIndex(12) - - self.ui.lineEdit_format.blockSignals(False) - self.ui.comboBox_fieldType.blockSignals(False) - self.ui.checkBox_smoothing.blockSignals(False) - self.ui.checkBox_skip_empty.blockSignals(False) - self.ui.horizontalSlider_conf_thresh.blockSignals(False) - self.ui.checkBox_autocrop.blockSignals(False) - self.ui.checkBox_skip_similar_image.blockSignals(False) - self.ui.horizontalSlider_cleanup.blockSignals(False) - self.ui.horizontalSlider_dilate.blockSignals(False) - self.ui.horizontalSlider_skew.blockSignals(False) - self.ui.horizontalSlider_vscale.blockSignals(False) - self.ui.checkBox_removeLeadingZeros.blockSignals(False) - self.ui.checkBox_rescalePatch.blockSignals(False) - self.ui.checkBox_normWHRatio.blockSignals(False) - self.ui.checkBox_invertPatch.blockSignals(False) - self.ui.checkBox_ordinalIndicator.blockSignals(False) - self.ui.comboBox_binarizationMethod.blockSignals(False) - self.ui.comboBox_formatPrefix.blockSignals(False) - self.ui.checkBox_templatefield.blockSignals(False) - self.ui.lineEdit_templatefield.blockSignals(False) - def listItemClicked(self, item): user_role = item.data(Qt.ItemDataRole.UserRole) if user_role in ["checked", "templatefield"] and item.column() == 0: @@ -978,13 +653,13 @@ def listItemClicked(self, item): self.ui.pushButton_makeBox.setEnabled(False) self.ui.pushButton_removeBox.setEnabled(user_role == "checked") self.ui.groupBox_target_settings.setEnabled(user_role == "checked") - self.populateSettings(item.text()) + self.boxSettingsUiHandler.populateSettings(item.text()) else: # enable the make box button and disable the remove box button self.ui.pushButton_removeBox.setEnabled(False) self.ui.pushButton_makeBox.setEnabled(item.column() == 0) self.ui.groupBox_target_settings.setEnabled(False) - self.populateSettings("") + self.boxSettingsUiHandler.populateSettings("") if item.column() == 0: # if this is not a default box - enable the template field checkbox @@ -995,6 +670,10 @@ def listItemClicked(self, item): self.ui.checkBox_templatefield.setEnabled(False) self.ui.lineEdit_templatefield.setEnabled(False) + # notify the image viewer to select the box + if self.image_viewer: + self.image_viewer.selectBox(item.text()) + def openOBSConnectModal(self): # disable OBS options self.ui.lineEdit_sceneName.setEnabled(False) @@ -1198,6 +877,12 @@ def sourceChanged(self, index): self.sourceSelectionSucessful() def itemSelected(self, item_name): + if item_name is None: + # clear the selected item + self.ui.tableWidget_boxes.clearSelection() + self.ui.groupBox_target_settings.setEnabled(False) + self.boxSettingsUiHandler.populateSettings("") + return # select the item in the tableWidget_boxes items = self.ui.tableWidget_boxes.findItems( item_name, Qt.MatchFlag.MatchExactly @@ -1293,6 +978,9 @@ def sourceSelectionSucessful(self): self.ui.frame_for_source_view_label.layout().addWidget(self.image_viewer) def cameraConnectedEnableUI(self): + if self.image_viewer is None: + self.updateError("Image viewer is None") + return self.ui.pushButton_fourCorner.toggled.connect( self.image_viewer.toggleFourCorner ) @@ -1325,7 +1013,10 @@ def ocrResult(self, results: list[TextDetectionTargetWithResult]): for targetWithResult in results: if not targetWithResult.settings["templatefield"]: continue - targetWithResult.result = evaluate_template_field(results, targetWithResult) + template_result = evaluate_template_field(results, targetWithResult) + targetWithResult.result = ( + template_result if template_result is not None else "" + ) targetWithResult.result_state = ( TextDetectionTargetWithResult.ResultState.Success if targetWithResult.result is not None @@ -1357,8 +1048,11 @@ def ocrResult(self, results: list[TextDetectionTargetWithResult]): if self.ui.checkBox_enableOutAPI.isChecked(): update_out_api(results) - # update vmix - self.vmixUpdater.update_vmix(results) + # update vmix and uno + if self.vmixUiHandler.vmixUpdater is not None: + self.vmixUiHandler.vmixUpdater.update_vmix(results) + if self.unoUiHandler.unoUpdater is not None: + self.unoUiHandler.unoUpdater.update_uno(results) if self.out_folder is None: return diff --git a/src/mainwindow.ui b/src/mainwindow.ui index 9f8645f..7a0ee6d 100644 --- a/src/mainwindow.ui +++ b/src/mainwindow.ui @@ -6,16 +6,16 @@ 0 0 - 901 - 725 + 958 + 730 ScoreSight - - + + @@ -101,7 +101,10 @@ false - + + 134 + + true @@ -233,92 +236,81 @@ - - - 2 - + - 3 + 2 0 - 3 + 2 - 3 + 2 - - + + 1 + + + - + 0 0 - - - 3 - - - 0 - - - 0 - - - 0 - - - 0 - - - - - Target: - - - - - - - Select an item above - - - - - - - Qt::Horizontal - - - - 40 - 20 - - - - - - - - Defaults - - - - + + Defaults + - - + + + + Average Output + + + + + + + + + V.Scale + + + + + + + 1 + + + 10 + + + 5 + + + 10 + + + Qt::Horizontal + + + + + + + - + 0 0 - + 0 @@ -332,96 +324,44 @@ 0 - - - Format - - - - - + - + 0 0 + + Binarize + - + - + 0 0 - - 0 - - - - Time mm:ss.d - - - - - Time mm:ss - - - - - Time ss.d - - - Time 0-59 - - - - - Shotclock 0-39 - - - - - Score 1dd - - - - - Score ddd - - - - - Period 1-4 - - - - - Period d - - - - - Alphanumeric + Global - Any text + No Binarization - Any number + Local - Select Preset + Adaptive @@ -429,12 +369,103 @@ - - - - - 3 + + + + + 0 + 0 + + + + 0 + + + + Time mm:ss.d + + + + + Time mm:ss + + + + + Time ss.d + + + + + Time 0-59 + + + + + Shotclock 0-39 + + + + + Score 1dd + + + + + Score ddd + + + + + Period 1-4 + + + + + Period d + + + + + Alphanumeric + + + + + Any text + + + + + Any number + + + + + Select Preset + + + + + + + Count dots/blobs instead of detecting characters + + + Dot Counter + + + + + + + + 0 + 0 + + + 0 @@ -449,6 +480,12 @@ + + + 0 + 0 + + Type @@ -456,6 +493,12 @@ + + + 0 + 0 + + Number 0-9 @@ -476,18 +519,15 @@ - - + + - + 0 0 - - - 3 - + 0 @@ -501,237 +541,119 @@ 0 - + + + + 0 + 0 + + - Average Output + Cleanup - - - Ordinal (1st, 2nd, ..) + + + + 0 + 0 + + + + Qt::Horizontal - - - - - 0 - 0 - + + + + Ordinal (1st, 2nd, ..) - - - 3 - - - 0 - - - 0 - - - 0 - - - 0 - - - - - Skip Empty Values - - - - - - - Skip Similar Image - - - - - - + + + + Skip Empty Values + + + + + - + 0 0 - - - 3 - - - 0 - - - 0 - - - 0 - - - 0 - - - - - Auto Crop - - - + - - - Invert Input + + + + 0 + 0 + - - - - - - - - - - 3 - - - 0 - - - 0 - - - 0 - - - 0 - - - - Remove leading 0s + Target: - - - Count dots/blobs instead of detecting characters - + - Dot Counter + Select an item above - - - - - 3 - - - 0 - - - 0 - - - 0 - - - 0 - - - - - Scale the image to 35 pixels height, a favorable size for OCR - - - Rescale Input - - - - - - - Scale to a favorable 1:2 width-to-height ratio - - - Normalize W-H Ratio - - - - - + + + + + + Skew + + + + + + + -10 + + + 10 + + + Qt::Horizontal + + + + - - - - - 3 - - - 0 - - - 0 - - - 0 - - - 0 - - - - - Binarize - - - - - - - - Global - - - - - No Binarization - - - - - Local - - - - - Adaptive - - - - - + + + + + 0 + 0 + + + + Composite (Per-Character) + - - + + 0 @@ -739,9 +661,6 @@ - - 3 - 0 @@ -755,39 +674,28 @@ 0 - - - Cleanup - - - - - - - Qt::Horizontal + + + + 0 + 0 + - - - - - V.Scale + Conf. Th - - - 1 - - - 10 - - - 5 + + + + 0 + 0 + - 10 + 50 Qt::Horizontal @@ -797,18 +705,55 @@ - - + + + + Scale to a favorable 1:2 width-to-height ratio + + + Normalize W-H Ratio + + + + + + + Invert Input + + + + + + + Skip Similar Image + + + + + + + false + + + + 0 + 0 + + + + Force Format + + + + + - + 0 0 - - - 3 - + 0 @@ -822,54 +767,40 @@ 0 - - - Dilate - - - - - - - 5 - - - 1 - - - Qt::Horizontal + + + + 0 + 0 + - - - - - Skew + Format - - - -10 - - - 10 - - - Qt::Horizontal + + + + 0 + 0 + - - - - - 3 - + + + + + 0 + 0 + + + 0 @@ -883,16 +814,31 @@ 0 - + + + + 0 + 0 + + - Conf. Th + Dilate - - - 50 + + + + 0 + 0 + + + + 5 + + + 1 Qt::Horizontal @@ -902,6 +848,30 @@ + + + + Auto Crop + + + + + + + Remove leading 0s + + + + + + + Scale the image to 35 pixels height, a favorable size for OCR + + + Rescale Input + + + @@ -996,7 +966,7 @@ QTabWidget::Rounded - 0 + 4 @@ -1009,6 +979,9 @@ Text Files + + QFormLayout::ExpandingFieldsGrow + 3 @@ -1174,6 +1147,19 @@ + + + + Qt::Vertical + + + + 20 + 40 + + + + @@ -1184,7 +1170,7 @@ - <html><head/><body><p>HTML Scoreboard: <a href="http://localhost:18099/scoresight"><span style=" text-decoration: underline; color:#0000ff;">http://localhost:18099/scoresight<br/></span></a>JSON: <a href="http://localhost:18099/json"><span style=" text-decoration: underline; color:#0000ff;">http://localhost:18099/json</span></a> (optional: ?pivot)<br/>XML: <a href="http://localhost:18099/xml"><span style=" text-decoration: underline; color:#0000ff;">http://localhost:18099/xml</span></a> (optional: ?pivot)<br/>CSV: <a href="http://localhost:18099/csv"><span style=" text-decoration: underline; color:#0000ff;">http://localhost:18099/csv</span></a></p></body></html> + <html><head/><body><p>Use these endpoints in external software to get live data updates</p><p>HTML Scoreboard: <a href="http://localhost:18099/scoresight"><span style=" text-decoration: underline; color:#0000ff;">http://localhost:18099/scoresight<br/></span></a>JSON: <a href="http://localhost:18099/json"><span style=" text-decoration: underline; color:#0000ff;">http://localhost:18099/json</span></a> (optional: ?pivot)<br/>XML: <a href="http://localhost:18099/xml"><span style=" text-decoration: underline; color:#0000ff;">http://localhost:18099/xml</span></a> (optional: ?pivot)<br/>CSV: <a href="http://localhost:18099/csv"><span style=" text-decoration: underline; color:#0000ff;">http://localhost:18099/csv</span></a></p></body></html> Qt::RichText @@ -1431,6 +1417,147 @@ + + + UNO + + + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + 3 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + URL + + + + + + + https://app.overlays.uno/apiv2/controlapps/.../api + + + + + + + ▶️ + + + true + + + + + + + + + + + 0 + 0 + + + + + 0 + + + 5 + + + 0 + + + 5 + + + + + Send only new detections or also existing? + + + Send Same? + + + + + + + + 0 + 0 + + + + Rate Limit + + + + + + + + 0 + 0 + + + + /second + + + 1 + + + + + + + + + + + + false + + + true + + + + + API @@ -1439,6 +1566,9 @@ QFormLayout::AllNonFixedFieldsGrow + + 3 + @@ -1642,7 +1772,7 @@ - + true @@ -1879,28 +2009,36 @@ - - - Show Statistics - - - OSD - - - true - - - true - + + + + No Box + + + + + Outline + + + + + Names + + + + + All + + - + - Show OCR Detection Boxes + Show Statistics - OCR + OSD true @@ -2152,7 +2290,7 @@ 0 0 - 901 + 958 20 diff --git a/src/resizable_rect.py b/src/resizable_rect.py index c42d210..c6235bc 100644 --- a/src/resizable_rect.py +++ b/src/resizable_rect.py @@ -23,6 +23,12 @@ def __init__(self, x, y, width, height, onCenter=False): self.setPen(QPen(QBrush(Qt.GlobalColor.red), 3)) def getOriginalRect(self): + """ + Retrieve the original rectangle adjusted by the pen width. + + Returns: + QRectF: The adjusted rectangle. + """ # get the original rect adjusted by the pen width rect = self.rect() border = 0 # self.pen().width() / 2 @@ -144,6 +150,23 @@ def hoverMoveEvent(self, event): super().hoverMoveEvent(event) +class MiniRect(ResizableRect): + def __init__(self, x, y, width, height, parent=None): + super().__init__(x, y, width, height) + self.setPen(QPen(QColor(255, 0, 0))) + self.setBrush(QBrush(QColor(255, 0, 0, 50))) + self.setParentItem(parent) + self.setFlags(QGraphicsItem.ItemIsMovable | QGraphicsItem.ItemIsSelectable) + + def mousePressEvent(self, event): + super().mousePressEvent(event) + self.setCursor(Qt.SizeAllCursor) + + def mouseReleaseEvent(self, event): + super().mouseReleaseEvent(event) + self.unsetCursor() + + class ResizableRectWithNameTypeAndResult(ResizableRect): def __init__( self, @@ -157,7 +180,7 @@ def __init__( onCenter=False, boxChangedCallback=None, itemSelectedCallback=None, - showOCRRects=True, + boxDisplayStyle: int = 1, ): super().__init__(x, y, width, height, onCenter) self.setAcceptedMouseButtons(Qt.MouseButton.LeftButton) @@ -166,20 +189,72 @@ def __init__( self.result = result self.boxChangedCallback = boxChangedCallback self.itemSelectedCallback = itemSelectedCallback + self.extraBoxes = [] + self.cornerBoxes = [] + self.cornerSize = 20 + self.posItem = QGraphicsSimpleTextItem("{}".format(self.name), parent=self) + self.resultItem = QGraphicsSimpleTextItem("{}".format(self.result), parent=self) + self.bgItem = QGraphicsRectItem(self.posItem.boundingRect(), parent=self) + + # Mini-rect related attributes + self.mini_rects = [] + self.mini_rect_mode = False + self.add_button = None + self.setupAddButton() + + self.setupTextItems(image_size, boxDisplayStyle) + self.updateTextLabelPosition() + self.setBoxDisplayStyle(boxDisplayStyle) + self.setupCornerBoxes() + self.updateCornerBoxes() + + def setupAddButton(self): + self.add_button = QGraphicsRectItem(0, 0, 60, 30, parent=self) + self.add_button.setBrush(QBrush(QColor(0, 255, 0))) + self.add_button.setPen(QPen(Qt.black)) + self.add_button.setPos(self.rect().topLeft() + QPointF(5, 5)) + self.add_button.setZValue(4) + self.add_button.setVisible(False) + + # Add a "+" text to the button + text = QGraphicsSimpleTextItem("Add", self.add_button) + text.setPos(5, 0) + text.setFont(QFont("Arial", 20)) + + def setMiniRectMode(self, enabled): + self.mini_rect_mode = enabled + self.add_button.setVisible(enabled) + + def setupTextItems(self, image_size, boxDisplayStyle): self.posItem.setBrush(QBrush(QColor("red"))) fontPos = QFont("Arial", int(image_size / 60) if image_size > 0 else 32) fontPos.setWeight(QFont.Weight.Bold) self.posItem.setFont(fontPos) - self.resultItem = QGraphicsSimpleTextItem("{}".format(self.result), parent=self) self.resultItem.setBrush(QBrush(QColor("red"))) fontRes = QFont("Arial", int(image_size / 75) if image_size > 0 else 20) fontRes.setWeight(QFont.Weight.Bold) self.resultItem.setFont(fontRes) # add a semitraansparent background to the text using another rect - self.bgItem = QGraphicsRectItem(self.posItem.boundingRect(), parent=self) self.bgItem.setBrush(QBrush(QColor(0, 0, 0, 128))) self.bgItem.setPen(QPen(Qt.GlobalColor.transparent)) + # z order the text over the rect + self.posItem.setZValue(2) + self.bgItem.setZValue(1) + self.effectiveRect = None + + def setupCornerBoxes(self): + for i in range(4): + cornerBox = QGraphicsRectItem( + 0, 0, self.cornerSize, self.cornerSize, parent=self + ) + cornerBox.setBrush(QBrush(QColor(255, 0, 0, 128))) # Light red inside + cornerBox.setPen(QPen(QColor("red"))) # Red borders + cornerBox.setZValue(3) + cornerBox.setVisible(False) # Initially hide the corner boxes + self.cornerBoxes.append(cornerBox) + + def updateTextLabelPosition(self): xpos = ( self.boundingRect().x() - self.posItem.boundingRect().width() / 2 @@ -189,12 +264,60 @@ def __init__( # set the text position to the top left corner of the rect self.posItem.setPos(xpos, ypos) self.bgItem.setPos(xpos, ypos) - # z order the text over the rect - self.posItem.setZValue(2) - self.bgItem.setZValue(1) - self.effectiveRect = None - self.extraBoxes = [] - self.showOCRRects = showOCRRects + + def setBoxDisplayStyle(self, boxDisplayStyle: int): + self.boxDisplayStyle = boxDisplayStyle + if self.boxDisplayStyle == 0: + # hide the rect and the text + self.hide() + self.posItem.hide() + self.bgItem.hide() + self.resultItem.hide() + elif self.boxDisplayStyle == 1: + # show the rect, but not the text + self.show() + self.posItem.hide() + self.bgItem.hide() + self.resultItem.hide() + else: + # show the rect and the text + self.show() + self.posItem.show() + self.bgItem.show() + self.resultItem.show() + + if self.boxDisplayStyle != 3: + # do not show the effective rect and extra boxes + if self.effectiveRect is not None: + self.effectiveRect.hide() + for extraBox in self.extraBoxes: + # remove from the scene + extraBox.hide() + + def updateCornerBoxes(self): + rect = self.boundingRect() + offset = QPointF(self.cornerSize / 2, self.cornerSize / 2) + self.cornerBoxes[0].setPos(rect.topLeft() - offset) + self.cornerBoxes[1].setPos(rect.topRight() - offset) + self.cornerBoxes[2].setPos(rect.bottomLeft() - offset) + self.cornerBoxes[3].setPos(rect.bottomRight() - offset) + + def setRect(self, *args, **kwargs): + super().setRect(*args, **kwargs) + self.updateCornerBoxes() + self.updateTextLabelPosition() + + def setSelected(self, selected): + super().setSelected(selected) + for cornerBox in self.cornerBoxes: + cornerBox.setVisible(selected) + if selected: + self.show() + self.posItem.show() + self.bgItem.show() + self.resultItem.show() + else: + self.setBoxDisplayStyle(self.boxDisplayStyle) def getRect(self): return self.getOriginalRect() @@ -235,19 +358,8 @@ def updateResult(self, targetWithResult: TextDetectionTargetWithResult): ) self.resultItem.setZValue(2) - if not self.showOCRRects: - # do not show the effective rect and extra boxes - if self.effectiveRect is not None: - self.effectiveRect.hide() - for extraBox in self.extraBoxes: - # remove from the scene - extraBox.hide() - self.scene().removeItem(extraBox) - self.extraBoxes.clear() + if self.boxDisplayStyle != 3: return - else: - if self.effectiveRect is not None: - self.effectiveRect.show() if targetWithResult.effectiveRect is not None: # draw the effective rect in the scene @@ -294,6 +406,24 @@ def updateResult(self, targetWithResult: TextDetectionTargetWithResult): extraRect.setZValue(-2) self.extraBoxes.append(extraRect) + def startCreateMiniRect(self, rect: QRectF): + new_mini_rect = MiniRect( + rect.x(), + rect.y(), + rect.width(), + rect.height(), + parent=self, + ) + self.mini_rects.append(new_mini_rect) + + def clearMiniRects(self): + for mini_rect in self.mini_rects: + self.scene().removeItem(mini_rect) + self.mini_rects.clear() + + def getMiniRects(self): + return [rect.rect() for rect in self.mini_rects] + def mouseReleaseEvent(self, event): super().mouseReleaseEvent(event) origRect = self.getRect() @@ -306,8 +436,30 @@ def mouseReleaseEvent(self, event): self.boxChangedCallback(self.name, boxRect) def mousePressEvent(self, event): - super().mousePressEvent(event) + if self.mini_rect_mode: + if self.add_button.contains(event.pos()): + self.startCreateMiniRect( + QRectF( + 10, + 10, + self.getRect().height() * 0.75, + self.getRect().width() / 3, + ) + ) + else: + super().mousePressEvent(event) + else: + super().mousePressEvent(event) self.itemSelectedCallback(self.name) def mouseMoveEvent(self, event): return super().mouseMoveEvent(event) + + def hoverMoveEvent(self, event): + super().hoverMoveEvent(event) + if self.mini_rect_mode: + if self.add_button.contains(event.pos()): + self.add_button.setBrush(QBrush(QColor(0, 255, 0, 128))) + self.setCursor(Qt.CursorShape.PointingHandCursor) + else: + self.add_button.setBrush(QBrush(QColor(0, 255, 0))) diff --git a/src/source_view.py b/src/source_view.py index 102d64a..344076f 100644 --- a/src/source_view.py +++ b/src/source_view.py @@ -13,6 +13,7 @@ fetch_data, remove_data, store_data, + subscribe_to_data, ) from text_detection_target import TextDetectionTarget, TextDetectionTargetWithResult from sc_logging import logger @@ -63,7 +64,13 @@ def __init__( self.setFourCorners(fetch_data("scoresight.json", "four_corners")) self.fourCornersAppliedCallback(self.fourCorners) self._isScaling = False - self.showOCRRects = True + self._isPanning = False + self._lastMousePosition = QPointF() + + self.boxDisplayStyleSetting: int = fetch_data( + "scoresight.json", "box_display_style", 3 + ) + subscribe_to_data("scoresight.json", "box_display_style", self.boxDisplayStyle) def resizeEvent(self, event): if self._isScaling: @@ -72,11 +79,11 @@ def resizeEvent(self, event): self.fitInView(self.scene.sceneRect(), Qt.AspectRatioMode.KeepAspectRatio) self.detectionTargetsChanged() - def toggleOCRRects(self, state): - self.showOCRRects = state + def boxDisplayStyle(self, state: int): + self.boxDisplayStyleSetting = state for item in self.scene.items(): if isinstance(item, ResizableRectWithNameTypeAndResult): - item.showOCRRects = state + item.setBoxDisplayStyle(state) def toggleStabilization(self, state): if self.firstFrameReceived and self.timerThread: @@ -102,21 +109,20 @@ def detectionTargetsChanged(self): if not self.firstFrameReceived: return - # clear the scene from all ResizableRectWithNameTypeAndResult - for item in self.scene.items(): - if isinstance(item, ResizableRectWithNameTypeAndResult): - self.scene.removeItem(item) # get the detection targets from the storage detectionTargets: list[TextDetectionTarget] = ( self.detectionTargetsStorage.get_data() ) + done_targets = [] # add the boxes to the scene for detectionTarget in detectionTargets: + done_targets.append(detectionTarget.name) if detectionTarget.settings["templatefield"]: # do not show the template fields continue - self.scene.addItem( - ResizableRectWithNameTypeAndResult( + boxFound = self.findBox(detectionTarget.name) + if boxFound is None: + boxFound = ResizableRectWithNameTypeAndResult( detectionTarget.x(), detectionTarget.y(), detectionTarget.width(), @@ -127,9 +133,23 @@ def detectionTargetsChanged(self): onCenter=False, boxChangedCallback=self.boxChanged, itemSelectedCallback=self.itemSelectedCallback, - showOCRRects=self.showOCRRects, + boxDisplayStyle=self.boxDisplayStyleSetting, ) - ) + self.scene.addItem(boxFound) + else: + boxFound.setRect( + detectionTarget.x() - boxFound.x(), + detectionTarget.y() - boxFound.y(), + detectionTarget.width(), + detectionTarget.height(), + ) + boxFound.setMiniRectMode(detectionTarget.settings["composite_box"]) + + # remove the boxes that are not in the storage + for item in self.scene.items(): + if isinstance(item, ResizableRectWithNameTypeAndResult): + if item.name not in done_targets: + self.scene.removeItem(item) def boxChanged(self, name, rect): # update the detection target in the storage @@ -161,8 +181,20 @@ def removeBox(self, name): if item: self.scene.removeItem(item) + def selectBox(self, name): + # deselect all the boxes and select the one with the name + for item in self.scene.items(): + if isinstance(item, ResizableRectWithNameTypeAndResult): + item.setSelected(item.name == name) + def mousePressEvent(self, event: QMouseEvent | None) -> None: - if self.fourCornerSelectionMode and event.button() == Qt.MouseButton.LeftButton: + if event.button() == Qt.MouseButton.MiddleButton: + self._isPanning = True + self._lastMousePosition = event.position() + self.setCursor(Qt.CursorShape.ClosedHandCursor) + elif ( + self.fourCornerSelectionMode and event.button() == Qt.MouseButton.LeftButton + ): # in four corner mode we want to add a point to the scene # and connect the points in a polygon # get the position of the click @@ -201,14 +233,31 @@ def mousePressEvent(self, event: QMouseEvent | None) -> None: for corner in self.fourCorners: corner.hide() else: + # deselect all the boxes + self.selectBox(None) + self.itemSelectedCallback(None) super().mousePressEvent(event) def mouseReleaseEvent(self, event: QMouseEvent | None) -> None: - super().mouseReleaseEvent(event) - self.detectionTargetsStorage.saveBoxesToStorage() + if event.button() == Qt.MouseButton.MiddleButton: + self._isPanning = False + self.setCursor(Qt.CursorShape.ArrowCursor) + else: + super().mouseReleaseEvent(event) + self.detectionTargetsStorage.saveBoxesToStorage() def mouseMoveEvent(self, event: QMouseEvent | None) -> None: - super().mouseMoveEvent(event) + if self._isPanning: + delta = event.position() - self._lastMousePosition + self._lastMousePosition = event.position() + self.horizontalScrollBar().setValue( + self.horizontalScrollBar().value() - delta.x() + ) + self.verticalScrollBar().setValue( + self.verticalScrollBar().value() - delta.y() + ) + else: + super().mouseMoveEvent(event) def wheelEvent(self, event): # check for ctrl key diff --git a/src/storage.py b/src/storage.py index e6b34ec..617c49d 100644 --- a/src/storage.py +++ b/src/storage.py @@ -132,11 +132,21 @@ def fetch_custom_box_names(): class TextDetectionTargetMemoryStorage(QObject): # This class is used to store the text detection targets in memory + _instance = None data_changed = Signal(list) + def __new__(cls, *args, **kwargs): + if cls._instance is None: + cls._instance = super(TextDetectionTargetMemoryStorage, cls).__new__( + cls, *args, **kwargs + ) + return cls._instance + def __init__(self): - super().__init__() - self._data: list[TextDetectionTarget] = [] + if not hasattr(self, "_initialized"): + super().__init__() + self._data: list[TextDetectionTarget] = [] + self._initialized = True def add_item(self, item: TextDetectionTarget): self._data.append(item) @@ -284,6 +294,7 @@ def getBoxesForStorage(self): "templatefield_text": detectionTarget.settings.get( "templatefield_text" ), + "composite_box": detectionTarget.settings.get("composite_box"), }, } ) diff --git a/src/text_detection_target.py b/src/text_detection_target.py index 451f309..b36a0b0 100644 --- a/src/text_detection_target.py +++ b/src/text_detection_target.py @@ -39,7 +39,7 @@ def clear(self): class TextDetectionTarget(QRectF): - def __init__(self, x, y, width, height, name: str, settings: dict | None = None): + def __init__(self, x, y, width, height, name: str, settings: dict = {}): super().__init__(x, y, width, height) self.name = name self.settings = settings diff --git a/src/ui_mainwindow.py b/src/ui_mainwindow.py index f27b34e..84e589a 100644 --- a/src/ui_mainwindow.py +++ b/src/ui_mainwindow.py @@ -27,11 +27,11 @@ class Ui_MainWindow(object): def setupUi(self, MainWindow): if not MainWindow.objectName(): MainWindow.setObjectName(u"MainWindow") - MainWindow.resize(901, 725) + MainWindow.resize(958, 730) self.centralwidget = QWidget(MainWindow) self.centralwidget.setObjectName(u"centralwidget") - self.formLayout = QFormLayout(self.centralwidget) - self.formLayout.setObjectName(u"formLayout") + self.horizontalLayout_31 = QHBoxLayout(self.centralwidget) + self.horizontalLayout_31.setObjectName(u"horizontalLayout_31") self.frame = QFrame(self.centralwidget) self.frame.setObjectName(u"frame") sizePolicy = QSizePolicy(QSizePolicy.Policy.Fixed, QSizePolicy.Policy.Preferred) @@ -73,7 +73,8 @@ def setupUi(self, MainWindow): self.tableWidget_boxes.setDragDropOverwriteMode(False) self.tableWidget_boxes.setSelectionMode(QAbstractItemView.SingleSelection) self.tableWidget_boxes.setShowGrid(False) - self.tableWidget_boxes.horizontalHeader().setCascadingSectionResizes(True) + self.tableWidget_boxes.horizontalHeader().setMinimumSectionSize(134) + self.tableWidget_boxes.horizontalHeader().setStretchLastSection(True) self.tableWidget_boxes.verticalHeader().setVisible(False) self.horizontalLayout_2.addWidget(self.tableWidget_boxes) @@ -136,69 +137,74 @@ def setupUi(self, MainWindow): self.groupBox_target_settings = QGroupBox(self.groupBox_sb_info) self.groupBox_target_settings.setObjectName(u"groupBox_target_settings") - self.verticalLayout_5 = QVBoxLayout(self.groupBox_target_settings) - self.verticalLayout_5.setSpacing(2) - self.verticalLayout_5.setObjectName(u"verticalLayout_5") - self.verticalLayout_5.setContentsMargins(3, 0, 3, 3) - self.widget_10 = QWidget(self.groupBox_target_settings) - self.widget_10.setObjectName(u"widget_10") - sizePolicy2 = QSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Preferred) + self.gridLayout_6 = QGridLayout(self.groupBox_target_settings) + self.gridLayout_6.setObjectName(u"gridLayout_6") + self.gridLayout_6.setVerticalSpacing(1) + self.gridLayout_6.setContentsMargins(2, 0, 2, 2) + self.pushButton_restoreDefaults = QPushButton(self.groupBox_target_settings) + self.pushButton_restoreDefaults.setObjectName(u"pushButton_restoreDefaults") + sizePolicy2 = QSizePolicy(QSizePolicy.Policy.Fixed, QSizePolicy.Policy.Fixed) sizePolicy2.setHorizontalStretch(0) sizePolicy2.setVerticalStretch(0) - sizePolicy2.setHeightForWidth(self.widget_10.sizePolicy().hasHeightForWidth()) - self.widget_10.setSizePolicy(sizePolicy2) - self.horizontalLayout_12 = QHBoxLayout(self.widget_10) - self.horizontalLayout_12.setSpacing(3) - self.horizontalLayout_12.setObjectName(u"horizontalLayout_12") - self.horizontalLayout_12.setContentsMargins(0, 0, 0, 0) - self.label_6 = QLabel(self.widget_10) - self.label_6.setObjectName(u"label_6") + sizePolicy2.setHeightForWidth(self.pushButton_restoreDefaults.sizePolicy().hasHeightForWidth()) + self.pushButton_restoreDefaults.setSizePolicy(sizePolicy2) - self.horizontalLayout_12.addWidget(self.label_6) + self.gridLayout_6.addWidget(self.pushButton_restoreDefaults, 0, 3, 1, 1) - self.label_selectedInfo = QLabel(self.widget_10) - self.label_selectedInfo.setObjectName(u"label_selectedInfo") + self.checkBox_smoothing = QCheckBox(self.groupBox_target_settings) + self.checkBox_smoothing.setObjectName(u"checkBox_smoothing") - self.horizontalLayout_12.addWidget(self.label_selectedInfo) + self.gridLayout_6.addWidget(self.checkBox_smoothing, 3, 2, 1, 1) - self.horizontalSpacer_3 = QSpacerItem(40, 20, QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Minimum) + self.horizontalLayout_11 = QHBoxLayout() + self.horizontalLayout_11.setObjectName(u"horizontalLayout_11") + self.label_15 = QLabel(self.groupBox_target_settings) + self.label_15.setObjectName(u"label_15") - self.horizontalLayout_12.addItem(self.horizontalSpacer_3) + self.horizontalLayout_11.addWidget(self.label_15) - self.pushButton_restoreDefaults = QPushButton(self.widget_10) - self.pushButton_restoreDefaults.setObjectName(u"pushButton_restoreDefaults") + self.horizontalSlider_vscale = QSlider(self.groupBox_target_settings) + self.horizontalSlider_vscale.setObjectName(u"horizontalSlider_vscale") + self.horizontalSlider_vscale.setMinimum(1) + self.horizontalSlider_vscale.setMaximum(10) + self.horizontalSlider_vscale.setPageStep(5) + self.horizontalSlider_vscale.setValue(10) + self.horizontalSlider_vscale.setOrientation(Qt.Horizontal) - self.horizontalLayout_12.addWidget(self.pushButton_restoreDefaults) + self.horizontalLayout_11.addWidget(self.horizontalSlider_vscale) - self.verticalLayout_5.addWidget(self.widget_10) + self.gridLayout_6.addLayout(self.horizontalLayout_11, 10, 3, 1, 1) - self.widget_7 = QWidget(self.groupBox_target_settings) - self.widget_7.setObjectName(u"widget_7") - sizePolicy3 = QSizePolicy(QSizePolicy.Policy.Preferred, QSizePolicy.Policy.Preferred) - sizePolicy3.setHorizontalStretch(0) - sizePolicy3.setVerticalStretch(0) - sizePolicy3.setHeightForWidth(self.widget_7.sizePolicy().hasHeightForWidth()) - self.widget_7.setSizePolicy(sizePolicy3) - self.horizontalLayout_8 = QHBoxLayout(self.widget_7) - self.horizontalLayout_8.setObjectName(u"horizontalLayout_8") - self.horizontalLayout_8.setContentsMargins(0, 0, 0, 0) - self.label_2 = QLabel(self.widget_7) - self.label_2.setObjectName(u"label_2") + self.widget_27 = QWidget(self.groupBox_target_settings) + self.widget_27.setObjectName(u"widget_27") + sizePolicy.setHeightForWidth(self.widget_27.sizePolicy().hasHeightForWidth()) + self.widget_27.setSizePolicy(sizePolicy) + self.horizontalLayout_32 = QHBoxLayout(self.widget_27) + self.horizontalLayout_32.setObjectName(u"horizontalLayout_32") + self.horizontalLayout_32.setContentsMargins(0, 0, 0, 0) + self.label_binarizationMethod = QLabel(self.widget_27) + self.label_binarizationMethod.setObjectName(u"label_binarizationMethod") + sizePolicy.setHeightForWidth(self.label_binarizationMethod.sizePolicy().hasHeightForWidth()) + self.label_binarizationMethod.setSizePolicy(sizePolicy) - self.horizontalLayout_8.addWidget(self.label_2) + self.horizontalLayout_32.addWidget(self.label_binarizationMethod) - self.lineEdit_format = QLineEdit(self.widget_7) - self.lineEdit_format.setObjectName(u"lineEdit_format") - sizePolicy4 = QSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed) - sizePolicy4.setHorizontalStretch(0) - sizePolicy4.setVerticalStretch(0) - sizePolicy4.setHeightForWidth(self.lineEdit_format.sizePolicy().hasHeightForWidth()) - self.lineEdit_format.setSizePolicy(sizePolicy4) + self.comboBox_binarizationMethod = QComboBox(self.widget_27) + self.comboBox_binarizationMethod.addItem("") + self.comboBox_binarizationMethod.addItem("") + self.comboBox_binarizationMethod.addItem("") + self.comboBox_binarizationMethod.addItem("") + self.comboBox_binarizationMethod.setObjectName(u"comboBox_binarizationMethod") + sizePolicy2.setHeightForWidth(self.comboBox_binarizationMethod.sizePolicy().hasHeightForWidth()) + self.comboBox_binarizationMethod.setSizePolicy(sizePolicy2) + + self.horizontalLayout_32.addWidget(self.comboBox_binarizationMethod) - self.horizontalLayout_8.addWidget(self.lineEdit_format) - self.comboBox_formatPrefix = QComboBox(self.widget_7) + self.gridLayout_6.addWidget(self.widget_27, 9, 2, 1, 1) + + self.comboBox_formatPrefix = QComboBox(self.groupBox_target_settings) self.comboBox_formatPrefix.addItem("") self.comboBox_formatPrefix.addItem("") self.comboBox_formatPrefix.addItem("") @@ -213,257 +219,250 @@ def setupUi(self, MainWindow): self.comboBox_formatPrefix.addItem("") self.comboBox_formatPrefix.addItem("") self.comboBox_formatPrefix.setObjectName(u"comboBox_formatPrefix") - sizePolicy5 = QSizePolicy(QSizePolicy.Policy.Minimum, QSizePolicy.Policy.Fixed) - sizePolicy5.setHorizontalStretch(0) - sizePolicy5.setVerticalStretch(0) - sizePolicy5.setHeightForWidth(self.comboBox_formatPrefix.sizePolicy().hasHeightForWidth()) - self.comboBox_formatPrefix.setSizePolicy(sizePolicy5) + sizePolicy3 = QSizePolicy(QSizePolicy.Policy.Minimum, QSizePolicy.Policy.Fixed) + sizePolicy3.setHorizontalStretch(0) + sizePolicy3.setVerticalStretch(0) + sizePolicy3.setHeightForWidth(self.comboBox_formatPrefix.sizePolicy().hasHeightForWidth()) + self.comboBox_formatPrefix.setSizePolicy(sizePolicy3) - self.horizontalLayout_8.addWidget(self.comboBox_formatPrefix) + self.gridLayout_6.addWidget(self.comboBox_formatPrefix, 1, 3, 1, 1) + self.checkBox_dotDetector = QCheckBox(self.groupBox_target_settings) + self.checkBox_dotDetector.setObjectName(u"checkBox_dotDetector") - self.verticalLayout_5.addWidget(self.widget_7) + self.gridLayout_6.addWidget(self.checkBox_dotDetector, 7, 3, 1, 1) - self.widget_19 = QWidget(self.groupBox_target_settings) - self.widget_19.setObjectName(u"widget_19") - self.horizontalLayout_21 = QHBoxLayout(self.widget_19) - self.horizontalLayout_21.setSpacing(3) - self.horizontalLayout_21.setObjectName(u"horizontalLayout_21") - self.horizontalLayout_21.setContentsMargins(0, 0, 0, 0) - self.label_13 = QLabel(self.widget_19) + self.horizontalWidget = QWidget(self.groupBox_target_settings) + self.horizontalWidget.setObjectName(u"horizontalWidget") + sizePolicy.setHeightForWidth(self.horizontalWidget.sizePolicy().hasHeightForWidth()) + self.horizontalWidget.setSizePolicy(sizePolicy) + self.horizontalLayout_8 = QHBoxLayout(self.horizontalWidget) + self.horizontalLayout_8.setObjectName(u"horizontalLayout_8") + self.horizontalLayout_8.setContentsMargins(0, 0, 0, 0) + self.label_13 = QLabel(self.horizontalWidget) self.label_13.setObjectName(u"label_13") + sizePolicy.setHeightForWidth(self.label_13.sizePolicy().hasHeightForWidth()) + self.label_13.setSizePolicy(sizePolicy) - self.horizontalLayout_21.addWidget(self.label_13) + self.horizontalLayout_8.addWidget(self.label_13) - self.comboBox_fieldType = QComboBox(self.widget_19) + self.comboBox_fieldType = QComboBox(self.horizontalWidget) self.comboBox_fieldType.addItem("") self.comboBox_fieldType.addItem("") self.comboBox_fieldType.addItem("") self.comboBox_fieldType.setObjectName(u"comboBox_fieldType") + sizePolicy2.setHeightForWidth(self.comboBox_fieldType.sizePolicy().hasHeightForWidth()) + self.comboBox_fieldType.setSizePolicy(sizePolicy2) - self.horizontalLayout_21.addWidget(self.comboBox_fieldType) + self.horizontalLayout_8.addWidget(self.comboBox_fieldType) - self.verticalLayout_5.addWidget(self.widget_19) + self.gridLayout_6.addWidget(self.horizontalWidget, 2, 2, 1, 1) - self.widget_14 = QWidget(self.groupBox_target_settings) - self.widget_14.setObjectName(u"widget_14") - sizePolicy2.setHeightForWidth(self.widget_14.sizePolicy().hasHeightForWidth()) - self.widget_14.setSizePolicy(sizePolicy2) - self.horizontalLayout_16 = QHBoxLayout(self.widget_14) - self.horizontalLayout_16.setSpacing(3) - self.horizontalLayout_16.setObjectName(u"horizontalLayout_16") - self.horizontalLayout_16.setContentsMargins(0, 0, 0, 0) - self.checkBox_smoothing = QCheckBox(self.widget_14) - self.checkBox_smoothing.setObjectName(u"checkBox_smoothing") - - self.horizontalLayout_16.addWidget(self.checkBox_smoothing) - - self.checkBox_ordinalIndicator = QCheckBox(self.widget_14) - self.checkBox_ordinalIndicator.setObjectName(u"checkBox_ordinalIndicator") + self.widget_17 = QWidget(self.groupBox_target_settings) + self.widget_17.setObjectName(u"widget_17") + sizePolicy4 = QSizePolicy(QSizePolicy.Policy.Preferred, QSizePolicy.Policy.Preferred) + sizePolicy4.setHorizontalStretch(0) + sizePolicy4.setVerticalStretch(0) + sizePolicy4.setHeightForWidth(self.widget_17.sizePolicy().hasHeightForWidth()) + self.widget_17.setSizePolicy(sizePolicy4) + self.horizontalLayout_15 = QHBoxLayout(self.widget_17) + self.horizontalLayout_15.setObjectName(u"horizontalLayout_15") + self.horizontalLayout_15.setContentsMargins(0, 0, 0, 0) + self.label_4 = QLabel(self.widget_17) + self.label_4.setObjectName(u"label_4") + sizePolicy.setHeightForWidth(self.label_4.sizePolicy().hasHeightForWidth()) + self.label_4.setSizePolicy(sizePolicy) - self.horizontalLayout_16.addWidget(self.checkBox_ordinalIndicator) + self.horizontalLayout_15.addWidget(self.label_4) + self.horizontalSlider_cleanup = QSlider(self.widget_17) + self.horizontalSlider_cleanup.setObjectName(u"horizontalSlider_cleanup") + sizePolicy5 = QSizePolicy(QSizePolicy.Policy.Preferred, QSizePolicy.Policy.Fixed) + sizePolicy5.setHorizontalStretch(0) + sizePolicy5.setVerticalStretch(0) + sizePolicy5.setHeightForWidth(self.horizontalSlider_cleanup.sizePolicy().hasHeightForWidth()) + self.horizontalSlider_cleanup.setSizePolicy(sizePolicy5) + self.horizontalSlider_cleanup.setOrientation(Qt.Horizontal) - self.verticalLayout_5.addWidget(self.widget_14) + self.horizontalLayout_15.addWidget(self.horizontalSlider_cleanup) - self.widget_11 = QWidget(self.groupBox_target_settings) - self.widget_11.setObjectName(u"widget_11") - sizePolicy2.setHeightForWidth(self.widget_11.sizePolicy().hasHeightForWidth()) - self.widget_11.setSizePolicy(sizePolicy2) - self.horizontalLayout_13 = QHBoxLayout(self.widget_11) - self.horizontalLayout_13.setSpacing(3) - self.horizontalLayout_13.setObjectName(u"horizontalLayout_13") - self.horizontalLayout_13.setContentsMargins(0, 0, 0, 0) - self.checkBox_skip_empty = QCheckBox(self.widget_11) - self.checkBox_skip_empty.setObjectName(u"checkBox_skip_empty") - self.horizontalLayout_13.addWidget(self.checkBox_skip_empty) + self.gridLayout_6.addWidget(self.widget_17, 10, 2, 1, 1) - self.checkBox_skip_similar_image = QCheckBox(self.widget_11) - self.checkBox_skip_similar_image.setObjectName(u"checkBox_skip_similar_image") + self.checkBox_ordinalIndicator = QCheckBox(self.groupBox_target_settings) + self.checkBox_ordinalIndicator.setObjectName(u"checkBox_ordinalIndicator") - self.horizontalLayout_13.addWidget(self.checkBox_skip_similar_image) + self.gridLayout_6.addWidget(self.checkBox_ordinalIndicator, 3, 3, 1, 1) + self.checkBox_skip_empty = QCheckBox(self.groupBox_target_settings) + self.checkBox_skip_empty.setObjectName(u"checkBox_skip_empty") - self.verticalLayout_5.addWidget(self.widget_11) + self.gridLayout_6.addWidget(self.checkBox_skip_empty, 4, 2, 1, 1) - self.widget_15 = QWidget(self.groupBox_target_settings) - self.widget_15.setObjectName(u"widget_15") - sizePolicy2.setHeightForWidth(self.widget_15.sizePolicy().hasHeightForWidth()) - self.widget_15.setSizePolicy(sizePolicy2) - self.horizontalLayout_17 = QHBoxLayout(self.widget_15) - self.horizontalLayout_17.setSpacing(3) - self.horizontalLayout_17.setObjectName(u"horizontalLayout_17") - self.horizontalLayout_17.setContentsMargins(0, 0, 0, 0) - self.checkBox_autocrop = QCheckBox(self.widget_15) - self.checkBox_autocrop.setObjectName(u"checkBox_autocrop") + self.widget_10 = QWidget(self.groupBox_target_settings) + self.widget_10.setObjectName(u"widget_10") + sizePolicy4.setHeightForWidth(self.widget_10.sizePolicy().hasHeightForWidth()) + self.widget_10.setSizePolicy(sizePolicy4) + self.horizontalLayout_16 = QHBoxLayout(self.widget_10) + self.horizontalLayout_16.setObjectName(u"horizontalLayout_16") + self.label_6 = QLabel(self.widget_10) + self.label_6.setObjectName(u"label_6") + sizePolicy.setHeightForWidth(self.label_6.sizePolicy().hasHeightForWidth()) + self.label_6.setSizePolicy(sizePolicy) - self.horizontalLayout_17.addWidget(self.checkBox_autocrop) + self.horizontalLayout_16.addWidget(self.label_6) - self.checkBox_invertPatch = QCheckBox(self.widget_15) - self.checkBox_invertPatch.setObjectName(u"checkBox_invertPatch") + self.label_selectedInfo = QLabel(self.widget_10) + self.label_selectedInfo.setObjectName(u"label_selectedInfo") - self.horizontalLayout_17.addWidget(self.checkBox_invertPatch) + self.horizontalLayout_16.addWidget(self.label_selectedInfo) - self.verticalLayout_5.addWidget(self.widget_15) + self.gridLayout_6.addWidget(self.widget_10, 0, 2, 1, 1) - self.widget_22 = QWidget(self.groupBox_target_settings) - self.widget_22.setObjectName(u"widget_22") - self.horizontalLayout_25 = QHBoxLayout(self.widget_22) - self.horizontalLayout_25.setSpacing(3) - self.horizontalLayout_25.setObjectName(u"horizontalLayout_25") - self.horizontalLayout_25.setContentsMargins(0, 0, 0, 0) - self.checkBox_removeLeadingZeros = QCheckBox(self.widget_22) - self.checkBox_removeLeadingZeros.setObjectName(u"checkBox_removeLeadingZeros") + self.horizontalLayout_12 = QHBoxLayout() + self.horizontalLayout_12.setObjectName(u"horizontalLayout_12") + self.label_14 = QLabel(self.groupBox_target_settings) + self.label_14.setObjectName(u"label_14") - self.horizontalLayout_25.addWidget(self.checkBox_removeLeadingZeros) + self.horizontalLayout_12.addWidget(self.label_14) - self.checkBox_dotDetector = QCheckBox(self.widget_22) - self.checkBox_dotDetector.setObjectName(u"checkBox_dotDetector") + self.horizontalSlider_skew = QSlider(self.groupBox_target_settings) + self.horizontalSlider_skew.setObjectName(u"horizontalSlider_skew") + self.horizontalSlider_skew.setMinimum(-10) + self.horizontalSlider_skew.setMaximum(10) + self.horizontalSlider_skew.setOrientation(Qt.Horizontal) - self.horizontalLayout_25.addWidget(self.checkBox_dotDetector) + self.horizontalLayout_12.addWidget(self.horizontalSlider_skew) - self.verticalLayout_5.addWidget(self.widget_22) + self.gridLayout_6.addLayout(self.horizontalLayout_12, 11, 3, 1, 1) - self.widget_9 = QWidget(self.groupBox_target_settings) - self.widget_9.setObjectName(u"widget_9") - self.horizontalLayout_11 = QHBoxLayout(self.widget_9) - self.horizontalLayout_11.setSpacing(3) - self.horizontalLayout_11.setObjectName(u"horizontalLayout_11") - self.horizontalLayout_11.setContentsMargins(0, 0, 0, 0) - self.checkBox_rescalePatch = QCheckBox(self.widget_9) - self.checkBox_rescalePatch.setObjectName(u"checkBox_rescalePatch") + self.checkBox_compositeBox = QCheckBox(self.groupBox_target_settings) + self.checkBox_compositeBox.setObjectName(u"checkBox_compositeBox") + sizePolicy6 = QSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed) + sizePolicy6.setHorizontalStretch(0) + sizePolicy6.setVerticalStretch(0) + sizePolicy6.setHeightForWidth(self.checkBox_compositeBox.sizePolicy().hasHeightForWidth()) + self.checkBox_compositeBox.setSizePolicy(sizePolicy6) - self.horizontalLayout_11.addWidget(self.checkBox_rescalePatch) + self.gridLayout_6.addWidget(self.checkBox_compositeBox, 9, 3, 1, 1) - self.checkBox_normWHRatio = QCheckBox(self.widget_9) - self.checkBox_normWHRatio.setObjectName(u"checkBox_normWHRatio") + self.widget_21 = QWidget(self.groupBox_target_settings) + self.widget_21.setObjectName(u"widget_21") + sizePolicy4.setHeightForWidth(self.widget_21.sizePolicy().hasHeightForWidth()) + self.widget_21.setSizePolicy(sizePolicy4) + self.horizontalLayout_20 = QHBoxLayout(self.widget_21) + self.horizontalLayout_20.setObjectName(u"horizontalLayout_20") + self.horizontalLayout_20.setContentsMargins(0, 0, 0, 0) + self.label_3 = QLabel(self.widget_21) + self.label_3.setObjectName(u"label_3") + sizePolicy.setHeightForWidth(self.label_3.sizePolicy().hasHeightForWidth()) + self.label_3.setSizePolicy(sizePolicy) - self.horizontalLayout_11.addWidget(self.checkBox_normWHRatio) + self.horizontalLayout_20.addWidget(self.label_3) + self.horizontalSlider_conf_thresh = QSlider(self.widget_21) + self.horizontalSlider_conf_thresh.setObjectName(u"horizontalSlider_conf_thresh") + sizePolicy5.setHeightForWidth(self.horizontalSlider_conf_thresh.sizePolicy().hasHeightForWidth()) + self.horizontalSlider_conf_thresh.setSizePolicy(sizePolicy5) + self.horizontalSlider_conf_thresh.setValue(50) + self.horizontalSlider_conf_thresh.setOrientation(Qt.Horizontal) - self.verticalLayout_5.addWidget(self.widget_9) + self.horizontalLayout_20.addWidget(self.horizontalSlider_conf_thresh) - self.widget_20 = QWidget(self.groupBox_target_settings) - self.widget_20.setObjectName(u"widget_20") - self.horizontalLayout_23 = QHBoxLayout(self.widget_20) - self.horizontalLayout_23.setSpacing(3) - self.horizontalLayout_23.setObjectName(u"horizontalLayout_23") - self.horizontalLayout_23.setContentsMargins(0, 0, 0, 0) - self.label_binarizationMethod = QLabel(self.widget_20) - self.label_binarizationMethod.setObjectName(u"label_binarizationMethod") - self.horizontalLayout_23.addWidget(self.label_binarizationMethod) + self.gridLayout_6.addWidget(self.widget_21, 12, 2, 1, 1) - self.comboBox_binarizationMethod = QComboBox(self.widget_20) - self.comboBox_binarizationMethod.addItem("") - self.comboBox_binarizationMethod.addItem("") - self.comboBox_binarizationMethod.addItem("") - self.comboBox_binarizationMethod.addItem("") - self.comboBox_binarizationMethod.setObjectName(u"comboBox_binarizationMethod") + self.checkBox_normWHRatio = QCheckBox(self.groupBox_target_settings) + self.checkBox_normWHRatio.setObjectName(u"checkBox_normWHRatio") - self.horizontalLayout_23.addWidget(self.comboBox_binarizationMethod) + self.gridLayout_6.addWidget(self.checkBox_normWHRatio, 8, 3, 1, 1) + self.checkBox_invertPatch = QCheckBox(self.groupBox_target_settings) + self.checkBox_invertPatch.setObjectName(u"checkBox_invertPatch") - self.verticalLayout_5.addWidget(self.widget_20) + self.gridLayout_6.addWidget(self.checkBox_invertPatch, 6, 3, 1, 1) - self.widget_17 = QWidget(self.groupBox_target_settings) - self.widget_17.setObjectName(u"widget_17") - sizePolicy3.setHeightForWidth(self.widget_17.sizePolicy().hasHeightForWidth()) - self.widget_17.setSizePolicy(sizePolicy3) - self.horizontalLayout_20 = QHBoxLayout(self.widget_17) - self.horizontalLayout_20.setSpacing(3) - self.horizontalLayout_20.setObjectName(u"horizontalLayout_20") - self.horizontalLayout_20.setContentsMargins(0, 0, 0, 0) - self.label_4 = QLabel(self.widget_17) - self.label_4.setObjectName(u"label_4") + self.checkBox_skip_similar_image = QCheckBox(self.groupBox_target_settings) + self.checkBox_skip_similar_image.setObjectName(u"checkBox_skip_similar_image") - self.horizontalLayout_20.addWidget(self.label_4) + self.gridLayout_6.addWidget(self.checkBox_skip_similar_image, 4, 3, 1, 1) - self.horizontalSlider_cleanup = QSlider(self.widget_17) - self.horizontalSlider_cleanup.setObjectName(u"horizontalSlider_cleanup") - self.horizontalSlider_cleanup.setOrientation(Qt.Horizontal) + self.checkBox = QCheckBox(self.groupBox_target_settings) + self.checkBox.setObjectName(u"checkBox") + self.checkBox.setEnabled(False) + sizePolicy6.setHeightForWidth(self.checkBox.sizePolicy().hasHeightForWidth()) + self.checkBox.setSizePolicy(sizePolicy6) - self.horizontalLayout_20.addWidget(self.horizontalSlider_cleanup) + self.gridLayout_6.addWidget(self.checkBox, 2, 3, 1, 1) - self.label_15 = QLabel(self.widget_17) - self.label_15.setObjectName(u"label_15") + self.widget_7 = QWidget(self.groupBox_target_settings) + self.widget_7.setObjectName(u"widget_7") + sizePolicy4.setHeightForWidth(self.widget_7.sizePolicy().hasHeightForWidth()) + self.widget_7.setSizePolicy(sizePolicy4) + self.horizontalLayout_17 = QHBoxLayout(self.widget_7) + self.horizontalLayout_17.setObjectName(u"horizontalLayout_17") + self.horizontalLayout_17.setContentsMargins(0, 0, 0, 0) + self.label_2 = QLabel(self.widget_7) + self.label_2.setObjectName(u"label_2") + sizePolicy.setHeightForWidth(self.label_2.sizePolicy().hasHeightForWidth()) + self.label_2.setSizePolicy(sizePolicy) - self.horizontalLayout_20.addWidget(self.label_15) + self.horizontalLayout_17.addWidget(self.label_2) - self.horizontalSlider_vscale = QSlider(self.widget_17) - self.horizontalSlider_vscale.setObjectName(u"horizontalSlider_vscale") - self.horizontalSlider_vscale.setMinimum(1) - self.horizontalSlider_vscale.setMaximum(10) - self.horizontalSlider_vscale.setPageStep(5) - self.horizontalSlider_vscale.setValue(10) - self.horizontalSlider_vscale.setOrientation(Qt.Horizontal) + self.lineEdit_format = QLineEdit(self.widget_7) + self.lineEdit_format.setObjectName(u"lineEdit_format") + sizePolicy5.setHeightForWidth(self.lineEdit_format.sizePolicy().hasHeightForWidth()) + self.lineEdit_format.setSizePolicy(sizePolicy5) - self.horizontalLayout_20.addWidget(self.horizontalSlider_vscale) + self.horizontalLayout_17.addWidget(self.lineEdit_format) - self.verticalLayout_5.addWidget(self.widget_17) + self.gridLayout_6.addWidget(self.widget_7, 1, 2, 1, 1) self.widget_13 = QWidget(self.groupBox_target_settings) self.widget_13.setObjectName(u"widget_13") - sizePolicy2.setHeightForWidth(self.widget_13.sizePolicy().hasHeightForWidth()) - self.widget_13.setSizePolicy(sizePolicy2) - self.horizontalLayout_15 = QHBoxLayout(self.widget_13) - self.horizontalLayout_15.setSpacing(3) - self.horizontalLayout_15.setObjectName(u"horizontalLayout_15") - self.horizontalLayout_15.setContentsMargins(0, 0, 0, 0) + sizePolicy4.setHeightForWidth(self.widget_13.sizePolicy().hasHeightForWidth()) + self.widget_13.setSizePolicy(sizePolicy4) + self.horizontalLayout_13 = QHBoxLayout(self.widget_13) + self.horizontalLayout_13.setObjectName(u"horizontalLayout_13") + self.horizontalLayout_13.setContentsMargins(0, 0, 0, 0) self.label_9 = QLabel(self.widget_13) self.label_9.setObjectName(u"label_9") + sizePolicy.setHeightForWidth(self.label_9.sizePolicy().hasHeightForWidth()) + self.label_9.setSizePolicy(sizePolicy) - self.horizontalLayout_15.addWidget(self.label_9) + self.horizontalLayout_13.addWidget(self.label_9) self.horizontalSlider_dilate = QSlider(self.widget_13) self.horizontalSlider_dilate.setObjectName(u"horizontalSlider_dilate") + sizePolicy5.setHeightForWidth(self.horizontalSlider_dilate.sizePolicy().hasHeightForWidth()) + self.horizontalSlider_dilate.setSizePolicy(sizePolicy5) self.horizontalSlider_dilate.setMaximum(5) self.horizontalSlider_dilate.setPageStep(1) self.horizontalSlider_dilate.setOrientation(Qt.Horizontal) - self.horizontalLayout_15.addWidget(self.horizontalSlider_dilate) - - self.label_14 = QLabel(self.widget_13) - self.label_14.setObjectName(u"label_14") - - self.horizontalLayout_15.addWidget(self.label_14) + self.horizontalLayout_13.addWidget(self.horizontalSlider_dilate) - self.horizontalSlider_skew = QSlider(self.widget_13) - self.horizontalSlider_skew.setObjectName(u"horizontalSlider_skew") - self.horizontalSlider_skew.setMinimum(-10) - self.horizontalSlider_skew.setMaximum(10) - self.horizontalSlider_skew.setOrientation(Qt.Horizontal) - self.horizontalLayout_15.addWidget(self.horizontalSlider_skew) + self.gridLayout_6.addWidget(self.widget_13, 11, 2, 1, 1) + self.checkBox_autocrop = QCheckBox(self.groupBox_target_settings) + self.checkBox_autocrop.setObjectName(u"checkBox_autocrop") - self.verticalLayout_5.addWidget(self.widget_13) - - self.widget_21 = QWidget(self.groupBox_target_settings) - self.widget_21.setObjectName(u"widget_21") - self.horizontalLayout_24 = QHBoxLayout(self.widget_21) - self.horizontalLayout_24.setSpacing(3) - self.horizontalLayout_24.setObjectName(u"horizontalLayout_24") - self.horizontalLayout_24.setContentsMargins(0, 0, 0, 0) - self.label_3 = QLabel(self.widget_21) - self.label_3.setObjectName(u"label_3") - - self.horizontalLayout_24.addWidget(self.label_3) + self.gridLayout_6.addWidget(self.checkBox_autocrop, 6, 2, 1, 1) - self.horizontalSlider_conf_thresh = QSlider(self.widget_21) - self.horizontalSlider_conf_thresh.setObjectName(u"horizontalSlider_conf_thresh") - self.horizontalSlider_conf_thresh.setValue(50) - self.horizontalSlider_conf_thresh.setOrientation(Qt.Horizontal) + self.checkBox_removeLeadingZeros = QCheckBox(self.groupBox_target_settings) + self.checkBox_removeLeadingZeros.setObjectName(u"checkBox_removeLeadingZeros") - self.horizontalLayout_24.addWidget(self.horizontalSlider_conf_thresh) + self.gridLayout_6.addWidget(self.checkBox_removeLeadingZeros, 7, 2, 1, 1) + self.checkBox_rescalePatch = QCheckBox(self.groupBox_target_settings) + self.checkBox_rescalePatch.setObjectName(u"checkBox_rescalePatch") - self.verticalLayout_5.addWidget(self.widget_21) + self.gridLayout_6.addWidget(self.checkBox_rescalePatch, 8, 2, 1, 1) self.verticalLayout_3.addWidget(self.groupBox_target_settings) @@ -522,10 +521,11 @@ def setupUi(self, MainWindow): self.tabWidget_outputs.setTabShape(QTabWidget.Rounded) self.tab_textFiles = QWidget() self.tab_textFiles.setObjectName(u"tab_textFiles") - sizePolicy3.setHeightForWidth(self.tab_textFiles.sizePolicy().hasHeightForWidth()) - self.tab_textFiles.setSizePolicy(sizePolicy3) + sizePolicy4.setHeightForWidth(self.tab_textFiles.sizePolicy().hasHeightForWidth()) + self.tab_textFiles.setSizePolicy(sizePolicy4) self.formLayout_2 = QFormLayout(self.tab_textFiles) self.formLayout_2.setObjectName(u"formLayout_2") + self.formLayout_2.setFieldGrowthPolicy(QFormLayout.ExpandingFieldsGrow) self.formLayout_2.setVerticalSpacing(3) self.formLayout_2.setContentsMargins(-1, -1, -1, 0) self.label_7 = QLabel(self.tab_textFiles) @@ -535,8 +535,8 @@ def setupUi(self, MainWindow): self.widget_5 = QWidget(self.tab_textFiles) self.widget_5.setObjectName(u"widget_5") - sizePolicy4.setHeightForWidth(self.widget_5.sizePolicy().hasHeightForWidth()) - self.widget_5.setSizePolicy(sizePolicy4) + sizePolicy6.setHeightForWidth(self.widget_5.sizePolicy().hasHeightForWidth()) + self.widget_5.setSizePolicy(sizePolicy6) self.horizontalLayout_6 = QHBoxLayout(self.widget_5) self.horizontalLayout_6.setObjectName(u"horizontalLayout_6") self.horizontalLayout_6.setContentsMargins(0, 0, 0, 0) @@ -561,8 +561,8 @@ def setupUi(self, MainWindow): self.widget_12 = QWidget(self.tab_textFiles) self.widget_12.setObjectName(u"widget_12") - sizePolicy4.setHeightForWidth(self.widget_12.sizePolicy().hasHeightForWidth()) - self.widget_12.setSizePolicy(sizePolicy4) + sizePolicy6.setHeightForWidth(self.widget_12.sizePolicy().hasHeightForWidth()) + self.widget_12.setSizePolicy(sizePolicy6) self.horizontalLayout_14 = QHBoxLayout(self.widget_12) self.horizontalLayout_14.setObjectName(u"horizontalLayout_14") self.horizontalLayout_14.setContentsMargins(0, 0, 0, 0) @@ -590,8 +590,8 @@ def setupUi(self, MainWindow): self.comboBox_appendMethod.addItem("") self.comboBox_appendMethod.addItem("") self.comboBox_appendMethod.setObjectName(u"comboBox_appendMethod") - sizePolicy4.setHeightForWidth(self.comboBox_appendMethod.sizePolicy().hasHeightForWidth()) - self.comboBox_appendMethod.setSizePolicy(sizePolicy4) + sizePolicy6.setHeightForWidth(self.comboBox_appendMethod.sizePolicy().hasHeightForWidth()) + self.comboBox_appendMethod.setSizePolicy(sizePolicy6) self.formLayout_2.setWidget(2, QFormLayout.FieldRole, self.comboBox_appendMethod) @@ -611,6 +611,10 @@ def setupUi(self, MainWindow): self.formLayout_2.setWidget(3, QFormLayout.FieldRole, self.horizontalSlider_aggsPerSecond) + self.verticalSpacer_2 = QSpacerItem(20, 40, QSizePolicy.Policy.Minimum, QSizePolicy.Policy.Expanding) + + self.formLayout_2.setItem(4, QFormLayout.FieldRole, self.verticalSpacer_2) + self.tabWidget_outputs.addTab(self.tab_textFiles, "") self.tab_browser = QWidget() self.tab_browser.setObjectName(u"tab_browser") @@ -632,11 +636,8 @@ def setupUi(self, MainWindow): self.gridLayout_2.setVerticalSpacing(2) self.pushButton_connectObs = QPushButton(self.tab_obs) self.pushButton_connectObs.setObjectName(u"pushButton_connectObs") - sizePolicy6 = QSizePolicy(QSizePolicy.Policy.Preferred, QSizePolicy.Policy.Fixed) - sizePolicy6.setHorizontalStretch(0) - sizePolicy6.setVerticalStretch(0) - sizePolicy6.setHeightForWidth(self.pushButton_connectObs.sizePolicy().hasHeightForWidth()) - self.pushButton_connectObs.setSizePolicy(sizePolicy6) + sizePolicy5.setHeightForWidth(self.pushButton_connectObs.sizePolicy().hasHeightForWidth()) + self.pushButton_connectObs.setSizePolicy(sizePolicy5) self.pushButton_connectObs.setMinimumSize(QSize(0, 0)) self.gridLayout_2.addWidget(self.pushButton_connectObs, 0, 0, 1, 1) @@ -706,19 +707,16 @@ def setupUi(self, MainWindow): self.lineEdit_vmixPort = QLineEdit(self.connectionWidget) self.lineEdit_vmixPort.setObjectName(u"lineEdit_vmixPort") - sizePolicy7 = QSizePolicy(QSizePolicy.Policy.Fixed, QSizePolicy.Policy.Fixed) - sizePolicy7.setHorizontalStretch(0) - sizePolicy7.setVerticalStretch(0) - sizePolicy7.setHeightForWidth(self.lineEdit_vmixPort.sizePolicy().hasHeightForWidth()) - self.lineEdit_vmixPort.setSizePolicy(sizePolicy7) + sizePolicy2.setHeightForWidth(self.lineEdit_vmixPort.sizePolicy().hasHeightForWidth()) + self.lineEdit_vmixPort.setSizePolicy(sizePolicy2) self.lineEdit_vmixPort.setMaximumSize(QSize(50, 16777215)) self.horizontalLayout_18.addWidget(self.lineEdit_vmixPort) self.pushButton_startvmix = QPushButton(self.connectionWidget) self.pushButton_startvmix.setObjectName(u"pushButton_startvmix") - sizePolicy7.setHeightForWidth(self.pushButton_startvmix.sizePolicy().hasHeightForWidth()) - self.pushButton_startvmix.setSizePolicy(sizePolicy7) + sizePolicy2.setHeightForWidth(self.pushButton_startvmix.sizePolicy().hasHeightForWidth()) + self.pushButton_startvmix.setSizePolicy(sizePolicy2) self.pushButton_startvmix.setCheckable(True) self.pushButton_startvmix.setChecked(False) @@ -755,11 +753,84 @@ def setupUi(self, MainWindow): self.gridLayout_3.addLayout(self.verticalLayout_6, 0, 0, 1, 1) self.tabWidget_outputs.addTab(self.tab_vmix, "") + self.tab_uno = QWidget() + self.tab_uno.setObjectName(u"tab_uno") + self.gridLayout_5 = QGridLayout(self.tab_uno) + self.gridLayout_5.setObjectName(u"gridLayout_5") + self.verticalLayout_8 = QVBoxLayout() + self.verticalLayout_8.setObjectName(u"verticalLayout_8") + self.verticalLayout_8.setContentsMargins(0, 0, 0, 0) + self.connectionWidget_2 = QWidget(self.tab_uno) + self.connectionWidget_2.setObjectName(u"connectionWidget_2") + self.horizontalLayout_29 = QHBoxLayout(self.connectionWidget_2) + self.horizontalLayout_29.setSpacing(3) + self.horizontalLayout_29.setObjectName(u"horizontalLayout_29") + self.horizontalLayout_29.setContentsMargins(0, 0, 0, 0) + self.connectionLabel_2 = QLabel(self.connectionWidget_2) + self.connectionLabel_2.setObjectName(u"connectionLabel_2") + + self.horizontalLayout_29.addWidget(self.connectionLabel_2) + + self.lineEdit_unoUrl = QLineEdit(self.connectionWidget_2) + self.lineEdit_unoUrl.setObjectName(u"lineEdit_unoUrl") + + self.horizontalLayout_29.addWidget(self.lineEdit_unoUrl) + + self.toolButton_toggleUno = QToolButton(self.connectionWidget_2) + self.toolButton_toggleUno.setObjectName(u"toolButton_toggleUno") + self.toolButton_toggleUno.setCheckable(True) + + self.horizontalLayout_29.addWidget(self.toolButton_toggleUno) + + + self.verticalLayout_8.addWidget(self.connectionWidget_2) + + self.widget_26 = QWidget(self.tab_uno) + self.widget_26.setObjectName(u"widget_26") + self.widget_26.setMinimumSize(QSize(0, 0)) + self.horizontalLayout_30 = QHBoxLayout(self.widget_26) + self.horizontalLayout_30.setObjectName(u"horizontalLayout_30") + self.horizontalLayout_30.setContentsMargins(0, 5, 0, 5) + self.checkBox_uno_send_same = QCheckBox(self.widget_26) + self.checkBox_uno_send_same.setObjectName(u"checkBox_uno_send_same") + + self.horizontalLayout_30.addWidget(self.checkBox_uno_send_same) + + self.label_23 = QLabel(self.widget_26) + self.label_23.setObjectName(u"label_23") + sizePolicy.setHeightForWidth(self.label_23.sizePolicy().hasHeightForWidth()) + self.label_23.setSizePolicy(sizePolicy) + + self.horizontalLayout_30.addWidget(self.label_23) + + self.spinBox = QSpinBox(self.widget_26) + self.spinBox.setObjectName(u"spinBox") + sizePolicy2.setHeightForWidth(self.spinBox.sizePolicy().hasHeightForWidth()) + self.spinBox.setSizePolicy(sizePolicy2) + self.spinBox.setMinimum(1) + + self.horizontalLayout_30.addWidget(self.spinBox) + + + self.verticalLayout_8.addWidget(self.widget_26) + + + self.gridLayout_5.addLayout(self.verticalLayout_8, 0, 0, 1, 1) + + self.tableView_unoMapping = QTableView(self.tab_uno) + self.tableView_unoMapping.setObjectName(u"tableView_unoMapping") + self.tableView_unoMapping.horizontalHeader().setVisible(False) + self.tableView_unoMapping.horizontalHeader().setStretchLastSection(True) + + self.gridLayout_5.addWidget(self.tableView_unoMapping, 1, 0, 1, 1) + + self.tabWidget_outputs.addTab(self.tab_uno, "") self.tab_api = QWidget() self.tab_api.setObjectName(u"tab_api") self.formLayout_3 = QFormLayout(self.tab_api) self.formLayout_3.setObjectName(u"formLayout_3") self.formLayout_3.setFieldGrowthPolicy(QFormLayout.AllNonFixedFieldsGrow) + self.formLayout_3.setVerticalSpacing(3) self.checkBox_enableOutAPI = QCheckBox(self.tab_api) self.checkBox_enableOutAPI.setObjectName(u"checkBox_enableOutAPI") @@ -856,13 +927,16 @@ def setupUi(self, MainWindow): self.verticalLayout.addWidget(self.widget_detectionCadence) - self.formLayout.setWidget(0, QFormLayout.LabelRole, self.frame) + self.horizontalLayout_31.addWidget(self.frame) self.frame_source_view = QFrame(self.centralwidget) self.frame_source_view.setObjectName(u"frame_source_view") self.frame_source_view.setEnabled(True) - sizePolicy2.setHeightForWidth(self.frame_source_view.sizePolicy().hasHeightForWidth()) - self.frame_source_view.setSizePolicy(sizePolicy2) + sizePolicy7 = QSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Preferred) + sizePolicy7.setHorizontalStretch(0) + sizePolicy7.setVerticalStretch(0) + sizePolicy7.setHeightForWidth(self.frame_source_view.sizePolicy().hasHeightForWidth()) + self.frame_source_view.setSizePolicy(sizePolicy7) self.frame_source_view.setFrameShape(QFrame.StyledPanel) self.frame_source_view.setFrameShadow(QFrame.Raised) self.verticalLayout_2 = QVBoxLayout(self.frame_source_view) @@ -969,6 +1043,15 @@ def setupUi(self, MainWindow): self.horizontalLayout_10.addItem(self.horizontalSpacer_2) + self.comboBox_boxDisplayStyle = QComboBox(self.widget_viewTools) + self.comboBox_boxDisplayStyle.addItem("") + self.comboBox_boxDisplayStyle.addItem("") + self.comboBox_boxDisplayStyle.addItem("") + self.comboBox_boxDisplayStyle.addItem("") + self.comboBox_boxDisplayStyle.setObjectName(u"comboBox_boxDisplayStyle") + + self.horizontalLayout_10.addWidget(self.comboBox_boxDisplayStyle) + self.toolButton_osd = QToolButton(self.widget_viewTools) self.toolButton_osd.setObjectName(u"toolButton_osd") self.toolButton_osd.setCheckable(True) @@ -976,13 +1059,6 @@ def setupUi(self, MainWindow): self.horizontalLayout_10.addWidget(self.toolButton_osd) - self.toolButton_showOCRrects = QToolButton(self.widget_viewTools) - self.toolButton_showOCRrects.setObjectName(u"toolButton_showOCRrects") - self.toolButton_showOCRrects.setCheckable(True) - self.toolButton_showOCRrects.setChecked(True) - - self.horizontalLayout_10.addWidget(self.toolButton_showOCRrects) - self.toolButton_zoomReset = QToolButton(self.widget_viewTools) self.toolButton_zoomReset.setObjectName(u"toolButton_zoomReset") @@ -1100,18 +1176,18 @@ def setupUi(self, MainWindow): self.verticalLayout_2.addWidget(self.frame_for_source_view_label) - self.formLayout.setWidget(0, QFormLayout.FieldRole, self.frame_source_view) + self.horizontalLayout_31.addWidget(self.frame_source_view) MainWindow.setCentralWidget(self.centralwidget) self.menubar = QMenuBar(MainWindow) self.menubar.setObjectName(u"menubar") - self.menubar.setGeometry(QRect(0, 0, 901, 20)) + self.menubar.setGeometry(QRect(0, 0, 958, 20)) MainWindow.setMenuBar(self.menubar) self.retranslateUi(MainWindow) self.comboBox_formatPrefix.setCurrentIndex(0) - self.tabWidget_outputs.setCurrentIndex(0) + self.tabWidget_outputs.setCurrentIndex(4) QMetaObject.connectSlotsByName(MainWindow) @@ -1127,10 +1203,15 @@ def retranslateUi(self, MainWindow): self.toolButton_removeBox.setText(QCoreApplication.translate("MainWindow", u"-", None)) self.pushButton_makeBox.setText(QCoreApplication.translate("MainWindow", u"Add to Scene ->", None)) self.pushButton_removeBox.setText(QCoreApplication.translate("MainWindow", u"Remove Selected", None)) - self.label_6.setText(QCoreApplication.translate("MainWindow", u"Target:", None)) - self.label_selectedInfo.setText(QCoreApplication.translate("MainWindow", u"Select an item above", None)) self.pushButton_restoreDefaults.setText(QCoreApplication.translate("MainWindow", u"Defaults", None)) - self.label_2.setText(QCoreApplication.translate("MainWindow", u"Format", None)) + self.checkBox_smoothing.setText(QCoreApplication.translate("MainWindow", u"Average Output", None)) + self.label_15.setText(QCoreApplication.translate("MainWindow", u"V.Scale", None)) + self.label_binarizationMethod.setText(QCoreApplication.translate("MainWindow", u"Binarize", None)) + self.comboBox_binarizationMethod.setItemText(0, QCoreApplication.translate("MainWindow", u"Global", None)) + self.comboBox_binarizationMethod.setItemText(1, QCoreApplication.translate("MainWindow", u"No Binarization", None)) + self.comboBox_binarizationMethod.setItemText(2, QCoreApplication.translate("MainWindow", u"Local", None)) + self.comboBox_binarizationMethod.setItemText(3, QCoreApplication.translate("MainWindow", u"Adaptive", None)) + self.comboBox_formatPrefix.setItemText(0, QCoreApplication.translate("MainWindow", u"Time mm:ss.d", None)) self.comboBox_formatPrefix.setItemText(1, QCoreApplication.translate("MainWindow", u"Time mm:ss", None)) self.comboBox_formatPrefix.setItemText(2, QCoreApplication.translate("MainWindow", u"Time ss.d", None)) @@ -1145,41 +1226,38 @@ def retranslateUi(self, MainWindow): self.comboBox_formatPrefix.setItemText(11, QCoreApplication.translate("MainWindow", u"Any number", None)) self.comboBox_formatPrefix.setItemText(12, QCoreApplication.translate("MainWindow", u"Select Preset", None)) +#if QT_CONFIG(tooltip) + self.checkBox_dotDetector.setToolTip(QCoreApplication.translate("MainWindow", u"Count dots/blobs instead of detecting characters", None)) +#endif // QT_CONFIG(tooltip) + self.checkBox_dotDetector.setText(QCoreApplication.translate("MainWindow", u"Dot Counter", None)) self.label_13.setText(QCoreApplication.translate("MainWindow", u"Type", None)) self.comboBox_fieldType.setItemText(0, QCoreApplication.translate("MainWindow", u"Number 0-9", None)) self.comboBox_fieldType.setItemText(1, QCoreApplication.translate("MainWindow", u"Time 0-9 , . :", None)) self.comboBox_fieldType.setItemText(2, QCoreApplication.translate("MainWindow", u"Text", None)) - self.checkBox_smoothing.setText(QCoreApplication.translate("MainWindow", u"Average Output", None)) + self.label_4.setText(QCoreApplication.translate("MainWindow", u"Cleanup", None)) self.checkBox_ordinalIndicator.setText(QCoreApplication.translate("MainWindow", u"Ordinal (1st, 2nd, ..)", None)) self.checkBox_skip_empty.setText(QCoreApplication.translate("MainWindow", u"Skip Empty Values", None)) + self.label_6.setText(QCoreApplication.translate("MainWindow", u"Target:", None)) + self.label_selectedInfo.setText(QCoreApplication.translate("MainWindow", u"Select an item above", None)) + self.label_14.setText(QCoreApplication.translate("MainWindow", u"Skew", None)) + self.checkBox_compositeBox.setText(QCoreApplication.translate("MainWindow", u"Composite (Per-Character)", None)) + self.label_3.setText(QCoreApplication.translate("MainWindow", u"Conf. Th", None)) +#if QT_CONFIG(tooltip) + self.checkBox_normWHRatio.setToolTip(QCoreApplication.translate("MainWindow", u"Scale to a favorable 1:2 width-to-height ratio", None)) +#endif // QT_CONFIG(tooltip) + self.checkBox_normWHRatio.setText(QCoreApplication.translate("MainWindow", u"Normalize W-H Ratio", None)) + self.checkBox_invertPatch.setText(QCoreApplication.translate("MainWindow", u"Invert Input", None)) self.checkBox_skip_similar_image.setText(QCoreApplication.translate("MainWindow", u"Skip Similar Image", None)) + self.checkBox.setText(QCoreApplication.translate("MainWindow", u"Force Format", None)) + self.label_2.setText(QCoreApplication.translate("MainWindow", u"Format", None)) + self.label_9.setText(QCoreApplication.translate("MainWindow", u"Dilate", None)) self.checkBox_autocrop.setText(QCoreApplication.translate("MainWindow", u"Auto Crop", None)) - self.checkBox_invertPatch.setText(QCoreApplication.translate("MainWindow", u"Invert Input", None)) self.checkBox_removeLeadingZeros.setText(QCoreApplication.translate("MainWindow", u"Remove leading 0s", None)) -#if QT_CONFIG(tooltip) - self.checkBox_dotDetector.setToolTip(QCoreApplication.translate("MainWindow", u"Count dots/blobs instead of detecting characters", None)) -#endif // QT_CONFIG(tooltip) - self.checkBox_dotDetector.setText(QCoreApplication.translate("MainWindow", u"Dot Counter", None)) #if QT_CONFIG(tooltip) self.checkBox_rescalePatch.setToolTip(QCoreApplication.translate("MainWindow", u"Scale the image to 35 pixels height, a favorable size for OCR", None)) #endif // QT_CONFIG(tooltip) self.checkBox_rescalePatch.setText(QCoreApplication.translate("MainWindow", u"Rescale Input", None)) -#if QT_CONFIG(tooltip) - self.checkBox_normWHRatio.setToolTip(QCoreApplication.translate("MainWindow", u"Scale to a favorable 1:2 width-to-height ratio", None)) -#endif // QT_CONFIG(tooltip) - self.checkBox_normWHRatio.setText(QCoreApplication.translate("MainWindow", u"Normalize W-H Ratio", None)) - self.label_binarizationMethod.setText(QCoreApplication.translate("MainWindow", u"Binarize", None)) - self.comboBox_binarizationMethod.setItemText(0, QCoreApplication.translate("MainWindow", u"Global", None)) - self.comboBox_binarizationMethod.setItemText(1, QCoreApplication.translate("MainWindow", u"No Binarization", None)) - self.comboBox_binarizationMethod.setItemText(2, QCoreApplication.translate("MainWindow", u"Local", None)) - self.comboBox_binarizationMethod.setItemText(3, QCoreApplication.translate("MainWindow", u"Adaptive", None)) - - self.label_4.setText(QCoreApplication.translate("MainWindow", u"Cleanup", None)) - self.label_15.setText(QCoreApplication.translate("MainWindow", u"V.Scale", None)) - self.label_9.setText(QCoreApplication.translate("MainWindow", u"Dilate", None)) - self.label_14.setText(QCoreApplication.translate("MainWindow", u"Skew", None)) - self.label_3.setText(QCoreApplication.translate("MainWindow", u"Conf. Th", None)) #if QT_CONFIG(tooltip) self.checkBox_templatefield.setToolTip(QCoreApplication.translate("MainWindow", u"This field is a combination of exising fields in a template", None)) #endif // QT_CONFIG(tooltip) @@ -1203,7 +1281,7 @@ def retranslateUi(self, MainWindow): #endif // QT_CONFIG(tooltip) self.label_savePerSec.setText(QCoreApplication.translate("MainWindow", u"Save / s", None)) self.tabWidget_outputs.setTabText(self.tabWidget_outputs.indexOf(self.tab_textFiles), QCoreApplication.translate("MainWindow", u"Text Files", None)) - self.label_8.setText(QCoreApplication.translate("MainWindow", u"

HTML Scoreboard: http://localhost:18099/scoresight
JSON: http://localhost:18099/json (optional: ?pivot)
XML: http://localhost:18099/xml (optional: ?pivot)
CSV: http://localhost:18099/csv

", None)) + self.label_8.setText(QCoreApplication.translate("MainWindow", u"

Use these endpoints in external software to get live data updates

HTML Scoreboard: http://localhost:18099/scoresight
JSON: http://localhost:18099/json (optional: ?pivot)
XML: http://localhost:18099/xml (optional: ?pivot)
CSV: http://localhost:18099/csv

", None)) self.tabWidget_outputs.setTabText(self.tabWidget_outputs.indexOf(self.tab_browser), QCoreApplication.translate("MainWindow", u"Browser", None)) self.pushButton_connectObs.setText(QCoreApplication.translate("MainWindow", u"Connect OBS", None)) self.lineEdit_sceneName.setText(QCoreApplication.translate("MainWindow", u"ScoreSight Scene", None)) @@ -1222,6 +1300,16 @@ def retranslateUi(self, MainWindow): #endif // QT_CONFIG(tooltip) self.checkBox_vmix_send_same.setText(QCoreApplication.translate("MainWindow", u"Send Same?", None)) self.tabWidget_outputs.setTabText(self.tabWidget_outputs.indexOf(self.tab_vmix), QCoreApplication.translate("MainWindow", u"VMix", None)) + self.connectionLabel_2.setText(QCoreApplication.translate("MainWindow", u"URL", None)) + self.lineEdit_unoUrl.setText(QCoreApplication.translate("MainWindow", u"https://app.overlays.uno/apiv2/controlapps/.../api", None)) + self.toolButton_toggleUno.setText(QCoreApplication.translate("MainWindow", u"\u25b6\ufe0f", None)) +#if QT_CONFIG(tooltip) + self.checkBox_uno_send_same.setToolTip(QCoreApplication.translate("MainWindow", u"Send only new detections or also existing?", None)) +#endif // QT_CONFIG(tooltip) + self.checkBox_uno_send_same.setText(QCoreApplication.translate("MainWindow", u"Send Same?", None)) + self.label_23.setText(QCoreApplication.translate("MainWindow", u"Rate Limit", None)) + self.spinBox.setSuffix(QCoreApplication.translate("MainWindow", u"/second", None)) + self.tabWidget_outputs.setTabText(self.tabWidget_outputs.indexOf(self.tab_uno), QCoreApplication.translate("MainWindow", u"UNO", None)) self.checkBox_enableOutAPI.setText(QCoreApplication.translate("MainWindow", u"Send out API requests to external services.", None)) self.label_21.setText(QCoreApplication.translate("MainWindow", u"Encode", None)) self.comboBox_api_encode.setItemText(0, QCoreApplication.translate("MainWindow", u"JSON (Full)", None)) @@ -1262,14 +1350,15 @@ def retranslateUi(self, MainWindow): self.toolButton_topCrop.setText(QCoreApplication.translate("MainWindow", u"Crop", None)) self.toolButton_rotate.setText(QCoreApplication.translate("MainWindow", u"Rotate", None)) self.pushButton_stabilize.setText(QCoreApplication.translate("MainWindow", u"Stabilize", None)) + self.comboBox_boxDisplayStyle.setItemText(0, QCoreApplication.translate("MainWindow", u"No Box", None)) + self.comboBox_boxDisplayStyle.setItemText(1, QCoreApplication.translate("MainWindow", u"Outline", None)) + self.comboBox_boxDisplayStyle.setItemText(2, QCoreApplication.translate("MainWindow", u"Names", None)) + self.comboBox_boxDisplayStyle.setItemText(3, QCoreApplication.translate("MainWindow", u"All", None)) + #if QT_CONFIG(tooltip) self.toolButton_osd.setToolTip(QCoreApplication.translate("MainWindow", u"Show Statistics", None)) #endif // QT_CONFIG(tooltip) self.toolButton_osd.setText(QCoreApplication.translate("MainWindow", u"OSD", None)) -#if QT_CONFIG(tooltip) - self.toolButton_showOCRrects.setToolTip(QCoreApplication.translate("MainWindow", u"Show OCR Detection Boxes", None)) -#endif // QT_CONFIG(tooltip) - self.toolButton_showOCRrects.setText(QCoreApplication.translate("MainWindow", u"OCR", None)) #if QT_CONFIG(tooltip) self.toolButton_zoomReset.setToolTip(QCoreApplication.translate("MainWindow", u"Reset zoom", None)) #endif // QT_CONFIG(tooltip) diff --git a/src/uno_output.py b/src/uno_output.py new file mode 100644 index 0000000..91ae8ff --- /dev/null +++ b/src/uno_output.py @@ -0,0 +1,77 @@ +import requests +from text_detection_target import TextDetectionTargetWithResult +from sc_logging import logger +from storage import subscribe_to_data, fetch_data + + +class UNOAPI: + def __init__(self, endpoint, field_mapping): + self.endpoint = endpoint + self.field_mapping = field_mapping + self.running = False + self.update_same = fetch_data("scoresight.json", "uno_send_same", False) + subscribe_to_data("scoresight.json", "uno_send_same", self.set_update_same) + + def set_update_same(self, update_same): + self.update_same = update_same + + def set_field_mapping(self, field_mapping): + logger.debug(f"Setting UNO field mapping: {field_mapping}") + self.field_mapping = field_mapping + + def update_uno(self, detection: list[TextDetectionTargetWithResult]): + if not self.running: + return + + if not self.field_mapping: + logger.debug("Field mapping is not set") + return + + look_in = [TextDetectionTargetWithResult.ResultState.Success] + if self.update_same: + look_in.append(TextDetectionTargetWithResult.ResultState.SameNoChange) + + for target in detection: + if target.result_state in look_in and target.name in self.field_mapping: + uno_command = self.field_mapping[target.name] + self.send_uno_command(uno_command, target.result) + + def send_uno_command(self, command, value): + payload = {"command": command, "value": value} + + try: + response = requests.put(self.endpoint, json=payload) + if response.status_code != 200: + logger.error( + f"Failed to send data to UNO API, status code: {response.status_code}" + ) + else: + logger.debug(f"Successfully sent {command}: {value} to UNO API") + + # Check rate limit headers + self.check_rate_limits(response.headers) + + except requests.exceptions.RequestException as e: + logger.error(f"Failed to send data to UNO API: {e}") + + def check_rate_limits(self, headers): + rate_limit_headers = [ + "X-Singular-Ratelimit-Burst-Calls", + "X-Singular-Ratelimit-Daily-Calls", + "X-Singular-Ratelimit-Burst-Data", + "X-Singular-Ratelimit-Daily-Data", + ] + + for header in rate_limit_headers: + if header in headers: + limit_info = headers[header] + logger.debug(f"Rate limit info for {header}: {limit_info}") + + # You can add more sophisticated rate limit handling here if needed + # For example, pause requests if limits are close to being reached + + def start(self): + self.running = True + + def stop(self): + self.running = False diff --git a/src/uno_ui_handler.py b/src/uno_ui_handler.py new file mode 100644 index 0000000..984f527 --- /dev/null +++ b/src/uno_ui_handler.py @@ -0,0 +1,130 @@ +from PySide6.QtGui import QStandardItemModel, QStandardItem +from PySide6.QtCore import Qt + +from text_detection_target import TextDetectionTarget +from ui_mainwindow import Ui_MainWindow +from uno_output import UNOAPI +from sc_logging import logger +from storage import fetch_data, store_data + +standard_uno_mapping = { + "Time": "SetMatchTime", + "Home Score": "SetGoalsHome", + "Away Score": "SetGoalsAway", + "Period": "SetPeriod", +} + + +class UNOUIHandler: + def __init__(self, ui: Ui_MainWindow): + self.ui = ui + self.unoUpdater = None + self.unoUiSetup() + + def globalSettingsChanged(self, settingName, value): + store_data("scoresight.json", settingName, value) + + def unoConnectionChanged(self): + self.unoUpdater = UNOAPI( + self.ui.lineEdit_unoUrl.text(), + {}, + ) + self.globalSettingsChanged("uno_url", self.ui.lineEdit_unoUrl.text()) + self.unoMappingChanged(True) + + def unoMappingChanged(self, shouldUpdateStorage: bool): + mapping = {} + model = self.ui.tableView_unoMapping.model() + if isinstance(model, QStandardItemModel): + for i in range(model.rowCount()): + item = model.item(i, 0) + value = model.item(i, 1) + if item and value: + mapping[item.text()] = value.text() + if shouldUpdateStorage: + self.globalSettingsChanged("uno_mapping", mapping) + self.unoUpdater.set_field_mapping(mapping) + else: + logger.error("unoMappingChanged: model is not a QStandardItemModel") + + def unoUiSetup(self): + # populate the UNO connection from storage + self.ui.lineEdit_unoUrl.setText( + fetch_data( + "scoresight.json", + "uno_url", + "https://app.overlays.uno/apiv2/controlapps/.../api", + ) + ) + # connect the lineEdit to unoConnectionChanged + self.ui.lineEdit_unoUrl.textChanged.connect(self.unoConnectionChanged) + + # create the unoUpdater + self.unoUpdater = UNOAPI( + self.ui.lineEdit_unoUrl.text(), + {}, + ) + # add standard item model to the tableView_unoMapping + self.ui.tableView_unoMapping.setModel(QStandardItemModel()) + mapping = fetch_data("scoresight.json", "uno_mapping", {}) + if mapping: + self.unoUpdater.set_field_mapping(mapping) + + self.ui.tableView_unoMapping.model().dataChanged.connect(self.unoMappingChanged) + + self.ui.toolButton_toggleUno.toggled.connect(self.toggleUNO) + + # Connect the "Send Same?" checkbox + self.ui.checkBox_uno_send_same.setChecked( + fetch_data("scoresight.json", "uno_send_same", False) + ) + self.ui.checkBox_uno_send_same.stateChanged.connect(self.unoSendSameChanged) + + def toggleUNO(self, value): + if not self.unoUpdater: + return + if value: + self.ui.toolButton_toggleUno.setText("🛑") + self.unoUpdater.start() + else: + self.ui.toolButton_toggleUno.setText("▶️") + self.unoUpdater.stop() + + def unoSendSameChanged(self, state): + self.globalSettingsChanged("uno_send_same", state == Qt.Checked) + + def updateUNOTable(self, detectionTargets: list[TextDetectionTarget]): + mapping_storage = fetch_data("scoresight.json", "uno_mapping") + model = QStandardItemModel() + model.blockSignals(True) + + for box in detectionTargets: + items = model.findItems(box.name, Qt.MatchFlag.MatchExactly) + if len(items) == 0: + row = model.rowCount() + model.insertRow(row) + model.setItem(row, 0, QStandardItem(box.name)) + model.item(row, 0).setFlags(Qt.ItemFlag.ItemIsEnabled) + else: + item = items[0] + row = item.row() + + new_item_value = None + if mapping_storage and box.name in mapping_storage: + new_item_value = mapping_storage[box.name] + else: + if box.name in standard_uno_mapping: + new_item_value = standard_uno_mapping[box.name] + else: + new_item_value = box.name + model.setItem(row, 1, QStandardItem(new_item_value)) + + for i in range(model.rowCount() - 1, -1, -1): + item = model.item(i, 0) + if not any([box.name == item.text() for box in detectionTargets]): + model.removeRow(i) + + model.blockSignals(False) + self.ui.tableView_unoMapping.setModel(model) + self.ui.tableView_unoMapping.model().dataChanged.connect(self.unoMappingChanged) + self.unoMappingChanged(False) diff --git a/src/vmix_ui_handler.py b/src/vmix_ui_handler.py new file mode 100644 index 0000000..8c9c059 --- /dev/null +++ b/src/vmix_ui_handler.py @@ -0,0 +1,127 @@ +from PySide6.QtGui import QStandardItemModel, QStandardItem +from PySide6.QtCore import Qt + +from text_detection_target import TextDetectionTarget +from ui_mainwindow import Ui_MainWindow +from vmix_output import VMixAPI +from sc_logging import logger +from storage import fetch_data, store_data + + +class VMixUIHanlder: + def __init__(self, ui: Ui_MainWindow): + self.ui = ui + self.vmixUpdater = None + self.vmixUiSetup() + + def globalSettingsChanged(self, settingName, value): + store_data("scoresight.json", settingName, value) + + def vmixConnectionChanged(self): + self.vmixUpdater = VMixAPI( + self.ui.lineEdit_vmixHost.text(), + self.ui.lineEdit_vmixPort.text(), + self.ui.inputLineEdit_vmix.text(), + {}, + ) + self.globalSettingsChanged("vmix_host", self.ui.lineEdit_vmixHost.text()) + self.globalSettingsChanged("vmix_port", self.ui.lineEdit_vmixPort.text()) + self.globalSettingsChanged("vmix_input", self.ui.inputLineEdit_vmix.text()) + + def vmixMappingChanged(self, _): + # store entire mapping data in scoresight.json + mapping = {} + model = self.ui.tableView_vmixMapping.model() + if isinstance(model, QStandardItemModel): + for i in range(model.rowCount()): + item = model.item(i, 0) + value = model.item(i, 1) + if item and value: + mapping[item.text()] = value.text() + self.globalSettingsChanged("vmix_mapping", mapping) + self.vmixUpdater.set_field_mapping(mapping) + else: + logger.error("vmixMappingChanged: model is not a QStandardItemModel") + + def vmixUiSetup(self): + # populate the vmix connection from storage + self.ui.lineEdit_vmixHost.setText( + fetch_data("scoresight.json", "vmix_host", "localhost") + ) + self.ui.lineEdit_vmixPort.setText( + fetch_data("scoresight.json", "vmix_port", "8099") + ) + self.ui.inputLineEdit_vmix.setText( + fetch_data("scoresight.json", "vmix_input", "1") + ) + # connect the lineEdits to vmixConnectionChanged + self.ui.lineEdit_vmixHost.textChanged.connect(self.vmixConnectionChanged) + self.ui.lineEdit_vmixPort.textChanged.connect(self.vmixConnectionChanged) + self.ui.inputLineEdit_vmix.textChanged.connect(self.vmixConnectionChanged) + + # create the vmixUpdater + self.vmixUpdater = VMixAPI( + self.ui.lineEdit_vmixHost.text(), + self.ui.lineEdit_vmixPort.text(), + self.ui.inputLineEdit_vmix.text(), + {}, + ) + # add standard item model to the tableView_vmixMapping + self.ui.tableView_vmixMapping.setModel(QStandardItemModel()) + mapping = fetch_data("scoresight.json", "vmix_mapping", {}) + if mapping: + self.vmixUpdater.set_field_mapping(mapping) + + self.ui.tableView_vmixMapping.model().dataChanged.connect( + self.vmixMappingChanged + ) + + self.ui.pushButton_startvmix.toggled.connect(self.togglevMix) + + def togglevMix(self, value): + if not self.vmixUpdater: + return + if value: + self.ui.pushButton_startvmix.setText("🛑 Stop vMix") + self.vmixUpdater.running = True + else: + self.ui.pushButton_startvmix.setText("▶️ Start vMix") + self.vmixUpdater.running = False + + def updatevMixTable(self, detectionTargets: list[TextDetectionTarget]): + mapping_storage = fetch_data("scoresight.json", "vmix_mapping") + model = QStandardItemModel() + model.blockSignals(True) + + for box in detectionTargets: + # add the detection to the vmix output mapping: tableView_vmixMapping + # check if the table already has the detectionTarget + items = model.findItems(box.name, Qt.MatchFlag.MatchExactly) + if len(items) == 0: + # add the item to the list + row = model.rowCount() + model.insertRow(row) + model.setItem(row, 0, QStandardItem(box.name)) + # the first item shouldn't be editable + model.item(row, 0).setFlags(Qt.ItemFlag.NoItemFlags) + else: + # update the item in the list + item = items[0] + row = item.row() + + # get value from storage or use the box name + if mapping_storage and box.name in mapping_storage: + model.setItem(row, 1, QStandardItem(mapping_storage[box.name])) + else: + model.setItem(row, 1, QStandardItem(box.name)) + # remove the items that are not in the detectionTargets + for i in range(model.rowCount()): + item = model.item(i, 0) + if not any([box.name == item.text() for box in detectionTargets]): + model.removeRow(i) + + model.blockSignals(False) + self.ui.tableView_vmixMapping.setModel(model) + self.ui.tableView_vmixMapping.model().dataChanged.connect( + self.vmixMappingChanged + )