5. 图像数据类型及其含义#

skimage 中,图像就是简单的 numpy 数组,它们支持多种数据类型 [1],即“dtypes”。为了避免扭曲图像强度(参见 Rescaling intensity values),我们假设图像使用以下 dtype 范围:

数据类型

范围

uint8

0 到 255

uint16

0 到 65535

uint32

0 到 232 - 1

浮动

-1 到 1 或 0 到 1

int8

-128 到 127

int16

-32768 到 32767

int32

-231 到 231 - 1

请注意,浮点图像应限制在 -1 到 1 的范围内,尽管数据类型本身可以超出此范围;另一方面,所有整数数据类型都有可以跨越整个数据类型范围的像素强度。除了少数例外,64 位 (u)int 图像不受支持

skimage 中的函数设计为可以接受这些数据类型中的任何一种,但是,为了效率,*可能会返回不同数据类型的图像*(参见 输出类型)。如果你需要特定的数据类型,skimage 提供了实用函数来转换数据类型并正确地重新调整图像强度(参见 输入类型)。你**绝不应该**在图像上使用 astype,因为它违反了关于数据类型范围的这些假设:

>>> import numpy as np
>>> import skimage as ski
>>> image = np.arange(0, 50, 10, dtype=np.uint8)
>>> print(image.astype(float)) # These float values are out of range.
[  0.  10.  20.  30.  40.]
>>> print(ski.util.img_as_float(image))
[ 0.          0.03921569  0.07843137  0.11764706  0.15686275]

5.1. 输入类型#

尽管我们旨在保留输入图像的数据范围和类型,但某些函数可能仅支持这些数据类型的一个子集。在这种情况下,如果可能,输入将被转换为所需类型,并且如果需要内存复制,则会在日志中打印一条警告消息。类型要求应在文档字符串中注明。

主包中的以下实用函数可供开发者和用户使用:

函数名称

描述

img_as_float

转换为浮点数(整数类型变为64位浮点数)

img_as_ubyte

转换为8位无符号整数。

img_as_uint

转换为16位无符号整数。

img_as_int

转换为16位整数。

这些函数将图像转换为所需的 dtype 并 适当地调整其值:

>>> import skimage as ski
>>> image = np.array([0, 0.5, 1], dtype=float)
>>> ski.util.img_as_ubyte(image)
array([  0, 128, 255], dtype=uint8)

小心!这些转换可能会导致精度损失,因为8位不能容纳与64位相同数量的信息:

>>> image = np.array([0, 0.5, 0.503, 1], dtype=float)
>>> ski.util.img_as_ubyte(image)
array([  0, 128, 128, 255], dtype=uint8)

注意,skimage.util.img_as_float() 将保留浮点类型的精度,并且不会自动重新调整浮点输入的范围。

此外,一些函数接受 preserve_range 参数,在这种情况下,范围转换虽然方便但并非必要。例如,skimage.transform.warp() 中的插值需要一个类型为浮点数的图像,其范围应在 [0, 1] 之间。因此,默认情况下,输入图像将被重新缩放到此范围。然而,在某些情况下,图像值代表物理测量值,如温度或降雨量,用户不希望这些值被重新缩放。使用 preserve_range=True 时,数据的原始范围将被保留,尽管输出是浮点图像。用户必须确保这种非标准图像能被下游函数正确处理,这些函数可能期望图像在 [0, 1] 范围内。通常,除非函数有 preserve_range=False 关键字参数,否则浮点输入不会自动重新缩放。

>>> image = ski.data.coins()
>>> image.dtype, image.min(), image.max(), image.shape
(dtype('uint8'), 1, 252, (303, 384))
>>> rescaled = ski.transform.rescale(image, 0.5)
>>> (rescaled.dtype, np.round(rescaled.min(), 4),
...  np.round(rescaled.max(), 4), rescaled.shape)
(dtype('float64'), 0.0147, 0.9456, (152, 192))
>>> rescaled = ski.transform.rescale(image, 0.5, preserve_range=True)
>>> (rescaled.dtype, np.round(rescaled.min()),
...  np.round(rescaled.max()), rescaled.shape)
(dtype('float64'), 4.0, 241.0, (152, 192))

5.2. 输出类型#

函数的输出类型由函数作者决定,并为了用户的利益而记录。虽然这要求用户显式地将输出转换为所需的格式,但它确保不会发生不必要的数据复制。

