init proj
This commit is contained in:
51
.dockerignore
Normal file
51
.dockerignore
Normal 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
38
.gitignore
vendored
Normal 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
1
.python-version
Normal file
@@ -0,0 +1 @@
|
||||
3.8
|
||||
32
Dockerfile
Normal file
32
Dockerfile
Normal 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
179
README.md
@@ -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
266
app.py
Normal 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
7
env.example
Normal 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
14
pyproject.toml
Normal 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",
|
||||
]
|
||||
Reference in New Issue
Block a user