计算视觉——无监督学习与图像分割

5,097 阅读15分钟

监督学习与无监督学习(Supervised Learning&Unsupervised Learning)

       在西瓜书( 《机器学习》周志华著 )中,就有对于监督学习与无监督学习的介绍。此处我们采用维基百科对于二者的解释:

       监督学习是基于输入-输出对将输入映射到输出的函数的机器学习任务。它从一组训练例子组成的带标记的训练数据推断出一个函数。在监督学习中,每个例子都是一个输入对象(通常是一个向量)和一个期望的输出值(也称为监督信号)组成的对。监督学习算法对训练数据进行分析,并生成一个推断函数,用于映射新的例子。在人类和动物心理学中,这类任务通常被称为概念学习。

       无监督学习是一种机器学习,它在没有预先存在的标签和最少的人工监督的情况下,在一个数据集中寻找以前未被发现的模式。与通常使用人类标记数据的监督学习不同,非监督学习(也称为自组织学习)允许对输入的概率密度建模。它与监督学习和强化学习一起构成了机器学习的三个主要类别之一。无监督学习的两种主要方法是主元分析法和聚类分析法,其中聚类分析是机器学习的一个分支,它将没有标记、分类或分类的数据分组。

       我们可以这么理解,区别于监督学习与无监督学习的关键在于是否有一套“标准的答案”,即是否有标签对学习的结果进行监督与改善。最典型的监督学习与无监督学习问题分别对应于分类(有监督学习)与聚类(无监督学习)。

       对于分类任务而言,就像我们已有了一批猫、狗的图像数据,每张图像都有它的标签,即已知图像内为猫或者为狗,那么我们就可以通过这些图像数据以及标签数据,泛化一个分类模型,使其可以对新的猫狗图像进行分类;而对于聚类问题而言,我们是没有标签的。即我们有一批图像数据,其中有猫有狗,但我们并不知道哪幅图像是猫,哪幅图像是狗。这个时候,因为没有标签,我们也就没有一套“标准答案”,只能从现有的图像数据中去学习,探寻潜在的规律,并将图像数据分成多个聚簇,这些聚簇中可能会同时含有猫、狗的图像数据,但在大致的分布上可以区分出猫图像数据与狗图像数据。

图像分割(Image Segmentation)

       在计算机视觉中,我们感兴趣的是如何识别一组像素,这称之为图像分割问题。例如,两个人在看同一幅视错觉图像时可能会看到不一样的东西。人类凭直觉进行图像分割。例如, 这完全取决于观察者在思考时如何分割图像。在下面的图片中你可能看到斑马,或者可能看到狮子。

       图像分割背后的动机之一是将图像分割成连贯的对象,如下所示:

       我们还可能希望根据附近像素的相似性将图像分割成许多组,这些组被称为“超像素”,“超像素”允许我们将许多单个像素视为一个簇,从而实现更快的计算,下面是一个超像素分割的图像实例。

       超像素分割和其它形式的分割都有利于提取图像特征。我们可以将像素组视为一个特征,从中获取图像信息。此外,图像分割也有利于一些常见的照片特效,如背景去除。如果我们能够正确地分割一幅图像,我们将能保留我们想留下的像素组并删除其他无关的像素组。

       虽然图像分割非常有用并且在多种场景下具有应用需求,但是没有一种“最优”的图像分割方法,我们必须比较不同的图像分割算法来找到我们最佳的解决方案。如果图像的组数太多或太少,就会出现过分割或欠分割的情况。

       为了解决图像分割的问题,我们可以将图像分割视为聚类。通过聚类,我们可以有效的把相似的数据点组合在一起,并用一个奇异值来表示它们,这对于我们对图像进行进一步的操作或提取图像特征非常有帮助。但是,面临的问题如下:

  • 如何确定两个像素、像素块或图像是否相似
  • 如何根据图像的空间信息计算局部的整体聚簇

       针对这些问题,不同的聚类算法有不同的答案。一般来说,聚类可以分为自上而下的和自下而上的。自上而下的聚类算法将位于同一视觉实体上聚为一个聚簇。而自下而上的算法将局部相关的像素分组在一起。

聚类

       聚类的运用非常广泛,预测、分析、归类等方面都可以使用聚类。本文将以Kmeans为例,介绍聚类算法。如下图所示,左上角的Input Image中有三个不同颜色区域,因此,通过左边的直方图我们可以很容易的对图像进行分割。然而,在左下角的Input图像所对应的直方图却没有均一的颜色区域。为了分割图像,我们可以采用Kmeans聚类算法。

       使用Kmeans,我们在此处的目标是确定三个聚簇中心作为代表强度,并根据其最近的中心标记每个像素。最好的聚类中心是那些将所有点与其最近的聚类中心之间的平方距离之和最小化的聚类中心c_i

