Przeglądaj źródła

评级报告信息生成和缺陷图的存储

AnlaAnla 1 tydzień temu
rodzic
commit
76cf14edb3

+ 1 - 1
Test/img_score_and_insert2.py

@@ -328,7 +328,7 @@ if __name__ == "__main__":
     BASE_PATH = r"C:\Code\ML\Image\Card\img20_test"
 
     # 模拟循环处理
-    for img_num in range(4, 11):
+    for img_num in range(1, 11):
         print(f">>>>> 处理图片组: {img_num}")
 
         # 构造路径 (假设文件名格式如下,可根据实际修改)

+ 285 - 5
Test/test02.json

@@ -1,7 +1,287 @@
 {
-  "wear": [
-    "轻微", "严重", "asdas", "afas"
-  ],
-  "loss": [],
-  "impact": []
+  "id": 41,
+  "id_prev": 40,
+  "id_next": null,
+  "card_name": "10 外框修正测试 0127_120050",
+  "created_at": "2026-01-27T04:00:56",
+  "updated_at": "2026-01-27T12:00:58",
+  "card_type": "pokemon",
+  "is_edited": false,
+  "detection_score": 8.32,
+  "modified_score": null,
+  "detection_score_detail": {
+    "detection_center_score": 8.95,
+    "detection_corner_score": 8.2,
+    "detection_edge_score": 6.2040000000000015,
+    "detection_face_score": 8.5855
+  },
+  "modified_score_detail": {
+    "modified_center_score": null,
+    "modified_corner_score": null,
+    "modified_edge_score": null,
+    "modified_face_score": null
+  },
+  "images": [
+    {
+      "id": 163,
+      "card_id": 41,
+      "image_type": "front_ring",
+      "image_name": null,
+      "image_path": "/Data/7e3a112d-f3cf-445a-9685-7db1bff15912.jpg",
+      "detection_image_path": null,
+      "modified_image_path": null,
+      "detection_json": {
+        "result": {
+          "card_score": 9.5709,
+          "imageWidth": 2915,
+          "imageHeight": 3969,
+          "center_result": {
+            "box_result": {
+              "inner_box": {
+                "shapes": [
+                  {
+                    "label": "inner_box",
+                    "rect_box": [
+                      [
+                        242,
+                        288
+                      ],
+                      [
+                        2643,
+                        288
+                      ],
+                      [
+                        2643,
+                        3666
+                      ],
+                      [
+                        242,
+                        3666
+                      ]
+                    ]
+                  }
+                ]
+              },
+              "outer_box": {
+                "shapes": [
+                  {
+                    "label": "outer_box",
+                    "rect_box": [
+                      [
+                        177,
+                        197
+                      ],
+                      [
+                        2739,
+                        197
+                      ],
+                      [
+                        2739,
+                        3773
+                      ],
+                      [
+                        177,
+                        3773
+                      ]
+                    ]
+                  }
+                ]
+              },
+              "center_inference": {
+                "angel_diff": 0.052398681640625,
+                "center_top": 40.5426492791776,
+                "center_left": 54.19024712318431,
+                "center_right": 45.80975287681569,
+                "center_bottom": 59.4573507208224,
+                "real_width_mm": 62.87148,
+                "real_height_mm": 87.75504
+              }
+            },
+            "deduct_score": -1.0499999999999998
+          },
+          "defect_result": {
+            "defects": [
+              {
+                "label": "scratch",
+                "score": -0.1,
+                "width": 0.10404939545631409,
+                "height": 0.23150989231109617,
+                "min_rect": [
+                  [
+                    1249.1236572265625,
+                    3748.7021484375
+                  ],
+                  [
+                    9.43398094177246,
+                    4.239991664886475
+                  ],
+                  32.0053825378418
+                ],
+                "edit_type": "",
+                "new_score": null,
+                "confidence": 0.5399131959882276,
+                "pixel_area": 16.5,
+                "actual_area": 0.0099364914,
+                "defect_type": "edge",
+                "scratch_length": 0.23150989231109617
+              }
+            ],
+            "statistics": {
+              "count_by_label": {
+                "wear": 5,
+                "scratch": 2
+              },
+              "total_pixel_area": 685,
+              "area_by_label_mm2": {
+                "wear": 0.3574125845999999,
+                "scratch": 0.055102361399999994
+              },
+              "total_defect_count": 7,
+              "total_defect_area_mm2": 0.41251494599999994
+            },
+            "front_edge_deduct_score": -0.27999999999999997,
+            "front_face_deduct_score": -0.3960000000000001,
+            "front_corner_deduct_score": 0
+          },
+          "card_center_deduct_score": -0.26249999999999996,
+          "card_defect_deduct_score": -0.16660000000000005,
+          "_used_compute_deduct_score": -0.4291
+        }
+      },
+      "modified_json": null,
+      "is_edited": false,
+      "created_at": "2026-01-27T04:00:57",
+      "updated_at": "2026-01-27T04:00:57"
+    },
+    {
+      "id": 164,
+      "card_id": 41,
+      "image_type": "back_ring",
+      "image_name": null,
+      "image_path": "/Data/5c3f1f42-fb6b-4b41-954f-0fd7abfbcfa9.jpg",
+      "detection_image_path": null,
+      "modified_image_path": null,
+      "detection_json": {
+        "result": {
+          "card_score": 9.1084,
+          "imageWidth": 2913,
+          "imageHeight": 3966,
+          "center_result": {
+            "box_result": {
+              "inner_box": {
+                "shapes": [
+                  {
+                    "label": "inner_box",
+                    "rect_box": [
+                      [
+                        297,
+                        335
+                      ],
+                      [
+                        2601,
+                        335
+                      ],
+                      [
+                        2601,
+                        3614
+                      ],
+                      [
+                        297,
+                        3614
+                      ]
+                    ]
+                  }
+                ]
+              },
+              "outer_box": {
+                "shapes": [
+                  {
+                    "label": "outer_box",
+                    "rect_box": [
+                      [
+                        176,
+                        195
+                      ],
+                      [
+                        2738,
+                        195
+                      ],
+                      [
+                        2738,
+                        3772
+                      ],
+                      [
+                        176,
+                        3772
+                      ]
+                    ]
+                  }
+                ]
+              },
+              "center_inference": {
+                "angel_diff": 0,
+                "center_top": 46.89931356610641,
+                "center_left": 53.02005725025111,
+                "center_right": 46.97994274974889,
+                "center_bottom": 53.10068643389359,
+                "real_width_mm": 62.87148,
+                "real_height_mm": 87.77958
+              }
+            },
+            "deduct_score": 0
+          },
+          "defect_result": {
+            "defects": [
+              {
+                "label": "wear",
+                "score": -0.1,
+                "width": 0.12931725737571717,
+                "height": 0.12931725737571717,
+                "min_rect": [
+                  [
+                    2398.26904296875,
+                    3754.345703125
+                  ],
+                  [
+                    5.269651889801025,
+                    5.269651889801025
+                  ],
+                  33.6900634765625
+                ],
+                "edit_type": "",
+                "new_score": null,
+                "confidence": 0.7706368033375058,
+                "pixel_area": 20,
+                "actual_area": 0.012044231999999998,
+                "defect_type": "edge"
+              }
+            ],
+            "statistics": {
+              "count_by_label": {
+                "wear": 21,
+                "scratch": 1
+              },
+              "total_pixel_area": 2681,
+              "area_by_label_mm2": {
+                "wear": 1.46036313,
+                "scratch": 0.1541661696
+              },
+              "total_defect_count": 22,
+              "total_defect_area_mm2": 1.6145292996
+            },
+            "back_edge_deduct_score": -3.5159999999999987,
+            "back_face_deduct_score": 0,
+            "back_corner_deduct_score": -1.7999999999999998
+          },
+          "card_center_deduct_score": 0,
+          "card_defect_deduct_score": -0.8915999999999998,
+          "_used_compute_deduct_score": -0.8915999999999998
+        }
+      },
+      "modified_json": null,
+      "is_edited": false,
+      "created_at": "2026-01-27T04:00:57",
+      "updated_at": "2026-01-27T04:00:57"
+    }
+  ]
 }

+ 12 - 0
Test/批量删除.py

@@ -0,0 +1,12 @@
+import requests
+
+response = requests.get("http://127.0.0.1:7755/api/cards/card_list_filter?sort_by=updated_at&sort_order=DESC&skip=0&limit=100")
+
+print(response.json())
+for item in response.json()['data']['list']:
+    id = item['id']
+    requests.delete(f"http://127.0.0.1:7755/api/cards/delete/{id}")
+    print(id)
+print('end')
+
+

+ 292 - 0
app/api/rating_report.py

@@ -0,0 +1,292 @@
+from fastapi import APIRouter, HTTPException, Depends, Query
+from mysql.connector.pooling import PooledMySQLConnection
+import os
+import json
+import math
+from pathlib import Path
+from typing import List, Dict, Any, Optional
+from PIL import Image
+
+from app.core.logger import get_logger
+from app.core.config import settings
+from app.core.database_loader import get_db_connection
+from app.crud import crud_card
+from app.utils.scheme import ImageType
+
+logger = get_logger(__name__)
+router = APIRouter()
+
+# 定义新的缺陷图片存储路径
+DEFECT_IMAGE_DIR = settings.DEFECT_IMAGE_DIR
+
+
+def _get_active_json(image_data: Any) -> Optional[Dict]:
+    """获取有效的json数据,优先 modified_json"""
+    if not image_data:
+        return None
+
+    # image_data 可能是 Pydantic 对象或 字典,做兼容处理
+    if hasattr(image_data, "modified_json"):
+        mj = image_data.modified_json
+        dj = image_data.detection_json
+    else:
+        mj = image_data.get("modified_json")
+        dj = image_data.get("detection_json")
+
+    # 注意:根据 schema.py,这里读出来已经是 dict 了,不需要 json.loads
+    # 如果数据库里存的是 null,读出来是 None
+    if mj:
+        return mj
+    return dj
+
+
+def _crop_defect_image(original_image_path_str: str, min_rect: List, output_filename: str) -> str:
+    """
+    切割缺陷图片为正方形
+    min_rect 结构: [[center_x, center_y], [width, height], angle]
+    """
+    try:
+        # 构建绝对路径
+        # 假设 original_image_path_str 是 "/Data/..." 格式
+        rel_path = original_image_path_str.lstrip('/\\')
+        full_path = settings.BASE_PATH / rel_path
+
+        if not full_path.exists():
+            logger.warning(f"原图不存在: {full_path}")
+            return ""
+
+        with Image.open(full_path) as img:
+            img_w, img_h = img.size
+
+            # 解析 min_rect
+            # min_rect[0] 是中心点 [x, y]
+            # min_rect[1] 是宽高 [w, h]
+            center_x, center_y = min_rect[0]
+            rect_w, rect_h = min_rect[1]
+
+            # 确定裁剪的正方形边长:取宽高的最大值,并适当外扩 (例如 1.5 倍) 以展示周围环境
+            # 如果缺陷非常小,设置一个最小尺寸(例如 100px),避免切图太模糊
+            side_length = max(rect_w, rect_h) * 1.5
+            side_length = max(side_length, 100)
+
+            half_side = side_length / 2
+
+            # 计算裁剪框 (left, top, right, bottom)
+            left = center_x - half_side
+            top = center_y - half_side
+            right = center_x + half_side
+            bottom = center_y + half_side
+
+            # 边界检查,防止超出图片范围
+            # 如果只是想保持正方形,超出部分可以填黑,或者简单的移动框的位置
+            # 这里简单处理:如果超出边界,就移动框,实在移不动就截断
+            if left < 0:
+                right -= left  # 往右移
+                left = 0
+            if top < 0:
+                bottom -= top  # 往下移
+                top = 0
+            if right > img_w:
+                left -= (right - img_w)  # 往左移
+                right = img_w
+            if bottom > img_h:
+                top -= (bottom - img_h)  # 往上移
+                bottom = img_h
+
+            # 再次检查防止负数(比如图片本身比框还小)
+            left = max(0, left)
+            top = max(0, top)
+            right = min(img_w, right)
+            bottom = min(img_h, bottom)
+
+            crop_box = (left, top, right, bottom)
+            cropped_img = img.crop(crop_box)
+
+            # 保存
+            save_path = DEFECT_IMAGE_DIR / output_filename
+            cropped_img.save(save_path, quality=95)
+
+            # 返回 URL 路径 (相对于项目根目录的 web 路径)
+            return f"/DefectImage/{output_filename}"
+
+    except Exception as e:
+        logger.error(f"切割图片失败: {e}")
+        return ""
+
+
+@router.get("/generate", status_code=200, summary="生成评级报告数据")
+def generate_rating_report(
+        card_id: int,
+        db_conn: PooledMySQLConnection = Depends(get_db_connection)
+):
+    top_n_defects = 3
+    """
+    根据 Card ID 生成评级报告 JSON
+    """
+    # 1. 获取卡片详情 (复用 Crud 逻辑,确保能拿到所有图片)
+    card_data = crud_card.get_card_with_details(db_conn, card_id)
+    if not card_data:
+        raise HTTPException(status_code=404, detail="未找到该卡片信息")
+
+    # 初始化返回结构
+    response_data = {
+        "backImageUrl": "",
+        "frontImageUrl": "",
+        "cardNo": "",
+        "centerBack": "",
+        "centerFront": "",
+        "measureLength": 0.0,
+        "measureWidth": 0.0,
+        "cornerBackNum": 0,
+        "sideBackNum": 0,
+        "surfaceBackNum": 0,
+        "cornerFrontNum": 0,
+        "sideFrontNum": 0,
+        "surfaceFrontNum": 0,
+        "popNum": 0,  # 暂时无数据来源,置0
+        "scoreThreshold": float(card_data.get("detection_score") or 0),
+        "evaluateNo": str(card_data.get("id")),
+        "recognizedInfoDTO": {
+            "cardSet": "",
+            "player": "",
+            "series": "",
+            "year": ""
+        },
+        "defectDetailList": []
+    }
+
+    # 临时列表用于收集所有缺陷,最后排序取 Top N
+    all_defects_collected = []
+
+    # 遍历图片寻找 Front Ring 和 Back Ring
+    images = card_data.get("images", [])
+
+    # 辅助字典:defect_type 到 统计字段 的映射
+    defect_map_keys = {
+        "front_ring": {
+            "corner": "cornerFrontNum",
+            "edge": "sideFrontNum",
+            "face": "surfaceFrontNum"
+        },
+        "back_ring": {
+            "corner": "cornerBackNum",
+            "edge": "sideBackNum",
+            "face": "surfaceBackNum"
+        }
+    }
+
+    for img in images:
+        img_type = img.image_type
+
+        # 只处理环光图
+        if img_type not in ["front_ring", "back_ring"]:
+            continue
+
+        # 设置主图 URL
+        if img_type == "front_ring":
+            response_data["frontImageUrl"] = img.image_path
+        elif img_type == "back_ring":
+            response_data["backImageUrl"] = img.image_path
+
+        # 获取有效 JSON
+        json_data = _get_active_json(img)
+        if not json_data or "result" not in json_data:
+            continue
+
+        result_node = json_data["result"]
+
+        # 1. 处理居中 (Center)
+        center_inf = result_node.get("center_result", {}).get("box_result", {}).get("center_inference", {})
+        if center_inf:
+            # 格式: L/R=47/53, T/B=51/49 (取整)
+            # center_inference 包含 center_left, center_right, center_top, center_bottom
+            c_str = (
+                f"L/R={int(round(center_inf.get('center_left', 0)))}/{int(round(center_inf.get('center_right', 0)))}, "
+                f"T/B={int(round(center_inf.get('center_top', 0)))}/{int(round(center_inf.get('center_bottom', 0)))}"
+            )
+            if img_type == "front_ring":
+                response_data["centerFront"] = c_str
+                # 2. 处理尺寸 (仅从正面取,或者只要有就取) - mm 转 cm,除以 10,保留2位
+                rw_mm = center_inf.get("real_width_mm", 0)
+                rh_mm = center_inf.get("real_height_mm", 0)
+                response_data["measureWidth"] = round(rw_mm / 10.0, 2)
+                response_data["measureLength"] = round(rh_mm / 10.0, 2)
+            else:
+                response_data["centerBack"] = c_str
+
+        # 2. 处理缺陷 (Defects)
+        defects = result_node.get("defect_result", {}).get("defects", [])
+
+        for defect in defects:
+            # 过滤 edit_type == 'del'
+            if defect.get("edit_type") == "del":
+                continue
+
+            d_type = defect.get("defect_type", "")  # corner, edge, face
+            d_label = defect.get("label", "")  # scratch, wear, etc.
+
+            # 统计数量
+            count_key = defect_map_keys.get(img_type, {}).get(d_type)
+            if count_key:
+                response_data[count_key] += 1
+
+            # 收集详细信息用于 Top N 列表
+            # 需要保存:缺陷对象本身,图片路径,正反面标识
+            side_str = "FRONT" if img_type == "front_ring" else "BACK"
+
+            all_defects_collected.append({
+                "defect_data": defect,
+                "image_path": img.image_path,
+                "side": side_str,
+                "area": defect.get("actual_area", 0)
+            })
+
+    # 3. 处理 defectDetailList (Top N 切图)
+    # 按实际面积从大到小排序
+    all_defects_collected.sort(key=lambda x: x["area"], reverse=True)
+
+    top_defects = all_defects_collected[:top_n_defects]
+
+    final_defect_list = []
+    for idx, item in enumerate(top_defects, start=1):
+        defect = item["defect_data"]
+        side = item["side"]
+        original_img_path = item["image_path"]
+
+        # 构造 ID
+        d_id = idx  # 1, 2, 3
+
+        # 构造文件名: {card_id}_{seq_id}.jpg
+        filename = f"{card_id}_{d_id}.jpg"
+
+        # 执行切图
+        min_rect = defect.get("min_rect")
+        defect_img_url = ""
+        location_str = ""
+
+        if min_rect and len(min_rect) == 3:
+            # 切图并保存
+            defect_img_url = _crop_defect_image(original_img_path, min_rect, filename)
+
+            # 计算 Location (中心坐标)
+            # min_rect[0] 是 [x, y]
+            cx, cy = min_rect[0]
+            location_str = f"{int(cx)},{int(cy)}"
+
+        # 构造 Type 字符串: defect_type + label (大写)
+        # 例如: defect_type="edge", label="wear" -> "EDGE WEAR"
+        d_type_raw = defect.get("defect_type", "")
+        d_label_raw = defect.get("label", "")
+        type_str = f"{d_type_raw.upper()} {d_label_raw.upper()}".strip()
+
+        final_defect_list.append({
+            "id": d_id,
+            "side": side,
+            "location": location_str,
+            "type": type_str,
+            "defectImgUrl": defect_img_url
+        })
+
+    response_data["defectDetailList"] = final_defect_list
+
+    return response_data

