《数据清洗的艺术:如何用20行核心逻辑优雅地删除无标签图片》

"这是我在整理训练数据时被逼出来的脚本,分享给和我一样痛苦的你。"

import os

import shutil

from pathlib import Path

def find_extra_image_by_labels(images_folder, labels_folder, backup_folder=None, label_extension=".txt"):

"""

根据标签数据找出多余的图片文件

复制代码
Args:
    images_folder: 图片文件夹路径
    labels_folder: 标签文件夹路径
    backup_folder: 备份文件夹路径(用于存放多余的图片)
    label_extension: 标签文件扩展名
"""
# 设置默认备份文件夹
if backup_folder is None:
    backup_folder = Path(images_folder).parent / "extra_images_backup"

# 确保备份文件夹存在
Path(backup_folder).mkdir(parents=True, exist_ok=True)

print("开始根据标签数据查找多余的图片...")
print(f"图片文件夹: {images_folder}")
print(f"标签文件夹: {labels_folder}")
print(f"备份文件夹: {backup_folder}")
print("-" * 60)

# 获取所有标签文件名(不含扩展名)
label_names = set()

print("收集标签文件名...")
for label_path in Path(labels_folder).rglob(f'*{label_extension}'):
    label_names.add(label_path.stem)

print(f"✓ 找到 {len(label_names)} 个标签文件")

# 获取所有图片文件
image_extensions = ['.jpg', '.jpeg', '.png', '.bmp', '.gif', '.tiff', '.webp']
image_files = []

print("收集图片文件...")
for ext in image_extensions:
    for img_path in Path(images_folder).rglob(f'*{ext}'):
        image_files.append(img_path)
    for img_path in Path(images_folder).rglob(f'*{ext.upper()}'):
        image_files.append(img_path)

print(f"✓ 找到 {len(image_files)} 个图片文件")

# 找出没有对应标签的图片文件
extra_images = []
for img_path in image_files:
    if img_path.stem not in label_names:
        extra_images.append(img_path)

print(f"⚠ 发现 {len(extra_images)} 个多余的图片文件")

# 验证数量
expected_extra = len(image_files) - len(label_names)
if len(extra_images) != expected_extra:
    print(f"⚠ 警告: 实际发现的多余图片数量({len(extra_images)})与预期({expected_extra})不符!")

if not extra_images:
    print("没有发现多余的图片文件!")
    return []

# 显示多余的图片文件
print(f"\n多余的图片文件列表:")
print("-" * 50)
for i, img_path in enumerate(extra_images):
    print(f"  {i + 1}. {img_path.relative_to(images_folder)}")

# 生成详细报告
report_path = generate_extra_images_report(
    images_folder, labels_folder, backup_folder,
    len(image_files), len(label_names), extra_images
)

# 提供处理选项
handle_extra_images(extra_images, backup_folder, images_folder)

return extra_images

def generate_extra_images_report(images_folder, labels_folder, backup_folder,

image_count, label_count, extra_images):

"""

生成多余图片文件的详细报告

"""

report_path = Path(backup_folder) / "多余图片文件报告.txt"

复制代码
with open(report_path, 'w', encoding='utf-8') as f:
    f.write("多余图片文件报告\n")
    f.write("=" * 50 + "\n")
    f.write(f"生成时间: {get_current_time()}\n")
    f.write(f"图片文件夹: {images_folder}\n")
    f.write(f"标签文件夹: {labels_folder}\n")
    f.write(f"备份文件夹: {backup_folder}\n")
    f.write(f"图片文件总数: {image_count}\n")
    f.write(f"标签文件总数: {label_count}\n")
    f.write(f"理论多余图片数量: {image_count - label_count}\n")
    f.write(f"实际发现多余图片数量: {len(extra_images)}\n")

    f.write("\n多余图片文件列表:\n")
    f.write("-" * 40 + "\n")
    for img_path in extra_images:
        f.write(f"{img_path.relative_to(images_folder)}\n")

    # 添加统计信息
    folder_stats = {}
    for img_path in extra_images:
        folder = img_path.parent.relative_to(images_folder)
        folder_stats[folder] = folder_stats.get(folder, 0) + 1

    f.write("\n按文件夹分布:\n")
    for folder, count in sorted(folder_stats.items()):
        f.write(f"  {folder}: {count} 个\n")

print(f"📊 详细报告已保存到: {report_path}")
return report_path

def handle_extra_images(extra_images, backup_folder, images_folder):

"""

处理多余的图片文件

"""

if not extra_images:

return

复制代码
print(f"\n🔧 处理选项:")
print("1. 移动多余的图片到备份文件夹")
print("2. 删除多余的图片")
print("3. 仅查看报告,不进行任何操作")

choice = input("请选择操作 (1/2/3): ").strip()

if choice == "1":
    move_extra_images(extra_images, backup_folder, images_folder)
elif choice == "2":
    delete_extra_images(extra_images, backup_folder, images_folder)
elif choice == "3":
    print("已选择不进行任何操作")
else:
    print("无效选择,已取消操作")

def move_extra_images(extra_images, backup_folder, images_folder):

"""

移动多余的图片到备份文件夹

"""

print(f"准备移动 {len(extra_images)} 个多余的图片文件到备份文件夹...")

confirm = input("确认移动? (y/n): ").strip().lower()

复制代码
if confirm != 'y':
    print("操作已取消")
    return

moved_count = 0
for img_path in extra_images:
    try:
        # 保持原有的目录结构
        relative_path = img_path.relative_to(images_folder)
        target_path = Path(backup_folder) / relative_path

        # 确保目标目录存在
        target_path.parent.mkdir(parents=True, exist_ok=True)

        # 移动文件
        shutil.move(str(img_path), str(target_path))
        moved_count += 1

        if moved_count % 10 == 0:
            print(f"已移动 {moved_count} 个文件...")

    except Exception as e:
        print(f"移动文件失败: {img_path} -> {e}")

print(f"✅ 已移动 {moved_count} 个多余的图片文件到备份文件夹")
print(f"📁 备份文件夹: {backup_folder}")

def delete_extra_images(extra_images, backup_folder, images_folder):

"""

删除多余的图片文件(先备份)

"""

print(f"准备删除 {len(extra_images)} 个多余的图片文件...")

print("⚠ 警告: 此操作将永久删除文件!")

confirm = input("确认删除? (输入 'DELETE' 确认): ").strip()

复制代码
if confirm != 'DELETE':
    print("操作已取消")
    return

deleted_count = 0
for img_path in extra_images:
    try:
        # 先备份文件
        relative_path = img_path.relative_to(images_folder)
        backup_path = Path(backup_folder) / "deleted_images" / relative_path
        backup_path.parent.mkdir(parents=True, exist_ok=True)
        shutil.copy2(str(img_path), str(backup_path))

        # 删除原文件
        img_path.unlink()
        deleted_count += 1

        if deleted_count % 10 == 0:
            print(f"已删除 {deleted_count} 个文件...")

    except Exception as e:
        print(f"删除文件失败: {img_path} -> {e}")

print(f"✅ 已删除 {deleted_count} 个多余的图片文件")
print(f"📁 备份文件保存在: {Path(backup_folder) / 'deleted_images'}")

def get_current_time():

"""获取当前时间字符串"""

from datetime import datetime

return datetime.now().strftime('%Y-%m-%d %H:%M:%S')

def verify_image_label_match(images_folder, labels_folder, label_extension=".txt"):

"""

验证图片和标签的匹配情况

"""

print("验证图片和标签的匹配情况...")

复制代码
# 获取所有标签文件名
label_names = set()
for label_path in Path(labels_folder).rglob(f'*{label_extension}'):
    label_names.add(label_path.stem)

# 获取所有图片文件名
image_extensions = ['.jpg', '.jpeg', '.png', '.bmp', '.gif', '.tiff', '.webp']
image_names = set()

for ext in image_extensions:
    for img_path in Path(images_folder).rglob(f'*{ext}'):
        image_names.add(img_path.stem)

# 计算匹配情况
matched = image_names & label_names
images_without_labels = image_names - label_names
labels_without_images = label_names - image_names

print(f"匹配统计:")
print(f"  图片文件总数: {len(image_names)}")
print(f"  标签文件总数: {len(label_names)}")
print(f"  完全匹配的文件对: {len(matched)}")
print(f"  有图片但无标签: {len(images_without_labels)}")
print(f"  有标签但无图片: {len(labels_without_images)}")

if images_without_labels:
    print(f"\n有图片但无标签的文件 (前10个):")
    for i, name in enumerate(list(images_without_labels)[:10]):
        print(f"  {i + 1}. {name}")

return len(matched), len(images_without_labels), len(labels_without_images)

def find_single_extra_image(images_folder, labels_folder, label_extension=".txt"):

"""

专门找出单个多余的图片(适用于你的情况)

"""

print("查找单个多余的图片文件...")

复制代码
# 获取所有标签文件名
label_names = set()
for label_path in Path(labels_folder).rglob(f'*{label_extension}'):
    label_names.add(label_path.stem)

# 获取所有图片文件
image_extensions = ['.jpg', '.jpeg', '.png', '.bmp', '.gif', '.tiff', '.webp']
image_files = []

for ext in image_extensions:
    for img_path in Path(images_folder).rglob(f'*{ext}'):
        image_files.append(img_path)

# 找出没有对应标签的图片文件
extra_images = []
for img_path in image_files:
    if img_path.stem not in label_names:
        extra_images.append(img_path)

if len(extra_images) == 1:
    print(f"✅ 找到唯一的多余图片文件:")
    print(f"   {extra_images[0].relative_to(images_folder)}")
    return extra_images[0]
elif len(extra_images) > 1:
    print(f"⚠ 发现 {len(extra_images)} 个多余的图片文件:")
    for img_path in extra_images:
        print(f"   {img_path.relative_to(images_folder)}")
    return extra_images
else:
    print("❌ 没有发现多余的图片文件")
    return None

def interactive_extra_image_finder():

"""

交互式多余图片查找工具

"""

print("=== 多余图片查找工具 ===")

复制代码
# 获取用户输入
images_folder = input("请输入图片文件夹路径: ").strip()
labels_folder = input("请输入标签文件夹路径: ").strip()

# 选择标签文件扩展名
print("\n请选择标签文件扩展名:")
print("1. .txt (默认)")
print("2. .json")
print("3. .xml")
print("4. 自定义")
ext_choice = input("请选择 (1/2/3/4): ").strip()

if ext_choice == "1":
    label_extension = ".txt"
elif ext_choice == "2":
    label_extension = ".json"
elif ext_choice == "3":
    label_extension = ".xml"
elif ext_choice == "4":
    label_extension = input("请输入自定义扩展名 (例如 .txt): ").strip()
else:
    label_extension = ".txt"

print(f"使用标签扩展名: {label_extension}")

# 选择操作
print("\n请选择操作:")
print("1. 仅验证匹配情况")
print("2. 查找单个多余的图片")
print("3. 查找并处理所有多余的图片")
choice = input("请选择 (1/2/3): ").strip()

if choice == "1":
    verify_image_label_match(images_folder, labels_folder, label_extension)
elif choice == "2":
    extra_image = find_single_extra_image(images_folder, labels_folder, label_extension)
    if extra_image:
        print(f"\n找到的多余图片: {extra_image}")
elif choice == "3":
    backup_folder = input("请输入备份文件夹路径 (留空使用默认路径): ").strip()
    if not backup_folder:
        backup_folder = None

    find_extra_image_by_labels(images_folder, labels_folder, backup_folder, label_extension)
else:
    print("无效选择")

使用示例

if name == "main ":

方法1: 直接调用(查找并处理所有多余的图片)

find_extra_image_by_labels(

images_folder=r"E:\data(修改调整的数据-模型迭代用)\imags\val",

labels_folder=r"E:\data(修改调整的数据-模型迭代用)\labels\val",

backup_folder="E:/extra_images_backup"

)

复制代码
# 方法2: 仅验证匹配情况
# verify_image_label_match("E:/images", "E:/labels")

# 方法3: 专门查找单个多余的图片
# find_single_extra_image("E:/images", "E:/labels")

# 方法4: 交互式使用
interactive_extra_image_finder()
相关推荐
编码者卢布5 小时前
【Azure Developer】通过 API 获取 Azure VM 信息实践指南 Part 2(Azure China)
python·flask·azure
Java小白笔记5 小时前
Codex CLI 使用与斜杠指令实战教程
服务器·数据库·oracle
南雨北斗5 小时前
vue3项目表单验证 input 添加高亮样式
前端
养生技术人5 小时前
Oracle OCP认证考试题目详解082系列第16题
数据库·sql·oracle·ocp
65岁退休Coder5 小时前
LangGraph v1.2.9 核心概念 & 流程控制
后端·python·langchain
搭贝5 小时前
国资报送责任制怎么建?三级责任矩阵设计
android·数据库·人工智能·线性代数·低代码·矩阵·制造
杨云龙UP5 小时前
Linux 下 MySQL 常用登录方式及 Socket 排查指南
linux·运维·数据库·mysql·登录·xtrabackup·主从复制
:-)5 小时前
idea中的vue文件没有高亮显示
前端·javascript·vue.js·ecmascript·intellij-idea
IPdodo_5 小时前
2026年AI 数据采集代理 IP 选型:成功率、并发、轮换与成本评估
前端·网络·人工智能·chrome·python·http·网络调试
计算机魔术师5 小时前
AI问诊5秒出结果,医生反而更忙了
前端