1. 从零开始:理解Cityscapes数据集与实例分割任务

如果你刚接触自动驾驶或者街景理解相关的计算机视觉项目,那你大概率会听说过Cityscapes这个数据集。我第一次用它的时候,感觉就像拿到了一本城市街道的“百科全书”,内容极其丰富,但也确实有点让人头大。简单来说,Cityscapes是一个专注于城市街景语义理解和实例分割的大型数据集。它包含了从50个不同城市采集的、在各种季节和天气条件下的高质量图像,每张图像都提供了像素级的精细标注。

什么是实例分割?你可以把它理解为目标检测和语义分割的“结合体”。目标检测是给你画个框,告诉你“这里有一辆车”;语义分割是告诉你“图像里每一个像素属于什么类别”,比如天空、道路、建筑;而实例分割则更进一步,它不仅要区分类别,还要区分同一类别下的不同个体,也就是告诉你“这是第一辆车,那是第二辆车,并且精确到它们的轮廓”。这对于自动驾驶汽车理解周围环境、区分行人、车辆等独立物体至关重要。

Cityscapes数据集的官方标注里,包含了30多个类别,但并不是所有类别都支持实例分割。只有那些 hasInstances=True 的类别才行,主要是“可移动的物体”,比如人、骑行者、小汽车、卡车、公交车、火车、摩托车、自行车这8类。这也是我们实战中主要关注的对象。数据集下载后,你会发现它的目录结构很有规律,主要包含 leftImg8bit(图像)和 gtFine(精细标注)两个文件夹,里面又按城市分成了子文件夹。gtFine里的标注文件,特别是 *_instanceIds.png 这个文件,是我们做实例分割的关键,它用不同的整数值来编码每个独立的物体实例。

直接拿Cityscapes的原始格式去训练YOLO v8是行不通的,因为两者的“语言”不通。主流的深度学习框架(包括Ultralytics的YOLO)更普遍支持的是COCO数据集格式。所以,我们这场实战的核心任务之一,就是充当一个“翻译官”,把Cityscapes的“方言”先翻译成COCO这个“通用语”,再转换成YOLO能直接听懂的“方言”。这个过程听起来有点绕,但一步步走下来,你会发现其实逻辑很清晰。接下来,我就带你亲手走一遍这个完整的流程,把路上容易踩的坑都给你标出来。

2. 数据预处理实战:从Cityscapes到COCO格式的完整转换

原始文章里给了一段转换代码,但说实话,直接跑起来可能会遇到不少问题。我结合自己的经验,给你梳理一个更稳健、更详细的转换流程。首先,你得把数据集准备好。从Cityscapes官网注册下载后,你会得到一堆压缩包。解压后,建议你建立一个清晰的项目目录,比如这样:

cityscapes_project/
├── raw_data/
│   ├── leftImg8bit/   # 原始图像
│   │   ├── train/
│   │   ├── val/
│   │   └── test/
│   └── gtFine/        # 精细标注
│       ├── train/
│       ├── val/
│       └── test/
└── coco_format/       # 转换后的COCO格式数据

转换的第一步,是理解Cityscapes的标注文件 *_instanceIds.png。这张图看起来是黑白的,但实际上每个像素点的值是一个整数。这个整数的构成很有讲究:它的 千位和百位代表类别ID个位和十位代表实例ID。举个例子,像素值 26005,其中 26 代表类别(小汽车),005 代表这是这张图片里的第5辆小汽车。背景和不需要的类别,其ID被定义在一个 background_label 列表里,转换时需要过滤掉。

原始文章中的代码核心是三个函数:image_trans(裁剪和保存图像与标注)、data_loader/masks_generator(生成每个实例的独立掩码图片)、json_generate(生成最终的COCO格式JSON文件)。这里我着重讲几个容易出错的细节和优化点。

首先是图像裁剪。Cityscapes图像是宽高比约为2:1的长条形(2048x1024),有时为了适配模型或聚焦道路区域,我们会做中心裁剪。代码里的 pic_scaleh_bias 参数就是干这个的。我建议你先把这两个参数都设为1.0,也就是不裁剪,先确保转换流程能跑通。等整个流程没问题了,再根据你的模型输入尺寸来调整裁剪策略,并务必对标注图像做完全相同的裁剪,否则标注就对不上了。

