迟早有一天,所有的一切都是数字,包括图像。
看过《终结者》的人肯定会认为这是那个时代最伟大的科幻电影。在这部电影中,James Cameron 引入了一个有趣的视觉效果概念,它可以使观众有可能躲在被称为终结者的电子人的眼睛后面。这种效应被称为「终结者视觉」,在某种程度上,它将人与背景分割开来。在当时,这听起来完全是天方夜谭,但在如今,图像分割已经成为了许多图像处理技术的重要组成部分。
图像分割
我们都很清楚,Photoshop 或类似的图形编辑器提供了无限的可能性,可以将一个人从一张图片中带到另一张图片中。然而,要这样做,首先需要确定那个人在源图像中的位置,这就需要用到图像分割技术了。有许多库是为图像分析而编写的。在本文中,我们将详细讨论基于 python 的图像处理库 scikit-image。
在这里给大家推荐一个python系统学习q群:250933691有免费开发工具以及初学资料,(数据分析,爬虫,AI, 机器学习,神经网络)每天有老师给大家免费授课,欢迎一起交流学习
scikit-image
SciKit Image 是一个专门用于图像处理的 python 包。
安装
可以按如下方式安装 scikit-image:
pip install -U scikit-image(Linux and OSX)
pip install scikit-image(Windows)
# For Conda-based distributions
conda install scikit-image
python 中的图像处理概述
在使用图像分割技术之前,有必要先了解 scikit image 以及它是如何处理图像的。
从 skimage 库导入灰度图像
skimage 数据模块包含一些内置示例数据集,这些数据集通常以 jpeg 或 png 格式存储。
from skimage import data
import numpy as np
import matplotlib.pyplot as plt
image = data.binary_blobs()
plt.imshow(image, cmap='gray')
从 skimage 库导入彩色图像
from skimage import data
import numpy as np
import matplotlib.pyplot as plt
image = data.astronaut()
plt.imshow(image)
从外部源导入图像
# The I/O module is used for importing the image
from skimage import data
import numpy as np
import matplotlib.pyplot as plt
from skimage import io
image = io.imread('skimage_logo.png')
plt.imshow(image);
加载多个图像
images = io.ImageCollection('../images/*.png:../images/*.jpg')
print('Type:', type(images))
images.files
Out[]: Type:
保存图像
#Saving file as 'logo.png'
io.imsave('logo.png', logo)
图像分割
现在我们已经了解了 scikit-image,接下来让我们来详细了解图像分割。图像分割本质上是将数字图像分割成多个片段的过程,以简化或将图像的表示方式更改为更有意义和更易于分析的内容。
在本文中,我们结合监督算法和无监督算法来处理分割过程。
scikit-image 库中可用的一些分割算法
监督分割算法:一些可能来自人类输入的先验知识被用来指导算法。
无监督分割算法:不需要先验知识。这些算法试图将图像自动细分到有意义的区域。用户仍然可以通过调整某些设置以获得想要的输出。
让我们从最简单的阈值分割算法(http://scikit-image.org/docs/dev/auto_examples/xx_applications/plot_thresholding.html)开始吧。
阈值算法
通过选择高于或低于某个阈值的像素,将对象从从背景中分割出来是最简单的方法。在北京分割中,这通常是一个非常有用的方法。了解更多可以查看这里。
让我们在 scikit-image 数据集的一张教科书图像上试试这个。
基本输入
import numpy as np
import matplotlib.pyplot as plt
import skimage.data as data
import skimage.segmentation as seg
import skimage.filters as filters
import skimage.draw as draw
import skimage.color as color
绘制图像的简单函数:
def image_show(image, nrows=1, ncols=1, cmap='gray'):
fig, ax = plt.subplots(nrows=nrows, ncols=ncols, figsize=(14, 14))
ax.imshow(image, cmap='gray')
ax.axis('off')
return fig, ax
图像
text = data.page()
image_show(text)
这个图像有点暗,但我们仍然可以选择一个值,它可以合理的分割图像,而不需要用到任何先进的算法。为了得到这个分割阈值,我们将使用直方图。
直方图是一种显示图像中不同强度值的像素数的图。简单地说,直方图是一个图表,其中 X 轴显示图像中的所有像素值,而 Y 轴显示这些值的频率。
fig, ax = plt.subplots(1, 1)
ax.hist(text.ravel(), bins=32, range=[0, 256])
ax.set_xlim(0, 256);
我们的示例恰好是一张 8-bit 图像,因此在 X 轴上总共有 256 个可能的值。在图像中,0 表示黑色,255 表示白色,我们观察到有些像素值很集中。这很可能是由于我们的文本背景比较淡,而其他部分则有点模糊不清。一个理想的分割直方图应该是有两个峰值,且两个峰值隔的较远,以便我们可以选择在这两个峰值中间选择一个数字作为阈值。现在,让我们试着根据简单的阈值法来分割图像。
有监督阈值
因为阈值是我们自己选择的,所以我们称之为监督阈值。
text_segmented = text > (value concluded from histogram i.e 50,70,120 )
image_show(text_segmented);
文本>50
文本>70
文本>120
我们没有得到理想的结果,因为左边的阴影会造成问题,接下来让我们尝试无监督的阈值。
无监督阈值
scikit-image 有许多自动阈值设定方法,在选择最佳阈值时不需要手动输入。其中常用的方法有 otsu, li, local 等等。
text_threshold = filters.threshold_ # Hit tab with the cursor after the underscore to get all the methods.
image_show(text < text_threshold);
otsu 算法分割
li 算法分割
在 local 算法中,我们还需要指定 block 的大小。offset 有助于调整图像,以获得更好的效果。
text_threshold = filters.threshold_local(text,block_size=51, offset=10)
image_show(text > text_threshold);
local 阈值法
这是一种很好的方法,它在很大程度上消除了噪声。
监督分割
阈值分割是一个非常基本的分割方法,但是它在高对比度图像中效果不是很好,因此我们需要采用更加先进的算法。
在本节中,我们将使用一个免费的示例图像,并尝试使用监督分割技术分割图像中人的头部。
# import the image
from skimage import io
image = io.imread('girl.jpg')
plt.imshow(image);
源图像
小 tip:在对图像进行任何分割之前,最好使用一些滤波器对其进行去噪。
但是,在我们的例子中,图像中的噪声很小,因此我们直接对其进行处理。接下来我们将要做的是使用 rgb2gray 将图像转换成灰度图。
image_gray = color.rgb2gray(image)
image_show(image_gray);
我们将使用两种原理完全不同的分割方法。
活动轮廓分割(Active Contour segmentation)
活动轮廓分割使用用户定义的轮廓或线在感兴趣的区域周围进行初始化,然后该轮廓慢慢收缩。
对于我们的示例图像,让我们围绕人的头部画一个圈来初始化轮廓。
def circle_points(resolution, center, radius):
"""Generate points which define a circle on an image.Centre refers to the centre of the circle"""
radians = np.linspace(0, 2*np.pi, resolution)
c = center[1] + radius*np.cos(radians)#polar co-ordinates
r = center[0] + radius*np.sin(radians)
return np.array([c, r]).T
# Exclude last point because a closed path should not have duplicate points
points = circle_points(200, [80, 250], 80)[:-1]
上面对圆环边缘点的 x 坐标和 y 坐标进行了计算。我们设置分辨率值的为 200,那么将计算 200 个这样的点。
fig, ax = image_show(image)
ax.plot(points[:, 0], points[:, 1], '--r', lw=3)
然后,该算法通过将闭合曲线拟合到人脸的边缘,将人脸与图像的其余部分分割开来。
snake = seg.active_contour(image_gray, points)
fig, ax = image_show(image)
ax.plot(points[:, 0], points[:, 1], '--r', lw=3)
ax.plot(snake[:, 0], snake[:, 1], '-b', lw=3);
我们可以调整参数 alpha 和 beta。alpha 值越高,轮廓的收缩速度越快,而 beta 越大收缩越缓慢。
snake = seg.active_contour(image_gray, points,alpha=0.06,beta=0.3)
fig, ax = image_show(image)
ax.plot(points[:, 0], points[:, 1], '--r', lw=3)
ax.plot(snake[:, 0], snake[:, 1], '-b', lw=3);
随机 walker 分割
在这种方法中,用户以交互方式标记少量的像素,这些像素称为标签。然后假设每个未标记的像素释放一个随机 walker,然后可以确定随机 walker 从每个未标记像素开始到达一个预标记像素的概率。通过将每个像素分配给计算出来的概率值最大的标签,可以获得高质量的分割图像。更多相关资料可以阅读参考文献(地址:https://ieeexplore.ieee.org/document/1704833)。
我们将在这里重新使用前面示例中的种子值。为了简单起见,让我们继续使用圆。
image_labels = np.zeros(image_gray.shape, dtype=np.uint8)
随机 Walker 算法需要一个标签图像作为输入。所以我们会有一个更大的圆,它包围了人的整个脸,还有一个靠近脸中间的小圆。
indices = draw.circle_perimeter(80, 250,20)#from here
image_labels[indices] = 1
image_labels[points[:, 1].astype(np.int), points[:, 0].astype(np.int)] = 2
image_show(image_labels);
现在,让我们使用随机 walker,并观察发生了什么。
image_segmented = seg.random_walker(image_gray, image_labels)
# Check our results
fig, ax = image_show(image_gray)
ax.imshow(image_segmented == 1, alpha=0.3);
它并没有如我们所预期的那样描绘出脸的边缘。为了解决这个问题,我们可以调整 beta 参数,直到得到所需的结果。经过几次尝试后,可以得到,当 beta 值为 3000 时,分割效果不错。
image_segmented = seg.random_walker(image_gray, image_labels, beta = 3000)
# Check our results
fig, ax = image_show(image_gray)
ax.imshow(image_segmented == 1, alpha=0.3);
以上就是监督分割,在这种算法中,我们必须提供某些输入,也必须调整某些参数。然而,我们不可能总是让人先看一张图像,然后再决定输入什么或者从哪里开始。幸运的是,对于这种情况,我们可以采用无监督分割技术。
无监督分割
无监督分割不需要事先了解图像。在一张图像太大的情况下,同时考虑所有像素是不可能的。因此,在这种情况下,无监督分割可以将图像分解为几个子区域,你可以使用数十到数百个区域来代替数百万像素。下面是两个无监督分割算法:
SLIC(简单线性迭代聚类)
SLIC 算法实际上使用了一种叫做 k-means 的机器学习算法。它接收图像的所有像素值,并尝试将它们分离到给定数量的子区域中。更多内容可以阅读参考文献(地址:https://ieeexplore.ieee.org/document/6205760)。
SLIC 是处理彩色图像的,所以我们将使用原始图像。
image_slic = seg.slic(image,n_segments=155)
我们所做的只是将图像的每个子图像或子区域像素设置为该区域像素的平均值。
# label2rgb replaces each discrete label with the average interior color
image_show(color.label2rgb(image_slic, image, kind='avg'));
我们已经将此图像从 512*512=262000 个像素减少到 155 个区域。
Felzenszwalb 算法
该算法也使用了一种机器学习算法,即最小生成树聚类算法。Felzenszwaib 算法并没有告诉我们图像将被分割成多少个集群。它将运行并生成尽可能多的适合它的集群。相关的参考文件可以前往 http://cs.brown.edu/people/pfelzens/segment/ 查阅。
image_felzenszwalb = seg.felzenszwalb(image)
image_show(image_felzenszwalb);
有很多区域,我们计算相互独立的区域数。
np.unique(image_felzenszwalb).size
3368
现在让我们使用区域像素平均值对它们重新着色,就像我们在 SLIC 算法中所做的那样。
image_felzenszwalb_colored = color.label2rgb(image_felzenszwalb, image, kind='avg')
image_show(image_felzenszwalb_colored);
现在我们将图像分成了合适的小区域。如果我们想要将图像分成更少的区域,可以更改比例参数或者继续组合它们。这种方法有时被称为过度分割。
这看起来更像是一个拆分后的图像,其本质上只是减少了颜色的数量。要再次组合它们,可以使用区域邻接图(RAG,http://scikit-image.org/docs/dev/auto_examples/segmentation/plot_rag.html),但这超出了本文的范围。
结论
图像分割是图像处理中非常重要的一个步骤。它是一个热门的研究领域,应用非常广泛,从计算机视觉到医学图像、从交通和视频监控等领域都有涉及。Python scikit-image 提供了一个非常强大的库,该库具有大量用于图像处理的算法。它是免费的,没有任何限制,在其背后有一个活跃的社区。你可以查看他们的文档,了解关于库及其用例的更多信息。
网友评论