camera_view.py 35 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885
  1. import os
  2. import cv2
  3. import numpy as np
  4. from datetime import datetime
  5. from PyQt5.QtWidgets import (QWidget, QVBoxLayout, QHBoxLayout, QLabel,
  6. QPushButton, QComboBox, QToolBar, QAction,
  7. QGridLayout, QFrame, QSplitter, QFileDialog,
  8. QMessageBox)
  9. from PyQt5.QtCore import Qt, QTimer, pyqtSlot, pyqtSignal, QSize, QRect, QThread
  10. from PyQt5.QtGui import QImage, QPixmap, QIcon, QPainter, QPen, QColor, QFont
  11. import random
  12. import time
  13. import torch
  14. import torch.backends.cudnn as cudnn
  15. from pathlib import Path
  16. import sys
  17. # 添加项目根目录到系统路径
  18. FILE = Path(__file__).resolve()
  19. ROOT = FILE.parents[2] # YOLOv5 root directory
  20. if str(ROOT) not in sys.path:
  21. sys.path.append(str(ROOT))
  22. from models.common import DetectMultiBackend
  23. from models.yolo import Detect
  24. from utils.datasets import IMG_FORMATS, VID_FORMATS, LoadImages, LoadStreams
  25. from utils.general import (LOGGER, check_file, check_img_size, check_imshow, check_requirements, colorstr,
  26. increment_path, non_max_suppression, print_args, scale_coords, strip_optimizer, xyxy2xywh)
  27. from utils.plots import Annotator, colors, save_one_box
  28. from utils.torch_utils import select_device, time_sync
  29. class VideoThread(QThread):
  30. """视频处理线程,用于读取摄像头或视频文件"""
  31. update_frame = pyqtSignal(np.ndarray)
  32. update_detections = pyqtSignal(list)
  33. fire_detected = pyqtSignal(str) # 修改为发送区域信息的信号
  34. error_signal = pyqtSignal(str)
  35. def __init__(self, source=0):
  36. super().__init__()
  37. self.source = source
  38. self.running = False
  39. self.model = None
  40. self.device = select_device('0' if torch.cuda.is_available() else 'cpu')
  41. self.half = False
  42. self.stride = 32
  43. self.imgsz = [640, 640]
  44. self.current_region = "中央林场" # 添加默认区域
  45. self.init_model()
  46. def init_model(self):
  47. """初始化YOLOv5火焰检测模型"""
  48. try:
  49. # 获取项目根目录路径
  50. weights = str(ROOT / 'weights/best.pt')
  51. if not os.path.exists(weights):
  52. print(f"错误:模型文件 {weights} 不存在")
  53. self.model = None
  54. return
  55. # 加载模型
  56. self.model = DetectMultiBackend(weights, device=self.device)
  57. self.stride = self.model.stride
  58. self.imgsz = check_img_size(self.imgsz, s=self.stride) # 检查图片尺寸
  59. # 打印模型支持的类别
  60. print("模型支持的类别:", self.model.names)
  61. # 设置半精度
  62. self.half = self.device.type != 'cpu' # 仅在GPU上使用半精度
  63. if self.half:
  64. self.model.half() # 转换模型为半精度
  65. else:
  66. self.model.float() # 使用单精度
  67. # 确保Detect层有inplace属性
  68. for m in self.model.model.modules():
  69. if isinstance(m, Detect):
  70. if not hasattr(m, 'inplace'):
  71. m.inplace = True
  72. # 预热模型
  73. if self.device.type != 'cpu':
  74. dummy = torch.zeros(1, 3, *self.imgsz).to(self.device)
  75. dummy = dummy.half() if self.half else dummy.float()
  76. for _ in range(3): # 预热3次
  77. with torch.no_grad():
  78. self.model(dummy) # 预热推理
  79. torch.cuda.empty_cache() # 清理显存
  80. print(f"火焰检测模型加载成功:{weights}")
  81. # 设置CUDA性能优化
  82. if self.device.type != 'cpu':
  83. cudnn.benchmark = True # 加速固定大小图像的推理
  84. cudnn.deterministic = False # 提高速度
  85. except Exception as e:
  86. print(f"火焰检测模型加载失败: {str(e)}")
  87. import traceback
  88. traceback.print_exc()
  89. self.model = None
  90. self.error_signal.emit(f"模型加载失败: {str(e)}")
  91. def set_source(self, source):
  92. """设置视频源"""
  93. self.source = source
  94. def run(self):
  95. """运行线程,读取视频并进行检测"""
  96. if self.model is None:
  97. self.error_signal.emit("错误:模型未加载")
  98. return
  99. try:
  100. self.running = True
  101. # 初始化参数
  102. conf_thres = 0.25 # 置信度阈值
  103. iou_thres = 0.45 # NMS IOU阈值
  104. max_det = 1000 # 每张图片最大检测数量
  105. line_thickness = 2 # 减小边框线条粗细以提高性能
  106. hide_labels = False # 是否隐藏标签
  107. hide_conf = False # 是否隐藏置信度
  108. # 设置数据源
  109. source = str(self.source)
  110. webcam = source.isnumeric()
  111. # 检查视频源
  112. if webcam:
  113. try:
  114. source = int(source)
  115. cap = cv2.VideoCapture(source)
  116. if not cap.isOpened():
  117. self.error_signal.emit(f"无法打开摄像头 {source}")
  118. return
  119. cap.release()
  120. except Exception as e:
  121. self.error_signal.emit(f"摄像头初始化失败: {str(e)}")
  122. return
  123. else:
  124. if not os.path.exists(source):
  125. self.error_signal.emit(f"视频文件不存在: {source}")
  126. return
  127. try:
  128. cap = cv2.VideoCapture(source)
  129. if not cap.isOpened():
  130. self.error_signal.emit(f"无法打开视频文件: {source}")
  131. return
  132. # 检查视频是否可读
  133. ret, frame = cap.read()
  134. if not ret:
  135. self.error_signal.emit(f"无法读取视频帧: {source}")
  136. return
  137. cap.release()
  138. except Exception as e:
  139. self.error_signal.emit(f"视频文件打开失败: {str(e)}")
  140. return
  141. # 获取模型信息
  142. stride = self.model.stride
  143. names = self.model.names
  144. pt = getattr(self.model, 'pt', True)
  145. # 检查图像尺寸
  146. self.imgsz = check_img_size(self.imgsz, s=stride)
  147. try:
  148. # 设置数据加载器
  149. if webcam:
  150. cudnn.benchmark = True
  151. dataset = LoadStreams(str(source), img_size=self.imgsz[0], stride=stride, auto=pt)
  152. else:
  153. dataset = LoadImages(source, img_size=self.imgsz[0], stride=stride, auto=pt)
  154. except Exception as e:
  155. self.error_signal.emit(f"数据加载失败: {str(e)}")
  156. return
  157. # 处理每一帧
  158. for path, im, im0s, vid_cap, s in dataset:
  159. if not self.running:
  160. break
  161. try:
  162. # 预处理图像
  163. im = torch.from_numpy(im).to(self.device)
  164. im = im.half() if self.half else im.float()
  165. im /= 255
  166. if len(im.shape) == 3:
  167. im = im[None]
  168. # 推理
  169. with torch.no_grad():
  170. pred = self.model(im, augment=False) # 禁用数据增强以提高速度
  171. if isinstance(pred, (list, tuple)):
  172. pred = pred[0]
  173. # NMS
  174. pred = non_max_suppression(pred, conf_thres, iou_thres, None, False, max_det=max_det)
  175. # 处理检测结果
  176. for i, det in enumerate(pred):
  177. if webcam:
  178. im0 = im0s[i].copy()
  179. else:
  180. im0 = im0s.copy()
  181. s += '%gx%g ' % im.shape[2:]
  182. annotator = Annotator(im0, line_width=line_thickness, example=str(names))
  183. if len(det):
  184. # 将边界框从img_size缩放到im0大小
  185. det[:, :4] = scale_coords(im.shape[2:], det[:, :4], im0.shape).round()
  186. detections = []
  187. fire_detected = False
  188. for c in det[:, -1].unique():
  189. n = (det[:, -1] == c).sum()
  190. s += f"{n} {names[int(c)]}{'s' * (n > 1)}, "
  191. for *xyxy, conf, cls in reversed(det):
  192. c = int(cls)
  193. if conf > conf_thres: # 如果置信度大于阈值
  194. label = None if hide_labels else (
  195. names[c] if hide_conf else f'{names[c]} {conf:.2f}'
  196. )
  197. annotator.box_label(xyxy, label, color=colors(c, True))
  198. # 如果检测到火焰,发送信号
  199. if names[c].lower() == 'fire' and conf > 0.5: # 提高火焰检测的置信度阈值
  200. print(f"检测到火焰!类别:{names[c]},置信度:{conf:.2f}") # 添加调试输出
  201. self.fire_detected.emit(self.current_region) # 发送当前区域信息
  202. self.update_detections.emit(detections)
  203. im0 = annotator.result()
  204. self.update_frame.emit(im0)
  205. except Exception as e:
  206. print(f"处理帧时出错: {str(e)}")
  207. import traceback
  208. traceback.print_exc()
  209. continue
  210. self.msleep(10) # 减小延迟时间,提高帧率
  211. except Exception as e:
  212. print(f"视频处理线程出错: {str(e)}")
  213. import traceback
  214. traceback.print_exc()
  215. self.error_signal.emit(f"视频处理出错: {str(e)}")
  216. finally:
  217. self.running = False
  218. def draw_box(self, img, xyxy, label):
  219. """在图像上绘制边界框和标签"""
  220. x1, y1, x2, y2 = [int(x) for x in xyxy]
  221. color = (0, 0, 255) # 红色
  222. # 绘制边界框
  223. cv2.rectangle(img, (x1, y1), (x2, y2), color, 2)
  224. # 绘制标签背景
  225. text_size = cv2.getTextSize(label, cv2.FONT_HERSHEY_SIMPLEX, 0.5, 2)[0]
  226. cv2.rectangle(img, (x1, y1 - text_size[1] - 5), (x1 + text_size[0], y1), color, -1)
  227. # 绘制标签
  228. cv2.putText(img, label, (x1, y1 - 5), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (255, 255, 255), 2)
  229. return img
  230. def stop(self):
  231. """停止线程"""
  232. self.running = False
  233. self.wait()
  234. def preprocess_frame(self, frame):
  235. """预处理帧,用于模型输入"""
  236. # 缩放到模型所需尺寸
  237. resized = cv2.resize(frame, (640, 640))
  238. # 转换为RGB
  239. rgb = cv2.cvtColor(resized, cv2.COLOR_BGR2RGB)
  240. # 归一化
  241. normalized = rgb / 255.0
  242. # 转换为tensor
  243. img = torch.from_numpy(normalized).float()
  244. # 调整维度顺序 (H, W, C) -> (C, H, W)
  245. img = img.permute(2, 0, 1)
  246. # 添加批次维度
  247. img = img.unsqueeze(0)
  248. return img
  249. def mock_detections(self, frame):
  250. """模拟检测结果,用于开发阶段"""
  251. height, width = frame.shape[:2]
  252. # 模拟一些检测结果
  253. detections = [
  254. {
  255. 'task': 'fire',
  256. 'class': 0, # 火焰
  257. 'label': '火灾',
  258. 'confidence': 0.85,
  259. 'bbox': [width * 0.1, height * 0.2, width * 0.2, height * 0.3] # [x1, y1, x2, y2]
  260. },
  261. {
  262. 'task': 'animal',
  263. 'class': 2, # 某种动物
  264. 'label': '野生动物',
  265. 'confidence': 0.76,
  266. 'bbox': [width * 0.6, height * 0.5, width * 0.8, height * 0.7]
  267. },
  268. {
  269. 'task': 'pest',
  270. 'class': 3, # 病虫害
  271. 'label': '病虫害-松毛虫',
  272. 'confidence': 0.82,
  273. 'bbox': [width * 0.3, height * 0.4, width * 0.5, height * 0.6],
  274. 'subtype': '松毛虫',
  275. 'severity': '中度'
  276. }
  277. ]
  278. # 每秒随机变化一下位置,模拟运动
  279. random.seed(int(time.time()))
  280. for det in detections:
  281. # 随机移动边界框
  282. x1, y1, x2, y2 = det['bbox']
  283. dx = random.uniform(-10, 10)
  284. dy = random.uniform(-10, 10)
  285. # 确保边界框在图像内
  286. x1 = max(0, min(width - 10, x1 + dx))
  287. y1 = max(0, min(height - 10, y1 + dy))
  288. x2 = max(x1 + 10, min(width, x2 + dx))
  289. y2 = max(y1 + 10, min(height, y2 + dy))
  290. det['bbox'] = [x1, y1, x2, y2]
  291. return detections
  292. def draw_detections(self, frame, detections):
  293. """在帧上绘制检测结果"""
  294. for det in detections:
  295. # 获取边界框和标签
  296. x1, y1, x2, y2 = [int(c) for c in det['bbox']]
  297. label = f"{det['label']} {det['confidence']:.2f}"
  298. # 根据任务选择颜色
  299. if det['task'] == 'fire':
  300. color = (0, 0, 255) # 红色
  301. elif det['task'] == 'animal':
  302. color = (0, 255, 0) # 绿色
  303. elif det['task'] == 'landslide':
  304. color = (255, 0, 0) # 蓝色
  305. elif det['task'] == 'pest':
  306. color = (128, 0, 128) # 紫色
  307. else:
  308. color = (255, 255, 0) # 青色
  309. # 绘制边界框
  310. cv2.rectangle(frame, (x1, y1), (x2, y2), color, 2)
  311. # 绘制标签背景
  312. text_size, _ = cv2.getTextSize(label, cv2.FONT_HERSHEY_SIMPLEX, 0.5, 2)
  313. cv2.rectangle(frame, (x1, y1 - text_size[1] - 5), (x1 + text_size[0], y1), color, -1)
  314. # 绘制标签
  315. cv2.putText(frame, label, (x1, y1 - 5), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (255, 255, 255), 2)
  316. # 如果是病虫害,添加额外信息
  317. if det['task'] == 'pest' and 'subtype' in det:
  318. severity_text = f"类型: {det['subtype']}"
  319. if 'severity' in det:
  320. severity_text += f" | 严重程度: {det['severity']}"
  321. cv2.putText(frame, severity_text, (x1, y1 - 25), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (255, 255, 255), 1)
  322. return frame
  323. class CameraView(QWidget):
  324. """摄像头视图组件,用于显示实时视频流和检测结果"""
  325. fire_detected = pyqtSignal(str) # 修改为发送区域信息的信号
  326. def __init__(self, config=None):
  327. super().__init__()
  328. self.config = config
  329. self.detection_results = []
  330. self.current_source = '0' # 默认使用摄像头
  331. self.output_size = 400
  332. self.current_region = "中央林场" # 默认区域
  333. # 修改设备选择逻辑
  334. try:
  335. if torch.cuda.is_available():
  336. self.device = select_device('0')
  337. else:
  338. self.device = select_device('cpu')
  339. except Exception as e:
  340. print(f"GPU初始化失败,使用CPU: {str(e)}")
  341. self.device = select_device('cpu')
  342. # 创建视频处理线程
  343. self.video_thread = VideoThread(self.current_source)
  344. self.video_thread.update_frame.connect(self.update_frame)
  345. self.video_thread.update_detections.connect(self.update_detections)
  346. self.video_thread.fire_detected.connect(self.on_fire_detected)
  347. self.video_thread.error_signal.connect(self.on_error)
  348. # 创建结果保存目录
  349. self.results_dir = str(ROOT / 'results')
  350. os.makedirs(self.results_dir, exist_ok=True)
  351. self.init_ui()
  352. def init_ui(self):
  353. """初始化UI"""
  354. # 创建主布局
  355. layout = QVBoxLayout(self)
  356. layout.setContentsMargins(0, 0, 0, 0)
  357. # 创建顶部工具栏布局
  358. toolbar_layout = QHBoxLayout()
  359. # 添加摄像头选择下拉框
  360. self.camera_combo = QComboBox()
  361. self.camera_combo.addItem("摄像头0", '0')
  362. self.camera_combo.addItem("摄像头1", '1')
  363. self.camera_combo.addItem("视频文件", '-1')
  364. self.camera_combo.currentIndexChanged.connect(self.change_camera)
  365. toolbar_layout.addWidget(QLabel("视频源:"))
  366. toolbar_layout.addWidget(self.camera_combo)
  367. # 添加空白占位
  368. toolbar_layout.addStretch(1)
  369. # 添加截图按钮
  370. self.snapshot_btn = QPushButton("截图")
  371. self.snapshot_btn.setIcon(QIcon(str(ROOT / 'ui/assets/snapshot.png')))
  372. self.snapshot_btn.clicked.connect(self.take_snapshot)
  373. toolbar_layout.addWidget(self.snapshot_btn)
  374. # 添加工具栏到布局
  375. layout.addLayout(toolbar_layout)
  376. # 创建视频显示区域
  377. self.video_frame = QLabel()
  378. self.video_frame.setAlignment(Qt.AlignCenter)
  379. self.video_frame.setMinimumSize(640, 480)
  380. self.video_frame.setStyleSheet("background-color: black;")
  381. # 添加视频帧到布局
  382. layout.addWidget(self.video_frame, 1)
  383. # 创建状态栏
  384. status_bar = QHBoxLayout()
  385. # 添加状态信息
  386. self.status_label = QLabel("状态: 未启动")
  387. status_bar.addWidget(self.status_label)
  388. # 添加FPS信息
  389. self.fps_label = QLabel("FPS: 0")
  390. status_bar.addWidget(self.fps_label)
  391. # 添加检测结果信息
  392. self.detection_label = QLabel("检测结果: 0")
  393. status_bar.addWidget(self.detection_label)
  394. # 添加时间戳
  395. self.timestamp_label = QLabel(datetime.now().strftime("%Y-%m-%d %H:%M:%S"))
  396. status_bar.addWidget(self.timestamp_label, 1, Qt.AlignRight)
  397. # 添加状态栏到布局
  398. layout.addLayout(status_bar)
  399. # 创建定时器更新时间戳
  400. self.timer = QTimer(self)
  401. self.timer.timeout.connect(self.update_timestamp)
  402. self.timer.start(1000) # 每秒更新一次
  403. # 计算FPS的变量
  404. self.frame_count = 0
  405. self.fps = 0
  406. self.fps_timer = QTimer(self)
  407. self.fps_timer.timeout.connect(self.calculate_fps)
  408. self.fps_timer.start(1000) # 每秒计算一次FPS
  409. def update_frame(self, frame):
  410. """更新视频帧"""
  411. # 计算帧数
  412. self.frame_count += 1
  413. # 转换OpenCV的BGR格式为RGB
  414. rgb_frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
  415. # 转换为QImage
  416. h, w, ch = rgb_frame.shape
  417. bytes_per_line = ch * w
  418. qt_image = QImage(rgb_frame.data, w, h, bytes_per_line, QImage.Format_RGB888)
  419. # 缩放到显示区域大小
  420. pixmap = QPixmap.fromImage(qt_image)
  421. pixmap = pixmap.scaled(self.video_frame.size(), Qt.KeepAspectRatio, Qt.SmoothTransformation)
  422. # 设置图像
  423. self.video_frame.setPixmap(pixmap)
  424. def update_detections(self, detections):
  425. """更新检测结果"""
  426. self.detection_results = detections
  427. self.detection_label.setText(f"检测结果: {len(detections)}")
  428. def update_timestamp(self):
  429. """更新时间戳"""
  430. self.timestamp_label.setText(datetime.now().strftime("%Y-%m-%d %H:%M:%S"))
  431. def calculate_fps(self):
  432. """计算FPS"""
  433. self.fps = self.frame_count
  434. self.fps_label.setText(f"FPS: {self.fps}")
  435. self.frame_count = 0
  436. def update_view(self):
  437. """更新视图(由外部定时器调用)"""
  438. # 由于视频帧由线程自动更新,这里主要更新其他UI元素
  439. if self.video_thread.isRunning():
  440. self.status_label.setText("状态: 监控中")
  441. else:
  442. self.status_label.setText("状态: 已停止")
  443. def start_monitoring(self):
  444. """开始监控"""
  445. if not self.video_thread.isRunning():
  446. self.video_thread.start()
  447. self.status_label.setText("状态: 监控中")
  448. def stop_monitoring(self):
  449. """停止监控"""
  450. if self.video_thread.isRunning():
  451. self.video_thread.stop()
  452. self.status_label.setText("状态: 已停止")
  453. @pyqtSlot(int)
  454. def change_camera(self, index):
  455. """改变摄像头"""
  456. # 获取选择的摄像头
  457. source = self.camera_combo.currentData()
  458. # 如果选择了视频文件
  459. if source == '-1':
  460. fileName, _ = QFileDialog.getOpenFileName(
  461. self,
  462. "选择视频文件",
  463. "",
  464. "视频文件 (*.mp4 *.avi *.mkv *.mov);;所有文件 (*.*)"
  465. )
  466. if not fileName:
  467. # 如果用户取消选择,恢复到之前的选项
  468. self.camera_combo.setCurrentIndex(0)
  469. return
  470. source = fileName
  471. # 停止当前视频
  472. self.stop_monitoring()
  473. # 设置新的视频源
  474. self.current_source = source
  475. self.video_thread.set_source(source)
  476. # 重新开始监控
  477. self.start_monitoring()
  478. @pyqtSlot()
  479. def take_snapshot(self):
  480. """截取当前帧"""
  481. # 获取当前显示的图像
  482. pixmap = self.video_frame.pixmap()
  483. if pixmap and not pixmap.isNull():
  484. # 创建保存目录
  485. snapshots_dir = os.path.join(self.results_dir, 'snapshots')
  486. os.makedirs(snapshots_dir, exist_ok=True)
  487. # 保存图像
  488. timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
  489. file_path = os.path.join(snapshots_dir, f"snapshot_{timestamp}.jpg")
  490. if pixmap.save(file_path, "JPG"):
  491. self.status_label.setText(f"状态: 已保存截图 {file_path}")
  492. else:
  493. self.status_label.setText("状态: 截图保存失败")
  494. def resizeEvent(self, event):
  495. """窗口大小改变事件"""
  496. # 如果有图像,重新缩放
  497. if self.video_frame.pixmap() and not self.video_frame.pixmap().isNull():
  498. pixmap = self.video_frame.pixmap().scaled(
  499. self.video_frame.size(),
  500. Qt.KeepAspectRatio,
  501. Qt.SmoothTransformation
  502. )
  503. self.video_frame.setPixmap(pixmap)
  504. def on_fire_detected(self, region):
  505. """处理火焰检测信号"""
  506. # 转发信号到主窗口,包含区域信息
  507. self.fire_detected.emit(region)
  508. def on_error(self, error_msg):
  509. """处理错误信息"""
  510. self.status_label.setText(f"状态: {error_msg}")
  511. QMessageBox.warning(self, "错误", error_msg)
  512. self.reset_camera()
  513. def reset_camera(self):
  514. """重置摄像头状态"""
  515. self.stop_monitoring()
  516. self.camera_combo.setCurrentIndex(0) # 切换回默认摄像头
  517. self.video_frame.clear() # 清空视频显示
  518. self.video_frame.setStyleSheet("background-color: black;")
  519. def detect_vid(self):
  520. """视频检测函数"""
  521. try:
  522. # 初始化模型参数
  523. model = self.model
  524. output_size = self.output_size
  525. imgsz = [640, 640] # 推理时的输入图像尺寸(像素)
  526. conf_thres = 0.25 # 置信度阈值
  527. iou_thres = 0.45 # NMS(非极大值抑制)IOU阈值
  528. max_det = 1000 # 每张图片最大检测数
  529. # 设备选择逻辑
  530. try:
  531. if torch.cuda.is_available():
  532. device = select_device('0')
  533. half = True # 在GPU上使用半精度
  534. else:
  535. device = select_device('cpu')
  536. half = False # CPU不使用半精度
  537. except Exception as e:
  538. print(f"GPU初始化失败,使用CPU: {str(e)}")
  539. device = select_device('cpu')
  540. half = False
  541. view_img = False # 是否显示检测结果
  542. save_txt = False # 是否保存结果到*.txt文件
  543. save_conf = False # 是否保存置信度到标签文件
  544. save_crop = False # 是否保存裁剪后的预测框图像
  545. nosave = False # 是否禁用保存图像/视频
  546. classes = None # 按类别过滤:--class 0,或者--class 0 2 3
  547. agnostic_nms = False # 是否使用类别无关的NMS
  548. augment = False # 是否进行增强推理
  549. visualize = False # 是否可视化特征
  550. line_thickness = 3 # 边框的厚度(像素)
  551. hide_labels = False # 是否隐藏标签
  552. hide_conf = False # 是否隐藏置信度
  553. dnn = False # 是否使用OpenCV DNN进行ONNX推理
  554. source = str(self.vid_source) # 设置视频源(文件/目录)
  555. webcam = self.webcam # 是否使用摄像头
  556. device = select_device(self.device) # 选择推理设备
  557. # 获取模型信息
  558. stride = model.stride
  559. names = model.names
  560. pt = getattr(model, 'pt', True)
  561. # 检查图像尺寸
  562. imgsz = check_img_size(imgsz, s=stride) # 检查图像尺寸是否合适
  563. # 检查视频源
  564. if webcam:
  565. try:
  566. source = int(source)
  567. cap = cv2.VideoCapture(source)
  568. if not cap.isOpened():
  569. self.error_signal.emit(f"无法打开摄像头 {source}")
  570. return
  571. cap.release()
  572. except Exception as e:
  573. self.error_signal.emit(f"摄像头初始化失败: {str(e)}")
  574. return
  575. else:
  576. if not os.path.exists(source):
  577. self.error_signal.emit(f"视频文件不存在: {source}")
  578. return
  579. try:
  580. cap = cv2.VideoCapture(source)
  581. if not cap.isOpened():
  582. self.error_signal.emit(f"无法打开视频文件: {source}")
  583. return
  584. ret, frame = cap.read()
  585. if not ret:
  586. self.error_signal.emit(f"无法读取视频帧: {source}")
  587. return
  588. cap.release()
  589. except Exception as e:
  590. self.error_signal.emit(f"视频文件打开失败: {str(e)}")
  591. return
  592. try:
  593. # 设置数据加载器
  594. if webcam:
  595. cudnn.benchmark = True # 设置为True以加速恒定图像大小的推理
  596. dataset = LoadStreams(str(source), img_size=imgsz[0], stride=stride, auto=pt)
  597. else:
  598. dataset = LoadImages(source, img_size=imgsz[0], stride=stride, auto=pt)
  599. except Exception as e:
  600. self.error_signal.emit(f"数据加载失败: {str(e)}")
  601. return
  602. # 预热模型
  603. if pt and device.type != 'cpu':
  604. model(torch.zeros(1, 3, *imgsz).to(device).type_as(next(model.parameters())))
  605. # 处理每一帧
  606. for path, im, im0s, vid_cap, s in dataset:
  607. try:
  608. # 预处理
  609. im = torch.from_numpy(im).to(device)
  610. im = im.half() if half else im.float() # uint8 转为 fp16/32
  611. im /= 255 # 将像素值从0-255归一化到0.0-1.0
  612. if len(im.shape) == 3:
  613. im = im[None] # 扩展维度以符合batch size要求
  614. # 推理过程
  615. with torch.no_grad():
  616. pred = model(im, augment=augment, visualize=visualize)
  617. if isinstance(pred, (list, tuple)):
  618. pred = pred[0]
  619. # NMS非极大值抑制
  620. pred = non_max_suppression(pred, conf_thres, iou_thres, classes, agnostic_nms, max_det=max_det)
  621. # 处理检测结果
  622. for i, det in enumerate(pred):
  623. if webcam: # 如果是摄像头流
  624. im0 = im0s[i].copy()
  625. else:
  626. im0 = im0s.copy()
  627. # 初始化标注器
  628. annotator = Annotator(im0, line_width=line_thickness, example=str(names))
  629. if len(det):
  630. # 将边框坐标从img_size映射到原始图像尺寸
  631. det[:, :4] = scale_coords(im.shape[2:], det[:, :4], im0.shape).round()
  632. # 处理每个检测结果
  633. for *xyxy, conf, cls in reversed(det):
  634. c = int(cls)
  635. if conf > conf_thres: # 如果置信度大于阈值
  636. label = None if hide_labels else (
  637. names[c] if hide_conf else f'{names[c]} {conf:.2f}'
  638. )
  639. annotator.box_label(xyxy, label, color=colors(c, True))
  640. # 如果检测到火焰,发送信号
  641. if names[c].lower() == 'fire' and conf > 0.5: # 提高火焰检测的置信度阈值
  642. print(f"检测到火焰!类别:{names[c]},置信度:{conf:.2f}") # 添加调试输出
  643. self.fire_detected.emit(self.current_region) # 发送当前区域信息
  644. # 获取标注后的图像
  645. im0 = annotator.result()
  646. # 调整图像大小并显示
  647. resize_scale = output_size / im0.shape[0]
  648. im0 = cv2.resize(im0, (0, 0), fx=resize_scale, fy=resize_scale)
  649. # 转换为Qt图像并显示
  650. rgb_image = cv2.cvtColor(im0, cv2.COLOR_BGR2RGB)
  651. h, w, ch = rgb_image.shape
  652. qt_image = QImage(rgb_image.data, w, h, w * ch, QImage.Format_RGB888)
  653. self.vid_img.setPixmap(QPixmap.fromImage(qt_image))
  654. except Exception as e:
  655. print(f"处理帧时出错: {str(e)}")
  656. import traceback
  657. traceback.print_exc()
  658. continue
  659. # 检查是否需要停止
  660. if self.stopEvent.is_set():
  661. break
  662. self.msleep(10) # 控制帧率
  663. except Exception as e:
  664. print(f"视频处理出错: {str(e)}")
  665. import traceback
  666. traceback.print_exc()
  667. self.error_signal.emit(f"视频处理出错: {str(e)}")
  668. finally:
  669. self.reset_vid()
  670. class MockDetector:
  671. """模拟检测器类,用于生成模拟检测结果"""
  672. def __init__(self):
  673. """初始化检测器"""
  674. self.detection_mode = "all" # 默认检测所有类型
  675. def set_mode(self, mode):
  676. """设置检测模式"""
  677. self.detection_mode = mode
  678. def detect(self, frame):
  679. """在图像上执行检测"""
  680. # 实际项目中,这里应该调用真实的检测模型
  681. # 这里仅做模拟,随机生成检测结果
  682. height, width = frame.shape[:2]
  683. detections = []
  684. # 随机决定是否生成检测结果
  685. if random.random() < 0.2: # 20%概率生成检测
  686. # 可检测的类型
  687. types = {
  688. 'all': ['fire', 'animal', 'landslide', 'forest', 'pest'],
  689. 'fire': ['fire'],
  690. 'animal': ['animal'],
  691. 'landslide': ['landslide'],
  692. 'forest': ['forest'],
  693. 'pest': ['pest']
  694. }
  695. # 根据检测模式选择可能的检测类型
  696. possible_types = types.get(self.detection_mode, ['fire'])
  697. # 随机选择一种类型
  698. detection_type = random.choice(possible_types)
  699. # 类型对应的标签和颜色
  700. labels = {
  701. 'fire': '火灾',
  702. 'animal': '野生动物',
  703. 'landslide': '滑坡',
  704. 'forest': '森林退化',
  705. 'pest': '病虫害'
  706. }
  707. colors = {
  708. 'fire': (0, 0, 255), # 红色
  709. 'animal': (0, 255, 0), # 绿色
  710. 'landslide': (255, 0, 0), # 蓝色
  711. 'forest': (255, 255, 0), # 青色
  712. 'pest': (128, 0, 128) # 紫色
  713. }
  714. # 对于病虫害类型,生成更详细的标签
  715. if detection_type == 'pest':
  716. pest_types = ['松毛虫', '美国白蛾', '落叶松毛虫', '杨树食叶害虫', '松材线虫病']
  717. pest_subtype = random.choice(pest_types)
  718. label = f"{labels[detection_type]}-{pest_subtype}"
  719. else:
  720. label = labels[detection_type]
  721. # 随机生成边界框
  722. x = random.randint(10, width - 100)
  723. y = random.randint(10, height - 100)
  724. w = random.randint(50, 150)
  725. h = random.randint(50, 150)
  726. # 随机生成置信度
  727. confidence = random.uniform(0.65, 0.95)
  728. # 创建检测结果
  729. detection = {
  730. 'type': detection_type,
  731. 'bbox': [x, y, x+w, y+h],
  732. 'confidence': confidence,
  733. 'label': label,
  734. 'color': colors[detection_type]
  735. }
  736. detections.append(detection)
  737. return detections