第二个关键点是生成实例掩码。原始代码为每一个实例ID都生成了一张独立的黑白掩码图片(instance_mask),这非常占用磁盘空间。如果你的数据集很大,比如用了完整的 train 集(近3000张图片),生成的掩码图片可能会达到几十万张,不仅慢,还占地方。一个更高效的实践是,不保存这些中间掩码图片,而是在内存中直接处理,并生成COCO的标注。我们可以利用 pycococreatortools 工具库,它需要的只是二值化的掩码数组(binary_mask),我们可以通过 annotation == id 直接得到这个数组,然后调用 binary_mask_to_polygon 函数将其转换为多边形轮廓,进而生成COCO的 segmentation 字段。这样可以省去大量的I/O操作。

第三个,也是最大的一个坑,就是多部分实例的处理。Cityscapes里,一个物体(比如一辆被电线杆部分遮挡的公交车)可能会被标注成多个不连通的部分(多个blob)。原始代码和很多网上的转换脚本在这里都会报错,错误信息就是文章末尾提到的那个 ValueError: setting an array element with a sequence。这是因为 pycococreatortoolsbinary_mask_to_polygon 函数在处理多个轮廓时,旧的写法 contours = np.subtract(contours, 1) 对列表操作不兼容。解决方法正如文中提到的,需要将这一行替换为 contours = [np.subtract(c, 1) for c in contours]。但更重要的是,你要理解,在COCO格式中,一个实例的多部分应该被编码为 segmentation 字段下的多个多边形列表。例如:"segmentation": [[x1,y1,x2,y2,...], [x3,y3,x4,y4,...]]。确保你的转换代码能正确处理这种情况,否则会丢失部分标注信息,严重影响模型训练效果。

最后是生成COCO的JSON文件。你需要按照COCO的标准结构来组织 infolicensesimagesannotationscategories 这几个部分。其中 annotations 里的每个条目都要包含 idimage_idcategory_idsegmentation(多边形点集)、bbox(外接矩形框)和 area(面积)等信息。pycococreatortools.create_annotation_info 这个函数能帮你处理大部分繁琐的工作,包括多边形简化(tolerance 参数)。转换完成后,你应该得到 instances_train2017.jsoninstances_val2017.json 这样的文件,以及对应的图片文件夹。你可以用官方的COCO API(pycocotools)加载一下JSON文件,验证一下标注是否正确可视化了。

3. 格式再转换:让COCO数据适配YOLO v8

拿到COCO格式的数据后,我们离用YOLO v8训练就只差最后一步了:转换成YOLO格式。YOLO v8实例分割需要的标签格式,和目标检测的 .txt 文件类似,但内容不同。每个图像对应一个 .txt 文件,文件里的每一行代表一个实例。每一行的格式是:

<class_id> <x1> <y1> <x2> <y2> ... <xn> <yn>

这里的 class_id 是类别索引(从0开始),而后面跟着的不是边界框,而是多边形轮廓的所有顶点坐标,并且这些坐标是归一化的(即坐标值除以图像的宽度或高度,取值范围在0到1之间)。这是最关键的一点。

怎么转换呢?最稳妥、最推荐的方法,就是使用YOLO v8官方自带的工具。Ultralytics框架在 ultralytics.data.converter 模块里提供了一个 convert_coco 函数,它就是专门干这个的。你只需要几行代码:

from ultralytics.data.converter import convert_coco

# 指定你的COCO标注JSON文件所在的目录
# 假设你的目录结构是:
# path/to/coco/
#   ├── annotations/instances_train2017.json
#   └── train2017/  # 存放训练图片
labels_dir = 'path/to/coco/annotations/'
# 关键参数:use_segments=True 表示转换实例分割标签(多边形),False则只转换检测框。
convert_coco(labels_dir=labels_dir, use_segments=True)

