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的


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM