图像分割 订阅
图像分割就是把图像分成若干个特定的、具有独特性质的区域并提出感兴趣目标的技术和过程。它是由图像处理到图像分析的关键步骤。现有的图像分割方法主要分以下几类:基于阈值的分割方法、基于区域的分割方法、基于边缘的分割方法以及基于特定理论的分割方法等。从数学角度来看,图像分割是将数字图像划分成互不相交的区域的过程。图像分割的过程也是一个标记过程,即把属于同一区域的像素赋予相同的编号。 [1] 展开全文
图像分割就是把图像分成若干个特定的、具有独特性质的区域并提出感兴趣目标的技术和过程。它是由图像处理到图像分析的关键步骤。现有的图像分割方法主要分以下几类:基于阈值的分割方法、基于区域的分割方法、基于边缘的分割方法以及基于特定理论的分割方法等。从数学角度来看,图像分割是将数字图像划分成互不相交的区域的过程。图像分割的过程也是一个标记过程,即把属于同一区域的像素赋予相同的编号。 [1]
信息
分割方法
阈值分割等
外文名
image segmentation
类    型
计算机技术
发展实践
1998
用    途
图像语义识别,图像搜索
中文名
图像分割
学    科
跨学科
图像分割引言
图像分割 图像分割 数字图像处理技术是一个跨学科的领域。随着计算机科学技术的不断发展,图像处理和分析逐渐形成了自己的科学体系,新的处理方法层出不穷,尽管其发展历史不长,但却引起各方面人士的广泛关注。首先,视觉是人类最重要的感知手段,图像又是视觉的基础,因此,数字图像成为心理学、生理学、计算机科学等诸多领域内的学者们研究视觉感知的有效工具。其次,图像处理在军事、遥感、气象等大型应用中有不断增长的需求。1998年以来,人工神经网络识别技术已经引起了广泛的关注,并且应用于图像分割。基于神经网络的分割方法的基本思想是通过训练多层感知机来得到线性决策函数,然后用决策函数对像素进行分类来达到分割的目的。这种方法需要大量的训练数据。神经网络存在巨量的连接,容易引入空间信息,能较好地解决图像中的噪声和不均匀问题。选择何种网络结构是这种方法要解决的主要问题。图像分割是图像识别和计算机视觉至关重要的预处理。没有正确的分割就不可能有正确的识别。但是,进行分割仅有的依据是图像中像素的亮度及颜色,由计算机自动处理分割时,将会遇到各种困难。例如,光照不均匀、噪声的影响、图像中存在不清晰的部分,以及阴影等,常常发生分割错误。因此图像分割是需要进一步研究的技术。人们希望引入一些人为的知识导向和人工智能的方法,用于纠正某些分割中的错误,是很有前途的方法,但是这又增加了解决问题的复杂性。在通信领域中,图像分割技术对可视电话等活动图像的传输很重要,需要把图像中活动部分与静止的背景分开,还要把活动部分中位移量不同的区域分开,对不同运动量的区域用不同的编码传输,以降低传输所需的码率。
收起全文
精华内容
下载资源
问答
  • 图像分割
    万次阅读 多人点赞
    2021-10-22 11:46:15

    有一定深度学习图像分割基础,至少阅读过部分语义分割或者医学图像分割文献

    开发环境 部分包版本

    python                    3.7.9
    torch                     1.9.1                   
    torchstat                 0.0.7                  
    torchsummary              1.5.1                
    torchvision               0.4.0
    cuda                      10.0
    cudatoolkit               10.1.243
    numpy                     1.19.2
    


    前面的一篇 医学图像分割多目标分割(多分类)实践文章记录了笔者在医学图像分割踩坑入门的实践,但当时的源码不够完整。通过博客的评论互动和私信发现有很多同学同样在做这个方向,最近空闲的时间也让我下定决心重新复现之前代码并进行一些注释和讲解,希望能对该方向入坑的同学提供一些帮助。

    先上源码。

    1 完整源码

    【完整源码地址】: pytorch-medical-image-segmentation

    重新整理了之前的代码,利用其中一个数据集(前面文章提到的基于磁共振成像的膀胱内外壁分割与肿瘤检测,)作为案例,但由于没有官方的数据授权,我仅将该数据集的一小部分数据拿来做演示。

    我将代码托管到了国内的Gitee上(主要觉得比Github速度快点),源码 pytorch-medical-image-segmentation可直接下载运行。

    【代码目录结构】:

     pytorch-medical-image-segmentation/
    |-- checkpoint               # 存放训练好的模型
    |-- dataprepare              # 数据预处理的一些方法
    |-- datasets                 # 数据加载的一些方法
    |-- log                      # 日志文件
    |-- media                    
    |   |-- Datasets             # 存放数据集
    |-- networks                 # 存放模型
    |-- test                     # 测试相关
    |-- train                    # 训练相关
    |-- utils                    # 一些工具函数
    |-- validate                 # 验证相关
    |-- README.md
    
    

    2 数据集

    来自ISICDM 2019 临床数据分析挑战赛的基于磁共振成像的膀胱内外壁分割与肿瘤检测数据集。
    在这里插入图片描述

    (原始图像)

    在这里插入图片描述

    (图像的ground truth)

    【说明】:笔者没有权限公开分享该数据集,需要完整数据集可通过官网获取。若官网数据集也不能获取,可利用其他数据集代替,本教程主要是提供分割的大体代码思路,不局限于某一个具体的数据集。

    【灰度值】:灰色128为膀胱内外壁,白色255为肿瘤。

    【分割任务】:同时分割出膀胱内外壁和肿瘤部分

    【分析】:我们需要分割出膀胱内外壁和肿瘤,再加上黑色背景,相当于是一个三分类问题。

    3 分割任务的思路

    根据笔者做分割的一些经验,医学图像分割任务的步骤大体是以下几个步骤:

    • 数据预处理
    • 模型设计
    • 评估指标和损失函数选择
    • 训练
    • 验证
    • 测试

    接下来我们通过代码一步步完成分割的过程。

    4 代码实现

    4.1 数据预处理

    此次的膀胱数据集本身是官方处理好的png图像,不像常规的MRI和CT图像是nii格式的,因此数据处理起来相对容易。
    为了简单起见,笔者主要对原始数据做了数据集划分、对标签进行One-hot、裁剪等操作。由于不同的数据集做的数据增广操作(一般会有旋转、缩放、弹性形变等)不太一样,本案例中省略了数据增广的操作。

    首先,我们对原始数据集进行重新数据划分,这里使用了五折交叉验证(5-fold validation)的方法对数据进行划分,不了解交叉验证的同学可以先去网上搜索了解一下。
    这里是将数据集的名字划分到不同txt文件中,而不是真正的将原始数据划分到不同的文件夹中,后面读取的时候也是通过名字来读取,这样更加方便。

    # /dataprepare/kfold.py
    import os, shutil
    from sklearn.model_selection import KFold
    
    
    # 按K折交叉验证划分数据集
    def dataset_kfold(dataset_dir, save_path):
        data_list = os.listdir(dataset_dir)
    
        kf = KFold(5, False, 12345)  # 使用5折交叉验证
    
        for i, (tr, val) in enumerate(kf.split(data_list), 1):
            print(len(tr), len(val))
            if os.path.exists(os.path.join(save_path, 'train{}.txt'.format(i))):
                # 若该目录已存在,则先删除,用来清空数据
                print('清空原始数据中...')
                os.remove(os.path.join(save_path, 'train{}.txt'.format(i)))
                os.remove(os.path.join(save_path, 'val{}.txt'.format(i)))
                print('原始数据已清空。')
    
            for item in tr:
                file_name = data_list[item]
                with open(os.path.join(save_path, 'train{}.txt'.format(i)), 'a') as f:
                    f.write(file_name)
                    f.write('\n')
    
            for item in val:
                file_name = data_list[item]
                with open(os.path.join(save_path, 'val{}.txt'.format(i)), 'a') as f:
                    f.write(file_name)
                    f.write('\n')
    
    
    if __name__ == '__main__':
        # 膀胱数据集划分
        # 首次划分数据集或者重新划分数据集时运行
        dataset_kfold(os.path.join('..\media\Datasets\Bladder', 'raw_data\Labels'),
                      os.path.join('..\media\Datasets\Bladder', 'raw_data'))
    

    运行后会生成以下文件,相当于是将数据集5份,每一份对应自己的训练集和验证集。
    在这里插入图片描述
    数据集划分好了,接下来就要写数据加载的类和方法,以便在训练的时候加载我们的数据。

    # /datasets/bladder.py
    import os
    import cv2
    import numpy as np
    from PIL import Image
    from torch.utils import data
    from utils import helpers
    
    '''
    128 = bladder
    255 = tumor
    0   = background 
    '''
    palette = [[0], [128], [255]]  # one-hot的颜色表
    num_classes = 3  # 分类数
    
    
    def make_dataset(root, mode, fold):
        assert mode in ['train', 'val', 'test']
        items = []
        if mode == 'train':
            img_path = os.path.join(root, 'Images')
            mask_path = os.path.join(root, 'Labels')
    
            if 'Augdata' in root:  # 当使用增广后的训练集
                data_list = os.listdir(os.path.join(root, 'Labels'))
            else:
                data_list = [l.strip('\n') for l in open(os.path.join(root, 'train{}.txt'.format(fold))).readlines()]
            for it in data_list:
                item = (os.path.join(img_path, it), os.path.join(mask_path, it))
                items.append(item)
        elif mode == 'val':
            img_path = os.path.join(root, 'Images')
            mask_path = os.path.join(root, 'Labels')
            data_list = [l.strip('\n') for l in open(os.path.join(
                root, 'val{}.txt'.format(fold))).readlines()]
            for it in data_list:
                item = (os.path.join(img_path, it), os.path.join(mask_path, it))
                items.append(item)
        else:
            img_path = os.path.join(root, 'Images')
            data_list = [l.strip('\n') for l in open(os.path.join(
                root, 'test.txt')).readlines()]
            for it in data_list:
                item = (os.path.join(img_path, 'c0', it))
                items.append(item)
        return items
    
    
    class Dataset(data.Dataset):
        def __init__(self, root, mode, fold, joint_transform=None, center_crop=None, transform=None, target_transform=None):
            self.imgs = make_dataset(root, mode, fold)
            self.palette = palette
            self.mode = mode
            if len(self.imgs) == 0:
                raise RuntimeError('Found 0 images, please check the data set')
            self.mode = mode
            self.joint_transform = joint_transform
            self.center_crop = center_crop
            self.transform = transform
            self.target_transform = target_transform
    
        def __getitem__(self, index):
    
            img_path, mask_path = self.imgs[index]
            file_name = mask_path.split('\\')[-1]
    
            img = Image.open(img_path)
            mask = Image.open(mask_path)
    
            if self.joint_transform is not None:
                img, mask = self.joint_transform(img, mask)
            if self.center_crop is not None:
                img, mask = self.center_crop(img, mask)
            img = np.array(img)
            mask = np.array(mask)
            # Image.open读取灰度图像时shape=(H, W) 而非(H, W, 1)
            # 因此先扩展出通道维度,以便在通道维度上进行one-hot映射
            img = np.expand_dims(img, axis=2)
            mask = np.expand_dims(mask, axis=2)
            mask = helpers.mask_to_onehot(mask, self.palette)
            # shape from (H, W, C) to (C, H, W)
            img = img.transpose([2, 0, 1])
            mask = mask.transpose([2, 0, 1])
            if self.transform is not None:
                img = self.transform(img)
            if self.target_transform is not None:
                mask = self.target_transform(mask)
            return (img, mask), file_name
    
    
    
        def __len__(self):
            return len(self.imgs)
    
    
    
    if __name__ == '__main__':
        np.set_printoptions(threshold=9999999)
    
        from torch.utils.data import DataLoader
        import utils.image_transforms as joint_transforms
        import utils.transforms as extended_transforms
    
        def demo():
            train_path = r'../media/Datasets/Bladder/raw_data'
            val_path = r'../media/Datasets/Bladder/raw_data'
            test_path = r'../media/Datasets/Bladder/test'
    
            center_crop = joint_transforms.CenterCrop(256)
            test_center_crop = joint_transforms.SingleCenterCrop(256)
            train_input_transform = extended_transforms.NpyToTensor()
            target_transform = extended_transforms.MaskToTensor()
    
            train_set = Dataset(train_path, 'train', 1,
                                  joint_transform=None, center_crop=center_crop,
                                  transform=train_input_transform, target_transform=target_transform)
            train_loader = DataLoader(train_set, batch_size=1, shuffle=False)
    
            for (input, mask), file_name in train_loader:
                print(input.shape)
                print(mask.shape)
                img = helpers.array_to_img(np.expand_dims(input.squeeze(), 2))
                gt = helpers.onehot_to_mask(np.array(mask.squeeze()).transpose(1, 2, 0), palette)
                gt = helpers.array_to_img(gt)
                cv2.imshow('img GT', np.uint8(np.hstack([img, gt])))
                cv2.waitKey(1000)
    
        demo()
    
    

    通常我会在数据预处理和加载类已写好后,运行代码测试数据的加载过程,看加载的数据是否有问题。通过可视化的结果可以看到加载的数据是正常的。
    在这里插入图片描述
    我们在对ground truth反one-hot进行可视化时,改变颜色表palette中的颜色值,就可以将ground truth重新映射成我们想要的颜色,例如:
    我们修改上面的部分代码,将颜色表palette修改成三色值([x, x, x]里边有三个数字,单色[x]就对应灰色图像)将gt映射成彩色图像。

      for (input, mask), file_name in train_loader:
                print(input.shape)
                print(mask.shape)
                img = helpers.array_to_img(np.expand_dims(input.squeeze(), 2))
                # 将gt反one-hot回去以便进行可视化
                palette = [[0, 0, 0], [246, 16, 16], [16, 136, 246]] 
                gt = helpers.onehot_to_mask(np.array(mask.squeeze()).transpose(1, 2, 0), palette)
                gt = helpers.array_to_img(gt)
                # cv2.imshow('img GT', np.uint8(np.hstack([img, gt])))
                cv2.imshow('img GT', np.uint8(gt))
                cv2.waitKey(1000)
    

    可视化的结果如下
    在这里插入图片描述

    4.2 模型设计

    直接用经典的U-Net作为演示模型。注意输入的图像是1个通道,输出是3个通道。

    # /networks/u_net.py
    from networks.custom_modules.basic_modules import *
    from utils.misc import initialize_weights
    
    
    class Baseline(nn.Module):
        def __init__(self, img_ch=1, num_classes=3, depth=2):
            super(Baseline, self).__init__()
    
            chs = [64, 128, 256, 512, 512]
    
            self.maxpool = nn.MaxPool2d(kernel_size=2, stride=2)
    
            self.enc1 = EncoderBlock(img_ch, chs[0], depth=depth)
            self.enc2 = EncoderBlock(chs[0], chs[1], depth=depth)
            self.enc3 = EncoderBlock(chs[1], chs[2], depth=depth)
            self.enc4 = EncoderBlock(chs[2], chs[3], depth=depth)
            self.enc5 = EncoderBlock(chs[3], chs[4], depth=depth)
    
            self.dec4 = DecoderBlock(chs[4], chs[3])
            self.decconv4 = EncoderBlock(chs[3] * 2, chs[3])
    
            self.dec3 = DecoderBlock(chs[3], chs[2])
            self.decconv3 = EncoderBlock(chs[2] * 2, chs[2])
    
            self.dec2 = DecoderBlock(chs[2], chs[1])
            self.decconv2 = EncoderBlock(chs[1] * 2, chs[1])
    
            self.dec1 = DecoderBlock(chs[1], chs[0])
            self.decconv1 = EncoderBlock(chs[0] * 2, chs[0])
    
            self.conv_1x1 = nn.Conv2d(chs[0], num_classes, 1, bias=False)
    
            initialize_weights(self)
    
        def forward(self, x):
            # encoding path
            x1 = self.enc1(x)
    
            x2 = self.maxpool(x1)
            x2 = self.enc2(x2)
    
            x3 = self.maxpool(x2)
            x3 = self.enc3(x3)
    
            x4 = self.maxpool(x3)
            x4 = self.enc4(x4)
    
            x5 = self.maxpool(x4)
            x5 = self.enc5(x5)
    
            # decoding + concat path
            d4 = self.dec4(x5)
            d4 = torch.cat((x4, d4), dim=1)
            d4 = self.decconv4(d4)
    
            d3 = self.dec3(d4)
            d3 = torch.cat((x3, d3), dim=1)
            d3 = self.decconv3(d3)
    
            d2 = self.dec2(d3)
            d2 = torch.cat((x2, d2), dim=1)
            d2 = self.decconv2(d2)
    
            d1 = self.dec1(d2)
            d1 = torch.cat((x1, d1), dim=1)
            d1 = self.decconv1(d1)
    
            d1 = self.conv_1x1(d1)
    
            return d1
    
    if __name__ == '__main__':
        # from torchstat import stat
        import torch
        from torchsummary import summary
        x = torch.randn([2, 1, 64, 64]).cuda()
        # # 参数计算
        model = Baseline(num_classes=3).cuda()
        total = sum([param.nelement() for param in model.parameters()])
        print("Number of parameter: %.3fM" % (total / 1e6))
        # # 参数计算
        # # stat(model, (1, 224, 224))
        # # 每层输出大小
        print(model(x).shape)
    

    可以直接运行该文件,测试模型的输入和输出是否符合预期。

    4.3 评估指标和损失函数

    这里选择医学图像分割中最常用的指标DiceDice loss。关于实现的讨论可参考【Pytorch】 Dice系数与Dice Loss损失函数实现

    Dice系数的实现核心代码:

    # /utils/metrics.py
    def diceCoeffv2(pred, gt, eps=1e-5):
        r""" computational formula:
            dice = (2 * tp) / (2 * tp + fp + fn)
        """
    
        N = gt.size(0)
        pred_flat = pred.view(N, -1)
        gt_flat = gt.view(N, -1)
    
        tp = torch.sum(gt_flat * pred_flat, dim=1)
        fp = torch.sum(pred_flat, dim=1) - tp
        fn = torch.sum(gt_flat, dim=1) - tp
        score = (2 * tp + eps) / (2 * tp + fp + fn + eps)
        return score.sum() / N
    

    多分类Dice loss实现的核心代码:

    # /utils/loss.py
    class SoftDiceLoss(_Loss):
    
        def __init__(self, num_classes):
            super(SoftDiceLoss, self).__init__()
            self.num_classes = num_classes
    
        def forward(self, y_pred, y_true):
            class_dice = []
            # 从1开始排除背景,前提是颜色表palette中背景放在第一个位置 [[0], ..., ...]
            for i in range(1, self.num_classes):
                class_dice.append(diceCoeffv2(y_pred[:, i:i + 1, :], y_true[:, i:i + 1, :]))
            mean_dice = sum(class_dice) / len(class_dice)
            return 1 - mean_dice
    

    如果只是二分类,用下面的损失函数:

    class BinarySoftDiceLoss(_Loss):
    
        def __init__(self):
            super(BinarySoftDiceLoss, self).__init__()
    
        def forward(self, y_pred, y_true):
            mean_dice = diceCoeffv2(y_pred, y_true)
            return 1 - mean_dice
    

    4.4 训练

    训练的整体思路就是,训练完一个epoch进行验证(注意验证的loss不反向传播,只验证不影响模型权重),在训练的过程中使用了早停机制(Early stopping)。只要在15个epoch内,验证集上的评价Dice指标增长不超过0.1%则停止训练,并保存之前在验证集上最好的模型。

    代码中Early Stopping提供两个版本,其中EarlyStopping传指标进去即可,EarlyStoppingV2传验证集的loss值,表示在15个epoch内,loss下降不超过0.001则停止训练。

    # /train/train_bladder.py
    import time
    import os
    import torch
    import random
    from torch.utils.data import DataLoader
    from tensorboardX import SummaryWriter
    from torch.optim import lr_scheduler
    from tqdm import tqdm
    import sys
    
    
    from datasets import bladder 
    import utils.image_transforms as joint_transforms
    import utils.transforms as extended_transforms
    from utils.loss import *
    from utils.metrics import diceCoeffv2
    from utils import misc
    from utils.pytorchtools import EarlyStopping
    from utils.LRScheduler import PolyLR
    
    # 超参设置
    crop_size = 256  # 输入裁剪大小
    batch_size = 2  # batch size
    n_epoch = 300  # 训练的最大epoch
    early_stop__eps = 1e-3  # 早停的指标阈值
    early_stop_patience = 15  # 早停的epoch阈值
    initial_lr = 1e-4  # 初始学习率
    threshold_lr = 1e-6  # 早停的学习率阈值
    weight_decay = 1e-5  # 学习率衰减率
    optimizer_type = 'adam'  # adam, sgd
    scheduler_type = 'no'  # ReduceLR, StepLR, poly
    label_smoothing = 0.01
    aux_loss = False
    gamma = 0.5
    alpha = 0.85
    model_number = random.randint(1, 1e6)
    
    
    model_type = "unet"
    
    if model_type == "unet":
        from networks.u_net import Baseline
    
    root_path = '../'
    fold = 1  # 训练集k-fold, 可设置1, 2, 3, 4, 5
    depth = 2  # unet编码器的卷积层数
    loss_name = 'dice'  # dice, bce, wbce, dual, wdual
    reduction = ''  # aug
    model_name = '{}_depth={}_fold_{}_{}_{}{}'.format(model_type, depth, fold, loss_name, reduction, model_number)
    
    # 训练日志
    writer = SummaryWriter(os.path.join(root_path, 'log/bladder/train', model_name + '_{}fold'.format(fold) + str(int(time.time()))))
    val_writer = SummaryWriter(os.path.join(os.path.join(root_path, 'log/bladder/val', model_name) + '_{}fold'.format(fold) + str(int(time.time()))))
    
    # 训练集路径
    # train_path = os.path.join(root_path, 'media/Datasets/bladder/Augdata_5folds', 'train{}'.format(fold), 'npy')
    train_path = os.path.join(root_path, 'media/Datasets/Bladder/raw_data')
    val_path = os.path.join(root_path, 'media/Datasets/Bladder/raw_data')
    
    
    def main():
        # 定义网络
        net = Baseline(num_classes=bladder.num_classes, depth=depth).cuda()
    
        # 数据预处理
        center_crop = joint_transforms.CenterCrop(crop_size)
        input_transform = extended_transforms.NpyToTensor()
        target_transform = extended_transforms.MaskToTensor()
    
        # 训练集加载
        train_set = bladder.Dataset(train_path, 'train', fold, joint_transform=None, center_crop=center_crop,
                                        transform=input_transform, target_transform=target_transform)
        train_loader = DataLoader(train_set, batch_size=batch_size, shuffle=True, num_workers=6)
        # 验证集加载
        val_set = bladder.Dataset(val_path, 'val', fold,
                                      joint_transform=None, transform=input_transform, center_crop=center_crop,
                                      target_transform=target_transform)
        val_loader = DataLoader(val_set, batch_size=1, shuffle=False)
    
        # 定义损失函数
        if loss_name == 'dice':
            criterion = SoftDiceLoss(bladder.num_classes).cuda()
    
        # 定义早停机制
        early_stopping = EarlyStopping(early_stop_patience, verbose=True, delta=early_stop__eps,
                                       path=os.path.join(root_path, 'checkpoint', '{}.pth'.format(model_name)))
    
        # 定义优化器
        if optimizer_type == 'adam':
            optimizer = torch.optim.Adam(net.parameters(), lr=initial_lr, weight_decay=weight_decay)
        else:
            optimizer = torch.optim.SGD(net.parameters(), lr=0.1, momentum=0.9)
    
        # 定义学习率衰减策略
        if scheduler_type == 'StepLR':
            scheduler = lr_scheduler.StepLR(optimizer, step_size=4, gamma=0.1)
        elif scheduler_type == 'ReduceLR':
            scheduler = lr_scheduler.ReduceLROnPlateau(optimizer, mode='min', factor=0.1, patience=5)
        elif scheduler_type == 'poly':
            scheduler = PolyLR(optimizer, max_iter=n_epoch, power=0.9)
        else:
            scheduler = None
    
        train(train_loader, val_loader, net, criterion, optimizer, scheduler, None, early_stopping, n_epoch, 0)
    
    
    def train(train_loader, val_loader, net, criterion, optimizer, scheduler, warm_scheduler, early_stopping, num_epoches,
              iters):
        for epoch in range(1, num_epoches + 1):
            st = time.time()
            train_class_dices = np.array([0] * (bladder.num_classes - 1), dtype=np.float)
            val_class_dices = np.array([0] * (bladder.num_classes - 1), dtype=np.float)
            val_dice_arr = []
            train_losses = []
            val_losses = []
    
            # 训练模型
            net.train()
            for batch, ((input, mask), file_name) in enumerate(train_loader, 1):
                X = input.cuda()
                y = mask.cuda()
                optimizer.zero_grad()
                output = net(X)
                output = torch.sigmoid(output)
                loss = criterion(output, y)
                loss.backward()
                optimizer.step()
                iters += 1
                train_losses.append(loss.item())
    
                class_dice = []
                for i in range(1, bladder.num_classes):
                    cur_dice = diceCoeffv2(output[:, i:i + 1, :], y[:, i:i + 1, :]).cpu().item()
                    class_dice.append(cur_dice)
    
                mean_dice = sum(class_dice) / len(class_dice)
                train_class_dices += np.array(class_dice)
                string_print = 'epoch: {} - iters: {} - loss: {:.4} - mean: {:.4} - bladder: {:.4}- tumor: {:.4}  - time: {:.2}' \
                    .format(epoch, iters, loss.data.cpu(), mean_dice, class_dice[0], class_dice[1], time.time() - st)
                misc.log(string_print)
                st = time.time()
    
            train_loss = np.average(train_losses)
            train_class_dices = train_class_dices / batch
            train_mean_dice = train_class_dices.sum() / train_class_dices.size
    
            writer.add_scalar('main_loss', train_loss, epoch)
            writer.add_scalar('main_dice', train_mean_dice, epoch)
    
            print('epoch {}/{} - train_loss: {:.4} - train_mean_dice: {:.4} - dice_bladder: {:.4} - dice_tumor: {:.4}'.format(
                    epoch, num_epoches, train_loss, train_mean_dice, train_class_dices[0], train_class_dices[1]))
    
            # 验证模型
            net.eval()
            for val_batch, ((input, mask), file_name) in tqdm(enumerate(val_loader, 1)):
                val_X = input.cuda()
                val_y = mask.cuda()
    
                pred = net(val_X)
                pred = torch.sigmoid(pred)
                val_loss = criterion(pred, val_y)
    
                val_losses.append(val_loss.item())
                pred = pred.cpu().detach()
                val_class_dice = []
                for i in range(1, bladder.num_classes):
                    val_class_dice.append(diceCoeffv2(pred[:, i:i + 1, :], mask[:, i:i + 1, :]))
    
                val_dice_arr.append(val_class_dice)
                val_class_dices += np.array(val_class_dice)
    
            val_loss = np.average(val_losses)
    
            val_dice_arr = np.array(val_dice_arr)
            val_class_dices = val_class_dices / val_batch
    
            val_mean_dice = val_class_dices.sum() / val_class_dices.size
    
            val_writer.add_scalar('lr', optimizer.param_groups[0]['lr'], epoch)
            val_writer.add_scalar('main_loss', val_loss, epoch)
            val_writer.add_scalar('main_dice', val_mean_dice, epoch)
    
            print('val_loss: {:.4} - val_mean_dice: {:.4} - bladder: {:.4}- tumor: {:.4}'
                .format(val_loss, val_mean_dice, val_class_dices[0], val_class_dices[1]))
            print('lr: {}'.format(optimizer.param_groups[0]['lr']))
    
            early_stopping(val_mean_dice, net, epoch)
            if early_stopping.early_stop or optimizer.param_groups[0]['lr'] < threshold_lr:
                print("Early stopping")
                # 结束模型训练
                break
    
        print('----------------------------------------------------------')
        print('save epoch {}'.format(early_stopping.save_epoch))
        print('stoped epoch {}'.format(epoch))
        print('----------------------------------------------------------')
    
    
    if __name__ == '__main__':
        main()
    
    

    4.5 模型验证

    按照加载训练集类似的方法,我们加载验证集或者测试集进行模型验证。

    # /validate/validate_bladder.py
    import os
    import cv2
    import torch
    import shutil
    import utils.image_transforms as joint_transforms
    from torch.utils.data import DataLoader
    import utils.transforms as extended_transforms
    from datasets import bladder
    from utils.loss import *
    from networks.u_net import Baseline
    from tqdm import tqdm
    
    crop_size = 256
    val_path = r'..\media/Datasets/Bladder/raw_data'
    center_crop = joint_transforms.CenterCrop(crop_size)
    val_input_transform = extended_transforms.NpyToTensor()
    target_transform = extended_transforms.MaskToTensor()
    
    val_set = bladder.Dataset(val_path, 'val', 1,
                                  joint_transform=None, transform=val_input_transform, center_crop=center_crop,
                                  target_transform=target_transform)
    val_loader = DataLoader(val_set, batch_size=1, shuffle=False)
    
    palette = bladder.palette
    num_classes = bladder.num_classes
    
    net = Baseline(img_ch=1, num_classes=num_classes, depth=2).cuda()
    net.load_state_dict(torch.load("../checkpoint/unet_depth=2_fold_1_dice_348055.pth"))
    net.eval()
    
    
    def auto_val(net):
        # 效果展示图片数
        dices = 0
        class_dices = np.array([0] * (num_classes - 1), dtype=np.float)
    
        save_path = './results'
        if os.path.exists(save_path):
            # 若该目录已存在,则先删除,用来清空数据
            shutil.rmtree(os.path.join(save_path))
        img_path = os.path.join(save_path, 'images')
        pred_path = os.path.join(save_path, 'pred')
        gt_path = os.path.join(save_path, 'gt')
        os.makedirs(img_path)
        os.makedirs(pred_path)
        os.makedirs(gt_path)
    
        val_dice_arr = []
        for (input, mask), file_name in tqdm(val_loader):
            file_name = file_name[0].split('.')[0]
    
            X = input.cuda()
            pred = net(X)
            pred = torch.sigmoid(pred)
            pred = pred.cpu().detach()
    
            # pred[pred < 0.5] = 0
            # pred[np.logical_and(pred > 0.5, pred == 0.5)] = 1
    
            # 原图
            m1 = np.array(input.squeeze())
            m1 = helpers.array_to_img(np.expand_dims(m1, 2))
    
            # gt
            gt = helpers.onehot_to_mask(np.array(mask.squeeze()).transpose([1, 2, 0]), palette)
            gt = helpers.array_to_img(gt)
    
            # pred
            save_pred = helpers.onehot_to_mask(np.array(pred.squeeze()).transpose([1, 2, 0]), palette)
            save_pred_png = helpers.array_to_img(save_pred)
    
            # png格式
            m1.save(os.path.join(img_path, file_name + '.png'))
            gt.save(os.path.join(gt_path, file_name + '.png'))
            save_pred_png.save(os.path.join(pred_path, file_name + '.png'))
    
            class_dice = []
            for i in range(1, num_classes):
                class_dice.append(diceCoeffv2(pred[:, i:i + 1, :], mask[:, i:i + 1, :]))
            mean_dice = sum(class_dice) / len(class_dice)
            val_dice_arr.append(class_dice)
            dices += mean_dice
            class_dices += np.array(class_dice)
            print('mean_dice: {:.4} - dice_bladder: {:.4} - dice_tumor: {:.4}'
                      .format(mean_dice, class_dice[0], class_dice[1]))
    
        val_mean_dice = dices / (len(val_loader) / 1)
        val_class_dice = class_dices / (len(val_loader) / 1)
        print('Val mean_dice: {:.4} - dice_bladder: {:.4} - dice_tumor: {:.4}'.format(val_mean_dice, val_class_dice[0], val_class_dice[1]))
    
    
    if __name__ == '__main__':
        np.set_printoptions(threshold=9999999)
        auto_val(net)
    

    直接运行该文件可生成我们的预测结果。
    虽然我们的U-Net只用了24张图进行训练,但从结果可以看到,模型也能大致分割出目标。
    在这里插入图片描述

    更多相关内容
  • 基于SVM的图像分割-真彩色图像分割,可以很好的分割彩色图像
  • 基于SVM的图像分割-真彩色图像分割,可以很好的分割彩色图像
  • python3.7可运行的kmeans聚类图像分割
  • 图像分割】基于GAC水平集方法实现图像分割matlab 源码.md
  • FCM算法图像分割MATLAB代码
  • 针对图像的超像素图像分割,聚类,效果不错
  • 用FCM算法实现了图像分割,效果较好,可以更改聚类数目
  • matlab 图像处理 图像分割 边缘检测
  • 图像分割 matlab

    2018-11-29 14:34:38
    图像中心定点 进行区域分割,时候用于人脸图像的局部分割
  • MATLAB实现的图像分割程序,通过读入一副图片,进行二值化和相应的分割算法之后,可以提取到相应的图像块,并且在不同的分割块上进行标注。代码中附带图片,可以直接运行。
  • 图像分割在图像处理过渡到图像分析这个过程中起着非常重要的作用,它是图像工程的核心,图像分割的研究具有重要的理论和应用价值。介绍了图像分割的基本理论和常用方法,借助Matlab平台对阈值的分割、区域特性的分割...
  • 图像分割是图像识别和机器视觉领域中关键的预处理操作。分割理论算法众多,文中 具体介绍基于聚类的分割算法的思想和原理,并将包含的典型算法的优缺点进行介绍和分析。 经过比较后,归纳了在具体应用中如何对图像...
  • 基于水平集CV模型的图像分割(Matlab代码),全套代码
  • 图像处理方面,利用matlab自带函数进行kmeans聚类,完成图像分割任务。完整代码,参数(k值)根据需求自行选择,当前代码中k=2。
  • 只要装有matlab就能跑通代码,采用的均值漂移(mean shift)方法进行图像分割
  • MRF图像分割步骤与MATLAB程序 MRF图像分割步骤与MATLAB程序 MRF图像分割步骤与MATLAB程序 MRF图像分割步骤与MATLAB程序
  • SVM图像分割matlab

    2015-12-14 07:55:31
    matlab源程序,基于svm的图像分割,类似抠图显示,主要根据颜色对比明显实现,另外运行此程序可能还需要下载svmlib,然后set path... 资源包里还包含图片,效果演示文档,源代码极其注释。
  • 通过MATLAB实现对图像的分割,包括基于全局阈值的图像分割以及基于区域的图像分割
  • 本文对近年在图像分割技术上出现的常用图像分割算法以及它们的优缺点进行了综述,并介绍了其在遥感图像中的应用,提出了图像分割算法的前景和面临的挑战。
  • PCNN 灰度图像,图像分割matlab程序
  • 可以实现医学图像分割,简单方便,帮助大家学习
  • 这个是用于图像分割中的处置投代码,用于图像分割
  • 基于SVM的图像分割-真彩色图像分割,含有源程序和数据
  • 在MATLAB软件中利用PCNN实现图像分割
  • 传统图像分割方法详解

    千次阅读 2022-02-11 10:41:04
    详解传统图像分割方法

    转载:

    所谓图像分割指的是根据灰度、颜色、纹理和形状等特征把图像划分成若干互不交迭的区域,并使这些特征在同一区域内呈现出相似性,而在不同区域间呈现出明显的差异性。我们先对目前主要的图像分割方法做个概述,后面再对个别方法做详细的了解和学习。

    1. 图像分割算法概述

    1.1 基于阈值的分割方法

    阈值法的基本思想是基于图像的灰度特征来计算一个或多个灰度阈值,并将图像中每个像素的灰度值与阈值相比较,最后将像素根据比较结果分到合适的类别中。因此,该类方法最为关键的一步就是按照某个准则函数来求解最佳灰度阈值。

    1.2 基于边缘的分割方法

    所谓边缘是指图像中两个不同区域的边界线上连续的像素点的集合,是图像局部特征不连续性的反映,体现了灰度、颜色、纹理等图像特性的突变。通常情况下,基于边缘的分割方法指的是基于灰度值的边缘检测,它是建立在边缘灰度值会呈现出阶跃型屋顶型变化这一观测基础上的方法。

    阶跃型边缘两边像素点的灰度值存在着明显的差异,而屋顶型边缘则位于灰度值上升或下降的转折处。正是基于这一特性,可以使用微分算子进行边缘检测,即使用一阶导数的极值与二阶导数的过零点来确定边缘,具体实现时可以使用图像与模板进行卷积来完成。

    1.3 基于区域的分割方法

    此类方法是将图像按照相似性准则分成不同的区域,主要包括种子区域生长法区域分裂合并法分水岭法等几种类型。

    种子区域生长法是从一组代表不同生长区域的种子像素开始,接下来将种子像素邻域里符合条件的像素合并到种子像素所代表的生长区域中,并将新添加的像素作为新的种子像素继续合并过程,直到找不到符合条件的新像素为止。该方法的关键是选择合适的初始种子像素以及合理的生长准则。

    区域分裂合并法(Gonzalez,2002)的基本思想是首先将图像任意分成若干互不相交的区域,然后再按照相关准则对这些区域进行分裂或者合并从而完成分割任务,该方法既适用于灰度图像分割也适用于纹理图像分割。

    分水岭法(Meyer,1990)是一种基于拓扑理论的数学形态学的分割方法,其基本思想是把图像看作是测地学上的拓扑地貌,图像中每一点像素的灰度值表示该点的海拔高度,每一个局部极小值及其影响区域称为集水盆,而集水盆的边界则形成分水岭。该算法的实现可以模拟成洪水淹没的过程,图像的最低点首先被淹没,然后水逐渐淹没整个山谷。当水位到达一定高度的时候将会溢出,这时在水溢出的地方修建堤坝,重复这个过程直到整个图像上的点全部被淹没,这时所建立的一系列堤坝就成为分开各个盆地的分水岭。分水岭算法对微弱的边缘有着良好的响应,但图像中的噪声会使分水岭算法产生过分割的现象。

    1.4 基于图论的分割方法

    此类方法把图像分割问题与图的最小割(min cut)问题相关联。首先将图像映射为带权无向图G=<V,E>,图中每个节点N∈V对应于图像中的每个像素,每条边∈E连接着一对相邻的像素,边的权值表示了相邻像素之间在灰度、颜色或纹理方面的非负相似度。而对图像的一个分割s就是对图的一个剪切,被分割的每个区域C∈S对应着图中的一个子图。而分割的最优原则就是使划分后的子图在内部保持相似度最大,而子图之间的相似度保持最小。基于图论的分割方法的本质就是移除特定的边,将图划分为若干子图从而实现分割。目前所了解到的基于图论的方法有GraphCutGrabCutRandom Walk等。

    1.5 基于能量泛函的分割方法

    该类方法主要指的是活动轮廓模型(active contour model)以及在其基础上发展出来的算法,其基本思想是使用连续曲线来表达目标边缘,并定义一个能量泛函使得其自变量包括边缘曲线,因此分割过程就转变为求解能量泛函的最小值的过程,一般可通过求解函数对应的欧拉(Euler.Lagrange)方程来实现,能量达到最小时的曲线位置就是目标的轮廓所在。按照模型中曲线表达形式的不同,活动轮廓模型可以分为两大类:参数活动轮廓模型(parametric active contour model)和几何活动轮廓模型(geometric active contour model)。

    参数活动轮廓模型是基于Lagrange框架,直接以曲线的参数化形式来表达曲线,最具代表性的是由Kasset a1(1987)所提出的Snake模型。该类模型在早期的生物图像分割领域得到了成功的应用,但其存在着分割结果受初始轮廓的设置影响较大以及难以处理曲线拓扑结构变化等缺点,此外其能量泛函只依赖于曲线参数的选择,与物体的几何形状无关,这也限制了其进一步的应用。

    几何活动轮廓模型的曲线运动过程是基于曲线的几何度量参数而非曲线的表达参数,因此可以较好地处理拓扑结构的变化,并可以解决参数活动轮廓模型难以解决的问题。而水平集(Level Set)方法(Osher,1988)的引入,则极大地推动了几何活动轮廓模型的发展,因此几何活动轮廓模型一般也可被称为水平集方法。

    1.6 基于小波分析和小波变换的图像分割方法

    小波变换是近年来得到的广泛应用的数学工具,也是现在数字图像处理必学部分,它在时间域和频率域上都有量高的局部化性质,能将时域和频域统一于一体来研究信号。而且小波变换具有多尺度特性,能够在不同尺度上对信号进行分析,因此在图像分割方面的得到了应用。

    二进小波变换具有检测二元函数的局部突变能力,因此可作为图像边缘检测工具。图像的边缘出现在图像局部灰度不连续处,对应于二进小波变换的模极大值点。通过检测小波变换模极大值点可以确定图像的边缘小波变换位于各个尺度上,而每个尺度上的小波变换都能提供一定的边缘信息,因此可进行多尺度边缘检测来得到比较理想的图像边缘。

    在这里插入图片描述
    上图左图是传统的阈值分割方法,右边的图像就是利用小波变换的图像分割。可以看出右图分割得到的边缘更加准确和清晰

    另外,将小波和其他方法结合起来处理图像分割的问题也得到了广泛研究,比如一种局部自适应阈值法就是将Hilbert图像扫描和小波相结合,从而获得了连续光滑的阈值曲线。

    1.7 基于遗传算法的图像分割

    遗传算法(Genetic Algorithms,简称GA)是1973年由美国教授Holland提出的,是一种借鉴生物界自然选择和自然遗传机制的随机化搜索算法。是仿生学在数学领域的应用。其基本思想是,模拟由一些基因串控制的生物群体的进化过程,把该过程的原理应用到搜索算法中,以提高寻优的速度和质量。此算法的搜索过程不直接作用在变量上,而是在参数集进行了编码的个体,这使得遗传算法可直接对结构对象(图像)进行操作。整个搜索过程是从一组解迭代到另一组解,采用同时处理群体中多个个体的方法,降低了陷入局部最优解的可能性,并易于并行化。搜索过程采用概率的变迁规则来指导搜索方向,而不采用确定性搜索规则,而且对搜索空间没有任何特殊要求(如连通性、凸性等),只利用适应性信息,不需要导数等其他辅助信息,适应范围广。

    遗传算法擅长于全局搜索,但局部搜索能力不足,所以常把遗传算法和其他算法结合起来应用。将遗传算法运用到图像处理主要是考虑到遗传算法具有与问题领域无关且快速随机的搜索能力。其搜索从群体出发,具有潜在的并行性,可以进行多个个体的同时比较,能有效的加快图像处理的速度。但是遗传算法也有其缺点:搜索所使用的评价函数的设计、初始种群的选择有一定的依赖性等。要是能够结合一些启发算法进行改进且遗传算法的并行机制的潜力得到充分的利用,这是当前遗传算法在图像处理中的一个研究热点。

    2. 图像分割之阈值分割

    2.1 OSTU算法

    Otsu算法,也叫最大类间方差法(大津算法),是一种全局阈值的算法。

    之所以称为最大类间方差法是因为,用该阈值进行的图像固定阈值二值化,类间方差最大,它是按图像的灰度特性,将图像分成背景和前景两部分,使类间方差最大的分割意味着错分概率最小。

    在这里插入图片描述

    原理:

    对于图像I(x,y),前景(即目标)和背景的分割阈值记作T,属于前景的像素点数占整幅图像的比例记为ω0,其平均灰度μ0;背景像素点数占整幅图像的比例为ω1,其平均灰度为μ1。图像的总平均灰度记为μ,类间方差记为g。
    假设图像的背景较暗,并且图像的大小为M×N,图像中像素的灰度值小于阈值T的像素个数记作N0,像素灰度大于阈值T的像素个数记作N1,则有:
          (1) ω0=N0/ (M×N)
          (2) ω1=N1/ (M×N)
          (3) N0 + N1 = M×N
          (4) ω0 + ω1 = 1
          (5) μ = ω0 * μ0 + ω1 * μ1
          (6) g = ω0 * (μ0 - μ)2 + ω1 * (μ1 - μ)2
    将式(5)代入式(6),得到等价公式:
          (7) g = ω0 *ω1 * (μ0 - μ1)2
    采用遍历的方法得到使类间方差g最大的阈值T。

    算法评价:

    优点:算法简单,当目标与背景的面积相差不大时,能够有效地对图像进行分割。
    缺点:当图像中的目标与背景的面积相差很大时,表现为直方图没有明显的双峰,或者两个峰的大小相差很大,分割效果不佳,或者目标与背景的灰度有较大的重叠时也不能准确的将目标与背景分开。
    原因:该方法忽略了图像的空间信息,同时将图像的灰度分布作为分割图像的依据,对噪声也相当敏感。

    OpenCV提供的API:

    double threshold(InputArray src, OutputArray dst, double thresh, double maxval, int type)
    

    参数:

    • src:输入图,只能输入单通道,8位或32位浮点数图像。
    • dst:输出图,尺寸大小、深度会和输入图相同。
    • thresh:阈值。
    • maxval:二值化结果的最大值。
    • type:二值化操作形态,共有THRESH_BINARY、THRESH_BINARY_INV、THRESH_TRUNC、THRESH_TOZERO、THRESH_TOZERO_INV五种。type从上述五种结合CV_THRESH_OTSU就是OSTU算法,写成:THRESH_BINARY | CV_THRESH_OTSU。

    2.1.1 基于OpenCV实现

    #include <cstdio>
    #include <opencv2/opencv.hpp>
    
    int main() {
        cv::Mat src = cv::imread("D:\\code\\C++\\图像分割\\image segmentation\\test_pic\\OSTU\\car.png");
        if (src.empty()) {
            return -1;
        }
        if (src.channels() > 1)
            cv::cvtColor(src, src, cv::COLOR_RGB2GRAY);
    
        cv::Mat dst;
        threshold(src, dst, 0, 255, cv::THRESH_BINARY | cv::THRESH_OTSU); // 由于是OSTU算法,threshold这个参数无用
    
        cv::namedWindow("src", cv::WINDOW_NORMAL);
        cv::imshow("src", src);
        cv::namedWindow("dst", cv::WINDOW_NORMAL);
        cv::imshow("dst", dst);
        cv::imwrite("D:\\code\\C++\\图像分割\\image segmentation\\test_pic\\OSTU\\car_output.png", dst);
        cv::waitKey(0);
    
        return 0;
    }
    

    我们对一张普通的车的图片进行了测试,结果如下所示:

    在这里插入图片描述

    下面我们对kitti图像和散斑结构光图像进行分割,结果如下所示:

    在这里插入图片描述

    • real-general

    在这里插入图片描述

    • real-indoor1
      在这里插入图片描述

    • real-indoor2
      在这里插入图片描述

    • real-indoor3
      在这里插入图片描述

    • real-outdoor
      在这里插入图片描述

    • synthetic-intel
      在这里插入图片描述

    • synthetic-ideal
      在这里插入图片描述

    • synthetic-polka

    在这里插入图片描述

    2.2 自适应阈值法

    自适应阈值法(adaptiveThreshold),它的思想不是计算全局图像的阈值,而是根据图像不同区域亮度分布,计算其局部阈值,所以对于图像不同区域,能够自适应计算不同的阈值,因此被称为自适应阈值法。(其实就是局部阈值法)

    如何确定局部阈值呢?可以计算某个邻域(局部)的均值、中值、高斯加权平均(高斯滤波)来确定阈值。值得说明的是:如果用局部的均值作为局部的阈值,就是常说的移动平均法。

    OpenCV提供的API:

    void adaptiveThreshold(InputArray src, OutputArray dst, double maxValue,
                          int adaptiveMethod, int thresholdType, int blockSize, double C)
    

    参数:

    • InputArray src:源图像
    • OutputArray dst:输出图像,与源图像大小一致
    • int adaptiveMethod:在一个邻域内计算阈值所采用的算法,有两个取值,分别为 ADAPTIVE_THRESH_MEAN_C 和 ADAPTIVE_THRESH_GAUSSIAN_C 。
      • ADAPTIVE_THRESH_MEAN_C的计算方法是计算出领域的平均值再减去第七个参数double C的值。
      • ADAPTIVE_THRESH_GAUSSIAN_C的计算方法是计算出领域的高斯均值再减去第七个参数double C的值。
    • int thresholdType:这是阈值类型,有以下几个选择。
      • THRESH_BINARY 二进制阈值化 -> 大于阈值为1 小于阈值为0
      • THRESH_BINARY_INV 反二进制阈值化 -> 大于阈值为0 小于阈值为1
      • THRESH_TRUNC 截断阈值化 -> 大于阈值为阈值,小于阈值不变
      • THRESH_TOZERO 阈值化为0 -> 大于阈值的不变,小于阈值的全为0
      • THRESH_TOZERO_INV 反阈值化为0 -> 大于阈值为0,小于阈值不变
    • int blockSize:adaptiveThreshold的计算单位是像素的邻域块,这是局部邻域大小,3、5、7等。
    • double C:这个参数实际上是一个偏移值调整量,用均值和高斯计算阈值后,再减这个值就是最终阈值。

    注:相比OpenCV的API,我多用了一个中值法确定阈值。

    2.2.1 基于OpenCV实现

    #include <iostream>
    #include <opencv2/core.hpp>
    #include <opencv2/highgui.hpp>
    #include <opencv2/imgproc.hpp>
    
    
    enum adaptiveMethod { meanFilter, gaaussianFilter, medianFilter };
    
    void AdaptiveThreshold(cv::Mat& src, cv::Mat& dst, double Maxval, int Subsize, double c, adaptiveMethod method = meanFilter) {
    
    	if (src.channels() > 1)
    		cv::cvtColor(src, src, cv::COLOR_RGB2GRAY);
    
    	cv::Mat smooth;
    	switch (method)
    	{
    	case  meanFilter:
    		cv::blur(src, smooth, cv::Size(Subsize, Subsize));  //均值滤波
    		break;
    	case gaaussianFilter:
    		cv::GaussianBlur(src, smooth, cv::Size(Subsize, Subsize), 0, 0); //高斯滤波
    		break;
    	case medianFilter:
    		cv::medianBlur(src, smooth, Subsize);   //中值滤波
    		break;
    	default:
    		break;
    	}
    
    	smooth = smooth - c;
    
    	//阈值处理
    	src.copyTo(dst);
    	for (int r = 0; r < src.rows; ++r) {
    		const uchar* srcptr = src.ptr<uchar>(r);
    		const uchar* smoothptr = smooth.ptr<uchar>(r);
    		uchar* dstptr = dst.ptr<uchar>(r);
    		for (int c = 0; c < src.cols; ++c) {
    			if (srcptr[c] > smoothptr[c]) {
    				dstptr[c] = Maxval;
    			}
    			else
    				dstptr[c] = 0;
    		}
    	}
    
    }
    
    int main() {
    	cv::Mat src = cv::imread("D:\\code\\C++\\图像分割\\image segmentation\\test_pic\\car.png");
    	if (src.empty()) {
    		return -1;
    	}
    	if (src.channels() > 1)
    		cv::cvtColor(src, src, cv::COLOR_RGB2GRAY);
    
    	cv::Mat dst, dst2;
    	double t2 = (double)cv::getTickCount();
    	AdaptiveThreshold(src, dst, 255, 21, 10, meanFilter);  //
    	t2 = (double)cv::getTickCount() - t2;
    	double time2 = (t2 * 1000.) / ((double)cv::getTickFrequency());
    	std::cout << "my_process=" << time2 << " ms. " << std::endl << std::endl;
    
    	cv::adaptiveThreshold(src, dst2, 255, cv::ADAPTIVE_THRESH_MEAN_C, cv::THRESH_BINARY, 21, 10);
    
    	cv::namedWindow("src", cv::WINDOW_NORMAL);
    	cv::imshow("src", src);
    	cv::namedWindow("dst", cv::WINDOW_NORMAL);
    	cv::imshow("dst", dst);
    	cv::namedWindow("dst2", cv::WINDOW_NORMAL);
    	cv::imshow("dst2", dst2);
    	cv::imwrite("D:\\code\\C++\\图像分割\\image segmentation\\test_pic\\car_output.png", dst);
    	cv::waitKey(0);
    }
    

    我们对一张普通的车的图片进行了测试,结果如下所示:

    在这里插入图片描述
    左边的图像是原图,中间的图像是我们自己写的阈值分割算法的结果,右边的图像是opencv自带的阈值分割算法的结果,可以看到,两种结果是一样的。而且分割的效果都不错。

    然后,我们还对kitti图像和散斑结构光图像(相当于向原图中加入了类似椒盐噪声)进行了阈值分割,先来看看结果。由于自己写的分割算法与opencv自带算法相当,由于我们写的算法还有中值滤波的选项,所以我们使用我们自己的算法。下面是结果:

    在这里插入图片描述

    • real-general

    在这里插入图片描述

    • real-indoor1
      在这里插入图片描述

    • real-indoor2
      在这里插入图片描述

    • real-indoor3
      在这里插入图片描述

    • real-outdoor
      在这里插入图片描述

    • synthetic-intel
      在这里插入图片描述

    • synthetic-ideal
      在这里插入图片描述

    • synthetic-polka
      在这里插入图片描述

    3. 基于边缘的分割方法

    基于边缘检测的图像分割方法的基本思路是先确定图像中的边缘像素,然后再把这些像素连接在一起就构成所需的区域边界。

    图像边缘,即表示图像中一个区域的终结和另一个区域的开始,图像中相邻区域之间的像素集合构成了图像的边缘。所以,图像边缘可以理解为图像灰度发生空间突变的像素的集合。图像边缘有两个要素,即:方向和幅度。沿着边缘走向的像素值变化比较平缓;而沿着垂直于边缘的走向,像素值则变化得比较大。因此,根据这一变化特点,通常会采用一阶和二阶导数来描述和检测边缘。

    综上,图像中的边缘检测可以通过对灰度值求导数来确定,而导数可以通过微分算子计算来实现。在数字图像处理中,通常是利用差分计算来近似代替微分运算。

    参考下面两篇博客:

    这里只列举其中的两种算子:LoG算子和Canny算子。因为这两种算子能够较好的抑制噪声。

    3.1 LoG算子

    在这里插入图片描述
    在这里插入图片描述

    OpenCV提供的API:

    void GaussianBlur(InputArray src, OutputArray dst, Size ksize, double sigmaX, double sigmaY=0, int borderType=BORDER_DEFAULT )
    

    参数:

    • InputArray src: 输入图像,可以是Mat类型,图像深度为CV_8U、CV_16U、CV_16S、CV_32F、CV_64F。
    • OutputArray dst: 输出图像,与输入图像有相同的类型和尺寸。
    • Size ksize: 高斯内核大小,这个尺寸与前面两个滤波kernel尺寸不同,ksize.width和ksize.height可以不相同但是这两个值必须为正奇数,如果这两个值为0,他们的值将由sigma计算。
    • double sigmaX: 高斯核函数在X方向上的标准偏差。
    • double sigmaY: 高斯核函数在Y方向上的标准偏差,如果sigmaY是0,则函数会自动将sigmaY的值设置为与sigmaX相同的值,如果sigmaX和sigmaY都是0,这两个值将由ksize.width和ksize.height计算而来。
    • int borderType=BORDER_DEFAULT: 推断图像外部像素的某种便捷模式,有默认值BORDER_DEFAULT,如果没有特殊需要不用更改,具体可以参考borderInterpolate()函数。
    void Laplacian( InputArray src, OutputArray dst, int ddepth,
                                 int ksize = 1, double scale = 1, double delta = 0,
                                 int borderType = BORDER_DEFAULT )
    

    参数:

    • src:输入原图像,可以是灰度图像或彩色图像。
    • dst:输出图像,与输入图像src具有相同的尺寸和通道数
    • ddepth:输出图像的数据类型(深度),根据输入图像的数据类型不同拥有不同的取值范围,因为输入图像一般为CV_8U,为了避免数据溢出,输出图像深度应该设置为CV_16S。
    • ksize:滤波器的大小,必须为正奇数。
    • scale:对导数计算结果进行缩放的缩放因子,默认系数为1,表示不进行缩放。
    • delta:额外加的数值,就是在卷积过程中该数值会添加到每个像素上。
    • borderType:界填充方式。默认BORDER_DEFAULT,表示不包含边界值倒序填充。

    3.1.1 基于OpenCV实现

    #include "opencv2/imgproc/imgproc.hpp"  
    #include "opencv2/highgui/highgui.hpp"  
    
    using namespace cv;
    
    int main()
    {
    	//使用LoG算子做边缘检测
    	Mat src;
    	int kernel_size = 3;
    
    	src = imread("D:\\code\\C++\\图像分割\\image segmentation\\test_pic\\LoG边缘检测\\car.png");
    	GaussianBlur(src, src, Size(3, 3), 0, 0, BORDER_DEFAULT);	//先通过高斯模糊去噪声
    	if (src.channels() > 1)
    		cv::cvtColor(src, src, cv::COLOR_RGB2GRAY);
    	
    	Mat dst, abs_dst;
    	Laplacian(src, dst, CV_16S, kernel_size);	//通过拉普拉斯算子做边缘检测
    	convertScaleAbs(dst, abs_dst);
    
    	namedWindow("src", WINDOW_NORMAL);
    	imshow("src", src);
    	namedWindow("dst", WINDOW_NORMAL);
    	imshow("dst", abs_dst);
    	imwrite("D:\\code\\C++\\图像分割\\image segmentation\\test_pic\\LoG边缘检测\\car_output.png", abs_dst);
    	waitKey(0);
    
    	//使用自定义滤波做边缘检测
    	//自定义滤波算子 1  1  1
    	//               1 -8  1
    	//               1  1  1
    	//Mat custom_src, custom_gray, Kernel;
    	//custom_src = imread("Lenna.jpg");
    	//GaussianBlur(custom_src, custom_src, Size(3, 3), 0, 0, BORDER_DEFAULT);	//先通过高斯模糊去噪声
    	//cvtColor(custom_src, custom_gray, COLOR_RGB2GRAY);
    	//namedWindow("Custom Filter", WINDOW_AUTOSIZE);
    
    	//Kernel = (Mat_<double>(3, 3) << 1, 1, 1, 1, -8, 1, 1, 1, 1);	//自定义滤波算子做边缘检测
    	//Mat custdst, abs_custdst;
    	//filter2D(custom_gray, custdst, CV_16S, Kernel, Point(-1, -1));
    	//convertScaleAbs(custdst, abs_custdst);
    
    	//imshow("Custom Filter", abs_custdst);
    	//waitKey(0);
    
    	return 0;
    }
    

    下面给出结果:
    在这里插入图片描述

    在这里插入图片描述

    • real-general

    在这里插入图片描述

    • real-indoor1
      在这里插入图片描述

    • real-indoor2
      在这里插入图片描述

    • real-indoor3
      在这里插入图片描述

    • real-outdoor
      在这里插入图片描述

    • synthetic-intel
      在这里插入图片描述

    • synthetic-ideal
      在这里插入图片描述

    • synthetic-polka
      在这里插入图片描述

    3.2 Canny算子

    在这里插入图片描述
    OpenCV提供的API:

    void Canny(InputArray image,OutputArray edges, double threshold1, double threshold2, int apertureSize=3,bool L2gradient=false )
    

    参数:

    • InputArray image:输入图像,即源图像,填Mat类的对象即可,且需为单通道8位图像。
    • OutputArray edges:输出的边缘图,需要和源图片有一样的尺寸和类型。
    • double threshold1:第一个滞后性阈值。
    • double threshold2:第二个滞后性阈值。
    • int apertureSize:表示应用Sobel算子的孔径大小,其有默认值3。
    • bool L2gradient:一个计算图像梯度幅值的标识,有默认值false。

    3.2.1 基于Opencv实现

    结果展示:

    在这里插入图片描述

    在这里插入图片描述

    • real-general

    在这里插入图片描述

    • real-indoor1
      在这里插入图片描述

    • real-indoor2
      在这里插入图片描述

    • real-indoor3
      在这里插入图片描述

    • real-outdoor
      在这里插入图片描述

    • synthetic-intel
      在这里插入图片描述

    • synthetic-ideal
      在这里插入图片描述

    • synthetic-polka
      在这里插入图片描述

    4. 图像分割之分水岭算法

    分水岭比较经典的计算方法是L.Vincent于1991年在PAMI上提出的。传统的分水岭分割方法,是一种基于拓扑理论的数学形态学的分割方法,其基本思想是把图像看作是测地学上的拓扑地貌,图像中每一像素的灰度值表示该点的海拔高度,每一个局部极小值及其影响区域称为集水盆地,而集水盆地的边界则形成分水岭。分水岭的概念和形成可以通过模拟浸入过程来说明。在每一个局部极小值表面,刺穿一个小孔,然后把整个模型慢慢浸人水中,随着浸入的加深,每一个局部极小值的影响域慢慢向外扩展,在两个集水盆汇合处构筑大坝如下图所示,即形成分水岭,下图为传统分水岭算法示意图。

    在这里插入图片描述
    然而基于梯度图像的直接分水岭算法(涨水过程都是基于梯度图)容易导致图像的过分割,产生这一现象的原因主要是由于输入的图像存在过多的极小区域(指的是梯度极小值)而产生许多小的集水盆地,从而导致分割后的图像不能将图像中有意义的区域表示出来。所以必须对分割结果的相似区域进行合并。

    为了解决过分割的问题,学者们提出了基于标记(mark)图像的分水岭算法,就是通过先验知识,来指导分水岭算法,以便获得更好的图像分割效果。通常的mark图像,都是在某个区域定义了一些灰度层级,在这个区域的洪水淹没过程中,水平面都是从定义的高度开始的,这样可以避免一些很小的噪声极小值区域的分割。

    在这里插入图片描述

    下面比较了两种分水岭的思路:

    在这里插入图片描述

    从Fig.2看,原图fig.2a,梯度图fig.2b(flooding涨水过程都是基于梯度图):

    1.通过最小值构建分水岭(Flooding from minima),即从最小的灰度值开始z涨水(flooding)。步骤如下:

    从b图看,得到4个局部最小区域,每一个区域都被标注为一个独立的颜色(fig.2c)(其中两块为背景)。接下来涨水过程如fig.2d-fig.2g所示,最后得到fig2.h的分割结果(当涨水到两个区域相邻时候为分水岭)。这样得到的分水岭容易过分割。

    2.通过markers构建分水岭(flooding from markers),步骤如下:

    如fig.2i,构建3个初始markers,涨水过程为fig.2j - fig.2m。着表明markers的选取不一定要在区域最小值位置。但是这种办法需要自己构建markers(背景也是要有一个marker)

    OpenCV提供了一种改进的分水岭算法,使用一系列预定义标记来引导图像分割的定义方式。使用OpenCV的分水岭算法cv::wathershed,需要输入一个标记图像,图像的像素值为32位有符号正数(CV_32S类型),每个非零像素代表一个标签。它的原理是对图像中部分像素做标记,表明它的所属区域是已知的。分水岭算法可以根据这个初始标签确定其他像素所属的区域。

    opencv提供的函数:

    void watershed( InputArray image, InputOutputArray markers );
    

    参数:

    • InputArray image:输入图像,必须为8位三通道图像。
    • InputOutputArray markers:标签图像。

    分水岭算法其实关键是研究如何生成标签图像,关于这一点,下面给出两篇博客提供的两种不同的方法。

    4.1 方法一:前景背景标签

    算法步骤:

    • 封装分水岭算法类
    • 获取标记图像
    • 获取前景像素,并用255标记前景
    • 获取背景像素,并用128标记背景,未知像素,使用0标记
    • 合成标记图像
    • 将原图和标记图像输入分水岭算法
    • 显示结果

    4.1.1 基于Opencv实现

    下面只给出代码,具体请看OpenCV—图像分割中的分水岭算法原理与应用

    #include <opencv2/core/core.hpp>
    #include <opencv2/imgproc/imgproc.hpp>
    #include <opencv2/imgcodecs.hpp>
    #include <opencv2/highgui.hpp>
    
    
    class WatershedSegmenter {
    
    private:
    
    	cv::Mat markers;
    
    public:
    
    	void setMarkers(const cv::Mat& markerImage) {
    
    		// Convert to image of ints
    		markerImage.convertTo(markers, CV_32S);
    	}
    
    	cv::Mat process(const cv::Mat& image) {
    
    		// Apply watershed
    		cv::watershed(image, markers);
    
    		return markers;
    	}
    
    	// Return result in the form of an image
    	cv::Mat getSegmentation() {
    
    		cv::Mat tmp;
    		// all segment with label higher than 255
    		// will be assigned value 255
    		markers.convertTo(tmp, CV_8U);
    
    		return tmp;
    	}
    
    	// Return watershed in the form of an image以图像的形式返回分水岭
    	cv::Mat getWatersheds() {
    
    		cv::Mat tmp;
    		//在变换前,把每个像素p转换为255p+255(在conertTo中实现)
    		markers.convertTo(tmp, CV_8U, 255, 255);
    
    		return tmp;
    	}
    };
    
    int main() {
    	// Read input image
    	cv::Mat image1 = cv::imread("C:\\Users\\Changming Deng\\Desktop\\飞机.png");
    	if (!image1.data)
    		return 0;
    	// Display the color image
    	cv::resize(image1, image1, cv::Size(), 0.7, 0.7);
    	cv::namedWindow("Original Image1");
    	cv::imshow("Original Image1", image1);
    
    	// Identify image pixels with object
    
    	cv::Mat binary;
    	cv::cvtColor(image1, binary, cv::COLOR_BGRA2GRAY);
    	cv::threshold(binary, binary, 30, 255, cv::THRESH_BINARY_INV);//阈值分割原图的灰度图,获得二值图像
    	// Display the binary image
    	cv::namedWindow("binary Image1");
    	cv::imshow("binary Image1", binary);
    	cv::waitKey();
    
    	// CLOSE operation
    	cv::Mat element5(5, 5, CV_8U, cv::Scalar(1));//5*5正方形,8位uchar型,全1结构元素
    	cv::Mat fg1;
    	cv::morphologyEx(binary, fg1, cv::MORPH_CLOSE, element5, cv::Point(-1, -1), 1);// 闭运算填充物体内细小空洞、连接邻近物体
    
    	// Display the foreground image
    	cv::namedWindow("Foreground Image");
    	cv::imshow("Foreground Image", fg1);
    	cv::waitKey(0);
    
    	// Identify image pixels without objects
    
    	cv::Mat bg1;
    	cv::dilate(binary, bg1, cv::Mat(), cv::Point(-1, -1), 4);//膨胀4次,锚点为结构元素中心点
    	cv::threshold(bg1, bg1, 1, 128, cv::THRESH_BINARY_INV);//>=1的像素设置为128(即背景)
    	// Display the background image
    	cv::namedWindow("Background Image");
    	cv::imshow("Background Image", bg1);
    	cv::waitKey();
    
    	//Get markers image
    
    	cv::Mat markers1 = fg1 + bg1; //使用Mat类的重载运算符+来合并图像。
    	cv::namedWindow("markers Image");
    	cv::imshow("markers Image", markers1);
    	cv::waitKey();
    
    	// Apply watershed segmentation
    
    	WatershedSegmenter segmenter1;  //实例化一个分水岭分割方法的对象
    	segmenter1.setMarkers(markers1);//设置算法的标记图像,使得水淹过程从这组预先定义好的标记像素开始
    	segmenter1.process(image1);     //传入待分割原图
    
    	// Display segmentation result
    	cv::namedWindow("Segmentation1");
    	cv::imshow("Segmentation1", segmenter1.getSegmentation());//将修改后的标记图markers转换为可显示的8位灰度图并返回分割结果(白色为前景,灰色为背景,0为边缘)
    	cv::waitKey();
    	// Display watersheds
    	cv::namedWindow("Watersheds1");
    	cv::imshow("Watersheds1", segmenter1.getWatersheds());//以图像的形式返回分水岭(分割线条)
    	cv::waitKey();
    
    	// Get the masked image
    	cv::Mat maskimage = segmenter1.getSegmentation();
    	cv::threshold(maskimage, maskimage, 250, 1, cv::THRESH_BINARY);
    	cv::cvtColor(maskimage, maskimage, cv::COLOR_GRAY2BGR);
    
    	maskimage = image1.mul(maskimage);
    	cv::namedWindow("maskimage");
    	cv::imshow("maskimage", maskimage);
    	cv::waitKey();
    
    	// Turn background (0) to white (255)
    	int nl = maskimage.rows; // number of lines
    	int nc = maskimage.cols * maskimage.channels(); // total number of elements per line
    
    	for (int j = 0; j < nl; j++) {
    		uchar* data = maskimage.ptr<uchar>(j);
    		for (int i = 0; i < nc; i++)
    		{
    			// process each pixel ---------------------
    			if (*data == 0) //将背景由黑色改为白色显示
    				*data = 255;
    			data++;//指针操作:如为uchar型指针则移动1个字节,即移动到下1列
    		}
    	}
    	cv::namedWindow("result");
    	cv::imshow("result", maskimage);
    	cv::waitKey();
    
    	return 0;
    }
    

    自己的思考:上面的代码中,marker分为前景(255)、背景(128)和未知(0),这个marker输入分水岭算法后,相当于规定了两个注水点(区域),分别是前景和背景两个注水区域,然后剩下的未知区域是利用分水岭的规则进行逐步的浸没,找到最终的分割边界。

    4.2 方法二:轮廓标签

    步骤:

    • 图像灰度化、滤波、Canny边缘检测
    • 查找轮廓,并且把轮廓信息按照不同的编号绘制到watershed的第二个入参merkers上,相当于标记注水点。
    • watershed分水岭运算
    • 绘制分割出来的区域,视觉控还可以使用随机颜色填充,或者跟原始图像融合以下,以得到更好的显示效果。

    4.2.1 基于Opencv实现

    下面只给出代码,详细请看:Opencv分水岭算法——watershed自动图像分割用法

    #include "opencv2/imgproc/imgproc.hpp"
    #include "opencv2/highgui/highgui.hpp"
    
    #include <iostream>
    
    using namespace cv;
    using namespace std;
    
    Vec3b RandomColor(int value);  //生成随机颜色函数
    
    int main()
    {
    	Mat image = imread("C:\\Users\\Changming Deng\\Desktop\\美女.png");    //载入RGB彩色图像
    	imshow("Source Image", image);
    
    	//灰度化,滤波,Canny边缘检测
    	Mat imageGray;
    	cvtColor(image, imageGray, COLOR_RGB2GRAY);//灰度转换
    	GaussianBlur(imageGray, imageGray, Size(5, 5), 2);   //高斯滤波
    	imshow("Gray Image", imageGray);
    	Canny(imageGray, imageGray, 80, 150);
    	imshow("Canny Image", imageGray);
    
    	//查找轮廓
    	vector<vector<Point>> contours;
    	vector<Vec4i> hierarchy;
    	findContours(imageGray, contours, hierarchy, RETR_TREE, CHAIN_APPROX_SIMPLE, Point());
    	Mat imageContours = Mat::zeros(image.size(), CV_8UC1);  //轮廓	
    	Mat marks(image.size(), CV_32S);   //Opencv分水岭第二个矩阵参数
    	marks = Scalar::all(0);
    	int index = 0;
    	int compCount = 0;
    	for (; index >= 0; index = hierarchy[index][0], compCount++)
    	{
    		//对marks进行标记,对不同区域的轮廓进行编号,相当于设置注水点,有多少轮廓,就有多少注水点
    		drawContours(marks, contours, index, Scalar::all(compCount + 1), 1, 8, hierarchy);
    		drawContours(imageContours, contours, index, Scalar(255), 1, 8, hierarchy);
    	}
    
    	//我们来看一下传入的矩阵marks里是什么东西
    	Mat marksShows;
    	convertScaleAbs(marks, marksShows);
    	imshow("marksShow", marksShows);
    	imshow("轮廓", imageContours);
    	watershed(image, marks);
    
    	//我们再来看一下分水岭算法之后的矩阵marks里是什么东西
    	Mat afterWatershed;
    	convertScaleAbs(marks, afterWatershed);
    	imshow("After Watershed", afterWatershed);
    
    	//对每一个区域进行颜色填充
    	Mat PerspectiveImage = Mat::zeros(image.size(), CV_8UC3);
    	for (int i = 0; i < marks.rows; i++)
    	{
    		for (int j = 0; j < marks.cols; j++)
    		{
    			int index = marks.at<int>(i, j);
    			if (marks.at<int>(i, j) == -1)
    			{
    				PerspectiveImage.at<Vec3b>(i, j) = Vec3b(255, 255, 255);
    			}
    			else
    			{
    				PerspectiveImage.at<Vec3b>(i, j) = RandomColor(index);
    			}
    		}
    	}
    	imshow("After ColorFill", PerspectiveImage);
    
    	//分割并填充颜色的结果跟原始图像融合
    	Mat wshed;
    	addWeighted(image, 0.4, PerspectiveImage, 0.6, 0, wshed);
    	imshow("AddWeighted Image", wshed);
    
    	waitKey();
    }
    
    Vec3b RandomColor(int value)
    {
    	value = value % 255;  //生成0~255的随机数
    	RNG rng;
    	int aa = rng.uniform(0, value);
    	int bb = rng.uniform(0, value);
    	int cc = rng.uniform(0, value);
    	return Vec3b(aa, bb, cc);
    }
    
    

    自己的思考:上面的代码中,marker由多个大于零且值不同的轮廓组成,其余区域未0,这个marker输入分水岭算法后,相当于规定了多个注水点(区域)(有多少个不同值的轮廓就有多少个注水区域),然后剩下的未知区域是利用分水岭的规则进行逐步的浸没,找到最终的分割边界。

    下面对车的图像、kitti图像和散斑结构光图像进行测试:
    在这里插入图片描述

    在这里插入图片描述

    • real-general

    在这里插入图片描述

    • real-indoor1
      在这里插入图片描述

    • real-indoor2
      在这里插入图片描述

    • real-indoor3
      在这里插入图片描述

    • real-outdoor
      在这里插入图片描述

    • synthetic-intel
      在这里插入图片描述

    • synthetic-ideal
      在这里插入图片描述

    • synthetic-polka

    在这里插入图片描述

    5. 图像分割之Mean shift算法

    Mean shift 算法是基于核密度估计的爬山算法,可用于聚类、图像分割、跟踪等。

    我们来谈谈Mean shift算法的基本思想。

    在这里插入图片描述
    在这里插入图片描述
    此外,从公式1中可以看到,只要是落入Sh的采样点,无论其离中心x的远近,对最终的Mh(x)计算的贡献是一样的。然而在现实跟踪过程中,当跟踪目标出现遮挡等影响时,由于外层的像素值容易受遮挡或背景的影响,所以目标模型中心附近的像素比靠外的像素更可靠。因此,对于所有采样点,每个样本点的重要性应该是不同的,离中心点越远,其权值应该越小。故引入核函数和权重系数来提高跟踪算法的鲁棒性并增加搜索跟踪能力。同理,对于图像分割,离中心点空间距离越近的点,都更可能是属于同一区域,因此图像分割也需要加入核函数。

    这个说的简单一点就是加入一个高斯权重,最后的漂移向量计算公式为:

    在这里插入图片描述

    因此每次更新的圆心坐标为:

    在这里插入图片描述

    接下来,谈谈核函数:

    在这里插入图片描述
    下面我们来介绍用Mean shift进行聚类的流程:

    1、在未被标记的数据点中随机选择一个点作为中心center;
    2、找出离center距离在bandwidth之内的所有点,记做集合M,认为这些点属于簇c。同时,把这些求内点属于这个类的概率加1,这个参数将用于最后步骤的分类
    3、以center为中心点,计算从center开始到集合M中每个元素的向量,将这些向量相加,得到向量shift。
    4、center = center+shift。即center沿着shift的方向移动,移动距离是||shift||。
    5、重复步骤2、3、4,直到shift的大小很小(就是迭代到收敛),记住此时的center。注意,这个迭代过程中遇到的点都应该归类到簇c。
    6、如果收敛时当前簇c的center与其它已经存在的簇c2中心的距离小于阈值,那么把c2和c合并。否则,把c作为新的聚类,增加1类。
    6、重复1、2、3、4、5直到所有的点都被标记访问。
    7、分类:根据每个类,对每个点的访问频率,取访问频率最大的那个类,作为当前点集的所属类。
    简单的说,mean shift就是沿着密度上升的方向寻找同属一个簇的数据点。

    5.1 基于Matlab的Mean shift聚类

    clc  
    close all;  
    clear  
    profile on  
    %生成随机数据点集  
    nPtsPerClust = 250;  
    nClust  = 3;  
    totalNumPts = nPtsPerClust*nClust;  
    m(:,1) = [1 1]';  
    m(:,2) = [-1 -1]';  
    m(:,3) = [1 -1]';  
    var = .6;  
    bandwidth = .75;  
    clustMed = [];  
    x = var*randn(2,nPtsPerClust*nClust);  
    for i = 1:nClust  
        x(:,1+(i-1)*nPtsPerClust:(i)*nPtsPerClust)       = x(:,1+(i-1)*nPtsPerClust:(i)*nPtsPerClust) + repmat(m(:,i),1,nPtsPerClust);     
    end  
    data=x';  
    % plot(data(:,1),data(:,2),'.')  
    
    %mean shift 算法  
    [m,n]=size(data);  
    index=1:m;  
    radius=0.75;  
    stopthresh=1e-3*radius;  
    visitflag=zeros(m,1);%标记是否被访问  
    count=[];  
    clustern=0;  
    clustercenter=[];  
      
    hold on;  
    while ~isempty(index)  
        cn=ceil((length(index)-1e-6)*rand);%随机选择一个未被标记的点,作为圆心,进行均值漂移迭代  
        center=data(index(cn),:);  
        this_class=zeros(m,1);%统计漂移过程中,每个点的访问频率  
          
          
        %步骤2345  
        while 1  
            %计算球半径内的点集  
            dis=sum((repmat(center,m,1)-data).^2,2);  
            radius2=radius*radius;  
            innerS=find(dis<radius*radius);  
            visitflag(innerS)=1;%在均值漂移过程中,记录已经被访问过得点  
            this_class(innerS)=this_class(innerS)+1;  
            %根据漂移公式,计算新的圆心位置  
            newcenter=zeros(1,2);  
           % newcenter= mean(data(innerS,:),1);   
            sumweight=0;  
            for i=1:length(innerS)  
                w=exp(dis(innerS(i))/(radius*radius));  
                sumweight=w+sumweight;  
                newcenter=newcenter+w*data(innerS(i),:);  
            end  
            newcenter=newcenter./sumweight;  
      
            if norm(newcenter-center) <stopthresh%计算漂移距离,如果漂移距离小于阈值,那么停止漂移  
                break;  
            end  
            center=newcenter;  
            plot(center(1),center(2),'*y');  
        end  
        %步骤6 判断是否需要合并,如果不需要则增加聚类个数1个  
        mergewith=0;  
        for i=1:clustern  
            betw=norm(center-clustercenter(i,:));  
            if betw<radius/2  
                mergewith=i;   
                break;  
            end  
        end  
        if mergewith==0           %不需要合并  
            clustern=clustern+1;  
            clustercenter(clustern,:)=center;  
            count(:,clustern)=this_class;  
        else                    %合并  
            clustercenter(mergewith,:)=0.5*(clustercenter(mergewith,:)+center);  
            count(:,mergewith)=count(:,mergewith)+this_class;    
        end  
        %重新统计未被访问过的点  
        index=find(visitflag==0);  
    end%结束所有数据点访问
    
    %绘制分类结果  
    for i=1:m  
        [value,index]=max(count(i,:));  
        Idx(i)=index;  
    end  
    figure(2);  
    hold on;  
    for i=1:m  
        if Idx(i)==1  
            plot(data(i,1),data(i,2),'.y');  
        elseif Idx(i)==2
             plot(data(i,1),data(i,2),'.b');  
        elseif Idx(i)==3 
             plot(data(i,1),data(i,2),'.r');  
        elseif Idx(i)==4
             plot(data(i,1),data(i,2),'.k');  
        elseif Idx(i)==5 
             plot(data(i,1),data(i,2),'.g');  
        end  
    end  
    cVec = 'bgrcmykbgrcmykbgrcmykbgrcmyk';  
    for k = 1:clustern  
        plot(clustercenter(k,1),clustercenter(k,2),'o','MarkerEdgeColor','k','MarkerFaceColor',cVec(k), 'MarkerSize',10)  
    end  
      
    

    结果:

    在这里插入图片描述

    5.2 基于Opecv的Mean shift图像分割

    一般而言一副图像的特征点至少可以提取出5维,即(x,y,r,g,b),众所周知,meanshift经常用来寻找模态点,即密度最大的点。所以这里同样可以用它来寻找这5维空间的模态点,由于不同的点最终会收敛到不同的峰值,所以这些点就形成了一类,这样就完成了图像分割的目的,有点聚类的意思在里面。

    有一点需要注意的是图像像素的变化范围和坐标的变化范围是不同的,所以我们在使用窗口对这些数据点进行模态检测时,需要使用不同的窗口半径。

    在opencv自带的meanshift分割函数pyrMeanShiftFiltering()函数中,就专门有2个参数供选择空间搜索窗口半径和颜色窗口搜索半径的。

    OpenCV提供的API:

    下面参考:

    void pyrMeanShiftFiltering( InputArray src, OutputArray dst,
                                             double sp, double sr, int maxLevel=1,
                                             TermCriteria termcrit=TermCriteria(
                                                TermCriteria::MAX_ITER+TermCriteria::EPS,5,1) );
    

    参数:

    • src,输入图像,8位,三通道的彩色图像,并不要求必须是RGB格式,HSV、YUV等Opencv中的彩色图像格式均可;

    • dst,输出图像,跟输入src有同样的大小和数据格式;

    • sp,定义的漂移物理空间半径大小;

    • sr,定义的漂移色彩空间半径大小;

    • maxLevel,定义金字塔的最大层数;

    • termcrit,定义的漂移迭代终止条件,可以设置为迭代次数满足终止,迭代目标与中心点偏差满足终止,或者两者的结合。

    在进行pyrMeanShiftFiltering滤波后,原图像的灰度级变成了中心点的数量,这就是使用了mean-shift算法滤波的结果,但是我们要的是分割图,因此需要借助另外一个漫水填充函数的进一步处理来实现,那就是floodFill:

    int floodFill( InputOutputArray image, InputOutputArray mask,
                                Point seedPoint, Scalar newVal, CV_OUT Rect* rect=0,
                                Scalar loDiff=Scalar(), Scalar upDiff=Scalar(),
                                int flags=4 );
    

    参数:

    • image,输入三通道8bit彩色图像,同时作为输出。

    • mask,是掩模图像,它的大小是输入图像的长宽左右各加1个像素,mask一方面作为输入的掩模图像,另一方面也会在填充的过程中不断被更新。floodFill漫水填充的过程并不会填充mask上灰度值不为0的像素点,所以可以使用一个图像边缘检测的输出作为mask,这样填充就不会填充或越过边缘轮廓。mask在填充的过程中被更新的过程是这样的:每当一个原始图上一个点位(x,y)被填充之后,该点位置对应的mask上的点(x+1,y+1)的灰度值随机被设置为1(原本该点的灰度值为0),代表该点已经被填充处理过。

    • seedPoint,是漫水填充的起始种子点。

    • newVal,被充填的色彩值。

    • rect,可选的参数,用于设置floodFill函数将要重绘区域的最小矩形区域;

    • loDiff和upDiff,用于定义跟种子点相比色彩的下限值和上限值,介于种子点减去loDiff和种子点加上upDiff的值会被填充为跟种子点同样的颜色;

    • flags定义漫水填充的模式,用于连通性、扩展方向等的定义。

    #include "opencv2/highgui/highgui.hpp"
    #include "opencv2/core/core.hpp"
    #include "opencv2/imgproc/imgproc.hpp"
    
    using namespace cv;
    
    int main()
    {
    	Mat img = imread("D:\\code\\C++\\图像分割\\image segmentation\\test_pic\\mean_shift\\草原.png"); //读入图像,RGB三通道  
    	imshow("原图像", img);
    	Mat res; //分割后图像
    	int spatialRad = 30;  //空间窗口大小
    	int colorRad = 30;   //色彩窗口大小
    	int maxPyrLevel = 2;  //金字塔层数
    	pyrMeanShiftFiltering(img, res, spatialRad, colorRad, maxPyrLevel); //色彩聚类平滑滤波
    	imshow("res", res);
    	RNG rng = theRNG();
    	Mat mask(res.rows + 2, res.cols + 2, CV_8UC1, Scalar::all(0));  //掩模
    	for (int y = 0; y < res.rows; y++)
    	{
    		for (int x = 0; x < res.cols; x++)
    		{
    			if (mask.at<uchar>(y + 1, x + 1) == 0)  //非0处即为1,表示已经经过填充,不再处理
    			{
    				Scalar newVal(rng(256), rng(256), rng(256));
    				floodFill(res, mask, Point(x, y), newVal, 0, Scalar::all(5), Scalar::all(5)); //执行漫水填充
    			}
    		}
    	}
    	imshow("meanShift图像分割", res);
    	waitKey();
    	return 0;
    }
    

    结果:

    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述

    下面将空间窗口和颜色窗口都改为20,给出对车的图像,kitti图像和散斑图像的分割结果:

    在这里插入图片描述

    在这里插入图片描述

    • real-general

    在这里插入图片描述

    • real-indoor1
      在这里插入图片描述

    • real-indoor2
      在这里插入图片描述

    • real-indoor3
      在这里插入图片描述

    • real-outdoor
      在这里插入图片描述

    • synthetic-intel
      在这里插入图片描述

    • synthetic-ideal
      在这里插入图片描述

    • synthetic-polka
      在这里插入图片描述

    mean-shift算法可以重复使用,也就是分割完再分割,还能对分割图应用canny算子再次提取边缘。当然,这样是很耗时的。

    展开全文
  • 基于SVM的图像分割-真彩色图像分割,给出了真彩色图像matlab分割的实例
  • 数字图像处理:实验六 图像分割

    千次阅读 2022-05-03 16:12:27
    实验六 图像分割 数据分割是由图像处理到图像分析的关键步骤,是图像识别和计算机视觉至关重要的预处理,图像分割后提取的目标可用于图像识别、特征提取,图像搜索等领域。图像分割的基本策略主要是基于图像灰度值的...

    实验六 图像分割

    数据分割是由图像处理到图像分析的关键步骤,是图像识别和计算机视觉至关重要的预处理,图像分割后提取的目标可用于图像识别、特征提取,图像搜索等领域。图像分割的基本策略主要是基于图像灰度值的两个特性,即灰度的不连续性和灰度的相似性,因此图像分割方法可分为基于边缘的分割方法和基于区域的分割方法。本次实验通过完成简单的图像分割进一步加深对图像分割的理解。

    一、实验目的

    进一步掌握MATLAB程序设计方法;
    巩固图像分割理论与方法;
    编程实现图像分割,比较基于阈值分割方法和基于边缘分割方法的效果,分析影响分割效果的因素。

    二、实验基础

    1.基于阈值的分割
    基于阈值的分割属于区域分割的一种,是图像分割中最常用的一类方法,对于一个具有双峰分布的简单图像阈值分割方法,实际上是将输入图像f(i,j)通过变换得到输出图像g(i,j)的过程,即二值化过程。
    阈值分割方法的关键是阈值T的确定,如果能确定一个合适的阈值,就可以准确地将图像分割开来,对于灰度直方图具有双峰分布的图像。可以选择古典处的灰度值作为阈值,在许多情况下物体和背景的对比度在图像中的各处是不同的,很难用一个统一的阈值将物体与背景分开。此时可以根据图像的局部特征分别采用不同的阈值进行分割。并分割的方法包括判断分析法,最佳熵自动阈值法,最小误差分割等。
    阈值分割的优点是直观计算简单,运算效率较高,速度快。
    
    2.基于边缘的分割
    图像分割的另一种方式是通过边缘检测时限的边缘是指通过中像素灰度有阶跃或屋顶变化的像素级和边缘能勾画出物体轮廓,对图像识别和分析十分有用,是图像识别时提取的重要信息,由于边缘处的灰度是不连续的,因此可以利用边缘检测算子将边缘点检测出来,常用的边缘检测算子有梯度算子、Prewitt算子、Sobel算子、LOG算子和Canny算子等。
    通过edge函数可以利用各种边缘检测算子检测图像边缘。
    
    3.边缘跟踪
    将检测的边缘点连接成线称为边缘跟踪,在识别图像中的目标时,往往需要对目标边缘做跟踪处理,即按一定顺序找出边缘点来绘制出编辑,如果图像是二值图像或图像中不同区域具有不同的像素值,但每个区域内的像素只是相同的,则可以完成基于四联通或八连通区域的轮廓跟踪。
    利用bwtraceboundary函数可以在二值图像中追踪目标的轮廓线。
    
    4.Hough变换检测
    Hough变换是Paul Hough于1962年提出,是图像处理技术中用于识别几何形状的一种常用方法,它实现了一种从图像空间到参数空间的映射关系。Hough变换的基本原理是利用点线间的对偶性,将原始图像空间的给定曲线通过曲线表达形式变换成为参数空间的一个点。
    MATLAB中常用的关于Hough变换的函数有:hough、houghpeaks和houghline。
    

    三、实验仪器及设备

    计算机、Matlab图像处理软件、待处理图像

    四、实验内容及步骤

    1. 熟悉MATLAB中相关函数,掌握各函数的具体用法
    (1)edge:边缘检测函数。
    BW = edge(I)
    采用灰度或一个二值化图像I作为它的输入,并返回一个与I相同大小的二值化图像BW,在函数检测到边缘的地方为1,其他地方为0。
    BW = edge(I,'sobel') 自动选择阈值用Sobel算子进行边缘检测。
    BW = edge(I,'sobel',thresh) 根据所指定的敏感度阈值thresh,用Sobel算子进行边缘检测,它忽略了所有小于阈值的边缘。当thresh为空时,自动选择阈值。
    BW = edge(I,'sobel',thresh,direction) 根据所指定的敏感度阈值thresh,在所指定的方向direction上,用Sobel 算子进行边缘检测。Direction可取的字符串值为horizontal(水平方向)、vertical(垂直方向)或both(两个方向)。
    [BW,thresh] = edge(I,'sobel',...) 返回阈值
    (2)graythresh:利用Otsu算法(最大类间方差)获取全局阈值。
    (3)im2bw:将灰度影像转换为二值图像。
    (4)bwtraceboundary:在二值图像中追踪目标轮廓线。
    

    help bwtraceboundary
    bwtraceboundary Trace object in binary image.
    B = bwtraceboundary(BW,P,FSTEP) traces the outline of an object in a binary image BW, in which nonzero pixels belong to an object and 0-pixels constitute the background. P is a two-element vector specifying the row and column coordinates of the initial point on the object boundary. FSTEP is a string or char vector specifying the initial search direction for the next object pixel connected to P. FSTEP can be any of the following strings or char vectors: ‘N’,‘NE’,‘E’,‘SE’,‘S’,‘SW’,‘W’,‘NW’, where N stands for north, NE stands for northeast, etc. B is a Q-by-2 matrix, where Q is the number of boundary pixels for the region. B holds the row and column coordinates of the boundary pixels.
    B = bwtraceboundary(BW,P,FSTEP,CONN) specifies the connectivity to use when tracing the boundary. CONN may be either 8 or 4. The default value for CONN is 8. For CONN equal to 4, FSTEP is limited to ‘N’,‘E’,‘S’ and ‘W’.
    B = bwtraceboundary(…,N,DIR) provides the option to specify the maximum number of boundary pixels, N, to extract and direction, DIR, in which to trace the boundary. DIR can be either ‘clockwise’ or ‘counterclockwise’. By default, or when N is set to Inf, the algorithmextracts all of the pixels from the boundary and, if DIR is not specified, it searches in the clockwise direction.

    (5)hough:Hough变换函数。
    1.hought函数
    语法:[H, theta, rho] = hough(BW)
          [H, theta, rho] = hough(BW, ParameterName, ParameterValue)
    描述:[H, theta, rho] = hough(BW)计算二值图像BW的标准hough变换。函数返回的H称为hough转换矩阵。theta和rho是一维数组,theta记录径,rho记录角度。BW可以是逻辑型,也可以是数值型。
    (6)houghpeaks:Hough变换峰值识别。
    peaks = houghpeaks(H, NumPeaks) 
    peaks = houghpeaks(..., 'Threshold', val1, 'NHoodSize', val2) 
    其中,"…"指出来自默认语法和peaks的输入是持有峰值行和列坐标的Q×2大小的矩阵。Q的范围是0到NumPeaks,H是霍夫变换矩阵。参数val1是非负的标量,指定了H中的什么值被考虑为峰值;val1可以从0到Inf变化,默认值是0.5*max(H(:))。参数val2是奇整数的两元素矢量,指定量围绕峰值的邻域大小。
    (7)houghline:基于Hough变换提取线元。
    lines = houghlines(f, theta, rho, peaks) 
    lines = houghlines(..., 'FillGap', val1, 'MinLength', val2)
    其中,theta和rho是来自函数hough的输出,peaks是函数houghpeaks的输出。输出lines是结构数组(可能检测到多条直线),长度等于找到的线段数。结构中的每个元素可以看成一条线,并含有下列字段:
    point1:两元素向量[r1, c1],指定了线段起点的行列坐标。
    point2:两元素向量[r2, c2],指定了线段终点的行列坐标。
    theta:与线相关的霍夫变换的以度计量的角度。
    rho:与线相关的霍夫变换的ρ轴位置。
    其他参数如下:
    val1是正的标量,指定了与相同的霍夫变换相关的两条线段的距离。当两条线段之间的距离小于指定的值时,函数houghlines把线段合并为一条线段(默认的距离是20个像素)。参数val2是正的标量,指定合并的线是保留还是丢弃。如果合并的线比val2指定的值短,就丢弃(默认值是40)。
    
    1. 简单图像的阈值分割
      (1)利用直方图进行阈值分割
      在这里插入图片描述
      在这里插入图片描述

    图一 阈值分割
    (2)利用最大类间方差自动确定阈值进行分割
    在这里插入图片描述
    在这里插入图片描述

    图二 最大类间方差
    3. 基于边缘的阈值分割
    (1)利用各种边缘检测算子检测边缘
    在这里插入图片描述
    在这里插入图片描述

    图三 边缘算子
    思考:比较不同

    Roberts算子检测方法对具有陡峭的低噪声的图像处理效果较好,但是利用roberts算子提取边缘的结果是边缘比较粗,因此边缘的定位不是很准确。
    
    Sobel算子检测方法对灰度渐变和噪声较多的图像处理效果较好,sobel算子对边缘定位不是很准确,图像的边缘不止一个像素。
    
    Prewitt算子检测方法对灰度渐变和噪声较多的图像处理效果较好。但边缘较宽,而且间断点多。
    
    Laplacian算子法对噪声比较敏感,所以很少用该算子检测边缘,而是用来判断边缘像素视为与图像的明区还是暗区。
    
    Canny方法不容易受噪声干扰,能够检测到真正的弱边缘。优点在于,使用两种不同的阈值分别检测强边缘和弱边缘,并且当弱边缘和强边缘相连时,才将弱边缘包含在输出图像中。
    

    (2)边缘跟踪(bwtraceboundary)函数
    在这里插入图片描述
    在这里插入图片描述

    图四 边缘跟踪

    思考:通过修改起始点的横纵坐标,可以改变找到的硬币边缘。
    
    1. Hough变换检测线段
    clc;clear;
    i=imread('circuit.tif');
    i1=imrotate(i,30,'crop');%用crop方式显示,旋转后的图像跟原图像大小一样,多余的地方舍去
    bw=edge(i1,'canny');
    subplot(2,3,1);imshow(i);xlabel('原始图像');
    subplot(2,3,2);imshow(i1);xlabel('旋转后的图像');
    subplot(2,3,3);imshow(bw);xlabel('边缘检测图像');
    [H,theta,rho] = hough(bw);%利用hough函数进行hough变换
    subplot(2,3,4);hold on;imshow(imadjust(mat2gray(H)),[],'XData',theta,'YData',rho,'Initialmagnification','fit');
    title('峰值检测');
    xlabel('\theta(degrees)');ylabel('\rho');
    axis on,axis normal,hold on
    P=houghpeaks(H,10,'threshold',ceil(0.3*max(H(:))));
    x=theta(P(:,2));
    y=rho(P(:,1));
    plot(x,y,'s','color','red');
    lines=houghlines(bw,theta,rho,P,'FillGap',5,'MinLength',7);
    subplot(2,3,5),imshow(i1);title('检测到的线段');hold on;
    max_len=0;
    for k=1:length(lines)
        xy=[lines(k).point1;lines(k).point2];
        plot(xy(:,1),xy(:,2),'LineWidth',2,'color','green');
        %绘制线段起终点
        plot(xy(1,1),xy(1,2),'LineWidth',2,'color','yellow');
        plot(xy(2,1),xy(2,2),'LineWidth',2,'color','blue');
        %确定最长线段的端点
        len=norm(lines(k).point1-lines(k).point2);
        if(len>max_len)
            max_len=len;
            xy_long=xy;
        end
    end
    %突出显示最长一条直线
    plot(xy_long(:,1),xy_long(:,2),'LineWidth',2,'color','red');
    

    在这里插入图片描述

    图五 hough变换

    五、实验心得体会

    1. 图像分割是图像处理的重要步骤之一,在图像分割过程中算法是其主要的手法之一,但是每种算法有每种算法的特点,它的运行成果也行差万别。
    2. 实验中算法的编写总是出现各种各样的错误,经过细心的检查和耐心的改正错误基本都会改正错误。类型大致可分为:语法错误,函数调用,错误函数无法调用等类型。
    展开全文
  • 图像分割的matlab程序 图像分割的matlab程序 图像分割的matlab程序
  • 图像分割 图像增长 边缘检测 医学图像分割 阈值分割

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 214,064
精华内容 85,625
关键字:

图像分割

友情链接: P11.zip