Opencv中文网

水印叠加(ROI 区域 + AddWeighted)

上一节介绍了AddWeighted函数,接下来以此为基础,拿工程实例代码进行分析,详细介绍AddWeighted做水印叠加的过程。

首先是看看C#工程中的实际代码:

/// <summary>
/// 水印叠加  ROI + AddWeighted(小图贴大图)
/// </summary>
[Node(Order = 6.3)]
public class WatermarkNodeModel : BaseNodeModel
{
    public override string Icon => "💧";
    public override string NodeName => "添加水印";
    public override string ToolTipDescription => "水印图片叠加,在原图固定位置嵌入标识或模板图案。";

    // 透明度 0~1
    private double _alpha = 0.5;
    public double Alpha
    {
        get => _alpha;
        set { _alpha = value; OnPropertyChanged(); }
    }

    // 水印图片
    public Mat WatermarkImage { get; set; }

    // 预览图
    private WriteableBitmap _smallWriteableBitmap;
    public WriteableBitmap SmallWriteableBitmap
    {
        get => _smallWriteableBitmap;
        set { _smallWriteableBitmap = value; OnPropertyChanged(); }
    }

    // 命令
    public ICommand OpenCommand { get; }
    public ICommand CaptureCommand { get; }

    public WatermarkNodeModel()
    {
        MouseMode = NodeMouseMode.DrawRect;
        Cursor = Cursors.Cross;
        OpenCommand = ReactiveCommand.Create(OnOpenCommand);
        CaptureCommand = ReactiveCommand.Create(OnCaptureCommand);

        Tutorial = "1. 加载或截图水印图\n2. 设置透明度\n3. 选择位置\n4. 支持选框自定义位置";
    }
    private void OnCaptureCommand()
    {
        ScreenCaptureHelper screenCapture = new ScreenCaptureHelper();
        screenCapture.OnCaptureCompleted += (src =>
        {
            WatermarkImage = src.Clone();
            SmallWriteableBitmap = WatermarkImage.ToWriteableBitmap();
            src.Dispose();
        });
        screenCapture.StartRegionCapture();
    }

    private void OnOpenCommand()
    {
        string filename = FileHelper.OpenDialog("*.*");
        if (string.IsNullOrEmpty(filename))
            return;

        //打开图像
        Mat src = Cv2.ImRead(filename, ImreadModes.AnyColor);
        if (src == null || src.Empty())
        {
            src?.Dispose();
            return;
        }

        WatermarkImage = src.Clone();
        SmallWriteableBitmap = WatermarkImage.ToWriteableBitmap();
        src.Dispose();
    }

    protected override bool Before(Mat image)
    {
        if (WatermarkImage == null || WatermarkImage.Empty())
        {
            Message += "水印图片不可为空";
            return false;
        }

        return base.Before(image);
    }

    /// <summary>
    /// 节点执行(核心:水印叠加)
    /// </summary>
    public override Mat Execute(Mat src)
    {
        Mat result = src.Clone();

        try
        {
            Mat watermark = WatermarkImage.Clone();

            // 1. 自动缩小水印
            if (watermark.Width > src.Width || watermark.Height > src.Height)
            {
                double scale = Math.Min(
                    (double)src.Width / watermark.Width * 0.2,
                    (double)src.Height / watermark.Height * 0.2
                );
                Cv2.Resize(watermark, watermark, new Size(0, 0), scale, scale);
            }

            // ==============================================
            // 🔥 终极修复:处理 4通道透明图(截图必加)
            // ==============================================
            Mat watermarkProcessed = new Mat();

            // 如果是 4通道 → 转 3通道 BGR
            if (watermark.Channels() == 4)
            {
                Cv2.CvtColor(watermark, watermarkProcessed, ColorConversionCodes.BGRA2BGR);
            }
            else
            {
                watermarkProcessed = watermark;
            }

            // 统一通道和原图一致
            if (watermarkProcessed.Channels() != src.Channels())
            {
                Mat temp = new Mat();
                if (src.Channels() == 1)
                    Cv2.CvtColor(watermarkProcessed, temp, ColorConversionCodes.BGR2GRAY);
                else
                    Cv2.CvtColor(watermarkProcessed, temp, ColorConversionCodes.GRAY2BGR);

                watermarkProcessed.Dispose();
                watermarkProcessed = temp;
            }

            // 2. 位置
            int x = (int)SelectRect.X;
            int y = (int)SelectRect.Y;

            x = Math.Max(0, x);
            y = Math.Max(0, y);
            if (x + watermarkProcessed.Width > src.Width) x = src.Width - watermarkProcessed.Width;
            if (y + watermarkProcessed.Height > src.Height) y = src.Height - watermarkProcessed.Height;

            // 3. ROI叠加
            Rect roi = new Rect(x, y, watermarkProcessed.Width, watermarkProcessed.Height);
            Mat roiSrc = result[roi];
            Cv2.AddWeighted(roiSrc, 1 - Alpha, watermarkProcessed, Alpha, 0, roiSrc);

            // 释放
            roiSrc.Dispose();
            watermarkProcessed.Dispose();
            watermark.Dispose();

            Success = true;
            Message = "✅ 水印添加成功";
        }
        catch (Exception ex)
        {
            Message = "❌ 失败:" + ex.Message;
            Success = false;
        }

        return result;
    }
}

本文围绕提供的WatermarkNodeModel 代码,完整解析 **OpenCVSharp 工业级水印叠加实现**——核心基于 Cv2.AddWeighted 线性混合,结合 ROI 区域操作,实现“小图贴大图”的半透明水印效果,支持自定义位置、透明度,还能处理截图/本地图片作为水印,适配各类图像场景。

该节点是 Cv2.AddWeighted 最经典、最实用的实战场景,完美解决“水印叠加”的核心需求,同时兼顾稳定性(通道统一、自动缩放、异常处理),完全适配可视化节点开发(如 FlowSoft 框架)。

一、节点核心定位与功能

1. 节点基础信息

该节点是 BaseNodeModel 的子类,用于在原图指定位置叠加半透明水印,属于 OpenCVSharp 图像处理中“图像融合”的典型应用,核心技术是 Cv2.AddWeighted + ROI 区域操作。

核心功能:

  • 加载本地图片作为水印,或截图生成水印
  • 自定义水印透明度(0~1 范围)
  • 鼠标选框指定水印叠加位置,自动适配原图尺寸
  • 自动缩放水印(避免水印超出原图范围)
  • 处理透明图(4通道转3通道)、通道统一(与原图保持一致)
  • 异常处理(水印为空、图像加载失败等)

2. 节点核心依赖

代码中关键依赖(理解代码必备):

  • OpenCvSharp:核心图像处理(AddWeighted、Resize、CvtColor 等)
  • FlowSoft 框架:节点基类(BaseNodeModel)、属性/命令封装、UI 交互(鼠标选框、命令绑定)
  • ReactiveUI:响应式命令(OpenCommand、CaptureCommand)
  • 辅助工具类:FileHelper(打开文件)、ScreenCaptureHelper(屏幕截图)

二、核心代码逐段解析(从基础到核心)

我们按“属性 → 构造函数 → 辅助方法 → 核心执行逻辑”的顺序,逐行拆解代码,重点对应之前讲的 Cv2.AddWeighted 知识点。

1. 节点基础属性(UI + 核心参数)

// 节点图标、名称、提示信息(继承自 BaseNodeModel)
public override string Icon => "💧";
public override string NodeName => "添加水印";
public override string ToolTipDescription => "水印图片叠加,在原图固定位置嵌入标识或模板图案。";

