From db5d203a7c51373f13d5bded5792145b386dcc9c Mon Sep 17 00:00:00 2001 From: Cloud Bai <46363139+cloudybai@users.noreply.github.com> Date: Wed, 3 Dec 2025 23:08:01 +0800 Subject: [PATCH] Add files via upload --- API/Dockerfile | 99 ++ API/README.md | 320 +++++ API/app.py | 11 + API/client_example.py | 215 +++ API/config.json | 12 + API/faiss_image_similarity.py | 694 +++++++++ API/faiss_service.py | 478 +++++++ API/faiss_service_ui.py | 1253 +++++++++++++++++ API/hetong_ui.html | 1041 ++++++++++++++ API/imagesim_ui_page1.html | 630 +++++++++ API/imagesim_ui_page2_1.html | 2498 +++++++++++++++++++++++++++++++++ API/imagesim_ui_page2_2.html | 210 +++ API/nginx.conf | 92 ++ API/requirements.txt | 18 + 14 files changed, 7571 insertions(+) create mode 100644 API/Dockerfile create mode 100644 API/README.md create mode 100644 API/app.py create mode 100644 API/client_example.py create mode 100644 API/config.json create mode 100644 API/faiss_image_similarity.py create mode 100644 API/faiss_service.py create mode 100644 API/faiss_service_ui.py create mode 100644 API/hetong_ui.html create mode 100644 API/imagesim_ui_page1.html create mode 100644 API/imagesim_ui_page2_1.html create mode 100644 API/imagesim_ui_page2_2.html create mode 100644 API/nginx.conf create mode 100644 API/requirements.txt diff --git a/API/Dockerfile b/API/Dockerfile new file mode 100644 index 0000000..7725988 --- /dev/null +++ b/API/Dockerfile @@ -0,0 +1,99 @@ +FROM ubuntu:latest +LABEL authors="cloudbai" + +ENTRYPOINT ["top", "-b"] + +# Dockerfile +FROM python:3.9-slim + +# 设置工作目录 +WORKDIR /app + +# 安装系统依赖 +RUN apt-get update && apt-get install -y \ + libgl1-mesa-glx \ + libglib2.0-0 \ + libsm6 \ + libxext6 \ + libxrender-dev \ + libgomp1 \ + git \ + && rm -rf /var/lib/apt/lists/* + +# 复制依赖文件 +COPY requirements.txt . + +# 安装Python依赖 +RUN pip install --no-cache-dir -r requirements.txt + +# 复制应用代码 +COPY . . + +# 创建必要的目录 +RUN mkdir -p uploads indices cache + +# 设置环境变量 +ENV PYTHONPATH=/app +ENV FLASK_APP=faiss_service.py + +# 暴露端口 +EXPOSE 8080 + +# 健康检查 +HEALTHCHECK --interval=30s --timeout=30s --start-period=5s --retries=3 \ + CMD curl -f http://localhost:8080/api/v1/health || exit 1 + +# 启动命令 +CMD ["python", "faiss_service.py", "--config", "config.json"] + +# ========================================== +# docker-compose.yml +# ========================================== + +version: '3.8' + +services: + faiss-image-service: + build: . + ports: + - "8080:8080" + volumes: + - ./data:/app/data + - ./indices:/app/indices + - ./cache:/app/cache + - ./uploads:/app/uploads + - ./config.json:/app/config.json + environment: + - PYTHONPATH=/app + restart: unless-stopped + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:8080/api/v1/health"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 40s + + # 可选:添加Redis用于缓存 + redis: + image: redis:7-alpine + ports: + - "6379:6379" + volumes: + - redis_data:/data + restart: unless-stopped + + # 可选:添加Nginx反向代理 + nginx: + image: nginx:alpine + ports: + - "80:80" + - "443:443" + volumes: + - ./nginx.conf:/etc/nginx/nginx.conf + - ./ssl:/etc/nginx/ssl + depends_on: + - faiss-image-service + restart: unless-stopped + +volumes: + redis_data: \ No newline at end of file diff --git a/API/README.md b/API/README.md new file mode 100644 index 0000000..3f06c82 --- /dev/null +++ b/API/README.md @@ -0,0 +1,320 @@ +# FAISS图像相似度检测服务 + +基于原始 [similarities](https://github.com/cloudybai/similarities) 项目封装的RESTful API服务,提供高性能的图像相似度检测功能。 + +## 功能特性 + +- 🚀 **高性能检索**: 基于FAISS向量搜索引擎,支持百万级图像库实时检索 +- 🧠 **多模态特征**: 融合ResNet-50、Vision Transformer和传统CV特征 +- 🔧 **RESTful API**: 标准化的Web API接口,易于集成 +- 📦 **Docker部署**: 开箱即用的容器化部署方案 +- 🎯 **高精度匹配**: 检索精度可达90%以上 +- ⚡ **异步处理**: 支持后台异步索引构建 + +## 系统架构 + +``` +┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ +│ Web Client │ │ Mobile App │ │ Other Services │ +└─────────┬───────┘ └─────────┬───────┘ └─────────┬───────┘ + │ │ │ + └──────────────────────┼──────────────────────┘ + │ + ┌─────────────▼──────────────┐ + │ Nginx (Optional) │ + └─────────────┬──────────────┘ + │ + ┌─────────────▼──────────────┐ + │ FAISS Image Service │ + │ - Flask REST API │ + │ - Multi-modal Features │ + │ - FAISS Index │ + └─────────────┬──────────────┘ + │ + ┌─────────────▼──────────────┐ + │ File Storage │ + │ - Images │ + │ - Indices │ + │ - Cache │ + └────────────────────────────┘ +``` + +## 快速开始 + +### 方式1: Docker部署(推荐) + +1. **克隆项目** +```bash +git clone https://github.com/cloudybai/similarities.git +cd similarities +``` + +2. **准备配置文件** +```bash +# 将配置文件复制到项目根目录 +cp config.json.example config.json +# 根据需要修改配置 +``` + +3. **启动服务** +```bash +# 构建并启动 +docker-compose up -d + +# 查看日志 +docker-compose logs -f faiss-image-service +``` + +4. **验证服务** +```bash +curl http://localhost:8080/api/v1/health +``` + +### 方式2: 本地部署 + +1. **安装依赖** +```bash +pip install -r requirements.txt +``` + +2. **准备原始检测器** +```bash +# 确保faiss_image_similarity.py在同一目录 +``` + +3. **启动服务** +```bash +python faiss_service.py --config config.json +``` + +## API使用说明 + +### 1. 健康检查 +```bash +GET /api/v1/health +``` + +### 2. 构建索引 +```bash +POST /api/v1/build_index +Content-Type: application/json + +{ + "index_name": "my_index", + "image_directory": "/path/to/images", + "model_config": { + "enable_resnet": true, + "enable_vit": true, + "enable_traditional": true, + "index_type": "flat", + "use_gpu": false + }, + "cache_file": "/path/to/cache.pkl" +} +``` + +### 3. 搜索相似图片 +```bash +POST /api/v1/search +Content-Type: multipart/form-data + +form-data: +- image: [图片文件] +- index_name: "my_index" +- top_k: 10 +- threshold: 0.5 +``` + +### 4. 获取服务状态 +```bash +GET /api/v1/status +``` + +### 5. 列出所有索引 +```bash +GET /api/v1/indices +``` + +### 6. 删除索引 +```bash +DELETE /api/v1/indices/{index_name} +``` + +## Python客户端使用 + +```python +from client_example import FAISSImageClient + +# 创建客户端 +client = FAISSImageClient("http://localhost:8080") + +# 构建索引 +result = client.build_index( + index_name="test_index", + image_directory="/path/to/images" +) + +# 等待索引构建完成 +client.wait_for_index_ready("test_index") + +# 搜索相似图片 +results = client.search_similar( + image_path="/path/to/query.jpg", + index_name="test_index", + top_k=5, + threshold=0.7 +) + +print(f"找到 {len(results['results'])} 张相似图片") +``` + +## 配置说明 + +### config.json 参数详解 + +```json +{ + "host": "0.0.0.0", // 服务绑定地址 + "port": 8080, // 服务端口 + "debug": false, // 调试模式 + "max_file_size": 16777216, // 最大文件大小(16MB) + "upload_folder": "./uploads", // 上传临时目录 + "index_folder": "./indices", // 索引存储目录 + "cache_folder": "./cache", // 缓存目录 + "allowed_extensions": [ // 支持的图片格式 + "jpg", "jpeg", "png", "bmp", "gif", "tiff", "webp" + ], + "enable_cors": true, // 启用CORS + "log_level": "INFO" // 日志级别 +} +``` + +### 模型配置参数 + +```json +{ + "enable_resnet": true, // 启用ResNet特征 (权重0.3) + "enable_vit": true, // 启用ViT特征 (权重0.5) + "enable_traditional": true, // 启用传统CV特征 (权重0.2) + "index_type": "flat", // FAISS索引类型 + "use_gpu": false // 是否使用GPU加速 +} +``` + +## 性能调优 + +### 1. 相似度阈值设置 +- **高精度场景**: threshold >= 0.85 +- **高召回场景**: threshold >= 0.65 +- **探索性检索**: threshold >= 0.45 + +### 2. 硬件优化 +- **CPU**: 推荐8核以上,支持AVX2指令集 +- **内存**: 建议16GB以上,约4KB/张图片 +- **GPU**: 支持CUDA的NVIDIA显卡(可选) + +### 3. 索引类型选择 +- **Flat索引**: 精度最高,适合中小规模数据集 +- **IVF索引**: 速度较快,适合大规模数据集 +- **HNSW索引**: 内存效率高,适合内存受限环境 + +## 生产环境部署 + +### 使用Nginx + Docker Compose + +```yaml +# docker-compose.prod.yml +version: '3.8' +services: + faiss-service: + build: . + volumes: + - /data/images:/app/data:ro + - ./indices:/app/indices + - ./cache:/app/cache + environment: + - WORKERS=4 + restart: always + + nginx: + image: nginx:alpine + ports: + - "80:80" + - "443:443" + volumes: + - ./nginx.conf:/etc/nginx/nginx.conf + - ./ssl:/etc/nginx/ssl + depends_on: + - faiss-service + restart: always +``` + +### 监控和日志 + +```bash +# 查看服务状态 +curl http://localhost:8080/api/v1/status + +# 查看容器日志 +docker-compose logs -f faiss-image-service + +# 监控资源使用 +docker stats faiss-image-service +``` + +## 故障排除 + +### 常见问题 + +1. **内存不足** + - 减少图片数量或降低特征维度 + - 增加系统内存或使用内存映射 + +2. **索引构建失败** + - 检查图片目录权限 + - 确保图片格式支持 + - 查看详细错误日志 + +3. **搜索速度慢** + - 考虑使用GPU加速 + - 调整FAISS索引类型 + - 增加系统资源 + +4. **特征提取失败** + - 检查深度学习模型是否正确加载 + - 确保图片文件完整性 + - 验证依赖库版本 + +### 调试模式 + +```bash +# 启用调试模式 +python faiss_service.py --debug --config config.json + +# 查看详细日志 +export LOG_LEVEL=DEBUG +python faiss_service.py --config config.json +``` + +## 贡献指南 + +1. Fork项目仓库 +2. 创建功能分支: `git checkout -b feature/new-feature` +3. 提交更改: `git commit -am 'Add new feature'` +4. 推送分支: `git push origin feature/new-feature` +5. 提交Pull Request + +## 许可证 + +本项目基于MIT许可证开源,详见 [LICENSE](LICENSE) 文件。 + +## 技术支持 + +- 📧 邮箱: cloud.bai@outlook.com +- 🔗 项目主页: https://github.com/cloudybai/similarities +- 📖 技术文档: https://github.com/cloudybai/similarities/wiki + +--- + +**版本**: v2.0.0 +**最后更新**: 2025年7月 \ No newline at end of file diff --git a/API/app.py b/API/app.py new file mode 100644 index 0000000..de3f6d0 --- /dev/null +++ b/API/app.py @@ -0,0 +1,11 @@ +from flask import Flask + +app = Flask(__name__) + +@app.route('/') +def home(): + return 'Welcome to the Home Page!' + +@app.route('/about') +def about(): + return 'This is the About Page.' \ No newline at end of file diff --git a/API/client_example.py b/API/client_example.py new file mode 100644 index 0000000..49f3f52 --- /dev/null +++ b/API/client_example.py @@ -0,0 +1,215 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +FAISS图像相似度检测服务客户端示例 +展示如何使用API进行图像索引构建和相似图片搜索 +""" + +import requests +import json +import time +import os +from typing import Dict, List, Optional + + +class FAISSImageClient: + """FAISS图像相似度检测服务客户端""" + + def __init__(self, base_url: str = "http://localhost:8080"): + self.base_url = base_url.rstrip('/') + self.api_base = f"{self.base_url}/api/v1" + + def health_check(self) -> Dict: + """健康检查""" + try: + response = requests.get(f"{self.api_base}/health") + response.raise_for_status() + return response.json() + except requests.RequestException as e: + return {"error": str(e)} + + def get_status(self) -> Dict: + """获取服务状态""" + try: + response = requests.get(f"{self.api_base}/status") + response.raise_for_status() + return response.json() + except requests.RequestException as e: + return {"error": str(e)} + + def build_index(self, + index_name: str, + image_directory: str, + model_config: Optional[Dict] = None, + cache_file: Optional[str] = None) -> Dict: + """构建索引""" + if model_config is None: + model_config = { + 'enable_resnet': True, + 'enable_vit': True, + 'enable_traditional': True, + 'index_type': 'flat', + 'use_gpu': False + } + + data = { + 'index_name': index_name, + 'image_directory': image_directory, + 'model_config': model_config + } + + if cache_file: + data['cache_file'] = cache_file + + try: + response = requests.post( + f"{self.api_base}/build_index", + json=data, + headers={'Content-Type': 'application/json'} + ) + response.raise_for_status() + return response.json() + except requests.RequestException as e: + return {"error": str(e)} + + def search_similar(self, + image_path: str, + index_name: str = "default", + top_k: int = 10, + threshold: float = 0.5) -> Dict: + """搜索相似图片""" + if not os.path.exists(image_path): + return {"error": f"图片文件不存在: {image_path}"} + + try: + with open(image_path, 'rb') as f: + files = {'image': f} + data = { + 'index_name': index_name, + 'top_k': str(top_k), + 'threshold': str(threshold) + } + + response = requests.post( + f"{self.api_base}/search", + files=files, + data=data + ) + response.raise_for_status() + return response.json() + except requests.RequestException as e: + return {"error": str(e)} + + def list_indices(self) -> Dict: + """列出所有索引""" + try: + response = requests.get(f"{self.api_base}/indices") + response.raise_for_status() + return response.json() + except requests.RequestException as e: + return {"error": str(e)} + + def delete_index(self, index_name: str) -> Dict: + """删除索引""" + try: + response = requests.delete(f"{self.api_base}/indices/{index_name}") + response.raise_for_status() + return response.json() + except requests.RequestException as e: + return {"error": str(e)} + + def wait_for_index_ready(self, index_name: str, max_wait: int = 3600) -> bool: + """等待索引构建完成""" + start_time = time.time() + + while time.time() - start_time < max_wait: + status = self.get_status() + if "error" in status: + print(f"获取状态失败: {status['error']}") + return False + + if index_name in status.get('indices', {}): + index_status = status['indices'][index_name]['status'] + + if index_status == 'ready': + print(f"索引 {index_name} 构建完成") + return True + elif index_status == 'error': + print(f"索引 {index_name} 构建失败") + return False + elif index_status == 'building': + print(f"索引 {index_name} 正在构建中...") + + time.sleep(10) # 等待10秒后重新检查 + + print(f"等待索引构建超时: {index_name}") + return False + + +def main(): + """示例使用""" + # 创建客户端 + client = FAISSImageClient("http://localhost:8080") + + print("=" * 60) + print("FAISS图像相似度检测服务客户端示例") + print("=" * 60) + + # 1. 健康检查 + print("\n1. 健康检查:") + health = client.health_check() + print(json.dumps(health, indent=2, ensure_ascii=False)) + + # 2. 获取服务状态 + print("\n2. 服务状态:") + status = client.get_status() + print(json.dumps(status, indent=2, ensure_ascii=False)) + + # 3. 构建索引示例(请修改为实际的图片目录) + image_directory = "/Users/cloudbai/PycharmProjects/imagesim/similarities/examples/data/shanqi4000_test_2" # 修改为实际路径 + + if os.path.exists(image_directory): + print(f"\n3. 构建索引:") + build_result = client.build_index( + index_name="test_index", + image_directory=image_directory, + model_config={ + 'enable_resnet': True, + 'enable_vit': True, + 'enable_traditional': True, + 'index_type': 'flat', + 'use_gpu': False + } + ) + print(json.dumps(build_result, indent=2, ensure_ascii=False)) + + # 等待索引构建完成 + if "error" not in build_result: + print("\n等待索引构建完成...") + if client.wait_for_index_ready("test_index"): + # 4. 搜索相似图片示例 + target_image = "/Users/cloudbai/PycharmProjects/imagesim/similarities/examples/data/shanqi4000_test_2/new17503808046569.jpg" # 修改为实际路径 + + if os.path.exists(target_image): + print(f"\n4. 搜索相似图片:") + search_result = client.search_similar( + image_path=target_image, + index_name="test_index", + top_k=5, + threshold=0.5 + ) + print(json.dumps(search_result, indent=2, ensure_ascii=False)) + else: + print(f"目标图片不存在: {target_image}") + else: + print(f"图片目录不存在: {image_directory}") + + # 5. 列出所有索引 + print(f"\n5. 列出所有索引:") + indices = client.list_indices() + print(json.dumps(indices, indent=2, ensure_ascii=False)) + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/API/config.json b/API/config.json new file mode 100644 index 0000000..6a8b330 --- /dev/null +++ b/API/config.json @@ -0,0 +1,12 @@ +{ + "host": "0.0.0.0", + "port": 8080, + "debug": false, + "max_file_size": 16777216, + "upload_folder": "./uploads", + "index_folder": "./indices", + "cache_folder": "./cache", + "allowed_extensions": ["jpg", "jpeg", "png", "bmp", "gif", "tiff", "webp"], + "enable_cors": true, + "log_level": "INFO" +} \ No newline at end of file diff --git a/API/faiss_image_similarity.py b/API/faiss_image_similarity.py new file mode 100644 index 0000000..94251c3 --- /dev/null +++ b/API/faiss_image_similarity.py @@ -0,0 +1,694 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +@author: Dr Yunpeng Cloud Bai + Data and AI Engineering Research Group + Shaanxi Big Data Group Co. Research Institute + +@description: 基于FAISS的高效图像相似度检测器 + +================================================================================ +项目概述 (Project Overview) +================================================================================ +本系统采用Facebook AI Research开发的FAISS (Facebook AI Similarity Search) 框架, +结合深度学习和传统计算机视觉技术,构建了一个高性能的图像相似度检测系统。 +系统支持百万级图像库的实时检索,检索精度高达90%以上。 + +核心技术栈: +- FAISS: 高效向量相似度搜索引擎 +- ResNet-50: 深度卷积神经网络特征提取 +- Vision Transformer (ViT): 注意力机制特征提取 +- 传统CV特征: 颜色直方图 + LBP纹理特征 +- 多模态特征融合: 加权组合多种特征向量 + +================================================================================ +系统架构 (System Architecture) +================================================================================ +1. 特征提取层 (Feature Extraction Layer) + - ResNet-50特征 (2048维): 权重0.3,擅长捕捉图像结构和语义信息 + - ViT特征 (768维): 权重0.5,具备全局上下文理解能力 + - 传统CV特征 (512维): 权重0.2,提供颜色和纹理的基础描述 + +2. 索引构建层 (Index Building Layer) + - 支持FAISS索引类型: + * Flat索引:向量搜索,精度最高,适合规模数据集 + +3. 检索服务层 (Retrieval Service Layer) + - 实时特征提取和相似度计算 + - 支持阈值过滤和Top-K检索 + - 可配置的多线程处理能力 + +================================================================================ +性能指标 (Performance Metrics) +================================================================================ +- 索引搜索速度: 1000张图片/分钟 (单GPU环境) +- 内存占用: 约4KB/张图片 +- 支持图片格式: JPG, PNG, BMP, GIF, TIFF + +================================================================================ +使用指南 (Usage Guide) +================================================================================ + +步骤1: 构建FAISS索引 (一次性操作) +---------------------------------------- + +python faiss_image_similarity.py --mode build --directory /path/to/images --cache-file features.pkl --index-file image_index.index + +eg: +python faiss_image_similarity.py --mode build --directory /Users/cloudbai/PycharmProjects/imagesim/similarities/examples/data/shanqi4000_test_1 --cache-file features.pkl --index-file image_index.index +python faiss_image_similarity.py --mode build --directory /Users/cloudbai/PycharmProjects/imagesim/similarities/examples/data/shanqi4000_test_2 --cache-file features2.pkl --index-file image_index2.index +python faiss_image_similarity.py --mode build --directory /Users/cloudbai/PycharmProjects/imagesim/similarities/examples/data/shanqi4000_test_3 --cache-file features3.pkl --index-file image_index3.index +python faiss_image_similarity.py --mode build --directory /Users/cloudbai/PycharmProjects/imagesim/similarities/examples/data/shanqi4000_test_4 --cache-file features4.pkl --index-file image_index4.index + +功能说明: +- 遍历指定目录下的所有图像文件 +- 提取每张图像的多模态特征向量 +- 构建FAISS索引并保存到磁盘 +- 生成特征缓存文件,支持增量更新 + +输出文件: +- image_index.index: FAISS索引文件 +- image_index_paths.pkl: 图像路径映射文件 +- features.pkl: 特征向量缓存文件 + +步骤2: 相似图像检索 (快速查询) +---------------------------------------- +python faiss_image_similarity.py --mode search --directory /path/to/images --target /path/to/target.jpg --index-file image_index.index --top-k 10 --threshold 0.5 + + +eg: + +python faiss_image_similarity.py --mode search --directory /Users/cloudbai/PycharmProjects/imagesim/similarities/examples/data/shanqi4000_test_1 --target /Users/cloudbai/PycharmProjects/imagesim/similarities/examples/data/shanqi4000_test_1/new17503489693520.jpg --index-file image_index.index --top-k 10 --threshold 0.5 +python faiss_image_similarity.py --mode search --directory /Users/cloudbai/PycharmProjects/imagesim/similarities/examples/data/shanqi4000_test_2 --target /Users/cloudbai/PycharmProjects/imagesim/similarities/examples/data/shanqi4000_test_2/new17503808046569.jpg --index-file image_index2.index --top-k 10 --threshold 0.5 +python faiss_image_similarity.py --mode search --directory /Users/cloudbai/PycharmProjects/imagesim/similarities/examples/data/shanqi4000_test_3 --target /Users/cloudbai/PycharmProjects/imagesim/similarities/examples/data/shanqi4000_test_3/new17504462374600.jpg --index-file image_index3.index --top-k 10 --threshold 0.5 +python faiss_image_similarity.py --mode search --directory /Users/cloudbai/PycharmProjects/imagesim/similarities/examples/data/shanqi4000_test_4 --target /Users/cloudbai/PycharmProjects/imagesim/similarities/examples/data/shanqi4000_test_4/new17504886913849.jpg --index-file image_index4.index --top-k 10 --threshold 0.5 + + + + +参数说明: +- --target: 查询图像路径 +- --top-k: 返回相似图像数量 (推荐: 10) +- --threshold: 相似度阈值 (推荐: 0.85, 范围: 0.0-1.0) + +================================================================================ +参数配置建议 (Configuration Recommendations) +================================================================================ + +1. 相似度阈值设置: + - 高精度场景: threshold >= 0.85 + - 高召回场景: threshold >= 0.65 + - 探索性检索: threshold >= 0.45 + +2. 索引类型选择: + - 建议使用 flat 索引 + +================================================================================ +技术支持 (Technical Support) +================================================================================ +如遇技术问题,请联系: +- 邮箱: cloud.bai@outlook.com +- 技术文档: https://github.com/cloudybai/similarities + +版本信息: v2.0.0 +最后更新: 2025年7月 +""" + + +import os +import sys +import argparse +import pickle +import time +from typing import List, Tuple, Dict, Optional +import numpy as np +from PIL import Image +import torch +import torch.nn.functional as F +import cv2 +import warnings +import faiss +from tqdm import tqdm + +warnings.filterwarnings('ignore') + +try: + import torchvision.transforms as transforms + import torchvision.models as models + import timm + from sklearn.preprocessing import normalize +except ImportError as e: + print(f"请安装所需库:") + print(f"pip install torch torchvision timm scikit-learn pillow opencv-python faiss-cpu tqdm") + print(f"或者使用GPU版本: pip install faiss-gpu") + print(f"缺失库:{e}") + sys.exit(1) + + +class FAISSImageSimilarityDetector: + """基于FAISS的图像相似度检测器""" + + def __init__(self, + enable_resnet: bool = True, + enable_vit: bool = True, + enable_traditional: bool = True, + index_type: str = 'flat', + use_gpu: bool = False): + """ + 初始化检测器 + + Args: + enable_resnet: 是否启用ResNet特征 + enable_vit: 是否启用ViT特征 + enable_traditional: 是否启用传统CV特征 + index_type: FAISS索引类型 ('flat', 'ivf', 'hnsw') + use_gpu: 是否使用GPU + """ + self.models = {} + self.indices = {} + self.image_paths = [] + self.features_cache = {} + self.index_type = index_type + self.use_gpu = use_gpu + + print("正在初始化FAISS图像相似度检测器...") + + # 检查GPU可用性 + if use_gpu and not torch.cuda.is_available(): + print("警告: 未检测到GPU,将使用CPU") + self.use_gpu = False + + # 初始化特征提取器 + if enable_resnet: + self._init_resnet() + + if enable_vit: + self._init_vit() + + if enable_traditional: + self._init_traditional() + + print("检测器初始化完成") + + def _init_resnet(self): + """初始化ResNet模型""" + try: + print("加载ResNet模型...") + resnet = models.resnet50(pretrained=True) + resnet.fc = torch.nn.Identity() + resnet.eval() + + if self.use_gpu: + resnet = resnet.cuda() + + self.models['resnet'] = resnet + + self.resnet_transform = transforms.Compose([ + transforms.Resize((224, 224)), + transforms.ToTensor(), + transforms.Normalize(mean=[0.485, 0.456, 0.406], + std=[0.229, 0.224, 0.225]) + ]) + print("ResNet模型加载成功") + except Exception as e: + print(f"ResNet模型加载失败: {e}") + + def _init_vit(self): + """初始化Vision Transformer""" + try: + print("加载Vision Transformer...") + vit_model = timm.create_model('vit_base_patch16_224', pretrained=True, num_classes=0) + vit_model.eval() + + if self.use_gpu: + vit_model = vit_model.cuda() + + self.models['vit'] = vit_model + + self.vit_transform = transforms.Compose([ + transforms.Resize((224, 224)), + transforms.ToTensor(), + transforms.Normalize(mean=[0.5, 0.5, 0.5], std=[0.5, 0.5, 0.5]) + ]) + print("ViT模型加载成功") + except Exception as e: + print(f"ViT模型加载失败: {e}") + + def _init_traditional(self): + """初始化传统特征提取器""" + try: + print("初始化传统CV特征提取器...") + self.traditional_enabled = True + print("传统CV特征提取器初始化成功") + except Exception as e: + print(f"传统CV特征初始化失败: {e}") + self.traditional_enabled = False + + def extract_resnet_features(self, image_path: str) -> Optional[np.ndarray]: + """提取ResNet特征""" + if 'resnet' not in self.models: + return None + + try: + image = Image.open(image_path).convert('RGB') + image_tensor = self.resnet_transform(image).unsqueeze(0) + + if self.use_gpu: + image_tensor = image_tensor.cuda() + + with torch.no_grad(): + features = self.models['resnet'](image_tensor) + features = features.cpu().squeeze().numpy() + features = features / np.linalg.norm(features) + + return features + except Exception as e: + print(f"ResNet特征提取失败 {image_path}: {e}") + return None + + def extract_vit_features(self, image_path: str) -> Optional[np.ndarray]: + """提取ViT特征""" + if 'vit' not in self.models: + return None + + try: + image = Image.open(image_path).convert('RGB') + image_tensor = self.vit_transform(image).unsqueeze(0) + + if self.use_gpu: + image_tensor = image_tensor.cuda() + + with torch.no_grad(): + features = self.models['vit'](image_tensor) + features = features.cpu().squeeze().numpy() + features = features / np.linalg.norm(features) + + return features + except Exception as e: + print(f"ViT特征提取失败 {image_path}: {e}") + return None + + def extract_traditional_features(self, image_path: str) -> Optional[np.ndarray]: + """提取传统特征""" + if not hasattr(self, 'traditional_enabled') or not self.traditional_enabled: + return None + + try: + image = cv2.imread(image_path) + if image is None: + return None + + features = [] + + # 颜色直方图 + hist_b = cv2.calcHist([image], [0], None, [32], [0, 256]) + hist_g = cv2.calcHist([image], [1], None, [32], [0, 256]) + hist_r = cv2.calcHist([image], [2], None, [32], [0, 256]) + + features.extend(hist_b.flatten()) + features.extend(hist_g.flatten()) + features.extend(hist_r.flatten()) + + # LBP纹理特征 + gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY) + lbp_hist = self._calculate_lbp_histogram(gray) + features.extend(lbp_hist) + + features = np.array(features) + features = features / np.linalg.norm(features) + + return features + except Exception as e: + print(f"传统特征提取失败 {image_path}: {e}") + return None + + def _calculate_lbp_histogram(self, gray_image: np.ndarray, radius: int = 1, n_points: int = 8) -> List[float]: + """计算LBP直方图""" + try: + h, w = gray_image.shape + lbp = np.zeros((h - 2 * radius, w - 2 * radius), dtype=np.uint8) + + for i in range(radius, h - radius): + for j in range(radius, w - radius): + center = gray_image[i, j] + binary = 0 + for k in range(n_points): + angle = 2 * np.pi * k / n_points + x = int(round(i + radius * np.cos(angle))) + y = int(round(j + radius * np.sin(angle))) + if 0 <= x < h and 0 <= y < w and gray_image[x, y] >= center: + binary += 2 ** k + lbp[i - radius, j - radius] = binary + + hist, _ = np.histogram(lbp.ravel(), bins=2 ** n_points, range=(0, 2 ** n_points)) + hist = hist.astype(float) + hist = hist / (hist.sum() + 1e-7) + + return hist.tolist() + except: + return [0.0] * (2 ** n_points) + + def extract_combined_features(self, image_path: str) -> Optional[np.ndarray]: + """提取组合特征""" + features = [] + + +#可以在这里修改权重!!!!!# + + # ResNet特征 + if 'resnet' in self.models: + resnet_feat = self.extract_resnet_features(image_path) + if resnet_feat is not None: + features.append(resnet_feat * 0.3) # 权重0.3 + + # ViT特征 + if 'vit' in self.models: + vit_feat = self.extract_vit_features(image_path) + if vit_feat is not None: + features.append(vit_feat * 0.5) # 权重0.5 + + # 传统特征 + if hasattr(self, 'traditional_enabled') and self.traditional_enabled: + trad_feat = self.extract_traditional_features(image_path) + if trad_feat is not None: + # 降维到合理大小 + if len(trad_feat) > 512: + trad_feat = trad_feat[:512] + features.append(trad_feat * 0.2) # 权重0.2 + + if not features: + return None + + # 拼接所有特征 + combined_features = np.concatenate(features) + # L2归一化 + combined_features = combined_features / np.linalg.norm(combined_features) + + return combined_features + + def create_faiss_index(self, feature_dim: int) -> faiss.Index: + """创建FAISS索引""" + if self.index_type == 'flat': + # 暴力搜索,精度最高 + index = faiss.IndexFlatIP(feature_dim) # 内积索引 + elif self.index_type == 'ivf': + # 倒排索引,速度快 + nlist = min(100, max(4, int(np.sqrt(len(self.image_paths))))) + quantizer = faiss.IndexFlatIP(feature_dim) + index = faiss.IndexIVFFlat(quantizer, feature_dim, nlist) + elif self.index_type == 'hnsw': + # HNSW图索引,内存效率高 + index = faiss.IndexHNSWFlat(feature_dim, 32) + index.hnsw.efConstruction = 200 + else: + raise ValueError(f"不支持的索引类型: {self.index_type}") + + # 如果使用GPU + if self.use_gpu and faiss.get_num_gpus() > 0: + print("使用GPU加速FAISS索引") + res = faiss.StandardGpuResources() + index = faiss.index_cpu_to_gpu(res, 0, index) + + return index + + def build_index(self, image_directory: str, cache_file: str = None) -> None: + """构建FAISS索引""" + print(f"开始构建索引,目录: {image_directory}") + + # 查找所有图片 + self.image_paths = self.find_images_in_directory(image_directory) + print(f"找到 {len(self.image_paths)} 张图片") + + if not self.image_paths: + print("未找到任何图片文件") + return + + # 检查是否有缓存文件 + if cache_file and os.path.exists(cache_file): + print(f"加载缓存文件: {cache_file}") + with open(cache_file, 'rb') as f: + cache_data = pickle.load(f) + self.image_paths = cache_data['image_paths'] + features_matrix = cache_data['features'] + print(f"从缓存加载了 {len(self.image_paths)} 张图片的特征") + else: + # 提取特征 + print("开始提取特征...") + features_list = [] + valid_paths = [] + + for i, image_path in enumerate(tqdm(self.image_paths, desc="提取特征")): + features = self.extract_combined_features(image_path) + if features is not None: + features_list.append(features) + valid_paths.append(image_path) + + # 每处理100张图片显示一次进度 + if (i + 1) % 100 == 0: + print(f"已处理 {i + 1}/{len(self.image_paths)} 张图片") + + if not features_list: + print("没有成功提取到任何特征") + return + + features_matrix = np.array(features_list).astype('float32') + self.image_paths = valid_paths + + # 保存缓存 + if cache_file: + print(f"保存缓存文件: {cache_file}") + cache_data = { + 'image_paths': self.image_paths, + 'features': features_matrix + } + with open(cache_file, 'wb') as f: + pickle.dump(cache_data, f) + + # 创建FAISS索引 + print(f"创建FAISS索引,特征维度: {features_matrix.shape[1]}") + index = self.create_faiss_index(features_matrix.shape[1]) + + # 训练索引(仅IVF需要) + if self.index_type == 'ivf': + print("训练IVF索引...") + index.train(features_matrix) + + # 添加特征到索引 + print("添加特征到索引...") + index.add(features_matrix) + + self.indices['combined'] = index + print(f"索引构建完成,包含 {index.ntotal} 个特征向量") + + def search_similar_images(self, + target_image: str, + k: int = 10, + threshold: float = 0.5) -> List[Tuple[str, float]]: + """搜索相似图片""" + if 'combined' not in self.indices: + print("错误:未构建索引") + return [] + + # 提取目标图片特征 + target_features = self.extract_combined_features(target_image) + if target_features is None: + print("错误:无法提取目标图片特征") + return [] + + # 搜索 + target_features = target_features.reshape(1, -1).astype('float32') + + # 搜索k+1个结果(包括自身) + search_k = min(k + 1, len(self.image_paths)) + scores, indices = self.indices['combined'].search(target_features, search_k) + + # 处理结果 + results = [] + target_abs_path = os.path.abspath(target_image) + + for score, idx in zip(scores[0], indices[0]): + if idx < len(self.image_paths): + candidate_path = self.image_paths[idx] + candidate_abs_path = os.path.abspath(candidate_path) + + # 跳过自身 + if candidate_abs_path == target_abs_path: + continue + + # 检查阈值 + if score >= threshold: + results.append((candidate_path, float(score))) + + return results + + def find_images_in_directory(self, directory: str) -> List[str]: + """查找目录中的图片文件""" + image_paths = [] + supported_formats = ['.jpg', '.jpeg', '.png', '.bmp', '.gif', '.tiff', '.webp'] + + for root, dirs, files in os.walk(directory): + for file in files: + if any(file.lower().endswith(fmt) for fmt in supported_formats): + image_paths.append(os.path.join(root, file)) + + return sorted(image_paths) + + def save_index(self, index_file: str) -> None: + """保存索引到文件""" + if 'combined' not in self.indices: + print("错误:没有可保存的索引") + return + + # 如果是GPU索引,先转换到CPU + index = self.indices['combined'] + if hasattr(index, 'index'): # GPU索引 + index = faiss.index_gpu_to_cpu(index) + + # 保存索引和图片路径 + faiss.write_index(index, index_file) + + # 保存图片路径映射 + paths_file = index_file.replace('.index', '_paths.pkl') + with open(paths_file, 'wb') as f: + pickle.dump(self.image_paths, f) + + print(f"索引已保存到: {index_file}") + print(f"路径映射已保存到: {paths_file}") + + def load_index(self, index_file: str) -> None: + """从文件加载索引""" + if not os.path.exists(index_file): + print(f"错误:索引文件不存在 {index_file}") + return + + # 加载索引 + index = faiss.read_index(index_file) + + # 如果使用GPU + if self.use_gpu and faiss.get_num_gpus() > 0: + res = faiss.StandardGpuResources() + index = faiss.index_cpu_to_gpu(res, 0, index) + + self.indices['combined'] = index + + # 加载图片路径映射 + paths_file = index_file.replace('.index', '_paths.pkl') + if os.path.exists(paths_file): + with open(paths_file, 'rb') as f: + self.image_paths = pickle.load(f) + else: + print(f"警告:路径映射文件不存在 {paths_file}") + + print(f"索引已加载: {index.ntotal} 个特征向量") + + +def main(): + """主函数""" + parser = argparse.ArgumentParser(description="基于FAISS的图像相似度检测工具") + + # 基本参数 + parser.add_argument("--mode", "-m", type=str, choices=['build', 'search'], required=True, + help="运行模式:build=构建索引, search=搜索相似图片") + parser.add_argument("--directory", "-d", type=str, required=True, + help="图片目录路径") + parser.add_argument("--cache-file", "-c", type=str, + help="特征缓存文件路径") + parser.add_argument("--index-file", "-i", type=str, default="image_index.index", + help="FAISS索引文件路径") + + # 搜索参数 + parser.add_argument("--target", "-t", type=str, + help="目标图片路径(搜索模式必需)") + parser.add_argument("--threshold", "-th", type=float, default=0.5, + help="相似度阈值") + parser.add_argument("--top-k", "-k", type=int, default=10, + help="返回前K个相似结果") + + # 模型参数 + parser.add_argument("--disable-resnet", action="store_true", + help="禁用ResNet特征") + parser.add_argument("--disable-vit", action="store_true", + help="禁用ViT特征") + parser.add_argument("--disable-traditional", action="store_true", + help="禁用传统CV特征") + + # FAISS参数 + parser.add_argument("--index-type", type=str, default="flat", + choices=['flat'], + help="FAISS索引类型") + parser.add_argument("--use-gpu", action="store_true", + help="使用GPU加速") + + args = parser.parse_args() + + # 验证参数 + if args.mode == 'search' and not args.target: + print("错误:搜索模式需要指定目标图片") + return + + if not os.path.exists(args.directory): + print(f"错误:目录不存在 {args.directory}") + return + + # 初始化检测器 + detector = FAISSImageSimilarityDetector( + enable_resnet=not args.disable_resnet, + enable_vit=not args.disable_vit, + enable_traditional=not args.disable_traditional, + index_type=args.index_type, + use_gpu=args.use_gpu + ) + + if args.mode == 'build': + # 构建索引模式 + print("=" * 60) + print("构建索引模式") + print("=" * 60) + + start_time = time.time() + detector.build_index(args.directory, args.cache_file) + build_time = time.time() - start_time + + # 保存索引 + detector.save_index(args.index_file) + + print(f"索引构建完成,耗时: {build_time:.2f}秒") + + elif args.mode == 'search': + # 搜索模式 + print("=" * 60) + print("搜索相似图片模式") + print("=" * 60) + + if not args.target or not os.path.exists(args.target): + print(f"错误:目标图片不存在 {args.target}") + return + + # 加载索引 + detector.load_index(args.index_file) + + # 搜索相似图片 + start_time = time.time() + results = detector.search_similar_images( + args.target, + k=args.top_k, + threshold=args.threshold + ) + search_time = time.time() - start_time + + # 显示结果 + print(f"目标图片: {args.target}") + print(f"搜索耗时: {search_time:.3f}秒") + print(f"找到 {len(results)} 张相似图片:") + print("-" * 60) + + for i, (image_path, score) in enumerate(results, 1): + print(f"{i:2d}. {os.path.basename(image_path):<30} 相似度: {score:.4f}") + + if not results: + print("未找到相似图片,请尝试降低阈值") + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/API/faiss_service.py b/API/faiss_service.py new file mode 100644 index 0000000..54c1c2d --- /dev/null +++ b/API/faiss_service.py @@ -0,0 +1,478 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +FAISS图像相似度检测服务 +基于原始的faiss_image_similarity.py封装的RESTful API服务 + +启动方式: +python faiss_service.py --config config.json + +API端点: +- POST /api/v1/build_index - 构建索引 +- POST /api/v1/search - 搜索相似图片 +- GET /api/v1/status - 获取服务状态 +- GET /api/v1/health - 健康检查 +""" + +import os +import json +import logging +import threading +import time +from typing import Dict, List, Tuple, Optional +from dataclasses import dataclass +from datetime import datetime +import argparse + +from flask import Flask, request, jsonify, send_file +from flask_cors import CORS +import werkzeug +from werkzeug.utils import secure_filename + +# 导入原始的检测器类 +from faiss_image_similarity import FAISSImageSimilarityDetector + + +@dataclass +class ServiceConfig: + """服务配置类""" + host: str = "0.0.0.0" + port: int = 8080 + debug: bool = False + max_file_size: int = 16 * 1024 * 1024 # 16MB + upload_folder: str = "/tmp/image_uploads" + index_folder: str = "./indices" + cache_folder: str = "./cache" + allowed_extensions: set = None + enable_cors: bool = True + log_level: str = "INFO" + + def __post_init__(self): + if self.allowed_extensions is None: + self.allowed_extensions = {'jpg', 'jpeg', 'png', 'bmp', 'gif', 'tiff', 'webp'} + + +class ImageSimilarityService: + """图像相似度检测服务""" + + def __init__(self, config: ServiceConfig): + self.config = config + self.app = Flask(__name__) + self.detector = None + self.index_status = {} + self.service_stats = { + 'start_time': datetime.now().isoformat(), + 'total_searches': 0, + 'total_builds': 0, + 'current_indices': {} + } + + self._setup_logging() + self._setup_directories() + self._setup_flask() + self._register_routes() + + def _setup_logging(self): + """设置日志""" + logging.basicConfig( + level=getattr(logging, self.config.log_level), + format='%(asctime)s - %(name)s - %(levelname)s - %(message)s' + ) + self.logger = logging.getLogger(__name__) + + def _setup_directories(self): + """创建必要的目录""" + for folder in [self.config.upload_folder, self.config.index_folder, self.config.cache_folder]: + os.makedirs(folder, exist_ok=True) + + def _setup_flask(self): + """设置Flask应用""" + self.app.config['MAX_CONTENT_LENGTH'] = self.config.max_file_size + self.app.config['UPLOAD_FOLDER'] = self.config.upload_folder + + if self.config.enable_cors: + CORS(self.app) + + def _allowed_file(self, filename: str) -> bool: + """检查文件扩展名是否允许""" + return '.' in filename and \ + filename.rsplit('.', 1)[1].lower() in self.config.allowed_extensions + + def _register_routes(self): + """注册API路由""" + + @self.app.route('/') + def index(): + """根路径 - 服务信息""" + return jsonify({ + 'service': 'FAISS Image Similarity Service', + 'status': 'running', + 'version': '1.0.0', + 'endpoints': { + 'health': '/api/v1/health', + 'status': '/api/v1/status', + 'build_index': '/api/v1/build_index', + 'search': '/api/v1/search', + 'indices': '/api/v1/indices' + }, + 'documentation': 'See API endpoints above for available operations' + }) + + @self.app.route('/api/v1/health', methods=['GET']) + def health_check(): + """健康检查""" + return jsonify({ + 'status': 'healthy', + 'timestamp': datetime.now().isoformat(), + 'service': 'FAISS Image Similarity Service' + }) + + @self.app.route('/api/v1/status', methods=['GET']) + def get_status(): + """获取服务状态""" + return jsonify({ + 'status': 'running', + 'stats': self.service_stats, + 'indices': self.index_status, + 'config': { + 'max_file_size': self.config.max_file_size, + 'allowed_extensions': list(self.config.allowed_extensions) + } + }) + + @self.app.route('/api/v1/build_index', methods=['POST']) + def build_index(): + """构建索引API""" + try: + data = request.get_json() + if not data: + return jsonify({'error': '请提供JSON数据'}), 400 + + # 验证必需参数 + required_params = ['index_name', 'image_directory'] + for param in required_params: + if param not in data: + return jsonify({'error': f'缺少必需参数: {param}'}), 400 + + index_name = data['index_name'] + image_directory = data['image_directory'] + + # 验证目录存在 + if not os.path.exists(image_directory): + return jsonify({'error': f'图片目录不存在: {image_directory}'}), 400 + + # 获取可选参数 + model_config = data.get('model_config', {}) + cache_file = data.get('cache_file') + + # 在后台线程中构建索引 + thread = threading.Thread( + target=self._build_index_async, + args=(index_name, image_directory, model_config, cache_file) + ) + thread.daemon = True + thread.start() + + return jsonify({ + 'message': f'开始构建索引: {index_name}', + 'status': 'building', + 'index_name': index_name + }) + + except Exception as e: + self.logger.error(f"构建索引错误: {e}") + return jsonify({'error': str(e)}), 500 + + @self.app.route('/api/v1/search', methods=['POST']) + def search_similar(): + """搜索相似图片API""" + try: + # 检查是否有文件上传 + if 'image' not in request.files: + return jsonify({'error': '请上传图片文件'}), 400 + + file = request.files['image'] + if file.filename == '': + return jsonify({'error': '未选择文件'}), 400 + + if not self._allowed_file(file.filename): + return jsonify({'error': '不支持的文件格式'}), 400 + + # 获取其他参数 + index_name = request.form.get('index_name', 'default') + top_k = int(request.form.get('top_k', 10)) + threshold = float(request.form.get('threshold', 0.5)) + + # 检查索引是否存在 + if index_name not in self.index_status or self.index_status[index_name]['status'] != 'ready': + return jsonify({'error': f'索引 {index_name} 不存在或未准备就绪'}), 400 + + # 保存上传的文件 + filename = secure_filename(file.filename) + timestamp = str(int(time.time())) + safe_filename = f"{timestamp}_{filename}" + file_path = os.path.join(self.config.upload_folder, safe_filename) + file.save(file_path) + + try: + # 加载对应的检测器和索引 + detector = self._get_detector(index_name) + if not detector: + return jsonify({'error': f'无法加载检测器: {index_name}'}), 500 + + # 搜索相似图片 + results = detector.search_similar_images(file_path, top_k, threshold) + + # 更新统计 + self.service_stats['total_searches'] += 1 + + # 格式化结果 + formatted_results = [] + for img_path, score in results: + formatted_results.append({ + 'image_path': img_path, + 'similarity_score': float(score), + 'filename': os.path.basename(img_path) + }) + + return jsonify({ + 'results': formatted_results, + 'total_found': len(formatted_results), + 'query_image': safe_filename, + 'parameters': { + 'index_name': index_name, + 'top_k': top_k, + 'threshold': threshold + } + }) + + finally: + # 清理上传的临时文件 + if os.path.exists(file_path): + os.remove(file_path) + + except Exception as e: + self.logger.error(f"搜索错误: {e}") + return jsonify({'error': str(e)}), 500 + + @self.app.route('/api/v1/indices', methods=['GET']) + def list_indices(): + """列出所有可用的索引""" + return jsonify({ + 'indices': self.index_status, + 'total': len(self.index_status) + }) + + @self.app.route('/api/v1/indices/', methods=['DELETE']) + def delete_index(index_name: str): + """删除指定索引""" + try: + if index_name not in self.index_status: + return jsonify({'error': f'索引 {index_name} 不存在'}), 404 + + # 删除索引文件 + index_file = os.path.join(self.config.index_folder, f"{index_name}.index") + paths_file = os.path.join(self.config.index_folder, f"{index_name}_paths.pkl") + + for file_path in [index_file, paths_file]: + if os.path.exists(file_path): + os.remove(file_path) + + # 从状态中移除 + del self.index_status[index_name] + if index_name in self.service_stats['current_indices']: + del self.service_stats['current_indices'][index_name] + + return jsonify({'message': f'索引 {index_name} 已删除'}) + + except Exception as e: + self.logger.error(f"删除索引错误: {e}") + return jsonify({'error': str(e)}), 500 + + def _build_index_async(self, index_name: str, image_directory: str, + model_config: dict, cache_file: Optional[str]): + """异步构建索引""" + try: + self.logger.info(f"开始构建索引: {index_name}") + + # 更新状态 + self.index_status[index_name] = { + 'status': 'building', + 'start_time': datetime.now().isoformat(), + 'image_directory': image_directory, + 'progress': 0 + } + + # 初始化检测器 + detector = FAISSImageSimilarityDetector( + enable_resnet=model_config.get('enable_resnet', True), + enable_vit=model_config.get('enable_vit', True), + enable_traditional=model_config.get('enable_traditional', True), + index_type=model_config.get('index_type', 'flat'), + use_gpu=model_config.get('use_gpu', False) + ) + + # 设置缓存文件路径 + if not cache_file: + cache_file = os.path.join(self.config.cache_folder, f"{index_name}_features.pkl") + + # 构建索引 + detector.build_index(image_directory, cache_file) + + # 保存索引 + index_file = os.path.join(self.config.index_folder, f"{index_name}.index") + detector.save_index(index_file) + + # 更新状态 + self.index_status[index_name] = { + 'status': 'ready', + 'build_time': datetime.now().isoformat(), + 'image_directory': image_directory, + 'index_file': index_file, + 'total_images': len(detector.image_paths), + 'feature_dim': detector.indices['combined'].d if 'combined' in detector.indices else 0 + } + + # 更新统计 + self.service_stats['total_builds'] += 1 + self.service_stats['current_indices'][index_name] = { + 'created': datetime.now().isoformat(), + 'images': len(detector.image_paths) + } + + self.logger.info(f"索引构建完成: {index_name}") + + except Exception as e: + self.logger.error(f"构建索引失败 {index_name}: {e}") + self.index_status[index_name] = { + 'status': 'error', + 'error': str(e), + 'timestamp': datetime.now().isoformat() + } + + def _get_detector(self, index_name: str) -> Optional[FAISSImageSimilarityDetector]: + """获取检测器实例""" + try: + if index_name not in self.index_status: + return None + + index_info = self.index_status[index_name] + if index_info['status'] != 'ready': + return None + + # 创建新的检测器实例 + detector = FAISSImageSimilarityDetector() + + # 加载索引 + index_file = index_info['index_file'] + detector.load_index(index_file) + + return detector + + except Exception as e: + self.logger.error(f"加载检测器失败 {index_name}: {e}") + return None + + def run(self): + """启动服务""" + self.logger.info(f"启动FAISS图像相似度检测服务") + self.logger.info(f"服务地址: http://{self.config.host}:{self.config.port}") + + # 扫描现有索引 + self._scan_existing_indices() + + self.app.run( + host=self.config.host, + port=self.config.port, + debug=self.config.debug, + threaded=True + ) + + def _scan_existing_indices(self): + """扫描现有的索引文件""" + try: + if not os.path.exists(self.config.index_folder): + return + + for filename in os.listdir(self.config.index_folder): + if filename.endswith('.index'): + index_name = filename[:-6] # 移除.index后缀 + index_file = os.path.join(self.config.index_folder, filename) + paths_file = os.path.join(self.config.index_folder, f"{index_name}_paths.pkl") + + if os.path.exists(paths_file): + # 尝试加载检测器以验证索引 + try: + detector = FAISSImageSimilarityDetector() + detector.load_index(index_file) + + self.index_status[index_name] = { + 'status': 'ready', + 'index_file': index_file, + 'total_images': len(detector.image_paths), + 'loaded_at': datetime.now().isoformat() + } + + self.service_stats['current_indices'][index_name] = { + 'loaded': datetime.now().isoformat(), + 'images': len(detector.image_paths) + } + + self.logger.info(f"发现现有索引: {index_name}") + + except Exception as e: + self.logger.warning(f"无法加载索引 {index_name}: {e}") + + except Exception as e: + self.logger.error(f"扫描现有索引失败: {e}") + + +def load_config(config_file: str) -> ServiceConfig: + """从文件加载配置""" + try: + with open(config_file, 'r', encoding='utf-8') as f: + config_dict = json.load(f) + + # 转换allowed_extensions为set + if 'allowed_extensions' in config_dict: + config_dict['allowed_extensions'] = set(config_dict['allowed_extensions']) + + return ServiceConfig(**config_dict) + except FileNotFoundError: + print(f"配置文件不存在: {config_file}") + return ServiceConfig() + except json.JSONDecodeError as e: + print(f"配置文件格式错误: {e}") + return ServiceConfig() + + +def main(): + """主函数""" + parser = argparse.ArgumentParser(description="FAISS图像相似度检测服务") + parser.add_argument("--config", "-c", type=str, default="config.json", + help="配置文件路径") + parser.add_argument("--host", type=str, help="服务主机地址") + parser.add_argument("--port", type=int, help="服务端口") + parser.add_argument("--debug", action="store_true", help="调试模式") + + args = parser.parse_args() + + # 加载配置 + config = load_config(args.config) + + # 命令行参数覆盖配置文件 + if args.host: + config.host = args.host + if args.port: + config.port = args.port + if args.debug: + config.debug = True + + # 创建并启动服务 + service = ImageSimilarityService(config) + service.run() + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/API/faiss_service_ui.py b/API/faiss_service_ui.py new file mode 100644 index 0000000..9a47878 --- /dev/null +++ b/API/faiss_service_ui.py @@ -0,0 +1,1253 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +FAISS图像相似度检测服务 +基于原始的faiss_image_similarity.py封装的RESTful API服务 + +启动方式: +python faiss_service.py --config config.json + +API端点: +- POST /api/v1/build_index - 构建索引 +- POST /api/v1/search - 搜索相似图片 +- GET /api/v1/status - 获取服务状态 +- GET /api/v1/health - 健康检查 +- GET / - Web UI界面 +""" + +import os +import json +import logging +import threading +import time +from typing import Dict, List, Tuple, Optional +from dataclasses import dataclass +from datetime import datetime +import argparse + +from flask import Flask, request, jsonify, send_file +from flask_cors import CORS +import werkzeug +from werkzeug.utils import secure_filename + +# 导入原始的检测器类 +from faiss_image_similarity import FAISSImageSimilarityDetector + + +@dataclass +class ServiceConfig: + """服务配置类""" + host: str = "0.0.0.0" + port: int = 8080 + debug: bool = False + max_file_size: int = 16 * 1024 * 1024 # 16MB + upload_folder: str = "/tmp/image_uploads" + index_folder: str = "./indices" + cache_folder: str = "./cache" + allowed_extensions: set = None + enable_cors: bool = True + log_level: str = "INFO" + + def __post_init__(self): + if self.allowed_extensions is None: + self.allowed_extensions = {'jpg', 'jpeg', 'png', 'bmp', 'gif', 'tiff', 'webp'} + + +class ImageSimilarityService: + """图像相似度检测服务""" + + def __init__(self, config: ServiceConfig): + self.config = config + self.app = Flask(__name__) + self.detector = None + self.index_status = {} + self.service_stats = { + 'start_time': datetime.now().isoformat(), + 'total_searches': 0, + 'total_builds': 0, + 'current_indices': {} + } + + self._setup_logging() + self._setup_directories() + self._setup_flask() + self._register_routes() + + def _setup_logging(self): + """设置日志""" + logging.basicConfig( + level=getattr(logging, self.config.log_level), + format='%(asctime)s - %(name)s - %(levelname)s - %(message)s' + ) + self.logger = logging.getLogger(__name__) + + def _setup_directories(self): + """创建必要的目录""" + for folder in [self.config.upload_folder, self.config.index_folder, self.config.cache_folder]: + os.makedirs(folder, exist_ok=True) + + def _setup_flask(self): + """设置Flask应用""" + self.app.config['MAX_CONTENT_LENGTH'] = self.config.max_file_size + self.app.config['UPLOAD_FOLDER'] = self.config.upload_folder + + if self.config.enable_cors: + CORS(self.app) + + def _allowed_file(self, filename: str) -> bool: + """检查文件扩展名是否允许""" + return '.' in filename and \ + filename.rsplit('.', 1)[1].lower() in self.config.allowed_extensions + + def _register_routes(self): + """注册API路由""" + + @self.app.route('/') + def serve_ui(): + """提供Web UI界面""" + return self._get_web_ui_html() + + @self.app.route('/favicon.ico') + def favicon(): + """处理favicon请求""" + return '', 204 + + @self.app.route('/api/v1/health', methods=['GET']) + def health_check(): + """健康检查""" + return jsonify({ + 'status': 'healthy', + 'timestamp': datetime.now().isoformat(), + 'service': 'FAISS Image Similarity Service' + }) + + @self.app.route('/api/v1/status', methods=['GET']) + def get_status(): + """获取服务状态""" + return jsonify({ + 'status': 'running', + 'stats': self.service_stats, + 'indices': self.index_status, + 'config': { + 'max_file_size': self.config.max_file_size, + 'allowed_extensions': list(self.config.allowed_extensions) + } + }) + + @self.app.route('/api/v1/build_index', methods=['POST']) + def build_index(): + """构建索引API""" + try: + data = request.get_json() + if not data: + return jsonify({'error': '请提供JSON数据'}), 400 + + # 验证必需参数 + required_params = ['index_name', 'image_directory'] + for param in required_params: + if param not in data: + return jsonify({'error': f'缺少必需参数: {param}'}), 400 + + index_name = data['index_name'] + image_directory = data['image_directory'] + + # 验证目录存在 + if not os.path.exists(image_directory): + return jsonify({'error': f'图片目录不存在: {image_directory}'}), 400 + + # 获取可选参数 + model_config = data.get('model_config', {}) + cache_file = data.get('cache_file') + + # 在后台线程中构建索引 + thread = threading.Thread( + target=self._build_index_async, + args=(index_name, image_directory, model_config, cache_file) + ) + thread.daemon = True + thread.start() + + return jsonify({ + 'message': f'开始构建索引: {index_name}', + 'status': 'building', + 'index_name': index_name + }) + + except Exception as e: + self.logger.error(f"构建索引错误: {e}") + return jsonify({'error': str(e)}), 500 + + @self.app.route('/api/v1/search', methods=['POST']) + def search_similar(): + """搜索相似图片API""" + try: + # 检查是否有文件上传 + if 'image' not in request.files: + return jsonify({'error': '请上传图片文件'}), 400 + + file = request.files['image'] + if file.filename == '': + return jsonify({'error': '未选择文件'}), 400 + + if not self._allowed_file(file.filename): + return jsonify({'error': '不支持的文件格式'}), 400 + + # 获取其他参数 + index_name = request.form.get('index_name', 'default') + top_k = int(request.form.get('top_k', 10)) + threshold = float(request.form.get('threshold', 0.5)) + + # 检查索引是否存在 + if index_name not in self.index_status or self.index_status[index_name]['status'] != 'ready': + return jsonify({'error': f'索引 {index_name} 不存在或未准备就绪'}), 400 + + # 保存上传的文件 + filename = secure_filename(file.filename) + timestamp = str(int(time.time())) + safe_filename = f"{timestamp}_{filename}" + file_path = os.path.join(self.config.upload_folder, safe_filename) + file.save(file_path) + + try: + # 加载对应的检测器和索引 + detector = self._get_detector(index_name) + if not detector: + return jsonify({'error': f'无法加载检测器: {index_name}'}), 500 + + # 搜索相似图片 + results = detector.search_similar_images(file_path, top_k, threshold) + + # 更新统计 + self.service_stats['total_searches'] += 1 + + # 格式化结果 + formatted_results = [] + for img_path, score in results: + formatted_results.append({ + 'image_path': img_path, + 'similarity_score': float(score), + 'filename': os.path.basename(img_path) + }) + + return jsonify({ + 'results': formatted_results, + 'total_found': len(formatted_results), + 'query_image': safe_filename, + 'parameters': { + 'index_name': index_name, + 'top_k': top_k, + 'threshold': threshold + } + }) + + finally: + # 清理上传的临时文件 + if os.path.exists(file_path): + os.remove(file_path) + + except Exception as e: + self.logger.error(f"搜索错误: {e}") + return jsonify({'error': str(e)}), 500 + + @self.app.route('/api/v1/indices', methods=['GET']) + def list_indices(): + """列出所有可用的索引""" + return jsonify({ + 'indices': self.index_status, + 'total': len(self.index_status) + }) + + @self.app.route('/api/v1/indices/', methods=['DELETE']) + def delete_index(index_name: str): + """删除指定索引""" + try: + if index_name not in self.index_status: + return jsonify({'error': f'索引 {index_name} 不存在'}), 404 + + # 删除索引文件 + index_file = os.path.join(self.config.index_folder, f"{index_name}.index") + paths_file = os.path.join(self.config.index_folder, f"{index_name}_paths.pkl") + + for file_path in [index_file, paths_file]: + if os.path.exists(file_path): + os.remove(file_path) + + # 从状态中移除 + del self.index_status[index_name] + if index_name in self.service_stats['current_indices']: + del self.service_stats['current_indices'][index_name] + + return jsonify({'message': f'索引 {index_name} 已删除'}) + + except Exception as e: + self.logger.error(f"删除索引错误: {e}") + return jsonify({'error': str(e)}), 500 + + def _build_index_async(self, index_name: str, image_directory: str, + model_config: dict, cache_file: Optional[str]): + """异步构建索引""" + try: + self.logger.info(f"开始构建索引: {index_name}") + + # 更新状态 + self.index_status[index_name] = { + 'status': 'building', + 'start_time': datetime.now().isoformat(), + 'image_directory': image_directory, + 'progress': 0 + } + + # 初始化检测器 + detector = FAISSImageSimilarityDetector( + enable_resnet=model_config.get('enable_resnet', True), + enable_vit=model_config.get('enable_vit', True), + enable_traditional=model_config.get('enable_traditional', True), + index_type=model_config.get('index_type', 'flat'), + use_gpu=model_config.get('use_gpu', False) + ) + + # 设置缓存文件路径 + if not cache_file: + cache_file = os.path.join(self.config.cache_folder, f"{index_name}_features.pkl") + + # 构建索引 + detector.build_index(image_directory, cache_file) + + # 保存索引 + index_file = os.path.join(self.config.index_folder, f"{index_name}.index") + detector.save_index(index_file) + + # 更新状态 + self.index_status[index_name] = { + 'status': 'ready', + 'build_time': datetime.now().isoformat(), + 'image_directory': image_directory, + 'index_file': index_file, + 'total_images': len(detector.image_paths), + 'feature_dim': detector.indices['combined'].d if 'combined' in detector.indices else 0 + } + + # 更新统计 + self.service_stats['total_builds'] += 1 + self.service_stats['current_indices'][index_name] = { + 'created': datetime.now().isoformat(), + 'images': len(detector.image_paths) + } + + self.logger.info(f"索引构建完成: {index_name}") + + except Exception as e: + self.logger.error(f"构建索引失败 {index_name}: {e}") + self.index_status[index_name] = { + 'status': 'error', + 'error': str(e), + 'timestamp': datetime.now().isoformat() + } + + def _get_detector(self, index_name: str) -> Optional[FAISSImageSimilarityDetector]: + """获取检测器实例""" + try: + if index_name not in self.index_status: + return None + + index_info = self.index_status[index_name] + if index_info['status'] != 'ready': + return None + + # 创建新的检测器实例 + detector = FAISSImageSimilarityDetector() + + # 加载索引 + index_file = index_info['index_file'] + detector.load_index(index_file) + + return detector + + except Exception as e: + self.logger.error(f"加载检测器失败 {index_name}: {e}") + return None + + def _get_web_ui_html(self) -> str: + """返回Web UI的HTML内容""" + return ''' + + + + + FAISS图像相似度检测服务 + + + +
+
+