SSD=\sum_{i\in clusters}\sum_{x\in clusters}(x-c_i)^2

算法

       找到聚类中心和聚簇成员可以被认为是一个“鸡和蛋”的问题。如果我们知道聚类中心,我们可以通过将各个点分配给最近的中心来将点分配给聚簇。另一方面,如果我们知道群成员的属性,我们可以通过计算各个聚簇的均值找到聚簇中心。

       为了找到聚簇中心和聚簇成员,我们首先初始化K个聚簇中心(K需要提前指定),通常采用随机生成的聚簇中心,随后,运行一个迭代过程,计算特定迭代次数后最佳的(也就是SSD最小的)聚簇中心与聚簇成员,或者聚簇中心收敛。

       算法流程如下:

  • 初始化聚簇中心c_1,c_2...c_K
  • 将数据集中的每个点分配给最近的中心。用欧氏距离作为距离度量。
  • 将聚簇中心更新为聚簇成员的平均值
  • 重复步骤2-3,直到聚簇中心的值停止更改或已达到算法最大迭代次数。

       算法流程图如下所示:

       在k均值聚类(K-means)这篇文章中举了一个很不错的应用例子,作者用亚洲15支足球队的2005年到2010年的战绩做了一个向量表,然后用K-Means算法(K=3)把球队聚为三个类,得出了下面的结果,非常真实。

亚洲一流:日本,韩国,伊朗,沙特
亚洲二流:乌兹别克斯坦,巴林,朝鲜
亚洲三流:中国,伊拉克,卡塔尔,阿联酋,泰国,越南,阿曼,印尼

       接下来,我们将聚类运用于实际的图像分割中。

代码实战部分

# segmentation.py
# python 3.6
import numpy as np
import random
from scipy.spatial.distance import squareform, pdist
from skimage.util import img_as_float

### Clustering Methods
def kmeans(features, k, num_iters=100):

    N, D = features.shape

    assert N >= k, 'Number of clusters cannot be greater than number of points'

    # Randomly initalize cluster centers
    idxs = np.random.choice(N, size=k, replace=False)
    centers = features[idxs]        # 1. 随机中心点
    assignments = np.zeros(N)

    for n in range(num_iters):
        ### YOUR CODE HERE
        # 2. 分类
        for i in range(N):
            dist = np.linalg.norm(features[i] - centers, axis=1)    # 每个点和中心点的距离
            assignments[i] = np.argmin(dist)        # 第i个点属于最近的中心点

        pre_centers = centers.copy()
        # 3. 重新计算中心点
        for j in range(k):
            centers[j] = np.mean(features[assignments == j], axis=0)

        # 4. 验证中心点是否改变
        if np.array_equal(pre_centers, centers):
            break
        ### END YOUR CODE

    return assignments

def kmeans_fast(features, k, num_iters=100):

    N, D = features.shape

    assert N >= k, 'Number of clusters cannot be greater than number of points'

    # Randomly initalize cluster centers
    idxs = np.random.choice(N, size=k, replace=False)
    centers = features[idxs]
    assignments = np.zeros(N)

    for n in range(num_iters):
        ### YOUR CODE HERE
        # 计算距离
        features_tmp = np.tile(features, (k, 1))        # (k*N, ...)
        centers_tmp = np.repeat(centers, N, axis=0)     # (N * k, ...)
        dist = np.sum((features_tmp - centers_tmp)**2, axis=1).reshape((k, N))      # 每列 即k个中心点
        assignments = np.argmin(dist, axis=0)   # 最近

        # 计算新的中心点
        pre_centers = centers
        # 3. 重新计算中心点
        for j in range(k):
            centers[j] = np.mean(features[assignments == j], axis=0)

        # 4. 验证中心点是否改变
        if np.array_equal(pre_centers, centers):
            break
        ### END YOUR CODE

    return assignments