// 核心参数:水印透明度(0~1,0=完全透明,1=完全不透明)
private double _alpha = 0.5;
public double Alpha
{
    get => _alpha;
    set { _alpha = value; OnPropertyChanged(); }
}

// 水印图片(Mat 格式,OpenCV 核心图像类型)
public Mat WatermarkImage { get; set; }

// 预览图(UI 显示用水印缩略图)
private WriteableBitmap _smallWriteableBitmap;
public WriteableBitmap SmallWriteableBitmap
{
    get => _smallWriteableBitmap;
    set { _smallWriteableBitmap = value; OnPropertyChanged(); }
}

// 交互命令:打开本地水印图、截图生成水印
public ICommand OpenCommand { get; }
public ICommand CaptureCommand { get; }

关键说明:

  • Alpha:水印透明度,与 Cv2.AddWeighted 中的“水印权重”完全对应,是控制水印深浅的核心参数。
  • WatermarkImage:存储水印图像的 Mat 对象,后续会经过缩放、通道转换等处理。
  • SmallWriteableBitmap:用于 UI 预览水印,通过 ToWriteableBitmap() 扩展方法将 Mat 转成 WPF 可显示的图像格式。

2. 构造函数(初始化交互逻辑)

public WatermarkNodeModel()
{
    MouseMode = NodeMouseMode.DrawRect; // 鼠标模式:绘制矩形(用于选择水印位置)
    Cursor = Cursors.Cross; // 鼠标样式:十字准星(方便选框)
    OpenCommand = ReactiveCommand.Create(OnOpenCommand); // 绑定“打开水印”命令
    CaptureCommand = ReactiveCommand.Create(OnCaptureCommand); // 绑定“截图水印”命令

    Tutorial = "1. 加载或截图水印图\n2. 设置透明度\n3. 选择位置\n4. 支持选框自定义位置";
}

关键说明:

  • MouseMode = NodeMouseMode.DrawRect:开启鼠标选框功能,用于在原图上指定水印的叠加位置(后续会用这个选框的坐标计算 ROI 区域)。
  • 两个命令:分别对应“打开本地水印”和“截图生成水印”,是用户交互的入口。

3. 辅助方法:加载/生成水印(OnOpenCommand + OnCaptureCommand)

(1)打开本地水印图(OnOpenCommand)

private void OnOpenCommand()
{
    string filename = FileHelper.OpenDialog("*.*"); // 打开文件选择对话框
    if (string.IsNullOrEmpty(filename))
        return;

    // 读取本地图像(支持任意颜色格式)
    Mat src = Cv2.ImRead(filename, ImreadModes.AnyColor);
    if (src == null || src.Empty()) // 校验图像是否读取成功
    {
        src?.Dispose(); // 释放资源,避免内存泄漏
        return;
    }

    WatermarkImage = src.Clone(); // 克隆图像,避免操作原图
    SmallWriteableBitmap = WatermarkImage.ToWriteableBitmap(); // 生成预览图
    src.Dispose(); // 释放原始读取的图像资源
}

(2)截图生成水印(OnCaptureCommand)

private void OnCaptureCommand()
{
    ScreenCaptureHelper screenCapture = new ScreenCaptureHelper();
    // 截图完成后的回调:获取截图图像,赋值给水印
    screenCapture.OnCaptureCompleted += (src =>
    {
        WatermarkImage = src.Clone(); // 克隆截图图像
        SmallWriteableBitmap = WatermarkImage.ToWriteableBitmap(); // 生成预览图
        src.Dispose(); // 释放截图资源
    });
    screenCapture.StartRegionCapture(); // 启动区域截图
}

两个方法的核心目的:

  • 获取水印图像(本地/截图),存储到WatermarkImage 中。
  • 生成 UI 预览图,让用户直观看到当前水印效果。
  • 严格的资源释放(Dispose()):避免 Mat 对象内存泄漏,这是 OpenCVSharp 开发的核心注意点。

