使用 C/C++ 和 OpenCV 添加图片水印
使用 C/C++ 和 OpenCV 添加图片水印 🖼️
在数字图像处理中,添加水印是一种常见的操作,可以用于版权保护、品牌宣传或信息标注。本文将介绍如何使用 C/C++ 和强大的计算机视觉库 OpenCV 来实现将自定义水印(图片或文字)添加到目标图片上。
准备工作 🛠️
在开始之前,请确保你已经具备以下条件:
- C/C++ 编译器: 如 GCC/G++, Clang, MSVC 等。
- OpenCV 库: 需要预先安装并配置好 OpenCV。你可以从 OpenCV 官网 (https://opencv.org/) 下载并根据你的操作系统进行安装。
- 一个集成开发环境 (IDE) (可选,但推荐): 如 Visual Studio, CLion, Code::Blocks 等,可以方便代码编写和项目管理。
- 待添加水印的图片: 准备一张你想要添加水印的图片。
- 水印图片 (可选): 如果你希望使用图片作为水印,请准备好水印图片。
核心概念 💡
实现图片水印的核心思路是将水印图像(或文字)与原始图像进行某种形式的融合或叠加。OpenCV 提供了丰富的图像处理函数,使得这个过程相对简单。
关键的 OpenCV 组件和函数包括:
cv::Mat
: OpenCV 中用于存储图像数据的核心数据结构。cv::imread()
: 用于加载图片到cv::Mat
对象。cv::imwrite()
: 用于将cv::Mat
对象中的图像数据保存到文件。cv::resize()
: 用于调整图像的大小,可以将水印调整到合适尺寸。cv::Rect
: 用于定义图像中的一个矩形区域 (Region of Interest, ROI),方便在特定位置操作。cv::addWeighted()
: 用于对两个图像进行加权混合,常用于实现半透明水印效果。cv::putText()
: 用于在图像上绘制文字,可以实现文字水印。
实现步骤 📝
下面将分步骤介绍如何实现图片水印和文字水印。
1. 包含头文件和命名空间
首先,在你的 C++ 代码中包含必要的 OpenCV 头文件并使用 cv
命名空间:
#include <opencv2/opencv.hpp> // 包含 OpenCV 的核心头文件
#include <iostream>using namespace cv;
using namespace std;
2. 加载原始图片和水印图片 (针对图片水印)
使用 cv::imread()
函数加载你的原始图片和水印图片。
Mat originalImage = imread("path/to/your/original_image.jpg");
Mat watermarkImage = imread("path/to/your/watermark_image.png", IMREAD_UNCHANGED); // IMREAD_UNCHANGED 保留 alpha 通道if (originalImage.empty()) {cerr << "错误: 无法加载原始图片!" << endl;return -1;
}if (watermarkImage.empty()) {cerr << "错误: 无法加载水印图片!" << endl;return -1;
}
注意: 如果你的水印图片是 PNG 格式并且包含透明通道 (alpha channel),使用 IMREAD_UNCHANGED
参数可以保留这些信息,从而实现更自然的融合效果。
3. 调整水印大小 (可选)
通常,水印图片的大小需要根据原始图片进行调整,以确保其不会过大或过小。
// 示例:将水印宽度调整为原始图片宽度的 1/5,并保持宽高比
double scaleFactor = (originalImage.cols / 5.0) / watermarkImage.cols;
Mat resizedWatermark;
resize(watermarkImage, resizedWatermark, Size(), scaleFactor, scaleFactor, INTER_LINEAR);
4. 定义水印位置
你需要确定水印在原始图片上的位置。通常可以选择图片的四个角或者中心。
// 示例:将水印放置在右下角,并留有一些边距
int margin = 10;
int x = originalImage.cols - resizedWatermark.cols - margin;
int y = originalImage.rows - resizedWatermark.rows - margin;// 或者放置在左上角
// int x = margin;
// int y = margin;// 或者放置在中心
// int x = (originalImage.cols - resizedWatermark.cols) / 2;
// int y = (originalImage.rows - resizedWatermark.rows) / 2;Rect roi(x, y, resizedWatermark.cols, resizedWatermark.rows);
确保定义的位置和水印大小不会超出原始图片的边界。
5. 将水印叠加到原始图片
方法一:使用 cv::addWeighted()
(适用于半透明效果)
如果水印图片没有 alpha 通道,或者你希望实现一个固定透明度的叠加效果,可以使用 cv::addWeighted()
。
// 确保水印图像和 ROI 区域的类型和通道数一致
// 如果原始图像是 3 通道 (BGR),水印图像也应该是 3 通道
Mat watermarkBGR;
if (resizedWatermark.channels() == 4) {cvtColor(resizedWatermark, watermarkBGR, COLOR_BGRA2BGR); // 如果有 alpha 通道,先转为 BGR
} else {watermarkBGR = resizedWatermark;
}Mat imageROI = originalImage(roi); // 获取原始图片中要放置水印的区域
double alpha = 0.5; // 水印的透明度 (0.0 完全透明, 1.0 完全不透明)
addWeighted(imageROI, 1.0 - alpha, watermarkBGR, alpha, 0.0, imageROI);
方法二:利用 Alpha 通道进行融合 (适用于 PNG 水印)
如果水印图片有 alpha 通道,可以实现更精细的融合,水印的非透明部分会完全覆盖,透明部分则显示背景。
if (resizedWatermark.channels() == 4) {vector<Mat> channels;split(resizedWatermark, channels); // 分离 RGBA 通道Mat bgr[3] = { channels[0], channels[1], channels[2] };Mat alphaChannel = channels[3]; // 获取 alpha 通道Mat watermarkBGR_alpha;merge(bgr, 3, watermarkBGR_alpha); // 合并 BGR 通道Mat imageROI = originalImage(roi);// 使用 alpha 通道作为掩码进行融合for (int r = 0; r < imageROI.rows; ++r) {for (int c = 0; c < imageROI.cols; ++c) {double alpha = alphaChannel.at<uchar>(r, c) / 255.0;if (alpha > 0) { // 只处理非完全透明的像素for (int channel = 0; channel < 3; ++channel) {imageROI.at<Vec3b>(r, c)[channel] =saturate_cast<uchar>((1.0 - alpha) * imageROI.at<Vec3b>(r, c)[channel] +alpha * watermarkBGR_alpha.at<Vec3b>(r, c)[channel]);}}}}
} else {// 如果水印没有 alpha 通道,可以简单复制或使用 addWeightedMat imageROI = originalImage(roi);resizedWatermark.copyTo(imageROI); // 直接覆盖
}
一个更简洁的使用 alpha 通道的方法是:
if (resizedWatermark.channels() == 4) {Mat imageROI = originalImage(roi);vector<Mat> bgra_channels;split(resizedWatermark, bgra_channels);// 创建一个掩码,其中 alpha > 0 的地方为 255Mat mask;compare(bgra_channels[3], 0, mask, CMP_GT);// 提取水印的 BGR 部分Mat watermark_bgr;vector<Mat> bgr_channels = {bgra_channels[0], bgra_channels[1], bgra_channels[2]};merge(bgr_channels, watermark_bgr);// 将水印的 BGR 部分拷贝到 ROI,使用掩码watermark_bgr.copyTo(imageROI, mask);
} else {// 处理没有 alpha 通道的情况Mat imageROI = originalImage(roi);resizedWatermark.copyTo(imageROI);
}
6. 添加文字水印
如果你想添加文字作为水印,可以使用 cv::putText()
函数。
string watermarkText = "My Copyright";
Point textOrg(originalImage.cols - 200, originalImage.rows - 30); // 文字的起始位置 (左下角)
int fontFace = FONT_HERSHEY_SIMPLEX;
double fontScale = 1.0;
Scalar color(0, 0, 255); // 字体颜色 (BGR) - 这里是红色
int thickness = 2;
int lineType = LINE_AA; // 抗锯齿putText(originalImage, watermarkText, textOrg, fontFace, fontScale, color, thickness, lineType);
你可以调整字体、大小、颜色、位置和透明度(通过在文字下方绘制一个半透明的矩形背景)。
7. 显示和保存结果
最后,你可以显示带有水印的图片,并将其保存到文件。
namedWindow("带水印的图片", WINDOW_AUTOSIZE);
imshow("带水印的图片", originalImage);imwrite("path/to/your/output_image_with_watermark.jpg", originalImage);waitKey(0); // 等待按键后关闭窗口
destroyAllWindows();
完整示例代码 (图片水印 - Alpha 融合)
#include <opencv2/opencv.hpp>
#include <iostream>using namespace cv;
using namespace std;int main() {// 1. 加载原始图片和水印图片Mat originalImage = imread("original.jpg"); // 替换为你的原始图片路径Mat watermarkImage = imread("watermark.png", IMREAD_UNCHANGED); // 替换为你的水印图片路径if (originalImage.empty()) {cerr << "错误: 无法加载原始图片!" << endl;return -1;}if (watermarkImage.empty()) {cerr << "错误: 无法加载水印图片!" << endl;return -1;}// 2. 调整水印大小 (可选)Mat resizedWatermark;double scaleFactor = (originalImage.cols / 8.0) / watermarkImage.cols; // 水印宽度为原图的1/8if (watermarkImage.cols > originalImage.cols * 0.1) { // 仅当水印宽度大于原图10%时缩放resize(watermarkImage, resizedWatermark, Size(), scaleFactor, scaleFactor, INTER_AREA);} else {resizedWatermark = watermarkImage;}// 3. 定义水印位置 (右下角)int margin = 20;int x = originalImage.cols - resizedWatermark.cols - margin;int y = originalImage.rows - resizedWatermark.rows - margin;// 确保 roi 不会超出原图边界if (x < 0) x = 0;if (y < 0) y = 0;if (x + resizedWatermark.cols > originalImage.cols) {resizedWatermark = resizedWatermark(Rect(0, 0, originalImage.cols - x, resizedWatermark.rows));}if (y + resizedWatermark.rows > originalImage.rows) {resizedWatermark = resizedWatermark(Rect(0, 0, resizedWatermark.cols, originalImage.rows - y));}Rect roi(x, y, resizedWatermark.cols, resizedWatermark.rows);Mat imageROI = originalImage(roi);// 4. 将水印叠加到原始图片 (使用 Alpha 通道)if (resizedWatermark.channels() == 4) { // 检查是否有 Alpha 通道vector<Mat> channels;split(resizedWatermark, channels); // 分离 B, G, R, Alpha 通道Mat mask;// Alpha 通道通常是最后一个通道 channels[3]// 如果你的 OpenCV 版本或水印图片通道顺序不同 (例如 ARGB),请相应调整if (channels.size() == 4) {mask = channels[3]; // Alpha 通道作为掩码} else {// 如果分离后不是4通道,说明水印图本身就没有alpha,需要特殊处理或报错// 这里我们创建一个全白的掩码,效果类似直接覆盖mask = Mat(resizedWatermark.rows, resizedWatermark.cols, CV_8UC1, Scalar(255));}// 提取水印的 BGR 部分 (前三个通道)Mat watermark_bgr;vector<Mat> bgr_channels_vec;if (channels.size() >= 3) {bgr_channels_vec.push_back(channels[0]);bgr_channels_vec.push_back(channels[1]);bgr_channels_vec.push_back(channels[2]);merge(bgr_channels_vec, watermark_bgr);// 将水印的 BGR 部分拷贝到 ROI,使用 Alpha 通道作为掩码watermark_bgr.copyTo(imageROI, mask);} else {// 如果水印不是至少3通道,则直接拷贝(可能不是期望的效果)resizedWatermark.copyTo(imageROI);}} else if (resizedWatermark.channels() == 3) { // 如果水印是 3 通道 BGR// 可以选择直接覆盖或使用 addWeighted// resizedWatermark.copyTo(imageROI); // 直接覆盖// 或者使用 addWeighted 实现半透明double alpha_blend = 0.7; // 透明度addWeighted(imageROI, 1.0 - alpha_blend, resizedWatermark, alpha_blend, 0.0, imageROI);} else {cerr << "错误: 水印图片的通道数不受支持 (" << resizedWatermark.channels() << ")。" << endl;// 可以选择直接拷贝单通道图像作为灰度水印if(resizedWatermark.channels() == 1){cvtColor(resizedWatermark, resizedWatermark, COLOR_GRAY2BGR); // 转为BGR再处理resizedWatermark.copyTo(imageROI);} else {return -1;}}// 5. 显示和保存结果namedWindow("带水印的图片", WINDOW_AUTOSIZE);imshow("带水印的图片", originalImage);if (imwrite("output_with_watermark.jpg", originalImage)) {cout << "成功保存带水印的图片: output_with_watermark.jpg" << endl;} else {cerr << "错误: 无法保存图片!" << endl;}waitKey(0);destroyAllWindows();return 0;
}
编译和运行:
你需要将上述代码保存为 .cpp
文件 (例如 add_watermark.cpp
),并使用你的 C++ 编译器链接 OpenCV 库进行编译。
例如,使用 g++:
g++ add_watermark.cpp -o add_watermark $(pkg-config --cflags --libs opencv4)
./add_watermark
(如果你的 pkg-config
配置的是 opencv
而不是 opencv4
,请相应修改)。
结论 🏁
通过 OpenCV,我们可以方便地在 C++ 程序中为图片添加各种类型的水印。无论是简单的文字水印还是带有透明效果的图片水印,OpenCV 都提供了相应的工具和函数来实现。关键在于理解图像的 ROI 操作以及如何有效地融合两个图像。希望本文能帮助你成功地为你的图片添加上自定义的水印!
相关文章:
使用 C/C++ 和 OpenCV 添加图片水印
使用 C/C 和 OpenCV 添加图片水印 🖼️ 在数字图像处理中,添加水印是一种常见的操作,可以用于版权保护、品牌宣传或信息标注。本文将介绍如何使用 C/C 和强大的计算机视觉库 OpenCV 来实现将自定义水印(图片或文字)添…...
Secs/Gem第十二讲(基于secs4net项目的ChatGpt介绍)
好,那我们进入最关键的一讲—— 第十二讲:完整事件通知流程全景图——CEID 触发到主机接收的全过程 关键词:CEID 事件上报、S6F11 报文、事件触发流程、数据驱动机制、Report Dispatch、主机解析流程 本讲目标 你将彻底理解: 设…...
FastAPI实战起步:从Python环境到你的第一个“Hello World”API接口
上一篇文章中介绍了有关FastAPI的优势,本篇文章我将手把手带你从零开始,搭建FastAPI的开发环境,并成功运行你的第一个“Hello World”API。在开始之前,请确保你的电脑已经安装了Python 3.7或更高版本,以及VS Code&…...

