☰
Mask R-CNN气球分割实战:小样本实例分割的完整指南
2026/10/11 21:48:56 网站建设 项目流程

简介:Mask R-CNN是兼顾目标检测与实例分割的深度学习架构,此代码包以气球识别为实战场景,完整提供基于TensorFlow/Keras的工程实现与实验数据,适合想快速上手实例分割的开发者与研究者。压缩包共76个文件,以Python源码和Jupyter Notebook为主,辅以大量jpg/png样例图片、gif演示图及说明文档,总大小73.68MB,结构清晰便于按模块查阅。目前已有339人学习下载。资源内包含模型配置、网络构建、工具函数等核心脚本,以及针对气球数据集的训练与推理notebook,配合检测过程的可视化截图和动图,可帮助读者从数据准备、模型训练、参数调优到效果评估完整跑通流程,并深入理解FPN、RPN和Mask分支的协作机制,为后续将Mask R-CNN迁移到其他检测分割任务打下扎实基础。

1. Mask R-CNN 气球分割:一百多张标注图也能跑通实例分割

Mask R-CNN 在真实需求里最常见的卡点不是模型有多深,而是拿不到成规模的标注数据。这份 mask_rcnn_ballon 资源正是奔着这个痛点来的:它用一套完整的气球检测与分割流程,示范了如何靠一百多张图片就训练出能输出像素级掩码的实例分割模型。它适合正在做目标检测预研、想顺手上实例分割的应用开发者,也适合课程设计和算法入门阶段需要完整可复现流程的读者。整条链路从多边形标注、自定义数据集类、迁移学习训练到 splash 可视化都有现成代码,拆开跑一遍,你对 Mask R-CNN 的工程心智模型会清晰很多。

2. 选型与结构:为什么 COCO 预训练权重是最省事的起点

2.1 实例分割选型:Mask R-CNN 的气球任务优势

做气球分割之前,先明确一个问题:为什么不是 U-Net?U-Net 做的是语义分割,输出每个像素的类别,两个气球重叠时会被归成同一个连通域,分不开实例。而气球这种场景往往有成串、相互遮挡的目标,业务上需要「图里有几个气球、每个气球各自的轮廓」这样的实例级结果,这就是 Mask R-CNN 的核心价值——检测框、类别、像素掩码三条分支同时输出。

模型结构上,Mask R-CNN 先靠 RPN 生成候选区域,再用 RoIAlign 把每个候选框的特征对齐到固定尺寸,最后在同一个特征图上分出分类、回归和掩码三个头。相比单阶段的实例分割方案,二阶段结构在目标重叠、尺度差异大的数据上更稳,且 mask 分支对边框回归精度的依赖更低——这在标注框比较随意、但多边形轮廓画得仔细的气球数据上恰好合适。

这份资源的实现默认使用 ResNet101 加 FPN 做特征提取。FPN 的意义在于把不同尺度的气球特征都送到 RPN 里,气球在图片里有时只占几十像素,有时铺满半屏,全靠多层特征图兜底。默认 backbone 在资源和精度之间取得平衡,如果你只有 CPU 或显存吃紧,改成 resnet50 也简单,代价是部分小目标的召回会下降。

2.2 资源包内结构:核心网络文件与 balloon 示例的关系

拿到 mask_rcnn_ballon 压缩包,第一件事不是急着跑训练,而是先分清哪些文件是通用框架、哪些是气球专用代码。真正要改的文件只有一个目录,其他大多数文件在迁移到自己数据集时不需要动。

文件路径作用迁移时是否要改
mrcnn/model.pyMask R-CNN 网络定义、损失计算、训练循环不改
mrcnn/utils.py锚点生成、NMS、数据增强、日志工具多数不改
mrcnn/visualize.py检测框、掩码、splash 可视化通常不改
mrcnn/config.py默认配置类,所有训练参数的基础一般只读
samples/balloon/balloon.py气球数据集类、训练/推理入口迁移时照此改写
samples/balloon/inspect_balloon_data.ipynb检查数据与 mask 对齐直接运行
samples/balloon/train_balloon.ipynb逐步训练、评估的交互入口直接运行

