2021SC@SDUSC
前言
对于图片抗扭曲功能算法的实现,可以划分为以下任务:
1.采⽤寻找轮廓的⽅法,⽤approxPolyDP函数,对图像轮廓点进⾏多边形拟合
2.把图像的四个顶点处的点归类,划分出四个区域{左上,右上,右下,左下},利⽤opencv的寻找轮廓,得到最⼤轮廓,然后⽣成最⼩外接矩形,确定四个顶点的⼤致位置。设置⼀个阀值,与上图中的点集合求距离,⼤于阀值的舍弃,⼩于的保留。
3.所有的点集都落到了四个区域,利⽤矩形中,对⻆线距离最⼤,确定四个顶点的位置
4.根据输⼊和输出点获得图像透视变换的矩阵
5.透视变换。透视变换(Perspective Transformation)是指利用透视中心、像点、目标点三点共线的条件,按透视旋转定律使承影面(透视面)绕迹线(透视轴)旋转某一角度,破坏原有的投影光线束,仍能保持承影面上投影几何图形不变的变换。
这一篇我们将继续分析寻找轮廓方法的代码
一、项目环境
android studio版本 4.1.2
sdk版本 Compile SDK version:30
Build Tools Version 30.0.3
gradle版本 6.8.3
二、代码分析
1.把点集划分到四个区域中
// px1 左上,px2左下,py1右上,py2右下
List<Point> px1 = new ArrayList<Point>(),
px2 = new ArrayList<Point>(),
py1 = new ArrayList<Point>(),
py2 = new ArrayList<Point>();
int thresold = 50;// 设置距离阀值
double distance = 0;
把点集划分到四个区域中,即左上,右上,右下,左下
四个参照点集(通过寻找最大轮廓,进行minAreaRect得到四个点[左上,右上,右下,左下])
for (int i = 0; i < referencePoints.length; i++) {
for (int j = 0; j < points.length; j++) {
distance = Math.pow(referencePoints[i].x - points[j].x, 2)
+ Math.pow(referencePoints[i].y - points[j].y, 2);
if (distance < Math.pow(thresold, 2)) {
if (i == 0) {
px1.add(points[j]);
} else if (i == 1) {
py1.add(points[j]);
} else if (i == 2) {
py2.add(points[j]);
} else if (i == 3) {
px2.add(points[j]);
}
} else {
continue;
}
}
}
双重for循环,一个循环四个顶点,一个循环点集,计算顶点[i]和点[j]的距离,如果小于距离阀值,我们就认为这个点归属于这个顶点的那个区域,并且加入所属区域
Map<String, List> map = new HashMap<String, List>();
map.put("px1", px1);
map.put("px2", px2);
map.put("py1", py1);
map.put("py2", py2);
return map;
把四个区域存入map中并且返回
2. 具体的寻找四个顶点的坐标
Point[] result = new Point[4];// [左上,右上,右下,左下]
List<Point> px1 = map.get("px1");// 左上
List<Point> px2 = map.get("px2");// 左下
List<Point> py1 = map.get("py1");// 右上
List<Point> py2 = map.get("py2");// 右下
if(px1.size()==0||px2.size()==0||py1.size()==0||py2.size()==0){
Toast.makeText(getApplicationContext(),"请拍摄清晰一点的图片哦",Toast.LENGTH_LONG).show();
}
当某个区域内没有任何的顶点,我们没办法进行下一步的操作,可以用消息提醒用户这一张无法进行图片抗扭曲修正,请重新拍摄
double maxDistance = 0;
double tempDistance;
int i, j;
int p1 = 0, p2 = 0;// 记录点的下标
// 寻找左上,右下
for (i = 0; i < px1.size(); i++) {
for (j = 0; j < py2.size(); j++) {
tempDistance = Math.pow(px1.get(i).x - py2.get(j).x, 2) + Math.pow(px1.get(i).y - py2.get(j).y, 2);
if (tempDistance > maxDistance) {
maxDistance = tempDistance;
p1 = i;
p2 = j;
}
}
}
// 寻找左下,右上
maxDistance = 0;
for (i = 0; i < px2.size(); i++) {
for (j = 0; j < py1.size(); j++) {
tempDistance = Math.pow(px2.get(i).x - py1.get(j).x, 2) + Math.pow(px2.get(i).y - py1.get(j).y, 2);
if (tempDistance > maxDistance) {
maxDistance = tempDistance;
p1 = i;
p2 = j;
}
}
}
找出左上和右下两个区域点集中距离最大的两个点,并且分别得出两个点的索引号,这一步应该是用来求对角线的距离,然后根据矩形中,对角线最长,找到矩形的四个顶点坐标,我们获取到这四个索引值,即知道了四个顶点的坐标
3.整合寻找四个顶点坐标函数
寻找四个顶点的坐标
思路:
- 1、canny描边
- 2、寻找最大轮廓
- 3、对最大轮廓点集合逼近,得到轮廓的大致点集合
- 4、把点集划分到四个区域中,即左上,右上,左下,右下
- 5、根据矩形中,对角线最长,找到矩形的四个顶点坐标
// 1、canny描边
Mat cannyMat = canny(src);
// 2、寻找最大轮廓;
//3、对最大轮廓点集合逼近,得到轮廓的大致点集合
Point[] points = useApproxPolyDPFindPoints(cannyMat);
//在图像上画出逼近的点
Mat approxPolyMat = src.clone();
//获取参照点集
Point[] referencePoints = findReferencePoint(cannyMat);
// 4、把点击划分到四个区域中,即左上,右上,左下,右下(效果还可以)
Map<String, List> map = pointsDivideArea(points, referencePoints);
// 画出标记四个区域中的点集
Mat areaMat = src.clone();
List<Point> px1 = map.get("px1");// 左上
List<Point> px2 = map.get("px2");// 左下
List<Point> py1 = map.get("py1");// 右上
List<Point> py2 = map.get("py2");// 右下
// 5、根据矩形中,对角线最长,找到矩形的四个顶点坐标
Point[] result = specificFindFourPoint(map);
return result;
}
将我们前几步的方法都整合到这一个函数中:
我们利用canny进行边缘检测后,将处理后的图像存储在cannyMat之中,利用 useApproxPolyDPFindPoints()来对cannyMat进行处理,得到最大轮廓之后将最大轮廓的点集存储在Point[]里,并且获得到顶点的参照点,通过参照点将点集划分为左上、左下、右上、右下四个区域,并且通过矩形中,对角线距离最长来计算出四个顶点的坐标。
4.透视变换,矫正图像
// 找到四个顶点
Point[] points = findFourPoint(src);
// Canny,得到边缘检测后的图像
Mat cannyMat = canny(src);
// 寻找最大矩形
RotatedRect rect =findMaxRect(cannyMat);
// 点的顺序[左上 ,右上 ,右下 ,左下]
List<Point> listSrcs = java.util.Arrays.asList(points[0], points[1], points[2], points[3]);
Mat srcPoints = Converters.vector_Point_to_Mat(listSrcs, CvType.CV_32F);
Converters.vector_Point_to_Mat()方法是opencv提供的将list转换为Mat的方法
Rect r = rect.boundingRect();
r.x = Math.abs(r.x);
r.y = Math.abs(r.y);
List<Point> listDsts = java.util.Arrays.asList(new Point(r.x, r.y), new Point(r.x + r.width, r.y),
new Point(r.x + r.width, r.y + r.height), new Point(r.x, r.y + r.height));
//得到r的四个顶点
Mat dstPoints = Converters.vector_Point_to_Mat(listDsts, CvType.CV_32F);
Mat perspectiveMmat = Imgproc.getPerspectiveTransform(srcPoints, dstPoints);
opencv2和opencv3中用于计算透视变换矩阵的函数是cv::getPerspectiveTransform(),C++接口其调用形式如下:
cv::Mat cv::getPerspectiveTransform( // 返回3x3透视变换矩阵
const cv::Point2f* src, // 源图像四个顶点坐标(点数组)
const cv::Point2f* dst // 目标图像上四个顶点的坐标(点数组)
);
输入srcPoints和dstPoints,得到透视变换的矩阵
三、总结
开始了解整个算法中非常重要的一部分:进行透视变换,首先我们要给出源顶点和转换后顶点的位置,从而得到透视变换的矩阵,得到矩阵之后,我们方便对整个图像进行透视变换,我们下一篇将会继续分析透视变换,并且为该项目收尾