OpenCV for Unity:4点校准/重投影

9

这是我在Stack上发布的第一篇文章,对于我的笨拙,请提前道歉。如果可以,敬请告知如何改进我的问题。

► 我想要实现的目标(长期目标):

我想使用OpenCV for Unity和激光指针来操作我的Unity3d演示。

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

enter image description here

► 问题是什么:

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

我认为这应该是非常基础和容易的事情,但我没有使用OpenCV的经验,也无法使其工作。

► 示例:

我制作了一个更简单的示例,没有任何激光检测和其他杂项。只有4个点的梯形,我试图将其重新投影到平面空间。

链接到整个示例项目: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;
    }
}

感谢您提前的任何帮助。
2个回答

1
这个问题不是opencv特定的,而是基于数学并且更常见于计算机图形领域。您要寻找的是称为投影变换的东西。
投影变换将一组坐标映射到某个物体上。在您的情况下,您想将相机视图中的2D点投影到平面上的2D点。
因此,我们需要一个用于2D空间的投影变换。要执行投影变换,我们需要找到所需应用的变换的投影矩阵。在这种情况下,我们需要一个表达相机与平面之间投影变形的矩阵。
要处理投影,我们首先需要将我们的点转换为齐次坐标。为此,我们只需向我们的向量添加一个值为1的新分量即可。因此,(x,y)变为(x,y,1)。我们将对所有可用的五个点都这样做。
现在我们开始实际的数学计算。首先是一些定义: 相机的视角和相应的坐标应为相机空间,与平面相关的坐标应为平面空间。让c₁c₄成为平面的角点,相对于相机空间为齐次向量。让p成为我们在相机空间中找到的点,p'成为我们想要在平面空间中找到的点,两者再次作为齐次向量。
从数学上讲,我们正在寻找一个矩阵C,它将允许我们通过给出p来计算p'
p' = C * p

现在我们需要找到C。为了找到二维空间的投影矩阵,我们需要四个点(多么方便)。我假设c₁将转移到(0,0)c₂将转移到(0,1)c₃将转移到(1,0)c₄将转移到(1,1)。您需要使用高斯行消元或LR分解算法解决两个矩阵方程。OpenCV应该包含可帮助您完成这些任务的函数,但要注意矩阵条件和它们对可用解决方案的影响。
现在回到矩阵。您需要计算两个所谓的基础变换矩阵。它们用于更改您坐标系的参考框架(正是我们想要做的)。第一个矩阵将使我们的坐标转换为三维基向量,第二个矩阵将使我们的二维平面转换为三维基向量。
对于第一个坐标系,您需要计算以下方程中的λμr
 c₁.x   c₂.x   c₃.x       λ      c₄.x 
  c₁.y   c₂.y   c₃.y   *    μ   =   c₄.y
   1      1      1        r       1   

这将引导您进入您的第一个矩阵,A
     λ*c₁.x   μ*c₂.x   r*c₃.x 
A =   λ*c₁.y   μ*c₂.y   r*c₃.y 
       λ         μ        r   

现在,A将把点 c₁ 映射到基坐标(1,0,0),(0,1,0),(0,0,1)和(1,1,1)的 c₄ 。现在我们对平面做同样的事情。首先解决
⌈ 0 0 1 ⌉     ⌈ λ ⌉    ⌈ 1 ⌉
  0 1 0   *    μ   =   1
⌊ 1 1 1 ⌋     ⌊ r ⌋    ⌊ 1 ⌋

并获得 B

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

AB现在将从这三维基向量映射到它们各自的空间。但这不是我们想要的。我们希望实现 相机空间 -> 基础 -> 平面空间,所以只有矩阵B能够正确地进行操作。但这很容易通过求逆矩阵A来解决。这将给我们矩阵C = B * A⁻¹(注意BA⁻¹的顺序不能交换)。这使我们得到了一个公式来计算p'

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

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

如果您记得正确的话,p'仍然有三个分量,因此我们需要先去齐次化p',然后才能使用它。这将产生

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

现在我们已经成功地将相机视图中的激光点转换到一张平面纸上。完全没有过于复杂或者什么的...


非常感谢 - 虽然不完全是答案,但帮助我理解了应该寻找什么 :) - IronWolf

1

我开发编程代码。当鼠标松开时调用该函数,并编辑分辨率;

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());                       
    }
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接