OpenCV for Unity:4 点 calibration/reprojection

OpenCV for Unity : 4-point calibration/reprojection

这是我在 Stack 上的第一个 post,因此对于我的笨拙,我深表歉意。请让我知道我是否可以改进我的问题。

► 我想达到的目标(长期而言):

我尝试使用 OpenCV fo Unity 使用激光指示器来操纵我的 Unity3d 演示文稿。

我相信一张图片胜过千言万语,所以这应该是最能说明问题的:

► 问题是什么:

我尝试从相机视图(某种梯形)到平面 space 进行简单的 4 点校准(投影)。

我认为这将是非常基础和简单的事情,但我没有使用 OpenCV 的经验,所以我无法让它工作。

► 示例:

我做了一个简单得多的例子,没有任何激光检测和所有其他东西。我尝试重新投影到平面中的只有 4 点梯形 space。

Link 到整个示例项目:https://1drv.ms/u/s!AiDsGecSyzmuujXGQUapcYrIvP7b

我的示例中的核心脚本:

using OpenCVForUnity;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
using System;

public class TestCalib : MonoBehaviour
{
    public RawImage displayDummy;
    public RectTransform[] handlers;
    public RectTransform dummyCross;
    public RectTransform dummyResult;

    public Vector2 webcamSize = new Vector2(640, 480);
    public Vector2 objectSize = new Vector2(1024, 768);

    private Texture2D texture;

    Mat cameraMatrix;
    MatOfDouble distCoeffs;

    MatOfPoint3f objectPoints;
    MatOfPoint2f imagePoints;

    Mat rvec;
    Mat tvec;
    Mat rotationMatrix;
    Mat imgMat;


    void Start()
    {
        texture = new Texture2D((int)webcamSize.x, (int)webcamSize.y, TextureFormat.RGB24, false);
        if (displayDummy) displayDummy.texture = texture;
        imgMat = new Mat(texture.height, texture.width, CvType.CV_8UC3);
    }


    void Update()
    {
        imgMat = new Mat(texture.height, texture.width, CvType.CV_8UC3);
        Test();
        DrawImagePoints();
        Utils.matToTexture2D(imgMat, texture);
    }

    void DrawImagePoints()
    {
        Point[] pointsArray = imagePoints.toArray();
        for (int i = 0; i < pointsArray.Length; i++)
        {
            Point p0 = pointsArray[i];
            int j = (i < pointsArray.Length - 1) ? i + 1 : 0;
            Point p1 = pointsArray[j];

            Imgproc.circle(imgMat, p0, 5, new Scalar(0, 255, 0, 150), 1);
            Imgproc.line(imgMat, p0, p1, new Scalar(255, 255, 0, 150), 1);
        }
    }


    private void DrawResults(MatOfPoint2f resultPoints)
    {
        Point[] pointsArray = resultPoints.toArray();
        for (int i = 0; i < pointsArray.Length; i++)
        {
            Point p = pointsArray[i];
            Imgproc.circle(imgMat, p, 5, new Scalar(255, 155, 0, 150), 1);
        }
    }

    public void Test()
    {
        float w2 = objectSize.x / 2F;
        float h2 = objectSize.y / 2F;

        /*
        objectPoints = new MatOfPoint3f(
            new Point3(-w2, -h2, 0),
            new Point3(w2, -h2, 0),
            new Point3(-w2, h2, 0),
            new Point3(w2, h2, 0)
        );
        */

        objectPoints = new MatOfPoint3f(
            new Point3(0, 0, 0),
            new Point3(objectSize.x, 0, 0),
            new Point3(objectSize.x, objectSize.y, 0),
            new Point3(0, objectSize.y, 0)
        );

        imagePoints = GetImagePointsFromHandlers();

        rvec = new Mat(1, 3, CvType.CV_64FC1);
        tvec = new Mat(1, 3, CvType.CV_64FC1);
        rotationMatrix = new Mat(3, 3, CvType.CV_64FC1);


        double fx = webcamSize.x / objectSize.x;
        double fy = webcamSize.y / objectSize.y;
        double cx = 0; // webcamSize.x / 2.0f;
        double cy = 0; // webcamSize.y / 2.0f;
        cameraMatrix = new Mat(3, 3, CvType.CV_64FC1);
        cameraMatrix.put(0, 0, fx);
        cameraMatrix.put(0, 1, 0);
        cameraMatrix.put(0, 2, cx);
        cameraMatrix.put(1, 0, 0);
        cameraMatrix.put(1, 1, fy);
        cameraMatrix.put(1, 2, cy);
        cameraMatrix.put(2, 0, 0);
        cameraMatrix.put(2, 1, 0);
        cameraMatrix.put(2, 2, 1.0f);

        distCoeffs = new MatOfDouble(0, 0, 0, 0);
        Calib3d.solvePnP(objectPoints, imagePoints, cameraMatrix, distCoeffs, rvec, tvec);

        Mat uv = new Mat(3, 1, CvType.CV_64FC1);
        uv.put(0, 0, dummyCross.anchoredPosition.x);
        uv.put(1, 0, dummyCross.anchoredPosition.y);
        uv.put(2, 0, 0);

        Calib3d.Rodrigues(rvec, rotationMatrix);
        Mat P = rotationMatrix.inv() * (cameraMatrix.inv() * uv - tvec);

        Vector2 v = new Vector2((float)P.get(0, 0)[0], (float)P.get(1, 0)[0]);
        dummyResult.anchoredPosition = v;
    }

    private MatOfPoint2f GetImagePointsFromHandlers()
    {
        MatOfPoint2f m = new MatOfPoint2f();
        List<Point> points = new List<Point>();
        foreach (RectTransform handler in handlers)
        {
            Point p = new Point(handler.anchoredPosition.x, handler.anchoredPosition.y);
            points.Add(p);
        }

        m.fromList(points);
        return m;
    }
}

在此先感谢您的帮助。

这个问题不是特定于 opencv 的,而是基于数学的,在计算机图形学领域更常见。你要找的是 Projective Transformation.

投影变换采用一组坐标并将它们投影到某物上。在您的情况下,您希望将相机视图中的 2D 点投影到平面上的 2D 点。

所以我们想要一个 2D-Space 的投影变换。要执行投影变换,我们需要找到我们想要应用的变换的投影矩阵。在这种情况下,我们需要一个矩阵来表示相机相对于平面的投影变形。

要使用投影,我们首先需要将我们的点转换为 homogeneous coordinates。为此,我们只需向向量中添加一个值为 1 的新分量。因此 (x,y) 变为 (x,y,1)。我们将用我们所有的五个可用点来做到这一点。

现在我们从实际的数学开始。先定义一下:摄像机的视角和各自的坐标为camera space,相对于平面的坐标在flat space。设 c₁c₄ 是平面相对于相机 space 作为齐次向量的角点。设 p 是我们在相机 space 中找到的点,p' 是我们想要在平面 space 中找到的点,它们都是齐次向量。

从数学上讲,我们正在寻找一个矩阵 C,它可以让我们通过给定 p.

来计算 p'
p' = C * p

现在我们显然需要找到 C。要找到二维 space 的投影矩阵,我们需要四个点(多么方便..)我假设 c₁ 会去 (0,0)c₂ 会去(0,1)c₃(1,0)c₄(1,1)。您需要使用例如求解两个矩阵方程高斯行消除或 LR 分解算法。 OpenCV 应该包含为您完成这些任务的函数,但要注意矩阵调节及其对可用解决方案的影响。

现在回到矩阵。您需要计算两个基本变化矩阵,因为它们被调用。它们用于更改坐标系的参考系(这正是我们想要做的)。第一个矩阵会将我们的坐标转换为三维基向量,第二个矩阵会将我们的 2D 平面转换为三维基向量。

对于坐标一,您需要使用以下等式计算 λμr

⌈ c₁.x   c₂.x   c₃.x ⌉     ⌈ λ ⌉    ⌈ c₄.x ⌉
  c₁.y   c₂.y   c₃.y   *    μ   =   c₄.y
