第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.imagealbumentations库实现。

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 课)
🌟 感谢您耐心阅读到这里!

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

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

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

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

🤗 我们下篇文章见~

相关推荐
RobinDevNotes1 小时前
Palmier Pro:AI时代的Mac视频编辑器
人工智能·ceph·macos·ai·音视频·视频编辑·mcp
AIGC大时代1 小时前
证据综合 × AI:BrainX 选型表、人工终裁与可审计产物
人工智能·brainx·证据综合·人工终裁
粉色大象1 小时前
handdrawn‑architecture‑video:开源SVG架构图转手绘4K动画视频|本地AI Agent Skill
人工智能·ai·ai作画·系统架构·开源·aigc·音视频
云生信1 小时前
服务器上新 OmicOS,体验生信 AI 计算
运维·服务器·人工智能
无敌的牛1 小时前
大模型推理理解
人工智能·深度学习
跨境小彭1 小时前
Temu 广告投放实操:ROAS 底层逻辑与批量广告作业方案
大数据·人工智能·自动化·temu
乱码三千1 小时前
开发了一套AI智能体协作知识体系
人工智能·架构·代码规范
Allen.Su1 小时前
大模型 LoRA 微调全流程实战 - 车载问答全流程(跑通 + 参数详解 + 训练日志逐行解读 + 模型合并)
人工智能·python·lora·大模型微调
吴佳浩 Alben2 小时前
走向 Memory OS:企业私有化 Agent 设计与实现
人工智能·深度学习·神经网络·语言模型·架构·自动化·ai编程