From 2bb67b46395246af9ec5035f7af68ff43880eb73 Mon Sep 17 00:00:00 2001 From: Roy Shilkrot Date: Sun, 8 Sep 2024 22:52:17 -0400 Subject: [PATCH 1/4] feat: Add template field icon and settings --- defaults.py | 6 ++ icons/template-field.svg | 4 ++ main.py | 128 ++++++++++++++++++++++++++++------ mainwindow.ui | 144 +++++++++++++++++++++++++++++++++++---- scoresight.spec | 1 + source_view.py | 6 ++ storage.py | 4 ++ template_fields.py | 18 +++++ ui_mainwindow.py | 98 +++++++++++++++++++++++--- 9 files changed, 367 insertions(+), 42 deletions(-) create mode 100644 icons/template-field.svg create mode 100644 template_fields.py diff --git a/defaults.py b/defaults.py index 57504e5..15f2115 100644 --- a/defaults.py +++ b/defaults.py @@ -212,4 +212,10 @@ def normalize_settings_dict(settings, box_info): if "ordinal_indicator" in settings else box_info["ordinal_indicator"] ), + "templatefield": ( + settings["templatefield"] if "templatefield" in settings else False + ), + "templatefield_text": ( + settings["templatefield_text"] if "templatefield_text" in settings else "" + ), } diff --git a/icons/template-field.svg b/icons/template-field.svg new file mode 100644 index 0000000..ebb974e --- /dev/null +++ b/icons/template-field.svg @@ -0,0 +1,4 @@ + + + T + diff --git a/main.py b/main.py index 8d81a3e..37e88db 100644 --- a/main.py +++ b/main.py @@ -58,6 +58,7 @@ update_text_source, ) +from template_fields import evaluate_template_field from text_detection_target import TextDetectionTarget, TextDetectionTargetWithResult from sc_logging import logger from update_check import check_for_updates @@ -277,6 +278,10 @@ def __init__(self, translator: QTranslator, parent: QObject): 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_formatPrefix.currentIndexChanged.connect( self.formatPrefixChanged ) @@ -546,12 +551,12 @@ def clearOutputFolder(self): def editSettings(self, settingsMutatorCallback): # update the selected item's settings in the detectionTargetsStorage item = self.ui.tableWidget_boxes.currentItem() - if not item: + 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 not item_obj: + if item_obj is None: logger.info("item not found: %s", item_name) return item_obj = settingsMutatorCallback(item_obj) @@ -725,14 +730,29 @@ def detectionTargetsChanged(self, detectionTargets): ) else: item = items[0] - item.setIcon( - QIcon( - path.abspath( - path.join(path.dirname(__file__), "icons/circle-check.svg") + + if not box.settings["templatefield"]: + # this is a detection target + item.setIcon( + QIcon( + path.abspath( + path.join(path.dirname(__file__), "icons/circle-check.svg") + ) ) ) - ) - item.setData(Qt.ItemDataRole.UserRole, "checked") + item.setData(Qt.ItemDataRole.UserRole, "checked") + else: + # this is a template field + item.setIcon( + QIcon( + path.abspath( + path.join( + path.dirname(__file__), "icons/template-field.svg" + ) + ) + ) + ) + item.setData(Qt.ItemDataRole.UserRole, "templatefield") self.updatevMixTable(detectionTargets) @@ -768,10 +788,12 @@ def populateSettings(self, name): 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 not item_obj: + if item_obj is None: self.ui.lineEdit_format.setText("") self.ui.comboBox_fieldType.setCurrentIndex(0) self.ui.checkBox_smoothing.setChecked(True) @@ -790,6 +812,8 @@ def populateSettings(self, name): 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) @@ -827,6 +851,12 @@ def populateSettings(self, name): 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) @@ -848,21 +878,33 @@ def populateSettings(self, name): 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): - if item.data(Qt.ItemDataRole.UserRole) == "checked": + user_role = item.data(Qt.ItemDataRole.UserRole) + if user_role in ["checked", "templatefield"] and item.column() == 0: # enable the remove box button and disable the make box button - self.ui.pushButton_removeBox.setEnabled(True) self.ui.pushButton_makeBox.setEnabled(False) - self.ui.groupBox_target_settings.setEnabled(True) + self.ui.pushButton_removeBox.setEnabled(user_role == "checked") + self.ui.groupBox_target_settings.setEnabled(user_role == "checked") self.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(True) + self.ui.pushButton_makeBox.setEnabled(item.column() == 0) self.ui.groupBox_target_settings.setEnabled(False) self.populateSettings("") + if item.column() == 0: + # if this is not a default box - enable the template field checkbox + if item.text() not in [box["name"] for box in default_boxes]: + self.ui.checkBox_templatefield.setEnabled(True) + self.ui.lineEdit_templatefield.setEnabled(True) + else: + self.ui.checkBox_templatefield.setEnabled(False) + self.ui.lineEdit_templatefield.setEnabled(False) + def openOBSConnectModal(self): # disable OBS options self.ui.lineEdit_sceneName.setEnabled(False) @@ -1187,14 +1229,16 @@ def updateError(self, error): self.ui.widget_viewTools.setEnabled(False) def ocrResult(self, results: list[TextDetectionTargetWithResult]): - if not self.updateOCRResults: - # don't update the results, the user has disabled updates - return - - update_http_server(results) - - # update vmix - self.vmixUpdater.update_vmix(results) + # update template fields + for targetWithResult in results: + if not targetWithResult.settings["templatefield"]: + continue + targetWithResult.result = evaluate_template_field(results, targetWithResult) + targetWithResult.result_state = ( + TextDetectionTargetWithResult.ResultState.Success + if targetWithResult.result is not None + else TextDetectionTargetWithResult.ResultState.Empty + ) # update the table widget value items for targetWithResult in results: @@ -1212,6 +1256,15 @@ def ocrResult(self, results: list[TextDetectionTargetWithResult]): item = self.ui.tableWidget_boxes.item(item.row(), 1) item.setText(targetWithResult.result) + if not self.updateOCRResults: + # don't update the results, the user has disabled updates + return + + update_http_server(results) + + # update vmix + self.vmixUpdater.update_vmix(results) + if self.out_folder is None: return @@ -1391,6 +1444,39 @@ def removeBox(self): self.listItemClicked(item) self.detectionTargetsStorage.remove_item(item.text()) + def makeTemplateField(self, toggled: bool): + item = self.ui.tableWidget_boxes.currentItem() + if not item: + return + + if not toggled: + self.removeBox() + return + + # create a new box on self.image_viewer with the name of the selected item from the tableWidget_boxes + # change the list icon to green checkmark + item.setIcon( + QIcon( + path.abspath( + path.join(path.dirname(__file__), "icons/template-field.svg") + ) + ) + ) + item.setData(Qt.ItemDataRole.UserRole, "templatefield") + + self.detectionTargetsStorage.add_item( + TextDetectionTarget( + 0, + 0, + 0, + 0, + item.text(), + normalize_settings_dict({"templatefield": True}, None), + ) + ) + + self.listItemClicked(item) + def createOBSScene(self): self.ui.statusbar.showMessage("Creating OBS scene") # get the scene name from the lineEdit_sceneName diff --git a/mainwindow.ui b/mainwindow.ui index 8c90928..f4c4976 100644 --- a/mainwindow.ui +++ b/mainwindow.ui @@ -7,18 +7,15 @@ 0 0 961 - 720 + 734 ScoreSight - - - 0 - - + + @@ -908,6 +905,44 @@ + + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + This field is a combination of exising fields in a template + + + Template Field + + + + + + + + + + {{template}} + + + + + + @@ -957,12 +992,6 @@ - - - 0 - 0 - - QTabWidget::Rounded @@ -1402,6 +1431,95 @@ + + + API + + + + QFormLayout::AllNonFixedFieldsGrow + + + + + URL + + + + + + + http:// + + + + + + + Encode + + + + + + + + JSON + + + + + XML + + + + + Key-Value + + + + + Plain Text + + + + + + + + Websocket? + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + Start + + + + + + + Send out API requests to external services. + + + + + @@ -1492,7 +1610,7 @@ - + true diff --git a/scoresight.spec b/scoresight.spec index 0c43fda..5627da4 100644 --- a/scoresight.spec +++ b/scoresight.spec @@ -15,6 +15,7 @@ datas = [ ('.env', '.'), ('icons/circle-check.svg', './icons'), ('icons/circle-x.svg', './icons'), + ('icons/template-field.svg', './icons'), ('icons/MacOS_icon.png', './icons'), ('icons/plus.svg', './icons'), ('icons/splash.png', './icons'), diff --git a/source_view.py b/source_view.py index add2555..f87bad1 100644 --- a/source_view.py +++ b/source_view.py @@ -390,6 +390,9 @@ def detectionTargetsChanged(self): ) # add the boxes to the scene for detectionTarget in detectionTargets: + if detectionTarget.settings["templatefield"]: + # do not show the template fields + continue self.scene.addItem( ResizableRectWithNameTypeAndResult( detectionTarget.x(), @@ -515,6 +518,9 @@ def ocrResult(self, results: list[TextDetectionTargetWithResult]): return # update the rect with the result for targetWithResult in results: + if targetWithResult.settings["templatefield"]: + # do not update template fields + continue item = self.findBox(targetWithResult.name) if item: item.updateResult(targetWithResult) diff --git a/storage.py b/storage.py index 3543bfe..0c1e042 100644 --- a/storage.py +++ b/storage.py @@ -280,6 +280,10 @@ def getBoxesForStorage(self): "binarization_method": detectionTarget.settings.get( "binarization_method" ), + "templatefield": detectionTarget.settings.get("templatefield"), + "templatefield_text": detectionTarget.settings.get( + "templatefield_text" + ), }, } ) diff --git a/template_fields.py b/template_fields.py new file mode 100644 index 0000000..78856d1 --- /dev/null +++ b/template_fields.py @@ -0,0 +1,18 @@ +from text_detection_target import TextDetectionTargetWithResult + + +def evaluate_template_field( + results: list[TextDetectionTargetWithResult], + template_field: TextDetectionTargetWithResult, +): + template = template_field.settings["templatefield_text"] + if not template: + return None + if not results or len(results) == 0: + return "" + + # replace the template field with the results by name + for result in results: + template = template.replace("{{" + result.name + "}}", result.result) + + return template diff --git a/ui_mainwindow.py b/ui_mainwindow.py index 735748c..a15d1bf 100644 --- a/ui_mainwindow.py +++ b/ui_mainwindow.py @@ -28,12 +28,11 @@ class Ui_MainWindow(object): def setupUi(self, MainWindow): if not MainWindow.objectName(): MainWindow.setObjectName(u"MainWindow") - MainWindow.resize(961, 720) + MainWindow.resize(961, 734) self.centralwidget = QWidget(MainWindow) self.centralwidget.setObjectName(u"centralwidget") - self.horizontalLayout = QHBoxLayout(self.centralwidget) - self.horizontalLayout.setSpacing(0) - self.horizontalLayout.setObjectName(u"horizontalLayout") + self.formLayout = QFormLayout(self.centralwidget) + self.formLayout.setObjectName(u"formLayout") self.frame = QFrame(self.centralwidget) self.frame.setObjectName(u"frame") sizePolicy = QSizePolicy(QSizePolicy.Policy.Fixed, QSizePolicy.Policy.Preferred) @@ -470,6 +469,24 @@ def setupUi(self, MainWindow): self.verticalLayout_3.addWidget(self.groupBox_target_settings) + self.widget_23 = QWidget(self.groupBox_sb_info) + self.widget_23.setObjectName(u"widget_23") + self.horizontalLayout = QHBoxLayout(self.widget_23) + self.horizontalLayout.setObjectName(u"horizontalLayout") + self.horizontalLayout.setContentsMargins(0, 0, 0, 0) + self.checkBox_templatefield = QCheckBox(self.widget_23) + self.checkBox_templatefield.setObjectName(u"checkBox_templatefield") + + self.horizontalLayout.addWidget(self.checkBox_templatefield) + + self.lineEdit_templatefield = QLineEdit(self.widget_23) + self.lineEdit_templatefield.setObjectName(u"lineEdit_templatefield") + + self.horizontalLayout.addWidget(self.lineEdit_templatefield) + + + self.verticalLayout_3.addWidget(self.widget_23) + self.widget_6 = QWidget(self.groupBox_sb_info) self.widget_6.setObjectName(u"widget_6") self.horizontalLayout_7 = QHBoxLayout(self.widget_6) @@ -503,8 +520,6 @@ def setupUi(self, MainWindow): self.tabWidget_outputs = QTabWidget(self.frame) self.tabWidget_outputs.setObjectName(u"tabWidget_outputs") - sizePolicy3.setHeightForWidth(self.tabWidget_outputs.sizePolicy().hasHeightForWidth()) - self.tabWidget_outputs.setSizePolicy(sizePolicy3) self.tabWidget_outputs.setTabShape(QTabWidget.Rounded) self.tab_textFiles = QWidget() self.tab_textFiles.setObjectName(u"tab_textFiles") @@ -741,6 +756,55 @@ def setupUi(self, MainWindow): self.gridLayout_3.addLayout(self.verticalLayout_6, 0, 0, 1, 1) self.tabWidget_outputs.addTab(self.tab_vmix, "") + 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.label_20 = QLabel(self.tab_api) + self.label_20.setObjectName(u"label_20") + + self.formLayout_3.setWidget(1, QFormLayout.LabelRole, self.label_20) + + self.lineEdit_api_url = QLineEdit(self.tab_api) + self.lineEdit_api_url.setObjectName(u"lineEdit_api_url") + + self.formLayout_3.setWidget(1, QFormLayout.FieldRole, self.lineEdit_api_url) + + self.label_21 = QLabel(self.tab_api) + self.label_21.setObjectName(u"label_21") + + self.formLayout_3.setWidget(3, QFormLayout.LabelRole, self.label_21) + + self.comboBox_api_encode = QComboBox(self.tab_api) + self.comboBox_api_encode.addItem("") + self.comboBox_api_encode.addItem("") + self.comboBox_api_encode.addItem("") + self.comboBox_api_encode.addItem("") + self.comboBox_api_encode.setObjectName(u"comboBox_api_encode") + + self.formLayout_3.setWidget(3, QFormLayout.FieldRole, self.comboBox_api_encode) + + self.checkBox_is_websocket = QCheckBox(self.tab_api) + self.checkBox_is_websocket.setObjectName(u"checkBox_is_websocket") + + self.formLayout_3.setWidget(2, QFormLayout.FieldRole, self.checkBox_is_websocket) + + self.verticalSpacer = QSpacerItem(20, 40, QSizePolicy.Policy.Minimum, QSizePolicy.Policy.Expanding) + + self.formLayout_3.setItem(5, QFormLayout.FieldRole, self.verticalSpacer) + + self.pushButton_api_start = QPushButton(self.tab_api) + self.pushButton_api_start.setObjectName(u"pushButton_api_start") + + self.formLayout_3.setWidget(4, QFormLayout.FieldRole, self.pushButton_api_start) + + self.label_22 = QLabel(self.tab_api) + self.label_22.setObjectName(u"label_22") + + self.formLayout_3.setWidget(0, QFormLayout.FieldRole, self.label_22) + + self.tabWidget_outputs.addTab(self.tab_api, "") self.verticalLayout.addWidget(self.tabWidget_outputs, 0, Qt.AlignTop) @@ -785,7 +849,7 @@ def setupUi(self, MainWindow): self.verticalLayout.addWidget(self.widget_detectionCadence) - self.horizontalLayout.addWidget(self.frame) + self.formLayout.setWidget(0, QFormLayout.LabelRole, self.frame) self.frame_source_view = QFrame(self.centralwidget) self.frame_source_view.setObjectName(u"frame_source_view") @@ -1005,7 +1069,7 @@ def setupUi(self, MainWindow): self.verticalLayout_2.addWidget(self.frame_for_source_view_label) - self.horizontalLayout.addWidget(self.frame_source_view) + self.formLayout.setWidget(0, QFormLayout.FieldRole, self.frame_source_view) MainWindow.setCentralWidget(self.centralwidget) self.menubar = QMenuBar(MainWindow) @@ -1088,6 +1152,12 @@ def retranslateUi(self, MainWindow): 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) + self.checkBox_templatefield.setText(QCoreApplication.translate("MainWindow", u"Template Field", None)) + self.lineEdit_templatefield.setText("") + self.lineEdit_templatefield.setPlaceholderText(QCoreApplication.translate("MainWindow", u"{{template}}", None)) self.label_10.setText(QCoreApplication.translate("MainWindow", u"OCR Model", None)) self.label_7.setText(QCoreApplication.translate("MainWindow", u"Folder", None)) self.pushButton_selectFolder.setText(QCoreApplication.translate("MainWindow", u"Open", None)) @@ -1124,6 +1194,18 @@ 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.label_20.setText(QCoreApplication.translate("MainWindow", u"URL", None)) + self.lineEdit_api_url.setPlaceholderText(QCoreApplication.translate("MainWindow", u"http://", None)) + self.label_21.setText(QCoreApplication.translate("MainWindow", u"Encode", None)) + self.comboBox_api_encode.setItemText(0, QCoreApplication.translate("MainWindow", u"JSON", None)) + self.comboBox_api_encode.setItemText(1, QCoreApplication.translate("MainWindow", u"XML", None)) + self.comboBox_api_encode.setItemText(2, QCoreApplication.translate("MainWindow", u"Key-Value", None)) + self.comboBox_api_encode.setItemText(3, QCoreApplication.translate("MainWindow", u"Plain Text", None)) + + self.checkBox_is_websocket.setText(QCoreApplication.translate("MainWindow", u"Websocket?", None)) + self.pushButton_api_start.setText(QCoreApplication.translate("MainWindow", u"Start", None)) + self.label_22.setText(QCoreApplication.translate("MainWindow", u"Send out API requests to external services.", None)) + self.tabWidget_outputs.setTabText(self.tabWidget_outputs.indexOf(self.tab_api), QCoreApplication.translate("MainWindow", u"API", 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) From d6b777a8d8a9aedd3e1f592b1874c157d34ecefb Mon Sep 17 00:00:00 2001 From: Roy Shilkrot Date: Sun, 8 Sep 2024 23:02:06 -0400 Subject: [PATCH 2/4] feat: Add template field for derived values and optional extra text --- docs/README.md | 1 + main.py | 7 ++++--- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/docs/README.md b/docs/README.md index dcbf658..486abf4 100644 --- a/docs/README.md +++ b/docs/README.md @@ -24,6 +24,7 @@ If you'd like to donate to help support the project, you can do so on [GitHub](h - Integrations: OBS (websocket), vMix (API), NewBlue FX Titler (API) - Up to 30 updates/s - Unlimited detection boxes +- Template fields: Derived from other fields and optional extra text - 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 diff --git a/main.py b/main.py index 37e88db..527bbad 100644 --- a/main.py +++ b/main.py @@ -1388,19 +1388,20 @@ def editBoxName(self, item): new_name, ok = QInputDialog.getText( self, "Edit Box Name", "New Name:", text=item.text() ) - if ok and new_name != "" and new_name != item.text(): + old_name = item.text() + if ok and new_name != "" and new_name != old_name: # check if name doesn't exist already for i in range(self.ui.tableWidget_boxes.rowCount()): if new_name == self.ui.tableWidget_boxes.item(i, 0).text(): logger.info("Name '%s' already exists", new_name) return # rename the item in the detectionTargetsStorage - if not self.detectionTargetsStorage.rename_item(item.text(), new_name): + if not self.detectionTargetsStorage.rename_item(old_name, new_name): logger.info("Error renaming item in application storage") return # rename the item in the tableWidget_boxes + rename_custom_box_name_in_storage(old_name, new_name) item.setText(new_name) - rename_custom_box_name_in_storage(item.text(), new_name) def makeBox(self): item = self.ui.tableWidget_boxes.currentItem() From 5686511a9c54843d31038687d62554c19f80721d Mon Sep 17 00:00:00 2001 From: Roy Shilkrot Date: Mon, 9 Sep 2024 08:54:07 -0400 Subject: [PATCH 3/4] chore: Prevent editing default boxes and remove non-templatefield items from tableWidget_boxes --- main.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/main.py b/main.py index 527bbad..ade025f 100644 --- a/main.py +++ b/main.py @@ -1384,6 +1384,7 @@ def removeCustomBox(self): def editBoxName(self, item): if item.text() in [o["name"] for o in default_boxes]: + # dont allow editing default boxes return new_name, ok = QInputDialog.getText( self, "Edit Box Name", "New Name:", text=item.text() @@ -1395,13 +1396,18 @@ def editBoxName(self, item): if new_name == self.ui.tableWidget_boxes.item(i, 0).text(): logger.info("Name '%s' already exists", new_name) return + # rename the item in the tableWidget_boxes + rename_custom_box_name_in_storage(old_name, new_name) + item.setText(new_name) # rename the item in the detectionTargetsStorage if not self.detectionTargetsStorage.rename_item(old_name, new_name): logger.info("Error renaming item in application storage") return - # rename the item in the tableWidget_boxes - rename_custom_box_name_in_storage(old_name, new_name) - item.setText(new_name) + else: + # check if the item role isn't "templatefield" + if item.data(Qt.ItemDataRole.UserRole) != "templatefield": + # remove the item from the tableWidget_boxes + self.ui.tableWidget_boxes.removeRow(item.row()) def makeBox(self): item = self.ui.tableWidget_boxes.currentItem() From 3292f3efe508a813ff8e41d63d2848ef722fb8ea Mon Sep 17 00:00:00 2001 From: Roy Shilkrot Date: Mon, 9 Sep 2024 21:57:09 -0400 Subject: [PATCH 4/4] feat: Add resizable rectangle component for better user interaction --- resizable_rect.py | 313 ++++++++++++++++++++++++++++++++++++++++++++ scoresight.spec | 1 + source_view.py | 323 +++------------------------------------------- 3 files changed, 334 insertions(+), 303 deletions(-) create mode 100644 resizable_rect.py diff --git a/resizable_rect.py b/resizable_rect.py new file mode 100644 index 0000000..c42d210 --- /dev/null +++ b/resizable_rect.py @@ -0,0 +1,313 @@ +from PySide6.QtCore import QPointF, QRectF, Qt +from PySide6.QtGui import QBrush, QColor, QFont, QPen +from PySide6.QtWidgets import ( + QGraphicsItem, + QGraphicsRectItem, + QGraphicsSimpleTextItem, +) + +from text_detection_target import TextDetectionTargetWithResult + + +class ResizableRect(QGraphicsRectItem): + selected_edge = None + + def __init__(self, x, y, width, height, onCenter=False): + if onCenter: + super().__init__(-width / 2, -height / 2, width, height) + else: + super().__init__(0, 0, width, height) + self.setPos(x, y) + self.setFlags(QGraphicsItem.GraphicsItemFlag.ItemIsMovable) + self.setAcceptHoverEvents(True) + self.setPen(QPen(QBrush(Qt.GlobalColor.red), 3)) + + def getOriginalRect(self): + # get the original rect adjusted by the pen width + rect = self.rect() + border = 0 # self.pen().width() / 2 + return QRectF( + rect.x() + border, + rect.y() + border, + rect.width() - border * 2, + rect.height() - border * 2, + ) + + def getEdges(self, pos): + rect = self.rect() + border = self.pen().width() + 2 + + edge = None + if pos.x() < rect.x() + border: + edge = edge | Qt.Edge.LeftEdge if edge else Qt.Edge.LeftEdge + elif pos.x() > rect.right() - border: + edge = edge | Qt.Edge.RightEdge if edge else Qt.Edge.RightEdge + if pos.y() < rect.y() + border: + edge = edge | Qt.Edge.TopEdge if edge else Qt.Edge.TopEdge + elif pos.y() > rect.bottom() - border: + edge = edge | Qt.Edge.BottomEdge if edge else Qt.Edge.BottomEdge + + return edge + + def mousePressEvent(self, event): + if event.button() == Qt.MouseButton.LeftButton: + self.selected_edge = self.getEdges(event.pos()) + self.offset = QPointF() + else: + self.selected_edge = None + super().mousePressEvent(event) + + def mouseMoveEvent(self, event): + if self.selected_edge: + mouse_delta = event.pos() - event.buttonDownPos(Qt.MouseButton.LeftButton) + rect = self.rect() + pos_delta = QPointF() + border = self.pen().width() + + if self.selected_edge & Qt.Edge.LeftEdge: + # ensure that the width is *always* positive, otherwise limit + # both the delta position and width, based on the border size + diff = min(mouse_delta.x() - self.offset.x(), rect.width() - border) + if rect.x() < 0: + offset = diff / 2 + self.offset.setX(self.offset.x() + offset) + pos_delta.setX(offset) + rect.adjust(offset, 0, -offset, 0) + else: + pos_delta.setX(diff) + rect.setWidth(rect.width() - diff) + elif self.selected_edge & Qt.Edge.RightEdge: + if rect.x() < 0: + diff = max(mouse_delta.x() - self.offset.x(), border - rect.width()) + offset = diff / 2 + self.offset.setX(self.offset.x() + offset) + pos_delta.setX(offset) + rect.adjust(-offset, 0, offset, 0) + else: + rect.setWidth(max(border, event.pos().x() - rect.x())) + + if self.selected_edge & Qt.Edge.TopEdge: + # similarly to what done for LeftEdge, but for the height + diff = min(mouse_delta.y() - self.offset.y(), rect.height() - border) + if rect.y() < 0: + offset = diff / 2 + self.offset.setY(self.offset.y() + offset) + pos_delta.setY(offset) + rect.adjust(0, offset, 0, -offset) + else: + pos_delta.setY(diff) + rect.setHeight(rect.height() - diff) + elif self.selected_edge & Qt.Edge.BottomEdge: + if rect.y() < 0: + diff = max( + mouse_delta.y() - self.offset.y(), border - rect.height() + ) + offset = diff / 2 + self.offset.setY(self.offset.y() + offset) + pos_delta.setY(offset) + rect.adjust(0, -offset, 0, offset) + else: + rect.setHeight(max(border, event.pos().y() - rect.y())) + + if rect != self.rect(): + self.setRect(rect) + if pos_delta: + self.setPos(self.pos() + pos_delta) + else: + # use the default implementation for ItemIsMovable + super().mouseMoveEvent(event) + + def mouseReleaseEvent(self, event): + self.selected_edge = None + super().mouseReleaseEvent(event) + + def hoverMoveEvent(self, event): + edges = self.getEdges(event.pos()) + if not edges: + # self.unsetCursor() + # show a moving cursor when the mouse is over the item + self.setCursor(Qt.CursorShape.OpenHandCursor) + elif edges in ( + Qt.Edge.TopEdge | Qt.Edge.LeftEdge, + Qt.Edge.BottomEdge | Qt.Edge.RightEdge, + ): + self.setCursor(Qt.CursorShape.SizeFDiagCursor) + elif edges in ( + Qt.Edge.BottomEdge | Qt.Edge.LeftEdge, + Qt.Edge.TopEdge | Qt.Edge.RightEdge, + ): + self.setCursor(Qt.CursorShape.SizeBDiagCursor) + elif edges in (Qt.Edge.LeftEdge, Qt.Edge.RightEdge): + self.setCursor(Qt.CursorShape.SizeHorCursor) + else: + self.setCursor(Qt.CursorShape.SizeVerCursor) + super().hoverMoveEvent(event) + + +class ResizableRectWithNameTypeAndResult(ResizableRect): + def __init__( + self, + x, + y, + width, + height, + name, + image_size, + result="", + onCenter=False, + boxChangedCallback=None, + itemSelectedCallback=None, + showOCRRects=True, + ): + super().__init__(x, y, width, height, onCenter) + self.setAcceptedMouseButtons(Qt.MouseButton.LeftButton) + self.setAcceptHoverEvents(True) + self.name = name + self.result = result + self.boxChangedCallback = boxChangedCallback + self.itemSelectedCallback = itemSelectedCallback + self.posItem = QGraphicsSimpleTextItem("{}".format(self.name), parent=self) + 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)) + xpos = ( + self.boundingRect().x() + - self.posItem.boundingRect().width() / 2 + + self.boundingRect().width() / 2 + ) + ypos = self.boundingRect().y() - self.posItem.boundingRect().height() + # 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 getRect(self): + return self.getOriginalRect() + + def updateResult(self, targetWithResult: TextDetectionTargetWithResult): + self.result = targetWithResult.result + self.resultItem.setText(targetWithResult.result) + # set the result color based on the state + if ( + targetWithResult.result_state + == 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 + ): + self.resultItem.setBrush(QBrush(QColor("yellow"))) + elif ( + targetWithResult.result_state + == TextDetectionTargetWithResult.ResultState.Empty + ): + self.resultItem.setText("EMP") + self.resultItem.setBrush(QBrush(QColor("red"))) + else: + self.resultItem.setBrush(QBrush(QColor("white"))) + # set the result position to the lower left corner of the rect + self.resultItem.setPos( + self.boundingRect().x() + self.pen().width(), + self.boundingRect().y() + + self.boundingRect().height() + - self.resultItem.boundingRect().height(), + ) + 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() + return + else: + if self.effectiveRect is not None: + self.effectiveRect.show() + + if targetWithResult.effectiveRect is not None: + # draw the effective rect in the scene + if self.effectiveRect is None: + self.effectiveRect = QGraphicsRectItem( + targetWithResult.effectiveRect, parent=self + ) + # ignore any mouse events on the effective rect + self.effectiveRect.setAcceptHoverEvents(False) + self.effectiveRect.setAcceptDrops(False) + self.effectiveRect.setAcceptedMouseButtons(Qt.MouseButton.NoButton) + self.effectiveRect.setBrush(QBrush(QColor(0, 0, 0, 0))) + self.effectiveRect.setPen(QPen(QColor("green"), 3)) + self.effectiveRect.setZValue(-1) + else: + self.effectiveRect.setRect(targetWithResult.effectiveRect) + else: + if self.effectiveRect is not None: + self.effectiveRect.hide() + if ( + targetWithResult.extras is not None + and "boxes" in targetWithResult.extras + and len(targetWithResult.extras["boxes"]) > 0 + ): + if len(self.extraBoxes) > 0: + for extraBox in self.extraBoxes: + # remove from the scene + extraBox.hide() + self.scene().removeItem(extraBox) + self.extraBoxes.clear() + for box in targetWithResult.extras["boxes"]: + if not ("x" in box and "y" in box and "w" in box and "h" in box): + continue + # draw the extra boxes in the scene + extraRect = QGraphicsRectItem( + QRectF(box["x"], box["y"], box["w"], box["h"]), parent=self + ) + # ignore any mouse events on the extra rect + extraRect.setAcceptHoverEvents(False) + extraRect.setAcceptDrops(False) + extraRect.setAcceptedMouseButtons(Qt.MouseButton.NoButton) + extraRect.setBrush(QBrush(QColor(0, 0, 0, 0))) + extraRect.setPen(QPen(QColor("blue"), 3)) + extraRect.setZValue(-2) + self.extraBoxes.append(extraRect) + + def mouseReleaseEvent(self, event): + super().mouseReleaseEvent(event) + origRect = self.getRect() + boxRect = QRectF( + origRect.x() + self.x(), + origRect.y() + self.y(), + origRect.width(), + origRect.height(), + ) + self.boxChangedCallback(self.name, boxRect) + + def mousePressEvent(self, event): + super().mousePressEvent(event) + self.itemSelectedCallback(self.name) + + def mouseMoveEvent(self, event): + return super().mouseMoveEvent(event) diff --git a/scoresight.spec b/scoresight.spec index 5627da4..bf23955 100644 --- a/scoresight.spec +++ b/scoresight.spec @@ -59,6 +59,7 @@ sources = [ 'main.py', 'ndi.py', 'obs_websocket.py', + 'resizable_rect.py', 'sc_logging.py', 'screen_capture_source.py', 'source_view.py', diff --git a/source_view.py b/source_view.py index f87bad1..9f1c2c7 100644 --- a/source_view.py +++ b/source_view.py @@ -1,321 +1,37 @@ -from PySide6.QtCore import QPointF, QRectF, Qt, QTimer -from PySide6.QtGui import QBrush, QColor, QFont, QMouseEvent, QPen, QPolygonF +import math +from PySide6.QtCore import QPointF, Qt, QTimer +from PySide6.QtGui import QBrush, QColor, QMouseEvent, QPen, QPolygonF from PySide6.QtWidgets import ( - QGraphicsItem, QGraphicsPolygonItem, QGraphicsRectItem, - QGraphicsSimpleTextItem, - QGraphicsView, ) from camera_view import CameraView from storage import fetch_data, remove_data, store_data from text_detection_target import TextDetectionTarget, TextDetectionTargetWithResult from sc_logging import logger +from resizable_rect import ResizableRectWithNameTypeAndResult -class ResizableRect(QGraphicsRectItem): - selected_edge = None +def sort_points_clockwise(points: list[QGraphicsRectItem]) -> list[QGraphicsRectItem]: + # Calculate the center point + center_x = sum(point.x() for point in points) / len(points) + center_y = sum(point.y() for point in points) / len(points) + center = QPointF(center_x, center_y) - def __init__(self, x, y, width, height, onCenter=False): - if onCenter: - super().__init__(-width / 2, -height / 2, width, height) - else: - super().__init__(0, 0, width, height) - self.setPos(x, y) - self.setFlags(QGraphicsItem.GraphicsItemFlag.ItemIsMovable) - self.setAcceptHoverEvents(True) - self.setPen(QPen(QBrush(Qt.GlobalColor.red), 3)) - - def getOriginalRect(self): - # get the original rect adjusted by the pen width - rect = self.rect() - border = 0 # self.pen().width() / 2 - return QRectF( - rect.x() + border, - rect.y() + border, - rect.width() - border * 2, - rect.height() - border * 2, - ) - - def getEdges(self, pos): - rect = self.rect() - border = self.pen().width() + 2 - - edge = None - if pos.x() < rect.x() + border: - edge = edge | Qt.Edge.LeftEdge if edge else Qt.Edge.LeftEdge - elif pos.x() > rect.right() - border: - edge = edge | Qt.Edge.RightEdge if edge else Qt.Edge.RightEdge - if pos.y() < rect.y() + border: - edge = edge | Qt.Edge.TopEdge if edge else Qt.Edge.TopEdge - elif pos.y() > rect.bottom() - border: - edge = edge | Qt.Edge.BottomEdge if edge else Qt.Edge.BottomEdge - - return edge - - def mousePressEvent(self, event): - if event.button() == Qt.MouseButton.LeftButton: - self.selected_edge = self.getEdges(event.pos()) - self.offset = QPointF() - else: - self.selected_edge = None - super().mousePressEvent(event) - - def mouseMoveEvent(self, event): - if self.selected_edge: - mouse_delta = event.pos() - event.buttonDownPos(Qt.MouseButton.LeftButton) - rect = self.rect() - pos_delta = QPointF() - border = self.pen().width() - - if self.selected_edge & Qt.Edge.LeftEdge: - # ensure that the width is *always* positive, otherwise limit - # both the delta position and width, based on the border size - diff = min(mouse_delta.x() - self.offset.x(), rect.width() - border) - if rect.x() < 0: - offset = diff / 2 - self.offset.setX(self.offset.x() + offset) - pos_delta.setX(offset) - rect.adjust(offset, 0, -offset, 0) - else: - pos_delta.setX(diff) - rect.setWidth(rect.width() - diff) - elif self.selected_edge & Qt.Edge.RightEdge: - if rect.x() < 0: - diff = max(mouse_delta.x() - self.offset.x(), border - rect.width()) - offset = diff / 2 - self.offset.setX(self.offset.x() + offset) - pos_delta.setX(offset) - rect.adjust(-offset, 0, offset, 0) - else: - rect.setWidth(max(border, event.pos().x() - rect.x())) - - if self.selected_edge & Qt.Edge.TopEdge: - # similarly to what done for LeftEdge, but for the height - diff = min(mouse_delta.y() - self.offset.y(), rect.height() - border) - if rect.y() < 0: - offset = diff / 2 - self.offset.setY(self.offset.y() + offset) - pos_delta.setY(offset) - rect.adjust(0, offset, 0, -offset) - else: - pos_delta.setY(diff) - rect.setHeight(rect.height() - diff) - elif self.selected_edge & Qt.Edge.BottomEdge: - if rect.y() < 0: - diff = max( - mouse_delta.y() - self.offset.y(), border - rect.height() - ) - offset = diff / 2 - self.offset.setY(self.offset.y() + offset) - pos_delta.setY(offset) - rect.adjust(0, -offset, 0, offset) - else: - rect.setHeight(max(border, event.pos().y() - rect.y())) - - if rect != self.rect(): - self.setRect(rect) - if pos_delta: - self.setPos(self.pos() + pos_delta) - else: - # use the default implementation for ItemIsMovable - super().mouseMoveEvent(event) - - def mouseReleaseEvent(self, event): - self.selected_edge = None - super().mouseReleaseEvent(event) - - def hoverMoveEvent(self, event): - edges = self.getEdges(event.pos()) - if not edges: - # self.unsetCursor() - # show a moving cursor when the mouse is over the item - self.setCursor(Qt.CursorShape.OpenHandCursor) - elif edges in ( - Qt.Edge.TopEdge | Qt.Edge.LeftEdge, - Qt.Edge.BottomEdge | Qt.Edge.RightEdge, - ): - self.setCursor(Qt.CursorShape.SizeFDiagCursor) - elif edges in ( - Qt.Edge.BottomEdge | Qt.Edge.LeftEdge, - Qt.Edge.TopEdge | Qt.Edge.RightEdge, - ): - self.setCursor(Qt.CursorShape.SizeBDiagCursor) - elif edges in (Qt.Edge.LeftEdge, Qt.Edge.RightEdge): - self.setCursor(Qt.CursorShape.SizeHorCursor) - else: - self.setCursor(Qt.CursorShape.SizeVerCursor) - super().hoverMoveEvent(event) - - -class ResizableRectWithNameTypeAndResult(ResizableRect): - def __init__( - self, - x, - y, - width, - height, - name, - image_size, - result="", - onCenter=False, - boxChangedCallback=None, - itemSelectedCallback=None, - showOCRRects=True, - ): - super().__init__(x, y, width, height, onCenter) - self.setAcceptedMouseButtons(Qt.MouseButton.LeftButton) - self.setAcceptHoverEvents(True) - self.name = name - self.result = result - self.boxChangedCallback = boxChangedCallback - self.itemSelectedCallback = itemSelectedCallback - self.posItem = QGraphicsSimpleTextItem("{}".format(self.name), parent=self) - 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)) - xpos = ( - self.boundingRect().x() - - self.posItem.boundingRect().width() / 2 - + self.boundingRect().width() / 2 - ) - ypos = self.boundingRect().y() - self.posItem.boundingRect().height() - # 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 + # Define a function to calculate the angle between a point and the center + def angle_from_center(point): + return math.atan2(point.y() - center.y(), point.x() - center.x()) - def getRect(self): - return self.getOriginalRect() - - def updateResult(self, targetWithResult: TextDetectionTargetWithResult): - self.result = targetWithResult.result - self.resultItem.setText(targetWithResult.result) - # set the result color based on the state - if ( - targetWithResult.result_state - == 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 - ): - self.resultItem.setBrush(QBrush(QColor("yellow"))) - elif ( - targetWithResult.result_state - == TextDetectionTargetWithResult.ResultState.Empty - ): - self.resultItem.setText("EMP") - self.resultItem.setBrush(QBrush(QColor("red"))) - else: - self.resultItem.setBrush(QBrush(QColor("white"))) - # set the result position to the lower left corner of the rect - self.resultItem.setPos( - self.boundingRect().x() + self.pen().width(), - self.boundingRect().y() - + self.boundingRect().height() - - self.resultItem.boundingRect().height(), - ) - 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() - return - else: - if self.effectiveRect is not None: - self.effectiveRect.show() - - if targetWithResult.effectiveRect is not None: - # draw the effective rect in the scene - if self.effectiveRect is None: - self.effectiveRect = QGraphicsRectItem( - targetWithResult.effectiveRect, parent=self - ) - # ignore any mouse events on the effective rect - self.effectiveRect.setAcceptHoverEvents(False) - self.effectiveRect.setAcceptDrops(False) - self.effectiveRect.setAcceptedMouseButtons(Qt.MouseButton.NoButton) - self.effectiveRect.setBrush(QBrush(QColor(0, 0, 0, 0))) - self.effectiveRect.setPen(QPen(QColor("green"), 3)) - self.effectiveRect.setZValue(-1) - else: - self.effectiveRect.setRect(targetWithResult.effectiveRect) - else: - if self.effectiveRect is not None: - self.effectiveRect.hide() - if ( - targetWithResult.extras is not None - and "boxes" in targetWithResult.extras - and len(targetWithResult.extras["boxes"]) > 0 - ): - if len(self.extraBoxes) > 0: - for extraBox in self.extraBoxes: - # remove from the scene - extraBox.hide() - self.scene().removeItem(extraBox) - self.extraBoxes.clear() - for box in targetWithResult.extras["boxes"]: - if not ("x" in box and "y" in box and "w" in box and "h" in box): - continue - # draw the extra boxes in the scene - extraRect = QGraphicsRectItem( - QRectF(box["x"], box["y"], box["w"], box["h"]), parent=self - ) - # ignore any mouse events on the extra rect - extraRect.setAcceptHoverEvents(False) - extraRect.setAcceptDrops(False) - extraRect.setAcceptedMouseButtons(Qt.MouseButton.NoButton) - extraRect.setBrush(QBrush(QColor(0, 0, 0, 0))) - extraRect.setPen(QPen(QColor("blue"), 3)) - extraRect.setZValue(-2) - self.extraBoxes.append(extraRect) - - def mouseReleaseEvent(self, event): - super().mouseReleaseEvent(event) - origRect = self.getRect() - boxRect = QRectF( - origRect.x() + self.x(), - origRect.y() + self.y(), - origRect.width(), - origRect.height(), - ) - self.boxChangedCallback(self.name, boxRect) + # Sort the points based on their angles + sorted_points = sorted(points, key=angle_from_center) - def mousePressEvent(self, event): - super().mousePressEvent(event) - self.itemSelectedCallback(self.name) + # Rotate the list so that the top-left point comes first + top_left = min(sorted_points, key=lambda p: p.x() + p.y()) + while sorted_points[0] != top_left: + sorted_points = sorted_points[1:] + [sorted_points[0]] - def mouseMoveEvent(self, event): - return super().mouseMoveEvent(event) + return sorted_points class ImageViewer(CameraView): @@ -453,6 +169,7 @@ def mousePressEvent(self, event: QMouseEvent | None) -> None: self.scene.addItem(point) # add the point to the list of points self.fourCorners.append(point) + self.fourCorners = sort_points_clockwise(self.fourCorners) # if we have 4 points, create a polygon if len(self.fourCorners) >= 2: if not self.fourCornerPolygon: