将后端的图片下载到数据集中
This commit is contained in:
@@ -0,0 +1,298 @@
|
|||||||
|
"""
|
||||||
|
批量下载所有图片到dataset目录
|
||||||
|
自动按类别分类到对应文件夹
|
||||||
|
"""
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
from datetime import datetime
|
||||||
|
from tqdm import tqdm
|
||||||
|
import requests
|
||||||
|
|
||||||
|
# 添加当前目录到路径
|
||||||
|
sys.path.append(os.path.dirname(os.path.abspath(__file__)))
|
||||||
|
|
||||||
|
from database import MySQLManager, SQLiteManager
|
||||||
|
from utils import ImageDownloader
|
||||||
|
from config import IMAGE_URL_PREFIX, DOWNLOAD_TIMEOUT, DOWNLOAD_THREADS
|
||||||
|
|
||||||
|
|
||||||
|
def sanitize_folder_name(name: str) -> str:
|
||||||
|
"""清理文件夹名称,移除非法字符"""
|
||||||
|
# 保留中文字符、字母、数字、括号、方括号、WP代码
|
||||||
|
import re
|
||||||
|
# 移除或替换非法字符
|
||||||
|
name = name.replace('/', '_').replace('\\', '_')
|
||||||
|
name = name.replace(':', '_').replace('*', '_')
|
||||||
|
name = name.replace('?', '_').replace('"', '_')
|
||||||
|
name = name.replace('<', '_').replace('>', '_')
|
||||||
|
name = name.replace('|', '_')
|
||||||
|
return name.strip()
|
||||||
|
|
||||||
|
|
||||||
|
def get_goods_code(goods_name: str) -> str:
|
||||||
|
"""从goods_name中提取WP代码(如果存在)"""
|
||||||
|
import re
|
||||||
|
# 匹配WP开头的代码,如 WP2025110700043
|
||||||
|
match = re.search(r'WP\d+', goods_name)
|
||||||
|
if match:
|
||||||
|
return match.group(0)
|
||||||
|
return ''
|
||||||
|
|
||||||
|
|
||||||
|
def batch_download_all(
|
||||||
|
datasource: str = '食材',
|
||||||
|
output_dir: str = None,
|
||||||
|
max_images: int = None,
|
||||||
|
skip_existing: bool = True
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
批量下载所有图片
|
||||||
|
|
||||||
|
Args:
|
||||||
|
datasource: 数据源('食材'/'菜品'/'半加工')
|
||||||
|
output_dir: 输出目录,如果为None则使用dataset目录
|
||||||
|
max_images: 最大下载数量(用于测试),None表示下载全部
|
||||||
|
skip_existing: 是否跳过已存在的文件
|
||||||
|
"""
|
||||||
|
print("="*80)
|
||||||
|
print(f"批量下载工具 - 数据源: {datasource}")
|
||||||
|
print("="*80)
|
||||||
|
|
||||||
|
# 初始化数据库管理器
|
||||||
|
print("\n[1/5] 连接数据库...")
|
||||||
|
mysql_manager = MySQLManager()
|
||||||
|
sqlite_manager = SQLiteManager()
|
||||||
|
|
||||||
|
# 查询所有图片数据
|
||||||
|
print("\n[2/5] 查询所有图片数据...")
|
||||||
|
all_data = []
|
||||||
|
page = 1
|
||||||
|
page_size = 1000 # 每次查询1000条
|
||||||
|
|
||||||
|
while True:
|
||||||
|
data_list, total_count = mysql_manager.query_images(
|
||||||
|
datasource=datasource,
|
||||||
|
page=page,
|
||||||
|
page_size=page_size
|
||||||
|
)
|
||||||
|
|
||||||
|
if not data_list:
|
||||||
|
break
|
||||||
|
|
||||||
|
all_data.extend(data_list)
|
||||||
|
print(f" 已查询: {len(all_data)} / {total_count} 张图片")
|
||||||
|
|
||||||
|
if len(all_data) >= total_count:
|
||||||
|
break
|
||||||
|
|
||||||
|
if max_images and len(all_data) >= max_images:
|
||||||
|
all_data = all_data[:max_images]
|
||||||
|
break
|
||||||
|
|
||||||
|
page += 1
|
||||||
|
|
||||||
|
total_images = len(all_data)
|
||||||
|
print(f"\n✓ 共找到 {total_images} 张图片")
|
||||||
|
|
||||||
|
if total_images == 0:
|
||||||
|
print("没有找到图片,退出")
|
||||||
|
return
|
||||||
|
|
||||||
|
# 确定输出目录
|
||||||
|
if output_dir is None:
|
||||||
|
# 根据数据源确定目录
|
||||||
|
base_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||||
|
if datasource == '食材':
|
||||||
|
output_dir = os.path.join(base_dir, 'dataset', 'WholeIngredientRecognition', 'train')
|
||||||
|
elif datasource == '菜品':
|
||||||
|
output_dir = os.path.join(base_dir, 'dataset', 'DishClassification', 'train')
|
||||||
|
elif datasource == '半加工':
|
||||||
|
output_dir = os.path.join(base_dir, 'dataset', 'ProcessedIngredientRecognition', 'train')
|
||||||
|
else:
|
||||||
|
output_dir = os.path.join(base_dir, 'dataset', datasource, 'train')
|
||||||
|
|
||||||
|
os.makedirs(output_dir, exist_ok=True)
|
||||||
|
print(f"\n[3/5] 输出目录: {output_dir}")
|
||||||
|
|
||||||
|
# 按类别分组
|
||||||
|
print("\n[4/5] 按类别分组...")
|
||||||
|
category_groups = {}
|
||||||
|
for data in all_data:
|
||||||
|
goods_name = data['goods_name']
|
||||||
|
# 清理文件夹名称
|
||||||
|
folder_name = sanitize_folder_name(goods_name)
|
||||||
|
|
||||||
|
if folder_name not in category_groups:
|
||||||
|
category_groups[folder_name] = []
|
||||||
|
category_groups[folder_name].append(data)
|
||||||
|
|
||||||
|
print(f"✓ 共 {len(category_groups)} 个类别")
|
||||||
|
|
||||||
|
# 显示类别统计
|
||||||
|
print("\n类别统计(前10个):")
|
||||||
|
sorted_categories = sorted(category_groups.items(), key=lambda x: len(x[1]), reverse=True)
|
||||||
|
for i, (category, items) in enumerate(sorted_categories[:10], 1):
|
||||||
|
print(f" {i}. {category}: {len(items)} 张")
|
||||||
|
|
||||||
|
# 下载图片
|
||||||
|
print(f"\n[5/5] 开始下载图片(并发数: {DOWNLOAD_THREADS})...")
|
||||||
|
|
||||||
|
success_count = 0
|
||||||
|
failed_count = 0
|
||||||
|
skipped_count = 0
|
||||||
|
error_messages = []
|
||||||
|
|
||||||
|
# 创建进度条
|
||||||
|
pbar = tqdm(total=total_images, desc="下载进度", unit="张")
|
||||||
|
|
||||||
|
def download_single(data: dict) -> tuple[bool, str]:
|
||||||
|
"""下载单张图片"""
|
||||||
|
try:
|
||||||
|
goods_name = data['goods_name']
|
||||||
|
folder_name = sanitize_folder_name(goods_name)
|
||||||
|
category_dir = os.path.join(output_dir, folder_name)
|
||||||
|
os.makedirs(category_dir, exist_ok=True)
|
||||||
|
|
||||||
|
# 构建文件名
|
||||||
|
filename = f"img_{data['id']}.jpg"
|
||||||
|
file_path = os.path.join(category_dir, filename)
|
||||||
|
|
||||||
|
# 检查文件是否已存在
|
||||||
|
if skip_existing and os.path.exists(file_path):
|
||||||
|
return True, "已存在"
|
||||||
|
|
||||||
|
# 下载图片
|
||||||
|
image_url = data['image_url']
|
||||||
|
if not image_url:
|
||||||
|
return False, "图片URL为空"
|
||||||
|
|
||||||
|
response = requests.get(image_url, timeout=DOWNLOAD_TIMEOUT, stream=True)
|
||||||
|
response.raise_for_status()
|
||||||
|
|
||||||
|
# 保存图片
|
||||||
|
with open(file_path, 'wb') as f:
|
||||||
|
for chunk in response.iter_content(chunk_size=8192):
|
||||||
|
if chunk:
|
||||||
|
f.write(chunk)
|
||||||
|
|
||||||
|
# 记录下载历史
|
||||||
|
sqlite_manager.add_download_record(
|
||||||
|
datasource=data['datasource'],
|
||||||
|
goods_id=data.get('goods_id'),
|
||||||
|
goods_name=goods_name,
|
||||||
|
image_url=image_url,
|
||||||
|
local_path=file_path,
|
||||||
|
file_size=os.path.getsize(file_path)
|
||||||
|
)
|
||||||
|
|
||||||
|
return True, None
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
return False, str(e)
|
||||||
|
|
||||||
|
# 使用线程池下载
|
||||||
|
from concurrent.futures import ThreadPoolExecutor, as_completed
|
||||||
|
|
||||||
|
with ThreadPoolExecutor(max_workers=DOWNLOAD_THREADS) as executor:
|
||||||
|
# 提交所有任务
|
||||||
|
futures = {executor.submit(download_single, data): data for data in all_data}
|
||||||
|
|
||||||
|
# 处理结果
|
||||||
|
for future in as_completed(futures):
|
||||||
|
data = futures[future]
|
||||||
|
try:
|
||||||
|
success, error = future.result()
|
||||||
|
if success:
|
||||||
|
if error == "已存在":
|
||||||
|
skipped_count += 1
|
||||||
|
else:
|
||||||
|
success_count += 1
|
||||||
|
else:
|
||||||
|
failed_count += 1
|
||||||
|
error_messages.append(f"{data['goods_name']} (ID: {data['id']}): {error}")
|
||||||
|
|
||||||
|
pbar.update(1)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
failed_count += 1
|
||||||
|
error_messages.append(f"{data['goods_name']} (ID: {data['id']}): {str(e)}")
|
||||||
|
pbar.update(1)
|
||||||
|
|
||||||
|
pbar.close()
|
||||||
|
|
||||||
|
# 打印结果
|
||||||
|
print("\n" + "="*80)
|
||||||
|
print("下载完成!")
|
||||||
|
print("="*80)
|
||||||
|
print(f"成功: {success_count} 张")
|
||||||
|
print(f"跳过: {skipped_count} 张(已存在)")
|
||||||
|
print(f"失败: {failed_count} 张")
|
||||||
|
print(f"总计: {total_images} 张")
|
||||||
|
print(f"\n输出目录: {output_dir}")
|
||||||
|
|
||||||
|
if error_messages:
|
||||||
|
print(f"\n错误信息(前10条):")
|
||||||
|
for msg in error_messages[:10]:
|
||||||
|
print(f" - {msg}")
|
||||||
|
if len(error_messages) > 10:
|
||||||
|
print(f" ... 还有 {len(error_messages) - 10} 条错误")
|
||||||
|
|
||||||
|
# 关闭数据库连接
|
||||||
|
mysql_manager.close()
|
||||||
|
|
||||||
|
print("\n✓ 完成!")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
import argparse
|
||||||
|
|
||||||
|
parser = argparse.ArgumentParser(description='批量下载所有图片到dataset目录')
|
||||||
|
parser.add_argument('--datasource', type=str, default='食材',
|
||||||
|
choices=['食材', '菜品', '半加工', 'all'],
|
||||||
|
help='数据源(默认: 食材,使用 all 下载所有三种)')
|
||||||
|
parser.add_argument('--output', type=str, default=None,
|
||||||
|
help='输出目录(默认: dataset/{datasource}/train)')
|
||||||
|
parser.add_argument('--max', type=int, default=None,
|
||||||
|
help='最大下载数量(用于测试)')
|
||||||
|
parser.add_argument('--no-skip', action='store_true',
|
||||||
|
help='不跳过已存在的文件(重新下载)')
|
||||||
|
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
# 如果选择 all,则依次下载所有三种数据源
|
||||||
|
if args.datasource == 'all':
|
||||||
|
datasources = ['食材', '菜品', '半加工']
|
||||||
|
print("\n" + "="*80)
|
||||||
|
print("将依次下载所有三种数据源:")
|
||||||
|
print(" 1. 食材 -> WholeIngredientRecognition")
|
||||||
|
print(" 2. 菜品 -> DishClassification")
|
||||||
|
print(" 3. 半加工 -> ProcessedIngredientRecognition")
|
||||||
|
print("="*80 + "\n")
|
||||||
|
|
||||||
|
for i, ds in enumerate(datasources, 1):
|
||||||
|
print(f"\n{'='*80}")
|
||||||
|
print(f"[{i}/3] 开始下载: {ds}")
|
||||||
|
print(f"{'='*80}\n")
|
||||||
|
|
||||||
|
batch_download_all(
|
||||||
|
datasource=ds,
|
||||||
|
output_dir=args.output,
|
||||||
|
max_images=args.max,
|
||||||
|
skip_existing=not args.no_skip
|
||||||
|
)
|
||||||
|
|
||||||
|
if i < len(datasources):
|
||||||
|
print(f"\n等待5秒后继续下一个数据源...")
|
||||||
|
import time
|
||||||
|
time.sleep(5)
|
||||||
|
|
||||||
|
print("\n" + "="*80)
|
||||||
|
print("✓ 所有数据源下载完成!")
|
||||||
|
print("="*80)
|
||||||
|
else:
|
||||||
|
batch_download_all(
|
||||||
|
datasource=args.datasource,
|
||||||
|
output_dir=args.output,
|
||||||
|
max_images=args.max,
|
||||||
|
skip_existing=not args.no_skip
|
||||||
|
)
|
||||||
Reference in New Issue
Block a user