3.3. scikit-image: 图像处理

作者: Emmanuelle Gouillart

scikit-image 是一个专门用于图像处理的 Python 包,它使用 NumPy 数组作为图像对象。本章描述了如何使用 scikit-image 来执行各种图像处理任务,以及它与其他科学 Python 模块(如 NumPy 和 SciPy)的关系。

另请参阅

对于基本的图像操作,如图像裁剪或简单滤波,可以使用 NumPy 和 SciPy 来实现大量的简单操作。请参阅 使用 NumPy 和 SciPy 进行图像操作和处理.

请注意,在阅读本章之前,您应该熟悉上一章的内容,因为诸如掩码和标记之类的基本操作是本章的先决条件。

3.3.1. 简介和概念

图像为 NumPy 数组 np.ndarray

图像:

np.ndarray

像素:

数组值:a[2, 3]

通道:

数组维度

图像编码:

dtype (np.uint8, np.uint16, np.float)

滤波器:

函数 (numpy, skimage, scipy)

>>> import numpy as np
>>> check = np.zeros((8, 8))
>>> check[::2, 1::2] = 1
>>> check[1::2, ::2] = 1
>>> import matplotlib.pyplot as plt
>>> plt.imshow(check, cmap='gray', interpolation='nearest')
<matplotlib.image.AxesImage object at ...>
../../_images/sphx_glr_plot_check_001.png

3.3.1.1. scikit-image 和科学 Python 生态系统

scikit-image 被打包在 pipconda 基于 Python 的安装中,以及大多数 Linux 发行版中。其他针对 NumPy 数组的图像处理和可视化 Python 包包括

scipy.ndimage

用于 N 维数组。基本滤波、数学形态学、区域属性

Mahotas

重点在于高速实现。

Napari

一个使用 Qt 构建的快速、交互式、多维图像查看器。

一些强大的 C++ 图像处理库也具有 Python 绑定

OpenCV

一个高度优化的计算机视觉库,重点在于实时应用程序。

ITK

Insight ToolKit,特别适用于配准和处理 3D 图像。

在不同程度上,这些库往往不太符合 Python 的风格,也不太适合 NumPy。

3.3.1.2. scikit-image 中包含的内容

该库主要包含图像处理算法,但也包含一些实用函数来简化数据处理和处理。它包含以下子模块

color

色彩空间转换。

data

测试图像和示例数据。

draw

在 NumPy 数组上操作的绘图基元(线条、文本等)。

exposure

图像强度调整,例如直方图均衡化等。

feature

特征检测和提取,例如纹理分析、角点等。

filters

锐化、边缘查找、秩滤波、阈值化等。

graph

图论运算,例如最短路径。

io

读取、保存和显示图像和视频。

measure

图像属性的测量,例如区域属性和轮廓。

metrics

对应于图像的指标,例如距离指标、相似度等。

morphology

形态学运算,例如开运算或骨架化。

restoration

恢复算法,例如反卷积算法、降噪等。

segmentation

将图像划分为多个区域。

transform

几何变换和其他变换,例如旋转或 Radon 变换。

util

通用实用程序。

3.3.2. 导入

我们使用以下约定导入 scikit-image

>>> import skimage as ski

大多数功能都位于子包中,例如

>>> image = ski.data.cat()

您可以使用以下命令列出所有子模块

>>> for m in dir(ski): print(m)
__version__
color
data
draw
exposure
feature
filters
future
graph
io
measure
metrics
morphology
registration
restoration
segmentation
transform
util

大多数 scikit-image 函数都将 NumPy ndarrays 作为参数

>>> camera = ski.data.camera()
>>> camera.dtype
dtype('uint8')
>>> camera.shape
(512, 512)
>>> filtered_camera = ski.filters.gaussian(camera, sigma=1)
>>> type(filtered_camera)
<class 'numpy.ndarray'>

3.3.3. 示例数据

首先,我们需要示例图像来进行操作。该库附带了一些示例图像

skimage.data

>>> image = ski.data.cat()
>>> image.shape
(300, 451, 3)

3.3.4. 输入/输出、数据类型和色彩空间

I/O: skimage.io

将图像保存到磁盘:skimage.io.imsave()