最核心的是 mrcnn/model.py,里面就像是 Mask R-CNN 的黑匣子拆解:锚点生成、RPN 损失、mask 分支的 sigmoid 二分类、训练数据组装全在这一处。实际项目里不要动它,除非你明确要做网络结构层面的改动。气球相关的逻辑都集中在 balloon.py,包括数据集加载、配置类、训练与启动参数解析,这份文件在迁移到别的目标时是主要改动对象。

2.3 环境依赖:把 TensorFlow 与 Keras 版本钉死

老一点的开源 Mask R-CNN 实现大多长在 TensorFlow 1.x 的时代,默认用keras.engine和tf.Session这类接口。这份资源包经过后人适配,很多版本改成tf.keras,但仍然挑 TensorFlow 版本,尤其是 TensorFlow 2.10 以上对旧 API 兼容性很差。我的习惯是先建独立环境,把依赖一次性装到位,避免和已有项目互相污染。

conda create -n maskrcnn python=3.7 -y conda activate maskrcnn pip install tensorflow==1.15.0 keras==2.2.5 pip install numpy==1.18.5 scipy==1.4.1 opencv-python==4.1.2.30 pip install scikit-image==0.16.2 cython pillow imgaug==0.2.8

逻辑说明:先用 conda 建立隔离环境,再把与代码兼容的 TensorFlow、Keras 和图像库版本一次性装齐,避免后续因为某个包升级把网络定义打崩。参数说明:python 版本锁定 3.7,是因为老一代 Mask R-CNN 实现里大量用到tf.Session与keras.backend的旧接口,python 3.8 以上配合旧 TensorFlow 会出现编译层面的兼容问题。

注意:如果你拿到的是已经适配 TensorFlow 2.x 的版本,keras包不要单独安装,直接用tf.keras的接口。最稳妥的做法是先看 balloon.py 里 import 的是keras还是tensorflow.keras,再决定装哪个版本。

装完依赖后,在项目根目录执行一个最小验证:能成功 import 框架、能实例化默认配置,就算环境通了。

import tensorflow as tf import mrcnn.model as modellib from mrcnn.config import Config class Cfg(Config): NAME = "test" GPU_COUNT = 1 IMAGES_PER_GPU = 1 NUM_CLASSES = 2 cfg = Cfg() print("TF:", tf.__version__, "| backbone:", cfg.BACKBONE)

这段代码的逻辑说明:通过自定义一个最小配置类验证网络构建接口没有被版本问题卡住。参数说明:NUM_CLASSES=2表示背景加一类目标,这是 Mask R-CNN 的固定规则;IMAGES_PER_GPU=1单卡一次只喂一张图,验证时最省显存。如果这里报get_default_graph或placeholder相关错误,说明当前 TF 版本与代码不匹配,直接回 5.1 节对照处理。

3. 数据管道:VIA 多边形标注怎么变成网络能吃的 mask 数组

3.1 标注文件结构:先看懂 via_region_data.json

气球数据集的标注不是 COCO 格式,而是 VIA 工具导出的 JSON。这种格式的特点是以图片为 key,每张图片下面挂若干 region,每个 region 用多边形点坐标描述目标轮廓。点坐标是图像上的绝对像素值,不是归一化值,这一点决定了后面读取时不需要再乘宽高。

{ "balloon_1_1535111237.jpg1356196": { "filename": "balloon_1_1535111237.jpg", "size": 1356196, "regions": [ { "shape_attributes": { "name": "polygon", "all_points_x": [412, 435, 460, 488], "all_points_y": [88, 79, 76, 82] }, "region_attributes": { "balloon": "1" } } ] } }

