使用 Python 批量添加图片水印
本文所给出的代码适用于为图片添加简单水印,实质上是通过程序实现两张图片的叠加,从而达到水印的效果。
文章前半段主要是介绍了几种 python
中图像合成的方式,如果需要代码,请直接查看文章后半部分。
Python 中关于图像合并的方式
这里主要借助了 PLI(Pillow) 库中的几个方法。合并图像的方式常用的有四种:
Image.paste()
Image.blend()
Image.composite()
Image.alpha_composite()
每种方法的特点各不相同,本文将简单介绍以上四种方法,最终的实现使用了 Image.paste()
进行处理。
这里可以参考:
Image.alpha_composite()——实现一张背景透明图像和一张背景不透明图像的合成
在正式了解这几个方法之前还有几个基础概念需要简单介绍一下:
Basic Concept
-
Image Mode
在 PIL 中对于图像有不同的表示模式,如常用的RGB
RGBA
CMYK
,不同图像模式操作上会有所不同,也可能会导致意想不到的效果,因此在进行处理的时候需要注意图像模式的问题。
关于 Python PIL 的 Image Mode ,可以参考这篇文章:
【Python】PIL库中图像的mode参数想要改变图像的 Image Mode 可以使用
Image.convert()
对其进行转化 -
Alpha – 透明度
在RGBA
通道中,A
表示了Alpha
,在原有的RGB
的基础上添加了透明度的描述。
在图像文件的格式上,.jpg
是无法表示透明度的,因此需要.png
图像来存储有透明度的图像。 -
Mask – 掩码
很容易想到一个名词 – “子网掩码”,这里图像掩码的作用也是一样,对粘贴的图像与掩码图像进行比较,从而确定保留哪一部分。更加通俗的可以理解为“蒙版”或者“遮罩”。
可以参考文章:对图像处理中掩码的理解官方的解释为:
If a mask is given, this method updates only the regions indicated by the mask. You can use either "1", "L", "LA", "RGBA" or "RGBa" images (if present, the alpha band is used as mask). Where the mask is 255, the given image is copied as is. Where the mask is 0, the current value is preserved. Intermediate values will mix the two images together, including their alpha channels if they have them.
当图像像素值为 255 时,粘贴的图像与其运算原值不变,如果为 0 ,则恒为0,也就是不粘贴该像素;对于有透明度的图像则也会对透明通道进行运算(混合在一起)。
Image.paste()
方法
paste
可以被翻译为“粘贴”,因此也很好理解它的效果逻辑,即图层的覆盖,因此 paste
的顺序是很重要的。
该方法需要通过一个实例对象进行调用。
函数原型
在 PIL 中的函数原型为:def paste(self, im, box=None, mask=None):
,共三个参数(包含两个可选参数)
self | box | mask |
---|---|---|
必选参数 | 可选参数 | 可选参数 |
要粘贴的图像 | 需要粘贴的位置,可以为 2 元组或 4 元组,不填写则默认为(0,0) | 掩码图像,不填写默认全部覆盖 |
对于掩码,可以使用的图像模式有:1
, L
, LA
, RGBA
。
测试示例
这里提供了几个测试:
rightPaste
正确的粘贴图像,即得到我们想要的效果noBox
不添加 Box 参数,默认从左上角进行粘贴noMask
不添加 Mask 参数,不使用遮罩,导致水印透明部分直接覆盖了origin
,在转换为RGB
之后显示为黑色swapOrder
交换粘贴的顺序,先粘贴水印再粘贴原图,完全看不到水印,类似图层覆盖的效果saveWithNoConvert
保存时不更改图像模式,该示例效果并不明显,在某些情况下会导致图片出现异常noConvert
读取图像时不更改图像模式,编译器报错,无法进行处理。
具体效果如下:
测试代码
from PIL import Image
from matplotlib import pyplot as plt
import os
def rightPaste(waterMark,origin):
waterMark = waterMark.convert('RGBA');
origin = origin.convert('RGBA');
new_width = (int)(origin.width/2);
new_height = (int)(waterMark.height*(new_width/waterMark.width));# 等比缩放
wMark = waterMark.resize((new_width,new_height))
# 创建底图
baseImg = Image.new('RGBA',origin.size);
# paste 原图和水印
baseImg.paste(origin,(0,0),origin);
baseImg.paste(wMark,((int)(origin.width/4),(int)((origin.height-wMark.height)/2)),wMark);
baseImg.convert('RGB').save("Blog/paste/paste-rightPaste.png");
def noConvert(waterMark,origin):
new_width = (int)(origin.width/2);
new_height = (int)(waterMark.height*(new_width/waterMark.width));# 等比缩放
wMark = waterMark.resize((new_width,new_height))
# 创建底图
baseImg = Image.new('RGBA',origin.size);
# paste 原图和水印
baseImg.paste(origin,(0,0),origin);
baseImg.paste(wMark,((int)(origin.width/4),(int)((origin.height-wMark.height)/2)),wMark);
baseImg.convert('RGB').save("Blog/paste/paste-noConvert.png");
def noMask(waterMark,origin):
waterMark = waterMark.convert('RGBA');
origin = origin.convert('RGBA');
new_width = (int)(origin.width/2);
new_height = (int)(waterMark.height*(new_width/waterMark.width));# 等比缩放
wMark = waterMark.resize((new_width,new_height))
# 创建底图
baseImg = Image.new('RGBA',origin.size);
# paste 原图和水印
baseImg.paste(origin,(0,0),origin);
baseImg.paste(wMark,((int)(origin.width/4),(int)((origin.height-wMark.height)/2)));
baseImg.convert('RGB').save("Blog/paste/paste-noMask.png");
def noBox(waterMark,origin):
waterMark = waterMark.convert('RGBA');
origin = origin.convert('RGBA');
new_width = (int)(origin.width/2);
new_height = (int)(waterMark.height*(new_width/waterMark.width));# 等比缩放
wMark = waterMark.resize((new_width,new_height))
# 创建底图
baseImg = Image.new('RGBA',origin.size);
# paste 原图和水印
baseImg.paste(origin,(0,0),origin);
baseImg.paste(wMark, box=None, mask=wMark);
baseImg.convert('RGB').save("Blog/paste/paste-noBox.png");
def swapOrder(waterMark,origin):
waterMark = waterMark.convert('RGBA');
origin = origin.convert('RGBA');
new_width = (int)(origin.width/2);
new_height = (int)(waterMark.height*(new_width/waterMark.width));# 等比缩放
wMark = waterMark.resize((new_width,new_height))
# 创建底图
baseImg = Image.new('RGBA',origin.size);
# paste 原图和水印
baseImg.paste(wMark,((int)(origin.width/4),(int)((origin.height-wMark.height)/2)),wMark);
baseImg.paste(origin,(0,0),origin);
baseImg.convert('RGB').save("Blog/paste/paste-swapOrder.png");
def saveWithNoConvert(waterMark,origin):
waterMark = waterMark.convert('RGBA');
origin = origin.convert('RGBA');
new_width = (int)(origin.width/2);
new_height = (int)(waterMark.height*(new_width/waterMark.width));# 等比缩放
wMark = waterMark.resize((new_width,new_height))
# 创建底图
baseImg = Image.new('RGBA',origin.size);
# paste 原图和水印
baseImg.paste(origin,(0,0),origin);
baseImg.paste(wMark,((int)(origin.width/4),(int)((origin.height-wMark.height)/2)),wMark);
baseImg.save("Blog/paste/paste-saveWithNoConvert.png");
def merge():
image = Image.open("Blog/paste/paste-rightPaste.png")
plt.subplot(231),plt.imshow(image,'gray'), plt.title('rightPaste')
plt.axis('off')
image = Image.open("Blog/paste/paste-noBox.png")
plt.subplot(232),plt.imshow(image,'gray'), plt.title('noBox')
plt.axis('off')
image = Image.open("Blog/paste/paste-noMask.png")
plt.subplot(233),plt.imshow(image,'gray'), plt.title('noMask')
plt.axis('off')
image = Image.open("Blog/paste/paste-swapOrder.png")
plt.subplot(234),plt.imshow(image,'gray'), plt.title('swapOrder')
plt.axis('off')
image = Image.open("Blog/paste/paste-saveWithNoConvert.png")
plt.subplot(235),plt.imshow(image,'gray'), plt.title('saveWithNoConvert')
plt.axis('off')
image = Image.open("Blog/paste/paste-noConvert.png")
plt.subplot(236),plt.imshow(image,'gray'), plt.title('noConvert')
plt.axis('off')
plt.savefig("Blog/paste/paste-all.png",dpi = 400)
waterMark = Image.open("Blog/logo.png")
origin = Image.open("Blog/origin3.jpg")
if(os.path.exists("Blog/paste") == False):
os.mkdir("Blog/paste")
rightPaste(waterMark,origin)
noBox(waterMark,origin)
noMask(waterMark,origin)
swapOrder(waterMark,origin)
saveWithNoConvert(waterMark,origin)
# noConvert(waterMark,origin)
"""
因为不转换图像模式程序无法正常运行
这里为了程序正常运行将其注释
并在对应的位置添加了一张报错的截图
"""
merge()
# By SDUT Bulbul
# In 2022.Nov.
Image.blend()
方法
该方法会使用插值生成一张新图片,因此在调用的时候为直接使用 Image.blend()
而不是创建实例对象后通过实例对象调用。
注意!该方法要求两张图片拥有相同的 图像模式 和 尺寸!
函数原型
在 PIL 中的函数原型为:def blend(im1, im2, alpha):
,共三个参数
im1 | im1 | alpha |
---|---|---|
必选参数 | 必选参数 | 必选参数 |
第一个图像 | 第二个图像 | 插值的 Alpha 因子,范围为 0~1的一个浮点数 |
函数的差值规则为:
$$
out = image_1 \times \left( 1.0 – alpha \right) + image_2 \times alpha
$$
测试示例
这里提供了几个测试:
alphaZero
透明度为 0,仅可看到第一张图alphaHalf
透明度为 0.5,两张照片融合显示, 均有 50% 的透明效果alphaOne
透明度为 1,仅可看到第二张图swapImageAlphaZero
交换图片位置, 透明度为 0,与 3 的效果相同swapImageAlphaHalf
交换图片位置, 透明度为 0.5,与 2 的效果相同 (这里选取的 0.5 不具有代表性)swapImageAlphaOne
交换图片位置, 透明度为 1,与 1 的效果相同diffImageMode
两张图片使用不同的图像模式,编译器报错,无法进行处理。diffSize
两张图片使用不同的大小,编译器报错,无法进行处理。
具体效果如下:
测试代码
from PIL import Image
from matplotlib import pyplot as plt
import os
def alphaZero(waterMark, origin):
waterMark = waterMark.resize(origin.size);
newImg = Image.blend(waterMark, origin,0);
newImg.convert('RGB').save("Blog/blend/alphaZero.png")
def alphaHalf(waterMark, origin):
waterMark = waterMark.resize(origin.size);
newImg = Image.blend(waterMark, origin,0.5);
newImg.convert('RGB').save("Blog/blend/alphaHalf.png")
def alphaOne(waterMark, origin):
waterMark = waterMark.resize(origin.size);
newImg = Image.blend(waterMark, origin,1);
newImg.convert('RGB').save("Blog/blend/alphaOne.png")
def swapImageAlphaZero(waterMark, origin):
waterMark = waterMark.resize(origin.size);
newImg = Image.blend(origin,waterMark,0);
newImg.convert('RGB').save("Blog/blend/swapImageAlphaZero.png")
def swapImageAlphaHalf(waterMark, origin):
waterMark = waterMark.resize(origin.size);
newImg = Image.blend(origin,waterMark,0.5);
newImg.convert('RGB').save("Blog/blend/swapImageAlphaHalf.png")
def swapImageAlphaOne(waterMark, origin):
waterMark = waterMark.resize(origin.size);
newImg = Image.blend(origin,waterMark,1);
newImg.convert('RGB').save("Blog/blend/swapImageAlphaOne.png")
def diffImageMode(waterMark, origin):
wMark = waterMark.convert('RGBA')
newOrigin = origin.convert('RGB')
waterMark = waterMark.resize(origin.size);
newImg = Image.blend(wMark,newOrigin,0.5);
newImg.convert('RGB').save("Blog/blend/diffImageMode.png")
def diffSize(waterMark, origin):
newImg = Image.blend(waterMark,origin,0.5);
newImg.convert('RGB').save("Blog/blend/diffSize.png")
def merge():
image = Image.open("Blog/blend/alphaZero.png")
plt.subplot(421),plt.imshow(image,'gray'), plt.title('alphaZero')
plt.axis('off')
image = Image.open("Blog/blend/alphaHalf.png")
plt.subplot(423),plt.imshow(image,'gray'), plt.title('alphaHalf')
plt.axis('off')
image = Image.open("Blog/blend/alphaOne.png")
plt.subplot(425),plt.imshow(image,'gray'), plt.title('alphaOne')
plt.axis('off')
image = Image.open("Blog/blend/diffImageMode.png")
plt.subplot(427),plt.imshow(image,'gray'), plt.title('diffImageMode')
plt.axis('off')
image = Image.open("Blog/blend/swapImageAlphaZero.png")
plt.subplot(422),plt.imshow(image,'gray'), plt.title('swapImageAlphaZero')
plt.axis('off')
image = Image.open("Blog/blend/swapImageAlphaHalf.png")
plt.subplot(424),plt.imshow(image,'gray'), plt.title('swapImageAlphaHalf')
plt.axis('off')
image = Image.open("Blog/blend/swapImageAlphaOne.png")
plt.subplot(426),plt.imshow(image,'gray'), plt.title('swapImageAlphaOne')
plt.axis('off')
image = Image.open("Blog/blend/diffSize.png")
plt.subplot(428),plt.imshow(image,'gray'), plt.title('diffSize')
plt.axis('off')
plt.tight_layout(pad=0.4, w_pad=1, h_pad=1) # 调整绘图布局
plt.savefig("Blog/blend/blend-all.png",dpi = 400)
waterMark = Image.open("Blog/logo.png").convert('RGBA')
origin = Image.open("Blog/origin3.jpg").convert('RGBA')
if(os.path.exists("Blog/blend") == False):
os.mkdir("Blog/blend")
alphaZero(waterMark,origin)
alphaHalf(waterMark,origin)
alphaOne(waterMark,origin)
swapImageAlphaZero(waterMark,origin)
swapImageAlphaHalf(waterMark,origin)
swapImageAlphaOne(waterMark,origin)
# diffImageMode(waterMark,origin)
# diffSize(waterMark,origin)
"""
不更改图像模式和大小会造成程序错误
因此这里将其注释并添加了两张报错图片
"""
merge()
# By SDUT Bulbul
# In 2022.Nov.
Image.composite()
方法
该方法实际上是调用了 paste()
方法,在源码中先对 image2
进行了复制,进行了 paste()
。
但由于需要传入两个图像参数,因此不需要实例对象来调用,直接使用 Image.composite()
即可。
# PIL 库中的部分源码
def composite(image1, image2, mask):
image = image2.copy()
image.paste(image1, None, mask)
return image
函数原型
在 PIL 中的函数原型为:def composite(image1, image2, mask):
,共三个参数
image1 | image2 | mask |
---|---|---|
必选参数 | 必选参数 | 必选参数 |
第一个图像 | 第二个图像 | 图像掩码 |
对于掩码,可以使用的图像模式有:1
, L
, RGBA
。
测试示例
simple
正常调用simpleChangeMask
1 的基础上更改Mask
,这里需要注意Mask
的大小,如果超出了image1
的大小则会报错swapImage
交换了两个图像参数的位置swapImageChangeMask
交换位置之后更改Mask
,这里不需要调整大小是因为此时的image1
对应着origin
,而从origin
和waterMark
中任选一个永远不会出现尺寸比image1
小的情况noMask
编译器报错,无法进行处理。
这里要注意添加的位置,默认是左上角(能否改变位置没有进行研究),不同的图片尺寸以及遮罩种类也会有不同的效果,有的甚至是出现了奇怪的错误。
具体效果如下:
测试代码
from PIL import Image
from matplotlib import pyplot as plt
import os
def simple(waterMark, origin):
newImg = Image.composite(waterMark, origin,waterMark);
newImg.convert('RGB').save("Blog/composite/composite-simple.png")
def simpleChangeMask(waterMark,orign):
wMark = waterMark.resize(origin.size)
# 不调整大小会报错
newImg = Image.composite(wMark,origin,origin);
newImg.convert('RGB').save("Blog/composite/composite-simpleChangeMask.png")
def swapImage(waterMark, origin):
wMark = waterMark.resize(origin.size)
newImg = Image.composite(origin,waterMark,wMark);
newImg.convert('RGB').save("Blog/composite/composite-swapImage.png")
def swapImageChangeMask(waterMark, origin):
newImg = Image.composite(origin, waterMark,origin);
newImg.convert('RGB').save("Blog/composite/composite-swapImageChangeMask.png")
def noMask(waterMark,origin):
newImg = Image.composite(waterMark, origin);
newImg.convert('RGB').save("Blog/composite/composite-noMask.png")
def merge():
image = Image.open("Blog/composite/composite-simple.png")
plt.subplot(231),plt.imshow(image,'gray'), plt.title('simple')
plt.axis('off')
image = Image.open("Blog/composite/composite-waterMarkInSimple.png")
plt.subplot(232),plt.imshow(image,'gray'), plt.title('waterMarkInSimple')
plt.axis('off')
image = Image.open("Blog/composite/composite-simpleChangeMask.png")
plt.subplot(233),plt.imshow(image,'gray'), plt.title('simpleChangeMask')
plt.axis('off')
image = Image.open("Blog/composite/composite-swapImage.png")
plt.subplot(234),plt.imshow(image,'gray'), plt.title('swapImage')
plt.axis('off')
image = Image.open("Blog/composite/composite-swapImageChangeMask.png")
plt.subplot(235),plt.imshow(image,'gray'), plt.title('swapImageChangeMask')
plt.axis('off')
image = Image.open("Blog/composite/composite-noMask.png")
plt.subplot(236),plt.imshow(image,'gray'), plt.title('noMask')
plt.axis('off')
plt.tight_layout(pad=0.4, w_pad=1, h_pad=1) # 调整绘图布局
plt.savefig("Blog/composite/composite-all.png",dpi = 400)
waterMark = Image.open("Blog/logo.png").convert('RGBA')
origin = Image.open("Blog/origin3.jpg").convert('RGBA')
if(os.path.exists("Blog/composite") == False):
os.mkdir("Blog/composite")
simple(waterMark, origin)
simpleChangeMask(waterMark, origin)
swapImage(waterMark, origin)
swapImageChangeMask(waterMark, origin)
noMask(waterMark, origin)
"""
不添加 Mask 属性会报错
因此这里将其注释并添加了一张报错图片
"""
merge()
# By SDUT Bulbul
# In 2022.Nov.
Image.alpha_composite()
方法
该方法只需要两个含有透明度的图像即可完成融合。
由于需要传入两个图像参数,因此不需要实例对象来调用,直接使用 Image.alpha_composite()
即可。
因为图像包含透明度,因此不需要图像掩码就可以直接进行融合。
注意!该方法要求两张图片拥有相同的尺寸!且图像模式必须为 RGBA
函数原型
在 PIL 中的函数原型为:def alpha_composite(im1, im2):
,共三个参数
im1 | im2 |
---|---|
必选参数 | 必选参数 |
第一个图像,必须为RGBA |
第二个图像,必须为RGBA |
测试示例
- simple
正常调用,类似粘贴的效果image2
覆盖了image1
- swapSimple – 改变两张图片的顺序
调换了顺序 - noRGBA
使用非RGBA
图像模式的图片进行合成,编译器报错,无法进行处理。
具体效果如下:
测试代码
from PIL import Image
from matplotlib import pyplot as plt
import os
def simple(waterMark, origin):
newImg = Image.alpha_composite(waterMark, origin);
newImg.convert('RGB').save("Blog/alpha_composite/alpha_composite-simple.png")
def swapSimple(waterMark, origin):
newImg = Image.alpha_composite(origin,waterMark)
newImg.convert('RGB').save("Blog/alpha_composite/alpha_composite-swapSimple.png")
def noRGBA(waterMark, origin):
wMark = waterMark.convert('RGB')
newOrigin = origin.convert('RGB')
newImg = Image.alpha_composite(wMark,newOrigin)
newImg.convert('RGB').save("Blog/alpha_composite/alpha_composite-noRGBA.png")
def merge():
image = Image.open("Blog/alpha_composite/alpha_composite-simple.png")
plt.subplot(131),plt.imshow(image,'gray'), plt.title('simple')
plt.axis('off')
image = Image.open("Blog/alpha_composite/alpha_composite-swapSimple.png")
plt.subplot(132),plt.imshow(image,'gray'), plt.title('swapSimple')
plt.axis('off')
image = Image.open("Blog/alpha_composite/alpha_composite-noRGBA.png")
plt.subplot(133),plt.imshow(image,'gray'), plt.title('noRGBA')
plt.axis('off')
plt.tight_layout(pad=0.4, w_pad=1, h_pad=1) # 调整绘图布局
plt.savefig("Blog/alpha_composite/alpha_composite-all.png",dpi = 400)
waterMark = Image.open("Blog/logo.png").convert('RGBA')
origin = Image.open("Blog/origin3.jpg").convert('RGBA')
waterMark = waterMark.resize(origin.size)
if(os.path.exists("Blog/alpha_composite") == False):
os.mkdir("Blog/alpha_composite")
# simple(waterMark,origin)
# swapSimple(waterMark,origin)
# noRGBA(waterMark,origin)
"""
不转换为 RGBA 会报错
因此这里将其注释并添加了一张报错图片
"""
merge()
# By SDUT Bulbul
# In 2022.Nov.
添加水印的解决思路
思路很简单:
- 获取指定路径下的所有图片
- 读取一张原图
- 获取原图的横纵比
- 按照指定的横纵比对水印图片进行缩放
- 创建一个与原图同等大小的底图
- 将原图
paste
到底图上 - 将水印图片
paste
到底图上 - 保存粘贴了两次的底图
- 重复
2-7
步,直到所有图片遍历结束
代码实现
from PIL import Image
import os
# 寻找水印图片,并提示信息
if(os.path.exists("./logo.png") == False):
print("没有找到水印图片,请检查命名是否为‘logo.png’!")
os.system("pause")
os._exit(0);
waterMark = Image.open("./logo.png").convert('RGBA')
# 合并函数,将目标图片与水印结合并且保存为一张新图片
def merge(imgPath,times):
print("第 "+ str(times) +" 张图片,图片名:\t"+ imgPath)
# 打开一张新图片,并且转化为 RGBA 模式
newImg = Image.open("./origin/"+imgPath).convert('RGBA')
# 获取宽高,用于缩放水印
new_width = (int)(newImg.width/10)
new_height = (int)(waterMark.height*(new_width/waterMark.width)) # 等比缩放
# 对水印进行缩放
wMark = waterMark.resize((new_width,new_height))
# 创建底图
baseImg = Image.new('RGBA',newImg.size);
# paste 原图和水印
baseImg.paste(newImg,(0,0),newImg);
baseImg.paste(wMark,(newImg.width - new_width-20,newImg.height-new_height-20),wMark);
# 保存最终图片,需要先转换模式为 RGB
baseImg = baseImg.convert('RGB');
if(os.path.exists("./result/")==False):
os.mkdir("./result/")
baseImg.save("./result/" + imgPath);
imgList= os.listdir('./origin');
print("共找到 " + str(imgList.__len__()) + " 张图片")
cnt = 0 # 记录第几张图片
# 循环遍历每张图片并调用函数处理
for img in imgList:
cnt+=1
merge(img,cnt)
# 提示处理完毕信息
print("处理完毕!")
os.system("pause")
# By SDUT Bulbul
# In 2022.Nov.
本程序的一些解释
本程序的水印缩放是按照原图宽度的 $\frac{1}{10}$ 进行缩放的,同时添加了 20px
,因此在分辨率较小的图片上,水印会有些糊,同时会显得不是那么“靠边”。
程序完成之后还使用了 pyinstaller
将其打包成了 exe
文件,在发给“甲方”的同时做了一个 pdf
的文档,这里放两个关键的截图吧:
关于水印,还可以做什么?
记得早些年,有一则阿里员工抢月饼被开除的新闻,新闻中提到了一种“隐水印”,隐隐约约记得在知乎看到一篇解析,似乎是和快速傅里叶变换(FFT)有关。
最早跟媒体中心的小伙伴交流的时候想到的就是这个,可以做到不影响图片效果,又可以进行防伪处理。
原理是通过 2D-FFT 将图片的时域转换为频域,然后在频域上添加水印,再通过 2D-IFFT(逆变换) 转换为原图。
经过了多次尝试后我也并没有实现预期的效果,因此暂时放弃了这个方向,只能做一个简易的替代品(就是本文的传统水印)。
关于 2D-FFT 制作“隐水印”(或者叫 盲水印),可以了解一下下面的几篇文章:
从傅立叶变换到盲水印(中)——图片盲水印实现
如何给图片加盲水印?盲水印和图片隐写术实现及原理
参考
[1] PRIS-SCMonkey . Image.alpha_composite( )——实现一张背景透明图像和一张背景不透明图像的合成.
[2] cugzyc . Python用image.paste进行图像处理,粘贴原图的裁剪区域,是黑色的一块.
[3] LXYnizhan . 对图像处理中掩码的理解
[4] mjiansun . 【Python】PIL库中图像的mode参数
[5] 温柔则刚 . python实现两个图片的叠加融合.
[6] 澪同学 . 从傅立叶变换到盲水印(中)——图片盲水印实现
[7] 老刘博客 . 如何给图片加盲水印?盲水印和图片隐写术实现及原理