Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add rotation functionality for camera view #35

Merged
merged 4 commits into from
Sep 18, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 31 additions & 3 deletions camera_view.py
Original file line number Diff line number Diff line change
Expand Up @@ -81,18 +81,20 @@ def set_camera_highest_resolution(cap):
set_resolution(cap, *highest_res)


class FrameCrop:
class FrameCropAndRotation:
def __init__(self):
self.isCropSet = fetch_data("scoresight.json", "crop_mode", False)
self.cropTop = fetch_data("scoresight.json", "top_crop", 0)
self.cropBottom = fetch_data("scoresight.json", "bottom_crop", 0)
self.cropLeft = fetch_data("scoresight.json", "left_crop", 0)
self.cropRight = fetch_data("scoresight.json", "right_crop", 0)
self.rotation = fetch_data("scoresight.json", "rotation", 0)
subscribe_to_data("scoresight.json", "crop_mode", self.setCropMode)
subscribe_to_data("scoresight.json", "top_crop", self.setCropTop)
subscribe_to_data("scoresight.json", "bottom_crop", self.setCropBottom)
subscribe_to_data("scoresight.json", "left_crop", self.setCropLeft)
subscribe_to_data("scoresight.json", "right_crop", self.setCropRight)
subscribe_to_data("scoresight.json", "rotation", self.setRotation)

def setCropMode(self, crop_mode):
self.isCropSet = crop_mode
Expand All @@ -109,6 +111,9 @@ def setCropLeft(self, crop_left):
def setCropRight(self, crop_right):
self.cropRight = crop_right

def setRotation(self, rotation):
self.rotation = rotation