4. 前置校验(Before 方法)

protected override bool Before(Mat image)
{
    if (WatermarkImage == null || WatermarkImage.Empty())
    {
        Message += "水印图片不可为空"; // 错误提示
        return false; // 校验失败,不执行后续操作
    }

    return base.Before(image); // 校验通过,执行核心逻辑
}

关键作用:执行水印叠加前的校验,避免因水印为空导致的程序异常,属于工业级代码的“防御性编程”。

5. 核心逻辑:水印叠加执行(Execute 方法,重中之重)

这是整个节点的核心,包含了 Cv2.AddWeighted 水印叠加、ROI 区域操作、水印缩放、通道统一等关键步骤,我们逐段拆解。

(1)初始化与水印克隆

public override Mat Execute(Mat src)
{
    Mat result = src.Clone(); // 克隆原图,避免修改原图(核心原则)

    try
    {
        Mat watermark = WatermarkImage.Clone(); // 克隆水印,避免操作原始水印

关键说明:

  • result = src.Clone():所有图像处理都基于原图克隆体,不直接修改输入的 src,避免影响后续节点的使用(OpenCV 中 Mat 是引用类型,直接操作会修改原图)。
  • watermark = WatermarkImage.Clone():同理,避免操作原始水印图像,保证下次使用时水印不变。

(2)自动缩放水印(避免水印超出原图)

// 1. 自动缩小水印:如果水印比原图大,按比例缩小(最大为原图的20%)
if (watermark.Width > src.Width || watermark.Height > src.Height)
{
    double scale = Math.Min(
        (double)src.Width / watermark.Width * 0.2,
        (double)src.Height / watermark.Height * 0.2
    );
    Cv2.Resize(watermark, watermark, new Size(0, 0), scale, scale);
}

逻辑说明:

  • 核心目的:防止水印尺寸过大,超出原图范围,导致叠加失败。
  • 缩放比例计算:取“原图宽度/水印宽度 × 0.2”和“原图高度/水印高度 × 0.2”的最小值,确保水印最大不超过原图的 20%,适配大部分场景。
  • Cv2.Resize:按比例缩放水印,new Size(0,0) 表示按 scale 自动计算宽高,避免拉伸变形。

(3)水印通道处理(关键:适配原图通道,处理透明图)

// ==============================================
// 🔥 终极修复:处理 4通道透明图(截图必加)
// ==============================================
Mat watermarkProcessed = new Mat();

// 如果是 4通道(BGRA,带透明通道)→ 转 3通道 BGR(去掉透明通道)
if (watermark.Channels() == 4)
{
    Cv2.CvtColor(watermark, watermarkProcessed, ColorConversionCodes.BGRA2BGR);
}
else
{
    watermarkProcessed = watermark;
}

// 统一通道和原图一致(避免 AddWeighted 报错)
if (watermarkProcessed.Channels() != src.Channels())
{
    Mat temp = new Mat();
    if (src.Channels() == 1) // 原图是灰度图(1通道),水印转灰度
        Cv2.CvtColor(watermarkProcessed, temp, ColorConversionCodes.BGR2GRAY);
    else // 原图是彩色图(3通道),水印转彩色
        Cv2.CvtColor(watermarkProcessed, temp, ColorConversionCodes.GRAY2BGR);

    watermarkProcessed.Dispose();
    watermarkProcessed = temp;
}

这是代码的“稳定性关键”,解决了 Cv2.AddWeighted 的核心使用限制:两张图像必须通道数一致

详细说明:

  • 处理 4通道透明图:截图生成的水印通常是 4通道(BGRA,包含透明通道),而 AddWeighted 不支持 4通道,因此转成 3通道 BGR。
  • 通道统一:如果原图是灰度图(1通道),水印也转成灰度;如果原图是彩色图(3通道),水印也转成彩色,确保和原图通道一致,避免 AddWeighted 报错。
  • 资源释放:watermarkProcessed.Dispose() 释放临时对象,避免内存泄漏。

(4)计算水印叠加位置(ROI 区域)

// 2. 计算水印叠加位置(基于鼠标选框 SelectRect)
int x = (int)SelectRect.X;
int y = (int)SelectRect.Y;

// 边界校验:确保水印不会超出原图范围
x = Math.Max(0, x); // 避免 x 为负数
y = Math.Max(0, y); // 避免 y 为负数
if (x + watermarkProcessed.Width > src.Width) x = src.Width - watermarkProcessed.Width; // 右边界不超出
if (y + watermarkProcessed.Height > src.Height) y = src.Height - watermarkProcessed.Height; // 下边界不超出

// 定义 ROI 区域:原图中要叠加水印的位置(和水印尺寸一致)
Rect roi = new Rect(x, y, watermarkProcessed.Width, watermarkProcessed.Height);
Mat roiSrc = result[roi]; // 获取原图的 ROI 区域(要叠加水印的背景区域)

关键知识点:ROI(Region of Interest,感兴趣区域)

  • 作用:只在原图的指定区域(ROI)叠加水印,而不是整个原图,实现“小图贴大图”的精准叠加。
  • Rect roi:定义 ROI 区域的坐标(x,y)和尺寸(宽、高),尺寸和处理后的水印完全一致。
  • result[roi]:获取原图中 ROI 区域的 Mat 对象(roiSrc),后续水印叠加只操作这个区域,不影响原图其他部分。
  • 边界校验:避免因鼠标选框超出原图范围,导致 ROI 区域无效,这是工业级代码的细节优化。

(5)核心:AddWeighted 半透明水印叠加(最关键一行)

// 3. ROI叠加:半透明水印核心逻辑
Cv2.AddWeighted(roiSrc, 1 - Alpha, watermarkProcessed, Alpha, 0, roiSrc);

这一行就是我们之前重点讲解的 Cv2.AddWeighted 水印用法,完全对应“半透明叠加”的标准公式,我们再次拆解对应关系:

AddWeighted 参数参数含义对应公式(dst = src1×alpha + src2×beta + gamma)
roiSrc背景区域(原图的 ROI 部分)src1(背景图)
1 - Alpha背景区域的权重(透明度越低,背景越明显)alpha(背景权重)
watermarkProcessed处理后的水印图像src2(水印图)
Alpha水印的权重(透明度越高,水印越明显)beta(水印权重)
0整体亮度偏移(不调整亮度,设为0)gamma(亮度偏移)
roiSrc输出图像(直接写回背景 ROI 区域,实现叠加)dst(输出融合后的图像)

核心逻辑:

叠加后的 ROI 区域 = 背景 × (1-透明度) + 水印 × 透明度

举例:当 Alpha = 0.5 时,背景和水印各占 50% 权重,呈现半透明叠加效果;当 Alpha = 0.2 时,水印更淡,不遮挡原图内容。

(6)资源释放与结果返回

// 释放所有临时 Mat 对象,避免内存泄漏
roiSrc.Dispose();
watermarkProcessed.Dispose();
watermark.Dispose();

Success = true;
Message = "✅ 水印添加成功";
}
catch (Exception ex)
{
    Message = "❌ 失败:" + ex.Message;
    Success = false;
}

return result;
}

