OpenCV-Python 实现数字水印的核心是将水印信息(文本、图像) 嵌入到原始图像中,且不影响原始图像的视觉效果,同时保证水印可被准确提取。根据水印的可见性,分为可见水印 (如 LOGO 叠加)和不可见水印 (隐藏在像素中);根据提取方式,分为盲水印 (无需原始图像即可提取)和非盲水印(需原始图像辅助提取)。以下详细介绍常用水印方案的实现及原理:
一、可见水印(LOGO / 文本叠加)
可见水印直接叠加在原始图像上,用于版权声明(如图片 LOGO、文字标识),核心是通过图像混合 或Alpha 通道控制透明度实现。
1. 图像 LOGO 水印(带透明度控制)
python
import cv2
import numpy as np
import matplotlib.pyplot as plt
def add_visible_logo_watermark(img, logo_path, alpha=0.3, position='bottom-right'):
"""
添加可见LOGO水印
:param img: 原始图像
:param logo_path: 水印LOGO路径(推荐PNG带Alpha通道)
:param alpha: 水印透明度(0-1,越小越透明)
:param position: 水印位置(top-left/top-right/bottom-left/bottom-right)
:return: 带水印的图像
"""
# 读取LOGO并保留Alpha通道
logo = cv2.imread(logo_path, cv2.IMREAD_UNCHANGED)
if logo is None:
raise ValueError("LOGO读取失败!")
# 调整LOGO尺寸(默认占原图1/5)
h, w = img.shape[:2]
logo_h, logo_w = logo.shape[:2]
scale = min(w//5 / logo_w, h//5 / logo_h) # 缩放比例
logo = cv2.resize(logo, (int(logo_w*scale), int(logo_h*scale)))
# 分离LOGO的RGB和Alpha通道
logo_rgb = logo[:, :, :3]
logo_alpha = logo[:, :, 3] / 255.0 # 归一化到0-1
# 计算水印位置
lh, lw = logo_rgb.shape[:2]
if position == 'bottom-right':
x = w - lw - 20 # 右偏移20像素
y = h - lh - 20 # 下偏移20像素
elif position == 'bottom-left':
x = 20
y = h - lh - 20
elif position == 'top-right':
x = w - lw - 20
y = 20
else: # top-left
x = 20
y = 20
# 提取原图对应区域并混合
roi = img[y:y+lh, x:x+lw]
# 混合公式:roi * (1 - alpha*logo_alpha) + logo_rgb * (alpha*logo_alpha)
for c in range(3):
roi[:, :, c] = np.uint8(
roi[:, :, c] * (1 - alpha * logo_alpha) + logo_rgb[:, :, c] * (alpha * logo_alpha)
)
return img
# 测试
img = cv2.imread('image.jpg')
plt.subplot(121), plt.imshow(cv2.cvtColor(img, cv2.COLOR_BGR2RGB)), plt.title('Original Img'), plt.axis('off')
watermarked_img = add_visible_logo_watermark(img, 'logo.png', alpha=0.4)
# 显示
plt.subplot(122), plt.imshow(cv2.cvtColor(watermarked_img, cv2.COLOR_BGR2RGB)), plt.title('LOGO Img'), plt.axis('off')
plt.show()
# 保存结果
cv2.imwrite('watermarked_logo.jpg', watermarked_img)
运行效果图:

2. 文本水印(添加版权文字)
python
def add_text_watermark(img, text, font_scale=1.0, color=(255, 255, 255), alpha=0.5, position='bottom-right'):
"""
添加文本水印
:param img: 原始图像
:param text: 水印文本(如"© 2025 版权所有")
:param font_scale: 字体大小
:param color: 文本颜色(BGR)
:param alpha: 透明度(0-1)
:param position: 位置
:return: 带水印的图像
"""
h, w = img.shape[:2]
# 定义字体
font = cv2.FONT_HERSHEY_SIMPLEX
# 计算文本尺寸
text_size, _ = cv2.getTextSize(text, font, font_scale, thickness=2)
tw, th = text_size
# 计算文本位置
if position == 'bottom-right':
x = w - tw - 20
y = h - th - 10
else: # top-left
x = 20
y = th + 20
# 创建透明文本层
text_layer = np.zeros_like(img, dtype=np.uint8)
cv2.putText(text_layer, text, (x, y), font, font_scale, color, thickness=2)
# 混合文本层和原图
watermarked = cv2.addWeighted(img, 1.0, text_layer, alpha, 0)
return watermarked
# 测试文本水印(添加版权文字)
text_watermarked = add_text_watermark(img, "@ 2025 CSDN:BangBangDePiPi", font_scale=0.8, color=(0, 0, 255), alpha=0.4)
plt.subplot(122), plt.imshow(cv2.cvtColor(text_watermarked, cv2.COLOR_BGR2RGB)), plt.title('Watermark'), plt.axis('off'), plt.show()
效果运行图:

3.完整代码:
python
import cv2
import numpy as np
import matplotlib.pyplot as plt
def add_visible_logo_watermark(img, logo_path, alpha=0.3, position='bottom-right'):
"""
添加可见LOGO水印
:param img: 原始图像
:param logo_path: 水印LOGO路径(推荐PNG带Alpha通道)
:param alpha: 水印透明度(0-1,越小越透明)
:param position: 水印位置(top-left/top-right/bottom-left/bottom-right)
:return: 带水印的图像
"""
# 读取LOGO并保留Alpha通道
logo = cv2.imread(logo_path, cv2.IMREAD_UNCHANGED)
if logo is None:
raise ValueError("LOGO读取失败!")
# 调整LOGO尺寸(默认占原图1/5)
h, w = img.shape[:2]
logo_h, logo_w = logo.shape[:2]
scale = min(w // 5 / logo_w, h // 5 / logo_h) # 缩放比例
logo = cv2.resize(logo, (int(logo_w * scale), int(logo_h * scale)))
# 分离LOGO的RGB和Alpha通道
logo_rgb = logo[:, :, :3]
logo_alpha = logo[:, :, 2] / 255.0 # 归一化到0-1
# 计算水印位置
lh, lw = logo_rgb.shape[:2]
if position == 'bottom-right':
x = w - lw - 20 # 右偏移20像素
y = h - lh - 20 # 下偏移20像素
elif position == 'bottom-left':
x = 20
y = h - lh - 20
elif position == 'top-right':
x = w - lw - 20
y = 20
else: # top-left
x = 20
y = 20
# 提取原图对应区域并混合
roi = img[y:y + lh, x:x + lw]
# 混合公式:roi * (1 - alpha*logo_alpha) + logo_rgb * (alpha*logo_alpha)
for c in range(3):
roi[:, :, c] = np.uint8(
roi[:, :, c] * (1 - alpha * logo_alpha) + logo_rgb[:, :, c] * (alpha * logo_alpha)
)
return img
def add_text_watermark(img, text, font_scale=1.0, color=(255, 255, 255), alpha=0.5, position='bottom-right'):
"""
添加文本水印
:param img: 原始图像
:param text: 水印文本(如"© 2025 版权所有")
:param font_scale: 字体大小
:param color: 文本颜色(BGR)
:param alpha: 透明度(0-1)
:param position: 位置
:return: 带水印的图像
"""
h, w = img.shape[:2]
# 定义字体
font = cv2.FONT_HERSHEY_SIMPLEX
# 计算文本尺寸
text_size, _ = cv2.getTextSize(text, font, font_scale, thickness=2)
tw, th = text_size
# 计算文本位置
if position == 'bottom-right':
x = w - tw - 20
y = h - th - 10
else: # top-left
x = 20
y = th + 20
# 创建透明文本层
text_layer = np.zeros_like(img, dtype=np.uint8)
cv2.putText(text_layer, text, (x, y), font, font_scale, color, thickness=2)
# 混合文本层和原图
watermarked = cv2.addWeighted(img, 1.0, text_layer, alpha, 0)
return watermarked
# 测试
img = cv2.imread('panorama.jpg')
plt.subplot(121), plt.imshow(cv2.cvtColor(img, cv2.COLOR_BGR2RGB)), plt.title('Original Img'), plt.axis('off')
# 测试文本水印(添加版权文字)
text_watermarked = add_text_watermark(img, "@ 2025 CSDN:BangBangDePiPi", font_scale=0.8, color=(0, 0, 255), alpha=0.4)
plt.subplot(122), plt.imshow(cv2.cvtColor(text_watermarked, cv2.COLOR_BGR2RGB)), plt.title('Watermark'), plt.axis('off'), plt.show()
# # 测试图像 LOGO 水印(带透明度控制)
# watermarked_img = add_visible_logo_watermark(img, 'Python-OpenCV.png', alpha=0.4)
# plt.subplot(122), plt.imshow(cv2.cvtColor(watermarked_img, cv2.COLOR_BGR2RGB)), plt.title('LOGO Img'), plt.axis('off')
# 显示
plt.show()
# 保存结果
# cv2.imwrite('watermarked_logo.jpg', watermarked_img)
二、不可见水印(LSB 算法)
不可见水印隐藏在图像的最低有效位(LSB) 中,人眼无法察觉,核心原理是:图像像素的低位(如第 0 位、第 1 位)对视觉影响极小,可替换为水印信息。
1. 原理
- 水印预处理:将二值水印图像(0/255)转为 0/1 的二进制序列;
- 嵌入:将原始图像像素的最低位(LSB)替换为水印的二进制位;
- 提取:读取含水印图像的最低位,恢复水印二进制序列,重构水印图像。
2. 实现代码(图像水印)
python
def embed_lsb_watermark(img, watermark_path):
"""
LSB不可见水印嵌入(非盲水印)
:param img: 原始图像(灰度图或彩色图)
:param watermark_path: 水印图像路径(建议二值图,尺寸小于原图1/8)
:return: 含水印图像
"""
# 读取并预处理水印(转为二值图)
watermark = cv2.imread(watermark_path, 0)
_, watermark_bin = cv2.threshold(watermark, 127, 1, cv2.THRESH_BINARY) # 0/1二值化
wm_h, wm_w = watermark_bin.shape
# 检查水印尺寸(需小于原图1/8,避免溢出)
h, w = img.shape[:2]
if wm_h > h//2 or wm_w > w//2:
raise ValueError("水印尺寸过大!请缩小水印至原图1/2以内")
# 若为彩色图,转为灰度图处理(或选择单个通道)
if len(img.shape) == 3:
img_gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
else:
img_gray = img.copy()
# 嵌入水印(替换最低位)
watermarked = img_gray.copy()
for i in range(wm_h):
for j in range(wm_w):
# 清空像素最低位,嵌入水印位
watermarked[i, j] = (watermarked[i, j] & ~1) | watermark_bin[i, j]
# 若为彩色图,将处理后的灰度图合并回彩色通道
if len(img.shape) == 3:
watermarked_color = img.copy()
watermarked_color[:, :, 0] = watermarked # 嵌入到蓝色通道
return watermarked_color
else:
return watermarked
def extract_lsb_watermark(watermarked_img, wm_size):
"""
提取LSB水印
:param watermarked_img: 含水印图像
:param wm_size: 水印尺寸(h, w)
:return: 提取的水印图像
"""
wm_h, wm_w = wm_size
# 若为彩色图,提取嵌入通道(如蓝色通道)
if len(watermarked_img.shape) == 3:
img_gray = watermarked_img[:, :, 0]
else:
img_gray = watermarked_img
# 提取最低位作为水印
watermark = np.zeros((wm_h, wm_w), dtype=np.uint8)
for i in range(wm_h):
for j in range(wm_w):
watermark[i, j] = img_gray[i, j] & 1 # 提取最低位
watermark = watermark * 255 # 恢复为0/255二值图
return watermark
# 测试
img = cv2.imread('image.jpg')
watermark = cv2.imread('watermark.png', 0)
wm_h, wm_w = watermark.shape
# 嵌入水印
watermarked = embed_lsb_watermark(img, 'watermark.png')
# 提取水印
extracted_wm = extract_lsb_watermark(watermarked, (wm_h, wm_w))
# 显示结果
plt.figure(figsize=(15, 5))
plt.subplot(131), plt.imshow(cv2.cvtColor(img, cv2.COLOR_BGR2RGB)), plt.title('原始图像'), plt.axis('off')
plt.subplot(132), plt.imshow(cv2.cvtColor(watermarked, cv2.COLOR_BGR2RGB)), plt.title('含水印图像'), plt.axis('off')
plt.subplot(133), plt.imshow(extracted_wm, cmap='gray'), plt.title('提取的水印'), plt.axis('off')
plt.show()
# 保存结果
cv2.imwrite('watermarked_lsb.jpg', watermarked)
cv2.imwrite('extracted_watermark.jpg', extracted_wm)
3. 优化:文本水印嵌入(LSB)
将文本转为二进制序列嵌入图像:
python
def text_to_bin(text):
"""文本转二进制序列"""
return ''.join([format(ord(c), '08b') for c in text])
def embed_text_lsb(img, text):
"""嵌入文本水印"""
bin_text = text_to_bin(text) + '11111111' # 结束标志(8个1)
text_len = len(bin_text)
# 检查文本长度(需小于图像像素数)
h, w = img.shape[:2]
pixel_count = h * w
if text_len > pixel_count:
raise ValueError("文本过长!请缩短文本或使用更大图像")
# 嵌入文本
img_gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) if len(img.shape) == 3 else img.copy()
idx = 0
for i in range(h):
for j in range(w):
if idx < text_len:
# 替换最低位为文本二进制位
img_gray[i, j] = (img_gray[i, j] & ~1) | int(bin_text[idx])
idx += 1
else:
break
if idx >= text_len:
break
# 转回彩色图
if len(img.shape) == 3:
img_color = img.copy()
img_color[:, :, 0] = img_gray
return img_color
return img_gray
def extract_text_lsb(watermarked_img):
"""提取文本水印"""
img_gray = watermarked_img[:, :, 0] if len(watermarked_img.shape) == 3 else watermarked_img
h, w = img_gray.shape
bin_text = ''
# 提取最低位,直到遇到结束标志
for i in range(h):
for j in range(w):
bin_text += str(img_gray[i, j] & 1)
# 检查是否到达结束标志
if len(bin_text) >= 8 and bin_text[-8:] == '11111111':
bin_text = bin_text[:-8] # 去掉结束标志
# 二进制转文本
text = ''
for k in range(0, len(bin_text), 8):
byte = bin_text[k:k+8]
if len(byte) == 8:
text += chr(int(byte, 2))
return text
return ""
# 测试
text = "© 2025 MyCopyright - Confidential"
watermarked_text = embed_text_lsb(img, text)
extracted_text = extract_text_lsb(watermarked_text)
print("提取的文本水印:", extracted_text)
plt.subplot(121), plt.imshow(cv2.cvtColor(img, cv2.COLOR_BGR2RGB)), plt.title('原始'), plt.axis('off')
plt.subplot(122), plt.imshow(cv2.cvtColor(watermarked_text, cv2.COLOR_BGR2RGB)), plt.title('文本水印'), plt.axis('off')
plt.show()
三、鲁棒性优化(结合加密)
LSB 水印易受图像压缩、噪声、裁剪等攻击,可结合之前的异或加密提升鲁棒性:
python
def embed_robust_lsb(img, watermark_path, key=123):
"""带加密的LSB水印"""
# 1. 水印加密(异或)
watermark = cv2.imread(watermark_path, 0)
_, wm_bin = cv2.threshold(watermark, 127, 1, cv2.THRESH_BINARY)
wm_encrypted = (wm_bin ^ (key % 2)) # 简单异或加密(可替换为混沌加密)
# 2. 嵌入加密后的水印
h, w = img.shape[:2]
wm_h, wm_w = wm_encrypted.shape
img_gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) if len(img.shape) == 3 else img.copy()
for i in range(wm_h):
for j in range(wm_w):
img_gray[i, j] = (img_gray[i, j] & ~1) | wm_encrypted[i, j]
# 3. 转回彩色图
if len(img.shape) == 3:
img_color = img.copy()
img_color[:, :, 0] = img_gray
return img_color
return img_gray
def extract_robust_lsb(watermarked_img, wm_size, key=123):
"""提取带加密的LSB水印"""
wm_h, wm_w = wm_size
img_gray = watermarked_img[:, :, 0] if len(watermarked_img.shape) == 3 else watermarked_img
wm_extracted = np.zeros((wm_h, wm_w), dtype=np.uint8)
# 1. 提取水印
for i in range(wm_h):
for j in range(wm_w):
wm_extracted[i, j] = img_gray[i, j] & 1
# 2. 水印解密
wm_decrypted = (wm_extracted ^ (key % 2)) * 255
return wm_decrypted
四、注意事项
- 水印尺寸:不可见水印尺寸不宜过大,否则会影响原始图像质量;
- 鲁棒性:LSB 水印对图像压缩(如 JPG)、噪声、裁剪敏感,需结合加密、冗余嵌入等提升鲁棒性;
- 视觉隐蔽性:嵌入深度不宜过深(如仅修改第 0 位,避免修改第 1-2 位),否则会出现视觉失真;
- 通道选择:彩色图建议嵌入到亮度通道(如 YUV 的 Y 通道)或蓝色通道(人眼对蓝色敏感度较低)。
五、总结
数字水印的核心是隐蔽性 和可提取性:
- 可见水印:用于显性版权声明,通过透明度控制平衡视觉效果;
- 不可见水印:用于隐性版权验证,LSB 算法简单高效,适合轻量级场景;
- 鲁棒性优化:结合加密、冗余嵌入、变换域(如 DCT、DWT)算法,可提升水印抗攻击能力。
根据实际需求选择合适的水印方案,例如:LOGO 水印用于图片版权标注,文本水印用于文档追踪,LSB 水印用于隐蔽版权验证。