OpencvSharp 算子学习教案之 - Cv2.Merge
大家好,Opencv在很多工程项目中都会用到,而OpencvSharp则是以C#开发与实现的Opencv操作库,对.NET开发人员友好,但很多API的中文资料、应用场景及常见坑点等缺乏系统性归纳,因此这系列博客将给大家带来Cv2及Mat对象全系列算子学习教案,供大家参考学习。
Cv2.Merge
- 教案版本:V1.0
- 面向对象:OpenCvSharp 初学者
- 所属模块:core
- 源码位置:OpenCvSharp/Cv2/Cv2_core.cs:907
摘要:这个函数会把多个单通道 Mat 按输入顺序合并成一个多通道 Mat。本文重点讲清楚通道顺序、输出像素结构以及它和 Split 的反向关系,便于初学者建立整体直觉。
1. 函数名称(带参数签名)
public static void Merge(Mat[] mv, Mat dst)
2. 函数用途
Cv2.Merge 的作用,是把多个单通道矩阵合成一个多通道矩阵。
这个函数最常见的用途有:
- 把三个灰度平面合成一张彩色图。
- 把多个特征通道拼成一个多通道输入。
- 把独立的分量数据组合成统一的像素结构。
- 在调试时,把拆开的通道重新拼回去验证是否一致。
它通常和 Cv2.Split 配套使用:Split 负责拆通道,Merge 负责拼通道。
3. 函数公式
如果输入是三个单通道矩阵,那么输出的每个像素可以写成:
dst(r,c)=(mv[0](r,c),mv[1](r,c),mv[2](r,c)) dst(r, c) = (mv[0](r, c), mv[1](r, c), mv[2](r, c)) dst(r,c)=(mv[0](r,c),mv[1](r,c),mv[2](r,c))
这里的 mv[0]、mv[1]、mv[2] 分别对应输出像素的第 0、1、2 个通道。
如果把三个输入通道写成分量形式,那么就是:
channel0→dst.Item0 channel_0 \rightarrow dst.Item0 channel0→dst.Item0
channel1→dst.Item1 channel_1 \rightarrow dst.Item1 channel1→dst.Item1
channel2→dst.Item2 channel_2 \rightarrow dst.Item2 channel2→dst.Item2
对初学者来说,最关键的点只有一个:输入数组的顺序,就是输出通道的顺序。
4. 函数原理说明
Cv2.Merge 的工作方式非常直接:
- 先拿到多个单通道矩阵。
- 检查这些矩阵的尺寸是否一致。
- 把同一位置上的多个通道值拼成一个像素。
- 输出一个多通道矩阵。
对初学者来说,最容易混淆的是“合并”和“混合”:
- Merge 不是做加法。
- Merge 不是做平均。
- Merge 只是把通道按位置拼接起来。
- 如果输入顺序变了,输出通道含义也会跟着变。
本页的三个场景会分别演示原顺序合并、反向合并和模式化数据合并,帮助你建立这个直觉。
5. 参数含义解析
| 参数名 | 类型 | 必填 | 含义 |
|---|---|---|---|
| mv | Mat[] | 是 | 要合并的单通道矩阵数组 |
| dst | Mat | 是 | 输出的多通道矩阵 |
补充说明:
mv里的每个 Mat 都应该是单通道矩阵。mv中的矩阵尺寸必须完全一致。dst可以是空的Mat,OpenCV 会根据输入自动生成输出。- 如果输入数量是 3,输出通常就是 3 通道矩阵。
6. 应用场景列表
| 场景名 | 场景说明 | 典型用途 |
|---|---|---|
| 场景A:彩色通道合成 | 把三个灰度平面拼成一张彩色图 | 图像生成、图像重建 |
| 场景B:特征通道拼接 | 把多个独立通道组合成一个特征体 | 多通道特征输入 |
| 场景C:通道重组 | 调整通道顺序后再合并 | 通道调试、颜色映射 |
| 场景D:验证 Split 结果 | 把拆开的通道再拼回去 | 调试、单元验证 |
7. 函数使用示例
下面的 Console 程序演示 Cv2.Merge 的三个教学场景。我们使用三个单通道矩阵来观察输入顺序如何直接决定输出像素里的通道顺序。
using System.Globalization;
using System.Text;
using OpenCvSharp;
internal static class Program
{
/// <summary>
/// 程序入口。
/// </summary>
private static void Main()
{
// 让控制台正确显示中文。
Console.OutputEncoding = Encoding.UTF8;
RunOrderedScenario();
RunReversedScenario();
RunPatternScenario();
}
/// <summary>
/// 场景A:按原顺序合并。
/// </summary>
private static void RunOrderedScenario()
{
var channel0 = new byte[,]
{
{ 10, 20, 30 },
{ 40, 50, 60 },
};
var channel1 = new byte[,]
{
{ 11, 21, 31 },
{ 41, 51, 61 },
};
var channel2 = new byte[,]
{
{ 12, 22, 32 },
{ 42, 52, 62 },
};
using var mat0 = CreateMat(channel0);
using var mat1 = CreateMat(channel1);
using var mat2 = CreateMat(channel2);
using var destination = new Mat();
// Merge 会把输入数组按顺序拼成一个多通道 Mat。
Cv2.Merge(new[] { mat0, mat1, mat2 }, destination);
var actual = ReadVec3bMatrix(destination);
var expected = ComputeExpectedMerge(channel0, channel1, channel2);
PrintScenario("场景A:按原顺序合并", "三个单通道 Mat 按原顺序合并成一个三通道 Mat。", channel0, channel1, channel2, actual, expected);
}
/// <summary>
/// 场景B:反向合并。
/// </summary>
private static void RunReversedScenario()
{
var channel0 = new byte[,]
{
{ 10, 20, 30 },
{ 40, 50, 60 },
};
var channel1 = new byte[,]
{
{ 11, 21, 31 },
{ 41, 51, 61 },
};
var channel2 = new byte[,]
{
{ 12, 22, 32 },
{ 42, 52, 62 },
};
using var mat0 = CreateMat(channel2);
using var mat1 = CreateMat(channel1);
using var mat2 = CreateMat(channel0);
using var destination = new Mat();
// 这里故意把输入顺序反过来,观察输出通道如何跟着变化。
Cv2.Merge(new[] { mat0, mat1, mat2 }, destination);
var actual = ReadVec3bMatrix(destination);
var expected = ComputeExpectedMerge(channel2, channel1, channel0);
PrintScenario("场景B:反向合并", "输入顺序变了,输出通道顺序也会跟着变。", channel2, channel1, channel0, actual, expected);
}
/// <summary>
/// 场景C:模式化三通道合并。
/// </summary>
private static void RunPatternScenario()
{
var channel0 = new byte[,]
{
{ 5, 5, 5 },
{ 5, 5, 5 },
};
var channel1 = new byte[,]
{
{ 0, 10, 20 },
{ 30, 40, 50 },
};
var channel2 = new byte[,]
{
{ 100, 110, 120 },
{ 130, 140, 150 },
};
using var mat0 = CreateMat(channel0);
using var mat1 = CreateMat(channel1);
using var mat2 = CreateMat(channel2);
using var destination = new Mat();
// 三个通道分别表现成常量、递增和高亮数值,方便观察通道独立性。
Cv2.Merge(new[] { mat0, mat1, mat2 }, destination);
var actual = ReadVec3bMatrix(destination);
var expected = ComputeExpectedMerge(channel0, channel1, channel2);
PrintScenario("场景C:模式化三通道合并", "三个通道各自保持独立,Merge 只是把它们拼成一个像素。", channel0, channel1, channel2, actual, expected);
}
/// <summary>
/// 统一打印一个 Merge 场景的报告。
/// </summary>
private static void PrintScenario(string title, string description, byte[,] channel0, byte[,] channel1, byte[,] channel2, Vec3b[,] actual, Vec3b[,] expected)
{
Console.WriteLine(title);
Console.WriteLine(description);
Console.WriteLine(new string('-', 40));
PrintMatrix("channel0", channel0, "F0");
PrintMatrix("channel1", channel1, "F0");
PrintMatrix("channel2", channel2, "F0");
Console.WriteLine("通道顺序说明:输入数组里的第 0 个 Mat 会成为输出的第 0 个通道,第 1 个 Mat 会成为输出的第 1 个通道,以此类推。");
PrintComparison("dst", actual, expected);
}
/// <summary>
/// 把二维 byte 数组写入 OpenCV Mat。
/// </summary>
private static Mat CreateMat(byte[,] values)
{
var rows = values.GetLength(0);
var cols = values.GetLength(1);
var mat = new Mat(rows, cols, MatType.CV_8UC1);
for (var row = 0; row < rows; row++)
{
for (var col = 0; col < cols; col++)
{
// 逐元素写入 Mat,后面就可以直接交给 OpenCV 做 Merge。
mat.At<byte>(row, col) = values[row, col];
}
}
return mat;
}
/// <summary>
/// 把二维 byte 数组转换成 double[,],便于统一打印。
/// </summary>
private static double[,] ToDoubleMatrix(byte[,] values)
{
var rows = values.GetLength(0);
var cols = values.GetLength(1);
var result = new double[rows, cols];
for (var row = 0; row < rows; row++)
{
for (var col = 0; col < cols; col++)
{
result[row, col] = values[row, col];
}
}
return result;
}
/// <summary>
/// 把三通道 Mat 读取成 Vec3b[,],适合展示 Merge 的输出结果。
/// </summary>
private static Vec3b[,] ReadVec3bMatrix(Mat source)
{
var result = new Vec3b[source.Rows, source.Cols];
for (var row = 0; row < source.Rows; row++)
{
for (var col = 0; col < source.Cols; col++)
{
result[row, col] = source.At<Vec3b>(row, col);
}
}
return result;
}
/// <summary>
/// 根据三个单通道矩阵构造期望的三通道矩阵。
/// </summary>
private static Vec3b[,] ComputeExpectedMerge(byte[,] channel0, byte[,] channel1, byte[,] channel2)
{
var rows = channel0.GetLength(0);
var cols = channel0.GetLength(1);
var result = new Vec3b[rows, cols];
for (var row = 0; row < rows; row++)
{
for (var col = 0; col < cols; col++)
{
result[row, col] = new Vec3b(channel0[row, col], channel1[row, col], channel2[row, col]);
}
}
return result;
}
/// <summary>
/// 打印一个单通道矩阵。
/// </summary>
private static void PrintMatrix(string title, byte[,] matrix, string numericFormat)
{
Console.WriteLine(title);
Console.WriteLine(FormatMatrixText(ToDoubleMatrix(matrix), numericFormat));
}
/// <summary>
/// 打印三通道矩阵对比结果。
/// </summary>
private static void PrintComparison(string title, Vec3b[,] actual, Vec3b[,] expected)
{
Console.WriteLine(title);
Console.WriteLine("实际结果:");
Console.WriteLine(FormatVec3bMatrixText(actual));
Console.WriteLine("期望结果:");
Console.WriteLine(FormatVec3bMatrixText(expected));
Console.WriteLine($"是否完全一致:{AreEqual(actual, expected)}");
Console.WriteLine();
}
/// <summary>
/// 格式化二维矩阵输出。
/// </summary>
private static string FormatMatrixText(double[,] matrix, string numericFormat)
{
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();
}
/// <summary>
/// 格式化三通道矩阵输出。
/// </summary>
private static string FormatVec3bMatrixText(Vec3b[,] matrix)
{
var sb = new StringBuilder();
for (var row = 0; row < matrix.GetLength(0); row++)
{
sb.Append("[");
for (var col = 0; col < matrix.GetLength(1); col++)
{
// 三通道像素用 (通道0, 通道1, 通道2) 的形式输出,方便教学阅读。
var pixel = matrix[row, col];
sb.Append($"({pixel.Item0}, {pixel.Item1}, {pixel.Item2})");
if (col < matrix.GetLength(1) - 1)
{
sb.Append(", ");
}
}
sb.AppendLine("]");
}
return sb.ToString();
}
/// <summary>
/// 判断两个三通道矩阵是否完全一致。
/// </summary>
private static bool AreEqual(Vec3b[,] actual, Vec3b[,] expected)
{
if (actual.GetLength(0) != expected.GetLength(0) || actual.GetLength(1) != expected.GetLength(1))
{
return false;
}
for (var row = 0; row < actual.GetLength(0); row++)
{
for (var col = 0; col < actual.GetLength(1); col++)
{
if (!actual[row, col].Equals(expected[row, col]))
{
return false;
}
}
}
return true;
}
}
8. 注意事项
mv里的所有 Mat 必须是单通道,而且尺寸必须一致。Merge不会帮你解释通道含义,它只会按顺序拼接通道。- 如果你把顺序写反了,输出图像的颜色含义也会跟着变。
dst可以是空矩阵,OpenCV 会根据输入自动创建输出。
9. 性能与调优建议
- 如果后续还要继续做像素级处理,先用
Merge把数据整理成统一的多通道结构,会更方便。 - 当你只是验证通道顺序时,可以用小矩阵先做单元测试,再切换到真实图像。
- 对需要高频组合通道的流程,建议保持输入矩阵尺寸固定,减少额外检查成本。
- 如果你后面还要拆回单通道,可以直接配合
Cv2.Split使用。
10. 运行说明
- 如果你运行的是本仓库的 WPF 示例程序,打开主窗口后选择
Cv2.Merge即可。 - 本页右侧的三个场景分别对应原顺序合并、反向合并和模式化三通道合并。
- 如果你运行的是上面的 Console 示例,请把代码放到
Program.cs,然后执行dotnet run。 - 建议先看输入通道,再看输出像素中的三个分量,最后再理解通道顺序。
11. 常见错误排查
- 把 Merge 当成加法:它不是做数值运算,而是做通道拼接。
- 输入 Mat 尺寸不一致:Merge 要求所有单通道矩阵大小一致。
- 把通道顺序传错:输入数组顺序会直接影响输出像素的通道顺序。
- 误以为 Merge 会自动补齐缺失通道:它不会自动帮你补值,输入几路就输出几路。
752

被折叠的 条评论
为什么被折叠?



