使用Numpy和Opencv完成图像的基本数据分析(Part III)
摘要: 使用Numpy和Opencv完成图像的基本数据分析第三部分,主要包含图像变换、卷积操作等。
引言
本文是使用python进行图像基本处理系列的第三部分,在本人之前的文章里介绍了一些非常基本的图像分析操作,见文章《使用Numpy和Opencv完成图像的基本数据分析Part I》和《使用Numpy和Opencv完成图像的基本数据分析 Part II》,下面我们将继续介绍一些有关图像处理的好玩内容。
本文介绍的内容基本反映了我本人学习的图像处理课程中的内容,并不会加入任何工程项目中的图像处理内容,本文目的是尝试实现一些基本图像处理技术的基础知识,出于这个原因,本文继续使用 SciKit-Image,numpy数据包执行大多数的操作,此外,还会时不时的使用其他类型的工具库,比如图像处理中常用的OpenCV等:
本系列分为三个部分,分别为part I、part II以及part III。刚开始想把这个系列分成两个部分,但由于内容丰富且各种处理操作获得的结果是令人着迷,因此不得不把它分成三个部分。系列所有的源代码地址:GitHub-Image-Processing-Python。
在上一篇文章中,我们已经完成了以下一些基本操作。为了跟上今天的内容,回顾一下之前的基本操作:
- 导入图像并观察其属性
- 拆分图层
- 灰度处理
- 对像素值使用逻辑运算符
- 使用逻辑运算符进行掩码
现在开始本节的内容:
强度变换|Intensity Transformation
首先导入一张图像作为开始:
%matplotlibinline import imageio import matplotlib.pyplot as plt import warnings import matplotlib.cbook warnings.filterwarnings("ignore",category=matplotlib.cbook.mplDeprecation) pic=imageio.imread('img/parrot.jpg') plt.figure(figsize=(6,6)) plt.imshow(pic); plt.axis('off');
图像底片|Image Negative
强度变换函数在数学上定义为:
S = T(r)
其中r是输入图像的像素,S是输出图像的像素,T是一个转换函数,它将r的每个像素值映射到s中对应的像素值。
负变换,即恒等变换的逆。在负变换中,输入图像的每个像素值从L-1中减去并映射到输出图像上。
在这种情况下,完成以下转换:
S =(L-1)-r
因此,每个像素值都减去255。这样的操作导致的结果是,较亮的像素变暗,较暗的图像变亮,类似于图像底片。
negative =255- pic # neg = (L-1) - img plt.figure(figsize= (6,6)) plt.imshow(negative); plt.axis('off');
对数变换|Log transformation
对数转换可以通过以下公式定义:
s = c *log(r + 1)
其中s和r是输出和输入图像的像素值,c是常数。输入图像的每个像素值都会加1,之后再进行对数操作,这是因为如果图像中的像素值为0时,log(0)的结果等于无穷大。因此,为了避免这种情况的发生,输入图像中的每个像素值都加1,使最小像素值至少为1。
在对数变换过程中,与较高像素值相比,图像中的低像素被扩展。较高的像素值在对数变换中被压缩,这导致图像增强。
对数变换中的c值调整了我们想要的增强程度:
%matplotlibinline import imageio import numpyasnp import matplotlib.pyplotasplt pic=imageio.imread('img/parrot.jpg') gray=lambdargb:np.dot(rgb[...,:3],[0.299,0.587,0.114]) gray=gray(pic) ''' log transform -> s = c*log(1+r) So, we calculate constant c to estimate s -> c = (L-1)/log(1+|I_max|) ''' max_=np.max(gray) def log_transform(): return(255/np.log(1+max_))*np.log(1+gray) plt.figure(figsize=(5,5)) plt.imshow(log_transform(),cmap=plt.get_cmap(name='gray')) plt.axis('off');
伽马校正| Gamma Correction
伽马校正,或通常简称为伽玛,是用于对视频或静止图像系统中的亮度或三刺激值进行编码和解码的非线性操作,伽玛校正也称为幂律变换。首先,图像的像素值大小范围必须从0~255被缩放至0~1.0。然后,通过应用以下等式获得伽马校正后的输出图像:
Vo = Vi ^(1 / G)
其中Vi是我们的输入图像,G是设置的伽玛值,然后将输出图像Vo缩放回0-255范围。
对于伽马值而言,G <1有时被称为编码伽玛,并且利用该压缩幂律非线性进行编码的过程被称为伽马压缩; Gamma值小于1会将图像移向光谱的较暗端。
相反,伽马值G> 1被称为解码伽马,并且膨胀幂律非线性的应用被称为伽马展开。Gamma值大于1将使图像显得更亮。将伽玛值设置为G = 1时对输入图像没有影响:
import imageio import matplotlib.pyplotasplt # Gamma encoding pic=image io.imread('img/parrot.jpg') gamma=2.2# Gamma < 1 ~ Dark ; Gamma > 1 ~ Bright gamma_correction=((pic/255)**(1/gamma)) plt.figure(figsize=(5,5)) plt.imshow(gamma_correction) plt.axis('off');
伽马校正的原因|Reason for Gamma Correction
我们应用伽马校正的原因是,由于我们的眼睛感知颜色和亮度这一过程与数码相机中的传感器的工作原理不同。当数码相机上的传感器获得两倍的光子量时,信号会加倍。但是,我们人类的眼睛的工作原理与这不同,当我们的眼睛感知两倍的光量时,视野中只有一小部分显得更亮。因此,数码相机在亮度之间具有线性关系,而我们人类的眼睛具有非线性关系。为了解释这种关系,我们应用伽玛校正。
还有一些其他的线性变换函数,比如:
- 对比度拉伸(Contrast Stretching)
- 强度切片(Intensity-Level Slicing)
- 位平面切片(Bit-Plane Slicing)
卷积|Convolution
在上一篇文章中,对卷积操作作了简要讨论。当计算机看到图像时,它看到不是一整幅图像,它的眼里看到的只是一个像素值数组。假设读取一个32X32大小的彩色图像,根据图像的分辨率和大小,计算机它将看到一个32 x 32 x 3维的数字数组,其中3表示RGB值或三通道。假设现在我们有一个PNG格式的彩色图像,它的大小是480 x 480。将其读入后,其表示数组将是480 x 480 x 3维。数组中的所有的每个数字值范围都在0到255之间,它描述的是那个点的像素强度。
就像我们刚才提到的那样,假设输入图像是一个32 x 32 x 3的像素值数组,解释卷积的最佳方法是想象一个闪烁在图像左上方的手电筒。假设手电筒照射区域大小为3 x 3。现在,让我们假设这个手电筒滑过输入图像的所有区域。在机器学习术语中,这个手电筒被称为过滤器(filter)或内核(kernel),或者有时被称为权重(weights) 或 掩模(mask),它所照射的区域称为 感受野(receptive field)。
现在,此过滤器也是一个数字数组,数组中的数字称为权重或参数,在这里要着重注意一点,此过滤器的深度必须与输入图像的深度相同,即通道数相同,因此此过滤器的尺寸为3 x 3 x 3。
图像内核 或过滤器是一个小矩阵,用于应用我们可能在Photoshop或Gimp中找到的效果,例如模糊、锐化、轮廓或浮雕等。此外,它们还被用于在机器学习中进行图像特征提取(CNN),这是一种用于确定图像最重要部分的技术。更多相关信息,请查看Gimp关于使用Image kernel的文档,我们可以该文档中找到最常见的内核列表 。
现在,让我们将过滤器放在图像的左上角。当滤波器围绕输入图像滑动或卷积时,它将滤波器中的值乘以图像的原始像素值(也称为计算元素乘法)。这些乘法操作最后都会求和,所以卷积操作后只得到一个数字值。请记住,此数字仅代表过滤器位于图像的左上角。现在,我们对输入图像上的每个位置重复此过程,移动过滤器使其与图像矩阵的每个像素值进行卷积操作,这个过程需要设置移动步幅,依此类推,完成整幅图像的卷积操作。输入图中的每个唯一位置都会生成一个数字。步幅的取值一般为1,也可以取其它大小的值,但我们关心的是它是否适合输入图像。
过滤器滑过输入图像上的所有位置后,我们会发现,我们剩下的是一个30 x 30 x 1的数组,我们将其称为激活图 或特征图。将3 x 3过滤器可以放在32 x 32输入图像上,可以得到30 x 30大小的阵列,原因是有300个不同的位置,这900个数字映射到30 x 30阵列。我们可以通过以下方式计算卷积图像后图像的大小:
- 卷积:(N-F)/ S + 1
其中N和F分别代表输入图像大小和卷积核大小,S代表步幅或步长。因此,对于上述情况,输出图像的大小将是
- 32-31 + 1 = 30
假设我们有一个3x3滤波器,在5x5大小的矩阵上进行卷积,根据等式,我们应该得到一个3x3矩阵,现在让我们看一下:
此外,我们实际上使用的过滤器不止一个,过滤器的数量自己设定,假设过滤器的数量设置为n,则我们的输出将是28x28xn大小(其中n是特征图的数量 )。
通过使用更多的过滤器,我们能够更好地保留空间维度信息。
然而,对于图像矩阵边界上的像素,卷积核的一些元素移动时会出现在图像矩阵之外,因此不具有来自图像矩阵的任何对应元素。在这种情况下,我们可以消除这些位置的卷积运算,最终输出矩阵大小将会小于输入图像,或者我们可以对输入图像矩阵进行填充(padding),以保证输出图像大小维度不变。
为了保持本系列的简洁而保持内容的完整性,本文提供了全部的资源链接,在其中更详细地解释了有关内容。
下面,让我们首先将一些自定义卷积核个数的窗口应用于图像中,这可以通过平均每个像素值与附近的像素值来处理图像:
%%time import numpy as np import imageio import matplotlib.pyplot as plt from scipy.signal import convolve2d def Convolution(image, kernel): conv_bucket= [] for d in range(image.ndim): conv_channel= convolve2d(image[:,:,d], kernel, mode="same", boundary="symm") conv_bucket.append(conv_channel) returnnp.stack(conv_bucket, axis=2).astype("uint8") kernel_sizes= [9,15,30,60] fig, axs=plt.subplots(nrows=1, ncols=len(kernel_sizes), figsize=(15,15)); pic =imageio.imread('img:/parrot.jpg') for k, ax in zip(kernel_sizes, axs): kernel =np.ones((k,k)) kernel /=np.sum(kernel) ax.imshow(Convolution(pic, kernel)); ax.set_title("Convolved By Kernel: {}".format(k)); ax.set_axis_off(); Wall time: 43.5 s
更多内容可以在此查看,其中已经深入讨论了各种类型的内核,并展示了它们之间的差异。
作者信息
Mohammed Innat,机器学习和数据科学研究者
本文由阿里云云栖社区组织翻译。
文章原标题《Basic Image Data Analysis Using Numpy and OpenCV – Part 3》,译者:海棠,审校:Uncle_LLD。