OpencvSharp 算子学习教案之 - Cv2.Subtract 重载1
重载1:Subtract(InputArray src1, InputArray src2, OutputArray dst, InputArray? mask = null, int dtype = -1)
大家好,Opencv在很多工程项目中都会用到,而OpencvSharp则是以C#开发与实现的Opencv操作库,对.NET开发人员友好,但很多API的中文资料、应用场景及常见坑点等缺乏系统性归纳,因此这系列博客将给大家带来Cv2及Mat对象全系列算子学习教案,供大家参考学习。
Cv2.Subtract
- 教案版本:V1.0
- 面向对象:OpenCvSharp 初学者
- 所属模块:core
- 源码位置:OpenCvSharp/Cv2/Cv2_core.cs:100
1. 函数名称(带参数签名)
csharp
public static void Subtract(
InputArray src1,
InputArray src2,
OutputArray dst,
InputArray? mask = null,
int dtype = -1)
2. 函数用途
这是 Subtract 的数组-数组重载,负责对两个同尺寸、同通道数的输入做逐元素相减。
这个重载最常见的用途有:
- 计算两张图像的差异图。
- 做运动检测、背景差分和局部变化提取。
- 配合
mask只更新目标区域。 - 配合
dtype保留负数、扩大动态范围,或者显式控制输出深度。
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。
3.1 输出深度的特殊规则
如果输出深度是 CV_32S,OpenCV 不会做饱和裁剪。其余整数输出通常都会做饱和处理,因此它和 CV_8U、CV_16S 的结果语义并不完全相同。
4. 函数原理说明
OpenCV 内部会先检查输入是否兼容,再把结果写到 dst:
src1和src2必须同尺寸、同通道数。- 如果输入深度不同,而你又没有显式给出
dtype,OpenCV 会报错,要求你明确输出类型。 mask只控制写入位置,不改变运算本身。- 当输出深度是整数类型时,OpenCV 会按目标类型做裁剪或保留符号。
对初学者来说,最需要记住的是:
Subtract的数学意义是"左边减右边",不是绝对值差。dtype决定最终结果装进什么类型里,而不是输入怎么减。mask不是"参与减法的第三个数组",而是"控制哪些位置允许写回 dst"。
5. 参数含义解析
| 参数名 | 类型 | 必填 | 含义 |
|---|---|---|---|
| src1 | InputArray | 是 | 左操作数 |
| src2 | InputArray | 是 | 右操作数 |
| dst | OutputArray | 是 | 输出结果 |
| mask | InputArray? | 否 | 8 位单通道掩码,只允许非零位置写入 dst |
| dtype | int | 否 | 输出深度,默认 -1 表示尽量沿用输入深度 |
6. 应用场景列表
| 场景名 | 场景说明 | 典型用途 |
|---|---|---|
| 场景A:图像差分 | 两张图像做逐元素相减,输出差异图 | 变化检测、背景建模 |
| 场景B:局部更新 | 配合 mask 只更新部分像素 |
ROI 处理、分段比较 |
| 场景C:保留负值 | 用 CV_16S 或 CV_32F 保留负差值 |
残差分析、视觉误差图 |
| 场景D:混合深度处理 | 输入类型不同但显式指定输出深度 | 数据归一化、类型转换演示 |
7. 函数使用示例(与 WPF 场景一一对应)
说明:下面代码与 WPF 控件中的场景A对应。这个场景专门演示逐元素相减、
mask选择性写入,以及CV_8U输出下的饱和裁剪。
csharp
using OpenCvSharp;
// 两张 8 位无符号矩阵,尺寸完全一致。
var src1Data = new byte[,]
{
{ 15, 80, 200 },
{ 0, 30, 100 },
{ 255, 128, 10 },
};
var src2Data = new byte[,]
{
{ 20, 50, 120 },
{ 1, 40, 110 },
{ 5, 200, 20 },
};
var maskData = new byte[,]
{
{ 255, 0, 255 },
{ 0, 255, 0 },
{ 255, 0, 255 },
};
var seedData = new byte[,]
{
{ 77, 77, 77 },
{ 77, 77, 77 },
{ 77, 77, 77 },
};
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);
using var dst = Mat.FromPixelData(3, 3, MatType.CV_8UC1, seedData);
// 这里显式指定 CV_8U,便于观察负值如何被裁剪到 0。
Cv2.Subtract(src1, src2, dst, mask, dtype: MatType.CV_8U);
// 当 src1 - src2 小于 0 时,输出会变成 0。
8. 函数使用注意事项
src1和src2必须同尺寸、同通道数。mask必须是 8 位单通道矩阵,并且尺寸要与输入一致。- 如果输入深度不同,请显式指定
dtype,不要依赖默认值。 - 当输出是
CV_8U等无符号类型时,负数会被裁剪到 0。 - 当输出是
CV_32S时,OpenCV 不做饱和裁剪,这一点和其他整数输出不同。
9. 参数调优建议
- 需要看"真实差值"时,优先把输出设成
CV_16S或CV_32F。 - 需要做图像差分显示时,
CV_8U常用于快速查看亮暗区域变化。 - 先用小矩阵验证
mask语义,再放大到图像尺寸,能更快排查问题。 - 如果你发现负值都变成了 0,通常是输出类型太窄,而不是减法算错了。
10. 示例代码运行说明(按场景关键逻辑)
- 准备两张 3x3 的
CV_8UC1矩阵。 - 准备一个 8 位单通道
mask,让部分位置允许写入。 - 先把
dst预填成 77,便于看出未命中的位置是否保留原值。 - 调用
Cv2.Subtract(src1, src2, dst, mask, dtype: MatType.CV_8U)。 - 观察负数是否被裁剪为 0,以及 mask 没命中的位置是否保持初始值。
11. 常见错误排查
- 错误:两个输入尺寸不一致
- 排查:确认
src1和src2的行列数完全一致。
- 排查:确认
- 错误:把
Subtract当成绝对值差- 排查:
Subtract保留方向,结果是src1 - src2。
- 排查:
- 错误:
mask不是CV_8UC1- 排查:把掩码改成 8 位单通道,并确认尺寸一致。
- 错误:输入深度不同却不指定
dtype- 排查:显式指定输出深度,例如
CV_32F或CV_16S。
- 排查:显式指定输出深度,例如
- 错误:期待负数能在
CV_8U中保留- 排查:改用有符号或浮点输出。
对应 WPF 演示控件与样例代码:
- Features/Cv2Subtract/Cv2SubtractControl.xaml
- Samples/Cv2Subtract/SubtractArrayArraySample.cs