启动py程序后,在浏览器访问,然后上传对应人物脸部照片,就可以在本地文件夹搜索查找同一个人的其他照片了。(代码在结尾处)

将网上找到的图片上传,也可以在本地找到对应人物照片:搜索时注意调整相似度即可,相似度建议设置到0.2以上,避免丢失准确率。

程序使用FaceAnalysis提取人物脸部特征并保存到图片文件夹根目录的SQLite数据库中,减少每次提取特征消耗时间,文件夹内添加或删除图片后,再次点击创建/更新索引即可;(本地图片库以开源图片库为例,避免肖像权风险

程序启动后,在浏览器访问http://127.0.0.1:5000即可,访问后将本地需要查找的路径粘贴到2的位置,然后创建索引,首次创建会消耗比较多的时间,索引创建完成后,就可以上传需要查找的人物脸部照片进行搜索了

可以设置不同的相似度进行搜索;

以下为代部分(千问及百度提供技术支持):默认文件夹路径,默认相似度,最多查找图片数量,修改代码中对应的以下变量值即可;

TARGET_FOLDER = "D:\study\AI\imgeDeal\commonPic_many2one"
DEFAULT_SIMILARITY_THRESHOLD = 0.8  # ✅ 降低阈值,更容易匹配
DEFAULT_RESULTS_COUNT = 5

#以图找人,人脸识别,SQLite创建索引
# pose_match_web.py
import os
import sys
import cv2
import sqlite3
import numpy as np
from pathlib import Path
import logging
from flask import Flask, render_template_string, send_file,request, jsonify, send_from_directory #, url_for
from werkzeug.utils import secure_filename
from urllib.parse import quote, unquote
import traceback
import base64
from insightface.app import FaceAnalysis  # 使用 InsightFace(ArcFace 实现)

# 初始化模型环境
# 获取 python.exe 所在目录 → D:\study\AI\imgeDeal\python-3.10.11-embed-amd64
PYTHON_DIR = os.path.dirname(os.path.abspath(sys.executable))
print("...python环境路径:",PYTHON_DIR)

# models 路径(models 与 python.exe 同级)
MODEL_DIR = PYTHON_DIR
print("...模型路径:", MODEL_DIR)

# 初始化人脸引擎(自动下载模型,仅首次需联网)
#模型说明:name='antelopev2',name='buffalo_l' antelopev2:早期,更快,点少;buffalo_l:新版2023,稍慢,点多
face_app = FaceAnalysis(name='buffalo_l',root=MODEL_DIR,  providers=['CPUExecutionProvider'])
face_app.prepare(ctx_id=0, det_size=(640, 640))
print("✅ 模型加载成功!路径:", MODEL_DIR)

# -----------------------------
# 配置
# -----------------------------
INDEX_DB = 'imperial_faces.db' #索引库文件
UPLOAD_FOLDER = 'uploads'
TARGET_FOLDER = "D:\study\AI\imgeDeal\commonPic_many2one"
DEFAULT_SIMILARITY_THRESHOLD = 0.8  # ✅ 降低阈值,更容易匹配
DEFAULT_RESULTS_COUNT = 5
os.makedirs(UPLOAD_FOLDER, exist_ok=True)

# -----------------------------
# 初始化 Flask
# Flask App
# -----------------------------
web_app = Flask(__name__)
web_app.config['UPLOAD_FOLDER'] = UPLOAD_FOLDER


# 初始化索引库
def init_database(db_folder):
    """处理 SQLite 数据库文件夹路径"""
    if not isinstance(db_folder, Path):
        db_folder = Path(db_folder)
    # 确保文件夹存在
    db_folder.mkdir(parents=True, exist_ok=True)
    
    # 构建数据库文件的完整路径
    db_path = db_folder / INDEX_DB
	
    """初始化 SQLite 数据库"""
    conn = sqlite3.connect(db_path)
    cursor = conn.cursor()
    
    cursor.execute('''
        CREATE TABLE IF NOT EXISTS faces_index (
            id INTEGER PRIMARY KEY,
            filename TEXT UNIQUE NOT NULL,
            file_path TEXT NOT NULL,
            file_size INTEGER NOT NULL,
            file_mtime REAL NOT NULL,
            age TEXT NOT NULL,
            gender TEXT NOT NULL,
            face_bbox BLOB NOT NULL, -- 存储人脸特征向量
            embedding BLOB NOT NULL
        )
    ''')
    
    # 创建索引,加速查询
    cursor.execute('CREATE INDEX IF NOT EXISTS idx_filename ON faces_index(filename)')
    cursor.execute('CREATE INDEX IF NOT EXISTS idx_mtime ON faces_index(file_mtime)')
    
    conn.commit()
    conn.close()
    print("✅ 索引数据库初始化完成")

SUPPORTED_EXTENSIONS = {'.jpg', '.jpeg', '.png', '.bmp', '.webp', '.tiff', '.tif'}

# -----------------------------
# 构建索引 → 保存在目标文件夹下
# -----------------------------
def build_or_update_index(image_folder = UPLOAD_FOLDER):
    """构建或更新 SQLite 索引"""
    image_folder = Path(image_folder)
    if not image_folder.exists():
        raise FileNotFoundError(f"图片文件夹不存在: {image_folder}")
    
    image_files = [f for f in image_folder.iterdir() 
                   if f.is_file() and f.suffix.lower() in SUPPORTED_EXTENSIONS]
    print("更新索引,目标文件夹:",image_folder,"文件数:",len(image_files))

	# 构建数据库文件的完整路径
    index_path = image_folder / INDEX_DB
    conn = sqlite3.connect(index_path)
    cursor = conn.cursor()
    
    processed = 0
    reused = 0
    
    for img_path in image_files:
        stat = img_path.stat()
        file_size = stat.st_size
        file_mtime = stat.st_mtime
        filename = img_path.name
        
        # 检查是否已存在且未修改
        cursor.execute(
            "SELECT file_mtime, embedding FROM faces_index WHERE filename = ?", 
            (filename,)
        )
        row = cursor.fetchone()
        
        if row and abs(row[0] - file_mtime) < 1:  # 时间差小于1秒视为未修改
            reused += 1
            continue  # 跳过,复用旧特征
        
        # 否则重新提取特征
        try:
            faces = extract_sift_faces(img_path)

            for face in faces:
                bbox = face.bbox.astype(np.float32).tobytes()
                embedding = face.embedding.astype(np.float32).tobytes()
                       
            # 插入或替换
            cursor.execute('''
                INSERT OR REPLACE INTO faces_index 
                (filename, file_path, file_size, file_mtime, age,gender,face_bbox,embedding)
                VALUES (?, ?, ?, ?, ?,?,?,?)
            ''', (filename, str(img_path), file_size, file_mtime, face.age,face.gender,bbox,embedding))
            
            processed += 1
            print(f"✅ 处理成功,计数{processed},文件{img_path}")
        except Exception as e:
            print(f"❌ 处理失败,计数{processed},文件{img_path}: {e}")
            traceback.print_exc()#打印追踪信息
            #break
            continue  # 出错也跳过,不要中断整个索引过程
    
    conn.commit()
    conn.close()
    
    print(f"✅ 索引更新完成 | 新增/更新: {processed}, 复用: {reused}, 总计: {processed + reused}")
    return processed,reused,processed + reused,index_path



#人脸提取
def extract_sift_faces(image_path):
	img = cv2.imread(image_path)
	faces = face_app.get(img)
	return faces


#获取上传图片的人脸特征
def get_query_faces(filename):
    if not filename:
        return jsonify({'error': '缺少查询图片'})
    
    query_path = os.path.join(UPLOAD_FOLDER, filename)
    if not os.path.exists(query_path):
        return jsonify({'error': f'查询图片不存在: {query_path}'})
    faces = extract_sift_faces(query_path)
    if len(faces) < 1:
        raise ValueError("未检测到人脸")
    return faces

def get_max_face(query_faces):
    return max(query_faces, key=lambda x: (x.bbox[2]-x.bbox[0])*(x.bbox[3]-x.bbox[1]))

def load_index_db():
	# 获取目标文件夹(默认 images)
    folder = request.args.get('folder', '').strip()
    image_folder = Path(folder)
    if not image_folder.is_absolute():
        image_folder = Path.cwd() / image_folder

    index_path = image_folder / INDEX_DB
    if not index_path.exists():
        return jsonify({'error': f'索引文件不存在: {index_path}'})

    return load_index_from_sqlite(index_path)


def load_index_from_sqlite(db_path):
    try:
        conn = sqlite3.connect(str(db_path))
        cursor = conn.cursor()
        
        cursor.execute("SELECT filename, file_path, file_size, file_mtime,  age,gender,face_bbox,embedding FROM faces_index")
        rows = cursor.fetchall()
        conn.close()
        
        return rows
    
    except Exception as e:
        raise RuntimeError(f"读取 SQLite 索引失败: {e}")


# HTML 模板(内嵌)
HTML_TEMPLATE = '''
<!DOCTYPE html>
<html lang="zh">
<head>
    <meta charset="UTF-8">
    <title>图片查找服务</title>
    <style>
        body { font-family: Arial, sans-serif; margin: 20px;}
        .all {display: flex; gap: 20px; /* 现代浏览器支持的间距属性 */ }
		.resultsAll {display: flex; gap: 20px; /* 现代浏览器支持的间距属性 */ align-items: flex-start;}
		.upload, .query, .index { margin: 20px 0; padding: 15px;width: 400px; border: 1px solid #ddd; border-radius: 5px; }
        .results { display: flex; flex-wrap: wrap; gap: 10px; margin-top: 20px; }
        .result-item { text-align: center; max-width: 150px; }
        .result-item img { width: 100%; height: auto; }
        input[type="text"], input[type="number"] { width: 200px; padding: 5px; margin: 5px 0; }
        button { padding: 8px 16px; margin: 5px; }
		
		/* 图片缩放功能 */ 
		.gallery { display: flex; flex-wrap: wrap; gap: 15px; justify-content: center; }
		.gallery img { width: 200px; height: 200px; object-fit: cover; border: 3px solid #ddd; border-radius: 8px; cursor: pointer; transition: transform 0.2s, box-shadow 0.2s; }
		.gallery img:hover { transform: scale(1.05); box-shadow: 0 4px 10px rgba(0,0,0,0.2); }
		.modal { display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%; background-color: rgba(0,0,0,0.9); justify-content: center; align-items: center; z-index: 1000; animation: fadeIn 0.3s ease-in-out; overflow: hidden; }
		.modal img { max-width: 90vw; max-height: 90vh; border: 4px solid white; border-radius: 6px; box-shadow: 0 0 20px rgba(255,255,255,0.1); animation: zoomIn 0.3s ease-out; transition: transform 0.2s; cursor: grab; }
		.modal img:active { cursor: grabbing; }
		@keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } }
		@keyframes zoomIn { from { transform: scale(0.8); opacity: 0; } to { transform: scale(1); opacity: 1; } }
	</style>
</head>
<body>
    <h1>📷 图片查找服务</h1>
	<div class = "all">
		<div class="upload">
			<h2>上传查询图片</h2>
			<form method="post" enctype="multipart/form-data" action="/upload" onsubmit="handleUpload(event)">
				<input onchange="handleUpload(event)" type="file" name="file" accept="image/*" required>
				<button type="submit">上传</button>
			</form>
			<p><strong>当前图片:</strong> <span id="currentFile">无</span></p>
		</div>

		<div class="index">
			<h2>创建/更新索引</h2>
			<form method="post" action="/create_index" onsubmit="updateIndex(event)">
				<label>目标文件夹路径(如:images, yoga_poses 等):</label><br>
				<input type="text" id = "folder1" name="folder" value="images" style="width: 350px;"><br>
				<button type="submit">🔍 创建/更新索引</button>
			</form>
			<p><strong>状态:</strong> <span id="indexStatus">未创建</span></p>
		</div>

		<div class="query">
			<h2>搜图</h2>
			<label>相似度阈值(0.0~1.0,默认 0.6):</label><br>
			<input type="number" id="similarity" step="0.01" min="0" max="1" value=""><br>
			<label>显示数量(默认 5):</label><br>
			<input type="number" id="count" min="1" value=""><br>

			<label>关注部位:</label><br>
			  <div class="controls" style="display:none">
				<button id="selectAll">全选/取消</button>
				<label><input type="checkbox" id="left_hand" checked> 左手</label>&nbsp;
				<label><input type="checkbox" id="right_hand" checked> 右手</label>&nbsp;
				<label><input type="checkbox" id="left_leg" checked> 左腿</label>&nbsp;
				<label><input type="checkbox" id="right_leg" checked> 右腿</label>&nbsp;
			  </div>
            <button onclick="findByFace()">🔎 人脸搜图</button>
			<!-- 
            <button onclick="findSimilar()">🔎 以图搜图</button>
			<button onclick="findPose()">🔎 姿势搜图</button>
            -->
		</div>
    </div>
	<div class = "resultsAll">
		<h2>上传图片</h2>
		<div id="uploadSource" class="results"></div>
		
		<h2>匹配结果</h2>
		<div id="results" class="results"></div>
	</div>
    
    <div class="modal" id="modal" onclick="closeModal(event)">
        <img id="modal-img" />
    </div>
    
	<script>
		document.addEventListener('DOMContentLoaded', function () {
			// DOM加载完成后执行的代码 而不需要等待样式表、图片和子框架的加载
            fetch('/getArgs', {
                method: 'POST'
            })
            .then(response => response.json())
            .then(data => {
                document.getElementById('similarity').value = data.similarity;
                document.getElementById('count').value = data.count;
                document.getElementsByName('folder')[0].value = data.targetFolder;
                
            });
		});

        let currentFile = null;
        function handleUpload(event) {
            const fileInput = event.target;
            const file = fileInput.files[0];
            
            if (!file) {
                return;
            }
			document.getElementById('currentFile').textContent = file.name;
            // 创建FormData对象并添加文件
            const formData = new FormData();
            formData.append('file', file);

            fetch('/upload', {
                method: 'POST',
                body: formData
            })
            .then(response => response.json())
            .then(data => {
                if (data.filename) {
                    currentFile = data.filename;
                    document.getElementById('currentFile').textContent = currentFile;
                    
					const uploadedUrl = data.uploaded_img_url;
					const names = "up_"+data.filename;
					document.getElementById('uploadSource').innerHTML = 
                    `<div class="result-item">
						<img src="${uploadedUrl}" class="result-item" onclick="zoomImg(event)" ><br>
						<p>性别:${data.gender} 年龄:${data.age}</p>
						<img id = "img_${names}" src="" onclick = zoomImg(event) >
					  </div>`
					document.getElementById('results').innerHTML = '';

                } else {
                    alert('上传失败: ' + (data.error || '未知错误'));
                }
            });
        }

        function updateIndex(event) {
            document.getElementById('indexStatus').textContent = '更新中...';
			event.preventDefault();
            const formData = new FormData(event.target);
            fetch('/create_index', {
                method: 'POST',
                body: formData
            })
            .then(response => response.json())
            .then(data => {
                if (data.error) {
                    document.getElementById('indexStatus').textContent = '错误: ' + data.error;
                } else {
                    document.getElementById('indexStatus').textContent = 
                        `✅ 索引已更新,新增 ${data.processed},
						更新 ${data.reused},共计${data.allcount}:张图片,索引路径${data.index_path}`;
                }
            });
        }

        function findByFace() {
            if (!currentFile) {
                alert('请先上传一张图片!');
                return;
            }
            document.getElementById('results').innerHTML = '🔍 匹配中...';

            const similarity = document.getElementById('similarity').value;
            const count = document.getElementById('count').value;
			const selected = getSelected();
			//const  folder1 = document.getElementById('folder1').value;
			const folder = document.getElementsByName('folder')[0].value;
			//alert(folder)
            fetch('/find_similar_by_face?filename=' + encodeURIComponent(currentFile) + 
                  '&similarity=' + similarity + '&count=' + count + '&folder=' + folder + '&selected=' + selected.join(','))
            .then(response => response.json())
            .then(data => {
                const resultsDiv = document.getElementById('results');
                resultsDiv.innerHTML = '';
                if (data.error) {
                    resultsDiv.innerHTML = '<p style="color:red;">' + data.error + '</p>';
                    return;
                }
                if (data.length === 0) {
                    resultsDiv.innerHTML = '<p>未找到相似姿势</p>';
                    return;
                }

                data.forEach(item => {
                    resultsDiv.innerHTML += `
                        <div class="result-item">
                            <img src="${item.img}" alt="match" onclick=zoomImg(event)>
							<p>名称:${item.image_name}</p>
                            <p>相似度: ${item.similarity.toFixed(3)}</p>
                        </div>`;
                });
            });
         }

        function findPose() {
            if (!currentFile) {
                alert('请先上传一张图片!');
                return;
            }
            document.getElementById('results').innerHTML = '🔍 匹配中...';

            const similarity = document.getElementById('similarity').value;
            const count = document.getElementById('count').value;
			const selected = getSelected();
			//const  folder1 = document.getElementById('folder1').value;
			const folder = document.getElementsByName('folder')[0].value;
			//alert(folder)
            fetch('/find_similar?filename=' + encodeURIComponent(currentFile) + 
                  '&similarity=' + similarity + '&count=' + count + '&folder=' + folder + '&selected=' + selected.join(','))
            .then(response => response.json())
            .then(data => {
                const resultsDiv = document.getElementById('results');
                resultsDiv.innerHTML = '';
                if (data.error) {
                    resultsDiv.innerHTML = '<p style="color:red;">' + data.error + '</p>';
                    return;
                }
                if (data.length === 0) {
                    resultsDiv.innerHTML = '<p>未找到相似姿势</p>';
                    return;
                }

                data.forEach(item => {
                    resultsDiv.innerHTML += `
                        <div class="result-item">
                            <img src="${item.img}" alt="match" onclick="showImage('${item.image_name}','${item.image_name}','null')">
							<p>名称:${item.image_name}</p>
                            <p>相似度: ${item.similarity.toFixed(3)}</p>
							<p>展示:<span id = "${item.image_name}"/></p>
							<img id = "img_${item.image_name}" src="" onclick = zoomImg(event) >
                        </div>`;
                });
            });
         }

        function showImage(name,names,folder) {
			if (folder == "null"){
				folder = document.getElementsByName('folder')[0].value;
			}
			types = name.split('.').pop();
			const selected = getSelected();
			const spanEle = document.getElementById(names);
			const imgEle = document.getElementById('img_'+names);
			spanEle.innerHTML = '';
			imgEle.src = '';
			const url = '/show_image?folder=' + folder + '&name=' + name + '&types=' + types + '&selected=' + selected.join(',');
            fetch(url, {
                method: 'POST',
				headers: {
					'Content-Type': 'web_application/x-www-form-urlencoded',
				},
            })
            .then(response => response.json())
            .then(data => {
                
                
                if (data.error) {
                    spanEle.innerHTML = 'data.error';
                    return;
                }

                spanEle.innerHTML = '√';
				//imgEle.src=`"data:image/${data.type};base64,${data.str}"` 
				const blobUrl = base64ToBlobUrl(`image/${data.type}`, data.str);
                imgEle.onload = () => {
                  URL.revokeObjectURL(blobUrl); // 自动释放
                  resolve(imgEle);
                };
                imgEle.onerror = () => {
                  URL.revokeObjectURL(url); // 错误时也要释放
                  reject(new Error('Load failed'));
                };
				imgEle.src = blobUrl;
				//alert(imgEle.src)
            });
         }
	
		function base64ToBlobUrl(mimeType,base64Str) {
			// 将 base64 转换为二进制数据
			const binaryString = atob(base64Str);
			const bytes = new Uint8Array(binaryString.length);
			for (let i = 0; i < binaryString.length; i++) {
				bytes[i] = binaryString.charCodeAt(i);
			}
			
			// 创建 Blob 和 URL
			const blob = new Blob([bytes], { type: mimeType });
			return URL.createObjectURL(blob);
		}

		document.getElementById('selectAll').addEventListener('click', function() {
		  const checkboxes = document.querySelectorAll('.controls input[type="checkbox"]');
		  const currentStatus = Array.from(checkboxes).some(cb => !cb.checked);
		  checkboxes.forEach(cb => cb.checked = currentStatus);
		});
		
		function getSelected() {
		  const selected = [];
		  if (document.getElementById('left_hand').checked) selected.push('0');
		  if (document.getElementById('right_hand').checked) selected.push('1');
		  if (document.getElementById('left_leg').checked) selected.push('2');
		  if (document.getElementById('right_leg').checked) selected.push('3');

		  if (selected.length === 0) {
			//alert("请至少选择一个部位");
		  }
		  return selected;
		}

		//图片缩放函数------start
		const gallery = document.getElementById('gallery');
		const modal = document.getElementById('modal');
		const modalImg = document.getElementById('modal-img');
		
		let scale = 1;
		let isDragging = false;
		let startPos = { x: 0, y: 0 };
		let currentTranslate = { x: 0, y: 0 };

		// 打开模态框
		function zoomImg(e) {
		  if (e.target.tagName === 'IMG') {
			modalImg.src = e.target.src;
			modal.style.display = 'flex';
			document.body.style.overflow = 'hidden';
			scale = 1;
			currentTranslate = { x: 0, y: 0 };
			modalImg.style.transform = `scale(${scale}) translate(0px, 0px)`;
		  }
		}

		// 关闭模态框(直接绑定在HTML上)
		function closeModal(e) {
		  if (e.target === modal || e.target === modalImg) {
			modal.style.display = 'none';
			document.body.style.overflow = 'auto';
			// 重置状态
			isDragging = false;
			scale = 1;
			currentTranslate = { x: 0, y: 0 };
		  }
		}

		// 滚轮缩放
		modalImg.addEventListener('wheel', function(e) {
		  e.preventDefault();
		  const delta = e.deltaY > 0 ? -0.1 : 0.1;
		  scale = Math.min(Math.max(scale + delta, 0.5), 5); // 支持放大到5倍
		  web_applyTransform();
		});

		// 鼠标按下:开始拖拽
		modalImg.addEventListener('mousedown', function(e) {
		  if (scale <= 1) return; // 仅在放大时允许拖拽
		  isDragging = true;
		  startPos = { x: e.clientX - currentTranslate.x, y: e.clientY - currentTranslate.y };
		  modalImg.style.cursor = 'grabbing';
		});

		// 鼠标移动:实时更新位置
		document.addEventListener('mousemove', function(e) {
		  if (!isDragging) return;
		  currentTranslate = {
			x: e.clientX - startPos.x,
			y: e.clientY - startPos.y
		  };
		  web_applyTransform();
		});

		// 鼠标松开:结束拖拽
		document.addEventListener('mouseup', function() {
		  isDragging = false;
		  modalImg.style.cursor = 'grab';
		});

		// 应用缩放和平移变换
		function web_applyTransform() {
		  modalImg.style.transform = `scale(${scale}) translate(${-currentTranslate.x}px, ${-currentTranslate.y}px)`;
		}
		//图片缩放函数------end

	</script>
</body>
</html>
'''

@web_app.route('/')
def index():
    return render_template_string(HTML_TEMPLATE)

@web_app.route('/upload', methods=['POST'])
def upload():
    try:
        if 'file' not in request.files:
            return jsonify({'error': '未选择文件'})
        file = request.files['file']
        if file.filename == '':
            return jsonify({'error': '文件名为空'})

        filename = secure_filename(file.filename)
        upload_path = os.path.join(web_app.config['UPLOAD_FOLDER'], filename)
        file.save(upload_path)

        # 返回上传图的 URL
        uploaded_img_url = f"/uploads/{filename}"

        # 提取上传图片人脸特征
        query_faces = get_query_faces(filename)
        # 取最大人脸(或可遍历所有人脸)
        query_face = get_max_face(query_faces)
        #query_face = query_faces[0]
        print("上传检测:上传图片人物:性别:", query_face.gender,"年龄:", query_face.age,"3D 姿态:", query_face.pose)

        return jsonify({
            'filename': filename,
            'uploaded_img_url': uploaded_img_url,
            "upload_path":web_app.config['UPLOAD_FOLDER'],
            'gender': "男" if str(query_face.gender) == "1" else "女",
            'age': int(query_face.age)
        })
    except Exception as e:
        traceback.print_exc()#打印追踪信息
        return jsonify({'error': str(e)})
		
@web_app.route('/getArgs', methods=['POST'])
def getArgs():
	return jsonify({
		'similarity': DEFAULT_SIMILARITY_THRESHOLD,
		'count': DEFAULT_RESULTS_COUNT,
        'targetFolder': TARGET_FOLDER,
	})

@web_app.route('/uploads/<filename>')
def uploaded_file(filename):
    return send_from_directory(UPLOAD_FOLDER, filename)

@web_app.route('/create_index', methods=['POST'])
def create_index():
    folder = request.form.get('folder', '').strip()
    if not folder:
        return jsonify({'error': '请填写图片文件夹路径'})

    try:
        print(f"=====目标路径文件夹{folder}")
        image_folder = Path(folder)
        if not image_folder.is_absolute():
            image_folder = Path.cwd() / image_folder
		
		# 1. 初始化数据库
        init_database(folder)
    
		# 2. 构建或更新索引
		#build_or_update_index(str(image_folder))
		
        processed, reused,allcount,index_path = build_or_update_index(str(folder))
        return jsonify({
            'status': 'success',
			'processed': processed,
			'reused': reused,
            'allcount': allcount,
            'index_path': str(index_path)
        })
    except Exception as e:
        traceback.print_exc()#打印追踪信息
        return jsonify({'error': str(e)})

@web_app.route('/find_similar_by_face')
def find_similar():
    print("-----开始匹配")
    filename = request.args.get('filename')

    try:
        similarity_threshold = float(request.args.get('similarity', 0.1))
        results_count = int(request.args.get('count', 5))
    except:
        return jsonify({'error': '参数错误'})

    # 提取上传图片人脸特征
    query_faces = get_query_faces(filename)

    try:
        # ✅ 使用 SQLite 加载索引
        index_data = load_index_db()
    except Exception as e:
        return jsonify({'error': f'加载索引失败: {str(e)}'})

    matches = search_similar_images(query_faces,index_data,similarity_threshold,results_count)

    print(f"🎯 匹配完成,匹配数: {len(matches)}")
    return jsonify(matches)



# -----------------------------
# 相似度匹配
# -----------------------------
def search_similar_images(query_faces, index_data,similarity_threshold, top_k=5):
    print(f"🔍 检测到 {len(query_faces)} 张人脸") 
    if query_faces is None or len(query_faces) == 0:
        return []
    
    # 取最大人脸(或可遍历所有人脸)
    query_face = get_max_face(query_faces)
    query_emb = query_face.embedding

    print("匹配检测:上传图片人物:性别:", query_face.gender,"年龄:", query_face.age,"3D 姿态:", query_face.pose)

    # 创建 FLANN 匹配器
    FLANN_INDEX_KDTREE = 1
    index_params = dict(algorithm=FLANN_INDEX_KDTREE, trees=5)
    search_params = dict(checks=50)
    flann = cv2.FlannBasedMatcher(index_params, search_params)

    results = []
	
    num = 0;
    for row in index_data:
        num = num + 1
        print("-----比对数:",num)
        img_name = row[0]
        img_path = row[1]
        db_emb = np.frombuffer(row[7], dtype=np.float32)
        # 余弦相似度
        sim = np.dot(query_emb, db_emb) / (np.linalg.norm(query_emb) * np.linalg.norm(db_emb))
        if sim >= similarity_threshold:
            print("✅✅✅✅✅✅✅✅命中")
			# 使用 URL 编码路径,防止特殊字符
            img_url = f"/image_proxy/{quote(str(img_path))}"
            results.append({
				"image_name": img_path,
				"img": img_url,
				"similarity": float(sim)  # 转为 Python float,便于 JSON 序列化
			})

    # 按 similarity 降序排序
    results.sort(key=lambda x: x["similarity"], reverse=True)

    return results[:top_k]


@web_app.route('/image_proxy/<path:encoded_path>')
def image_proxy(encoded_path):
    try:
        img_path = unquote(encoded_path)
        img_path = Path(img_path)

        if not img_path.exists() or not img_path.is_file():
            return 'Image not found', 404

        # 自动判断 MIME 类型
        mimetype = 'image/jpeg'
        ext = img_path.suffix.lower()
        if ext in ['.png']: mimetype = 'image/png'
        elif ext in ['.gif']: mimetype = 'image/gif'
        elif ext in ['.webp']: mimetype = 'image/webp'
        elif ext in ['.bmp']: mimetype = 'image/bmp'

        return send_file(str(img_path), mimetype=mimetype)

    except Exception as e:
        print(f"Error serving image: {e}")
        return 'Error loading image', 500

if __name__ == '__main__':
    os.makedirs(UPLOAD_FOLDER, exist_ok=True)
    print("✅ 服务启动中...")
    print(f"📁 上传目录: {os.path.abspath(UPLOAD_FOLDER)}")
    print("🌐 访问 http://127.0.0.1:5000")
    web_app.run(debug=False, host='127.0.0.1', port=5000)

更多推荐