init proj

This commit is contained in:
贺海国
2025-12-25 15:32:44 +08:00
parent 0cb1c0f6e3
commit dac8a55983
9 changed files with 2353 additions and 2 deletions

51
.dockerignore Normal file
View File

@@ -0,0 +1,51 @@
# Git
.git
.gitignore
.gitattributes
# Python
__pycache__
*.py[cod]
*$py.class
*.so
.Python
*.egg-info
dist
build
.venv
venv
env
ENV
# 环境变量文件(应该通过环境变量或 secrets 传递)
.env
.env.local
.env.*.local
env.example
# IDE
.vscode
.idea
*.swp
*.swo
*~
# 文档
README.md
*.md
# 其他
.DS_Store
*.log
.pytest_cache
.coverage
htmlcov
# Docker
Dockerfile*
.dockerignore
docker-compose*.yml
# 锁定文件(可选,如果使用 uv 可以保留)
# uv.lock

38
.gitignore vendored Normal file
View File

@@ -0,0 +1,38 @@
# Python
__pycache__/
*.py[cod]
*$py.class
*.so
.Python
env/
venv/
ENV/
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
*.egg-info/
.installed.cfg
*.egg
# 环境变量
.env
# IDE
.vscode/
.idea/
*.swp
*.swo
*~
# OS
.DS_Store
Thumbs.db

1
.python-version Normal file
View File

@@ -0,0 +1 @@
3.8

32
Dockerfile Normal file
View File

