前两天翻相册的时候我特别烦——好好一张照片,垃圾桶、路人、电线杆全在里面,想发朋友圈都得先修半天。以前我得打开修图软件,用内容识别填充一点一点蹭;现在不一样了,我把照片拖进自己搭的小工具里,用鼠标刷一扫,几秒钟就把碍眼的东西抹干净,背景还自动补得跟原图一样。整个过程从想法到跑起来,我只花了大概三分钟。
这个“AI魔法橡皮擦”本质上是一个本地搭建的智能图像去除工具,核心用的是一种叫 LaMa 的图像修复模型。它不需要联网、不消耗在线次数、也不吃高配显卡,普通电脑就能跑。这篇文章我把完整的搭建思路、代码、踩坑点全写出来,照着操作,你也可以在三分钟左右拥有一个能抹掉杂物、水印、电线、行人的本地AI橡皮擦。
1. 项目拆解:这个“魔法橡皮擦”到底在做什么
1.1 一句话讲透图像修复原理
很多人一听到AI修图就想到重绘、生成,好像特别玄乎。其实“橡皮擦”背后的技术叫图像修复(Image Inpainting),逻辑非常简单:你把照片里想去掉的东西涂成一个区域,在AI眼里这个区域就是“缺失部分”,它的工作就是用周围的环境线索去猜测缺失的部分应该长什么样,再填进去。
这跟你小时候玩填色游戏差不多——画纸破了个洞,你看着旁边的天空是蓝的、草地是绿的,就能把洞补成一样的颜色。AI做的是同一件事,只不过它能理解纹理走向、透视关系、光影方向这些更复杂的线索。比如你要抹掉一面墙上的空调外机,它能根据墙面砖缝的走向,把那些砖纹原样接上,而不是填一块简单的模糊色块。
这里的“魔法”不在渲染引擎,而在那个用来做判断的模型。模型看过海量真实照片,知道“墙面通常是什么样子”“地面砖缝常见是横的还是竖的”,所以它能输出异常自然的修复结果。
1.2 为什么选 LaMa 而不是扩散模型
当时我对比过好几类方案,最后选了 LaMa,不是因为它最贵,而是因为它在“大面积涂抹+普通硬件+快速出图”这三个维度上平衡得最好。
扩散模型(Diffusion Model)这类方案效果确实惊艳,尤其是针对复杂物体,比如被物体遮挡的完整人体、墙面背后的复杂结构,都能脑补得非常合理。但它的硬伤也很明显:推理速度慢,一张图少说也要十几秒,吃显存也凶,手里只有4G显存的卡很容易爆掉。而且部署环境复杂,装依赖都够喝一壶的。
LaMa 全称是 Large Mask Inpainting,是专门为“大块涂改”设计的模型。它的拿手好戏正是把大范围遮挡的区域修复得纹理连续。我实测下来,一张 1080p 的照片跑一次只要两三秒,4G显存就能轻松运行,就算纯CPU跑,也就十几秒出图。对“快速修一张照片”这种需求来说,这个节奏太舒服了。
1.3 这套方案能用在哪、不适合用在哪
先说实话,它不是万能的。我把它的能力边界摸了一遍:
能很好处理的对象:
- 照片里的路人、垃圾桶、路牌、电线杆这类“多出来的物体”
- 水印、日期戳、Logo 和文字
- 墙上的污渍、地面上的杂物
- 风景照里飞进画面的鸟类或无人机
处理效果一般或者不适合的对象:
- 被整个物体挡住的人物肢体,比如一个人站在另一个人前面,后面的部分完全不可见,填出来是“脑补”痕迹
- 有明显几何结构的场景,比如人脸、车牌、复杂建筑线条,修复后可能出现轻微的纹理错位
- 想“无中生有”造内容,比如让照片里多一只狗——这不是它的活
我的用法总结是:要抹掉“干扰项”用它,要创造“新内容”得换扩散模型。搞清楚这个区别,你就不会拿它乱来了。
2. 三分钟上手的环境准备
2.1 Python 虚拟环境与依赖清单
先说结论:整个依赖只有几个包,不需要搞什么重型框架。我当时用 Python 3.10 的虚拟环境,实测下来最稳。为什么不推荐 Python 3.11?主要是 PyTorch 在某些低版本 CUDA 环境下编译出来的 wheel 对 3.11 支持有坑,为省事直接用 3.10。
创建虚拟环境的命令:
python3.10 -m venv venv source venv/bin/activate然后是依赖清单,直接用 pip 装:
pip install torch torchvision --index-url https://download.pytorch.org/whl/cu118 pip install gradio==4.44.1 opencv-python pillow numpy albumentations hydra-core pytorch-lightning这里有几个版本相关的细节值得记住:
| 包名 | 版本建议 | 用途 |
|---|---|---|
| gradio | 4.44.1 或同批次 4.x | 提供绘图涂抹界面,低于 4.o 的 API 差别很大 |
| torch/torchvision | 2.x + 对应 CUDA | 模型推理的主力框架 |
| opencv-python | 最新即可 | 图像读写、mask 膨胀、后期融合 |
| albumentations | 1.3 左右 | LaMa 数据预处理时用的增强工具,推理时也会调用 |
| hydra-core | 1.3 左右 | 读取模型配置文件,路径写错会直接白屏 |
注意:Gradio 的版本别乱升级。我吃过亏,到 5.x 之后 ImageEditor 组件的返回结构变了,代码里取 mask 的方式得跟着改,不如直接锁在 4.44.1,省心。
2.2 模型权重下载与目录摆放
LaMa 官方没有给你现成的推理脚本,但社区里有不少打包好的权重文件,搜索“big-lama checkpoint”或者“LaMa Pytorch 权重”就能找到。我用的是一套包含两个模型的权重组合:
- Big LaMa 512:主模型,处理粗略的修复填充
- LaMa Refine:精修模型,把填充区域的边缘和纹理磨得更自然
下载之后,目录结构我建议这样摆,跟脚本读取逻辑对应:
lama_eraser/ ├── venv/ ├── app.py └── models/ ├── big-lama/ │ ├── config.yaml │ └── big-lama-regular.pt └── lama_refine/ ├── config.yaml └── refine.pt权重文件的文件名和 config.yaml 里的读取路径是严格对应的,别乱改名。我一开始图省事把 refine.pt 改成了 model.pt,结果 hydra 加载配置时直接给我抛 KeyError,折腾了十分钟才发现是目录和配置不匹配。
2.3 没有独显也能跑,CPU 推理的取舍
这套东西对硬件的要求比想象中低。我用纯 CPU 的 MacBook Air 实测过,一张 1200 像素见方的图,推理一次大约 12 秒;换成 N 卡,哪怕是 1650 这种老卡,也能控制在 3 秒内。
如果你只有 CPU,也不影响体验,只是点完“运行”要稍微等一会儿。代码里设备选择这样写:
device = "cuda" if torch.cuda.is_available() else "cpu"但注意,纯 CPU 跑的时候建议把输入图的长边缩到 1024 以内,不然预处理和推理加起来可能要两分钟,体验很差。
3. 核心推理代码与橡皮擦界面实现
3.1 模型加载与预处理脚本
直接上代码。下面的脚本是我自己在用的版本,注释都写清楚了,复制后改一下模型路径就能跑通。
import os import cv2 import torch import numpy as np from PIL import Image import gradio as gr import torch.nn.functional as F # 模型路径按上面的目录结构调整 BASE_DIR = os.path.dirname(os.path.abspath(__file__)) DEVICE = "cuda" if torch.cuda.is_available() else "cpu" def load_model(): """加载 LaMa 主模型和 Refine 精修模型""" big_lama = torch.jit.load( os.path.join(BASE_DIR, "models/big-lama/big-lama-regular.pt"), map_location=DEVICE, ).eval() refine = torch.jit.load( os.path.join(BASE_DIR, "models/lama_refine/refine.pt"), map_location=DEVICE, ).eval() return big_lama, refine BIG_LAMA, REFINE = load_model() def preprocess(image: np.ndarray, mask: np.ndarray, max_side: int = 1024): """把原图和 mask 统一缩放到模型输入尺寸""" h, w = image.shape[:2] scale = max_side / max(h, w) if scale < 1.0: new_w, new_h = int(w * scale), int(h * scale) image = cv2.resize(image, (new_w, new_h), interpolation=cv2.INTER_LINEAR) mask = cv2.resize(mask, (new_w, new_h), interpolation=cv2.INTER_NEAREST) # LaMa 要求输入长宽是 32 的倍数,不足的用边缘填充 pad_h = (32 - image.shape[0] % 32) % 32 pad_w = (32 - image.shape[1] % 32) % 32 image = cv2.copyMakeBorder(image, 0, pad_h, 0, pad_w, cv2.BORDER_REFLECT_101) mask = cv2.copyMakeBorder(mask, 0, pad_h, 0, pad_w, cv2.BORDER_CONSTANT, value=0) # 归一化到 0~1 并转成 Tensor image_t = torch.from_numpy(image.astype(np.float32) / 255.0).permute(2, 0, 1).unsqueeze(0) mask_t = torch.from_numpy(mask.astype(np.float32) / 255.0).unsqueeze(0).unsqueeze(0) return image_t.to(DEVICE), mask_t.to(DEVICE), pad_h, pad_w, scale这段代码里有几个关键点我特别说一下:
- mask 的 resize 插值必须用
INTER_NEAREST。如果用线性插值,边缘会出现半透明的灰色过渡区域,模型会把这种模糊区域当成“半遮挡”,修复结果边缘会很脏。 - 填充用
BORDER_REFLECT_101,这是 OpenCV 的反射填充模式,对边缘纹理的延续很有帮助。 - 设备统一放在
DEVICE变量里,换机器只需要改这一处。
3.2 Gradio 交互:怎么把画笔变成遮罩
界面我用的是 Gradio 的ImageEditor组件,它自带画笔分层功能。你可以理解为一张纸上面盖了一层透明膜,你在膜上涂红色的地方就是要去除的区域,底层原图完全不动。
核心交互代码:
def erase_objects(image: dict, mask: np.ndarray): if image is None: return None if mask is None or np.max(mask) < 1: return image # Gradio ImageEditor 返回的 image 是包含背景层的 dict,通常取 "background" if isinstance(image, dict): image = image["background"] if isinstance(image, dict): image = image["composite"] orig = image.copy() # 把 mask 统一成 0/255,二值化 mask = (mask > 10).astype(np.uint8) * 255 # 形态学膨胀,让涂抹区域稍微扩一圈,防止修复边缘出现残留 kernel = np.ones((5, 5), np.uint8) mask = cv2.dilate(mask, kernel, iterations=2) image_t, mask_t, pad_h, pad_w, scale = preprocess(image, mask) with torch.no_grad(): # 先用大模型粗修 coarse = BIG_LAMA(image_t, mask_t) # 这里注意输入给 refine 的是完整图+mask+初步结果 refined = REFINE(image_t, mask_t, coarse)[0] result = refined.squeeze(0).permute(1, 2, 0).cpu().numpy() result = (result * 255).clip(0, 255).astype(np.uint8) # 去掉预留的 padding,还原到缩放后的尺寸 if pad_h > 0: result = result[:-pad_h, :] if pad_w > 0: result = result[:, :-pad_w] # 缩放回原图尺寸 if scale < 1.0: result = cv2.resize(result, (orig.shape[1], orig.shape[0])) # 用 mask 做融合,保留原图非涂抹区域 mask_resized = cv2.resize(mask, (orig.shape[1], orig.shape[0]), interpolation=cv2.INTER_NEAREST) mask_3 = (mask_resized / 255.0)[:, :, None] final = (orig * (1 - mask_3) + result * mask_3).astype(np.uint8) return final这里要特别讲清楚一个细节:为什么要做“融合”这一步而不是直接用整张修复图?
因为 LaMa 是把整张图都跑了一遍,理论上它会全图重绘,但只保证遮罩区域修改得最多。为了让未涂抹区域的原图像素完全不变,我就把“原图”和“模型输出”按 mask 权重做叠加。不属于涂抹区域的地方,权重为0,输出的就是原图;涂抹区域内,权重为1,输出的就是修复后的结果。这个操作可以让你对比前后图时,除了目标区域外,其他地方零变化。
Gradio 的界面部分很简单:
with gr.Blocks() as demo: gr.HTML("本地 AI 橡皮擦") with gr.Row(): with gr.Column(): img_in = gr.ImageEditor(type="numpy", layers=True, label="拖入图片,然后用画笔涂抹要去除的区域") run_btn = gr.Button("开始修复", variant="primary") with gr.Column(): img_out = gr.Image(type="numpy", label="修复结果") run_btn.click(erase_objects, inputs=[img_in, img_in], outputs=img_out) demo.launch(inbrowser=True)注意到我把同一个img_in同时传给了erase_objects的两个参数:一个是图像层,一个是蒙版层。Gradio 的 ImageEditor 组件返回时,会同时给出绘制层的数据,这样正好映射到代码里的image和mask。
3.3 后处理细节决定成败
后处理这部分最容易被忽略,但它是最终出图质量的分水岭。我前几次跑出来的结果总有几个问题:涂抹边缘有一条白边、整张图颜色偏灰、未涂抹区域居然也被改动。后来逐个排查,发现都是后处理姿势不对。
先说白边问题。模型输出的结果,在 mask 边缘往往有一圈过渡区域,如果不处理,放大看就像贴了个补丁。解决办法有两个:
- 用膨胀操作把 mask 稍微扩大,让模型多修一点边缘地带;
- 最后融合时,把 mask 做一次高斯模糊,让过渡更柔和。
我上文的代码里cv2.dilate就是干这个的。膨胀两轮差不多就够了,多了会把不该修的区域也修进去,少了边缘还是会露馅。这个参数其实是个经验值,不同图可以微调。
再说整图偏色。如果直接拿模型输出当最终结果,偶尔会发现整张图饱和度低了一点。原因在于模型训练时对输入做了归一化,输出时虽然从 0~1 还原回 0~255,但色域映射跟原图 SOP 不一定完全一致。所以我在最后融合前,用原图作为“底色”,确保大范围背景颜色保留原图的风格。
最后说未涂抹区域被改动。这个问题多半是 mask 没有正确二值化导致的。如果 mask 里有浅灰色像素,模型的输入会被错误地理解成“半修复区域”,输出就会连带周边一起变。二值化和膨胀这两步,一个都不能省。
3.4 顺手加一个批量处理的隐藏功能
只有单张处理的界面,用了几次之后就不满足了。尤其是处理一整批带水印的素材图时,一张张拖进去点按钮太磨人。我后来顺手加了批量模式,直接指定文件夹,把固定区域(比如角落Logo)自动生成 mask 批量跑。
逻辑其实不复杂:同一批图的水印位置基本固定,你只要提供一张示范 mask,然后对整个文件夹循环调用同一个推理函数就行。这个功能不强求,但建议你保留个扩展接口,后续工作量会省很多。
4. 实战效果与踩坑记录
4.1 实测三张图的清理效果
我拿三张典型图片跑了一遍,感受很直观:
第一张是街景照片,画面边缘有一根电线杆和几根拉线。涂抹+修复大概4秒,电线杆消失后,后边的楼体边缘接得很好,肉眼几乎看不出嫁接痕迹。第二张是桌面杂物照,抹掉桌面上的一瓶矿泉水,桌面木纹的走向保持得不错,但瓶子的高光反射处留下了一点淡淡的亮斑,放大到100%才看得出来。第三张是人物照片,我想抹掉背景里一个拍糊的路人,效果可以用“惊了”形容,路人消失后露出的墙面纹理非常统一,毫无违和感。
总体评价:对“多出来的物体”清除,这个工具是实用级的;但如果目标物体后面是高频纹理(树叶、窗帘褶皱),偶尔会出细小的波浪纹,重新涂抹一次通常能修复。
4.2 高频报错排雷清单
我把自己踩过的坑和群友问过的问题整理成了表格:
| 报错现象 | 原因 | 解决办法 |
|---|---|---|
| 启动报错找不到 config.yaml | hydra 默认读取当前工作目录,models 目录不在脚本同级 | 确认目录结构按上文摆放,或在代码里指定绝对路径 |
| torch.cuda.is_available() 返回 False | 安装的是 CPU 版 torch | 重新安装对应 CUDA 版本的 torch,或直接跑 CPU 模式 |
| 推理显存溢出 | 输入图太大,模型尺寸超过显存 | 把 max_side 调到 768 或 512,同时关掉其他占显存程序 |
| Gradio 界面画笔画不上 | ImageEditor 组件版本不匹配 | 锁定 gradio==4.44.1 重新安装 |
| 修复后涂抹区域一片黑 | 归一化时忘记把图像转成 float | 检查astype(np.float32) / 255.0这一步是否漏掉 |
| 边缘有明显的方块感 | resize mask 时用了线性插值 | 改成INTER_NEAREST并做二值化 |
4.3 遇到抹不干净时怎么办
这是最常被问到的问题:“我涂了,也跑了,但东西还在边缘冒头,怎么回事?”
我的经验是先排查两件事。第一,mask 是不是完全覆盖了目标物体?很多时候你看着屏幕觉得涂满了,实际上目标物体的高光阴影部分超出了画笔范围,AI 不知道这里也要修。第二,目标物体是不是跟背景颜色太像?比如白色栏杆在浅灰墙前面,模型很难判断“栏杆”和“墙”的边界,修复结果就会选择把栏杆当成墙的一部分保留下来。
解决办法是:把涂抹区域画得比目标物体大一圈,或者分两次修。第一次涂大范围,把整个干扰区域粗修掉;第二次只涂第一次留下的瑕疵,精修。这不是模型能力问题,而是使用手法。任何橡皮擦工具,哪怕是手工PS,也会遵循这个逻辑。
4.4 为什么不建议直接用在线工具
我自己最开始也是用在线工具,点两下就把图删了,确实省心。但用多了就发现两个问题让人难受:一是免费次数有限制,用几次就要充值或者等广告;二是有隐私顾虑,修图必须先上传原图,有时候修的是身份证扫描件、内部资料照片,传上去总觉得心里不踏实。
本地部署的好处是一次投入、永久免费,而且所有图像都只在你的电脑里跑。3分钟换来的这个“AI橡皮擦”,本质上是把主动权拿回自己手上。以后不管是修朋友圈照片,还是批量处理素材,想到就能开,不需要排队,也不需要担心时效问题。
我个人在实际操作中的体会是:这套东西最值钱的不是“省了几十块钱”,而是那种“想怎么改就怎么改”的自由度。有一次我修一张活动合影,全场几十号人,背景板被拍进去一个灭火器,这个项目里没法AI生成新的背景,但用这橡皮擦抹掉灭火器不过几秒钟,整张照片立刻干净了。这种小需求,以前要么求人,要么开PS慢慢磨,现在三分钟搭好的工具随叫随到。
最后再多说一个小技巧,你可以把preprocess里的max_side改成 512,修图速度还能再快一截,适合大批量清稿时用;如果是要放大打印或仔细修细节的照片,建议保持 1024。这个参数就是精度和速度之间的天平,按需调就好。