GUI: axis camera optimization

This commit is contained in:
leonarski_f committed 2025-07-17 14:27:19 +02:00
1 parent e3e1e97712
commit 177199c60a
1 file changed
+96 -44
+96 -44
View File
@@ -1,6 +1,9 @@
import cv2
import requests
import numpy as np
from PySide6.QtCore import QThread, Signal
from PySide6.QtGui import QImage
from io import BytesIO
class VideoThread(QThread):
@@ -11,71 +14,120 @@ class VideoThread(QThread):
super().__init__()
self.camera_ip = ip
self.running = False
self.cap = None
self.session = None
self.last_good_frame = None
def set_camera_ip(self, ip: str):
cv2.setLogLevel(cv2.LOG_LEVEL_ERROR)
self.camera_ip = ip
def run(self):
# RTSP URL without credentials
rtsp_url = f"rtsp://{self.camera_ip}:554/axis-media/media.amp?videocodec=h264&resolution=1280x720"
# MJPEG URL
mjpeg_url = f"http://{self.camera_ip}/axis-cgi/mjpg/video.cgi?resolution=1280x720&fps=10"
self.cap = cv2.VideoCapture(rtsp_url)
# Create a session for connection reuse
self.session = requests.Session()
if not self.cap.isOpened():
self.error_occurred.emit(f"Could not connect to camera at {self.camera_ip}")
return
# Set connection timeout and read timeout
timeout = (5, 30) # (connect_timeout, read_timeout)
# Set buffer size to reduce latency
self.cap.set(cv2.CAP_PROP_BUFFERSIZE, 1)
self.cap.set(cv2.CAP_PROP_FPS, 10) # Request 10 FPSs
try:
# Start streaming
response = self.session.get(mjpeg_url, stream=True, timeout=timeout)
self.running = True
if response.status_code != 200:
self.error_occurred.emit(
f"Could not connect to camera at {self.camera_ip}. Status: {response.status_code}")
return
while self.running:
ret, frame = self.cap.read()
self.running = True
if ret and frame is not None:
# Check if frame is valid (not corrupted)
if frame.shape[0] > 0 and frame.shape[1] > 0:
try:
# Convert BGR to RGB
rgb_frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
# Buffer to accumulate data
buffer = b''
# Convert to QImage
height, width, channel = rgb_frame.shape
bytes_per_line = 3 * width
qt_image = QImage(rgb_frame.data, width, height, bytes_per_line, QImage.Format.Format_RGB888)
# MJPEG boundary detection
boundary = None
# Store as last good frame
self.last_good_frame = qt_image
self.frame_ready.emit(qt_image)
for chunk in response.iter_content(chunk_size=1024):
if not self.running:
break
except Exception as e:
# If frame processing fails, use last good frame if available
if self.last_good_frame is not None:
self.frame_ready.emit(self.last_good_frame)
# Don't emit error for minor decoding issues, just continue
pass
else:
# Invalid frame dimensions, use last good frame if available
if self.last_good_frame is not None:
self.frame_ready.emit(self.last_good_frame)
else:
# Frame read failed, use last good frame if available
if chunk:
buffer += chunk
# Find boundary on first iteration
if boundary is None:
boundary_start = buffer.find(b'--')
if boundary_start != -1:
boundary_end = buffer.find(b'\r\n', boundary_start)
if boundary_end != -1:
boundary = buffer[boundary_start:boundary_end]
# Process frames if we have a boundary
if boundary:
self._process_buffer(buffer, boundary)
# Keep only the last part of buffer that might contain partial frame
last_boundary = buffer.rfind(boundary)
if last_boundary != -1:
buffer = buffer[last_boundary:]
except requests.exceptions.RequestException as e:
self.error_occurred.emit(f"Connection error: {str(e)}")
except Exception as e:
self.error_occurred.emit(f"Unexpected error: {str(e)}")
finally:
if self.session:
self.session.close()
def _process_buffer(self, buffer, boundary):
"""Process buffer to extract JPEG frames"""
parts = buffer.split(boundary)
for part in parts[:-1]: # Skip the last part as it might be incomplete
if not part:
continue
# Find the start of JPEG data (after headers)
jpeg_start = part.find(b'\xff\xd8') # JPEG SOI marker
if jpeg_start == -1:
continue
# Find the end of JPEG data
jpeg_end = part.find(b'\xff\xd9', jpeg_start) # JPEG EOI marker
if jpeg_end == -1:
continue
# Extract JPEG data
jpeg_data = part[jpeg_start:jpeg_end + 2]
try:
# Decode JPEG using OpenCV
nparr = np.frombuffer(jpeg_data, np.uint8)
frame = cv2.imdecode(nparr, cv2.IMREAD_COLOR)
if frame is not None and frame.shape[0] > 0 and frame.shape[1] > 0:
# Convert BGR to RGB
rgb_frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
# Convert to QImage
height, width, channel = rgb_frame.shape
bytes_per_line = 3 * width
qt_image = QImage(rgb_frame.data, width, height, bytes_per_line, QImage.Format.Format_RGB888)
# Store as last good frame
self.last_good_frame = qt_image
self.frame_ready.emit(qt_image)
except Exception as e:
# If frame processing fails, use last good frame if available
if self.last_good_frame is not None:
self.frame_ready.emit(self.last_good_frame)
# Small delay to prevent overwhelming the GUI
self.msleep(100) # ~10 FPS
def stop(self):
self.running = False
print("Video thread stop requested")
if self.cap:
self.cap.release()
if self.session:
self.session.close()
self.quit()
print("Video thread stopped. Exiting...")
self.wait()