目录

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

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

若该文为原创文章,转载请注明原文出处

本文章博客地址:

上一篇:《 》

下一篇:持续补充中…

前言

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

Demo

https://i-blog.csdnimg.cn/direct/15a6fed53dd04181b2cb70d8ed49d183.png

https://i-blog.csdnimg.cn/direct/3d240ba9aa974452abe13d563a00b5b4.png

https://i-blog.csdnimg.cn/direct/560cffde46c74d2a99573ea9254a2ed7.png

https://i-blog.csdnimg.cn/direct/5a3f664cb4d64212b2423ba763d82c1c.png

基于原始算法,可以进行二次开发,实现一些其他效果:

https://i-blog.csdnimg.cn/direct/f1c0f879bda540e6bfefa36a434c4fa2.png

https://i-blog.csdnimg.cn/direct/513db5d90aec4b1495da79535efa1061.png

https://i-blog.csdnimg.cn/direct/b8a7bfd0a61c473e878836ac9bdc7596.png

矫正映射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,由于坐标是浮点数,那么需要插值得到整数位置以及中间过渡的区域颜色(平滑处理)。

https://i-blog.csdnimg.cn/direct/ee93249333414c56a7d5cefc9a7a6e99.png

双矩阵浮点数表示方式

双矩阵浮点数表示,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坐标)。

https://i-blog.csdnimg.cn/direct/9009727184204b4b94e3a60b9f13553e.png

定点表示方式

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

https://i-blog.csdnimg.cn/direct/aab604deb41445ba974c5a555c27bd0a.png

remap核心关键

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

remap演示

为了更好的展示remap的作用,我们使用一张100x100的图,这样可以更好的看到remap的原理效果。

先做一张100x100的图,图里面用不同的颜色,如下:

https://i-blog.csdnimg.cn/direct/c3e8db203f064e95b787cec0e9125155.png

使用opencv打开:

https://i-blog.csdnimg.cn/direct/79f95f459cfc477fadc4d3eeb82a5eda.png

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

https://i-blog.csdnimg.cn/direct/24e7ae79b0754462be83f2050e4323a0.png

这是相当于把点提取出来映射到一个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));
}

https://i-blog.csdnimg.cn/direct/c0616d4200d1448faefa4330eab6d72c.png

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:

https://i-blog.csdnimg.cn/direct/67409b3d0b0e46e09208ed9f8e2bca6b.png

https://i-blog.csdnimg.cn/direct/bd93511059534f82ae5425d25366c58f.png

https://i-blog.csdnimg.cn/direct/3387421fb2a84b75adeb44b84286b38f.png

https://i-blog.csdnimg.cn/direct/5cf256caf2fd418f8ee13c631d239c78.png

https://i-blog.csdnimg.cn/direct/09177c14d7d5417aa3aee631977b13ee.png

https://i-blog.csdnimg.cn/direct/bc9e28881e4e4c49b05df5d06dbd3a33.png

这里map保存的是原来现在这个位置的点映射到原来图片哪个坐标点(注意:不是原来哪个位置映射到map哪个位置,是map的纵横坐标点映射原来值里面的那个坐标)左右从中间向两边拉伸则是:

https://i-blog.csdnimg.cn/direct/f55e568cccf8443e99e0c3e55e651aee.png

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

https://i-blog.csdnimg.cn/direct/054a181141b540d7ad6693f766a98abd.png

核心桥梁:椭圆

https://i-blog.csdnimg.cn/direct/3d9c9f74ff3f4e90a0d88fa875a82cf7.png

https://i-blog.csdnimg.cn/direct/ca780ca7a21b4dc9be3700049673c197.png

https://i-blog.csdnimg.cn/direct/b8c47e81fa124eec81d8ad83a136dc27.png

椭圆的标准方程,对于一个中心在原点、长轴在x轴上的椭圆,其标准方程为:

https://i-blog.csdnimg.cn/direct/9d0df740c59145fab69315d85983e4b4.png

其中,a是椭圆长轴的一半,b是椭圆短轴的一半。

给定一个参数t(通常称为参数或偏心率角),椭圆上的点(x,y)可以用以下参数方程表示:

https://i-blog.csdnimg.cn/direct/5b8d6231ff314d21b3643cb3eaf98de4.png

其中,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,使用的插值方法;

    https://i-blog.csdnimg.cn/direct/4358e876dc204802b837c6af487d817c.png

  • 参数六:int类型的borderMode,边界处理方式;

  • 参数七:Scalar类型的borderValue,重映射后,离群点的背景,需要broderMode设置为BORDER_CONSTRANT时才有效。(离群点:当图片大小为400x300,那么对应的map1和map2范围为0 399、0 299,小于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

https://i-blog.csdnimg.cn/direct/1310ab5abf164957b99c1a53632c8289.png

入坑

入坑一:map1和map2映射崩溃

问题

映射崩溃,图是100x100,那么坐标x和y都是0 99,0 99,但是运行崩溃。

https://i-blog.csdnimg.cn/direct/b97b78a68885407ea2856f09ad2446a8.png

https://i-blog.csdnimg.cn/direct/5ce2c1d3661f4dc0bdba1fc164072862.png

原因

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

解决

https://i-blog.csdnimg.cn/direct/62f28284cf53413eb3e7ba21b209557d.png

https://i-blog.csdnimg.cn/direct/c560ec92bcf748769128b11a43a7f0ab.png

上一篇:《 》

下一篇:持续补充中…

本文章博客地址: