前言

  对图像进行非规则的扭曲,实现哈哈镜就是一种非常规的扭曲方式,本文先描述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使用第一种表示点的方式

  使用点的方式映射:

// 表示点的第一种
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里面,一直堆下去:

// 表示点的第一种
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使用第二种表示点的方式

  直接初始化:

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的纵横坐标点映射原来值里面的那个坐标)左右从中间向两边拉伸则是:
  

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的值,可以得到椭圆上的任意点。
 

哈哈镜实现

  

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);
}
}
 

函数原型

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源码

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。

解决

  

   

OpenCV开发笔记(八十三):图像remap实现哈哈镜效果的更多相关文章

  1. OpenCV开发笔记(七十三):红胖子8分钟带你使用opencv+dnn+yolov3识别物体

      前言   级联分类器的效果并不是很好,准确度相对深度学习较低,上一章节使用了dnn中的tensorflow,本章使用yolov3模型,识别出具体的分类.   Demo   320x320,置信度0 ...

  2. OpenCV开发笔记(六十五):红胖子8分钟带你深入了解ORB特征点(图文并茂+浅显易懂+程序源码)

    若该文为原创文章,未经允许不得转载原博主博客地址:https://blog.csdn.net/qq21497936原博主博客导航:https://blog.csdn.net/qq21497936/ar ...

  3. OpenCV开发笔记(六十九):红胖子8分钟带你使用传统方法识别已知物体(图文并茂+浅显易懂+程序源码)

    若该文为原创文章,未经允许不得转载原博主博客地址:https://blog.csdn.net/qq21497936原博主博客导航:https://blog.csdn.net/qq21497936/ar ...

  4. OpenCV开发笔记(六十四):红胖子8分钟带你深入了解SURF特征点(图文并茂+浅显易懂+程序源码)

    若该文为原创文章,未经允许不得转载原博主博客地址:https://blog.csdn.net/qq21497936原博主博客导航:https://blog.csdn.net/qq21497936/ar ...

  5. 【opencv学习笔记八】创建TrackBar轨迹条

    createTrackbar这个函数我们以后会经常用到,它创建一个可以调整数值的轨迹条,并将轨迹条附加到指定的窗口上,使用起来很方便.首先大家要记住,它往往会和一个回调函数配合起来使用.先看下他的函数 ...

  6. OpenCV开发笔记(五十五):红胖子8分钟带你深入了解Haar、LBP特征以及级联分类器识别过程(图文并茂+浅显易懂+程序源码)

    若该文为原创文章,未经允许不得转载原博主博客地址:https://blog.csdn.net/qq21497936原博主博客导航:https://blog.csdn.net/qq21497936/ar ...

  7. OpenCV开发笔记(五十六):红胖子8分钟带你深入了解多种图形拟合逼近轮廓(图文并茂+浅显易懂+程序源码)

    若该文为原创文章,未经允许不得转载原博主博客地址:https://blog.csdn.net/qq21497936原博主博客导航:https://blog.csdn.net/qq21497936/ar ...

  8. OpenCV开发笔记(七十一):红胖子8分钟带你深入级联分类器训练

    前言   红胖子,来也!  做图像处理,经常头痛的是明明分离出来了(非颜色的),分为几块区域,那怎么知道这几块区域到底哪一块是我们需要的,那么这部分就涉及到需要识别了.  识别可以自己写模板匹配.特征 ...

  9. OpenCV开发笔记(七十二):红胖子8分钟带你使用opencv+dnn+tensorFlow识别物体

    前言   级联分类器的效果并不是很好,准确度相对深度学习较低,本章使用opencv通过tensorflow深度学习,检测已有模型的分类.   Demo       可以猜测,1其实是人,18序号类是狗 ...

  10. OpenCV开发笔记(七十四):OpenCV3.4.1+ffmpeg3.4.8交叉编译移植到海思平台Hi35xx平台

    前言   移植opencv到海思平台,opencv支持对视频进行解码,需要对应的ffmpeg支持.   Ffmpeg的移植   Ffmpeg的移植请参考之前的文章:<FFmpeg开发笔记(十): ...

随机推荐

  1. PpcProcessResult

    @Data @Accessors(chain = true) public class PpcProcessResult { public static volatile int globalThre ...

  2. 查看MySQL数据库所有的表名、表注释、字段名称、类型、长度、备注,一键导出生成数据库字典

    一.先了解下INFORMATION_SCHEMA1.在MySQL中,把INFORMATION_SCHEMA看作是一个数据库,确切说是信息数据库.其中保存着关于MySQL服务器所维护的所有其他数据库的信 ...

  3. Qt编写物联网管理平台46-云端数据同步

    一.前言 在上一篇文章说的采集数据转发的基础上,针对方案一还做了云端数据同步功能,满足各式各样的用户需求.云端数据库同步,相当于把本地采集到的数据实时存储到云端,至于这些记录到了云端后什么用途,客户端 ...

  4. 《Bootstrap4Web设计与开发实战》源代码下载

    <Bootstrap4Web设计与开发实战>源代码下载: 链接:https://pan.baidu.com/s/1GaIo390c-l-gsT6-6RaaJA 提取码:fgiq 版权声明: ...

  5. IntelliJ IDEA安装与配置(支持最新2020.2)

    前言 我是从eclipse转IDEA的,对于习惯了eclipse快捷键的我来说,转IDEA开始很不习惯,IDEA快捷键多,组合多,记不住,虽然可以设置使用eclipse的快捷键,但是总感觉怪怪的.开始 ...

  6. 网络编程入门从未如此简单(二):假如你来设计TCP协议,会怎么做?

    本文原题"你管这破玩意儿叫TCP?",由闪客sun分享,转载请联系作者. 1.引言 网络编程能力对于即时通讯技术开发者来说是基本功,而计算机网络又是网络编程的理论根基,因而深刻准确 ...

  7. 【Java 温故而知新系列】基础知识-02 数据基本类型

    1.Java基本数据类型 Java语言是强类型语言,对于每一种数据都定义了明确的具体的数据类型,在内存中分配了不同大小的内存空间. 基本数据类型 数值型:整数类型(byte,short,int,lon ...

  8. [LC735]行星碰撞

    题目描述 给定一个整数数组 asteroids,表示在同一行的行星.对于数组中的每一个元素,其绝对值表示行星的大小,正负表示行星的移动方向(正表示向右移动,负表示向左移动).每一颗行星以相同的速度移动 ...

  9. vue的element,el-select与el-tree配合使用,实现下拉树状

    HTML <el-select ref="stlectTree" v-model="treeSelectText" placeholder="请 ...

  10. c# 判断当前用户是否是管理员权限启动应用程序。

    . using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; u ...