关键说明:

  • 资源释放:所有临时创建的 Mat 对象(roiSrc、watermarkProcessed、watermark)都必须调用Dispose(),这是 OpenCVSharp 开发中避免内存泄漏的核心操作。
  • 异常处理:用 try-catch 捕获所有异常,返回错误信息,提升节点的稳定性,避免程序崩溃。
  • 返回 result:返回叠加水印后的图像,供后续节点使用。

三、核心技术关联(与之前讲解的知识点对应)

1. 与 Cv2.AddWeighted 的完全对应

该节点的水印叠加逻辑,就是我们之前讲解的 Cv2.AddWeighted 最经典的实战用法:

  • 核心公式一致:dst = src1×alpha + src2×beta + gamma,这里 gamma=0,alpha=1-Alpha(背景权重),beta=Alpha(水印权重)。
  • 使用场景一致:两张图像线性混合,实现半透明叠加,适合水印、图像融合等需求。
  • 使用限制规避:代码中通过“通道统一”“尺寸适配”,解决了 AddWeighted 要求“两张图大小、通道一致”的限制。

2. 新增知识点:ROI 区域操作

ROI 是“小图贴大图”的核心,也是水印叠加的关键技巧:

  • 定义:通过 Rect 指定原图中的一块区域,后续操作只针对这块区域,不影响其他部分。
  • 优势:高效、精准,避免对整个原图进行混合操作,提升性能。
  • 用法:Mat roi = result[new Rect(x,y,width,height)],获取 ROI 区域后,直接对 roi 执行 AddWeighted,实现局部叠加。

