Ver código fonte

增加自动上传6个图片接口

AnlaAnla 2 semanas atrás
pai
commit
357b313bfd
4 arquivos alterados com 357 adições e 14 exclusões
  1. 101 0
      Test/process_and_import_api_test.py
  2. 253 0
      app/api/auto_import.py
  3. 0 14
      app/core/database_loader.py
  4. 3 0
      app/main.py

+ 101 - 0
Test/process_and_import_api_test.py

@@ -0,0 +1,101 @@
+import requests
+import os
+from datetime import datetime
+
+# --- 配置区域 ---
+# 替换为你的存储服务端实际地址和端口
+SERVER_URL = "http://127.0.0.1:7755"
+API_ENDPOINT = f"{SERVER_URL}/api/import/process_and_import"
+
+
+def import_card(
+        card_name: str,
+        cardNo: str,
+        card_type: str,
+        is_reflect_card: bool,
+        strict_mode: bool,
+        paths: dict
+):
+    """
+    请求后端的自动导入接口
+    """
+    # 构造请求的 JSON Payload
+    payload = {
+        "card_name": card_name,
+        "cardNo": cardNo,
+        "card_type": card_type,
+        "is_reflect_card": is_reflect_card,
+        "strict_mode": strict_mode,
+        "path_front_ring": paths.get("front_ring"),
+        "path_front_coaxial": paths.get("front_coaxial"),
+        "path_back_ring": paths.get("back_ring"),
+        "path_back_coaxial": paths.get("back_coaxial"),
+        "path_front_gray": paths.get("front_gray"),
+        "path_back_gray": paths.get("back_gray")
+    }
+
+    # 可选:在客户端进行一次基础的文件存在性校验,避免浪费网络请求
+    for key, path in payload.items():
+        if key.startswith("path_") and path is not None:
+            if not os.path.exists(path):
+                print(f"[警告] 本地文件不存在: {path}")
+                # 如果文件不存在,将其置为 None,交由服务端的 strict_mode 决定是否报错
+                payload[key] = None
+
+    print(f"\n🚀 开始请求导入接口: {card_name} (编号: {cardNo})")
+
+    try:
+        # 注意:由于后端要并发调用推理服务、转正并保存数据库,这里把 timeout 设置得稍微长一点 (120秒)
+        response = requests.post(API_ENDPOINT, json=payload, timeout=120)
+
+        if response.status_code == 200:
+            data = response.json()
+            print(f"✅ 导入成功! 返回数据: {data}")
+        else:
+            print(f"❌ 导入失败! 状态码: {response.status_code}")
+            try:
+                print(f"❌ 错误详情: {response.json()}")
+            except:
+                print(f"❌ 错误详情: {response.text}")
+
+    except requests.exceptions.Timeout:
+        print("❌ 请求超时!后端可能还在处理,请检查服务端日志。")
+    except requests.exceptions.RequestException as e:
+        print(f"❌ 请求发生异常: {e}")
+
+
+if __name__ == "__main__":
+    # --- 测试配置 ---
+    BASE_PATH = r"C:\Code\ML\Image\Card\img20_test"
+    IS_REFLECT = True
+    CARD_TYPE = "pokemon"
+
+    # 模拟循环处理 (类似你原来的逻辑)
+    for img_num in range(2, 3):  # 测试 1 到 3
+        print(f"\n>>>>> 准备处理图片组: {img_num}")
+
+        # 构造卡牌信息
+        timestamp = datetime.now().strftime('%m%d_%H%M%S')
+        card_name = f"API测试卡_{img_num}_{timestamp}"
+        cardNo = f"SN-TEST-{img_num:03d}"
+
+        # 构造路径字典 (根据你的实际命名规则修改)
+        # 注意:我在 back_ring 的地方复用了你之前脚本里硬编码的路径作为示例
+        image_paths = {
+            "front_coaxial": os.path.join(BASE_PATH, f"{img_num}_front_coaxial.jpg"),
+            "front_ring": os.path.join(BASE_PATH, f"{img_num}_front_ring.jpg"),
+            "back_coaxial": os.path.join(BASE_PATH, f"{img_num}_back_coaxial.jpg"),
+            "back_ring": r"C:\Code\ML\Image\Card\b2.jpg",  # 你原脚本中的硬编码测试路径
+            "front_gray": os.path.join(BASE_PATH, f"{img_num}_front_gray.jpg"),
+            "back_gray": os.path.join(BASE_PATH, f"{img_num}_back_gray.jpg")
+        }
+
+        # 调用封装的函数发送 HTTP 请求
+        import_card(
+            card_name=card_name,
+            cardNo=cardNo,
+            card_type=CARD_TYPE,
+            is_reflect_card=IS_REFLECT,
+            strict_mode=True,  # 严格模式:如果缺主图,服务端会直接拦截报错
+            paths=image_paths
+        )

+ 253 - 0
app/api/auto_import.py

@@ -0,0 +1,253 @@
+import os
+import json
+import asyncio
+import aiohttp
+import aiofiles
+from typing import Dict, Any, Tuple, Optional
+from fastapi import APIRouter, HTTPException, Request
+from pydantic import BaseModel, Field
+
+from app.core.config import settings
+from app.core.logger import get_logger
+from app.utils.scheme import CardType
+
+logger = get_logger(__name__)
+router = APIRouter()
+
+
+# 定义请求参数模型
+class AutoImportRequest(BaseModel):
+    card_name: str = Field(..., description="卡牌名称")
+    cardNo: Optional[str] = Field(None, description="卡牌编号")
+    card_type: CardType = Field(CardType.pokemon, description="卡牌类型")
+    is_reflect_card: bool = Field(True, description="是否是反光卡")
+    strict_mode: bool = Field(False, description="如果为True,必须提供所有4张主图")
+
+    path_front_ring: Optional[str] = Field(None, description="正面环光图绝对路径")
+    path_front_coaxial: Optional[str] = Field(None, description="正面同轴光图绝对路径")
+    path_back_ring: Optional[str] = Field(None, description="背面环光图绝对路径")
+    path_back_coaxial: Optional[str] = Field(None, description="背面同轴光图绝对路径")
+    path_front_gray: Optional[str] = Field(None, description="正面灰度图绝对路径")
+    path_back_gray: Optional[str] = Field(None, description="背面灰度图绝对路径")
+
+
+# --- 内部辅助函数 ---
+async def call_api_with_file(
+        session: aiohttp.ClientSession,
+        url: str,
+        file_path: str,
+        params: Dict[str, Any] = None,
+        form_fields: Dict[str, Any] = None,
+        file_field_name: str = 'file'
+) -> Tuple[int, bytes]:
+    """通用的文件上传API调用函数"""
+    form_data = aiohttp.FormData()
+
+    if form_fields:
+        for key, value in form_fields.items():
+            form_data.add_field(key, str(value))
+
+    if not os.path.exists(file_path):
+        raise FileNotFoundError(f"文件不存在: {file_path}")
+
+    async with aiofiles.open(file_path, 'rb') as f:
+        content = await f.read()
+        form_data.add_field(
+            file_field_name,
+            content,
+            filename=os.path.basename(file_path),
+            content_type='image/jpeg'
+        )
+
+    try:
+        async with session.post(url, data=form_data, params=params) as response:
+            response_content = await response.read()
+            if not response.ok:
+                logger.error(
+                    f"[API Error] {url} -> Status: {response.status}, Msg: {response_content.decode('utf-8')[:100]}")
+            return response.status, response_content
+    except Exception as e:
+        logger.error(f"[Conn Error] {url} -> {e}")
+        raise e
+
+
+async def process_main_image(
+        session: aiohttp.ClientSession,
+        image_path: str,
+        score_type: str,
+        is_reflect_card: str
+) -> Dict[str, Any]:
+    """调用推理服务,处理主图片"""
+    logger.info(f"处理主图: {score_type} -> {os.path.basename(image_path)}")
+    inference_base_url = settings.SCORE_UPDATE_SERVER_URL
+
+    # 1. 获取转正后的图片
+    rectify_url = f"{inference_base_url}/api/card_inference/card_rectify_and_center"
+    rectify_status, rectified_image_bytes = await call_api_with_file(
+        session, url=rectify_url, file_path=image_path
+    )
+    if rectify_status >= 300:
+        raise HTTPException(status_code=500, detail=f"图片转正失败: {score_type}")
+
+    # 2. 获取分数JSON
+    score_url = f"{inference_base_url}/api/card_score/score_inference"
+    score_params = {"score_type": score_type, "is_reflect_card": is_reflect_card}
+
+    score_status, score_json_bytes = await call_api_with_file(
+        session, url=score_url, file_path=image_path, params=score_params
+    )
+    if score_status >= 300:
+        raise HTTPException(status_code=500, detail=f"推理分数失败: {score_type}")
+
+    return {
+        "image_type": score_type,
+        "rectified_image": rectified_image_bytes,
+        "score_json": json.loads(score_json_bytes)
+    }
+
+
+async def create_card_record(session: aiohttp.ClientSession, base_url: str, card_name: str, cardNo: Optional[str],
+                             card_type: CardType) -> int:
+    """调用自身服务创建新的卡牌记录"""
+    url = f"{base_url}{settings.API_PREFIX}/cards/created"
+    params = {'card_name': card_name, 'card_type': card_type.value}
+    if cardNo:
+        params['cardNo'] = cardNo
+
+    async with session.post(url, params=params) as response:
+        if response.status == 201:
+            data = await response.json()
+            return data.get('id')
+        else:
+            text = await response.text()
+            raise HTTPException(status_code=response.status, detail=f"创建卡牌记录失败: {text}")
+
+
+async def upload_main_image(session: aiohttp.ClientSession, base_url: str, card_id: int,
+                            processed_data: Dict[str, Any]):
+    """将处理后的主图和JSON上传到自身服务"""
+    image_type = processed_data['image_type']
+    url = f"{base_url}{settings.API_PREFIX}/images/insert/{card_id}"
+
+    form_data = aiohttp.FormData()
+    form_data.add_field('image_type', image_type)
+    form_data.add_field('json_data_str', json.dumps(processed_data['score_json'], ensure_ascii=False))
+    form_data.add_field(
+        'image',
+        processed_data['rectified_image'],
+        filename=f'{image_type}_rectified.jpg',
+        content_type='image/jpeg'
+    )
+
+    async with session.post(url, data=form_data) as response:
+        if response.status != 201:
+            logger.error(f"[主图上传失败] {image_type} code={response.status}")
+            raise HTTPException(status_code=500, detail=f"主图保存失败: {image_type}")
+
+
+async def upload_gray_image(session: aiohttp.ClientSession, base_url: str, card_id: int, image_type: str,
+                            file_path: str):
+    """将灰度图源文件上传到自身服务"""
+    url = f"{base_url}{settings.API_PREFIX}/images/insert/gray/{card_id}"
+    form_fields = {'image_type': image_type}
+
+    status, _ = await call_api_with_file(
+        session, url=url, file_path=file_path, form_fields=form_fields, file_field_name='image'
+    )
+    if status != 201:
+        logger.error(f"[灰度图上传失败] {image_type} code={status}")
+        raise HTTPException(status_code=500, detail=f"灰度图保存失败: {image_type}")
+
+
+# --- 暴露的API接口 ---
+
+@router.post("/process_and_import", summary="自动化处理并导入卡牌数据")
+async def auto_import_script_api(request: Request, req_data: AutoImportRequest):
+    """
+    接收本地的图片路径,自动转正、推理分数、建立卡片关联并入库。
+参数如下:
+
+    card_name: 卡牌名称
+    cardNo: 卡牌编号
+    card_type: 卡牌类型
+    is_reflect_card: 是否是反光卡
+    strict_mode: 如果为True,必须提供所有4张主图
+    path_front_ring: 正面环光图绝对路径
+    path_front_coaxial: 正面同轴光图绝对路径
+    path_back_ring: 背面环光图绝对路径
+    path_back_coaxial: 背面同轴光图绝对路径
+    path_front_gray: 正面灰度图绝对路径
+    path_back_gray: 背面灰度图绝对路径
+    """
+    # 动态获取当前服务的 base_url (例如 http://127.0.0.1:7755)
+    local_base_url = str(request.base_url).rstrip('/')
+
+    main_inputs = {
+        "front_ring": req_data.path_front_ring,
+        "front_coaxial": req_data.path_front_coaxial,
+        "back_ring": req_data.path_back_ring,
+        "back_coaxial": req_data.path_back_coaxial
+    }
+    gray_inputs = {
+        "front_gray": req_data.path_front_gray,
+        "back_gray": req_data.path_back_gray
+    }
+
+    # 1. 严格模式与路径检查
+    provided_main_count = sum(1 for p in main_inputs.values() if p is not None)
+    if req_data.strict_mode and provided_main_count != 4:
+        raise HTTPException(status_code=400,
+                            detail=f"严格模式开启,必须提供所有4张主图。当前提供了 {provided_main_count} 张。")
+    if not req_data.strict_mode and provided_main_count == 0 and not any(gray_inputs.values()):
+        raise HTTPException(status_code=400, detail="未提供任何图片,无法创建。")
+
+    for p in list(main_inputs.values()) + list(gray_inputs.values()):
+        if p and not os.path.exists(p):
+            raise HTTPException(status_code=400, detail=f"图片文件不存在: {p}")
+
+    is_reflect_str = "true" if req_data.is_reflect_card else "false"
+
+    async with aiohttp.ClientSession() as session:
+        try:
+            # Step 1: 主图并发推理
+            logger.info(f"--- 开始自动导入任务: {req_data.card_name} ---")
+            processing_tasks = []
+            for img_type, path in main_inputs.items():
+                if path:
+                    processing_tasks.append(process_main_image(session, path, img_type, is_reflect_str))
+
+            processed_results = []
+            if processing_tasks:
+                processed_results = await asyncio.gather(*processing_tasks)
+
+            # Step 2: 在自身数据库创建卡片记录 (携带新加的 cardNo)
+            card_id = await create_card_record(
+                session, local_base_url, req_data.card_name, req_data.cardNo, req_data.card_type
+            )
+            logger.info(f"卡片记录创建成功,ID: {card_id}")
+
+            # Step 3: 并发调用自身的图片保存接口
+            upload_tasks = []
+            for res in processed_results:
+                upload_tasks.append(upload_main_image(session, local_base_url, card_id, res))
+
+            for img_type, path in gray_inputs.items():
+                if path:
+                    upload_tasks.append(upload_gray_image(session, local_base_url, card_id, img_type, path))
+
+            if upload_tasks:
+                await asyncio.gather(*upload_tasks)
+
+            logger.info(f"--- 自动导入流程结束, Card ID: {card_id} ---")
+            return {
+                "message": "导入成功",
+                "card_id": card_id,
+                "card_name": req_data.card_name,
+                "cardNo": req_data.cardNo
+            }
+
+        except HTTPException:
+            raise
+        except Exception as e:
+            logger.error(f"[流程终止] 发生异常: {e}")
+            raise HTTPException(status_code=500, detail=f"自动化处理异常: {str(e)}")

