【04】OpenCV C++实战篇——实战:发票精准定位,提取指定单元格数据。(倾角计算、旋转矫正、产品定位、目标定位、OCR文字提取)

文章目录

  • [1 用户需求描述](#1 用户需求描述)
  • [2 方案制定](#2 方案制定)
  • [3 方案可行性实验](#3 方案可行性实验)
      • [2.1 基于外轮廓框及表格最外层框的定位](#2.1 基于外轮廓框及表格最外层框的定位)
      • [2.2 基于条形码矫正定位](#2.2 基于条形码矫正定位)
  • [4 优化改进](#4 优化改进)
  • [5 代码实现](#5 代码实现)
  • [6 项目代码规范](#6 项目代码规范)
  • [7. 变换系数合并(旋转平移一次完成)](#7. 变换系数合并(旋转平移一次完成))
    • [7.2 合并问题,再探究](#7.2 合并问题,再探究)
    • 7.3合并总结
  • [8 关于旋转矩形的角度、顶点、宽高等介绍](#8 关于旋转矩形的角度、顶点、宽高等介绍)

1 用户需求描述

用户需求描述:

  • 1.相机与工作台垂直,工作人员将品票放在相机下,自动拍照,提取表格中提取表格中关键文字信息,分类存储;
  • 2.人工放置品票会存在歪斜,照片不完整,由于品票重复使用,边缘存在褶皱翘起现象;
  • 3.品票大致相同,但不同的品票表格细节和要读取的位置不同;

2 方案制定

1.自动拍照: 工作台垂直方向安装一对红外对管,当品票放置在工作台上,红外对管被遮挡触发自动拍照及后续自动识别工作;
2.旋转矫正: 使图片水平;
3.产品定位: 确定一个定位点,每张图片都固定在相同的位置:
4.目标ROI: 以定位点为参考点确定n个目标提取位置的坐标,截取ROI;
4.文字提取: OCR文字提取;

这里重点讲解 :旋转矫正、产品定位、目标ROI获取等问题;

自动拍照OCR文字提取的实现这里不做重点讲解。

3 方案可行性实验

1.水平旋转矫正;

  • a.图纸轮廓: 求斜率,旋转,简单,不准(轮廓弯曲);(舍弃)

  • b.条码框轮廓: 发现所有发票 上侧中间都一个条形码,对其腐蚀膨胀等操作,使条码成为一个实心的矩形,用轮廓检测找到条码矩形框,获取矩中心坐标,将其作为旋转中心,求斜率,旋转;(最终使用方法)

2.固定点定位(位移);

  • 以条码矩形框中心为定位点,将整张图以这个点为中心,移动到规定的位置,使每次传来的图片处理后都会钉子案这个位置。

3.OCR文字提取(此处不是研究重点)

2.1 基于外轮廓框及表格最外层框的定位

2.2 基于条形码矫正定位

  • 1.图像反色,使条码区域白色的更凸显,弱化其他区域;
  • 2.碰撞操作,使条码区域形成一个实心的矩形块;
  • 3.腐蚀操作,去掉其他线框等非目标物;
  • 4.目标矩形块定位查找;



4 优化改进

图图像进过反色、腐蚀、膨胀后,如下图。目标条形码已经很凸显了。但是还有白色小斑点其他白色矩形 干扰,这样会增加算法筛选查找的时间。

①中值滤波去图像上残余斑点,减少轮廓对象,从而较少查找时间。

图像残余斑点,会增加轮廓寻找时间

②设定ROI区域可进一步缩条形码搜索范围,可减少运行时间。

尽管品票会歪斜偏移,但条形码总体还是在图像上半部分区域。

③设备拍照时可能会拍到设备的两个脚,造成右上角出现与检测目标相似的区域,出现误检测,加区域限制。

5 代码实现

cpp 复制代码
#include <opencv2/opencv.hpp>
#include <iostream>

using namespace std;
using namespace cv;

// 最终图片条码的中心点位置,同时也是旋转中心
Point CPoint= Point(1700, 350);

//第一个参数:输入图片名称;第二个参数:输出图片名称
int GetContoursPic(const char* pSrcFileName, const char* pDstFileName)
{
    //载入原图 
    Mat img = imread(pSrcFileName);
    Mat gray, gray2;
    cvtColor(img, gray, COLOR_BGR2GRAY);  //转化成灰度图

    //cv::GaussianBlur(gray, gray, cv::Size(3, 3), 1, 0);
    cv::threshold(gray, gray, 60, 255, cv::THRESH_BINARY);
    gray = 255 - gray;//图像取反
    //获取自定义核
    Mat element1 = getStructuringElement(MORPH_RECT, Size(11, 11));//生成矩形结构元素,腐蚀
    Mat element2 = getStructuringElement(MORPH_RECT, Size(37, 37));//膨胀
    Mat out1, out2;
    out1= gray;

 

    //进行腐蚀操作
    //erode(gray, out, element2);
    //imshow("腐蚀", out);
    //waitKey(0);

    //进行膨胀操作
    
    Mat edge, binary;
    //检测边缘图像,并二值化
    //Canny(out, edge, 30, 200, 3);
    dilate(out1, out1, element1);
    erode(out1, out2, element2);
    //dilate(out2, out2, element2);
    //Canny(out2, edge, 80, 180, 3, false);
   // threshold(out, binary, 0, 10, THRESH_BINARY);

    /*namedWindow("膨胀", WINDOW_NORMAL);
    namedWindow("腐蚀", WINDOW_NORMAL);
    imshow("膨胀", out1);
    imshow("腐蚀", out2);
    waitKey(0);*/

    /*threshold(out2, binary, 100, 255, THRESH_BINARY_INV);
    imshow("binary", binary);*/


    // 轮廓发现与绘制
    vector<vector<Point>> contours;  //轮廓
    vector<Vec4i> hierarchy;  //存放轮廓结构变量


    //CV_CHAIN_APPROX_SIMPLE:仅保存轮廓的拐点信息,把所有轮廓拐点处的点保存入contours向量内,拐点与拐点之间直线段上的信息点不予保留;
    //如果是水平的矩形,那么构成矩形只有4个点,但现在矩形线条是由很多个小线段组成,,,,所以输出的点有很多
    findContours(out2, contours, hierarchy, 3, CHAIN_APPROX_SIMPLE, Point());

    //绘制轮廓
   // cout << "contours.size()=" << contours.size() << endl;//轮廓条数
    for (int t = contours.size() - 1; t >= 0; t--)//倒着找更快

   // for (int t =0; t<contours.size();t++)
    {
        ////////看看几次可以找到条形码
        //drawContours(img, contours, t, Scalar(0, 0, 255), 6, 8);//目标图,要绘制的矩形轮廓,轮廓条数,绘制颜色红,粗度6,
        //namedWindow("轮廓检测结果", WINDOW_NORMAL);
        //imshow("轮廓检测结果", img);
        //imwrite("轮廓检测结果.bmp", img);
        //waitKey();
        float line1, line2;

        CvPoint2D32f rectpoint[4];
         CvBox2D rect = minAreaRect(Mat(contours[t]));//生成最小外接矩形(随着外轮廓整体扥角度倾斜)。找到一个环绕输入2D点集的最小区域的旋转矩形
        //RotatedRect rect = minAreaRect(Mat(contours[t]));

        cvBoxPoints(rect, rectpoint); //获取4个顶点坐标  
        
        //获取生成最小外接矩形的长和宽
      
        ////距离公式|AB|=sqrt((x1-x2)*(x1-x2)+(y1-y2)*(y1-y2))
        //line1 = sqrt((rectpoint[1].y - rectpoint[0].y) * (rectpoint[1].y - rectpoint[0].y) + (rectpoint[1].x - rectpoint[0].x) * (rectpoint[1].x - rectpoint[0].x));
        //line2 = sqrt((rectpoint[3].y - rectpoint[0].y) * (rectpoint[3].y - rectpoint[0].y) + (rectpoint[3].x - rectpoint[0].x) * (rectpoint[3].x - rectpoint[0].x));
        
        //方法替换,直接引用
        Size2f rectangle = rect.size;
        line1= rectangle.width;
        line2 = rectangle.height;



        //面积太小的直接pass
        // 条形码轮廓尺寸大概是613*152, line1横向,line2纵行
        //经过预处理之后,实际检测到条码的尺寸3264*2448,min(566,90),max(580,94);3264*1832,min(639,103),max(647,106)
        if ((rectpoint[0].y < 700)&&(rectpoint[0].x>750 && rectpoint[0].x<2514))//确保是在上半部分的中间部分(减少干扰、节省时间)
        {
            if ((line1 * line2 >38000) && (line1 * line2 < 85000))//确保找到的矩形框与条形码面积差不多
            {
                //cout << "contours" << contours[t] << endl;//输出第t个矩形框的关键点坐标,如果是水平的矩形,那么构成矩形只有4个点,但现在矩形线条是由很多个小线段组成,所以输出的点有很多
             //   drawContours(img, contours, t, Scalar(0, 0, 255), 6, 8);//目标图,要绘制的矩形轮廓,轮廓条数,绘制颜色红,粗度6,
               // namedWindow("轮廓检测结果", WINDOW_NORMAL);
               // imshow("轮廓检测结果", img);
              //  imwrite("轮廓检测结果.bmp", img);
              //  waitKey();
                cout << "line1=" << line1 << ",line2=" << line2 << endl;
                cout << "检测到的条码矩形框尺寸" << rectangle << endl;
                cout << "rectpoint[4]的四个坐标你" << endl;
                cout << rectpoint[0].x << "," << rectpoint[0].y << endl;
                cout << rectpoint[1].x << "," << rectpoint[1].y << endl;
                cout << rectpoint[2].x << "," << rectpoint[2].y << endl;
                cout << rectpoint[3].x << "," << rectpoint[3].y << endl;

                //获取条码矩形框中心
                Point2f Center = rect.center;
                Center.x;
                Center.y;
              //  cout << "x=" << Center.x << "," << "y=" << Center.y << endl;
                //图像移动到定位点需要的距离
                float x = CPoint.x - Center.x;
                float y = CPoint.y - Center.y;
                //利用放射变换函数平移
                //变换矩阵M需要自己写,M是2*3矩阵,前两列是线性变换,设置为单位矩阵即可,第三列x,y平移参数
                //例如x,y轴方向各平移50 ,Mat moving = (Mat_<double>(2, 3) << 1, 0, 50, 0, 1, 50);//1,0;1,0是一个2*2的单位举证,保持线性变换不受影响,50,50是2*1的平移矩阵   
                Mat MovedImg;
                Mat moving = (Mat_<double>(2, 3) << 1, 0, x, 0, 1, y);
                warpAffine(img, MovedImg, moving, img.size(), 1, 0, Scalar(0));//仿射变换,进行平移 
               //imshow("平移之后", MovedImg);
               // waitKey();

                //与水平线的角度  
                float angle = rect.angle;
                cout << "angle=" << angle << endl;
                ////为了让正方形横着放,所以旋转角度是不一样的。竖放的,给他加90度,翻过来  
            ////这里的横竖,由倾斜方向所致,所以会出现width小于height的情况
                if (line1 < line2)
                {
                    angle = 90 + angle;
                }
                cout << "angle=" << angle << endl;

               
                Mat RatationedImg;
               
                Mat M2 = getRotationMatrix2D(CPoint, angle, 1);//计算旋转、平移、缩放的变换矩阵 
                warpAffine(MovedImg, RatationedImg, M2, img.size(), 1, 0, Scalar(0));//仿射变换 
                //namedWindow("旋转之后", WINDOW_NORMAL);
                 //imshow("旋转之后", RatationedImg);
                 //waitKey(0);
                imwrite(pDstFileName, RatationedImg); //将矫正后的图片保存下来
              
                

                break;//满足条件输出后,立马跳出,不在继续i+1之后的循环
            }
        }


    }
   
    //destroyAllWindows();
    return 0;
}

void main()
{
    char SrcfileName[50];
    char DstfileName1[50];

    //运行时间测试
    clock_t start, stop;     //clock_t是clock()函数返回的变量类型
    double duration;

    start = clock();
   
    for (int i = 9; i < 11; i++) {


        sprintf_s(SrcfileName, "E:\\Download\\xpp\\xpp_%d.bmp", i);  //读取原图片的路径
        sprintf_s(DstfileName1, "E:\\Download\\xpp\\xpp_dst1\\xpp_%d.bmp", i); //矫正定位图片,保存路径


        //旋转矫正图片
        //第一个参数:输入图片名称;第二个参数:输出图片名称
        GetContoursPic(SrcfileName, DstfileName1);
     
    }

    stop = clock();
    //CLOCKS_PER_SEC是一个常数,表示机器时钟每秒所走的时钟打点数,有的IDE下叫CLK_TCK
    duration = (double)(stop - start) / CLOCKS_PER_SEC;
    cout << "运行时间是:" << duration << "秒" << endl << endl;

}
  

原图

当然这个是原图的截图,所以用这个图复现得的话,肯定要调参数,因为截图分别率啥的都变了。

效果图就不放了,就是摆正的图。

6 项目代码规范

6.1将上面实例进行类封装规范化

将上面实例进行类封装

头文件

cpp 复制代码
#ifndef PINGPIAO_H
#define PINGPIAO_H

#include <opencv2/opencv.hpp>
#include <opencv2/imgproc/imgproc.hpp>
#include <opencv2/highgui/highgui.hpp>
#include <stdio.h>
#include<iostream>


class Pingpiao
{
public:
    // 初始化函数
    int init(std::string file);
    // 函数实现
    int RotationLocate(const cv::Mat SrcImg, cv::Mat& DstImg);

private:
    // 最终图片条码的中心点位置
    cv::Point CPoint = cv::Point(1680, 310);
  
};

#endif // PINGPIAO_H

.cpp文件

cpp 复制代码
#include "Pingpiao.h"


//第一个参数:输入图片名称;第二个参数:输出图片名称
int Pingpiao::RotationLocate(const cv::Mat SrcImg, cv::Mat& DstImg)
{
  
    cv::Mat gray;
    if (SrcImg.channels() > 1)
    {
       
        cvtColor(SrcImg, gray, cv::COLOR_BGR2GRAY);//转化成灰度图
    }

    cv::threshold(gray, gray, 60, 255, cv::THRESH_BINARY);
    gray = 255 - gray;//图像取反
    //获取自定义核
    cv::Mat element1 = getStructuringElement(cv::MORPH_RECT, cv::Size(11, 11));//生成矩形结构元素,腐蚀
    cv::Mat element2 = getStructuringElement(cv::MORPH_RECT, cv::Size(37, 37));//膨胀
    cv::Mat out1, out2;
    out1 = gray;

    //先膨胀、腐蚀
    cv::Mat edge, binary;
    dilate(out1, out1, element1);
    erode(out1, out2, element2);


    //输出测试
    /*namedWindow("膨胀", WINDOW_NORMAL);
    namedWindow("腐蚀", WINDOW_NORMAL);
    imshow("膨胀", out1);
    imshow("腐蚀", out2);
    waitKey(0);*/


    // 轮廓发现与绘制
    std::vector<std::vector<cv::Point>> contours;  //轮廓
    std::vector<cv::Vec4i> hierarchy;  //存放轮廓结构变量


    //CV_CHAIN_APPROX_SIMPLE:仅保存轮廓的拐点信息,把所有轮廓拐点处的点保存入contours向量内,拐点与拐点之间直线段上的信息点不予保留;
    //如果是水平的矩形,那么构成矩形只有4个点,但现在矩形线条是由很多个小线段组成,,,,所以输出的点有很多
    findContours(out2, contours, hierarchy, cv::RETR_TREE, cv::CHAIN_APPROX_SIMPLE, cv::Point());

    //绘制轮廓
    std::cout << "contours.size()=" << contours.size() << std::endl;//轮廓条数
    for (int t = contours.size() - 1; t >= 0; t--)//倒着找更快
    {
        ////////看看几次可以找到条形码
        //drawContours(img, contours, t, Scalar(0, 0, 255), 6, 8);//目标图,要绘制的矩形轮廓,轮廓条数,绘制颜色红,粗度6,
        //namedWindow("轮廓检测结果", WINDOW_NORMAL);
        //imshow("轮廓检测结果", img);
        //imwrite("轮廓检测结果.bmp", img);
        //waitKey();
        float line1, line2;

        CvPoint2D32f rectpoint[4];
        //CvBox2D rect = minAreaRect(Mat(contours[t]));//生成最小外接矩形(随着外轮廓整体扥角度倾斜)。找到一个环绕输入2D点集的最小区域的旋转矩形
        cv::RotatedRect rect = minAreaRect(cv::Mat(contours[t]));
        cvBoxPoints(rect, rectpoint); //获取4个顶点坐标  

        //获取生成最小外接矩形的长和宽
        cv::Size2f rectangle = rect.size;
        line1 = rectangle.height;
        line2 = rectangle.width;

        //面积太小的直接pass
        // 条形码轮廓尺寸大概是613*152, line1横向,line2纵行
        //经过预处理之后,实际检测到条码的尺寸3264*2448,min(566,90),max(580,94);3264*1832,min(639,103),max(647,106)
        if ((rectpoint[0].y < 700) && (rectpoint[0].x > 750 && rectpoint[0].x < 2514))//确保是在上半部分的中间部分(减少干扰、节省时间)
        {
            if ((line1 * line2 > 38000) && (line1 * line2 < 85000))//确保找到的矩形框与条形码面积差不多
            {
                /////输出测试
                //cout << "contours" << contours[t] << endl;//输出第t个矩形框的关键点坐标,如果是水平的矩形,那么构成矩形只有4个点,但现在矩形线条是由很多个小线段组成,所以输出的点有很多
                //drawContours(img, contours, t, Scalar(0, 0, 255), 6, 8);//目标图,要绘制的矩形轮廓,轮廓条数,绘制颜色红,粗度6,
                //namedWindow("轮廓检测结果", WINDOW_NORMAL);
                //imshow("轮廓检测结果", img);
                //imwrite("轮廓检测结果.bmp", img);
                //waitKey();
                /////输出测试 
              /*  std::cout << "line1=" << line1 << ",line2=" << line2 << std::endl;
                std::cout << "检测到的条码矩形框尺寸" << rectangle << std::endl;*/
                //cout << "rectpoint[4]的四个坐标你" << endl;
                //cout << rectpoint[0].x << "," << rectpoint[0].y << endl;
                //cout << rectpoint[1].x << "," << rectpoint[1].y << endl;
                //cout << rectpoint[2].x << "," << rectpoint[2].y << endl;
                //cout << rectpoint[3].x << "," << rectpoint[3].y << endl;

                //获取条码矩形框中心
                cv::Point2f Center = rect.center;
                Center.x;
                Center.y;
                //图像移动到定位点CPoint需要的距离
                float x = CPoint.x - Center.x;
                float y = CPoint.y - Center.y;
                //利用放射变换函数平移
                //变换矩阵M需要自己写,M是2*3矩阵,前两列是线性变换,设置为单位矩阵即可,第三列x,y平移参数
                //例如x,y轴方向各平移50 ,Mat moving = (Mat_<double>(2, 3) << 1, 0, 50, 0, 1, 50);
                cv::Mat MovedImg;
                cv::Mat moving = (cv::Mat_<double>(2, 3) << 1, 0, x, 0, 1, y);
                warpAffine(SrcImg, MovedImg, moving, SrcImg.size(), 1, 0, cv::Scalar(0));//仿射变换,进行平移 
               /* imshow("平移之后", MovedImg);
                waitKey();*/

                //与水平线的角度  
                float angle = rect.angle;
               // cout << "angle=" << angle << endl;
                ////为了让正方形横着放,所以旋转角度是不一样的。竖放的,给他加90度,翻过来  
            ////这里的横竖,line1和line2的参考位置不一样,斜率大于0和小于0时,长宽位置调换了
                if (line1 > line2)
                {
                    angle = 90 + angle;
                }
                // cout << "angle=" << angle << endl;


                cv::Mat RatationedImg;
                cv::Mat M2 = getRotationMatrix2D(CPoint, angle, 1);//计算旋转缩放的变换矩阵 
                warpAffine(MovedImg, RatationedImg, M2, SrcImg.size(), 1, 0, cv::Scalar(0));//仿射变换 
                //namedWindow("旋转之后", WINDOW_NORMAL);
                //imshow("旋转之后", RatationedImg);
                //imwrite("DstImg.bmp", RatationedImg); //将矫正后的图片保存下来
                //cv::waitKey(0);
                DstImg = RatationedImg.clone();
                
                break;//满足条件输出后,立马跳出,不在继续i+1之后的循环
            }
        }


    }
  //  destroyAllWindows();
    return 0;

}

mian文件

cpp 复制代码
#include "Pingpiao.h"
int main()
{
    Pingpiao* t = new Pingpiao();

    cv::Mat src, dst;
    src = cv::imread("E:\\Download\\xpp\\xxp2\\xpp_16.bmp");

    //第一个参数:输入图片名称;第二个参数:输出图片名称
    t->RotationLocate(src, dst);

    return 0;
}

6.2 加上作用域

平时为了方便,我们在代码开头加上命名空间 ,那么写代码的时候就不用加作用域cv::std::了;

但这种方法仅适用于简单的项目,复杂的项目中不推荐;

cpp 复制代码
using namespace std;
using namespace cv;

但实际项目开发时,可能会调用多个库文,那么这是就会出现 相同名称但属于不同命名空间的类 如std::Rect和cv::Rect

  • 这时若不加作用域就会报错;
  • 直接在代码前另一个好处 就是增强代码可读性。

在使用的地方加了作用于那么代码开头就不要加 using namespace std;using namespace cv;这些了

cpp 复制代码
cv::Mat RatationedImg;
cv::Mat M2 = getRotationMatrix2D(CPoint, angle, 1);//计算旋转缩放的变换矩阵 

6.3 函数带返回值的作用?

有时候,就觉得函数类型void就行了,明明没有返回值的必要,为啥不用void,偏要写成返回值形式呢 ?

可通过返回值判断函数执行过程是否出错、函数功能是否执行成功,然后决定下一步的行为。

如:

  • 当前函数功能执行成功,则进行下一步正常执行;
  • 当前函数功能执行不成功,则做出错误提示或者 终止执行等;

7. 变换系数合并(旋转平移一次完成)

上面实例中,先将图片平移到固定位置,在水平旋转。

进行了两次仿射变换,那么我们是否可以将两次变换系数合并,进行一次仿射变换,达到效果呢 ?

cpp 复制代码
 //先平移,在旋转,两次仿射变换
Mat MovedImg;
Mat moving = (Mat_<double>(2, 3) << 1, 0, x, 0, 1, y);//平移进行平移
warpAffine(img, MovedImg, moving, img.size(), 1, 0, Scalar(0));//仿射变换,进行平移 

Mat RatationedImg;
Mat M2 = getRotationMatrix2D(CPoint, angle, 1);//计算旋转变换矩阵 
warpAffine(MovedImg, RatationedImg, M2, img.size(), 1, 0, Scalar(0));//仿射变换,进行旋转

合并M矩阵,旋转平移一次完成

变换系数是2*3的矩阵,前两列使线性变换(旋转、缩放),第三列平移参数。

Mat moving = (Mat_(2, 3) << 1, 0, x, 0, 1, y);

这里合并的时候,我将moving做了改变,前两列原本是一个单位矩阵,都改为0(为了保证相加之后前两列不会改变),只在M的第三列加上平移参数。

改变后

Mat moving = (Mat_(2, 3) << 0, 0, x, 0, 0, y);

cpp 复制代码
//合并M矩阵,旋转平移一次完成
Mat moving = (Mat_<double>(2, 3) << 0, 0, x, 0, 0, y);//平移

Mat RatationedImg;
Mat M = getRotationMatrix2D(CPoint, angle, 1);//计算旋转变换矩阵
Mat M2 = moving + M;//合并M矩阵

warpAffine(img, RatationedImg, M2, img.size(), 1, 0, Scalar(0));//仿射变换(旋转平移一次完成)

看似完美,合并前后的代码,分别测试了10张图片,第二种合并后的结果,不稳定,最后两张图出现了轻微偏差

moving矩阵恢复为moving = (Mat_(2, 3) << 1, 0, x, 0, 1, y);

cpp 复制代码
Mat M2 = moving *M//直接相乘运行出错
Mat M2 = M*moving ;//直接相乘运行出错

为何?

cpp 复制代码
Mat M2 = moving + M;
Mat M2 = moving *M;
Mat M2 = M*moving ;
//合并实现有问题,并不简单的相加关系,有点像相乘,但不完全是;

需要专门针对仿射变换的变换系数合并的函数

cpp 复制代码
inline cv::Mat affineMatMat(const cv::Mat& affineMat1, const cv::Mat& affineMat2) {
    // check input 
    if (affineMat1.size() != cv::Size(3, 2)) {
        return cv::Mat();
    }
    if (affineMat1.size() != affineMat2.size()) {
        return cv::Mat();
    }
    if (affineMat1.type() != 6) {
        affineMat1.convertTo(affineMat1, CV_64F);
    }
    if (affineMat2.type() != 6) {
        affineMat2.convertTo(affineMat2, CV_64F);
    }
    // doing 
    cv::Mat m1, m2;
    m1 = cv::Mat::eye(3, 3, CV_64F);
    m2 = cv::Mat::eye(3, 3, CV_64F);
    affineMat1.copyTo(m1(cv::Rect(0, 0, 3, 2)));
    affineMat2.copyTo(m2(cv::Rect(0, 0, 3, 2)));

    cv::Mat m3 = m2 * m1;
    m3 = m3(cv::Rect(0, 0, 3, 2));
    return m3;

}

调用

cpp 复制代码
Mat M2=affineMatMat(moving, M);

OK,问题解决,效果和合并前一样。

如果,moving, M位置调换,

Mat M2=affineMatMat(M,moving);

测试了10张图片,结果,不稳定,还是上述10张图,最后一张张出现了轻微偏差。

cpp 复制代码
//Mat M2 = moving+M;//测试了10张图片,结果,不稳定,有两张出现了轻微偏差
//Mat M2 =M* moving;//////合并实现有问题,并不简单的相加关系,有点像相乘,直接相乘运行出错,但不完全是;
Mat M2=affineMatMat(moving, M);//调用M矩阵合并函数,完美。但是注意moving, M顺序,反过来,结果也不稳定;还是少用合并

所以,还是老老实实两次仿射变换吧!

7.2 合并问题,再探究

先旋转,再平移

先平移,再旋转

会一样吗?

cpp 复制代码
 Mat RatationedImg;
Mat M2 = getRotationMatrix2D(CPoint, angle, 1);//计算旋转变换矩阵 
warpAffine(img, RatationedImg, M2, img.size(), 1, 0, Scalar(0));//仿射变换,进行旋转

Mat MovedImg;
Mat moving = (Mat_<double>(2, 3) << 1, 0, x, 0, 1, y);//平移进行平移
warpAffine(RatationedImg, MovedImg, moving, img.size(), 1, 0, Scalar(0));//仿射变换,进行平移 

结果不一样(同M2 = moving+M,一样)

测试了10张图片,结果不稳定,最后两张图出现了轻微偏差(图9偏下,图10偏上)

M2 = moving+M,M2 = M+moving测试

cpp 复制代码
////合并M矩阵,旋转平移一次完成
Mat moving = (Mat_<double>(2, 3) << 0, 0, x, 0, 0, y);//平移进行平移
Mat RatationedImg;
Mat M = getRotationMatrix2D(CPoint, angle, 1);//计算旋转变换矩阵
Mat M2 = moving+M;//测试了10张图片,结果不稳定,最后两张图出现了轻微偏差(图9偏下,图10偏上)
 warpAffine(img, RatationedImg, M2, img.size(), 1, 0, Scalar(0));//仿射变换(旋转平移一次完成)

结果,

M2 = moving+M; M2 = M+moving; 先旋转,再平移;

三者结果相同,

为什么?

先平移,再旋转,是以条码中心,为中心旋转的,准!

先旋转,在平移,是以定位点旋转的。

虽然这两种情况,旋转中心都是CPoint= Point(1700, 350);对整张图来说,旋转中心没变,但对图中的品票来说,旋转点不一样。

7.3合并总结

先平移,再旋转;(旋转时,二心合一,旋转中心既是条码中心,又是定位中心,)

VS

合并变换矩阵,旋转平移,一次完成。(旋转时,旋转中心是定位中心,旋转移动完毕,二心才合一)

这两个,看似等价,实则旋转中心变了。所以,会出现不稳定,部分存在轻微偏差情况。

8 关于旋转矩形的角度、顶点、宽高等介绍

【OpenCV C++】minAreaRect()最小外接旋转矩形,旋转矩形的4个顶点顺序、中心坐标、宽度、高度、旋转角度(是度数,不是弧度)的对应关系

相关推荐
汤永红12 分钟前
第4章 程序段的反复执行4 多重循环练习(题及答案)
数据结构·c++·算法·信睡奥赛
稚肩4 小时前
如何在linux中使用Makefile构建一个C++工程?
linux·运维·c++
啊森要自信4 小时前
【QT】常⽤控件详解(七)容器类控件 GroupBox && TabWidget && 布局管理器 && Spacer
linux·开发语言·c++·qt·adb
源代码•宸4 小时前
C++高频知识点(二十)
开发语言·c++·经验分享·epoll·拆包封包·名称修饰
重启的码农5 小时前
ZeroTier 源码解析 (2) 节点 (Node)
c++·网络协议
重启的码农5 小时前
ZeroTier源码解析 (3) 身份 (Identity)
c++·网络协议
遇见你的雩风5 小时前
C++结构体的赋形之记
c++
郝学胜-神的一滴6 小时前
Horse3D引擎研发笔记(一):从使用Qt的OpenGL库绘制三角形开始
c++·qt·3d·unity·图形渲染·unreal engine
草莓熊Lotso6 小时前
《解锁 C++ 起源与核心:命名空间用法 + 版本演进全知道》
c++·经验分享·笔记·其他