图像缩放-高斯金字塔
原理
在图像处理中,经常需要将图像转化成不同的尺寸,即放大或缩小。
除了直接用resize()
函数重新设置图片尺寸,另一种常用的方法就是"图像金字塔 "。
图像金字塔是从底层的原始图像开始,通过层层放大叠加而成的金字塔形状的图像集合。
主要有两种图像金字塔:
- 高斯金字塔:常用来生成图片的缩小版本
- 拉普拉斯金字塔:常用来将图片的缩小版本放大
高斯金字塔
用图片来表示高斯金字塔会更形象:
图层越高,图像越小。
图像的编号又下往上依次增大。从下往上数第 i + 1 i+1 i+1个图层,记为 G i G_{i} Gi;则第 i + 2 i+2 i+2个图层 G i + 1 G_{i+1} Gi+1比 G i G_i Gi小。
要生成 G i + 1 G_{i+1} Gi+1就要对 G i G_i Gi进行如下操作:
- 用高斯卷积核 K K K对 G i G_i Gi进行卷积运算;
K = 1 256 [ 1 4 6 4 1 4 16 24 16 4 6 24 36 24 6 4 16 24 16 4 1 4 6 4 1 ] K=\frac{1}{256} \begin{bmatrix} 1 & 4 & 6 & 4 & 1 \\ 4 & 16 & 24 &16 & 4 \\ 6 & 24 & 36 & 24 & 6 \\ 4 & 16 & 24 & 16 & 4 \\ 1 & 4 & 6 & 4 & 1 \end{bmatrix} K=2561 1464141624164624362464162416414641 - 移除所有的偶数行和偶数列
很显然,最后的结果会变成原来图片的1/4。将这个操作迭代地运用在原图 G 0 G_0 G0上,就会形成上图所示的金字塔。
其实高斯金字塔方法也可以用来放大图片。只要进行以下操作: - 将行和列都扩大到原来的2倍;
- 将上面的卷积核 K K K乘以4之后近似地模拟出填充值来填充扩大出来的行和列
注意:缩小图片实际上丢失了一部分原图的信息。
代码实现
这里使用的图片是OpenCV安装文件中自带的示例图,其路径为:"...\opencv\sources\samples\data\chicky_512.png"
放大
在OpenCV中可以pyrUp()
函数来进行高斯金字塔中的放大操作。其函数原型如下:
cpp
void cv::pyrUp(InputArray src,
OutputArray dst,
const Size& dstsize = size(),
int borderType = BORDER_DEFAULT)
第1个和第2个参数不用介绍了,分别是输入矩阵和输出矩阵。
第3个参数为放大后的尺寸,默认为Size(src.cols*2, src.rows*2)
,即行和列都放大为原来的2倍。但是如果用户要自定义放大倍数的话,需要满足以下条件:
{ ∣ d s t s i z e . w i d t h − s r c . c o l s ∗ 2 ∣ ≤ ( d s t s i z e . w i d t h m o d 2 ) ∣ d s t s i z e . h e i g h t − s r c . r o w s ∗ 2 ∣ ≤ ( d s t s i z e . h e i g h t m o d 2 ) \begin{cases} |dstsize.width - src.cols*2| \leq (dstsize.width mod 2) \\ |dstsize.height - src.rows*2| \leq (dstsize.height mod 2) \end{cases} {∣dstsize.width−src.cols∗2∣≤(dstsize.widthmod2)∣dstsize.height−src.rows∗2∣≤(dstsize.heightmod2)
乍一看有点复杂,我们来仔细分析一下:
首先看两个不等式的右边,因为图片的宽和高一般都是整数,所以无论是宽还是高,除以2的余数都只有两种情况:0或1。
- 不等式右边为0:说明放大后的图片的宽或高是偶数,且等式左边必须小于等于0;又因为不等式左边为一个绝对值的结果,那么只有一种可能------不等式左边等于0,即 d s t s i z e . w i d t h = s r c . c o l s ∗ 2 dstsize.width = src.cols*2 dstsize.width=src.cols∗2或 d s t s i z e . h e i g h t = s r c . r o w s ∗ 2 dstsize.height = src.rows*2 dstsize.height=src.rows∗2,也就是这个参数的默认情况。
- 不等式右边为1:说明放大的图片的宽或高是奇数,且等式左边必须小于等于1;因为奇数减偶数只能等于奇数,所以等式左边的结果只能为1,即只能是 s r c . c o l s ∗ 2 + 1 src.cols * 2 + 1 src.cols∗2+1或者 s r c . r o w s ∗ 2 + 1 src.rows*2+1 src.rows∗2+1。
综上所述,第3个参数只能有两种:
- 宽和高都放大为原来的2倍,即默认情况
- 宽和高都放大为原来的2倍加1。
为什么有这种限制?答案在最后一节。
第4个参数基本使用默认值,先不用管它。
我们看看宽和高都放大为原来的宽和高的2倍的具体实现:
cpp
pyrUp(src, //原图
up_dst, //输出图
Size(src.cols*2, src.rows*2)); //放大后的尺寸:原图的2*2
一次放大后的效果:
缩小
类似的,可以使用pryDown()
函数来缩小图片。这个函数的参数与pryUp()
相同,但第3个参数,即缩小后的尺寸的具体条件不一样。
在pryDown()
中,第3个参数dstsize
默认值为Size((src.cols+1)/2, (src.rows+1)/2)
,即原图的宽和高加1的一半。如果用户要自定义缩小尺寸,需要满足以下条件:
{ ∣ d s t s i z e . w i d t h ∗ 2 − s r c . c o l s ∣ ≤ 2 ∣ d s t s i z e . h e i g h t ∗ 2 − s r c . r o w s ∣ ≤ 2 \begin{cases} |dstsize.width*2 - src.cols| \leq 2 \\ |dstsize.height*2 - src.rows| \leq 2 \end{cases} {∣dstsize.width∗2−src.cols∣≤2∣dstsize.height∗2−src.rows∣≤2
这也就是说缩小之后的图片的宽和高的2倍不能与原来的宽高相差2以上,即缩小后的图片的尺寸只有这些选择:
- 原图的宽高的一半,此时不等式左边就是0
- 原图的宽高加1的一半,即默认情况,不等式左边为1
- 原图的宽高加2的一半,此时不等式左边为2
为什么有这种限制?答案在最后一节。
第1中情况的具体用法如下:
cpp
pryDown(src, //原图
down_dst, //输出图
Size( src.cols/2, src.rows/2)); //缩小后的尺寸:原图的(1/2)*(1/2)
原图缩小一次的效果如下:
形成金字塔
要形成金字塔不能只进行单次缩放的操作,需要进行迭代。
正是因为要迭代,所以前面才会对pyrUp()
和pyrDown()
函数的第3个参数有那么严格的限制。如果缩放的尺寸没有限制,那可以1次迭代就放大100或缩小100倍,那很有可能就没有进行第2次迭代的必要了。要形成有意义的金字塔,缩放的比例必须要控制在一个相对较小的范围内,而这两个函数都将比例限制在了2或1/2左右。
如果我们将迭代次数限定为5次,我们可以进行这样的缩小图片的迭代:
cpp
#include <opencv2/imgproc.hpp>
#include <opencv2/imgcodecs.hpp>
#include <opencv2/highgui.hpp>
using namespace cv;
using namespace std;
int main() {
Mat src{ imread("chicky_512.png") };
//缩小迭代
String winName{ "缩小" };
for (int i{ 0 }; i < 5; i++) {
imshow(winName+to_string(i), src);
pyrDown(src,
src,
Size(src.cols / 2, src.rows / 2));
}
waitKey(0);
}
结果如下: