浏览代码

图像的记录, 数据库, 增删查改

AnlaAnla 3 月之前
父节点
当前提交
92c4dc2224

二进制
Model/pokemon_back_corner_defect.pth


+ 16 - 9
Test/model_test01.py

@@ -55,13 +55,20 @@ def predict_single_image(config_params: dict,
 
 
 
 
 if __name__ == '__main__':
 if __name__ == '__main__':
-    config = settings.CARD_MODELS_CONFIG
-    predict_single_image(config['pokemon_front_inner_box'],
-                         img_path=r"C:\Users\wow38\Downloads\_250829_1656_最新模型汇总\_250829_1814_宝可梦背面内框模型\pth_and_images\images\Pokémon_back_0805_0001.jpg",
-                         output_dir=r"C:\Code\ML\Project\CheckCardBoxAndDefectServer\temp\inner")
+    big_img_path = r"C:\Code\ML\Project\CheckCardBoxAndDefectServer\temp\250805_pokemon_0001.jpg"
 
 
+    config = settings.CARD_MODELS_CONFIG
+    # predict_single_image(config['pokemon_front_inner_box'],
+    #                      img_path=big_img_path,
+    #                      output_dir=r"C:\Code\ML\Project\CheckCardBoxAndDefectServer\temp\inner")
+    #
+    # predict_single_image(config['pokemon_back_inner_box'],
+    #                      img_path=r"C:\Users\wow38\Downloads\_250829_1656_最新模型汇总\_250829_1814_宝可梦背面内框模型\pth_and_images\images\Pokémon_back_0805_0001.jpg",
+    #                      output_dir=r"C:\Code\ML\Project\CheckCardBoxAndDefectServer\temp\back_inner")
+    #
+    #
     # predict_single_image(config['outer_box'],
     # predict_single_image(config['outer_box'],
-    #                      img_path=r"C:\Code\ML\Project\CheckCardBoxAndDefectServer\temp\250805_pokemon_0001.jpg",
+    #                      big_img_path,
     #                      output_dir=r"C:\Code\ML\Project\CheckCardBoxAndDefectServer\temp\outer")
     #                      output_dir=r"C:\Code\ML\Project\CheckCardBoxAndDefectServer\temp\outer")
 
 
     # predict_single_image(config['pokemon_back_corner_defect'],
     # predict_single_image(config['pokemon_back_corner_defect'],
@@ -69,12 +76,12 @@ if __name__ == '__main__':
     #                      output_dir=r"C:\Code\ML\Project\CheckCardBoxAndDefectServer\temp\back_corner")
     #                      output_dir=r"C:\Code\ML\Project\CheckCardBoxAndDefectServer\temp\back_corner")
 
 
     # predict_single_image(config['pokemon_front_face_no_reflect_defect'],
     # predict_single_image(config['pokemon_front_face_no_reflect_defect'],
-    #                      img_path=r"C:\Code\ML\Project\CheckCardBoxAndDefectServer\temp\250805_pokemon_0001.jpg",
+    #                      img_path=r"C:\Code\ML\Project\CheckCardBoxAndDefectServer\temp\250805_pokemon_0001_grid_r3_c4.jpg",
     #                      output_dir=r"C:\Code\ML\Project\CheckCardBoxAndDefectServer\temp\face_no_reflect")
     #                      output_dir=r"C:\Code\ML\Project\CheckCardBoxAndDefectServer\temp\face_no_reflect")
 
 
-    # result = predict_single_image(config['pokemon_front_corner_no_reflect_defect'],
-    #                               img_path=r"C:\Code\ML\Project\CheckCardBoxAndDefectServer\temp\00006_250805_pokemon_0001_bottom_grid_r0_c5.jpg",
-    #                               output_dir=r"C:\Code\ML\Project\CheckCardBoxAndDefectServer\temp\corner_no_reflect")
+    result = predict_single_image(config['pokemon_front_corner_no_reflect_defect'],
+                                  img_path=r"C:\Users\wow38\Downloads\_250829_1656_最新模型汇总\_250829_1817_宝可梦非闪光卡正面边角模型\pth_and_images\images\250730_pokemon_0031_bottom_grid_r0_c5.jpg",
+                                  output_dir=r"C:\Code\ML\Project\CheckCardBoxAndDefectServer\temp\corner_no_reflect")
 
 
     # result = predict_single_image(config['pokemon_front_face_no_reflect_defect'],
     # result = predict_single_image(config['pokemon_front_face_no_reflect_defect'],
     #                      img_path=r"C:\Code\ML\Project\CheckCardBoxAndDefectServer\temp\250805_pokemon_0001_grid_r3_c4.jpg",
     #                      img_path=r"C:\Code\ML\Project\CheckCardBoxAndDefectServer\temp\250805_pokemon_0001_grid_r3_c4.jpg",

+ 33 - 4
Test/test01.py

@@ -1,6 +1,35 @@
-import json
+from multiprocessing import Process, Queue, Lock
+import time
 
 
-text = '''{"asda": 666, "abb": 656, "gsd": 123}'''
-data = json.loads(text)
+lock = Lock()
 
 
-print(data)
+
+def f(q, a):
+    lock.acquire()
+    if a == 3:
+        time.sleep(2)
+
+        q.put(666)
+        print('input')
+    if a == 4:
+        data = q.get()
+        print(data)
+    print(a)
+
+    lock.release()
+
+
+if __name__ == '__main__':
+    q = Queue()
+    p_list = []
+    for i in range(8):
+        p = Process(target=f, args=(q, i,))
+        p_list.append(p)
+
+    for p in p_list:
+        p.start()
+
+    print('-=------')
+
+    for p in p_list:
+        p.join()

+ 0 - 0
Test/test02.py


+ 68 - 0
Test/数据库测试.py

@@ -0,0 +1,68 @@
+import mysql.connector
+from mysql.connector import errorcode
+
+# 你的数据库连接配置
+config = {
+    'user': 'root',
+    'password': '123456',
+    'host': '127.0.0.1',
+    'database': 'card_score_database'
+}
+TABLES = {}
+TABLES['employees'] = (
+    "CREATE TABLE `employees` ("
+    "  `id` int(11) NOT NULL AUTO_INCREMENT,"
+    "  `name` varchar(50) NOT NULL,"
+    "  `position` varchar(50) NOT NULL,"
+    "  `hire_date` date NOT NULL,"
+    "  PRIMARY KEY (`id`)"
+    ") ENGINE=InnoDB")
+
+connection = None
+cursor = None
+database_name = "card_score_database"
+
+
+def connect_mysql():
+    # 尝试连接
+    connection = mysql.connector.connect(**config)
+    print("成功连接到 MySQL 服务器!")
+
+    # 获取一个游标对象
+    cursor = connection.cursor()
+
+    # 打印服务器版本信息
+    cursor.execute("SELECT VERSION()")
+    version = cursor.fetchone()
+    print(f"数据库版本: {version[0]}")
+    return connection, cursor
+
+
+def creat_database(cursor, database_name):
+    cursor.execute(f"CREATE DATABASE IF NOT EXISTS {database_name} DEFAULT CHARACTER SET 'utf8mb4'")
+
+
+if __name__ == '__main__':
+    connection, cursor = connect_mysql()
+
+    # creat_database(cursor, database_name)
+    # 创建 employees 表
+    # cursor.execute(TABLES['employees'])
+
+    # add_employee_sql = ("INSERT INTO employees "
+    #                     "(name, position, hire_date) "
+    #                     "VALUES (%s, %s, %s)")
+    # employee_data = ('张三', '软件工程师', '2023-01-15')
+    # cursor.execute(add_employee_sql, employee_data)
+    # employee_id = cursor.lastrowid  # 获取刚插入行的ID
+    # print(f"成功插入一条记录, ID: {employee_id}")
+    #
+    # connection.commit()
+
+    # 1. 查询所有员工
+    query_all = "SELECT id, name, position, hire_date FROM employees"
+    cursor.execute(query_all)
+
+    print("\n--- 所有员工信息 ---")
+    for row in cursor.fetchall():
+        print(f"ID: {row['id']}, 姓名: {row['name']}, 职位: {row['position']}, 入职日期: {row['hire_date']}")

+ 244 - 0
app/api/image_data.py

@@ -0,0 +1,244 @@
+# app/api/image_data.py
+
+import os
+import uuid
+import json  # <-- 确保导入了json库
+from typing import Optional, Dict, Any, List
+from fastapi import APIRouter, File, UploadFile, Depends, HTTPException, Form, Query
+from fastapi.responses import JSONResponse
+from fastapi.concurrency import run_in_threadpool
+from pydantic import BaseModel, field_validator
+import mysql.connector
+
+from app.core.config import settings
+from app.core.logger import get_logger
+from app.core.database_loader import get_db_connection
+from app.services.score_service import ScoreService
+from app.api.score_inference import ScoreType
+
+logger = get_logger(__name__)
+router = APIRouter()
+
+
+# --- Pydantic 模型定义 ---
+class ImageDataUpdate(BaseModel):
+    img_result_json: Dict[str, Any]
+
+
+class ImageDataResponse(BaseModel):
+    img_id: int
+    img_name: Optional[str] = None
+    img_path: str
+    img_result_json: Dict[str, Any]
+
+    # 添加一个验证器来自动处理从数据库来的字符串
+    @field_validator('img_result_json', mode='before')
+    @classmethod
+    def parse_json_string(cls, value):
+        if isinstance(value, str):
+            try:
+                return json.loads(value)
+            except json.JSONDecodeError:
+                raise ValueError("Invalid JSON string provided")
+        return value
+
+
+# --- API 端点实现 ---
+
+@router.post("/", response_model=ImageDataResponse, summary="创建新的图片记录")
+async def create_image_data(
+        score_type: ScoreType = Form(...),
+        is_reflect_card: bool = Form(False),
+        img_name: Optional[str] = Form(None),
+        file: UploadFile = File(...),
+        db_conn: mysql.connector.connection.MySQLConnection = Depends(get_db_connection)
+):
+    # 1. 保存图片到本地
+    file_extension = os.path.splitext(file.filename)[1]
+    unique_filename = f"{uuid.uuid4()}{file_extension}"
+    img_path = settings.DATA_DIR / unique_filename
+
+    try:
+        image_bytes = await file.read()
+        with open(img_path, "wb") as f:
+            f.write(image_bytes)
+    except Exception as e:
+        logger.error(f"保存图片失败: {e}")
+        raise HTTPException(status_code=500, detail="无法保存图片文件")
+
+    # 2. 调用 ScoreService 生成 JSON 数据
+    try:
+        service = ScoreService()
+        json_result = await run_in_threadpool(
+            service.score_inference,
+            score_type=score_type.value,
+            is_reflect_card=is_reflect_card,
+            image_bytes=image_bytes
+        )
+    except Exception as e:
+        # 如果推理失败,删除已保存的图片
+        os.remove(img_path)
+        logger.error(f"分数推理失败: {e}")
+        raise HTTPException(status_code=500, detail=f"分数推理时发生错误: {e}")
+
+    # 3. 存入数据库
+    cursor = None
+    try:
+        cursor = db_conn.cursor(dictionary=True)
+        insert_query = (
+            f"INSERT INTO {settings.DB_TABLE_NAME} (img_name, img_path, img_result_json) "
+            "VALUES (%s, %s, %s)"
+        )
+        json_string = json.dumps(json_result, ensure_ascii=False)
+        cursor.execute(insert_query, (img_name, str(img_path), json_string))
+        new_id = cursor.lastrowid
+        db_conn.commit()
+
+        logger.info(f"成功创建记录, ID: {new_id}")
+        return {
+            "img_id": new_id,
+            "img_name": img_name,
+            "img_path": str(img_path),
+            "img_result_json": json_result
+        }
+
+    except mysql.connector.Error as err:
+        db_conn.rollback()
+        os.remove(img_path)
+        logger.error(f"数据库插入失败: {err}")
+        raise HTTPException(status_code=500, detail="数据库操作失败")
+    finally:
+        if cursor:
+            cursor.close()
+
+
+@router.get("/{img_id}", response_model=ImageDataResponse, summary="根据ID查询记录")
+def get_image_data_by_id(
+        img_id: int,
+        db_conn: mysql.connector.connection.MySQLConnection = Depends(get_db_connection)
+):
+    cursor = None
+    try:
+        cursor = db_conn.cursor(dictionary=True)
+        query = f"SELECT * FROM {settings.DB_TABLE_NAME} WHERE img_id = %s"
+        cursor.execute(query, (img_id,))
+        record = cursor.fetchone()
+
+        if record is None:
+            raise HTTPException(status_code=404, detail="记录未找到")
+
+        # Pydantic 验证器会自动处理转换,这里不再需要手动转换
+        return record
+
+    except mysql.connector.Error as err:
+        logger.error(f"数据库查询失败: {err}")
+        raise HTTPException(status_code=500, detail="数据库查询失败")
+    finally:
+        if cursor:
+            cursor.close()
+
+
+@router.get("/", response_model=List[ImageDataResponse], summary="根据名称查询记录")
+def get_image_data_by_name(
+        img_name: str,
+        db_conn: mysql.connector.connection.MySQLConnection = Depends(get_db_connection)
+):
+    cursor = None
+    try:
+        cursor = db_conn.cursor(dictionary=True)
+        query = f"SELECT * FROM {settings.DB_TABLE_NAME} WHERE img_name = %s"
+        cursor.execute(query, (img_name,))
+        records = cursor.fetchall()
+
+        # Pydantic 验证器会自动处理转换,这里不再需要手动转换
+        return records
+
+    except mysql.connector.Error as err:
+        logger.error(f"数据库查询失败: {err}")
+        raise HTTPException(status_code=500, detail="数据库查询失败")
+    finally:
+        if cursor:
+            cursor.close()
+
+
+@router.put("/{img_id}", response_model=ImageDataResponse, summary="更新记录的JSON数据")
+def update_image_data_json(
+        img_id: int,
+        data: ImageDataUpdate,
+        db_conn: mysql.connector.connection.MySQLConnection = Depends(get_db_connection)
+):
+    # ... (这个函数逻辑保持不变)
+    cursor = None
+    try:
+        cursor = db_conn.cursor(dictionary=True)
+        check_query = f"SELECT img_path FROM {settings.DB_TABLE_NAME} WHERE img_id = %s"
+        cursor.execute(check_query, (img_id,))
+        record = cursor.fetchone()
+        if not record:
+            raise HTTPException(status_code=404, detail="记录未找到")
+
+        update_query = (
+            f"UPDATE {settings.DB_TABLE_NAME} SET img_result_json = %s WHERE img_id = %s"
+        )
+        json_string = json.dumps(data.img_result_json, ensure_ascii=False)
+        cursor.execute(update_query, (json_string, img_id))
+        db_conn.commit()
+
+        if cursor.rowcount == 0:
+            raise HTTPException(status_code=404, detail="记录未找到或数据未改变")
+
+        logger.info(f"成功更新记录, ID: {img_id}")
+        return get_image_data_by_id(img_id, db_conn)
+
+    except mysql.connector.Error as err:
+        db_conn.rollback()
+        logger.error(f"数据库更新失败: {err}")
+        raise HTTPException(status_code=500, detail="数据库更新失败")
+    finally:
+        if cursor:
+            cursor.close()
+
+
+@router.delete("/{img_id}", summary="删除记录和对应的图片文件")
+def delete_image_data(
+        img_id: int,
+        db_conn: mysql.connector.connection.MySQLConnection = Depends(get_db_connection)
+):
+    # ... (这个函数无需修改)
+    cursor = None
+    try:
+        cursor = db_conn.cursor(dictionary=True)
+
+        query = f"SELECT img_path FROM {settings.DB_TABLE_NAME} WHERE img_id = %s"
+        cursor.execute(query, (img_id,))
+        record = cursor.fetchone()
+
+        if not record:
+            raise HTTPException(status_code=404, detail="记录未找到")
+
+        img_path = record['img_path']
+
+        delete_query = f"DELETE FROM {settings.DB_TABLE_NAME} WHERE img_id = %s"
+        cursor.execute(delete_query, (img_id,))
+        db_conn.commit()
+
+        if cursor.rowcount == 0:
+            raise HTTPException(status_code=404, detail="记录删除失败,可能已被删除")
+
+        try:
+            if os.path.exists(img_path):
+                os.remove(img_path)
+                logger.info(f"成功删除图片文件: {img_path}")
+        except Exception as e:
+            logger.error(f"删除图片文件失败: {img_path}, 错误: {e}")
+
+        logger.info(f"成功删除记录, ID: {img_id}")
+        return JSONResponse(content={"message": f"记录 {img_id} 已成功删除"}, status_code=200)
+
+    except mysql.connector.Error as err:
+        db_conn.rollback()
+        logger.error(f"数据库删除失败: {err}")
+        raise HTTPException(status_code=500, detail="数据库删除失败")
+    finally:
+        if cursor:
+            cursor.close()

+ 32 - 5
app/core/config.py

@@ -15,18 +15,43 @@ class CardModelConfig:
 class Settings:
 class Settings:
     API_Inference_prefix: str = "/api/card_inference"
     API_Inference_prefix: str = "/api/card_inference"
     API_Score_prefix: str = "/api/card_score"
     API_Score_prefix: str = "/api/card_score"
+    API_ImageData_prefix: str = "/api/image_data"
 
 
     BASE_PATH = Path(__file__).parent.parent.parent.absolute()
     BASE_PATH = Path(__file__).parent.parent.parent.absolute()
 
 
     TEMP_WORK_DIR = BASE_PATH / "_temp_work"
     TEMP_WORK_DIR = BASE_PATH / "_temp_work"
+    DATA_DIR = BASE_PATH / "Data"
     SCORE_CONFIG_PATH = BASE_PATH / "app/core/scoring_config.json"
     SCORE_CONFIG_PATH = BASE_PATH / "app/core/scoring_config.json"
 
 
     # 图片像素与真实图片缩放比例
     # 图片像素与真实图片缩放比例
     PIXEL_RESOLUTION = 24.54
     PIXEL_RESOLUTION = 24.54
     CORNER_SIZE_MM = 3.0
     CORNER_SIZE_MM = 3.0
 
 
+    # --- 数据库配置 ---
+    DB_NAME = 'card_score_database'
+    DB_TABLE_NAME = 'image_records'
+    DATABASE_CONFIG: Dict[str, str] = {
+        'user': 'root',
+        'password': '123456',
+        'host': '127.0.0.1',
+    }
+    # 连接到指定数据库的配置
+    DATABASE_CONFIG_WITH_DB: Dict[str, str] = {
+        **DATABASE_CONFIG,
+        'database': DB_NAME
+    }
+
     # 使用一个字典来管理所有卡片检测模型
     # 使用一个字典来管理所有卡片检测模型
     # key (如 'outer_box') 将成为 API 路径中的 {inference_type}
     # key (如 'outer_box') 将成为 API 路径中的 {inference_type}
+    '''
+    face: "1": "wear", "2": "scratch", "3": "stain",
+        "4": "scuff", "5": "impact", "6": "damaged",
+        "7": "wear_and_impact"
+        "8": "stain", "9": "pit"
+    
+    corner: "1": "wear", "2": "wear_and_impact", "3": "damaged",
+                           "4": "impact", "5": "wear_and_stain"
+    '''
     CARD_MODELS_CONFIG: Dict[str, CardModelConfig] = {
     CARD_MODELS_CONFIG: Dict[str, CardModelConfig] = {
         "outer_box": {
         "outer_box": {
             "pth_path": "Model/outer_box.pth",
             "pth_path": "Model/outer_box.pth",
@@ -51,10 +76,8 @@ class Settings:
         },
         },
         "pokemon_back_corner_defect": {
         "pokemon_back_corner_defect": {
             "pth_path": "Model/pokemon_back_corner_defect.pth",
             "pth_path": "Model/pokemon_back_corner_defect.pth",
-            "class_dict": {
-                1: 'wear', 2: 'wear_and_impact', 3: 'impact',
-                4: 'damaged', 5: 'wear_and_stain',
-            },
+            "class_dict": {"1": "wear", "2": "wear_and_impact", "3": "damaged",
+                           "4": "impact", "5": "wear_and_stain"},
             "img_size": {'width': 512, 'height': 512},
             "img_size": {'width': 512, 'height': 512},
             "confidence": 0.5,
             "confidence": 0.5,
             "input_channels": 3,
             "input_channels": 3,
@@ -121,6 +144,9 @@ class Settings:
         "pokemon_front_corner_reflect_defect": {
         "pokemon_front_corner_reflect_defect": {
             "model_type": "pokemon_front_corner_reflect_defect"
             "model_type": "pokemon_front_corner_reflect_defect"
         },
         },
+        "pokemon_front_face_reflect_defect": {
+            "model_type": "pokemon_front_face_reflect_defect"
+        },
         "pokemon_front_corner_no_reflect_defect": {
         "pokemon_front_corner_no_reflect_defect": {
             "model_type": "pokemon_front_corner_no_reflect_defect",
             "model_type": "pokemon_front_corner_no_reflect_defect",
         },
         },
@@ -134,7 +160,8 @@ class Settings:
 
 
 
 
 settings = Settings()
 settings = Settings()
-print(settings.BASE_PATH)
+print(f"项目根目录: {settings.BASE_PATH}")
+print(f"数据存储目录: {settings.DATA_DIR}")
 
 
 # DefectType = Enum("InferenceType", {name: name for name in settings.DEFECT_TYPE})
 # DefectType = Enum("InferenceType", {name: name for name in settings.DEFECT_TYPE})
 # print()
 # print()

+ 100 - 0
app/core/database_loader.py

@@ -0,0 +1,100 @@
+import mysql.connector
+from mysql.connector import errorcode
+from .config import settings
+from app.core.logger import get_logger
+
+logger = get_logger(__name__)
+
+# 全局的连接池
+db_connection_pool = None
+
+
+def init_database():
+    """
+    初始化数据库:如果数据库或表不存在,则创建它们。
+    """
+    logger.info("--- 开始初始化数据库 ---")
+
+    # 1. 尝试连接MySQL服务器(不指定数据库)
+    try:
+        cnx = mysql.connector.connect(**settings.DATABASE_CONFIG)
+        cursor = cnx.cursor()
+
+        # 2. 创建数据库(如果不存在)
+        cursor.execute(f"CREATE DATABASE IF NOT EXISTS {settings.DB_NAME} DEFAULT CHARACTER SET 'utf8mb4'")
+        logger.info(f"数据库 '{settings.DB_NAME}' 已准备就绪。")
+
+        # 3. 切换到目标数据库
+        cnx.database = settings.DB_NAME
+
+        # 4. 创建表(如果不存在)
+        table_description = (
+            f"CREATE TABLE IF NOT EXISTS `{settings.DB_TABLE_NAME}` ("
+            "  `img_id` INT AUTO_INCREMENT PRIMARY KEY,"
+            "  `img_name` VARCHAR(255) NULL,"
+            "  `img_path` VARCHAR(512) NOT NULL,"
+            "  `img_result_json` JSON NOT NULL,"
+            "  `created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP"
+            ") ENGINE=InnoDB"
+        )
+        cursor.execute(table_description)
+        logger.info(f"数据表 '{settings.DB_TABLE_NAME}' 已准备就绪。")
+
+    except mysql.connector.Error as err:
+        logger.error(f"数据库初始化失败: {err}")
+        exit(1)  # 初始化失败直接退出程序
+    finally:
+        if 'cursor' in locals() and cursor:
+            cursor.close()
+        if 'cnx' in locals() and cnx.is_connected():
+            cnx.close()
+
+    logger.info("--- 数据库初始化完成 ---")
+
+
+def load_database_pool():
+    """
+    在应用启动时创建数据库连接池。
+    """
+    global db_connection_pool
+    if db_connection_pool is None:
+        logger.info("--- 创建数据库连接池 ---")
+        try:
+            db_connection_pool = mysql.connector.pooling.MySQLConnectionPool(
+                pool_name="mypool",
+                pool_size=5,  # 池中保持的连接数
+                **settings.DATABASE_CONFIG_WITH_DB
+            )
+            logger.info("--- 数据库连接池创建成功 ---")
+        except mysql.connector.Error as err:
+            logger.error(f"创建数据库连接池失败: {err}")
+            exit(1)
+
+
+def close_database_pool():
+    """
+    在应用关闭时,不需要手动关闭连接池,连接器会自动处理。
+    这个函数留作备用。
+    """
+    logger.info("--- 数据库连接池将自动关闭 ---")
+
+
+# --- FastAPI 依赖注入 ---
+def get_db_connection():
+    """
+    一个FastAPI依赖项,用于从池中获取数据库连接。
+    它确保连接在使用后返回到池中。
+    """
+    if db_connection_pool is None:
+        raise RuntimeError("数据库连接池未初始化")
+
+    db_conn = None
+    try:
+        db_conn = db_connection_pool.get_connection()
+        yield db_conn
+    except mysql.connector.Error as err:
+        logger.error(f"获取数据库连接失败: {err}")
+        # 这里可以根据需要抛出HTTPException
+    finally:
+        if db_conn and db_conn.is_connected():
+            db_conn.close()

+ 8 - 3
app/core/logger.py

@@ -1,10 +1,15 @@
 import logging
 import logging
 
 
-
 logging.basicConfig(
 logging.basicConfig(
     level=logging.INFO,  # 生产环境通常设置为 INFO 或 WARNING
     level=logging.INFO,  # 生产环境通常设置为 INFO 或 WARNING
     format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
     format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
+    filename='app.log',  # 指定日志输出到文件 app.log
+    filemode='w'
 )
 )
-
-# 获取一个日志器实例,通常以模块名命名
 logger = logging.getLogger(__name__)
 logger = logging.getLogger(__name__)
+
+
+def get_logger(name):
+    # 获取一个日志器实例,通常以模块名命名
+    logger = logging.getLogger(name)
+    return logger

+ 26 - 3
app/core/scoring_config.json

@@ -122,6 +122,28 @@
   },
   },
   "face": {
   "face": {
     "rules": {
     "rules": {
+      "wear_area": [
+        [
+          0.05,
+          -0.1
+        ],
+        [
+          0.1,
+          -0.5
+        ],
+        [
+          0.25,
+          -1.5
+        ],
+        [
+          0.5,
+          -3.0
+        ],
+        [
+          "inf",
+          -5.0
+        ]
+      ],
       "pit_area": [
       "pit_area": [
         [
         [
           0.05,
           0.05,
@@ -198,9 +220,10 @@
       ]
       ]
     },
     },
     "coefficients": {
     "coefficients": {
-      "scratch_length": 1.0,
-      "dent_area": 1.0,
-      "stain_area": 1.0
+      "wear_area": 0.25,
+      "scratch_length": 0.25,
+      "dent_area": 0.25,
+      "stain_area": 0.25
     },
     },
     "final_weights": {
     "final_weights": {
       "front": 0.75,
       "front": 0.75,

+ 13 - 0
app/main.py

@@ -1,8 +1,11 @@
 from fastapi import FastAPI
 from fastapi import FastAPI
 from contextlib import asynccontextmanager
 from contextlib import asynccontextmanager
+
+from .core.database_loader import init_database, load_database_pool, close_database_pool
 from .core.model_loader import load_models, unload_models
 from .core.model_loader import load_models, unload_models
 from app.api.card_inference import router as card_inference_router
 from app.api.card_inference import router as card_inference_router
 from app.api.score_inference import router as score_inference_router
 from app.api.score_inference import router as score_inference_router
+from app.api.image_data import router as image_data_router
 import os
 import os
 
 
 from .core.config import settings
 from .core.config import settings
@@ -11,11 +14,20 @@ from .core.config import settings
 @asynccontextmanager
 @asynccontextmanager
 async def lifespan(main_app: FastAPI):
 async def lifespan(main_app: FastAPI):
     print("--- 应用启动 ---")
     print("--- 应用启动 ---")
+    # --- 文件和目录准备 ---
     os.makedirs(settings.TEMP_WORK_DIR, exist_ok=True)
     os.makedirs(settings.TEMP_WORK_DIR, exist_ok=True)
+    os.makedirs(settings.DATA_DIR, exist_ok=True)
+
+    # --- 数据库初始化 ---
+    init_database()
+    load_database_pool()
+
+    # --- 模型加载 ---
     load_models()
     load_models()
     yield
     yield
 
 
     print("--- 应用关闭 ---")
     print("--- 应用关闭 ---")
+    close_database_pool()
     unload_models()
     unload_models()
 
 
 
 
@@ -23,3 +35,4 @@ app = FastAPI(title="卡片框和缺陷检测服务", lifespan=lifespan)
 
 
 app.include_router(card_inference_router, prefix=settings.API_Inference_prefix)
 app.include_router(card_inference_router, prefix=settings.API_Inference_prefix)
 app.include_router(score_inference_router, prefix=settings.API_Score_prefix)
 app.include_router(score_inference_router, prefix=settings.API_Score_prefix)
+app.include_router(image_data_router, prefix=settings.API_ImageData_prefix, tags=["Image Data"])

+ 5 - 3
app/services/defect_service.py

@@ -7,12 +7,13 @@ from app.utils.defect_inference.AnalyzeCenter import analyze_centering_rotated
 from app.utils.defect_inference.ClassifyEdgeCorner import ClassifyEdgeCorner
 from app.utils.defect_inference.ClassifyEdgeCorner import ClassifyEdgeCorner
 from app.utils.json_data_formate import formate_center_data, formate_face_data
 from app.utils.json_data_formate import formate_center_data, formate_face_data
 from app.core.config import settings
 from app.core.config import settings
-from app.core.logger import logger
+from app.core.logger import get_logger
 import json
 import json
 
 
+logger = get_logger(__name__)
 
 
 class DefectInferenceService:
 class DefectInferenceService:
-    def defect_inference(self, inference_type: str, image_bytes: bytes,
+    def defect_inference(self, inference_type: str , image_bytes: bytes,
                          is_draw_image=False) -> dict:
                          is_draw_image=False) -> dict:
         """
         """
         执行卡片识别推理。
         执行卡片识别推理。
@@ -110,7 +111,8 @@ class DefectInferenceService:
                 temp_img_path = settings.TEMP_WORK_DIR / f'{inference_type}-corner_result.jpg'
                 temp_img_path = settings.TEMP_WORK_DIR / f'{inference_type}-corner_result.jpg'
                 cv2.imwrite(temp_img_path, drawn_image)
                 cv2.imwrite(temp_img_path, drawn_image)
             else:
             else:
-                area_json = processor.analyze_from_json(json_data)
+                area_json: dict = processor.analyze_from_json(json_data)
+            logger.info("边角缺陷面积计算结束")
 
 
             # 推理外框
             # 推理外框
             predictor_outer = get_predictor("outer_box")
             predictor_outer = get_predictor("outer_box")

+ 7 - 4
app/services/score_service.py

@@ -1,10 +1,12 @@
 from app.core.config import settings
 from app.core.config import settings
-from app.core.logger import logger
+from app.core.logger import get_logger
 from app.services.defect_service import DefectInferenceService
 from app.services.defect_service import DefectInferenceService
 from app.utils.score_inference.CardScorer import CardScorer
 from app.utils.score_inference.CardScorer import CardScorer
 from app.utils.json_data_formate import formate_one_card_result
 from app.utils.json_data_formate import formate_one_card_result
 import json
 import json
 
 
+logger = get_logger(__name__)
+
 
 
 class ScoreService:
 class ScoreService:
     def __init__(self):
     def __init__(self):
@@ -25,11 +27,11 @@ class ScoreService:
             if score_type == 'front_corner_edge':
             if score_type == 'front_corner_edge':
                 defect_data = defect_service.defect_inference('pokemon_front_corner_reflect_defect', image_bytes)
                 defect_data = defect_service.defect_inference('pokemon_front_corner_reflect_defect', image_bytes)
             elif score_type == 'front_face':
             elif score_type == 'front_face':
-                return {"result": "目前缺少该模型"}
+                defect_data = defect_service.defect_inference('pokemon_front_face_reflect_defect', image_bytes)
             elif score_type == 'back_corner_edge':
             elif score_type == 'back_corner_edge':
                 defect_data = defect_service.defect_inference('pokemon_back_corner_defect', image_bytes)
                 defect_data = defect_service.defect_inference('pokemon_back_corner_defect', image_bytes)
             elif score_type == 'back_face':
             elif score_type == 'back_face':
-                return {"result": "目前缺少该模型"}
+                defect_data = defect_service.defect_inference('pokemon_back_face_defect', image_bytes)
             else:
             else:
                 return {}
                 return {}
         else:
         else:
@@ -40,7 +42,8 @@ class ScoreService:
             elif score_type == 'back_corner_edge':
             elif score_type == 'back_corner_edge':
                 defect_data = defect_service.defect_inference('pokemon_back_corner_defect', image_bytes)
                 defect_data = defect_service.defect_inference('pokemon_back_corner_defect', image_bytes)
             elif score_type == 'back_face':
             elif score_type == 'back_face':
-                return {"result": "目前缺少该模型"}
+                defect_data = defect_service.defect_inference('pokemon_back_face_defect', image_bytes)
+
             else:
             else:
                 return {}
                 return {}
 
 

+ 12 - 6
app/utils/defect_inference/ClassifyEdgeCorner.py

@@ -2,6 +2,8 @@ import json
 import cv2
 import cv2
 import numpy as np
 import numpy as np
 import math
 import math
+import logging
+logger = logging.getLogger('ClassifyEdgeCorner')
 
 
 
 
 class ClassifyEdgeCorner:
 class ClassifyEdgeCorner:
@@ -75,31 +77,35 @@ class ClassifyEdgeCorner:
         主函数,对缺陷数据进行分类并添加 "defect_type" 标签。
         主函数,对缺陷数据进行分类并添加 "defect_type" 标签。
         """
         """
         if not defect_data or 'defects' not in defect_data:
         if not defect_data or 'defects' not in defect_data:
-            print("警告: 缺陷数据为空或格式不正确,跳过处理。")
+            logger.warn("警告: 缺陷数据为空或格式不正确,跳过处理。")
+            return defect_data
+
+        if not defect_data['defects']:
+            logger.warn("没有缺陷数据")
             return defect_data
             return defect_data
 
 
         # 1. 单位转换
         # 1. 单位转换
         pixel_to_mm = self.PIXEL_RESOLUTION_UM / 1000.0
         pixel_to_mm = self.PIXEL_RESOLUTION_UM / 1000.0
         corner_size_px = self.CORNER_SIZE_MM / pixel_to_mm
         corner_size_px = self.CORNER_SIZE_MM / pixel_to_mm
-        print(f"3mm角区尺寸已转换为 {corner_size_px:.2f} 像素。")
+        logger.info(f"3mm角区尺寸已转换为 {corner_size_px:.2f} 像素。")
 
 
         # 2. 获取外框几何信息
         # 2. 获取外框几何信息
         try:
         try:
             outer_box_corners = self.get_outer_box_corners(outer_box_data)
             outer_box_corners = self.get_outer_box_corners(outer_box_data)
         except ValueError as e:
         except ValueError as e:
-            print(f"错误: {e}")
+            logger.info(f"错误: {e}")
             return None
             return None
 
 
         # 3. 定义四个角区
         # 3. 定义四个角区
         corner_regions = self.create_corner_regions(outer_box_corners, corner_size_px)
         corner_regions = self.create_corner_regions(outer_box_corners, corner_size_px)
-        print(f"已成功定义 {len(corner_regions)} 个角区。")
+        logger.info(f"已成功定义 {len(corner_regions)} 个角区。")
 
 
         # 4. 遍历并分类所有缺陷
         # 4. 遍历并分类所有缺陷
         processed_count = 0
         processed_count = 0
         for defect in defect_data['defects']:
         for defect in defect_data['defects']:
             # 使用JSON中预先计算好的min_rect中心点,更高效
             # 使用JSON中预先计算好的min_rect中心点,更高效
             if 'min_rect' not in defect or not defect['min_rect']:
             if 'min_rect' not in defect or not defect['min_rect']:
-                print(f"警告: 缺陷 '{defect.get('label')}' 缺少 'min_rect' 信息,跳过。")
+                logger.warn(f"警告: 缺陷 '{defect.get('label')}' 缺少 'min_rect' 信息,跳过。")
                 continue
                 continue
 
 
             center_point = tuple(defect['min_rect'][0])
             center_point = tuple(defect['min_rect'][0])
@@ -120,5 +126,5 @@ class ClassifyEdgeCorner:
 
 
             processed_count += 1
             processed_count += 1
 
 
-        print(f"处理完成!共为 {processed_count} 个缺陷添加了 'defect_type' 标签。")
+        logger.info(f"处理完成!共为 {processed_count} 个缺陷添加了 'defect_type' 标签。")
         return defect_data
         return defect_data

+ 26 - 16
app/utils/defect_inference/arean_anylize_draw.py

@@ -6,10 +6,12 @@ import random
 from dataclasses import dataclass, field
 from dataclasses import dataclass, field
 from typing import Dict, List, Optional, Any, Tuple, Union
 from typing import Dict, List, Optional, Any, Tuple, Union
 from collections import defaultdict
 from collections import defaultdict
+import logging
 
 
+logger = logging.getLogger('ClassifyEdgeCorner')
 
 
 def fry_algo_print(level_str: str, info_str: str):
 def fry_algo_print(level_str: str, info_str: str):
-    print(f"[{level_str}] : {info_str}")
+    logger.info(f"[{level_str}] : {info_str}")
 
 
 
 
 def fry_cv2_imread(filename, flags=cv2.IMREAD_COLOR):
 def fry_cv2_imread(filename, flags=cv2.IMREAD_COLOR):
@@ -213,11 +215,16 @@ class DefectProcessor:
         Returns:
         Returns:
             AnalysisResult: 包含所有缺陷信息和统计结果的数据对象。
             AnalysisResult: 包含所有缺陷信息和统计结果的数据对象。
         """
         """
-        if not json_data or 'shapes' not in json_data:
-            return AnalysisResult()
 
 
         result = AnalysisResult()
         result = AnalysisResult()
 
 
+        if not json_data or 'shapes' not in json_data:
+            if is_return_obj:
+                return result
+            result_json = to_json_serializable(result.to_dict())
+            result_json = json.loads(result_json)
+            return result_json
+
         for shape in json_data['shapes']:
         for shape in json_data['shapes']:
             label = shape.get('label', 'unlabeled')
             label = shape.get('label', 'unlabeled')
             points = shape.get('points')
             points = shape.get('points')
@@ -273,7 +280,10 @@ class DefectProcessor:
 
 
         # 2. 如果没有缺陷,直接返回原图和分析结果
         # 2. 如果没有缺陷,直接返回原图和分析结果
         if not analysis_result.defects:
         if not analysis_result.defects:
-            return image, analysis_result
+            result_json = to_json_serializable(analysis_result.to_dict())
+            result_json = json.loads(result_json)
+
+            return image, result_json
 
 
         # 3. 使用DefectVisualizer进行绘图
         # 3. 使用DefectVisualizer进行绘图
         visualizer = DefectVisualizer(drawing_params)
         visualizer = DefectVisualizer(drawing_params)
@@ -303,12 +313,12 @@ def run_json_only_analysis_example(json_path: str, output_json_path: str):
 
 
     # 3. 打印统计结果
     # 3. 打印统计结果
     fry_algo_print("信息", f"分析完成: {os.path.basename(json_path)}")
     fry_algo_print("信息", f"分析完成: {os.path.basename(json_path)}")
-    stats = analysis_result.to_dict()["statistics"]
+    stats = analysis_result["statistics"]
     print(json.dumps(stats, indent=2, ensure_ascii=False))
     print(json.dumps(stats, indent=2, ensure_ascii=False))
 
 
     # 4. 将完整结果保存为新的JSON文件
     # 4. 将完整结果保存为新的JSON文件
     with open(output_json_path, 'w', encoding='utf-8') as f:
     with open(output_json_path, 'w', encoding='utf-8') as f:
-        json.dump(analysis_result.to_dict(), f, ensure_ascii=False, indent=2, default=to_json_serializable)
+        json.dump(analysis_result, f, ensure_ascii=False, indent=2, default=to_json_serializable)
     fry_algo_print("成功", f"详细分析结果已保存到: {output_json_path}")
     fry_algo_print("成功", f"详细分析结果已保存到: {output_json_path}")
 
 
 
 
@@ -367,10 +377,10 @@ def run_image_and_json_analysis_example(image_path: str, json_path: str, output_
 
 
 
 
 if __name__ == "__main__":
 if __name__ == "__main__":
-    image_file_path = r"C:\Code\ML\Project\卡片缺陷检测项目组\计算边角缺陷大小\测试数据\250805_pokemon_0001.jpg"
-    json_file_path = r"C:\Code\ML\Project\卡片缺陷检测项目组\计算边角缺陷大小\测试数据\250805_pokemon_0001.json"
+    image_file_path = r"C:\Code\ML\Project\CheckCardBoxAndDefectServer\temp\250805_pokemon_0001.jpg"
+    json_file_path = r"C:\Code\ML\Project\CheckCardBoxAndDefectServer\_temp_work\pokemon_front_corner_no_reflect_defect-merge.json"
 
 
-    output_dir = r"C:\Code\ML\Project\卡片缺陷检测项目组\计算边角缺陷大小\测试数据_my"
+    output_dir = r"C:\Code\ML\Project\CheckCardBoxAndDefectServer\temp\测试数据_my"
     os.makedirs(output_dir, exist_ok=True)
     os.makedirs(output_dir, exist_ok=True)
 
 
     # 1. 仅JSON分析
     # 1. 仅JSON分析
@@ -382,10 +392,10 @@ if __name__ == "__main__":
     # print("\n" + "=" * 50 + "\n")
     # print("\n" + "=" * 50 + "\n")
 
 
     # 2. 图像和JSON结合分析
     # 2. 图像和JSON结合分析
-    # run_image_and_json_analysis_example(
-    #     image_path=image_file_path,
-    #     json_path=json_file_path,
-    #     output_dir=output_dir
-    # )
-    #
-    # fry_algo_print("重要", "所有示例运行完毕!")
+    run_image_and_json_analysis_example(
+        image_path=image_file_path,
+        json_path=json_file_path,
+        output_dir=output_dir
+    )
+
+    fry_algo_print("重要", "所有示例运行完毕!")

+ 0 - 2
app/utils/json_data_formate.py

@@ -27,8 +27,6 @@ def formate_face_data(area_json: dict):
 
 
 def formate_one_card_result(center_result: dict, defect_result: dict):
 def formate_one_card_result(center_result: dict, defect_result: dict):
     data = {
     data = {
-        "img_id": 2,
-        "img_url": "https://123.jpg",
         "result": {
         "result": {
             "center_result": center_result,
             "center_result": center_result,
             "defect_result": defect_result
             "defect_result": defect_result

+ 11 - 5
app/utils/score_inference/CardScorer.py

@@ -1,5 +1,8 @@
 import json
 import json
 from typing import List, Dict, Any, Union
 from typing import List, Dict, Any, Union
+from app.core.logger import get_logger
+
+logger = get_logger(__name__)
 
 
 
 
 class CardScorer:
 class CardScorer:
@@ -58,15 +61,19 @@ class CardScorer:
                 elif defect['label'] in ['impact', 'damaged']:
                 elif defect['label'] in ['impact', 'damaged']:
                     defect_type = "loss_area"
                     defect_type = "loss_area"
                 else:
                 else:
+                    logger.error(f"数据缺陷类型不存在: {defect['label']}")
                     raise TypeError(f"数据缺陷类型不存在: {defect['label']}")
                     raise TypeError(f"数据缺陷类型不存在: {defect['label']}")
             else:
             else:
-                if defect['label'] in ['scratch']:
+                if defect['label'] in ['wear', 'wear_and_impact', 'damaged']:
+                    defect_type = "wear_area"
+                elif defect['label'] in ['scratch', 'scuff']:
                     defect_type = "scratch_length"
                     defect_type = "scratch_length"
-                elif defect['label'] in ['pit']:
+                elif defect['label'] in ['pit', 'impact']:
                     defect_type = "pit_area"
                     defect_type = "pit_area"
                 elif defect['label'] in ['stain']:
                 elif defect['label'] in ['stain']:
                     defect_type = "stain_area"
                     defect_type = "stain_area"
                 else:
                 else:
+                    logger.error(f"数据缺陷类型不存在: {defect['label']}")
                     raise TypeError(f"数据缺陷类型不存在: {defect['label']}")
                     raise TypeError(f"数据缺陷类型不存在: {defect['label']}")
 
 
             rules = aspect_config['rules'][defect_type]
             rules = aspect_config['rules'][defect_type]
@@ -99,7 +106,7 @@ class CardScorer:
 
 
         final_weights = aspect_config["final_weights"][card_aspect]
         final_weights = aspect_config["final_weights"][card_aspect]
         final_score = total_deduction * final_weights
         final_score = total_deduction * final_weights
-        print(f"final weights: {final_weights}, final score: {final_score}")
+        logger.info(f"final weights: {final_weights}, final score: {final_score}")
 
 
         if is_write_score:
         if is_write_score:
             defect_data[f'{card_aspect}_{card_defect_type}_score'] = final_score
             defect_data[f'{card_aspect}_{card_defect_type}_score'] = final_score
@@ -139,7 +146,7 @@ class CardScorer:
         final_weight = self.config['centering']["final_weights"][card_aspect]
         final_weight = self.config['centering']["final_weights"][card_aspect]
         final_score = (h_deduction + v_deduction) * final_weight
         final_score = (h_deduction + v_deduction) * final_weight
 
 
-        print(f"final weight: {final_weight}, final score: {final_score}")
+        logger.info(f"final weight: {final_weight}, final score: {final_score}")
 
 
         if is_write_score:
         if is_write_score:
             center_data[f'{card_aspect}_score'] = final_score
             center_data[f'{card_aspect}_score'] = final_score
@@ -164,7 +171,6 @@ if __name__ == '__main__':
     center_data = scorer.calculate_centering_score("front", center_data, True)
     center_data = scorer.calculate_centering_score("front", center_data, True)
     print(center_data)
     print(center_data)
 
 
-
     # 边角分数
     # 边角分数
     edge_corner_data_path = r"C:\Code\ML\Project\CheckCardBoxAndDefectServer\_temp_work\pokemon_front_corner_no_reflect_defect-corner_result.json"
     edge_corner_data_path = r"C:\Code\ML\Project\CheckCardBoxAndDefectServer\_temp_work\pokemon_front_corner_no_reflect_defect-corner_result.json"
     with open(edge_corner_data_path, 'r', encoding='utf-8') as f:
     with open(edge_corner_data_path, 'r', encoding='utf-8') as f: