7.3 重映射
本节中,我们主要一起了解重映射的概念,以及OpenCV中相关的实现函数remap()。
7.3.1 重映射的概念
重映射,就是把一幅图像中某位置的像素放置到另一个图片指定位置的过程。为了完成映射过程,需要获得一些插值为非整数像素的坐标,因为源图像与目标图像的像素坐标不是一一对应的。一般情况下,我们通过重映射来表达每个像素的位置(x,y),像这样:
图像会按照x轴方向发生翻转。那么,源图像和效果图分别如图7.27和7.28所示。
在OpenCV中,可以使用函数remap()来实现简单重映射,下面我们就一起来看看这个函数。
7.3.2 实现重映射:remap()函数
remap()函数会根据指定的映射形式,将源图像进行重映射几何变换,基于的公式如下:
需要注意,此函数不支持就地(in-place)操作。看看其原型和参数。
void remap( InputArray src, OutputArray dst,
InputArray map1, InputArray map2,
int interpolation, int borderMode = BORDER_CONSTANT,
const Scalar& borderValue = Scalar());
● 第一个参数,InputArray类型的src,输入图像,即源图像,填Mat类的对象即可,且需为单通道8位或者浮点型图像。
● 第二个参数,OutputArray类型的dst,函数调用后的运算结果存在这里,即这个参数用于存放函数调用后的输出结果,需和源图片有一样的尺寸和类型。
● 第三个参数,InputArray类型的map1,它有两种可能的表示对象。
■表示点(x, y)的第一个映射。
■表示CV_16SC2、CV_32FC1或CV_32FC2类型的X值。
● 第四个参数,InputArray类型的map2,同样,它也有两种可能的表示对象,而且它会根据map1来确定表示那种对象。
■若map1表示点(x, y)时。这个参数不代表任何值。
■表示CV_16UC1,CV_32FC1类型的Y值(第二个值)。
● 第五个参数,int类型的interpolation,插值方式,之前的resize()函数中有讲到,需要注意,resize()函数中提到的INTER_AREA插值方式在这里是不支持的,所以可选的插值方式如下(需要注意,这些宏相应的OpenCV2版为在它们的宏名称前面加上“CV_”前缀,比如“INTER_LINEAR”的OpenCV2版为“CV_INTER_LINEAR”):
■INTER_NEAREST——最近邻插值
■INTER_LINEAR——双线性插值(默认值)
■INTER_CUBIC——双三次样条插值(逾4×4像素邻域内的双三次插值)
■INTER_LANCZOS4——Lanczos插值(逾8×8像素邻域的Lanczos插值)
● 第六个参数,int类型的borderMode,边界模式,有默认值BORDER_CONSTANT,表示目标图像中“离群点(outliers)”的像素值不会被此函数修改。
● 第七个参数,const Scalar&类型的borderValue,当有常数边界时使用的值,其有默认值Scalar(),即默认值为0。
7.3.3 基础示例程序:基本重映射
下面将贴出精简后的以remap函数为核心的示例程序,方便大家快速掌握remap函数的使用方法。
/* @File : 65_remap.cpp
* @Brief : 示例程序65
* @Details : remap函数用法示例
* @Date : 2015-11-01
* @OpenCV Version : 4.8.0
* @Development Tools : Windows 11 64bit && Visual Studio 2017
* @Modify : 2024-04-27
*/
#include "opencv2/highgui/highgui.hpp"
#include "opencv2/imgproc/imgproc.hpp"
#include <iostream>
using namespace cv;
int main( )
{
//【0】变量定义
Mat srcImage, dstImage;
Mat map_x, map_y;
//【1】载入原始图
srcImage = imread( "1.jpg", 1 );
if(!srcImage.data ) { printf("读取图片错误,请确定目录下是否有imread函数指定的图片存在~! \n"); return false; }
imshow("原始图",srcImage);
//【2】创建和原始图一样的效果图,x重映射图,y重映射图
dstImage.create( srcImage.size(), srcImage.type() );
map_x.create( srcImage.size(), CV_32FC1 );
map_y.create( srcImage.size(), CV_32FC1 );
//【3】双层循环,遍历每一个像素点,改变map_x & map_y的值
for( int j = 0; j < srcImage.rows;j++)
{
for( int i = 0; i < srcImage.cols;i++)
{
//改变map_x & map_y的值.
map_x.at<float>(j,i) = static_cast<float>(i);
map_y.at<float>(j,i) = static_cast<float>(srcImage.rows - j);
}
}
//【4】进行重映射操作
remap( srcImage, dstImage, map_x, map_y, INTER_LINEAR, BORDER_CONSTANT, Scalar(0,0, 0) );
//【5】显示效果图
imshow( "【程序窗口】", dstImage );
waitKey();
return 0;
}
运行截图
7.3.4 综合示例程序:实现多种重映射
先放出以remap为核心的综合示例程序,可以用按键控制四种不同的映射模式。如图
所示。
这个程序详细注释的源代码如下。
/* @File : 66_remap2.cpp
* @Brief : 示例程序66
* @Details : 实现多种重映射综合示例
* @Date : 2015-11-01
* @OpenCV Version : 4.8.0
* @Development Tools : Windows 11 64bit && Visual Studio 2017
* @Modify : 2024-04-27
*/
#include "opencv2/highgui/highgui.hpp"
#include "opencv2/imgproc/imgproc.hpp"
#include <iostream>
using namespace cv;
using namespace std;
#define WINDOW_NAME "【程序窗口】" //为窗口标题定义的宏
Mat g_srcImage, g_dstImage;
Mat g_map_x, g_map_y;
int update_map( int key);
static void ShowHelpText( );//输出帮助文字
int main( int argc, char** argv )
{
//改变console字体颜色
system("color 5F");
//显示帮助文字
ShowHelpText();
//【1】载入原始图
g_srcImage = imread( "1.jpg", 1 );
if(!g_srcImage.data ) { printf("读取图片错误,请确定目录下是否有imread函数指定的图片存在~! \n"); return false; }
imshow("原始图",g_srcImage);
//【2】创建和原始图一样的效果图,x重映射图,y重映射图
g_dstImage.create( g_srcImage.size(), g_srcImage.type() );
g_map_x.create( g_srcImage.size(), CV_32FC1 );
g_map_y.create( g_srcImage.size(), CV_32FC1 );
//【3】创建窗口并显示
namedWindow( WINDOW_NAME, WINDOW_AUTOSIZE );
imshow(WINDOW_NAME,g_srcImage);
//【4】轮询按键,更新map_x和map_y的值,进行重映射操作并显示效果图
while( 1 )
{
//获取键盘按键
int key = waitKey(0);
//判断ESC是否按下,若按下便退出
if( (key & 255) == 27 )
{
cout << "程序退出...........\n";
break;
}
//根据按下的键盘按键来更新 map_x & map_y的值. 然后调用remap( )进行重映射
update_map(key);
remap( g_srcImage, g_dstImage, g_map_x, g_map_y, INTER_LINEAR, BORDER_CONSTANT, Scalar(0,0, 0) );
//显示效果图
imshow( WINDOW_NAME, g_dstImage );
}
return 0;
}
//-----------------------------------【update_map( )函数】--------------------------------
// 描述:根据按键来更新map_x与map_x的值
//----------------------------------------------------------------------------------------------
int update_map( int key )
{
//双层循环,遍历每一个像素点
for( int j = 0; j < g_srcImage.rows;j++)
{
for( int i = 0; i < g_srcImage.cols;i++)
{
switch(key)
{
case '1': // 键盘【1】键按下,进行第一种重映射操作
if( i > g_srcImage.cols*0.25 && i < g_srcImage.cols*0.75 && j > g_srcImage.rows*0.25 && j < g_srcImage.rows*0.75)
{
g_map_x.at<float>(j,i) = static_cast<float>(2*( i - g_srcImage.cols*0.25 ) + 0.5);
g_map_y.at<float>(j,i) = static_cast<float>(2*( j - g_srcImage.rows*0.25 ) + 0.5);
}
else
{
g_map_x.at<float>(j,i) = 0;
g_map_y.at<float>(j,i) = 0;
}
break;
case '2':// 键盘【2】键按下,进行第二种重映射操作
g_map_x.at<float>(j,i) = static_cast<float>(i);
g_map_y.at<float>(j,i) = static_cast<float>(g_srcImage.rows - j);
break;
case '3':// 键盘【3】键按下,进行第三种重映射操作
g_map_x.at<float>(j,i) = static_cast<float>(g_srcImage.cols - i);
g_map_y.at<float>(j,i) = static_cast<float>(j);
break;
case '4':// 键盘【4】键按下,进行第四种重映射操作
g_map_x.at<float>(j,i) = static_cast<float>(g_srcImage.cols - i);
g_map_y.at<float>(j,i) = static_cast<float>(g_srcImage.rows - j);
break;
}
}
}
return 1;
}
//-----------------------------------【ShowHelpText( )函数】----------------------------------
// 描述:输出一些帮助信息
//----------------------------------------------------------------------------------------------
static void ShowHelpText()
{
//输出欢迎信息和OpenCV版本
printf("\n\n\t\t\t非常感谢购买《OpenCV3编程入门》一书!\n");
printf("\n\n\t\t\t此为本书OpenCV3版的第66个配套示例程序\n");
printf("\n\n\t\t\t 当前使用的OpenCV版本为:" CV_VERSION );
printf("\n\n ----------------------------------------------------------------------------\n");
//输出一些帮助信息
printf("\n\t欢迎来到重映射示例程序~\n\n");
printf( "\n\t按键操作说明: \n\n"
"\t\t键盘按键【ESC】- 退出程序\n"
"\t\t键盘按键【1】- 第一种映射方式\n"
"\t\t键盘按键【2】- 第二种映射方式\n"
"\t\t键盘按键【3】- 第三种映射方式\n"
"\t\t键盘按键【4】- 第四种映射方式\n" );
}
运行截图:
第1种映射
第2种映射
第3种映射
第4种映射
7.4 仿射变换
本节中,我们将一起了解仿射变换的概念,以及OpenCV中相关的实现函数warpAffine和getRotationMatrix2D。
7.4.1 认识仿射变换
仿射变换(Affine Transformation或Affine Map),又称仿射映射,是指在几何中,一个向量空间进行一次线性变换并接上一个平移,变换为另一个向量空间的过程。它保持了二维图形的“平直性”(直线经过变换之后依然是直线)和“平行性”(二维图形之间的相对位置关系保持不变,平行线依然是平行线,且直线上点的位置顺序不变)。
一个任意的仿射变换都能表示为乘以一个矩阵(线性变换)接着再加上一个向量(平移)的形式。
那么,我们能够用仿射变换来表示如下三种常见的变换形式:
● 旋转,rotation(线性变换)
● 平移,translation(向量加)
● 缩放,scale(线性变换)
进行更深层次的理解,仿射变换代表的是两幅图之间的一种映射关系。
而我们通常使用2 x 3的矩阵来表示仿射变换。
考虑到我们要使用矩阵A和B对二维向量x=[]做变换,所以也能表示为下列形式。
7.4.2 仿射变换的求法
我们知道,仿射变换表示的就是两幅图片之间的一种联系,关于这种联系的信息大致可从以下两种场景获得。
● 已知X和T,而且已知它们是有联系的。接下来的工作就是求出矩阵M。
● 已知M和X,想求得T。只要应用算式T=M·X即可。对于这种联系的信息可以用矩阵M清晰地表达(即给出明确的2×3矩阵),也可以用两幅图片点之间几何关系来表达。
形象地说明一下,因为矩阵M联系着两幅图片,就以其表示两图中各三点直接的联系为例。如图7.37所示。
图中,点1、2和3(在Image1中形成一个三角形)与Image2中的三个点是一一映射的关系,且它们仍然形成三角形,但形状已经和之前不一样了。我们能通过这样两组三点求出仿射变换(可以选择自己喜欢的点),接着就可以把仿射变换应用到图像中去。
OpenCV仿射变换相关的函数一般涉及到warpAffine和getRotationMatrix2D这两个函数:
● 使用OpenCV函数warpAffine来实现一些简单的重映射。
● 使用OpenCV函数getRotationMatrix2D来获得旋转矩阵。
下面分别对其进行讲解。
7.4.3 进行仿射变换:warpAffine()函数
warpAffine函数的作用是依据以下公式子,对图像做仿射变换。
函数原型如下。
void warpAffine( InputArray src, OutputArray dst,
InputArray M, Size dsize,
int flags = INTER_LINEAR,
int borderMode = BORDER_CONSTANT,
const Scalar& borderValue = Scalar());
● 第一个参数,InputArray类型的src,输入图像,即源图像,填Mat类的对象即可。
● 第二个参数,OutputArray类型的dst,函数调用后的运算结果存在这里,需和源图片有一样的尺寸和类型。
● 第三个参数,InputArray类型的M,2×3的变换矩阵。
● 第四个参数,Size类型的dsize,表示输出图像的尺寸。
● 第五个参数,int类型的flags,插值方法的标识符。此参数有默认值INTER_LINEAR(线性插值),可选的插值方式如表7.1所示。
● 第六个参数,int类型的borderMode,边界像素模式,默认值为BORDER_CONSTANT。
● 第七个参数,const Scalar&类型的borderValue,在恒定的边界情况下取的值,默认值为Scalar(),即0。
另外提一点,WarpAffine函数与一个叫做cvGetQuadrangleSubPix()的函数类似,但是不完全相同。WarpAffine要求输入和输出图像具有同样的数据类型,有更大的资源开销(因此对小图像不太合适)而且输出图像的部分可以保留不变。而cvGetQuadrangleSubPix可以精确地从8位图像中提取四边形到浮点数缓存区中,具有比较小的系统开销,而且总是全部改变输出图像的内容。
7.4.4 计算二维旋转变换矩阵:getRotationMatrix2D()函数
getRotationMatrix2D()函数用于计算二维旋转变换矩阵。变换会将旋转中心映射到它自身。
Mat getRotationMatrix2D(Point2f center, double angle, double scale);
● 第一个参数,Point2f类型的center,表示源图像的旋转中心。
● 第二个参数,double类型的angle,旋转角度。角度为正值表示向逆时针旋转(坐标原点是左上角)。
● 第三个参数,double类型的scale,缩放系数。
此函数计算以下矩阵:
7.4.5 示例程序:仿射变换
学习完上面的讲解和函数实现,下面是一个以warpAffine和getRotationMatrix2D函数为核心的对图像进行仿射变换的示例程序。
/* @File : 67_AffineTransform.cpp
* @Brief : 示例程序67
* @Details : 仿射变换综合示例
* @Date : 2015-11-01
* @OpenCV Version : 4.8.0
* @Development Tools : Windows 11 64bit && Visual Studio 2017
* @Modify : 2024-04-27
*/
#include "opencv2/highgui/highgui.hpp"
#include "opencv2/imgproc/imgproc.hpp"
#include <iostream>
using namespace cv;
using namespace std;
#define WINDOW_NAME1 "【原始图窗口】" //为窗口标题定义的宏
#define WINDOW_NAME2 "【经过Warp后的图像】" //为窗口标题定义的宏
#define WINDOW_NAME3 "【经过Warp和Rotate后的图像】" //为窗口标题定义的宏
static void ShowHelpText( );
int main( )
{
//【0】改变console字体颜色
system("color 1F");
//【0】显示欢迎和帮助文字
ShowHelpText( );
//【1】参数准备
//定义两组点,代表两个三角形
Point2f srcTriangle[3];
Point2f dstTriangle[3];
//定义一些Mat变量
Mat rotMat( 2, 3, CV_32FC1 );
Mat warpMat( 2, 3, CV_32FC1 );
Mat srcImage, dstImage_warp, dstImage_warp_rotate;
//【2】加载源图像并作一些初始化
srcImage = imread( "1.jpg", 1 );
if(!srcImage.data ) { printf("读取图片错误,请确定目录下是否有imread函数指定的图片存在~! \n"); return false; }
// 设置目标图像的大小和类型与源图像一致
dstImage_warp = Mat::zeros( srcImage.rows, srcImage.cols, srcImage.type() );
//【3】设置源图像和目标图像上的三组点以计算仿射变换
srcTriangle[0] = Point2f( 0,0 );
srcTriangle[1] = Point2f( static_cast<float>(srcImage.cols - 1), 0 );
srcTriangle[2] = Point2f( 0, static_cast<float>(srcImage.rows - 1 ));
dstTriangle[0] = Point2f( static_cast<float>(srcImage.cols*0.0), static_cast<float>(srcImage.rows*0.33));
dstTriangle[1] = Point2f( static_cast<float>(srcImage.cols*0.65), static_cast<float>(srcImage.rows*0.35));
dstTriangle[2] = Point2f( static_cast<float>(srcImage.cols*0.15), static_cast<float>(srcImage.rows*0.6));
//【4】求得仿射变换
warpMat = getAffineTransform( srcTriangle, dstTriangle );
//【5】对源图像应用刚刚求得的仿射变换
warpAffine( srcImage, dstImage_warp, warpMat, dstImage_warp.size() );
//【6】对图像进行缩放后再旋转
// 计算绕图像中点顺时针旋转50度缩放因子为0.6的旋转矩阵
Point center = Point( dstImage_warp.cols/2, dstImage_warp.rows/2 );
double angle = -50.0;
double scale = 0.6;
// 通过上面的旋转细节信息求得旋转矩阵
rotMat = getRotationMatrix2D( center, angle, scale );
// 旋转已缩放后的图像
warpAffine( dstImage_warp, dstImage_warp_rotate, rotMat, dstImage_warp.size() );
//【7】显示结果
imshow( WINDOW_NAME1, srcImage );
imshow( WINDOW_NAME2, dstImage_warp );
imshow( WINDOW_NAME3, dstImage_warp_rotate );
// 等待用户按任意按键退出程序
waitKey(0);
return 0;
}
//-----------------------------------【ShowHelpText( )函数】----------------------------------
// 描述:输出一些帮助信息
//----------------------------------------------------------------------------------------------
static void ShowHelpText()
{
//输出欢迎信息和OpenCV版本
printf("\n\n\t\t\t非常感谢购买《OpenCV3编程入门》一书!\n");
printf("\n\n\t\t\t此为本书OpenCV3版的第67个配套示例程序\n");
printf("\n\n\t\t\t 当前使用的OpenCV版本为:" CV_VERSION );
printf("\n\n ----------------------------------------------------------------------------\n");
//输出一些帮助信息
printf( "\n\n\t\t欢迎来到仿射变换综合示例程序\n\n");
printf( "\t\t键盘按键【ESC】- 退出程序\n" );
}
运行截图:
7.5 直方图均衡化
很多时候,我们用相机拍摄的照片的效果往往会不尽人意。这时,可以对这些图像进行一些处理,来扩大图像的动态范围。这种情况下最常用到的技术就是直方图均衡化。未经均衡化的图片范例如图7.41、7.42所示。
在图7.41中,我们可以看到,左边的图像比较淡,因为其数值范围变化比较小,可以在这幅图的直方图(图7.42)中明显地看到。因为处理的是8位图像,其亮度值是从0到255,但直方图值显示的实际亮度却集中在亮度范围的中间区域。为了解决这个问题,就可以用到直方图均衡化技术,先来看看其概念。
7.5.1 直方图均衡化的概念和特点
直方图均衡化是灰度变换的一个重要应用,它高效且易于实现,广泛应用于图像增强处理中。图像的像素灰度变化是随机的,直方图的图形高低不齐,直方图均衡化就是用一定的算法使直方图大致平和的方法。均衡化效果示例如图7.43、7.44所示。
简而言之,直方图均衡化是通过拉伸像素强度分布范围来增强图像对比度的一种方法。
均衡化处理后的图像只能是近似均匀分布。均衡化图像的动态范围扩大了,但其本质是扩大了量化间隔,而量化级别反而减少了,因此,原来灰度不同的象素经处理后可能变的相同,形成了一片相同灰度的区域,各区域之间有明显的边界,从而出现了伪轮廓。
在原始图像对比度本来就很高的情况下,如果再均衡化则灰度调和,对比度会降低。在泛白缓和的图像中,均衡化会合并一些象素灰度,从而增大对比度。均衡化后的图片如果再对其均衡化,则图像不会有任何变化。如图7.45、7.46所示。
通过图7.46可以发现,经过均衡化的图像,其频谱更加舒展,有效地利用了0~255的空间,图像表现力更加出色。
7.5.2 实现直方图均衡化:equalizeHist()函数
在OpenCV中,直方图均衡化的功能实现由equalizeHist函数完成。我们一起看看它的函数描述。
void equalizeHist( InputArray src, OutputArray dst );
● 第一个参数,InputArray类型的src,输入图像,即源图像,填Mat类的对象即可,需为8位单通道的图像。
● 第二个参数,OutputArray类型的dst,函数调用后的运算结果存在这里,需和源图片有一样的尺寸和类型。
采用如下步骤对输入图像进行直方图均衡化。
1)计算输入图像的直方图H。
2)进行直方图归一化,直方图的组距的和为255。
3)计算直方图积分:
言而言之,由equalizeHist()函数实现的灰度直方图均衡化算法,就是把直方图的每个灰度级进行归一化处理,求每种灰度的累积分布,得到一个映射的灰度映射表,然后根据相应的灰度值来修正原图中的每个像素。
7.5.3 示例程序:直方图均衡化
这一节将给大家演示的示例程序简明扼要而“字字珠玑”,非常直观地演示出了如何用equalizeHist()函数来进行图像的直方图均衡化,详细注释的代码如下。
/* @File : 68_equalizeHist.cpp
* @Brief : 示例程序68
* @Details : 直方图均衡化
* @Date : 2015-11-01
* @OpenCV Version : 4.8.0
* @Development Tools : Windows 11 64bit && Visual Studio 2017
* @Modify : 2024-04-27
*/
#include "opencv2/highgui/highgui.hpp"
#include "opencv2/imgproc/imgproc.hpp"
using namespace cv;
int main( )
{
// 【1】加载源图像
Mat srcImage, dstImage;
srcImage = imread( "1.jpg", 1 );
if(!srcImage.data ) { printf("读取图片错误,请确定目录下是否有imread函数指定图片存在~! \n"); return false; }
// 【2】转为灰度图并显示出来
cvtColor( srcImage, srcImage, COLOR_BGR2GRAY );
imshow( "原始图", srcImage );
// 【3】进行直方图均衡化
equalizeHist( srcImage, dstImage );
// 【4】显示结果
imshow( "经过直方图均衡化后的图", dstImage );
// 等待用户按键退出程序
waitKey(0);
return 0;
}
看完详细注释的代码,一起看看程序运行后得到的窗口。如图所示。
7.6 本章小结
在这章中我们学习了很多类型的图像变换方法。包括利用OpenCV进行边缘检测所用到的canny算子、sobel算子,Laplace算子以及scharr滤波器;进行图像特征提取的霍夫线变换、霍夫圆变换,重映射和仿射变换以及直方图均衡化。
本章核心函数清单