OpencvSharp 算子学习教案之 - Cv2.Add
大家好,Opencv在很多工程项目中都会用到,而OpencvSharp则是以C#开发与实现的Opencv操作库,对.NET开发人员友好,但很多API的中文资料、应用场景及常见坑点等缺乏系统性归纳,因此这系列博客将给大家带来Cv2及Mat对象全系列算子学习教案,供大家参考学习。
Cv2.Add
- 教案版本:V1.0
- 面向对象:OpenCvSharp 初学者
- 所属模块:core
- 源码位置:OpenCvSharp/Cv2/Cv2_core.cs:69
1. 函数名称(带参数签名)
csharp
public static void Add(
InputArray src1,
InputArray src2,
OutputArray dst,
InputArray? mask = null,
int dtype = -1)
2. 函数用途
Cv2.Add 用来做逐元素加法。它是 OpenCV 里最基础、也最常用的算子之一。
这个函数最常见的三个用途是:
- 两张同尺寸图像相加,例如做像素叠加或融合。
- 通过
mask只更新部分像素,其余位置保持原值。 - 通过
dtype明确指定输出深度,例如把 8 位输入提升到 32 位浮点输出。
3. 函数公式
在没有 mask 时,可以把 OpenCV 的行为写成:
dst(I)=saturate(src1(I)+src2(I)) dst(I) = \operatorname{saturate}(src1(I) + src2(I)) dst(I)=saturate(src1(I)+src2(I))
当提供 mask 时,只有 mask(I) \ne 0 的位置才会更新:
dst(I)←{saturate(src1(I)+src2(I)),mask(I)≠0dst(I),mask(I)=0 dst(I) \leftarrow \begin{cases} \operatorname{saturate}(src1(I) + src2(I)), & mask(I) \ne 0 \\ dst(I), & mask(I) = 0 \end{cases} dst(I)←{saturate(src1(I)+src2(I)),dst(I),mask(I)=0mask(I)=0
这里的 saturate 表示按输出深度做饱和裁剪,例如 CV_8U 会限制在 [0,255][0, 255][0,255]。
3.1 输出深度的特殊规则
如果输出深度是 CV_32S,OpenCV 不会再做饱和裁剪。这个规则很重要,因为它和 CV_8U、CV_16S 的整数输出行为不同。
4. 函数原理说明
从 OpenCV 源码看,Add 的处理流程可以概括为:
- 先检查两个输入是否为空。
- 如果输入深度不同,并且
dtype < 0,OpenCV 会直接报错,要求你显式指定输出类型。 - 若
mask不为空,OpenCV 只会写入mask非零的位置。 - 结果会按照输出深度做类型转换;整数输出通常会饱和,
CV_32S除外。
对初学者来说,最容易混淆的地方有两个:
mask不是"额外叠加一张图",而是"决定哪些位置可以写入 dst"。dtype不是"输入类型自动随便猜",而是输出深度的明确约定。
5. 参数含义解析
| 参数名 | 类型 | 必填 | 含义 |
|---|---|---|---|
| src1 | InputArray | 是 | 第一个输入数组 |
| src2 | InputArray | 是 | 第二个输入数组 |
| dst | OutputArray | 是 | 输出数组 |
| mask | InputArray? | 否 | 8 位单通道掩码,只允许非零位置写入 dst |
| dtype | int | 否 | 输出深度,默认 -1 表示尽量沿用输入深度 |
6. 应用场景列表
| 场景名 | 场景说明 | 典型用途 |
|---|---|---|
| 场景A:8U 饱和加法 | 观察 8 位无符号加法的饱和裁剪 | 图像叠加、像素合成 |
| 场景B:mask 选择性写入 | 只让 mask 命中的位置更新 |
局部融合、ROI 处理 |
| 场景C:混合深度与 dtype 提升 | 用 CV_32F 接住不同深度的输入 |
混合精度处理、数值分析 |
| 场景D:不同深度必须显式指定 dtype | 先触发异常,再用 dtype 修正 |
调试输入类型不一致的问题 |
7. 函数使用示例(与 WPF 场景一一对应)
说明:下面四段代码与 WPF 示例中的四个场景对应。代码尽量保持初学者友好,重点展示
mask、dtype和饱和行为。
7.1 场景A:8U 饱和加法
csharp
using OpenCvSharp;
// 两个 8 位无符号矩阵,尺寸完全一致。
var src1Data = new byte[,]
{
{ 220, 15 },
{ 120, 250 },
};
var src2Data = new byte[,]
{
{ 50, 40 },
{ 200, 30 },
};
using var src1 = Mat.FromPixelData(2, 2, MatType.CV_8UC1, src1Data);
using var src2 = Mat.FromPixelData(2, 2, MatType.CV_8UC1, src2Data);
using var dst = new Mat();
// 这里不传 mask,也不指定 dtype,OpenCV 会按输入深度直接计算。
Cv2.Add(src1, src2, dst);
// 220 + 50 = 270,会被饱和成 255。
// 120 + 200 = 320,也会被饱和成 255。
7.2 场景B:mask 选择性写入
csharp
using OpenCvSharp;
var src1Data = new byte[,]
{
{ 200, 10, 240 },
{ 40, 50, 60 },
{ 1, 2, 3 },
};
var src2Data = new byte[,]
{
{ 100, 20, 30 },
{ 4, 5, 6 },
{ 7, 8, 9 },
};
var maskData = new byte[,]
{
{ 255, 0, 255 },
{ 0, 255, 0 },
{ 255, 0, 255 },
};
using var src1 = Mat.FromPixelData(3, 3, MatType.CV_8UC1, src1Data);
using var src2 = Mat.FromPixelData(3, 3, MatType.CV_8UC1, src2Data);
using var mask = Mat.FromPixelData(3, 3, MatType.CV_8UC1, maskData);
// 先准备一个已有的 dst,这样就能看出 mask 没有命中的位置会保留原值。
using var dst = new Mat(3, 3, MatType.CV_8UC1, new Scalar(200));
Cv2.Add(src1, src2, dst, mask);
// mask=0 的位置仍然是原来的 dst 值,mask!=0 的位置才会被更新。
7.3 场景C:混合深度与 dtype 提升
csharp
using OpenCvSharp;
// src1 是 8 位无符号,src2 是 16 位无符号。
// 这时如果想安全地接住结果,就应该显式指定输出深度。
var src1Data = new byte[,]
{
{ 15, 240 },
{ 100, 200 },
};
var src2Data = new ushort[,]
{
{ 300, 500 },
{ 600, 700 },
};
using var src1 = Mat.FromPixelData(2, 2, MatType.CV_8UC1, src1Data);
using var src2 = Mat.FromPixelData(2, 2, MatType.CV_16UC1, src2Data);
using var dst = new Mat();
// dtype 这里显式写成 CV_32F,这样可以把不同深度的输入统一到浮点输出。
Cv2.Add(src1, src2, dst, dtype: MatType.CV_32F);
// 15 + 300 = 315
// 240 + 500 = 740
// 100 + 600 = 700
// 200 + 700 = 900
7.4 场景D:不同深度必须显式指定 dtype
csharp
using OpenCvSharp;
var src1Data = new byte[,]
{
{ 1, 2 },
{ 3, 4 },
};
var src2Data = new ushort[,]
{
{ 1000, 2000 },
{ 3000, 4000 },
};
using var src1 = Mat.FromPixelData(2, 2, MatType.CV_8UC1, src1Data);
using var src2 = Mat.FromPixelData(2, 2, MatType.CV_16UC1, src2Data);
try
{
using var failedDst = new Mat();
// 这里故意不写 dtype。因为输入深度不同,OpenCV 会抛出异常。
Cv2.Add(src1, src2, failedDst);
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
}
using var fixedDst = new Mat();
Cv2.Add(src1, src2, fixedDst, dtype: MatType.CV_32F);
// 正确结果应为:1001、2002、3003、4004。
8. 函数使用注意事项
src1和src2必须同尺寸、同通道数,才能逐元素相加。mask必须是 8 位单通道矩阵,并且尺寸要和输入一致。- 当输入深度不同的时候,不要依赖默认
dtype = -1,请显式指定输出深度。 - 整数输出会发生饱和裁剪,但
CV_32S是例外,不会做饱和。 - 如果你预先创建了
dst,那么mask没有命中的位置会保留原值;如果dst需要重新分配,未命中位置通常会被清零。
9. 参数调优建议
- 想做混合深度演示时,优先用
CV_32F作为输出深度,结果最直观。 - 研究饱和行为时,优先选择
CV_8U和CV_16S,它们最容易观察到裁剪。 - 研究
mask时,先把dst预填成一组容易辨认的数字,这样更容易看出未命中的位置。 - 如果输入深度不同,先把输出深度写明确,再看结果是否符合预期。
- 若需要排查异常,优先检查
src1和src2的深度、尺寸以及mask的类型是否正确。
10. 示例代码运行说明(按场景关键逻辑)
场景A(8U 饱和加法)
- 准备两个
CV_8UC1矩阵。 - 调用
Cv2.Add(src1, src2, dst)。 - 观察超过 255 的位置被裁剪成 255。
场景B(mask 选择性写入)
- 先把
dst预填成固定值。 - 再传入
mask执行加法。 - 检查
mask = 0的位置是否保持原值。
场景C(混合深度与 dtype 提升)
- 准备
CV_8UC1和CV_16UC1两种输入。 - 把
dtype显式设为MatType.CV_32F。 - 查看浮点输出是否等于手工相加结果。
场景D(不同深度必须显式指定 dtype)
- 先故意省略
dtype。 - 捕获 OpenCV 抛出的异常。
- 再用
dtype: MatType.CV_32F修正调用。
11. 常见错误排查
- 错误:
src1和src2尺寸不同- 排查:确认两个输入矩阵的行列数完全一致。
- 错误:输入深度不同却没有指定
dtype- 排查:输入类型混合时,请显式写出输出深度,例如
MatType.CV_32F。
- 排查:输入类型混合时,请显式写出输出深度,例如
- 错误:
mask不是 8 位单通道- 排查:把
mask改成CV_8UC1,并确认尺寸一致。
- 排查:把
- 错误:以为所有整数输出都会饱和
- 排查:记住
CV_32S是特例,它不会走饱和裁剪。
- 排查:记住
- 错误:
mask没命中的位置变成了 0- 排查:确认
dst是否在调用前已经存在;如果dst被重新分配,未命中位置可能会被清零。
- 排查:确认
对应 WPF 演示控件与样例代码:
- Features/Cv2Add/Cv2AddControl.xaml
- Samples/Cv2Add/AddSaturationSample.cs
- Samples/Cv2Add/AddMaskedDestinationSample.cs
- Samples/Cv2Add/AddMixedDepthPromotionSample.cs
- Samples/Cv2Add/AddMixedDepthRequirementSample.cs