目标检测yolo数据处理全流程(网络爬取,视频截取/抽帧,数据标注,txt/xml格式转换)

237 阅读12分钟

#职场大白的经验贴  #非大佬自救联盟

yolo原理,部署训练这些都老生常谈了,我下边分享工作中最麻烦占大头的数据处理工作,虽说前期做数据简单,但是上手马上整明白还是需要时间的,有问题的宝子可以留言交流哈

目录

一、数据获取

二、数据处理

三、数据标注

四、图片标签数据训练前梳理

五、数据集划分


一、数据获取

1.数据获取可以直接买个摄像头去录

     这里边涉及到得问题:

    1.录取的数据是不是合法(偷录小心被告)

    2.摄像头分好多种,球机枪机或者其他机器录取的数据是否符合你要的数据要求(比如有的摄像头出来的图片是鱼眼效果)

2.网络爬取

     爬取的数据分为图片视频数据

图片数据可以直接写脚本爬取百度等浏览器的图片,可以多换几个搜索关键词

参考脚本片段:

class BaiduImageSpider(object):
    def __init__(self):
        self.json_count = 0  # 请求到的json文件数量(一个json文件包含30个图像文件)
        self.url = 'https://image.baidu.com/search/acjson?tn=resultjson_com&logid=5179920884740494226&ipn=rj&ct' \
                   '=201326592&is=&fp=result&queryWord={' \
                   '}&cl=2&lm=-1&ie=utf-8&oe=utf-8&adpicid=&st=-1&z=&ic=0&hd=&latest=&copyright=&word={' \
                   '}&s=&se=&tab=&width=&height=&face=0&istype=2&qc=&nc=1&fr=&expermode=&nojc=&pn={' \
                   '}&rn=30&gsm=1e&1635054081427= '
        self.directory = r"C:\temp{}"  # 存储目录  这里需要修改为自己希望保存的目录  {}不要丢
        self.header = {
            'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) '
                          'Chrome/95.0.4638.54 Safari/537.36 Edg/95.0.1020.30 '
        }

    # 创建存储文件夹
    def create_directory(self, name):
        self.directory = self.directory.format(name)
        # 如果目录不存在则创建
        if not os.path.exists(self.directory):
            os.makedirs(self.directory)
        self.directory += r'{}'

    # 获取图像链接
    def get_image_link(self, url):
        list_image_link = []
        strhtml = requests.get(url, headers=self.header)  # Get方式获取网页数据
        jsonInfo = json.loads(strhtml.text)
        for index in range(30):
            list_image_link.append(jsonInfo['data'][index]['thumbURL'])
        return list_image_link

    # 下载图片
    def save_image(self, img_link, filename):
        res = requests.get(img_link, headers=self.header)
        if res.status_code == 404:
            print(f"图片{img_link}下载出错------->")
        with open(filename, "wb") as f:
            f.write(res.content)
            print("存储路径:" + filename)

    # 入口函数
    def run(self):
        searchName = input("查询内容:")
        searchName_parse = parse.quote(searchName)  # 编码

        self.create_directory(searchName)

        pic_number = 0  # 图像数量
        for index in range(self.json_count):
            pn = (index+1)*30
            request_url = self.url.format(searchName_parse, searchName_parse, str(pn))
            list_image_link = self.get_image_link(request_url)
            for link in list_image_link:
                pic_number += 1
                self.save_image(link, self.directory.format(str(pic_number)+'.jpg'))
                time.sleep(0.2)  # 休眠0.2秒,防止封ip
        print(searchName+"----图像下载完成--------->")

视频可以参考下边的教程去爬取

github.com/NanmiCoder/…

另: 如果爬取不了,也可以登录那些社交平台一个一个下,可以打开网页开发者模式找到视频的地址再去下载(例如下边图里,找到网络下的img和媒体,点击可以看到原始的图片或者视频链接)

​编辑

二、数据处理

视频截取/抽帧:

     收集好的图片数据可以直接肉眼删选,但是视频数据就需要抽成图片了,如果视频特别长还涉及到先把视频中的有效片段截取出来

视频截取思路:

     1.根据前后帧图像变换的程度大小来截取

     2.利用开源的检测模型先把有目标的部分检测出来再截取(这种对于做小众检测目标类别不太友好)

思路1的参考代码:

import cv2
import os
import subprocess

