精华内容
下载资源
问答
  • 如何判断图像分割好
    千次阅读
    2022-01-18 20:45:30

    1 图像分割定义和方法分类

    图像分割就是指把图像分成各具特性的区域并提取出感兴趣目标的技术和过程。这里特性可以是灰度、颜色、纹理等,目标可以对应单个区域,也可以对应多个区域。图像分割多年来一直得到人们的高度重视,至今已提出了上千种各种类型的分割算法,而且近年来每年都有上百篇有关研究报道发表。

    首先,对灰度图像的分割常可基于像素灰度值的2个性质:不连续性和相似性。区域内部的像素一般具有灰度相似性,而在区域之问的边界上般具有灰度不连续性。所以分割算法可据此分为利用区域间灰度不连续性的基于边界的算法和利用区域内灰度相似性的基于区域的算法。其次,根据分割过程中处理策略的不同,分割算法又可分为并行算法和串行算法。在并行算法中,所有判断和决定都可独立地和同时地做出,而在串行算法中,早期处理的结果可被其后的处理过橾所利用。一般串行算法所需计算时间常比并行算法要长,但抗噪声能力也常较强。分割算法可分成4类:①并行边界类;②串行边界类;③并行区域类;④串行区域类。

    2 边缘检测

    边缘检测是所有基于边界的分割方法的第一步。两个具有不同灰度值的相邻区域之总存在边缘。边缘是灰度值不连续的结果,这种不连续常可利用求导数方便地检测到般常用一阶和二阶导数来检测边缘。

    对图像中边缘的检测可借助空域微分算子通过卷积完成。实际上数字图像中求导数是利用差分近似微分来进行的。下面介绍几种简单的空域微分算子。

    (1)梯度算子,梯度对应一阶导数,梯度算子是一阶导数算子,比如Roberts算子,Prewitt算子,Sobel算子。

    (2)拉普拉斯算子,

    拉普拉斯( Laplacian)算子是1种二阶导数算子, 在数字图像中,对拉普拉斯值的计算也可借助各种模板实现。这里对模板的基本要求是对应中心像素的系数应是正的,而对应中心像素邻近像素的系数应是负的,且它们的和应该是零。拉普拉斯算子是1种二阶导数算子,所以对图像中的噪声相当敏感。另外它常产生双像素宽的边缘,精度较低,且也不能提供边缘方向的信息。由于以上原因,拉普拉斯算子主要用于已知边缘像素后确定该像素是在图像的暗区或明区一边。

     

    3 轮廓跟踪和图搜索

    实际图像分割中,在检测出边缘后还需将它们连接起来构成闭合的轮廓。由于用差分算子并行地检测边缘对噪声比较敏感,所以实际中常采用先检测可能的边缘点再串行跟连接成闭合轮廓的方法。由于串行方法可以在跟踪过程中充分利用先前获取的信息,常可取得较好的效果。另外,也可采用将边缘检测和轮廓跟踪互相结合顺序进行的方法。

    轮廓跟踪( boundary tracking)也称边缘点链接( edge point linking),是由(梯度图)一个边缘点出发,依次搜索并连接相邻边缘点从而逐步检测出轮廓的方法。为了消除噪声的影响,保持轮廓的光滑性(也可称刚性, rigidness),在搜索时每确定一个新的轮廊点都要考虑先前已得到的轮廓点。而为了克服噪声造成的边缘点之间的不连通,对梯度廊点都要考虑先前已得到的轮廓点。而为了克服噪声造成的边缘点之间的不连通,对梯度图要充分保持其已有的信息(常不能再取阈值去除低梯度像素)。一般来说轮廓跟踪包括三个步骤:

    (1)确定作为搜索起点的边缘点(根据算法不同,可以是一个点或多个点),起点的选择很重要,整个算法对此的依赖很大;

    (2)确定和采取一种合适的数据结构和搜索机理,在已发现的轮廓点基础上确定新的轮廓点,这里要注意研究先前的结果对选择下一个检测像素和下个结果的影响;

    (3)确定搜索终结的准则或终止条件(如封闭轮廓则回到起点),并在满足条件时停止进程,结束搜索。

    另一种比较复杂,计算量也较大,但在图像受噪声影响较大讨效果仍可比较好的方法是图搜索法。它借助状态空间搜索来寻求全局最优的轮廓。具体是将轮廓点和轮廓段用图( graph)结构表示,通过在图中进行搜索对应最小代价的通道来寻找闭合轮廓。

    4 阈值分割

    取阈值是最常见的并行的直接检测区域的分割方法,其他同类方法如像素特征空间分类可看做是取阈值技术的推广。假设图像由具有单峰灰度分布的目标和背景组成,在目标或背景内部的相邻像素间的灰度值是高度相关的,但在目标和背景交界处两边的像素在灰度值上有很大的差别。如果1幅图像满足这些条件,它的灰度直方图基本上可看做是由分别对应目标和背景的2个单峰直方图混合而成。此时如果这2个分布大小(数量)接近且均值相距足够远,而且均方差也足够小,则直方图应是双峰的。对这类图像常可用取阈值方法来较好地分割。

    5 基于变换直方图选取阈值

    在实际应用图像常受到噪声等的影响而使直方图中原本分离的峰之间的谷被填充,使得谷的检测很困难。为解决这类问题可以利用一些像素邻域的局部性质。比如直方图变换,灰度-梯度散射图。

    6 空间聚类

    利用特征空间聚类的方法进行图像分割可看做是对阈值分割概念的推广。它将图像空间中的元素按照从它们测得的特征值用对应的特征空间点表示,通过将特征空间的点聚集成对应不同区域的类团,然后再将它们划分开,并映射回原图像空间以得到分割的结果。

    在利用直方图的阈值分割中,取像素灰度为特征,用灰度直方图作为特征空间,对特征空间的划分利用灰度阙值进行。在利用灰度-梯度散射图分割的方法中,取像灰度和梯度为特征,用散射图作为特征空间,对特征空间的划分利用灰度阈值和梯度阈值进行。与取阈值分割类似,聚类方法也是一种全局的方法,比仅基于边缘检测的方法更抗噪声。但特征空间的聚类有时也常会导致产生图像空间不连通的分割区域,这也是因为没有利用图像像素空间分布的信息。

    聚类的方法很多,下面介绍几种常用的聚类方法。

    (1)k均值聚类

    将一个特征空间分成K个聚类的一种常用方法是K-均值法。K均值聚类具体算法不再详述。运用 K-均值法时理论上并未设类的数目已知,实际中常使用试探法来确定 K。为此需要测定聚类品质(quality),常用的判别准则多基于分割后类内和类间特征值的散布图,要求类内接近而类间区别大。可以先采用不同的 K 值进行聚类,根据聚类品质确定最后的类别数。

    (2)ISODATA聚类

    ISODATA聚类方法是在K-均值算法上发展起来的。它是一种非分层的聚类方法,其主要步骤如下:

    (1)设定N个聚类中心位置的初始值

    (2)对每个特征点求取离其最近的聚类中心位置,通过赋值把特征空间分成N个区域;

    (3)分别计算属于各聚类模式的平均值;

    (4)将最初的聚类中心位置与新的平均值比较,如果相问则停止;如果不同,则返回

    步骤(2)继续进行。

    理论上讲 ISODATA算法也需要预先知道聚类的数目,但实际中常根据经验先取稍大点的值,然后通过合并距离较近的聚类以得到最后的聚类数目。

    7 区域生长分割算法

    串行区域分割技术指采用串行处理的策略通过对目标区域的直接检测来实现图像分割的技术。串行分割方法的特点是将整个处理过程分解为顺序的多个步骤逐次进行,其中对后续步骤的处理要根据对前面已完成步骤的处理结果进行判断而确定。这里判断是要据一定的准则来进行的。一般说来如果准则是基于图像灰度特性的,则该方法可用于灰度图像的分割,如果准则是基于图像的其他特性(如纹理)的,则该方法也可用于相应图像的分割。

    串行区域分割中常利用图像多分辨率的表达结构,如金字塔结构。基于区域的串行分割技术有两种基本形式,一种是从单个像素出发,逐渐合并以形成所需的分割区域,称为区域生长。另一种是从全图出发,逐渐分裂切割至所需的分割区域,这两种方法可以结合使用。

    区域生长的基本思想是将具有相似性质的像素结合起来构成区域。具体先对每个需要分割的区域找一个种子像素作为生长的起点,然后将种子像素周围邻域中与种子像素有相同或相似性质的像素(根据某种事先确定的生长或相似准则来判定)合并到种子像素所在的区域中。将这些新像素当做新的种子像素继续进行上面的过程,直到再没有满足条件的像素可被包括进来。这样一个区域就长成了。由此可知,在实际应用区域生长法时需要解决3个问题:

    (1)选择或确定一组能正确代表所需区域的种子像素;

    (2)确定在生长过程中能将相邻像素包括进来的准则;

    (3)制定让生长过程停止的条件或规则。

    种子像素的选取常可借助具体问题的特点进行。如在军用红外图像中检测目标时由于一般情况下目标辐射较大,所以可选用图中最亮的像素作为种子像素。要是对具体问题没有先验知识,则常可借助生长所用准则对每个像素进行相应计算。如果计算结果呈现聚类的情况则接近聚类重心的像素可取为种子像素。

    区域生长的一个关键是选择合适的生长或相似准则,生长准则的选取不仅依赖于具体问题本身,也和所用图像数据的种类有关。生长准则可根据不同原则制定,而使用不同的生长准则会影响区域生长的过程。基于区域灰度差的方法主要有如下步骤:

    (1)对图像进行逐行扫描,找出尚没有归属的像素;

    (2)以该像素为中心检査它的邻域像素,即将邻域中的像素逐个与它比较,如果灰度差小于预先确定的阈值,将它们合并;

    (3)以新合并的像素为中心,返回到步骤(2),检査新像素的邻域,直到区域不能进步扩张;

    (4)返回到步骤(1),继续扫描直到不能发现没有归属的像素,则结束整个生长过程。

    8 彩色图像分割

    对颜色的感受是人类对电磁辐射中可见部分里不同频率知觉的体现。随着技术的进步,彩色图像使用得越来越多,彩色图像的分割在最近几年也越来越引起人们的重视。

    在许多实际应用中,可对彩色图像的各个分量进行适当的组合转化为灰度图像,然后可用对灰度图像的分割算法进行分割。下面仅考虑专门用于彩色图像分割的方法。要分割一幅彩色图像,首先要选好合适的彩色空间;其次要采用适合于此空间的分割策略和方法。

    (1)分割所用的彩色空间

    表达颜色的彩色空间有许多种,它们常是根据不同的应用目的提出的。下面围绕图像分割,介绍几种常用的彩色空间和它们的特点。

    最常见的彩色空间是红绿蓝( red green blue,RGB)空间,它是一种矩形直角空间结构的模型,是通过对颜色进行加运算完成颜色综合的彩色系统的基础。它用R、G、B个基本分量的值来表示颜色(三分量之间常有很高的相关性),它是面向硬件设备的(如CRT),物理意义明确但缺乏直感。

    通过对不同类型图像的分析,有人经过大量试验提出可用由R、G、B经过线性变换得到的3个正交彩色特征。

    另一种常用的彩色空间是HSI( hue saturation intensity)空问。HSI彩色空间的表示比较接近人眼的视觉生理特性,人眼对H、S、I变化的区分能力要比对R、G、B变化的区分能力强。另外在HSI空间中彩色图像的每一个均匀性彩色区域都对应一个相对一致的色调(H),这说明色调能够被用来进行独立于阴影的彩色区域的分割。

    (2)分割策略

    测量空间聚类法是分割彩色图像常用的方法。彩色图像在各个空间均可看做由3个分量构成,所以分割彩色图像的1种方法是建立一个“3-D直方图”,它可用一个3-D数组表示。这个3-D数组中的每个元素代表图像中具有给定3个分量值的像素的个数。阈值分割的概念可以扩展为在3-D空间搜索像素的聚类,并根据聚类来分割图像。

    彩色图像可以分步分割。有一种分两步的彩色图像分割算法,第一步借助取阈值方法进行粗略分割将图像转化为若干个区域,第一步利用(模糊)均值聚类法将第一步剩下的像素进一步分类。这种方法可看做是由粗到细进行的,先用取阈值方法是为了减少运用模糊均值聚类所需的计算量。

    更多相关内容
  • 图像分割】医学图像分割入门实践(附源码)

    万次阅读 多人点赞 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张图进行训练,但从结果可以看到,模型也能大致分割出目标。
    在这里插入图片描述

    展开全文
  • 传统图像分割方法详解

    千次阅读 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算子再次提取边缘。当然,这样是很耗时的。

    展开全文
  • Otsu图像分割

    千次阅读 2022-04-02 21:04:51
    opencv自带Otsu算法,只需要在分割时将参数选择为“cv2.THRESH_OTSU”即可 #coding:utf-8 import cv2 import numpy as np from matplotlib import pyplot as plt image = cv2.imread('E:/shale10053.bmp') ...

    opencv自带Otsu算法,只需要在分割时将参数选择为“cv2.THRESH_OTSU”即可

    #coding:utf-8
    import cv2
    import numpy as np
    from matplotlib import pyplot as plt
     
    image = cv2.imread('E:/shale10053.bmp')
    grayimage = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
    gray = cv2.GaussianBlur(grayimage, (3,3), 0)
    
    plt.figure(figsize=(5,5))
    plt.subplot(), plt.imshow(image, "gray")
    plt.title("source image"), plt.xticks([]), plt.yticks([])
    
    
    ret1, th1 = cv2.threshold(gray, 0, 255, cv2.THRESH_OTSU)  #方法选择为THRESH_OTSU
    
    plt.figure(figsize=(5,5))
    plt.subplot(), plt.imshow(th1, "gray")
    plt.title("Otsu,threshold is " + str(ret1)), plt.xticks([]), plt.yticks([])
    
    plt.figure(figsize=(12,5))
    plt.subplot() 
    plt.title("Histogram")
    plt.hist(image.ravel(),255,color='red',alpha=1,rwidth=0.3)

     

     

     

    展开全文
  • 基于神经网络的图像分割

    千次阅读 2022-02-21 08:46:19
    一、图像分割简介与分类 ????图像分割:把图像分成若干个特定的、具有独特性质的区域并提出感兴趣目标。简单来说就是把图像中的对象分割出来 ????常用方法:基于阈值的分割方法;基于区域的分割方法;基于边缘的...
  • 数字图像处理:实验六 图像分割

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

    万次阅读 多人点赞 2019-07-09 22:03:48
    图像分割是计算机视觉研究中的一个经典难题,已经成为图像理解领域关注的一个热点,图像分割是图像分析的第一步,是计算机视觉的基础,是图像理解的重要组成部分,同时也是图像处理中最困难的问题之一。所谓图像分割...
  • 图像分割算法的分析和评价主要分为两个方面:视觉效果和性能指标。前者主要是对图像的视觉效果和直观感受做出评价,而后者则是利用严格的数学公式,计算出算法的数值化指标。这里总结出10种常见的图像分割评价指标:...
  • 图像分割—基于区域的图像分割

    万次阅读 多人点赞 2019-08-17 10:40:40
    基于区域的分割是以直接寻找区域为基础的分割技术,实际上类似基于边界的图像分割技术一样利用了对象与背景灰度分布的相似性。大体上基于区域的图像分割方法可以分为两大类:区域生长法区域分裂与合并1 区域生长法...
  • 图像分割总结

    千次阅读 2022-03-08 16:48:42
    这篇总结主要以梳理deeplab系列为主,除此之外图像分割领域还有很多的东西,推荐了一篇github上的总结 啥是图像分割 AI的图像领域工作,大致可以分为这么几个方向,图像分类、目标检测、图像分割,而图像分割又分为...
  • 常见图像分割算法实现源代码

    热门讨论 2014-09-30 16:31:01
    常见图像分割算法实现源代码,对于在选择合适的分割算法,可以做分析对比,减少开发时间,包括边界分割,阈值分割,区域分割等
  • 图像分割汇总

    千次阅读 2022-01-26 20:14:39
    Image Segmentation(图像分割):所谓图像分割是指根据灰度、彩色、空间纹理、几何形状等特征把图像划分成若干个互不相交的区域,使得这些特征在同一区域内表现出一致性或相似性,而在不同区域间表现出明显的不同。...
  • 【数字图像处理】实验三 图像分割(MATLAB实现)

    千次阅读 多人点赞 2022-05-03 14:05:39
    目录 一、实验意义及目的 二、实验内容 ...(4)自行设计方法实现图像分割,并计算分割区域相关参数 一、实验意义及目的 (1)进一步掌握图像处理工具 Matlab,熟悉基于 Matlab 的图像处理函.
  • 数字图像处理-图像分割-复习总结

    千次阅读 2021-12-14 19:15:08
    文章目录数学图像处理图像分割图像分割基础基于边界的图像分割(非连续性分割)边缘检测**一阶差分算子**(掌握)二阶差分算子边缘检测算子的比较(掌握)基于阈值的图像分割(相似性分割)**交互方式全局阈值法****...
  • 图像分割方法

    千次阅读 2022-01-09 18:54:54
    图像分割
  • 【深度学习】图像分割的难点

    千次阅读 2022-04-27 23:58:25
    图像分割领域的难点: (1)多尺度问题; (2)物体多姿态(或者多视角)问题; (3)光照问题; (4)分割边缘不准的问题; 因为相邻临的像素对应感受野内的图像信息太过相似导致,解决方向:对网络输出的分割的...
  • 高斯混合模型(GMM)实现图像分割

    千次阅读 2022-01-27 10:14:08
    高斯混合模型(GMM)实现图像分割,能分割普通光学图像、微波图像、SAR图像、遥感图像等。
  • 图像分割基础及经典网络结构

    千次阅读 2022-02-11 17:12:19
    (23条消息) 图像分割汇总_啊啊啊啊的博客-CSDN博客 根据不同的任务和数据类型: 人像分割(头发分割、人脸分割、背景分割) 自动驾驶(行人、车辆分割,车道线检测) 医学图像(病理、CT、MRl ) 工业质检、分拣...
  • 图像分割、图像超分辨率简介

    千次阅读 2021-11-07 17:24:06
    图像分割是指根据灰度、彩色、空间纹理、几何形状等特征把图像划分成若干个互不相交的区域,使得这些特征在同一区域内表现出一致性或相似性,而在不同区域间表现出明显的不同。简单的说就是在一副图像中,把目标从...
  • 图像分割概述

    千次阅读 2021-11-10 19:26:58
    图像分割是计算机视觉研究的一个重要且基础的课题,主要有三种划分方式:普通分割(按像素区域分开)、语义分割(不同区域的语义)、实例分割(像素级别区分图中的实体)。 图像分割早期采用传统图像处理方法,现...
  • 图像分割:提取图像中哪些像素是用于表述已知目标的目标种类与数量问题、目标尺度问题、外在环境干扰问题、物体边缘等,目前分为语义分割、实例分割、全景分割。 Semantic Segmentation(语义分割)   &...
  • 我们在用一个算法对一幅图像进行分割之后,总会面临这样一个问题,分割的结果到底不好。用眼睛可以看出好坏,但这只是主观的好坏,如何量化的对分割的结果进行评价呢,这是这篇文章我要讨论的主题。   我查阅...
  • 图像分割算法

    千次阅读 2022-03-20 20:13:30
    我的毕业论文是做图像分割的,至于为啥要做这个,千万别问我,因为我自己都不知道,更别提做它的意义了,如果非要说,那就是单纯的为了毕业! 面对毕业写文章就是为了记录一下整个过程中我遇到的疑问,以及经过各种...
  • 图像分割】走进基于深度学习的图像分割

    千次阅读 多人点赞 2021-06-25 17:23:49
    深度学习中的图像分割 图像分割就是把图像分成若干个特定的、具有独特性质的区域并提出感兴趣目标的技术和过程。它是由图像处理到图像分析的关键步骤。现有的图像分割方法主要分以下几类:基于阈值的分割方法、基于...
  • 图像分割vs抠图

    千次阅读 2022-03-10 18:20:48
    图像分割: 就是把图像分成若干个特定的、具有独特性质的区域并提出感兴趣目标的技术和过程。图像分割方法主要分以下几类:基于阈值的分割方法、基于区域的分割方法、基于边缘的分割方法以及基于特定理论的分割方法...
  • 一、贝叶斯图像分割简介(具体理论见参考文献) 针对海底鱼图像中鱼难以从背景中分割出来的问题,本文对贝叶斯决策中常用的两种方法进行了分析和研究,提出了一种基于最小错误率决策的海底鱼图像分割方法。具体的...
  • 图像分割《python图像处理篇》

    千次阅读 2021-11-30 11:10:07
    引言:图像分割是目前图像处理领域中的一大热点问题,该领域随着处理技术的不断发展,分为两大类,一类是传统分割方法,一类是基于深度学习的分割方法。随着深度学习的火热,传统的提携分割算法也遮住了其光芒所在,...
  • 基于K-means聚类算法的图像分割 算法的基本原理:  基于K-means聚类算法的图像分割以图像中的像素为数据点,按照指定的簇数进行聚类,然后将每个像素点以其对应的聚类中心替代,重构该图像。 算法步骤: ①随机选取...
  • 自动驾驶场景图像分割(Unet)

    千次阅读 2021-12-06 17:22:10
    使用Unet进行自动驾驶场景图像分割
  • 基于小波变换的医学图像分割

    千次阅读 多人点赞 2020-10-18 16:02:45
    图像分割是一种重要的图像分析技术。对图像分割的研究一直是图像技术研究中的热点和焦点。医学图像分割图像分割的一个重要应用领域,也是一个经典难题,至今已有上千种分割方法,既有经典的方法也有结合新兴理论的...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 208,706
精华内容 83,482
热门标签
关键字:

如何判断图像分割好