>>> ski.io.imsave("cat.png", image)

从文件读取:skimage.io.imread()

>>> cat = ski.io.imread("cat.png")
../../_images/sphx_glr_plot_camera_001.png

这适用于 ImageIO 库支持的许多数据格式。

加载也可以使用 URL

>>> logo = ski.io.imread('https://scikit-image.cn/_static/img/logo.png')

3.3.4.1. 数据类型

../../_images/sphx_glr_plot_camera_uint_001.png

图像 ndarrays 可以用整数(有符号或无符号)或浮点数表示。

注意整数数据类型的溢出

>>> camera = ski.data.camera()
>>> camera.dtype
dtype('uint8')
>>> camera_multiply = 3 * camera

可以有不同的整数大小:8 位、16 位或 32 位,有符号或无符号。

警告

一个重要(如果说有问题的话)的 skimage 约定:浮点图像应位于 [-1, 1] 之间(为了使所有浮点图像具有可比的对比度)

>>> camera_float = ski.util.img_as_float(camera)
>>> camera.max(), camera_float.max()
(np.uint8(255), np.float64(1.0))

一些图像处理例程需要使用浮点数组,因此可能输出具有不同类型和数据范围的数组,与输入数组不同

>>> camera_sobel = ski.filters.sobel(camera)
>>> camera_sobel.max()
np.float64(0.644...)

skimage 中提供了实用函数来转换 dtype 和数据范围,遵循 skimage 的约定:util.img_as_float, util.img_as_ubyte 等。

有关更多详细信息,请参阅 用户指南.

3.3.4.2. 色彩空间

彩色图像的形状为 (N, M, 3) 或 (N, M, 4)(当 alpha 通道编码透明度时)

>>> face = sp.datasets.face()
>>> face.shape
(768, 1024, 3)

skimage.color 中提供了在不同色彩空间(RGB、HSV、LAB 等)之间转换的例程:color.rgb2hsv, color.lab2rgb 等。检查文档字符串以了解输入图像的预期 dtype(和数据范围)。

3.3.5. 图像预处理/增强

目标:降噪、特征(边缘)提取,…

3.3.5.1. 局部滤波器

局部滤波器用相邻像素值的函数替换像素值。该函数可以是线性的或非线性的。

邻域:正方形(选择大小)、圆盘或更复杂的结构元素

../../_images/kernels.png

示例:水平 Sobel 滤波器

>>> text = ski.data.text()
>>> hsobel_text = ski.filters.sobel_h(text)

使用以下线性核来计算水平梯度

1   2   1
0 0 0
-1 -2 -1
../../_images/sphx_glr_plot_sobel_001.png

3.3.5.2. 非局部滤波器

非局部滤波器使用图像的大区域(或整个图像)来变换单个像素的值

>>> camera = ski.data.camera()
>>> camera_equalized = ski.exposure.equalize_hist(camera)

增强几乎均匀的大区域的对比度。

../../_images/sphx_glr_plot_equalize_hist_001.png

3.3.5.3. 数学形态学

请参阅 维基百科,了解有关数学形态学的介绍。

使用一个简单的形状(一个结构元素)探测图像,并根据该形状在局部上如何拟合或错过图像来修改图像。

默认结构元素:像素的 4 连接

>>> # Import structuring elements to make them more easily accessible
>>> from skimage.morphology import disk, diamond
>>> diamond(1)
array([[0, 1, 0],
[1, 1, 1],
[0, 1, 0]], dtype=uint8)
../../_images/diamond_kernel.png

腐蚀 = 最小滤波器。用结构元素覆盖的最小值替换像素的值。

>>> a = np.zeros((7,7), dtype=np.uint8)
>>> a[1:6, 2:5] = 1
>>> a
array([[0, 0, 0, 0, 0, 0, 0],
[0, 0, 1, 1, 1, 0, 0],
[0, 0, 1, 1, 1, 0, 0],
[0, 0, 1, 1, 1, 0, 0],
[0, 0, 1, 1, 1, 0, 0],
[0, 0, 1, 1, 1, 0, 0],
[0, 0, 0, 0, 0, 0, 0]], dtype=uint8)
>>> ski.morphology.binary_erosion(a, diamond(1)).astype(np.uint8)
array([[0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 1, 0, 0, 0],
[0, 0, 0, 1, 0, 0, 0],
[0, 0, 0, 1, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0]], dtype=uint8)
>>> #Erosion removes objects smaller than the structure
>>> ski.morphology.binary_erosion(a, diamond(2)).astype(np.uint8)
array([[0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0]], dtype=uint8)