+ 0 - 14
app/core/database_loader.py

@@ -41,20 +41,6 @@ def init_database():
         logger.info(f"数据表 '{settings.DB_CARD_TABLE_NAME}' 已准备就绪。")
 
 
-        # 临时代码
-        # ----------------- 新增无损增加字段的逻辑 -----------------
-        try:
-            cursor.execute(f"SHOW COLUMNS FROM `{settings.DB_CARD_TABLE_NAME}` LIKE 'cardNo'")
-            if not cursor.fetchone():
-                logger.info(f"表 '{settings.DB_CARD_TABLE_NAME}' 中不存在 'cardNo' 字段,正在安全添加以兼容旧数据...")
-                # ALTER TABLE 添加列到 card_name 之后
-                cursor.execute(
-                    f"ALTER TABLE `{settings.DB_CARD_TABLE_NAME}` ADD COLUMN `cardNo` VARCHAR(100) NULL COMMENT '卡牌编号' AFTER `card_name`")
-                logger.info("字段 'cardNo' 添加成功。")
-        except mysql.connector.Error as err:
-            logger.error(f"检查/添加 'cardNo' 字段失败: {err}")
-        # --------------------------------------------------------
-
         # 2. 创建 card_images 表 (主要计算图)
         card_images_table = (
             f"CREATE TABLE IF NOT EXISTS `{settings.DB_IMAGE_TABLE_NAME}` ("

+ 3 - 0
app/main.py

@@ -11,6 +11,8 @@ 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 app.api import auto_import as auto_import_router
+
 from .core.config import settings
 from .core.logger import setup_logging, get_logger
 
@@ -49,5 +51,6 @@ app.include_router(cards_router.router, prefix=f"{settings.API_PREFIX}/cards", t
 app.include_router(images_router.router, prefix=f"{settings.API_PREFIX}/images", tags=["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(auto_import_router.router, prefix=f"{settings.API_PREFIX}/import", tags=["AutoImport"])
 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"])