| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109 |
- from fastapi import APIRouter, UploadFile, File, Form
- from typing import Optional
- from app.agents.multimodal_agent import TongueDiagnosisAgent
- import logging
- logger = logging.getLogger(__name__)
- router = APIRouter(prefix="/api/v1", tags=["tongue"])
- _agent: Optional[TongueDiagnosisAgent] = None
- def get_agent() -> TongueDiagnosisAgent:
- global _agent
- if _agent is None:
- _agent = TongueDiagnosisAgent()
- return _agent
- @router.post("/tongue/diagnose")
- async def tongue_diagnose(
- file: UploadFile = File(...),
- user_id: int = Form(...),
- prompt_template: Optional[str] = Form(None),
- ):
- """舌诊分析: 上传舌苔图片, 返回分析结果"""
- agent = get_agent()
- import tempfile, os
- ext = os.path.splitext(file.filename or "tongue.jpg")[1] or ".jpg"
- tmp = tempfile.NamedTemporaryFile(delete=False, suffix=ext)
- content = await file.read()
- tmp.write(content)
- tmp.close()
- try:
- import base64
- b64 = base64.b64encode(content).decode()
- data_url = f"data:image/{ext[1:]};base64,{b64}"
- result = await agent.diagnose(image_url=data_url, user_id=user_id, prompt_template=prompt_template)
- return {"code": 200, "data": result}
- except Exception as e:
- logger.error("舌诊分析失败: %s", e, exc_info=True)
- return {"code": 500, "message": "舌诊分析失败"}
- finally:
- os.unlink(tmp.name)
- # ── 舌象快速分类(纯颜色启发式,<50ms)────────────────────────────
- def _quick_tongue_detect(content: bytes) -> bool:
- """基于颜色分布的轻量级舌象分类,避免调用 LLM。
- 判据:
- 1. 全图中偏红/暖色像素占比 >= 30%(舌体是红/粉色)
- 2. 白色/近白像素占比 < 65%(排除大面积白纸报告)
- 3. 纯黑文字像素占比 < 25%(排除文档/报告截图)
- """
- from PIL import Image
- import io
- try:
- img = Image.open(io.BytesIO(content)).convert("RGB")
- small = img.resize((200, 200), Image.LANCZOS)
- pixels = list(small.getdata())
- n = len(pixels)
- reddish = warm = white_px = black_px = 0
- for r, g, b in pixels:
- if r > g * 1.15 and r > b * 1.15:
- reddish += 1
- if r > g and r > b and (r - b) > 15:
- warm += 1
- if r > 200 and g > 200 and b > 200:
- white_px += 1
- if r < 80 and g < 80 and b < 80:
- black_px += 1
- red_ratio = reddish / n
- warm_ratio = warm / n
- white_ratio = white_px / n
- text_ratio = black_px / n
- # 舌象:≥30% 偏红 + 白色占比不高 + 文字占比不高
- is_tongue = (red_ratio >= 0.30) and (white_ratio < 0.65) and (text_ratio < 0.25)
- logger.debug(
- "舌象分类 [red=%.3f warm=%.3f white=%.3f text=%.3f] → %s",
- red_ratio, warm_ratio, white_ratio, text_ratio, is_tongue,
- )
- return is_tongue
- except Exception as e:
- logger.warning("舌象颜色分析失败,降级为 false: %s", e)
- return False
- @router.post("/tongue/detect")
- async def tongue_detect(file: UploadFile = File(...)):
- """舌象照片分类: 判断上传图片是否为舌头(舌象)照片,用于图片上传分流。
- 返回 {"is_tongue": bool}。非舌象(报告截图/文档/其他部位)返回 false。
- 分类失败时返回 false(安全降级:走通用报告流程,不误伤正常报告)。
- """
- content = await file.read()
- try:
- is_tongue = _quick_tongue_detect(content)
- logger.info("舌象分类结果 is_tongue=%s (filename=%s)", is_tongue, file.filename)
- return {"code": 200, "data": {"is_tongue": is_tongue}}
- except Exception as e:
- logger.error("舌象分类失败: %s", e, exc_info=True)
- return {"code": 200, "data": {"is_tongue": False}}
|