@@ -0,0 +1,32 @@
FROM 172.16.102.3:30648/astral-sh/uv:python3.14-bookworm
# 设置工作目录
WORKDIR /app
# 设置环境变量
ENV PYTHONUNBUFFERED=1 \
PYTHONDONTWRITEBYTECODE=1 \
PIP_NO_CACHE_DIR=1 \
PIP_DISABLE_PIP_VERSION_CHECK=1
# 安装系统依赖
RUN apt-get update && apt-get install -y --no-install-recommends \
gcc \
&& rm -rf /var/lib/apt/lists/*
# 复制项目文件
COPY pyproject.toml ./
COPY app.py ./
RUN uv sync
# 暴露端口
EXPOSE 8000
# 健康检查
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
CMD python -c "import httpx; httpx.get('http://localhost:8000/health', timeout=5)" || exit 1
# 启动应用
CMD ["uv", "run", "app.py"]

179
README.md
View File

@@ -1,3 +1,178 @@
# crop-disease # 病虫害识别 API
基于 OpenRouter 的 Qwen3 VL 8B Instruct 模型构建的病虫害识别 FastAPI 服务。
## 功能特性
- 🖼️ 支持图片上传识别病虫害
- 🔍 智能识别植物病虫害类型和症状
- 💡 提供详细的防治建议
- 📡 支持 base64 编码图片识别
- 🚀 基于 FastAPI 的高性能异步接口
## 安装
1. 克隆项目并进入目录
```bash
cd hm-qwen3-vl
```
2. 安装依赖
```bash
pip install -e .
```
或者使用 uv推荐
```bash
uv pip install -e .
```
3. 配置环境变量
复制 `env.example``.env` 并填入你的 OpenRouter API 密钥:
```bash
cp env.example .env
```
编辑 `.env` 文件,填入你的 API 密钥:
```
OPENROUTER_API_KEY=your_openrouter_api_key_here
```
获取 API 密钥:访问 [OpenRouter](https://openrouter.ai/keys) 注册并获取 API 密钥。
## 运行服务
```bash
python main.py
```
或者使用 uvicorn 直接运行:
```bash
uvicorn main:app --host 0.0.0.0 --port 8000 --reload
```
服务启动后,访问以下地址:
- API 文档http://localhost:8000/docs
- 健康检查http://localhost:8000/health
## API 接口
### 1. 图片上传识别
**POST** `/api/v1/identify`
上传图片文件进行病虫害识别。
**请求参数:**
- `file` (file, required): 植物图片文件(支持 jpg, jpeg, png
- `question` (string, optional): 自定义问题,不提供则使用默认提示
**示例(使用 curl**
```bash
curl -X POST "http://localhost:8000/api/v1/identify" \
-H "accept: application/json" \
-H "Content-Type: multipart/form-data" \
-F "file=@/path/to/plant_image.jpg" \
-F "question=请识别这张图片中的病虫害问题"
```
**示例(使用 Python requests**
```python
import requests
url = "http://localhost:8000/api/v1/identify"
files = {"file": open("plant_image.jpg", "rb")}
data = {"question": "请识别这张图片中的病虫害问题"}
response = requests.post(url, files=files, data=data)
print(response.json())
```
**响应示例:**
```json
{
"success": true,
"result": "根据图片分析,该植物叶片上出现了...",
"image_info": {
"format": "JPEG",
"size": [1920, 1080],
"mode": "RGB"
}
}
```
### 2. Base64 图片识别
**POST** `/api/v1/identify/base64`
使用 base64 编码的图片进行识别。
**请求体JSON**
```json
{
"image_base64": "iVBORw0KGgoAAAANS...",
"question": "请识别这张图片中的病虫害问题"
}
```
**示例:**
```bash
curl -X POST "http://localhost:8000/api/v1/identify/base64" \
-H "Content-Type: application/json" \
-d '{
"image_base64": "base64_encoded_image_string",
"question": "请识别病虫害"
}'
```
### 3. 健康检查
**GET** `/health`
检查服务状态。
**响应:**
```json
{
"status": "healthy",
"model": "qwen/qwen-3-vl-8b-instruct"
}
```
## 使用 Swagger UI
启动服务后,访问 http://localhost:8000/docs 可以使用交互式 API 文档进行测试。
## 注意事项
1. **API 密钥安全**:请妥善保管你的 OpenRouter API 密钥,不要将其提交到版本控制系统
2. **图片大小**:图片会自动缩放到最大 4096x4096 像素
3. **请求超时**API 请求超时时间设置为 60 秒
4. **费用**:使用 OpenRouter API 可能会产生费用,请查看 [OpenRouter 定价](https://openrouter.ai/models)
## 技术栈
- FastAPI现代、快速的 Web 框架
- OpenRouter统一的 AI 模型 API 接口
- Qwen3 VL 8B Instruct多模态视觉语言模型
- Pillow图片处理库
- httpx异步 HTTP 客户端
## 许可证
MIT
基于Qwen3 VL 模型的病虫害识别 FastAPI 服务

266
app.py Normal file
View File

@@ -0,0 +1,266 @@
"""
基于 OpenRouter Qwen3 VL 8B Instruct 模型的病虫害识别 FastAPI 服务
"""
import os
import base64
import binascii
from io import BytesIO
from typing import Optional
from dotenv import load_dotenv
import httpx
from fastapi import FastAPI, File, UploadFile, HTTPException
from fastapi.responses import JSONResponse
from pydantic import BaseModel
from PIL import Image
# 加载环境变量
load_dotenv()
app = FastAPI(
title="病虫害识别 API",
description="基于 OpenRouter Qwen3 VL 8B Instruct 模型的病虫害识别服务",
version="1.0.0"
)
# OpenRouter API 配置
OPENROUTER_API_KEY = os.getenv("OPENROUTER_API_KEY")
OPENROUTER_API_URL = "https://openrouter.ai/api/v1/chat/completions"
MODEL_NAME = os.getenv("OPENROUTER_MODEL", "qwen/qwen3-vl-8b-instruct") # 可通过环境变量配置
if not OPENROUTER_API_KEY:
raise ValueError("请设置 OPENROUTER_API_KEY 环境变量")
class Base64ImageRequest(BaseModel):
"""Base64 图片识别请求模型"""
image_base64: str
question: Optional[str] = None
def encode_image_to_base64(image: Image.Image) -> str:
"""将 PIL Image 转换为 base64 编码字符串"""
buffered = BytesIO()
# 转换为 RGB 模式(如果不是的话)
if image.mode != "RGB":
image = image.convert("RGB")
image.save(buffered, format="JPEG")
# 确保使用 UTF-8 编码解码 base64
img_str = base64.b64encode(buffered.getvalue()).decode('utf-8')
return img_str
async def call_openrouter_api(image_base64: str, user_message: str) -> str:
"""调用 OpenRouter API"""
headers = {
"Authorization": f"Bearer {OPENROUTER_API_KEY}",
"Content-Type": "application/json",
# "HTTP-Referer": "https://github.com/your-repo", # 可选:用于跟踪
# "X-Title": "Pest Disease Identification Service", # 可选:应用名称(使用英文避免编码问题)
}
payload = {
"model": MODEL_NAME,
"messages": [
{
"role": "system",
"content": "你是一位专业的农业病虫害识别专家。请仔细分析用户提供的植物图片识别可能存在的病虫害问题并提供详细的诊断信息包括1. 病虫害类型和名称 2. 严重程度 3. 可能的病因 4. 防治建议。请用中文回答。"
},
{
"role": "user",
"content": [
{
"type": "image_url",
"image_url": {
"url": f"data:image/jpeg;base64,{image_base64}"
}
},
{
"type": "text",
"text": user_message
}
]
}
],
"max_tokens": 2000,
"temperature": 0.7,
}
async with httpx.AsyncClient(timeout=60.0) as client:
try:
response = await client.post(OPENROUTER_API_URL, json=payload, headers=headers)
response.raise_for_status()
result = response.json()
if "choices" in result and len(result["choices"]) > 0:
return result["choices"][0]["message"]["content"]
else:
raise HTTPException(status_code=500, detail="API 响应格式错误")
except httpx.HTTPStatusError as e:
error_text = e.response.text
try:
error_text.encode('utf-8')
except (UnicodeEncodeError, AttributeError):
error_text = repr(e.response.text)
raise HTTPException(status_code=e.response.status_code, detail=f"OpenRouter API 错误: {error_text}")
except httpx.RequestError as e:
error_msg = str(e)
try:
error_msg.encode('utf-8')
except UnicodeEncodeError:
error_msg = repr(e)
raise HTTPException(status_code=500, detail=f"请求错误: {error_msg}")
@app.get("/")
async def root():
"""根路径,返回 API 信息"""
return {
"message": "病虫害识别 API",
"version": "1.0.0",
"model": MODEL_NAME,
"endpoints": {
"识别病虫害": "/api/v1/identify",
"健康检查": "/health"
}
}
@app.get("/health")
async def health_check():
"""健康检查端点"""
return {"status": "healthy", "model": MODEL_NAME}
@app.post("/api/v1/identify")
async def identify_pest_disease(
file: UploadFile = File(..., description="植物图片文件"),
question: Optional[str] = None
):
"""
病虫害识别接口
- **file**: 上传的植物图片(支持 jpg, jpeg, png 格式)
- **question**: 可选的自定义问题,如果不提供则使用默认提示
"""
# 验证文件类型
if not file.content_type or not file.content_type.startswith("image/"):
raise HTTPException(status_code=400, detail="请上传图片文件jpg, jpeg, png")
try:
# 读取并验证图片
image_data = await file.read()
image = Image.open(BytesIO(image_data))
# 验证图片大小(可选:限制最大尺寸)
max_size = (4096, 4096)
if image.size[0] > max_size[0] or image.size[1] > max_size[1]:
image.thumbnail(max_size, Image.Resampling.LANCZOS)
# 转换为 base64
image_base64 = encode_image_to_base64(image)
# 构建用户消息
user_message = question or "请识别这张图片中的植物是否存在病虫害问题,如果存在,请详细说明病虫害类型、症状、严重程度和防治建议。"
# 调用 OpenRouter API
result = await call_openrouter_api(image_base64, user_message)
return JSONResponse(content={
"success": True,
"result": result,
"image_info": {
"format": image.format,
"size": image.size,
"mode": image.mode
}
})
except UnicodeEncodeError as e:
raise HTTPException(status_code=500, detail=f"处理图片时出错: 编码错误 - {repr(e)}")
except Exception as e:
error_msg = str(e)
# 确保错误信息可以正确编码
try:
error_msg.encode('utf-8')
except UnicodeEncodeError:
error_msg = repr(e)
raise HTTPException(status_code=500, detail=f"处理图片时出错: {error_msg}")
@app.post("/api/v1/identify/base64")
async def identify_pest_disease_base64(request: Base64ImageRequest):
"""
使用 base64 编码图片的病虫害识别接口
- **image_base64**: base64 编码的图片数据(不包含 data:image 前缀)
- **question**: 可选的自定义问题
"""
try:
# 清理 base64 字符串(移除可能的 data:image 前缀和空白字符)
base64_str = request.image_base64.strip()
if "," in base64_str:
# 如果包含 data:image 前缀,提取 base64 部分
base64_str = base64_str.split(",")[-1]
# 确保 base64 字符串只包含有效的 base64 字符
# 移除所有空白字符(包括换行符)
base64_str = ''.join(base64_str.split())
# 解码 base64 图片
try:
# 确保字符串是有效的 base64 格式
if not base64_str:
raise ValueError("Base64 字符串为空")
image_data = base64.b64decode(base64_str, validate=True)
except binascii.Error as decode_error:
raise ValueError(f"Base64 解码失败: 无效的 base64 格式")
except Exception as decode_error:
error_msg = str(decode_error)
try:
error_msg.encode('utf-8')
except UnicodeEncodeError:
error_msg = repr(decode_error)
raise ValueError(f"Base64 解码失败: {error_msg}")
image = Image.open(BytesIO(image_data))
# 验证图片大小
max_size = (4096, 4096)
if image.size[0] > max_size[0] or image.size[1] > max_size[1]:
image.thumbnail(max_size, Image.Resampling.LANCZOS)
# 重新编码
image_base64_encoded = encode_image_to_base64(image)
# 构建用户消息
user_message = request.question or "请识别这张图片中的植物是否存在病虫害问题,如果存在,请详细说明病虫害类型、症状、严重程度和防治建议。"
# 调用 OpenRouter API
result = await call_openrouter_api(image_base64_encoded, user_message)
return JSONResponse(content={
"success": True,
"result": result,
"image_info": {
"format": image.format,
"size": image.size,
"mode": image.mode
}
})
except UnicodeEncodeError as e:
raise HTTPException(status_code=500, detail=f"处理图片时出错: 编码错误 - {repr(e)}")
except Exception as e:
error_msg = str(e)
# 确保错误信息可以正确编码
try:
error_msg.encode('utf-8')
except UnicodeEncodeError:
error_msg = repr(e)
raise HTTPException(status_code=500, detail=f"处理图片时出错: {error_msg}")
if __name__ == "__main__":
import uvicorn
uvicorn.run(app, host="0.0.0.0", port=8000)

7
env.example Normal file
View File

@@ -0,0 +1,7 @@
# OpenRouter API 密钥
# 从 https://openrouter.ai/keys 获取
OPENROUTER_API_KEY=your_openrouter_api_key_here
# OpenRouter 模型名称(可选,默认为 qwen/qwen3-vl-8b-instruct
# OPENROUTER_MODEL=qwen/qwen3-vl-8b-instruct

14
pyproject.toml Normal file
View File

@@ -0,0 +1,14 @@
[project]
name = "hm-qwen3-vl"
version = "0.1.0"
description = "基于 OpenRouter Qwen3 VL 8B 的病虫害识别 FastAPI 服务"
readme = "README.md"
requires-python = ">=3.8"
dependencies = [
"fastapi>=0.104.0",
"uvicorn[standard]>=0.24.0",
"httpx>=0.25.0",
"python-multipart>=0.0.6",
"pillow>=10.0.0",
"python-dotenv>=1.0.0",
]

1767
uv.lock generated Normal file

File diff suppressed because it is too large Load Diff