def hierarchical_clustering(features, k):

    N, D = features.shape

    assert N >= k, 'Number of clusters cannot be greater than number of points'

    # Assign each point to its own cluster
    assignments = np.arange(N)
    centers = np.copy(features)
    n_clusters = N

    while n_clusters > k:
        ### YOUR CODE HERE
        dist = pdist(centers)       # 计算相互之间的距离
        matrixDist = squareform(dist)   # 将向量形式变化为矩阵形式
        matrixDist = np.where(matrixDist != 0.0, matrixDist, 1e10)      # 将0.0的变为1e10,即为了矩阵中相同的点计算的距离去掉

        minValue = np.argmin(matrixDist)        # 最小的值的位置
        min_i = minValue // n_clusters          # 行号
        min_j = minValue - min_i * n_clusters   # 列号

        if min_j < min_i:       # 归并到小号的cluster
            min_i, min_j = min_j, min_i  # 交换一下

        for i in range(N):
            if assignments[i] == min_j:
                assignments[i] = min_i     # 两者合并

        for i in range(N):
            if assignments[i] > min_j:
                assignments[i] -= 1     # 合并了一个cluster,因此n_clusters减少一位

        centers = np.delete(centers, min_j, axis=0)  # 减少一个
        centers[min_i] = np.mean(features[assignments == min_i], axis=0)        # 重新计算中心点

        n_clusters -= 1     # 减去1

        ### END YOUR CODE

    return assignments


### Pixel-Level Features
def color_features(img):
    H, W, C = img.shape
    img = img_as_float(img)
    features = np.zeros((H*W, C))

    ### YOUR CODE HERE
    features = img.reshape(H * W, C)        # color作为特征
    ### END YOUR CODE

    return features

def color_position_features(img):
    H, W, C = img.shape
    color = img_as_float(img)
    features = np.zeros((H*W, C+2))

    ### YOUR CODE HERE
    # 坐标
    cord = np.dstack(np.mgrid[0:H, 0:W]).reshape((H*W, 2))      # mgrid生成坐标,重新格式为(x,y)的二维
    features[:, 0:C] = color.reshape((H*W, C))      # r,g,b
    features[:, C:C+2] = cord
    features = (features - np.mean(features, axis=0)) / np.std(features, axis=0,  ddof = 0)     # 对特征归一化处理
    ### END YOUR CODE

    return features

def my_features(img):
    """ Implement your own features

    Args:
        img - array of shape (H, W, C)

    Returns:
        features - array of (H * W, C)
    """
    features = None
    ### YOUR CODE HERE
    features = color_position_features(img)
    ### END YOUR CODE
    return features


### Quantitative Evaluation
def compute_accuracy(mask_gt, mask):
    accuracy = None
    ### YOUR CODE HERE
    mask_end = mask_gt - mask
    count = len(mask_end[np.where(mask_end == 0)])
    accuracy = count / (mask_gt.shape[0] * mask_gt.shape[1])
    ### END YOUR CODE

    return accuracy

def evaluate_segmentation(mask_gt, segments):
    num_segments = np.max(segments) + 1
    best_accuracy = 0

    # 将分割结果与真实值进行对比
    for i in range(num_segments):
        mask = (segments == i).astype(int)
        accuracy = compute_accuracy(mask_gt, mask)
        best_accuracy = max(accuracy, best_accuracy)

    return best_accuracy
# test.py
import  numpy as np
from scipy.spatial.distance import  pdist, squareform

if __name__ == "__main__":
    a = np.array([[1,1,1,1],[1,0,0,0]])
    b = np.array([[1,0,0,1],[1,1,0,0]])

    c = (a == 1)
    print(c)

# utils.py
import numpy as np
import matplotlib.pyplot as plt
from skimage.util import img_as_float
from skimage import transform
from skimage import io

from segmentation import *

import os

def visualize_mean_color_image(img, segments):

    img = img_as_float(img)
    k = np.max(segments) + 1
    mean_color_img = np.zeros(img.shape)

    for i in range(k):
        mean_color = np.mean(img[segments == i], axis=0)
        mean_color_img[segments == i] = mean_color

    plt.imshow(mean_color_img)
    plt.axis('off')
    plt.show()

def compute_segmentation(img, k,
        clustering_fn=kmeans_fast,
        feature_fn=color_position_features,
        scale=0):
    """ 计算图像分割结果

    首先,从图像的每个像素中提取一个特征向量。然后将聚类算法应用于所有特征向量的集合。当且仅当两个像素的特征向量被分配到同一簇时,两个像素会被分配到同一聚簇。

    """

    assert scale <= 1 and scale >= 0, \
        'Scale should be in the range between 0 and 1'

    H, W, C = img.shape

    if scale > 0:
        # 缩小图像来获得更快的计算速度
        img = transform.rescale(img, scale)

    features = feature_fn(img)
    assignments = clustering_fn(features, k)
    segments = assignments.reshape((img.shape[:2]))

    if scale > 0:
        # 调整大小分割回图像的原始大小
        segments = transform.resize(segments, (H, W), preserve_range=True)

        # 调整大小会导致像素值不重叠。
        # 像素值四舍五入为最接近的整数
        segments = np.rint(segments).astype(int)

    return segments