膨胀:最大滤波器

>>> a = np.zeros((5, 5))
>>> a[2, 2] = 1
>>> a
array([[0., 0., 0., 0., 0.],
[0., 0., 0., 0., 0.],
[0., 0., 1., 0., 0.],
[0., 0., 0., 0., 0.],
[0., 0., 0., 0., 0.]])
>>> ski.morphology.binary_dilation(a, diamond(1)).astype(np.uint8)
array([[0, 0, 0, 0, 0],
[0, 0, 1, 0, 0],
[0, 1, 1, 1, 0],
[0, 0, 1, 0, 0],
[0, 0, 0, 0, 0]], dtype=uint8)

开运算:腐蚀 + 膨胀

>>> a = np.zeros((5,5), dtype=int)
>>> a[1:4, 1:4] = 1; a[4, 4] = 1
>>> a
array([[0, 0, 0, 0, 0],
[0, 1, 1, 1, 0],
[0, 1, 1, 1, 0],
[0, 1, 1, 1, 0],
[0, 0, 0, 0, 1]])
>>> ski.morphology.binary_opening(a, diamond(1)).astype(np.uint8)
array([[0, 0, 0, 0, 0],
[0, 0, 1, 0, 0],
[0, 1, 1, 1, 0],
[0, 0, 1, 0, 0],
[0, 0, 0, 0, 0]], dtype=uint8)

开运算会去除小物体并平滑角点。

有更高级的数学形态学可用:顶帽、骨架化等。

另请参阅

基本数学形态学也在 scipy.ndimage.morphology 中实现。 scipy.ndimage 实现适用于任意维度的数组。


3.3.6. 图像分割

图像分割是将不同的标签分配给图像的不同区域,例如为了提取感兴趣对象的像素。

3.3.6.1. 二值分割:前景 + 背景

基于直方图的方法:Otsu 阈值法

提示

Otsu 方法 是一种简单的启发式方法,用于查找阈值以将前景与背景分离。

camera = ski.data.camera()
val = ski.filters.threshold_otsu(camera)
mask = camera < val
../../_images/sphx_glr_plot_threshold_001.png

标记离散图像的连通分量

提示

一旦你将前景对象分离出来,就可以用它们彼此分离。为此,我们可以为每个对象分配一个不同的整数标签。

合成数据

>>> n = 20
>>> l = 256
>>> im = np.zeros((l, l))
>>> rng = np.random.default_rng()
>>> points = l * rng.random((2, n ** 2))
>>> im[(points[0]).astype(int), (points[1]).astype(int)] = 1
>>> im = ski.filters.gaussian(im, sigma=l / (4. * n))
>>> blobs = im > im.mean()

标记所有连通分量

>>> all_labels = ski.measure.label(blobs)

仅标记前景连通分量

>>> blobs_labels = ski.measure.label(blobs, background=0)
../../_images/sphx_glr_plot_labels_001.png

另请参阅

scipy.ndimage.find_objects() 用于返回图像中对象的切片。

3.3.6.2. 基于标记的方法

如果你在一组区域内有标记,你可以使用这些标记来分割这些区域。

分水岭 分割

分水岭 (skimage.segmentation.watershed()) 是一种区域增长方法,它会填充图像中的“盆地”。

