detection_visualizer.py 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425
  1. import pandas as pd
  2. import numpy as np
  3. import matplotlib.pyplot as plt
  4. import cv2
  5. import os
  6. import re
  7. import argparse
  8. from typing import Dict, List, Tuple, Optional
  9. class DetectionVisualizer:
  10. """在原始图像上直接绘制目标检测框,并按摄像头ID分组"""
  11. def __init__(self, csv_path: str, output_dir: str = None):
  12. """初始化检测框可视化器
  13. Args:
  14. csv_path: 检测报告CSV文件路径
  15. output_dir: 输出目录,默认为CSV文件所在目录
  16. """
  17. self.csv_path = csv_path
  18. self.data = None
  19. self.camera_data = {}
  20. # 设置输出目录
  21. if output_dir is None:
  22. self.output_dir = os.path.join(os.path.dirname(csv_path), "visualization_results")
  23. else:
  24. self.output_dir = output_dir
  25. os.makedirs(self.output_dir, exist_ok=True)
  26. def load_data(self) -> None:
  27. """加载CSV数据"""
  28. try:
  29. self.data = pd.read_csv(self.csv_path)
  30. print(f"成功加载数据,共 {len(self.data)} 条记录")
  31. except Exception as e:
  32. print(f"加载CSV文件失败: {e}")
  33. raise
  34. def extract_camera_id(self, file_path: str) -> str:
  35. """从文件路径中提取摄像头ID
  36. Args:
  37. file_path: 图像文件路径
  38. Returns:
  39. 摄像头ID
  40. """
  41. # 尝试匹配常见的摄像头ID模式
  42. # 例如: cam_08_18 或 08_18
  43. patterns = [
  44. r'cam_(\d+)_(\d+)', # 匹配 cam_08_18 格式
  45. r'(\d+)_(\d+)_cam_(\d+)_(\d+)', # 匹配 192_168_210_2_cam_08_18 格式
  46. ]
  47. for pattern in patterns:
  48. match = re.search(pattern, file_path)
  49. if match:
  50. if pattern == patterns[0]:
  51. return f"cam_{match.group(1)}_{match.group(2)}"
  52. elif pattern == patterns[1]:
  53. return f"cam_{match.group(3)}_{match.group(4)}"
  54. # 如果无法提取,返回文件名作为ID
  55. return os.path.basename(file_path).split('.')[0]
  56. def process_data(self) -> None:
  57. """处理数据,按摄像头ID分组"""
  58. if self.data is None:
  59. self.load_data()
  60. # 提取摄像头ID并分组
  61. self.data['Camera ID'] = self.data['Image File'].apply(self.extract_camera_id)
  62. # 按摄像头ID分组
  63. for camera_id, group in self.data.groupby('Camera ID'):
  64. self.camera_data[camera_id] = group
  65. detection_count = len(group[group['Object Count'] > 0])
  66. print(f"摄像头 {camera_id}: {len(group)} 个图像, {detection_count} 个检测结果")
  67. def draw_detection_on_image(self, image_path: str, detections: pd.DataFrame, resolution: Tuple[int, int] = None) -> np.ndarray:
  68. """在单个图像上绘制检测框
  69. Args:
  70. image_path: 图像文件路径
  71. detections: 该图像的检测数据
  72. resolution: 图像分辨率,如果为None则使用图像实际分辨率
  73. Returns:
  74. 绘制了检测框的图像
  75. """
  76. try:
  77. # 读取图像
  78. image = cv2.imread(image_path)
  79. if image is None:
  80. print(f"无法读取图像: {image_path}")
  81. return None
  82. # 转换为RGB格式(OpenCV默认为BGR)
  83. image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)
  84. # 获取图像实际分辨率
  85. actual_height, actual_width = image.shape[:2]
  86. if resolution is None:
  87. resolution = (actual_width, actual_height)
  88. # 创建一个透明度图层用于叠加检测框
  89. overlay = image.copy()
  90. # 绘制所有检测框
  91. detection_count = 0
  92. for _, row in detections.iterrows():
  93. if row['Object Count'] <= 0:
  94. continue
  95. try:
  96. # 获取归一化坐标和宽高
  97. if 'Normalized Coordinates' in row and pd.notna(row['Normalized Coordinates']):
  98. try:
  99. norm_coords = row['Normalized Coordinates'].split(',')
  100. if len(norm_coords) >= 4: # 中心点x,y,宽,高
  101. norm_x, norm_y, norm_w, norm_h = map(float, norm_coords[:4])
  102. else:
  103. continue
  104. except (ValueError, IndexError):
  105. continue
  106. else:
  107. # 如果没有归一化坐标,使用中心点坐标和宽高计算
  108. if (pd.notna(row.get('BBox Center X')) and pd.notna(row.get('BBox Center Y')) and
  109. pd.notna(row.get('BBox Width')) and pd.notna(row.get('BBox Height'))):
  110. center_x = row['BBox Center X']
  111. center_y = row['BBox Center Y']
  112. width = row['BBox Width']
  113. height = row['BBox Height']
  114. norm_x = center_x / resolution[0]
  115. norm_y = center_y / resolution[1]
  116. norm_w = width / resolution[0]
  117. norm_h = height / resolution[1]
  118. else:
  119. continue
  120. # 将归一化坐标转换为像素坐标
  121. center_x = int(norm_x * resolution[0])
  122. center_y = int(norm_y * resolution[1])
  123. width = int(norm_w * resolution[0])
  124. height = int(norm_h * resolution[1])
  125. # 计算左上角和右下角坐标
  126. x1 = max(0, int(center_x - width / 2))
  127. y1 = max(0, int(center_y - height / 2))
  128. x2 = min(resolution[0] - 1, int(center_x + width / 2))
  129. y2 = min(resolution[1] - 1, int(center_y + height / 2))
  130. # 绘制白色边框轮廓
  131. cv2.rectangle(overlay, (x1, y1), (x2, y2), (255, 255, 255), 3)
  132. # 绘制主黄色检测框
  133. cv2.rectangle(overlay, (x1, y1), (x2, y2), (0, 255, 255), 3)
  134. # 在右下角添加带背景的置信度显示
  135. if 'Max Confidence' in row and pd.notna(row['Max Confidence']):
  136. confidence = float(row['Max Confidence'])
  137. text = f'Conf: {confidence:.2f}'
  138. (text_width, text_height), baseline = cv2.getTextSize(text, cv2.FONT_HERSHEY_SIMPLEX, 0.5, 1)
  139. text_x = x2 - text_width - 5
  140. text_y = y2 - 5
  141. # 绘制文本背景
  142. cv2.rectangle(overlay, (text_x, text_y - text_height), (text_x + text_width, text_y), (255, 255, 255), -1)
  143. # 绘制文本
  144. cv2.putText(overlay, text, (text_x, text_y), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 0, 0), 1)
  145. detection_count += 1
  146. except Exception as e:
  147. print(f"处理检测框时出错: {e}")
  148. # 设置透明度
  149. alpha = 0.6
  150. output_image = cv2.addWeighted(overlay, alpha, image, 1 - alpha, 0)
  151. return output_image, detection_count
  152. except Exception as e:
  153. print(f"处理图像时出错: {e}")
  154. return None, 0
  155. def visualize_camera_detections(self, camera_id: str) -> None:
  156. """可视化单个摄像头的所有检测结果
  157. Args:
  158. camera_id: 摄像头ID
  159. """
  160. if not self.camera_data:
  161. self.process_data()
  162. if camera_id not in self.camera_data:
  163. print(f"未找到摄像头 {camera_id} 的数据")
  164. return
  165. # 获取该摄像头的所有数据
  166. cam_data = self.camera_data[camera_id]
  167. detection_data = cam_data[cam_data['Object Count'] > 0]
  168. if len(detection_data) == 0:
  169. print(f"摄像头 {camera_id} 没有检测结果")
  170. return
  171. print(f"处理摄像头 {camera_id} 的 {len(detection_data)} 个检测结果...")
  172. # 创建一个大图像,用于显示所有检测结果
  173. # 首先选择一个图像作为背景
  174. sample_image_path = detection_data.iloc[0]['Image File']
  175. # 检查文件路径是否存在,如果是相对路径则转换为绝对路径
  176. if not os.path.isabs(sample_image_path):
  177. # 尝试从CSV文件所在目录解析相对路径
  178. base_dir = os.path.dirname(self.csv_path)
  179. sample_image_path = os.path.join(base_dir, sample_image_path)
  180. # 如果文件不存在,尝试修复路径
  181. if not os.path.exists(sample_image_path):
  182. # 尝试从CSV文件中提取的路径中获取文件名
  183. file_name = os.path.basename(sample_image_path)
  184. # 系统化搜索上级目录
  185. current_dir = os.path.dirname(self.csv_path)
  186. max_depth = 3
  187. for _ in range(max_depth):
  188. current_dir = os.path.dirname(current_dir)
  189. for root, _, files in os.walk(current_dir):
  190. if file_name in files:
  191. sample_image_path = os.path.join(root, file_name)
  192. print(f"找到匹配图像: {sample_image_path}")
  193. break
  194. if os.path.exists(sample_image_path):
  195. break
  196. # 如果仍然找不到文件,报错并返回
  197. if not os.path.exists(sample_image_path):
  198. print(f"无法找到图像文件: {sample_image_path}")
  199. return
  200. # 读取并验证样本图像
  201. sample_image = cv2.imread(sample_image_path)
  202. if sample_image is None:
  203. print(f"无法读取图像: {sample_image_path},请检查文件格式(支持JPG/PNG)")
  204. return
  205. # 转换颜色空间并创建副本
  206. sample_image = cv2.cvtColor(sample_image, cv2.COLOR_BGR2RGB)
  207. print(f"成功加载底图图像,分辨率: {sample_image.shape[1]}x{sample_image.shape[0]}")
  208. height, width = sample_image.shape[:2]
  209. # 创建一个合成图像,将所有检测框绘制在同一张图上
  210. # 使用样本图像作为背景
  211. composite_image = sample_image.copy()
  212. # 处理每个检测结果
  213. total_detections = 0
  214. for _, row in detection_data.iterrows():
  215. image_path = row['Image File']
  216. # 检查文件路径是否存在,如果是相对路径则转换为绝对路径
  217. if not os.path.isabs(image_path):
  218. # 尝试从CSV文件所在目录解析相对路径
  219. base_dir = os.path.dirname(self.csv_path)
  220. image_path = os.path.join(base_dir, image_path)
  221. # 如果文件不存在,尝试修复路径
  222. if not os.path.exists(image_path):
  223. # 尝试从CSV文件中提取的路径中获取文件名
  224. file_name = os.path.basename(image_path)
  225. # 在CSV文件所在目录的上级目录中查找
  226. parent_dir = os.path.dirname(os.path.dirname(self.csv_path))
  227. for root, _, files in os.walk(parent_dir):
  228. if file_name in files:
  229. image_path = os.path.join(root, file_name)
  230. break
  231. # 如果仍然找不到文件,跳过此检测结果
  232. if not os.path.exists(image_path):
  233. print(f"无法找到图像文件: {image_path},跳过此检测结果")
  234. continue
  235. # 绘制检测框
  236. try:
  237. # 获取归一化坐标和宽高
  238. if 'Normalized Coordinates' in row and pd.notna(row['Normalized Coordinates']):
  239. try:
  240. norm_coords = row['Normalized Coordinates'].split(',')
  241. if len(norm_coords) >= 4: # 中心点x,y,宽,高
  242. norm_x, norm_y, norm_w, norm_h = map(float, norm_coords[:4])
  243. else:
  244. continue
  245. except (ValueError, IndexError):
  246. continue
  247. else:
  248. # 如果没有归一化坐标,使用中心点坐标和宽高计算
  249. if (pd.notna(row.get('BBox Center X')) and pd.notna(row.get('BBox Center Y')) and
  250. pd.notna(row.get('BBox Width')) and pd.notna(row.get('BBox Height'))):
  251. center_x = row['BBox Center X']
  252. center_y = row['BBox Center Y']
  253. width_px = row['BBox Width']
  254. height_px = row['BBox Height']
  255. norm_x = center_x / width
  256. norm_y = center_y / height
  257. norm_w = width_px / width
  258. norm_h = height_px / height
  259. else:
  260. continue
  261. # 将归一化坐标转换为像素坐标
  262. center_x = int(norm_x * width)
  263. center_y = int(norm_y * height)
  264. width_px = int(norm_w * width)
  265. height_px = int(norm_h * height)
  266. # 计算左上角和右下角坐标
  267. x1 = max(0, int(center_x - width_px / 2))
  268. y1 = max(0, int(center_y - height_px / 2))
  269. x2 = min(width - 1, int(center_x + width_px / 2))
  270. y2 = min(height - 1, int(center_y + height_px / 2))
  271. # 在合成图像上绘制检测框
  272. # 使用不同的颜色表示不同的置信度
  273. confidence = float(row['Max Confidence']) if pd.notna(row.get('Max Confidence')) else 0.5
  274. # 颜色从蓝色(低置信度)到红色(高置信度)
  275. color = (
  276. int(255 * (1 - confidence)), # B
  277. 0, # G
  278. int(255 * confidence) # R
  279. )
  280. cv2.rectangle(composite_image, (x1, y1), (x2, y2), color, 1)
  281. # 在检测框中心绘制一个小点
  282. cv2.circle(composite_image, (center_x, center_y), 2, color, -1)
  283. total_detections += 1
  284. except Exception as e:
  285. print(f"处理检测框时出错: {e}")
  286. # 保存合成图像
  287. output_path = os.path.join(self.output_dir, f"{camera_id}_all_detections.png")
  288. cv2.imwrite(output_path, composite_image)
  289. print(f"已生成摄像头 {camera_id} 的检测框合成图像,包含 {total_detections} 个检测框: {output_path}")
  290. # 使用matplotlib创建热力图
  291. plt.figure(figsize=(width/100, height/100), dpi=100)
  292. # 提取所有检测框的中心点
  293. centers_x = []
  294. centers_y = []
  295. for _, row in detection_data.iterrows():
  296. try:
  297. if 'Normalized Coordinates' in row and pd.notna(row['Normalized Coordinates']):
  298. norm_coords = row['Normalized Coordinates'].split(',')
  299. if len(norm_coords) >= 4:
  300. norm_x, norm_y = map(float, norm_coords[:2])
  301. centers_x.append(norm_x * width)
  302. centers_y.append(norm_y * height)
  303. elif (pd.notna(row.get('BBox Center X')) and pd.notna(row.get('BBox Center Y'))):
  304. centers_x.append(row['BBox Center X'])
  305. centers_y.append(row['BBox Center Y'])
  306. except Exception:
  307. continue
  308. if centers_x and centers_y:
  309. # 创建热力图
  310. plt.hexbin(
  311. x=centers_x,
  312. y=centers_y,
  313. gridsize=50,
  314. cmap='plasma',
  315. alpha=0.7,
  316. mincnt=1
  317. )
  318. plt.colorbar(label='检测数量')
  319. # 反转Y轴,使坐标系与图像一致
  320. plt.gca().invert_yaxis()
  321. plt.title(f"摄像头 {camera_id} 检测热力图 ({total_detections} 个检测)")
  322. plt.axis('off')
  323. # 保存热力图
  324. heatmap_path = os.path.join(self.output_dir, f"{camera_id}_heatmap.png")
  325. plt.savefig(heatmap_path, bbox_inches='tight', pad_inches=0)
  326. plt.close()
  327. print(f"已生成摄像头 {camera_id} 的检测热力图: {heatmap_path}")
  328. def visualize_all_cameras(self) -> None:
  329. """可视化所有摄像头的检测结果"""
  330. if not self.camera_data:
  331. self.process_data()
  332. if not self.camera_data:
  333. print("没有可用的检测数据")
  334. return
  335. for camera_id in self.camera_data.keys():
  336. self.visualize_camera_detections(camera_id)
  337. def main():
  338. parser = argparse.ArgumentParser(description='目标检测结果可视化工具')
  339. parser.add_argument('--csv', type=str, required=True, help='检测报告CSV文件路径')
  340. parser.add_argument('--output', type=str, default=None, help='输出目录路径')
  341. parser.add_argument('--camera', type=str, default=None, help='指定摄像头ID进行分析,不指定则分析所有摄像头')
  342. args = parser.parse_args()
  343. # 创建可视化器并处理数据
  344. visualizer = DetectionVisualizer(args.csv, args.output)
  345. visualizer.load_data()
  346. visualizer.process_data()
  347. # 可视化检测结果
  348. if args.camera:
  349. visualizer.visualize_camera_detections(args.camera)
  350. else:
  351. visualizer.visualize_all_cameras()
  352. if __name__ == "__main__":
  353. main()