bbox_visualizer.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273
  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. from matplotlib.colors import LinearSegmentedColormap
  10. class BBoxVisualizer:
  11. """在原始图像上直接绘制目标检测框"""
  12. def __init__(self, csv_path: str, image_dir: str, output_dir: str = None):
  13. """初始化检测框可视化器
  14. Args:
  15. csv_path: 检测报告CSV文件路径
  16. image_dir: 原始图像目录
  17. output_dir: 输出目录,默认为CSV文件所在目录
  18. """
  19. self.csv_path = csv_path
  20. self.image_dir = image_dir
  21. self.data = None
  22. self.camera_data = {}
  23. # 设置输出目录
  24. if output_dir is None:
  25. self.output_dir = os.path.dirname(csv_path)
  26. else:
  27. self.output_dir = output_dir
  28. os.makedirs(output_dir, exist_ok=True)
  29. def load_data(self) -> None:
  30. """加载CSV数据"""
  31. try:
  32. self.data = pd.read_csv(self.csv_path)
  33. print(f"成功加载数据,共 {len(self.data)} 条记录")
  34. except Exception as e:
  35. print(f"加载CSV文件失败: {e}")
  36. raise
  37. def extract_camera_id(self, file_path: str) -> Optional[str]:
  38. """从文件路径中提取摄像头ID
  39. Args:
  40. file_path: 图像文件路径
  41. Returns:
  42. 摄像头ID,如果无法提取则返回None
  43. """
  44. # 尝试匹配常见的摄像头ID模式
  45. # 例如: cam_08_18 或 08_18
  46. patterns = [
  47. r'cam_(\d+)_(\d+)', # 匹配 cam_08_18 格式
  48. r'(\d+)_(\d+)_(\d+)', # 匹配 192_168_210_2_cam_08_18 格式中的 08_18
  49. r'(\d+)_(\d+)_\d+\.jpg$' # 匹配 01_07_00000.jpg 格式中的 01_07
  50. ]
  51. for pattern in patterns:
  52. match = re.search(pattern, file_path)
  53. if match:
  54. if len(match.groups()) >= 2:
  55. return f"{match.group(1)}_{match.group(2)}"
  56. else:
  57. return match.group(1)
  58. # 如果无法提取,返回文件名作为ID
  59. return os.path.basename(file_path).split('.')[0]
  60. def process_data(self) -> None:
  61. """处理数据,按摄像头ID分组"""
  62. if self.data is None:
  63. self.load_data()
  64. # 只处理有检测结果的数据
  65. detection_data = self.data[self.data['Object Count'] > 0].copy()
  66. if len(detection_data) == 0:
  67. print("警告: 没有找到任何检测结果")
  68. return
  69. # 提取摄像头ID并分组
  70. detection_data['Camera ID'] = detection_data['Image File'].apply(self.extract_camera_id)
  71. # 按摄像头ID分组
  72. for camera_id, group in detection_data.groupby('Camera ID'):
  73. self.camera_data[camera_id] = group
  74. print(f"摄像头 {camera_id}: {len(group)} 个检测结果")
  75. def find_image_file(self, camera_id: str) -> Optional[str]:
  76. """查找指定摄像头ID的图像文件
  77. Args:
  78. camera_id: 摄像头ID
  79. Returns:
  80. 图像文件路径,如果未找到则返回None
  81. """
  82. # 查找匹配的图像文件
  83. pattern = f"{camera_id}_\d+\.jpg"
  84. for root, _, files in os.walk(self.image_dir):
  85. for file in files:
  86. if re.match(pattern, file):
  87. return os.path.join(root, file)
  88. # 如果没有找到精确匹配,尝试模糊匹配
  89. pattern = f"{camera_id.split('_')[0]}.*{camera_id.split('_')[1]}.*\.jpg"
  90. for root, _, files in os.walk(self.image_dir):
  91. for file in files:
  92. if re.search(pattern, file):
  93. return os.path.join(root, file)
  94. return None
  95. def draw_bboxes(self, camera_id: str, resolution: Tuple[int, int] = (3840, 2160)) -> None:
  96. """在原始图像上绘制检测框
  97. Args:
  98. camera_id: 摄像头ID
  99. resolution: 图像分辨率,默认为3840x2160
  100. """
  101. if not self.camera_data:
  102. self.process_data()
  103. if not self.camera_data:
  104. print("没有可用的检测数据来绘制检测框")
  105. return
  106. if camera_id not in self.camera_data:
  107. print(f"未找到摄像头 {camera_id} 的数据")
  108. return
  109. # 获取该摄像头的检测数据
  110. cam_data = self.camera_data[camera_id]
  111. # 查找原始图像
  112. image_path = self.find_image_file(camera_id)
  113. if image_path is None:
  114. print(f"未找到摄像头 {camera_id} 的原始图像")
  115. return
  116. # 读取原始图像
  117. try:
  118. image = cv2.imread(image_path)
  119. if image is None:
  120. raise ValueError(f"无法读取图像: {image_path}")
  121. # 转换为RGB格式(OpenCV默认为BGR)
  122. image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)
  123. # 获取图像实际分辨率
  124. actual_height, actual_width = image.shape[:2]
  125. resolution = (actual_width, actual_height)
  126. print(f"已加载图像: {image_path}, 分辨率: {resolution}")
  127. except Exception as e:
  128. print(f"读取图像时出错: {e}")
  129. return
  130. # 创建一个透明度图层用于叠加检测框
  131. overlay = image.copy()
  132. # 绘制所有检测框
  133. detection_count = 0
  134. for _, row in cam_data.iterrows():
  135. try:
  136. # 获取归一化坐标和宽高
  137. if 'Normalized Coordinates' in row and pd.notna(row['Normalized Coordinates']):
  138. try:
  139. norm_coords = row['Normalized Coordinates'].split(',')
  140. if len(norm_coords) >= 4: # 中心点x,y,宽,高
  141. norm_x, norm_y, norm_w, norm_h = map(float, norm_coords[:4])
  142. else:
  143. continue
  144. except (ValueError, IndexError):
  145. continue
  146. else:
  147. # 如果没有归一化坐标,使用中心点坐标和宽高计算
  148. if (pd.notna(row.get('BBox Center X')) and pd.notna(row.get('BBox Center Y')) and
  149. pd.notna(row.get('BBox Width')) and pd.notna(row.get('BBox Height'))):
  150. center_x = row['BBox Center X']
  151. center_y = row['BBox Center Y']
  152. width = row['BBox Width']
  153. height = row['BBox Height']
  154. norm_x = center_x / resolution[0]
  155. norm_y = center_y / resolution[1]
  156. norm_w = width / resolution[0]
  157. norm_h = height / resolution[1]
  158. else:
  159. continue
  160. # 将归一化坐标转换为像素坐标
  161. center_x = int(norm_x * resolution[0])
  162. center_y = int(norm_y * resolution[1])
  163. width = int(norm_w * resolution[0])
  164. height = int(norm_h * resolution[1])
  165. # 计算左上角和右下角坐标
  166. x1 = max(0, int(center_x - width / 2))
  167. y1 = max(0, int(center_y - height / 2))
  168. x2 = min(resolution[0] - 1, int(center_x + width / 2))
  169. y2 = min(resolution[1] - 1, int(center_y + height / 2))
  170. # 绘制检测框,使用半透明的红色
  171. cv2.rectangle(overlay, (x1, y1), (x2, y2), (255, 0, 0), 2)
  172. # 如果有置信度信息,显示在框上方
  173. if 'Confidence' in row and pd.notna(row['Confidence']):
  174. confidence = float(row['Confidence'])
  175. cv2.putText(overlay, f"{confidence:.2f}", (x1, y1 - 5),
  176. cv2.FONT_HERSHEY_SIMPLEX, 0.5, (255, 0, 0), 1)
  177. detection_count += 1
  178. except Exception as e:
  179. print(f"处理检测框时出错: {e}")
  180. # 设置透明度
  181. alpha = 0.6
  182. output_image = cv2.addWeighted(overlay, alpha, image, 1 - alpha, 0)
  183. # 保存结果图像
  184. output_path = os.path.join(self.output_dir, f"bbox_overlay_{camera_id}.jpg")
  185. output_image_rgb = cv2.cvtColor(output_image, cv2.COLOR_RGB2BGR) # 转回BGR格式保存
  186. cv2.imwrite(output_path, output_image_rgb)
  187. print(f"已在图像上绘制 {detection_count} 个检测框: {output_path}")
  188. # 使用matplotlib显示结果
  189. plt.figure(figsize=(12, 8))
  190. plt.imshow(output_image)
  191. plt.title(f"摄像头 {camera_id} 目标检测框叠加图 ({detection_count} 个检测)")
  192. plt.axis('off')
  193. # 保存matplotlib版本的图像
  194. plt_output_path = os.path.join(self.output_dir, f"bbox_overlay_{camera_id}_plt.png")
  195. plt.savefig(plt_output_path, dpi=300, bbox_inches='tight')
  196. plt.close()
  197. def draw_all_bboxes(self, resolution: Tuple[int, int] = (3840, 2160)) -> None:
  198. """为所有摄像头绘制检测框"""
  199. if not self.camera_data:
  200. self.process_data()
  201. if not self.camera_data:
  202. print("没有可用的检测数据来绘制检测框")
  203. return
  204. for camera_id in self.camera_data.keys():
  205. self.draw_bboxes(camera_id, resolution)
  206. def main():
  207. parser = argparse.ArgumentParser(description='目标检测框可视化工具')
  208. parser.add_argument('--csv', type=str, required=True, help='检测报告CSV文件路径')
  209. parser.add_argument('--image_dir', type=str, required=True, help='原始图像目录')
  210. parser.add_argument('--output', type=str, default=None, help='输出目录路径')
  211. parser.add_argument('--camera', type=str, default=None, help='指定摄像头ID进行分析,不指定则分析所有摄像头')
  212. args = parser.parse_args()
  213. # 创建可视化器并处理数据
  214. visualizer = BBoxVisualizer(args.csv, args.image_dir, args.output)
  215. visualizer.load_data()
  216. visualizer.process_data()
  217. # 绘制检测框
  218. if args.camera:
  219. visualizer.draw_bboxes(args.camera)
  220. else:
  221. visualizer.draw_all_bboxes()
  222. if __name__ == "__main__":
  223. main()