这段 JSON 的关键点有两处。第一,顶层 key 是「文件名加哈希后缀」拼出来的字符串,取值时不能直接拿它当文件名用,要读里头的filename字段。第二,all_points_x与all_points_y逐位配对成一个多边形的顶点,读的时候必须按配对循环zip(points_x, points_y),很多初学者只取 x 或 y 导致 mask 变成一条线。region_attributes里的键名是这个数据集的类别标签,迁移到自己的数据集后键名通常要跟着改。

与 COCO 标注相比,VIA 格式没有单独的annotations数组,也没有统一的category_id映射,所以自定义 Dataset 类里要把「每个 region 的类别」映射成从 1 开始的整型 ID。注意 Mask R-CNN 里 0 永远留给背景,类别 ID 从 1 起步,漏了这条规则会在训练第一轮就报维度错误。

3.2 自定义 Dataset:load_image 与 load_mask 的实现

训练时模型对数据加载器有两个硬性要求:load_image返回解码后的图像矩阵,load_mask返回这个图像对应的掩码栈和类别 ID 列表。气球 demo 的核心就在这两个函数里,下面是精简后可以直接照抄再扩展的骨架。

import os import json import numpy as np import cv2 from mrcnn.utils import Dataset class BalloonDataset(Dataset): def load_balloon(self, dataset_dir, subset): self.add_class("balloon", 1, "balloon") json_path = os.path.join(dataset_dir, subset, "via_region_data.json") with open(json_path) as f: annotations = json.load(f) for key, ann in annotations.items(): if not ann["regions"]: continue polygons = [] for r in ann["regions"]: sx = r["shape_attributes"]["all_points_x"] sy = r["shape_attributes"]["all_points_y"] polygons.append({"name": "polygon", "all_points_x": sx, "all_points_y": sy}) image_path = os.path.join(dataset_dir, subset, ann["filename"]) self.add_image("balloon", image_id=key, path=image_path, polygons=polygons) def load_mask(self, image_id): info = self.image_info[image_id] mask = np.zeros([info["height"], info["width"], len(info["polygons"])], dtype=np.uint8) for i, p in enumerate(info["polygons"]): pts = np.array([list(zip(p["all_points_x"], p["all_points_y"]))], dtype=np.int32) cv2.fillPoly(mask[:, :, i], pts, 1) return mask, np.ones([mask.shape[-1]], dtype=np.int32)

逻辑说明:load_balloon负责把 JSON 里的 filename、多边形点对和本地图片路径绑定在一起,挂到父类add_image上;load_mask在训练迭代时被调用,它按图像的实际高度宽度先建一个空的三维数组,再用 OpenCV 的fillPoly把每个多边形的封闭区域填成 1,返回的 masks 数组是H×W×N,N 表示这一张图里气球实例的个数。class_ids 全部返回 1,因为当前只处理「气球」这一个前景类。

参数说明:dataset_dir是数据根目录,subset是train或val子目录名;add_class("balloon", 1, "balloon")的第二个参数 1 是类别 ID,必须大于 0;load_mask返回的 uint8 数组内部会被转成 bool,参与 mask 分支损失计算时逐像素做 sigmoid 二分类。如果某个多边形点的坐标超出图像边界,fillPoly会自动裁剪,不会崩,但会留下半截 mask,训练时最好在检查环节用可视化确认。

3.3 训练集与验证集划分:别把同一串气球分进两个集合

气球数据量不大,常见的切法是按图片相对路径的散列值做 80/20 划分。这里有个隐藏的项目经验:不要随机 shuffle,因为相邻拍摄的气球图像往往高度相似,随机切分容易把同一场景的不同帧分别拆进训练和验证,导致验证结果虚高。

import random from pathlib import Path all_images = list(Path("balloon/dataset/train").rglob("*.jpg")) random.Random(42).shuffle(all_images) val_ratio = 0.2 val_count = int(len(all_images) * val_ratio) val_set, train_set = all_images[:val_count], all_images[val_count:]

