第42课:TensorFlow|计算机视觉实战二【图像分割基础流程与数据标注规范】

文章目录

    • [1. 课前导读](#1. 课前导读)
      • [1.1 本节课学习目标](#1.1 本节课学习目标)
      • [1.2 知识重难点](#1.2 知识重难点)
      • [1.3 学习前置条件](#1.3 学习前置条件)
      • [1.4 学完可掌握能力](#1.4 学完可掌握能力)
      • [1.5 行业应用场景](#1.5 行业应用场景)
    • [2. 核心理论精讲](#2. 核心理论精讲)
      • [2.1 图像分割任务类型](#2.1 图像分割任务类型)
      • [2.2 常用数据集与标注格式](#2.2 常用数据集与标注格式)
      • [2.3 标注规范与数据预处理](#2.3 标注规范与数据预处理)
      • [2.4 U-Net架构](#2.4 U-Net架构)
      • [2.5 分割评估指标](#2.5 分割评估指标)
      • [2.6 损失函数](#2.6 损失函数)
    • [3. 环境搭建与工具配置](#3. 环境搭建与工具配置)
    • [4. 代码实战教学](#4. 代码实战教学)
      • [4.1 使用LabelMe标注并转换掩码](#4.1 使用LabelMe标注并转换掩码)
      • [4.2 加载自定义分割数据集](#4.2 加载自定义分割数据集)
      • [4.3 搭建U-Net模型](#4.3 搭建U-Net模型)
      • [4.4 损失函数与训练](#4.4 损失函数与训练)
      • [4.5 评估mIoU](#4.5 评估mIoU)
      • [4.6 推理与可视化](#4.6 推理与可视化)
    • [5. 案例实操演练](#5. 案例实操演练)
      • [5.1 数据准备(简化版)](#5.1 数据准备(简化版))
      • [5.2 训练U-Net](#5.2 训练U-Net)
      • [5.3 后处理与可视化](#5.3 后处理与可视化)
    • [6. 常见坑点与排错总结](#6. 常见坑点与排错总结)
      • [6.1 标注与数据预处理坑点](#6.1 标注与数据预处理坑点)
      • [6.2 U-Net训练坑点](#6.2 U-Net训练坑点)
      • [6.3 评估坑点](#6.3 评估坑点)
    • [7. 知识点总结 + 课后作业](#7. 知识点总结 + 课后作业)
      • [7.1 核心知识点梳理](#7.1 核心知识点梳理)
      • [7.2 基础作业](#7.2 基础作业)
      • [7.3 进阶实操作业](#7.3 进阶实操作业)
      • [7.4 思考拓展题](#7.4 思考拓展题)
  • [🔗《TensorFlow2.x: 深度学习入门到高阶实战教程》系列课程导航](#🔗《TensorFlow2.x: 深度学习入门到高阶实战教程》系列课程导航)

1. 课前导读

1.1 本节课学习目标

  • 理解图像分割的定义和分类(语义分割 vs 实例分割 vs 全景分割)。
  • 掌握图像分割的常用数据集格式(COCO JSON、VOC、Cityscapes)和标注规范。
  • 学会使用LabelMe或COCO Annotator工具进行图像标注,并导出分割掩码。
  • 掌握分割模型的评估指标:像素准确率(PA)、平均交并比(mIoU)、Dice系数。
  • 理解U-Net架构(编码器、解码器、跳跃连接)的原理。
  • 能够使用TensorFlow搭建U-Net模型,并在自定义数据集上进行训练和推理。

1.2 知识重难点

类别 内容
重点 语义分割与实例分割的区别;U-Net的对称结构与跳跃连接;mIoU的计算方法;标注数据到掩码的转换
难点 实例分割的掩码处理(每个实例独立);数据不平衡(小物体、背景主导)的处理;U-Net中不同层特征的融合
易混淆点 语义分割的类别索引与颜色映射;标注多边形转换为二值掩码时的坐标缩放;训练时loss函数的选择(交叉熵 vs Dice loss)

1.3 学习前置条件

  • 已掌握CNN基本知识和分类模型训练(第21-25课)。
  • 了解目标检测的锚框概念(第41课)。
  • 能够使用TensorFlow加载图像和自定义数据集。

1.4 学完可掌握能力

  • 独立完成图像分割数据的标注、格式转换和预处理。
  • 构建并训练U-Net模型实现语义分割。
  • 评估分割模型性能,使用mIoU和Dice指标。
  • 为医学图像、遥感图像等像素级任务提供解决方案。

1.5 行业应用场景

  • 医学影像:肿瘤分割、器官勾画。
  • 自动驾驶:道路、车辆、行人分割。
  • 遥感图像:土地利用分类、建筑物提取。
  • 工业质检:缺陷区域分割。

2. 核心理论精讲

2.1 图像分割任务类型

语义分割(Semantic Segmentation):为图像中的每个像素分配一个类别标签(如道路、行人、背景),不同实例不区分。输出是单通道的类别图。

实例分割(Instance Segmentation):在语义分割的基础上,区分同一类别的不同个体(如行人1、行人2)。输出通常是一组掩码和对应的类别。

全景分割(Panoptic Segmentation):结合语义分割(背景区域)和实例分割(可数物体)。

本课主要聚焦语义分割,因为它是基础且应用广泛;实例分割的标注更复杂,但理解语义分割后可拓展。

2.2 常用数据集与标注格式

  • PASCAL VOC:20类+背景,标注为PNG索引图(每个像素值为类别索引)。
  • COCO:80类,实例分割标注为多边形(polygon)格式,保存在JSON文件中。
  • Cityscapes:30类,标注为精细的像素级掩码,用于自动驾驶场景。

标注工具:

  • LabelMe:开源工具,标注多边形,输出JSON文件,可转换为掩码。
  • COCO Annotator:Web工具,支持多边形、矩形。
  • VGG Image Annotator (VIA):简单易用。

2.3 标注规范与数据预处理

多边形标注:每个物体由一系列点围成,需转换为二值掩码(与图像同尺寸的0/1矩阵)。转换时需考虑坐标缩放(如果原始图像被resize)。

类别不平衡:背景像素通常远多于前景,需使用加权损失或采样。

数据增强 :对于分割任务,图像和掩码必须进行相同的几何变换(旋转、翻转、缩放)。可使用tf.image或albumentations库实现。

2.4 U-Net架构

U-Net是医学图像分割中广泛使用的经典模型,特点:

  • 编码器(下采样路径):卷积层+池化,提取高层次特征,逐步降低空间分辨率。
  • 解码器(上采样路径):转置卷积或上采样层,恢复空间分辨率。
  • 跳跃连接:将编码器对应层的特征与解码器上采样后的特征拼接,保留细节信息,利于精确分割。

网络结构(输入HxW):

  • 编码器:每个阶段两个3x3卷积(ReLU)+ 2x2最大池化,通道数倍增。
  • 解码器:每个阶段上采样(2x2转置卷积),与编码器对应层裁剪后拼接,再两个3x3卷积。
  • 最后一层1x1卷积,输出类别数(使用softmax或sigmoid)。

参数数量适中,训练稳定,是小数据集上的首选。

2.5 分割评估指标

像素准确率(Pixel Accuracy, PA):正确分类的像素数/总像素数。易受类别不平衡影响。

平均交并比(Mean Intersection over Union, mIoU):对每个类别计算IoU(预测区域与真实区域的交集/并集),然后取平均。mIoU对不平衡更鲁棒。

\\text{IoU}*c = \\frac{TP_c}{TP_c + FP_c + FN_c}, \\quad \\text{mIoU} = \\frac{1}{C}\\sum* {c=1}\^{C} \\text{IoU}_c

Dice系数(F1 score for segmentation) :

\\text{Dice} = \\frac{2\|A \\cap B\|}{\|A\| + \|B\|}

常用于医学图像分割,与IoU近似。

2.6 损失函数

  • 交叉熵损失:逐像素分类损失,适合类别平衡的数据。
  • Dice损失 :1 - Dice,对前景-背景不平衡问题更鲁棒。
  • 组合损失:交叉熵 + Dice损失,综合二者优点。

3. 环境搭建与工具配置

沿用第41课环境,安装额外工具:

bash 复制代码
conda activate tf213
pip install opencv-python labelme pillow matplotlib

创建项目目录:

复制代码
segmentation/
    ├── data/
    │   ├── raw/          # 原始图像和JSON标注
    │   ├── masks/        # 生成的掩码PNG
    │   └── train.txt     # 训练集列表
    ├── tools/
    │   └── labelme2mask.py
    ├── train.py
    └── predict.py

导入模块:

python 复制代码
import tensorflow as tf
import numpy as np
import matplotlib.pyplot as plt
import os
import cv2
from tensorflow.keras import layers, models, callbacks

4. 代码实战教学

4.1 使用LabelMe标注并转换掩码

标注步骤:

  1. 安装LabelMe:pip install labelme
  2. 启动:labelme,打开图像,用多边形标注物体,保存JSON文件。
  3. 转换脚本:
python 复制代码
# labelme2mask.py
import json
import numpy as np
import cv2
import os

def json2mask(json_path, output_mask_path, class_map):
    """
    class_map: dict {'class_name': class_id}
    """
    with open(json_path, 'r') as f:
        data = json.load(f)
    img_h, img_w = data['imageHeight'], data['imageWidth']
    mask = np.zeros((img_h, img_w), dtype=np.uint8)
    for shape in data['shapes']:
        label = shape['label']
        if label not in class_map:
            continue
        class_id = class_map[label]
        points = np.array(shape['points'], dtype=np.int32)
        cv2.fillPoly(mask, [points], class_id)
    cv2.imwrite(output_mask_path, mask)
    return mask

# 示例
class_map = {'cat': 1, 'dog': 2}  # 背景为0
json_path = 'data/raw/001.json'
output_path = 'data/masks/001.png'
json2mask(json_path, output_path, class_map)

4.2 加载自定义分割数据集

python 复制代码
def load_data(image_dir, mask_dir, image_size=(256,256)):
    image_paths = sorted([os.path.join(image_dir, f) for f in os.listdir(image_dir) if f.endswith('.jpg')])
    mask_paths = sorted([os.path.join(mask_dir, f) for f in os.listdir(mask_dir) if f.endswith('.png')])
    images, masks = [], []
    for img_path, msk_path in zip(image_paths, mask_paths):
        img = cv2.imread(img_path)
        img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
        img = cv2.resize(img, image_size)
        mask = cv2.imread(msk_path, cv2.IMREAD_GRAYSCALE)
        mask = cv2.resize(mask, image_size, interpolation=cv2.INTER_NEAREST)  # 保持标签离散
        images.append(img.astype(np.float32) / 255.0)
        masks.append(mask)
    return np.array(images), np.array(masks)

# 生成模拟数据集(如果不使用真实标注)
def generate_synthetic_dataset(num_samples=1000, size=128):
    images = []
    masks = []
    for _ in range(num_samples):
        img = np.ones((size, size, 3), dtype=np.float32)
        mask = np.zeros((size, size), dtype=np.uint8)
        # 随机绘制圆形、矩形、三角形
        shape_type = np.random.choice([1,2,3])  # 1矩形,2圆形,3三角形
        # 坐标和大小
        cx = np.random.randint(32, size-32)
        cy = np.random.randint(32, size-32)
        r = np.random.randint(20, 50)
        if shape_type == 1:  # 矩形
            x1 = cx - r
            y1 = cy - r
            x2 = cx + r
            y2 = cy + r
            cv2.rectangle(img, (x1,y1), (x2,y2), (0,0,0), -1)
            cv2.rectangle(mask, (x1,y1), (x2,y2), shape_type, -1)
        elif shape_type == 2:  # 圆形
            cv2.circle(img, (cx,cy), r, (0,0,0), -1)
            cv2.circle(mask, (cx,cy), r, shape_type, -1)
        else:  # 三角形
            pts = np.array([[cx, cy-r], [cx-r, cy+r], [cx+r, cy+r]], np.int32)
            cv2.fillPoly(img, [pts], (0,0,0))
            cv2.fillPoly(mask, [pts], shape_type)
        images.append(img)
        masks.append(mask)
    return np.array(images), np.array(masks)

4.3 搭建U-Net模型

python 复制代码
def unet(input_size=(256,256,3), num_classes=4):  # 包括背景0, 1,2,3
    inputs = layers.Input(input_size)
    # 编码器
    conv1 = layers.Conv2D(64, 3, activation='relu', padding='same')(inputs)
    conv1 = layers.Conv2D(64, 3, activation='relu', padding='same')(conv1)
    pool1 = layers.MaxPooling2D(pool_size=(2,2))(conv1)
    
    conv2 = layers.Conv2D(128, 3, activation='relu', padding='same')(pool1)
    conv2 = layers.Conv2D(128, 3, activation='relu', padding='same')(conv2)
    pool2 = layers.MaxPooling2D((2,2))(conv2)
    
    conv3 = layers.Conv2D(256, 3, activation='relu', padding='same')(pool2)
    conv3 = layers.Conv2D(256, 3, activation='relu', padding='same')(conv3)
    pool3 = layers.MaxPooling2D((2,2))(conv3)
    
    conv4 = layers.Conv2D(512, 3, activation='relu', padding='same')(pool3)
    conv4 = layers.Conv2D(512, 3, activation='relu', padding='same')(conv4)
    drop4 = layers.Dropout(0.5)(conv4)
    pool4 = layers.MaxPooling2D((2,2))(drop4)
    
    # 底部
    conv5 = layers.Conv2D(1024, 3, activation='relu', padding='same')(pool4)
    conv5 = layers.Conv2D(1024, 3, activation='relu', padding='same')(conv5)
    drop5 = layers.Dropout(0.5)(conv5)
    
    # 解码器
    up6 = layers.Conv2DTranspose(512, 2, strides=(2,2), padding='same')(drop5)
    concat6 = layers.concatenate([up6, conv4], axis=-1)
    conv6 = layers.Conv2D(512, 3, activation='relu', padding='same')(concat6)
    conv6 = layers.Conv2D(512, 3, activation='relu', padding='same')(conv6)
    
    up7 = layers.Conv2DTranspose(256, 2, strides=(2,2), padding='same')(conv6)
    concat7 = layers.concatenate([up7, conv3], axis=-1)
    conv7 = layers.Conv2D(256, 3, activation='relu', padding='same')(concat7)
    conv7 = layers.Conv2D(256, 3, activation='relu', padding='same')(conv7)
    
    up8 = layers.Conv2DTranspose(128, 2, strides=(2,2), padding='same')(conv7)
    concat8 = layers.concatenate([up8, conv2], axis=-1)
    conv8 = layers.Conv2D(128, 3, activation='relu', padding='same')(concat8)
    conv8 = layers.Conv2D(128, 3, activation='relu', padding='same')(conv8)
    
    up9 = layers.Conv2DTranspose(64, 2, strides=(2,2), padding='same')(conv8)
    concat9 = layers.concatenate([up9, conv1], axis=-1)
    conv9 = layers.Conv2D(64, 3, activation='relu', padding='same')(concat9)
    conv9 = layers.Conv2D(64, 3, activation='relu', padding='same')(conv9)
    
    outputs = layers.Conv2D(num_classes, 1, activation='softmax')(conv9)
    
    model = tf.keras.Model(inputs=inputs, outputs=outputs)
    return model

model = unet((128,128,3), num_classes=4)
model.summary()

4.4 损失函数与训练

python 复制代码
def dice_coef(y_true, y_pred, smooth=1e-7):
    y_true_f = tf.reshape(y_true, [-1, num_classes])
    y_pred_f = tf.reshape(y_pred, [-1, num_classes])
    intersection = tf.reduce_sum(y_true_f * y_pred_f, axis=0)
    union = tf.reduce_sum(y_true_f, axis=0) + tf.reduce_sum(y_pred_f, axis=0)
    dice = (2. * intersection + smooth) / (union + smooth)
    return tf.reduce_mean(dice)

def dice_loss(y_true, y_pred):
    return 1 - dice_coef(y_true, y_pred)

# 组合损失
def combined_loss(y_true, y_pred):
    ce = tf.keras.losses.categorical_crossentropy(y_true, y_pred)
    dice = dice_loss(y_true, y_pred)
    return ce + dice

# 准备数据
X, y = generate_synthetic_dataset(2000, 128)
# 标签转为one-hot
y_onehot = tf.keras.utils.to_categorical(y, num_classes=4)
# 划分训练验证集
split = int(0.8 * len(X))
X_train, X_val = X[:split], X[split:]
y_train, y_val = y_onehot[:split], y_onehot[split:]

model.compile(optimizer='adam', loss=combined_loss, metrics=['accuracy', dice_coef])
model.fit(X_train, y_train, epochs=20, batch_size=16, validation_data=(X_val, y_val), callbacks=[callbacks.EarlyStopping(patience=5)])

4.5 评估mIoU

python 复制代码
def compute_iou_per_class(y_true, y_pred, num_classes):
    ious = []
    y_pred_class = np.argmax(y_pred, axis=-1)
    y_true_class = np.argmax(y_true, axis=-1)
    for c in range(num_classes):
        intersection = np.sum((y_pred_class == c) & (y_true_class == c))
        union = np.sum((y_pred_class == c) | (y_true_class == c))
        if union == 0:
            iou = 1.0 if intersection==0 else 0.0
        else:
            iou = intersection / union
        ious.append(iou)
    return np.mean(ious)

# 预测验证集
y_pred = model.predict(X_val)
miou = compute_iou_per_class(y_val, y_pred, num_classes=4)
print(f"mIoU on validation: {miou:.4f}")

4.6 推理与可视化

python 复制代码
def predict_and_show(model, image, num_classes=4):
    # image: (H,W,3) normalized
    input_tensor = np.expand_dims(image, axis=0)
    pred = model.predict(input_tensor)[0]
    pred_class = np.argmax(pred, axis=-1)
    plt.figure(figsize=(12,4))
    plt.subplot(1,3,1)
    plt.imshow(image)
    plt.title('Input')
    plt.subplot(1,3,2)
    plt.imshow(pred_class, cmap='jet')
    plt.title('Prediction')
    plt.subplot(1,3,3)
    # 显示颜色映射(自定义)
    plt.imshow(pred_class, cmap='viridis')
    plt.title('Segmentation')
    plt.show()

sample = X_val[0]
predict_and_show(model, sample)

5. 案例实操演练

案例:医学图像细胞分割(U-Net)

使用公开数据集(如ISBI细胞分割),下载数据,预处理,训练U-Net,评估Dice系数。

5.1 数据准备(简化版)

假设数据目录:images/ 和 masks/,图像和掩码名称对应。

python 复制代码
# 加载真实数据
X_cell, y_cell = load_data('data/cell/images', 'data/cell/masks', (256,256))
y_cell_cat = tf.keras.utils.to_categorical(y_cell, num_classes=2)  # 背景+细胞
# 标准化图像
X_cell = X_cell / 255.0

# 划分
split = 0.8
# ...

5.2 训练U-Net

python 复制代码
model = unet((256,256,3), num_classes=2)
model.compile(optimizer='adam', loss='binary_crossentropy', metrics=[dice_coef])
history = model.fit(X_cell_train, y_cell_train, epochs=30, validation_data=(X_cell_val, y_cell_val))

5.3 后处理与可视化

可对预测概率图阈值0.5生成二值掩码。

6. 常见坑点与排错总结

6.1 标注与数据预处理坑点

  • 坑1:多边形转换掩码时,坐标超出图像边界或缩放后导致掩码偏移。

    • 解决 :在转换前裁剪坐标到0, width-1范围,使用cv2.fillPoly时确保坐标整数。
  • 坑2:图像与掩码大小不一致,训练时无法对齐。

    • 解决:统一resize,掩码使用最近邻插值保持类别标签。
  • 坑3:类别数量包含背景但标签从0开始,one-hot编码时需注意深度。

6.2 U-Net训练坑点

  • 坑4:跳跃连接时编码器特征与解码器特征尺寸不完全匹配(由于padding)。

    • 解决 :使用crop_and_concat或设置padding='same'并确保尺寸一致。
  • 坑5:正负样本极度不平衡(如背景占99%),模型倾向于预测背景。

    • 解决:使用Dice损失或加权交叉熵。
  • 坑6:显存不足,U-Net全尺寸输入时消耗大。

    • 解决:减小batch size,降低输入分辨率,或使用更小的编码器。

6.3 评估坑点

  • 坑7 :mIoU计算时忽略无像素的类别(如某些类不出现在验证集中),会导致分母为零。
    • 解决:对于没有GT的类别,IoU定义为1(如果预测也为0)或0。

7. 知识点总结 + 课后作业

7.1 核心知识点梳理

  • 语义分割:像素级分类,输出类别图。
  • 标注工具:LabelMe、COCO Annotator,多边形转掩码。
  • U-Net:编码器-解码器+跳跃连接,适合医学分割。
  • 评估指标:mIoU、Dice系数,对不平衡鲁棒。
  • 损失函数:交叉熵、Dice损失、组合损失。

7.2 基础作业

  1. 使用LabelMe标注3张图像(例如猫狗),编写脚本将其转换为掩码,并可视化。
  2. 修改U-Net的深度(减少编码器层数),对比分割精度和参数量。
  3. 计算合成数据集的mIoU(每个类别的IoU单独打印)。

7.3 进阶实操作业

任务:使用U-Net进行道路分割

  • 下载Cityscapes mini数据集或使用Carla模拟器生成道路场景。
  • 实现数据加载(图像+掩码)。
  • 训练U-Net(二分类:道路/非道路)。
  • 测试并可视化预测结果,计算IoU。

7.4 思考拓展题

  1. 如果背景类别占据了绝大多数像素,使用交叉熵损失会导致模型偏向背景。请推导Dice损失的梯度为什么能够缓解这一问题。

  2. U-Net中的跳跃连接与ResNet的残差连接有何异同?在分割任务中跳跃连接的作用是什么?

  3. 对于实例分割,通常需要先检测物体,再为每个检测框分割掩码(如Mask R-CNN)。请简述Mask R-CNN的基本思想。


下一课预告:NLP实战一:文本情感分类完整工业级项目------我们将使用LSTM、GRU和预训练词向量构建情感分类器,涵盖数据清洗、平衡采样、模型调优和部署。


🔗《TensorFlow2.x: 深度学习入门到高阶实战教程》系列课程导航

去订阅

第一部分:基础入门(1-10 课)

第二部分:神经网络核心(11-25 课)

第三部分:进阶网络与框架高阶(26-40 课)

第四部分:企业实战与项目落地(41-50 课)
🌟 感谢您耐心阅读到这里!

💡 如果本文对您有所启发欢迎:

👍 点赞📌 收藏 📤 分享给更多需要的伙伴。

🗣️ 期待在评论区看到您的想法, 共同进步。

🔔 关注我,持续获取更多干货内容~

🤗 我们下篇文章见~

相关推荐
yukai080085 分钟前
【203篇系列】055 十个月Agent变革回顾
人工智能
陈工大模型6 分钟前
2026年9月AI可见度监测工具横评:从采样一致性与中立性出发的技术选型笔记
大数据·人工智能·笔记
海宇数据9 分钟前
零信任架构实战:基于海宇身份证OCR构建自动化证照采集网关
人工智能·架构·自动化·ocr
一只桃子~12 分钟前
Ai大模型数据标注与质检评测面试题
人工智能
枯木◊靠推文躺平版15 分钟前
2026企业AI办公工具选型全指南
大数据·人工智能
地理探险家18 分钟前
图片中的目标如何定位和计数?从数据检查到模型预测的完整实践
人工智能·深度学习·数据集
东方芷兰25 分钟前
Agent 技术摘要 03 —— 基座模型、推理模型、Flash、联邦学习、分布式机器学习
人工智能·分布式·机器学习
reasonsummer26 分钟前
【办公类-200-01】20260820课题的查重报告比例(AI写结题报告+人工插图+PaperYY免费查重(标红文字修改:口语化)+知网查重结果对比)
人工智能·aigc·知网查重·paperyy
xianghongtao011634 分钟前
Deloitte_2026_GenZ_Millennial_CSDN_解读
人工智能
天远数科34 分钟前
零信任架构实战:基于天远股权穿透构建自动化供应商准入合规网关
大数据·人工智能·架构·自动化