diff --git a/.gitignore b/.gitignore
index 7e7acbf..4d01637 100644
--- a/.gitignore
+++ b/.gitignore
@@ -29,3 +29,4 @@ captures/videos/*
runs/
*.onnx
*.engine
+*.log
\ No newline at end of file
diff --git a/app/camera.py b/app/camera.py
new file mode 100644
index 0000000..729aaf2
--- /dev/null
+++ b/app/camera.py
@@ -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)
\ No newline at end of file
diff --git a/app/logging_config.py b/app/logging_config.py
new file mode 100644
index 0000000..00cd319
--- /dev/null
+++ b/app/logging_config.py
@@ -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)
\ No newline at end of file
diff --git a/app/main_window.py b/app/main_window.py
new file mode 100644
index 0000000..d93f146
--- /dev/null
+++ b/app/main_window.py
@@ -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")
\ No newline at end of file
diff --git a/assets/circle.svg b/assets/circle.svg
new file mode 100644
index 0000000..398da57
--- /dev/null
+++ b/assets/circle.svg
@@ -0,0 +1,12 @@
+
\ No newline at end of file
diff --git a/assets/close-square-svgrepo-com.svg b/assets/close-square-svgrepo-com.svg
new file mode 100644
index 0000000..30c965f
--- /dev/null
+++ b/assets/close-square-svgrepo-com.svg
@@ -0,0 +1,5 @@
+
+
\ No newline at end of file
diff --git a/assets/folder-svgrepo-com.svg b/assets/folder-svgrepo-com.svg
new file mode 100644
index 0000000..cc2c153
--- /dev/null
+++ b/assets/folder-svgrepo-com.svg
@@ -0,0 +1,4 @@
+
+
\ No newline at end of file
diff --git a/assets/pause-svgrepo-com.svg b/assets/pause-svgrepo-com.svg
new file mode 100644
index 0000000..15a0152
--- /dev/null
+++ b/assets/pause-svgrepo-com.svg
@@ -0,0 +1,5 @@
+
+
\ No newline at end of file
diff --git a/assets/play-svgrepo-com.svg b/assets/play-svgrepo-com.svg
new file mode 100644
index 0000000..0f6ed67
--- /dev/null
+++ b/assets/play-svgrepo-com.svg
@@ -0,0 +1,4 @@
+
+
\ No newline at end of file
diff --git a/assets/record_btn_off.svg b/assets/record_btn_off.svg
new file mode 100644
index 0000000..398da57
--- /dev/null
+++ b/assets/record_btn_off.svg
@@ -0,0 +1,12 @@
+
\ No newline at end of file
diff --git a/assets/record_btn_on.svg b/assets/record_btn_on.svg
new file mode 100644
index 0000000..61f2f7b
--- /dev/null
+++ b/assets/record_btn_on.svg
@@ -0,0 +1,12 @@
+
\ No newline at end of file
diff --git a/captures/photos/.gitkeep b/captures/photos/.gitkeep
new file mode 100644
index 0000000..e69de29
diff --git a/captures/videos/.gitkeep b/captures/videos/.gitkeep
new file mode 100644
index 0000000..e69de29
diff --git a/duck-ocr.py b/duck-ocr.py
new file mode 100644
index 0000000..2e488ee
--- /dev/null
+++ b/duck-ocr.py
@@ -0,0 +1,21 @@
+import logging
+import sys
+
+from PySide6.QtWidgets import QApplication
+from app.main_window import MainWindow
+from app.logging_config import setup_logging
+
+setup_logging()
+
+logger = logging.getLogger(__name__)
+
+def main() -> None:
+ logger.info("Starting application")
+
+ app = QApplication(sys.argv)
+ main_window = MainWindow()
+ main_window.show()
+ sys.exit(app.exec())
+
+if __name__ == "__main__":
+ main()
\ No newline at end of file