grid_camera_view.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303
  1. from PyQt5.QtWidgets import (QWidget, QGridLayout, QLabel, QPushButton,
  2. QVBoxLayout, QHBoxLayout, QComboBox, QMenu)
  3. from PyQt5.QtCore import Qt, QTimer, pyqtSignal, QSize
  4. from PyQt5.QtGui import QImage, QPixmap, QIcon
  5. import cv2
  6. import numpy as np
  7. import base64
  8. from ..assets.icons import GRID_ICON, SINGLE_ICON, REFRESH_ICON
  9. def create_icon_from_base64(base64_str):
  10. """从Base64字符串创建图标"""
  11. pixmap = QPixmap()
  12. pixmap.loadFromData(base64.b64decode(base64_str))
  13. return QIcon(pixmap)
  14. class CameraGridCell(QLabel):
  15. """单个摄像头格子组件"""
  16. clicked = pyqtSignal(int) # 点击信号,传递格子索引
  17. fire_detected = pyqtSignal(str) # 火灾检测信号
  18. animal_detected = pyqtSignal(object, str, float) # 动物检测信号
  19. def __init__(self, index, parent=None):
  20. super().__init__(parent)
  21. self.index = index
  22. self.active = False
  23. self.last_fire_check = 0 # 上次火情检查时间
  24. self.fire_check_interval = 1.0 # 火情检查间隔(秒)
  25. self.setAlignment(Qt.AlignCenter)
  26. self.setMinimumSize(320, 240)
  27. self.setStyleSheet("""
  28. QLabel {
  29. border: 2px solid #1e3a5a;
  30. background-color: #0a1a2a;
  31. color: white;
  32. }
  33. QLabel:hover {
  34. border: 2px solid #3a6a9a;
  35. }
  36. """)
  37. self.setText("摄像头未连接")
  38. def mousePressEvent(self, event):
  39. if event.button() == Qt.LeftButton:
  40. self.clicked.emit(self.index)
  41. super().mousePressEvent(event)
  42. def setImage(self, image, camera_info=None):
  43. """设置图像并进行灾害检测"""
  44. if isinstance(image, np.ndarray):
  45. # 保存原始图像用于显示
  46. display_image = image.copy()
  47. # 进行火灾检测
  48. current_time = cv2.getTickCount() / cv2.getTickFrequency()
  49. if current_time - self.last_fire_check >= self.fire_check_interval:
  50. self.last_fire_check = current_time
  51. # 火灾检测
  52. if self.detect_fire(image):
  53. region = camera_info['name'] if camera_info else f"摄像头 {self.index + 1}"
  54. self.fire_detected.emit(region)
  55. # 在图像上标注火灾警告
  56. cv2.putText(display_image, "火灾警告!", (10, 30),
  57. cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 0, 255), 2)
  58. # 动物检测
  59. animal_result = self.detect_animal(image)
  60. if animal_result:
  61. species, confidence = animal_result
  62. self.animal_detected.emit(image, species, confidence)
  63. # 在图像上标注动物信息
  64. cv2.putText(display_image, f"{species} ({confidence:.2f}%)", (10, 60),
  65. cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 255, 0), 2)
  66. # 转换BGR到RGB并显示
  67. display_image = cv2.cvtColor(display_image, cv2.COLOR_BGR2RGB)
  68. height, width, channel = display_image.shape
  69. bytesPerLine = 3 * width
  70. qImg = QImage(display_image.data, width, height, bytesPerLine, QImage.Format_RGB888)
  71. self.setPixmap(QPixmap.fromImage(qImg).scaled(
  72. self.size(), Qt.KeepAspectRatio, Qt.SmoothTransformation))
  73. self.active = True
  74. else:
  75. self.setText("摄像头未连接")
  76. self.active = False
  77. def detect_fire(self, image):
  78. """检测火灾
  79. 使用简单的颜色阈值方法检测火焰
  80. """
  81. try:
  82. # 转换到HSV颜色空间
  83. hsv = cv2.cvtColor(image, cv2.COLOR_BGR2HSV)
  84. # 定义火焰的颜色范围(红色和橙色)
  85. lower_red1 = np.array([0, 120, 70])
  86. upper_red1 = np.array([10, 255, 255])
  87. lower_red2 = np.array([170, 120, 70])
  88. upper_red2 = np.array([180, 255, 255])
  89. # 创建掩码
  90. mask1 = cv2.inRange(hsv, lower_red1, upper_red1)
  91. mask2 = cv2.inRange(hsv, lower_red2, upper_red2)
  92. mask = cv2.bitwise_or(mask1, mask2)
  93. # 计算火焰像素占比
  94. fire_ratio = np.sum(mask > 0) / (mask.shape[0] * mask.shape[1])
  95. # 如果火焰像素占比超过阈值,认为检测到火灾
  96. return fire_ratio > 0.01
  97. except Exception as e:
  98. print(f"火灾检测出错: {e}")
  99. return False
  100. def detect_animal(self, image):
  101. """检测动物
  102. 这里使用简单的运动检测作为示例
  103. 实际项目中应该使用更复杂的目标检测模型
  104. """
  105. try:
  106. # 在实际项目中,这里应该使用预训练的目标检测模型
  107. # 例如 YOLO 或 SSD
  108. # 这里仅作为示例返回模拟结果
  109. return None
  110. except Exception as e:
  111. print(f"动物检测出错: {e}")
  112. return None
  113. class GridCameraView(QWidget):
  114. """九宫格摄像头视图"""
  115. # 添加灾害检测信号
  116. fire_detected = pyqtSignal(str) # 火灾检测信号
  117. animal_detected = pyqtSignal(object, str, float) # 动物检测信号
  118. def __init__(self, parent=None):
  119. super().__init__(parent)
  120. self.cameras = {} # 存储摄像头对象
  121. self.current_layout = "grid" # grid或single
  122. self.active_cell = None
  123. # 创建图标
  124. self.grid_icon = create_icon_from_base64(GRID_ICON)
  125. self.single_icon = create_icon_from_base64(SINGLE_ICON)
  126. self.refresh_icon = create_icon_from_base64(REFRESH_ICON)
  127. self.init_ui()
  128. def init_ui(self):
  129. """初始化UI"""
  130. # 创建主布局
  131. self.main_layout = QVBoxLayout(self)
  132. self.main_layout.setContentsMargins(5, 5, 5, 5)
  133. self.main_layout.setSpacing(5)
  134. # 创建工具栏
  135. toolbar = QHBoxLayout()
  136. # 添加布局切换按钮
  137. self.layout_btn = QPushButton("切换视图")
  138. self.layout_btn.setIcon(self.grid_icon)
  139. self.layout_btn.clicked.connect(self.toggle_layout)
  140. toolbar.addWidget(self.layout_btn)
  141. # 添加摄像头选择下拉框
  142. self.camera_combo = QComboBox()
  143. self.camera_combo.addItem("所有摄像头")
  144. self.camera_combo.addItem("地面摄像头")
  145. self.camera_combo.addItem("无人机摄像头")
  146. self.camera_combo.currentIndexChanged.connect(self.on_camera_type_changed)
  147. toolbar.addWidget(QLabel("摄像头类型:"))
  148. toolbar.addWidget(self.camera_combo)
  149. toolbar.addStretch()
  150. # 添加刷新按钮
  151. refresh_btn = QPushButton("刷新")
  152. refresh_btn.setIcon(self.refresh_icon)
  153. refresh_btn.clicked.connect(self.refresh_cameras)
  154. toolbar.addWidget(refresh_btn)
  155. self.main_layout.addLayout(toolbar)
  156. # 创建九宫格容器
  157. self.grid_container = QWidget()
  158. self.grid_layout = QGridLayout(self.grid_container)
  159. self.grid_layout.setSpacing(5)
  160. # 创建9个摄像头格子
  161. self.cells = []
  162. for i in range(9):
  163. cell = CameraGridCell(i)
  164. cell.clicked.connect(self.on_cell_clicked)
  165. # 连接灾害检测信号
  166. cell.fire_detected.connect(self.fire_detected)
  167. cell.animal_detected.connect(self.animal_detected)
  168. self.cells.append(cell)
  169. self.grid_layout.addWidget(cell, i // 3, i % 3)
  170. self.main_layout.addWidget(self.grid_container)
  171. # 创建定时器用于更新画面
  172. self.update_timer = QTimer(self)
  173. self.update_timer.timeout.connect(self.update_frames)
  174. self.update_timer.start(33) # 约30fps
  175. # 初始化摄像头
  176. self.refresh_cameras()
  177. def toggle_layout(self):
  178. """切换布局模式"""
  179. if self.current_layout == "grid":
  180. self.current_layout = "single"
  181. self.layout_btn.setIcon(self.single_icon)
  182. # 隐藏除了活动格子之外的所有格子
  183. for i, cell in enumerate(self.cells):
  184. cell.setVisible(i == self.active_cell if self.active_cell is not None else i == 0)
  185. else:
  186. self.current_layout = "grid"
  187. self.layout_btn.setIcon(self.grid_icon)
  188. # 显示所有格子
  189. for cell in self.cells:
  190. cell.setVisible(True)
  191. def on_cell_clicked(self, index):
  192. """处理格子点击事件"""
  193. self.active_cell = index
  194. if self.current_layout == "grid":
  195. self.toggle_layout() # 切换到单视图模式
  196. def on_camera_type_changed(self, index):
  197. """处理摄像头类型切换"""
  198. self.refresh_cameras()
  199. def refresh_cameras(self):
  200. """刷新摄像头列表"""
  201. # 这里应该实现摄像头检测和连接逻辑
  202. # 示例:模拟9个摄像头
  203. self.cameras.clear()
  204. camera_type = self.camera_combo.currentText()
  205. if camera_type in ["所有摄像头", "地面摄像头"]:
  206. # 添加4个地面摄像头
  207. for i in range(4):
  208. try:
  209. cap = cv2.VideoCapture(i)
  210. if cap.isOpened():
  211. self.cameras[i] = {
  212. 'capture': cap,
  213. 'type': 'ground',
  214. 'name': f'地面摄像头 {i+1}'
  215. }
  216. except Exception as e:
  217. print(f"连接摄像头 {i} 失败: {e}")
  218. if camera_type in ["所有摄像头", "无人机摄像头"]:
  219. # 模拟5个无人机摄像头
  220. for i in range(5):
  221. self.cameras[i+4] = {
  222. 'capture': None, # 实际项目中应该连接到无人机视频流
  223. 'type': 'drone',
  224. 'name': f'无人机 {i+1}'
  225. }
  226. def update_frames(self):
  227. """更新所有摄像头画面"""
  228. if self.current_layout == "single" and self.active_cell is not None:
  229. # 单视图模式只更新活动格子
  230. self.update_cell(self.active_cell)
  231. else:
  232. # 网格模式更新所有格子
  233. for i in range(9):
  234. self.update_cell(i)
  235. def update_cell(self, index):
  236. """更新单个格子的画面"""
  237. if index in self.cameras:
  238. camera = self.cameras[index]
  239. if camera['type'] == 'ground' and camera['capture'] is not None:
  240. ret, frame = camera['capture'].read()
  241. if ret:
  242. self.cells[index].setImage(frame, camera)
  243. return
  244. elif camera['type'] == 'drone':
  245. # 模拟无人机画面
  246. frame = np.zeros((480, 640, 3), dtype=np.uint8)
  247. cv2.putText(frame, f"无人机 {index-3} 画面", (50, 240),
  248. cv2.FONT_HERSHEY_SIMPLEX, 1, (255, 255, 255), 2)
  249. self.cells[index].setImage(frame, camera)
  250. return
  251. # 如果没有摄像头或获取画面失败
  252. self.cells[index].setImage(None)
  253. def closeEvent(self, event):
  254. """关闭时释放摄像头资源"""
  255. self.update_timer.stop()
  256. for camera in self.cameras.values():
  257. if camera['type'] == 'ground' and camera['capture'] is not None:
  258. camera['capture'].release()
  259. super().closeEvent(event)