前言
对图像进行非规则的扭曲,实现哈哈镜就是一种非常规的扭曲方式,本文先描述remap的原理,然后通过remap实现哈哈镜。
Demo
基于原始算法,可以进行二次开发,实现一些其他效果:
矫正映射remap(畸变映射)
当进行图像矫正时,必须指定输入图像的每个像素在输出图像中移动到的位置,成为"矫正映射"(畸变映射)。
双通道浮点数表示方式
N x M的矩阵A中,重映射由双通道浮点数的N x M的矩阵B表示,对于图像A中的任意一点aPoint(i, j),映射为b1Point(i', j')和b2Point(i', j'),在A中假设i=2,j=3,那么(假设重映射之后4.5,5.5)在B1中b1Point(i', j')值为4.5,b2Point(i', j')值为5.5,由于坐标是浮点数,那么需要插值得到整数位置以及中间过渡的区域颜色(平滑处理)。

双矩阵浮点数表示方式
双矩阵浮点数表示,N x M的矩阵A中,重映射由一对N x M的矩阵B和C描述,这里所有的N x M矩阵都是单通道浮点矩阵,在A中的点aPoint(i, j),重映射矩阵B中的点bPoint(i,j)存储了重映射后的i' (映射后的i坐标), 重映射矩阵C中的点cPoint(i,j)存储了重映射后的j'(映射后的j坐标)。

定点表示方式
映射由双通道有符号整数矩阵(即CV_16SC2类型)表示。该方式与双通道浮点数表示方式相同,但使用此格式要快得多(笔者理解:由浮点数插值改为整数插值,会要快一些,但是肯定双通道浮点数的表示方式图像效果会稍微好一些)。

remap核心关键
在于得到插值的坐标系来映射新位置的x和y位置,要渐近等,所以本方法的核心关键在于得到标定后的矩阵,得到映射矩阵的方式可以自己写算法,也可以使用其他方式,后续文章继续深入这块。
remap演示
为了更好的展示remap的作用,我们使用一张100x100的图,这样可以更好的看到remap的原理效果。
先做一张100x100的图,图里面用不同的颜色,如下:
使用opencv打开:

Map1使用第一种表示点的方式
使用点的方式映射:
cpp
// 表示点的第一种
std::vector<cv::Point2f> vectorPoints; for(int index = 0; index < 10; index++) { vectorPoints.push_back(cv::Point2f(0, 0)); vectorPoints.push_back(cv::Point2f(10, 10)); vectorPoints.push_back(cv::Point2f(20, 20)); vectorPoints.push_back(cv::Point2f(30, 30)); vectorPoints.push_back(cv::Point2f(40, 40)); vectorPoints.push_back(cv::Point2f(50, 50)); vectorPoints.push_back(cv::Point2f(60, 60)); vectorPoints.push_back(cv::Point2f(70, 70)); vectorPoints.push_back(cv::Point2f(80, 80)); vectorPoints.push_back(cv::Point2f(90, 90)); }
这是相当于把点提取出来映射到一个mat里面,一直堆下去:
cpp
// 表示点的第一种
std::vector<cv::Point2f> vectorPoints; for(int index = 0; index < 50; index++) { vectorPoints.push_back(cv::Point2f(0, 0)); vectorPoints.push_back(cv::Point2f(10, 10)); vectorPoints.push_back(cv::Point2f(20, 20)); vectorPoints.push_back(cv::Point2f(30, 30)); vectorPoints.push_back(cv::Point2f(40, 40)); vectorPoints.push_back(cv::Point2f(50, 50)); vectorPoints.push_back(cv::Point2f(60, 60)); vectorPoints.push_back(cv::Point2f(70, 70)); vectorPoints.push_back(cv::Point2f(80, 80)); vectorPoints.push_back(cv::Point2f(90, 90)); }

Map1使用第二种表示点的方式
直接初始化:
cpp
cv::Mat mapX(srcMat.rows, srcMat.cols, CV_32F); // x 方向 cv::Mat mapY(srcMat.rows, srcMat.cols, CV_32F); // y 方向 for(int row = 0; row < 100; row++) { for(int col = 0; col < 100; col++) { std::cout << mapX.at<double>(row, col); } } cv::remap(srcMat, dstMat, mapX, mapY, cv::INTER_LINEAR);
打印输出都是0:
这里map保存的是原来现在这个位置的点映射到原来图片哪个坐标点(注意:不是原来哪个位置映射到map哪个位置,是map的纵横坐标点映射原来值里面的那个坐标)左右从中间向两边拉伸则是:

cpp
cv::Mat mapX(srcMat.rows, srcMat.cols, CV_32F); // x 方向 cv::Mat mapY(srcMat.rows, srcMat.cols, CV_32F); // y 方向 for(int row = 0; row < 100; row++) { for(int col = 0; col < 100; col++) { if(col < 25) { // 0~24 mapX.at<float>(row, col) = col * 2; mapY.at<float>(row, col) = row; }else if(col < 50) { // 25-49 mapX.at<float>(row, col) = 49; mapY.at<float>(row, col) = row; }else if(col < 75) { // 50~74 mapX.at<float>(row, col) = 50; mapY.at<float>(row, col) = row; }else { // 75~99 mapX.at<float>(row, col) = 99 - (99 - col) * 2; mapY.at<float>(row, col) = row; } } } cv::remap(srcMat, dstMat, mapX, mapY, cv::INTER_LINEAR);

