Files
duck-preview/test_qcamera.py
2026-05-14 16:08:17 +02:00

363 lines
10 KiB
Python

import sys
import logging
from pathlib import Path
from PySide6.QtCore import Qt, QTimer
from PySide6.QtGui import QAction
from PySide6.QtWidgets import QApplication, QMainWindow, QLabel
from PySide6.QtMultimedia import (
QMediaDevices,
QCamera,
QCameraDevice,
QMediaFormat,
QVideoFrameFormat,
QMediaCaptureSession
)
from PySide6.QtMultimediaWidgets import QVideoWidget
# ============================================================
# LOGGER
# ============================================================
LOG_DIR = Path("logs")
LOG_DIR.mkdir(exist_ok=True)
logging.basicConfig(
level=logging.DEBUG,
format="%(asctime)s [%(levelname)s] %(message)s",
handlers=[
logging.FileHandler(LOG_DIR / "camera_debug.log", encoding="utf-8"),
logging.StreamHandler(sys.stdout),
],
)
logger = logging.getLogger("camera_app")
# ============================================================
# CAMERA WINDOW
# ============================================================
class CameraWindow(QMainWindow):
def __init__(self):
super().__init__()
self.setWindowTitle("Camera Preview")
# Widget wyświetlający wyłącznie obraz
self.video_widget = QVideoWidget()
self.setCentralWidget(self.video_widget)
self.camera = None
self.camera_device = None
self.capture_session = QMediaCaptureSession()
self.init_camera()
# ========================================================
# CAMERA INIT
# ========================================================
def init_camera(self):
devices = QMediaDevices.videoInputs()
logger.info("==========================================")
logger.info("VIDEO DEVICES ENUMERATION")
logger.info("==========================================")
if not devices:
logger.error("Nie znaleziono kamer.")
return
for idx, device in enumerate(devices):
self.log_camera_device(device, idx)
# wybór pierwszej kamery
self.camera_device = devices[0]
logger.info(f"Wybrano kamerę: {self.camera_device.description()}")
self.camera = QCamera(self.camera_device)
# sygnały
self.camera.errorOccurred.connect(self.on_camera_error)
self.camera.activeChanged.connect(self.on_active_changed)
# video sink
# self.camera.setVideoOutput(self.video_widget)
self.capture_session.setCamera(self.camera)
self.capture_session.setVideoOutput(self.video_widget)
# przykładowe ustawienia
self.set_camera_format(
width=1280,
height=720,
fps=30,
preferred_pixel_format=QVideoFrameFormat.PixelFormat.Format_Jpeg
)
self.log_runtime_camera_capabilities()
self.camera.start()
logger.info("Kamera uruchomiona.")
# ========================================================
# FORMAT SELECTION
# ========================================================
def set_camera_format(
self,
width: int,
height: int,
fps: int,
preferred_pixel_format=QVideoFrameFormat.PixelFormat.Format_Jpeg,
):
# def set_camera_format(self, width: int, height: int, fps: int):
"""
Prototyp funkcji ustawiającej:
- rozdzielczość
- FPS
Wybiera najbliższy pasujący format.
"""
logger.info(
f"Próba ustawienia formatu: {width}x{height} @ {fps} FPS"
)
best_match = None
for fmt in self.camera_device.videoFormats():
resolution = fmt.resolution()
min_fps = fmt.minFrameRate()
max_fps = fmt.maxFrameRate()
pixel_format = fmt.pixelFormat()
logger.debug(
f"Sprawdzam format -> "
f"{resolution.width()}x{resolution.height()} "
f"FPS:{min_fps}-{max_fps} "
f"PIX:{pixel_format}"
)
if (
resolution.width() == width
and resolution.height() == height
and min_fps <= fps <= max_fps
and pixel_format == preferred_pixel_format
):
best_match = fmt
break
if best_match:
self.camera.setCameraFormat(best_match)
logger.info("Ustawiono format kamery:")
logger.info(
f"Resolution: "
f"{best_match.resolution().width()}x"
f"{best_match.resolution().height()}"
)
logger.info(
f"FPS range: "
f"{best_match.minFrameRate()} - "
f"{best_match.maxFrameRate()}"
)
logger.info(
f"Pixel format: {best_match.pixelFormat()}"
)
else:
logger.warning("Nie znaleziono pasującego formatu.")
# ========================================================
# DEVICE LOGGER
# ========================================================
def log_camera_device(self, device: QCameraDevice, idx: int):
logger.info("------------------------------------------")
logger.info(f"KAMERA #{idx}")
logger.info("------------------------------------------")
logger.info(f"Description: {device.description()}")
logger.info(f"ID: {device.id().data().decode(errors='ignore')}")
try:
logger.info(f"Is default: {device.isDefault()}")
except Exception as e:
logger.warning(f"isDefault() unsupported: {e}")
formats = device.videoFormats()
logger.info(f"Liczba formatów: {len(formats)}")
for i, fmt in enumerate(formats):
resolution = fmt.resolution()
logger.info(f"")
logger.info(f"FORMAT #{i}")
logger.info(
f"Resolution: "
f"{resolution.width()}x{resolution.height()}"
)
logger.info(
f"FPS min/max: "
f"{fmt.minFrameRate()} / {fmt.maxFrameRate()}"
)
logger.info(
f"Pixel format enum: {fmt.pixelFormat()}"
)
logger.info(
f"Pixel format name: "
f"{self.pixel_format_to_string(fmt.pixelFormat())}"
)
# ========================================================
# RUNTIME CAMERA CAPABILITIES
# ========================================================
def log_runtime_camera_capabilities(self):
logger.info("")
logger.info("==========================================")
logger.info("QCAMERA RUNTIME CAPABILITIES")
logger.info("==========================================")
try:
logger.info(f"Camera active: {self.camera.isActive()}")
except Exception as e:
logger.warning(e)
# Dostępne w zależności od backendu/platformy
properties = [
"focusMode",
"exposureMode",
"whiteBalanceMode",
"flashMode",
"torchMode",
]
for prop in properties:
try:
value = getattr(self.camera, prop)()
logger.info(f"{prop}: {value}")
except Exception as e:
logger.warning(f"{prop} unsupported: {e}")
# backend/platform info
logger.info("")
logger.info("QT MULTIMEDIA INFO")
try:
logger.info(f"Qt version: {QApplication.qtVersion()}")
except Exception:
pass
logger.info(
"Backend zależy od platformy:"
)
logger.info(
"- Windows -> MediaFoundation"
)
logger.info(
"- Linux -> GStreamer / PipeWire / V4L2"
)
logger.info(
"- macOS -> AVFoundation"
)
# ========================================================
# PIXEL FORMAT NAME
# ========================================================
def pixel_format_to_string(self, pixel_format):
mapping = {
QVideoFrameFormat.PixelFormat.Format_YUV420P: "YUV420P",
QVideoFrameFormat.PixelFormat.Format_NV12: "NV12",
QVideoFrameFormat.PixelFormat.Format_NV21: "NV21",
QVideoFrameFormat.PixelFormat.Format_UYVY: "UYVY",
QVideoFrameFormat.PixelFormat.Format_YUYV: "YUYV",
QVideoFrameFormat.PixelFormat.Format_Jpeg: "MJPG/JPEG",
}
return mapping.get(pixel_format, str(pixel_format))
# ========================================================
# SIGNALS
# ========================================================
def on_camera_error(self, error, error_string):
logger.error(f"CAMERA ERROR: {error}")
logger.error(f"ERROR STRING: {error_string}")
def on_active_changed(self, active):
logger.info(f"Camera active changed: {active}")
# ========================================================
# CLEANUP
# ========================================================
# def closeEvent(self, event):
# logger.info("Zamykanie aplikacji.")
# if self.camera:
# self.camera.stop()
# super().closeEvent(event)
def closeEvent(self, event):
logger.info("Zamykanie aplikacji.")
try:
if self.camera:
logger.info("Stopping camera...")
self.camera.stop()
if self.capture_session:
logger.info("Detaching camera from capture session...")
self.capture_session.setCamera(None)
self.capture_session.setVideoOutput(None)
except Exception as e:
logger.exception(e)
super().closeEvent(event)
# ============================================================
# MAIN
# ============================================================
def main():
app = QApplication(sys.argv)
window = CameraWindow()
# tylko obraz
window.resize(1280, 720)
# fullscreen:
# window.showFullScreen()
window.show()
sys.exit(app.exec())
if __name__ == "__main__":
main()