运行这个命令,它会自动在 labels_dir 的同级目录下,创建符合YOLO格式要求的 labels 文件夹,里面就是每个图片对应的 .txt 标注文件。同时,它通常也会帮你整理好图像的软链接或目录结构。这个方法之所以最推荐,是因为它直接来自框架开发者,能最大程度保证和训练代码的兼容性,并且内置了对多部分实例(即一个实例对应多个多边形)的正确处理

那为什么不要轻易用网上找的“别人的代码”呢?原始文章最后对比了两张图,直观地展示了问题。很多自定义脚本在处理COCO的 segmentation 字段时,默认它只包含一个多边形列表(即 segmentation[0])。但如果遇到我们前面提到的“多部分实例”,segmentation 就是一个包含多个多边形列表的列表。自定义脚本如果只取第一个列表,就会丢失其他部分,导致转换后的标签不完整。从文章里的效果图就能看出,官方工具转换的标签完整勾勒了物体轮廓,而一个有问题的自定义脚本转换的标签可能只覆盖了物体的一部分,这样的数据拿去训练,模型肯定学不好。

如果你出于某些原因必须自己写转换脚本,那么核心逻辑就是遍历COCO JSON中的每一个 annotation,提取 category_id 并减1(转为0起始),然后处理 segmentation。这里必须用循环判断 segmentation 是包含多个多边形还是单个,然后将每个多边形的所有点坐标逐一归一化,最后按顺序写入文件。这个过程需要非常小心,确保多边形的点序正确(通常是顺时针或逆时针闭合),并且归一化计算准确。

4. 模型训练与调优:用YOLO v8真正跑起来

数据格式终于搞定了,现在让我们用YOLO v8来训练模型。首先确保你的环境已经安装了Ultralytics库:pip install ultralytics。准备好你的数据,按照YOLO喜欢的格式组织:

datasets/cityscapes_yolo/
├── images/
│   ├── train/
│   │   ├── aachen_000000_000019_leftImg8bit.png
│   │   └── ...
│   └── val/
│       ├── bielefeld_000000_000019_leftImg8bit.png
│       └── ...
└── labels/
    ├── train/
    │   ├── aachen_000000_000019_leftImg8bit.txt
    │   └── ...
    └── val/
        ├── bielefeld_000000_000019_leftImg8bit.txt
        └── ...

接下来,你需要一个数据集配置文件,通常是一个 .yaml 文件,比如 cityscapes.yaml

# cityscapes.yaml
path: /path/to/your/datasets/cityscapes_yolo  # 数据集根目录
train: images/train  # 训练集图像路径(相对于path)
val: images/val      # 验证集图像路径(相对于path)

# 类别数量和名称
nc: 8  # 我们使用的8个实例类别
names: ['person', 'rider', 'car', 'truck', 'bus', 'train', 'motorcycle', 'bicycle']

训练命令非常简单,使用 yolo 命令行接口或者Python API都可以。命令行方式最直接:

yolo segment train data=cityscapes.yaml model=yolov8n-seg.pt epochs=100 imgsz=640 batch=16

我来解释一下这几个关键参数:

  • segment:指定进行实例分割任务。
  • data:指向你的数据集配置文件。
  • model:指定预训练模型。yolov8n-seg.pt 是纳米尺度的分割模型,体积小速度快,适合快速验证。还有 s(小)、m(中)、l(大)、x(特大)等尺寸,模型越大通常精度越高,但训练和推理也越慢。
  • epochs:训练轮数。对于Cityscapes这样的复杂数据集,100轮可能只是个起点,你可能需要更多。
  • imgsz:输入图像尺寸。YOLO v8训练时会自动将图像缩放到这个尺寸。640是常用尺寸,你也可以尝试更大的尺寸如1024来获取更好精度(但需要更多显存)。
  • batch:批次大小。取决于你的GPU显存,16是一个常见的起始值。如果出现CUDA out of memory错误,就需要减小这个值。

在训练过程中,我强烈建议你开启验证和可视化。Ultralytics会实时在终端输出损失曲线、精度指标(如mAP50、mAP50-95),并保存最好的模型权重(best.pt)和最后的模型权重(last.pt)。训练结束后,你可以用以下命令在验证集上评估模型效果:

yolo segment val model=runs/segment/train/weights/best.pt data=cityscapes.yaml