使用vue3+ts+input封装上传组件,上传文件显示文件图标
效果图: 代码 <template><div class"custom-file-upload"><div class"upload"><!-- 显示已选择的文件 --><div class"file-list"><div v-for"(item, index) in state.filsList" :key&q…...
iOS 抖音导航栏首页一键分两列功能的实现
要实现 iOS 抖音首页导航栏的“一键分两列”功能(通常指将单列内容切换为双列瀑布流布局),需结合自定义导航栏控件与布局动态切换逻辑。以下是关键实现步骤和技术要点,基于 iOS 原生开发框架(Swift/Objective-C&#x…...
零基础入门 C 语言基础知识(含面试题):结构体、联合体、枚举、链表、环形队列、指针全解析!
🌟 零基础入门 C 语言基础知识(含面试题):结构体、联合体、枚举、链表、环形队列、指针全解析! C 语言是所有程序员通向“系统世界”的第一把钥匙。很多嵌入式开发、操作系统内核、网络通信、图形引擎,背后…...

【Linux】Ubuntu 创建应用图标的方式汇总,deb/appimage/通用方法
Ubuntu 创建应用图标的方式汇总,deb/appimage/通用方法 对于标准的 Ubuntu(使用 GNOME 桌面),desktop 后缀的桌面图标文件主要保存在以下三个路径: 当前用户的桌面目录(这是最常见的位置)。所…...
【Unity】R3 CSharp 响应式编程 - 使用篇(集合)(三)
1、ObservableList 基础 List 类型测试 using System;using System.Collections.Specialized;using ObservableCollections;using UnityEngine;namespace Aladdin.Standard.Observable.Collections.List{public class ObservableListTest : MonoBehaviour{protected readonly O…...
振动力学:弹性杆的纵向振动(固有振动和固有频率的概念)
文章1、2、3中讨论的是离散系统的振动特性,然而实际系统的惯性质量、弹性、阻尼等特性都是连续分布的,因而成为连续系统或分布参数系统。确定连续介质中无数个点的运动需要无限个广义坐标,因此也称为无限自由度系统,典型的结构例如:弦、杆、膜、环、梁、板、壳等,也称为弹…...

LangGraph--Agent工作流
Agent的工作流 下面展示了如何创建一个“计划并执行”风格的代理。 这在很大程度上借鉴了 计划和解决 论文以及Baby-AGI项目。 核心思想是先制定一个多步骤计划,然后逐项执行。完成一项特定任务后,您可以重新审视计划并根据需要进行修改。 般的计算图如…...

Spring Boot 常用注解面试题深度解析
🤟致敬读者 🟩感谢阅读🟦笑口常开🟪生日快乐⬛早点睡觉 📘博主相关 🟧博主信息🟨博客首页🟫专栏推荐🟥活动信息 文章目录 Spring Boot 常用注解面试题深度解析一、核心…...

Linux系统的CentOS7发行版安装MySQL80
文章目录 前言Linux命令行内的”应用商店”安装CentOS的安装软件的yum命令安装MySQL1. 配置yum仓库2. 使用yum安装MySQL3. 安装完成后,启动MySQL并配置开机自启动4. 检查MySQL的运行状态 MySQL的配置1. 获取MySQL的初始密码2. 登录MySQL数据库系统3. 修改root密码4.…...

408第一季 - 数据结构 - 栈与队列
栈 闲聊 栈是一个线性表 栈的特点是后进先出 然后是一个公式 比如123要入栈,一共有5种排列组合的出栈 栈的数组实现 这里有两种情况,,一个是有下标为-1的,一个没有 代码不用看,真题不会考 栈的链式存储结构 L ->…...

【RTP】Intra-Refresh模式下的 H.264 输出,RTP打包的方式和普通 H.264 流并没有本质区别
对于 Intra-Refresh 模式下的 H.264 输出,RTP 打包 的方式和普通 H.264 流并没有本质区别:你依然是在对一帧一帧的 NAL 单元进行 RTP 分包,只不过这些 NAL 单元内部有部分宏块是 “帧内编码” 而已。下面分步骤说明: 1. 原理回顾:RFC 6184 H.264 over RTP 按照 RFC 6184 …...
nano编辑器的详细使用教程
以下是 Linux 下 nano 编辑器 的详细使用指南,涵盖安装、基础操作、高级功能、快捷键以及常见问题处理。 一、安装 nano 大多数 Linux 发行版已预装 nano。如果没有安装,可以通过以下命令安装: Debian/Ubuntu 系:sudo apt update…...

Redis实战-消息队列篇
前言: 讲讲做消息队列遇到的问题。 今日所学: 异步优化消息队列基于stream实现异步下单 1. 异步优化 1.1 需求分析 1.1.1 现有下单流程: 1.查询优惠劵 2.判断是否是秒杀时间,库存是否充足 3.实现一人一单 在这个功能中&…...

(三)Linux性能优化-CPU-CPU 使用率
CPU使用率 user(通常缩写为 us),代表用户态 CPU 时间。注意,它不包括下面的 nice 时间,但包括了 guest 时间。nice(通常缩写为 ni),代表低优先级用户态 CPU 时间,也就是进…...

佰力博科技与您探讨材料介电性能测试的影响因素
1、频率依赖性 材料的介电性能通常具有显著的频率依赖性。在低频下,偶极子的取向极化占主导,介电常数较高;而在高频下,偶极子的取向极化滞后,导致介电常数下降,同时介电损耗增加。例如,VHB4910…...

K8S认证|CKS题库+答案| 4. RBAC - RoleBinding
目录 4. RBAC - RoleBinding 免费获取并激活 CKA_v1.31_模拟系统 题目 开始操作: 1)、切换集群 2)、查看SA和role 3)、编辑 role-1 权限 4)、检查role 5)、创建 role和 rolebinding 6࿰…...

