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 转换函数(这里,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)
这种行为是对称的:无符号数据类型的值仅分布在有符号数据类型的正数范围内。