Add main application structure and logging configuration
- Implement main application entry point in duck-ocr.py - Create logging configuration in logging_config.py - Add video streaming functionality in camera.py - Introduce main window UI in main_window.py - Include SVG assets for UI buttons and icons - Update .gitignore to exclude log files - Add placeholder .gitkeep files for empty directories
This commit is contained in:
148
app/camera.py
Normal file
148
app/camera.py
Normal file
@@ -0,0 +1,148 @@
|
||||
from PySide6.QtCore import QThread, QTimer, QMutex, QWaitCondition, Signal, QObject, QElapsedTimer
|
||||
import cv2
|
||||
import time
|
||||
import logging
|
||||
from dataclasses import dataclass
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@dataclass
|
||||
class VideoMetrics:
|
||||
frames_processed: int
|
||||
frames_dropped: int
|
||||
last_frame_time: float
|
||||
last_cap_time: float
|
||||
fps_average: float
|
||||
fps_last_time: float
|
||||
fps_frame_count: float
|
||||
|
||||
def update_fps(self) -> None:
|
||||
fps_now = time.perf_counter()
|
||||
elypsed = fps_now - self.fps_last_time
|
||||
if elypsed < 1.0:
|
||||
return
|
||||
|
||||
self.fps_frame_count = self.frames_processed - self.fps_frame_count
|
||||
self.fps_average = self.fps_frame_count / elypsed
|
||||
self.fps_frame_count = self.frames_processed
|
||||
self.fps_last_time = fps_now
|
||||
|
||||
|
||||
class VideoStreamWorker(QObject):
|
||||
# Emitowany z wątku roboczego - będzie przenoszony do głównego wątku przez VideoStream
|
||||
_internal_frame = Signal(object)
|
||||
|
||||
def __init__(self, source):
|
||||
super().__init__()
|
||||
self.source = source
|
||||
self.cap = None
|
||||
self.fps = 30.0
|
||||
self.width = 0
|
||||
self.height = 0
|
||||
self.running = False
|
||||
self.metrics = VideoMetrics(
|
||||
frames_processed=0,
|
||||
frames_dropped=0,
|
||||
last_frame_time=0.0,
|
||||
last_cap_time=0.0,
|
||||
fps_average=0.0,
|
||||
fps_last_time=time.perf_counter(),
|
||||
fps_frame_count=0.0
|
||||
)
|
||||
|
||||
logger.debug(f"VideoStreamWorker initialized with source: {source}")
|
||||
|
||||
def set_source(self, source):
|
||||
logger.debug(f"Setting new video source: {source}")
|
||||
self.source = source
|
||||
if self.running:
|
||||
self.stop()
|
||||
self.run()
|
||||
|
||||
def run(self):
|
||||
"""Główna pętla wątku roboczego."""
|
||||
if self.source is None:
|
||||
logger.warning("No video source provided")
|
||||
return
|
||||
|
||||
self.cap = cv2.VideoCapture(self.source)
|
||||
if not self.cap.isOpened():
|
||||
logger.error(f"Failed to open video source: {self.source}")
|
||||
return
|
||||
|
||||
self.fps = self.cap.get(cv2.CAP_PROP_FPS)
|
||||
if self.fps <= 0:
|
||||
self.fps = 30.0
|
||||
|
||||
self.width = int(self.cap.get(cv2.CAP_PROP_FRAME_WIDTH))
|
||||
self.height = int(self.cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
|
||||
|
||||
logger.debug(f"Video opened: {self.source} (fps: {self.fps}, size: {self.width}x{self.height})")
|
||||
|
||||
frame_interval = 1.0 / self.fps
|
||||
self.running = True
|
||||
frame_emit_time = time.perf_counter()
|
||||
|
||||
while self.running:
|
||||
next_frame_time = frame_emit_time + frame_interval
|
||||
|
||||
ret, frame = self.cap.read()
|
||||
|
||||
if not ret:
|
||||
logger.debug("End of video stream or read error")
|
||||
break
|
||||
|
||||
current_time = time.perf_counter()
|
||||
self.metrics.last_cap_time = current_time - frame_emit_time
|
||||
|
||||
if current_time < next_frame_time:
|
||||
sleep_time = next_frame_time - current_time
|
||||
time.sleep(sleep_time)
|
||||
else:
|
||||
self.metrics.frames_dropped += 1
|
||||
logger.debug(f"Frame drops counted: {self.metrics.frames_dropped}")
|
||||
|
||||
frame_emit_time = time.perf_counter()
|
||||
self._internal_frame.emit(frame)
|
||||
|
||||
self.metrics.frames_processed += 1
|
||||
self.metrics.update_fps()
|
||||
|
||||
self.cap.release()
|
||||
|
||||
def stop(self):
|
||||
self.running = False
|
||||
self.mutex.lock()
|
||||
self.condition.wakeAll()
|
||||
self.mutex.unlock()
|
||||
|
||||
|
||||
class VideoStream(QObject):
|
||||
"""Klasa fasadowa do użycia w głównym wątku GUI."""
|
||||
frame_ready = Signal(object) # To będzie emitowane z głównego wątku
|
||||
|
||||
def __init__(self, source):
|
||||
super().__init__()
|
||||
self.worker = VideoStreamWorker(source)
|
||||
self.worker_thread = QThread()
|
||||
|
||||
# Przenosimy workera do osobnego wątku
|
||||
self.worker.moveToThread(self.worker_thread)
|
||||
|
||||
# Łączymy sygnały
|
||||
self.worker._internal_frame.connect(self._on_frame)
|
||||
self.worker_thread.started.connect(self.worker.run)
|
||||
self.worker_thread.finished.connect(self.worker.deleteLater)
|
||||
self.worker_thread.finished.connect(self.worker_thread.deleteLater)
|
||||
|
||||
def start(self):
|
||||
self.worker_thread.start()
|
||||
|
||||
def stop(self):
|
||||
self.worker.stop()
|
||||
self.worker_thread.quit()
|
||||
self.worker_thread.wait()
|
||||
|
||||
def _on_frame(self, frame):
|
||||
"""Slot wywoływany w głównym wątku po otrzymaniu klatki."""
|
||||
# Tutaj możesz dodać korekcję ekspozycji jeśli nie zrobiłeś tego w workerze
|
||||
self.frame_ready.emit(frame)
|
||||
46
app/logging_config.py
Normal file
46
app/logging_config.py
Normal file
@@ -0,0 +1,46 @@
|
||||
# logging_config.py
|
||||
|
||||
import logging
|
||||
import logging.handlers
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
LOG_DIR = Path("logs")
|
||||
LOG_DIR.mkdir(exist_ok=True)
|
||||
|
||||
LOG_FILE = LOG_DIR / "app.log"
|
||||
|
||||
|
||||
def setup_logging():
|
||||
formatter = logging.Formatter(
|
||||
fmt="%(asctime)s | %(levelname)-8s | %(threadName)s | %(name)s | %(message)s",
|
||||
datefmt="%Y-%m-%d %H:%M:%S",
|
||||
)
|
||||
|
||||
# logger główny
|
||||
logger = logging.getLogger()
|
||||
|
||||
logger.setLevel(logging.DEBUG)
|
||||
|
||||
# zabezpieczenie przed dodaniem handlerów drugi raz
|
||||
if logger.handlers:
|
||||
return
|
||||
|
||||
# console
|
||||
console_handler = logging.StreamHandler()
|
||||
console_handler.setLevel(logging.INFO)
|
||||
console_handler.setFormatter(formatter)
|
||||
|
||||
# plik
|
||||
file_handler = logging.handlers.RotatingFileHandler(
|
||||
LOG_FILE,
|
||||
maxBytes=5_000_000,
|
||||
backupCount=3,
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
file_handler.setLevel(logging.DEBUG)
|
||||
file_handler.setFormatter(formatter)
|
||||
|
||||
logger.addHandler(console_handler)
|
||||
logger.addHandler(file_handler)
|
||||
188
app/main_window.py
Normal file
188
app/main_window.py
Normal file
@@ -0,0 +1,188 @@
|
||||
from enum import Enum
|
||||
from typing import Any
|
||||
import logging
|
||||
|
||||
from PySide6.QtWidgets import QGridLayout, QHBoxLayout, QMainWindow, QStyle, QToolButton, QWidget, QVBoxLayout, QPushButton
|
||||
from PySide6.QtGui import QIcon
|
||||
from PySide6.QtCore import Qt, QSize
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class VideoMode(Enum):
|
||||
STREAMING = "streaming"
|
||||
RECORDING = "recording"
|
||||
PLAYING = "playing"
|
||||
STOPPED = "stopped"
|
||||
|
||||
icons = {
|
||||
"load_file":{
|
||||
"path": "assets/folder-svgrepo-com.svg",
|
||||
"tooltip": "Open File",
|
||||
"size": QSize(48, 48)
|
||||
},
|
||||
"close_file": {
|
||||
"path": "assets/close-square-svgrepo-com.svg",
|
||||
"tooltip": "Close File",
|
||||
"size": QSize(32, 32)
|
||||
},
|
||||
"record_on": {
|
||||
"path": "assets/record_btn_on.svg",
|
||||
"tooltip": "Start Recording",
|
||||
"size": QSize(48, 48)
|
||||
},
|
||||
"record_off": {
|
||||
"path": "assets/record_btn_off.svg",
|
||||
"tooltip": "Stop Recording",
|
||||
"size": QSize(48, 48)
|
||||
},
|
||||
|
||||
"play": {
|
||||
"path": "assets/play-svgrepo-com.svg",
|
||||
"tooltip": "Play Video",
|
||||
"size": QSize(48, 48)
|
||||
},
|
||||
"pause": {
|
||||
"path": "assets/pause-svgrepo-com.svg",
|
||||
"tooltip": "Pause Video",
|
||||
"size": QSize(32, 32)
|
||||
}
|
||||
}
|
||||
|
||||
def set_icon(button: QToolButton, icon: str):
|
||||
button.setIcon(QIcon(icons[icon]["path"]))
|
||||
button.setToolTip(icons[icon]["tooltip"])
|
||||
button.setIconSize(icons[icon]["size"])
|
||||
|
||||
|
||||
def create_tool_button(icon: str) -> QToolButton:
|
||||
button = QToolButton()
|
||||
set_icon(button, icon)
|
||||
button.setFixedSize(48, 48)
|
||||
button.setStyleSheet("background-color: transparent; border: none;")
|
||||
return button
|
||||
|
||||
class MainWindow(QMainWindow):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.setWindowTitle("Duck Stain OCR")
|
||||
self.resize(1280, 720)
|
||||
|
||||
self.video_mode = VideoMode.STREAMING
|
||||
logger.debug(f"Initial video mode: {self.video_mode}")
|
||||
|
||||
self.setup_ui()
|
||||
|
||||
def setup_ui(self):
|
||||
self.central_widget = QWidget()
|
||||
self.setCentralWidget(self.central_widget)
|
||||
self.central_widget.setStyleSheet("background-color: #001e1e;")
|
||||
|
||||
self.toolbar_widget = QWidget(self.central_widget)
|
||||
self.toolbar_widget.setMinimumWidth(400)
|
||||
self.toolbar_widget.setObjectName("bottomToolbar")
|
||||
self.toolbar_widget.setStyleSheet(
|
||||
"""
|
||||
QWidget#bottomToolbar {
|
||||
background: rgba(0, 0, 0, 0.1);
|
||||
border-radius: 18px;
|
||||
}
|
||||
"""
|
||||
)
|
||||
|
||||
left_layout = QHBoxLayout()
|
||||
right_layout = QHBoxLayout()
|
||||
|
||||
self.toolbar_layout = QGridLayout(self.toolbar_widget)
|
||||
self.toolbar_layout.setContentsMargins(18, 16, 18, 16)
|
||||
self.toolbar_layout.setColumnStretch(0, 1)
|
||||
self.toolbar_layout.setColumnStretch(1, 0)
|
||||
self.toolbar_layout.setColumnStretch(2, 1)
|
||||
|
||||
self.toolbar_layout.addLayout(left_layout, 0, 0)
|
||||
self.toolbar_layout.addLayout(right_layout, 0, 2)
|
||||
|
||||
# Add buttons to the left layout
|
||||
self.open_file_button = create_tool_button("load_file")
|
||||
self.open_file_button.clicked.connect(self.load_video)
|
||||
left_layout.addWidget(self.open_file_button)
|
||||
self.close_file_button = create_tool_button("close_file")
|
||||
self.close_file_button.clicked.connect(self.close_video)
|
||||
self.close_file_button.setEnabled(False)
|
||||
left_layout.addWidget(self.close_file_button)
|
||||
|
||||
left_layout.addStretch()
|
||||
|
||||
# Add the record button to the center of the toolbar
|
||||
self.action_button = create_tool_button("record_off")
|
||||
self.action_button.clicked.connect(self.toggle_action)
|
||||
self.toolbar_layout.addWidget(self.action_button, 0, 1, alignment=Qt.AlignmentFlag.AlignCenter)
|
||||
|
||||
# Add buttons to the right layout
|
||||
right_layout.addStretch()
|
||||
|
||||
|
||||
def resizeEvent(self, event: Any) -> None:
|
||||
super().resizeEvent(event)
|
||||
|
||||
self.toolbar_widget.adjustSize()
|
||||
toolbar_size = self.toolbar_widget.sizeHint()
|
||||
self.toolbar_widget.setGeometry(
|
||||
(self.central_widget.width() - self.toolbar_widget.width()) // 2,
|
||||
self.central_widget.height() - self.toolbar_widget.height() - 80,
|
||||
self.toolbar_widget.width(),
|
||||
self.toolbar_widget.height(),
|
||||
)
|
||||
self.toolbar_widget.raise_() # Ensure the toolbar is above other widgets
|
||||
|
||||
def toggle_action(self):
|
||||
match self.video_mode:
|
||||
case VideoMode.STREAMING:
|
||||
self.start_recording()
|
||||
case VideoMode.RECORDING:
|
||||
self.stop_recording()
|
||||
case VideoMode.PLAYING:
|
||||
self.pause_video()
|
||||
case VideoMode.STOPPED:
|
||||
self.play_video()
|
||||
case _:
|
||||
pass
|
||||
|
||||
def start_recording(self):
|
||||
logger.debug("Starting recording")
|
||||
self.video_mode = VideoMode.RECORDING
|
||||
set_icon(self.action_button, "record_on")
|
||||
|
||||
self.open_file_button.setEnabled(False)
|
||||
self.close_file_button.setEnabled(False)
|
||||
|
||||
def stop_recording(self):
|
||||
logger.debug("Stopping recording")
|
||||
self.video_mode = VideoMode.STREAMING
|
||||
set_icon(self.action_button, "record_off")
|
||||
|
||||
self.open_file_button.setEnabled(True)
|
||||
self.close_file_button.setEnabled(False)
|
||||
|
||||
def load_video(self):
|
||||
logger.debug("Loading video")
|
||||
self.video_mode = VideoMode.STOPPED
|
||||
set_icon(self.action_button, "play")
|
||||
|
||||
self.close_file_button.setEnabled(True)
|
||||
|
||||
def close_video(self):
|
||||
logger.debug("Closing video")
|
||||
self.video_mode = VideoMode.STREAMING
|
||||
set_icon(self.action_button, "record_off")
|
||||
|
||||
self.close_file_button.setEnabled(False)
|
||||
|
||||
def play_video(self):
|
||||
logger.debug("Playing video")
|
||||
self.video_mode = VideoMode.PLAYING
|
||||
set_icon(self.action_button, "pause")
|
||||
|
||||
def pause_video(self):
|
||||
logger.debug("Pausing video")
|
||||
self.video_mode = VideoMode.STOPPED
|
||||
set_icon(self.action_button, "play")
|
||||
Reference in New Issue
Block a user