OpenCV透视校正非线性畸变:图像X轴区域拉伸收缩异常求助
透视校正后图像左右拉伸/收缩异常的解决方法
问题描述
使用OpenCV进行透视校正时,输出图像出现左侧(小X坐标区域)拉伸、右侧(大X坐标区域)收缩的问题。正常情况下,原图中心的垂直绿线应位于输出图像X轴中间,但实际偏离明显,变换效果不符合预期。
原图(标注转换区域):
输出图像:
问题代码
import org.opencv.core.*; import org.opencv.imgcodecs.Imgcodecs; import org.opencv.imgproc.Imgproc; public class ImagePerspective { static { System.loadLibrary(Core.NATIVE_LIBRARY_NAME); } /** * Transforms an image using four points, adjusting the size of the output image to be * as large as possible based on the points and the aspect ratio. * * @param fileIn Path to the input image. * @param points Four points in the original image defined in the following order: * - (x1, y1): top-left * - (x2, y2): top-right * - (x3, y3): bottom-right * - (x4, y4): bottom-left * @param fileOut Path where the transformed image will be saved. * @param aspectRatio Width/height ratio of the output image. <=0 to maintain the original ratio. */ public static void transformImage(String fileIn, Point[] points, String fileOut, int finalWidth, int finalHeight) { // load original image Mat imageOrig = Imgcodecs.imread(fileIn); if (imageOrig.empty()) { throw new IllegalArgumentException("Can not read image: " + fileIn); } log("Original size: " + imageOrig.width() + "x" + imageOrig.height()); log("Final size : " + finalWidth + "x" + finalHeight); // Define the destination points MatOfPoint2f pointsDest = new MatOfPoint2f( new Point(0, 0), // top-left new Point(finalWidth - 1, 0), // top-right new Point(finalWidth - 1, finalHeight - 1), // bottom-right new Point(0, finalHeight - 1) // bottom-left ); // Define the original points MatOfPoint2f pointsOrig = new MatOfPoint2f(points); // Calculate the perspective transformation matrix Mat transformationMat = Imgproc.getPerspectiveTransform(pointsOrig, pointsDest); // Create the transformed image with white background Mat imageTransformed = new Mat(finalHeight, finalWidth, imageOrig.type(), new Scalar(255, 255, 255)); // Apply transformation Imgproc.warpPerspective( imageOrig, imageTransformed, transformationMat , new Size(finalWidth, finalHeight), Imgproc.INTER_LINEAR , Core.BORDER_CONSTANT, new Scalar(255, 255, 255) ); // Save final image boolean saved = Imgcodecs.imwrite(fileOut, imageTransformed); if (saved) { log("Image saved to " + fileOut); } else { log("Error saving image " + fileOut); } } private static void log(String msg) { System.out.println(msg); } public static void main(String[] args) { String fileIn = "original.png"; String fileOut = "resul.png"; Point[] points = { new Point(12, 0), // top-left new Point(173, 13), // top-right new Point(174, 141), // bottom-right new Point(0, 64) // bottom-left }; // Transforms the image transformImage(fileIn, points, fileOut, 200, 100); } }
问题原因
- 原始四点映射逻辑错误:输入的待校正区域四点中,
bottom-left点(0,64)的X坐标小于top-left点(12,0),导致透视变换的横向映射关系混乱,直接引发左右区域的拉伸/收缩异常。 - 输出尺寸宽高比不匹配:强制指定输出尺寸为200x100(宽高比2:1),与原始待校正区域的实际宽高比不符,进一步放大了变形问题。
解决方案
1. 修正原始四点的顺序与映射关系
确保输入的四点严格按照top-left → top-right → bottom-right → bottom-left的顺序,且每个点对应待校正区域的真实角点。同时确认四点构成的区域符合透视变换的矩形逻辑。
2. 匹配输出图像与原始区域的宽高比
先计算原始待校正区域的实际宽高比,再基于该比例设置输出尺寸,避免强制缩放导致的变形:
// 计算原始区域的实际宽度和高度 double widthTop = Math.hypot(points[1].x - points[0].x, points[1].y - points[0].y); double widthBottom = Math.hypot(points[2].x - points[3].x, points[2].y - points[3].y); double actualWidth = Math.max(widthTop, widthBottom); double heightLeft = Math.hypot(points[3].x - points[0].x, points[3].y - points[0].y); double heightRight = Math.hypot(points[2].x - points[1].x, points[2].y - points[1].y); double actualHeight = Math.max(heightLeft, heightRight); // 基于原始宽高比设置输出尺寸(固定宽度为200,高度按比例计算) int finalWidth = 200; int finalHeight = (int) Math.round(finalWidth * actualHeight / actualWidth);
3. 完整修正后的代码
import org.opencv.core.*; import org.opencv.imgcodecs.Imgcodecs; import org.opencv.imgproc.Imgproc; public class ImagePerspective { static { System.loadLibrary(Core.NATIVE_LIBRARY_NAME); } public static void transformImage(String fileIn, Point[] points, String fileOut, int finalWidth, int finalHeight) { Mat imageOrig = Imgcodecs.imread(fileIn); if (imageOrig.empty()) { throw new IllegalArgumentException("无法读取图像: " + fileIn); } log("原始尺寸: " + imageOrig.width() + "x" + imageOrig.height()); log("输出尺寸: " + finalWidth + "x" + finalHeight); MatOfPoint2f pointsDest = new MatOfPoint2f( new Point(0, 0), // top-left new Point(finalWidth - 1, 0), // top-right new Point(finalWidth - 1, finalHeight - 1), // bottom-right new Point(0, finalHeight - 1) // bottom-left ); MatOfPoint2f pointsOrig = new MatOfPoint2f(points); Mat transformationMat = Imgproc.getPerspectiveTransform(pointsOrig, pointsDest); Mat imageTransformed = new Mat(finalHeight, finalWidth, imageOrig.type(), new Scalar(255, 255, 255)); Imgproc.warpPerspective( imageOrig, imageTransformed, transformationMat , new Size(finalWidth, finalHeight), Imgproc.INTER_LINEAR , Core.BORDER_CONSTANT, new Scalar(255, 255, 255) ); boolean saved = Imgcodecs.imwrite(fileOut, imageTransformed); if (saved) { log("图像已保存至 " + fileOut); } else { log("保存图像失败: " + fileOut); } } private static void log(String msg) { System.out.println(msg); } public static void main(String[] args) { String fileIn = "original.png"; String fileOut = "result.png"; // 确认四点顺序:top-left → top-right → bottom-right → bottom-left Point[] points = { new Point(12, 0), // top-left new Point(173, 13), // top-right new Point(174, 141), // bottom-right new Point(0, 64) // bottom-left }; // 计算原始区域实际宽高比 double widthTop = Math.hypot(points[1].x - points[0].x, points[1].y - points[0].y); double widthBottom = Math.hypot(points[2].x - points[3].x, points[2].y - points[3].y); double actualWidth = Math.max(widthTop, widthBottom); double heightLeft = Math.hypot(points[3].x - points[0].x, points[3].y - points[0].y); double heightRight = Math.hypot(points[2].x - points[1].x, points[2].y - points[1].y); double actualHeight = Math.max(heightLeft, heightRight); // 按原始比例设置输出尺寸 int finalWidth = 200; int finalHeight = (int) Math.round(finalWidth * actualHeight / actualWidth); transformImage(fileIn, points, fileOut, finalWidth, finalHeight); } }
关键说明
- 透视变换的四点对应关系必须严格一致,任何一点的顺序或位置错误都会导致严重变形。
- 保持输出图像与原始区域的宽高比一致,是避免拉伸/收缩异常的核心前提。若必须使用固定尺寸,建议先按比例缩放原始区域再执行透视变换。
内容的提问来源于stack exchange,提问作者Gegomu
相关产品推荐
相关产品推荐

