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

前言

对图像进行非规则的扭曲,实现哈哈镜就是一种非常规的扭曲方式,本文先描述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,0 99,但是运行崩溃。   

原因

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

解决

相关推荐
冬奇Lab2 小时前
一天一个开源项目(第17篇):ViMax - 多智能体视频生成框架,导演、编剧、制片人全包
开源·音视频开发
一个处女座的程序猿3 小时前
AI之Agent之VibeCoding:《Vibe Coding Kills Open Source》翻译与解读
人工智能·开源·vibecoding·氛围编程
一只大侠的侠5 小时前
React Native开源鸿蒙跨平台训练营 Day16自定义 useForm 高性能验证
flutter·开源·harmonyos
IvorySQL5 小时前
PostgreSQL 分区表的 ALTER TABLE 语句执行机制解析
数据库·postgresql·开源
一只大侠的侠6 小时前
Flutter开源鸿蒙跨平台训练营 Day11从零开发商品详情页面
flutter·开源·harmonyos
一只大侠的侠6 小时前
React Native开源鸿蒙跨平台训练营 Day18自定义useForm表单管理实战实现
flutter·开源·harmonyos
一只大侠的侠6 小时前
React Native开源鸿蒙跨平台训练营 Day20自定义 useValidator 实现高性能表单验证
flutter·开源·harmonyos
晚霞的不甘7 小时前
Flutter for OpenHarmony 可视化教学:A* 寻路算法的交互式演示
人工智能·算法·flutter·架构·开源·音视频
Dfreedom.7 小时前
图像直方图完全解析:从原理到实战应用
图像处理·python·opencv·直方图·直方图均衡化
晚霞的不甘8 小时前
Flutter for OpenHarmony 实现计算几何:Graham Scan 凸包算法的可视化演示
人工智能·算法·flutter·架构·开源·音视频