简介:面向行星探测与遥感图像分析场景,这份火星月球陨石坑检测数据集专为目标检测任务设计,可帮助研究者快速开展陨石坑识别、定位与计数实验,也可为相关算法提供标准化训练样本。数据集采用Pascal VOC与YOLO双格式组织,压缩包共398个文件,含132张jpg原图、132个xml标注文件和134个txt文件,其中xml与txt分别对应VOC与YOLO格式,整体约10.27MB,便于下载与复现。标注由labelImg工具完成,对图像中的陨石坑绘制矩形框,共1044个框,类别统一为'keng',标注规范,可直接用于常见检测框架。目前已有244人学习浏览,适合计算机视觉初学者练习数据集格式转换与YOLO训练流程,也可作为月球/火星陨石坑检测的小规模验证集。需要说明的是,这份数据集仅保证标注准确合理,不附带任何训练权重,也不对模型精度作任何承诺,使用时应自行训练与验证。
1. 火星月球陨石坑检测数据集:132张图与1044个框,能做什么
这份火星月球陨石坑检测数据集(VOC+YOLO格式,132张,1类别)拆包之前,请先调整一个预期:它不是训练好的模型,也不是开箱即用的推理器,而是带完整标注的YOLO训练素材。132张jpg图片,对应132个xml和132个txt,标签名统一叫keng(拼音的“坑”),累计1044个矩形框,平均每张图接近8个目标,单类检测,标注工具是labelImg。适合两类人:一是想把YOLOv5/YOLOv8全流程跑通的入门者,二是做行星影像、遥感小样本目标检测的从业者。我拿到这类资源不会急着解压开训,而是先做一轮格式对位和坐标校验。这份笔记就按这个顺序展开:结构、转换、训练、避坑、验证。
2. 目录与标注格式解构:VOC、YOLO、labelImg三方对位
2.1 解压后的第一件事:别急着开训,先数文件
下载来的压缩包解开后,最常见的情况是三类文件平铺在一个目录里:jpg、xml、txt混在一起。这份数据集摘要里写了“图片132个、xml 132个、txt 132个”,但网盘传输丢包、解压工具改名、Windows路径里有中文导致文件写失败,这些情况我都遇过。所以第一件事永远是静态检查,不上来就跑训练脚本。
ls -1 *.jpg | wc -l # 统计jpg数量,期望132 ls -1 *.xml | wc -l # 统计xml数量,期望132 ls -1 *.txt | wc -l # 统计txt数量,期望132数量一致只是第一步,还要确认三类文件的basename一一对应。资源展示页里给的文件名是firc_yunshi_109.jpg、firc_yunshi_82.jpg这样的命名,前缀统一、编号不连续——编号从27跳到39再跳到41,中间有断号很正常,不代表丢文件。真正的检查标准是同一个stem同时存在jpg、xml、txt三个扩展名:
for f in *.jpg; do stem="${f%.jpg}" [ -f "$stem.xml" ] || echo "missing xml: $stem" [ -f "$stem.txt" ] || echo "missing txt: $stem" done提示:跑任何数据清洗脚本之前,先把原始目录复制一份。
cp -r 原始目录 备份目录,这条习惯能救回很多次手误。我吃过直接mv后脚本跑错、原始标注被覆盖的亏,从那以后备份优先级最高。
2.2 VOC XML:labelImg默认产物的真实结构
labelImg画矩形框时,默认导出格式就是Pascal VOC的xml。很多人以为xml是给人类读的杂项文件,真正训练时只需要txt——这个理解会误导后续操作。xml才是标注母本,txt只是从它派生的产物。这份资源两个格式都给,恰恰方便我们做交叉校验。
下面是一段示意性的XML结构(尺寸和坐标是示例值,不是从压缩包里读出来的真实数据,别拿它当输入):
<annotation> <folder>firc_yunshi</folder> <filename>firc_yunshi_39.jpg</filename> <size> <width>1280</width> <height>960</height> <depth>3</depth> </size> <object> <name>keng</name> <pose>Unspecified</pose> <truncated>0</truncated> <difficult>0</difficult> <bndbox> <xmin>214</xmin> <ymin>156</ymin> <xmax>402</xmax> <ymax>341</ymax> </bndbox> </object> </annotation>这段XML里,训练真正关心的字段只有几个:filename决定对应哪张图,size记录图片宽高,object可能重复出现多次(一张图多个框就是多个object),name是类别名,bndbox的四个值是像素级坐标。字段说明整理如下:
| 字段 | 含义 | 训练管线里谁消费它 |
|---|---|---|
| filename | 图片文件名 | 转换脚本用它拼接图片路径 |
| size / width、height | 图片原始分辨率 | 归一化坐标时必须用到 |
| object / name | 类别名 | 类别白名单过滤,这里为keng |
| object / bndbox | 左上右下像素坐标 | 转成YOLO格式的中心点宽高 |
| difficult | 难例标记 | 部分框架会跳过difficult=1的框 |
需要注意difficult字段:一些检测框架对difficult=1的框直接忽略。这份数据的标注规则是“对类别画矩形框”,摘要里没有对难例做特殊标记,转换时按全部object正常处理即可。单个xml里有多个object是常态,解析时用findall('object')循环,不要取单个。
2.3 YOLO TXT:归一化坐标的换算关系
YOLO的txt每行代表一个目标,五列分别是:类别索引、归一化中心x、归一化中心y、归一化宽、归一化高。这份数据集只有一个类别keng,所以第一列永远是0。归一化意味着所有数值都在0到1之间,一旦出现小于0或大于1的数,说明转换链路里有越界。
对应上面那段示意XML,它的YOLO行应该是:
0 0.240625 0.258854 0.146875 0.192708推导过程不复杂:框的像素宽度是402-214=188,除以图像宽度1280得到0.146875;中心x坐标为(214+402)/2=308,除以1280得到0.240625。y方向同理,高度341-156=185,除以960约0.192708。这个换算关系是后续所有校验脚本的基础。
注意:txt里的浮点数别为了好看截成两位小数。归一化坐标截断两位,等于把边界框精度损失到百分之几像素级别,对陨石坑这种边缘纹理丰富的目标,精度损失会直接反映在mAP50-95上。我习惯保留六位小数。
这份数据的txt是作者转录好的,但训练前建议理解它的来源。另外摘要里特别写了一句“不包含分割路径的txt文件”——意思是这份数据只有检测框,没有多边形分割标注。如果拿它去跑YOLOv8-seg,会发现txt的行数解析对不上,这是预期行为,不是资源损坏。
3. 把VOC拉回YOLO训练管线:目录重组、转换脚本与单类yaml
3.1 目录重组:对齐YOLOv5/v8的images与labels约定
YOLO训练时按目录约定找数据:图片放images目录,同名txt放labels目录。这份资源解压后是平铺的,所以训练前必须先重组。常见做法是随机按8:2切训练集和验证集,132张图切完大约是105张训练、27张验证。切分时固定随机种子,保证换机器复现同一份划分。
我习惯在重组时保留xml作为独立母本:jpg和txt进images/labels,xml单独复制到xmls目录。这样后面修改标注后还能重建txt,相当于给自己留后悔药。
import random import shutil from pathlib import Path base = Path('.') backup = Path('backup_raw') # 先复制一份原始目录再操作 backup.mkdir(exist_ok=True) for f in list(base.glob('*.jpg')) + list(base.glob('*.xml')) + list(base.glob('*.txt')): shutil.copy2(f, backup / f.name) imgs = sorted(base.glob('*.jpg')) random.seed(42) # 固定种子,保证划分可复现 random.shuffle(imgs) n_train = int(len(imgs) * 0.8) train, val = imgs[:n_train], imgs[n_train:] for img_dir, parts in [('craters/images/train', train), ('craters/images/val', val)]: Path(img_dir).mkdir(parents=True, exist_ok=True) Path(img_dir.replace('images', 'labels')).mkdir(parents=True, exist_ok=True) Path('xmls/train').mkdir(parents=True, exist_ok=True) Path('xmls/val').mkdir(parents=True, exist_ok=True) for f in train: shutil.copy2(f, Path('craters/images/train') / f.name) shutil.copy2(base / (f.stem + '.txt'), Path('craters/labels/train') / (f.stem + '.txt')) shutil.copy2(base / (f.stem + '.xml'), Path('xmls/train') / (f.stem + '.xml')) for f in val: shutil.copy2(f, Path('craters/images/val') / f.name) shutil.copy2(base / (f.stem + '.txt'), Path('craters/labels/val') / (f.stem + '.txt')) shutil.copy2(base / (f.stem + '.xml'), Path('xmls/val') / (f.stem + '.xml'))这段脚本用的是copy不是move,跑两遍不会出错,对新手更友好。seed=42这个参数很关键:换一个seed,训练验证划分就完全不同,复现别人的实验对不上metrics时,先核对seed和版本。这里的8:2比例对132张的小数据集够用,不建议再砍验证集,27张验证图已经偏少了,再少mAP波动会大到没法看。
3.2 以XML为母本重建TXT:一份带坐标校验的转换脚本
既然资源里txt是现成的,为什么还要给一套转换脚本?因为实际工作中txt和xml不一致的概率不小:标注完xml忘记重新导出、手工改txt漏更新中心点、转手时txt被截断。我处理数据集的原则是“xml是唯一可信源,txt随时可以重建”。每次拿到这类双格式资源,我都会用xml重生成txt,然后和作者给的txt做对账。
import xml.etree.ElementTree as ET from pathlib import Path from PIL import Image def convert(xml_path: Path, img_dir: Path, out_dir: Path): root = ET.parse(xml_path).getroot() img_file = img_dir / root.findtext('filename') img = Image.open(img_file) img_w, img_h = img.size # 以实际图像尺寸为准,不信任xml里的size lines = [] for obj in root.findall('object'): if obj.findtext('name') != 'keng': continue box = obj.find('bndbox') xmin = float(box.findtext('xmin')) ymin = float(box.findtext('ymin')) xmax = float(box.findtext('xmax')) ymax = float(box.findtext('ymax')) cx = ((xmin + xmax) / 2) / img_w cy = ((ymin + ymax) / 2) / img_h w = (xmax - xmin) / img_w h = (ymax - ymin) / img_h if not (0 <= cx <= 1 and 0 <= cy <= 1 and w > 0 and h > 0): print(f'越界框: {xml_path.name}: {xmin},{ymin},{xmax},{ymax}') lines.append(f'0 {cx:.6f} {cy:.6f} {w:.6f} {h:.6f}') out_path = out_dir / (xml_path.stem + '.txt') out_path.write_text('\n'.join(lines) + '\n') img_dir = Path('craters/images/train') xml_dir = Path('xmls/train') out_dir = Path('craters/labels/train') for xml_path in sorted(xml_dir.glob('*.xml')): convert(xml_path, img_dir, out_dir)这段脚本的核心是第6行:用PIL重新读图拿真实尺寸,而不是直接取xml里的size字段。labelImg在不同环境下写出的size偶发和实际图片不一致,一旦出现偏差,所有归一化坐标会整体偏移。越界框打印而不是静默跳过,这样转换完能看到哪些文件有问题。
转换后做一个全局对账:
find craters/labels -name '*.txt' -exec cat {} + | wc -l摘要注明总框数1044,所以上面命令的输出期望是1044。如果少几个,用下面的脚本定位具体是哪个文件对不上:
import xml.etree.ElementTree as ET from pathlib import Path for xml_path in sorted(Path('xmls').glob('**/*.xml')): root = ET.parse(xml_path).getroot() n_xml = len(root.findall('object')) txt_path = Path('craters/labels/val') / (xml_path.stem + '.txt') if not txt_path.exists(): txt_path = Path('craters/labels/train') / (xml_path.stem + '.txt') n_txt = len(txt_path.read_text().strip().splitlines()) if txt_path.exists() else 0 if n_xml != n_txt: print(f'{xml_path.name}: xml={n_xml} 框, txt={n_txt} 行')这份数据集正常情况xml和txt是完全对齐的,如果不齐,优先信xml。
3.3 单类yaml与超参选择:132张小图怎么开参数
YOLO用yaml文件描述数据集路径和类别,这是训练管线的接缝。路径用相对路径配合path字段,换机器不用改代码:
path: craters/ train: images/train val: images/val nc: 1 names: 0: kengnc=1是单类,names的索引0对应txt每行的第一列0。如果你想把显示名改成crate或crater,只改names不影响训练,因为YOLO按索引对齐;但注意保持txt第一列不变。改类别名前先想清楚后续要不要继续在旧txt上标注,避免标注工具和训练管线的类别表对不上。
训练命令以YOLOv8为例:
yolo detect train \ data=craters.yaml \ model=yolov8n.yaml \ epochs=200 \ imgsz=640 \ batch=16 \ patience=50 \ cache=True \ mosaic=0.2关键参数怎么定,直接说结论:
| 参数 | 建议值 | 理由 |
|---|---|---|
| epochs | 200 | 小数据集收敛快,但给足余量配合早停 |
| patience | 50 | 连续50轮验证集无提升就停,132张图通常跑不到200轮 |
| imgsz | 640 | 接近常规目标检测默认分辨率;如果原图本身边长低于640,调低到416更保险 |
| batch | 16 | 单卡显存8G左右能跑,显存小就降到8 |
| mosaic | 0.2或0.0 | 小数据集mosaic越大越容易出现目标被切没,陨石坑这种圆形小目标尤其明显 |
小样本单类任务,期望值要放对位置:1044个框足够跑通完整流程并得到一个可复现的baseline,但别指望它跑到95%的mAP。这类深空目标检测常见mAP50平台期在0.6到0.8之间,如果远低于这个区间,优先检查目录对位和标注对齐,而不是换模型。
4. 避坑排查:小样本单类数据集最容易翻车的5个位置
4.1 现象:训练启动就提示找不到labels,或者0张训练图
操作YOLO时报错“No labels found in images/train”是最常见的启动失败。原因基本是目录约定没对齐:labels目录放的位置不对,或者txt扩展名大小写不统一。YOLO默认图片目录的同级目录下找同名txt,且要求绝对名为labels。排查就一条命令:
find craters -maxdepth 3 -type d -print看输出里craters/images/train和craters/labels/train是不是同级存在。另外确认jpg是.jpg而不是.JPG,Windows解压时偶尔会把扩展名改成大写,Linux下大小写敏感,直接导致找不到配对。
4.2 现象:框整体偏移,越靠近图片边缘偏差越大
训练能跑,但可视化时框和陨石坑位置有固定偏移,边缘区域尤其明显。原因多数是转换脚本用了xml里的size字段做归一化分母,而这个字段和图片真实分辨率不一致。解决办法就是3.2脚本里体现的:转换前用PIL读img.size作为分母,xml里的size只当参考。另外一个低频原因:图片被resize过但txt没跟着更新,这种只能返工重标。
4.3 现象:转换后总框数对不上1044
对账时发现总框数少了几个,或者某个文件xml框数和txt行数不相等。优先对照2.1的basename检查脚本,确认是不是某个txt在传输中变成空文件。再就是检查是否有object的difficult=1,如果作者导出txt时按difficult过滤过,xml和txt天然不等。这份数据的标注规则是纯矩形框全覆盖,没有difficult特殊化,所以遇到差值优先怀疑文件损坏或手工编辑残留。
4.4 现象:验证集mAP不错,拿到另一批影像上召回惨淡
这是深空影像最阴险的坑。标题写的“火星月球”在这份压缩包里没有子目录区分,文件名前缀统一是firc_yunshi,看不出天体来源。火星和月球的陨石坑在表面亮度、坑缘阴影、背景纹理上差异很大,如果132张图里两类天体混在一起,随机划分会让验证集“沾光”——模型背下了背景纹理而不是坑的本质特征。解决思路是:如果后续要跨天体泛化,不要随机划分,得按来源分层留验证集;但当前文件名没提供这个信息,所以现阶段目标应该定为“在同类影像上稳定复现”,跨天体推理需要自行补数据。
4.5 现象:单类混淆矩阵有问题,mAP50-95一直上不去
网上很多人说YOLO的混淆矩阵总和不是100%,其实单类任务里对角线之外基本都是背景误检,不用慌,重点看召回率。mAP50-95上不去的常见原因是目标尺度:陨石坑是典型的圆形小目标,默认anchor偏大,IOU 0.5能容忍,IOU 0.75就暴露边界回归误差。先统计框的尺度分布再决定策略:
import xml.etree.ElementTree as ET from pathlib import Path areas = [] for p in Path('xmls').glob('**/*.xml'): root = ET.parse(p).getroot() for obj in root.findall('object'): b = obj.find('bndbox') w = float(b.findtext('xmax')) - float(b.findtext('xmin')) h = float(b.findtext('ymax')) - float(b.findtext('ymin')) areas.append((w * h) ** 0.5) areas.sort() print(f'框数: {len(areas)}, 中位边长: {areas[len(areas)//2]:.1f}px')如果中位边长只占原图宽度的3%到5%,优先考虑用更高分辨率训练,或者把原图切成patch再训,而不是硬调anchor。小目标检测里,让目标占更多像素比换损失函数管用得多。
5. 小样本收尾:先验证再训练,把1044个框真正用起来
5.1 训练后的检查顺序
训练完成后不要只盯终端最后一行mAP。按这个顺序看:先看runs/detect/train/results.png里的loss曲线,确认train loss和val loss没有在最后阶段分叉——分叉就是过拟合信号;再看验证集PR曲线,单类任务里曲线下面积比单点mAP更有信息量;最后打开weights/best.pt和weights/last.pt,小数据集上best和last往往差距很大,用best。
5.2 用预测图做人工复核
指标好看不等于框准。陨石坑边缘阴影变化剧烈,有时候框中心对但边界错半个坑位。跑一遍预测,把验证集结果存图:
yolo predict model=runs/detect/train/weights/best.pt \ source=craters/images/val save=True imgsz=640人工看一遍预测图,重点看三类错误:漏检(没框上)、半框(只框住坑缘一部分)、多检(一块岩石纹理被当成坑)。这三类错误在mAP曲线上都是零分,但只有人眼能分清楚是哪种。
5.3 增量比换骨架有用
132张的小数据集,与其堆epoch或者换更大的模型,不如做增量:把预测结果里置信度在0.2到0.5之间的框导出来,用labelImg打开原图二次确认,把确认的框补进标注。我一般先跑一轮预测,把漏检和误检筛出来,人工挑拣后合并进xml,然后重新执行3.2的转换脚本重建txt。
从那以后,我每次拿到标注资源都强制走一遍流程:先数文件对齐basename,再以xml为母本重建txt并核对总框数,然后才进yaml开训。这套流程听着笨,但对小样本数据集,它能在两小时内拦住八成以上的翻车,省下的调试时间远超转换脚本那几分钟。希望帮到你。
本文还有配套的精品资源,点击获取