commit 9c2229fa47ebee67414b5d0d3f82facc9bb4623b Author: eust-w Date: Wed Apr 9 11:13:17 2025 +0800 code init diff --git a/.env b/.env new file mode 100644 index 0000000..c4c61a4 --- /dev/null +++ b/.env @@ -0,0 +1,2 @@ +BAIDU_API_KEY=ALTAKmzKDy1OqhqmepD2OeXqbN +BAIDU_SECRET_KEY=b79c5fbc26344868916ec6e9e2ff65f0 \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..67be4b4 --- /dev/null +++ b/README.md @@ -0,0 +1,99 @@ +# 相似图片搜索系统 + +这是一个基于百度AI开放平台的相似图片搜索API的封装系统,提供了图片入库、检索、删除和更新等功能,并配有简洁美观的Web界面。 + +## 功能特点 + +- **图片入库**:将图片添加到百度相似图片搜索库中,支持添加名称、ID和标签 +- **图片检索**:上传图片搜索相似的图片,支持标签过滤 +- **图库管理**:更新和删除图库中的图片信息 +- **直观界面**:简洁美观的Web界面,操作便捷 + +## 安装与使用 + +### 环境要求 + +- Python 3.9+ +- Conda +- Poetry + +### 安装步骤 + +1. 克隆或下载本项目到本地 + +2. 使用Conda创建虚拟环境: + ```bash + conda env create -f environment.yml + conda activate imgsearcher + ``` + +3. 使用Poetry安装依赖: + ```bash + poetry install + ``` + +4. 配置API密钥: + 在项目根目录创建`.env`文件,填入百度AI平台的API密钥: + ``` + BAIDU_API_KEY=你的API_KEY + BAIDU_SECRET_KEY=你的SECRET_KEY + ``` + +5. 创建上传目录: + ```bash + mkdir uploads + ``` + +6. 运行应用: + ```bash + poetry run python app.py + ``` + +7. 在浏览器中访问: + ``` + http://localhost:5000 + ``` + +## 使用说明 + +### 图片入库 + +1. 在"图片入库"标签页中,选择要上传的图片 +2. 填写图片名称和ID(用于后续识别) +3. 可选择添加标签(最多2个,用逗号分隔) +4. 点击"上传入库"按钮 + +### 图片搜索 + +1. 在"图片搜索"标签页中,选择要搜索的图片 +2. 可选择添加标签过滤条件 +3. 选择标签逻辑(AND或OR) +4. 点击"搜索"按钮 +5. 查看搜索结果 + +### 图库管理 + +1. 先在"图片搜索"标签页进行搜索 +2. 在搜索结果中,可以对图片进行编辑或删除操作 + +## API说明 + +本系统封装了百度相似图片搜索API的以下功能: + +- 图片入库:`/upload` (POST) +- 图片检索:`/search` (POST) +- 图片删除:`/delete` (POST) +- 图片更新:`/update` (POST) +- 获取Token:`/api/token` (GET) + +## 注意事项 + +- 上传的图片最短边至少50px,最长边最大4096px +- 支持JPG、PNG、BMP格式的图片 +- 每个图片可以添加最多2个标签 +- 检索接口不返回原图,只返回入库时填写的brief信息 +- 图片删除可能会有延时生效(百度API特性) + +## 许可证 + +MIT diff --git a/app.py b/app.py new file mode 100644 index 0000000..7d59e81 --- /dev/null +++ b/app.py @@ -0,0 +1,165 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +import os +import json +import base64 +from flask import Flask, render_template, request, jsonify, redirect, url_for +from flask_cors import CORS +from werkzeug.utils import secure_filename +from app.api.baidu_image_search import BaiduImageSearch +from app.api.image_utils import ImageUtils + +app = Flask(__name__, template_folder='app/templates', static_folder='app/static') +CORS(app) # 启用CORS支持跨域请求 + +# 配置上传文件夹 +UPLOAD_FOLDER = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'uploads') +if not os.path.exists(UPLOAD_FOLDER): + os.makedirs(UPLOAD_FOLDER) +app.config['UPLOAD_FOLDER'] = UPLOAD_FOLDER + +# 允许的文件扩展名 +ALLOWED_EXTENSIONS = {'png', 'jpg', 'jpeg', 'gif'} + +# 初始化百度图像搜索API +image_search_api = BaiduImageSearch() + +def allowed_file(filename): + """检查文件扩展名是否允许""" + return '.' in filename and filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS + +@app.route('/') +def index(): + """首页""" + return render_template('index.html') + +@app.route('/upload', methods=['POST']) +def upload_image(): + """上传图片到图库""" + if 'file' not in request.files: + return jsonify({'error': '没有文件上传'}), 400 + + file = request.files['file'] + if file.filename == '': + return jsonify({'error': '没有选择文件'}), 400 + + if not allowed_file(file.filename): + return jsonify({'error': '不支持的文件类型'}), 400 + + # 获取表单数据 + name = request.form.get('name', '') + image_id = request.form.get('id', '') + tags = request.form.get('tags', '') + + # 创建brief信息 + brief = { + 'name': name, + 'id': image_id + } + + # 保存文件 + filename = secure_filename(file.filename) + file_path = os.path.join(app.config['UPLOAD_FOLDER'], filename) + file.save(file_path) + + try: + # 调用API添加图片 + result = image_search_api.add_image( + image_path=file_path, + brief=brief, + tags=tags + ) + + # 添加本地存储路径到结果 + result['file_path'] = file_path + + return jsonify(result) + except Exception as e: + return jsonify({'error': str(e)}), 500 + +@app.route('/search', methods=['POST']) +def search_image(): + """搜索相似图片""" + if 'file' not in request.files: + return jsonify({'error': '没有文件上传'}), 400 + + file = request.files['file'] + if file.filename == '': + return jsonify({'error': '没有选择文件'}), 400 + + if not allowed_file(file.filename): + return jsonify({'error': '不支持的文件类型'}), 400 + + # 获取表单数据 + tags = request.form.get('tags', '') + tag_logic = request.form.get('tag_logic', '0') + + # 保存文件 + filename = secure_filename(file.filename) + file_path = os.path.join(app.config['UPLOAD_FOLDER'], filename) + file.save(file_path) + + try: + # 调用API搜索图片 + result = image_search_api.search_image( + image_path=file_path, + tags=tags, + tag_logic=tag_logic + ) + + return jsonify(result) + except Exception as e: + return jsonify({'error': str(e)}), 500 + +@app.route('/delete', methods=['POST']) +def delete_image(): + """删除图库中的图片""" + data = request.get_json() + + if not data or 'cont_sign' not in data: + return jsonify({'error': '缺少必要参数'}), 400 + + cont_sign = data['cont_sign'] + + try: + # 调用API删除图片 + result = image_search_api.delete_image(cont_sign=cont_sign) + return jsonify(result) + except Exception as e: + return jsonify({'error': str(e)}), 500 + +@app.route('/update', methods=['POST']) +def update_image(): + """更新图库中的图片信息""" + data = request.get_json() + + if not data or 'cont_sign' not in data: + return jsonify({'error': '缺少必要参数'}), 400 + + cont_sign = data['cont_sign'] + brief = data.get('brief') + tags = data.get('tags') + + try: + # 调用API更新图片 + result = image_search_api.update_image( + cont_sign=cont_sign, + brief=brief, + tags=tags + ) + return jsonify(result) + except Exception as e: + return jsonify({'error': str(e)}), 500 + +@app.route('/api/token') +def get_token(): + """获取API访问令牌""" + try: + token = image_search_api.get_access_token() + return jsonify({'access_token': token}) + except Exception as e: + return jsonify({'error': str(e)}), 500 + +if __name__ == '__main__': + app.run(debug=True, host='0.0.0.0', port=5000) diff --git a/app/__init__.py b/app/__init__.py new file mode 100644 index 0000000..2310428 --- /dev/null +++ b/app/__init__.py @@ -0,0 +1 @@ +# 初始化应用包 diff --git a/app/__pycache__/__init__.cpython-39.pyc b/app/__pycache__/__init__.cpython-39.pyc new file mode 100644 index 0000000..0dee61c Binary files /dev/null and b/app/__pycache__/__init__.cpython-39.pyc differ diff --git a/app/api/__pycache__/baidu_image_search.cpython-39.pyc b/app/api/__pycache__/baidu_image_search.cpython-39.pyc new file mode 100644 index 0000000..eeafe83 Binary files /dev/null and b/app/api/__pycache__/baidu_image_search.cpython-39.pyc differ diff --git a/app/api/__pycache__/image_utils.cpython-39.pyc b/app/api/__pycache__/image_utils.cpython-39.pyc new file mode 100644 index 0000000..618f073 Binary files /dev/null and b/app/api/__pycache__/image_utils.cpython-39.pyc differ diff --git a/app/api/baidu_image_search.py b/app/api/baidu_image_search.py new file mode 100644 index 0000000..054107c --- /dev/null +++ b/app/api/baidu_image_search.py @@ -0,0 +1,226 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +import base64 +import json +import os +import requests +from dotenv import load_dotenv + +# 加载环境变量 +load_dotenv() + +class BaiduImageSearch: + """百度相似图片搜索API封装类""" + + def __init__(self): + """初始化,获取API密钥""" + self.api_key = os.getenv('BAIDU_API_KEY') + self.secret_key = os.getenv('BAIDU_SECRET_KEY') + self.access_token = None + self.get_access_token() + + def get_access_token(self): + """获取百度API的access_token""" + url = "https://aip.baidubce.com/oauth/2.0/token" + params = { + "grant_type": "client_credentials", + "client_id": self.api_key, + "client_secret": self.secret_key + } + response = requests.post(url, params=params) + if response.status_code == 200: + result = response.json() + self.access_token = result.get('access_token') + return self.access_token + else: + raise Exception(f"获取access_token失败: {response.text}") + + def add_image(self, image_path=None, image_base64=None, url=None, brief=None, tags=None): + """ + 添加图片到图库 + + Args: + image_path: 本地图片路径 + image_base64: 图片的base64编码 + url: 图片URL + brief: 图片摘要信息,最长256B,如:{"name":"图片名称", "id":"123"} + tags: 分类信息,最多2个tag,如:"1,2" + + Returns: + dict: API返回的结果 + """ + request_url = f"https://aip.baidubce.com/rest/2.0/image-classify/v1/realtime_search/similar/add?access_token={self.access_token}" + + params = {} + + # 设置brief信息(必选) + if brief: + if isinstance(brief, dict): + brief = json.dumps(brief, ensure_ascii=False) + params['brief'] = brief + else: + raise ValueError("brief参数是必须的") + + # 设置图片信息(三选一) + if image_path: + with open(image_path, 'rb') as f: + image = base64.b64encode(f.read()) + params['image'] = image + elif image_base64: + params['image'] = image_base64 + elif url: + params['url'] = url + else: + raise ValueError("必须提供image_path、image_base64或url其中之一") + + # 设置可选的tags + if tags: + params['tags'] = tags + + headers = {'Content-Type': 'application/x-www-form-urlencoded'} + response = requests.post(request_url, data=params, headers=headers) + + if response.status_code == 200: + return response.json() + else: + raise Exception(f"添加图片失败: {response.text}") + + def search_image(self, image_path=None, image_base64=None, url=None, tags=None, tag_logic=None, pn=0, rn=300): + """ + 检索相似图片 + + Args: + image_path: 本地图片路径 + image_base64: 图片的base64编码 + url: 图片URL + tags: 分类信息过滤,如:"1,2" + tag_logic: 标签逻辑,0表示逻辑and,1表示逻辑or + pn: 分页起始位置,默认0 + rn: 返回结果数量,默认300,最大1000 + + Returns: + dict: API返回的结果 + """ + request_url = f"https://aip.baidubce.com/rest/2.0/image-classify/v1/realtime_search/similar/search?access_token={self.access_token}" + + params = {} + + # 设置图片信息(三选一) + if image_path: + with open(image_path, 'rb') as f: + image = base64.b64encode(f.read()) + params['image'] = image + elif image_base64: + params['image'] = image_base64 + elif url: + params['url'] = url + else: + raise ValueError("必须提供image_path、image_base64或url其中之一") + + # 设置可选参数 + if tags: + params['tags'] = tags + if tag_logic is not None: + params['tag_logic'] = tag_logic + if pn is not None: + params['pn'] = pn + if rn is not None: + params['rn'] = rn + + headers = {'Content-Type': 'application/x-www-form-urlencoded'} + response = requests.post(request_url, data=params, headers=headers) + + if response.status_code == 200: + return response.json() + else: + raise Exception(f"检索图片失败: {response.text}") + + def delete_image(self, image_path=None, image_base64=None, url=None, cont_sign=None): + """ + 删除图库中的图片 + + Args: + image_path: 本地图片路径 + image_base64: 图片的base64编码 + url: 图片URL + cont_sign: 图片签名,支持批量删除,格式如:"932301884,1068006219;316336521,553141152" + + Returns: + dict: API返回的结果 + """ + request_url = f"https://aip.baidubce.com/rest/2.0/image-classify/v1/realtime_search/similar/delete?access_token={self.access_token}" + + params = {} + + # 设置图片信息(三选一) + if image_path: + with open(image_path, 'rb') as f: + image = base64.b64encode(f.read()) + params['image'] = image + elif image_base64: + params['image'] = image_base64 + elif url: + params['url'] = url + elif cont_sign: + params['cont_sign'] = cont_sign + else: + raise ValueError("必须提供image_path、image_base64、url或cont_sign其中之一") + + headers = {'Content-Type': 'application/x-www-form-urlencoded'} + response = requests.post(request_url, data=params, headers=headers) + + if response.status_code == 200: + return response.json() + else: + raise Exception(f"删除图片失败: {response.text}") + + def update_image(self, image_path=None, image_base64=None, url=None, cont_sign=None, brief=None, tags=None): + """ + 更新图库中图片的摘要和分类信息 + + Args: + image_path: 本地图片路径 + image_base64: 图片的base64编码 + url: 图片URL + cont_sign: 图片签名 + brief: 更新的摘要信息,最长256B + tags: 更新的分类信息,最多2个tag,如:"1,2" + + Returns: + dict: API返回的结果 + """ + request_url = f"https://aip.baidubce.com/rest/2.0/image-classify/v1/realtime_search/similar/update?access_token={self.access_token}" + + params = {} + + # 设置图片信息(三选一) + if image_path: + with open(image_path, 'rb') as f: + image = base64.b64encode(f.read()) + params['image'] = image + elif image_base64: + params['image'] = image_base64 + elif url: + params['url'] = url + elif cont_sign: + params['cont_sign'] = cont_sign + else: + raise ValueError("必须提供image_path、image_base64、url或cont_sign其中之一") + + # 设置更新信息 + if brief: + if isinstance(brief, dict): + brief = json.dumps(brief, ensure_ascii=False) + params['brief'] = brief + + if tags: + params['tags'] = tags + + headers = {'Content-Type': 'application/x-www-form-urlencoded'} + response = requests.post(request_url, data=params, headers=headers) + + if response.status_code == 200: + return response.json() + else: + raise Exception(f"更新图片失败: {response.text}") diff --git a/app/api/image_utils.py b/app/api/image_utils.py new file mode 100644 index 0000000..c076fc6 --- /dev/null +++ b/app/api/image_utils.py @@ -0,0 +1,74 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +import base64 +import io +from PIL import Image + +class ImageUtils: + """图片处理工具类""" + + @staticmethod + def resize_image(image_path, max_size=(1024, 1024)): + """ + 调整图片大小,确保不超过最大尺寸 + + Args: + image_path: 图片路径 + max_size: 最大尺寸 (宽, 高) + + Returns: + PIL.Image: 调整大小后的图片对象 + """ + img = Image.open(image_path) + img.thumbnail(max_size, Image.LANCZOS) + return img + + @staticmethod + def convert_to_base64(image, format='JPEG'): + """ + 将PIL图片对象转换为base64编码 + + Args: + image: PIL图片对象 + format: 图片格式,如'JPEG', 'PNG' + + Returns: + str: base64编码的图片数据 + """ + buffer = io.BytesIO() + image.save(buffer, format=format) + img_str = base64.b64encode(buffer.getvalue()) + return img_str + + @staticmethod + def image_to_base64(image_path, max_size=(1024, 1024), format='JPEG'): + """ + 将图片文件转换为base64编码,并可选调整大小 + + Args: + image_path: 图片路径 + max_size: 最大尺寸 (宽, 高) + format: 图片格式,如'JPEG', 'PNG' + + Returns: + str: base64编码的图片数据 + """ + img = ImageUtils.resize_image(image_path, max_size) + return ImageUtils.convert_to_base64(img, format) + + @staticmethod + def base64_to_image(base64_str): + """ + 将base64编码转换为PIL图片对象 + + Args: + base64_str: base64编码的图片数据 + + Returns: + PIL.Image: 图片对象 + """ + img_data = base64.b64decode(base64_str) + buffer = io.BytesIO(img_data) + img = Image.open(buffer) + return img diff --git a/app/static/css/style.css b/app/static/css/style.css new file mode 100644 index 0000000..594fd97 --- /dev/null +++ b/app/static/css/style.css @@ -0,0 +1,160 @@ +/* 全局样式 */ +body { + background-color: #f8f9fa; + font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; +} + +.container { + max-width: 1200px; +} + +/* 标题样式 */ +header h1 { + color: #343a40; + font-weight: 600; +} + +header .lead { + color: #6c757d; +} + +/* 卡片样式 */ +.card { + border: none; + border-radius: 10px; + box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); + transition: transform 0.3s ease; +} + +.card:hover { + transform: translateY(-5px); +} + +.card-title { + color: #343a40; + font-weight: 600; + margin-bottom: 20px; +} + +/* 表单样式 */ +.form-control { + border-radius: 5px; + padding: 10px 15px; +} + +.form-control:focus { + box-shadow: 0 0 0 0.25rem rgba(13, 110, 253, 0.25); +} + +.form-text { + color: #6c757d; + font-size: 0.85rem; +} + +/* 按钮样式 */ +.btn { + border-radius: 5px; + padding: 8px 20px; + font-weight: 500; +} + +.btn-primary { + background-color: #0d6efd; + border-color: #0d6efd; +} + +.btn-primary:hover { + background-color: #0b5ed7; + border-color: #0a58ca; +} + +/* 图片预览 */ +.img-preview { + max-width: 100%; + max-height: 300px; + border-radius: 5px; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); +} + +.upload-preview-container, +.search-preview-container { + text-align: center; + margin-bottom: 20px; +} + +/* 搜索结果样式 */ +.result-item { + margin-bottom: 20px; + transition: transform 0.3s ease; +} + +.result-item:hover { + transform: scale(1.03); +} + +.result-card { + height: 100%; + display: flex; + flex-direction: column; +} + +.result-img-container { + height: 200px; + overflow: hidden; + display: flex; + align-items: center; + justify-content: center; + background-color: #f8f9fa; + border-radius: 5px; +} + +.result-img { + max-width: 100%; + max-height: 100%; + object-fit: contain; +} + +.result-info { + padding: 15px; + flex-grow: 1; +} + +.result-score { + font-weight: bold; + color: #0d6efd; +} + +.result-actions { + display: flex; + justify-content: space-between; + margin-top: 10px; +} + +/* 标签页样式 */ +.nav-tabs .nav-link { + color: #6c757d; + font-weight: 500; +} + +.nav-tabs .nav-link.active { + color: #0d6efd; + font-weight: 600; +} + +/* 加载动画 */ +.spinner-border { + width: 1.5rem; + height: 1.5rem; + margin-right: 0.5rem; +} + +/* 响应式调整 */ +@media (max-width: 768px) { + .result-item { + margin-bottom: 15px; + } + + .result-img-container { + height: 150px; + } +} diff --git a/app/static/img/placeholder.png b/app/static/img/placeholder.png new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/app/static/img/placeholder.png @@ -0,0 +1 @@ + diff --git a/app/static/js/main.js b/app/static/js/main.js new file mode 100644 index 0000000..bc00172 --- /dev/null +++ b/app/static/js/main.js @@ -0,0 +1,282 @@ +// 页面加载完成后执行 +document.addEventListener('DOMContentLoaded', function() { + // 初始化图片预览功能 + initImagePreview('uploadFile', 'uploadPreview'); + initImagePreview('searchFile', 'searchPreview'); + + // 初始化表单提交事件 + initUploadForm(); + initSearchForm(); + initUpdateForm(); + initDeleteForm(); +}); + +// 初始化图片预览功能 +function initImagePreview(inputId, previewId) { + const input = document.getElementById(inputId); + const preview = document.getElementById(previewId); + + if (input && preview) { + input.addEventListener('change', function() { + if (this.files && this.files[0]) { + const reader = new FileReader(); + + reader.onload = function(e) { + preview.src = e.target.result; + preview.classList.remove('d-none'); + }; + + reader.readAsDataURL(this.files[0]); + } + }); + } +} + +// 初始化上传表单 +function initUploadForm() { + const form = document.getElementById('uploadForm'); + + if (form) { + form.addEventListener('submit', function(e) { + e.preventDefault(); + + const formData = new FormData(this); + const resultDiv = document.getElementById('uploadResult'); + + resultDiv.innerHTML = '
正在上传,请稍候...
'; + + fetch('/upload', { + method: 'POST', + body: formData + }) + .then(response => response.json()) + .then(data => { + if (data.error) { + resultDiv.innerHTML = `
${data.error}
`; + } else { + resultDiv.innerHTML = ` +
+
上传成功!
+