如果想直观地看预测效果,可以用预测模式:

yolo segment predict model=runs/segment/train/weights/best.pt source=path/to/your/test/image.jpg

训练时可能会遇到一些挑战。一是类别不平衡:Cityscapes里“小汽车”的数量远远多于“火车”或“摩托车”。这可能导致模型对稀少类别学习不足。解决方法包括使用带权重的损失函数,或者在数据增强时对稀少类别进行过采样。二是显存限制:实例分割任务比目标检测更耗显存,尤其是使用大模型或大图像时。如果显存不够,除了减小 batchimgsz,还可以尝试使用梯度累积(accumulate 参数)来模拟更大的批次。三是过拟合:如果训练集精度很高但验证集精度上不去,可能就是过拟合了。除了使用数据增强(YOLO v8默认已启用),还可以尝试增加 dropout 率、使用更早的停止策略(patience 参数),或者收集更多样化的训练数据。

5. 模型部署与性能优化:让模型在实际场景中跑得更快更稳

模型训练好了,精度也不错,接下来就是把它用起来,也就是部署。部署的环境可能多种多样,比如服务器(Linux)、边缘计算设备(Jetson系列)、甚至是手机。不同的环境对模型格式和推理速度有不同的要求。

最直接的方式就是使用训练好的 best.pt 文件,用Ultralytics库加载并进行推理。这在Python环境中非常方便:

from ultralytics import YOLO

model = YOLO('runs/segment/train/weights/best.pt')
results = model('your_image.jpg', save=True)  # 保存预测结果

但很多时候,我们需要将模型转换成其他格式,以便在不同的推理引擎上运行。ONNX 是一个开放的模型交换格式,被众多推理框架支持(如OpenVINO, TensorRT, ONNX Runtime等)。用YOLO v8导出ONNX非常简单:

yolo export model=runs/segment/train/weights/best.pt format=onnx

导出的ONNX模型就可以被其他框架加载了。不过,直接导出的ONNX模型可能没有针对特定硬件做优化。如果你追求极致的推理速度,尤其是在边缘设备上,TensorRT 是NVIDIA GPU上的不二之选。你可以使用 export 命令直接导出TensorRT引擎,或者先将模型导出为ONNX,再用TensorRT的 trtexec 工具进行转换和优化。这个过程会进行图层融合、精度校准(FP16/INT8量化),能显著提升推理速度。

对于没有GPU的环境或者移动端,可以考虑 OpenVINOCoreML 格式。OpenVINO可以优化Intel CPU、集成显卡和神经计算棒的推理性能。导出命令是 format=openvino。CoreML则是苹果生态(iOS/macOS)的标准格式。

在部署时,有几点性能优化技巧值得注意。一是动态批处理:如果你的应用场景需要同时处理多张图片,确保推理引擎支持动态批处理,这能更好地利用GPU并行计算能力。二是输入尺寸:训练时我们可能用了640x640,但部署时输入的图像尺寸可能不固定。ONNX导出时可以使用 dynamic 参数指定动态的维度,但要注意,有些推理引擎对动态尺寸的支持不如固定尺寸好。如果可能,在部署前将输入图像固定为训练尺寸,或者使用简单的填充/缩放策略。三是后处理优化:YOLO的输出后处理(非极大值抑制NMS)也是一个计算环节。在导出ONNX时,可以选择包含NMS(iou_thresconf_thres 参数),这样推理引擎的输出就是最终的检测框和掩码,省去了在应用层再做后处理的开销。

最后,别忘了进行端到端的性能测试。不仅仅是在验证集上测mAP,还要在目标部署环境中,用真实的输入数据流测试帧率(FPS)、延迟和内存占用。有时候,模型精度轻微下降一点,换来的推理速度提升可能是几倍,这在实时性要求高的场景(如自动驾驶)下是非常值得的。我自己的经验是,在Jetson Orin Nano上,将YOLOv8s-seg模型转换为FP16精度的TensorRT引擎,推理速度能从原来的每秒十几帧提升到三十帧以上,效果立竿见影。

Logo

北京人形旗下天工造物具身智能开源社区,聚焦具身天工与慧思开物两大平台

更多推荐