class TimerThread(QThread):
update_signal = Signal(object)
Expand All @@ -134,14 +139,22 @@ def __init__(
self.video_capture = None
self.should_stop = False
self.frame_interval = 30
self.update_frame_interval = 200
self.update_frame_interval = 1000 / fetch_data(
"scoresight.json", "detection_cadence", 5
)
subscribe_to_data(
"scoresight.json", "detection_cadence", self.setUpdateFrameInterval
)
self.preview_frame_interval = 1000
self.fps = 1000 / self.frame_interval # frames per second
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
self.crop = FrameCrop()
self.crop = FrameCropAndRotation()

def setUpdateFrameInterval(self, cadence):
self.update_frame_interval = 1000 / cadence

def connect_video_capture(self) -> bool:
if self.camera_info.type == CameraInfo.CameraType.NDI:
Expand All @@ -153,6 +166,7 @@ def connect_video_capture(self) -> bool:
if (
os_name == "Windows"
and self.camera_info.type != CameraInfo.CameraType.FILE
and self.camera_info.type != CameraInfo.CameraType.URL
):
# on windows use the dshow backend
self.video_capture = cv2.VideoCapture(
Expand Down Expand Up @@ -278,6 +292,20 @@ def run(self):
+ (1.0 - self.fps_alpha) * self.ups
)

# apply rotation if set
if self.crop.rotation != 0:
# use cv2.rotate to rotate the frame
rotateCode = (
cv2.ROTATE_90_CLOCKWISE
if self.crop.rotation == 90
else (
cv2.ROTATE_90_COUNTERCLOCKWISE
if self.crop.rotation == 270
else cv2.ROTATE_180
)
)
frame_rgb = cv2.rotate(frame_rgb, rotateCode)

# apply top-level crop if set
if self.crop.isCropSet:
frame_rgb = frame_rgb[
Expand Down
50 changes: 32 additions & 18 deletions main.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,13 @@
import datetime
from PySide6.QtWidgets import (
QApplication,
QMainWindow,
QDialog,
QFileDialog,
QInputDialog,
QLabel,
QMainWindow,
QMenu,
QDialog,
QInputDialog,
QMessageBox,
QTableWidgetItem,
)
from PySide6.QtGui import QIcon, QStandardItemModel, QStandardItem, QDesktopServices
Expand Down Expand Up @@ -145,7 +146,6 @@ def __init__(self, translator: QTranslator, parent: QObject):
self.installEventFilter(self)

self.ui.pushButton_connectObs.clicked.connect(self.openOBSConnectModal)
self.ui.statusbar.showMessage("OBS: Not Connected")

self.vmixUiSetup()

Expand Down Expand Up @@ -181,6 +181,15 @@ def __init__(self, translator: QTranslator, parent: QObject):
self.ui.spinBox_bottomCrop.setValue(
fetch_data("scoresight.json", "bottom_crop", 0)
)
self.ui.checkBox_enableOutAPI.toggled.connect(
partial(self.globalSettingsChanged, "enable_out_api")
)
self.ui.checkBox_enableOutAPI.setChecked(
fetch_data("scoresight.json", "enable_out_api", False)
)

# connect toolButton_rotate
self.ui.toolButton_rotate.clicked.connect(self.rotateImage)

self.ui.widget_detectionCadence.setVisible(True)
self.ui.horizontalSlider_detectionCadence.setValue(
Expand Down Expand Up @@ -370,6 +379,14 @@ def __init__(self, translator: QTranslator, parent: QObject):
self.get_sources.connect(self.getSources)
self.get_sources.emit()

def rotateImage(self):
# store the rotation in the scoresight.json
rotation = fetch_data("scoresight.json", "rotation", 0)
rotation += 90
if rotation >= 360:
rotation = 0
self.globalSettingsChanged("rotation", rotation)

def cropMode(self):
# if the toolButton_topCrop is unchecked, go to crop mode
if self.ui.toolButton_topCrop.isChecked():
Expand Down Expand Up @@ -441,8 +458,15 @@ def importConfiguration(self):
return
# load the configuration from the file
if not self.detectionTargetsStorage.loadBoxesFromFile(file):
# show an error message
self.ui.statusbar.showMessage("Error loading configuration file")
# show an error qmessagebox
logger.error("Error loading configuration file")
QMessageBox.critical(
self,
"Error",
"Error loading configuration file",
QMessageBox.StandardButton.Ok,
)
return

def exportConfiguration(self):
# open a file dialog to select the output file
Expand Down Expand Up @@ -516,11 +540,6 @@ def toggleStabilize(self):
self.image_viewer.toggleStabilization(self.ui.pushButton_stabilize.isChecked())

def toggleStopUpdates(self, value):
self.ui.statusbar.showMessage(
self.translator.translate("main", "Stopped updates")
if value
else self.translator.translate("main", "Resumed updates")
)
self.updateOCRResults = not value
# change the text on the button
self.ui.pushButton_stopUpdates.setText(
Expand Down Expand Up @@ -969,8 +988,7 @@ def connectObs(self):
self.ui.checkBox_recreate.setEnabled(True)
self.ui.pushButton_createOBSScene.setEnabled(True)

# set OBS status to connected in the status bar
self.ui.statusbar.showMessage("OBS: Connected")
logger.info("OBS: Connected")

@Slot()
def getSources(self):
Expand Down Expand Up @@ -1221,10 +1239,8 @@ def cameraConnectedEnableUI(self):

def updateError(self, error):
if not error:
self.ui.statusbar.clearMessage()
return
# show the error in the status bar
self.ui.statusbar.showMessage(error)
logger.error(error)
self.ui.frame_source_view.setEnabled(True)
self.ui.widget_viewTools.setEnabled(False)

Expand Down Expand Up @@ -1485,12 +1501,10 @@ def makeTemplateField(self, toggled: bool):
self.listItemClicked(item)

def createOBSScene(self):
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")

# on destroy, close the OBS connection
def closeEvent(self, event):
Expand Down
78 changes: 49 additions & 29 deletions mainwindow.ui
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
<x>0</x>
<y>0</y>
<width>961</width>
<height>734</height>
<height>725</height>
</rect>
</property>
<property name="windowTitle">
Expand Down Expand Up @@ -1439,28 +1439,21 @@
<property name="fieldGrowthPolicy">
<enum>QFormLayout::AllNonFixedFieldsGrow</enum>
</property>
<item row="1" column="0">
<widget class="QLabel" name="label_20">
<item row="0" column="1">
<widget class="QCheckBox" name="checkBox_enableOutAPI">
<property name="text">
<string>URL</string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QLineEdit" name="lineEdit_api_url">
<property name="placeholderText">
<string>http://</string>
<string>Send out API requests to external services.</string>
</property>
</widget>
</item>
<item row="3" column="0">
<item row="4" column="0">
<widget class="QLabel" name="label_21">
<property name="text">
<string>Encode</string>
</property>
</widget>
</item>
<item row="3" column="1">
<item row="4" column="1">
<widget class="QComboBox" name="comboBox_api_encode">
<item>
<property name="text">
Expand All @@ -1484,13 +1477,6 @@
</item>
</widget>
</item>
<item row="2" column="1">
<widget class="QCheckBox" name="checkBox_is_websocket">
<property name="text">
<string>Websocket?</string>
</property>
</widget>
</item>
<item row="5" column="1">
<spacer name="verticalSpacer">
<property name="orientation">
Expand All @@ -1504,17 +1490,45 @@
</property>
</spacer>
</item>
<item row="4" column="1">
<widget class="QPushButton" name="pushButton_api_start">
<property name="text">
<string>Start</string>
</property>
<item row="1" column="1">
<widget class="QWidget" name="widget_24" native="true">
<layout class="QHBoxLayout" name="horizontalLayout_27">
<property name="spacing">
<number>2</number>
</property>
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item>
<widget class="QLineEdit" name="lineEdit_api_url">
<property name="placeholderText">
<string>http://</string>
</property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="checkBox_is_websocket">
<property name="text">
<string>Websocket</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item row="0" column="1">
<widget class="QLabel" name="label_22">
<item row="1" column="0">
<widget class="QLabel" name="label_20">
<property name="text">
<string>Send out API requests to external services.</string>
<string>URL</string>
</property>
</widget>
</item>
Expand Down Expand Up @@ -1802,6 +1816,13 @@
</property>
</widget>
</item>
<item>
<widget class="QToolButton" name="toolButton_rotate">
<property name="text">
<string>Rotate</string>
</property>
</widget>
</item>
<item>
<widget class="QToolButton" name="pushButton_stabilize">
<property name="enabled">
Expand Down Expand Up @@ -2093,7 +2114,6 @@
</rect>
</property>
</widget>
<widget class="QStatusBar" name="statusbar"/>
</widget>
<resources/>
<connections/>
Expand Down
Loading