逻辑说明:先用固定随机种子做一次全量 shuffle,再按比例切成验证与训练两批。固定种子保证每次拆出来的集合完全一致,便于复现训练结果。参数说明:val_ratio在气球这种百张图片规模的数据上取 0.2 比较合理,验证集留十几张足够看出趋势;如果数据总量只有三五十张,建议降到 0.15,否则验证 loss 的置信区间会大得没法看。

实际使用时,你完全可以不手动切分,而是沿用 balloon.py 里现成的划分逻辑:它读图片的 hash 值做模运算,保证每次运行时划分结果稳定。重点是想清楚一个原则——所有操作同一文件夹几帧连拍图片的样本必须进同一边,宁可训练少一张,也别让验证集泄露。

4. 训练与推理实操:参数设置、三阶段迁移与输出验证

4.1 配置类与超参数:BalloonConfig 到底在调什么

整个训练的魂在 Config 子类。气球样例的配置类写得极简,但每个字段背后都有坑,先看代码再对表。

class BalloonConfig(Config): NAME = "balloon" IMAGES_PER_GPU = 2 STEPS_PER_EPOCH = 100 VALIDATION_STEPS = 50 NUM_CLASSES = 1 + 1 RPN_ANCHOR_SCALES = (16, 32, 64, 128, 256) DETECTION_MIN_CONFIDENCE = 0.7

这段配置的含义:NAME决定了日志和权重文件的命名前缀;NUM_CLASSES = 1 + 1里左 1 是背景,右 1 是气球;RPN_ANCHOR_SCALES控制了锚点基础尺寸,默认可覆盖从几十像素到两百像素左右的常见目标。

参数取值调整逻辑
IMAGES_PER_GPU2直接影响 batch size,2 是 11GB 显存下的稳妥起点;8GB 卡必须降到 1
STEPS_PER_EPOCH100每个 epoch 的迭代数,百张图级别保持 100 已有足够多样性
VALIDATION_STEPS50验证阶段的迭代数,设太大只是浪费时间
DETECTION_MIN_CONFIDENCE0.7推理时过滤低置信度框,过小会输出一堆重叠框
RPN_ANCHOR_SCALES(16,...,256)锚点尺度上限偏小,若目标占画面一半要加 512

参数调整要结合显存和骨干网络一起考虑。IMAGES_PER_GPU=2意味着每个 step 同时前向两张 1024×1024 图像,特征图和 RoI 特征一起占显存,8GB 卡经常会崩,优先降这个参数而不是盲目调锚点。DETECTION_MIN_CONFIDENCE只影响推理,不影响训练,验证时如果发现框很多,先把阈值拉到 0.9 看结果再回退。

4.2 三阶段迁移:heads、all 与 last 分别怎么用

预训练权重的迁移方式是这份资源最值得抄的部分。COCO 上训出来的网络已经会识别「物体」的通用特征,气球的标注量小,从头训练几乎必翻车,所以标准流程分三步:先用 COCO 权重只训练新加的分类与掩码头,让新类别头尽快收敛;再针对全部层做微调;后续迭代用上一次的 last 权重继续。

# 第一阶段:只训练新头,用 COCO 预训练权重 python samples/balloon/balloon.py train \ --dataset=path/to/balloon/dataset \ --weights=coco # 第二阶段:加载刚才的 last 权重,放开全部层微调 python samples/balloon/balloon.py train \ --dataset=path/to/balloon/dataset \ --weights=last

逻辑说明:--weights=coco会走框架里的下载逻辑拉取 COCO 预训练权重,下载失败时把它手动放到mask_rcnn_coco.h5的预期路径即可。第二阶段--weights=last读取上一轮保存的mask_rcnn_balloon.h5,在它的基础上全量更新。参数说明:第一阶段优先跑够 5 到 10 个 epoch,确认分类损失有下降趋势再进第二阶段;第二阶段学习率会按配置自动衰减,如果你的自定义实现里没写衰减逻辑,就把第二阶段 epoch 数减半观察。

