构建一个AffineTransform对象来匹配3个点的变换

Cl0*_*ent 2 java transformation image-processing

我知道仿射变换前后 3 个点(p0、p1、p2)的位置(X 和 Y)。我想构建与此转换匹配的 AffineTransformation 对象。换句话说,我想找到将已知点 p0、p1、p2 移动到其已知目的地的仿射变换。

这是我到目前为止所做的:

package image_transformation;

import java.awt.geom.AffineTransform;
import java.awt.image.AffineTransformOp;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;

import javax.imageio.ImageIO;

import math.Vector2d;

public class ImageTransformation {

    public static void main(String[] args) throws IOException {

        // the position of the points before the transformation
        Vector2d[] src = new Vector2d[] {
                new Vector2d(486, 191),
                new Vector2d(456, 565),
                new Vector2d(149, 353)
        };

        // the position of the points after the transformation
        Vector2d[] dest = new Vector2d[] {
                new Vector2d(0, 0),
                new Vector2d(0, 600),
                new Vector2d(600, 600)
        };

        // the transformation that we are building
        AffineTransform at = new AffineTransform();

        // the translation to move the p0 to its destination
        Vector2d translationVec = dest[0].sub(src[0]);
        at.translate(translationVec.x, translationVec.y);

        // the rotation around p0 (it will not move) to align p0, p1 and p1's destination
        Vector2d vec0 = src[1].sub(src[0]);
        Vector2d vec1 = dest[1].sub(dest[0]);
        double angle = orientedAngle(vec0, vec1);
        at.rotate(angle, src[0].x, src[0].y);

        // the scaling to adjust the distance between p0 and p1
        // problem: it will induce a translation
        Vector2d origin = src[1].sub(src[0]);
        Vector2d target = origin.normalize().mult(dest[1].sub(dest[0]).length());
        Vector2d scale = new Vector2d(target.x / origin.x, target.y / origin.y);
        if (Double.isNaN(scale.x)) scale.x = 1D;
        if (Double.isNaN(scale.y)) scale.y = 1D;
        at.scale(scale.x, scale.y);

        // TODO compute the induced translation and apply its inverse to move p0 and p1 to their destination

        // TODO terminate the transformation to move p2 to its destination

        // apply the transformation to an image to check if it works
        BufferedImage inImg = ImageIO.read(new File("input.png"));
        BufferedImage outImg = new BufferedImage(inImg.getWidth(), inImg.getHeight(), BufferedImage.TYPE_INT_ARGB);
        new AffineTransformOp(at, AffineTransformOp.TYPE_BICUBIC).filter(inImg, outImg);
        File outFile = new File("output.png");
        outFile.createNewFile();
        ImageIO.write(outImg, "png", outFile);
    }

    private static double orientedAngle(Vector2d vec0, Vector2d vec1) {
        return Math.atan2(vec0.x * vec1.y - vec0.y * vec1.x, vec0.x * vec1.x + vec0.y * vec1.y);
    }

}
Run Code Online (Sandbox Code Playgroud)

Vector2d 类对向量进行一些基本数学运算,其每个方法的名称都是不言自明的(sub[stract]、mult[iply]、length、normalize 等)。

我不知道如何终止这个算法。另外,如果已经存在一种可以完成所有这些操作的方法,我将非常乐意使用它。

Mar*_*o13 5

这至少与纹理变形密切相关,4 个点,但我不会说它可以被视为重复。

你在那里做了很多数学工作。但也许这没有必要。如果方法正确,问题本身就相当微不足道。考虑二维仿射变换的含义:它将一个空间变换为另一个空间。这里的关键点是:

矩阵列是将矩阵应用于单位向量的结果

现在,当你有 3 个点时,你可以根据它们计算向量:

double dx1 = p1.getX() - p0.getX();
double dy1 = p1.getY() - p0.getY();

double dx2 = p2.getX() - p0.getX();
double dy2 = p2.getY() - p0.getY();
Run Code Online (Sandbox Code Playgroud)

然后您可以简单地将这些值插入到AffineTransform. 的最后一列AffineTransform包含由 给出的翻译p0。结果是AffineTransform将点 (0,0)、(1,0) 和 (0,1) 分别变换为点p0p1p2。当您反转此变换时,它会将点p0p1和转换p2为点 (0,0)、(1,0) 和 (0,1) 。

所以你所要做的就是

  • 创建将源点转换为单位向量的变换
  • 创建将单位向量转换为目标点的变换
  • 将两者连接起来

伪代码(!)真的就是这么简单

    AffineTransform unitToSrc = computeTransform(src[0], src[1], src[2]);
    AffineTransform unitToDst = computeTransform(dst[0], dst[1], dst[2]);
    AffineTransform at = new AffineTransform();
    at.concatenate(unitToDst);
    at.concatenate(unitToSrc.inverted());
Run Code Online (Sandbox Code Playgroud)

整个事情都是在这里实现的,作为一个 MCVE。红点是“源”点,绿点是“目的地”点。您可以用鼠标拖动它们:

从点进行仿射变换

蓝色圆圈表示将变换应用于源点的结果,您可以看到它们最终到达了所需的目标位置。

实际计算是通过computeTransform方法完成的。请注意,这是基于java.awt.geom.Point2D类(而不是Vector2d您省略的类)实现的,但这应该很容易更改:点或向量类中使用的唯一内容是 x/y 坐标。除此之外,实现过程中根本不涉及任何(自定义)数学。唯一的数学运算是仿射变换的反转,但有一个内置的功能可以实现这一点。

import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.RenderingHints;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.awt.event.MouseMotionListener;
import java.awt.geom.AffineTransform;
import java.awt.geom.Ellipse2D;
import java.awt.geom.NoninvertibleTransformException;
import java.awt.geom.Point2D;
import java.util.Arrays;

import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.SwingUtilities;

public class AffineTransformFromPoints
{
    public static void main(String[] args)
    {
        SwingUtilities.invokeLater(() -> createAndShowGUI());
    }

    private static void createAndShowGUI()
    {
        JFrame f = new JFrame();
        f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        AffineTransformFromPointsPanel panel = 
            new AffineTransformFromPointsPanel();
        f.getContentPane().setLayout(new BorderLayout());
        f.getContentPane().add(panel, BorderLayout.CENTER);
        f.setSize(1200,900);
        f.setLocationRelativeTo(null);
        f.setVisible(true);
    }

}

class AffineTransformFromPointsPanel extends JPanel 
    implements MouseListener, MouseMotionListener
{
    private Point2D draggedPoint;

    // the position of the points before the transformation
    Point2D[] src = new Point2D[] {
        new Point2D.Double(486, 191),
        new Point2D.Double(456, 565),
        new Point2D.Double(149, 353)
    };

    // the position of the points after the transformation
    Point2D[] dst = new Point2D[] {
        new Point2D.Double(0, 0),
        new Point2D.Double(0, 600),
        new Point2D.Double(600, 600)
    };


    public AffineTransformFromPointsPanel()
    {
        addMouseListener(this);
        addMouseMotionListener(this);
    }

    @Override
    protected void paintComponent(Graphics gr)
    {
        super.paintComponent(gr);
        Graphics2D g = (Graphics2D)gr;
        g.setColor(Color.WHITE);
        g.fillRect(0, 0, getWidth(), getHeight());

        g.setRenderingHint(
            RenderingHints.KEY_ANTIALIASING, 
            RenderingHints.VALUE_ANTIALIAS_ON);

        g.setColor(Color.RED);
        for (Point2D v : src)
        {
            paint(g, v);
        }

        g.setColor(Color.GREEN);
        for (Point2D v : dst)
        {
            paint(g, v);
        }

        g.setColor(Color.BLUE);
        AffineTransform at = computeTransform(src, dst);
        for (Point2D v : src)
        {
            draw(g, v, at);
        }
    }

    private static AffineTransform computeTransform(
        Point2D src[], Point2D dst[])
    {
        AffineTransform unitToSrc = computeTransform(src[0], src[1], src[2]);
        AffineTransform unitToDst = computeTransform(dst[0], dst[1], dst[2]);
        AffineTransform srcToUnit = null;
        try
        {
            srcToUnit = unitToSrc.createInverse();
        }
        catch (NoninvertibleTransformException e)
        {
            System.out.println(e.getMessage());
            return new AffineTransform();
        }
        AffineTransform at = new AffineTransform();
        at.concatenate(unitToDst);
        at.concatenate(srcToUnit);
        return at;
    }

    private static AffineTransform computeTransform(
        Point2D p0, Point2D p1, Point2D p2)
    {
        AffineTransform at = new AffineTransform();
        double dx1 = p1.getX() - p0.getX();
        double dy1 = p1.getY() - p0.getY();
        double dx2 = p2.getX() - p0.getX();
        double dy2 = p2.getY() - p0.getY();
        at.setTransform(dx1, dy1, dx2, dy2, p0.getX(), p0.getY());
        return at;
    }

    private static void paint(Graphics2D g, Point2D p)
    {
        double r = 6;
        g.fill(new Ellipse2D.Double(
            p.getX() - r, p.getY() - r, r + r, r + r));
    }

    private static void draw(Graphics2D g, Point2D v, AffineTransform at)
    {
        double r = 8;
        Point2D p = new Point2D.Double(v.getX(), v.getY());
        at.transform(p, p);
        g.draw(new Ellipse2D.Double(
            p.getX() - r, p.getY() - r, r + r, r + r));
    }

    @Override
    public void mouseDragged(MouseEvent e)
    {
        if (draggedPoint != null)
        {
            draggedPoint.setLocation(e.getPoint());
            repaint();
        }
    }


    @Override
    public void mousePressed(MouseEvent e)
    {
        draggedPoint = closest(e.getPoint(), Arrays.asList(src));
        if (draggedPoint == null)
        {
            draggedPoint = closest(e.getPoint(), Arrays.asList(dst));
        }
    }

    private static Point2D closest(
        Point2D p, Iterable<? extends Point2D> points)
    {
        final double threshold = 10;
        Point2D closestPoint = null;
        double minDistance = Double.MAX_VALUE;

        for (Point2D point : points)
        {
            double dd = point.distance(p);
            if (dd < threshold && dd < minDistance)
            {
                minDistance = dd;
                closestPoint = point;
            }
        }
        return closestPoint;
    }

    @Override
    public void mouseReleased(MouseEvent e)
    {
        draggedPoint = null;
    }

    @Override
    public void mouseMoved(MouseEvent e)
    {
        // Nothing to do here
    }

    @Override
    public void mouseClicked(MouseEvent e)
    {
        // Nothing to do here
    }

    @Override
    public void mouseEntered(MouseEvent e)
    {
        // Nothing to do here
    }

    @Override
    public void mouseExited(MouseEvent e)
    {
        // Nothing to do here
    }

}
Run Code Online (Sandbox Code Playgroud)