OpencvSharp 算子学习教案之 - Cv2.Add

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 里最基础、也最常用的算子之一。

这个函数最常见的三个用途是:

  1. 两张同尺寸图像相加,例如做像素叠加或融合。
  2. 通过 mask 只更新部分像素,其余位置保持原值。
  3. 通过 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_8UCV_16S 的整数输出行为不同。

4. 函数原理说明

从 OpenCV 源码看,Add 的处理流程可以概括为:

  1. 先检查两个输入是否为空。
  2. 如果输入深度不同,并且 dtype < 0,OpenCV 会直接报错,要求你显式指定输出类型。
  3. mask 不为空,OpenCV 只会写入 mask 非零的位置。
  4. 结果会按照输出深度做类型转换;整数输出通常会饱和,CV_32S 除外。

对初学者来说,最容易混淆的地方有两个:

  1. mask 不是"额外叠加一张图",而是"决定哪些位置可以写入 dst"。
  2. 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 示例中的四个场景对应。代码尽量保持初学者友好,重点展示 maskdtype 和饱和行为。

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. 函数使用注意事项

  1. src1src2 必须同尺寸、同通道数,才能逐元素相加。
  2. mask 必须是 8 位单通道矩阵,并且尺寸要和输入一致。
  3. 当输入深度不同的时候,不要依赖默认 dtype = -1,请显式指定输出深度。
  4. 整数输出会发生饱和裁剪,但 CV_32S 是例外,不会做饱和。
  5. 如果你预先创建了 dst,那么 mask 没有命中的位置会保留原值;如果 dst 需要重新分配,未命中位置通常会被清零。

9. 参数调优建议

  1. 想做混合深度演示时,优先用 CV_32F 作为输出深度,结果最直观。
  2. 研究饱和行为时,优先选择 CV_8UCV_16S,它们最容易观察到裁剪。
  3. 研究 mask 时,先把 dst 预填成一组容易辨认的数字,这样更容易看出未命中的位置。
  4. 如果输入深度不同,先把输出深度写明确,再看结果是否符合预期。
  5. 若需要排查异常,优先检查 src1src2 的深度、尺寸以及 mask 的类型是否正确。

10. 示例代码运行说明(按场景关键逻辑)

场景A(8U 饱和加法)

  1. 准备两个 CV_8UC1 矩阵。
  2. 调用 Cv2.Add(src1, src2, dst)
  3. 观察超过 255 的位置被裁剪成 255。

场景B(mask 选择性写入)

  1. 先把 dst 预填成固定值。
  2. 再传入 mask 执行加法。
  3. 检查 mask = 0 的位置是否保持原值。

场景C(混合深度与 dtype 提升)

  1. 准备 CV_8UC1CV_16UC1 两种输入。
  2. dtype 显式设为 MatType.CV_32F
  3. 查看浮点输出是否等于手工相加结果。

场景D(不同深度必须显式指定 dtype)

  1. 先故意省略 dtype
  2. 捕获 OpenCV 抛出的异常。
  3. 再用 dtype: MatType.CV_32F 修正调用。

11. 常见错误排查

  1. 错误:src1src2 尺寸不同
    • 排查:确认两个输入矩阵的行列数完全一致。
  2. 错误:输入深度不同却没有指定 dtype
    • 排查:输入类型混合时,请显式写出输出深度,例如 MatType.CV_32F
  3. 错误:mask 不是 8 位单通道
    • 排查:把 mask 改成 CV_8UC1,并确认尺寸一致。
  4. 错误:以为所有整数输出都会饱和
    • 排查:记住 CV_32S 是特例,它不会走饱和裁剪。
  5. 错误: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
相关推荐
CompaqCV2 小时前
OpencvSharp 算子学习教案之 - Cv2.Subtract 重载3
学习·c#·opencvsharp算子·opencv教程
HERR_QQ2 小时前
端到端课程自用 1课 感知部分
笔记·学习·自动驾驶
辞旧 lekkk2 小时前
【Git】远程操作与标签管理
linux·git·学习·萌新
jrlong2 小时前
HelloAgents 进阶篇 task00,task01
笔记·学习
fengci.2 小时前
php反序列化(复习)(第五章)
android·开发语言·学习·php
ICscholar3 小时前
MoE负载均衡损失 & 梯度累加除法
人工智能·学习·算法
嵌入式小企鹅3 小时前
国产AI全栈迁移、涨价潮蔓延、RISC-V测评工具发布
人工智能·学习·开源·嵌入式·边缘计算·半导体·昇腾
殇淋狱陌3 小时前
【初始Python】Python学习基础(数据类型、定义、变量、下标、目前的开发语言对比)
开发语言·python·学习
xiaoxiaoxiaolll3 小时前
数据驱动下的人工电磁材料逆向设计与智能优化研究
人工智能·学习