def load_dataset(data_dir):
    """
    载入数据集
    'imgs/aaa.jpg' is 'gt/aaa.png'
    """

    imgs = []
    gt_masks = []

    for fname in sorted(os.listdir(os.path.join(data_dir, 'imgs'))):
        if fname.endswith('.jpg'):
            # 读入图像
            img = io.imread(os.path.join(data_dir, 'imgs', fname))
            imgs.append(img)

            # 加载相应的分割mask
            mask_fname = fname[:-4] + '.png'
            gt_mask = io.imread(os.path.join(data_dir, 'gt', mask_fname))
            gt_mask = (gt_mask != 0).astype(int) # 将mask进行二值化
            gt_masks.append(gt_mask)

    return imgs, gt_masks

运行实施例

# 初始化
from time import time
import numpy as np
import matplotlib.pyplot as plt
from matplotlib import rc
from skimage import io

from __future__ import print_function

%matplotlib内联
plt.rcParams['figure.figsize'] = (15.0, 12.0) # set default size of plots
plt.rcParams['image.interpolation'] = 'nearest'
plt.rcParams['image.cmap'] = 'gray'

# 自动重载外部模块
%load_ext autoreload
%autoreload 2
# 为聚类生成随机数据点

# 采用seed保证生成结果的一致
np.random.seed(0)

# 聚簇 1
mean1 = [-1, 0]
cov1 = [[0.1, 0], [0, 0.1]]
X1 = np.random.multivariate_normal(mean1, cov1, 100)

# 聚簇 2
mean2 = [0, 1]
cov2 = [[0.1, 0], [0, 0.1]]
X2 = np.random.multivariate_normal(mean2, cov2, 100)

# 聚簇 3
mean3 = [1, 0]
cov3 = [[0.1, 0], [0, 0.1]]
X3 = np.random.multivariate_normal(mean3, cov3, 100)

# 聚簇 4
mean4 = [0, -1]
cov4 = [[0.1, 0], [0, 0.1]]
X4 = np.random.multivariate_normal(mean4, cov4, 100)

# 合并两组数据点
X = np.concatenate((X1, X2, X3, X4))

# 绘制数据点
plt.scatter(X[:, 0], X[:, 1])
plt.axis('equal')
plt.show()

from segmentation import kmeans

np.random.seed(0)
start = time()
assignments = kmeans(X, 4)
end = time()

kmeans_runtime = end - start

print("kmeans running time: %f seconds." % kmeans_runtime)

for i in range(4):
    cluster_i = X[assignments==i]
    plt.scatter(cluster_i[:, 0], cluster_i[:, 1])

plt.axis('equal')
plt.show()

kmeans running time: 0.027956 seconds.

from segmentation import hierarchical_clustering

start = time()
assignments = hierarchical_clustering(X, 4)
end = time()

print("hierarchical_clustering running time: %f seconds." % (end - start))

for i in range(4):
    cluster_i = X[assignments==i]
    plt.scatter(cluster_i[:, 0], cluster_i[:, 1])

plt.axis('equal')
plt.show()

hierarchical_clustering running time: 0.793070 seconds.

       在使用聚类算法分割图像之前,我们必须为每个像素计算一些特征向量。每个像素的特征向量应该对我们在好的分割中所关心的质量进行编码。更具体地说,对于一对具有相应特征向量f_if_j的像素p_ip_j,如果为p_ip_j在同一聚簇内上,那么f_if_j之间的距离应该很小;反之,则应该很大。

# 载入并显示图像
img = io.imread('train.jpg')
H, W, C = img.shape

plt.imshow(img)
plt.axis('off')
plt.show()

       一个像素最简单的特征向量就是该像素的颜色向量。输出如下图所示:

from segmentation import color_features
np.random.seed(0)

features = color_features(img)

# 结果检测
assert features.shape == (H * W, C),\
    "Incorrect shape! Check your implementation."

assert features.dtype == np.float,\
    "dtype of color_features should be float."

assignments = kmeans_fast(features, 8)
segments = assignments.reshape((H, W))

# 展示图像分割结果
plt.imshow(segments, cmap='viridis')
plt.axis('off')
plt.show()

