182 lines
6.5 KiB
Python
182 lines
6.5 KiB
Python
|
|
# OCR 识别路由 - 专业人民币生肖纪念钞鉴定
|
|||
|
|
import os
|
|||
|
|
import base64
|
|||
|
|
import httpx
|
|||
|
|
from fastapi import APIRouter, Depends, HTTPException, UploadFile, File
|
|||
|
|
from sqlalchemy.orm import Session
|
|||
|
|
from app.core.database import get_db
|
|||
|
|
from app.core.auth import get_current_user
|
|||
|
|
from app.models.models import User
|
|||
|
|
|
|||
|
|
router = APIRouter(prefix="/api/ocr", tags=["OCR 识别"])
|
|||
|
|
|
|||
|
|
# 阿里云 DashScope API 配置
|
|||
|
|
DASHSCOPE_API_KEY = os.getenv("DASHSCOPE_API_KEY", "sk-9389024a37da4f7bb455ac9a6b28776f")
|
|||
|
|
|
|||
|
|
# 专业提示词
|
|||
|
|
PROFESSIONAL_PROMPT = """你是一名专业的人民币生肖纪念钞鉴定专家。请严格按照以下步骤分析这张图片,并提取准确信息。
|
|||
|
|
|
|||
|
|
【识别流程】
|
|||
|
|
1. 判断类型是否评级钞:首先确认是否为裸钞还是评级钞(有封装盒和标签)
|
|||
|
|
2. 验证纪念钞特征:对照生肖纪念钞特征进行确认
|
|||
|
|
3. 验证评级类型:有'标十'字眼的为标十,有'百连'字眼的为标百,其他为单张
|
|||
|
|
4. 提取信息:仔细阅读标签上的所有文字内容
|
|||
|
|
|
|||
|
|
【版别格式要求】
|
|||
|
|
只需要:年份 + 属相,例如:
|
|||
|
|
- 2024 龙
|
|||
|
|
- 2025 蛇
|
|||
|
|
- 2026 马
|
|||
|
|
|
|||
|
|
【输出要求】
|
|||
|
|
严格按照以下格式输出,每个字段必须填写具体值:
|
|||
|
|
✅ 1 发行机构:中国人民银行
|
|||
|
|
✅ 2 发行版别:2024 龙
|
|||
|
|
✅ 3 面额:贰拾圆
|
|||
|
|
✅ 4 是否评级:是/否
|
|||
|
|
✅ 5 封装类型:裸钞/单张/标十/标百
|
|||
|
|
✅ 6 冠字序号:J0xxxxxxxx
|
|||
|
|
✅ 7 评级机构:ACG/PCGS/PMG
|
|||
|
|
✅ 8 评级分数:67/68/69
|
|||
|
|
✅ 9 是否三星:是/否
|
|||
|
|
✅ 10 特殊标识:金山标/天马标/红绳版等
|
|||
|
|
✅ 11 号码特征:金山号 2 张,天马号 3 张等
|
|||
|
|
|
|||
|
|
现在请仔细分析提供的图片,按上述格式输出结果。"""
|
|||
|
|
|
|||
|
|
|
|||
|
|
@router.post("/recognize")
|
|||
|
|
async def recognize_image(
|
|||
|
|
image: UploadFile = File(...),
|
|||
|
|
current_user: User = Depends(get_current_user),
|
|||
|
|
db: Session = Depends(get_db)
|
|||
|
|
):
|
|||
|
|
"""OCR 图片识别"""
|
|||
|
|
try:
|
|||
|
|
image_data = await image.read()
|
|||
|
|
image_base64 = base64.b64encode(image_data).decode('utf-8')
|
|||
|
|
|
|||
|
|
headers = {
|
|||
|
|
"Authorization": f"Bearer {DASHSCOPE_API_KEY}",
|
|||
|
|
"Content-Type": "application/json"
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
# 阿里云 DashScope API 格式 (qwen-vl-max 视觉模型)
|
|||
|
|
payload = {
|
|||
|
|
"model": "qwen-vl-max",
|
|||
|
|
"messages": [{
|
|||
|
|
"role": "user",
|
|||
|
|
"content": [
|
|||
|
|
{
|
|||
|
|
"type": "image_url",
|
|||
|
|
"image_url": {
|
|||
|
|
"url": f"data:image/jpeg;base64,{image_base64}"
|
|||
|
|
}
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
"type": "text",
|
|||
|
|
"text": PROFESSIONAL_PROMPT
|
|||
|
|
}
|
|||
|
|
]
|
|||
|
|
}],
|
|||
|
|
"max_tokens": 1000
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
async with httpx.AsyncClient(timeout=60.0) as client:
|
|||
|
|
response = await client.post(
|
|||
|
|
"https://dashscope.aliyuncs.com/compatible-mode/v1/chat/completions",
|
|||
|
|
json=payload,
|
|||
|
|
headers=headers
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
if response.status_code != 200:
|
|||
|
|
raise HTTPException(status_code=500, detail=f"OCR API 调用失败:{response.text[:200]}")
|
|||
|
|
|
|||
|
|
ocr_result = response.json()
|
|||
|
|
text_content = ""
|
|||
|
|
if "choices" in ocr_result and len(ocr_result["choices"]) > 0:
|
|||
|
|
text_content = ocr_result["choices"][0]["message"]["content"]
|
|||
|
|
|
|||
|
|
fields = extract_fields(text_content)
|
|||
|
|
|
|||
|
|
# 调试日志:打印提取的字段
|
|||
|
|
import logging
|
|||
|
|
logging.info(f"OCR 提取的字段:{fields}")
|
|||
|
|
|
|||
|
|
return {"success": True, "text": text_content, "fields": fields}
|
|||
|
|
|
|||
|
|
except Exception as e:
|
|||
|
|
raise HTTPException(status_code=500, detail=f"识别失败:{str(e)}")
|
|||
|
|
|
|||
|
|
|
|||
|
|
def extract_fields(text: str) -> dict:
|
|||
|
|
"""从 OCR 文本中提取字段 - 直接返回 AI 识别结果"""
|
|||
|
|
import re
|
|||
|
|
fields = {}
|
|||
|
|
|
|||
|
|
# 解析结构化输出
|
|||
|
|
patterns = {
|
|||
|
|
'issuer': r'✅.*?1.*?发行机构.*?[::]\s*(.+?)(?:\n|$)',
|
|||
|
|
'version': r'✅.*?2.*?发行版别.*?[::]\s*(.+?)(?:\n|$)',
|
|||
|
|
'denomination': r'✅.*?3.*?面额.*?[::]\s*(.+?)(?:\n|$)',
|
|||
|
|
'is_graded_text': r'✅.*?4.*?是否评级.*?[::]\s*(.+?)(?:\n|$)',
|
|||
|
|
'packaging': r'✅.*?5.*?封装类型.*?[::]\s*(.+?)(?:\n|$)',
|
|||
|
|
'prefix_serial': r'✅.*?6.*?冠字序号.*?[::]\s*(.+?)(?:\n|$)',
|
|||
|
|
'grading_company': r'✅.*?7.*?评级机构.*?[::]\s*(.+?)(?:\n|$)',
|
|||
|
|
'grading_score': r'✅.*?8.*?评级分数.*?[::]\s*(.+?)(?:\n|$)',
|
|||
|
|
'three_star_text': r'✅.*?9.*?是否三星.*?[::]\s*(.+?)(?:\n|$)',
|
|||
|
|
'special_mark': r'✅.*?10.*?特殊标识.*?[::]\s*(.+?)(?:\n|$)',
|
|||
|
|
'serial_feature': r'✅.*?11.*?号码特征.*?[::]\s*(.+?)(?:\n|$)'
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
for field, pattern in patterns.items():
|
|||
|
|
match = re.search(pattern, text, re.IGNORECASE | re.DOTALL)
|
|||
|
|
if match:
|
|||
|
|
value = match.group(1).strip()
|
|||
|
|
# 保留所有值,包括"无"和"未识别",让前端处理
|
|||
|
|
fields[field] = value
|
|||
|
|
|
|||
|
|
# 处理是否评级
|
|||
|
|
if 'is_graded_text' in fields:
|
|||
|
|
fields['is_graded'] = '是' in fields.pop('is_graded_text')
|
|||
|
|
|
|||
|
|
# 处理是否三星
|
|||
|
|
if 'three_star_text' in fields:
|
|||
|
|
fields['three_star'] = '是' in fields.pop('three_star_text')
|
|||
|
|
|
|||
|
|
# 简化版别字段(2024 龙年贺岁纪念钞(标十) → 2024 龙)
|
|||
|
|
if 'version' in fields:
|
|||
|
|
version = fields['version']
|
|||
|
|
# 提取年份和生肖
|
|||
|
|
year_match = re.search(r'(20\d{2})', version)
|
|||
|
|
animal = ''
|
|||
|
|
if '龙' in version:
|
|||
|
|
animal = '龙'
|
|||
|
|
elif '蛇' in version:
|
|||
|
|
animal = '蛇'
|
|||
|
|
elif '马' in version:
|
|||
|
|
animal = '马'
|
|||
|
|
elif '羊' in version:
|
|||
|
|
animal = '羊'
|
|||
|
|
elif '猴' in version:
|
|||
|
|
animal = '猴'
|
|||
|
|
elif '鸡' in version:
|
|||
|
|
animal = '鸡'
|
|||
|
|
elif '狗' in version:
|
|||
|
|
animal = '狗'
|
|||
|
|
elif '猪' in version:
|
|||
|
|
animal = '猪'
|
|||
|
|
elif '鼠' in version:
|
|||
|
|
animal = '鼠'
|
|||
|
|
elif '牛' in version:
|
|||
|
|
animal = '牛'
|
|||
|
|
elif '虎' in version:
|
|||
|
|
animal = '虎'
|
|||
|
|
elif '兔' in version:
|
|||
|
|
animal = '兔'
|
|||
|
|
|
|||
|
|
if year_match and animal:
|
|||
|
|
fields['version'] = f"{year_match.group(1)}{animal}"
|
|||
|
|
|
|||
|
|
return fields
|