增加FAISS向量数据库对所有训练图片进行向量化的过程。

This commit is contained in:
zhanghuan
2025-09-22 11:29:22 +08:00
parent be284442e4
commit 8d6555747b
4 changed files with 1307 additions and 0 deletions
+228
View File
@@ -0,0 +1,228 @@
# FAISS相似度检索系统
基于ResNet50特征提取和FAISS索引的食物图片相似度检索系统。
## 功能特性
- ✅ 使用训练好的ResNet50 Embedding模型提取图片特征向量
- ✅ 构建高效的FAISS索引用于快速相似度检索
- ✅ 支持多种索引类型(精确搜索、近似搜索)
- ✅ 完整的标签映射和元数据管理
- ✅ 命令行界面和可视化结果展示
- ✅ 批量测试和性能评估
## 文件结构
```
FoodClassifier/
├── build_faiss_index.py # FAISS索引构建器
├── faiss_similarity_search.py # 相似度检索演示程序
├── README_FAISS.md # 使用说明文档
├── net/
│ ├── resnet_embedding.py # ResNet50特征提取网络
│ └── food_net.py # 原始分类网络
├── model/
│ └── embedding_20250917_145342/
│ └── best_embedding_model.pth # 训练好的模型
├── dataset/
│ └── train/ # 训练数据集
│ ├── 回锅肉/
│ ├── 炒细面/
│ ├── 西红柿鸡蛋/
│ └── 麻辣小面/
└── faiss_vector_db/
└── faiss_index/ # 生成的FAISS索引文件
├── faiss_index.bin # FAISS索引
├── image_paths.pkl # 图片路径映射
├── labels.pkl # 标签映射
├── class_info.json # 类别信息
└── embeddings.npy # 特征向量数组
```
## 安装依赖
```bash
pip install torch torchvision
pip install faiss-cpu # 或 faiss-gpu(如果有GPU
pip install pillow numpy tqdm matplotlib
```
## 使用方法
### 1. 构建FAISS索引
首次使用需要构建FAISS索引:
```bash
python build_faiss_index.py
```
这将:
- 加载训练好的ResNet50 Embedding模型
- 遍历训练集所有图片,提取512维特征向量
- 构建FAISS索引文件
- 保存标签映射和元数据
### 2. 相似度检索
#### 查看索引信息
```bash
python faiss_similarity_search.py --info
```
#### 单张图片相似度检索
```bash
# 基本搜索
python faiss_similarity_search.py --query dataset/test/回锅肉/aug_000.jpg
# 显示图片结果
python faiss_similarity_search.py --query dataset/test/回锅肉/aug_000.jpg --show_images
# 保存结果图片
python faiss_similarity_search.py --query dataset/test/回锅肉/aug_000.jpg --show_images --save_result
# 返回更多结果
python faiss_similarity_search.py --query dataset/test/回锅肉/aug_000.jpg --k 10
```
#### 批量测试
```bash
# 批量测试测试集
python faiss_similarity_search.py --batch_test dataset/test --max_tests 20
# 批量测试验证集
python faiss_similarity_search.py --batch_test dataset/val --max_tests 10
```
## 核心类说明
### FAISSIndexBuilder
负责构建FAISS索引的核心类:
```python
from faiss_vector_db.build_faiss_index import FAISSIndexBuilder
# 创建索引构建器
builder = FAISSIndexBuilder(
model_path="../model/embedding_20250917_145342/best_embedding_model.pth",
embedding_dim=512
)
# 构建完整索引
index = builder.build_complete_index(
train_dir="../dataset/train",
output_dir="faiss_index",
batch_size=16,
index_type='flat' # 'flat', 'ivf', 'hnsw'
)
```
### FAISSSearcher
负责相似度检索的核心类:
```python
from faiss_vector_db.build_faiss_index import FAISSSearcher
# 创建检索器
searcher = FAISSSearcher(
index_dir="faiss_vector_db/faiss_index",
model_path="model/embedding_20250917_145342/best_embedding_model.pth"
)
# 搜索相似图片
results = searcher.search_similar("test_image.jpg", k=5)
# 返回: [(图片路径, 类别名称, 相似度分数), ...]
# 按类别搜索
class_images = searcher.search_by_class("回锅肉", k=10)
# 返回: [(图片路径, 索引), ...]
```
## 索引类型说明
### 1. Flat索引 (推荐)
- **类型**: `'flat'`
- **特点**: 精确搜索,速度快
- **适用**: 中小规模数据集(< 100万向量)
- **优点**: 搜索结果完全准确
- **缺点**: 内存占用较大
### 2. IVF索引
- **类型**: `'ivf'`
- **特点**: 近似搜索,内存效率高
- **适用**: 大规模数据集(> 100万向量)
- **优点**: 内存占用小,搜索速度快
- **缺点**: 搜索结果可能不是最优
### 3. HNSW索引
- **类型**: `'hnsw'`
- **特点**: 图结构索引,搜索速度极快
- **适用**: 需要极快搜索速度的场景
- **优点**: 搜索速度最快
- **缺点**: 构建时间较长,内存占用中等
## 性能优化建议
### 1. 批处理大小
- GPU内存充足:`batch_size=32`
- GPU内存有限:`batch_size=16`
- 仅CPU`batch_size=8`
### 2. 索引选择
- 数据量 < 10万:使用 `'flat'`
- 数据量 10万-100万:使用 `'ivf'`
- 数据量 > 100万:使用 `'hnsw'`
### 3. 特征向量维度
- 默认512维提供良好的精度
- 可调整为256维以节省存储空间
- 1024维可提供更高精度(需重新训练)
## 输出文件说明
构建完成后,`faiss_vector_db/faiss_index/` 目录包含:
- **faiss_index.bin**: FAISS索引文件
- **image_paths.pkl**: 图片路径列表(pickle格式)
- **labels.pkl**: 对应的标签列表(pickle格式)
- **class_info.json**: 类别信息和元数据(JSON格式)
- **embeddings.npy**: 特征向量数组(NumPy格式,可选)
## 常见问题
### Q: 如何更新索引?
A: 重新运行 `build_faiss_index.py`,会覆盖原有索引。
### Q: 如何添加新的图片类别?
A: 将新类别图片添加到 `dataset/train/` 目录,然后重新构建索引。
### Q: 搜索速度慢怎么办?
A: 尝试使用 `'ivf'``'hnsw'` 索引类型,或减少返回结果数量。
### Q: 内存不足怎么办?
A: 减少 `batch_size`,或使用 `'ivf'` 索引类型。
### Q: 如何在其他项目中使用?
A: 复制 `build_faiss_index.py` 和相关模型文件,修改路径配置即可。
## 扩展功能
系统支持以下扩展:
1. **多模态检索**: 结合文本特征进行检索
2. **在线更新**: 支持动态添加新图片到索引
3. **分布式部署**: 支持多机部署和负载均衡
4. **Web接口**: 提供REST API接口
5. **移动端集成**: 支持移动端实时检索
## 技术细节
- **特征提取**: ResNet50 + 全连接层 → 512维向量
- **相似度计算**: 余弦相似度(内积,向量已归一化)
- **索引结构**: FAISS多种索引类型支持
- **数据格式**: 支持 JPG、PNG、BMP 等常见图片格式
- **预处理**: ImageNet标准化,224x224分辨率
## 许可证
本项目遵循 MIT 许可证。
+517
View File
@@ -0,0 +1,517 @@
"""
食物分类器 - FAISS索引构建器
将训练集图片转换为特征向量并构建FAISS索引用于快速相似度检索
功能:
1. 加载训练好的ResNet50 Embedding模型
2. 遍历训练集所有图片,提取特征向量
3. 构建FAISS索引文件
4. 创建标签映射文件
5. 实现快速相似度检索功能
"""
import os
import json
import pickle
import numpy as np
import torch
from PIL import Image
from tqdm import tqdm
from datetime import datetime
from typing import List, Tuple, Dict, Optional
import faiss
# 导入项目模块
from net.resnet_embedding import create_resnet50_embedding
from settings import settings
class FAISSIndexBuilder:
"""FAISS索引构建器"""
def __init__(self, model_path: str, embedding_dim: int = 512):
"""
初始化FAISS索引构建器
Args:
model_path: 训练好的模型路径
embedding_dim: 特征向量维度
"""
self.model_path = model_path
self.embedding_dim = embedding_dim
self.device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
# 初始化模型
self.model = None
self.load_model()
# 数据存储
self.embeddings = []
self.image_paths = []
self.labels = []
self.class_names = []
self.class_to_idx = {}
self.idx_to_class = {}
print(f"使用设备: {self.device}")
print(f"特征向量维度: {self.embedding_dim}")
def load_model(self):
"""加载训练好的ResNet50 Embedding模型"""
try:
print(f"正在加载模型: {self.model_path}")
# 创建模型
self.model = create_resnet50_embedding(
embedding_dim=self.embedding_dim,
pretrained=True,
use_internal_preprocess=False
)
# 加载权重
checkpoint = torch.load(self.model_path, map_location=self.device)
# 处理不同的保存格式
if isinstance(checkpoint, dict):
if 'model_state_dict' in checkpoint:
self.model.load_state_dict(checkpoint['model_state_dict'])
elif 'state_dict' in checkpoint:
self.model.load_state_dict(checkpoint['state_dict'])
else:
self.model.load_state_dict(checkpoint)
else:
self.model.load_state_dict(checkpoint)
# 移动到设备并设置为评估模式
self.model.to(self.device)
self.model.eval()
print("模型加载成功!")
except Exception as e:
print(f"模型加载失败: {e}")
raise
def scan_training_data(self, train_dir: str) -> Tuple[List[str], List[str], List[int]]:
"""
扫描训练数据目录,获取所有图片路径和标签
Args:
train_dir: 训练数据目录路径
Returns:
Tuple[List[str], List[str], List[int]]: (图片路径列表, 类别名称列表, 标签索引列表)
"""
print(f"正在扫描训练数据目录: {train_dir}")
image_paths = []
class_names = []
labels = []
# 获取所有类别目录
class_dirs = [d for d in os.listdir(train_dir)
if os.path.isdir(os.path.join(train_dir, d))]
class_dirs.sort() # 确保顺序一致
print(f"发现类别: {class_dirs}")
# 创建类别映射
self.class_names = class_dirs
self.class_to_idx = {class_name: idx for idx, class_name in enumerate(class_dirs)}
self.idx_to_class = {idx: class_name for idx, class_name in enumerate(class_dirs)}
# 扫描每个类别目录
for class_name in class_dirs:
class_dir = os.path.join(train_dir, class_name)
class_idx = self.class_to_idx[class_name]
# 获取该类别下的所有图片
image_files = [f for f in os.listdir(class_dir)
if f.lower().endswith(('.jpg', '.jpeg', '.png', '.bmp'))]
print(f"类别 '{class_name}': {len(image_files)} 张图片")
for image_file in image_files:
image_path = os.path.join(class_dir, image_file)
image_paths.append(image_path)
class_names.append(class_name)
labels.append(class_idx)
print(f"总计扫描到 {len(image_paths)} 张图片")
return image_paths, class_names, labels
def extract_features_batch(self, image_paths: List[str], batch_size: int = 32) -> np.ndarray:
"""
批量提取图片特征向量
Args:
image_paths: 图片路径列表
batch_size: 批处理大小
Returns:
np.ndarray: 特征向量数组,形状为 (N, embedding_dim)
"""
print(f"开始提取特征向量,批大小: {batch_size}")
all_embeddings = []
with torch.no_grad():
for i in tqdm(range(0, len(image_paths), batch_size), desc="提取特征"):
batch_paths = image_paths[i:i + batch_size]
batch_images = []
valid_indices = []
# 加载并预处理批次图片
for j, img_path in enumerate(batch_paths):
try:
# 加载图片
image = Image.open(img_path).convert('RGB')
# 预处理
tensor = self.model.preprocess_image(image)
batch_images.append(tensor.squeeze(0)) # 移除batch维度
valid_indices.append(i + j)
except Exception as e:
print(f"警告: 无法处理图片 {img_path}: {e}")
continue
if not batch_images:
continue
# 堆叠成批次并移动到设备
batch_tensor = torch.stack(batch_images).to(self.device)
# 提取特征
batch_embeddings = self.model(batch_tensor, normalize=True)
# 转换为numpy并添加到结果中
all_embeddings.append(batch_embeddings.cpu().numpy())
# 合并所有批次的结果
if all_embeddings:
embeddings = np.vstack(all_embeddings)
print(f"成功提取 {embeddings.shape[0]} 个特征向量")
return embeddings
else:
raise ValueError("没有成功提取任何特征向量")
def build_faiss_index(self, embeddings: np.ndarray, index_type: str = 'flat') -> faiss.Index:
"""
构建FAISS索引
Args:
embeddings: 特征向量数组
index_type: 索引类型 ('flat', 'ivf', 'hnsw')
Returns:
faiss.Index: 构建好的FAISS索引
"""
print(f"构建FAISS索引,类型: {index_type}")
dimension = embeddings.shape[1]
n_vectors = embeddings.shape[0]
if index_type == 'flat':
# 精确搜索索引
index = faiss.IndexFlatIP(dimension) # 内积索引(适合归一化向量)
elif index_type == 'ivf':
# IVF索引(适合大规模数据)
nlist = min(100, max(1, n_vectors // 10)) # 聚类中心数量
quantizer = faiss.IndexFlatIP(dimension)
index = faiss.IndexIVFFlat(quantizer, dimension, nlist)
# 训练索引
print("训练IVF索引...")
index.train(embeddings.astype(np.float32))
elif index_type == 'hnsw':
# HNSW索引(快速近似搜索)
M = 16 # 连接数
index = faiss.IndexHNSWFlat(dimension, M)
index.hnsw.efConstruction = 200
else:
raise ValueError(f"不支持的索引类型: {index_type}")
# 添加向量到索引
print("添加向量到索引...")
index.add(embeddings.astype(np.float32))
print(f"索引构建完成,包含 {index.ntotal} 个向量")
return index
def save_index_and_metadata(self, index: faiss.Index, output_dir: str):
"""
保存FAISS索引和相关元数据
Args:
index: FAISS索引
output_dir: 输出目录
"""
# 创建输出目录
os.makedirs(output_dir, exist_ok=True)
# 保存FAISS索引
index_path = os.path.join(output_dir, 'faiss_index.bin')
faiss.write_index(index, index_path)
print(f"FAISS索引已保存到: {index_path}")
# 保存图片路径映射
paths_path = os.path.join(output_dir, 'image_paths.pkl')
with open(paths_path, 'wb') as f:
pickle.dump(self.image_paths, f)
print(f"图片路径映射已保存到: {paths_path}")
# 保存标签映射
labels_path = os.path.join(output_dir, 'labels.pkl')
with open(labels_path, 'wb') as f:
pickle.dump(self.labels, f)
print(f"标签映射已保存到: {labels_path}")
# 保存类别信息
class_info = {
'class_names': self.class_names,
'class_to_idx': self.class_to_idx,
'idx_to_class': self.idx_to_class,
'embedding_dim': self.embedding_dim,
'total_images': len(self.image_paths),
'build_time': datetime.now().isoformat()
}
class_info_path = os.path.join(output_dir, 'class_info.json')
with open(class_info_path, 'w', encoding='utf-8') as f:
json.dump(class_info, f, ensure_ascii=False, indent=2)
print(f"类别信息已保存到: {class_info_path}")
# 保存特征向量(可选,用于调试)
embeddings_path = os.path.join(output_dir, 'embeddings.npy')
embeddings_array = np.array(self.embeddings)
np.save(embeddings_path, embeddings_array)
print(f"特征向量已保存到: {embeddings_path}")
def build_complete_index(self, train_dir: str, output_dir: str,
batch_size: int = 32, index_type: str = 'flat'):
"""
完整的索引构建流程
Args:
train_dir: 训练数据目录
output_dir: 输出目录
batch_size: 批处理大小
index_type: 索引类型
"""
print("=" * 60)
print("开始构建FAISS索引")
print("=" * 60)
# 1. 扫描训练数据
image_paths, class_names, labels = self.scan_training_data(train_dir)
self.image_paths = image_paths
self.labels = labels
# 2. 提取特征向量
embeddings = self.extract_features_batch(image_paths, batch_size)
self.embeddings = embeddings
# 3. 构建FAISS索引
index = self.build_faiss_index(embeddings, index_type)
# 4. 保存索引和元数据
self.save_index_and_metadata(index, output_dir)
print("=" * 60)
print("FAISS索引构建完成!")
print(f"输出目录: {output_dir}")
print(f"总图片数: {len(image_paths)}")
print(f"类别数: {len(self.class_names)}")
print(f"特征维度: {self.embedding_dim}")
print("=" * 60)
return index
class FAISSSearcher:
"""FAISS相似度检索器"""
def __init__(self, index_dir: str, model_path: str):
"""
初始化FAISS检索器
Args:
index_dir: 索引文件目录
model_path: 模型路径
"""
self.index_dir = index_dir
self.model_path = model_path
self.device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
# 加载索引和元数据
self.load_index_and_metadata()
# 加载模型
self.load_model()
def load_index_and_metadata(self):
"""加载FAISS索引和元数据"""
# 加载FAISS索引
index_path = os.path.join(self.index_dir, 'faiss_index.bin')
self.index = faiss.read_index(index_path)
print(f"FAISS索引已加载: {self.index.ntotal} 个向量")
# 加载图片路径
paths_path = os.path.join(self.index_dir, 'image_paths.pkl')
with open(paths_path, 'rb') as f:
self.image_paths = pickle.load(f)
# 加载标签
labels_path = os.path.join(self.index_dir, 'labels.pkl')
with open(labels_path, 'rb') as f:
self.labels = pickle.load(f)
# 加载类别信息
class_info_path = os.path.join(self.index_dir, 'class_info.json')
with open(class_info_path, 'r', encoding='utf-8') as f:
self.class_info = json.load(f)
self.class_names = self.class_info['class_names']
self.class_to_idx = self.class_info['class_to_idx']
self.idx_to_class = self.class_info['idx_to_class']
self.embedding_dim = self.class_info['embedding_dim']
print(f"元数据已加载: {len(self.image_paths)} 张图片, {len(self.class_names)} 个类别")
def load_model(self):
"""加载特征提取模型"""
self.model = create_resnet50_embedding(
embedding_dim=self.embedding_dim,
pretrained=True,
use_internal_preprocess=False
)
checkpoint = torch.load(self.model_path, map_location=self.device)
if isinstance(checkpoint, dict):
if 'model_state_dict' in checkpoint:
self.model.load_state_dict(checkpoint['model_state_dict'])
elif 'state_dict' in checkpoint:
self.model.load_state_dict(checkpoint['state_dict'])
else:
self.model.load_state_dict(checkpoint)
else:
self.model.load_state_dict(checkpoint)
self.model.to(self.device)
self.model.eval()
print("特征提取模型已加载")
def search_similar(self, query_image_path: str, k: int = 5) -> List[Tuple[str, str, float]]:
"""
搜索相似图片
Args:
query_image_path: 查询图片路径
k: 返回最相似的k张图片
Returns:
List[Tuple[str, str, float]]: [(图片路径, 类别名称, 相似度分数), ...]
"""
# 提取查询图片的特征
query_image = Image.open(query_image_path).convert('RGB')
query_embedding = self.model.extract_embedding(query_image, normalize=True)
query_embedding = query_embedding.reshape(1, -1).astype(np.float32)
# 在FAISS索引中搜索
scores, indices = self.index.search(query_embedding, k)
# 整理结果
results = []
for i in range(k):
if i < len(indices[0]):
idx = indices[0][i]
score = scores[0][i]
image_path = self.image_paths[idx]
class_name = self.class_names[self.labels[idx]]
results.append((image_path, class_name, float(score)))
return results
def search_by_class(self, class_name: str, k: int = 10) -> List[Tuple[str, float]]:
"""
按类别搜索图片
Args:
class_name: 类别名称
k: 返回的图片数量
Returns:
List[Tuple[str, float]]: [(图片路径, 索引), ...]
"""
if class_name not in self.class_to_idx:
raise ValueError(f"未知类别: {class_name}")
class_idx = self.class_to_idx[class_name]
class_images = []
for i, label in enumerate(self.labels):
if label == class_idx:
class_images.append((self.image_paths[i], i))
if len(class_images) >= k:
break
return class_images
def main():
"""主函数"""
# 配置参数
MODEL_PATH = "../model/embedding_20250917_145342/best_embedding_model.pth"
TRAIN_DIR = "../dataset/train"
OUTPUT_DIR = "faiss_index"
BATCH_SIZE = 16
INDEX_TYPE = 'flat' # 'flat', 'ivf', 'hnsw'
EMBEDDING_DIM = 512
try:
# 构建索引
builder = FAISSIndexBuilder(MODEL_PATH, EMBEDDING_DIM)
index = builder.build_complete_index(
train_dir=TRAIN_DIR,
output_dir=OUTPUT_DIR,
batch_size=BATCH_SIZE,
index_type=INDEX_TYPE
)
print("\n" + "=" * 60)
print("测试相似度检索功能")
print("=" * 60)
# 测试检索功能
searcher = FAISSSearcher(OUTPUT_DIR, MODEL_PATH)
# 随机选择一张图片进行测试
test_image_path = builder.image_paths[0]
print(f"测试图片: {test_image_path}")
# 搜索相似图片
similar_images = searcher.search_similar(test_image_path, k=5)
print("\n最相似的5张图片:")
for i, (img_path, class_name, score) in enumerate(similar_images, 1):
print(f"{i}. {os.path.basename(img_path)} (类别: {class_name}, 相似度: {score:.4f})")
# 按类别搜索
print(f"\n'{builder.class_names[0]}'类别的前5张图片:")
class_images = searcher.search_by_class(builder.class_names[0], k=5)
for i, (img_path, idx) in enumerate(class_images, 1):
print(f"{i}. {os.path.basename(img_path)} (索引: {idx})")
except Exception as e:
print(f"错误: {e}")
import traceback
traceback.print_exc()
if __name__ == "__main__":
main()
+318
View File
@@ -0,0 +1,318 @@
"""
FAISS相似度检索演示程序
提供简单的命令行界面来测试相似度检索功能
功能:
1. 加载预构建的FAISS索引
2. 对指定图片进行相似度检索
3. 显示检索结果和统计信息
4. 支持批量检索和性能测试
"""
import os
import sys
import time
import argparse
from typing import List, Tuple
from PIL import Image
import matplotlib.pyplot as plt
# 导入构建的FAISS检索器
from faiss_vector_db.build_faiss_index import FAISSSearcher
class SimilaritySearchDemo:
"""相似度检索演示类"""
def __init__(self, index_dir: str, model_path: str):
"""
初始化演示程序
Args:
index_dir: FAISS索引目录
model_path: 模型路径
"""
self.searcher = FAISSSearcher(index_dir, model_path)
# 设置中文字体(用于matplotlib显示)
try:
# 尝试使用系统中文字体
plt.rcParams['font.sans-serif'] = ['SimHei', 'Microsoft YaHei', 'DejaVu Sans']
plt.rcParams['axes.unicode_minus'] = False
except:
print("警告: 无法设置中文字体,图表中的中文可能显示异常")
def search_and_display(self, query_image_path: str, k: int = 5,
show_images: bool = False, save_result: bool = False):
"""
搜索相似图片并显示结果
Args:
query_image_path: 查询图片路径
k: 返回最相似的k张图片
show_images: 是否显示图片
save_result: 是否保存结果图片
"""
print(f"\n查询图片: {query_image_path}")
print("-" * 50)
# 检查文件是否存在
if not os.path.exists(query_image_path):
print(f"错误: 文件不存在 - {query_image_path}")
return
# 开始计时
start_time = time.time()
try:
# 执行相似度搜索
results = self.searcher.search_similar(query_image_path, k)
# 计算耗时
search_time = time.time() - start_time
# 显示结果
print(f"搜索完成,耗时: {search_time:.3f}")
print(f"找到 {len(results)} 个相似结果:\n")
for i, (img_path, class_name, score) in enumerate(results, 1):
rel_path = os.path.relpath(img_path)
print(f"{i:2d}. {os.path.basename(img_path):20s} "
f"类别: {class_name:8s} "
f"相似度: {score:.4f}")
# 统计类别分布
class_counts = {}
for _, class_name, _ in results:
class_counts[class_name] = class_counts.get(class_name, 0) + 1
print(f"\n类别分布:")
for class_name, count in class_counts.items():
print(f" {class_name}: {count}")
# 显示图片
if show_images:
self.display_results(query_image_path, results, save_result)
except Exception as e:
print(f"搜索失败: {e}")
import traceback
traceback.print_exc()
def display_results(self, query_path: str, results: List[Tuple[str, str, float]],
save_result: bool = False):
"""
显示搜索结果图片
Args:
query_path: 查询图片路径
results: 搜索结果
save_result: 是否保存结果图片
"""
try:
# 计算子图布局
n_results = len(results)
n_cols = min(3, n_results + 1) # 包括查询图片
n_rows = (n_results + 1 + n_cols - 1) // n_cols
fig, axes = plt.subplots(n_rows, n_cols, figsize=(4*n_cols, 4*n_rows))
if n_rows == 1:
axes = axes.reshape(1, -1)
# 显示查询图片
query_img = Image.open(query_path).convert('RGB')
axes[0, 0].imshow(query_img)
axes[0, 0].set_title(f"查询图片\n{os.path.basename(query_path)}", fontsize=10)
axes[0, 0].axis('off')
# 显示搜索结果
for i, (img_path, class_name, score) in enumerate(results):
row = (i + 1) // n_cols
col = (i + 1) % n_cols
if row < n_rows and col < n_cols:
try:
img = Image.open(img_path).convert('RGB')
axes[row, col].imshow(img)
axes[row, col].set_title(
f"#{i+1} {class_name}\n"
f"{os.path.basename(img_path)}\n"
f"相似度: {score:.3f}",
fontsize=9
)
axes[row, col].axis('off')
except Exception as e:
axes[row, col].text(0.5, 0.5, f"加载失败\n{e}",
ha='center', va='center')
axes[row, col].axis('off')
# 隐藏多余的子图
for i in range(n_results + 1, n_rows * n_cols):
row = i // n_cols
col = i % n_cols
if row < n_rows and col < n_cols:
axes[row, col].axis('off')
plt.tight_layout()
# 保存结果
if save_result:
timestamp = time.strftime("%Y%m%d_%H%M%S")
save_path = f"similarity_search_result_{timestamp}.png"
plt.savefig(save_path, dpi=150, bbox_inches='tight')
print(f"结果图片已保存到: {save_path}")
plt.show()
except Exception as e:
print(f"显示图片失败: {e}")
def batch_search_test(self, test_dir: str, k: int = 5, max_tests: int = 10):
"""
批量搜索测试
Args:
test_dir: 测试图片目录
k: 每次搜索返回的结果数
max_tests: 最大测试数量
"""
print(f"\n批量搜索测试 - 目录: {test_dir}")
print("=" * 60)
# 获取测试图片
test_images = []
for root, dirs, files in os.walk(test_dir):
for file in files:
if file.lower().endswith(('.jpg', '.jpeg', '.png', '.bmp')):
test_images.append(os.path.join(root, file))
if len(test_images) >= max_tests:
break
if len(test_images) >= max_tests:
break
if not test_images:
print("未找到测试图片")
return
print(f"找到 {len(test_images)} 张测试图片")
# 执行批量测试
total_time = 0
success_count = 0
for i, img_path in enumerate(test_images, 1):
print(f"\n[{i}/{len(test_images)}] 测试: {os.path.basename(img_path)}")
try:
start_time = time.time()
results = self.searcher.search_similar(img_path, k)
search_time = time.time() - start_time
total_time += search_time
success_count += 1
print(f" 搜索耗时: {search_time:.3f}")
print(f" 前3个结果:")
for j, (result_path, class_name, score) in enumerate(results[:3], 1):
print(f" {j}. {class_name} (相似度: {score:.3f})")
except Exception as e:
print(f" 搜索失败: {e}")
# 显示统计信息
print("\n" + "=" * 60)
print("批量测试统计:")
print(f" 总测试数: {len(test_images)}")
print(f" 成功数: {success_count}")
print(f" 失败数: {len(test_images) - success_count}")
if success_count > 0:
print(f" 平均搜索时间: {total_time/success_count:.3f}")
print(f" 总耗时: {total_time:.3f}")
def show_index_info(self):
"""显示索引信息"""
print("\n" + "=" * 60)
print("FAISS索引信息")
print("=" * 60)
print(f"索引目录: {self.searcher.index_dir}")
print(f"总向量数: {self.searcher.index.ntotal}")
print(f"特征维度: {self.searcher.embedding_dim}")
print(f"类别数: {len(self.searcher.class_names)}")
print(f"类别列表: {', '.join(self.searcher.class_names)}")
# 显示每个类别的图片数量
print(f"\n各类别图片数量:")
class_counts = {}
for label in self.searcher.labels:
class_name = self.searcher.class_names[label]
class_counts[class_name] = class_counts.get(class_name, 0) + 1
for class_name in self.searcher.class_names:
count = class_counts.get(class_name, 0)
print(f" {class_name}: {count}")
print(f"\n构建时间: {self.searcher.class_info.get('build_time', '未知')}")
def main():
"""主函数"""
parser = argparse.ArgumentParser(description='FAISS相似度检索演示程序')
parser.add_argument('--index_dir', type=str, default='faiss_vector_db/faiss_index',
help='FAISS索引目录路径')
parser.add_argument('--model_path', type=str,
default='model/embedding_20250917_145342/best_embedding_model.pth',
help='模型文件路径')
parser.add_argument('--query', type=str, help='查询图片路径')
parser.add_argument('--k', type=int, default=5, help='返回最相似的k张图片')
parser.add_argument('--show_images', action='store_true', help='显示搜索结果图片')
parser.add_argument('--save_result', action='store_true', help='保存结果图片')
parser.add_argument('--batch_test', type=str, help='批量测试目录')
parser.add_argument('--max_tests', type=int, default=10, help='批量测试最大数量')
parser.add_argument('--info', action='store_true', help='显示索引信息')
args = parser.parse_args()
try:
# 检查索引目录
if not os.path.exists(args.index_dir):
print(f"错误: 索引目录不存在 - {args.index_dir}")
print("请先运行 build_faiss_index.py 构建索引")
return
# 检查模型文件
if not os.path.exists(args.model_path):
print(f"错误: 模型文件不存在 - {args.model_path}")
return
# 初始化演示程序
demo = SimilaritySearchDemo(args.index_dir, args.model_path)
# 显示索引信息
if args.info:
demo.show_index_info()
# 单张图片搜索
if args.query:
demo.search_and_display(args.query, args.k, args.show_images, args.save_result)
# 批量测试
if args.batch_test:
demo.batch_search_test(args.batch_test, args.k, args.max_tests)
# 如果没有指定任何操作,显示帮助信息
if not any([args.query, args.batch_test, args.info]):
print("使用示例:")
print(f" python {sys.argv[0]} --info # 显示索引信息")
print(f" python {sys.argv[0]} --query dataset/test/回锅肉/aug_000.jpg --show_images")
print(f" python {sys.argv[0]} --batch_test dataset/test --max_tests 5")
except Exception as e:
print(f"程序执行失败: {e}")
import traceback
traceback.print_exc()
if __name__ == "__main__":
main()
+244
View File
@@ -0,0 +1,244 @@
"""
FAISS相似度检索系统快速演示脚本
一键运行完整的索引构建和检索演示
"""
import os
import time
def check_dependencies():
"""检查依赖包"""
required_packages = [
'torch', 'torchvision', 'faiss', 'PIL', 'numpy', 'tqdm', 'matplotlib'
]
missing_packages = []
for package in required_packages:
try:
if package == 'PIL':
import PIL
elif package == 'faiss':
import faiss
else:
__import__(package)
except ImportError:
missing_packages.append(package)
if missing_packages:
print("缺少以下依赖包:")
for pkg in missing_packages:
if pkg == 'faiss':
print(f" {pkg} (安装命令: pip install faiss-cpu)")
else:
print(f" {pkg}")
print("\n请先安装缺少的依赖包")
return False
return True
def check_files():
"""检查必要文件"""
required_files = [
'net/resnet_embedding.py',
'settings/settings.py',
'model/embedding_20250917_145342/best_embedding_model.pth',
'dataset/train'
]
missing_files = []
for file_path in required_files:
if not os.path.exists(file_path):
missing_files.append(file_path)
if missing_files:
print("缺少以下必要文件:")
for file_path in missing_files:
print(f" {file_path}")
return False
return True
def run_index_building():
"""运行索引构建"""
print("=" * 60)
print("步骤 1: 构建FAISS索引")
print("=" * 60)
try:
from faiss_vector_db.build_faiss_index import main as build_main
build_main()
return True
except Exception as e:
print(f"索引构建失败: {e}")
import traceback
traceback.print_exc()
return False
def run_search_demo():
"""运行检索演示"""
print("\n" + "=" * 60)
print("步骤 2: 相似度检索演示")
print("=" * 60)
try:
from faiss_vector_db.build_faiss_index import FAISSSearcher
# 配置参数
index_dir = "faiss_index"
model_path = "../model/embedding_20250917_145342/best_embedding_model.pth"
# 检查索引是否存在
if not os.path.exists(os.path.join(index_dir, 'faiss_index.bin')):
print("错误: FAISS索引不存在,请先构建索引")
return False
# 创建检索器
searcher = FAISSSearcher(index_dir, model_path)
# 显示索引信息
print(f"索引信息:")
print(f" 总向量数: {searcher.index.ntotal}")
print(f" 特征维度: {searcher.embedding_dim}")
print(f" 类别数: {len(searcher.class_names)}")
print(f" 类别: {', '.join(searcher.class_names)}")
# 找一些测试图片
test_images = []
test_dirs = ['dataset/test', 'dataset/val', 'dataset/train']
for test_dir in test_dirs:
if os.path.exists(test_dir):
for root, dirs, files in os.walk(test_dir):
for file in files:
if file.lower().endswith(('.jpg', '.jpeg', '.png')):
test_images.append(os.path.join(root, file))
if len(test_images) >= 3: # 只测试3张图片
break
if len(test_images) >= 3:
break
if len(test_images) >= 3:
break
if not test_images:
print("未找到测试图片")
return False
# 执行相似度检索演示
print(f"\n开始相似度检索演示 (测试 {len(test_images)} 张图片):")
print("-" * 50)
total_time = 0
for i, test_image in enumerate(test_images, 1):
print(f"\n[{i}/{len(test_images)}] 测试图片: {os.path.relpath(test_image)}")
start_time = time.time()
results = searcher.search_similar(test_image, k=5)
search_time = time.time() - start_time
total_time += search_time
print(f"搜索耗时: {search_time:.3f}")
print("最相似的5张图片:")
for j, (img_path, class_name, score) in enumerate(results, 1):
rel_path = os.path.relpath(img_path)
print(f" {j}. {os.path.basename(img_path):20s} "
f"类别: {class_name:8s} "
f"相似度: {score:.4f}")
# 统计类别分布
class_counts = {}
for _, class_name, _ in results:
class_counts[class_name] = class_counts.get(class_name, 0) + 1
print(f" 类别分布: {dict(class_counts)}")
print(f"\n演示完成!")
print(f"平均搜索时间: {total_time/len(test_images):.3f}")
return True
except Exception as e:
print(f"检索演示失败: {e}")
import traceback
traceback.print_exc()
return False
def show_usage_examples():
"""显示使用示例"""
print("\n" + "=" * 60)
print("使用示例")
print("=" * 60)
print("\n1. 查看索引信息:")
print(" python faiss_similarity_search.py --info")
print("\n2. 单张图片相似度检索:")
print(" python faiss_similarity_search.py --query dataset/test/回锅肉/aug_000.jpg")
print("\n3. 显示检索结果图片:")
print(" python faiss_similarity_search.py --query dataset/test/回锅肉/aug_000.jpg --show_images")
print("\n4. 批量测试:")
print(" python faiss_similarity_search.py --batch_test dataset/test --max_tests 10")
print("\n5. 保存结果图片:")
print(" python faiss_similarity_search.py --query dataset/test/回锅肉/aug_000.jpg --show_images --save_result")
def main():
"""主函数"""
print("FAISS相似度检索系统 - 快速演示")
print("=" * 60)
# 检查依赖
print("检查依赖包...")
if not check_dependencies():
return
print("✓ 依赖包检查通过")
# 检查文件
print("检查必要文件...")
if not check_files():
return
print("✓ 文件检查通过")
# 检查是否已有索引
index_exists = os.path.exists("faiss_index/faiss_index.bin")
if index_exists:
print("✓ 发现已存在的FAISS索引")
# 询问是否重新构建
while True:
choice = input("\n是否重新构建索引? (y/n): ").lower().strip()
if choice in ['y', 'yes', '']:
if not run_index_building():
return
break
elif choice in ['n', 'no', '']:
print("跳过索引构建,直接进行检索演示")
break
else:
print("请输入 y 或 n")
else:
print("未发现FAISS索引,开始构建...")
if not run_index_building():
return
# 运行检索演示
if not run_search_demo():
return
# 显示使用示例
show_usage_examples()
print("\n" + "=" * 60)
print("演示完成! 🎉")
print("您现在可以使用 faiss_similarity_search.py 进行更多测试")
print("=" * 60)
if __name__ == "__main__":
main()