一、为什么我们要聊这个东西
做图像修复的时候,经常遇到一种情况:照片上有个污点或者破损,我们想从别的地方复制一块看起来差不多的纹理,贴上去把坏掉的部分盖住。直接用电脑自带的画图工具粘贴,你会看到明显的边界,颜色也不对,就像补丁一样难看。于是我们想到,把粘贴过来的区域边缘做一点模糊,让它和周围融在一起,效果会不会好点?这个思路听起来简单,但实际用Pillow库去实现的时候,你会踩到不少坑。今天就把这些坑一个一个扒出来,用最糙的话讲清楚,顺便带上能跑通的代码,保证不管你是刚摸Python的小白还是写过几年代码的老鸟,都能看明白。
顺便说一下,我这里用的技术栈是 Python + Pillow。Pillow是Python里最常用的图像处理库,没有之一,它自带了图片裁剪、粘贴、滤波等各种基本操作,完全够我们玩了。
二、做纹理填充的基础操作
2.1 粘贴(paste)姿势要对
Pillow里复制一块区域贴到另一块区域,最直接的方法就是打开两张图,或者在同一张图上操作。比如,你有一张照片img,想从坐标(100,100)到(200,200)这块区域复制下来,然后粘贴到(300,300)的位置,代码是这样写的:
from PIL import Image
# 打开图片
img = Image.open("photo.jpg")
# 复制一块区域
region = img.crop((100, 100, 200, 200))
# 粘贴到新位置
img.paste(region, (300, 300))
img.save("output.jpg")
这代码看起来简单,但有个大坑:paste默认是直接把像素盖上去,完全不考虑原本的背景是什么。如果你粘贴的区域背景颜色和新的位置差很多,边缘就会像贴了块狗皮膏药。更糟的是,如果region是带有透明通道的图片,paste还可以指定一个mask参数来混合,但大多数日常图片是没有透明通道的。
2.2 滤波(滤波)是混搭利器
Pillow里的ImageFilter模块提供了很多现成的滤镜,比如高斯模糊(GaussianBlur)、中值滤波(MedianFilter)等。我们想模糊边缘,第一个想到的就是高斯模糊。用法很简单:
from PIL import Image, ImageFilter
img = Image.open("photo.jpg")
# 对整个图做高斯模糊,半径5
blurred = img.filter(ImageFilter.GaussianBlur(radius=5))
blurred.save("blurred.jpg")
但注意,滤波是对整张图生效的,如果我们只想模糊粘贴区域的边缘,就不能简单地对整张图滤波,否则其他清晰的地方也会变糊。所以需要更精细的操作。
三、克隆算法与滤波结合的常见套路
所谓“克隆算法”在这里就是指复制粘贴纹理区域,然后结合滤波让边缘过渡自然。常见的做法是:先复制源区域,粘贴到目标区域,然后对粘贴区域的边界部分做局部模糊,或者对整个粘贴区域做一次模糊后再用蒙版把内部清晰的像素保留下来。
3.1 简单粘贴的灾难现场
假设我们有一张有瑕疵的照片,瑕疵在坐标(x=300, y=300)附近,我们找一个干净的纹理区域(比如左上角的草地)复制过去。直接粘贴后,效果是这样的:粘贴的块颜色和周围明显不同,边缘是一条硬线。
from PIL import Image, ImageFilter
img = Image.open("bad_grass.jpg").convert("RGB")
# 源纹理区域:左上角
source_region = img.crop((50, 50, 150, 150))
# 目标区域:瑕疵处
target_box = (300, 300, 400, 400)
img.paste(source_region, target_box)
# 保存一看,惨不忍睹
img.save("direct_paste.jpg")
3.2 给边缘打个“马赛克式”的过渡
为了消除硬边,一个粗糙但有效的方法是:对粘贴区域做一次高斯模糊,但只保留边缘一定宽度内的模糊结果,内部还是用原始清晰纹理。这就需要创建一个渐变蒙版。我们来写一个完整的示例,让大家直观看到过程。
from PIL import Image, ImageFilter, ImageDraw
def seamless_paste_with_blur(base_img, source_region, target_box, blur_radius=10, edge_width=20):
"""
将 source_region 粘贴到 base_img 的 target_box 位置,
并对边缘进行高斯模糊过渡。
参数:
base_img: PIL Image,目标图片
source_region: PIL Image,源纹理区域
target_box: (x1, y1, x2, y2) 粘贴位置
blur_radius: 高斯模糊半径
edge_width: 边缘过渡宽度(像素)
"""
# 复制原图,避免修改原图
result = base_img.copy()
# 先直接粘贴纹理
result.paste(source_region, target_box)
# 获取目标区域的位置大小
x1, y1, x2, y2 = target_box
w = x2 - x1
h = y2 - y1
# 创建一个渐变蒙版:边缘部分半透明,内部全白
mask = Image.new('L', (w, h), 255) # 白色:完全不透明
draw = ImageDraw.Draw(mask)
# 在边缘画渐变:从边缘到内部edge_width像素,透明度从0到255
# 这里偷懒用矩形框:左上角到右下角,边缘alpha渐变
# 实际上更精确的做法是分别处理四个边,但为了示例易懂,我们用一个简单的渐变框
# 我们创建一个边缘为渐变的矩形,内部255,边缘0(全透明)
# 但是我们要的是边缘模糊后和背景混合,所以蒙版应该让边缘部分透明,内部不透明
# 更直观:先在结果图上模糊整块区域,然后用蒙版把内部清晰的替换回来
# 所以我们要创建内部为不透明,边缘为透明的蒙版,但这样边缘模糊会被透明覆盖?逻辑绕
# 我们换一种更稳定的方式:先模糊整个粘贴区域,然后基于边缘宽度混合
# 这里我们走另一种经典方法:对source_region做个模糊版本,再按距离边缘的权重混合
# 但为了代码简洁,我直接展示最常见的做法:对整个粘贴区模糊,然后用一个内部硬边蒙版替换回清晰纹理
# 这个方法在边缘会产生模糊光环,但也算一种过渡。
# 看下面:
# 对结果图的粘贴区域做高斯模糊
# 先截取粘贴区域,模糊,再贴回去
paste_area = result.crop(target_box)
blurred_paste = paste_area.filter(ImageFilter.GaussianBlur(radius=blur_radius))
# 创建一个内部白色、边缘渐变的蒙版(边缘过渡宽度内从0渐变到255)
edge_mask = Image.new('L', (w, h), 0)
draw = ImageDraw.Draw(edge_mask)
# 内部矩形填充白色(不透明)
inner_x1 = edge_width
inner_y1 = edge_width
inner_x2 = w - edge_width
inner_y2 = h - edge_width
# 确保宽度为正
if inner_x1 < inner_x2 and inner_y1 < inner_y2:
draw.rectangle([inner_x1, inner_y1, inner_x2, inner_y2], fill=255)
# 对边缘蒙版做模糊,使其渐变
edge_mask = edge_mask.filter(ImageFilter.GaussianBlur(radius=edge_width // 2))
# 然后用这个蒙版将模糊后的粘贴区域和原始清晰纹理混合
# 注意:这里原始清晰纹理其实是直接粘贴的那块,但也是清晰的,我们想要内部清晰,外部模糊
# 所以用蒙版:模糊后的图作为背景,清晰纹理作为前景(或者反过来)
# 正确:将模糊后的粘贴区作为结果,然后使用模糊蒙版把内部清晰纹理覆盖上去
# 蒙版为白色处表示使用前景(清晰纹理),黑色处使用背景(模糊)
# 所以我们把模糊后的粘贴区作为base,把原始清晰纹理(即source_region)作为overlay,
# 使用边缘_mask(白色内部)作为前景透明度
# 但Pillow的paste支持mask参数,我们可以直接paste混合
# 把清晰纹理粘贴到模糊后的粘贴区上,使用edge_mask
result.paste(source_region, target_box) # 先放清晰纹理
# 再把模糊后的粘贴区放在上面?不对,应该先清晰后模糊?我们调整顺序:
# 先放模糊版本,再放清晰版本,清晰版本只在内部显示,外部透出模糊
# 正确做法:
result.paste(blurred_paste, target_box) # 先放模糊版本
# 再放清晰纹理,使用mask,mask白色处显示清晰纹理
clarity_mask = edge_mask # 白色表示显示清晰纹理
# 注意:paste第二个参数是位置,第三个参数是蒙版
result.paste(source_region, target_box, clarity_mask) # 蒙版决定了清晰纹理的透明度
return result
# 使用示例
img = Image.open("bad_grass.jpg").convert("RGB")
source = img.crop((50, 50, 150, 150))
target = (300, 300, 400, 400)
result = seamless_paste_with_blur(img, source, target, blur_radius=8, edge_width=15)
result.save("seamless_paste.jpg")
上面这个代码看起来有点绕,但核心思想就是:先对整个粘贴区域做模糊,再通过一个从边缘到内部渐变的蒙版,把内部的清晰纹理覆盖回去。这样边缘就是模糊的,内部清晰,过渡就自然了。
四、那些让人头大的坑点
4.1 颜色和亮度完全不匹配
你费了半天劲把纹理复制过去,也做了模糊过渡,但一眼看上去,粘贴的那块颜色和周围还是不一样,就好像从不同光线环境下的照片里截了一块贴过来。这是最常遇到的问题。原因很简单:源区域和目标区域虽然在纹理图案上相似,但受光照、色调、饱和度影响,颜色分布不一样。比如源草地偏黄,目标草地偏绿,贴上去就算边缘糊了,色差也还在。
解决这个问题的常用手段是直方图匹配或颜色迁移,但是Pillow没有现成函数,得自己算。对于轻度色差,可以简单地调整源区域的亮度和对比度,或者用滤镜里的Colorize。但最暴力的方法是:在做粘贴之前,先对源区域做一次平均颜色调整——算出源区域和目标区域的平均颜色差值,然后对源区域的每个像素加上这个差值。不过要注意,如果纹理复杂,加常数会导致对比度丢失,看起来像塑料。
4.2 边缘融合要么太假要么太脏
上面那个模糊过渡方法里,blur_radius和edge_width这两个参数非常敏感。调大一点,边缘是模糊掉了,但粘贴块的轮廓变成了一圈光晕,显得很假;调小一点,硬边又出来了。更糟的是如果纹理本身有高频细节(比如草叶的纹理),模糊过后边缘区域变成一团浆糊,和背景的清晰纹理对比明显。
一个改进做法是只对边缘几个像素做中值滤波,而不是高斯模糊。中值滤波在去除噪点的同时能保留边缘,但缺点是速度慢。另外可以用双边滤波(Bilateral Filter),但Pillow没有直接提供,需要自己实现或装opencv。
4.3 纹理重复感(类似墙纸效果)
当你从源区域复制一块纹理时,如果源区域里的纹理特征非常明显(比如一块有花纹的布料),粘贴后由于区域形状和边界限制,花纹的布置会显得和周围不延续,形成“拼接感”。即使边缘过渡做好了,内部的纹理排列也会让人觉得这块是“克隆”上去的。这就是为什么会有人用仿制图章工具时,要不断切换采样点。
在代码层面,我们可以通过多次采样、随机旋转或者混合多个源区域来减轻重复感。但Pillow对这些操作的支持有限,需要程序员自己设计算法,比如把源区域分割成小块,随机打乱后再拼贴。
4.4 滤波半径选得不对就翻车
高斯模糊的半径如果太大,不光边缘糊了,内部纹理也糊了,整个粘贴块变成一块色斑。如果太小,边缘根本盖不住贴痕。我试过用一个简单测试:在一个纯色背景上贴一个纹理,调半径,你会发现最佳半径是边缘宽度的一半左右,而且和图像分辨率有关。对于1000x1000的图,边缘宽度设20,半径设10效果还行,但对于200x200的小图,同样参数就会把整个块都模糊掉。所以根本还是要根据图片尺寸和目标区域大小来动态计算。
一个实用的经验公式:边缘宽度 = min(width, height) * 0.05,模糊半径 = 边缘宽度 * 0.3。你可以把这套公式写进函数里,省得每次手动调。
4.5 性能?慢得让人崩溃
上面那个带蒙版的混合方法,涉及到多次crop、filter、paste,如果图片很大,或者一次要修复几十个点,生成速度会慢到让人想砸键盘。特别是filter操作,高斯模糊虽然快,但配合蒙版反复paste,Python的循环开销不可忽视。更不要提如果用Python自己写双边滤波,一张图可能卡十几秒。
优化思路:尽量用Image.filter的内置函数,少用自定义循环;如果可能,用numpy先把图片转为数组,用向量化操作计算蒙版和混合,比Pillow的像素级操作快两个数量级。但这就要求引入numpy,技术栈变成了Pillow+numpy,也不算偏离主题,因为Pillow本身可以和numpy互转。
五、这东西到底能用在哪,有什么好处和坏处
应用场景:最常见的是照片修复——比如去除旧照片上的污渍、折痕、或者去掉水印(不讨论版权问题)。还有一些设计软件里的“内容感知填充”简化版,当你不想要那么复杂的算法时,用这个组合拳也能凑合。另外在游戏贴图制作中,纹理修复也可以用到。
优点:
- 纯Python+Pillow,不需要安装别的库,环境简单。
- 代码逻辑直观,容易理解,适合新手学习和实验。
- 对于大面积平坦纹理(如草地、墙壁、水面)效果不错,基本上可以糊弄人眼。
缺点:
- 对复杂纹理(如毛发、树枝)几乎无效,拼接感非常强。
- 颜色匹配不好时,需要额外做很多处理,不然一眼假。
- 参数敏感,需要针对每张图手动调试,自动化程度低。
- 性能瓶颈,不适用于批量处理大量图片。
六、一些要注意的小事和总结
注意事项:
- 粘贴之前一定要确保源区域和目标区域大小一致,不然Pillow会报错或者自动拉伸,导致失真。
- 使用
paste时,如果不带mask参数,像素是直接覆盖,Alpha通道也会被覆盖,如果图片有透明背景要小心。 - 滤波时注意边界处理:模糊操作默认会补边,但有时会导致粘贴块外面一圈也受影响,可以先用
crop截取再模糊再粘贴。 - 尽量把图片转换成RGB模式再操作,避免RGBA的透明通道干扰。
- 对于非常精细的修复,别指望这个简单方法,老老实实学OpenCV的
seamlessClone或者用专业软件。
结尾总结:Pillow的粘贴加滤波做纹理填充,就像用锤子螺丝刀改装修图,够用但不多。它能帮你应对七八十分的要求,但想做到专业级效果,还得研究更高级的算法。了解这些坑,不是为了否定它,而是为了在需要时能用得更顺手、少踩雷。
Comments