現(xiàn)自然磨皮:保邊去噪原理與OpenCV實(shí)戰(zhàn))
前兩天有朋友問我為什么有些美顏App磨皮一開臉立馬變成“瓷娃娃”光滑是光滑了可五官線條、發(fā)絲邊緣全都糊成一團(tuán)像戴了層毛玻璃。而有些算法處理過的照片毛孔淡了、皮膚質(zhì)感還在眉眼輪廓依然鋒利連頭發(fā)絲都根根分明。差別在哪答案就是雙邊濾波Bilateral Filter——這個(gè)看似傳統(tǒng)的老算法至今仍然是人臉磨皮、美顏相機(jī)、影視后期皮膚修飾領(lǐng)域最常用的基礎(chǔ)操作之一也是我每年給新人講圖像處理時(shí)必拆的一個(gè)經(jīng)典案例。我是做圖像算法落地的磨皮這塊從最早的表面模糊一路做到深度學(xué)習(xí)美顏但每次回過頭看雙邊濾波始終是那條“不能繞過的路”。即使后面出現(xiàn)了引導(dǎo)濾波、各種保邊平滑網(wǎng)絡(luò)雙邊濾波的“保邊去噪”思路仍然是理解這些進(jìn)階方案的第一級(jí)臺(tái)階。這篇文章就以一個(gè)完整的實(shí)操案例為線索把雙邊濾波做人臉磨皮的原理、參數(shù)、代碼、踩坑全部講透適合正在學(xué)圖像處理的學(xué)生、做美顏算法優(yōu)化的工程師以及任何想搞清楚美顏App磨皮邏輯的愛好者。1. 為什么磨皮首選雙邊濾波先看清難題在哪人臉磨皮這個(gè)任務(wù)本質(zhì)上是兩件事的平衡一是把皮膚上的細(xì)小紋理、毛孔、痘印、噪點(diǎn)抹平也就是“低頻化”二是不能把眼睛、眉毛、嘴唇、發(fā)絲這些高頻細(xì)節(jié)也一起抹掉。前者是去噪后者是保邊而大多數(shù)簡單的濾波算法比如高斯模糊、均值模糊做不了這個(gè)平衡。1.1 保邊和去噪的天然矛盾高斯模糊的思路很直接把中心像素和周圍像素按距離加權(quán)平均距離越近權(quán)重越大距離越遠(yuǎn)權(quán)重越小。問題在于這個(gè)權(quán)重只看“空間距離”完全不看“像素值差異”。于是當(dāng)你在處理一張人臉照片時(shí)高斯模糊會(huì)一視同仁地把皮膚區(qū)域和眉毛邊緣一起模糊眉毛和皮膚的邊界處兩邊顏色被拉平整個(gè)圖像變“肉”。打個(gè)比方高斯模糊就像用一張細(xì)砂紙打磨木板它不管哪是紋理哪是劃痕全都給你磨一遍結(jié)果紋理沒了劃痕也沒了。而磨皮想要的是“只磨掉劃痕保留木紋”這就需要平滑算法具備一個(gè)能力遇到像素值差異大的邊緣時(shí)主動(dòng)“剎車”不參與平滑。1.2 雙邊濾波的數(shù)學(xué)直覺雙邊濾波解決這個(gè)矛盾的方式非常巧妙。它在高斯模糊的基礎(chǔ)上多引入了一個(gè)“灰度權(quán)重”或者說“值域權(quán)重”——不僅看兩個(gè)像素離得有多遠(yuǎn)還要看兩個(gè)像素的值差多少。如果鄰域像素和中心像素的灰度值差異很小就認(rèn)為它屬于同一塊平滑區(qū)域給較大的權(quán)重參與平均如果差異很大就認(rèn)為它屬于邊緣另一側(cè)給極小的權(quán)重基本不參與平均。這樣帶來的直接效果就是在皮膚這種灰度變化平緩的區(qū)域內(nèi)雙邊濾波像普通高斯模糊一樣做平滑而在眼眉、嘴唇、發(fā)絲這些邊緣處梯度大灰度權(quán)重趨近于零邊緣像素被“保護(hù)”下來不會(huì)模糊。用大白話說雙邊濾波學(xué)會(huì)了“看臉色行事”——你是同一張臉皮它就幫你磨你突然跳出一根眉毛來它就不動(dòng)了。在人臉磨皮這個(gè)場景里這個(gè)特性簡直是量身定做的。人臉皮膚區(qū)域的特征就是“大面積、低梯度、細(xì)微噪點(diǎn)多”而五官邊緣的特征是“小區(qū)域、高梯度、結(jié)構(gòu)清晰”。雙邊濾波剛好同時(shí)在兩個(gè)方向上都討好這就是它成為磨皮首選算法的根本原因。2. 雙邊濾波的原理拆解兩個(gè)權(quán)重是怎么協(xié)同工作的很多教程直接甩出雙邊濾波公式讀者一看求和符號(hào)就頭皮發(fā)麻。其實(shí)這個(gè)公式?jīng)]那么可怕拆成兩塊看就行。2.1 空間權(quán)重與灰度權(quán)重一個(gè)管距離一個(gè)管差異雙邊濾波的輸出像素值可以理解為g(i, j) 歸一化系數(shù) * 鄰域內(nèi)所有像素的加權(quán)求和其中每個(gè)鄰域像素的權(quán)重由兩部分相乘得到空間權(quán)重Spatial Weight和高斯核一樣衡量鄰域像素與中心像素的歐氏距離距離越遠(yuǎn)權(quán)重越小。它決定了“在多大范圍內(nèi)找平均值”?;叶葯?quán)重Range Weight衡量鄰域像素與中心像素的灰度值差異差異越大權(quán)重越小。它決定了“什么樣的像素能被算作同一伙的”。兩個(gè)權(quán)重通過指數(shù)函數(shù)組合空間部分用sigma_space通常記作σs控制灰度部分用sigma_color通常記作σr控制。公式寫出來長這樣w exp(-0.5 * (d^2 / σs^2)) * exp(-0.5 * (δ^2 / σr^2))其中d是空間距離δ是灰度差。這段公式不背也行關(guān)鍵要理解的是最左邊的空間權(quán)重保證平滑是局部鄰域行為不會(huì)跨出半徑之外最右邊的灰度權(quán)重則像一個(gè)“門衛(wèi)”遇到灰度跳變就放行——不拉入平均。兩者相乘之后邊緣兩側(cè)的像素權(quán)重被壓到極低邊緣自然就保住了。2.2 參數(shù)決定風(fēng)格σs管磨皮范圍σr管磨皮力度實(shí)操中最容易出錯(cuò)的就是搞不清兩個(gè)sigma參數(shù)分別控制什么。我的經(jīng)驗(yàn)是**σs空間sigma**決定濾波窗口的有效半徑。σs越大參與平均的鄰域越大皮膚上的微小噪點(diǎn)被抹得越開磨皮范圍越廣但窗口太大也會(huì)讓五官邊緣變軟甚至產(chǎn)生“塑料感”。在磨皮場景里σs過大是照片變假的最大元兇。**σr灰度sigma**決定灰差容忍度。σr越大灰度差多大的像素都能被拉進(jìn)來平均邊緣保護(hù)能力越弱σr越小只有灰度差很小的像素才參與平滑保護(hù)能力強(qiáng)但磨皮力度也弱。在磨皮場景里σr是控制“保留多少皮膚質(zhì)感”的關(guān)鍵旋鈕。我把這兩個(gè)參數(shù)的關(guān)系理解為“音量”和“音色”。σs是音量決定你調(diào)了多大的范圍σr是音色決定這個(gè)范圍內(nèi)哪些聲音被保留。兩者必須配合調(diào)整只調(diào)大σs會(huì)讓磨皮出現(xiàn)水彩暈染感只調(diào)大σr則會(huì)讓皮膚顯得斑駁噪點(diǎn)沒去干凈。OpenCV里對應(yīng)的調(diào)用是img cv2.imread(portrait.jpg) result cv2.bilateralFilter(img, d9, sigmaColor75, sigmaSpace75)這里的d是鄰域直徑實(shí)際卷積范圍為d*dd9大約就是周圍4個(gè)像素。d、sigmaColor即σr、sigmaSpace即σs三者有聯(lián)動(dòng)d一般取sigmaSpace的2到3倍小于等于0時(shí)OpenCV會(huì)根據(jù)sigmaSpace自動(dòng)計(jì)算。但是注意d越大計(jì)算量呈平方級(jí)上升所以大圖用雙邊濾波時(shí)性能壓力很大這點(diǎn)到第4章再細(xì)說。3. 實(shí)操案例把“宋茜”臉上的瑕疵磨掉而不動(dòng)五官光講理論不夠我拿一個(gè)實(shí)際的案例來走一遍完整流程。這里選了一張宋茜的正面高清宣傳照來做演示——為什么選這張因?yàn)樗奶攸c(diǎn)非常典型皮膚區(qū)域大有輕微毛孔和噪點(diǎn)眼妝、口紅、發(fā)絲邊緣非常清晰。如果用高斯模糊這些邊緣全部會(huì)糊用雙邊濾波正好可以看到算法是怎么“聰明”地繞開邊緣的。3.1 完整的OpenCV實(shí)現(xiàn)流程我習(xí)慣的流程分三步讀圖、參數(shù)預(yù)估、迭代微調(diào)。先看一眼原圖尺寸再預(yù)估sigma值。通常我的經(jīng)驗(yàn)是對于2000px寬的照片sigmaColor取50到80之間sigmaSpace取30到60之間。如果超過4000px的圖sigmaSpace就要適當(dāng)放大到80到100左右不然鄰域相對整圖的占比太小磨皮效果過于微弱。核心代碼差不多是這樣import cv2 import numpy as np def bilateral_skin_smooth(img_path, sigma_color75, sigma_space60): img cv2.imread(img_path) # OpenCV的bilateralFilter要求d為非零奇數(shù)常用9或15 smooth cv2.bilateralFilter(img, d15, sigmaColorsigma_color, sigmaSpacesigma_space) return smooth執(zhí)行這一步之后仔細(xì)觀察輸出皮膚上的細(xì)微噪點(diǎn)基本消失臉色變得勻凈透亮但眉毛的根根線條、上下眼瞼的妝容邊界、口紅的銳利輪廓全部保留。這就是雙邊濾波區(qū)別于高斯模糊的“戰(zhàn)場表現(xiàn)”。不過直接用一次雙邊濾波處理整張臉也有一個(gè)隱藏問題——它會(huì)連臉頰和下巴交界的光影漸變也當(dāng)作“邊緣”稍微壓一下導(dǎo)致臉部輪廓過渡變得略硬。這個(gè)問題在高清大圖上尤明顯后面我通過“低高頻分層處理”來解決。3.2 參數(shù)微調(diào)的真實(shí)經(jīng)驗(yàn)從“磨不動(dòng)”到“磨過頭”我第一次拿宋茜這張圖調(diào)試時(shí)給了個(gè)保守參數(shù)sigmaColor30, sigmaSpace30。輸出結(jié)果幾乎沒什么變化只有湊到100%放大才能看出皮膚噪點(diǎn)稍微淡了一點(diǎn)。這就是“磨皮力度不足”的典型癥狀——看似不犯錯(cuò)實(shí)際上等于沒處理。接著我把sigmaColor加到120sigmaSpace不變這回翻車了皮膚確實(shí)光滑如鏡但嘴角和鼻翼附近的光影被“抹平”成漫畫風(fēng)格下頜線的輪廓也出現(xiàn)輕微“光暈”感像描了一道白邊。原因很簡單sigmaColor太大之后灰度權(quán)重對邊緣的抑制力減弱邊緣兩側(cè)的像素開始互相“污染”這正是邊緣保護(hù)的失效信號(hào)。最后折中的一組參數(shù)是sigmaColor75, sigmaSpace45。輸出效果膚質(zhì)干凈同時(shí)臉型自然沒有塑膠感眼妝和唇線完好多清晰。所以如果你要做磨皮建議從這個(gè)量級(jí)開始調(diào)而不是上來一口吃個(gè)胖子。3.3 為什么這個(gè)效果好從頻域看雙邊濾波換一個(gè)視角來看雙邊濾波在磨皮中的行為會(huì)更有意思。圖像可以粗略拆成低頻信息皮膚底色、光照過渡和高頻信息紋理細(xì)節(jié)、邊緣、噪點(diǎn)。磨皮本質(zhì)上是要壓掉一部分高頻噪聲但又不能把所有高頻都刪除因?yàn)檫吘壱彩歉哳l。雙邊濾波的灰度權(quán)重在這里起到了“頻率選擇性”的作用。在平緩區(qū)域相鄰像素的灰度差小權(quán)重大高頻噪點(diǎn)被抑制在邊緣區(qū)域灰度差大權(quán)重小高頻結(jié)構(gòu)被保留。相當(dāng)于一個(gè)“自適應(yīng)低通濾波器”對平坦區(qū)域做低通對邊緣區(qū)域做全通。理解到這一層后面再去學(xué)引導(dǎo)濾波、表面模糊這些進(jìn)階方案時(shí)你會(huì)發(fā)現(xiàn)自己已經(jīng)站在同一個(gè)認(rèn)知高度上。4. 進(jìn)階優(yōu)化如何做到自然妝感的磨皮效果單次雙邊濾波雖然能保邊但直接在原圖上整臉處理容易丟失皮膚光影過渡的細(xì)膩感。這也是很多美顏App只輕磨兩分就停手的原因——磨皮要的不是“絕對光滑”而是“似磨非磨”的妝感。要做到這一點(diǎn)通常需要把雙邊濾波放進(jìn)一個(gè)更大的方法論里來用。4.1 低高頻分層磨皮只磨低頻細(xì)節(jié)靠疊加這是我在實(shí)際項(xiàng)目中經(jīng)常用的方案思路是把原圖拆成“低頻層”和“高頻層”只對低頻層做平滑再和保留的高頻層合回去。步驟很簡單對原圖做一次雙邊濾波得到低頻層皮膚基本面。用原圖減去低頻層得到高頻層紋理和邊緣細(xì)節(jié)。低頻層按一定比例保留混合系數(shù)比如0.8高頻層乘以一個(gè)小的增益比如0.3到0.5再相加融合。smooth cv2.bilateralFilter(img, 15, 75, 45) high_freq cv2.subtract(img, smooth) # 低頻層削弱高頻層也降一點(diǎn)避免細(xì)節(jié)過銳 out cv2.addWeighted(smooth, 0.85, high_freq, 0.35, 0)這樣一來磨皮力度不變但皮膚紋理不會(huì)完全消失而是呈現(xiàn)出一種“毛孔細(xì)化了但還在”的自然質(zhì)感同時(shí)眼睛睫毛、發(fā)絲邊緣的高頻成分沒有受到任何損傷。磨皮算法行業(yè)里管這種操作叫“高低頻磨皮”或者“頻域占比調(diào)整”本質(zhì)上就是在處理“平滑與細(xì)節(jié)的度”這個(gè)世紀(jì)難題。4.2 膚色掩膜只磨皮膚不磨背景和衣物另一個(gè)實(shí)戰(zhàn)中躲不開的問題是雙邊濾波可沒有內(nèi)置“人臉檢測”它對整張圖一視同仁。于是一張照片里毛衣的絨面紋理、皮包的顆粒表面、地毯的噪聲全都被當(dāng)作“皮膚”磨了一遍。結(jié)果就是畫面整體變“悶”沒有層次感。這時(shí)候就需要引入膚色掩膜Skin Mask。我常用的做法是先把RGB圖像轉(zhuǎn)到Y(jié)CbCr空間然后用Cb和Cr通道的閾值范圍圈出膚色區(qū)域img_ycrcb cv2.cvtColor(img, cv2.COLOR_BGR2YCrCb) skin_mask cv2.inRange(img_ycrcb, (0, 133, 77), (255, 173, 127)) # 做一下形態(tài)學(xué)開閉運(yùn)算填掉鼻孔、眼窩等空洞 skin_mask cv2.morphologyEx(skin_mask, cv2.MORPH_OPEN, np.ones((5,5), np.uint8)) skin_mask cv2.morphologyEx(skin_mask, cv2.MORPH_CLOSE, np.ones((15,15), np.uint8)) # 平滑掩膜邊緣避免后續(xù)融合出現(xiàn)硬邊界 skin_mask cv2.GaussianBlur(skin_mask.astype(np.float32), (31, 31), 15)生成掩膜后用它對平滑圖和原圖做alpha融合這樣磨皮只出現(xiàn)在皮膚區(qū)域衣服、背景、頭發(fā)保持原來的顆粒感畫面整體更“透氣”。這里有一個(gè)關(guān)鍵細(xì)節(jié)掩膜的邊緣不要用二值化的硬邊界一定要用高斯模糊軟化。否則融合以后臉頰和頭發(fā)的交界處會(huì)出現(xiàn)一圈明顯的“裁切線”這是新手最容易忽略的瑕疵。4.3 性能優(yōu)化大圖上的雙邊濾波不能硬扛雙邊濾波的計(jì)算復(fù)雜度是O(σs^2)窗口稍大就非常吃力。一張4000px的圖如果σs取100那每個(gè)像素都要算大約4萬個(gè)鄰域權(quán)重全圖算下來是天文數(shù)字。實(shí)際項(xiàng)目中我一般不會(huì)直接在大圖上跑原始雙邊濾波而是先降采樣處理完再放大也就是“縮小-磨皮-放大”三步small cv2.resize(img, None, fx0.5, fy0.5, interpolationcv2.INTER_AREA) small_smooth cv2.bilateralFilter(small, 9, 75, 45) out cv2.resize(small_smooth, None, fx2.0, fy2.0, interpolationcv2.INTER_LINEAR)這樣處理速度提升4倍以上而且因?yàn)殡p邊濾波本來就是低頻主導(dǎo)的結(jié)果放大后不會(huì)有明顯的畫質(zhì)損失。如果你想追求更快還有兩條路一是用域變換濾波或引導(dǎo)濾波來近似雙邊濾波的效果速度能快一個(gè)數(shù)量級(jí)二是直接用支持實(shí)時(shí)預(yù)覽的膚質(zhì)統(tǒng)一算法做替代比如基于導(dǎo)向圖的快速保邊平滑。在我的實(shí)測里引導(dǎo)濾波Guided Filter在磨皮任務(wù)上幾乎可以做到雙邊濾波95%的視覺質(zhì)量但速度是幾十倍級(jí)別。所以現(xiàn)在很多實(shí)時(shí)美顏App里跑的并不是經(jīng)典雙邊濾波而是雙邊濾波的“思想繼承者”這一點(diǎn)值得留意。5. 實(shí)戰(zhàn)問題速查雙邊濾波磨皮高頻踩坑實(shí)錄再好的算法落到真實(shí)項(xiàng)目里都會(huì)遇到一堆文檔里沒寫的問題。我把這幾年在磨皮調(diào)優(yōu)過程中碰到的典型問題整理成一張速查表每個(gè)問題后面附帶我的排查思路希望對你有直接幫助。5.1 常見問題與解決方案對照現(xiàn)象根因解決方向磨皮后出現(xiàn)“塑膠臉”sigmaColor設(shè)置過大邊緣保護(hù)失效降低sigmaColor到40到80區(qū)間皮膚變光滑但五官邊緣發(fā)灰鄰域直徑d過大邊緣兩側(cè)像素互相滲入d從15降到9或降低sigmaSpace眼睛、嘴巴周圍出現(xiàn)白色光暈高頻層被過度抑制邊緣兩側(cè)亮度差異被拉平改用低高頻分層方案提高高頻層占比處理整圖時(shí)背景紋理也被磨掉沒有做膚色區(qū)域掩膜用YCbCr膚色檢測生成mask只對皮膚區(qū)磨皮大圖上運(yùn)算非常卡頓雙邊濾波復(fù)雜度隨窗口平方增長降采樣處理或改用引導(dǎo)濾波近似磨皮后皮膚像是“紙糊的”毫無質(zhì)感低頻保留比例太高細(xì)節(jié)層丟失增加高頻層回灌比例保留15%到40%紋理磨皮不徹底臉上仍有噪點(diǎn)鄰域太小未覆蓋噪點(diǎn)尺度適當(dāng)增大sigmaSpace或者先做一次輕量降噪5.2 最容易出現(xiàn)的邏輯錯(cuò)誤參數(shù)單位不統(tǒng)一調(diào)試雙邊濾波參數(shù)時(shí)我遇到過不止一次這樣的怪事同一組參數(shù)在一張800px的圖上效果很好換到一張1600px的圖上效果減半。原因在于sigmaSpace這個(gè)參數(shù)的單位是像素但它本質(zhì)上是“相對特征尺寸”的度量。同一張臉的毛孔在800px圖里占2個(gè)像素到1600px圖里占4個(gè)像素。如果不把sigmaSpace也等比放大濾波窗口在物理尺度上就變小了磨皮力度自然不一致。所以做批量處理時(shí)我一般先根據(jù)圖像的短邊尺寸動(dòng)態(tài)計(jì)算sigmaSpacedef adaptive_sigma(width, base_width800, base_sigma45): return int(base_sigma * width / base_width)這個(gè)邏輯看著簡單但在線上環(huán)境里很管用。很多人問為什么自己寫的美顏對不同分辨率的圖效果漂移十有八九就是栽在這里。5.3 邊界問題圖像邊緣的黑框從哪來雙邊濾波在圖像邊緣處也會(huì)“翻車”——處理后的圖像四周有時(shí)會(huì)出現(xiàn)一圈暗邊。原因是卷積窗口在靠近圖像邊界時(shí)一部分鄰域像素缺失OpenCV默認(rèn)按邊填充的方式會(huì)讓邊界像素值被重復(fù)計(jì)算灰度權(quán)重計(jì)算出現(xiàn)偏差導(dǎo)致邊緣像素被壓暗。我的處理辦法是先對圖像做邊緣擴(kuò)展反射填充濾波完成后再裁回原尺寸。OpenCV的copyMakeBorder配合BORDER_REFLECT就能解決border cv2.copyMakeBorder(img, 20, 20, 20, 20, cv2.BORDER_REFLECT) smooth_border cv2.bilateralFilter(border, 15, 75, 45) out smooth_border[20:-20, 20:-20]這個(gè)小技巧能徹底消滅暗邊問題雖然大多數(shù)情況下暗邊只有幾個(gè)像素寬但在高清人臉特寫上非常顯眼值得處理。6. 磨皮的度算法之外的一些個(gè)人體會(huì)聊到這兒雙邊濾波的技術(shù)內(nèi)容基本講完了但我還想說一些算法之外、卻同樣重要的東西。磨皮這個(gè)技術(shù)難的不是把皮膚磨光而是磨得“讓人看不出來磨過”。在我實(shí)際接觸過的項(xiàng)目里甲方把sigmaColor從80一路推到200只想追求極致膚感結(jié)果輸出的照片被用戶評價(jià)為“假面感太重”。后來我們往回調(diào)整參數(shù)并加入高頻細(xì)節(jié)回灌用戶滿意度反而明顯回升。這里面其實(shí)是一個(gè)很樸素的道理人臉不是瓷器有紋理才有真實(shí)感磨皮是讓皮膚變好而不是讓皮膚消失。另外現(xiàn)在的美顏算法已經(jīng)大量引入深度學(xué)習(xí)路線各種人像分割、皮膚分割、局部修飾網(wǎng)絡(luò)效果確實(shí)驚艷。但經(jīng)典算法依然沒有退場原因有幾個(gè)一是可解釋性強(qiáng)每一個(gè)參數(shù)都有明確物理含義出了問題知道在哪調(diào)二是算力開銷小端側(cè)設(shè)備都能流暢運(yùn)行三是它作為底層保邊平滑工具依然嵌入在很多深度學(xué)習(xí)方案里作為后處理環(huán)節(jié)或數(shù)據(jù)增強(qiáng)手段。如果你想從事圖像算法相關(guān)的工作把雙邊濾波吃透絕對不是一個(gè)過時(shí)的選擇。以后你再看到某張明星照片被夸“這個(gè)磨皮好自然”不妨在腦海里猜一猜它大概率是兩個(gè)策略的組合——一個(gè)保邊平滑算子控制低頻一個(gè)高頻層控制細(xì)節(jié)比例中間邊緣保護(hù)做得滴水不漏。看到這一步你就已經(jīng)比大多數(shù)只看熱鬧的人要懂行了。