文章目录
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 代码实现
#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将上面实例进行类封装规范化
将上面实例进行类封装
头文件
#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文件
#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文件
#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::
了;
但这种方法仅适用于简单的项目,复杂的项目中不推荐;
using namespace std;
using namespace cv;
但实际项目开发时,可能会调用多个库文,那么这是就会出现 相同名称但属于不同命名空间的类 如std::Rect和cv::Rect
,
- 这时若不加作用域就会报错;
- 直接在代码前另一个好处 就是增强代码可读性。
在使用的地方加了作用于那么代码开头就不要加using namespace std;using namespace cv;
这些了
cv::Mat RatationedImg;
cv::Mat M2 = getRotationMatrix2D(CPoint, angle, 1);//计算旋转缩放的变换矩阵
6.3 函数带返回值的作用?
有时候,就觉得函数类型void就行了,明明没有返回值的必要,为啥不用void,偏要写成返回值形式呢 ?
可通过返回值判断函数执行过程是否出错、函数功能是否执行成功,然后决定下一步的行为。
如:
- 当前函数功能执行成功,则进行下一步正常执行;
- 当前函数功能执行不成功,则做出错误提示或者 终止执行等;
7. 变换系数合并(旋转平移一次完成)
上面实例中,先将图片平移到固定位置,在水平旋转。
进行了两次仿射变换,那么我们是否可以将两次变换系数合并,进行一次仿射变换,达到效果呢 ?
//先平移,在旋转,两次仿射变换
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);
//合并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);
Mat M2 = moving *M//直接相乘运行出错
Mat M2 = M*moving ;//直接相乘运行出错
为何?
Mat M2 = moving + M;
Mat M2 = moving *M;
Mat M2 = M*moving ;
//合并实现有问题,并不简单的相加关系,有点像相乘,但不完全是;
需要专门针对仿射变换的变换系数合并的函数
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;
}
调用
Mat M2=affineMatMat(moving, M);
OK,问题解决,效果和合并前一样。
如果,moving, M位置调换,
Mat M2=affineMatMat(M,moving);
测试了10张图片,结果,不稳定,还是上述10张图,最后一张张出现了轻微偏差。
//Mat M2 = moving+M;//测试了10张图片,结果,不稳定,有两张出现了轻微偏差
//Mat M2 =M* moving;//////合并实现有问题,并不简单的相加关系,有点像相乘,直接相乘运行出错,但不完全是;
Mat M2=affineMatMat(moving, M);//调用M矩阵合并函数,完美。但是注意moving, M顺序,反过来,结果也不稳定;还是少用合并
所以,还是老老实实两次仿射变换吧!
7.2 合并问题,再探究
先旋转,再平移
先平移,再旋转
会一样吗?
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测试
////合并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个顶点顺序、中心坐标、宽度、高度、旋转角度(是度数,不是弧度)的对应关系