⌊   1      1      1  ⌋     ⌊ r ⌋    ⌊  1   ⌋

这将带您进入您的第一个 Matrix,A

    ⌈ λ*c₁.x   μ*c₂.x   r*c₃.x ⌉
A =   λ*c₁.y   μ*c₂.y   r*c₃.y 
    ⌊   λ         μ        r   ⌋

A 现在会将点 c₁c₄ 映射到基础坐标 (1,0,0)(0,1,0)(0,0,1)(1,1,1)。我们现在为我们的飞机做同样的事情。先解决

⌈ 0 0 1 ⌉     ⌈ λ ⌉    ⌈ 1 ⌉
  0 1 0   *    μ   =   1
⌊ 1 1 1 ⌋     ⌊ r ⌋    ⌊ 1 ⌋

得到B

    ⌈ 0 0 r ⌉
B =   0 μ 0 
    ⌊ λ μ r ⌋

AB 现在将从这些三维基础向量映射到您各自的 space。但这并不是我们想要的。我们想要 camera space -> basis -> flat space,所以只有矩阵 B 在正确的方向上操作。但这很容易通过反转 A 来解决。这将为我们提供矩阵 C = B * A⁻¹(注意 BA⁻¹ 的顺序不可互换)。这给我们留下了一个公式来计算 p' out of p.

p' = C * p
p' = B * A⁻¹ * p

从左到右阅读它,例如:取 p,将 p 从相机 space 转换为基向量并将其转换为平面 space。

如果没记错的话,p'还是三个分量,所以我们需要先去均质化p'才能使用。这将产生

x' = p'.x / p'.z
y' = p'.y / p'.z

和中提琴,我们已经成功地将激光点从相机视图转换到一张平面纸上。完全没有太复杂......

我开发代码。 MouseUp 调用此函数。和分辨率编辑;

void Cal()
{
    // Webcam Resolution 1280*720
    MatOfPoint2f pts_src = new MatOfPoint2f(
        new Point(Double.Parse(imagePoints.get(0,0).GetValue(0).ToString()), Double.Parse(imagePoints.get(0, 0).GetValue(1).ToString())),
        new Point(Double.Parse(imagePoints.get(1,0).GetValue(0).ToString()), Double.Parse(imagePoints.get(1, 0).GetValue(1).ToString())),
        new Point(Double.Parse(imagePoints.get(2,0).GetValue(0).ToString()), Double.Parse(imagePoints.get(2, 0).GetValue(1).ToString())),
        new Point(Double.Parse(imagePoints.get(3,0).GetValue(0).ToString()), Double.Parse(imagePoints.get(3, 0).GetValue(1).ToString()))
        );

    //Resolution 1920*1080
    MatOfPoint2f pts_dst = new MatOfPoint2f(
       new Point(0, 0),
       new Point(1920, 0),
       new Point(1920, 1080),
       new Point(0, 1080)
       );

    // 1. Calculate Homography
    Mat h = Calib3d.findHomography((pts_src), (pts_dst));

    // Pick Point (WebcamDummy Cavas : 1280*0.5f / 720*0.5f)
    MatOfPoint2f srcPointMat = new MatOfPoint2f(
        new Point(dummyCross.anchoredPosition.x*2.0f, dummyCross.anchoredPosition.y*2.0f)
        );

    MatOfPoint2f dstPointMat = new MatOfPoint2f();
    {
        //2. h Mat Mul srcPoint to dstPoint
        Core.perspectiveTransform(srcPointMat, dstPointMat, h);

        Vector2 v = new Vector2((float)dstPointMat.get(0, 0)[0], (float)dstPointMat.get(0, 0)[1]);
        //(ResultDummy Cavas: 1920 * 0.5f / 1080 * 0.5f)
        dummyResult.anchoredPosition = v*0.5f;

        Debug.Log(dummyCross.anchoredPosition.ToString() + "\n" + dummyResult.anchoredPosition.ToString());                       
    }
}