图像金字塔是图像多尺度表达的一种。
尺度,顾名思义,可以理解为图像的尺寸和分辨率。处理图像时,经常对源图像的尺寸进行缩放变换,进而变换为适合我们后续处理的大小的目标图像。这个对尺寸进行放大缩小的变换过程,称之为尺度调整。
图像金字塔则是图像多尺度调整表达的一种重要的方式,图像金字塔方法的原理是:将参加整合的每幅图像分解为拥有多个尺度的金字塔图像序列,越低分辨率的图像,位于金字塔的越上层,反之,越高分辨率的图像,则位于金字塔的越下层。金字塔的层数,则由下而上分别为0, 1, 2, …, N.
每一层的图像的大小都为上一层的1/4。
把这些不同尺度的金字塔图像按照上述规则组合起来,则得到一个形如金字塔的结构,即称为图像金字塔。
图像金字塔最初用于机器视觉和图像压缩,主要用于图像的分割和融合。
有两种类型的金字塔经常出现在文献和应用当中,它们分别是:
在图像向下取样中,一般分两步:
(1) 对图像
G
i
G_i
Gi进行高斯卷积核(高斯滤波);
(2) 删除所有的偶数行和列。
在第一步中,使用的高斯卷积核为:
1
16
[
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
]
\frac{1}{16}\left[
卷积完成后,将偶数行和偶数列抽走删除,即得到下一层图像。
若是向上取样,一般也分两步:
(1) 对图像
G
i
+
1
G_{i+1}
Gi+1 增加偶数行和偶数列,都用0填充;
(2) 对图像进行高斯卷积,插值时滤波器乘以系数4。
在高斯金字塔下采样的过程中,可以看出图像信息出现了损失。这意味着如果下采样之后再上采样,图像并不能还原。图像将丢失掉部分高频信息。
此时如果想要保留图像的高频信息,则需要引入拉普拉斯金字塔:
L
i
=
G
i
−
P
y
r
U
p
(
P
y
r
D
o
w
n
(
G
i
)
)
L_i = G_i - PyrUp(PyrDown(G_i))
Li=Gi−PyrUp(PyrDown(Gi))
即第i层的拉普拉斯金字塔图像,先计算高斯金字塔下采样再上采样的图像
P
y
r
U
p
(
P
y
r
D
o
w
n
(
G
i
)
)
PyrUp(PyrDown(G_i))
PyrUp(PyrDown(Gi)),然后用第i层的原图
G
i
G_i
Gi减去它。
OpenCV中的pyrUp函数原型为:
void cv::pyrUp(InputArray src,
OutputArray dst,
const Size & dstsize = Size(),
int borderType = BORDER_DEFAULT)
pyrDown函数原型为:
void cv::pyrDown(InputArray src,
OutputArray dst,
const Size & dstSize = Size(),
int borderType = BORDER_DEFAULT)
其中,src为原始图像,dst为上采样/下采样的目标图像, dstSize为目标图像的尺寸, borderType为边界类型,这里默认为BORDER_DEFAULT.
原理部分讲述结束,下面进入OpenCV的例程:
OpenCV的Sample文件夹下有关于图像金字塔的例程:
samples/cpp/tutorial_code/ImgProc/Pyramids/Pyramids.cpp
#include "iostream"
#include "opencv2/imgproc.hpp"
#include "opencv2/imgcodecs.hpp"
#include "opencv2/highgui.hpp"
using namespace std;
using namespace cv;
const char* window_name = "Pyramids Demo";
int main(int argc, char** argv)
{
cout << "\n Zoom In-Out demo \n "
"------------------ \n"
" * [i] -> Zoom in \n"
" * [o] -> Zoom out \n"
" * [ESC] -> Close program \n" << endl;
const char* filename = argc >= 2 ? argv[1] : "../data/chicky_512.png";
// Loads an image
Mat src = imread(samples::findFile(filename));
// Check if image is loaded fine
if (src.empty()) {
printf(" Error opening image\n");
printf(" Program Arguments: [image_name -- default chicky_512.png] \n");
return EXIT_FAILURE;
}
for (;;)
{
imshow(window_name, src);
char c = (char)waitKey(0);
if (c == 27)
{
break;
}
else if (c == 'i')
{
pyrUp(src, src, Size(src.cols * 2, src.rows * 2));
printf("** Zoom In: Image x 2 \n");
}
else if (c == 'o')
{
pyrDown(src, src, Size(src.cols / 2, src.rows / 2));
printf("** Zoom Out: Image / 2 \n");
}
}
return EXIT_SUCCESS;
}
注意以上源码是被小白修改了的,因为小白的工程里面用于放缩的图片的位置是在data文件夹下
const char* filename = argc >= 2 ? argv[1] : "../data/chicky_512.png";
这个程序打开后会显示一幅小狗的图像,并一直等待键盘输入。
如果你输入一个"i",则图像将被放大,即Zoom In;
如果你输入一个"o",则图像将被缩小,即Zoom Out;
直到你输入一个"ESC",程序退出。
但是这个程序只使用了高斯金字塔,反复测试几次你就会发现:如果先缩小了很多倍,再重新放大,那么由于下采样时总是在丢失图像信息,多操作几次之后,小狗的面目就变得非常模糊。
下图就是缩小了3次之后再放大到原始尺寸时所看到的小狗:
那么有没有办法把这个例程改造成可以带有拉普拉斯金字塔,可以在放缩后仍然还原原始分辨率细节的小狗图呢?当然有。
使用STL中的stack来保存拉普拉斯金字塔图像,则可以简单地实现分辨率还原的功能:
#include "iostream"
#include "opencv2/imgproc.hpp"
#include "opencv2/imgcodecs.hpp"
#include "opencv2/highgui.hpp"
#include
using namespace std;
using namespace cv;
const char* window_name = "Pyramids Demo";
int main(int argc, char** argv)
{
cout << "\n Zoom In-Out demo \n "
"------------------ \n"
" * [i] -> Zoom in \n"
" * [o] -> Zoom out \n"
" * [ESC] -> Close program \n" << endl;
const char* filename = argc >= 2 ? argv[1] : "../data/chicky_512.png";
// 加载一幅图像
Mat src = imread(samples::findFile(filename));
int picMaxWidth = src.cols;
int picMaxHeight = src.rows;
// 检查是否加载成功
if (src.empty()) {
printf(" Error opening image\n");
printf(" Program Arguments: [image_name -- default chicky_512.png] \n");
return EXIT_FAILURE;
}
// 用于存储拉普拉斯金字塔的栈结构
stack<Mat> stackMat;
for (;;)
{
imshow(window_name, src);
char c = (char)waitKey(0);
if (c == 27) // ESC Key 则退出
{
break;
}
// 放大图像但不允许超过原图大小
else if (c == 'i')
{
if (src.cols >= picMaxWidth)
{
printf("** cannot Zoom In: Image reaches max size!\n");
continue;
}
pyrUp(src, src, Size(src.cols * 2, src.rows * 2));
if (!stackMat.empty())
{
Mat temp;
temp = stackMat.top();
src.convertTo(src, CV_16SC3); // 对图像格式进行转换,目的是进行负差值的正确计算
src += temp; // 取栈顶的拉普拉斯金字塔图像进行原图还原
src.convertTo(src, CV_8UC3); // 将图像还原成8位3通道,便于显示
stackMat.pop();
}
printf("** Zoom In: Image x 2 \n");
}
// 缩小图像
else if (c == 'o')
{
Mat temp1, temp2;
src.copyTo(temp1);
pyrDown(src, src, Size(src.cols / 2, src.rows / 2));
pyrUp(src, temp2, Size(src.cols * 2, src.rows * 2));
// 对图像进行格式转换, 目的是得到差值图像的正确负值
temp1.convertTo(temp1, CV_16SC3);
temp2.convertTo(temp2, CV_16SC3);
temp1 -= temp2; // 计算拉普拉斯金字塔差值图像
stackMat.push(temp1); // 将拉普拉斯金字塔压进栈中
printf("** Zoom Out: Image / 2 \n");
}
}
return EXIT_SUCCESS;
}
这段代码中:
细节其实挺多,针对第三点和第四点简单铺开讲一下:
如果不进行图像类型的转换(大家可以试试把那几句图像类型转换的代码注释掉),那么在计算拉普拉斯金字塔时,图像差值将无法保留负值,这将会导致在反复缩放过程中,图像的部分信息发生丢失,小狗图中的高频细节会越来越亮,像下图中一样;
如果不限制图像的放缩倍率,那么当图像先放大到原始图像尺寸的2倍,再重新缩小到原始尺寸时,图像先经历了一次上采样,放大时的插值部分属于无中生有,再重新下采样时,就引入了信息失真,因此对原例程进行了修改,使其无法将图像放大到超过原始尺寸,这样就保证了在图像回到原始尺寸大小时,其图像细节能与原始图像保持一致。下图是如果不限制放大倍率,在放大超过原始尺寸后再重新回到原始尺寸时,图像的效果:(如果你将它和原始图像比较一下,会发现有细微的差别)
至此,小白和大家一起学习了这个例程,并对高斯图像金字塔和拉普拉斯图像金字塔有了更深层次的理解。