From 514faddee77c25e7d3d152532d09f9c3bcc18c30 Mon Sep 17 00:00:00 2001 From: Roy Shilkrot Date: Sun, 28 Apr 2024 09:01:58 -0400 Subject: [PATCH] Refactor code to handle text detection target result states and update UI accordingly --- camera_view.py | 21 ++++++++++++++++++++- main.py | 13 ++++++++++--- mainwindow.ui | 16 ++++++++++++++++ source_view.py | 5 +++++ tesseract.py | 11 +++++++++-- text_detection_target.py | 2 ++ ui_mainwindow.py | 11 +++++++++++ vmix_output.py | 5 +++-- 8 files changed, 76 insertions(+), 8 deletions(-) diff --git a/camera_view.py b/camera_view.py index 92fea71..41b6887 100644 --- a/camera_view.py +++ b/camera_view.py @@ -14,6 +14,7 @@ from ndi import NDICapture from screen_capture_source import ScreenCapture +from storage import TextDetectionTargetMemoryStorage from tesseract import TextDetector import datetime from datetime import datetime @@ -146,7 +147,11 @@ class TimerThread(QThread): update_error = Signal(object) ocr_result_signal = Signal(list) - def __init__(self, camera_info: CameraInfo, detectionTargetsStorage): + def __init__( + self, + camera_info: CameraInfo, + detectionTargetsStorage: TextDetectionTargetMemoryStorage, + ): super().__init__() self.camera_info = camera_info self.homography = None @@ -167,6 +172,7 @@ def __init__(self, camera_info: CameraInfo, detectionTargetsStorage): self.pps = 1000 / self.preview_frame_interval # previews per second self.ups = 1000 / self.update_frame_interval # updates per second self.fps_alpha = 0.1 # Smoothing factor + self.updateOnChange = True def connect_video_capture(self) -> bool: if self.camera_info.type == CameraInfo.CameraType.NDI: @@ -326,6 +332,15 @@ def run(self): # augment the text detection targets with the results results = [] for i, result in enumerate(texts): + if self.updateOnChange: + if ( + detectionTargets[i].last_text is not None + and detectionTargets[i].last_text == result.text + ): + result.state = ( + TextDetectionTargetWithResult.ResultState.SameNoChange + ) + results.append( TextDetectionTargetWithResult( detectionTargets[i], @@ -335,6 +350,7 @@ def run(self): result.extra, ) ) + detectionTargets[i].last_text = result.text # emit the results self.ocr_result_signal.emit(results) @@ -396,6 +412,9 @@ def __init__(self, camera_index, detectionTargetsStorage=None): self.error_text = None self.showOSD = True + def setUpdateOnChange(self, updateOnChange): + self.timerThread.updateOnChange = updateOnChange + def toggleOSD(self, state): self.showOSD = state if self.fps_text is not None: diff --git a/main.py b/main.py index 63c50ef..a92cf86 100644 --- a/main.py +++ b/main.py @@ -82,9 +82,7 @@ def __init__(self): self.ui.setupUi(self) # load env variables load_dotenv() - self.setWindowTitle( - f"{fetch_data('scoresight.json', 'product_name')} - v{os.getenv('LOCAL_RELEASE_TAG')} - Registered to: {fetch_data('scoresight.json', 'customer_name')}" - ) + self.setWindowTitle(f"ScoreSight - v{os.getenv('LOCAL_RELEASE_TAG')}") if platform.system() == "Windows": # set the icon self.setWindowIcon( @@ -213,6 +211,7 @@ def __init__(self): self.ui.comboBox_formatPrefix.currentIndexChanged.connect( self.formatPrefixChanged ) + self.ui.checkBox_updateOnchange.toggled.connect(self.toggleUpdateOnChange) # populate the tableWidget_boxes with the default and custom boxes custom_boxes_names = fetch_custom_box_names() @@ -276,11 +275,19 @@ def __init__(self): self.ui.horizontalSlider_aggsPerSecond.setValue( fetch_data("scoresight.json", "aggs_per_second", 5) ) + self.ui.checkBox_updateOnchange.setChecked( + fetch_data("scoresight.json", "update_on_change", True) + ) self.update_sources.connect(self.updateSources) self.get_sources.connect(self.getSources) self.get_sources.emit() + def toggleUpdateOnChange(self, value): + store_data("scoresight.json", "update_on_change", 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 diff --git a/mainwindow.ui b/mainwindow.ui index ee63134..e95e0be 100644 --- a/mainwindow.ui +++ b/mainwindow.ui @@ -1370,11 +1370,27 @@ false + + QSlider::TicksBelow + 5 + + + + Only send an update if the field value has changed + + + Update on change + + + true + + + diff --git a/source_view.py b/source_view.py index 5c637f1..42c512a 100644 --- a/source_view.py +++ b/source_view.py @@ -213,6 +213,11 @@ def updateResult(self, targetWithResult: TextDetectionTargetWithResult): == TextDetectionTargetWithResult.ResultState.Success ): self.resultItem.setBrush(QBrush(QColor("green"))) + elif ( + targetWithResult.result_state + == TextDetectionTargetWithResult.ResultState.SameNoChange + ): + self.resultItem.setBrush(QBrush(QColor("lightgreen"))) elif ( targetWithResult.result_state == TextDetectionTargetWithResult.ResultState.FailedFilter diff --git a/tesseract.py b/tesseract.py index 8eafcb3..a737e29 100644 --- a/tesseract.py +++ b/tesseract.py @@ -180,7 +180,8 @@ def detect_multi_text( scale_y = 1.0 if multi_crop: if ( - rect.x() < 0 + rect is None + or rect.x() < 0 or rect.y() < 0 or rect.width() < 1 or rect.height() < 1 @@ -192,6 +193,12 @@ def detect_multi_text( ) continue + if rect.x() >= binary.shape[1]: + # move the rect inside the image + rect.setX(binary.shape[1] - rect.width()) + if rect.y() >= binary.shape[0]: + # move the rect inside the image + rect.setY(binary.shape[0] - rect.height()) if rect.x() + rect.width() > binary.shape[1]: rect.setWidth(binary.shape[1] - rect.x()) if rect.y() + rect.height() > binary.shape[0]: @@ -499,7 +506,7 @@ def detect_multi_text( box["y"] = int(box["y"] + effectiveRect.y()) extras["boxes"].append(box) # if char is a "wide character" (like 0,2,3,4,5,6,7,8,9), add the width-to-height ratio - if char in "023456789": + if char in "023456789" and box["h"] > 0: wh_ratios.append(box["w"] / box["h"]) if ( "normalize_wh_ratio" in rect.settings diff --git a/text_detection_target.py b/text_detection_target.py index 5759606..da9a4d4 100644 --- a/text_detection_target.py +++ b/text_detection_target.py @@ -45,6 +45,7 @@ def __init__(self, x, y, width, height, name, settings: dict | None = None): self.settings = settings self.ocrResultPerCharacterSmoother = OCRResultPerCharacterSmoother() self.last_image = None + self.last_text = None class TextDetectionTargetWithResult(TextDetectionTarget): @@ -52,6 +53,7 @@ class ResultState(enum.Enum): Success = 0 FailedFilter = 1 Empty = 2 + SameNoChange = 3 def __init__( self, diff --git a/ui_mainwindow.py b/ui_mainwindow.py index d99b70d..b31cd33 100644 --- a/ui_mainwindow.py +++ b/ui_mainwindow.py @@ -726,10 +726,17 @@ def setupUi(self, MainWindow): self.horizontalSlider_detectionCadence.setOrientation(Qt.Horizontal) self.horizontalSlider_detectionCadence.setInvertedAppearance(False) self.horizontalSlider_detectionCadence.setInvertedControls(False) + self.horizontalSlider_detectionCadence.setTickPosition(QSlider.TicksBelow) self.horizontalSlider_detectionCadence.setTickInterval(5) self.horizontalLayout_9.addWidget(self.horizontalSlider_detectionCadence) + self.checkBox_updateOnchange = QCheckBox(self.widget_detectionCadence) + self.checkBox_updateOnchange.setObjectName(u"checkBox_updateOnchange") + self.checkBox_updateOnchange.setChecked(True) + + self.horizontalLayout_9.addWidget(self.checkBox_updateOnchange) + self.verticalLayout.addWidget(self.widget_detectionCadence) @@ -998,6 +1005,10 @@ def retranslateUi(self, MainWindow): self.tabWidget_outputs.setTabText(self.tabWidget_outputs.indexOf(self.tab_vmix), QCoreApplication.translate("MainWindow", u"VMix", None)) self.pushButton_stopUpdates.setText(QCoreApplication.translate("MainWindow", u"Stop Updates", None)) self.label_detectionCadence.setText(QCoreApplication.translate("MainWindow", u"Detections / s", None)) +#if QT_CONFIG(tooltip) + self.checkBox_updateOnchange.setToolTip(QCoreApplication.translate("MainWindow", u"Only send an update if the field value has changed", None)) +#endif // QT_CONFIG(tooltip) + self.checkBox_updateOnchange.setText(QCoreApplication.translate("MainWindow", u"Update on change", None)) self.label.setText(QCoreApplication.translate("MainWindow", u"Source", None)) #if QT_CONFIG(tooltip) self.pushButton_refresh_sources.setToolTip(QCoreApplication.translate("MainWindow", u"Refresh Sources", None)) diff --git a/vmix_output.py b/vmix_output.py index fecb4d7..c6af5f4 100644 --- a/vmix_output.py +++ b/vmix_output.py @@ -26,8 +26,9 @@ def update_vmix(self, detection: list[TextDetectionTargetWithResult]): # Prepare the data to send data = {} for target in detection: - if target.name in self.field_mapping: - data[self.field_mapping[target.name]] = target.result + if target.result_state == TextDetectionTargetWithResult.ResultState.Success: + if target.name in self.field_mapping: + data[self.field_mapping[target.name]] = target.result if not data: logger.debug("No data to send")