• Java基于opencv—透视变换矫正图像


    很多时候我们拍摄的照片都会产生一点畸变的,就像下面的这张图

    Java基于opencv—透视变换矫正图像

    虽然不是很明显,但还是有一点畸变的,而我们要做的就是把它变成下面的这张图

    Java基于opencv—透视变换矫正图像

    效果看起来并不是很好,主要是四个顶点找的不准确,会有一些偏差,而且矫正后产生的目标图是倒着的,哪位好心人给说说为啥

    因为我也没有测试畸变很大的图像,也不能保证方法适用于每个图像,这里仅提供我的思路供大家参考。

    思路:

    我们最重要的就是找到图像的四个顶点,有利用hough直线,求直线交点确定四个顶点,有采用寻找轮廓确定四个顶点等等;今天我提供的思路,也是采用寻找轮廓的方法,用approxPolyDP函数,对图像轮廓点进行多边形拟合,可以得到大概的一个这样的图

    Java基于opencv—透视变换矫正图像

    可以看到图像的四个顶点处,都有小白点。接下来我们要做的就是把这些点归类,即划分出四个区域[左上,右上,右下,左下];我采用的是利用opencv的寻找轮廓,得到最大轮廓,然后生成最小外接矩形,确定四个顶点的大致位置;然后设置一个阀值,与上图中的点集合求距离,大于阀值的舍弃,小于的保留,可以得到如下的图像

    Java基于opencv—透视变换矫正图像

    这样所有的点集都落到了四个区域,利用矩形中,对角线距离最大,确定四个顶点的位置,发现效果并不是很好,如下图

    Java基于opencv—透视变换矫正图像

    到此四个顶点的位置大概的确定了,就只需要根据输入和输出点获得图像透视变换的矩阵,然后透视变换;

    我们把思路再理一下:

    1、寻找图像的四个顶点的坐标(重要)
    思路: 1、canny描边 2、寻找最大轮廓 3、对最大轮廓点集合逼近,得到轮廓的大致点集合 4、把点击划分到四个区域中,即左上,右上,左下,右下 5、根据矩形中,对角线最长,找到矩形的四个顶点坐标
    2、根据输入和输出点获得图像透视变换的矩阵
    3、透视变换

    我们来跟着思路实现一下代码

    1、canny描边

    /**
    	 * canny算法,边缘检测
    	 * 
    	 * @param src
    	 * @return
     */
    public static Mat canny(Mat src) {
    	Mat mat = src.clone();
    	Imgproc.Canny(src, mat, 60, 200);
    	HandleImgUtils.saveImg(mat, "C:/Users/admin/Desktop/opencv/open/x/canny.jpg");
    	return mat;
    }
    

    2、寻找最大轮廓;3、对最大轮廓点集合逼近,得到轮廓的大致点集合(代码中有很多冗余,后期会进行优化)

    /**
    	 * 利用函数approxPolyDP来对指定的点集进行逼近 精确度设置好,效果还是比较好的
    	 * 
    	 * @param cannyMat
    	 */
    	public static Point[] useApproxPolyDPFindPoints(Mat cannyMat) {
    
    		List<MatOfPoint> contours = new ArrayList<MatOfPoint>();
    		Mat hierarchy = new Mat();
    
    		// 寻找轮廓
    		Imgproc.findContours(cannyMat, contours, hierarchy, Imgproc.RETR_EXTERNAL, Imgproc.CHAIN_APPROX_NONE,
    				new Point(0, 0));
    
    		// 找出匹配到的最大轮廓
    		double area = Imgproc.boundingRect(contours.get(0)).area();
    		int index = 0;
    
    		// 找出匹配到的最大轮廓
    		for (int i = 0; i < contours.size(); i++) {
    			double tempArea = Imgproc.boundingRect(contours.get(i)).area();
    			if (tempArea > area) {
    				area = tempArea;
    				index = i;
    			}
    		}
    
    		MatOfPoint2f approxCurve = new MatOfPoint2f();
    		MatOfPoint2f matOfPoint2f = new MatOfPoint2f(contours.get(index).toArray());
    
    		// 原始曲线与近似曲线之间的最大距离设置为0.01,true表示是闭合的曲线
    		Imgproc.approxPolyDP(matOfPoint2f, approxCurve, 0.01, true);
    
    		Point[] points = approxCurve.toArray();
    
    		return points;
    	}
    

    获取四个顶点的参照点

    /**
    	 * 获取四个顶点的参照点,返回Point数组[左上,右上,右下,左下] 思路: 我们可以把四个点分成两部分,左部分,右部分
    	 * 左部分:高的为左上,低的为左下(高低是以人的视觉) 右部分同理 首先我们找到最左和最右的位置,以它们的两个中间为分界点,
    	 * 靠左的划分到左部分,靠右的划分到右部分 如果一个区域有三个或更多,哪个比较靠近分界线,划分到少的那个区域
    	 * 
    	 * @param cannyMat
    	 * @return
    	 */
    	public static Point[] findReferencePoint(Mat cannyMat) {
    		RotatedRect rect = findMaxRect(cannyMat);
    		Point[] referencePoints = new Point[4];
    		rect.points(referencePoints);
    		double minX = Double.MAX_VALUE;
    		double maxX = Double.MIN_VALUE;
    		for (int i = 0; i < referencePoints.length; i++) {
    			referencePoints[i].x = Math.abs(referencePoints[i].x);
    			referencePoints[i].y = Math.abs(referencePoints[i].y);
    			minX = referencePoints[i].x < minX ? referencePoints[i].x : minX;
    			maxX = referencePoints[i].x > maxX ? referencePoints[i].x : maxX;
    		}
    
    		double center = (minX + maxX) / 2;
    		List<Point> leftPart = new ArrayList<Point>();
    		List<Point> rightPart = new ArrayList<Point>();
    		// 划分左右两个部分
    		for (int i = 0; i < referencePoints.length; i++) {
    			if (referencePoints[i].x < center) {
    				leftPart.add(referencePoints[i]);
    			} else if (referencePoints[i].x > center) {
    				rightPart.add(referencePoints[i]);
    			} else {
    				if (leftPart.size() < rightPart.size()) {
    					leftPart.add(referencePoints[i]);
    				} else {
    					rightPart.add(referencePoints[i]);
    				}
    			}
    		}
    		double minDistance = 0;
    		int minIndex = 0;
    		if (leftPart.size() < rightPart.size()) {
    			// 左部分少
    			minDistance = rightPart.get(0).x - center;
    			minIndex = 0;
    			for (int i = 1; i < rightPart.size(); i++) {
    				if (rightPart.get(i).x - center < minDistance) {
    					minDistance = rightPart.get(i).x - center;
    					minIndex = i;
    				}
    			}
    			leftPart.add(rightPart.remove(minIndex));
    
    		} else if (leftPart.size() > rightPart.size()) {
    			// 右部分少
    			minDistance = center - leftPart.get(0).x;
    			minIndex = 0;
    			for (int i = 1; i < leftPart.size(); i++) {
    				if (center - leftPart.get(0).x < minDistance) {
    					minDistance = center - leftPart.get(0).x;
    					minIndex = i;
    				}
    			}
    			rightPart.add(leftPart.remove(minIndex));
    		}
    
    		if (leftPart.get(0).y < leftPart.get(1).y) {
    			referencePoints[0] = leftPart.get(0);
    			referencePoints[3] = leftPart.get(1);
    		}
    
    		if (rightPart.get(0).y < rightPart.get(1).y) {
    			referencePoints[1] = rightPart.get(0);
    			referencePoints[2] = rightPart.get(1);
    		}
    
    		return referencePoints;
    	}
    

    4、把点击划分到四个区域中,即左上,右上,右下,左下(效果还可以)

    /**
    	 * 把点击划分到四个区域中,即左上,右上,右下,左下
    	 * 
    	 * @param points
    	 *            逼近的点集
    	 * @param referencePoints
    	 *            四个参照点集(通过寻找最大轮廓,进行minAreaRect得到四个点[左上,右上,右下,左下])
    	 */
    	public static Map<String, List> pointsDivideArea(Point[] points, Point[] referencePoints) {
    		// 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;
    		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;
    				}
    			}
    		}
    		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;
    	}
    

    5、根据矩形中,对角线最长,找到矩形的四个顶点坐标(效果不好)

    /**
    	 * 具体的寻找四个顶点的坐标
    	 * 
    	 * @param map
    	 *            四个点集域 即左上,右上,右下,左下
    	 * @return
    	 */
    	public static Point[] specificFindFourPoint(Map<String, List> map) {
    		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");// 右下
    
    		System.out.println("px1.size() " + px1.size());
    		System.out.println("px2.size() " + px2.size());
    		System.out.println("py1.size() " + py1.size());
    		System.out.println("py2.size() " + py2.size());
    
    		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;
    				}
    			}
    		}
    		result[0] = px1.get(p1);
    		result[2] = py2.get(p2);
    
    		// 寻找左下,右上
    		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;
    				}
    			}
    		}
    		result[1] = py1.get(p2);
    		result[3] = px2.get(p1);
    		return result;
    	}
    

    整合寻找四个顶点坐标函数

    /**
    	 * 寻找四个顶点的坐标 思路: 1、canny描边 2、寻找最大轮廓 3、对最大轮廓点集合逼近,得到轮廓的大致点集合
    	 * 4、把点击划分到四个区域中,即左上,右上,左下,右下 5、根据矩形中,对角线最长,找到矩形的四个顶点坐标
    	 * 
    	 * @param src
    	 */
    	public static Point[] findFourPoint(Mat src) {
    		// 1、canny描边
    		Mat cannyMat = canny(src);
    		// 2、寻找最大轮廓;3、对最大轮廓点集合逼近,得到轮廓的大致点集合
    		Point[] points = useApproxPolyDPFindPoints(cannyMat);
    		
    		//在图像上画出逼近的点
    		Mat approxPolyMat = src.clone();
    		for( int i = 0; i < points.length ; i++) {
    			setPixel(approxPolyMat, (int)points[i].y, (int) points[i].x, 255);
    		}
    		
    		saveImg(approxPolyMat, "C:/Users/admin/Desktop/opencv/open/q/x11-approxPolyMat.jpg");
    		
    		// 获取参照点集
    		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");// 右下
    
    		for (int i = 0; i < px1.size(); i++) {
    			setPixel(areaMat, (int) px1.get(i).y, (int) px1.get(i).x, 255);
    		}
    
    		for (int i = 0; i < px2.size(); i++) {
    			setPixel(areaMat, (int) px2.get(i).y, (int) px2.get(i).x, 255);
    		}
    
    		for (int i = 0; i < py1.size(); i++) {
    			setPixel(areaMat, (int) py1.get(i).y, (int) py1.get(i).x, 255);
    		}
    
    		for (int i = 0; i < py2.size(); i++) {
    			setPixel(areaMat, (int) py2.get(i).y, (int) py2.get(i).x, 255);
    		}
    
    		saveImg(areaMat, "C:/Users/admin/Desktop/opencv/open/q/x11-pointsDivideArea.jpg");
    
    		// 5、根据矩形中,对角线最长,找到矩形的四个顶点坐标(效果不好)
    		Point[] result = specificFindFourPoint(map);
    
    		return result;
    	}
    

    透视变换,矫正图像

    /**
    	 * 透视变换,矫正图像 思路: 1、寻找图像的四个顶点的坐标(重要) 思路: 1、canny描边 2、寻找最大轮廓
    	 * 3、对最大轮廓点集合逼近,得到轮廓的大致点集合 4、把点击划分到四个区域中,即左上,右上,左下,右下 5、根据矩形中,对角线最长,找到矩形的四个顶点坐标
    	 * 2、根据输入和输出点获得图像透视变换的矩阵 3、透视变换
    	 * 
    	 * @param src
    	 */
    	public static Mat warpPerspective(Mat src) {
    		// 灰度话
    		src = HandleImgUtils.gray(src);
    		// 找到四个点
    		Point[] points = HandleImgUtils.findFourPoint(src);
    
    		// Canny
    		Mat cannyMat = HandleImgUtils.canny(src);
    		// 寻找最大矩形
    		RotatedRect rect = HandleImgUtils.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);
    
    		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));
    
    		System.out.println(r.x + "," + r.y);
    
    		Mat dstPoints = Converters.vector_Point_to_Mat(listDsts, CvType.CV_32F);
    
    		Mat perspectiveMmat = Imgproc.getPerspectiveTransform(srcPoints, dstPoints);
    
    		Mat dst = new Mat();
    
    		Imgproc.warpPerspective(src, dst, perspectiveMmat, src.size(), Imgproc.INTER_LINEAR + Imgproc.WARP_INVERSE_MAP,
    				1, new Scalar(0));
    		
    		return dst;
    
    	}
    

    测试函数

    /**
    	 * 测试透视变换
    	 */
    	public void testWarpPerspective() {
    		System.loadLibrary(Core.NATIVE_LIBRARY_NAME);
    		Mat src = HandleImgUtils.matFactory("C:/Users/admin/Desktop/opencv/open/q/x10.jpg");
    		src = HandleImgUtils.warpPerspective(src);
    		HandleImgUtils.saveImg(src, "C:/Users/admin/Desktop/opencv/open/q/x10-testWarpPerspective.jpg");
    	}
    

    本项目所有代码地址:https://github.com/YLDarren/opencvHandleImg
    觉得写的不错话,还是希望能给个Star的

  • 相关阅读:
    Linux 的硬链接与软链接
    Django补遗(一)
    Django之Form组件
    Django进阶(三)
    Django进阶(二)
    Web请求提交页面--防重提交
    Lucene的搭建(3)
    Lucene的搭建(2)
    Redis-cluster集群搭建
    Redis安装
  • 原文地址:https://www.cnblogs.com/qjmnong/p/9721395.html
Copyright © 2020-2023  润新知