Pārlūkot izejas kodu

大改, 分数计算的外框使用转正前的旋转框, 并跟随图片旋转

AnlaAnla 2 nedēļas atpakaļ
vecāks
revīzija
4613213dee

+ 5 - 2
app/api/card_inference.py

@@ -101,12 +101,15 @@ async def card_rectify_and_center(
         raise ValueError("无法解码图像,请确保上传的是有效的图片格式 (JPG, PNG, etc.)")
 
     try:
-        # 3. 传递参数时,使用 .value 获取 Enum 的字符串值
-        img_result = await run_in_threadpool(
+        # 3. 接收解包后的结果,只取 image 部分进行返回
+        img_result, _ = await run_in_threadpool(
             service.rectify_and_center,
             img_bgr=img_bgr
         )
 
+        if img_result is None:
+            raise HTTPException(status_code=500, detail="图像处理失败")
+
         is_success, buffer = cv2.imencode(".jpg", img_result)
         jpeg_bytes = buffer.tobytes()
 

+ 26 - 6
app/services/card_rectify_and_center.py

@@ -1,5 +1,6 @@
 import cv2
 import numpy as np
+import copy
 from ..core.model_loader import get_predictor
 from app.utils.defect_inference.img_rectify_and_center import (
     FryCardProcessParams, FryCardProcessor, CenterMode, FillMode)
@@ -45,7 +46,10 @@ class CardRectifyAndCenter:
 
         return json_data
 
-    def rectify_and_center(self, img_bgr: np.ndarray) -> np.ndarray:
+    def rectify_and_center(self, img_bgr: np.ndarray) -> tuple[np.ndarray, dict]:
+        """
+        修改返回值: (final_image, rectified_json)
+        """
         # 1. 设置处理参数
         params = FryCardProcessParams(
             debug_level="detail",
@@ -57,13 +61,29 @@ class CardRectifyAndCenter:
         # 2. 初始化处理器
         processor = FryCardProcessor()
 
+        # 3. 原始外框推理
         seg_json = self.outer_box_model.predict_from_image(img_bgr)
-        # 过滤噪点,只留最大置信度的外框
         seg_json = self._keep_max_prob_shape(seg_json)
 
-        # 4. 执行处理
-        final_image = processor.process_image_with_json(img_bgr, seg_json, params)
+        # 4. 执行处理,获取图像和变换矩阵
+        final_image, matrix = processor.process_image_with_json(img_bgr, seg_json, params)
+
+        # 5. 生成变换后的 JSON 数据
+        rectified_json = copy.deepcopy(seg_json)
+        if matrix is not None and 'shapes' in rectified_json:
+            logger.info("正在将原始外框坐标映射到转正后的坐标系...")
+            for shape in rectified_json['shapes']:
+                original_points = shape.get('points', [])
+                if original_points:
+                    # 使用矩阵变换点
+                    new_points = processor.transform_points_with_matrix(original_points, matrix)
+                    shape['points'] = new_points
+                    # 清除可能存在的 rect_box,确保后续逻辑重新计算最小外接矩形
+                    if 'rect_box' in shape:
+                        del shape['rect_box']
+
         temp_img_path = settings.TEMP_WORK_DIR / "rectify_center_img.jpg"
-        cv2.imwrite(temp_img_path, final_image)
+        if final_image is not None:
+            cv2.imwrite(temp_img_path, final_image)
 
-        return final_image
+        return final_image, rectified_json

+ 31 - 29
app/services/defect_service.py

@@ -38,13 +38,15 @@ class DefectInferenceService:
         return json_data
 
     def defect_inference(self, inference_type: str, img_bgr: np.ndarray,
-                         is_draw_image=True) -> dict:
+                         is_draw_image=True,
+                         pre_calculated_outer_result: dict = None) -> dict:
         """
         执行卡片识别推理。
 
         Args:
             inference_type: 模型类型 (e.g., 'outer_box').
             img_bgr: 图像。
+            pre_calculated_outer_result: 如果提供了转正后的外框数据,则不再重新推理外框
 
         Returns:
             一个包含推理结果的字典。
@@ -52,6 +54,20 @@ class DefectInferenceService:
         simplifyPoints = SimplifyPoints()
         outside_filter = FilterOutsideDefects(expansion_pixel=30)
 
+        # 辅助函数:获取外框结果(优先使用传入的,否则现场推理)
+        def get_outer_result(tag=""):
+            if pre_calculated_outer_result:
+                logger.info(f"[{tag}] 使用预计算的转正外框数据")
+                return pre_calculated_outer_result
+            else:
+                logger.info(f"[{tag}] 未提供外框数据,开始推理外框...")
+                p_outer = get_predictor("outer_box")
+                res = p_outer.predict_from_image(img_bgr)
+                # 如果是现场推理的,需要过滤一下
+                if inference_type in ["pokemon_front_card_center", "pokemon_back_card_center"]:
+                    res = self._filter_max_prob_shape(res, tag=f"{tag}-过滤")
+                return res
+
         # 面
         if (inference_type == "pokemon_back_face_coaxial_light_defect"
                 or inference_type == "pokemon_front_face_reflect_coaxial_light_defect"
@@ -77,18 +93,14 @@ class DefectInferenceService:
             # 简化点数
             logger.info("开始简化点数")
             for shapes in json_data["shapes"]:
-                points = shapes["points"]
+                shapes["points"] = simplifyPoints.simplify_points(shapes["points"])
                 # num1 = len(points)
-                simplify_points = simplifyPoints.simplify_points(points)
-                shapes["points"] = simplify_points
-                # new_num1 = len(simplify_points)
                 # logger.info(f"num: {num1}, new_num1: {new_num1}")
 
             logger.info("开始执行外框过滤...")
             try:
-                # 1. 因为面原本没有外框推理,这里需要专门加载并推理一次
-                predictor_outer = get_predictor("outer_box")
-                outer_result = predictor_outer.predict_from_image(img_bgr)
+                # 获取外框
+                outer_result = get_outer_result(tag="面流程")
 
                 # 2. 执行过滤
                 json_data = outside_filter.execute(json_data, outer_result)
@@ -120,7 +132,7 @@ class DefectInferenceService:
 
             return face_json_result
 
-        # 存在边角判断的情况
+        # 边角
         elif (inference_type == "pokemon_back_face_ring_light_defect"
               or inference_type == "pokemon_front_face_reflect_ring_light_defect"
               or inference_type == "pokemon_front_face_no_reflect_ring_light_defect"):
@@ -140,13 +152,7 @@ class DefectInferenceService:
             # 简化点数
             logger.info("开始进行点数简化")
             for shapes in json_data["shapes"]:
-                points = shapes["points"]
-                num1 = len(points)
-                simplify_points = simplifyPoints.simplify_points(points)
-                shapes["points"] = simplify_points
-                new_num1 = len(simplify_points)
-                # logger.info(f"num: {num1}, new_num1: {new_num1}")
-            logger.info("点数简化结束")
+                shapes["points"] = simplifyPoints.simplify_points(shapes["points"])
 
             # merge_json_path = settings.TEMP_WORK_DIR / f'{inference_type}-merge.json'
             # with open(merge_json_path, 'w', encoding='utf-8') as f:
@@ -155,11 +161,11 @@ class DefectInferenceService:
 
             logger.info("开始执行外框过滤...")
             # 外框推理
-            predictor_outer = get_predictor("outer_box")
-            outer_result = predictor_outer.predict_from_image(img_bgr)
+            outer_result = get_outer_result(tag="Corner流程")
 
             # 过滤外框,只留一个
-            outer_result = self._filter_max_prob_shape(outer_result, tag="Corner流程-外框")
+            if not pre_calculated_outer_result:
+                outer_result = self._filter_max_prob_shape(outer_result, tag="Corner流程-外框")
 
             # 2. 执行过滤, 去掉外框外的缺陷
             json_data = outside_filter.execute(json_data, outer_result)
@@ -188,25 +194,21 @@ class DefectInferenceService:
 
             return edge_corner_data
 
+        # 居中
         elif inference_type == "pokemon_front_card_center" \
                 or inference_type == "pokemon_back_card_center":
 
             predictor_inner = get_predictor(settings.DEFECT_TYPE[inference_type]['inner_box'])
-            predictor_outer = get_predictor(settings.DEFECT_TYPE[inference_type]['outer_box'])
+            # predictor_outer = get_predictor(settings.DEFECT_TYPE[inference_type]['outer_box'])
 
             inner_result = predictor_inner.predict_from_image(img_bgr)
-            outer_result = predictor_outer.predict_from_image(img_bgr)
+            outer_result = get_outer_result(tag="Center流程")
 
             # 过滤内框和外框,只留最大置信度的框
             inner_result = self._filter_max_prob_shape(inner_result, tag="Center流程-内框")
-            outer_result = self._filter_max_prob_shape(outer_result, tag="Center流程-外框")
-
-            # temp_inner_json_path = settings.TEMP_WORK_DIR / f'{inference_type}-inner_result.json'
-            # temp_outer_json_path = settings.TEMP_WORK_DIR / f'{inference_type}-outer_result.json'
-            # with open(temp_inner_json_path, 'w', encoding='utf-8') as f:
-            #     json.dump(inner_result, f, ensure_ascii=False, indent=4)
-            # with open(temp_outer_json_path, 'w', encoding='utf-8') as f:
-            #     json.dump(outer_result, f, ensure_ascii=False, indent=4)
+            # 如果是传入的预计算结果,它已经过滤过了;如果是现算的,需要过滤
+            if not pre_calculated_outer_result:
+                outer_result = self._filter_max_prob_shape(outer_result, tag="Center流程-外框")
 
             inner_points = inner_result['shapes'][0]['points']
             outer_points = outer_result['shapes'][0]['points']

+ 23 - 13
app/services/score_service.py

@@ -22,44 +22,54 @@ class ScoreService:
     def score_inference(self, score_type: str, is_reflect_card: bool,
                         img_bgr: np.ndarray) -> dict:
 
-        logger.info("开始进行卡片居中和转正")
-        img_bgr = self.rectify_center_service.rectify_and_center(img_bgr)
+        # 解包返回值,获取转正后的外框数据
+        img_bgr, transformed_outer_json = self.rectify_center_service.rectify_and_center(img_bgr)
+        if img_bgr is None:
+            raise ValueError("图像转正处理失败")
+
         imageHeight, imageWidth = img_bgr.shape[:2]
 
-        logger.info("开始进行卡片分数推理")
+        logger.info("开始进行卡片分数推理, 使用变换后的外框")
+        # 定义通用参数,传入 pre_calculated_outer_result
+        defect_kwargs = {
+            "img_bgr": img_bgr.copy(),
+            "pre_calculated_outer_result": transformed_outer_json
+        }
+
+
         if score_type == 'front_ring' or score_type == 'front_coaxial':
-            center_data = self.defect_service.defect_inference("pokemon_front_card_center", img_bgr.copy())
+            center_data = self.defect_service.defect_inference("pokemon_front_card_center", **defect_kwargs)
         else:
-            center_data = self.defect_service.defect_inference("pokemon_back_card_center", img_bgr.copy())
+            center_data = self.defect_service.defect_inference("pokemon_back_card_center", **defect_kwargs)
 
         if is_reflect_card:
             if score_type == 'front_ring':
                 defect_data = self.defect_service.defect_inference('pokemon_front_face_reflect_ring_light_defect',
-                                                                   img_bgr.copy())
+                                                                   **defect_kwargs)
             elif score_type == 'front_coaxial':
                 defect_data = self.defect_service.defect_inference('pokemon_front_face_reflect_coaxial_light_defect',
-                                                                   img_bgr.copy())
+                                                                   **defect_kwargs)
             elif score_type == 'back_ring':
                 defect_data = self.defect_service.defect_inference('pokemon_back_face_ring_light_defect',
-                                                                   img_bgr.copy())
+                                                                   **defect_kwargs)
             elif score_type == 'back_coaxial':
                 defect_data = self.defect_service.defect_inference('pokemon_back_face_coaxial_light_defect',
-                                                                   img_bgr.copy())
+                                                                   **defect_kwargs)
             else:
                 return {}
         else:
             if score_type == 'front_ring':
                 defect_data = self.defect_service.defect_inference('pokemon_front_face_no_reflect_ring_light_defect',
-                                                                   img_bgr.copy())
+                                                                   **defect_kwargs)
             elif score_type == 'front_coaxial':
                 defect_data = self.defect_service.defect_inference('pokemon_front_face_no_reflect_coaxial_light_defect',
-                                                                   img_bgr.copy())
+                                                                   **defect_kwargs)
             elif score_type == 'back_ring':
                 defect_data = self.defect_service.defect_inference('pokemon_back_face_ring_light_defect',
-                                                                   img_bgr.copy())
+                                                                   **defect_kwargs)
             elif score_type == 'back_coaxial':
                 defect_data = self.defect_service.defect_inference('pokemon_back_face_coaxial_light_defect',
-                                                                   img_bgr.copy())
+                                                                   **defect_kwargs)
             else:
                 return {}
 

+ 25 - 7
app/utils/defect_inference/img_rectify_and_center.py

@@ -11,6 +11,7 @@ from app.core.logger import get_logger
 
 logger = get_logger(__name__)
 
+
 def fry_algo_print(level_str: str, info_str: str):
     """通用日志打印函数"""
     logger.info(f"[{level_str.upper()}] : {info_str}")
@@ -79,6 +80,7 @@ class EdgeType(Enum):
     LEFT = "左边"
     RIGHT = "右边"
 
+
 @dataclass
 class ContourInfo:
     """轮廓信息数据类"""
@@ -118,8 +120,8 @@ class FryCardProcessor:
             params: FryCardProcessParams
     ) -> Optional[np.ndarray]:
         """
-        (V2: 简化版,合并旋转与居中)
         使用预先计算的分割JSON对单个图像进行转正和居中处理,并保持原图大小。
+        修改返回值: (final_image, transformation_matrix)
         """
         if params.debug_level in ['normal', 'detail']:
             fry_algo_print("信息", "开始处理图像...")
@@ -138,8 +140,8 @@ class FryCardProcessor:
         if params.debug_level == 'detail':
             fry_algo_print("信息", "成功拟合卡片的最小外接矩形。")
 
-        # ==================== 步骤 3: 图像转正与居中 (一步完成) ====================
-        final_image = self._rectify_and_center_image(image, fitted_quad)
+        # ==================== 步骤 3: 图像转正与居中 并获取矩阵 ====================
+        final_image, M = self._rectify_and_center_image(image, fitted_quad)
 
         if final_image is None:
             fry_algo_print("错误", "图像转正与居中失败。")
@@ -148,7 +150,7 @@ class FryCardProcessor:
         if params.debug_level in ['normal', 'detail']:
             fry_algo_print("成功", f"处理完成。输入尺寸: {image.shape[:2]}, 输出尺寸: {final_image.shape[:2]}")
 
-        return final_image
+        return final_image, M
 
     # ------------------- 辅助方法: 轮廓处理 -------------------
     @staticmethod
@@ -181,7 +183,8 @@ class FryCardProcessor:
         return np.array(interpolated_points, dtype=np.int32)
 
     @staticmethod
-    def _rectify_and_center_image(image: np.ndarray, quad: np.ndarray) -> Optional[np.ndarray]:
+    def _rectify_and_center_image(image: np.ndarray, quad: np.ndarray) -> (
+            Tuple)[Optional[np.ndarray], Optional[np.ndarray]]:
         """
         一步完成图像的旋转矫正和居中,并保持原始图像尺寸。
 
@@ -190,7 +193,7 @@ class FryCardProcessor:
             quad (np.ndarray): 卡片的四个角点.
 
         Returns:
-            Optional[np.ndarray]: 处理后的图像.
+            修改返回值: (final_image, M)
         """
         # 获取原始图像的尺寸,这将是我们最终输出的尺寸
         h_img, w_img = image.shape[:2]
@@ -227,7 +230,7 @@ class FryCardProcessor:
             borderValue=(0, 0, 0)  # 默认使用黑色填充
         )
 
-        return final_image
+        return final_image, M
 
     # ------------------- 辅助方法: 居中 (Centering) -------------------
     @staticmethod
@@ -281,6 +284,21 @@ class FryCardProcessor:
 
         return cv2.warpAffine(image, M, (w, h), borderMode=borderMode, borderValue=borderValue)
 
+    @staticmethod
+    def transform_points_with_matrix(points: List[List[float]], M: np.ndarray) -> List[List[float]]:
+        """
+        辅助函数:使用变换矩阵 M 将点映射到新坐标系
+        """
+        if not points or M is None:
+            return points
+
+        # 转换为 numpy 数组 (N, 1, 2) 格式用于 cv2.transform
+        pts_np = np.array([points], dtype=np.float32)
+        # 变换
+        transformed_pts = cv2.transform(pts_np, M)
+        # 转回 list
+        return transformed_pts[0].tolist()
+
     # ------------------- RANSAC 拟合静态方法集 -------------------
     @staticmethod
     def _classify_contour_points(contour: np.ndarray, rect_corners: np.ndarray, corner_ratio: float) -> Dict[str, List]: