《数据清洗的艺术:如何用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()
相关推荐
梦想的旅途21 小时前
企业微信API二次开发:外部群模块功能清单与全场景对接
java·python·企业微信
Bigger1 小时前
Han:一个让 AI Agent 也能做出高级中国风页面的 CSS 设计系统
前端·ai编程·设计
0566461 小时前
Python高级——迭代器
开发语言·python·学习
XR1234567881 小时前
学校图书馆网络改造,方案谁更优?
网络·数据库·php
梦想的旅途21 小时前
企业微信自动化:自动发送文本、图片、文件
前端·数据库·microsoft
IvorySQL2 小时前
PostgreSQL 日报|复制槽泄漏修复(8 月 6 日)
数据库·postgresql
XWalnut2 小时前
MySQL入门到精通
数据库·mysql
老白干2 小时前
基于 Spring AOP 的操作日志记录:以 DeptController 增删接口为例
java·python·spring