开门见山,直接使用 skimage 库为图像添加高斯噪声是很简单的:
import skimage
origin = skimage.io.imread("./lena.png")
noisy = skimage.util.random_noise(origin, mode='gaussian', var=0.01)
但是如果不用库函数而自己实现的话,有几个问题是值得注意的。
彩图 or 灰度图
读取图片时,图片可能是有三通道的RGB图片,也有可能是单通道的灰度图,甚至四通道的RGBA图。
通道数不同会影响图像数据的 shape
,例如: (256, 256, 3) 、(256, 256)
很多人按照MATLAB的习惯,使用 np.random.randn
来生成高斯噪声,则需要根据通道数调整参数。
# RGB
noise = sigma * np.random.randn(256, 256, 3)
# GRAY
noise = sigma * np.random.randn(256, 256)
为了通用的处理,最好使用 np.random.normal
生成高斯噪声。
noise = np.random.normal(mean, var ** 0.5, image.shape)
前两个参数分别为 均值和标准差,第三个参数为生成数据的 shape,直接将图像本身shape输入进去,更加优雅。
uint8 or float64
一般遇到的图像都是8bit的,用skimage或opencv读取后会发现数据类型是uint8的ndarray,取值范围是 [0, 255] 。
如果手贱直接在整型数据上添加高斯噪声,如:
image = io.imread("lena.png")
noise = np.random.normal(0, 10, image.shape)
noisy = image + noise
你会发现 plt.imshow
出来的是一片空白,或者有零星几个噪点。
以一个像素为例分析原因:
-
图像本身是[0, 255]的整数:[226 137 125]
-
生成的噪声是浮点数:[-2.92864248 4.04786763 12.23436435]
-
相加后最后的数据:[223.07135752 141.04786763 137.23436435]
matplotlib 的 imshow 要求输入是 (0-1 float or 0-255 int),所以上述不伦不类的数据是无法正确显示的(只显示了恰好落在0-1之间的像素)。
在很多应用中,为了方便计算,都会将图像数据转换为浮点数,float64,取值范围为 [0, 1]。
为了转换数据类型,最简单的方式是直接除以255:
image = io.imread("./lena.png")
print(image.dtype) # uint8
image = image/255
print(image.dtype) # float64
更稳妥的做法,可以使用skimage的img_to_float()
:
image = img_as_float(image)
这样再添加高斯噪声就可以正确显示。
方差 or 标准差
高斯噪声符合一个均值为0,方差为 σ 2 \sigma^2 σ2 的高斯分布。
均值为0,是保证图像的亮度不会有变化,而方差大小则决定了高斯噪声的强度。
方差/标准差越大,噪声越强。
这里有一点点初中数学的细节,就是在[0, 1]区间内, y = x y=\sqrt{x} y=x 比 y = x y=x y=x 要大。
所以,设置方差为0.1,噪声要比设置标准差为0.1大不少。注意不要用混了就可以。
是否截断(clip)
由于需要把噪声叠加到原图像中,因此叠加后的数据值就可能超出对应数据类型的取值范围。
如果用matplotlib显示超出范围的彩色图像,则可能遇到以下提示:
Clipping input data to the valid range for imshow with RGB data ([0..1] for floats or [0..255] for integers).
matplotlib自动将图片做了截断。
而不知为何,matplotlib并不会自动对灰度图进行截断,例如:
叠加噪声之后,图片数据的最小值和最大值分别为 -0.32 和 1.25,这明显超过了[0, 1]的范围。
这样显示出的图片是不正确的(中间图像),更像是重新将图像缩放到了[0, 1]范围内,就像将色阶向外扩了一样,对比度也下降了。
使用 np.clip
,将图像截断到 [0, 1]之间,如右图所示,图像明显正常了很多。
总结
完整的代码如下:
from skimage import io, img_as_float
import numpy as np
mean = 0
var = 0.01
image = io.imread("./lena.png")
image = img_as_float(image)
noise = np.random.normal(mean, var**0.5, image.shape)
noisy = image + noise
noisy = np.clip(noisy, 0.0, 1.0)
当然,上述问题在 skimage.util.random_noise
中都已解决,工程中可以直接使用。
import skimage
origin = skimage.io.imread("./lena.png")
noisy = skimage.util.random_noise(origin, mode='gaussian', var=0.01)
推荐学习skimage的源码。
参考
https://zhuanlan.zhihu.com/p/50820267
https://blog.csdn.net/u013044310/article/details/80739623
https://matplotlib.org/3.1.1/api/_as_gen/matplotlib.pyplot.imshow.html
https://github.com/scikit-image/scikit-image/blob/v0.17.2/skimage/util/noise.py#L8