1 import sys
2 from PyQt5.QtWidgets import QApplication, QWidget, QVBoxLayout, QHBoxLayout, QGridLayout, QPushButton, QLabel, QLineEdit, QTextEdit
3 from PyQt5.QtGui import QImage, QPixmap, QPainter, QPen, QColor, QPalette
4 from PyQt5.QtMultimedia import QSound
5 from PyQt5.QtCore import Qt, QThread, pyqtSignal, pyqtSlot, QTimer,QMetaObject, QMetaMethod
6 import cv2
7 import time
8 import cProfile
9 from datetime import datetime
10 import numpy as np
11
12 class VideoThread(QThread):
13 change_pixmap_signal = pyqtSignal(QImage)
14 change_zoom_signal = pyqtSignal(QImage)
15 change_fps_signal = pyqtSignal(str)
16 change_process_time_signal = pyqtSignal(float)
17 motion_detected_signal = pyqtSignal(bool, np.ndarray)
18 error_signal = pyqtSignal(str)
19 connection_status_signal = pyqtSignal(bool) # 定义新的信号
20
21
22 def __init__(self, get_coordinates, get_zoom_factor,get_address):
23 super().__init__()
24 self.get_coordinates = get_coordinates
25 self.get_zoom_factor = get_zoom_factor
26 self.get_address=get_address
27 self.fgbg = cv2.createBackgroundSubtractorMOG2()
28 self.motion_detected = False
29 self.stream_connected = False # 新增标志,表示视频流是否已经连接
30 self.prev_frame = None # 新增变量,用于保存前一帧
31
32
33 def run(self):
34 frame_counter = 0 # 新增帧计数器
35 max_retries = 5 # 最大重试次数
36 retry_delay = 1 # 重试延迟,单位为秒
37 retries = 0 # 当前的重试次数
38 while True:
39
40 cap = cv2.VideoCapture(self.get_address())
41
42
43 if not cap.isOpened():
44 if retries < max_retries:
45 time.sleep(retry_delay) # 等待一段时间再尝试重新连接
46 retries += 1
47 continue
48 else:
49 print('Failed to connect to video stream after %s retries.' % retries)
50 self.error_signal.emit('重连 %s 次失败.' % retries)
51 return
52 self.video_width = cap.get(cv2.CAP_PROP_FRAME_WIDTH)
53 self.video_height = cap.get(cv2.CAP_PROP_FRAME_HEIGHT)
54 prev_time = time.time()
55
56 while True:
57
58 if cap.get(cv2.CAP_PROP_FPS) == 0: # 如果获取帧率失败,表示视频流已经中断
59 if self.stream_connected:
60 self.connection_status_signal.emit(False)
61 self.stream_connected = False
62 break
63
64 start_time = time.time() # 开始处理的时间
65 ret, frame = cap.read()
66
67 if not ret:
68 if self.stream_connected: # 如果之前的状态是已连接
69 self.connection_status_signal.emit(False) # 发出连接状态改变的信号
70 self.stream_connected = False # 如果读取帧失败,设置视频流未连接
71 break
72
73 if not self.stream_connected: # 如果之前的状态是未连接
74 self.connection_status_signal.emit(True) # 发出连接状态改变的信号
75 self.stream_connected = True # 如果读取帧成功,设置视频流已连接
76
77
78
79
80
81 frame_counter += 1 # 每处理一帧视频,帧计数器就加1
82
83 if frame_counter % 3 != 0: # 如果帧计数器不是5的倍数,跳过这一帧
84 continue
85 rgb_image = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
86 h, w, ch = rgb_image.shape
87 bytes_per_line = ch * w
88 convert_to_Qt_format = QImage(rgb_image.data, w, h, bytes_per_line, QImage.Format_RGB888)
89 p = convert_to_Qt_format.scaled(270, 480, Qt.KeepAspectRatio)
90 self.change_pixmap_signal.emit(p)
91
92 x, y, width, height = self.get_coordinates()
93 zoom_image = rgb_image[y:y+height, x:x+width]
94 zoom_image = cv2.GaussianBlur(zoom_image, (7, 7), 1)
95 #锐化边缘
96 laplacian = cv2.Laplacian(zoom_image, cv2.CV_64F)
97 zoom_image = cv2.convertScaleAbs(zoom_image - laplacian)
98
99 #--------------------------------
100 fgmask = self.fgbg.apply(zoom_image)
101 contours, _ = cv2.findContours(fgmask, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)
102
103
104
105 motion_detected = False
106 for contour in contours:
107 if cv2.contourArea(contour) > 300:
108 x, y, w, h = cv2.boundingRect(contour)
109 cv2.rectangle(zoom_image, (x, y), (x+w, y+h), (0, 255, 0), 2)
110 motion_detected = True
111
112 self.motion_detected = motion_detected
113 self.motion_detected_signal.emit(motion_detected,zoom_image)
114 # 使用帧差法检测运动
115 #--------------------------------
116 # gray = cv2.cvtColor(zoom_image, cv2.COLOR_BGR2GRAY)
117
118 # if self.prev_frame is not None:
119 # frame_diff = cv2.absdiff(self.prev_frame, gray)
120 # _, fgmask = cv2.threshold(frame_diff, 25, 255, cv2.THRESH_BINARY)
121 # contours, _ = cv2.findContours(fgmask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
122
123 # motion_detected = False
124 # for contour in contours:
125 # if cv2.contourArea(contour) > 300:
126 # x, y, w, h = cv2.boundingRect(contour)
127 # cv2.rectangle(zoom_image, (x, y), (x+w, y+h), (0, 255, 0), 2)
128 # motion_detected = True
129
130 # self.motion_detected = motion_detected
131 # self.motion_detected_signal.emit(motion_detected, zoom_image)
132
133 # self.prev_frame = gray # 保存当前帧,以便下一次循环使用
134 #--------------------------------
135
136
137
138
139
140 #--------------------------------
141
142
143
144 zoom_factor = self.get_zoom_factor()
145 zoom_image = cv2.resize(zoom_image, None, fx=zoom_factor, fy=zoom_factor)
146 h, w, ch = zoom_image.shape
147 bytes_per_line = ch * w
148 convert_to_Qt_format = QImage(zoom_image.data, w, h, bytes_per_line, QImage.Format_RGB888)
149 self.change_zoom_signal.emit(convert_to_Qt_format)
150 end_time = time.time() # 结束处理的时间
151 self.change_process_time_signal.emit(end_time - start_time) # 发送处理时间
152
153 curr_time = time.time()
154 fps = 1 / (curr_time - prev_time)
155 prev_time = curr_time
156 self.change_fps_signal.emit('FPS 帧率: %s' % str(int(fps)))
157 cap.release() # 释放VideoCapture对象
158
159
160 class App(QWidget):
161 def __init__(self):
162 super().__init__()
163 self.setWindowTitle("Glass Flow Monitor ver0.0")
164 self.disply_width = 270
165 self.display_height = 480
166 self.image_label = QLabel(self)
167 self.image_label.resize(self.disply_width, self.display_height)
168 self.zoom_label = QLabel(self)
169 self.fps_label = QLabel(self)
170 self.process_time_label = QLabel(self)
171 self.alarm_delay_input = QLineEdit(self)
172 self.alarm_delay_input.setText('10')
173 self.alarm_countdown_label = QLabel(self)
174 self.alarm_text = QTextEdit(self)
175 self.alarm_text.setReadOnly(True)
176 self.x_input = QLineEdit(self)
177 self.x_input.setText('320')
178 self.y_input = QLineEdit(self)
179 self.y_input.setText('490')
180 self.width_input = QLineEdit(self)
181 self.width_input.setText('100')
182 self.height_input = QLineEdit(self)
183 self.height_input.setText('150')
184 self.zoom_input = QLineEdit(self)
185 self.zoom_input.setText('2')
186 self.ip_address_input=QLineEdit(self)
187 self.ip_address_input.setText('192.168.1.1')
188 self.ip_address_input.textChanged.connect(self.change_address)
189
190
191 self.thread = self.create_video_thread() # 使用新的方法创建 VideoThread
192
193 self.layout = QHBoxLayout()
194 self.setLayout(self.layout)
195
196 self.left_layout = QVBoxLayout()
197 self.left_layout.addWidget(self.image_label)
198
199 self.input_layout = QGridLayout()
200
201 self.input_layout.addWidget(QLabel('检测区X坐标:'), 0, 0)
202 self.input_layout.addWidget(self.x_input, 0, 1)
203 self.input_layout.addWidget(QLabel('检测区Y坐标:'), 1, 0)
204 self.input_layout.addWidget(self.y_input, 1, 1)
205 self.input_layout.addWidget(QLabel('检测区宽 Width:'), 2, 0)
206 self.input_layout.addWidget(self.width_input, 2, 1)
207 self.input_layout.addWidget(QLabel('检测区高 Height:'), 3, 0)
208 self.input_layout.addWidget(self.height_input, 3, 1)
209 self.input_layout.addWidget(QLabel('缩放 Zoom:'), 4, 0)
210 self.input_layout.addWidget(self.zoom_input, 4, 1)
211 self.left_layout.addLayout(self.input_layout)
212
213 self.input_layout.addWidget(QLabel('IP:'), 5, 0)
214 self.input_layout.addWidget(self.ip_address_input, 5, 1)
215 self.left_layout.addLayout(self.input_layout)
216
217 self.layout.addLayout(self.left_layout)
218
219 self.middle_layout = QVBoxLayout()
220 self.middle_layout.addWidget(self.zoom_label)
221 self.middle_layout.addWidget(self.fps_label)
222 self.middle_layout.addWidget(self.process_time_label)
223 self.middle_layout.addWidget(QLabel('Alarm Delay 报警阈值(S):'))
224 self.middle_layout.addWidget(self.alarm_delay_input)
225
226 self.middle_layout.addWidget(self.alarm_countdown_label)
227 self.middle_layout.addWidget(self.alarm_text)
228 self.middle_layout.addStretch()
229
230 self.layout.addLayout(self.middle_layout)
231
232
233 self.password_input = QLineEdit(self)
234 self.password_input.setEchoMode(QLineEdit.Password) # 设置为密码输入框
235 self.login_button = QPushButton('Login 登录', self)
236 self.login_button.clicked.connect(self.login)
237 self.middle_layout.addWidget(QLabel('Password 密码:'))
238 self.middle_layout.addWidget(self.password_input)
239 self.middle_layout.addWidget(self.login_button)
240
241 self.logout_timer = QTimer(self)
242 self.logout_timer.timeout.connect(self.logout)
243 self.logout_timer.start(30000) # 设置定时器超时时间为1分钟
244
245
246
247
248
249
250 self.btn_quit = QPushButton('Quit 关闭', self)
251 self.btn_quit.clicked.connect(self.close)
252 self.middle_layout.addWidget(self.btn_quit)
253
254 self.alarm_timer = QTimer(self)
255 self.alarm_timer.timeout.connect(self.on_alarm_timeout)
256 self.alarm_time = None
257 self.alarm_timer.start(1000)
258 self.alarm_generated = False # 新增标志,表示是否已经生成了报警信息
259
260 self.alarm_sound = QSound('alarm.wav') # 报警音,需要一个.wav文件
261 self.alarm_sound.setLoops(-1) # 设置无限循环
262 self.alarm_playing = False # 新增标志位,表示是否正在播放报警音
263 self.acknowledge_button = QPushButton('Acknowledge Alarm 确认报警', self) # 新增确认按钮
264 self.acknowledge_button.clicked.connect(self.acknowledge_alarm)
265 self.middle_layout.addWidget(self.acknowledge_button)
266
267 self.enable_controls(False) # 初始化时禁用所有的控件
268 self.thread.error_signal.connect(self.update_alarm_text)
269
270
271 self.alarm_image_saved = False # 新增标志位,表示是否已经保存过报警图片
272
273
274
275
276
277 def acknowledge_alarm(self):
278 self.alarm_playing = False # 复位标志位
279 self.alarm_sound.stop() # 停止播放报警音
280
281 def login(self):
282 password = self.password_input.text()
283 if password == '8888': # 这里替换为你的密码
284 self.enable_controls(True)
285 self.reset_logout_timer()
286
287 def logout(self):
288 self.enable_controls(False)
289
290 def reset_logout_timer(self):
291 self.logout_timer.start(30000) # 重置定时器
292
293
294 def enable_controls(self, enabled):
295 self.x_input.setEnabled(enabled)
296 self.y_input.setEnabled(enabled)
297 self.width_input.setEnabled(enabled)
298 self.height_input.setEnabled(enabled)
299 self.zoom_input.setEnabled(enabled)
300 self.alarm_delay_input.setEnabled(enabled)
301 self.btn_quit.setEnabled(enabled)
302 self.acknowledge_button.setEnabled(True) # 报警确认按钮总是可用
303
304
305 def create_video_thread(self):
306 # 创建一个新的 VideoThread
307 thread = VideoThread(self.get_coordinates, self.get_zoom_factor, self.get_address)
308 thread.change_pixmap_signal.connect(self.update_image)
309 thread.change_zoom_signal.connect(self.update_zoom)
310 thread.change_fps_signal.connect(self.update_fps)
311 thread.change_process_time_signal.connect(self.update_process_time)
312 thread.motion_detected_signal.connect(self.on_motion_detected)
313 thread.start()
314 return thread
315
316 def change_address(self):
317 # 停止当前的 VideoThread
318 self.thread.terminate()
319 # 创建一个新的 VideoThread
320 self.thread = self.create_video_thread()
321
322
323
324 @pyqtSlot(QImage)
325 def update_image(self, qt_image):
326 x = int(self.x_input.text())
327 y = int(self.y_input.text())
328 width = int(self.width_input.text())
329 height = int(self.height_input.text())
330 x_scaled = int(x * 270 / self.thread.video_width)
331 y_scaled = int(y * 480 / self.thread.video_height)
332 width_scaled = int(width * 270 / self.thread.video_width)
333 height_scaled = int(height * 480 / self.thread.video_height)
334 painter_instance = QPainter(qt_image)
335 painter_instance.setPen(QPen(Qt.blue, 3))
336 painter_instance.drawRect(x_scaled, y_scaled, width_scaled, height_scaled)
337 self.image_label.setPixmap(QPixmap.fromImage(qt_image))
338
339 @pyqtSlot(QImage)
340 def update_zoom(self, qt_image):
341 self.zoom_label.setPixmap(QPixmap.fromImage(qt_image))
342
343 @pyqtSlot(str)
344 def update_fps(self, fps):
345 self.fps_label.setText(fps)
346
347 @pyqtSlot(float)
348 def update_process_time(self, process_time):
349 self.process_time_label.setText('Process time 处理时间: %.1f ms' % (process_time * 1000))
350
351 @pyqtSlot(bool, np.ndarray)
352 def on_motion_detected(self, motion_detected, image):
353 if motion_detected and self.thread.stream_connected:
354 self.alarm_time = time.time()
355 self.alarm_image = image # 保存截取的图像,以便在需要时保存为文件
356
357 @pyqtSlot(str)
358 def update_alarm_text(self, text):
359 self.alarm_text.append(text)
360
361
362 @pyqtSlot(bool)
363 def update_connection_status(self, connected):
364 if connected:
365 self.connection_status_light.setStyleSheet('background-color: green; border-radius: 10px;') # 设置标签的背景颜色为绿色
366 self.connection_status_text.setText('Connected')
367 else:
368 self.connection_status_light.setStyleSheet('background-color: red; border-radius: 10px;') # 设置标签的背景颜色为红色
369 self.connection_status_text.setText('Disconnected')
370
371 @pyqtSlot()
372 def on_alarm_timeout(self):
373 if self.alarm_time is not None and self.thread.stream_connected: # 如果视频流已经连接:
374 countdown = int(self.alarm_delay_input.text()) - int(time.time() - self.alarm_time)
375 if countdown <= 0:
376 if not self.alarm_generated: # 如果还没有生成报警信息
377 timestamp = datetime.now().strftime('%Y-%m-%d %H-%M-%S')
378 self.alarm_text.append('%s Alarm: 未检测到移动玻璃液 %s seconds.' % (timestamp, self.alarm_delay_input.text()))
379 self.alarm_generated = True # 设置标志,表示已经生成了报警信息
380 self.alarm_playing = True # 设置标志,表示应该播放报警音
381 self.alarm_sound.play() # 开始播放报警音
382 if not self.alarm_image_saved: # 如果还没有保存过报警图片
383 bgr_image = cv2.cvtColor(self.alarm_image, cv2.COLOR_RGB2BGR) # 将图像从 RGB 格式转换为 BGR 格式
384 try:
385 cv2.imwrite('alarm_%s.png' % timestamp, bgr_image) # 保存报警图片
386 except Exception as e:
387 print('Failed to save alarm image:', e)
388 cv2.imwrite('alarm_%s.png' % timestamp, bgr_image) # 保存报警图片
389 self.alarm_image_saved = True # 设置标志,表示已经保存过报警图片
390
391 palette = self.palette()
392 if palette.color(QPalette.Background) == Qt.red:
393 palette.setColor(QPalette.Background, Qt.yellow)
394 else:
395 palette.setColor(QPalette.Background, Qt.red)
396 self.setPalette(palette)
397 else:
398 self.alarm_generated = False # 重置标志,因为报警已经结束
399 self.alarm_image_saved = False # 重置标志,因为报警已经结束
400 self.acknowledge_alarm() # 停止播放报警音
401 palette = self.palette()
402 palette.setColor(QPalette.Background, Qt.white)
403 self.setPalette(palette)
404 self.alarm_countdown_label.setText('Alarm Countdown 倒计时(s): %s s' % countdown)
405 else:
406 palette = self.palette()
407 palette.setColor(QPalette.Background, Qt.white)
408 self.setPalette(palette)
409 self.alarm_countdown_label.setText('Alarm Countdown 倒计时(s):: no motion detected')
410 self.acknowledge_alarm() # 停止播放报警音
411
412 def closeEvent(self, event):
413 self.thread.terminate()
414
415 def get_coordinates(self):
416 return int(self.x_input.text()), int(self.y_input.text()), int(self.width_input.text()), int(self.height_input.text())
417
418 def get_zoom_factor(self):
419 return float(self.zoom_input.text())
420
421 def get_address(self):
422 PATH='rtsp://root:root@'+str(self.ip_address_input.text())+':554/axis-media/media.amp'
423 print(PATH)
424 return PATH
425
426 def main():
427 start_time = time.time()
428 app = QApplication(sys.argv)
429 ex = App()
430 ex.show()
431 sys.exit(app.exec_())
432 print("Total execution time: %.2f ms" % ((time.time() - start_time) * 1000))
433
434 if __name__ == '__main__':
435 cProfile.run('main()')