def process_video(video_path):
    # 获取视频文件的名称,不包括扩展名
    video_name = os.path.splitext(os.path.basename(video_path))[0]
    
    # 获取视频所在的目录
    video_dir = os.path.dirname(video_path)
    
    # 创建与视频同名的输出文件夹
    output_dir = os.path.join(video_dir, video_name)
    os.makedirs(output_dir, exist_ok=True)

    # 修复视频并生成临时文件(忽略解码错误)
    fixed_video_path = os.path.join(video_dir, f"fixed_{video_name}.mp4")
    ffmpeg_command = [
        "ffmpeg", "-err_detect", "ignore_err", "-i", video_path, "-c", "copy", fixed_video_path
    ]
    subprocess.run(ffmpeg_command, stdout=subprocess.PIPE, stderr=subprocess.PIPE)

    # 使用OpenCV读取修复后的视频
    video = cv2.VideoCapture(fixed_video_path)

    # 获取视频的帧率和大小
    fps = video.get(cv2.CAP_PROP_FPS)
    width = int(video.get(cv2.CAP_PROP_FRAME_WIDTH))
    height = int(video.get(cv2.CAP_PROP_FRAME_HEIGHT))
    fourcc = cv2.VideoWriter_fourcc(*'mp4v')  # 保存为MP4格式

    # 初始化背景帧
    ret, background = video.read()
    if not ret:
        print(f"无法读取视频: {video_path}")
        return

    background = cv2.cvtColor(background, cv2.COLOR_BGR2GRAY)
    background = cv2.GaussianBlur(background, (21, 21), 0)

    # 定义一些变量
    recording = False
    out = None
    frame_count = 0
    clip_count = 1
    motion_frames = 0
    motion_threshold = 10  # 连续运动的帧数阈值

    while True:
        ret, frame = video.read()
        if not ret:
            break

        # 转换为灰度图像并进行模糊处理
        gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
        gray = cv2.GaussianBlur(gray, (21, 21), 0)

        # 计算帧差异
        diff = cv2.absdiff(background, gray)
        _, thresh = cv2.threshold(diff, 25, 255, cv2.THRESH_BINARY)

        # 找到运动的轮廓
        contours, _ = cv2.findContours(thresh.copy(), cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)

        # 如果检测到运动
        detected = False
        for contour in contours:
            if cv2.contourArea(contour) > 8000:  # 增大阈值,过滤掉更小的噪声
                detected = True
                break

        if detected:
            motion_frames += 1  # 累计运动帧数
            if motion_frames > motion_threshold:  # 只有运动超过一定帧数才认为有效
                if not recording:
                    # 开始录制新的视频片段
                    video_filename = os.path.join(output_dir, f'clip_{video_name}_{clip_count}.mp4')
                    out = cv2.VideoWriter(video_filename, fourcc, fps, (width, height))
                    recording = True
                    clip_count += 1
                    print(f"开始保存片段: {video_filename}")

                # 写入当前帧到视频文件
                out.write(frame)
                frame_count += 1
        else:
            motion_frames = 0  # 如果没有检测到运动,重置累计帧数
            if recording:
                # 如果已经开始录制且运动停止,停止录制
                out.release()
                recording = False
                print(f"片段保存结束,帧数: {frame_count}")
                frame_count = 0

        # 更新背景帧
        background = gray

        # 按 'q' 退出
        if cv2.waitKey(30) & 0xFF == ord('q'):
            break

    # 释放资源
    video.release()
    if out is not None:
        out.release()
    cv2.destroyAllWindows()

    # 删除修复后的视频
    if os.path.exists(fixed_video_path):
        os.remove(fixed_video_path)

# 遍历文件夹中的所有MP4文件
folder_path = '/media/26x/T71/'  # 替换为你的视频文件夹路径
for filename in os.listdir(folder_path):
    if filename.endswith('.mp4'):
        video_path = os.path.join(folder_path, filename)
        process_video(video_path)

print("操作完成!")

    截取好的视频就可以按照你想要的抽帧频率来抽了,但是注意要选择合理的大小,太大遇到的问题就是后续删选图片会很费时

抽帧代码参考:

import cv2
import os
from concurrent.futures import ThreadPoolExecutor
from tqdm import tqdm

video_folder = r"D:\Desktop\detection\lll"  # 视频文件夹路径
output_folder = r"D:\Desktop\detection\s_backpack/lllo"  # 保存帧的文件夹路径
save_step = 10  # 间隔帧