训练过程中需要盯的指标不是准确率,而是总体 loss 曲线和专门打印的 mask 分支 loss。气球数据上典型的表现是前两三个 epoch loss 快速下降,随后变缓;如果出现 loss 直接 NAN,优先排查 5.1 节的版本问题和学习率过大。

4.3 推理与 splash 可视化:看模型真正输出什么

训练完成后最直观的验证方式就是让模型跑一张没见过的图,把 mask 叠加回原图。balloon.py 已经封装好了 splash 入口。

python samples/balloon/balloon.py splash \ --image=path/to/test.jpg \ --weights=mask_rcnn_balloon.h5

逻辑说明:splash 模式加载权重后,把输入图缩放到模型要求的 1024×1024,前向得到每个实例的框、类别和 H×W 的像素掩码,再把掩码 resize 回原始尺寸,用半透明颜色叠加在原图上输出。参数说明:--image指向单张图片文件;如果想批量处理一个文件夹,改成--images=dir并配合--limit限制数量。输出图不会自动覆盖原图,会写到项目目录下带splash前缀的新图,方便前后对比。

推理输出的 mask 是每个实例独立的一份,同一像素可能属于多个重叠气球,这正是实例分割区别于语义分割的实际表现。验证时重点看两个位置:两个气球互相遮挡的边缘 mask 是否被分开,以及小气球是否被漏检。如果小目标成串漏掉,就回 4.1 节给锚点加 512 级别的尺度,或者把DETECTION_MIN_CONFIDENCE降到 0.5 观察。

5. 避坑与常见问题:四个最容易让新手翻车的运行位置

5.1 TensorFlow 与 Keras 版本不匹配,第一步就崩

现象:装好依赖后 import 框架或跑第一个 epoch,报AttributeError: module 'tensorflow' has no attribute 'get_default_graph',或者cannot import name 'BatchNorm'。

原因:这份资源是较长生命周期内的产物,早期版本用独立 Keras 包,后期改造成tf.keras,两套接口在 TensorFlow 2.10 以上版本里不再是同一套 API。get_default_graph、tf.placeholder这类符号在 TF 2.x 里被彻底移掉,而网络定义里还残存着这些调用。

解决:先看 balloon.py 与 mrcnn/model.py 顶部 import,是import keras还是from tensorflow import keras。前者就上 TensorFlow 1.15 加 Keras 2.2.5;后者用 TensorFlow 2.4 到 2.8 之间的版本,并装配套tf.keras。定好版本后不要再顺手升级其他依赖,尤其是 numpy 高于 1.24 会频繁触发抛错。

5.2 显存溢出:一张 1024 图也能把 8GB 卡吃满

现象:训练跑到中途报ResourceExhaustedError: OOM when allocating tensor,有时发生在第一个 epoch,有时发生在第几十个 step,触发点不确定。

原因:Mask R-CNN 的训练显存大头既不在输入图,也不在模型权重,而在 RoI 特征本身。每个训练图会采样 200 个 RoI,每个 RoI 再做 RoIAlign 池化,特征图叠加后显存开销成倍放大。气球场景下如果IMAGES_PER_GPU=2,8GB 卡几乎必爆。

解决:先把IMAGES_PER_GPU降到 1,这是性价比最高的操作;依然超了就调低TRAIN_ROIS_PER_IMAGE,从默认 200 降到 128,代价是每步采样到的正样本变少,但不会结构性破坏训练。再不行,把IMAGE_MIN_DIM与IMAGE_MAX_DIM从 1024 调到 800,只不过最终评估时应回到 1024。

5.3 mask 与图像错位:训练曲线正常,可视化却张张对不上

现象:inspect 检查或 splash 输出里,气球掩码整体偏到图的另一个位置,有些 mask 形状完全对不上,但训练 loss 又表现出「正常下降」的假象。

原因:数据集类的 image_id 用了顶层 key,而 load_image 又按filename字段去读文件,若本地文件被重新组织过目录,JSON 里的 filename 和实际路径对不上。另一层原因是多边形点坐标是绝对像素值,如果 load_image 里对图像做了 resize 而没有同步缩放 mask,就会整体错位。