我们将每个像素用所在聚簇的平均颜色替代,结果如下:

from utils import visualize_mean_color_image
visualize_mean_color_image(img, segments)

       但我们可以发现,这样所得的结果没有考虑图像的空间相关信息,只是单纯的从颜色来进行聚类。而我们可以将像素在图像中的颜色和位置连接起来。简而言之,对于位于图像中(x,y)位置的颜色(r,g,b)像素,其特征向量为(r,g,b,x,y)。由于颜色和位置的动态范围可能有很大的不同范围。如,图像的每个颜色通道可能在[0,255]范围内,而每个像素的位置可能有更大的范围。特征向量中不同特征之间的不均匀缩放可能会导致聚类算法性能不佳。修正不同特征之间动态范围不同的一种方法是对特征向量进行标准化。

from segmentation import color_position_features
np.random.seed(0)

features = color_position_features(img)

# 结果检测
assert features.shape == (H * W, C + 2),\
    "Incorrect shape! Check your implementation."

assert features.dtype == np.float,\
    "dtype of color_features should be float."

assignments = kmeans_fast(features, 8)
segments = assignments.reshape((H, W))

# 图像分割结果显示
plt.imshow(segments, cmap='viridis')
plt.axis('off')
plt.show()

       虽然还是存在分割不平滑的问题,但已经远好于之前不考虑空域信息的结果了。

visualize_mean_color_image(img, segments)

       为了量化评估聚类算法的性能,我们可以进行定量评估。 我们利用了一个小型的猫咪图像数据集,并将这些图像分割为前景(cat)和背景(background)。我们将在这个数据集上定量评估不同的分割方法(特征和聚类方法)。

from utils import load_dataset, compute_segmentation
from segmentation import evaluate_segmentation

# 载入该小型数据集
imgs, gt_masks = load_dataset('./data')

# 设置图像分割的参数
num_segments = 3
clustering_fn = kmeans_fast
feature_fn = color_features
scale = 0.5

mean_accuracy = 0.0

segmentations = []

for i, (img, gt_mask) in enumerate(zip(imgs, gt_masks)):
    # Compute a segmentation for this image
    segments = compute_segmentation(img, num_segments,
                                    clustering_fn=clustering_fn,
                                    feature_fn=feature_fn,
                                    scale=scale)
    
    segmentations.append(segments)
    
    # 评估图像分割结果
    accuracy = evaluate_segmentation(gt_mask, segments)
    
    print('Accuracy for image %d: %0.4f' %(i, accuracy))
    mean_accuracy += accuracy
    
mean_accuracy = mean_accuracy / len(imgs)
print('Mean accuracy: %0.4f' % mean_accuracy)

输出结果如下:

Accuracy for image 0: 0.8612
Accuracy for image 1: 0.9571
Accuracy for image 2: 0.9824
Accuracy for image 3: 0.9206
Accuracy for image 4: 0.7642
Accuracy for image 5: 0.8062
Accuracy for image 6: 0.6617
Accuracy for image 7: 0.4726
Accuracy for image 8: 0.8317
Accuracy for image 9: 0.7580
Accuracy for image 10: 0.6515
Accuracy for image 11: 0.8261
Accuracy for image 12: 0.7105
Accuracy for image 13: 0.6667
Accuracy for image 14: 0.7623
Accuracy for image 15: 0.5223
Mean accuracy: 0.7597
# 可视化图像分割结果

N = len(imgs)
plt.figure(figsize=(15,60))
for i in range(N):

    plt.subplot(N, 3, (i * 3) + 1)
    plt.imshow(imgs[i])
    plt.axis('off')

    plt.subplot(N, 3, (i * 3) + 2)
    plt.imshow(gt_masks[i])
    plt.axis('off')

    plt.subplot(N, 3, (i * 3) + 3)
    plt.imshow(segmentations[i], cmap='viridis')
    plt.axis('off')

plt.show()

你可能还会感兴趣

       科研基本功——高效文献检索与文献阅读保姆级教程

       计算视觉——图像、质量、评价

       颜色视觉——杂谈篇

       计算视觉——基于暗通道先验的图像去雾算法

       基于内容的图像缩放

       图像手绘/素描风格转换

       图像的局部特征信息及全景图像拼接

       图像梯度、图像边缘、几何特征、检测与提取

       本博客的代码与数据集可在我的github仓库中找到。

       最后求点赞关注二连,您的点赞和关注能让更多人看到这篇文章,蟹蟹!