OpenCV开发笔记(八十三):图像remap实现哈哈镜效果

news/2025/3/13 17:26:29/

若该文为原创文章,转载请注明原文出处
本文章博客地址:https://hpzwl.blog.csdn.net/article/details/146213444

长沙红胖子Qt(长沙创微智科)博文大全:开发技术集合(包含Qt实用技术、树莓派、三维、OpenCV、OpenGL、ffmpeg、OSG、单片机、软硬结合等等)持续更新中…

OpenCV开发专栏(点击传送门)

上一篇:《OpenCV开发笔记(八十二):两图拼接使用渐进色蒙版场景过渡缝隙》
下一篇:持续补充中…


前言

  对图像进行非规则的扭曲,实现哈哈镜就是一种非常规的扭曲方式,本文先描述remap的原理,然后通过remap实现哈哈镜


Demo

  在这里插入图片描述

  在这里插入图片描述

  在这里插入图片描述

  在这里插入图片描述
  基于原始算法,可以进行二次开发,实现一些其他效果:

  在这里插入图片描述

  在这里插入图片描述

  在这里插入图片描述


remap_34">矫正映射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_51">remap核心关键

  在于得到插值的坐标系来映射新位置的x和y位置,要渐近等,所以本方法的核心关键在于得到标定后的矩阵,得到映射矩阵的方式可以自己写算法,也可以使用其他方式,后续文章继续深入这块。


remap_56">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~24mapX.at<float>(row, col) = col * 2;mapY.at<float>(row, col) = row;}else if(col < 50){// 25-49mapX.at<float>(row, col) = 49;mapY.at<float>(row, col) = row;}else if(col < 75){// 50~74mapX.at<float>(row, col) = 50;mapY.at<float>(row, col) = row;}else{// 75~99mapX.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操作,这里防止为0if(r == 0){r = 1e-6;}// 求出角度double theta = atan(r);// 求出最新比例覆盖点的rXdouble rDistortedX = horizontalStrength * theta / r;// 求出最新比例覆盖点的rYdouble 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 0cv::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~24mapX.at<float>(row, col) = col * 2;mapY.at<float>(row, col) = row;}else if(col < 50){// 25-49mapX.at<float>(row, col) = 49;mapY.at<float>(row, col) = row;}else if(col < 75){// 50~74mapX.at<float>(row, col) = 50;mapY.at<float>(row, col) = row;}else{// 75~99mapX.at<float>(row, col) = 99 - (99 - col) * 2;mapY.at<float>(row, col) = row;}}}cv::remap(srcMat, dstMat, mapX, mapY, cv::INTER_LINEAR);
#endifcv::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操作,这里防止为0if(r == 0){r = 1e-6;}// 求出角度double theta = atan(r);// 求出最新比例覆盖点的rXdouble rDistortedX = horizontalStrength * theta / r;// 求出最新比例覆盖点的rYdouble 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操作,这里防止为0if(r == 0){r = 1e-6;}// 求出角度double theta = atan(r);// 求出最新比例覆盖点的rXdouble rDistortedX = horizontalStrength * theta / r;// 求出最新比例覆盖点的rYdouble 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);}
#endifcv::imshow("dstMat", dstMat);while(true){cv::waitKey(0);}
#endif
}

工程模板v1.73.0

   在这里插入图片描述


入坑

入坑一:map1和map2映射崩溃

问题

   映射崩溃,图是100x100,那么坐标x和y都是099,099,但是运行崩溃。
   在这里插入图片描述

   在这里插入图片描述

原因

   定位到remap奔溃,发现map的类型是float不是double。

解决

  在这里插入图片描述

   在这里插入图片描述


上一篇:《OpenCV开发笔记(八十二):两图拼接使用渐进色蒙版场景过渡缝隙》
下一篇:持续补充中…


本文章博客地址:https://hpzwl.blog.csdn.net/article/details/146213444


http://www.ppmy.cn/news/1578838.html

相关文章

Windows CMD 命令大全(综合开发整理版)

CMD Windows CMD 命令大全(综合整理版)基础操作与文件管理类系统维护与配置类网络与连接类开发者常用命令CMD 黑窗口使用技巧1. **效率操作**2. **高级功能**3. **开发者高效技巧**注意事项**微软官方文档****其他实用资源****如何高效使用官方文档**Windows CMD 命令大全(综…

云效、流水线、Gradle缓存问题、build.gradle配置snapshot

云效 大部分研发者们或许都听说过阿里云云效&#xff0c;甚至有不少使用经验&#xff1b;但是几乎很少有人会关注过云效的&#xff08;子&#xff09;域名&#xff1a; 如上图&#xff0c;云效的定位&#xff08;愿景&#xff09;就是为研发者们提供一站式的团队协作、效能提…

vscode自定义背景色

故事背景,vscode默认的是黑色&#xff0c;windows默认的是白色&#xff0c;白色看久了&#xff0c;看黑色看不清楚。以前写C灰色习惯了。需要修改背景色。 自定义背景色 打开 VSCode。 按下 Ctrl , 打开设置。 在搜索栏中输入 workbench.colorCustomizations。 点击 “编辑 i…

数字IC/FPGA校招笔试题解析(一)

数字IC/FPGA校招笔试题解析&#xff08;一&#xff09; 题目1 问题&#xff1a;数字电路设计中&#xff0c;下列哪种手段无法消除竞争冒险现象&#xff1f; 选项&#xff1a; a. 加滤波电容&#xff0c;消除毛刺 b. 增加冗余项消除逻辑冒险 c. 增加选通信号&#xff0c;避开毛…

纯前端全文检索的两种实现方案:ElasticLunr.js 和 libsearch

纯前端全文检索的两种实现方案&#xff1a;ElasticLunr.js 和 libsearch 在前端开发中&#xff0c;实现全文检索功能可以显著提升用户体验&#xff0c;尤其是在处理大量文本数据时。本文将介绍两种流行的纯前端全文检索方案&#xff1a;ElasticLunr.js 和 libsearch。这两种方…

【反无人机目标检测数据集】MIDGARD:关于基于机器学习的微型无人机视觉相对定位的训练数据集

On training datasets for machine learning-based visual relative localization of micro-scale UAVs 关于基于机器学习的微型无人机视觉相对定位的训练数据集 0.论文摘要 摘要——通过利用我们相对微型无人机定位传感器UVDAR&#xff0c;我们生成了一个自动标注的数据集MID…

LeetCodehot 力扣热题100 零钱兑换

详细运行思路 这段代码使用动态规划&#xff08;Dynamic Programming, DP&#xff09;解决最少硬币找零问题。 它的核心思想是&#xff1a; 状态转移方程&#xff1a; dp[i] min(dp[i], dp[i - coins[j]] 1) 表示兑换 i 元最少需要的硬币数&#xff0c;取决于使用 coins[j…

Android app:layout_constraintHorizontal_bias=“0“属性详解

在 Android 的 ConstraintLayout 中&#xff0c;app:layout_constraintHorizontal_bias 是用于控制控件在水平方向上相对约束位置的关键属性。以下是其详细解析&#xff1a; 一、核心作用 该属性用于调整控件在两个水平约束点之间的偏移比例&#xff0c;取值范围为 0.0&#…