核心桥梁:椭圆
椭圆的标准方程,对于一个中心在原点、长轴在x轴上的椭圆,其标准方程为:
其中,a是椭圆长轴的一半,b是椭圆短轴的一半。
给定一个参数t(通常称为参数或偏心率角),椭圆上的点(x,y)可以用以下参数方程表示:
其中,t的取值范围是[0,2π),通过改变t的值,可以得到椭圆上的不同点。例如,假设有一个椭圆,其长轴为10,短轴为6。那么,a=5,b=3。
- 当t=0时,点(x,y)=(5,0),这是椭圆长轴上的一个端点。
- 当 t=2π时,点(x,y)=(0,3),这是椭圆短轴上的一个端点。
- 当t=π时,点(x,y)=(−5,0),这是椭圆长轴上的另一个端点。
- 当t=2/3*π时,点(x,y)=(0,−3),这是椭圆短轴上的另一个端点。
通过改变t的值,可以得到椭圆上的任意点。
哈哈镜实现
cpp
int cols = srcMat.cols;
int rows = srcMat.rows; double horizontalStrength = 1.0f; double verticalStrength = 1.0f; double zoom = 1.0; int cx = cols / 2; int cy = rows / 2; for(int x = 0; x < cols; x++) { for(int y = 0; y < rows; y++) { // 先求的范围内的点离中心点的偏移比例 double dx = (x - cx) * 1.0f / cx; double dy = (y - cy) * 1.0f / cy; // 求得中心点的距离 double distance = sqrt(dx * dx + dy * dy); // 缩放半径 double r = distance / zoom; // 后面除0操作,这里防止为0 if(r == 0) { r = 1e-6; } // 求出角度 double theta = atan(r); // 求出最新比例覆盖点的rX double rDistortedX = horizontalStrength * theta / r; // 求出最新比例覆盖点的rY double rDistortedY = verticalStrength * theta / r; // 求出当前这个点使用原来哪个点映射 double dstX = cx + rDistortedX * dx * cx; double dstY = cy + rDistortedY * dy * cy; // 给map赋值 mapX.at<float>(y, x) = static_cast<float>(dstX); mapY.at<float>(y, x) = static_cast<float>(dstY); } }
函数原型
cpp
void remap(InputArray src,
OutputArray dst, InputArray map1, InputArray map2, int interpolation, int borderMode = BORDER_CONSTANT, const Scalar& borderValue = Scalar());
-
参数一:InputArray类型的src,一般为cv::Mat;
-
参数二:OutputArray类型的dst,目标图像。它的大小与map1相同,类型与src相同。
-
参数三:InputArray类型的map1,它有两种可能的表示对象:表示点(x,y)的第一个映射或者表示CV_16SC2 , CV_32FC1或CV_32FC2类型的x值。
-
参数四:InputArray类型的map2,它也有两种可能的表示对象,而且他是根据map1来确定表示哪种对象。若map1表示点(x,y)时,这个参数不代表任何值,否则,表示CV_16UC1 , rCV_32FC1类型的y值(第二个值)。
-
参数五:int类型的interpolation,使用的插值方法;
-
参数六:int类型的borderMode,边界处理方式;
-
参数七:Scalar类型的borderValue,重映射后,离群点的背景,需要broderMode设置为BORDER_CONSTRANT时才有效。(离群点:当图片大小为400x300,那么对应的map1和map2范围为0399、0299,小于0或者大于299的则为离散点,使用该颜色填充);
Demo源码
cpp
void OpenCVManager::testDistortingMirror() { #if 1 // 测试remap的Demo // cv::Mat srcMat = cv::imread("D:/qtProject/openCVDemo/openCVDemo/modules/openCVManager/images/41.png"); // cv::Mat srcMat = cv::imread("D:/qtProject/openCVDemo/openCVDemo/modules/openCVManager/images/25.jpg"); cv::Mat srcMat = cv::imread("D:/qtProject/openCVDemo/openCVDemo/modules/openCVManager/images/42.jpg"); if(srcMat.data == 0) { return; } cv::imshow("srcMat", srcMat); // remap的 /* 插值方法: INTER_NEAREST = 0, INTER_LINEAR = 1, INTER_CUBIC = 2, INTER_AREA = 3, INTER_LANCZOS4 = 4, INTER_LINEAR_EXACT = 5, INTER_MAX = 7, WARP_FILL_OUTLIERS = 8, WARP_INVERSE_MAP = 16 */ cv::Mat dstMat = cv::Mat(srcMat.rows, srcMat.cols, srcMat.type()); #if 0 // 表示点的第一种 std::vector<cv::Point2f> vectorPoints; for(int index = 0; index < 50; index++) { vectorPoints.push_back(cv::Point2f(0, 0)); vectorPoints.push_back(cv::Point2f(10, 10)); vectorPoints.push_back(cv::Point2f(20, 20)); vectorPoints.push_back(cv::Point2f(30, 30)); vectorPoints.push_back(cv::Point2f(40, 40)); vectorPoints.push_back(cv::Point2f(50, 50)); vectorPoints.push_back(cv::Point2f(60, 60)); vectorPoints.push_back(cv::Point2f(70, 70)); vectorPoints.push_back(cv::Point2f(80, 80)); vectorPoints.push_back(cv::Point2f(90, 90)); } cv::remap(srcMat, dstMat, vectorPoints, cv::Mat(), cv::INTER_LINEAR); #endif #if 0 cv::Mat mapX(srcMat.rows, srcMat.cols, CV_32F); // x 方向 cv::Mat mapY(srcMat.rows, srcMat.cols, CV_32F); // y 方向 for(int row = 0; row < 100; row++) { for(int col = 0; col < 100; col++) { if(col < 25) { // 0~24 mapX.at<float>(row, col) = col * 2; mapY.at<float>(row, col) = row; }else if(col < 50) { // 25-49 mapX.at<float>(row, col) = 49; mapY.at<float>(row, col) = row; }else if(col < 75) { // 50~74 mapX.at<float>(row, col) = 50; mapY.at<float>(row, col) = row; }else { // 75~99 mapX.at<float>(row, col) = 99 - (99 - col) * 2; mapY.at<float>(row, col) = row; } } } cv::remap(srcMat, dstMat, mapX, mapY, cv::INTER_LINEAR); #endif cv::Mat mapX(srcMat.rows, srcMat.cols, CV_32F); // x 方向 cv::Mat mapY(srcMat.rows, srcMat.cols, CV_32F); // y 方向 // 这里显示原本的图 for(int row = 0; row < srcMat.rows; row++) { for(int col = 0; col < srcMat.cols; col++) { mapX.at<float>(row, col) = col; mapY.at<float>(row, col) = row; } } #if 0 // 使用径向畸变 { // 这里a永远是长边,长边是纵向的 { int cols = srcMat.cols; int rows = srcMat.rows; double horizontalStrength = 2.0f; double verticalStrength = 2.0f; double zoom = 1.0; int cx = cols / 2; int cy = rows / 2; for(int x = 0; x < cols; x++) { for(int y = 0; y < rows; y++) { // 先求的范围内的点离中心点的偏移比例 double dx = (x - cx) * 1.0f / cx; double dy = (y - cy) * 1.0f / cy; // 求得中心点的距离 double distance = sqrt(dx * dx + dy * dy); // 缩放半径 double r = distance / zoom; // 后面除0操作,这里防止为0 if(r == 0) { r = 1e-6; } // 求出角度 double theta = atan(r); // 求出最新比例覆盖点的rX double rDistortedX = horizontalStrength * theta / r; // 求出最新比例覆盖点的rY double rDistortedY = verticalStrength * theta / r; // 求出当前这个点使用原来哪个点映射 double dstX = cx + rDistortedX * dx * cx; double dstY = cy + rDistortedY * dy * cy; // 给map赋值 mapX.at<float>(y, x) = static_cast<float>(dstX); mapY.at<float>(y, x) = static_cast<float>(dstY); } } } cv::remap(srcMat, dstMat, mapX, mapY, cv::INTER_LINEAR); } #endif #if 1 // 使用径向畸变 { // 这里a永远是长边,长边是纵向的 { int cols = srcMat.cols; int rows = srcMat.rows; double horizontalStrength = 1.0f; double verticalStrength = 1.0f; double zoom = 1.0; int cx = cols / 2; int cy = rows / 2; for(int x = 0; x < cols; x++) { for(int y = 0; y < rows; y++) { // 先求的范围内的点离中心点的偏移比例 double dx = (x - cx) * 1.0f / cx; double dy = (y - cy) * 1.0f / cy; // 求得中心点的距离 double distance = sqrt(dx * dx + dy * dy); // 缩放半径 double r = distance / zoom; // 后面除0操作,这里防止为0 if(r == 0) { r = 1e-6; } // 求出角度 double theta = atan(r); // 求出最新比例覆盖点的rX double rDistortedX = horizontalStrength * theta / r; // 求出最新比例覆盖点的rY double rDistortedY = verticalStrength * theta / r; // 求出当前这个点使用原来哪个点映射 double dstX = cx + rDistortedX * dx * cx; double dstY = cy + rDistortedY * dy * cy; // 给map赋值 mapX.at<float>(y, x) = static_cast<float>(dstX); mapY.at<float>(y, x) = static_cast<float>(dstY); } } } cv::remap(srcMat, dstMat, mapX, mapY, cv::INTER_LINEAR); } #endif cv::imshow("dstMat", dstMat); while(true) { cv::waitKey(0); } #endif }
工程模板v1.73.0

入坑
入坑一:map1和map2映射崩溃
问题
映射崩溃,图是100x100,那么坐标x和y都是099,099,但是运行崩溃。
原因
定位到remap奔溃,发现map的类型是float不是double。
解决