
1. 为什么自己动手做数据集现成公开数据集解决不了你的问题做目标检测训练第一反应肯定是去下载公开数据集比如COCO、VOC这些。真到自己上手时会发现一个很尴尬的问题——公开数据集里的类别和你的场景大概率对不上。我做过一个某生产线质检的模拟项目X需要识别的是特定型号的零件瑕疵COCO里根本没有这个类后来帮某开发者做某跨平台系统的检测功能要识别的是某种特定包装的饮料瓶公开数据集里同样找不到。这个时候就只能走自采数据手工标注这条路。很多第一次接触这个流程的人会觉得数据量要求吓人动辄几千上万张图但其实用一套合理的采集和标注流程几百张高质量图片就能让一个轻量级模型跑出不错的效果。关键在于图片的质量和标注的规范性而不是单纯堆数量。这篇内容围绕批量下载百度搜索图片 labelimg制作数据集 转换YOLOv5训练格式这条完整链路来写把我实际操作中踩过的坑、总结的经验全部拆开讲清楚。适合以下几类人准备训练自己的目标检测模型但不知道怎么获取数据的新手已经会跑YOLOv5但苦于没有合适数据集的开发者以及想建立一套可复用的数据集生产流程的算法工程师。2. 图片采集批量下载百度搜索结果的高效思路与坑2.1 先明确采集策略搜索词怎么定、要下载多少张采集图片看着简单其实第一步定搜索词就很有讲究。比如你要识别的是红富士苹果直接搜红富士苹果会得到大量商品图、海报图、插画图这些图混进训练集就是灾难。正确的做法是把搜索词拆成多个维度场景维度红富士苹果 果篮红富士苹果 超市货架红富士苹果 果园角度维度红富士苹果 俯拍红富士苹果 侧面状态维度红富士苹果 切开红富士苹果 带叶每个搜索词下载100到200张多组合几个最后汇总去重。这样做的好处是让模型看到不同光照、不同背景、不同角度下的目标泛化能力会好很多。如果只用一个搜索词下载几百张图大概率全是同一类构图的图训出来的模型换个环境就废了。数量上建议每个类别至少300张有效图片。注意我说的是有效——去重、去无效图、去纯文字截图之后的数量。比如每个搜索词下载150张5个搜索词就是750张清洗完大概能剩400到500张这样就够用了。2.2 百度图片的请求机制与批量下载脚本百度图片的搜索页面是动态加载的直接requests去抓HTML只能拿到一堆空壳。它背后有一个JSON数据接口理解了这套接口批量下载就变成了一个简单的请求循环。核心逻辑是这样通过构造包含搜索词、页码、图片数量等参数的HTTP请求获取图片URL列表再逐一下载。请求到的数据中包含了图片的原始地址和缩略图地址注意一定要用原始地址缩略图下载下来分辨率太低没法用于训练。下面是我实际用过的脚本框架经过了多次断点续传和异常处理加固import requests import re import json import os import time def baidu_image_search(search_word, page_num10, per_page30): 搜索百度图片返回图片URL列表 基于通用fbs接口逻辑不同时期的接口参数可能略有变化 image_urls [] headers { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36, Referer: https://image.baidu.com/ } for page in range(page_num): url https://image.baidu.com/search/acjson params { tn: resultjson_com, logid: , ipn: rj, word: search_word, pn: page * per_page, rn: per_page, ie: utf-8, oe: utf-8, } try: resp requests.get(url, paramsparams, headersheaders, timeout10) data resp.json() for item in data.get(data, []): if item.get(thumbURL): image_urls.append(item[thumbURL]) elif item.get(middleURL): image_urls.append(item[middleURL]) print(f第{page1}页完成累计已获取{len(image_urls)}个链接) except Exception as e: print(f第{page1}页请求失败: {e}) continue time.sleep(1) return list(set(image_urls)) def download_images(url_list, save_dir, prefiximg): 批量下载图片带格式校验和异常跳过 os.makedirs(save_dir, exist_okTrue) headers { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 } valid_ext (.jpg, .jpeg, .png, .webp) count 0 for idx, url in enumerate(url_list): try: resp requests.get(url, headersheaders, timeout10) if resp.status_code ! 200: continue # 从Content-Type或URL后缀判断格式 content_type resp.headers.get(Content-Type, ) if jpeg in content_type or jpg in content_type: ext .jpg elif png in content_type: ext .png elif webp in content_type: ext .webp else: # 尝试从URL推断 ext_match re.search(r\.(jpg|jpeg|png|webp), url.lower()) ext . ext_match.group(1) if ext_match else .jpg # 校验图片大小小于2KB的基本是占位图 if len(resp.content) 2048: continue file_path os.path.join(save_dir, f{prefix}_{idx:05d}{ext}) with open(file_path, wb) as f: f.write(resp.content) count 1 if count % 50 0: print(f已下载{count}张) except Exception as e: print(f下载失败 [{idx}] {url}: {e}) continue print(f本次下载完成有效图片{count}张) return count # 使用示例 if __name__ __main__: search_words [红富士苹果 果篮, 红富士苹果 超市货架] all_urls [] for word in search_words: urls baidu_image_search(word, page_num8, per_page30) all_urls.extend(urls) time.sleep(2) # 两个搜索词之间间隔避免请求过密 download_images(all_urls, ./datasets/raw/apple, prefixapple)几个容易踩的坑大图URL失效百度返回的图片URL一部分是CDN临时链接有有效期。所以最好拿到URL后尽快下载不要攒着过夜再下。下载到缩略图thumbURL是压缩过的缩略图直接在搜索页展示的预览图分辨率通常只有几百像素。检测训练建议至少640像素以上。如果发现下载的图普遍模糊要么换用中图URL要么用网页端的原图跳转逻辑。反爬拦截短时间内请求太密集会触发风控表现为突然返回一堆错误页面或空数据。解决办法就是脚本里加随机延时1到3秒之间随机另外多准备几个User-Agent轮流用。2.3 图片清洗这一步省不得下载完图片只能算素材还不能叫数据。清洗的目的是把明显不能用于训练的图删掉。我的清洗标准是剔除类型判断方法原因纯色占位图人工快速扫一眼无有效目标浪费标注精力文字截图/海报看图中是否含大面积文字干扰模型特征学习重复图用感知哈希算法比对重复图会导致训练集冗余分辨率过低程序统计低于300x300的删除YOLO下采样后目标太小明显带水印的图人工处理水印纹理会被模型误学为特征这里特别说一下重复图的问题。百度搜索同一个词的多页结果里重复图比例相当高。如果只是靠眼睛看几百张图看下来很累。可以写个小脚本用imghdr读图片基础信息再配合imagehash库算感知哈希相似度超过95%的直接删掉。这个库安装很简单处理几百张图也就几秒钟的事。import imagehash from PIL import Image import os from collections import defaultdict def remove_duplicates(img_dir, threshold5): 用感知哈希去除近似重复图片 threshold越小判定为重复的阈值越高 hash_dict defaultdict(list) for filename in os.listdir(img_dir): file_path os.path.join(img_dir, filename) if not os.path.isfile(file_path): continue try: with Image.open(file_path) as img: img_hash imagehash.phash(img) # 把hash值转成字符串便于比较 hash_str str(img_hash) hash_dict[hash_str].append(file_path) except Exception as e: print(f处理失败 {file_path}: {e}) # 对于完全相同的hash进行简单分组这里略去汉明距离比较 remove_list [] for hash_str, paths in hash_dict.items(): if len(paths) 1: # 保留第一张删除其余 remove_list.extend(paths[1:]) for path in remove_list: os.remove(path) print(f去重完成删除了{len(remove_list)}张重复图片)清洗完的图最好按类别分目录存放。比如你准备做苹果和香蕉两类检测目录结构就是datasets/ ├── raw/ │ ├── apple/ │ │ ├── img_00001.jpg │ │ └── ... │ └── banana/ │ ├── img_00001.jpg │ └── ...3. labelimg标注从安装到高效应答的关键细节3.1 标注工具的选型为什么选labelimg而不是其他目标检测标注工具有不少选择labelimg、labelme、CVAT、精灵标注助手等。我的经验是项目刚起步、数据量在千张级别以下、对标注效率要求不是极端高时labelimg是最省心的选择。labelimg直接生成XML文件符合PascalVOC格式标准后续要转YOLO格式、COCO格式都很方便不需要部署服务端纯本地应用数据安全有保障操作逻辑简单学习成本低一个下午就能上手CVAT这类在线工具确实在多人协作上有优势但如果是一个人做数据集完全没必要引入分布式标注的复杂度。labelimg够用。3.2 安装与基础操作labelimg的安装很简单前提是你有Python环境pip install labelimg安装完成后命令行输入labelimg就能启动。macOS和Linux上如果遇到PyQt5相关的问题一般是Python版本太高或缺少系统依赖装一下PyQt5的完整包即可解决。打开软件后按以下步骤来打开图片目录菜单栏 File → Open Dir选择上面建好的某个类别的图片文件夹设置保存目录Change Save Dir建议和图片目录相同这样XML文件会直接生成在图片旁边开启自动保存菜单 View → Auto Save mode勾上标注完一张自动保存省去手动CtrlS创建类别列表在标注之前先创建好类别文件classes.txt里写你的类别名每行一个类。这样标注时直接下拉选择不会出现同一个目标被标成不同名字的尴尬情况标注框的方式非常直观按住鼠标左键从目标左上角拖到右下角松开即完成一个框。一个目标只能有一个框如果目标被严重遮挡宁可框露出的一部分也不要硬包一个包含大量背景的大框。提示标注框应该紧贴目标边界尽量精准贴合目标轮廓的矩形外接框。框大了会让模型学到背景特征框小了会截断目标关键部位。3.3 标注规范与效率技巧标注这件事看起来没有技术含量但规范与否直接影响模型效果。几个我在实践中总结的标注原则第一框的边界判定要统一。同一张图里如果一辆车头朝左车身占了大半张图框的边界应该贴着车身最外沿。不同标注人之间对边界的理解差异是数据集最大的质量杀手。如果数据集是多人标注的一定要先开个短会统一标准否则模型会学得很混乱。第二遮挡目标的标注策略。目标遮挡超过一半的时候我一般选择不标。标一个大部分被挡住的框框里真正有效的像素可能只有一小撮反而给模型训练带来噪声。如果目标只是部分遮挡轮廓仍然清晰可辨那照常标注完整外接框。第三不同类别的重叠框处理。比如一张图里有一整箱苹果还有散落在旁边的单个苹果。整箱和单个苹果都属于苹果这个类这时候不需要分开标注统一框就行。但如果你的模型需要区分整箱苹果和单个苹果两个类那就要注意当单个苹果被箱子遮挡时只标能看到的部分。提高标注效率的几个实用技巧快捷键P/W/A/D切换图片右手操作鼠标标注左手按快捷键速度能快一倍Ctrl鼠标滚轮放大局部精确标注小目标时非常好用D键删除上一个框标错了快速撤销不用重新标整张图间隙休息标注是高度重复的精细劳动容易疲劳出错。建议每标30分钟休息5分钟回头抽查一下之前的标注结果3.4 标注质量抽检别让错误流入训练集标注完成后不要急着转格式。抽检的目的是把明显的标注错误找出来——比如框的位置偏移、类别标错、漏标。我的经验是每张图在标注完成后重新快速过一遍不放大就看整体布局重点检查是否有漏标的小目标。一个更可靠的方法是做二次验证把生成的XML文件里的bounding box绘制回原图肉眼对比一遍。这个过程可以写个简单脚本把所有标注框画在图上输出成一张大图集快速滚动浏览比一张张打开检查高效得多。标注质量直接决定训练上限模型不会学到你标注规范之外的东西。花在质量抽检上的时间比之后处理训练异常的时间要划算得多。4. 从labelimg到YOLOv5格式转换的完整动作拆解4.1 理解PascalVOC格式与YOLO格式的本质区别labelimg默认输出的是PascalVOC格式的XML文件而YOLOv5训练需要的是纯文本的txt标注文件。这两种格式最大的区别在于坐标系的表示方式。XML里记录的bounding box是像素绝对坐标bndbox xmin120/xmin ymin180/ymin xmax300/xmax ymax410/ymax /bndbox这个坐标只有在特定分辨率下才有意义。YOLOv5用的txt文件则记录归一化后的相对坐标0 0.398437 0.466102 0.287500 0.469492一行对应一个目标5个值依次是类别编号、中心点x坐标、中心点y坐标、宽度、高度。x坐标和宽度是相对图片宽度的比例y坐标和高度是相对图片高度的比例。因为归一化了所以不同分辨率图片才能放在同一个训练集里混合使用。4.2 转换脚本XML转YOLO格式的核心代码转换逻辑的核心就是坐标换算公式center_x (xmin xmax) / 2 / image_width center_y (ymin ymax) / 2 / image_height width (xmax - xmin) / image_width height (ymax - ymin) / image_height我基于某图像处理Demo的实际开发经验写过一个比较完整的转换脚本关键部分如下import xml.etree.ElementTree as ET import os from tqdm import tqdm def convert_voc_to_yolo(xml_path, classes, output_dir): 将PascalVOC格式的XML标注转换为YOLO格式的txt标注 Args: xml_path: XML文件路径 classes: 类别列表索引即类别编号 output_dir: 输出目录 tree ET.parse(xml_path) root tree.getroot() # 读取图片尺寸 size root.find(size) image_width int(size.find(width).text) image_height int(size.find(height).text) # 获取图片文件名用于生成对应的txt文件名 image_name root.find(filename).text txt_name os.path.splitext(image_name)[0] .txt yolo_lines [] for obj in root.iter(object): # 跳过difficult标签的目标 difficult obj.find(difficult) if difficult is not None and int(difficult.text) 1: continue name obj.find(name).text if name not in classes: print(f警告: XML分类 {name} 不在类别列表中跳过) continue class_id classes.index(name) bndbox obj.find(bndbox) xmin float(bndbox.find(xmin).text) ymin float(bndbox.find(ymin).text) xmax float(bndbox.find(xmax).text) ymax float(bndbox.find(ymax).text) # 坐标归一化计算 center_x (xmin xmax) / 2.0 / image_width center_y (ymin ymax) / 2.0 / image_height width (xmax - xmin) / image_width height (ymax - ymin) / image_height # 限制在0~1之间防止越界标注 center_x min(max(center_x, 0), 1) center_y min(max(center_y, 0), 1) width min(max(width, 0), 1) height min(max(height, 0), 1) yolo_lines.append(f{class_id} {center_x:.6f} {center_y:.6f} {width:.6f} {height:.6f}) # 写入txt文件 txt_path os.path.join(output_dir, txt_name) with open(txt_path, w) as f: f.write(\n.join(yolo_lines)) def batch_convert(xml_dir, classes, output_dir): 批量转换整个目录下的XML文件 os.makedirs(output_dir, exist_okTrue) xml_files [f for f in os.listdir(xml_dir) if f.endswith(.xml)] print(f找到{len(xml_files)}个XML标注文件) for xml_file in tqdm(xml_files, desc转换中): xml_path os.path.join(xml_dir, xml_file) convert_voc_to_yolo(xml_path, classes, output_dir) print(批量转换完成) # 使用示例 if __name__ __main__: # 注意类别列表中名字要和标注时完全一致顺序即最终模型类别编号 classes [apple, banana] # 分别转换每个类别的标注 for cls in classes: xml_dir f./datasets/annotations/{cls} output_dir f./datasets/labels/{cls} if os.path.exists(xml_dir): batch_convert(xml_dir, classes, output_dir)转换过程容易出错的一个点是某个类别标错了名或者在classes列表里顺序改变了导致txt里类别编号对不上。我建议在转换前用Excel或文本工具把所有XML里的类别名统计一遍确认只有你预期的那几个类并且拼写完全一致。4.3 数据集目录结构与YOLOv5配置文件YOLOv5要求的数据集目录结构固定图片和标签要严格对应。我习惯的目录布局是这样的yolov5/ ├── datasets/ │ └── my_dataset/ │ ├── images/ │ │ ├── train/ │ │ │ ├── img_00001.jpg │ │ │ └── ... │ │ └── val/ │ │ ├── img_00050.jpg │ │ └── ... │ ├── labels/ │ │ ├── train/ │ │ │ ├── img_00001.txt │ │ │ └── ... │ │ └── val/ │ │ ├── img_00050.txt │ │ └── ... ├── my_dataset.yaml图片和标注文件的名字必须一一对应——img_00001.jpg必须对应img_00001.txtYOLOv5的加载逻辑是按文件名匹配的。数据集划分比例我一般用8:2也就是80%的训练集、20%的验证集。划分时要注意随机性我写过一个小脚本用random.shuffle打乱所有图片文件名然后按比例切割。千万别手动挑图手动挑一定会带入主观偏差可能把相同场景的图都分到一边去了。数据集的yaml配置文件也很关键这是YOLOv5识别数据集的说明书# my_dataset.yaml train: datasets/my_dataset/images/train val: datasets/my_dataset/images/val nc: 2 names: [apple, banana]注意nc类别数量和names列表里的类别顺序必须和之前转换脚本里的classes列表完全一致。常见错误是改了names忘了改nc导致训练直接报错。5. 最终验证与常见坑让训练跑通只是起点5.1 用YOLOv5自带的检查工具做最后把关转换完成后有一个很容易被忽略的步骤——用YOLOv5自带的datasets.py脚本验证数据集格式。在YOLOv5根目录下执行python datasets.py --data my_dataset.yaml这个脚本会检查训练集和验证集的图片数量每张图片是否有对应的标签文件标签文件中的标注框是否在图片范围内标签文件中的类别编号是否超出nc设置的范围如果数据集有问题它会直接报错并告诉你问题出在哪张图上。这个检查一定要跑我遇到过因为一张图在裁剪后没有同步标注导致训练时反复报assertion error的情况。用这个脚本提前检查能省下大量排查时间。5.2 训练过程中的数据质量反馈很多人以为数据集做完送进训练就万事大吉了其实训练过程会反过来告诉你数据集质量如何。几个关键信号loss一直降不下去大概率是标注质量参差不齐。建议回去抽样检查标注框是否有的松有的紧、有的偏左上有的偏右下某个类别AP特别低大概率是这个类别的图片数量太少或场景多样性不足。回去检查这个类的图片是否全是同一色背景、同一角度输出大量无意义的边框可能是某个类别标得不准或者类别间外观高度相似但标注不一致我在某次做某跨平台系统的物体检测Demo时训练时发现有两个类别的AP差异巨大排查后发现其中一类图片质量参差不齐——有相当一部分是从视频里截的帧模糊程度远超另一类的图片。后来把模糊图片全部删除重新标注了一批清晰的两类的AP就基本持平了。5.3 几个容易被忽视的坑汇总这里把我在多次做数据集流程中踩过的坑统一说一下很多都是回头才意识到的问题坑一图片里有大量角落小目标标注时容易漏掉。训练时模型就会把这些漏标的目标当作背景久而久之它学到的特征是这个区域即使是目标也不用检测。抽检时重点回头看图的四角和边缘位置。坑二标注完成后重命名了图片文件。一旦图片重命名XML里的filename标签并不会自动更新结果就是YOLO加载时找不到匹配的标注。所以要么不重命名要么重命名后用脚本同步修改XML里的文件名标签。坑三批量下载图片包含了GIF动图。部分GIF帧内容完全不同训练时会被当作动态目标导致检测输出不稳定。清洗阶段应该用os.path.splitext检查扩展名把所有.gif文件全部剔除。坑四搜图时一个关键词下载的图里很可能混有相似的连环画截图。画风统一的卡通图片会形成一种风格特征模型容易把这个风格误学为目标特征。清洗时留意是否有大面积的插画或漫画风格图片混入。坑点现象解决方案XML文件名与图片不一致训练报找不到标签统一重命名并同步修改XMLGIF动图混入训练loss异常波动清洗阶段检查扩展名排除类别拼写不一致转换时类别全部被跳过转换前统计XML类别名标注框坐标越界训练时断言报错转换脚本中加坐标裁剪6. 最后一个经验从数据到模型这是一条需要打磨的流水线做了几次完整的采集-标注-转换-训练流程之后我最大的体会是**这条链路里没有一步是可以糊弄过去的。**采集阶段偷懒少换几个搜索词清洗阶段多放过几张模糊图标注阶段快了几分钟没做抽检这些小问题积累到训练阶段都会被放大成模型性能的大问题。如果要从零再走一遍这个流程我会把时间分配成这样采集和清洗占总时间的三成标注占总时间的四成格式转换和验证占总时间的三成。很多人天然地把注意力放在下载和转换脚本上觉得标注是体力活可以随便应付这个认知是反的——标注才是最值得下功夫的环节因为它直接决定模型学习的上限。实际操作中我还有一个习惯值得分享每一次完整流程做完都把搜索词列表、清洗规则、标注规范、转换脚本、踩坑记录整理成一份文档放在项目目录里。第二次做类似项目时直接复用这些沉淀整个流程能提速一半以上。数据集生产这件事越往后越省力前提是第一次就把方法论立起来。