React 新项目
使用git bash 创建一个新项目 建议一开始就创建TS项目 原因在Webpack中改配置麻烦 编译方法:ts compiler 另一种 bable 最好都配置 $ create-react-app cloundmusic --template typescript 早期react项目 yarn 居多 目前npm包管理居多 目前pnpm不通用 icon 在public文件夹中…...
解决MySQL8.4报错ERROR 1524 (HY000): Plugin ‘mysql_native_password‘ is not loaded
最近使用了MySQL8.4 , 服务启动成功,但是就是无法登陆,并且报错: ERROR 1524 (HY000): Plugin mysql_native_password is not loaded 使用如下的命令也报错 mysql -u root -p -P 3306 问题分析: 在MySQL 8.0版本中,默认的认证插件从mysql_native_password变更为cachi…...

AI编程在BOSS项目的实践经验分享
前言 在人工智能技术革新浪潮的推动下,智能编程助手正以前所未有的速度重塑开发领域。这些基于AI的代码辅助工具通过智能提示生成、实时错误检测和自动化重构等功能,显著提升了软件工程的全流程效率。无论是初入行业的开发者还是资深程序员,…...
力扣-131.分割回文串
题目描述 给你一个字符串 s,请你将 s 分割成一些 子串,使每个子串都是 回文串 。返回 s 所有可能的分割方案。 class Solution {List<List<String>> res new ArrayList<>();List<String> path new ArrayList<>();void…...

数学:”度量空间”了解一下?
度量空间是现代数学中一种基本且重要的抽象空间。以下是对它的详细介绍: 定义 相关概念 常见的度量空间举例 度量空间的类型 度量空间的作用 度量空间是拓扑空间的一种特殊情况,它为拓扑空间的研究提供了具体的模型和实例。同时,度量空间在…...

jenkins脚本查看及备份
位置与备份 要完整备份 Jenkins 的所有脚本和相关配置,包括 Jenkinsfile、构建脚本(如 .sh / .bat)、Job 配置、插件、凭据等,你可以从两个层面入手: ✅ 一、完整备份 Jenkins 主目录(最全面) …...

用电脑通过网口控制keysight示波器
KEYSIGHT示波器HD304MSO性能 亮点: 体验 200 MHz 至 1 GHz 的带宽和 4 个模拟通道。与 12 位 ADC 相比,使用 14 位模数转换器 (ADC) 将垂直分辨率提高四倍。使用 10.1 英寸电容式触摸屏轻松查看和分析您的信号。捕获 50 μVRMS 本底噪声的较小信号。使用独有区域触摸在几秒…...
嵌入式面试提纲
一、TCP/IP 协议 1.1 TCP/IP 五层模型概述 链路层(Link Layer) 包括网卡驱动、以太网、Wi‑Fi、PPP 等。负责把数据帧(Frame)在相邻节点间传输。 网络层(Internet Layer) 最典型的是 IP 协议 (IPv4/IPv6)。负责 路由选路、分片与重组。 其他:ICMP(Ping、目的不可达等)…...
算法工程师认知水平要求总结
要成为一名合格的算法工程师或算法科学家,需要达到的认知水平不仅包括扎实的技术功底,更涵盖系统性思维、问题抽象能力和工程实践智慧。以下是关键维度的认知能力要求: 一、理论基础认知深度 数学根基 概率统计:深刻理解贝叶斯推断…...

《如何使用MinGW-w64编译OpenCV和opencv_contrib》
《如何使用MinGW-w64编译OpenCV和opencv_contrib》 在Windows环境下使用MinGW编译OpenCV和opencv_contrib是一个常见需求,尤其是对于那些希望使用GCC工具链而非Visual Studio的开发者。下面我将详细介绍这个过程。 准备工作 首先需要安装和准备以下工具和库: MinGW(建议使…...
数据库、数据仓库、数据中台、数据湖相关概念
文章目录 序言1数据库,数据仓库,数据中台,数据湖-概念对比释义1.1概念产生的时间顺序1.2在使用功能方面对比1.3在使用工具方面对比 2数据仓库2.1数据仓库的发展阶段2.2 数据仓库的设计2.3数据仓库常用工具,方法2.3.1分析型数据库和…...