欢迎光临
我们一直在努力

OpencvSharp 算子学习教案之 - Cv2.PerspectiveTransform 重载1

OpencvSharp 算子学习教案之 – Cv2.PerspectiveTransform 重载1

大家好,Opencv在很多工程项目中都会用到,而OpencvSharp则是以C#开发与实现的Opencv操作库,对.NET开发人员友好,但很多API的中文资料、应用场景及常见坑点等缺乏系统性归纳,因此这系列博客将给大家带来Cv2及Mat对象全系列算子学习教案,供大家参考学习。

Cv2.PerspectiveTransform

  • 教案版本:V1.0
  • 面向对象:OpenCvSharp 初学者
  • 所属模块:core
  • 源码位置:OpenCvSharp/Cv2/Cv2_core.cs:2065

摘要:PerspectiveTransform 的 InputArray / OutputArray 重载会把 2 通道或 3 通道浮点矩阵中的每个向量按透视矩阵映射到新坐标,并把结果写回 OutputArray。本文用一个 CV_32FC2 的点矩阵演示 Mat 工作流,帮助初学者理解“输入矩阵”和“输出矩阵”在 OpenCvSharp 里的实际含义。

1. 函数名称(带参数签名)

public static void PerspectiveTransform(InputArray src, OutputArray dst, InputArray m)

2. 函数用途

Cv2.PerspectiveTransform 的这个重载,会对输入矩阵中的每个多通道向量执行透视变换,并把结果写入 dst。

它最常见的用途有:

  • 在已经拥有 Mat 的代码里,直接做点集或向量投影。
  • 处理 2 通道或 3 通道浮点矩阵中的坐标向量。
  • 在图像配准、文档矫正、空间映射中观察透视变换的输出形态。
  • 这个重载和后面的 Point2f[]、Point2d[]、Point3f[]、Point3d[] 版本本质上一致,只是这里直接走 Mat -> Mat 的工作流,更贴近底层 OpenCV 的使用方式。

    3. 函数公式

    如果输入向量是二维点 (x,y)(x, y)(x,y),透视矩阵是:

    M=[m00m01m02m10m11m12m20m21m22]
    M =
    \\begin{bmatrix}
    m_{00} & m_{01} & m_{02} \\\\
    m_{10} & m_{11} & m_{12} \\\\
    m_{20} & m_{21} & m_{22}
    \\end{bmatrix}
    M=m00m10m20m01m11m21m02m12m22

    那么先计算分母:

    w=m20x+m21y+m22
    w = m_{20}x + m_{21}y + m_{22}
    w=m20x+m21y+m22

    再计算输出坐标:

    x′=m00x+m01y+m02w
    x' = \\frac{m_{00}x + m_{01}y + m_{02}}{w}
    x=wm00x+m01y+m02

    y′=m10x+m11y+m12w
    y' = \\frac{m_{10}x + m_{11}y + m_{12}}{w}
    y=wm10x+m11y+m12

    如果输入矩阵是 3 通道浮点向量,那么原理完全一样,只是向量维度变成三维,透视矩阵也会变成 4×4。

    4. 函数原理说明

    PerspectiveTransform 的处理过程可以理解为:

  • 读取输入矩阵中的每个向量元素。
  • 根据透视矩阵计算分子和分母。
  • 用分母把结果归一化。
  • 把输出写入 dst。
  • 对初学者来说,最重要的是理解这几点:

  • 这个重载直接处理 Mat,不要求你先转换成 Point2f[]。
  • dst 是输出容器,OpenCV 会把结果写到里面。
  • 输出矩阵的尺寸和类型会和输入保持一致。
  • 如果你已经在使用 Mat,这通常是最直接、最少转换成本的写法。
  • 5. 参数含义解析

    参数名类型必填含义
    src InputArray 输入 2 通道或 3 通道浮点矩阵
    dst OutputArray 输出矩阵,尺寸和类型与 src 一致
    m InputArray 3×3 或 4×4 透视矩阵

    补充说明:

  • src 必须是浮点矩阵,常见类型是 CV_32FC2、CV_64FC2、CV_32FC3、CV_64FC3。
  • 2 通道矩阵通常表示二维点或二维向量。
  • 3 通道矩阵通常表示三维点或三维向量。
  • dst 可以直接传一个空的 Mat,OpenCV 会在内部帮你分配结果。
  • 6. 应用场景列表

    场景名场景说明典型用途
    场景A:Mat 点矩阵投影 直接处理 CV_32FC2 点矩阵 点集透视映射、文档矫正
    场景B:Mat 工作流 已经拿到 Mat 时直接输出结果 OpenCV 管线、数据流处理
    场景C:多通道向量归一化 处理 3 通道浮点向量 三维坐标、点云教学
    场景D:输出矩阵复用 让结果直接写进目标 Mat 性能优化、减少类型转换

    7. 函数使用示例

    下面的 Console 程序演示 Cv2.PerspectiveTransform(InputArray src, OutputArray dst, InputArray m)。为了让 Mat -> Mat 的行为更直观,我们使用一个 CV_32FC2 点矩阵作为输入,再把结果写入另一个 Mat。

    using System.Globalization;
    using System.Text;
    using OpenCvSharp;

    internal static class Program
    {
    /// <summary>
    /// 程序入口。
    /// </summary>
    private static void Main()
    {
    // 控制台输出要支持中文,避免教学文字乱码。
    Console.OutputEncoding = Encoding.UTF8;
    RunPerspectiveTransformScenario();
    }

    /// <summary>
    /// 运行 Mat 输入输出重载示例。
    /// </summary>
    private static void RunPerspectiveTransformScenario()
    {
    // 这组点保留为二维矩阵,便于观察 InputArray / OutputArray 重载如何原样返回同尺寸结果。
    var sourcePoints = new Point2f[,]
    {
    {
    new Point2f(0f, 0f),
    new Point2f(80f, 0f),
    },
    {
    new Point2f(0f, 60f),
    new Point2f(80f, 60f),
    },
    };

    // 使用和二维点重载相同的 3×3 透视矩阵,方便对比 Mat 工作流和点数组工作流。
    var perspectiveMatrix = new double[,]
    {
    { 1.0, 0.10, 12.0 },
    { 0.04, 1.0, 6.0 },
    { 0.001, 0.002, 1.0 },
    };

    using var sourceMat = CreatePointMatrix(sourcePoints);
    using var matrixMat = CreateMatrix(perspectiveMatrix);
    using var destinationMat = new Mat();

    // 这里直接把 Mat 传给 PerspectiveTransform,使用的就是 InputArray / OutputArray 重载。
    Cv2.PerspectiveTransform(sourceMat, destinationMat, matrixMat);

    var actualPoints = ReadPointMatrix(destinationMat);
    var expectedPoints = ApplyPerspectiveTransformExpected(sourcePoints, perspectiveMatrix);

    PrintHeader("Mat 输入输出重载", "这个重载最接近底层 Mat 工作流,适合已经手里有 CV_32FC2 矩阵的场景。");
    PrintPointMatrix("源点矩阵", sourcePoints);
    PrintMatrix("透视矩阵", perspectiveMatrix);
    PrintPointMatrixComparison("输出矩阵对照", actualPoints, expectedPoints);
    Console.WriteLine("教学提示:当你已经拿到 Mat 作为输入时,InputArray / OutputArray 重载最直接。\\n");
    }

    /// <summary>
    /// 把二维点矩阵写入 CV_32FC2 Mat。
    /// </summary>
    /// <param name="values">二维点矩阵。</param>
    /// <returns>CV_32FC2 矩阵。</returns>
    private static Mat CreatePointMatrix(Point2f[,] values)
    {
    var rows = values.GetLength(0);
    var cols = values.GetLength(1);
    var mat = new Mat(rows, cols, MatType.CV_32FC2);

    for (var row = 0; row < rows; row++)
    {
    for (var col = 0; col < cols; col++)
    {
    // 每个元素都是一个二维向量,所以要写入 Vec2f。
    mat.At<Vec2f>(row, col) = new Vec2f(values[row, col].X, values[row, col].Y);
    }
    }

    return mat;
    }

    /// <summary>
    /// 把 CV_32FC2 Mat 读回二维点矩阵。
    /// </summary>
    /// <param name="source">输入矩阵。</param>
    /// <returns>二维点矩阵。</returns>
    private static Point2f[,] ReadPointMatrix(Mat source)
    {
    var result = new Point2f[source.Rows, source.Cols];

    for (var row = 0; row < source.Rows; row++)
    {
    for (var col = 0; col < source.Cols; col++)
    {
    var value = source.At<Vec2f>(row, col);
    result[row, col] = new Point2f(value.Item0, value.Item1);
    }
    }

    return result;
    }

    /// <summary>
    /// 手工计算二维点矩阵的期望透视变换结果。
    /// </summary>
    /// <param name="source">源点矩阵。</param>
    /// <param name="matrix">3×3 透视矩阵。</param>
    /// <returns>期望结果。</returns>
    private static Point2f[,] ApplyPerspectiveTransformExpected(Point2f[,] source, double[,] matrix)
    {
    var result = new Point2f[source.GetLength(0), source.GetLength(1)];

    for (var row = 0; row < source.GetLength(0); row++)
    {
    for (var col = 0; col < source.GetLength(1); col++)
    {
    result[row, col] = ApplyPerspectiveTransform(source[row, col], matrix);
    }
    }

    return result;
    }

    /// <summary>
    /// 追加二维点矩阵块。
    /// </summary>
    /// <param name="sb">文本构建器。</param>
    /// <param name="title">标题。</param>
    /// <param name="matrix">二维点矩阵。</param>
    /// <param name="numericFormat">数字格式。</param>
    private static void PrintPointMatrix(StringBuilder sb, string title, Point2f[,] matrix, string numericFormat = "F2")
    {
    sb.AppendLine(title);
    sb.AppendLine(FormatPointMatrixText(matrix, numericFormat));
    }

    /// <summary>
    /// 追加二维点矩阵对比结果。
    /// </summary>
    /// <param name="sb">文本构建器。</param>
    /// <param name="title">标题。</param>
    /// <param name="actual">实际结果。</param>
    /// <param name="expected">期望结果。</param>
    /// <param name="numericFormat">数字格式。</param>
    private static void PrintPointMatrixComparison(StringBuilder sb, string title, Point2f[,] actual, Point2f[,] expected, string numericFormat = "F6")
    {
    ValidateSameShape(actual, expected);

    sb.AppendLine(title);

    for (var row = 0; row < actual.GetLength(0); row++)
    {
    for (var col = 0; col < actual.GetLength(1); col++)
    {
    var dx = Math.Abs(actual[row, col].X expected[row, col].X);
    var dy = Math.Abs(actual[row, col].Y expected[row, col].Y);

    sb.AppendLine($"[{row}, {col}]");
    sb.AppendLine($"实际结果:{FormatPoint(actual[row, col], numericFormat)}");
    sb.AppendLine($"期望结果:{FormatPoint(expected[row, col], numericFormat)}");
    sb.AppendLine($"X 误差:{dx.ToString(numericFormat, CultureInfo.InvariantCulture)}");
    sb.AppendLine($"Y 误差:{dy.ToString(numericFormat, CultureInfo.InvariantCulture)}");
    sb.AppendLine($"是否一致:{dx <= 1e-4 && dy <= 1e-4}");
    sb.AppendLine();
    }
    }
    }

    /// <summary>
    /// 将二维点矩阵格式化成多行文本。
    /// </summary>
    /// <param name="matrix">二维点矩阵。</param>
    /// <param name="numericFormat">数字格式。</param>
    /// <returns>格式化文本。</returns>
    private static string FormatPointMatrixText(Point2f[,] matrix, string numericFormat = "F2")
    {
    var sb = new StringBuilder();

    for (var row = 0; row < matrix.GetLength(0); row++)
    {
    sb.Append("[");

    for (var col = 0; col < matrix.GetLength(1); col++)
    {
    sb.Append(FormatPoint(matrix[row, col], numericFormat));

    if (col < matrix.GetLength(1) 1)
    {
    sb.Append(", ");
    }
    }

    sb.AppendLine("]");
    }

    return sb.ToString();
    }

    /// <summary>
    /// 验证两个二维点矩阵形状是否一致。
    /// </summary>
    /// <param name="left">左矩阵。</param>
    /// <param name="right">右矩阵。</param>
    private static void ValidateSameShape(Point2f[,] left, Point2f[,] right)
    {
    if (left.GetLength(0) != right.GetLength(0) || left.GetLength(1) != right.GetLength(1))
    {
    throw new ArgumentException("两个二维点矩阵的形状必须完全一致。", nameof(right));
    }
    }

    /// <summary>
    /// 打印程序标题。
    /// </summary>
    private static void PrintHeader(string title, string description)
    {
    Console.WriteLine(title);
    Console.WriteLine(description);
    Console.WriteLine(new string('-', 40));
    }

    /// <summary>
    /// 打印矩阵。
    /// </summary>
    private static void PrintMatrix(string title, double[,] matrix)
    {
    Console.WriteLine(title);
    Console.WriteLine(FormatMatrixText(matrix));
    }

    /// <summary>
    /// 手工计算单个二维点的透视变换结果。
    /// </summary>
    /// <param name="point">输入点。</param>
    /// <param name="matrix">3×3 透视矩阵。</param>
    /// <returns>变换后的点。</returns>
    private static Point2f ApplyPerspectiveTransform(Point2f point, double[,] matrix)
    {
    var denominator = matrix[2, 0] * point.X + matrix[2, 1] * point.Y + matrix[2, 2];
    if (Math.Abs(denominator) < 1e-12)
    {
    throw new InvalidOperationException("透视变换分母接近 0,无法继续计算。");
    }

    var x = (matrix[0, 0] * point.X + matrix[0, 1] * point.Y + matrix[0, 2]) / denominator;
    var y = (matrix[1, 0] * point.X + matrix[1, 1] * point.Y + matrix[1, 2]) / denominator;
    return new Point2f((float)x, (float)y);
    }

    /// <summary>
    /// 将二维点格式化成易读字符串。
    /// </summary>
    /// <param name="point">点坐标。</param>
    /// <param name="numericFormat">数字格式。</param>
    /// <returns>格式化后的点文本。</returns>
    private static string FormatPoint(Point2f point, string numericFormat = "F6")
    {
    return $"({point.X.ToString(numericFormat, CultureInfo.InvariantCulture)}, {point.Y.ToString(numericFormat, CultureInfo.InvariantCulture)})";
    }

    /// <summary>
    /// 将矩阵格式化成多行文本。
    /// </summary>
    /// <param name="matrix">二维矩阵。</param>
    /// <param name="numericFormat">数字格式。</param>
    /// <returns>格式化后的文本。</returns>
    private static string FormatMatrixText(double[,] matrix, string numericFormat = "F3")
    {
    var sb = new StringBuilder();

    for (var row = 0; row < matrix.GetLength(0); row++)
    {
    sb.Append("[");

    for (var col = 0; col < matrix.GetLength(1); col++)
    {
    sb.Append(matrix[row, col].ToString(numericFormat, CultureInfo.InvariantCulture));

    if (col < matrix.GetLength(1) 1)
    {
    sb.Append(", ");
    }
    }

    sb.AppendLine("]");
    }

    return sb.ToString();
    }
    }

    8. 注意事项

  • src 必须是 2 通道或 3 通道浮点矩阵。
  • dst 是输出容器,通常可以直接传一个空的 Mat。
  • 输出矩阵的尺寸和类型会和输入保持一致。
  • 如果输入矩阵不是浮点类型,通常不能直接调用这个重载。
  • 9. 调优建议

  • 如果你的代码已经在处理 Mat,优先用这个重载,转换成本最小。
  • 先用一个很小的点矩阵做测试,最容易看清输出类型和尺寸。
  • 如果你只想观察二维点,先用 CV_32FC2;如果你在做三维向量,再考虑 CV_32FC3。
  • 先把输入和输出打印出来,再看每个点的误差,最适合初学者调试。
  • 10. 运行说明

  • 如果你在控制台工程里运行本文示例,直接把代码放进 Program.cs 即可。
  • 如果你在本仓库里学习,请直接打开 WPF 控件 Cv2.PerspectiveTransform,点击按钮查看结果。
  • WPF 示例会把源矩阵、透视矩阵和输出矩阵逐段显示出来。
  • 11. 常见错误排查

  • 把 src 传成整型矩阵,导致透视变换无法按浮点语义工作。
  • 把 dst 误以为要预先和 src 完全一样大小,其实空 Mat 也可以作为输出容器。
  • 把这个重载和 Point2f[]、Point3d[] 版本混为一谈。
  • 忘记 src 必须是浮点多通道矩阵,而不是普通单通道灰度图。
  • 赞(0)
    未经允许不得转载:171主机测评 » OpencvSharp 算子学习教案之 - Cv2.PerspectiveTransform 重载1
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址