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 的范围内,即使数据类型本身可以超出此范围;另一方面,所有整数 dtypes 都具有可以跨越整个数据类型范围的像素强度。除少数例外,不支持 64 位 (u)int 图像。
skimage
中的函数被设计为接受任何这些 dtypes,但是,为了提高效率,可能会返回不同 dtype 的图像(参见 输出类型)。如果您需要特定的 dtype,skimage
提供了转换 dtypes 并正确重缩放图像强度的实用函数(参见 输入类型)。您应该永远不要使用图像上的 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. 输入类型#
尽管我们的目标是保留输入图像的数据范围和类型,但函数可能仅支持这些数据类型的一个子集。在这种情况下,输入将被转换为所需的类型(如果可能),并且如果需要内存复制,则会在日志中打印警告消息。类型要求应在 docstring 中注明。
主软件包中的以下实用函数可供开发人员和用户使用
函数名 |
描述 |
---|---|
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
,即使输出是浮点图像,也将保留数据的原始范围。然后,用户必须确保下游函数正确处理此非标准图像,这些函数可能期望图像在 [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)
这种行为是对称的:无符号数据类型中的值仅在有符号数据类型的正值范围内扩展。