From 177199c60ac6bba8cdb7af1854802a94da2a3048 Mon Sep 17 00:00:00 2001 From: Filip Leonarski Date: Thu, 17 Jul 2025 11:14:03 +0200 Subject: [PATCH] GUI: axis camera optimization --- gui/src/aaregui/threads/axis_video_thread.py | 140 +++++++++++++------ 1 file changed, 96 insertions(+), 44 deletions(-) diff --git a/gui/src/aaregui/threads/axis_video_thread.py b/gui/src/aaregui/threads/axis_video_thread.py index dde696da..97d10d13 100644 --- a/gui/src/aaregui/threads/axis_video_thread.py +++ b/gui/src/aaregui/threads/axis_video_thread.py @@ -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() +