🔍 FAISS 图像相似度检测

+

智能图像搜索与相似度分析服务

+
+ +
+
+
+ 正在连接... +
+
+
+ +
+
+ + + +
+ + +
+
+

搜索相似图片

+
+
+ +
+ + +
+
+ +
+ + +
+ +
+
+ + +
+
+ + +
+
+ + +
+ +
+
+

正在搜索相似图片...

+
+ +
+
+
+ + +
+
+

构建图片索引

+
+
+ + +
+ +
+ + +
+ +
+ +
+ + + + +
+
+ + +
+ +
+
+

正在构建索引,请耐心等待...

+
+ +
+
+
+ + +
+
+
+

索引管理

+ +
+
+
+
+
+
+ + + +''' + + def run(self): + """启动服务""" + self.logger.info(f"启动FAISS图像相似度检测服务") + self.logger.info(f"服务地址: http://{self.config.host}:{self.config.port}") + + # 扫描现有索引 + self._scan_existing_indices() + + self.app.run( + host=self.config.host, + port=self.config.port, + debug=self.config.debug, + threaded=True + ) + + def _scan_existing_indices(self): + """扫描现有的索引文件""" + try: + if not os.path.exists(self.config.index_folder): + return + + for filename in os.listdir(self.config.index_folder): + if filename.endswith('.index'): + index_name = filename[:-6] # 移除.index后缀 + index_file = os.path.join(self.config.index_folder, filename) + paths_file = os.path.join(self.config.index_folder, f"{index_name}_paths.pkl") + + if os.path.exists(paths_file): + # 尝试加载检测器以验证索引 + try: + detector = FAISSImageSimilarityDetector() + detector.load_index(index_file) + + self.index_status[index_name] = { + 'status': 'ready', + 'index_file': index_file, + 'total_images': len(detector.image_paths), + 'loaded_at': datetime.now().isoformat() + } + + self.service_stats['current_indices'][index_name] = { + 'loaded': datetime.now().isoformat(), + 'images': len(detector.image_paths) + } + + self.logger.info(f"发现现有索引: {index_name}") + + except Exception as e: + self.logger.warning(f"无法加载索引 {index_name}: {e}") + + except Exception as e: + self.logger.error(f"扫描现有索引失败: {e}") + + +def load_config(config_file: str) -> ServiceConfig: + """从文件加载配置""" + try: + with open(config_file, 'r', encoding='utf-8') as f: + config_dict = json.load(f) + + # 转换allowed_extensions为set + if 'allowed_extensions' in config_dict: + config_dict['allowed_extensions'] = set(config_dict['allowed_extensions']) + + return ServiceConfig(**config_dict) + except FileNotFoundError: + print(f"配置文件不存在: {config_file}") + return ServiceConfig() + except json.JSONDecodeError as e: + print(f"配置文件格式错误: {e}") + return ServiceConfig() + + +def main(): + """主函数""" + parser = argparse.ArgumentParser(description="FAISS图像相似度检测服务") + parser.add_argument("--config", "-c", type=str, default="config.json", + help="配置文件路径") + parser.add_argument("--host", type=str, help="服务主机地址") + parser.add_argument("--port", type=int, help="服务端口") + parser.add_argument("--debug", action="store_true", help="调试模式") + + args = parser.parse_args() + + # 加载配置 + config = load_config(args.config) + + # 命令行参数覆盖配置文件 + if args.host: + config.host = args.host + if args.port: + config.port = args.port + if args.debug: + config.debug = True + + # 创建并启动服务 + service = ImageSimilarityService(config) + service.run() + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/API/hetong_ui.html b/API/hetong_ui.html new file mode 100644 index 0000000..ec812eb --- /dev/null +++ b/API/hetong_ui.html @@ -0,0 +1,1041 @@ + + + + + + 智能方案审核系统 + + + + + + + +
+
+
智能方案审核系统
+ +
+ +
+ + +
+ +
+

新建方案审核

+ +
+
📄
+

点击上传方案文件

+

支持 PDF、DOC、DOCX 格式,文件大小不超过 50MB

+ +
+
+ +

选择方案类型

+
+
+

通用类

+

适用于一般性方案审核

+
+
+

采购类

+

适用于采购相关方案

+
+
+

技术开发类

+

适用于技术研发方案

+
+
+

建筑工程类

+

适用于工程建设方案

+
+
+ +
+ +
+
+ + +
+

审核结果管理

+ +
+
定制化初审
+
智能复审
+
综合审核结果
+
结果下载
+
+ +
+

定制化初审结果

+

依据企业知识库标准,检查方案是否符合内部管理要求

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
检查项目检查结果风险等级建议
格式规范性通过符合企业标准格式
内容完整性待完善缺少风险评估章节
审批流程通过审批流程符合规定
+
+ + + + + + +
+ + +
+

审查规则配置

+ +
+

上传审核标准文件

+

将审核标准文件上传至知识库,系统将自动解析并更新审核规则

+ +
+
📋
+

点击上传标准文件

+

支持 PDF、DOC、DOCX、TXT 格式

+ +
+
+ +
+ +
+
+ +

当前规则配置

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
规则类型规则名称更新时间状态操作
通用规则企业标准格式规范2024-03-15生效中
采购规则政府采购法规检查2024-03-10生效中
技术规则技术开发标准2024-03-08待审核
+
+ + +
+

系统设置

+ +
+
+

用户权限管理

+ +
+ + + + +
+ + +
+ +
+

系统配置

+ +
+ + +
+ +
+ + +
+ +
+ + +
+ + +
+
+ +
+

当前用户列表

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
用户名邮箱角色状态最后登录操作
adminadmin@company.com管理员活跃2024-03-15 09:30 + + +
user001user001@company.com普通用户活跃2024-03-14 16:45 + + +
auditor01auditor01@company.com审核员离线2024-03-12 14:20 + + +
+
+
+
+
+
+ + + + \ No newline at end of file diff --git a/API/imagesim_ui_page1.html b/API/imagesim_ui_page1.html new file mode 100644 index 0000000..9bf4792 --- /dev/null +++ b/API/imagesim_ui_page1.html @@ -0,0 +1,630 @@ + + + + + + 图像相似度检索系统 + + + +
+
+

🔍 图像相似度检索系统

+

智能识别和检索相似图片,支持批量去重和相似度分析

+
+ +
+
+
图片检索
+
+ + +
+
+
+
+
+
+
点击上传图片或拖拽到此处
+
支持 JPG、PNG、GIF 格式,最大 10MB
+ +
+ +
+
+ +
+
+ +
50%
+
+
+ + + +
+
+
+ +
+ + +
+ + +
+
+ + + + +
+
+ +
+
+
检索结果
+
+ +
+
+ +
+
+
+

暂无检索结果

+

上传图片开始检索相似的图片

+
+
+
+
+ + + + \ No newline at end of file diff --git a/API/imagesim_ui_page2_1.html b/API/imagesim_ui_page2_1.html new file mode 100644 index 0000000..be30900 --- /dev/null +++ b/API/imagesim_ui_page2_1.html @@ -0,0 +1,2498 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 图像检索系统 + + + +
+ +
+

图像相似度检测系统

+
+
检索图片
+
图片库查看
+
+
+ + +
+
📁
+
点击上传图片或拖拽到此处
+
支持 JPG、PNG、GIF 格式,单文件最大 10MB
+ +
+ + +
+
+
156
+
总文件数
+
+
+
245.8
+
总大小 (MB)
+
+
+ + +
+
+ + +
已选择 0 个文件
+
+
+ + +
+ + + + + + + + + + + + + + + + +
缩略图文件名大小上传时间操作
+
+ + + +
+ + +
+
+
+

相似图片对比

+

发现高度相似的图片,建议处理重复项

+
+
+ +
+
+ + + + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/API/nginx.conf b/API/nginx.conf new file mode 100644 index 0000000..a1b4598 --- /dev/null +++ b/API/nginx.conf @@ -0,0 +1,92 @@ +events { + worker_connections 1024; +} + +http { + upstream faiss_backend { + server faiss-image-service:8080; + } + + # 设置上传文件大小限制 + client_max_body_size 50M; + + # 超时设置 + proxy_connect_timeout 300s; + proxy_send_timeout 300s; + proxy_read_timeout 300s; + + server { + listen 80; + server_name localhost; + + # 健康检查端点 + location /health { + proxy_pass http://faiss_backend/api/v1/health; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + + # API路由 + location /api/ { + proxy_pass http://faiss_backend; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + + # 支持大文件上传 + proxy_request_buffering off; + + # CORS支持 + add_header 'Access-Control-Allow-Origin' '*' always; + add_header 'Access-Control-Allow-Methods' 'GET, POST, DELETE, OPTIONS' always; + add_header 'Access-Control-Allow-Headers' 'Content-Type, Authorization' always; + + if ($request_method = 'OPTIONS') { + add_header 'Access-Control-Allow-Origin' '*'; + add_header 'Access-Control-Allow-Methods' 'GET, POST, DELETE, OPTIONS'; + add_header 'Access-Control-Allow-Headers' 'Content-Type, Authorization'; + return 204; + } + } + + # 静态文件服务(可选) + location /static/ { + alias /app/static/; + expires 1d; + add_header Cache-Control "public, immutable"; + } + + # 默认路由 + location / { + proxy_pass http://faiss_backend; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + } + + # HTTPS配置(如果需要SSL) + # server { + # listen 443 ssl http2; + # server_name your-domain.com; + # + # ssl_certificate /etc/nginx/ssl/cert.pem; + # ssl_certificate_key /etc/nginx/ssl/key.pem; + # + # ssl_protocols TLSv1.2 TLSv1.3; + # ssl_ciphers ECDHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384; + # ssl_prefer_server_ciphers off; + # + # location / { + # proxy_pass http://faiss_backend; + # proxy_set_header Host $host; + # proxy_set_header X-Real-IP $remote_addr; + # proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + # proxy_set_header X-Forwarded-Proto $scheme; + # } + # } +} \ No newline at end of file diff --git a/API/requirements.txt b/API/requirements.txt new file mode 100644 index 0000000..85a7472 --- /dev/null +++ b/API/requirements.txt @@ -0,0 +1,18 @@ +# FAISS图像相似度检测服务依赖 + +# Web框架 +Flask==3.0.0 +Flask-CORS==4.0.0 +Werkzeug==3.0.1 + +torch>=2.0.0 +torchvision>=0.15.0 +timm>=0.9.0 +faiss-cpu>=1.7.4 +# 如果有GPU,使用: faiss-gpu>=1.7.4 +Pillow>=10.0.0 +opencv-python>=4.8.0 +numpy>=1.24.0 +scikit-learn>=1.3.0 +tqdm>=4.65.0 +requests>=2.31.0 \ No newline at end of file