You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

使用Java OpenCV实现文档扫描仪时透视变换后输出全灰图像问题

问题:Java OpenCV文档扫描仪透视变换后输出全灰色图像

我用Java OpenCV构建文档扫描仪,流程是图像预处理(灰度化/归一化)→自动检测角点→透视变换将梯形文档转为矩形,但变换完成后得到全灰色的输出图像,已确认检测到的角点是正确的。


代码实现

SkewHandler类

@Component
public class SkewHandler implements PreprocessingHandler{

    private ShapeDetector shapeDetector;

    @Autowired
    SkewHandler(ShapeDetector shapeDetector) {
        this.shapeDetector = shapeDetector;
    }

    @Override
    public Mat doPreprocessing(Mat inputImage) {
        System.out.println("skew");

        Mat greyImage = new GreyScaleHandler().doPreprocessing(inputImage);
        Mat normalizeImage = new NormalizeHandler().doPreprocessing(greyImage);
        Mat threseholdImage = new ThresholdHandler().doPreprocessing(normalizeImage);


        MatOfPoint corners = shapeDetector.getCorners(threseholdImage);
        MatOfPoint polygonCorners = shapeDetector.getApproximatePolygonCorners(corners);
        MatOfPoint frontPerspectiveCorners = shapeDetector.getFrontPerspectiveCorners(corners);
        Size frontPerspectiveSize = shapeDetector.getFrontPerspectiveSize(corners);

        Mat perspectiveTransformMatrix = Imgproc.getPerspectiveTransform(
                        convertPoints(polygonCorners), convertPoints(frontPerspectiveCorners));

        Mat outputImage = new Mat(inputImage.size(), inputImage.type());
        Imgproc.warpPerspective(inputImage, outputImage, perspectiveTransformMatrix,frontPerspectiveSize);

        return outputImage;
    }

    private static MatOfPoint2f convertPoints(MatOfPoint points) {
        MatOfPoint2f points2f = new MatOfPoint2f();
        points.convertTo(points2f, CvType.CV_32FC1);
        return points2f;
    }
}

ShapeDetector类

@Component
public class ShapeDetector {

    public MatOfPoint getCorners(Mat inputImage) {
        List<MatOfPoint> corners = new ArrayList<>();
        Imgproc.findContours(inputImage,corners,new Mat(), Imgproc.RETR_EXTERNAL,Imgproc.CHAIN_APPROX_TC89_L1);

        return corners.stream()
                .max(Comparator.comparing(Imgproc::contourArea))
                .orElse(null);
    }

    public Double getCornersPerimeter(MatOfPoint2f corners) {
        return Imgproc.arcLength(corners, true);
    }

    public MatOfPoint getApproximatePolygonCorners(MatOfPoint corners) {
        MatOfPoint2f corners2f = new MatOfPoint2f();
        corners.convertTo(corners2f, CvType.CV_32FC2);

        MatOfPoint2f polygonCorners2f = new MatOfPoint2f();
        Imgproc.approxPolyDP(corners2f,polygonCorners2f, 0.015*getCornersPerimeter(corners2f),true);

        MatOfPoint polygonCorners = new MatOfPoint();
        polygonCorners2f.convertTo(polygonCorners, CvType.CV_32S);

        return polygonCorners;
    }

    public MatOfPoint getFrontPerspectiveCorners(MatOfPoint corners) {
        double maxX = corners.toList().stream()
                .mapToDouble(point -> point.x).max().getAsDouble();

        double minX = corners.toList().stream()
                .mapToDouble(point -> point.x).min().getAsDouble();

        double maxY = corners.toList().stream()
                .mapToDouble(point -> point.y).max().getAsDouble();

        double minY = corners.toList().stream().
                mapToDouble(point -> point.y).min().getAsDouble();

        double maxWidth = maxX - minX;
        double maxHeight = maxY -minY;

        return new MatOfPoint(
                new Point(0,0),
                new Point(maxWidth, 0),
                new Point(maxWidth, maxHeight),
                new Point(maxHeight, 0));
    }

    public Size getFrontPerspectiveSize(MatOfPoint corners) {
        double maxX = corners.toList().stream()
                .mapToDouble(point -> point.x).max().getAsDouble();

        double minX = corners.toList().stream()
                .mapToDouble(point -> point.x).min().getAsDouble();

        double maxY = corners.toList().stream()
                .mapToDouble(point -> point.y).max().getAsDouble();

        double minY = corners.toList().stream().
                mapToDouble(point -> point.y).min().getAsDouble();

        double maxWidth = maxX - minX;
        double maxHeight = maxY -minY;

        return new Size(maxWidth, maxHeight);
    }
}

问题根源与修复方案

1. 目标角点坐标错误

getFrontPerspectiveCorners方法最后一个点写错,导致目标区域完全错位:
修复代码:

public MatOfPoint getFrontPerspectiveCorners(MatOfPoint corners) {
    double maxX = corners.toList().stream()
            .mapToDouble(point -> point.x).max().getAsDouble();
    double minX = corners.toList().stream()
            .mapToDouble(point -> point.x).min().getAsDouble();
    double maxY = corners.toList().stream()
            .mapToDouble(point -> point.y).max().getAsDouble();
    double minY = corners.toList().stream()
            .mapToDouble(point -> point.y).min().getAsDouble();

    double maxWidth = maxX - minX;
    double maxHeight = maxY - minY;

    // 修正为左上、右上、右下、左下的正确顺序
    return new MatOfPoint(
            new Point(0, 0),
            new Point(maxWidth, 0),
            new Point(maxWidth, maxHeight),
            new Point(0, maxHeight));
}

2. 点类型转换错误

convertPoints方法使用CV_32FC1错误,应该用CV_32FC2对应点的x/y双分量:
修复代码:

private static MatOfPoint2f convertPoints(MatOfPoint points) {
    MatOfPoint2f points2f = new MatOfPoint2f();
    points.convertTo(points2f, CvType.CV_32FC2);
    return points2f;
}

3. 源角点与目标角点顺序不匹配

approxPolyDP返回的角点顺序随机,需按固定顺序排序:
添加角点排序方法到ShapeDetector:

public MatOfPoint sortCorners(MatOfPoint corners) {
    List<Point> pointList = corners.toList();
    // 左上:x+y最小
    Point topLeft = pointList.stream().min(Comparator.comparingDouble(p -> p.x + p.y)).get();
    // 右上:x-y最小
    Point topRight = pointList.stream().min(Comparator.comparingDouble(p -> p.x - p.y)).get();
    // 右下:x+y最大
    Point bottomRight = pointList.stream().max(Comparator.comparingDouble(p -> p.x + p.y)).get();
    // 左下:x-y最大
    Point bottomLeft = pointList.stream().max(Comparator.comparingDouble(p -> p.x - p.y)).get();

    return new MatOfPoint(topLeft, topRight, bottomRight, bottomLeft);
}

在SkewHandler中调用排序:

MatOfPoint polygonCorners = shapeDetector.getApproximatePolygonCorners(corners);
polygonCorners = shapeDetector.sortCorners(polygonCorners);

4. 优化输出Mat创建

warpPerspective会自动根据目标尺寸创建Mat,无需提前指定原尺寸:

// 移除原语句,替换为
Mat outputImage = new Mat();
Imgproc.warpPerspective(inputImage, outputImage, perspectiveTransformMatrix, frontPerspectiveSize);

内容的提问来源于stack exchange,提问作者Cosmin Ştefan

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.30 18:06:40