AnalyzeCenter.py 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316
  1. import cv2
  2. import numpy as np
  3. import json
  4. import matplotlib.pyplot as plt
  5. import math
  6. from typing import Union, List
  7. from app.core.logger import get_logger
  8. logger = get_logger(__name__)
  9. def get_points_from_file(json_file):
  10. """辅助函数,从JSON文件加载点"""
  11. with open(json_file, 'r') as f:
  12. data = json.load(f)
  13. return data['shapes'][0]['points']
  14. def draw_rotated_bounding_boxes(image_path, inner_box_file, outer_box_file, output_path=None):
  15. """
  16. 在图片上绘制内框和外框的旋转矩形。
  17. Args:
  18. image_path (str): 原始图片的文件路径。
  19. inner_box_file (str): 包含内框坐标的JSON文件路径。
  20. outer_box_file (str): 包含外框坐标的JSON文件路径。
  21. output_path (str, optional): 如果提供,结果图片将被保存到此路径。
  22. 默认为None,不保存。
  23. """
  24. # 1. 读取图片
  25. image = cv2.imread(image_path)
  26. if image is None:
  27. print(f"错误: 无法读取图片 -> {image_path}")
  28. return
  29. print(f"成功加载图片: {image_path}")
  30. # 2. 读取坐标点
  31. inner_points = get_points_from_file(inner_box_file)
  32. outer_points = get_points_from_file(outer_box_file)
  33. if inner_points is None or outer_points is None:
  34. print("因无法加载坐标点,程序终止。")
  35. return
  36. # 定义颜色 (BGR格式)
  37. OUTER_BOX_COLOR = (0, 255, 0) # 绿色
  38. INNER_BOX_COLOR = (0, 0, 255) # 红色
  39. THICKNESS = 10 # 可以根据你的图片分辨率调整线宽
  40. # 3. 处理并绘制外框
  41. # 将点列表转换为OpenCV需要的NumPy数组
  42. outer_contour = np.array(outer_points, dtype=np.int32)
  43. # 计算最小面积旋转矩形
  44. outer_rect = cv2.minAreaRect(outer_contour)
  45. # 获取矩形的4个角点
  46. outer_box_corners = cv2.boxPoints(outer_rect)
  47. # 转换为整数,以便绘制
  48. outer_box_corners_int = np.intp(outer_box_corners)
  49. # 在图片上绘制轮廓
  50. cv2.drawContours(image, [outer_box_corners_int], 0, OUTER_BOX_COLOR, THICKNESS)
  51. print("已绘制外框 (绿色)。")
  52. # 4. 处理并绘制内框
  53. inner_contour = np.array(inner_points, dtype=np.int32)
  54. inner_rect = cv2.minAreaRect(inner_contour)
  55. inner_box_corners = cv2.boxPoints(inner_rect)
  56. inner_box_corners_int = np.intp(inner_box_corners)
  57. cv2.drawContours(image, [inner_box_corners_int], 0, INNER_BOX_COLOR, THICKNESS)
  58. print("已绘制内框 (红色)。")
  59. # 5. 保存结果 (如果指定了路径)
  60. if output_path:
  61. cv2.imwrite(output_path, image)
  62. print(f"结果已保存到: {output_path}")
  63. # 6. 显示结果
  64. # OpenCV使用BGR,Matplotlib使用RGB,需要转换颜色通道
  65. image_rgb = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)
  66. # 使用Matplotlib显示,因为它在各种环境中(如Jupyter)表现更好
  67. plt.figure(figsize=(10, 15)) # 调整显示窗口大小
  68. plt.imshow(image_rgb)
  69. plt.title('旋转框可视化 (外框: 绿色, 内框: 红色)')
  70. plt.axis('off') # 不显示坐标轴
  71. plt.show()
  72. def analyze_centering_rotated(inner_points: Union[str, List], outer_points: Union[str, List], ):
  73. """
  74. 使用旋转矩形进行最精确的分析, 包括定向边距和精确的浮点数比值。
  75. """
  76. if isinstance(inner_points, str):
  77. inner_points = get_points_from_file(inner_points)
  78. if isinstance(outer_points, str):
  79. outer_points = get_points_from_file(outer_points)
  80. # 将点列表转换为OpenCV需要的NumPy数组格式
  81. inner_contour = np.array(inner_points, dtype=np.int32)
  82. outer_contour = np.array(outer_points, dtype=np.int32)
  83. # 计算最小面积的旋转矩形
  84. # 结果格式: ((中心点x, 中心点y), (宽度, 高度), 旋转角度)
  85. inner_rect = cv2.minAreaRect(inner_contour)
  86. outer_rect = cv2.minAreaRect(outer_contour)
  87. # 获取矩形的4个角点, 并转化为坐标
  88. # outer_box_corners = cv2.boxPoints(outer_rect)
  89. # outer_box_corners_int = np.intp(outer_box_corners).tolist()
  90. #
  91. # inner_box_corners = cv2.boxPoints(inner_rect)
  92. # inner_box_corners_int = np.intp(inner_box_corners).tolist()
  93. def get_rect(contour):
  94. # 获取矩形框
  95. x, y, w, h = cv2.boundingRect(contour)
  96. # 2. 构造 4 个顶点坐标
  97. box_corners_int = np.array([
  98. [x, y], # 左上
  99. [x + w, y], # 右上
  100. [x + w, y + h], # 右下
  101. [x, y + h] # 左下
  102. ], dtype=np.int32).tolist()
  103. return box_corners_int
  104. inner_box_corners_int = get_rect(inner_contour)
  105. outer_box_corners_int = get_rect(outer_contour)
  106. # --- 1. 标准化矩形尺寸,确保宽度总是长边 ---
  107. def standardize_rect(rect):
  108. (cx, cy), (w, h), angle = rect
  109. if w < h:
  110. # 如果宽度小于高度,交换它们,并调整角度
  111. w, h = h, w
  112. angle += 90
  113. return (cx, cy), (w, h), angle
  114. inner_center, inner_dims, inner_angle = standardize_rect(inner_rect)
  115. outer_center, outer_dims, outer_angle = standardize_rect(outer_rect)
  116. print("\n--- 基于旋转矩形的精确分析 ---")
  117. print(
  118. f"内框 (标准化后): 中心=({inner_center[0]:.1f}, {inner_center[1]:.1f}), 尺寸=({inner_dims[0]:.1f}, {inner_dims[1]:.1f}), 角度={inner_angle:.1f}°")
  119. print(
  120. f"外框 (标准化后): 中心=({outer_center[0]:.1f}, {outer_center[1]:.1f}), 尺寸=({outer_dims[0]:.1f}, {outer_dims[1]:.1f}), 角度={outer_angle:.1f}°")
  121. # --- 2. 评估居中度 (基于中心点) ---
  122. offset_x_abs = abs(inner_center[0] - outer_center[0])
  123. offset_y_abs = abs(inner_center[1] - outer_center[1])
  124. print(f"\n[居中度] 图像坐标系绝对中心偏移: 水平={offset_x_abs:.2f} px | 垂直={offset_y_abs:.2f} px")
  125. # --- 3. 评估平行度 ---
  126. angle_diff = abs(inner_angle - outer_angle)
  127. # 处理角度环绕问题 (例如 -89° 和 89° 其实只差 2°)
  128. angle_diff = min(angle_diff, 180 - angle_diff)
  129. print(f"[平行度] 角度差异: {angle_diff:.2f}° (值越小,内外框越平行)")
  130. # --- 4. 计算定向边距和比值 (核心部分) ---
  131. # 计算从外框中心指向内框中心的向量
  132. center_delta_vec = (inner_center[0] - outer_center[0], inner_center[1] - outer_center[1])
  133. # 将外框的旋转角度转换为弧度,用于三角函数计算
  134. angle_rad = math.radians(outer_angle)
  135. # 计算外框自身的坐标轴单位向量(相对于图像坐标系)
  136. # local_x_axis: 沿着外框宽度的方向向量
  137. # local_y_axis: 沿着外框高度的方向向量
  138. local_x_axis = (math.cos(angle_rad), math.sin(angle_rad))
  139. local_y_axis = (-math.sin(angle_rad), math.cos(angle_rad))
  140. # 使用点积 (dot product) 将中心偏移向量投影到外框的局部坐标轴上
  141. # 这能告诉我们,内框中心相对于外框中心,在“卡片自己的水平和垂直方向上”移动了多少
  142. offset_along_width = center_delta_vec[0] * local_x_axis[0] + center_delta_vec[1] * local_x_axis[1]
  143. offset_along_height = center_delta_vec[0] * local_y_axis[0] + center_delta_vec[1] * local_y_axis[1]
  144. # 计算水平和垂直方向上的总边距
  145. total_horizontal_margin = outer_dims[0] - inner_dims[0]
  146. total_vertical_margin = outer_dims[1] - inner_dims[1]
  147. # 根据投影的偏移量来分配总边距
  148. # 理想情况下,偏移为0,左右/上下边距各分一半
  149. # 如果有偏移,一侧增加,另一侧减少
  150. margin_left = (total_horizontal_margin / 2) - offset_along_width
  151. margin_right = (total_horizontal_margin / 2) + offset_along_width
  152. margin_top = (total_vertical_margin / 2) - offset_along_height
  153. margin_bottom = (total_vertical_margin / 2) + offset_along_height
  154. # print("\n[定向边距分析 (沿卡片方向)]")
  155. # print(f"水平边距 (像素): 左={margin_left:.1f} | 右={margin_right:.1f}")
  156. # print(f"垂直边距 (像素): 上={margin_top:.1f} | 下={margin_bottom:.1f}")
  157. #
  158. # print("\n[精确边距比值分析]")
  159. # # 直接展示浮点数比值
  160. # print(f"水平比值 (左:右) = {margin_left:.2f} : {margin_right:.2f}")
  161. # print(f"垂直比值 (上:下) = {margin_top:.2f} : {margin_bottom:.2f}")
  162. # 为了更直观,也可以计算一个百分比
  163. if total_horizontal_margin > 0:
  164. left_percent = (margin_left / total_horizontal_margin) * 100
  165. right_percent = (margin_right / total_horizontal_margin) * 100
  166. print(f"水平边距分布: 左 {left_percent:.1f}% | 右 {right_percent:.1f}%")
  167. if total_vertical_margin > 0:
  168. top_percent = (margin_top / total_vertical_margin) * 100
  169. bottom_percent = (margin_bottom / total_vertical_margin) * 100
  170. print(f"垂直边距分布: 上 {top_percent:.1f}% | 下 {bottom_percent:.1f}%")
  171. return ((left_percent, right_percent),
  172. (top_percent, bottom_percent),
  173. angle_diff), inner_box_corners_int, outer_box_corners_int
  174. def analyze_centering_rect(inner_points: Union[str, List], outer_points: Union[str, List]):
  175. """
  176. 使用横平竖直的矩形 (Axis-Aligned Bounding Box) 进行居中分析。
  177. 适用于前端已经修正过的矩形数据。
  178. """
  179. if isinstance(inner_points, str):
  180. inner_points = get_points_from_file(inner_points)
  181. if isinstance(outer_points, str):
  182. outer_points = get_points_from_file(outer_points)
  183. # 转换为 numpy 数组
  184. inner_contour = np.array(inner_points, dtype=np.int32)
  185. outer_contour = np.array(outer_points, dtype=np.int32)
  186. # --- 1. 获取横平竖直的矩形参数 (x, y, w, h) ---
  187. # 即使传入的是4个点,用 boundingRect 也能确保取出准确的边界
  188. ix, iy, iw, ih = cv2.boundingRect(inner_contour)
  189. ox, oy, ow, oh = cv2.boundingRect(outer_contour)
  190. # 构造标准的4点格式返回 (保持和 analyze_centering_rotated 输出一致)
  191. def get_box_corners(x, y, w, h):
  192. return [[x, y], [x + w, y], [x + w, y + h], [x, y + h]]
  193. inner_box_corners_int = get_box_corners(ix, iy, iw, ih)
  194. outer_box_corners_int = get_box_corners(ox, oy, ow, oh)
  195. print("\n--- 基于修正矩形(横平竖直)的分析 ---")
  196. print(f"内框: x={ix}, y={iy}, w={iw}, h={ih}")
  197. print(f"外框: x={ox}, y={oy}, w={ow}, h={oh}")
  198. # --- 2. 计算边距 (Margins) ---
  199. # 图像坐标系:x向右增加,y向下增加
  200. # 左边距:内框左边 - 外框左边
  201. margin_left = ix - ox
  202. # 右边距:外框右边(x+w) - 内框右边(x+w)
  203. margin_right = (ox + ow) - (ix + iw)
  204. # 上边距:内框上边 - 外框上边
  205. margin_top = iy - oy
  206. # 下边距:外框下边(y+h) - 内框下边(y+h)
  207. margin_bottom = (oy + oh) - (iy + ih)
  208. # --- 3. 计算百分比 ---
  209. total_horizontal_margin = margin_left + margin_right
  210. total_vertical_margin = margin_top + margin_bottom
  211. left_percent = 50.0
  212. right_percent = 50.0
  213. top_percent = 50.0
  214. bottom_percent = 50.0
  215. if total_horizontal_margin > 0:
  216. left_percent = (margin_left / total_horizontal_margin) * 100
  217. right_percent = (margin_right / total_horizontal_margin) * 100
  218. print(f"水平边距分布: 左 {left_percent:.1f}% | 右 {right_percent:.1f}%")
  219. if total_vertical_margin > 0:
  220. top_percent = (margin_top / total_vertical_margin) * 100
  221. bottom_percent = (margin_bottom / total_vertical_margin) * 100
  222. print(f"垂直边距分布: 上 {top_percent:.1f}% | 下 {bottom_percent:.1f}%")
  223. # --- 4. 角度差异 ---
  224. # 因为已经是横平竖直的矩形,角度差默认为 0
  225. angle_diff = 0.0
  226. # 返回格式保持与原函数 analyze_centering_rotated 一致:
  227. # ((左%, 右%), (上%, 下%), 角度差), 内框点集, 外框点集
  228. return ((left_percent, right_percent),
  229. (top_percent, bottom_percent),
  230. angle_diff), inner_box_corners_int, outer_box_corners_int
  231. def formate_center_data(center_result,
  232. inner_data: dict, outer_data: dict,
  233. inner_rect_points: List, outer_rect_points: List):
  234. data = {
  235. "box_result": {
  236. "center_inference": {
  237. "angel_diff": center_result[2],
  238. "center_left": center_result[0][0],
  239. "center_right": center_result[0][1],
  240. "center_top": center_result[1][0],
  241. "center_bottom": center_result[1][1]
  242. }
  243. }
  244. }
  245. data['box_result']['inner_box'] = inner_data
  246. data['box_result']['outer_box'] = outer_data
  247. data['box_result']['inner_box']['shapes'][0]['rect_box'] = inner_rect_points
  248. data['box_result']['outer_box']['shapes'][0]['rect_box'] = outer_rect_points
  249. return data
  250. # if __name__ == "__main__":
  251. # img_path = r"C:\Code\ML\Project\CheckCardBoxAndDefectServer\temp\250805_pokemon_0001.jpg"
  252. # inner_file_path = r"C:\Code\ML\Project\CheckCardBoxAndDefectServer\temp\inner\250805_pokemon_0001.json"
  253. # outer_file_path = r"C:\Code\ML\Project\CheckCardBoxAndDefectServer\temp\outer\250805_pokemon_0001.json"
  254. #
  255. # inner_pts = get_points_from_file(inner_file_path)
  256. # print(inner_pts)
  257. # print(type(inner_pts))
  258. #
  259. # result = analyze_centering_rotated(inner_file_path, outer_file_path)
  260. # print(result)
  261. # draw_rotated_bounding_boxes(img_path, inner_file_path, outer_file_path)