From 0f37eb6742cb55b65e99ad978b184f46fe98abaa Mon Sep 17 00:00:00 2001
From: Roy Shilkrot
Date: Wed, 22 May 2024 23:04:43 -0400
Subject: [PATCH 1/2] Refactor code to handle text detection target result
states and update UI accordingly
---
README.md | 16 ++--
camera_view.py | 75 ++----------------
frame_stabilizer.py | 74 ++++++++++++++++++
main.py | 37 ++++++---
mainwindow.ui | 169 ++++++++++++++++++++---------------------
sc_logging.py | 30 ++++----
scoresight.spec | 2 +-
scripts/compile_ui.ps1 | 7 ++
source_view.py | 2 +-
storage.py | 2 +-
ui_about.py | 94 +++++++----------------
ui_mainwindow.py | 107 +++++++++++++-------------
12 files changed, 303 insertions(+), 312 deletions(-)
create mode 100644 frame_stabilizer.py
create mode 100644 scripts/compile_ui.ps1
diff --git a/README.md b/README.md
index 0e18fce..28e8c0d 100644
--- a/README.md
+++ b/README.md
@@ -94,13 +94,19 @@ There are some extra steps for installation on Windows:
### Running from source
-1. Once everything is installed launch the application:
+1. Compile the UI files into Python:
- ```shell
- python main.py
- ```
+ ```powershell
+ ./scripts/compile_ui.ps1
+ ```
+
+1. Launch the application:
+
+ ```shell
+ python main.py
+ ```
-2. Follow the on-screen instructions to load an image of the scoreboard and extract the text.
+1. Follow the on-screen instructions to load an image of the scoreboard and extract the text.
### Build an executable
diff --git a/camera_view.py b/camera_view.py
index 41b6887..62c5533 100644
--- a/camera_view.py
+++ b/camera_view.py
@@ -1,5 +1,3 @@
-import platform
-import time
from PySide6.QtWidgets import (
QGraphicsView,
QGraphicsScene,
@@ -8,19 +6,22 @@
from PySide6.QtCore import Qt
from PySide6.QtGui import QImage, QPixmap, QPainter
from PySide6.QtCore import QThread, Signal
+
+import platform
+import time
import cv2
import numpy as np
+import datetime
+from datetime import datetime
+
from camera_info import CameraInfo
from ndi import NDICapture
from screen_capture_source import ScreenCapture
-
from storage import TextDetectionTargetMemoryStorage
from tesseract import TextDetector
-import datetime
-from datetime import datetime
-
from text_detection_target import TextDetectionTargetWithResult
from sc_logging import logger
+from frame_stabilizer import FrameStabilizer
# Function to set the resolution
@@ -80,68 +81,6 @@ def set_camera_highest_resolution(cap):
set_resolution(cap, *highest_res)
-class FrameStabilizer:
- def __init__(self):
- self.stabilizationFrame = None
- self.stabilizationFrameCount = 0
- self.stabilizationBurnInCompleted = False
- self.stabilizationKPs = None
- self.stabilizationDesc = None
- self.orb = None
- self.matcher = None
-
- def reset(self):
- self.stabilizationFrame = None
- self.stabilizationFrameCount = 0
- self.stabilizationBurnInCompleted = False
- self.stabilizationKPs = None
- self.stabilizationDesc = None
-
- def stabilize_frame(self, frame_rgb):
- if self.stabilizationFrame is None:
- self.stabilizationFrame = frame_rgb
- self.stabilizationFrameCount = 0
- elif not self.stabilizationBurnInCompleted:
- self.stabilizationFrameCount += 1
- # add the new frame to the stabilization frame
- frame_rgb = cv2.addWeighted(frame_rgb, 0.5, self.stabilizationFrame, 0.5, 0)
- if self.stabilizationFrameCount == 10:
- self.stabilizationBurnInCompleted = True
- # extract ORB features from the stabilization frame
- self.orb = cv2.ORB_create()
- self.stabilizationKPs, self.stabilizationDesc = (
- self.orb.detectAndCompute(self.stabilizationFrame, None)
- )
- self.matcher = cv2.BFMatcher(cv2.NORM_HAMMING, crossCheck=True)
-
- if self.stabilizationBurnInCompleted:
- # stabilization burn-in period is over, start stabilization
- # extract features from the current frame
- kps, desc = self.orb.detectAndCompute(frame_rgb, None)
- # match the features
- matches = self.matcher.match(self.stabilizationDesc, desc)
- # sort the matches by distance
- matches = sorted(matches, key=lambda x: x.distance)
- # calculate an affine transform from the matched keypoints
- src_pts = np.float32(
- [self.stabilizationKPs[m.queryIdx].pt for m in matches]
- ).reshape(-1, 1, 2)
- dst_pts = np.float32([kps[m.trainIdx].pt for m in matches]).reshape(
- -1, 1, 2
- )
- h, _ = cv2.estimateAffinePartial2D(src_pts, dst_pts)
- # warp the frame
- if h is not None:
- frame_rgb = cv2.warpAffine(
- frame_rgb,
- h,
- (frame_rgb.shape[1], frame_rgb.shape[0]),
- flags=cv2.WARP_INVERSE_MAP | cv2.INTER_LINEAR,
- )
-
- return frame_rgb
-
-
class TimerThread(QThread):
update_signal = Signal(object)
update_error = Signal(object)
diff --git a/frame_stabilizer.py b/frame_stabilizer.py
new file mode 100644
index 0000000..4217601
--- /dev/null
+++ b/frame_stabilizer.py
@@ -0,0 +1,74 @@
+import cv2
+import numpy as np
+
+
+# This class is used to stabilize the frames of the video.
+# It uses ORB features to match keypoints between frames and calculate an affine transform to
+# warp the frame.
+class FrameStabilizer:
+ def __init__(self):
+ self.stabilizationFrame = None
+ self.stabilizationFrameCount = 0
+ self.stabilizationBurnInCompleted = False
+ self.stabilizationKPs = None
+ self.stabilizationDesc = None
+ self.orb = None
+ self.matcher = None
+
+ def reset(self):
+ self.stabilizationFrame = None
+ self.stabilizationFrameCount = 0
+ self.stabilizationBurnInCompleted = False
+ self.stabilizationKPs = None
+ self.stabilizationDesc = None
+
+ def stabilize_frame(self, frame_rgb):
+ if self.stabilizationFrame is None:
+ self.stabilizationFrame = frame_rgb
+ self.stabilizationFrameCount = 0
+ elif not self.stabilizationBurnInCompleted:
+ self.stabilizationFrameCount += 1
+ # add the new frame to the stabilization frame
+ frame_rgb = cv2.addWeighted(frame_rgb, 0.5, self.stabilizationFrame, 0.5, 0)
+ if self.stabilizationFrameCount == 10:
+ self.stabilizationBurnInCompleted = True
+ # extract ORB features from the stabilization frame
+ self.orb = cv2.ORB_create()
+ self.stabilizationKPs, self.stabilizationDesc = (
+ self.orb.detectAndCompute(self.stabilizationFrame, None)
+ )
+ self.matcher = cv2.BFMatcher(cv2.NORM_HAMMING, crossCheck=True)
+
+ if (
+ self.stabilizationBurnInCompleted
+ and self.stabilizationFrame is not None
+ and self.orb is not None
+ and self.matcher is not None
+ and self.stabilizationKPs is not None
+ and self.stabilizationDesc is not None
+ ):
+ # stabilization burn-in period is over, start stabilization
+ # extract features from the current frame
+ kps, desc = self.orb.detectAndCompute(frame_rgb, None)
+ # match the features
+ matches = self.matcher.match(self.stabilizationDesc, desc)
+ # sort the matches by distance
+ matches = sorted(matches, key=lambda x: x.distance)
+ # calculate an affine transform from the matched keypoints
+ src_pts = np.float32(
+ [self.stabilizationKPs[m.queryIdx].pt for m in matches]
+ ).reshape(-1, 1, 2)
+ dst_pts = np.float32([kps[m.trainIdx].pt for m in matches]).reshape(
+ -1, 1, 2
+ )
+ h, _ = cv2.estimateAffinePartial2D(src_pts, dst_pts)
+ # warp the frame
+ if h is not None:
+ frame_rgb = cv2.warpAffine(
+ frame_rgb,
+ h,
+ (frame_rgb.shape[1], frame_rgb.shape[0]),
+ flags=cv2.WARP_INVERSE_MAP | cv2.INTER_LINEAR,
+ )
+
+ return frame_rgb
diff --git a/main.py b/main.py
index a92cf86..c7c9a74 100644
--- a/main.py
+++ b/main.py
@@ -408,7 +408,7 @@ def selectOutputFolder(self):
folder = QFileDialog.getExistingDirectory(
self,
"Select Output Folder",
- fetch_data("scoresight.json", "output_folder"),
+ fetch_data("scoresight.json", "output_folder", ""),
options=QFileDialog.Option.ShowDirsOnly,
)
if folder and len(folder) > 0:
@@ -522,7 +522,7 @@ def vmixUiSetup(self):
if mapping:
self.vmixUpdater.set_field_mapping(mapping)
- self.ui.tableView_vmixMapping.model().itemChanged.connect(
+ self.ui.tableView_vmixMapping.model().dataChanged.connect(
self.vmixMappingChanged
)
@@ -759,9 +759,9 @@ def connectObs(self):
if self.obs_connect_modal is not None:
self.obs_websocket_client = open_obs_websocket(
{
- "ip": self.obs_modal_ui.obs_connect_modal.lineEdit_ip.text(),
- "port": self.obs_modal_ui.obs_connect_modal.lineEdit_port.text(),
- "password": self.obs_modal_ui.obs_connect_modal.lineEdit_password.text(),
+ "ip": self.obs_modal_ui.lineEdit_ip.text(),
+ "port": self.obs_modal_ui.lineEdit_port.text(),
+ "password": self.obs_modal_ui.lineEdit_password.text(),
}
)
else:
@@ -865,6 +865,8 @@ def sourceChanged(self, index):
self, "Open Video File", "", "Video Files (*.mp4 *.avi *.mov)"
)
if not file:
+ # no file selected - change source to "Select a source"
+ self.ui.comboBox_camera_source.setCurrentText("Select a source")
return
self.source_name = file
if self.source_name == "URL Source (HTTP, RTSP)":
@@ -872,16 +874,17 @@ def sourceChanged(self, index):
url_dialog = QDialog()
ui_urlsource = Ui_UrlSource()
ui_urlsource.setupUi(url_dialog)
-
url_dialog.setWindowTitle("URL Source")
# focus on url input
ui_urlsource.lineEdit_url.setFocus()
url_dialog.exec() # wait for the dialog to close
# check if the dialog was accepted
if url_dialog.result() != QDialog.DialogCode.Accepted:
+ self.ui.comboBox_camera_source.setCurrentText("Select a source")
return
self.source_name = ui_urlsource.lineEdit_url.text()
if self.source_name == "":
+ self.ui.comboBox_camera_source.setCurrentText("Select a source")
return
if self.source_name == "Screen Capture":
# open a dialog to select the screen
@@ -898,6 +901,7 @@ def sourceChanged(self, index):
screen_dialog.exec()
# check if the dialog was accepted
if screen_dialog.result() != QDialog.DialogCode.Accepted:
+ self.ui.comboBox_camera_source.setCurrentText("Select a source")
return
# get the window ID from the comboBox_window
window_id = ui_screencapture.comboBox_window.currentData()
@@ -932,6 +936,9 @@ def sourceSelectionSucessful(self):
self.ui.frame_source_view.setEnabled(False)
if self.ui.comboBox_camera_source.currentData() == "file":
+ if self.source_name is None:
+ logger.error("No file selected")
+ return
camera_info = CameraInfo(
self.source_name,
self.source_name,
@@ -939,6 +946,9 @@ def sourceSelectionSucessful(self):
CameraInfo.CameraType.FILE,
)
elif self.ui.comboBox_camera_source.currentData() == "url":
+ if self.source_name is None:
+ logger.error("No url entered")
+ return
camera_info = CameraInfo(
self.source_name,
self.source_name,
@@ -946,6 +956,9 @@ def sourceSelectionSucessful(self):
CameraInfo.CameraType.URL,
)
elif self.ui.comboBox_camera_source.currentData() == "screen_capture":
+ if self.source_name is None:
+ logger.error("No screen capture selected")
+ return
camera_info = CameraInfo(
self.source_name,
self.source_name,
@@ -1056,7 +1069,8 @@ def ocrResult(self, results: list[TextDetectionTargetWithResult]):
if targetWithResult.result is None:
continue
if (
- "skip_empty" in targetWithResult.settings
+ targetWithResult.settings is not None
+ and "skip_empty" in targetWithResult.settings
and targetWithResult.settings["skip_empty"]
and len(targetWithResult.result) == 0
):
@@ -1067,7 +1081,10 @@ def ocrResult(self, results: list[TextDetectionTargetWithResult]):
):
continue
- if self.obs_websocket_client is not None:
+ if (
+ self.obs_websocket_client is not None
+ and targetWithResult.settings is not None
+ ):
# find the source name for the target from the default boxes
update_text_source(
self.obs_websocket_client,
@@ -1202,12 +1219,12 @@ def removeBox(self):
self.detectionTargetsStorage.remove_item(item.text())
def createOBSScene(self):
- self.ui.statusbar().showMessage("Creating OBS scene")
+ self.ui.statusbar.showMessage("Creating OBS scene")
# get the scene name from the lineEdit_sceneName
scene_name = self.ui.lineEdit_sceneName.text()
# clear or create a new scene
create_obs_scene_from_export(self.obs_websocket_client, scene_name)
- self.ui.statusbar().showMessage("Finished creating scene")
+ self.ui.statusbar.showMessage("Finished creating scene")
# on destroy, close the OBS connection
def closeEvent(self, event):
diff --git a/mainwindow.ui b/mainwindow.ui
index e95e0be..8f1f37a 100644
--- a/mainwindow.ui
+++ b/mainwindow.ui
@@ -7,7 +7,7 @@
0
0
961
- 839
+ 824
@@ -74,6 +74,12 @@
-
+
+
+ 0
+ 100
+
+
Qt::ScrollBarAlwaysOff
@@ -867,8 +873,11 @@
0
+
+ QTabWidget::Rounded
+
- 0
+ 3
@@ -881,17 +890,11 @@
Text Files
-
- QFormLayout::ExpandingFieldsGrow
-
-
- Qt::AlignRight|Qt::AlignTop|Qt::AlignTrailing
-
-
- Qt::AlignHCenter|Qt::AlignTop
-
- 6
+ 2
+
+
+ 0
-
@@ -1059,14 +1062,7 @@
Browser
-
-
-
-
- Server is running.
-
-
-
- -
+
-
<html><head/><body><p>HTML Scoreboard: <a href="http://localhost:18099/scoresight"><span style=" text-decoration: underline; color:#0000ff;">http://localhost:18099/scoresight</span></a></p><p>JSON: <a href="http://localhost:18099/json"><span style=" text-decoration: underline; color:#0000ff;">http://localhost:18099/json</span></a> (optional: ?pivot)</p><p>XML: <a href="http://localhost:18099/xml"><span style=" text-decoration: underline; color:#0000ff;">http://localhost:18099/xml</span></a> (optional: ?pivot)</p><p>CSV: <a href="http://localhost:18099/csv"><span style=" text-decoration: underline; color:#0000ff;">http://localhost:18099/csv</span></a></p></body></html>
@@ -1089,39 +1085,9 @@
OBS
-
-
-
-
- false
-
-
- ScoreSight Scene
-
-
-
- -
-
-
- false
-
-
- Recreate if exists
-
-
- true
-
-
-
- -
-
-
- false
-
-
- Create OBS Scene
-
-
-
+
+ 2
+
-
@@ -1133,7 +1099,7 @@
0
- 40
+ 0
@@ -1141,6 +1107,45 @@
+ -
+
+
+
-
+
+
+ false
+
+
+ ScoreSight Scene
+
+
+
+ -
+
+
+ false
+
+
+ Create OBS Scene
+
+
+
+ -
+
+
+ false
+
+
+ Recreate
+
+
+ true
+
+
+
+
+
+
@@ -1148,14 +1153,11 @@
VMix
+
+ 2
+
-
-
-
- 0
- 0
-
-
false
@@ -1179,32 +1181,6 @@
0
-
-
-
-
- Output Mapping
-
-
-
- -
-
-
-
- 0
- 0
-
-
-
- Start
-
-
- true
-
-
- false
-
-
-
@@ -1286,6 +1262,25 @@
+ -
+
+
+
+ 0
+ 0
+
+
+
+ Start
+
+
+ true
+
+
+ false
+
+
+
@@ -1314,7 +1309,7 @@
0
- 40
+ 0
diff --git a/sc_logging.py b/sc_logging.py
index d3eaee9..09930c0 100644
--- a/sc_logging.py
+++ b/sc_logging.py
@@ -27,21 +27,6 @@
# prepend the user data directory
log_file_path = os.path.join(data_dir, f"scoresight_{current_time}.log")
-# check to see if there are more log files, and only keep the most recent 10
-log_files = [
- f
- for f in os.listdir(data_dir)
- if f.startswith("scoresight_") and f.endswith(".log")
-]
-# sort log files by date
-log_files.sort()
-if len(log_files) > 10:
- for f in log_files[:-10]:
- try:
- os.remove(os.path.join(data_dir, f))
- except Exception as e:
- logger.error(f"Failed to remove log file: {f}")
-
# Create a file handler
file_handler = logging.FileHandler(log_file_path)
file_handler.setLevel(logging.DEBUG)
@@ -60,3 +45,18 @@
console_handler.setFormatter(formatter)
logger.addHandler(console_handler)
logger.debug("Debug mode enabled")
+
+# check to see if there are more log files, and only keep the most recent 10
+log_files = [
+ f
+ for f in os.listdir(data_dir)
+ if f.startswith("scoresight_") and f.endswith(".log")
+]
+# sort log files by date
+log_files.sort()
+if len(log_files) > 10:
+ for f in log_files[:-10]:
+ try:
+ os.remove(os.path.join(data_dir, f))
+ except PermissionError as e:
+ logger.error(f"Failed to remove log file: {f}")
diff --git a/scoresight.spec b/scoresight.spec
index 78ea4d1..e55bb1e 100644
--- a/scoresight.spec
+++ b/scoresight.spec
@@ -74,7 +74,7 @@ a = Analysis(
hookspath=[],
hooksconfig={},
runtime_hooks=[],
- excludes=[],
+ excludes=['PyQt6'],
noarchive=False,
)
pyz = PYZ(a.pure)
diff --git a/scripts/compile_ui.ps1 b/scripts/compile_ui.ps1
new file mode 100644
index 0000000..a90e049
--- /dev/null
+++ b/scripts/compile_ui.ps1
@@ -0,0 +1,7 @@
+Get-ChildItem -Filter *.ui | ForEach-Object {
+ $uiFile = $_.FullName
+ $pyFile = [System.IO.Path]::ChangeExtension($uiFile, ".py")
+ # add "ui_" prefix to the file name
+ $pyFile = [System.IO.Path]::Combine($([System.IO.Path]::GetDirectoryName($pyFile)), "ui_$([System.IO.Path]::GetFileName($pyFile))")
+ pyside6-uic $uiFile -o $pyFile
+}
diff --git a/source_view.py b/source_view.py
index 42c512a..add2555 100644
--- a/source_view.py
+++ b/source_view.py
@@ -40,7 +40,7 @@ def getOriginalRect(self):
def getEdges(self, pos):
rect = self.rect()
- border = self.pen().width() / 2
+ border = self.pen().width() + 2
edge = None
if pos.x() < rect.x() + border:
diff --git a/storage.py b/storage.py
index 55a82a4..e8a0812 100644
--- a/storage.py
+++ b/storage.py
@@ -164,7 +164,7 @@ def loadBoxesFromStorage(self) -> bool:
# load the boxes from scoresight.json
boxes = fetch_data("scoresight.json", "boxes")
if not boxes:
- return
+ return False
return self.loadBoxesFromDict(boxes)
def loadBoxesFromFile(self, file_path) -> bool:
diff --git a/ui_about.py b/ui_about.py
index 6a4196b..81d4122 100644
--- a/ui_about.py
+++ b/ui_about.py
@@ -8,76 +8,40 @@
## WARNING! All changes made in this file will be lost when recompiling UI file!
################################################################################
-from PySide6.QtCore import (
- QCoreApplication,
- QDate,
- QDateTime,
- QLocale,
- QMetaObject,
- QObject,
- QPoint,
- QRect,
- QSize,
- QTime,
- QUrl,
- Qt,
-)
-from PySide6.QtGui import (
- QBrush,
- QColor,
- QConicalGradient,
- QCursor,
- QFont,
- QFontDatabase,
- QGradient,
- QIcon,
- QImage,
- QKeySequence,
- QLinearGradient,
- QPainter,
- QPalette,
- QPixmap,
- QRadialGradient,
- QTransform,
-)
-from PySide6.QtWidgets import (
- QAbstractButton,
- QApplication,
- QDialog,
- QDialogButtonBox,
- QGridLayout,
- QLabel,
- QScrollArea,
- QSizePolicy,
- QWidget,
-)
-
+from PySide6.QtCore import (QCoreApplication, QDate, QDateTime, QLocale,
+ QMetaObject, QObject, QPoint, QRect,
+ QSize, QTime, QUrl, Qt)
+from PySide6.QtGui import (QBrush, QColor, QConicalGradient, QCursor,
+ QFont, QFontDatabase, QGradient, QIcon,
+ QImage, QKeySequence, QLinearGradient, QPainter,
+ QPalette, QPixmap, QRadialGradient, QTransform)
+from PySide6.QtWidgets import (QAbstractButton, QApplication, QDialog, QDialogButtonBox,
+ QGridLayout, QLabel, QScrollArea, QSizePolicy,
+ QWidget)
class Ui_Dialog(object):
def setupUi(self, Dialog):
if not Dialog.objectName():
- Dialog.setObjectName("Dialog")
+ Dialog.setObjectName(u"Dialog")
Dialog.resize(400, 459)
- sizePolicy = QSizePolicy(
- QSizePolicy.Policy.Preferred, QSizePolicy.Policy.Preferred
- )
+ sizePolicy = QSizePolicy(QSizePolicy.Policy.Preferred, QSizePolicy.Policy.Preferred)
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
sizePolicy.setHeightForWidth(Dialog.sizePolicy().hasHeightForWidth())
Dialog.setSizePolicy(sizePolicy)
Dialog.setMaximumSize(QSize(400, 16777215))
self.gridLayout = QGridLayout(Dialog)
- self.gridLayout.setObjectName("gridLayout")
+ self.gridLayout.setObjectName(u"gridLayout")
self.scrollArea = QScrollArea(Dialog)
- self.scrollArea.setObjectName("scrollArea")
+ self.scrollArea.setObjectName(u"scrollArea")
self.scrollArea.setWidgetResizable(True)
self.scrollAreaWidgetContents = QWidget()
- self.scrollAreaWidgetContents.setObjectName("scrollAreaWidgetContents")
+ self.scrollAreaWidgetContents.setObjectName(u"scrollAreaWidgetContents")
self.scrollAreaWidgetContents.setGeometry(QRect(0, 0, 366, 984))
self.gridLayout_2 = QGridLayout(self.scrollAreaWidgetContents)
- self.gridLayout_2.setObjectName("gridLayout_2")
+ self.gridLayout_2.setObjectName(u"gridLayout_2")
self.label = QLabel(self.scrollAreaWidgetContents)
- self.label.setObjectName("label")
+ self.label.setObjectName(u"label")
self.label.setWordWrap(True)
self.label.setOpenExternalLinks(True)
@@ -88,32 +52,26 @@ def setupUi(self, Dialog):
self.gridLayout.addWidget(self.scrollArea, 0, 0, 1, 1)
self.buttonBox = QDialogButtonBox(Dialog)
- self.buttonBox.setObjectName("buttonBox")
+ self.buttonBox.setObjectName(u"buttonBox")
self.buttonBox.setOrientation(Qt.Horizontal)
self.buttonBox.setStandardButtons(QDialogButtonBox.Ok)
self.gridLayout.addWidget(self.buttonBox, 1, 0, 1, 1)
+
self.retranslateUi(Dialog)
self.buttonBox.accepted.connect(Dialog.accept)
self.buttonBox.rejected.connect(Dialog.reject)
QMetaObject.connectSlotsByName(Dialog)
-
# setupUi
def retranslateUi(self, Dialog):
- Dialog.setWindowTitle(QCoreApplication.translate("Dialog", "Dialog", None))
- self.label.setText(
- QCoreApplication.translate(
- "Dialog",
- 'About ScoreSight
ScoreSight is a cutting-edge software solution designed to simplify visual reading of scoreboards.
License
MIT License
Copyright (c) 2024 OCC AI: Open tools for Content Creators and Streamers
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIE'
- 'D, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
Third-Party Software
ScoreSight incorporates components from third-party sources under their respective licenses:
- OpenCV is used under the Apache 2 License, permitting the use, distribution, and modification of the software provided that certain conditions are met. More information can be found at OpenCV License.
- Tesseract OCR is used under the Apache 2 License, which allows for the use, distribution, and modification of the software in accordance with the terms set forth in the license. More details are available at Tesseract OCR License.
Detailed licensing information for these components is included within the software distribution.
Qt Application Framework
This application uses the Qt application framework, which is a comprehensive C++ library for cross-platform development of GUI applications. Qt is used under the terms of the GNU Lesser General Public License (LGPL) version 3. Qt is a registered trademark of The Qt Company Ltd and is developed and maintained by The Qt Project and various contributors.
For more information about Qt, including source code of Qt libraries used by thi'
- 's application and guidance on how to obtain or replace Qt libraries, please visit the Qt Project\'s official website at http://www.qt.io.
We are committed to ensuring compliance with the LGPL v3 license and support the principles of open source software development. If you have any questions or concerns regarding our use of Qt, please contact us directly.
Disclaimer of Warranty
ScoreSight is provided "AS IS", without warranty of any kind, express or implied, including but not limited to the warranties of merchantability, fitness for a particular purpose, and noninfringement. In no event shall Roy Shilkrot be liable for any claim, damages, or other liability, whether in an action of contract, tort or otherwise, arising from, out of, or in connection with the software or the use or other dealings in the software.
Contact Information
For support, feedback, or more information, please visit https://scoresight.live or contact us at info@scoresight.live.
',
- None,
- )
- )
-
+ Dialog.setWindowTitle(QCoreApplication.translate("Dialog", u"Dialog", None))
+ self.label.setText(QCoreApplication.translate("Dialog", u"
About ScoreSight
ScoreSight is a cutting-edge software solution designed to simplify visual reading of scoreboards.
License
MIT License
Copyright (c) 2024 OCC AI: Open tools for Content Creators and Streamers
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIE"
+ "D, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
Third-Party Software
ScoreSight incorporates components from third-party sources under their respective licenses:
- OpenCV is used under the Apache 2 License, permitting the use, distribution, and modification of the software provided that certain conditions are met. More information can be found at OpenCV License.
- Tesseract OCR is used under the Apache 2 License, which allows for the use, distribution, and modification of the software in accordance with the terms set forth in the license. More details are available at Tesseract OCR License.
Detailed licensing information for these components is included within the software distribution.
Qt Application Framework
This application uses the Qt application framework, which is a comprehensive C++ library for cross-platform development of GUI applications. Qt is used under the terms of the GNU Lesser General Public License (LGPL) version 3. Qt is a registered trademark of The Qt Company Ltd and is developed and maintained by The Qt Project and various contributors.
For more information about Qt, including source code of Qt libraries used by thi"
+ "s application and guidance on how to obtain or replace Qt libraries, please visit the Qt Project's official website at http://www.qt.io.
We are committed to ensuring compliance with the LGPL v3 license and support the principles of open source software development. If you have any questions or concerns regarding our use of Qt, please contact us directly.
Disclaimer of Warranty
ScoreSight is provided "AS IS", without warranty of any kind, express or implied, including but not limited to the warranties of merchantability, fitness for a particular purpose, and noninfringement. In no event shall Roy Shilkrot be liable for any claim, damages, or other liability, whether in an action of contract, tort or otherwise, arising from, out of, or in connection with the software or the use or other dealings in the software.
Contact Information
For support, feedback, or more information, please visit https://scoresight.live or contact us at info@scoresight.live.
", None))
# retranslateUi
+
diff --git a/ui_mainwindow.py b/ui_mainwindow.py
index b31cd33..677eb30 100644
--- a/ui_mainwindow.py
+++ b/ui_mainwindow.py
@@ -27,7 +27,7 @@ class Ui_MainWindow(object):
def setupUi(self, MainWindow):
if not MainWindow.objectName():
MainWindow.setObjectName(u"MainWindow")
- MainWindow.resize(961, 839)
+ MainWindow.resize(961, 824)
self.centralwidget = QWidget(MainWindow)
self.centralwidget.setObjectName(u"centralwidget")
self.horizontalLayout = QHBoxLayout(self.centralwidget)
@@ -67,6 +67,7 @@ def setupUi(self, MainWindow):
__qtablewidgetitem1 = QTableWidgetItem()
self.tableWidget_boxes.setHorizontalHeaderItem(1, __qtablewidgetitem1)
self.tableWidget_boxes.setObjectName(u"tableWidget_boxes")
+ self.tableWidget_boxes.setMinimumSize(QSize(0, 100))
self.tableWidget_boxes.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
self.tableWidget_boxes.setEditTriggers(QAbstractItemView.NoEditTriggers)
self.tableWidget_boxes.setTabKeyNavigation(False)
@@ -461,16 +462,15 @@ def setupUi(self, MainWindow):
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")
sizePolicy3.setHeightForWidth(self.tab_textFiles.sizePolicy().hasHeightForWidth())
self.tab_textFiles.setSizePolicy(sizePolicy3)
self.formLayout_2 = QFormLayout(self.tab_textFiles)
self.formLayout_2.setObjectName(u"formLayout_2")
- self.formLayout_2.setFieldGrowthPolicy(QFormLayout.ExpandingFieldsGrow)
- self.formLayout_2.setLabelAlignment(Qt.AlignRight|Qt.AlignTop|Qt.AlignTrailing)
- self.formLayout_2.setFormAlignment(Qt.AlignHCenter|Qt.AlignTop)
- self.formLayout_2.setVerticalSpacing(6)
+ self.formLayout_2.setVerticalSpacing(2)
+ self.formLayout_2.setContentsMargins(-1, -1, -1, 0)
self.label_7 = QLabel(self.tab_textFiles)
self.label_7.setObjectName(u"label_7")
@@ -559,63 +559,65 @@ def setupUi(self, MainWindow):
self.tab_browser.setObjectName(u"tab_browser")
self.verticalLayout_4 = QVBoxLayout(self.tab_browser)
self.verticalLayout_4.setObjectName(u"verticalLayout_4")
- self.label_serverRunning = QLabel(self.tab_browser)
- self.label_serverRunning.setObjectName(u"label_serverRunning")
-
- self.verticalLayout_4.addWidget(self.label_serverRunning)
-
self.label_8 = QLabel(self.tab_browser)
self.label_8.setObjectName(u"label_8")
self.label_8.setTextFormat(Qt.RichText)
self.label_8.setOpenExternalLinks(True)
self.label_8.setTextInteractionFlags(Qt.LinksAccessibleByMouse|Qt.TextSelectableByMouse)
- self.verticalLayout_4.addWidget(self.label_8)
+ self.verticalLayout_4.addWidget(self.label_8, 0, Qt.AlignTop)
self.tabWidget_outputs.addTab(self.tab_browser, "")
self.tab_obs = QWidget()
self.tab_obs.setObjectName(u"tab_obs")
self.gridLayout_2 = QGridLayout(self.tab_obs)
self.gridLayout_2.setObjectName(u"gridLayout_2")
- self.lineEdit_sceneName = QLineEdit(self.tab_obs)
+ 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)
+ self.pushButton_connectObs.setMinimumSize(QSize(0, 0))
+
+ self.gridLayout_2.addWidget(self.pushButton_connectObs, 0, 0, 1, 1)
+
+ self.widget_18 = QWidget(self.tab_obs)
+ self.widget_18.setObjectName(u"widget_18")
+ self.horizontalLayout_22 = QHBoxLayout(self.widget_18)
+ self.horizontalLayout_22.setObjectName(u"horizontalLayout_22")
+ self.lineEdit_sceneName = QLineEdit(self.widget_18)
self.lineEdit_sceneName.setObjectName(u"lineEdit_sceneName")
self.lineEdit_sceneName.setEnabled(False)
- self.gridLayout_2.addWidget(self.lineEdit_sceneName, 1, 0, 1, 1)
+ self.horizontalLayout_22.addWidget(self.lineEdit_sceneName)
- self.checkBox_recreate = QCheckBox(self.tab_obs)
+ self.pushButton_createOBSScene = QPushButton(self.widget_18)
+ self.pushButton_createOBSScene.setObjectName(u"pushButton_createOBSScene")
+ self.pushButton_createOBSScene.setEnabled(False)
+
+ self.horizontalLayout_22.addWidget(self.pushButton_createOBSScene)
+
+ self.checkBox_recreate = QCheckBox(self.widget_18)
self.checkBox_recreate.setObjectName(u"checkBox_recreate")
self.checkBox_recreate.setEnabled(False)
self.checkBox_recreate.setChecked(True)
- self.gridLayout_2.addWidget(self.checkBox_recreate, 2, 0, 1, 1)
+ self.horizontalLayout_22.addWidget(self.checkBox_recreate)
- self.pushButton_createOBSScene = QPushButton(self.tab_obs)
- self.pushButton_createOBSScene.setObjectName(u"pushButton_createOBSScene")
- self.pushButton_createOBSScene.setEnabled(False)
-
- self.gridLayout_2.addWidget(self.pushButton_createOBSScene, 3, 0, 1, 1)
-
- 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)
- self.pushButton_connectObs.setMinimumSize(QSize(0, 40))
- self.gridLayout_2.addWidget(self.pushButton_connectObs, 0, 0, 1, 1)
+ self.gridLayout_2.addWidget(self.widget_18, 1, 0, 1, 1, Qt.AlignTop)
self.tabWidget_outputs.addTab(self.tab_obs, "")
self.tab_vmix = QWidget()
self.tab_vmix.setObjectName(u"tab_vmix")
self.gridLayout_3 = QGridLayout(self.tab_vmix)
self.gridLayout_3.setObjectName(u"gridLayout_3")
+ self.gridLayout_3.setVerticalSpacing(2)
self.tableView_vmixMapping = QTableView(self.tab_vmix)
self.tableView_vmixMapping.setObjectName(u"tableView_vmixMapping")
- sizePolicy2.setHeightForWidth(self.tableView_vmixMapping.sizePolicy().hasHeightForWidth())
- self.tableView_vmixMapping.setSizePolicy(sizePolicy2)
self.tableView_vmixMapping.horizontalHeader().setVisible(False)
self.tableView_vmixMapping.horizontalHeader().setStretchLastSection(True)
@@ -626,23 +628,6 @@ def setupUi(self, MainWindow):
self.horizontalLayout_19 = QHBoxLayout(self.widget_16)
self.horizontalLayout_19.setObjectName(u"horizontalLayout_19")
self.horizontalLayout_19.setContentsMargins(0, 0, 0, 0)
- self.label_6 = QLabel(self.widget_16)
- self.label_6.setObjectName(u"label_6")
-
- self.horizontalLayout_19.addWidget(self.label_6)
-
- self.pushButton_startvmix = QPushButton(self.widget_16)
- self.pushButton_startvmix.setObjectName(u"pushButton_startvmix")
- sizePolicy7 = QSizePolicy(QSizePolicy.Policy.Fixed, QSizePolicy.Policy.Fixed)
- sizePolicy7.setHorizontalStretch(0)
- sizePolicy7.setVerticalStretch(0)
- sizePolicy7.setHeightForWidth(self.pushButton_startvmix.sizePolicy().hasHeightForWidth())
- self.pushButton_startvmix.setSizePolicy(sizePolicy7)
- self.pushButton_startvmix.setCheckable(True)
- self.pushButton_startvmix.setChecked(False)
-
- self.horizontalLayout_19.addWidget(self.pushButton_startvmix)
-
self.gridLayout_3.addWidget(self.widget_16, 1, 0, 1, 1)
@@ -674,12 +659,24 @@ 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)
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)
+ self.pushButton_startvmix.setCheckable(True)
+ self.pushButton_startvmix.setChecked(False)
+
+ self.horizontalLayout_18.addWidget(self.pushButton_startvmix)
+
self.formLayout_3.setWidget(0, QFormLayout.FieldRole, self.connectionWidget)
@@ -702,7 +699,7 @@ def setupUi(self, MainWindow):
self.pushButton_stopUpdates = QPushButton(self.frame)
self.pushButton_stopUpdates.setObjectName(u"pushButton_stopUpdates")
- self.pushButton_stopUpdates.setMinimumSize(QSize(0, 40))
+ self.pushButton_stopUpdates.setMinimumSize(QSize(0, 0))
self.pushButton_stopUpdates.setCheckable(True)
self.verticalLayout.addWidget(self.pushButton_stopUpdates)
@@ -903,7 +900,7 @@ def setupUi(self, MainWindow):
self.retranslateUi(MainWindow)
self.comboBox_formatPrefix.setCurrentIndex(0)
- self.tabWidget_outputs.setCurrentIndex(0)
+ self.tabWidget_outputs.setCurrentIndex(3)
QMetaObject.connectSlotsByName(MainWindow)
@@ -986,20 +983,18 @@ 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_serverRunning.setText(QCoreApplication.translate("MainWindow", u"Server is running.", 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.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))
- self.checkBox_recreate.setText(QCoreApplication.translate("MainWindow", u"Recreate if exists", None))
self.pushButton_createOBSScene.setText(QCoreApplication.translate("MainWindow", u"Create OBS Scene", None))
- self.pushButton_connectObs.setText(QCoreApplication.translate("MainWindow", u"Connect OBS", None))
+ self.checkBox_recreate.setText(QCoreApplication.translate("MainWindow", u"Recreate", None))
self.tabWidget_outputs.setTabText(self.tabWidget_outputs.indexOf(self.tab_obs), QCoreApplication.translate("MainWindow", u"OBS", None))
- self.label_6.setText(QCoreApplication.translate("MainWindow", u"Output Mapping", None))
- self.pushButton_startvmix.setText(QCoreApplication.translate("MainWindow", u"Start", None))
self.connectionLabel.setText(QCoreApplication.translate("MainWindow", u"Connection", None))
self.lineEdit_vmixHost.setText(QCoreApplication.translate("MainWindow", u"localhost", None))
self.label_5.setText(QCoreApplication.translate("MainWindow", u":", None))
self.lineEdit_vmixPort.setText(QCoreApplication.translate("MainWindow", u"8099", None))
+ self.pushButton_startvmix.setText(QCoreApplication.translate("MainWindow", u"Start", None))
self.vmixinputLabel.setText(QCoreApplication.translate("MainWindow", u"Input", None))
self.inputLineEdit_vmix.setText(QCoreApplication.translate("MainWindow", u"1", None))
self.tabWidget_outputs.setTabText(self.tabWidget_outputs.indexOf(self.tab_vmix), QCoreApplication.translate("MainWindow", u"VMix", None))
From d5e4028fe50e966938a6e16eec366fff2294017e Mon Sep 17 00:00:00 2001
From: Roy Shilkrot
Date: Wed, 22 May 2024 23:09:00 -0400
Subject: [PATCH 2/2] Add 'frame_stabilizer.py' to the project
---
scoresight.spec | 1 +
1 file changed, 1 insertion(+)
diff --git a/scoresight.spec b/scoresight.spec
index e55bb1e..3b37540 100644
--- a/scoresight.spec
+++ b/scoresight.spec
@@ -42,6 +42,7 @@ a = Analysis(
'camera_view.py',
'defaults.py',
'file_output.py',
+ 'frame_stabilizer.py',
'get_camera_info.py',
'http_server.py',
'log_view.py',