3. 工业级优化细节

这段代码不是简单的 AddWeighted 调用,而是包含了多个工业级优化,确保稳定性和实用性:

  • 资源管理:所有 Mat 对象都及时 Dispose(),避免内存泄漏。
  • 异常处理:校验水印是否为空、捕获执行过程中的异常,返回明确的提示信息。
  • 边界校验:水印位置、尺寸都做了边界处理,避免超出原图范围。
  • 通道适配:处理 4通道透明图、统一水印与原图通道,避免报错。
  • 自动缩放:水印过大时自动按比例缩小,适配原图尺寸。

四、常见问题与注意事项(必看)

1. 常见报错及解决方法

  • 报错1:AddWeighted 报错:size not match → 原因:水印尺寸与 ROI 尺寸不一致,或水印与原图通道数不一致;解决:检查水印缩放逻辑、通道转换逻辑。
  • 报错2:Mat is empty → 原因:水印未加载(为空),或本地图片读取失败;解决:先加载水印,校验水印是否有效。
  • 报错3:内存泄漏 → 原因:临时 Mat 对象未 Dispose();解决:确保所有创建的 Mat 对象都调用 Dispose()

2. 关键注意点

  • Alpha 范围:必须是 0~1,超出范围会导致水印效果异常(如透明度过高/过低)。
  • 水印尺寸:建议水印不要过大,代码中默认缩小到原图的 20%,可根据需求调整缩放比例。
  • 图像类型:支持彩色图、灰度图,但水印会自动适配原图通道,无需手动处理。
  • 资源释放:这是 OpenCVSharp 开发的重中之重,遗漏 Dispose() 会导致内存占用持续升高。

五、总结(核心记忆点)

1. 节点核心逻辑

WatermarkNodeModel = 水印加载/截图 + 水印缩放 + 通道统一 + ROI 区域 + Cv2.AddWeighted 半透明叠加

2. 核心代码一句话总结

Cv2.AddWeighted(roiSrc, 1 - Alpha, watermarkProcessed, Alpha, 0, roiSrc) → 半透明水印叠加的标准实现,背景权重 + 水印权重 = 1,保证叠加自然。

3. 与之前知识点的关联

该节点是 Cv2.AddWeighted 的实战落地,比基础用法多了“ROI 局部叠加”“水印适配”“异常处理”,是工业级项目中水印功能的标准写法,完美衔接之前讲解的图像融合知识点。

4. 适用场景

适用于所有需要“半透明水印叠加”的场景,如:图片版权标识、模板嵌入、图像标注等,可直接集成到可视化图像处理框架(如 FlowSoft)中,支持用户交互操作。

copyright @重庆教主 WPF中文网 联系站长:(QQ)23611316 (微信)movieclip (QQ群).NET小白课堂:864486030 | 本文由WPF中文网原创发布,谢绝转载 渝ICP备2023009518号-1