【问题标题】:How do you transform a quadrilateral area of a BufferedImage into a rectangular BufferedImage in Java?如何在 Java 中将 BufferedImage 的四边形区域转换为矩形 BufferedImage?
【发布时间】:2020-11-01 10:39:17
【问题描述】:

我正在尝试从 3D 中看到的矩形反转透视偏移,使其显示为四边形。这是我要处理的示例图像:

我知道图片中四边形4个角的坐标。

我一直在使用 AffineTransform,特别是剪切方法。但是,我找不到任何关于如何正确确定任意四边形的 shx 和 shy 值的好信息。

最终图像还需要是一个不包含任何黑色背景的矩形,仅包含内部图像。所以我需要一些只选择四边形进行转换的方法。我尝试使用 java.awt 形状,如 Polygon 和 Area 来描述四边形,但它似乎只考虑了轮廓,而不是 Shape 中包含的像素。

【问题讨论】:

  • 有趣的问题。你确定图像是平行四边形吗?对我来说看起来像一个一般的四边形。透视投影不是仿射变换:平行线不会变成平行线,而是在“无穷远点”相交
  • 角的坐标,从左上角到左下角,分别是(439, 42), (841, 3), (816, 574), (472, 683)。跨度>
  • @Joni 这可能不是平行四边形,我认为你是对的。这是我正在使用 Java 开发的 3D 图形引擎的一部分,特别是用于镜像。我已经实现了这个问题的反面,通过创建一个自定义坐标系并逐个像素地绘制它,将一个矩形图像放在这样的形状上。但是,如果我可以一次绘制整个图像,那效率会高得多。可以在 draw() 方法中看到相反的代码(低效):github.com/ZGorlock/Graphy/blob/master/src/objects/complex/pane/…

标签: java awt graphics2d image-manipulation affinetransform


【解决方案1】:

我能够通过投影变换解决这个问题。它的运行速度没有我希望的那么快,但仍然有效。在我的电脑上执行 1000 次迭代大约需要 24 秒;我的目标是至少 60 fps。我想也许 Java 会有一种内置的方式来处理这些图像转换。

这是输出图像:

这是我的代码:

/*
 * File:    ImageUtility.java
 * Package: utility
 * Author:  Zachary Gill
 */

package utility;

import java.awt.Color;
import java.awt.Graphics2D;
import java.awt.Point;
import java.awt.Polygon;
import java.awt.Shape;
import java.awt.image.BufferedImage;
import java.awt.image.DataBufferInt;
import java.io.File;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.Stack;
import javax.imageio.ImageIO;

import math.matrix.Matrix3;
import math.vector.Vector;

/**
 * Handles image operations.
 */
public class ImageUtility {
    
    public static void main(String[] args) throws Exception {
        File image = new File("test2.jpg");
        BufferedImage src = loadImage(image);
        
        List<Vector> srcBounds = new ArrayList<>();
        srcBounds.add(new Vector(439, 42));
        srcBounds.add(new Vector(841, 3));
        srcBounds.add(new Vector(816, 574));
        srcBounds.add(new Vector(472, 683));
        
        int width = (int) ((Math.abs(srcBounds.get(1).getX() - srcBounds.get(0).getX()) + Math.abs(srcBounds.get(3).getX() - srcBounds.get(2).getX())) / 2);
        int height = (int) ((Math.abs(srcBounds.get(3).getY() - srcBounds.get(0).getY()) + Math.abs(srcBounds.get(2).getY() - srcBounds.get(1).getY())) / 2);
        BufferedImage dest = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
        List<Vector> destBounds = getBoundsForImage(dest);
        
        transformImage(src, srcBounds, dest, destBounds);
        ImageIO.write(dest, "jpg", new File("result.jpg"));
    }
    
    /**
     * Performs a quad to quad image transformation.
     *
     * @param src        The source image.
     * @param srcBounds  The bounds from the source image of the quad to transform.
     * @param dest       The destination image.
     * @param destBounds The bounds from the destination image of the quad to place the result of the transformation.
     */
    public static void transformImage(BufferedImage src, List<Vector> srcBounds, BufferedImage dest, List<Vector> destBounds) {
        Graphics2D destGraphics = dest.createGraphics();
        transformImage(src, srcBounds, destGraphics, dest.getWidth(), dest.getHeight(), destBounds);
        destGraphics.dispose();
    }
    
    /**
     * Performs a quad to quad image transformation.
     * 
     * @param src        The source image.
     * @param srcBounds  The bounds from the source image of the quad to transform.
     * @param dest       The destination graphics.
     * @param destWidth  The width of the destination graphics.
     * @param destHeight The height of the destination graphics.
     * @param destBounds The bounds from the destination graphics of the quad to place the result of the transformation.
     */
    @SuppressWarnings("IntegerDivisionInFloatingPointContext")
    public static void transformImage(BufferedImage src, List<Vector> srcBounds, Graphics2D dest, int destWidth, int destHeight, List<Vector> destBounds) {
        if ((src == null) || (srcBounds == null) || (dest == null) || (destBounds == null) ||
                (srcBounds.size() != 4) || (destBounds.size() != 4)) {
            return;
        }
        
        Matrix3 projectiveMatrix = calculateProjectiveMatrix(srcBounds, destBounds);
        if (projectiveMatrix == null) {
            return;
        }
        
        final int filterColor = new Color(0, 255, 0).getRGB();
        
        BufferedImage maskImage = new BufferedImage(destWidth, destHeight, BufferedImage.TYPE_INT_RGB);
        Graphics2D maskGraphics = maskImage.createGraphics();
        maskGraphics.setColor(new Color(filterColor));
        maskGraphics.fillRect(0, 0, maskImage.getWidth(), maskImage.getHeight());
        Polygon mask = new Polygon(
                destBounds.stream().map(e -> (int) e.getX()).mapToInt(Integer::valueOf).toArray(),
                destBounds.stream().map(e -> (int) e.getY()).mapToInt(Integer::valueOf).toArray(),
                4
        );
        Vector maskCenter = Vector.averageVector(destBounds);
        maskGraphics.setColor(new Color(0, 0, 0));
        maskGraphics.fillPolygon(mask);
        maskGraphics.dispose();
        
        int srcWidth = src.getWidth();
        int srcHeight = src.getHeight();
        int maskWidth = maskImage.getWidth();
        int maskHeight = maskImage.getHeight();
        
        int[] srcData = ((DataBufferInt) src.getRaster().getDataBuffer()).getData();
        int[] maskData = ((DataBufferInt) maskImage.getRaster().getDataBuffer()).getData();
        
        Set<Integer> visited = new HashSet<>();
        Stack<Point> stack = new Stack<>();
        stack.push(new Point((int) maskCenter.getX(), (int) maskCenter.getY()));
        while (!stack.isEmpty()) {
            Point p = stack.pop();
            int x = (int) p.getX();
            int y = (int) p.getY();
            int index = (y * maskImage.getWidth()) + x;
            
            if ((x < 0) || (x >= maskWidth) || (y < 0) || (y >= maskHeight) ||
                    visited.contains(index) || (maskData[y * maskWidth + x] == filterColor)) {
                continue;
            }
            visited.add(index);
            
            stack.push(new Point(x + 1, y));
            stack.push(new Point(x - 1, y));
            stack.push(new Point(x, y + 1));
            stack.push(new Point(x, y - 1));
        }
        
        visited.parallelStream().forEach(p -> {
            Vector homogeneousSourcePoint = projectiveMatrix.multiply(new Vector(p % maskWidth, p / maskWidth, 1.0));
            int sX = BoundUtility.truncateNum(homogeneousSourcePoint.getX() / homogeneousSourcePoint.getZ(), 0, srcWidth - 1).intValue();
            int sY = BoundUtility.truncateNum(homogeneousSourcePoint.getY() / homogeneousSourcePoint.getZ(), 0, srcHeight - 1).intValue();
            maskData[p] = srcData[sY * srcWidth + sX];
        });
        visited.clear();
        
        Shape saveClip = dest.getClip();
        dest.setClip(mask);
        dest.drawImage(maskImage, 0, 0, maskWidth, maskHeight, null);
        dest.setClip(saveClip);
    }
    
