rating_report.py 9.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289
  1. from fastapi import APIRouter, HTTPException, Depends, Query
  2. from mysql.connector.pooling import PooledMySQLConnection
  3. import io
  4. from app.core.minio_client import minio_client
  5. from typing import List, Dict, Any, Optional
  6. from PIL import Image
  7. from app.core.logger import get_logger
  8. from app.core.config import settings
  9. from app.core.database_loader import get_db_connection
  10. from app.crud import crud_card
  11. from app.utils.scheme import ImageType
  12. logger = get_logger(__name__)
  13. router = APIRouter()
  14. def _get_active_json(image_data: Any) -> Optional[Dict]:
  15. """获取有效的json数据,优先 modified_json"""
  16. if not image_data:
  17. return None
  18. # image_data 可能是 Pydantic 对象或 字典,做兼容处理
  19. if hasattr(image_data, "modified_json"):
  20. mj = image_data.modified_json
  21. dj = image_data.detection_json
  22. else:
  23. mj = image_data.get("modified_json")
  24. dj = image_data.get("detection_json")
  25. # 注意:根据 schema.py,这里读出来已经是 dict 了,不需要 json.loads
  26. # 如果数据库里存的是 null,读出来是 None
  27. if mj:
  28. return mj
  29. return dj
  30. def _crop_defect_image(original_image_path_str: str, min_rect: List, output_filename: str) -> str:
  31. """
  32. 通过 MinIO 切割缺陷图片为正方形并保存
  33. """
  34. try:
  35. # ★ 将进来的全路径 URL 剥离为相对路径 (如 /Data/xxx.jpg) 供 MinIO 读取
  36. rel_path = original_image_path_str.replace(settings.DATA_HOST_URL, "")
  37. rel_path = "/" + rel_path.lstrip('/\\')
  38. object_name = f"{settings.MINIO_BASE_PREFIX}{rel_path}"
  39. # 1. 从 MinIO 获取原图字节
  40. try:
  41. response = minio_client.get_object(settings.MINIO_BUCKET, object_name)
  42. image_bytes = response.read()
  43. response.close()
  44. response.release_conn()
  45. except Exception as e:
  46. logger.warning(f"从MinIO获取原图失败: {object_name} -> {e}")
  47. return ""
  48. # 2. 在内存中用 PIL 切图
  49. with Image.open(io.BytesIO(image_bytes)) as img:
  50. img_w, img_h = img.size
  51. center_x, center_y = min_rect[0]
  52. rect_w, rect_h = min_rect[1]
  53. side_length = max(max(rect_w, rect_h) * 1.5, 100)
  54. half_side = side_length / 2
  55. left, top = max(0, center_x - half_side), max(0, center_y - half_side)
  56. right, bottom = min(img_w, center_x + half_side), min(img_h, center_y + half_side)
  57. cropped_img = img.crop((left, top, right, bottom))
  58. # 3. 将切割后的图存入内存流,并上传到 MinIO
  59. out_bytes = io.BytesIO()
  60. cropped_img.save(out_bytes, format="JPEG", quality=95)
  61. out_bytes.seek(0)
  62. out_rel_path = f"/DefectImage/{output_filename}"
  63. out_object_name = f"{settings.MINIO_BASE_PREFIX}{out_rel_path}"
  64. minio_client.put_object(
  65. settings.MINIO_BUCKET,
  66. out_object_name,
  67. out_bytes,
  68. len(out_bytes.getvalue()),
  69. content_type="image/jpeg"
  70. )
  71. return settings.get_full_url(out_rel_path)
  72. except Exception as e:
  73. logger.error(f"切割并上传图片失败: {e}")
  74. return ""
  75. @router.get("/generate", status_code=200, summary="生成评级报告数据")
  76. def generate_rating_report(
  77. cardNo: str,
  78. db_conn: PooledMySQLConnection = Depends(get_db_connection)
  79. ):
  80. if not cardNo or not cardNo.strip():
  81. raise HTTPException(status_code=400, detail="cardNo 不能为空")
  82. # 根据cardNo 查询id
  83. try:
  84. with db_conn.cursor(buffered=True) as cursor:
  85. query_sql = f"SELECT id FROM {settings.DB_CARD_TABLE_NAME} WHERE cardNo = %s LIMIT 1"
  86. cursor.execute(query_sql, (cardNo,))
  87. row = cursor.fetchone()
  88. except Exception as e:
  89. logger.error(f"创建卡牌失败: {e}")
  90. raise HTTPException(status_code=500, detail="数据库查询失败。")
  91. if not row:
  92. raise HTTPException(
  93. status_code=404,
  94. detail=f"未找到卡牌编号为 {cardNo} 的相关记录"
  95. )
  96. card_id = row[0]
  97. top_n_defects = 3
  98. """
  99. 根据 Card ID 生成评级报告 JSON
  100. """
  101. # 1. 获取卡片详情 (复用 Crud 逻辑,确保能拿到所有图片)
  102. card_data = crud_card.get_card_with_details(db_conn, card_id)
  103. if not card_data:
  104. raise HTTPException(status_code=404, detail="未找到该卡片信息")
  105. # 初始化返回结构
  106. response_data = {
  107. "backImageUrl": "",
  108. "frontImageUrl": "",
  109. "cardNo": cardNo,
  110. "centerBack": "",
  111. "centerFront": "",
  112. "measureLength": 0.0,
  113. "measureWidth": 0.0,
  114. "cornerBackNum": 0,
  115. "sideBackNum": 0,
  116. "surfaceBackNum": 0,
  117. "cornerFrontNum": 0,
  118. "sideFrontNum": 0,
  119. "surfaceFrontNum": 0,
  120. "scoreThreshold": float(card_data.get("detection_score") or 0),
  121. "evaluateNo": str(card_data.get("id")),
  122. "defectDetailList": []
  123. }
  124. # 临时列表用于收集所有缺陷,最后排序取 Top N
  125. all_defects_collected = []
  126. # 遍历图片寻找 Front Ring 和 Back Ring
  127. images = card_data.get("images", [])
  128. # 辅助字典:defect_type 到 统计字段 的映射
  129. defect_map_keys = {
  130. "front_ring": {
  131. "corner": "cornerFrontNum",
  132. "edge": "sideFrontNum",
  133. "face": "surfaceFrontNum"
  134. },
  135. "back_ring": {
  136. "corner": "cornerBackNum",
  137. "edge": "sideBackNum",
  138. "face": "surfaceBackNum"
  139. }
  140. }
  141. for img in images:
  142. img_type = img.image_type
  143. # 只处理环光图
  144. if img_type not in ["front_ring", "back_ring"]:
  145. continue
  146. # 设置主图 URL
  147. if img_type == "front_ring":
  148. response_data["frontImageUrl"] = img.image_path
  149. elif img_type == "back_ring":
  150. response_data["backImageUrl"] = img.image_path
  151. # 获取有效 JSON
  152. json_data = _get_active_json(img)
  153. if not json_data or "result" not in json_data:
  154. continue
  155. result_node = json_data["result"]
  156. # 1. 处理居中 (Center)
  157. center_inf = result_node.get("center_result", {}).get("box_result", {}).get("center_inference", {})
  158. if center_inf:
  159. # 格式: L/R=47/53, T/B=51/49 (取整)
  160. # center_inference 包含 center_left, center_right, center_top, center_bottom
  161. c_str = (
  162. f"L/R={int(round(center_inf.get('center_left', 0)))}/{int(round(center_inf.get('center_right', 0)))}, "
  163. f"T/B={int(round(center_inf.get('center_top', 0)))}/{int(round(center_inf.get('center_bottom', 0)))}"
  164. )
  165. if img_type == "front_ring":
  166. response_data["centerFront"] = c_str
  167. # 2. 处理尺寸 (仅从正面取,或者只要有就取) - mm 转 cm,除以 10,保留2位
  168. rw_mm = center_inf.get("real_width_mm", 0)
  169. rh_mm = center_inf.get("real_height_mm", 0)
  170. response_data["measureWidth"] = round(rw_mm / 10.0, 2)
  171. response_data["measureLength"] = round(rh_mm / 10.0, 2)
  172. else:
  173. response_data["centerBack"] = c_str
  174. # 2. 处理缺陷 (Defects)
  175. defects = result_node.get("defect_result", {}).get("defects", [])
  176. for defect in defects:
  177. # 过滤 edit_type == 'del'
  178. if defect.get("edit_type") == "del":
  179. continue
  180. d_type = defect.get("defect_type", "") # corner, edge, face
  181. d_label = defect.get("label", "") # scratch, wear, etc.
  182. # 统计数量
  183. count_key = defect_map_keys.get(img_type, {}).get(d_type)
  184. if count_key:
  185. response_data[count_key] += 1
  186. # 收集详细信息用于 Top N 列表
  187. # 需要保存:缺陷对象本身,图片路径,正反面标识
  188. side_str = "FRONT" if img_type == "front_ring" else "BACK"
  189. all_defects_collected.append({
  190. "defect_data": defect,
  191. "image_path": img.image_path,
  192. "side": side_str,
  193. "area": defect.get("actual_area", 0)
  194. })
  195. # 3. 处理 defectDetailList (Top N 切图)
  196. # 按实际面积从大到小排序
  197. all_defects_collected.sort(key=lambda x: x["area"], reverse=True)
  198. top_defects = all_defects_collected[:top_n_defects]
  199. final_defect_list = []
  200. for idx, item in enumerate(top_defects, start=1):
  201. defect = item["defect_data"]
  202. side = item["side"]
  203. original_img_path = item["image_path"]
  204. # 构造 ID
  205. d_id = idx # 1, 2, 3
  206. # 构造文件名: {card_id}_{seq_id}.jpg
  207. filename = f"{card_id}_{d_id}.jpg"
  208. # 执行切图
  209. min_rect = defect.get("min_rect")
  210. defect_img_url = ""
  211. location_str = ""
  212. if min_rect and len(min_rect) == 3:
  213. # 切图并保存
  214. defect_img_url = _crop_defect_image(original_img_path, min_rect, filename)
  215. # 计算 Location (中心坐标)
  216. # min_rect[0] 是 [x, y]
  217. cx, cy = min_rect[0]
  218. location_str = f"{int(cx)},{int(cy)}"
  219. # 构造 Type 字符串: defect_type + label (大写)
  220. # 例如: defect_type="edge", label="wear" -> "EDGE WEAR"
  221. d_type_raw = defect.get("defect_type", "")
  222. # d_label_raw = defect.get("label", "")
  223. type_str = f"{d_type_raw.upper()}".strip()
  224. type_str_map = {"CORNER": "CORNER",
  225. "EDGE": "SIDE",
  226. "FACE": "SURFACE"}
  227. type_str = type_str_map.get(type_str)
  228. final_defect_list.append({
  229. "id": d_id,
  230. "side": side,
  231. "location": location_str,
  232. "type": type_str,
  233. "defectImgUrl": defect_img_url
  234. })
  235. response_data["defectDetailList"] = final_defect_list
  236. return response_data