+ 7 - 0
app/core/config.py

@@ -14,12 +14,19 @@ class Settings:
     DATA_DIR = BASE_PATH / "Data"
     SCORE_CONFIG_PATH = BASE_PATH / "app/core/scoring_config.json"
 
+    # 缺陷图片存储位置
+    DEFECT_IMAGE_DIR = BASE_PATH / "DefectImage"
+
     # 分数计算接口url
     SCORE_UPDATE_SERVER_URL = "http://127.0.0.1:7754"
     SCORE_RECALCULATE_ENDPOINT = f"{SCORE_UPDATE_SERVER_URL}/api/card_score/score_recalculate"
     # 分数计算配置接口
     SCORE_SERVER_CONFIG_URL = f"{SCORE_UPDATE_SERVER_URL}/api/config/scoring_config"
 
+    # 评级后台接口url
+    RATING_SERVER_URL = "http://192.168.77.89:8090"
+    RATING_REPORT_SAVE_API = f"{RATING_SERVER_URL}/rating/card/ratingReport/save"
+
     # --- 数据库配置 ---
     DB_NAME = 'card_score_gray_database'
     DB_CARD_TABLE_NAME = 'cards'

+ 0 - 1
app/crud/crud_card.py

@@ -199,7 +199,6 @@ def get_card_with_details(db_conn: PooledMySQLConnection, card_id: int) -> Optio
 
 
 def get_card_list_with_images(
-        # ... (参数保持不变)
         db_conn: PooledMySQLConnection,
         card_id: Optional[int],
         card_name: Optional[str],

+ 6 - 1
app/main.py

@@ -1,4 +1,3 @@
-
 from fastapi import FastAPI
 from fastapi.staticfiles import StaticFiles
 from fastapi.middleware.cors import CORSMiddleware
@@ -11,6 +10,7 @@ from app.api import images as images_router
 from app.api import labelme as labelme_router
 from app.api import formate_xy as formate_xy_router
 from app.api import config_proxy as config_proxy_router
+from app.api import rating_report as rating_report_router
 from .core.config import settings
 from .core.logger import setup_logging, get_logger
 
@@ -19,6 +19,8 @@ logger = get_logger(__name__)
 
 settings.set_config()
 os.makedirs(settings.DATA_DIR, exist_ok=True)
+os.makedirs(settings.DEFECT_IMAGE_DIR, exist_ok=True)
+
 
 @asynccontextmanager
 async def lifespan(main_app: FastAPI):
@@ -30,9 +32,11 @@ async def lifespan(main_app: FastAPI):
     print("--- 应用关闭 ---")
     close_database_pool()
 
+
 app = FastAPI(title="卡片分数数据存储服务", lifespan=lifespan)
 
 app.mount("/Data", StaticFiles(directory="Data"), name="Data")
+app.mount("/DefectImage", StaticFiles(directory="DefectImage"), name="DefectImage")
 app.add_middleware(
     CORSMiddleware,
     allow_origins=["*"],
@@ -46,3 +50,4 @@ app.include_router(images_router.router, prefix=f"{settings.API_PREFIX}/images",
 app.include_router(labelme_router.router, prefix=f"{settings.API_PREFIX}/labelme", tags=["Labelme"])
 app.include_router(formate_xy_router.router, prefix=f"{settings.API_PREFIX}/formate_xy", tags=["Formate"])
 app.include_router(config_proxy_router.router, prefix=f"{settings.API_PREFIX}/config", tags=["ConfigProxy"])
+app.include_router(rating_report_router.router, prefix=f"{settings.API_PREFIX}/rating", tags=["Rating"])