    /**
     * Calculates the projective matrix for a quad to quad image transformation.
     * 
     * @param src  The bounds of the quad in the source.
     * @param dest The bounds of the quad in the destination.
     * @return The projective matrix.
     */
    private static Matrix3 calculateProjectiveMatrix(List<Vector> src, List<Vector> dest) {
        Matrix3 projectiveMatrixSrc = new Matrix3(new double[] {
                src.get(0).getX(), src.get(1).getX(), src.get(3).getX(),
                src.get(0).getY(), src.get(1).getY(), src.get(3).getY(),
                1.0, 1.0, 1.0});
        Vector solutionSrc = new Vector(src.get(2).getX(), src.get(2).getY(), 1.0);
        Vector coordinateSystemSrc = projectiveMatrixSrc.solveSystem(solutionSrc);
        Matrix3 coordinateMatrixSrc = new Matrix3(new double[] {
                coordinateSystemSrc.getX(), coordinateSystemSrc.getY(), coordinateSystemSrc.getZ(),
                coordinateSystemSrc.getX(), coordinateSystemSrc.getY(), coordinateSystemSrc.getZ(),
                coordinateSystemSrc.getX(), coordinateSystemSrc.getY(), coordinateSystemSrc.getZ()
        });
        projectiveMatrixSrc = projectiveMatrixSrc.scale(coordinateMatrixSrc);
        
        Matrix3 projectiveMatrixDest = new Matrix3(new double[] {
                dest.get(0).getX(), dest.get(1).getX(), dest.get(3).getX(),
                dest.get(0).getY(), dest.get(1).getY(), dest.get(3).getY(),
                1.0, 1.0, 1.0});
        Vector solutionDest = new Vector(dest.get(2).getX(), dest.get(2).getY(), 1.0);
        Vector coordinateSystemDest = projectiveMatrixDest.solveSystem(solutionDest);
        Matrix3 coordinateMatrixDest = new Matrix3(new double[] {
                coordinateSystemDest.getX(), coordinateSystemDest.getY(), coordinateSystemDest.getZ(),
                coordinateSystemDest.getX(), coordinateSystemDest.getY(), coordinateSystemDest.getZ(),
                coordinateSystemDest.getX(), coordinateSystemDest.getY(), coordinateSystemDest.getZ()
        });
        projectiveMatrixDest = projectiveMatrixDest.scale(coordinateMatrixDest);
        
        try {
            projectiveMatrixDest = projectiveMatrixDest.inverse();
        } catch (ArithmeticException ignored) {
            return null;
        }
        return projectiveMatrixSrc.multiply(projectiveMatrixDest);
    }
    
    /**
     * Loads an image.
     * 
     * @param file The image file.
     * @return The BufferedImage loaded from the file, or null if there was an error.
     */
    public static BufferedImage loadImage(File file) {
        try {
            BufferedImage tmpImage = ImageIO.read(file);
            BufferedImage image = new BufferedImage(tmpImage.getWidth(), tmpImage.getHeight(), BufferedImage.TYPE_INT_RGB);
            Graphics2D imageGraphics = image.createGraphics();
            imageGraphics.drawImage(tmpImage, 0, 0, tmpImage.getWidth(), tmpImage.getHeight(), null);
            imageGraphics.dispose();
            return image;
        } catch (Exception ignored) {
            return null;
        }
    }
    
    /**
     * Creates the default bounds for an image.
     * 
     * @param image The image.
     * @return The default bounds for the image.
     */
    public static List<Vector> getBoundsForImage(BufferedImage image) {
        List<Vector> bounds = new ArrayList<>();
        bounds.add(new Vector(0, 0));
        bounds.add(new Vector(image.getWidth() - 1, 0));
        bounds.add(new Vector(image.getWidth() - 1, image.getHeight() - 1));
        bounds.add(new Vector(0, image.getHeight() - 1));
        return bounds;
    }
    
}

如果您想自己运行,可以在这里找到 Matrix3 和 Vector 操作: https://github.com/ZGorlock/Graphy/blob/master/src/math/matrix/Matrix3.java https://github.com/ZGorlock/Graphy/blob/master/src/math/vector/Vector.java

此外,这里有一些很好的射影变换参考资料:

http://graphics.cs.cmu.edu/courses/15-463/2006_fall/www/Papers/proj.pdf

https://mc.ai/part-ii-projective-transformations-in-2d/

【讨论】:

    猜你喜欢
    • 2013-03-07
    • 1970-01-01
    • 1970-01-01
    • 2015-12-19
    • 1970-01-01
    • 2012-11-16
    • 2012-03-17
    • 1970-01-01
    相关资源
    最近更新 更多