橡皮擦工具)
前兩天翻相冊的時候我特別煩——好好一張照片垃圾桶、路人、電線桿全在里面想發(fā)朋友圈都得先修半天。以前我得打開修圖軟件用內(nèi)容識別填充一點(diǎn)一點(diǎn)蹭現(xiàn)在不一樣了我把照片拖進(jìn)自己搭的小工具里用鼠標(biāo)刷一掃幾秒鐘就把礙眼的東西抹干凈背景還自動補(bǔ)得跟原圖一樣。整個過程從想法到跑起來我只花了大概三分鐘。這個“AI魔法橡皮擦”本質(zhì)上是一個本地搭建的智能圖像去除工具核心用的是一種叫 LaMa 的圖像修復(fù)模型。它不需要聯(lián)網(wǎng)、不消耗在線次數(shù)、也不吃高配顯卡普通電腦就能跑。這篇文章我把完整的搭建思路、代碼、踩坑點(diǎn)全寫出來照著操作你也可以在三分鐘左右擁有一個能抹掉雜物、水印、電線、行人的本地AI橡皮擦。1. 項(xiàng)目拆解這個“魔法橡皮擦”到底在做什么1.1 一句話講透圖像修復(fù)原理很多人一聽到AI修圖就想到重繪、生成好像特別玄乎。其實(shí)“橡皮擦”背后的技術(shù)叫圖像修復(fù)Image Inpainting邏輯非常簡單你把照片里想去掉的東西涂成一個區(qū)域在AI眼里這個區(qū)域就是“缺失部分”它的工作就是用周圍的環(huán)境線索去猜測缺失的部分應(yīng)該長什么樣再填進(jìn)去。這跟你小時候玩填色游戲差不多——畫紙破了個洞你看著旁邊的天空是藍(lán)的、草地是綠的就能把洞補(bǔ)成一樣的顏色。AI做的是同一件事只不過它能理解紋理走向、透視關(guān)系、光影方向這些更復(fù)雜的線索。比如你要抹掉一面墻上的空調(diào)外機(jī)它能根據(jù)墻面磚縫的走向把那些磚紋原樣接上而不是填一塊簡單的模糊色塊。這里的“魔法”不在渲染引擎而在那個用來做判斷的模型。模型看過海量真實(shí)照片知道“墻面通常是什么樣子”“地面磚縫常見是橫的還是豎的”所以它能輸出異常自然的修復(fù)結(jié)果。1.2 為什么選 LaMa 而不是擴(kuò)散模型當(dāng)時我對比過好幾類方案最后選了 LaMa不是因?yàn)樗钯F而是因?yàn)樗凇按竺娣e涂抹普通硬件快速出圖”這三個維度上平衡得最好。擴(kuò)散模型Diffusion Model這類方案效果確實(shí)驚艷尤其是針對復(fù)雜物體比如被物體遮擋的完整人體、墻面背后的復(fù)雜結(jié)構(gòu)都能腦補(bǔ)得非常合理。但它的硬傷也很明顯推理速度慢一張圖少說也要十幾秒吃顯存也兇手里只有4G顯存的卡很容易爆掉。而且部署環(huán)境復(fù)雜裝依賴都夠喝一壺的。LaMa 全稱是 Large Mask Inpainting是專門為“大塊涂改”設(shè)計的模型。它的拿手好戲正是把大范圍遮擋的區(qū)域修復(fù)得紋理連續(xù)。我實(shí)測下來一張 1080p 的照片跑一次只要兩三秒4G顯存就能輕松運(yùn)行就算純CPU跑也就十幾秒出圖。對“快速修一張照片”這種需求來說這個節(jié)奏太舒服了。1.3 這套方案能用在哪、不適合用在哪先說實(shí)話它不是萬能的。我把它的能力邊界摸了一遍能很好處理的對象照片里的路人、垃圾桶、路牌、電線桿這類“多出來的物體”水印、日期戳、Logo 和文字墻上的污漬、地面上的雜物風(fēng)景照里飛進(jìn)畫面的鳥類或無人機(jī)處理效果一般或者不適合的對象被整個物體擋住的人物肢體比如一個人站在另一個人前面后面的部分完全不可見填出來是“腦補(bǔ)”痕跡有明顯幾何結(jié)構(gòu)的場景比如人臉、車牌、復(fù)雜建筑線條修復(fù)后可能出現(xiàn)輕微的紋理錯位想“無中生有”造內(nèi)容比如讓照片里多一只狗——這不是它的活我的用法總結(jié)是要抹掉“干擾項(xiàng)”用它要創(chuàng)造“新內(nèi)容”得換擴(kuò)散模型。搞清楚這個區(qū)別你就不會拿它亂來了。2. 三分鐘上手的環(huán)境準(zhǔn)備2.1 Python 虛擬環(huán)境與依賴清單先說結(jié)論整個依賴只有幾個包不需要搞什么重型框架。我當(dāng)時用 Python 3.10 的虛擬環(huán)境實(shí)測下來最穩(wěn)。為什么不推薦 Python 3.11主要是 PyTorch 在某些低版本 CUDA 環(huán)境下編譯出來的 wheel 對 3.11 支持有坑為省事直接用 3.10。創(chuàng)建虛擬環(huán)境的命令python3.10 -m venv venv source venv/bin/activate然后是依賴清單直接用 pip 裝pip install torch torchvision --index-url https://download.pytorch.org/whl/cu118 pip install gradio4.44.1 opencv-python pillow numpy albumentations hydra-core pytorch-lightning這里有幾個版本相關(guān)的細(xì)節(jié)值得記住包名版本建議用途gradio4.44.1 或同批次 4.x提供繪圖涂抹界面低于 4.o 的 API 差別很大torch/torchvision2.x 對應(yīng) CUDA模型推理的主力框架opencv-python最新即可圖像讀寫、mask 膨脹、后期融合albumentations1.3 左右LaMa 數(shù)據(jù)預(yù)處理時用的增強(qiáng)工具推理時也會調(diào)用hydra-core1.3 左右讀取模型配置文件路徑寫錯會直接白屏注意Gradio 的版本別亂升級。我吃過虧到 5.x 之后 ImageEditor 組件的返回結(jié)構(gòu)變了代碼里取 mask 的方式得跟著改不如直接鎖在 4.44.1省心。2.2 模型權(quán)重下載與目錄擺放LaMa 官方?jīng)]有給你現(xiàn)成的推理腳本但社區(qū)里有不少打包好的權(quán)重文件搜索“big-lama checkpoint”或者“LaMa Pytorch 權(quán)重”就能找到。我用的是一套包含兩個模型的權(quán)重組合Big LaMa 512主模型處理粗略的修復(fù)填充LaMa Refine精修模型把填充區(qū)域的邊緣和紋理磨得更自然下載之后目錄結(jié)構(gòu)我建議這樣擺跟腳本讀取邏輯對應(yīng)lama_eraser/ ├── venv/ ├── app.py └── models/ ├── big-lama/ │ ├── config.yaml │ └── big-lama-regular.pt └── lama_refine/ ├── config.yaml └── refine.pt權(quán)重文件的文件名和 config.yaml 里的讀取路徑是嚴(yán)格對應(yīng)的別亂改名。我一開始圖省事把 refine.pt 改成了 model.pt結(jié)果 hydra 加載配置時直接給我拋 KeyError折騰了十分鐘才發(fā)現(xiàn)是目錄和配置不匹配。2.3 沒有獨(dú)顯也能跑CPU 推理的取舍這套東西對硬件的要求比想象中低。我用純 CPU 的 MacBook Air 實(shí)測過一張 1200 像素見方的圖推理一次大約 12 秒換成 N 卡哪怕是 1650 這種老卡也能控制在 3 秒內(nèi)。如果你只有 CPU也不影響體驗(yàn)只是點(diǎn)完“運(yùn)行”要稍微等一會兒。代碼里設(shè)備選擇這樣寫device cuda if torch.cuda.is_available() else cpu但注意純 CPU 跑的時候建議把輸入圖的長邊縮到 1024 以內(nèi)不然預(yù)處理和推理加起來可能要兩分鐘體驗(yàn)很差。3. 核心推理代碼與橡皮擦界面實(shí)現(xiàn)3.1 模型加載與預(yù)處理腳本直接上代碼。下面的腳本是我自己在用的版本注釋都寫清楚了復(fù)制后改一下模型路徑就能跑通。import os import cv2 import torch import numpy as np from PIL import Image import gradio as gr import torch.nn.functional as F # 模型路徑按上面的目錄結(jié)構(gòu)調(diào)整 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_locationDEVICE, ).eval() refine torch.jit.load( os.path.join(BASE_DIR, models/lama_refine/refine.pt), map_locationDEVICE, ).eval() return big_lama, refine BIG_LAMA, REFINE load_model() def preprocess(image: np.ndarray, mask: np.ndarray, max_side: int 1024): 把原圖和 mask 統(tǒng)一縮放到模型輸入尺寸 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), interpolationcv2.INTER_LINEAR) mask cv2.resize(mask, (new_w, new_h), interpolationcv2.INTER_NEAREST) # LaMa 要求輸入長寬是 32 的倍數(shù)不足的用邊緣填充 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, value0) # 歸一化到 0~1 并轉(zhuǎn)成 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這段代碼里有幾個關(guān)鍵點(diǎn)我特別說一下mask 的 resize 插值必須用INTER_NEAREST。如果用線性插值邊緣會出現(xiàn)半透明的灰色過渡區(qū)域模型會把這種模糊區(qū)域當(dāng)成“半遮擋”修復(fù)結(jié)果邊緣會很臟。填充用BORDER_REFLECT_101這是 OpenCV 的反射填充模式對邊緣紋理的延續(xù)很有幫助。設(shè)備統(tǒng)一放在DEVICE變量里換機(jī)器只需要改這一處。3.2 Gradio 交互怎么把畫筆變成遮罩界面我用的是 Gradio 的ImageEditor組件它自帶畫筆分層功能。你可以理解為一張紙上面蓋了一層透明膜你在膜上涂紅色的地方就是要去除的區(qū)域底層原圖完全不動。核心交互代碼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 統(tǒng)一成 0/255二值化 mask (mask 10).astype(np.uint8) * 255 # 形態(tài)學(xué)膨脹讓涂抹區(qū)域稍微擴(kuò)一圈防止修復(fù)邊緣出現(xiàn)殘留 kernel np.ones((5, 5), np.uint8) mask cv2.dilate(mask, kernel, iterations2) 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初步結(jié)果 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) # 去掉預(yù)留的 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 做融合保留原圖非涂抹區(qū)域 mask_resized cv2.resize(mask, (orig.shape[1], orig.shape[0]), interpolationcv2.INTER_NEAREST) mask_3 (mask_resized / 255.0)[:, :, None] final (orig * (1 - mask_3) result * mask_3).astype(np.uint8) return final這里要特別講清楚一個細(xì)節(jié)為什么要做“融合”這一步而不是直接用整張修復(fù)圖因?yàn)?LaMa 是把整張圖都跑了一遍理論上它會全圖重繪但只保證遮罩區(qū)域修改得最多。為了讓未涂抹區(qū)域的原圖像素完全不變我就把“原圖”和“模型輸出”按 mask 權(quán)重做疊加。不屬于涂抹區(qū)域的地方權(quán)重為0輸出的就是原圖涂抹區(qū)域內(nèi)權(quán)重為1輸出的就是修復(fù)后的結(jié)果。這個操作可以讓你對比前后圖時除了目標(biāo)區(qū)域外其他地方零變化。Gradio 的界面部分很簡單with gr.Blocks() as demo: gr.HTML(本地 AI 橡皮擦) with gr.Row(): with gr.Column(): img_in gr.ImageEditor(typenumpy, layersTrue, label拖入圖片然后用畫筆涂抹要去除的區(qū)域) run_btn gr.Button(開始修復(fù), variantprimary) with gr.Column(): img_out gr.Image(typenumpy, label修復(fù)結(jié)果) run_btn.click(erase_objects, inputs[img_in, img_in], outputsimg_out) demo.launch(inbrowserTrue)注意到我把同一個img_in同時傳給了erase_objects的兩個參數(shù)一個是圖像層一個是蒙版層。Gradio 的 ImageEditor 組件返回時會同時給出繪制層的數(shù)據(jù)這樣正好映射到代碼里的image和mask。3.3 后處理細(xì)節(jié)決定成敗后處理這部分最容易被忽略但它是最終出圖質(zhì)量的分水嶺。我前幾次跑出來的結(jié)果總有幾個問題涂抹邊緣有一條白邊、整張圖顏色偏灰、未涂抹區(qū)域居然也被改動。后來逐個排查發(fā)現(xiàn)都是后處理姿勢不對。先說白邊問題。模型輸出的結(jié)果在 mask 邊緣往往有一圈過渡區(qū)域如果不處理放大看就像貼了個補(bǔ)丁。解決辦法有兩個用膨脹操作把 mask 稍微擴(kuò)大讓模型多修一點(diǎn)邊緣地帶最后融合時把 mask 做一次高斯模糊讓過渡更柔和。我上文的代碼里cv2.dilate就是干這個的。膨脹兩輪差不多就夠了多了會把不該修的區(qū)域也修進(jìn)去少了邊緣還是會露餡。這個參數(shù)其實(shí)是個經(jīng)驗(yàn)值不同圖可以微調(diào)。再說整圖偏色。如果直接拿模型輸出當(dāng)最終結(jié)果偶爾會發(fā)現(xiàn)整張圖飽和度低了一點(diǎn)。原因在于模型訓(xùn)練時對輸入做了歸一化輸出時雖然從 0~1 還原回 0~255但色域映射跟原圖 SOP 不一定完全一致。所以我在最后融合前用原圖作為“底色”確保大范圍背景顏色保留原圖的風(fēng)格。最后說未涂抹區(qū)域被改動。這個問題多半是 mask 沒有正確二值化導(dǎo)致的。如果 mask 里有淺灰色像素模型的輸入會被錯誤地理解成“半修復(fù)區(qū)域”輸出就會連帶周邊一起變。二值化和膨脹這兩步一個都不能省。3.4 順手加一個批量處理的隱藏功能只有單張?zhí)幚淼慕缑嬗昧藥状沃缶筒粷M足了。尤其是處理一整批帶水印的素材圖時一張張拖進(jìn)去點(diǎn)按鈕太磨人。我后來順手加了批量模式直接指定文件夾把固定區(qū)域比如角落Logo自動生成 mask 批量跑。邏輯其實(shí)不復(fù)雜同一批圖的水印位置基本固定你只要提供一張示范 mask然后對整個文件夾循環(huán)調(diào)用同一個推理函數(shù)就行。這個功能不強(qiáng)求但建議你保留個擴(kuò)展接口后續(xù)工作量會省很多。4. 實(shí)戰(zhàn)效果與踩坑記錄4.1 實(shí)測三張圖的清理效果我拿三張典型圖片跑了一遍感受很直觀第一張是街景照片畫面邊緣有一根電線桿和幾根拉線。涂抹修復(fù)大概4秒電線桿消失后后邊的樓體邊緣接得很好肉眼幾乎看不出嫁接痕跡。第二張是桌面雜物照抹掉桌面上的一瓶礦泉水桌面木紋的走向保持得不錯但瓶子的高光反射處留下了一點(diǎn)淡淡的亮斑放大到100%才看得出來。第三張是人物照片我想抹掉背景里一個拍糊的路人效果可以用“驚了”形容路人消失后露出的墻面紋理非常統(tǒng)一毫無違和感??傮w評價對“多出來的物體”清除這個工具是實(shí)用級的但如果目標(biāo)物體后面是高頻紋理樹葉、窗簾褶皺偶爾會出細(xì)小的波浪紋重新涂抹一次通常能修復(fù)。4.2 高頻報錯排雷清單我把自己踩過的坑和群友問過的問題整理成了表格報錯現(xiàn)象原因解決辦法啟動報錯找不到 config.yamlhydra 默認(rèn)讀取當(dāng)前工作目錄models 目錄不在腳本同級確認(rèn)目錄結(jié)構(gòu)按上文擺放或在代碼里指定絕對路徑torch.cuda.is_available() 返回 False安裝的是 CPU 版 torch重新安裝對應(yīng) CUDA 版本的 torch或直接跑 CPU 模式推理顯存溢出輸入圖太大模型尺寸超過顯存把 max_side 調(diào)到 768 或 512同時關(guān)掉其他占顯存程序Gradio 界面畫筆畫不上ImageEditor 組件版本不匹配鎖定 gradio4.44.1 重新安裝修復(fù)后涂抹區(qū)域一片黑歸一化時忘記把圖像轉(zhuǎn)成 float檢查astype(np.float32) / 255.0這一步是否漏掉邊緣有明顯的方塊感resize mask 時用了線性插值改成INTER_NEAREST并做二值化4.3 遇到抹不干凈時怎么辦這是最常被問到的問題“我涂了也跑了但東西還在邊緣冒頭怎么回事”我的經(jīng)驗(yàn)是先排查兩件事。第一mask 是不是完全覆蓋了目標(biāo)物體很多時候你看著屏幕覺得涂滿了實(shí)際上目標(biāo)物體的高光陰影部分超出了畫筆范圍AI 不知道這里也要修。第二目標(biāo)物體是不是跟背景顏色太像比如白色欄桿在淺灰墻前面模型很難判斷“欄桿”和“墻”的邊界修復(fù)結(jié)果就會選擇把欄桿當(dāng)成墻的一部分保留下來。解決辦法是把涂抹區(qū)域畫得比目標(biāo)物體大一圈或者分兩次修。第一次涂大范圍把整個干擾區(qū)域粗修掉第二次只涂第一次留下的瑕疵精修。這不是模型能力問題而是使用手法。任何橡皮擦工具哪怕是手工PS也會遵循這個邏輯。4.4 為什么不建議直接用在線工具我自己最開始也是用在線工具點(diǎn)兩下就把圖刪了確實(shí)省心。但用多了就發(fā)現(xiàn)兩個問題讓人難受一是免費(fèi)次數(shù)有限制用幾次就要充值或者等廣告二是有隱私顧慮修圖必須先上傳原圖有時候修的是身份證掃描件、內(nèi)部資料照片傳上去總覺得心里不踏實(shí)。本地部署的好處是一次投入、永久免費(fèi)而且所有圖像都只在你的電腦里跑。3分鐘換來的這個“AI橡皮擦”本質(zhì)上是把主動權(quán)拿回自己手上。以后不管是修朋友圈照片還是批量處理素材想到就能開不需要排隊(duì)也不需要擔(dān)心時效問題。我個人在實(shí)際操作中的體會是這套東西最值錢的不是“省了幾十塊錢”而是那種“想怎么改就怎么改”的自由度。有一次我修一張活動合影全場幾十號人背景板被拍進(jìn)去一個滅火器這個項(xiàng)目里沒法AI生成新的背景但用這橡皮擦抹掉滅火器不過幾秒鐘整張照片立刻干凈了。這種小需求以前要么求人要么開PS慢慢磨現(xiàn)在三分鐘搭好的工具隨叫隨到。最后再多說一個小技巧你可以把preprocess里的max_side改成 512修圖速度還能再快一截適合大批量清稿時用如果是要放大打印或仔細(xì)修細(xì)節(jié)的照片建議保持 1024。這個參數(shù)就是精度和速度之間的天平按需調(diào)就好。