tongue.py 3.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109
  1. from fastapi import APIRouter, UploadFile, File, Form
  2. from typing import Optional
  3. from app.agents.multimodal_agent import TongueDiagnosisAgent
  4. import logging
  5. logger = logging.getLogger(__name__)
  6. router = APIRouter(prefix="/api/v1", tags=["tongue"])
  7. _agent: Optional[TongueDiagnosisAgent] = None
  8. def get_agent() -> TongueDiagnosisAgent:
  9. global _agent
  10. if _agent is None:
  11. _agent = TongueDiagnosisAgent()
  12. return _agent
  13. @router.post("/tongue/diagnose")
  14. async def tongue_diagnose(
  15. file: UploadFile = File(...),
  16. user_id: int = Form(...),
  17. prompt_template: Optional[str] = Form(None),
  18. ):
  19. """舌诊分析: 上传舌苔图片, 返回分析结果"""
  20. agent = get_agent()
  21. import tempfile, os
  22. ext = os.path.splitext(file.filename or "tongue.jpg")[1] or ".jpg"
  23. tmp = tempfile.NamedTemporaryFile(delete=False, suffix=ext)
  24. content = await file.read()
  25. tmp.write(content)
  26. tmp.close()
  27. try:
  28. import base64
  29. b64 = base64.b64encode(content).decode()
  30. data_url = f"data:image/{ext[1:]};base64,{b64}"
  31. result = await agent.diagnose(image_url=data_url, user_id=user_id, prompt_template=prompt_template)
  32. return {"code": 200, "data": result}
  33. except Exception as e:
  34. logger.error("舌诊分析失败: %s", e, exc_info=True)
  35. return {"code": 500, "message": "舌诊分析失败"}
  36. finally:
  37. os.unlink(tmp.name)
  38. # ── 舌象快速分类(纯颜色启发式,<50ms)────────────────────────────
  39. def _quick_tongue_detect(content: bytes) -> bool:
  40. """基于颜色分布的轻量级舌象分类,避免调用 LLM。
  41. 判据:
  42. 1. 全图中偏红/暖色像素占比 >= 30%(舌体是红/粉色)
  43. 2. 白色/近白像素占比 < 65%(排除大面积白纸报告)
  44. 3. 纯黑文字像素占比 < 25%(排除文档/报告截图)
  45. """
  46. from PIL import Image
  47. import io
  48. try:
  49. img = Image.open(io.BytesIO(content)).convert("RGB")
  50. small = img.resize((200, 200), Image.LANCZOS)
  51. pixels = list(small.getdata())
  52. n = len(pixels)
  53. reddish = warm = white_px = black_px = 0
  54. for r, g, b in pixels:
  55. if r > g * 1.15 and r > b * 1.15:
  56. reddish += 1
  57. if r > g and r > b and (r - b) > 15:
  58. warm += 1
  59. if r > 200 and g > 200 and b > 200:
  60. white_px += 1
  61. if r < 80 and g < 80 and b < 80:
  62. black_px += 1
  63. red_ratio = reddish / n
  64. warm_ratio = warm / n
  65. white_ratio = white_px / n
  66. text_ratio = black_px / n
  67. # 舌象:≥30% 偏红 + 白色占比不高 + 文字占比不高
  68. is_tongue = (red_ratio >= 0.30) and (white_ratio < 0.65) and (text_ratio < 0.25)
  69. logger.debug(
  70. "舌象分类 [red=%.3f warm=%.3f white=%.3f text=%.3f] → %s",
  71. red_ratio, warm_ratio, white_ratio, text_ratio, is_tongue,
  72. )
  73. return is_tongue
  74. except Exception as e:
  75. logger.warning("舌象颜色分析失败,降级为 false: %s", e)
  76. return False
  77. @router.post("/tongue/detect")
  78. async def tongue_detect(file: UploadFile = File(...)):
  79. """舌象照片分类: 判断上传图片是否为舌头(舌象)照片,用于图片上传分流。
  80. 返回 {"is_tongue": bool}。非舌象(报告截图/文档/其他部位)返回 false。
  81. 分类失败时返回 false(安全降级:走通用报告流程,不误伤正常报告)。
  82. """
  83. content = await file.read()
  84. try:
  85. is_tongue = _quick_tongue_detect(content)
  86. logger.info("舌象分类结果 is_tongue=%s (filename=%s)", is_tongue, file.filename)
  87. return {"code": 200, "data": {"is_tongue": is_tongue}}
  88. except Exception as e:
  89. logger.error("舌象分类失败: %s", e, exc_info=True)
  90. return {"code": 200, "data": {"is_tongue": False}}