查看原文
其他

如何用Python抠图?试试scikit-image

ipython的 Python大本营 2019-03-31

作者 | Parul Pandey

译者 | 刘旭坤

整理 | Jane

出品 | Python大本营(公众号 id:pythonnews)


《终结者》可谓是上世纪八十年代科幻电影中的翘楚。电影中有一段剧情是像下图这样以终结者的第一视角呈现的。我们从图中可以看到终结者能把人和背景进行分割,这在当时还是科幻,但今天图像分割技术已经成了图像处理中很重要的一部分了。


(图片来源:https://i.imgflip.com/nuf6y.jpg)


图像分割


大家如果用过 Photoshop 或者类似的图像编辑软件的话可能都试过抠图,简单地说就是剔除背景只选中图片中的目标人物,这其实就是手动进行了图像分割。有很多图像处理库都可以自动完成图像分割,今天我们就来介绍一下基于 Python 的 scikit-imge。


完整的代码:

https://github.com/parulnith/Image-Processing/tree/master/Image%20Segmentation%20using%20Python%27s%20scikit-image%20module


scikit-image

     


scikit-image 的主要功能和它的名字描述的一样,就是进行图像处理。


安装


可以在命令行输入下面的命令用 pip 或者 conda 来安装 scikit-image:


pip install -U scikit-image(Linux and OSX)
pip install scikit-image(Windows)
# For Conda-based distributions
conda install scikit-image


读取图片


在介绍具体的图像分割方法之前我们先来看看如何读取图片。


  • 读取 scikit-image 自带的灰度图片


scikit-image 的 data 模块自带了一些内置的 jpeg 和 png 格式的图片可以同来测似。(skimage 就是 scikit-image)


from skimage import data
import numpy as np
import matplotlib.pyplot as plt
image = data.binary_blobs()
plt.imshow(image, cmap='gray')

     


  • 读取彩色图片     


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: <class ‘skimage.io.collection.ImageCollection’>


  • 保存图片

#Saving file as ‘logo.png’
io.imsave('logo.png', logo)


图像分割


简单地说来图像分割就是将一张图片分割成不同的部分来简化识别或者用分割后的部分来进行进一步的分析。


本文中我们主要介绍图像分割中的监督和无监督学习方法。



这里监督和无监督分割的含义与机器学习中的概念完全相同,指的是算法是否有来自外界的指导。唯一的区别可能就是分割的或称比较直观,所以用户使用无监督分割方法之后进行调试时比较容易。


图像分割算法中最简单的是阈值分割法。


阈值分割法的原理非常简单,设定一个阈值,在阈值之上或者之下的所有像素都会被分割出来。如果对象与背景之间存在非常打的反差那么阈值分割法就够用了。


下面我们就来看一个例子。先把需要用到的库都读进来:


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


1.使用下面这个函数来显示图片:


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


2.调用 image_show 方法:


text = data.page()
image_show(text)



虽然图片中文字和背景的对比不是很强但还是值得一试。要选择一个合适的阈值我们需要看一下图片的灰度直方图(histogram)。


直方图显示的是图片中不同灰度像素的个数,比如这张图片是 8 位图,所以横轴亮度的范围是 0 到 255,纵轴则是图片中此亮度像素点的个数。


fig, ax = plt.subplots(1, 1)
ax.hist(text.ravel(), bins=32, range=[0, 256])
ax.set_xlim(0, 256);



我们可以看到像素在 225 附近出现了集中,这应该是比较接近白色的背景。最理想的情况是在直方图中出现双峰,这样表示目标和背景亮度反差很大所以我们比较好阈值,也就是双峰中间的部分。这张图片中没有双峰所以我们就先试试不同阈值分割的效果如何。


3.手动选择阈值


text_segmented = text > (value concluded from histogram i.e 50,70,120 )
image_show(text_segmented);


(阈值为 50)

(阈值为 70)

(阈值为 120)


这三张图中我们将阈值设为 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_size 参数。block_size 必须是奇数,这里我们选的是 51。此外还有一个默认值为零的 offset 参数,使用这个参数的目的是降低图片局部的亮度来实现更好的分割效果。


text_threshold = filters.threshold_local(text,block_size=51, offset=10) 
image_show(text > text_threshold);



分割出来的效果还是不错的。


监督式分割


阈值分割在特定场景中有用,但大部分时候我们都需要使用更高级一些的工具。下面我们就来试试从下面这张图片中把人物的头部分割出来。


# import the image
from skimage import io
image = io.imread('girl.jpg') 
plt.imshow(image);



一般来说对图片进行分割之前需要去掉图片中的噪点,但我们用的这张图噪点不多所以这里就跳过除噪点这一步。接下来将图片转化为灰度图:


image_gray = color.rgb2gray(image) 
image_show(image_gray);



下面我们介绍两种不同原理的分割方法。


▌主动轮廓分割 (Active Contour)


主动轮廓模型也叫 snakes 模型,用户需要把要分割的部分圈起一个轮廓,轮廓会逐渐收缩到目标区域的边界上。


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]


这里我们先绕着头部画个圈:



把这些点在图片上画出来看看:


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 用来调整轮廓收缩的速度和轮廓的光滑程度。


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);



▌随机游走分割(Random Walker)


随机游走分割的思想是让用户用几个像素点来对目标和背景进行标记。没有被标记的像素则计算它能随机游走到目标和背景的概率。如果一个像素游走到目标的概率比背景大,就把此像素点归为目标,反之则归为背景。


这里我们还用主动轮廓里面用过的圆圈来进行标记。


image_labels = np.zeros(image_gray.shape, dtype=np.uint8)


随机游走需要我们把背景和目标都进行标记,所以这里还有一个小圆圈来标记头部。


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);



接下来我们调用随机游走方法来看看分割的效果:


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 值发现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 在图像分割上的一种应用。主要的思想是将所有的像素点聚类为指定数量的区域。


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'));



图像从有 512X512=262000 个像素需要处理转化为只需要处理聚类之后的 155 个区域。


图论分割(Graph Based Image Segmentation)


基于图的图像分割方法或称图论分割,是基于最小生成树聚类设计的,但我们没法指定要分割成区域的数目。


image_felzenszwalb = seg.felzenszwalb(image) 
image_show(image_felzenszwalb);



结果就是图片被分成了超过 3000 个子区域。


np.unique(image_felzenszwalb).size
3368


下面我们来每个区域的平均值来填上颜色看看效果:



image_felzenszwalb_colored = color.label2rgb(image_felzenszwalb, image, kind='avg')
image_show(image_felzenszwalb_colored);


颜色可以让我们分辨出不同区域的边界。进一步减少区域的数目可以通过调整 scale 参数来实现,当然也可以用图论分割的结果为基础使用区域邻接图 (RAG) 等方法将区域进行合并来减少区域数目, 这种现象我们称为过分割。


总结


图像分割是图像处理中的重要一环,也因为应用范围广泛称为了研究中的一个热点领域。scikit-image 有活跃的社区和大量经过测试的算法,更可贵的是对商业应用并没有做出限制。如果大家感兴趣的话赶快去看看 scikit-image 的文档吧。


原文链接:

https://towardsdatascience.com/image-segmentation-using-pythons-scikit-image-module-533a61ecc980


(本文为Python大本营翻译文章,转载请微信联系 1092722531)


 精彩推荐


推荐阅读:

    您可能也对以下帖子感兴趣

    文章有问题?点此查看未经处理的缓存