def process_video(video_path):
    video = cv2.VideoCapture(video_path)
    video_name = os.path.splitext(os.path.basename(video_path))[0]  # 提取视频文件名(不包含扩展名)
    num = 0  # 计数器
    total_frames = int(video.get(cv2.CAP_PROP_FRAME_COUNT))  # 获取视频总帧数

    with tqdm(total=total_frames // save_step, desc=f"Processing {video_name}") as pbar:
        while True:
            ret, frame = video.read()
            if not ret:
                break
            num += 1
            if num % save_step == 0:
                output_path = os.path.join(output_folder, f"{video_name}_{num}.jpg")
                cv2.imwrite(output_path, frame)
                pbar.update(1)  # 更新进度条

    video.release()  # 释放视频捕获对象

# 遍历文件夹中的所有视频文件
video_files = [os.path.join(video_folder, filename) for filename in os.listdir(video_folder) if filename.endswith(".mp4")]

# 使用多线程处理视频文件
with ThreadPoolExecutor(max_workers=4) as executor:  # 根据 CPU 核心数调整 max_workers
    executor.map(process_video, video_files)

cv2.destroyAllWindows()  # 关闭所有 OpenCV 窗口

 注: 所有数据处理完,需要全部肉眼过滤一遍,把(模糊/重复多次/大部分无目标图片/部分负样本)按照需要删除掉,不然后续数据标注和模型训练加载都会带来不必要的工作量

三、数据标注

     使用labelimg或者其他标注工具进行标注,这些教程太多了就不赘述了。下面介绍常见的会遇到的问题和快速标注的方式

labelimg常见的会遇到的问题:

1.软件闪退:

      一般这种情况去C:\Users\AI下找下有没有.labelImgSettings.pkl文件,把这个文件删掉重新打开标注软件就好了(其他复杂情况可以看他弹出的后台界面,上边写的报错是什么,再针对性解决)

2.标注格式问题:

      1.如果多人一起标注,注意classes.txt的标签要保持一致,尤其标注yolo格式的,举例:要标注的类型有ABC,如果其他小伙伴写成了BCA,因为yolo格式里的class类别是从上往下按照0123....往下排的,就会造成标注错乱了。如果标注格式是pacalVOC的话还有拯救办法,就是大家一起标完再写脚本把标签给改掉。

      2.另外需要标注的图片,如果多人一起操作,需注意图片命名的唯一性,举例:我的文件夹的图片按照img_数字.jpg格式来的,如果其他也有人的图片(和我不重复的图片)也是这个命名规则,那最后合并的时候会造成标签不一致的问题。

快速标注/自动标注的方式:

1.使用开源OVD(万物检测)大模型进行自动标注。

         缺点:有的大模型接口需要付费

         标注的不准,肯定会漏标或者错标,需要二次校验标注

  2.模型训练迭代标注:

         如果需要标注的数据量很大的话,先标注一小部分数据,直接训练一个模型,然后拿这个模型去推理剩余未标注的图片,得到的标签再去人工核验,再继续迭代。训练的时候加上一个参数就可以生成标签文件,例如: 

yolo task=detect mode=predict model=/media/26x/T71/手机/第一次标注训练/best.pt source=/media/26x/T71/手机/第一次数据汇总 imgsz=1280 save_txt=True save=True

四、图片标签数据训练前梳理

     1.txt/xml(yolo/pacalVOC)格式转换

****参考脚本

# -*- coding: utf-8 -*-
import os
import xml.etree.ElementTree as ET

# 输入和输出路径
dirpath = r'D:\Desktop\datasets\labels_xml'  # 原始 XML 文件目录
newdir = r'D:\Desktop\datasets\label'  # 修改后生成的 TXT 文件目录

# 如果输出目录不存在,则创建
if not os.path.exists(newdir):
    os.makedirs(newdir)

# 读取类别文件并生成字典
class_file = r'D:\Desktop\classes.txt'
with open(class_file, 'r', encoding='utf-8') as f:
    data_info = f.readlines()

dict_info = {}
for idx, inf in enumerate(data_info):
    inf = inf.strip()  # 去除换行符和空格
    dict_info[inf] = idx

# 处理 XML 文件
xml_files = [f for f in os.listdir(dirpath) if f.endswith('.xml')]
for fp in xml_files:
    try:
        # 解析 XML 文件
        tree = ET.parse(os.path.join(dirpath, fp))
        root = tree.getroot()

        # 解析图像大小
        sz = root.find('size')
        if sz is None or len(sz) < 2:
            print(f"文件 {fp} 的 size 节点有问题,跳过")
            continue

        width = float(sz.find('width').text)
        height = float(sz.find('height').text)

        # 输出文件路径
        output_file = os.path.join(newdir, fp.replace('.xml', '.txt'))
        with open(output_file, 'w', encoding='utf-8') as f_out:
            for obj in root.findall('object'):
                # 获取标签和边界框
                label = obj.find('name').text
                sub = obj.find('bndbox')
                if label is None or sub is None:
                    print(f"文件 {fp} 的 object 节点有问题,跳过")
                    continue

                # 获取坐标
                try:
                    xmin = float(sub.find('xmin').text)
                    ymin = float(sub.find('ymin').text)
                    xmax = float(sub.find('xmax').text)
                    ymax = float(sub.find('ymax').text)

                    # 转换为 YOLO 格式
                    x_center = (xmin + xmax) / (2 * width)
                    y_center = (ymin + ymax) / (2 * height)
                    w = (xmax - xmin) / width
                    h = (ymax - ymin) / height

                    # 获取标签索引
                    label_index = dict_info.get(label, -1)  # 默认值为 -1,表示未知类别
                    if label_index == -1:
                        print(f"文件 {fp} 中的未知标签 {label},跳过")
                        continue

                    # 写入文件
                    f_out.write(f"{label_index} {x_center:.6f} {y_center:.6f} {w:.6f} {h:.6f}\n")
                except (ValueError, ZeroDivisionError) as e:
                    print(f"文件 {fp} 中的坐标有问题:{e}")
    except ET.ParseError as e:
        print(f"解析 XML 文件 {fp} 时出错:{e}")

五、数据集划分

       准备好的图片和标签数据需要按照train/val/test划分数据集,具体看你项目的需要。

       参考脚本:

import os
import random
import shutil


def split_dataset(srcDir, trainDir, valDir, split_ratio=0.9):
    """
    将数据集划分为训练集和验证集,并保存到相应的文件夹中。

    Parameters:
    - srcDir: 原始数据集文件夹路径,包含图像和标签文件。
    - /media/ImageSets/imagesDir: 训练集文件夹路径,包含 'images' 和 'labels' 子文件夹。
    - valDir: 验证集文件夹路径,包含 'images' 和 'labels' 子文件夹。
    - split_ratio: 数据集划分比例,默认为 0.9,表示将 90% 的数据用于训练集,10% 用于验证集。
    """
    os.makedirs(os.path.join(trainDir, 'images'), exist_ok=True)
    os.makedirs(os.path.join(trainDir, 'labels'), exist_ok=True)
    os.makedirs(os.path.join(valDir, 'images'), exist_ok=True)
    os.makedirs(os.path.join(valDir, 'labels'), exist_ok=True)
    # 获取数据集中所有文件的列表
    file_list = os.listdir(srcDir)
    random.shuffle(file_list)
    print(file_list)
    # 根据划分比例计算训练集和验证集的边界索引
    split_index = int(len(file_list) * split_ratio)
    train_files = file_list[:split_index]
    val_files = file_list[split_index:]
    # 将训练集数据移动到相应文件夹
    # print(split_index)
    for file in train_files:
        # print(train_files)
        # print(file)
        if file.endswith('.jpg'):
            # print('3ok')
            img_src = os.path.join(srcDir, file)
            label_src = os.path.join(srcDir, file[:-4] + '.txt')
            # print('ok')
            # print(img_src)
            shutil.move(img_src, os.path.join(trainDir, 'images', file))
            if os.path.exists(label_src):
                shutil.move(label_src, os.path.join(trainDir, 'labels', file[:-4] + '.txt'))

        if file.endswith('.png'):
            # print('3ok')
            img_src = os.path.join(srcDir, file)
            label_src = os.path.join(srcDir, file[:-4] + '.txt')
            # print('ok')
            # print(img_src)
            shutil.move(img_src, os.path.join(trainDir, 'images', file))
            if os.path.exists(label_src):
                shutil.move(label_src, os.path.join(trainDir, 'labels', file[:-4] + '.txt'))
                # 将验证集数据移动到相应文件夹
    for file in val_files:
        if file.endswith('.jpg'):
            img_src = os.path.join(srcDir, file)
            label_src = os.path.join(srcDir, file[:-4] + '.txt')
            shutil.move(img_src, os.path.join(valDir, 'images', file))
            if os.path.exists(label_src):
                shutil.move(label_src, os.path.join(valDir, 'labels', file[:-4] + '.txt'))


if __name__ == '__main__':
    # 输入文件夹路径
    srcDir = 'D:\Desktop\datasets/all'
    trainDir = 'D:\Desktop\datasets\datasets/train'
    valDir = 'D:\Desktop\datasets\datasets/val'

    # 调用函数划分数据集
    split_dataset(srcDir, trainDir, valDir)

好了,基本到这所有数据处理的苦力算是结束了,后边我会不定期继续分享图像类的部分算法的基础实操参考教程,希望家人们多多支持~