|
|
@@ -32,6 +32,81 @@ class QueryMode(str, Enum):
|
|
|
prev = "prev"
|
|
|
|
|
|
|
|
|
+def _is_center_box_shapes_empty(center_result: dict) -> bool:
|
|
|
+ """
|
|
|
+ 判断 center_result 中 inner/outer box 的 shapes 是否都为空。
|
|
|
+ 前端某些场景会传空 shapes,直接下发给算分服务可能触发其内部越界。
|
|
|
+ """
|
|
|
+ if not isinstance(center_result, dict):
|
|
|
+ return True
|
|
|
+ box_result = center_result.get("box_result", {})
|
|
|
+ if not isinstance(box_result, dict):
|
|
|
+ return True
|
|
|
+
|
|
|
+ inner_shapes = box_result.get("inner_box", {}).get("shapes", [])
|
|
|
+ outer_shapes = box_result.get("outer_box", {}).get("shapes", [])
|
|
|
+ return not inner_shapes and not outer_shapes
|
|
|
+
|
|
|
+
|
|
|
+def _prepare_recalculate_payload(edited_json: dict, source_json: dict) -> dict:
|
|
|
+ """
|
|
|
+ 以数据库里的原始 JSON 为底稿,合并前端编辑结果,得到更稳定的重算入参。
|
|
|
+ 当前仅明确覆盖 defects;center_result 只有在前端传了非空 shapes 时才覆盖。
|
|
|
+ """
|
|
|
+ base = copy.deepcopy(source_json) if isinstance(source_json, dict) else {}
|
|
|
+ incoming = edited_json if isinstance(edited_json, dict) else {}
|
|
|
+
|
|
|
+ if "id" in incoming:
|
|
|
+ base["id"] = incoming["id"]
|
|
|
+ if "imageWidth" in incoming:
|
|
|
+ base["imageWidth"] = incoming["imageWidth"]
|
|
|
+ if "imageHeight" in incoming:
|
|
|
+ base["imageHeight"] = incoming["imageHeight"]
|
|
|
+
|
|
|
+ base.setdefault("result", {})
|
|
|
+ incoming_result = incoming.get("result", {})
|
|
|
+ if not isinstance(incoming_result, dict):
|
|
|
+ incoming_result = {}
|
|
|
+
|
|
|
+ # defects 使用前端编辑结果覆盖
|
|
|
+ incoming_defects = (
|
|
|
+ incoming_result.get("defect_result", {}).get("defects", [])
|
|
|
+ if isinstance(incoming_result.get("defect_result", {}), dict)
|
|
|
+ else []
|
|
|
+ )
|
|
|
+ base["result"].setdefault("defect_result", {})
|
|
|
+ base["result"]["defect_result"]["defects"] = incoming_defects if isinstance(incoming_defects, list) else []
|
|
|
+
|
|
|
+ # center_result 仅在前端有有效 shapes 时覆盖;否则沿用底稿
|
|
|
+ incoming_center = incoming_result.get("center_result")
|
|
|
+ if isinstance(incoming_center, dict) and not _is_center_box_shapes_empty(incoming_center):
|
|
|
+ base["result"]["center_result"] = incoming_center
|
|
|
+ elif "center_result" not in base["result"]:
|
|
|
+ base["result"]["center_result"] = incoming_center if isinstance(incoming_center, dict) else {}
|
|
|
+
|
|
|
+ return base
|
|
|
+
|
|
|
+
|
|
|
+def _sanitize_defects_for_recalculate(defects: list):
|
|
|
+ """
|
|
|
+ 清理前端展示/编辑辅助字段,减少算分服务解析失败概率。
|
|
|
+ """
|
|
|
+ if not isinstance(defects, list):
|
|
|
+ return
|
|
|
+ for d in defects:
|
|
|
+ if not isinstance(d, dict):
|
|
|
+ continue
|
|
|
+ if d.get("label") == "slight_scratch":
|
|
|
+ d["label"] = "scratch"
|
|
|
+ d.pop("defectImgUrl", None)
|
|
|
+ d.pop("defectImgUrls", None)
|
|
|
+ d.pop("gray_id", None)
|
|
|
+ d.pop("fusion_id", None)
|
|
|
+ d.pop("edit_type", None)
|
|
|
+ d.pop("severity_level", None)
|
|
|
+ d.pop("new_score", None)
|
|
|
+
|
|
|
+
|
|
|
def _process_defects_for_json(card_id: int, img_id: int, img_path: str, json_data: dict, side: str, all_images: list = None):
|
|
|
if not json_data or "result" not in json_data:
|
|
|
return
|
|
|
@@ -255,24 +330,18 @@ async def update_image_modified_json(
|
|
|
cursor = None
|
|
|
|
|
|
# *** 1. 格式还原 ***
|
|
|
- # 将前端的 xy dict 格式转回 [[x,y]],并丢弃 points 里的 id
|
|
|
+ # 将前端的 xy dict 格式转回 [[x,y]]
|
|
|
internal_json_payload = convert_xy_to_internal_format(new_json_data)
|
|
|
-
|
|
|
- # 丢弃前端展示用的辅助字段,防止传给算分服务导致报错
|
|
|
- _defects = internal_json_payload.get("result", {}).get("defect_result", {}).get("defects", [])
|
|
|
- for d in _defects:
|
|
|
- if d.get("label") == "slight_scratch":
|
|
|
- d["label"] = "scratch"
|
|
|
- d.pop("defectImgUrl", None)
|
|
|
- d.pop("defectImgUrls", None)
|
|
|
- d.pop("gray_id", None)
|
|
|
- d.pop("fusion_id", None)
|
|
|
|
|
|
try:
|
|
|
cursor = db_conn.cursor(dictionary=True)
|
|
|
|
|
|
# 2. 获取图片信息
|
|
|
- cursor.execute(f"SELECT image_type, card_id FROM {settings.DB_IMAGE_TABLE_NAME} WHERE id = %s", (id,))
|
|
|
+ cursor.execute(
|
|
|
+ f"SELECT image_type, card_id, detection_json, modified_json "
|
|
|
+ f"FROM {settings.DB_IMAGE_TABLE_NAME} WHERE id = %s",
|
|
|
+ (id,)
|
|
|
+ )
|
|
|
row = cursor.fetchone()
|
|
|
if not row:
|
|
|
raise HTTPException(status_code=404, detail=f"ID为 {id} 的图片未找到。")
|
|
|
@@ -293,15 +362,26 @@ async def update_image_modified_json(
|
|
|
if not score_type:
|
|
|
raise HTTPException(status_code=400, detail=f"未知的 image_type: {image_type}")
|
|
|
|
|
|
+ # 3. 准备重算 payload:以库内原始 JSON 为底稿,仅覆盖编辑后的 defects
|
|
|
+ source_json_str = row["modified_json"] if row["modified_json"] else row["detection_json"]
|
|
|
+ if isinstance(source_json_str, str):
|
|
|
+ source_json_data = json.loads(source_json_str)
|
|
|
+ else:
|
|
|
+ source_json_data = source_json_str if isinstance(source_json_str, dict) else {}
|
|
|
+
|
|
|
+ payload_for_recalculate = _prepare_recalculate_payload(internal_json_payload, source_json_data)
|
|
|
+ _defects = payload_for_recalculate.get("result", {}).get("defect_result", {}).get("defects", [])
|
|
|
+ _sanitize_defects_for_recalculate(_defects)
|
|
|
+
|
|
|
logger.info(f"开始计算分数 (ID: {id}, Type: {score_type})")
|
|
|
|
|
|
- # 3. 调用远程计算接口 (使用还原后的 JSON)
|
|
|
+ # 4. 调用远程计算接口
|
|
|
try:
|
|
|
response = await run_in_threadpool(
|
|
|
lambda: requests.post(
|
|
|
settings.SCORE_RECALCULATE_ENDPOINT,
|
|
|
params={"score_type": score_type},
|
|
|
- json=internal_json_payload, # 传递还原后的数据
|
|
|
+ json=payload_for_recalculate,
|
|
|
timeout=20
|
|
|
)
|
|
|
)
|
|
|
@@ -317,7 +397,7 @@ async def update_image_modified_json(
|
|
|
# 获取计算服务返回的结果(这个结果通常已经是标准的 internal 格式,带有分数和面积)
|
|
|
final_json_data = response.json()
|
|
|
|
|
|
- # 4. 保存结果到数据库
|
|
|
+ # 5. 保存结果到数据库
|
|
|
recalculated_json_str = json.dumps(final_json_data, ensure_ascii=False)
|
|
|
update_query = (f"UPDATE {settings.DB_IMAGE_TABLE_NAME} "
|
|
|
f"SET modified_json = %s, is_edited = TRUE "
|