图片签名: ${data.cont_sign}

+

日志ID: ${data.log_id}

+
+ `; + // 重置表单 + form.reset(); + document.getElementById('uploadPreview').classList.add('d-none'); + } + }) + .catch(error => { + resultDiv.innerHTML = `
上传失败: ${error.message}
`; + }); + }); + } +} + +// 初始化搜索表单 +function initSearchForm() { + const form = document.getElementById('searchForm'); + + if (form) { + form.addEventListener('submit', function(e) { + e.preventDefault(); + + const formData = new FormData(this); + const resultDiv = document.getElementById('searchResult'); + const resultList = document.getElementById('searchResultList'); + const resultStats = document.getElementById('searchStats'); + const resultCount = document.getElementById('resultCount'); + + resultList.innerHTML = '
正在搜索,请稍候...
'; + resultStats.classList.add('d-none'); + + fetch('/search', { + method: 'POST', + body: formData + }) + .then(response => response.json()) + .then(data => { + if (data.error) { + resultList.innerHTML = `
${data.error}
`; + } else { + // 显示搜索结果统计 + resultCount.textContent = data.result_num || 0; + resultStats.classList.remove('d-none'); + + // 清空结果列表 + resultList.innerHTML = ''; + + if (data.result && data.result.length > 0) { + // 显示搜索结果 + data.result.forEach(item => { + // 解析brief信息 + let briefInfo = {}; + try { + briefInfo = JSON.parse(item.brief); + } catch (e) { + briefInfo = { name: '未知', id: '未知' }; + } + + const resultItem = document.createElement('div'); + resultItem.className = 'col-md-4 col-sm-6 result-item'; + resultItem.innerHTML = ` +
+
+ ${briefInfo.name || '图片'} +
+
+
${briefInfo.name || '未命名图片'}
+

ID: ${briefInfo.id || '无ID'}

+

相似度: ${(item.score * 100).toFixed(2)}%

+
+ + +
+
+
+ `; + resultList.appendChild(resultItem); + }); + + // 初始化编辑和删除按钮事件 + initResultButtons(); + } else { + resultList.innerHTML = '
没有找到匹配的图片
'; + } + } + }) + .catch(error => { + resultList.innerHTML = `
搜索失败: ${error.message}
`; + }); + }); + } +} + +// 初始化搜索结果中的按钮事件 +function initResultButtons() { + // 更新按钮点击事件 + document.querySelectorAll('.update-btn').forEach(button => { + button.addEventListener('click', function() { + const contSign = this.getAttribute('data-cont-sign'); + const name = this.getAttribute('data-name'); + const id = this.getAttribute('data-id'); + + document.getElementById('updateContSign').value = contSign; + document.getElementById('updateName').value = name; + document.getElementById('updateId').value = id; + }); + }); + + // 删除按钮点击事件 + document.querySelectorAll('.delete-btn').forEach(button => { + button.addEventListener('click', function() { + const contSign = this.getAttribute('data-cont-sign'); + document.getElementById('deleteContSign').value = contSign; + }); + }); +} + +// 初始化更新表单 +function initUpdateForm() { + const updateButton = document.getElementById('updateSubmit'); + + if (updateButton) { + updateButton.addEventListener('click', function() { + const contSign = document.getElementById('updateContSign').value; + const name = document.getElementById('updateName').value; + const id = document.getElementById('updateId').value; + const tags = document.getElementById('updateTags').value; + + // 创建brief信息 + const brief = { + name: name, + id: id + }; + + // 发送更新请求 + fetch('/update', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + cont_sign: contSign, + brief: brief, + tags: tags + }) + }) + .then(response => response.json()) + .then(data => { + // 关闭模态框 + const modal = bootstrap.Modal.getInstance(document.getElementById('updateModal')); + modal.hide(); + + if (data.error) { + alert(`更新失败: ${data.error}`); + } else { + alert('更新成功!'); + // 如果当前在搜索标签页,刷新搜索结果 + if (document.getElementById('search-tab').classList.contains('active')) { + document.getElementById('searchForm').dispatchEvent(new Event('submit')); + } + } + }) + .catch(error => { + alert(`更新失败: ${error.message}`); + }); + }); + } +} + +// 初始化删除表单 +function initDeleteForm() { + const deleteButton = document.getElementById('deleteSubmit'); + + if (deleteButton) { + deleteButton.addEventListener('click', function() { + const contSign = document.getElementById('deleteContSign').value; + + // 发送删除请求 + fetch('/delete', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + cont_sign: contSign + }) + }) + .then(response => response.json()) + .then(data => { + // 关闭模态框 + const modal = bootstrap.Modal.getInstance(document.getElementById('deleteModal')); + modal.hide(); + + if (data.error) { + alert(`删除失败: ${data.error}`); + } else { + alert('删除成功!'); + // 如果当前在搜索标签页,刷新搜索结果 + if (document.getElementById('search-tab').classList.contains('active')) { + document.getElementById('searchForm').dispatchEvent(new Event('submit')); + } + } + }) + .catch(error => { + alert(`删除失败: ${error.message}`); + }); + }); + } +} diff --git a/app/templates/index.html b/app/templates/index.html new file mode 100644 index 0000000..07580bb --- /dev/null +++ b/app/templates/index.html @@ -0,0 +1,192 @@ + + + + + + 相似图片搜索系统 + + + + +
+
+

相似图片搜索系统

+

基于百度AI开放平台的相似图片搜索API

+
+ +
+
+ +
+ +
+
+
+
图片入库
+
+
+ + +
支持JPG、PNG、BMP格式,最短边至少50px,最长边最大4096px
+
+
+ + +
+
+ + +
用于关联至本地图库的标识
+
+
+ + +
最多2个标签,用逗号分隔
+
+
+
+ 预览图 +
+
+ +
+
+
+
+
+ + + + + +
+
+
+
图库管理
+

此功能需要先搜索图片,然后可以对搜索结果进行管理操作。

+
+ 提示:百度相似图片搜索API不提供直接查看所有图库内容的功能,需要通过搜索相似图片来获取图库内容。 +
+
+

请先在"图片搜索"标签页进行搜索,然后可以对搜索结果进行管理。

+
+
+
+
+
+
+
+
+ + + + + + + + + + + diff --git a/environment.yml b/environment.yml new file mode 100644 index 0000000..e8e25d0 --- /dev/null +++ b/environment.yml @@ -0,0 +1,9 @@ +name: imgsearcher +channels: + - defaults + - conda-forge +dependencies: + - python=3.9 + - pip + - pip: + - poetry diff --git a/mock_baidu_api.py b/mock_baidu_api.py new file mode 100644 index 0000000..70553c4 --- /dev/null +++ b/mock_baidu_api.py @@ -0,0 +1,155 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +百度图像搜索API模拟类 +用于在没有有效API密钥的情况下进行测试 +""" + +class MockBaiduImageSearch: + """模拟百度相似图片搜索API的类""" + + def __init__(self): + """初始化模拟API""" + self.access_token = "mock_access_token_12345678900987654321" + self.images = {} # 存储模拟图片数据 + print("已初始化模拟百度图像搜索API") + + def add_image(self, image_path=None, image_base64=None, url=None, brief=None, tags=None): + """ + 模拟添加图片到图库 + + Args: + image_path: 本地图片路径 + image_base64: 图片的base64编码 + url: 图片URL + brief: 图片摘要信息 + tags: 分类信息 + + Returns: + dict: 模拟API返回的结果 + """ + # 生成一个唯一的图片ID + import uuid + cont_sign = str(uuid.uuid4()) + + # 存储图片信息 + self.images[cont_sign] = { + 'brief': brief, + 'tags': tags, + 'url': url or image_path or "模拟图片路径", + 'add_time': '2025-04-09 11:00:00' + } + + return { + 'log_id': 123456789, + 'cont_sign': cont_sign, + 'error_code': 0, + 'error_msg': 'success' + } + + def search_image(self, image_path=None, image_base64=None, url=None, tags=None, tag_logic=None, pn=0, rn=300): + """ + 模拟搜索相似图片 + + Args: + image_path: 本地图片路径 + image_base64: 图片的base64编码 + url: 图片URL + tags: 分类信息 + tag_logic: 标签逻辑,'AND'或'OR' + pn: 分页起始位置 + rn: 返回结果数量 + + Returns: + dict: 模拟API返回的结果 + """ + # 模拟搜索结果 + result_list = [] + + # 如果有图片,则返回它们作为搜索结果 + for cont_sign, image_info in self.images.items(): + # 如果指定了标签,则进行标签过滤 + if tags: + image_tags = image_info.get('tags', '') + if tag_logic == 'AND': + # 所有标签都必须匹配 + if not all(tag in image_tags for tag in tags.split(',')): + continue + else: + # 任意标签匹配即可 + if not any(tag in image_tags for tag in tags.split(',')): + continue + + # 添加到结果列表 + result_list.append({ + 'cont_sign': cont_sign, + 'score': 0.95, # 模拟相似度分数 + 'brief': image_info.get('brief', '') + }) + + return { + 'log_id': 987654321, + 'result_num': len(result_list), + 'result': result_list, + 'has_more': False, + 'error_code': 0, + 'error_msg': 'success' + } + + def update_image(self, cont_sign, brief=None, tags=None): + """ + 模拟更新图片信息 + + Args: + cont_sign: 图片ID + brief: 新的图片摘要信息 + tags: 新的分类信息 + + Returns: + dict: 模拟API返回的结果 + """ + if cont_sign not in self.images: + return { + 'log_id': 135792468, + 'error_code': 222202, + 'error_msg': '图片不存在' + } + + # 更新图片信息 + if brief: + self.images[cont_sign]['brief'] = brief + if tags: + self.images[cont_sign]['tags'] = tags + + return { + 'log_id': 135792468, + 'error_code': 0, + 'error_msg': 'success' + } + + def delete_image(self, cont_sign): + """ + 模拟删除图片 + + Args: + cont_sign: 图片ID + + Returns: + dict: 模拟API返回的结果 + """ + if cont_sign not in self.images: + return { + 'log_id': 246813579, + 'error_code': 222202, + 'error_msg': '图片不存在' + } + + # 删除图片 + del self.images[cont_sign] + + return { + 'log_id': 246813579, + 'error_code': 0, + 'error_msg': 'success' + } diff --git a/poetry.lock b/poetry.lock new file mode 100644 index 0000000..33eb4c0 --- /dev/null +++ b/poetry.lock @@ -0,0 +1,334 @@ +# This file is automatically @generated by Poetry 2.1.2 and should not be changed by hand. + +[[package]] +name = "certifi" +version = "2023.5.7" +description = "Python package for providing Mozilla's CA Bundle." +optional = false +python-versions = ">=3.6" +groups = ["main"] +files = [ + {file = "certifi-2023.5.7-py3-none-any.whl", hash = "sha256:c6c2e98f5c7869efca1f8916fed228dd91539f9f1b444c314c06eef02980c716"}, + {file = "certifi-2023.5.7.tar.gz", hash = "sha256:0f0d56dc5a6ad56fd4ba36484d6cc34451e1c6548c61daad8c320169f91eddc7"}, +] + +[[package]] +name = "charset-normalizer" +version = "2.0.12" +description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." +optional = false +python-versions = ">=3.5.0" +groups = ["main"] +files = [ + {file = "charset-normalizer-2.0.12.tar.gz", hash = "sha256:2857e29ff0d34db842cd7ca3230549d1a697f96ee6d3fb071cfa6c7393832597"}, + {file = "charset_normalizer-2.0.12-py3-none-any.whl", hash = "sha256:6881edbebdb17b39b4eaaa821b438bf6eddffb4468cf344f09f89def34a8b1df"}, +] + +[package.extras] +unicode-backport = ["unicodedata2"] + +[[package]] +name = "click" +version = "8.1.3" +description = "Composable command line interface toolkit" +optional = false +python-versions = ">=3.7" +groups = ["main"] +files = [ + {file = "click-8.1.3-py3-none-any.whl", hash = "sha256:bb4d8133cb15a609f44e8213d9b391b0809795062913b383c62be0ee95b1db48"}, + {file = "click-8.1.3.tar.gz", hash = "sha256:7682dc8afb30297001674575ea00d1814d808d6a36af415a82bd481d37ba7b8e"}, +] + +[package.dependencies] +colorama = {version = "*", markers = "platform_system == \"Windows\""} + +[[package]] +name = "colorama" +version = "0.4.6" +description = "Cross-platform colored terminal text." +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" +groups = ["main"] +markers = "platform_system == \"Windows\"" +files = [ + {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, + {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, +] + +[[package]] +name = "flask" +version = "2.0.1" +description = "A simple framework for building complex web applications." +optional = false +python-versions = ">=3.6" +groups = ["main"] +files = [ + {file = "Flask-2.0.1-py3-none-any.whl", hash = "sha256:a6209ca15eb63fc9385f38e452704113d679511d9574d09b2cf9183ae7d20dc9"}, + {file = "Flask-2.0.1.tar.gz", hash = "sha256:1c4c257b1892aec1398784c63791cbaa43062f1f7aeb555c4da961b20ee68f55"}, +] + +[package.dependencies] +click = ">=7.1.2" +itsdangerous = ">=2.0" +Jinja2 = ">=3.0" +Werkzeug = ">=2.0" + +[package.extras] +async = ["asgiref (>=3.2)"] +dotenv = ["python-dotenv"] + +[[package]] +name = "flask-cors" +version = "3.0.10" +description = "A Flask extension adding a decorator for CORS support" +optional = false +python-versions = "*" +groups = ["main"] +files = [ + {file = "Flask-Cors-3.0.10.tar.gz", hash = "sha256:b60839393f3b84a0f3746f6cdca56c1ad7426aa738b70d6c61375857823181de"}, + {file = "Flask_Cors-3.0.10-py2.py3-none-any.whl", hash = "sha256:74efc975af1194fc7891ff5cd85b0f7478be4f7f59fe158102e91abb72bb4438"}, +] + +[package.dependencies] +Flask = ">=0.9" +Six = "*" + +[[package]] +name = "idna" +version = "3.4" +description = "Internationalized Domain Names in Applications (IDNA)" +optional = false +python-versions = ">=3.5" +groups = ["main"] +files = [ + {file = "idna-3.4-py3-none-any.whl", hash = "sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2"}, + {file = "idna-3.4.tar.gz", hash = "sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4"}, +] + +[[package]] +name = "itsdangerous" +version = "2.1.2" +description = "Safely pass data to untrusted environments and back." +optional = false +python-versions = ">=3.7" +groups = ["main"] +files = [ + {file = "itsdangerous-2.1.2-py3-none-any.whl", hash = "sha256:2c2349112351b88699d8d4b6b075022c0808887cb7ad10069318a8b0bc88db44"}, + {file = "itsdangerous-2.1.2.tar.gz", hash = "sha256:5dbbc68b317e5e42f327f9021763545dc3fc3bfe22e6deb96aaf1fc38874156a"}, +] + +[[package]] +name = "jinja2" +version = "3.1.2" +description = "A very fast and expressive template engine." +optional = false +python-versions = ">=3.7" +groups = ["main"] +files = [ + {file = "Jinja2-3.1.2-py3-none-any.whl", hash = "sha256:6088930bfe239f0e6710546ab9c19c9ef35e29792895fed6e6e31a023a182a61"}, + {file = "Jinja2-3.1.2.tar.gz", hash = "sha256:31351a702a408a9e7595a8fc6150fc3f43bb6bf7e319770cbc0db9df9437e852"}, +] + +[package.dependencies] +MarkupSafe = ">=2.0" + +[package.extras] +i18n = ["Babel (>=2.7)"] + +[[package]] +name = "markupsafe" +version = "2.1.2" +description = "Safely add untrusted strings to HTML/XML markup." +optional = false +python-versions = ">=3.7" +groups = ["main"] +files = [ + {file = "MarkupSafe-2.1.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:665a36ae6f8f20a4676b53224e33d456a6f5a72657d9c83c2aa00765072f31f7"}, + {file = "MarkupSafe-2.1.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:340bea174e9761308703ae988e982005aedf427de816d1afe98147668cc03036"}, + {file = "MarkupSafe-2.1.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22152d00bf4a9c7c83960521fc558f55a1adbc0631fbb00a9471e097b19d72e1"}, + {file = "MarkupSafe-2.1.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:28057e985dace2f478e042eaa15606c7efccb700797660629da387eb289b9323"}, + {file = "MarkupSafe-2.1.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ca244fa73f50a800cf8c3ebf7fd93149ec37f5cb9596aa8873ae2c1d23498601"}, + {file = "MarkupSafe-2.1.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:d9d971ec1e79906046aa3ca266de79eac42f1dbf3612a05dc9368125952bd1a1"}, + {file = "MarkupSafe-2.1.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:7e007132af78ea9df29495dbf7b5824cb71648d7133cf7848a2a5dd00d36f9ff"}, + {file = "MarkupSafe-2.1.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:7313ce6a199651c4ed9d7e4cfb4aa56fe923b1adf9af3b420ee14e6d9a73df65"}, + {file = "MarkupSafe-2.1.2-cp310-cp310-win32.whl", hash = "sha256:c4a549890a45f57f1ebf99c067a4ad0cb423a05544accaf2b065246827ed9603"}, + {file = "MarkupSafe-2.1.2-cp310-cp310-win_amd64.whl", hash = "sha256:835fb5e38fd89328e9c81067fd642b3593c33e1e17e2fdbf77f5676abb14a156"}, + {file = "MarkupSafe-2.1.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:2ec4f2d48ae59bbb9d1f9d7efb9236ab81429a764dedca114f5fdabbc3788013"}, + {file = "MarkupSafe-2.1.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:608e7073dfa9e38a85d38474c082d4281f4ce276ac0010224eaba11e929dd53a"}, + {file = "MarkupSafe-2.1.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:65608c35bfb8a76763f37036547f7adfd09270fbdbf96608be2bead319728fcd"}, + {file = "MarkupSafe-2.1.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f2bfb563d0211ce16b63c7cb9395d2c682a23187f54c3d79bfec33e6705473c6"}, + {file = "MarkupSafe-2.1.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:da25303d91526aac3672ee6d49a2f3db2d9502a4a60b55519feb1a4c7714e07d"}, + {file = "MarkupSafe-2.1.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:9cad97ab29dfc3f0249b483412c85c8ef4766d96cdf9dcf5a1e3caa3f3661cf1"}, + {file = "MarkupSafe-2.1.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:085fd3201e7b12809f9e6e9bc1e5c96a368c8523fad5afb02afe3c051ae4afcc"}, + {file = "MarkupSafe-2.1.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:1bea30e9bf331f3fef67e0a3877b2288593c98a21ccb2cf29b74c581a4eb3af0"}, + {file = "MarkupSafe-2.1.2-cp311-cp311-win32.whl", hash = "sha256:7df70907e00c970c60b9ef2938d894a9381f38e6b9db73c5be35e59d92e06625"}, + {file = "MarkupSafe-2.1.2-cp311-cp311-win_amd64.whl", hash = "sha256:e55e40ff0cc8cc5c07996915ad367fa47da6b3fc091fdadca7f5403239c5fec3"}, + {file = "MarkupSafe-2.1.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:a6e40afa7f45939ca356f348c8e23048e02cb109ced1eb8420961b2f40fb373a"}, + {file = "MarkupSafe-2.1.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cf877ab4ed6e302ec1d04952ca358b381a882fbd9d1b07cccbfd61783561f98a"}, + {file = "MarkupSafe-2.1.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:63ba06c9941e46fa389d389644e2d8225e0e3e5ebcc4ff1ea8506dce646f8c8a"}, + {file = "MarkupSafe-2.1.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f1cd098434e83e656abf198f103a8207a8187c0fc110306691a2e94a78d0abb2"}, + {file = "MarkupSafe-2.1.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:55f44b440d491028addb3b88f72207d71eeebfb7b5dbf0643f7c023ae1fba619"}, + {file = "MarkupSafe-2.1.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:a6f2fcca746e8d5910e18782f976489939d54a91f9411c32051b4aab2bd7c513"}, + {file = "MarkupSafe-2.1.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:0b462104ba25f1ac006fdab8b6a01ebbfbce9ed37fd37fd4acd70c67c973e460"}, + {file = "MarkupSafe-2.1.2-cp37-cp37m-win32.whl", hash = "sha256:7668b52e102d0ed87cb082380a7e2e1e78737ddecdde129acadb0eccc5423859"}, + {file = "MarkupSafe-2.1.2-cp37-cp37m-win_amd64.whl", hash = "sha256:6d6607f98fcf17e534162f0709aaad3ab7a96032723d8ac8750ffe17ae5a0666"}, + {file = "MarkupSafe-2.1.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:a806db027852538d2ad7555b203300173dd1b77ba116de92da9afbc3a3be3eed"}, + {file = "MarkupSafe-2.1.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:a4abaec6ca3ad8660690236d11bfe28dfd707778e2442b45addd2f086d6ef094"}, + {file = "MarkupSafe-2.1.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f03a532d7dee1bed20bc4884194a16160a2de9ffc6354b3878ec9682bb623c54"}, + {file = "MarkupSafe-2.1.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4cf06cdc1dda95223e9d2d3c58d3b178aa5dacb35ee7e3bbac10e4e1faacb419"}, + {file = "MarkupSafe-2.1.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:22731d79ed2eb25059ae3df1dfc9cb1546691cc41f4e3130fe6bfbc3ecbbecfa"}, + {file = "MarkupSafe-2.1.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:f8ffb705ffcf5ddd0e80b65ddf7bed7ee4f5a441ea7d3419e861a12eaf41af58"}, + {file = "MarkupSafe-2.1.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:8db032bf0ce9022a8e41a22598eefc802314e81b879ae093f36ce9ddf39ab1ba"}, + {file = "MarkupSafe-2.1.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:2298c859cfc5463f1b64bd55cb3e602528db6fa0f3cfd568d3605c50678f8f03"}, + {file = "MarkupSafe-2.1.2-cp38-cp38-win32.whl", hash = "sha256:50c42830a633fa0cf9e7d27664637532791bfc31c731a87b202d2d8ac40c3ea2"}, + {file = "MarkupSafe-2.1.2-cp38-cp38-win_amd64.whl", hash = "sha256:bb06feb762bade6bf3c8b844462274db0c76acc95c52abe8dbed28ae3d44a147"}, + {file = "MarkupSafe-2.1.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:99625a92da8229df6d44335e6fcc558a5037dd0a760e11d84be2260e6f37002f"}, + {file = "MarkupSafe-2.1.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:8bca7e26c1dd751236cfb0c6c72d4ad61d986e9a41bbf76cb445f69488b2a2bd"}, + {file = "MarkupSafe-2.1.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:40627dcf047dadb22cd25ea7ecfe9cbf3bbbad0482ee5920b582f3809c97654f"}, + {file = "MarkupSafe-2.1.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:40dfd3fefbef579ee058f139733ac336312663c6706d1163b82b3003fb1925c4"}, + {file = "MarkupSafe-2.1.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:090376d812fb6ac5f171e5938e82e7f2d7adc2b629101cec0db8b267815c85e2"}, + {file = "MarkupSafe-2.1.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:2e7821bffe00aa6bd07a23913b7f4e01328c3d5cc0b40b36c0bd81d362faeb65"}, + {file = "MarkupSafe-2.1.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:c0a33bc9f02c2b17c3ea382f91b4db0e6cde90b63b296422a939886a7a80de1c"}, + {file = "MarkupSafe-2.1.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:b8526c6d437855442cdd3d87eede9c425c4445ea011ca38d937db299382e6fa3"}, + {file = "MarkupSafe-2.1.2-cp39-cp39-win32.whl", hash = "sha256:137678c63c977754abe9086a3ec011e8fd985ab90631145dfb9294ad09c102a7"}, + {file = "MarkupSafe-2.1.2-cp39-cp39-win_amd64.whl", hash = "sha256:0576fe974b40a400449768941d5d0858cc624e3249dfd1e0c33674e5c7ca7aed"}, + {file = "MarkupSafe-2.1.2.tar.gz", hash = "sha256:abcabc8c2b26036d62d4c746381a6f7cf60aafcc653198ad678306986b09450d"}, +] + +[[package]] +name = "pillow" +version = "8.3.1" +description = "Python Imaging Library (Fork)" +optional = false +python-versions = ">=3.6" +groups = ["main"] +files = [ + {file = "Pillow-8.3.1-1-cp36-cp36m-win_amd64.whl", hash = "sha256:fd7eef578f5b2200d066db1b50c4aa66410786201669fb76d5238b007918fb24"}, + {file = "Pillow-8.3.1-1-cp37-cp37m-win_amd64.whl", hash = "sha256:75e09042a3b39e0ea61ce37e941221313d51a9c26b8e54e12b3ececccb71718a"}, + {file = "Pillow-8.3.1-1-cp38-cp38-win_amd64.whl", hash = "sha256:c0e0550a404c69aab1e04ae89cca3e2a042b56ab043f7f729d984bf73ed2a093"}, + {file = "Pillow-8.3.1-1-cp39-cp39-win_amd64.whl", hash = "sha256:479ab11cbd69612acefa8286481f65c5dece2002ffaa4f9db62682379ca3bb77"}, + {file = "Pillow-8.3.1-1-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:f156d6ecfc747ee111c167f8faf5f4953761b5e66e91a4e6767e548d0f80129c"}, + {file = "Pillow-8.3.1-cp36-cp36m-macosx_10_10_x86_64.whl", hash = "sha256:196560dba4da7a72c5e7085fccc5938ab4075fd37fe8b5468869724109812edd"}, + {file = "Pillow-8.3.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:29c9569049d04aaacd690573a0398dbd8e0bf0255684fee512b413c2142ab723"}, + {file = "Pillow-8.3.1-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c088a000dfdd88c184cc7271bfac8c5b82d9efa8637cd2b68183771e3cf56f04"}, + {file = "Pillow-8.3.1-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:fc214a6b75d2e0ea7745488da7da3c381f41790812988c7a92345978414fad37"}, + {file = "Pillow-8.3.1-cp36-cp36m-win32.whl", hash = "sha256:a17ca41f45cf78c2216ebfab03add7cc350c305c38ff34ef4eef66b7d76c5229"}, + {file = "Pillow-8.3.1-cp36-cp36m-win_amd64.whl", hash = "sha256:67b3666b544b953a2777cb3f5a922e991be73ab32635666ee72e05876b8a92de"}, + {file = "Pillow-8.3.1-cp37-cp37m-macosx_10_10_x86_64.whl", hash = "sha256:ff04c373477723430dce2e9d024c708a047d44cf17166bf16e604b379bf0ca14"}, + {file = "Pillow-8.3.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9364c81b252d8348e9cc0cb63e856b8f7c1b340caba6ee7a7a65c968312f7dab"}, + {file = "Pillow-8.3.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a2f381932dca2cf775811a008aa3027671ace723b7a38838045b1aee8669fdcf"}, + {file = "Pillow-8.3.1-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:d0da39795049a9afcaadec532e7b669b5ebbb2a9134576ebcc15dd5bdae33cc0"}, + {file = "Pillow-8.3.1-cp37-cp37m-win32.whl", hash = "sha256:2b6dfa068a8b6137da34a4936f5a816aba0ecc967af2feeb32c4393ddd671cba"}, + {file = "Pillow-8.3.1-cp37-cp37m-win_amd64.whl", hash = "sha256:a4eef1ff2d62676deabf076f963eda4da34b51bc0517c70239fafed1d5b51500"}, + {file = "Pillow-8.3.1-cp38-cp38-macosx_10_10_x86_64.whl", hash = "sha256:660a87085925c61a0dcc80efb967512ac34dbb256ff7dd2b9b4ee8dbdab58cf4"}, + {file = "Pillow-8.3.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:15a2808e269a1cf2131930183dcc0419bc77bb73eb54285dde2706ac9939fa8e"}, + {file = "Pillow-8.3.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:969cc558cca859cadf24f890fc009e1bce7d7d0386ba7c0478641a60199adf79"}, + {file = "Pillow-8.3.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2ee77c14a0299d0541d26f3d8500bb57e081233e3fa915fa35abd02c51fa7fae"}, + {file = "Pillow-8.3.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:c11003197f908878164f0e6da15fce22373ac3fc320cda8c9d16e6bba105b844"}, + {file = "Pillow-8.3.1-cp38-cp38-win32.whl", hash = "sha256:3f08bd8d785204149b5b33e3b5f0ebbfe2190ea58d1a051c578e29e39bfd2367"}, + {file = "Pillow-8.3.1-cp38-cp38-win_amd64.whl", hash = "sha256:70af7d222df0ff81a2da601fab42decb009dc721545ed78549cb96e3a1c5f0c8"}, + {file = "Pillow-8.3.1-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:37730f6e68bdc6a3f02d2079c34c532330d206429f3cee651aab6b66839a9f0e"}, + {file = "Pillow-8.3.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:4bc3c7ef940eeb200ca65bd83005eb3aae8083d47e8fcbf5f0943baa50726856"}, + {file = "Pillow-8.3.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c35d09db702f4185ba22bb33ef1751ad49c266534339a5cebeb5159d364f6f82"}, + {file = "Pillow-8.3.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:0b2efa07f69dc395d95bb9ef3299f4ca29bcb2157dc615bae0b42c3c20668ffc"}, + {file = "Pillow-8.3.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:cc866706d56bd3a7dbf8bac8660c6f6462f2f2b8a49add2ba617bc0c54473d83"}, + {file = "Pillow-8.3.1-cp39-cp39-win32.whl", hash = "sha256:9a211b663cf2314edbdb4cf897beeb5c9ee3810d1d53f0e423f06d6ebbf9cd5d"}, + {file = "Pillow-8.3.1-cp39-cp39-win_amd64.whl", hash = "sha256:c2a5ff58751670292b406b9f06e07ed1446a4b13ffced6b6cab75b857485cbc8"}, + {file = "Pillow-8.3.1-pp36-pypy36_pp73-macosx_10_10_x86_64.whl", hash = "sha256:c379425c2707078dfb6bfad2430728831d399dc95a7deeb92015eb4c92345eaf"}, + {file = "Pillow-8.3.1-pp36-pypy36_pp73-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:114f816e4f73f9ec06997b2fde81a92cbf0777c9e8f462005550eed6bae57e63"}, + {file = "Pillow-8.3.1-pp36-pypy36_pp73-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:8960a8a9f4598974e4c2aeb1bff9bdd5db03ee65fd1fce8adf3223721aa2a636"}, + {file = "Pillow-8.3.1-pp37-pypy37_pp73-macosx_10_10_x86_64.whl", hash = "sha256:147bd9e71fb9dcf08357b4d530b5167941e222a6fd21f869c7911bac40b9994d"}, + {file = "Pillow-8.3.1-pp37-pypy37_pp73-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:1fd5066cd343b5db88c048d971994e56b296868766e461b82fa4e22498f34d77"}, + {file = "Pillow-8.3.1-pp37-pypy37_pp73-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:f4ebde71785f8bceb39dcd1e7f06bcc5d5c3cf48b9f69ab52636309387b097c8"}, + {file = "Pillow-8.3.1-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:1c03e24be975e2afe70dfc5da6f187eea0b49a68bb2b69db0f30a61b7031cee4"}, + {file = "Pillow-8.3.1.tar.gz", hash = "sha256:2cac53839bfc5cece8fdbe7f084d5e3ee61e1303cccc86511d351adcb9e2c792"}, +] + +[[package]] +name = "python-dotenv" +version = "0.19.0" +description = "Read key-value pairs from a .env file and set them as environment variables" +optional = false +python-versions = ">=3.5" +groups = ["main"] +files = [ + {file = "python-dotenv-0.19.0.tar.gz", hash = "sha256:f521bc2ac9a8e03c736f62911605c5d83970021e3fa95b37d769e2bbbe9b6172"}, + {file = "python_dotenv-0.19.0-py2.py3-none-any.whl", hash = "sha256:aae25dc1ebe97c420f50b81fb0e5c949659af713f31fdb63c749ca68748f34b1"}, +] + +[package.extras] +cli = ["click (>=5.0)"] + +[[package]] +name = "requests" +version = "2.26.0" +description = "Python HTTP for Humans." +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*" +groups = ["main"] +files = [ + {file = "requests-2.26.0-py2.py3-none-any.whl", hash = "sha256:6c1246513ecd5ecd4528a0906f910e8f0f9c6b8ec72030dc9fd154dc1a6efd24"}, + {file = "requests-2.26.0.tar.gz", hash = "sha256:b8aa58f8cf793ffd8782d3d8cb19e66ef36f7aba4353eec859e74678b01b07a7"}, +] + +[package.dependencies] +certifi = ">=2017.4.17" +charset-normalizer = {version = ">=2.0.0,<2.1.0", markers = "python_version >= \"3\""} +idna = {version = ">=2.5,<4", markers = "python_version >= \"3\""} +urllib3 = ">=1.21.1,<1.27" + +[package.extras] +socks = ["PySocks (>=1.5.6,!=1.5.7)", "win-inet-pton ; sys_platform == \"win32\" and python_version == \"2.7\""] +use-chardet-on-py3 = ["chardet (>=3.0.2,<5)"] + +[[package]] +name = "six" +version = "1.16.0" +description = "Python 2 and 3 compatibility utilities" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" +groups = ["main"] +files = [ + {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, + {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, +] + +[[package]] +name = "urllib3" +version = "1.26.16" +description = "HTTP library with thread-safe connection pooling, file post, and more." +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*" +groups = ["main"] +files = [ + {file = "urllib3-1.26.16-py2.py3-none-any.whl", hash = "sha256:8d36afa7616d8ab714608411b4a3b13e58f463aee519024578e062e141dce20f"}, + {file = "urllib3-1.26.16.tar.gz", hash = "sha256:8f135f6502756bde6b2a9b28989df5fbe87c9970cecaa69041edcce7f0589b14"}, +] + +[package.extras] +brotli = ["brotli (>=1.0.9) ; (os_name != \"nt\" or python_version >= \"3\") and platform_python_implementation == \"CPython\"", "brotlicffi (>=0.8.0) ; (os_name != \"nt\" or python_version >= \"3\") and platform_python_implementation != \"CPython\"", "brotlipy (>=0.6.0) ; os_name == \"nt\" and python_version < \"3\""] +secure = ["certifi", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "ipaddress ; python_version == \"2.7\"", "pyOpenSSL (>=0.14)", "urllib3-secure-extra"] +socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"] + +[[package]] +name = "werkzeug" +version = "2.2.3" +description = "The comprehensive WSGI web application library." +optional = false +python-versions = ">=3.7" +groups = ["main"] +files = [ + {file = "Werkzeug-2.2.3-py3-none-any.whl", hash = "sha256:56433961bc1f12533306c624f3be5e744389ac61d722175d543e1751285da612"}, + {file = "Werkzeug-2.2.3.tar.gz", hash = "sha256:2e1ccc9417d4da358b9de6f174e3ac094391ea1d4fbef2d667865d819dfd0afe"}, +] + +[package.dependencies] +MarkupSafe = ">=2.1.1" + +[package.extras] +watchdog = ["watchdog"] + +[metadata] +lock-version = "2.1" +python-versions = "^3.9" +content-hash = "c80c5641fe75055f2a80acf7a46ab694e2c875a0bc58f86bb52205c2dd34e5af" diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..fda54d1 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,19 @@ +[tool.poetry] +name = "imgsearcher" +version = "0.1.0" +description = "基于百度AI开放平台的相似图片搜索系统" +authors = ["Your Name "] +readme = "README.md" +package-mode = false + +[tool.poetry.dependencies] +python = "^3.9" +flask = "^2.0.1" +requests = "^2.26.0" +python-dotenv = "^0.19.0" +pillow = "^8.3.1" +flask-cors = "^3.0.10" + +[build-system] +requires = ["poetry-core"] +build-backend = "poetry.core.masonry.api" diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..c2b1ee1 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,5 @@ +flask==2.0.1 +requests==2.26.0 +python-dotenv==0.19.0 +Pillow==8.3.1 +flask-cors==3.0.10 diff --git a/run.sh b/run.sh new file mode 100644 index 0000000..63b5f93 --- /dev/null +++ b/run.sh @@ -0,0 +1,8 @@ +#!/bin/bash + +# 激活conda环境 +eval "$(conda shell.bash hook)" +conda activate imgsearcher + +# 运行应用 +poetry run python app.py diff --git a/test_baidu_api.py b/test_baidu_api.py new file mode 100644 index 0000000..a04b8bb --- /dev/null +++ b/test_baidu_api.py @@ -0,0 +1,187 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +百度图像搜索API测试脚本 +简单测试API功能,打印结果 +支持实际API和模拟模式 +""" + +import os +import json +import argparse +from dotenv import load_dotenv +from app.api.baidu_image_search import BaiduImageSearch +from app.api.image_utils import ImageUtils + +# 导入模拟类 +from mock_baidu_api import MockBaiduImageSearch + +# 全局变量,用于存储测试过程中的图片ID +test_cont_sign = None + +def parse_arguments(): + """解析命令行参数""" + parser = argparse.ArgumentParser(description='百度图像搜索API测试脚本') + parser.add_argument('--mock', action='store_true', help='使用模拟模式,不连接实际API') + return parser.parse_args() + +def check_api_keys(): + """检查API密钥是否已配置""" + api_key = os.getenv('BAIDU_API_KEY') + secret_key = os.getenv('BAIDU_SECRET_KEY') + + if not api_key or not secret_key: + print("\n\033[91m错误: API密钥未配置\033[0m") + print("\n请在.env文件中添加以下内容:") + print("BAIDU_API_KEY=你的API密钥") + print("BAIDU_SECRET_KEY=你的密钥") + print("\n您可以从百度AI开放平台获取密钥: https://ai.baidu.com/") + print("\n或者使用 --mock 参数运行模拟模式: python test_baidu_api.py --mock") + return False + + print(f"API密钥: {api_key[:4]}...") + print(f"Secret密钥: {secret_key[:4]}...") + return True + +def main(): + # 解析命令行参数 + args = parse_arguments() + + # 加载环境变量 + print("加载环境变量...") + load_dotenv() + + # 判断是否使用模拟模式 + if args.mock: + print("\n\033[93m注意: 使用模拟模式,不连接实际API\033[0m") + api = MockBaiduImageSearch() + + # 测试添加图片 + test_add_image(api) + + # 测试搜索图片 + test_search_image(api) + + # 测试更新图片 + test_update_image(api) + + # 测试删除图片 + test_delete_image(api) + return + + # 如果不是模拟模式,检查API密钥 + if not check_api_keys(): + return + + # 初始化实际API + print("\n初始化百度图像搜索API...") + try: + api = BaiduImageSearch() + if api.access_token: + print(f"获取到的Access Token: {api.access_token[:10]}...(已截断)") + + # 测试添加图片 + test_add_image(api) + + # 测试搜索图片 + test_search_image(api) + + # 测试更新图片 + test_update_image(api) + + # 测试删除图片 + test_delete_image(api) + else: + print("\033[91m错误: 无法获取Access Token\033[0m") + print("\n尝试使用模拟模式: python test_baidu_api.py --mock") + except Exception as e: + print(f"\033[91m错误: {e}\033[0m") + print("\n尝试使用模拟模式: python test_baidu_api.py --mock") + +def test_add_image(api): + """测试添加图片功能""" + print("\n===== 测试添加图片 =====") + + # 使用URL方式添加图片 + test_image_url = "https://www.baidu.com/img/PCtm_d9c8750bed0b3c7d089fa7d55720d6cf.png" # 百度logo + brief = json.dumps({"name": "测试图片", "id": "test001"}, ensure_ascii=False) + tags = "测试,图片" + + try: + result = api.add_image( + url=test_image_url, + brief=brief, + tags=tags + ) + print("添加图片结果:") + print(json.dumps(result, indent=2, ensure_ascii=False)) + + # 保存contSign用于后续测试 + if result.get("cont_sign"): + global test_cont_sign + test_cont_sign = result.get("cont_sign") + print(f"已保存图片ID: {test_cont_sign} 用于后续测试") + except Exception as e: + print(f"添加图片失败: {e}") + +def test_search_image(api): + """测试搜索图片功能""" + print("\n===== 测试搜索图片 =====") + + # 使用URL方式搜索图片 + test_image_url = "https://www.baidu.com/img/PCtm_d9c8750bed0b3c7d089fa7d55720d6cf.png" # 百度logo + + try: + result = api.search_image( + url=test_image_url, + tags="测试" + ) + print("搜索图片结果:") + print(f"找到 {len(result.get('result', []))} 个匹配项") + + # 只打印前2个结果 + for i, item in enumerate(result.get("result", [])[:2]): + print(f"结果 {i+1}:") + print(f" 相似度: {item.get('score', 0)}") + print(f" 简介: {item.get('brief', '')}") + except Exception as e: + print(f"搜索图片失败: {e}") + +def test_update_image(api): + """测试更新图片功能""" + print("\n===== 测试更新图片 =====") + + global test_cont_sign + if not test_cont_sign: + print("没有可用的图片ID,跳过更新图片测试") + return + + try: + new_brief = json.dumps({"name": "更新后的测试图片", "id": "test001"}, ensure_ascii=False) + result = api.update_image(test_cont_sign, new_brief) + print("更新图片结果:") + print(json.dumps(result, indent=2, ensure_ascii=False)) + except Exception as e: + print(f"更新图片失败: {e}") + +def test_delete_image(api): + """测试删除图片功能""" + print("\n===== 测试删除图片 =====") + + global test_cont_sign + if not test_cont_sign: + print("没有可用的图片ID,跳过删除图片测试") + return + + try: + result = api.delete_image(test_cont_sign) + print("删除图片结果:") + print(json.dumps(result, indent=2, ensure_ascii=False)) + # 清除已删除的图片ID + test_cont_sign = None + except Exception as e: + print(f"删除图片失败: {e}") + +if __name__ == "__main__": + main()