解决:把顶层 key 到本地路径的映射统一收敛到一个字典,打开 JSON 后先打印十条 filename,和实际文件列表比对。load_image 与 load_mask 里不要各自独立推导路径,而是共用info["path"]。加载完数据后必须跑一遍inspect_balloon_data.ipynb里的可视化 cell,一口气看几十张,确认 mask 贴合边框再开训练。

5.4 小数据集的评估波动:mAP 上蹿下跳是常态

现象:训练 loss 已经收敛,验证集每次评估 mAP 在 0.5 到 0.85 之间来回跳,模型保存时好坏全看运气。

原因:气球验证集往往只有十几张图,每张图里目标数量差异极大,一张图多两个气球,AP 就变一个档次。加上实例分割评估要求 mask 级 IoU,小目标稍有偏差就掉出阈值区间,数值波动被小样本基数放大。

解决:评估时以多个 checkpoint 的平均值代替单次结果,取inspect_balloon_model.ipynb里对同一批验证图跑 3 到 5 次、报平均 AP 的做法。训练保存最好按验证 loss 高低选择 checkpoint,而不是默认的每个 epoch 都存、最后只看最后一个。数据允许的话,把验证集扩充到三十张以上,波动会显著变小。

6. 从气球迁移到自己的类别:最小改动路径与最终验证

6.1 四个必改点,改完就能训练新目标

迁移一个自己的检测分割任务,改动集中在四处,按成本从低到高排列:标注工具导出格式、数据集类、配置类、输出可视化。表格里标出的位置基本就是全部工作。

改动点做法对应位置
标注导出用 VIA 或 LabelMe 导出的多边形 JSON,统一成 via 格式数据
Dataset 类改写 load_balloon 的类别名与 region 解析samples/balloon/balloon.py
配置类更新 NAME、NUM_CLASSES、锚点尺度BalloonConfig
可视化确认 splash 叠加色与类别 ID 对应mrcnn/visualize.py

具体到代码,通常只需要把add_class的类名换成自己的,把region_attributes的字段名从balloon改成自己的标签键,再根据目标大小调整RPN_ANCHOR_SCALES。如果你的标注里有三种目标,NUM_CLASSES = 1 + 3,class_ids 返回各标注对应的 ID,而不是全部返回 1。

6.2 用 COCO API 做最终验收,确认能交付

训练结束后,不要只靠 splash 目测。项目里配套的评估逻辑基于 COCO API,能输出 AP、AR 这类量化指标,这个数字才是你向别人说明「模型行不行」的依据。评估时把所有预测结果收集成 COCO 格式的字典,再调用cocoEval计算。

from pycocotools.coco import COCO from pycocotools.cocoeval import COCOeval # predictions 为 COCO 格式的检测与掩码结果 coco_gt = COCO(gt_annotation_path) coco_dt = coco_gt.loadRes(predictions) ev = COCOeval(coco_gt, coco_dt, "segm") ev.evaluate() ev.accumulate() print("mAP@[.5:.95]:", ev.stats[0], "AP@.5:", ev.stats[1])

逻辑说明:COCOeval把模型输出的框和 mask 与真实标注做逐层匹配,包含 IoU 阈值从 0.5 到 0.95 的多档评估,统计维度对应ev.stats数组的前几个指标。参数说明:"segm"表示用掩码 IoU 作为匹配标准,如果你的业务只关心框,可换"bbox",但实例分割交付场景一般看segm。

我自己的习惯是每次迁移完新类别,先跑这套评估拿到一组基线 mAP,再回来调锚点或置信度阈值,凡是改过配置都强制重跑一遍同样数据,确保对比口径一致。从那以后,我每次拿到一个新的标注集,都会强制走一遍这个最小改动路径,把数据检查摆到训练之前,模型迁移这件事就没再翻过车。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询