card_box_straight_detection.py 49 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383
  1. import os
  2. import cv2
  3. import json
  4. import numpy as np
  5. from pathlib import Path
  6. from dataclasses import dataclass, field
  7. from typing import Dict, List, Tuple, Optional, Any
  8. from enum import Enum
  9. import matplotlib.pyplot as plt
  10. from sklearn.linear_model import RANSACRegressor
  11. # matplotlib解决中文乱码
  12. plt.rcParams["font.sans-serif"] = ["SimHei"]
  13. plt.rcParams["font.family"] = "sans-serif"
  14. plt.rcParams['axes.unicode_minus'] = False
  15. def fry_algo_print(level_str: str, info_str: str):
  16. print(f"[{level_str}] : {info_str}")
  17. def fry_cv2_imread(filename, flags=cv2.IMREAD_COLOR):
  18. """支持中文路径的imread"""
  19. try:
  20. with open(filename, 'rb') as f:
  21. chunk = f.read()
  22. chunk_arr = np.frombuffer(chunk, dtype=np.uint8)
  23. img = cv2.imdecode(chunk_arr, flags)
  24. if img is None:
  25. fry_algo_print("警告", f"Warning: Unable to decode image: {filename}")
  26. return img
  27. except IOError as e:
  28. fry_algo_print("错误", f"IOError: Unable to read file: {filename}")
  29. fry_algo_print("错误", f"Error details: {str(e)}")
  30. return None
  31. def fry_cv2_imwrite(filename, img, params=None):
  32. """支持中文路径的imwrite"""
  33. try:
  34. ext = os.path.splitext(filename)[1].lower()
  35. result, encoded_img = cv2.imencode(ext, img, params)
  36. if result:
  37. with open(filename, 'wb') as f:
  38. encoded_img.tofile(f)
  39. return True
  40. else:
  41. fry_algo_print("警告", f"Warning: Unable to encode image: {filename}")
  42. return False
  43. except Exception as e:
  44. fry_algo_print("错误", f"Error: Unable to write file: {filename}")
  45. fry_algo_print("错误", f"Error details: {str(e)}")
  46. return False
  47. def fry_opencv_Chinese_path_init():
  48. """覆盖OpenCV的原始函数"""
  49. cv2.imread = fry_cv2_imread
  50. cv2.imwrite = fry_cv2_imwrite
  51. OPENCV_IO_ALREADY_INIT = False
  52. if not OPENCV_IO_ALREADY_INIT:
  53. fry_opencv_Chinese_path_init()
  54. OPENCV_IO_ALREADY_INIT = True
  55. def to_json_serializable(obj):
  56. """将包含自定义对象的数据结构转换为JSON可序列化的格式"""
  57. if obj is None or isinstance(obj, (bool, int, float, str)):
  58. return obj
  59. elif isinstance(obj, dict):
  60. return {key: to_json_serializable(value) for key, value in obj.items()}
  61. elif isinstance(obj, (list, tuple)):
  62. return [to_json_serializable(item) for item in obj]
  63. elif isinstance(obj, set):
  64. return [to_json_serializable(item) for item in obj]
  65. elif isinstance(obj, bytes):
  66. return obj.decode('utf-8', errors='ignore')
  67. else:
  68. if hasattr(obj, '__dict__'):
  69. return to_json_serializable(obj.__dict__)
  70. elif hasattr(obj, 'to_dict'):
  71. return to_json_serializable(obj.to_dict())
  72. elif hasattr(obj, 'to_json'):
  73. return to_json_serializable(obj.to_json())
  74. else:
  75. return str(obj)
  76. class EdgeCornerType(Enum):
  77. """边角类型枚举"""
  78. TOP_LEFT = "左上角"
  79. TOP_RIGHT = "右上角"
  80. BOTTOM_LEFT = "左下角"
  81. BOTTOM_RIGHT = "右下角"
  82. TOP = "上边"
  83. BOTTOM = "下边"
  84. LEFT = "左边"
  85. RIGHT = "右边"
  86. @dataclass
  87. class EdgeCornerDefect:
  88. """边角缺陷数据结构"""
  89. type: EdgeCornerType
  90. protrusion_area: float = 0.0 # 突出面积(像素)
  91. depression_area: float = 0.0 # 凹陷面积(像素)
  92. protrusion_pixels: int = 0 # 突出像素数
  93. depression_pixels: int = 0 # 凹陷像素数
  94. contour_points: List[List[float]] = field(default_factory=list) # 该边角的轮廓点
  95. fitted_points: List[List[float]] = field(default_factory=list) # 拟合的边角点
  96. region_points: List[List[float]] = field(default_factory=list) # 区域边界点
  97. def to_dict(self) -> Dict[str, Any]:
  98. """转换为字典"""
  99. return {
  100. "type": self.type.value,
  101. "protrusion_area": float(self.protrusion_area),
  102. "depression_area": float(self.depression_area),
  103. "protrusion_pixels": int(self.protrusion_pixels),
  104. "depression_pixels": int(self.depression_pixels),
  105. "contour_points": self.contour_points,
  106. "fitted_points": self.fitted_points,
  107. "region_points": self.region_points
  108. }
  109. @classmethod
  110. def load_from_dict(cls, data: Dict[str, Any]) -> 'EdgeCornerDefect':
  111. """从字典加载"""
  112. return cls(
  113. type=EdgeCornerType(data["type"]),
  114. protrusion_area=data.get("protrusion_area", 0.0),
  115. depression_area=data.get("depression_area", 0.0),
  116. protrusion_pixels=data.get("protrusion_pixels", 0),
  117. depression_pixels=data.get("depression_pixels", 0),
  118. contour_points=data.get("contour_points", []),
  119. fitted_points=data.get("fitted_points", []),
  120. region_points=data.get("region_points", [])
  121. )
  122. @dataclass
  123. class FryAlgoParamsBase:
  124. """算法参数基类"""
  125. algo_name: Optional[str] = None
  126. debug_level: str = "no" # no, normal, detail
  127. save_json_params: bool = False
  128. is_first_algo: bool = False
  129. is_last_algo: bool = False
  130. def to_dict(self) -> Dict[str, Any]:
  131. """转换为字典"""
  132. result = {}
  133. for key, value in self.__dict__.items():
  134. if hasattr(value, 'to_dict'):
  135. result[key] = value.to_dict()
  136. elif isinstance(value, Enum):
  137. result[key] = value.value
  138. elif isinstance(value, Path):
  139. result[key] = str(value)
  140. else:
  141. result[key] = value
  142. return result
  143. @dataclass
  144. class CardDefectDetectionParams(FryAlgoParamsBase):
  145. """卡片缺陷检测算法参数"""
  146. algo_name: str = "卡片边角缺陷检测"
  147. label_name: str = "outer_box" # JSON中的框标签名称
  148. long_edge_corner_length: float = 200 # 矩形长边的角的长度
  149. short_edge_corner_length: float = 200 # 矩形短边的角的长度
  150. edge_width: float = 50 # 边区域向内延伸的宽度
  151. iteration_rounds: int = 5 # 步骤d和e迭代轮数
  152. ransac_residual_threshold: float = 5.0 # RANSAC残差阈值
  153. save_intermediate_results: bool = True # 是否保存中间结果
  154. save_overlay_images: bool = True # 是否保存叠加图像
  155. class MaskProcessor:
  156. """Mask处理工具类"""
  157. @staticmethod
  158. def polygon_to_mask(polygon_points: np.ndarray, img_shape: Tuple[int, int]) -> np.ndarray:
  159. """将多边形转换为mask
  160. Args:
  161. polygon_points: 多边形顶点坐标
  162. img_shape: (height, width)
  163. Returns:
  164. 二值mask图像
  165. """
  166. mask = np.zeros(img_shape[:2], dtype=np.uint8)
  167. if len(polygon_points) > 0:
  168. cv2.fillPoly(mask, [polygon_points.astype(np.int32)], 255)
  169. return mask
  170. @staticmethod
  171. def calculate_inner_points(
  172. fitted_quad: np.ndarray,
  173. long_edge_indices: List[int],
  174. short_edge_indices: List[int],
  175. long_corner_length: float,
  176. short_corner_length: float
  177. ) -> np.ndarray:
  178. """计算内部四个点B1-B4
  179. Args:
  180. fitted_quad: 拟合四边形的四个顶点
  181. long_edge_indices: 长边索引
  182. short_edge_indices: 短边索引
  183. long_corner_length: 长边角长度
  184. short_corner_length: 短边角长度
  185. Returns:
  186. 内部四个点坐标
  187. """
  188. inner_points = np.zeros((4, 2))
  189. for i in range(4):
  190. # 获取当前顶点
  191. current_point = fitted_quad[i]
  192. # 获取相邻两条边的向量
  193. prev_idx = (i - 1) % 4
  194. next_idx = (i + 1) % 4
  195. edge_to_prev = fitted_quad[prev_idx] - current_point
  196. edge_to_next = fitted_quad[next_idx] - current_point
  197. # 判断边的类型并设置对应的长度
  198. # 边索引: 0(0-1), 1(1-2), 2(2-3), 3(3-0)
  199. edge_prev_idx = prev_idx if prev_idx < i else 3
  200. edge_next_idx = i
  201. # 根据边类型设置移动距离
  202. if edge_prev_idx in long_edge_indices:
  203. dist_prev = long_corner_length
  204. else:
  205. dist_prev = short_corner_length
  206. if edge_next_idx in long_edge_indices:
  207. dist_next = long_corner_length
  208. else:
  209. dist_next = short_corner_length
  210. # 单位化向量
  211. edge_to_prev_unit = edge_to_prev / np.linalg.norm(edge_to_prev)
  212. edge_to_next_unit = edge_to_next / np.linalg.norm(edge_to_next)
  213. # 计算内部点
  214. inner_points[i] = current_point + dist_prev * edge_to_prev_unit + dist_next * edge_to_next_unit
  215. return inner_points
  216. @staticmethod
  217. def get_corner_region_mask(
  218. corner_idx: int,
  219. fitted_quad: np.ndarray,
  220. inner_points: np.ndarray,
  221. img_shape: Tuple[int, int]
  222. ) -> np.ndarray:
  223. """获取角区域mask
  224. Args:
  225. corner_idx: 角索引(0-3)
  226. fitted_quad: 拟合四边形顶点
  227. inner_points: 内部四个点
  228. img_shape: 图像尺寸
  229. Returns:
  230. 角区域mask
  231. """
  232. mask = np.zeros(img_shape[:2], dtype=np.uint8)
  233. # 获取角区域的四个顶点
  234. prev_idx = (corner_idx - 1) % 4
  235. next_idx = (corner_idx + 1) % 4
  236. # 计算角区域的边界点
  237. corner_point = fitted_quad[corner_idx]
  238. inner_point = inner_points[corner_idx]
  239. # 从内部点向两条边做垂线得到边界点
  240. edge_to_prev = fitted_quad[prev_idx] - corner_point
  241. edge_to_next = fitted_quad[next_idx] - corner_point
  242. edge_to_prev_unit = edge_to_prev / np.linalg.norm(edge_to_prev)
  243. edge_to_next_unit = edge_to_next / np.linalg.norm(edge_to_next)
  244. # 计算垂足
  245. proj_prev = np.dot(inner_point - corner_point, edge_to_prev_unit)
  246. proj_next = np.dot(inner_point - corner_point, edge_to_next_unit)
  247. point_on_prev_edge = corner_point + proj_prev * edge_to_prev_unit
  248. point_on_next_edge = corner_point + proj_next * edge_to_next_unit
  249. # 创建角区域多边形
  250. corner_polygon = np.array([
  251. corner_point,
  252. point_on_prev_edge,
  253. inner_point,
  254. point_on_next_edge
  255. ], dtype=np.int32)
  256. cv2.fillPoly(mask, [corner_polygon], 255)
  257. return mask
  258. @staticmethod
  259. def get_edge_region_mask(
  260. edge_idx: int,
  261. fitted_quad: np.ndarray,
  262. inner_points: np.ndarray,
  263. edge_width: float,
  264. img_shape: Tuple[int, int]
  265. ) -> np.ndarray:
  266. """获取边区域mask
  267. Args:
  268. edge_idx: 边索引(0-3)
  269. fitted_quad: 拟合四边形顶点
  270. inner_points: 内部四个点
  271. edge_width: 边区域宽度
  272. img_shape: 图像尺寸
  273. Returns:
  274. 边区域mask
  275. """
  276. mask = np.zeros(img_shape[:2], dtype=np.uint8)
  277. # 获取边的两个端点
  278. p1_idx = edge_idx
  279. p2_idx = (edge_idx + 1) % 4
  280. p1 = fitted_quad[p1_idx]
  281. p2 = fitted_quad[p2_idx]
  282. # 获取对应的内部点
  283. inner_p1 = inner_points[p1_idx]
  284. inner_p2 = inner_points[p2_idx]
  285. # 计算边向量和法向量
  286. edge_vec = p2 - p1
  287. edge_unit = edge_vec / np.linalg.norm(edge_vec)
  288. # 法向量(向内)
  289. normal = np.array([-edge_unit[1], edge_unit[0]])
  290. # 确保法向量指向内部
  291. center = np.mean(fitted_quad, axis=0)
  292. if np.dot(normal, center - (p1 + p2) / 2) < 0:
  293. normal = -normal
  294. # 从内部点向边做垂线
  295. proj1 = np.dot(inner_p1 - p1, edge_unit)
  296. proj2 = np.dot(inner_p2 - p1, edge_unit)
  297. point_on_edge_1 = p1 + proj1 * edge_unit
  298. point_on_edge_2 = p1 + proj2 * edge_unit
  299. # 创建边区域梯形
  300. inner_edge_p1 = point_on_edge_1 + edge_width * normal
  301. inner_edge_p2 = point_on_edge_2 + edge_width * normal
  302. edge_polygon = np.array([
  303. point_on_edge_1,
  304. point_on_edge_2,
  305. inner_edge_p2,
  306. inner_edge_p1
  307. ], dtype=np.int32)
  308. cv2.fillPoly(mask, [edge_polygon], 255)
  309. return mask
  310. @staticmethod
  311. def calculate_defect_pixels(
  312. contour_mask: np.ndarray,
  313. fitted_mask: np.ndarray,
  314. region_mask: np.ndarray
  315. ) -> Tuple[int, int]:
  316. """计算缺陷像素数
  317. Args:
  318. contour_mask: 轮廓mask
  319. fitted_mask: 拟合mask
  320. region_mask: 区域mask
  321. Returns:
  322. (突出像素数, 凹陷像素数)
  323. """
  324. # 限制在区域内
  325. contour_in_region = cv2.bitwise_and(contour_mask, region_mask)
  326. fitted_in_region = cv2.bitwise_and(fitted_mask, region_mask)
  327. # 计算并集
  328. union_mask = cv2.bitwise_or(contour_in_region, fitted_in_region)
  329. # 突出:并集减去拟合区域
  330. protrusion_mask = cv2.bitwise_and(union_mask, cv2.bitwise_not(fitted_in_region))
  331. protrusion_pixels = np.sum(protrusion_mask > 0)
  332. # 凹陷:并集减去轮廓区域
  333. depression_mask = cv2.bitwise_and(union_mask, cv2.bitwise_not(contour_in_region))
  334. depression_pixels = np.sum(depression_mask > 0)
  335. return protrusion_pixels, depression_pixels
  336. class CardDefectDetectionAlgo:
  337. """卡片边角缺陷检测算法类"""
  338. def __init__(self):
  339. """初始化"""
  340. self.intermediate_results = {}
  341. self.mask_processor = MaskProcessor()
  342. @staticmethod
  343. def load_json_data(json_path) -> Dict[str, Any]:
  344. """加载JSON数据"""
  345. with open(json_path, 'r', encoding='utf-8') as f:
  346. return json.load(f)
  347. @staticmethod
  348. def _create_blank_image(width: int, height: int) -> np.ndarray:
  349. """创建空白图像"""
  350. return np.ones((height, width, 3), dtype=np.uint8) * 255
  351. @staticmethod
  352. def load_or_create_image(image_path: str) -> np.ndarray:
  353. """加载图像或创建空白图像"""
  354. if os.path.exists(image_path):
  355. img = cv2.imread(str(image_path))
  356. if img is not None:
  357. return img
  358. raise ValueError("警告", f"图像不存在或无法读取: {image_path},创建空白图像")
  359. @staticmethod
  360. def _get_unique_shape(shapes: List[Dict], label_name: str) -> Optional[Dict]:
  361. """获取唯一的形状"""
  362. target_shapes = [s for s in shapes if s.get('label') == label_name]
  363. if not target_shapes:
  364. return None
  365. if len(target_shapes) == 1:
  366. return target_shapes[0]
  367. def get_shape_score(shape):
  368. confidence = shape.get('probability', 0)
  369. points = np.array(shape['points'])
  370. area = cv2.contourArea(points.astype(np.float32))
  371. return (confidence, area)
  372. target_shapes.sort(key=get_shape_score, reverse=True)
  373. return target_shapes[0]
  374. @staticmethod
  375. def _get_min_area_rect(points: np.ndarray) -> Tuple[np.ndarray, float, float, List[int], List[int]]:
  376. """获取最小外接矩形并识别长短边
  377. Returns:
  378. (矩形顶点, 长边长度, 短边长度, 长边索引列表, 短边索引列表)
  379. """
  380. rect = cv2.minAreaRect(points.astype(np.float32))
  381. box = cv2.boxPoints(rect)
  382. box = np.intp(box)
  383. # 计算每条边的长度
  384. edge_lengths = []
  385. for i in range(4):
  386. p1 = box[i]
  387. p2 = box[(i + 1) % 4]
  388. length = np.linalg.norm(p2 - p1)
  389. edge_lengths.append(length)
  390. # 识别长边和短边
  391. sorted_indices = np.argsort(edge_lengths)
  392. short_edge_indices = sorted_indices[:2].tolist()
  393. long_edge_indices = sorted_indices[2:].tolist()
  394. long_edge = np.mean([edge_lengths[i] for i in long_edge_indices])
  395. short_edge = np.mean([edge_lengths[i] for i in short_edge_indices])
  396. return box, long_edge, short_edge, long_edge_indices, short_edge_indices
  397. @staticmethod
  398. def _classify_points_to_edges_corners(
  399. contour_points: np.ndarray,
  400. rect_corners: np.ndarray,
  401. long_edge: float,
  402. short_edge: float,
  403. long_corner_length: float,
  404. short_corner_length: float
  405. ) -> Dict[EdgeCornerType, np.ndarray]:
  406. """将轮廓点分类到边和角"""
  407. classified_points = {edge_type: [] for edge_type in EdgeCornerType}
  408. edges = []
  409. for i in range(4):
  410. p1 = rect_corners[i]
  411. p2 = rect_corners[(i + 1) % 4]
  412. edges.append((p1, p2))
  413. edge_lengths = [np.linalg.norm(e[1] - e[0]) for e in edges]
  414. long_edge_indices = np.argsort(edge_lengths)[-2:]
  415. short_edge_indices = np.argsort(edge_lengths)[:2]
  416. for point in contour_points:
  417. min_dist = float('inf')
  418. min_type = None
  419. for i, corner in enumerate(rect_corners):
  420. dist = np.linalg.norm(point - corner)
  421. if dist < min_dist:
  422. min_dist = dist
  423. if i == 0:
  424. min_type = EdgeCornerType.TOP_LEFT
  425. elif i == 1:
  426. min_type = EdgeCornerType.TOP_RIGHT
  427. elif i == 2:
  428. min_type = EdgeCornerType.BOTTOM_RIGHT
  429. else:
  430. min_type = EdgeCornerType.BOTTOM_LEFT
  431. for i, (p1, p2) in enumerate(edges):
  432. dist = CardDefectDetectionAlgo._point_to_segment_distance(point, p1, p2)
  433. edge_vector = p2 - p1
  434. edge_length = np.linalg.norm(edge_vector)
  435. projection = np.dot(point - p1, edge_vector) / edge_length
  436. if i in long_edge_indices:
  437. corner_ratio = long_corner_length / edge_length
  438. else:
  439. corner_ratio = short_corner_length / edge_length
  440. if corner_ratio < projection / edge_length < (1 - corner_ratio):
  441. if dist < min_dist:
  442. min_dist = dist
  443. if i == 0:
  444. min_type = EdgeCornerType.TOP
  445. elif i == 1:
  446. min_type = EdgeCornerType.RIGHT
  447. elif i == 2:
  448. min_type = EdgeCornerType.BOTTOM
  449. else:
  450. min_type = EdgeCornerType.LEFT
  451. if min_type:
  452. classified_points[min_type].append(point)
  453. for key in classified_points:
  454. if classified_points[key]:
  455. classified_points[key] = np.array(classified_points[key])
  456. else:
  457. classified_points[key] = np.array([]).reshape(0, 2)
  458. return classified_points
  459. @staticmethod
  460. def _point_to_segment_distance(point: np.ndarray, seg_p1: np.ndarray, seg_p2: np.ndarray) -> float:
  461. """计算点到线段的距离"""
  462. line_vec = seg_p2 - seg_p1
  463. point_vec = point - seg_p1
  464. line_len = np.linalg.norm(line_vec)
  465. if line_len == 0:
  466. return np.linalg.norm(point - seg_p1)
  467. line_unitvec = line_vec / line_len
  468. proj_length = np.dot(point_vec, line_unitvec)
  469. if proj_length < 0:
  470. return np.linalg.norm(point - seg_p1)
  471. elif proj_length > line_len:
  472. return np.linalg.norm(point - seg_p2)
  473. else:
  474. proj_point = seg_p1 + proj_length * line_unitvec
  475. return np.linalg.norm(point - proj_point)
  476. @staticmethod
  477. def _fit_line_with_ransac(points: np.ndarray, max_iterations: int = 1000,
  478. threshold: float = 5.0) -> Tuple[float, float, float]:
  479. """使用RANSAC拟合直线
  480. Args:
  481. points: 点集 (N, 2)
  482. max_iterations: 最大迭代次数
  483. threshold: 内点判定阈值
  484. Returns:
  485. (a, b, c) 直线方程系数 ax + by + c = 0
  486. """
  487. if len(points) < 2:
  488. return None
  489. best_line = None
  490. best_inliers = 0
  491. for _ in range(max_iterations):
  492. # 随机选择两个点
  493. idx = np.random.choice(len(points), 2, replace=False)
  494. p1, p2 = points[idx]
  495. # 计算直线参数
  496. if abs(p2[0] - p1[0]) < 1e-6: # 垂直线
  497. a, b, c = 1, 0, -p1[0]
  498. else:
  499. # 一般直线 y = kx + m => kx - y + m = 0
  500. k = (p2[1] - p1[1]) / (p2[0] - p1[0])
  501. m = p1[1] - k * p1[0]
  502. a, b, c = k, -1, m
  503. # 归一化
  504. norm = np.sqrt(a * a + b * b)
  505. a, b, c = a / norm, b / norm, c / norm
  506. # 计算内点数
  507. distances = np.abs(a * points[:, 0] + b * points[:, 1] + c)
  508. inliers = np.sum(distances < threshold)
  509. if inliers > best_inliers:
  510. best_inliers = inliers
  511. best_line = (a, b, c)
  512. # 使用所有内点重新拟合(精细化)
  513. if best_line:
  514. a, b, c = best_line
  515. distances = np.abs(a * points[:, 0] + b * points[:, 1] + c)
  516. inlier_points = points[distances < threshold]
  517. if len(inlier_points) >= 2:
  518. # 使用OpenCV的fitLine进行最终拟合
  519. vx, vy, x0, y0 = cv2.fitLine(inlier_points, cv2.DIST_L2, 0, 0.01, 0.01)
  520. # 转换为一般式 ax + by + c = 0
  521. # 直线方向向量(vx, vy),点(x0, y0)在直线上
  522. # 法向量为(-vy, vx)
  523. a = -vy[0]
  524. b = vx[0]
  525. c = -(a * x0[0] + b * y0[0])
  526. # 归一化
  527. norm = np.sqrt(a * a + b * b)
  528. return (a / norm, b / norm, c / norm)
  529. return best_line
  530. @staticmethod
  531. def _fit_lines_opencv(
  532. classified_points: Dict[EdgeCornerType, np.ndarray],
  533. threshold: float = 5.0
  534. ) -> Dict[EdgeCornerType, Tuple[float, float, float]]:
  535. """使用OpenCV拟合四条边的直线(只使用边点,不使用角点)
  536. Args:
  537. classified_points: 分类后的点
  538. threshold: RANSAC阈值
  539. Returns:
  540. 拟合的直线字典 {边类型: (a, b, c)}
  541. """
  542. fitted_lines = {}
  543. # 边类型映射
  544. edge_only_types = {
  545. EdgeCornerType.TOP: EdgeCornerType.TOP,
  546. EdgeCornerType.BOTTOM: EdgeCornerType.BOTTOM,
  547. EdgeCornerType.LEFT: EdgeCornerType.LEFT,
  548. EdgeCornerType.RIGHT: EdgeCornerType.RIGHT
  549. }
  550. for edge_type in edge_only_types:
  551. # 只使用纯边点,不包含角点
  552. edge_points = classified_points.get(edge_type, np.array([]))
  553. if len(edge_points) < 2:
  554. fry_algo_print("警告", f"{edge_type.value}边点数不足,跳过拟合")
  555. continue
  556. # 使用RANSAC拟合直线
  557. line_params = CardDefectDetectionAlgo._fit_line_with_ransac(
  558. edge_points, threshold=threshold
  559. )
  560. if line_params:
  561. fitted_lines[edge_type] = line_params
  562. fry_algo_print("信息", f"{edge_type.value}拟合成功,点数: {len(edge_points)}")
  563. else:
  564. fry_algo_print("警告", f"{edge_type.value}拟合失败")
  565. return fitted_lines
  566. @staticmethod
  567. def _get_line_intersection(line1: Tuple[float, float, float],
  568. line2: Tuple[float, float, float]) -> Optional[np.ndarray]:
  569. """计算两条直线的交点
  570. Args:
  571. line1: 直线1参数 (a1, b1, c1)
  572. line2: 直线2参数 (a2, b2, c2)
  573. Returns:
  574. 交点坐标 [x, y] 或 None
  575. """
  576. a1, b1, c1 = line1
  577. a2, b2, c2 = line2
  578. det = a1 * b2 - a2 * b1
  579. if abs(det) < 1e-10:
  580. return None
  581. x = (b1 * c2 - b2 * c1) / det
  582. y = (a2 * c1 - a1 * c2) / det
  583. return np.array([x, y])
  584. @staticmethod
  585. def _get_quadrilateral_from_lines(fitted_lines: Dict[EdgeCornerType, Tuple[float, float, float]]) -> np.ndarray:
  586. """从拟合的直线获取四边形的四个角点
  587. Args:
  588. fitted_lines: 拟合的直线字典
  589. Returns:
  590. 四个角点坐标,按左上、右上、右下、左下顺序
  591. """
  592. # 检查是否有四条边
  593. required_edges = [EdgeCornerType.TOP, EdgeCornerType.RIGHT,
  594. EdgeCornerType.BOTTOM, EdgeCornerType.LEFT]
  595. for edge in required_edges:
  596. if edge not in fitted_lines:
  597. fry_algo_print("警告", f"缺少{edge.value}的拟合直线")
  598. return None
  599. # 计算四个角点
  600. corners = {}
  601. # 左上角 = 左边 ∩ 上边
  602. top_left = CardDefectDetectionAlgo._get_line_intersection(
  603. fitted_lines[EdgeCornerType.LEFT],
  604. fitted_lines[EdgeCornerType.TOP]
  605. )
  606. if top_left is not None:
  607. corners['top_left'] = top_left
  608. # 右上角 = 右边 ∩ 上边
  609. top_right = CardDefectDetectionAlgo._get_line_intersection(
  610. fitted_lines[EdgeCornerType.RIGHT],
  611. fitted_lines[EdgeCornerType.TOP]
  612. )
  613. if top_right is not None:
  614. corners['top_right'] = top_right
  615. # 右下角 = 右边 ∩ 下边
  616. bottom_right = CardDefectDetectionAlgo._get_line_intersection(
  617. fitted_lines[EdgeCornerType.RIGHT],
  618. fitted_lines[EdgeCornerType.BOTTOM]
  619. )
  620. if bottom_right is not None:
  621. corners['bottom_right'] = bottom_right
  622. # 左下角 = 左边 ∩ 下边
  623. bottom_left = CardDefectDetectionAlgo._get_line_intersection(
  624. fitted_lines[EdgeCornerType.LEFT],
  625. fitted_lines[EdgeCornerType.BOTTOM]
  626. )
  627. if bottom_left is not None:
  628. corners['bottom_left'] = bottom_left
  629. # 检查是否获得了所有角点
  630. if len(corners) != 4:
  631. fry_algo_print("警告", f"只计算出{len(corners)}个角点")
  632. return None
  633. # 按顺序返回
  634. return np.array([
  635. corners['top_left'],
  636. corners['top_right'],
  637. corners['bottom_right'],
  638. corners['bottom_left']
  639. ])
  640. def _calculate_defects_with_mask(
  641. self,
  642. contour_points: np.ndarray,
  643. fitted_quad: np.ndarray,
  644. classified_points: Dict[EdgeCornerType, np.ndarray],
  645. img_shape: Tuple[int, int],
  646. long_corner_length: float,
  647. short_corner_length: float,
  648. edge_width: float
  649. ) -> Tuple[Dict[EdgeCornerType, EdgeCornerDefect], np.ndarray]:
  650. """使用mask方法计算缺陷
  651. Returns:
  652. (缺陷字典, 内部四个点)
  653. """
  654. defects = {}
  655. # 获取拟合四边形的最小外接矩形,识别长短边
  656. _, _, _, long_edge_indices, short_edge_indices = self._get_min_area_rect(fitted_quad)
  657. # 计算内部四个点
  658. inner_points = self.mask_processor.calculate_inner_points(
  659. fitted_quad, long_edge_indices, short_edge_indices,
  660. long_corner_length, short_corner_length
  661. )
  662. # 创建轮廓和拟合四边形的mask
  663. contour_mask = self.mask_processor.polygon_to_mask(contour_points, img_shape)
  664. fitted_mask = self.mask_processor.polygon_to_mask(fitted_quad, img_shape)
  665. # 处理四个角
  666. corner_types = [
  667. EdgeCornerType.TOP_LEFT,
  668. EdgeCornerType.TOP_RIGHT,
  669. EdgeCornerType.BOTTOM_RIGHT,
  670. EdgeCornerType.BOTTOM_LEFT
  671. ]
  672. for i, corner_type in enumerate(corner_types):
  673. defect = EdgeCornerDefect(type=corner_type)
  674. # 获取角区域mask
  675. region_mask = self.mask_processor.get_corner_region_mask(
  676. i, fitted_quad, inner_points, img_shape
  677. )
  678. # 计算缺陷
  679. protrusion_pixels, depression_pixels = self.mask_processor.calculate_defect_pixels(
  680. contour_mask, fitted_mask, region_mask
  681. )
  682. defect.protrusion_pixels = protrusion_pixels
  683. defect.depression_pixels = depression_pixels
  684. defect.protrusion_area = float(protrusion_pixels)
  685. defect.depression_area = float(depression_pixels)
  686. # 保存该区域的点
  687. edge_corner_points = classified_points.get(corner_type, np.array([]))
  688. if len(edge_corner_points) > 0:
  689. defect.contour_points = edge_corner_points.tolist()
  690. defects[corner_type] = defect
  691. # 处理四条边
  692. edge_types = [
  693. EdgeCornerType.TOP,
  694. EdgeCornerType.RIGHT,
  695. EdgeCornerType.BOTTOM,
  696. EdgeCornerType.LEFT
  697. ]
  698. for i, edge_type in enumerate(edge_types):
  699. defect = EdgeCornerDefect(type=edge_type)
  700. # 获取边区域mask
  701. region_mask = self.mask_processor.get_edge_region_mask(
  702. i, fitted_quad, inner_points, edge_width, img_shape
  703. )
  704. # 计算缺陷
  705. protrusion_pixels, depression_pixels = self.mask_processor.calculate_defect_pixels(
  706. contour_mask, fitted_mask, region_mask
  707. )
  708. defect.protrusion_pixels = protrusion_pixels
  709. defect.depression_pixels = depression_pixels
  710. defect.protrusion_area = float(protrusion_pixels)
  711. defect.depression_area = float(depression_pixels)
  712. # 保存该区域的点
  713. edge_points = classified_points.get(edge_type, np.array([]))
  714. if len(edge_points) > 0:
  715. defect.contour_points = edge_points.tolist()
  716. defects[edge_type] = defect
  717. return defects, inner_points
  718. def _draw_text_with_background(
  719. self,
  720. img: np.ndarray,
  721. text: str,
  722. position: Tuple[int, int],
  723. font_scale: float = 0.6,
  724. font_thickness: int = 1,
  725. text_color: Tuple[int, int, int] = (255, 255, 255),
  726. bg_color: Tuple[int, int, int] = (0, 0, 0),
  727. bg_opacity: float = 0.7
  728. ) -> np.ndarray:
  729. """在图像上绘制带半透明背景的文字"""
  730. font = cv2.FONT_HERSHEY_SIMPLEX
  731. (text_width, text_height), baseline = cv2.getTextSize(
  732. text, font, font_scale, font_thickness
  733. )
  734. x, y = position
  735. padding = 5
  736. bg_pt1 = (x - padding, y - text_height - padding)
  737. bg_pt2 = (x + text_width + padding, y + baseline + padding)
  738. overlay = img.copy()
  739. cv2.rectangle(overlay, bg_pt1, bg_pt2, bg_color, -1)
  740. cv2.addWeighted(overlay, bg_opacity, img, 1 - bg_opacity, 0, img)
  741. cv2.putText(img, text, position, font, font_scale, text_color, font_thickness)
  742. return img
  743. def _draw_overlay_on_image(
  744. self,
  745. image: np.ndarray,
  746. step_name: str,
  747. contour_points: Optional[np.ndarray] = None,
  748. min_rect: Optional[np.ndarray] = None,
  749. fitted_quad: Optional[np.ndarray] = None,
  750. inner_points: Optional[np.ndarray] = None,
  751. classified_points: Optional[Dict[EdgeCornerType, np.ndarray]] = None,
  752. defects: Optional[Dict[EdgeCornerType, EdgeCornerDefect]] = None
  753. ) -> np.ndarray:
  754. """在图像上绘制叠加可视化"""
  755. overlay = image.copy()
  756. # 绘制轮廓
  757. if contour_points is not None and len(contour_points) > 0:
  758. cv2.polylines(overlay, [contour_points.astype(np.int32)], True, (255, 0, 0), 2)
  759. # 绘制最小外接矩形
  760. if min_rect is not None and len(min_rect) == 4:
  761. cv2.polylines(overlay, [min_rect.astype(np.int32)], True, (0, 255, 0), 2)
  762. # 绘制拟合四边形
  763. if fitted_quad is not None and len(fitted_quad) == 4:
  764. cv2.polylines(overlay, [fitted_quad.astype(np.int32)], True, (0, 0, 255), 3)
  765. for i, point in enumerate(fitted_quad):
  766. cv2.circle(overlay, tuple(point.astype(int)), 8, (0, 0, 255), -1)
  767. self._draw_text_with_background(
  768. overlay, f"C{i}", tuple(point.astype(int)),
  769. font_scale=0.8, text_color=(255, 255, 255)
  770. )
  771. # 绘制内部四个点
  772. if inner_points is not None and len(inner_points) == 4:
  773. for i, point in enumerate(inner_points):
  774. cv2.circle(overlay, tuple(point.astype(int)), 10, (255, 0, 255), -1)
  775. self._draw_text_with_background(
  776. overlay, f"B{i + 1}", tuple(point.astype(int) + np.array([15, 0])),
  777. font_scale=0.8, text_color=(255, 255, 0), bg_color=(128, 0, 128)
  778. )
  779. # 绘制连接线(可选)
  780. if fitted_quad is not None:
  781. cv2.line(overlay, tuple(fitted_quad[i].astype(int)),
  782. tuple(point.astype(int)), (255, 0, 255), 1, cv2.LINE_AA)
  783. # 绘制分类点
  784. if classified_points:
  785. colors = {
  786. EdgeCornerType.TOP_LEFT: (255, 0, 0),
  787. EdgeCornerType.TOP_RIGHT: (0, 255, 0),
  788. EdgeCornerType.BOTTOM_LEFT: (255, 255, 0),
  789. EdgeCornerType.BOTTOM_RIGHT: (0, 255, 255),
  790. EdgeCornerType.TOP: (255, 0, 255),
  791. EdgeCornerType.BOTTOM: (128, 0, 255),
  792. EdgeCornerType.LEFT: (0, 128, 255),
  793. EdgeCornerType.RIGHT: (255, 128, 0)
  794. }
  795. for edge_type, points in classified_points.items():
  796. if len(points) > 0:
  797. color = colors.get(edge_type, (128, 128, 128))
  798. for point in points:
  799. cv2.circle(overlay, tuple(point.astype(int)), 3, color, -1)
  800. # 绘制缺陷信息
  801. if defects:
  802. y_offset = 30
  803. for edge_type, defect in defects.items():
  804. if defect.protrusion_pixels >= 0 or defect.depression_pixels >= 0:
  805. text = f"{edge_type.name}: protrusion={defect.protrusion_pixels}px, depression={defect.depression_pixels}px"
  806. self._draw_text_with_background(
  807. overlay, text, (10, y_offset),
  808. font_scale=0.5, text_color=(255, 255, 255),
  809. bg_color=(0, 0, 128), bg_opacity=0.7
  810. )
  811. y_offset += 25
  812. # 添加步骤标题
  813. self._draw_text_with_background(
  814. overlay, step_name, (10, overlay.shape[0] - 20),
  815. font_scale=1.0, text_color=(255, 255, 255),
  816. bg_color=(255, 0, 0), bg_opacity=0.7
  817. )
  818. return overlay
  819. def _save_intermediate_result(
  820. self,
  821. step_num: int,
  822. step_name: str,
  823. data: Any,
  824. output_dir: Path,
  825. image: Optional[np.ndarray] = None,
  826. save_overlay: bool = True,
  827. inner_points: Optional[np.ndarray] = None
  828. ):
  829. """保存中间结果"""
  830. step_dir = output_dir / "intermediate_results"
  831. step_dir.mkdir(parents=True, exist_ok=True)
  832. json_path = step_dir / f"step_{step_num:02d}_{step_name}.json"
  833. with open(json_path, 'w', encoding='utf-8') as f:
  834. json.dump(to_json_serializable(data), f, ensure_ascii=False, indent=2)
  835. if save_overlay and image is not None:
  836. overlay_image = self._draw_overlay_on_image(
  837. image=image,
  838. step_name=f"Step {step_num}: {step_name}",
  839. contour_points=np.array(data.get("contour", [])) if "contour" in data else None,
  840. min_rect=np.array(data.get("min_rect", [])) if "min_rect" in data else None,
  841. fitted_quad=np.array(data.get("fitted_quad", [])) if "fitted_quad" in data else None,
  842. inner_points=inner_points,
  843. classified_points={EdgeCornerType(k): np.array(v) for k, v in
  844. data.get("classified_points", {}).items()} if "classified_points" in data else None,
  845. defects={EdgeCornerType(k): EdgeCornerDefect.load_from_dict(v) for k, v in
  846. data.get("defects", {}).items()} if "defects" in data else None
  847. )
  848. overlay_path = step_dir / f"step_{step_num:02d}_{step_name}_overlay.jpg"
  849. cv2.imwrite(str(overlay_path), overlay_image)
  850. def _interpolate_contour_points(self, contour_points: np.ndarray,
  851. interpolation_method: str = "linear") -> np.ndarray:
  852. """
  853. 在轮廓点之间插入新点,增加点的密度
  854. Args:
  855. contour_points: 原始轮廓点
  856. interpolation_method: 插值方法,可选 "linear" 或 "midpoint"
  857. Returns:
  858. 插值后的轮廓点
  859. """
  860. if len(contour_points) < 2:
  861. return contour_points
  862. interpolated_points = []
  863. for i in range(len(contour_points)):
  864. # 添加原始点
  865. interpolated_points.append(contour_points[i])
  866. # 获取下一个点(形成闭合轮廓)
  867. next_idx = (i + 1) % len(contour_points)
  868. next_point = contour_points[next_idx]
  869. if interpolation_method == "midpoint":
  870. # 简单的中点插值
  871. mid_point = (contour_points[i] + next_point) / 2.0
  872. interpolated_points.append(mid_point)
  873. elif interpolation_method == "linear":
  874. # 线性插值,可以插入多个点
  875. distance = np.linalg.norm(next_point - contour_points[i])
  876. # 根据距离决定插入点的数量(每50像素插入一个点)
  877. num_insert = max(1, int(distance / 50))
  878. for j in range(1, num_insert + 1):
  879. t = j / (num_insert + 1)
  880. interp_point = (1 - t) * contour_points[i] + t * next_point
  881. interpolated_points.append(interp_point)
  882. return np.array(interpolated_points, dtype=np.int32)
  883. def process_single_group_entrance(
  884. self,
  885. data_dict: Dict,
  886. algo_params: CardDefectDetectionParams
  887. ) -> Dict:
  888. """处理单个数据组的核心逻辑"""
  889. if algo_params.debug_level == 'detail':
  890. fry_algo_print("信息", f"开始处理数据,参数: {algo_params.to_dict()}")
  891. image = data_dict.get("image")
  892. json_data = data_dict.get("label", {})
  893. output_dir = Path(data_dict.get("output_dir", "./output"))
  894. output_dir.mkdir(parents=True, exist_ok=True)
  895. # 获取图像尺寸
  896. if image is not None:
  897. img_shape = image.shape
  898. else:
  899. img_shape = (json_data.get("imageHeight", 2048), json_data.get("imageWidth", 2048), 3)
  900. # 步骤1:读取并筛选唯一形状
  901. shapes = json_data.get("shapes", [])
  902. unique_shape = self._get_unique_shape(shapes, algo_params.label_name)
  903. if unique_shape is None:
  904. fry_algo_print("错误", f"未找到标签为{algo_params.label_name}的形状")
  905. return data_dict
  906. contour_points = np.array(unique_shape["points"])
  907. if algo_params.save_intermediate_results:
  908. self._save_intermediate_result(
  909. 1, "筛选唯一形状",
  910. {"contour": contour_points.tolist(), "shape_info": unique_shape},
  911. output_dir, image, algo_params.save_overlay_images
  912. )
  913. # ========== 步骤1.5: 轮廓点插值(新增) ==========
  914. interpolated_contour = self._interpolate_contour_points(contour_points, "linear")
  915. if algo_params.debug_level == 'detail':
  916. fry_algo_print("信息", f"轮廓点插值: {len(contour_points)} -> {len(interpolated_contour)} 点")
  917. # 使用插值后的轮廓点进行后续处理
  918. contour_points = interpolated_contour
  919. # 步骤2:获取最小外接矩形
  920. min_rect, long_edge, short_edge, _, _ = self._get_min_area_rect(contour_points)
  921. if algo_params.save_intermediate_results:
  922. self._save_intermediate_result(
  923. 2, "最小外接矩形",
  924. {
  925. "contour": contour_points.tolist(),
  926. "min_rect": min_rect.tolist(),
  927. "long_edge": float(long_edge),
  928. "short_edge": float(short_edge)
  929. },
  930. output_dir, image, algo_params.save_overlay_images
  931. )
  932. # 步骤3:初始分类
  933. classified_points = self._classify_points_to_edges_corners(
  934. contour_points, min_rect, long_edge, short_edge,
  935. algo_params.long_edge_corner_length,
  936. algo_params.short_edge_corner_length
  937. )
  938. if algo_params.save_intermediate_results:
  939. classified_dict = {k.value: v.tolist() if len(v) > 0 else []
  940. for k, v in classified_points.items()}
  941. self._save_intermediate_result(
  942. 3, "初始点分类",
  943. {"classified_points": classified_dict, "min_rect": min_rect.tolist()},
  944. output_dir, image, algo_params.save_overlay_images
  945. )
  946. # 步骤4-5:迭代拟合
  947. fitted_quad = min_rect
  948. for iteration in range(algo_params.iteration_rounds):
  949. # 使用新的OpenCV拟合方法(只用边点)
  950. fitted_lines = self._fit_lines_opencv(
  951. classified_points,
  952. algo_params.ransac_residual_threshold
  953. )
  954. new_quad = self._get_quadrilateral_from_lines(fitted_lines)
  955. if new_quad is not None:
  956. fitted_quad = new_quad
  957. # 重新分类点
  958. classified_points = self._classify_points_to_edges_corners(
  959. contour_points, fitted_quad, long_edge, short_edge,
  960. algo_params.long_edge_corner_length,
  961. algo_params.short_edge_corner_length
  962. )
  963. if algo_params.save_intermediate_results and iteration == algo_params.iteration_rounds - 1:
  964. classified_dict = {k.value: v.tolist() if len(v) > 0 else []
  965. for k, v in classified_points.items()}
  966. # 转换fitted_lines格式用于保存
  967. fitted_lines_dict = {k.value: v for k, v in fitted_lines.items()}
  968. self._save_intermediate_result(
  969. 4 + iteration, f"迭代{iteration + 1}_拟合结果",
  970. {
  971. "classified_points": classified_dict,
  972. "fitted_quad": fitted_quad.tolist(),
  973. "fitted_lines": fitted_lines_dict
  974. },
  975. output_dir, image, algo_params.save_overlay_images
  976. )
  977. # 步骤6:计算缺陷
  978. defects, inner_points = self._calculate_defects_with_mask(
  979. contour_points, fitted_quad, classified_points,
  980. img_shape,
  981. algo_params.long_edge_corner_length,
  982. algo_params.short_edge_corner_length,
  983. algo_params.edge_width
  984. )
  985. defects_dict = {k.value: v.to_dict() for k, v in defects.items()}
  986. if algo_params.save_intermediate_results:
  987. self._save_intermediate_result(
  988. 5 + algo_params.iteration_rounds, "最终缺陷结果",
  989. {
  990. "defects": defects_dict,
  991. "fitted_quad": fitted_quad.tolist(),
  992. "inner_points": inner_points.tolist(),
  993. "contour": contour_points.tolist()
  994. },
  995. output_dir, image, algo_params.save_overlay_images,
  996. inner_points
  997. )
  998. # 保存最终结果
  999. result_path = output_dir / "defect_detection_result.json"
  1000. final_result = {
  1001. "defects": defects_dict,
  1002. "fitted_quad": fitted_quad.tolist(),
  1003. "inner_points": inner_points.tolist()
  1004. }
  1005. with open(result_path, 'w', encoding='utf-8') as f:
  1006. json.dump(final_result, f, ensure_ascii=False, indent=2)
  1007. if algo_params.debug_level in ['normal', 'detail']:
  1008. fry_algo_print("成功", f"缺陷检测完成,结果已保存到: {result_path}")
  1009. data_dict["defects"] = defects_dict
  1010. data_dict["fitted_quad"] = fitted_quad.tolist()
  1011. data_dict["inner_points"] = inner_points.tolist()
  1012. return data_dict
  1013. def process_batch_group_entrance(
  1014. self,
  1015. input_dir_str: str,
  1016. output_dir_str: str,
  1017. algo_params: CardDefectDetectionParams
  1018. ) -> None:
  1019. """批量处理多个JSON文件"""
  1020. input_dir = Path(input_dir_str)
  1021. output_dir = Path(output_dir_str)
  1022. output_dir.mkdir(parents=True, exist_ok=True)
  1023. json_files = list(input_dir.rglob("*.json"))
  1024. if not json_files:
  1025. fry_algo_print("警告", f"未在{input_dir}中找到JSON文件")
  1026. return
  1027. fry_algo_print("信息", f"找到{len(json_files)}个JSON文件")
  1028. for json_file in json_files:
  1029. fry_algo_print("组开始", f"处理文件: {json_file.name}")
  1030. try:
  1031. json_data = self.load_json_data(json_file)
  1032. image_stem = json_file.stem
  1033. image_extensions = ['.jpg', '.jpeg', '.png', '.bmp']
  1034. image_path = None
  1035. for ext in image_extensions:
  1036. potential_path = json_file.parent / f"{image_stem}{ext}"
  1037. if potential_path.exists():
  1038. image_path = potential_path
  1039. break
  1040. image_width = json_data.get("imageWidth", 2048)
  1041. image_height = json_data.get("imageHeight", 2048)
  1042. if image_path:
  1043. image = self.load_or_create_image(image_path, image_width, image_height)
  1044. else:
  1045. raise ValueError("警告", f"未找到对应的图像文件")
  1046. relative_path = json_file.relative_to(input_dir)
  1047. file_output_dir = output_dir / relative_path.parent / json_file.stem
  1048. file_output_dir.mkdir(parents=True, exist_ok=True)
  1049. data_dict = {
  1050. "image": image,
  1051. "label": json_data,
  1052. "output_dir": str(file_output_dir),
  1053. "answer_json": {}
  1054. }
  1055. result = self.process_single_group_entrance(data_dict, algo_params)
  1056. fry_algo_print("成功", f"文件{json_file.name}处理完成")
  1057. except Exception as e:
  1058. fry_algo_print("错误", f"处理文件{json_file.name}时出错: {str(e)}")
  1059. import traceback
  1060. traceback.print_exc()
  1061. fry_algo_print("组结束", f"文件{json_file.name}处理结束")
  1062. fry_algo_print("成功", "批量处理完成")
  1063. def _test_card_defect_detection_real():
  1064. """测试真实数据"""
  1065. temp_dir = Path("_test_real").resolve()
  1066. input_dir = temp_dir / "input"
  1067. output_dir = temp_dir / "output"
  1068. try:
  1069. algo = CardDefectDetectionAlgo()
  1070. params = CardDefectDetectionParams(
  1071. # debug_level="detail",
  1072. debug_level="no",
  1073. save_intermediate_results=True,
  1074. save_overlay_images=True,
  1075. label_name="outer_box",
  1076. long_edge_corner_length=200,
  1077. short_edge_corner_length=200,
  1078. edge_width=50,
  1079. iteration_rounds=3,
  1080. ransac_residual_threshold=5.0
  1081. )
  1082. algo.process_batch_group_entrance(
  1083. str(input_dir),
  1084. str(output_dir),
  1085. params
  1086. )
  1087. fry_algo_print("成功", f"测试完成,结果保存在: {output_dir}")
  1088. result_files = list(output_dir.rglob("*"))
  1089. for result_file in result_files:
  1090. if result_file.is_file():
  1091. fry_algo_print("信息", f"生成文件: {result_file}")
  1092. finally:
  1093. pass
  1094. def _test_one_img():
  1095. algo = CardDefectDetectionAlgo()
  1096. image_path = r"C:\Code\ML\Image\Card\_250915_many_capture_img\_250915_1743_reflect_nature_defrct\1_front_coaxial_1_0.jpg"
  1097. json_path = r"C:\Code\ML\Project\卡片缺陷检测项目组\_250903_1644_边角不直脚本优化\_test_real\input\1_front_coaxial_1_0.json"
  1098. image = algo.load_or_create_image(image_path)
  1099. json_data = algo.load_json_data(json_path)
  1100. temp_out_dir = r"temp_out_dir"
  1101. data_dict = {
  1102. "image": image,
  1103. "label": json_data,
  1104. "output_dir": str(temp_out_dir),
  1105. "answer_json": {}
  1106. }
  1107. algo_params = CardDefectDetectionParams(
  1108. # debug_level="detail",
  1109. debug_level="no",
  1110. save_intermediate_results=True,
  1111. save_overlay_images=True,
  1112. label_name="outer_box",
  1113. long_edge_corner_length=200,
  1114. short_edge_corner_length=200,
  1115. edge_width=50,
  1116. iteration_rounds=3,
  1117. ransac_residual_threshold=5.0
  1118. )
  1119. result = algo.process_single_group_entrance(data_dict, algo_params)
  1120. print(result)
  1121. if __name__ == "__main__":
  1122. # _test_card_defect_detection_real()
  1123. _test_one_img()