From afdefbaca63c4313da20e47262f35b9a397ff389 Mon Sep 17 00:00:00 2001 From: yanganjie Date: Mon, 20 Oct 2025 18:10:07 +0800 Subject: [PATCH] =?UTF-8?q?add=20=E8=A7=86=E9=A2=91=E6=98=BE=E7=A4=BA?= =?UTF-8?q?=E6=A1=86=E3=80=81=E8=B0=83=E6=95=B4=E5=B8=83=E5=B1=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- view/main_window.py | 27 ++- view/widgets/vibration_video_widget.py | 251 +++++++++++++++++++++++++ 2 files changed, 270 insertions(+), 8 deletions(-) create mode 100644 view/widgets/vibration_video_widget.py diff --git a/view/main_window.py b/view/main_window.py index 9b6a630..7a5e0a3 100644 --- a/view/main_window.py +++ b/view/main_window.py @@ -10,7 +10,7 @@ from .widgets.hopper_widget import HopperWidget from .widgets.arc_progress_widget import ArcProgressWidget from .widgets.production_progress_widget import ProductionProgressWidget from .widgets.system_button_widget import SystemButtonWidget - +from .widgets.vibration_video_widget import VibrationVideoWidget class MainWindow(QWidget): def __init__(self): @@ -20,8 +20,6 @@ class MainWindow(QWidget): self.setupLayout() # 设置布局 self.connectSignalToSlot() - - def connectSignalToSlot(self): # 可添加信号槽连接 self.system_button_widget.buttons["系统启动"].clicked.connect(self.handleSystemStart) @@ -53,6 +51,7 @@ class MainWindow(QWidget): self.arc_progress = ArcProgressWidget() # 中间2:弧形进度部件 self.production_progress = ProductionProgressWidget() # 生产进度部件 self.system_button_widget = SystemButtonWidget() # 系统控制按钮 + self.vibration_video = VibrationVideoWidget() # 振捣视频控件 def setupLayout(self): """设置垂直布局,从上到下排列部件""" @@ -61,12 +60,24 @@ class MainWindow(QWidget): main_layout.setSpacing(0) # 部件间距0px main_layout.setContentsMargins(15, 15, 15, 15) # 上下左右边距15px + sub_v_layout = QVBoxLayout() + sub_v_layout.setSpacing(0) # 依次添加部件到布局(从上到下) - main_layout.addWidget(self.status_monitor, alignment=Qt.AlignHCenter) - main_layout.addWidget(self.hopper_widget, alignment=Qt.AlignHCenter) - main_layout.addWidget(self.arc_progress, alignment=Qt.AlignHCenter) - main_layout.addWidget(self.production_progress, alignment=Qt.AlignHCenter) - main_layout.addWidget(self.system_button_widget, alignment=Qt.AlignHCenter) + # sub_v_layout.addWidget(self.status_monitor, alignment=Qt.AlignHCenter) + sub_v_layout.addWidget(self.hopper_widget, alignment=Qt.AlignHCenter) + sub_v_layout.addWidget(self.arc_progress, alignment=Qt.AlignHCenter) + sub_v_layout.addWidget(self.production_progress, alignment=Qt.AlignHCenter) + # sub_v_layout.addWidget(self.system_button_widget, alignment=Qt.AlignHCenter) + + middle_h_layout = QHBoxLayout() + middle_h_layout.setSpacing(20) + # 加入垂直子布局(设置拉伸因子1,让其占满水平剩余空间) + middle_h_layout.addLayout(sub_v_layout, stretch=1) + # 加入振捣视频控件(对齐方式为顶部) + middle_h_layout.addWidget(self.vibration_video, alignment=Qt.AlignTop) + + # === 添加到著布局 + main_layout.addLayout(middle_h_layout) # 将布局应用到主窗口 self.setLayout(main_layout) diff --git a/view/widgets/vibration_video_widget.py b/view/widgets/vibration_video_widget.py new file mode 100644 index 0000000..53a27e4 --- /dev/null +++ b/view/widgets/vibration_video_widget.py @@ -0,0 +1,251 @@ +# coding:utf-8 +from PySide6.QtCore import Qt, QTimer, Signal, QObject +from PySide6.QtGui import QImage, QPixmap +from PySide6.QtWidgets import QWidget, QVBoxLayout, QHBoxLayout, QLabel, QApplication +from typing import Optional + +import cv2 +import time +from threading import Thread, Lock +import sys + + +# ====================== 后台视频流管理(自动重连)====================== +class VideoStream: + """后台读取 RTSP 流,自动重连,只返回新鲜帧""" + + def __init__(self, rtsp_url, name="Stream"): + self.rtsp_url = rtsp_url + self.name = name + self.cap = None + self.frame = None + self.timestamp = 0 + self.lock = Lock() + self.running = False + self.thread = None + + def start(self): + self.running = True + self.thread = Thread(target=self.update, args=(), daemon=True) + self.thread.start() + return self + + def update(self): + while self.running: + if self.cap is None or not self.cap.isOpened(): + print(f"[{self.name}] 正在连接 RTSP: {self.rtsp_url}") + try: + self.cap = cv2.VideoCapture(self.rtsp_url, cv2.CAP_FFMPEG) + if hasattr(cv2, 'CAP_PROP_BUFFERSIZE'): + self.cap.set(cv2.CAP_PROP_BUFFERSIZE, 1) + if hasattr(cv2, 'CAP_PROP_READ_TIMEOUT'): + self.cap.set(cv2.CAP_PROP_READ_TIMEOUT, 2000) + if hasattr(cv2, 'CAP_PROP_TCP_NODELAY'): + self.cap.set(cv2.CAP_PROP_TCP_NODELAY, 1) + except Exception as e: + print(f"[{self.name}] 连接失败: {e}") + time.sleep(1) + continue + + ret, frame = self.cap.read() + if ret: + with self.lock: + self.frame = frame.copy() + self.timestamp = time.time() + else: + print(f"[{self.name}] 读取失败,准备重连...") + if self.cap: + self.cap.release() + self.cap = None + time.sleep(1) + + def read(self): + with self.lock: + if self.frame is not None and time.time() - self.timestamp < 1.5: + return self.frame.copy() + else: + return None + + def stop(self): + self.running = False + if self.thread is not None: + self.thread.join(timeout=0.5) + if self.cap is not None: + self.cap.release() + print(f"[{self.name}] 视频流已停止") + + +# ====================== 摄像头功能模块 ====================== +class CameraModule(QWidget): + """单个摄像头模块:原图显示""" + + def __init__(self, camera_name="摄像头", rtsp_url="", need_rotate_180=True, parent=None): + super().__init__(parent) + self.camera_name = camera_name + self.rtsp_url = rtsp_url + self.need_rotate_180 = need_rotate_180 # 画面是否需要旋转180度后显示 + self.setup_ui() + + def setup_ui(self): + layout = QVBoxLayout(self) + layout.setSpacing(0) + # layout.setContentsMargins(8, 8, 8, 8) + layout.setContentsMargins(0, 0, 0, 0) + + # --- 0. 标题 --- + self.title_label = QLabel() + self.title_label.setFixedSize(106, 25) + self.title_label.setAlignment(Qt.AlignLeft) + self.title_label.setText(f"{self.camera_name}视频") + self.title_label.setObjectName("cameraTitleLabel") + self.title_label.setStyleSheet(""" + #cameraTitleLabel { + font-size: 16px; + color: black; + font-weight: bold; + } + """) + + # --- 1. 原始图像 --- + self.raw_label = QLabel() # 显示图像的 label + # self.raw_label.setFixedSize(320, 240) + self.raw_label.setFixedSize(327, 199) # 显示的图像的宽、高 + + palette = self.raw_label.palette() + palette.setColor(self.raw_label.foregroundRole(), Qt.white) # 字体颜色:白色 + self.raw_label.setPalette(palette) + + self.raw_label.setAlignment(Qt.AlignCenter) + # self.raw_label.setStyleSheet("background: black; border: 1px solid #ccc;") + self.raw_label.setStyleSheet("background: #033474; border: none") # 视频显示框样式 + self.raw_label.setText(f"{self.camera_name}摄像头, 连接中...") + + # --- 添加到主布局 --- + layout.addWidget(self.title_label) # 标题 + layout.addWidget(self.raw_label) # 图像 + + def update_raw_image(self, image: Optional[QImage]): + if image: + pixmap = QPixmap.fromImage(image) + self.raw_label.setPixmap(pixmap.scaled( + self.raw_label.size(), + Qt.KeepAspectRatio, + Qt.SmoothTransformation + )) + self.raw_label.setText("") + else: + self.raw_label.setPixmap(QPixmap()) + self.raw_label.setText(f"{self.camera_name}摄像头, 断线中...") + +class VibrationVideoWidget(QWidget): + def __init__(self, parent=None): + super().__init__(parent=parent) + self.setObjectName("vibrationVideoWidget") + + # 显示摄像头画面的模组 + # 需要修改为相应的地址!!! + self.cam1 = CameraModule("上位料斗", "rtsp://admin:XJ123456@192.168.1.50:554/streaming/channels/101") + self.cam2 = CameraModule("下位料斗", "rtsp://admin:XJ123456@192.168.1.51:554/streaming/channels/101") + self.cam3 = CameraModule("模具车", "rtsp://admin:XJ123456@192.168.1.51:554/streaming/channels/101") + + self.setup_ui() + self.init_streams() + self.connect_signals() + + def setup_ui(self): + # 视频widget样式 + self.setFixedSize(387, 720) # 宽387、高792 + self.setAutoFillBackground(True) + self.setStyleSheet(""" + #vibrationVideoWidget { + background-color: #043d76; + border: none; /* 可选:去除默认边框,避免视觉干扰 */ + } + """) + + # 布局设置 + layout = QVBoxLayout(self) + layout.setContentsMargins(16, 16, 16, 16) + layout.setSpacing(6) # 每两个摄像头模块间隔6px + layout.addWidget(self.cam1) + layout.addWidget(self.cam2) + layout.addWidget(self.cam3) + + def init_streams(self): + url1 = self.cam1.rtsp_url + url2 = self.cam2.rtsp_url + url3 = self.cam3.rtsp_url + + self.stream1 = VideoStream(url1, "Cam1").start() + self.stream2 = VideoStream(url2, "Cam2").start() + self.stream3 = VideoStream(url3, "Cam3").start() + + self.timer1 = QTimer() + self.timer1.timeout.connect(self.update_frame1) + + self.timer2 = QTimer() + self.timer2.timeout.connect(self.update_frame2) + + self.timer3 = QTimer() + self.timer3.timeout.connect(self.update_frame3) + + # 定时读取视频流 + self.timer1.start(33) + self.timer2.start(33) + self.timer3.start(33) + + def connect_signals(self): + pass + + def update_frame1(self): + frame = self.stream1.read() + if frame is not None: + frame_rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) + if self.cam1.need_rotate_180: + frame_rgb = cv2.rotate(frame_rgb, cv2.ROTATE_180) + h, w, ch = frame_rgb.shape + q_img = QImage(frame_rgb.data, w, h, ch * w, QImage.Format_RGB888) + self.cam1.update_raw_image(q_img) + else: + self.cam1.update_raw_image(None) + + def update_frame2(self): + frame = self.stream2.read() + if frame is not None: + frame_rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) + if self.cam2.need_rotate_180: + frame_rgb = cv2.rotate(frame_rgb, cv2.ROTATE_180) + h, w, ch = frame_rgb.shape + q_img = QImage(frame_rgb.data, w, h, ch * w, QImage.Format_RGB888) + self.cam2.update_raw_image(q_img) + else: + self.cam2.update_raw_image(None) + + def update_frame3(self): + frame = self.stream3.read() + if frame is not None: + frame_rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) + if self.cam3.need_rotate_180: + frame_rgb = cv2.rotate(frame_rgb, cv2.ROTATE_180) + h, w, ch = frame_rgb.shape + q_img = QImage(frame_rgb.data, w, h, ch * w, QImage.Format_RGB888) + self.cam3.update_raw_image(q_img) + else: + self.cam3.update_raw_image(None) + + # ====================== 清理资源 ====================== + def closeEvent(self, event): + self.hide() + self.stream1.stop() + self.stream2.stop() + self.stream3.stop() + self.timer1.stop() + self.timer2.stop() + self.timer3.stop() + super().closeEvent(event) + +if __name__ == "__main__": + app = QApplication([]) + window = VibrationVideoWidget() + window.show() + sys.exit(app.exec())