>>> # Generate an initial image with two overlapping circles
>>> x, y = np.indices((80, 80))
>>> x1, y1, x2, y2 = 28, 28, 44, 52
>>> r1, r2 = 16, 20
>>> mask_circle1 = (x - x1) ** 2 + (y - y1) ** 2 < r1 ** 2
>>> mask_circle2 = (x - x2) ** 2 + (y - y2) ** 2 < r2 ** 2
>>> image = np.logical_or(mask_circle1, mask_circle2)
>>> # Now we want to separate the two objects in image
>>> # Generate the markers as local maxima of the distance
>>> # to the background
>>> import scipy as sp
>>> distance = sp.ndimage.distance_transform_edt(image)
>>> peak_idx = ski.feature.peak_local_max(
... distance, footprint=np.ones((3, 3)), labels=image
... )
>>> peak_mask = np.zeros_like(distance, dtype=bool)
>>> peak_mask[tuple(peak_idx.T)] = True
>>> markers = ski.morphology.label(peak_mask)
>>> labels_ws = ski.segmentation.watershed(
... -distance, markers, mask=image
... )

随机游走 分割

随机游走算法 (skimage.segmentation.random_walker()) 与分水岭类似,但采用了一种更“概率”的方法。它基于标签在图像中扩散的概念。

>>> # Transform markers image so that 0-valued pixels are to
>>> # be labelled, and -1-valued pixels represent background
>>> markers[~image] = -1
>>> labels_rw = ski.segmentation.random_walker(image, markers)
../../_images/sphx_glr_plot_segmentations_001.png

3.3.7. 测量区域属性

示例:计算两个分割区域的大小和周长。

>>> properties = ski.measure.regionprops(labels_rw)
>>> [float(prop.area) for prop in properties]
[770.0, 1168.0]
>>> [prop.perimeter for prop in properties]
[np.float64(100.91...), np.float64(126.81...)]

另请参阅

对于某些属性,函数也在 scipy.ndimage.measurements 中以不同的 API 提供(返回一个列表)。

3.3.8. 数据可视化和交互

在测试给定的处理管道时,有意义的可视化很有用。

一些图像处理操作

>>> coins = ski.data.coins()
>>> mask = coins > ski.filters.threshold_otsu(coins)
>>> clean_border = ski.segmentation.clear_border(mask)

可视化二进制结果

>>> plt.figure()
<Figure size ... with 0 Axes>
>>> plt.imshow(clean_border, cmap='gray')
<matplotlib.image.AxesImage object at 0x...>

可视化轮廓

>>> plt.figure()
<Figure size ... with 0 Axes>
>>> plt.imshow(coins, cmap='gray')
<matplotlib.image.AxesImage object at 0x...>
>>> plt.contour(clean_border, [0.5])
<matplotlib.contour.QuadContourSet ...>

使用 skimage 专用实用程序函数

>>> coins_edges = ski.segmentation.mark_boundaries(
... coins, clean_border.astype(int)
... )
../../_images/sphx_glr_plot_boundaries_001.png

3.3.9. 计算机视觉的特征提取

可以从图像中提取几何或纹理描述符,以便

  • 对图像的部分进行分类(例如天空与建筑物)

  • 匹配不同图像的部分(例如用于目标检测)

  • 以及许多其他应用 计算机视觉

示例:使用 Harris 检测器检测角点

tform = ski.transform.AffineTransform(
scale=(1.3, 1.1), rotation=1, shear=0.7,
translation=(210, 50)
)
image = ski.transform.warp(
data.checkerboard(), tform.inverse, output_shape=(350, 350)
)
coords = ski.feature.corner_peaks(
ski.feature.corner_harris(image), min_distance=5
)
coords_subpix = ski.feature.corner_subpix(
image, coords, window_size=13
)
../../_images/sphx_glr_plot_features_001.png

(此示例取自 scikit-image 中的 plot_corner 示例)

然后可以使用兴趣点(如角点)在不同的图像中匹配物体,如 scikit-image 的 plot_matching 示例中所述。

3.3.10. 完整代码示例

3.3.11. scikit-image 章示例

创建图像

创建图像

显示简单图像

显示简单图像

整数会溢出

整数会溢出

均衡图像的直方图

均衡图像的直方图

使用 Sobel 滤波器计算水平梯度

使用 Sobel 滤波器计算水平梯度

分割轮廓

分割轮廓

Otsu 阈值法

Otsu 阈值法

仿射变换

仿射变换

标记图像的连通分量

标记图像的连通分量

各种降噪滤波器

各种降噪滤波器

分水岭和随机游走用于分割

分水岭和随机游走用于分割

Sphinx-Gallery 生成的画廊