From ec7c9f8dbecbc05f09c9985524d2bea31d57d8ae Mon Sep 17 00:00:00 2001 From: zhenghu <1831829219@qq.com> Date: Tue, 14 Apr 2026 16:24:04 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=88=9D=E5=A7=8B=E5=8C=96=E7=97=85?= =?UTF-8?q?=E8=99=AB=E5=AE=B3=E4=BB=A5=E5=9B=BE=E6=90=9C=E5=9B=BE=E5=BA=94?= =?UTF-8?q?=E7=94=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 基于 CLIP 模型实现图片相似度搜索(app.py / main.py) - 新增 Streamlit 可视化交互界面 - 新增 pyproject.toml、justfile、Dockerfile 项目配置 - 补充完整 README 文档(功能介绍、快速开始、Docker 部署) - 新增 .gitignore --- .gitignore | 26 +++ Dockerfile | 44 ++++ README.md | 81 ++++++- app.py | 599 +++++++++++++++++++++++++++++++++++++++++++++++++ justfile | 30 +++ main.py | 7 + pyproject.toml | 20 ++ 7 files changed, 805 insertions(+), 2 deletions(-) create mode 100644 .gitignore create mode 100644 Dockerfile create mode 100644 app.py create mode 100644 justfile create mode 100644 main.py create mode 100644 pyproject.toml diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5dccf25 --- /dev/null +++ b/.gitignore @@ -0,0 +1,26 @@ +# Python +__pycache__/ +*.py[cod] +*.egg-info/ +dist/ +build/ + +# Virtual environment +.venv/ + +# IDE +.idea/ +.vscode/ +*.swp +*.swo + +# Streamlit +.streamlit_cache/ + +# Ruff +.ruff_cache/ + +# OS +.DS_Store +Thumbs.db +/.doc/ diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..6940273 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,44 @@ +# 使用 uv 官方镜像作为基础镜像(已包含 Python 3.14 和 uv) +FROM 172.16.102.3:30648/astral-sh/uv:python3.14-bookworm + +# 设置工作目录 +WORKDIR /app + +# 配置 apt 使用阿里云镜像源 +RUN sed -i 's/httpredir.debian.org/mirrors.aliyun.com/g' /etc/apt/sources.list.d/debian.sources + +# 安装系统依赖 +RUN apt-get update && apt-get install -y --no-install-recommends \ + curl \ + && rm -rf /var/lib/apt/lists/* + +# 复制项目配置文件和锁定文件 +COPY pyproject.toml justfile uv.lock ./ + +# 配置 uv 使用阿里云镜像源(通过环境变量) +ENV UV_INDEX_URL=https://mirrors.aliyun.com/pypi/simple/ \ + UV_HTTP_TIMEOUT=300 + +# 安装 Python 依赖(使用 uv,锁定版本) +RUN uv sync --frozen --no-dev + +# 复制应用代码和其他文件 +COPY . . + +# 暴露端口 +EXPOSE 8000 + +# 设置环境变量 +ENV STREAMLIT_SERVER_PORT=8000 \ + STREAMLIT_SERVER_ADDRESS=0.0.0.0 \ + STREAMLIT_SERVER_ENABLE_XSRF_PROTECTION=false \ + STREAMLIT_SERVER_ENABLE_CORS=false \ + STREAMLIT_SERVER_HEADLESS=true \ + STREAMLIT_BROWSER_GATHER_USAGE_STATS=false + +# 健康检查 +HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \ + CMD curl -f http://localhost:8000/_stcore/health || exit 1 + +# 运行 Streamlit 应用 +CMD ["uv", "run", "streamlit", "run", "app.py", "--server.port=8000", "--server.address=0.0.0.0"] diff --git a/README.md b/README.md index 64153a3..f8e3c0f 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,80 @@ -# pest-image-search +# pest-image-search 病虫害以图搜图 -病虫害以图搜图 \ No newline at end of file +基于 CLIP 视觉模型的病虫害图片相似度搜索应用。上传病虫害患处图片,系统自动提取视觉特征并检索知识库中最相似的病虫害类型,提供症状描述与防治建议。 + +## 功能特性 + +- 🖼️ 支持上传本地图片、输入图片 URL、选择示例图片三种查询方式 +- 🧠 基于 `openai/clip-vit-base-patch32` 本地视觉模型提取图像特征 +- 📊 相似度可视化条形图 +- 🏷️ 覆盖水稻、小麦、玉米、大豆、番茄、黄瓜等常见作物的病虫害知识库 +- 💡 智能推荐最可能的病虫害及防治方案 + +## 技术栈 + +- Python 3.14+ +- Streamlit 1.52.1 +- Plotly 6.5.0 +- Transformers 4.51.3 + PyTorch 2.7.0 (CLIP 模型) +- Pillow、NumPy、Requests + +## 快速开始 + +### 使用 uv(推荐) + +```bash +# 安装依赖 +uv sync + +# 运行应用 +uv run streamlit run app.py +``` + +### 使用 just + +```bash +# 查看所有可用命令 +just --list + +# 运行应用 +just run + +# 代码格式化 +just format + +# 代码检查 +just check +``` + +## Docker 部署 + +```bash +# 构建镜像 +docker build -t pest-image-search . + +# 运行容器 +docker run -p 8000:8000 pest-image-search +``` + +## 项目结构 + +``` +pest-image-search/ +├── app.py # 主应用文件(Streamlit) +├── main.py # 入口文件 +├── pyproject.toml # 项目配置 +├── justfile # 任务自动化 +├── Dockerfile # Docker 配置 +└── README.md # 项目文档 +``` + +## 使用说明 + +1. 首次启动时会自动下载 CLIP 模型(约 300MB),请保持网络畅通 +2. 加载完成后自动构建病虫害图片索引 +3. 上传或选择查询图片后点击「开始搜索」,即可获得 Top-K 相似病虫害结果 +4. 结果仅供参考,实际防治请结合田间情况或咨询农业专家 + +## 许可证 + +MIT License diff --git a/app.py b/app.py new file mode 100644 index 0000000..ca503ee --- /dev/null +++ b/app.py @@ -0,0 +1,599 @@ +""" +病虫害以图搜图 +基于 CLIP 本地模型的图片 Embedding 相似度搜索 +""" + +from __future__ import annotations + +import io +import os +from dataclasses import dataclass +from typing import Literal + +import numpy as np +import plotly.graph_objects as go +import requests +import streamlit as st +from PIL import Image +from transformers import CLIPModel, CLIPProcessor + +# ─── Page Config ──────────────────────────────────────────────────────────── +st.set_page_config( + page_title="病虫害以图搜图", + page_icon="🌿", + layout="wide", + initial_sidebar_state="expanded", +) + +# ─── Custom CSS ────────────────────────────────────────────────────────────── +st.markdown(""" + +""", unsafe_allow_html=True) + + +# ─── Knowledge Base ────────────────────────────────────────────────────────── +@dataclass(frozen=True) +class PestItem: + name: str + url: str + symptoms: str + treatment: str + crop: str + category: Literal["病害", "虫害"] + + +PEST_KNOWLEDGE: list[PestItem] = [ + PestItem( + name="水稻稻瘟病", + url="https://minio.dev.maimaiag.com/crop-prod-bucket/field_photo/20260410_151854_dc9667cf_%E6%B0%B4%E7%A8%BB%E7%A8%BB%E7%98%9F%E7%97%851.jpeg", + symptoms="叶片出现梭形或纺锤形病斑,中央灰白色,边缘褐色,严重时病斑连片导致叶片枯死", + treatment="选用抗病品种,合理施肥避免偏施氮肥,发病初期喷施三环唑或稻瘟灵", + crop="水稻", + category="病害", + ), + PestItem( + name="水稻纹枯病", + url="https://minio.dev.maimaiag.com/crop-prod-bucket/field_photo/20260410_152022_9f3124ab_%E6%B0%B4%E7%A8%BB%E7%BA%B9%E6%9E%AF%E7%97%851.jpeg", + symptoms="叶鞘和叶片上出现云纹状灰绿色至灰褐色病斑,后期病斑边缘褐色、中央灰白色", + treatment="合理密植,科学管水,发病初期喷施井冈霉素或噻呋酰胺", + crop="水稻", + category="病害", + ), + PestItem( + name="水稻胡麻叶斑病", + url="https://minio.dev.maimaiag.com/crop-prod-bucket/field_photo/20260410_151936_41fdb1dc_%E6%B0%B4%E7%A8%BB%E8%83%A1%E9%BA%BB%E5%8F%B6%E6%96%91%E7%97%851.jpeg", + symptoms="叶片上出现暗褐色芝麻粒大小的椭圆形病斑,病斑周围有黄色晕圈", + treatment="增施硅肥和钾肥提高抗病力,喷施丙环唑或咪鲜胺防治", + crop="水稻", + category="病害", + ), + PestItem( + name="小麦锈病", + url="https://minio.dev.maimaiag.com/crop-prod-bucket/field_photo/20260410_153814_3e175ca3_%E5%B0%8F%E9%BA%A6%E9%94%88%E7%97%851.jpeg", + symptoms="叶片和叶鞘上出现铁锈色粉状疱疹(夏孢子堆),后期变为黑色冬孢子堆", + treatment="种植抗锈品种,发病初期喷施三唑酮或烯唑醇,注意轮作", + crop="小麦", + category="病害", + ), + PestItem( + name="小麦赤霉病", + url="https://minio.dev.maimaiag.com/crop-prod-bucket/field_photo/20260410_152112_2e1f530e_%E5%B0%8F%E9%BA%A6%E8%B5%A4%E9%9C%89%E7%97%851.jpeg", + symptoms="穗部小穗发病,颖壳上出现水浸状褐色斑,后期产生粉红色霉层", + treatment="选用抗病品种,齐穗至扬花初期喷施多菌灵或戊唑醇", + crop="小麦", + category="病害", + ), + PestItem( + name="玉米大斑病", + url="https://minio.dev.maimaiag.com/crop-prod-bucket/field_photo/20260410_153911_ee5a72be_%E7%8E%89%E7%B1%B3%E5%A4%A7%E6%96%91%E7%97%851.jpeg", + symptoms="叶片上出现灰绿色水浸状斑点,扩展为长梭形灰褐色大型病斑", + treatment="种植抗病品种,适时早播,发病初期喷施多菌灵或代森锰锌", + crop="玉米", + category="病害", + ), + PestItem( + name="玉米小斑病", + url="https://minio.dev.maimaiag.com/crop-prod-bucket/field_photo/20260410_154001_e31a0103_%E7%8E%89%E7%B1%B3%E5%B0%8F%E6%96%91%E7%97%851.jpeg", + symptoms="叶片上出现椭圆形黄褐色小病斑,有2-3圈同心轮纹,边缘紫褐色", + treatment="轮作倒茬,清除病残体,喷施百菌清或甲基托布津", + crop="玉米", + category="病害", + ), + PestItem( + name="玉米螟", + url="https://minio.dev.maimaiag.com/crop-prod-bucket/field_photo/20260410_153938_8be05006_%E7%8E%89%E7%B1%B3%E8%9E%9F1.jpeg", + symptoms="幼虫蛀食茎秆和穗轴,茎秆上有蛀孔,孔口有虫粪,造成茎秆折断", + treatment="心叶期撒施白僵菌颗粒剂,释放赤眼蜂生物防治,大喇叭口期灌心", + crop="玉米", + category="虫害", + ), + PestItem( + name="稻飞虱", + url="https://minio.dev.maimaiag.com/crop-prod-bucket/field_photo/20260410_151643_db5e1d36_%E7%A8%BB%E9%A3%9E%E8%99%AB1.jpeg", + symptoms="稻株基部聚集大量褐色或白色小型飞虫,受害稻株发黄矮缩,严重时枯死倒伏", + treatment="合理施肥避免贪青晚熟,选用吡蚜酮或烯啶虫胺防治,保护利用天敌", + crop="水稻", + category="虫害", + ), + PestItem( + name="大豆蚜虫", + url="https://minio.dev.maimaiag.com/crop-prod-bucket/field_photo/20260410_151549_d9cf327b_%E5%A4%A7%E8%B1%86%E8%9A%9C%E8%99%AB1.jpeg", + symptoms="嫩叶和茎尖聚集大量绿色或黄色蚜虫,叶片卷缩变形,植株矮化", + treatment="保护瓢虫等天敌,百株蚜量达1000头时喷施吡虫啉或啶虫脒", + crop="大豆", + category="虫害", + ), + PestItem( + name="番茄晚疫病", + url="https://minio.dev.maimaiag.com/crop-prod-bucket/field_photo/20260410_151705_3dd8baab_%E7%95%AA%E8%8C%84%E6%99%9A%E7%96%AB%E7%97%851.jpeg", + symptoms="叶片出现水浸状暗绿色不规则病斑,潮湿时叶背面产生白色霉层,果实变褐硬化", + treatment="控制温湿度,及时通风降湿,发病初期喷施甲霜灵锰锌或霜脲氰", + crop="番茄", + category="病害", + ), + PestItem( + name="黄瓜霜霉病", + url="https://minio.dev.maimaiag.com/crop-prod-bucket/field_photo/20260410_151804_7be515fa_%E9%BB%84%E7%93%9C%E9%9C%9C%E9%9C%89%E7%97%851.jpeg", + symptoms="叶片正面出现黄色多角形病斑,叶背面潮湿时产生灰黑色霉层", + treatment="选用抗病品种,膜下滴灌降低湿度,喷施百菌清或霜霉威盐酸盐", + crop="黄瓜", + category="病害", + ), +] + +EXAMPLE_IMAGES: list[tuple[str, str]] = [ + ( + "水稻稻瘟病", + "https://minio.dev.maimaiag.com/crop-prod-bucket/field_photo/20260410_151914_4f5b8fef_%E6%B0%B4%E7%A8%BB%E7%A8%BB%E7%98%9F%E7%97%852.jpeg", + ), + ( + "番茄晚疫病", + "https://minio.dev.maimaiag.com/crop-prod-bucket/field_photo/20260410_151726_a8f31320_%E7%95%AA%E8%8C%84%E6%99%9A%E7%96%AB%E7%97%852.jpeg", + ), + ( + "小麦锈病", + "https://minio.dev.maimaiag.com/crop-prod-bucket/field_photo/20260410_153837_e8ae9f43_%E5%B0%8F%E9%BA%A6%E9%94%88%E7%97%852.jpeg", + ), + ( + "水稻纹枯病", + "https://minio.dev.maimaiag.com/crop-prod-bucket/field_photo/20260410_152050_77d568b1_%E6%B0%B4%E7%A8%BB%E7%BA%B9%E6%9E%AF%E7%97%852.jpeg", + ), +] + + +# ─── CLIP Embedder ─────────────────────────────────────────────────────────── +class CLIPEmbedder: + MODEL_NAME = "openai/clip-vit-base-patch32" + + def __init__(self) -> None: + self._processor: CLIPProcessor | None = None + self._model: CLIPModel | None = None + + def _load(self) -> tuple[CLIPProcessor, CLIPModel]: + if self._processor is None or self._model is None: + with st.spinner("首次启动正在加载 CLIP 模型,请稍候..."): + self._processor = CLIPProcessor.from_pretrained(self.MODEL_NAME) + self._model = CLIPModel.from_pretrained(self.MODEL_NAME) + return self._processor, self._model + + def embed(self, image: Image.Image) -> np.ndarray: + processor, model = self._load() + inputs = processor(images=image, return_tensors="pt") + image_features = model.get_image_features(**inputs) + vec = image_features.detach().cpu().numpy().flatten() + norm = np.linalg.norm(vec) + if norm == 0: + return vec + return vec / norm + + +@st.cache_resource(show_spinner=False) +def get_embedder() -> CLIPEmbedder: + return CLIPEmbedder() + + +# ─── Utilities ─────────────────────────────────────────────────────────────── +def load_image(source: str | io.BytesIO) -> Image.Image | None: + try: + if isinstance(source, str): + resp = requests.get(source, timeout=30) + resp.raise_for_status() + return Image.open(io.BytesIO(resp.content)).convert("RGB") + return Image.open(source).convert("RGB") + except Exception as e: + st.error(f"图片加载失败: {e}") + return None + + +def cosine_similarity(a: np.ndarray, b: np.ndarray) -> float: + return float(np.dot(a, b)) + + +@st.cache_data(show_spinner=False) +def build_index() -> tuple[list[dict], list[str], list[str]]: + embedder = get_embedder() + items, succeeded, failed = [], [], [] + progress = st.progress(0, text="正在构建病虫害图片索引...") + total = len(PEST_KNOWLEDGE) + for i, pest in enumerate(PEST_KNOWLEDGE): + img = load_image(pest.url) + if img is None: + failed.append(pest.name) + progress.progress((i + 1) / total, text=f"索引构建中 ({i + 1}/{total})...") + continue + try: + embedding = embedder.embed(img) + items.append({ + "name": pest.name, + "url": pest.url, + "embedding": embedding, + "symptoms": pest.symptoms, + "treatment": pest.treatment, + "crop": pest.crop, + "category": pest.category, + }) + succeeded.append(pest.name) + except Exception: + failed.append(pest.name) + progress.progress((i + 1) / total, text=f"索引构建中 ({i + 1}/{total})...") + progress.empty() + return items, succeeded, failed + + +# ─── Sidebar ───────────────────────────────────────────────────────────────── +with st.sidebar: + st.markdown('
', unsafe_allow_html=True) + st.markdown('', unsafe_allow_html=True) + st.markdown("