需要特定类型输出的用户(例如,用于显示目的),可以编写:

>>> out = ski.util.img_as_uint(sobel(image))
>>> plt.imshow(out)

5.3. 使用 OpenCV#

您可能需要将使用 skimage 创建的图像与 OpenCV 一起使用,反之亦然。OpenCV 图像数据可以在 NumPy(因此也在 scikit-image 中)中访问(无需复制)。OpenCV 对彩色图像使用 BGR(而不是 scikit-image 的 RGB),其 dtype 默认为 uint8(参见 图像数据类型及其含义)。BGR 代表蓝绿红。

5.3.1. 将BGR转换为RGB或反之#

skimage 和 OpenCV 中的彩色图像有三个维度:宽度、高度和颜色。RGB 和 BGR 使用相同的颜色空间,只是颜色的顺序相反。

请注意,在 scikit-image 中,我们通常使用 而不是宽度和高度(参见 坐标约定)。

对于最后一个轴上有颜色的图像,以下指令有效地反转了颜色的顺序,行和列不受影响。

>>> image = image[:, :, ::-1]

5.3.2. 使用 OpenCV 的图像与 skimage#

如果 cv_image 是一个无符号字节的数组,skimage 将默认理解它。如果你更喜欢使用浮点图像,可以使用 img_as_float() 来转换图像:

>>> import skimage as ski
>>> image = ski.util.img_as_float(any_opencv_image)

5.3.3. 使用 skimage 中的图像与 OpenCV#

可以使用 img_as_ubyte() 实现反向操作:

>>> import skimage as ski
>>> cv_image = ski.util.img_as_ubyte(any_skimage_image)

5.4. 图像处理流水线#

这种 dtype 行为允许你将任何 skimage 函数串联起来,而不用担心图像的 dtype。另一方面,如果你想使用一个需要特定 dtype 的自定义函数,你应该调用其中一个 dtype 转换函数(这里,func1func2skimage 函数):

>>> import skimage as ski
>>> image = ski.util.img_as_float(func1(func2(image)))
>>> processed_image = custom_func(image)

更好的是,你可以在内部转换图像并使用简化的处理管道:

>>> def custom_func(image):
...     image = ski.util.img_as_float(image)
...     # do something
...
>>> processed_image = custom_func(func1(func2(image)))

5.5. 重新缩放强度值#

如果可能,函数应避免盲目拉伸图像强度(例如,将浮点图像重新缩放,使得最小和最大强度分别为0和1),因为这可能会严重扭曲图像。例如,如果你在寻找暗图像中的明亮标记,可能会有一个没有标记的图像;将其输入强度拉伸到全范围会使背景噪声看起来像标记。

然而,有时你会有一些图像应该覆盖整个强度范围,但实际上并没有。例如,一些相机存储的图像每个像素的深度为10位、12位或14位。如果这些图像存储在一个dtype为uint16的数组中,那么图像不会扩展到完整的强度范围,因此,看起来会比实际更暗。为了纠正这一点,你可以使用 rescale_intensity() 函数来重新调整图像,使其使用完整的dtype范围:

>>> import skimage as ski
>>> image = ski.exposure.rescale_intensity(img10bit, in_range=(0, 2**10 - 1))

在这里,in_range 参数设置为 10 位图像的最大范围。默认情况下,rescale_intensity() 会将 in_range 的值拉伸以匹配 dtype 的范围。rescale_intensity() 还接受字符串作为 in_rangeout_range 的输入,因此上面的示例也可以写成:

>>> image = ski.exposure.rescale_intensity(img10bit, in_range='uint10')

5.6. 关于负值的说明#

人们经常在有符号的数据类型中表示图像,尽管他们只操作图像的正值(例如,在int8图像中只使用0-127)。因此,转换函数*只将正值*在无符号数据类型的整个范围内展开。换句话说,从有符号到无符号数据类型转换时,负值会被裁剪为0。(在有符号数据类型之间转换时,负值会被保留。)为了避免这种裁剪行为,你应该在转换前重新调整你的图像:

>>> image = ski.exposure.rescale_intensity(img_int32, out_range=(0, 2**31 - 1))
>>> img_uint8 = ski.util.img_as_ubyte(image)

这种行为是对称的:无符号数据类型的值仅分布在有符号数据类型的正数范围内。

5.7. 参考文献#