5. 图像数据类型及其含义#
在 skimage
中,图像仅仅是 numpy 数组,它支持各种数据类型 [1],即“dtypes”。为了避免扭曲图像强度(参见 重新缩放强度值),我们假设图像使用以下 dtype 范围
数据类型 |
范围 |
---|---|
uint8 |
0 到 255 |
uint16 |
0 到 65535 |
uint32 |
0 到 232 - 1 |
float |
-1 到 1 或 0 到 1 |
int8 |
-128 到 127 |
int16 |
-32768 到 32767 |
int32 |
-231 到 231 - 1 |
请注意,即使浮点类型本身可以超过此范围,浮点图像也应限制在 -1 到 1 的范围内;另一方面,所有整数 dtype 的像素强度都可以跨越整个数据类型范围。除少数例外情况外,不支持 64 位 (u)int 图像。
skimage
中的函数设计为可以接受任何这些 dtype,但为了提高效率,可能会返回不同 dtype 的图像(参见 输出类型)。如果您需要特定的 dtype,skimage
提供了实用程序函数来转换 dtype 并正确重新缩放图像强度(参见 输入类型)。您永远不要对图像使用 astype
,因为它违反了关于 dtype 范围的这些假设
>>> 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 位 uint。 |
img_as_uint |
转换为 16 位 uint。 |
img_as_int |
转换为 16 位 int。 |
这些函数将图像转换为所需的 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()
中的插值需要类型为 float 的图像,该图像的范围应在 [0, 1] 中。因此,默认情况下,输入图像将重新缩放至此范围。但是,在某些情况下,图像值表示物理测量值,例如温度或降雨量值,用户不希望对其进行重新缩放。使用 preserve_range=True
,将保留数据的原始范围,即使输出是 float 图像。然后,用户必须确保此非标准图像由下游函数正确处理,这些函数可能期望 [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 一起使用,反之亦然。可以在 NumPy(以及 scikit-image)中访问(无需复制)OpenCV 图像数据。OpenCV 对彩色图像使用 BGR(而不是 scikit-image 的 RGB),并且其 dtype 默认情况下为 uint8(参见 图像数据类型及其含义)。BGR 代表蓝绿红。
5.3.1. 将 BGR 转换为 RGB 或反之亦然#
skimage
和 OpenCV 中的彩色图像具有 3 个维度:宽度、高度和颜色。RGB 和 BGR 使用相同的颜色空间,除了颜色的顺序相反。
请注意,在 scikit-image
中,我们通常指的是 rows
和 columns
而不是宽度和高度(参见 坐标约定)。
对于颜色位于最后一个轴上的图像,以下指令有效地反转了颜色的顺序,而行和列不受影响。
>>> 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 转换函数(此处,func1
和 func2
是 skimage
函数)
>>> 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_range
和 out_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)
这种行为是对称的:无符号数据类型中的值仅扩展到有符号数据类型的正数范围。