摳圖實(shí)現(xiàn))
1. 項(xiàng)目背景與到底要解決什么問題不知道你有沒有過這種經(jīng)歷要簽一份電子合同對方要求“請上傳您的簽名圖片”結(jié)果你拿著手機(jī)對著白紙上的簽名拍了一張背景泛黃、光線不均勻、還有紙紋和墨跡暈染摳都摳不干凈?;蛘吣闶亲鲈O(shè)計(jì)、做文檔自動化的經(jīng)常需要把客戶的手寫簽名從掃描件里單獨(dú)提取出來生成透明底PNG方便在PDF、合同模板、電子簽章系統(tǒng)里直接調(diào)用。我這個(gè)“手寫簽名提取工具(圖片)”就是干這個(gè)的輸入一張帶有手寫簽名的照片或掃描件自動把背景去掉把簽名區(qū)域精準(zhǔn)摳出來輸出一張高清、透明背景的簽名圖。說白了就是一個(gè)專為“簽名”這種特定對象優(yōu)化的圖像分割小工具。說實(shí)話市面上通用的摳圖工具也能做但用過的都知道有多別扭。通用摳圖是給“人像”“物品”設(shè)計(jì)的模型大、依賴GPU對“白紙上黑字”這種高對比、低語義的簡單對象反而容易翻車——麻煩在人像邊緣、發(fā)絲處理上簽名這種連續(xù)筆跡處理不好反而容易缺筆畫。所以我才決定自己寫一個(gè)專業(yè)的提取工具針對簽名圖像的特性做優(yōu)化。這篇文章就把我的完整實(shí)現(xiàn)思路、參數(shù)調(diào)優(yōu)過程、踩過的坑全部記錄下來給同樣有需求的朋友參考。適合誰看兩類人第一類是處理合同、公文、法務(wù)材料的辦公人員不需要會寫代碼能用現(xiàn)成工具就行我會講清楚使用場景和注意事項(xiàng)第二類是像我一樣想自己動手實(shí)現(xiàn)或二次開發(fā)的技術(shù)人員我會把核心算法、代碼、參數(shù)都拆開講。2. 核心思路拆解為什么“簡單摳圖”不夠用2.1 簽名圖像的三個(gè)典型特征手寫簽名提取不只是一個(gè)“去掉背景”的問題。要做得好用必須先搞清楚簽名圖像到底特殊在哪。第一個(gè)特征是背景單一但質(zhì)量差。絕大多數(shù)簽名都是黑色或藍(lán)色筆跡寫在白紙或輕微泛黃的紙上理論上用“黑/白”二分就能分開。但現(xiàn)實(shí)情況是有人用手機(jī)隨手拍的紙上還有陰影、紙紋、墨水洇開的痕跡甚至有些是快遞單、報(bào)銷單背景有表格線、公章、二維碼純粹按顏色二分會把一堆垃圾摳出來。第二個(gè)特征是筆跡是連續(xù)筆畫。簽名是人的筆鋒一筆一劃都有粗細(xì)變化很多筆畫很細(xì)只有1-3個(gè)像素寬。如果用太強(qiáng)的模糊、去噪或者形態(tài)學(xué)操作細(xì)筆畫直接斷了簽名就“缺胳膊少腿”。這是我反復(fù)調(diào)參之后最大的體會簽名的連續(xù)性比什么都重要。第三個(gè)特征是顏色犯沖。常見的圓珠筆是藍(lán)色跟紅章、灰色表格線混在一起。如果直接在RGB上做閾值分割藍(lán)筆和黑筆的特性完全不一樣簡單地選中“深色”會把紅章也選中紅色灰度值低選中“藍(lán)色”又會把黑色簽名漏掉。所以一個(gè)干凈的提取方案必須同時(shí)處理不同顏色的筆跡。2.2 通用摳圖 vs 專用提取工具通用摳圖工具比如那些基于深度學(xué)習(xí)的分割模型處理復(fù)雜場景確實(shí)強(qiáng)但用于簽名提取有“殺雞用牛刀”而且效果不一定好的問題。簽名提取和通用摳圖最大的不同在于通用摳圖靠“語義”——模型知道什么是人、什么是貓、什么是桌子靠大數(shù)據(jù)學(xué)出來的經(jīng)驗(yàn)把前景分出來但簽名沒有這種語義模型不知道“哪個(gè)形狀是簽名”只能靠“哪些像素和周邊不太一樣”來分。而且簽名往往只占整張圖的百分之幾模型很難聚焦到這個(gè)區(qū)域。專用提取工具的優(yōu)勢在于它可以把“簽名”這個(gè)特定對象的核心特征全部編碼進(jìn)算法里簽名筆跡通常顏色上偏向藍(lán)/黑與白色的紙張背景區(qū)分度較大簽名所在的紙張雖然是白色的但受光照影響可能有漸變所以需要自適應(yīng)閾值簽名是連續(xù)筆跡所以提取后要加強(qiáng)連通性不能出現(xiàn)斷裂簽名對精度要求高畢竟是法律效力的證明缺筆畫會很麻煩。3. 技術(shù)選型用哪種方案最穩(wěn)妥3.1 主流技術(shù)方案對比寫這個(gè)工具之前我把可能的路子都捋了一遍大致有四條路第一種是純閾值分割。把圖像轉(zhuǎn)成灰度圖然后設(shè)定一個(gè)閾值高于閾值的變白低于閾值的變黑。實(shí)現(xiàn)簡單但非常依賴光照條件和紙張顏色。同樣一個(gè)簽名在陽光下拍出來和臺燈下拍出來灰階分布完全不一樣固定閾值經(jīng)常把淺色筆跡抹掉。這種方式適合“掃描儀掃出來的規(guī)范白紙”不適合手機(jī)拍照。第二種是顏色空間分割。把圖像從RGB轉(zhuǎn)到HSV直接在色相H和飽和度S上做范圍篩選。簽名中常見的是藍(lán)、黑筆跡將它們映射到HSV空間中深黑色的飽和度偏低藍(lán)色的飽和度較高。單純只看H和S容易把黑色筆跡或部分深色紙張?jiān)肼曁蕴?。更穩(wěn)妥的做法是結(jié)合亮度分量和顏色分量一起判斷。第三種是基于梯度的邊緣檢測。先通過邊緣特征定位簽名所在位置再做內(nèi)外填充。理論上能適應(yīng)復(fù)雜背景但邊緣檢測受噪聲影響極大紙張的褶皺和顆粒感會產(chǎn)生大量假邊緣。簽名筆畫又寬又實(shí)檢測出來的邊緣往往是粗邊框想要填成實(shí)心區(qū)域還需要額外操作反而得不償失。第四種是傳統(tǒng)圖像處理加啟發(fā)式規(guī)則。把名字總結(jié)為“一張圖中顏色最深的連續(xù)區(qū)域”并結(jié)合亮度、飽和度、連通域大小綜合判斷。這套思路可以兼容各種拍攝環(huán)境和背景干擾并且輕量級不需要GPU普通電腦跑幾千張圖也很快。缺點(diǎn)是寫起來費(fèi)勁規(guī)則要一個(gè)個(gè)試。我最終選擇的是“顏色空間篩選 自適應(yīng)閾值 形態(tài)學(xué)修復(fù)”的組合方案。核心邏輯是用飽和度S判斷筆跡區(qū)域因?yàn)椴还苁撬{(lán)色圓珠筆還是黑色簽字筆在白色紙上書寫時(shí)飽和度相對于背景都會有明顯差異用自適應(yīng)閾值處理光照不均勻問題局部亮暗不一致時(shí)也能找對邊界用形態(tài)學(xué)膨脹連接斷裂的筆畫最后用輪廓篩選過濾掉比較小的噪聲。3.2 為什么不用深度學(xué)習(xí)模型我知道肯定有人要問現(xiàn)在隨便一個(gè)分割模型精度都比傳統(tǒng)算法高為什么不用我做過對比測試。用YOLO系列做檢測、用分割模型做mask效果確實(shí)不錯(cuò)但有幾個(gè)繞不開的麻煩一是不夠輕。模型文件動輒幾百M(fèi)B啟動一次推理要一兩秒處理批量的掃描件效率很低。二是數(shù)據(jù)集不好搞。真實(shí)簽名數(shù)據(jù)涉及隱私很難找到公開的大規(guī)模標(biāo)注數(shù)據(jù)集自己標(biāo)注幾百張勉強(qiáng)能用但泛化到各種筆、各種紙、各種光線還是有問題。三是邊緣不夠銳利。神經(jīng)網(wǎng)絡(luò)輸出的mask天然是帶模糊的做完閾值處理后往往還要接一遍形態(tài)學(xué)跟傳統(tǒng)算法一樣麻煩。傳統(tǒng)圖像處理方案的優(yōu)點(diǎn)是完全可控、沒有黑盒、單張圖處理可以控制在100毫秒以內(nèi)而且出問題的時(shí)候能清楚知道是哪一步的參數(shù)不對。對于簽名提取這種“規(guī)則非常明確”的問題它反而是性價(jià)比更高的選擇。4. 實(shí)操全過程手把手教你實(shí)現(xiàn)一個(gè)簽名提取工具4.1 環(huán)境準(zhǔn)備我的開發(fā)環(huán)境是Python 3.9圖像處理使用OpenCVopencv-python和Pillow基本就這兩個(gè)依賴。安裝命令pip install opencv-python pillow numpyOpenCV負(fù)責(zé)核心的圖像處理算法Pillow用來處理PNG透明通道的讀寫NumPy做數(shù)組運(yùn)算。如果你不寫代碼直接看后面的命令行工具和圖形界面工具也可以。4.2 第一步圖片預(yù)處理去掉無用的環(huán)境信息原始照片進(jìn)來后不能直接做分割。手機(jī)拍出來的照片可能角度偏了也可能四周留白太多所以要先做裁剪和校正。首先把圖片縮放到合理尺寸。過大的圖片會拖慢處理速度過小的圖片會丟失筆畫細(xì)節(jié)。我實(shí)測下來最合適的處理寬度是1500像素左右這個(gè)尺寸下細(xì)筆畫的保留度和處理速度能保持很好的平衡。import cv2 import numpy as np def resize_image(image, max_width1500): h, w image.shape[:2] if w max_width: scale max_width / w new_h int(h * scale) new_w max_width image cv2.resize(image, (new_w, new_h), interpolationcv2.INTER_AREA) return image注意這里用的是INTER_AREA插值算法它縮放時(shí)能保留更多邊緣信息不容易產(chǎn)生鋸齒感。如果是放大簽名區(qū)域則要用INTER_CUBIC效果更平滑。然后做灰度化和高斯模糊去噪。高斯模糊做得很輕用3x3的小核就夠了主要目的是去掉照片的顆粒感同時(shí)盡量保留筆跡邊緣。def preprocess(image): image resize_image(image) # 轉(zhuǎn)換到LAB顏色空間重點(diǎn)用L通道亮度做后續(xù)分析 lab cv2.cvtColor(image, cv2.COLOR_BGR2LAB) l_channel, a_channel, b_channel cv2.split(lab) return image, l_channel, a_channel, b_channel這里有個(gè)很重要的設(shè)計(jì)決策為什么不直接用灰度圖而是轉(zhuǎn)到LAB顏色空間因?yàn)榛叶葓D是RGB三個(gè)通道的加權(quán)平均黑筆和藍(lán)筆在灰度圖上的表現(xiàn)差異很大黑筆很黑藍(lán)筆偏灰而LAB空間的L通道代表“亮度”能更純粹地反映出筆跡和紙張?jiān)诿靼瞪系牟顒e。在處理顏色時(shí)A通道和B通道保存了顏色信息。將圖像轉(zhuǎn)換到LAB后可以沿著A通道和B通道區(qū)分藍(lán)筆、黑筆和紅章比單純在RGB上做分割要穩(wěn)定得多。4.3 第二步顏色空間篩選把藍(lán)筆和黑筆都留下來簽名中最常見的是黑色簽字筆和藍(lán)色圓珠筆。如果只按灰度閾值提取黑筆能提取得很干凈藍(lán)筆則非常容易丟失因?yàn)樗幕疑蹈垙埍尘疤咏?。為了解決這個(gè)問題我決定同時(shí)用兩條路徑做提取最后再疊加路徑一基于L通道亮度的閾值提取專門負(fù)責(zé)黑筆路徑二基于A/B通道的顏色特征提取專門負(fù)責(zé)藍(lán)筆和其他彩色筆跡。先看路徑一的代碼def extract_by_brightness(l_channel): # 使用Otsu自適應(yīng)閾值自動算出區(qū)分前景和背景的最佳分界值 _, binary cv2.threshold(l_channel, 0, 255, cv2.THRESH_BINARY_INV cv2.THRESH_OTSU) return binary這一步直接交給出名的Otsu大津法。它的原理是讓前景和背景兩類像素的類內(nèi)方差最小、類間方差最大從而自動找到一個(gè)合適的閾值不需要人工調(diào)參。在光照均勻的情況下準(zhǔn)確率非常可觀。再看路徑二的代碼def extract_by_color(image, l_channel): hsv cv2.cvtColor(image, cv2.COLOR_BGR2HSV) h, s, v cv2.split(hsv) # 藍(lán)色筆跡色相在90-130之間飽和度大于50亮度大于30 blue_mask cv2.inRange(hsv, (90, 50, 30), (130, 255, 255)) # 黑色筆跡的亮度低、飽和度也較低單獨(dú)提取 black_mask cv2.inRange(hsv, (0, 0, 0), (180, 60, 80)) # 兩者合并 color_mask cv2.bitwise_or(blue_mask, black_mask) return color_mask這里我用HSV來提取彩色筆跡色相H區(qū)分“是不是藍(lán)顏色”飽和度S區(qū)分“是不是淡色的紙”亮度V區(qū)分“是不是清晰的筆跡”。這個(gè)篩選規(guī)則同樣會把部分深色背景當(dāng)成前景沒關(guān)系后面還有一道形態(tài)學(xué)過濾來清理。4.4 第三步結(jié)合閾值與顏色信息得到初步簽名區(qū)域把路徑一和路徑二的輸出疊加在一起就是“可能是簽名”的候選區(qū)域。def generate_candidate(l_channel, color_mask, brightness_mask): # 兩路提取結(jié)果取并集 merged cv2.bitwise_or(brightness_mask, color_mask) # 用閉運(yùn)算連接斷裂的筆畫 kernel_close cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (5, 5)) merged cv2.morphologyEx(merged, cv2.MORPH_CLOSE, kernel_close, iterations2) return merged這一步很關(guān)鍵。閉運(yùn)算是“先膨脹再腐蝕”它的作用是把筆跡內(nèi)部的小空洞填上把筆畫之間很近的斷點(diǎn)連接起來。注意核的大小——5x5的橢圓核已經(jīng)是比較保守的了核太大會把兩個(gè)不相干的筆畫糊成一個(gè)整體核太小又連不上斷點(diǎn)需要用迭代次數(shù)來控制連接強(qiáng)度。實(shí)測下來用橢圓核而不是矩形核原因在于橢圓核更接近筆跡的橫截面形狀在處理手寫筆鋒時(shí)不至于把連續(xù)筆畫的轉(zhuǎn)角“磨方”。4.5 第四步形態(tài)學(xué)細(xì)化和清理噪點(diǎn)到了這一步候選區(qū)域里除了簽名本身可能還有紙上的小污點(diǎn)、掃描件上的噪點(diǎn)、拍照時(shí)留下的陰影邊緣。如果直接輸出出來的PNG會有一堆密密麻麻的小黑點(diǎn)不僅不美觀有時(shí)還會干擾印章的效果。清理噪點(diǎn)最標(biāo)準(zhǔn)的方法是“連通域分析”。每個(gè)連續(xù)的黑色像素塊就是一個(gè)連通域我只需要把面積過小的連通域刪掉就可以了。def remove_small_components(mask, min_area20): num_labels, labels, stats, centroids cv2.connectedComponentsWithStats(mask, connectivity8) # 生成一個(gè)全是背景的掩膜 cleaned np.zeros_like(mask) for i in range(1, num_labels): area stats[i, cv2.CC_STAT_AREA] if area min_area: cleaned[labels i] 255 return cleaned這里連通域分析后的第0個(gè)標(biāo)簽是背景處理時(shí)跳過就行。min_area的取值需要根據(jù)圖片實(shí)際尺寸做比例換算我一般會計(jì)算一下圖片總像素的萬分之幾作為閾值比如1500x1000的圖像總像素150萬萬分之幾就是150。這個(gè)值既不會誤刪簽名中細(xì)小的筆畫又能有效清除顆粒噪點(diǎn)。還有一個(gè)容易被忽略的問題紙張邊緣。如果照片里拍到桌子邊緣或者紙張本身有折痕那在提取時(shí)會產(chǎn)生細(xì)長的暗色條帶連通域面積可能很大單純的面積過濾根本攔不住。我的處理辦法是額外檢查連通域的形狀特征如果某個(gè)連通域的長寬比大于10同時(shí)面積占比又特別大就判定為紙張邊緣直接刪除。簽名筆畫不會長成這種形態(tài)所以這個(gè)規(guī)則很安全。4.6 第五步生成透明背景PNG清潔干凈的二值mask就是簽名的“選區(qū)”。在它里面白色255的位置對應(yīng)簽名筆跡黑色0的位置對應(yīng)背景。把這個(gè)mask套回原圖上生成帶Alpha通道的透明PNG。def save_transparent_result(image, mask, output_path): # 將原圖BGRA化先保留原圖的RGB信息 result cv2.cvtColor(image, cv2.COLOR_BGR2BGRA) # 將mask作為Alpha通道即mask為白處不透明mask為黑處透明 result[:, :, 3] mask cv2.imwrite(output_path, result)這里有一個(gè)技術(shù)細(xì)節(jié)需要特別注意mask是提取后的二值區(qū)域把它作為Alpha通道意味著簽名筆跡部分完全不透明背景部分完全透明。但如果你觀察仔細(xì)會發(fā)現(xiàn)簽名筆跡在RGB通道上還保留著原來的顏色——黑色簽字筆是黑的藍(lán)色圓珠筆是藍(lán)的。這樣保存下來的PNG插到合同里時(shí)顏色非常自然不會像某些工具那樣把藍(lán)筆提取成純黑色。如果你希望輸出的是“黑筆跡 透明背景”的純黑色簽名可以在保存前把RGB通道全部置為0只保留Alpha通道的形狀信息。兩種方式我都做過從實(shí)際使用反饋看法律文書場景更偏愛保留原始筆跡顏色的方案而設(shè)計(jì)素材場景更愛純黑色。4.6 完整流程串聯(lián)把上面所有步驟串聯(lián)起來就是完整的處理函數(shù)def extract_signature(image_path, output_path): image cv2.imread(image_path) if image is None: raise ValueError(f無法讀取圖片: {image_path}) # 預(yù)處理 image, l_channel, a_channel, b_channel preprocess(image) # 雙路徑提取 brightness_mask extract_by_brightness(l_channel) color_mask extract_by_color(image, l_channel) # 合并候選區(qū)域 merged generate_candidate(l_channel, color_mask, brightness_mask) # 清理噪點(diǎn) cleaned remove_small_components(merged, min_area150) # 再做一個(gè)輕微的膨脹操作把筆跡邊緣補(bǔ)圓潤 kernel_clean cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (2, 2)) cleaned cv2.dilate(cleaned, kernel_clean, iterations1) # 輸出透明背景 save_transparent_result(image, cleaned, output_path) print(f已提取簽名保存至: {output_path})整套流程在普通家用電腦上處理一張1500像素寬的照片耗時(shí)大約50到120毫秒處理一批100張的掃描件只需要十幾秒實(shí)用性完全沒問題。5. 參數(shù)調(diào)優(yōu)讓工具適應(yīng)不同場景的三種模式5.1 模式一掃描件模式掃描件的特征是光線均勻、背景接近純白不會有太大動態(tài)范圍但可能存在掃描儀帶來的細(xì)密噪點(diǎn)。此模式適合“無腦”使用Otsu全局閾值不依賴顏色通道。我把掃描件模式的參數(shù)配置為亮度閾值直接交給Otsu顏色通道的篩選強(qiáng)度降到最低形態(tài)學(xué)閉運(yùn)算的核從5x5降到3x3避免把靠近的兩個(gè)字糊在一起。實(shí)踐下來掃描件的提取準(zhǔn)確率幾乎百分之百不需要額外調(diào)參。5.2 模式二手機(jī)拍照模式手機(jī)拍照模式是問題最多的情況。光照不均導(dǎo)致紙張明暗不均Otsu全局閾值容易把暗處的淺筆跡抹掉陰影也會被誤判為前景。在這種模式下我把亮度提取從Otsu全局閾值換成OpenCV的自適應(yīng)閾值也就是局部區(qū)域的亮度二分def adaptive_brightness_mask(l_channel): # 對局部區(qū)域做高斯加權(quán)閾值窗口大小是奇數(shù)通常取11或15C為偏移量 binary cv2.adaptiveThreshold( l_channel, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C, cv2.THRESH_BINARY_INV, 15, 10 ) return binary參數(shù)解釋窗口大小取15表示每個(gè)像素考慮周圍15x15像素的局部亮度分布來計(jì)算閾值窗口太大則失去“局部”意義窗口太小則把筆跡本身也算進(jìn)背景里。偏移量C取10表示閾值要比局部均值暗10個(gè)灰階才判為前景這樣亮度平緩的陰影次區(qū)域不容易被當(dāng)成前景誤提取。但自適應(yīng)閾值有個(gè)副作用在紙張有輕微起伏的區(qū)域背景里的細(xì)小紋理很容易像云朵一樣被大片激活。所以我加了接下來的“顏色通道限制”只保留那些顏色上接近藍(lán)/黑、但亮度上確實(shí)很暗的像素可以消除大量陰影誤判。5.3 模式三復(fù)雜背景模式如果簽名蓋在報(bào)紙上、快遞單上或者背景里有公章和表格線那就要開啟“高嚴(yán)格模式”。高嚴(yán)格模式下提取邏輯從“尋找深色區(qū)域”變成“尋找跟周圍顏色差異最大的連續(xù)區(qū)域”。處理思路是先用L通道定位“比周圍暗”的像素再用B通道LAB的B通道不是灰度B定位“帶藍(lán)色或偏冷色傾向”的像素。如果兩個(gè)條件同時(shí)滿足才判為簽名筆跡。這個(gè)規(guī)則會損失掉一部分黑色簽字筆的痕跡但能有效避開紅色公章和紅色格線。這里我需要誠實(shí)地說一句遇到紅章蓋住簽名這種極端情況純靠顏色信息基本無解因?yàn)榧t章和黑筆重疊的地方RGB三通道都已經(jīng)混在一起了。如果要處理這種圖技術(shù)上需要做內(nèi)容修復(fù)或者人工介入不是這個(gè)工具能自動解決的。5.4 參數(shù)自適應(yīng)的設(shè)計(jì)原則我最終實(shí)現(xiàn)的工具并不要求用戶自己調(diào)參數(shù)而是內(nèi)置了幾套“預(yù)設(shè)檔位”清晰白底掃描件推薦全局閾值 弱形態(tài)學(xué)手機(jī)拍攝偏暗推薦自適應(yīng)閾值 顏色限制復(fù)雜背景干擾推薦高嚴(yán)格模式犧牲部分召回率換取準(zhǔn)確率。如果你要自己改代碼記住一條核心規(guī)律需求是“準(zhǔn)確優(yōu)先”還是“完整優(yōu)先”。合同簽章場景寧可漏掉一點(diǎn)點(diǎn)顏色淡的筆跡也不能把紅章和雜點(diǎn)加進(jìn)來個(gè)人簽名素材場景寧可多一點(diǎn)背景噪點(diǎn)也不能讓筆畫斷裂。6. 常見問題與排查技巧6.1 筆跡斷裂嚴(yán)重簽名看上去不連續(xù)這種情況多數(shù)是預(yù)處理階段的高斯模糊核太大了或者是形態(tài)學(xué)閉運(yùn)算的迭代次數(shù)不夠。檢查一下代碼里的高斯核如果從3x3改成了7x7或更大細(xì)筆畫很容易就被抹掉另外閉運(yùn)算的迭代次數(shù)取2次到一個(gè)溫和的范圍比較好不是越多越好次數(shù)太多會把所有東西糊在一起。還有一個(gè)隱蔽原因圖像壓縮導(dǎo)致的偽影。用手機(jī)拍完照片發(fā)送到電腦時(shí)微信等聊天軟件會默認(rèn)壓縮圖片簽名筆畫在低碼率壓縮下會明顯變糊、筆畫斷裂。根治辦法是把原圖通過文件傳輸而不是圖片發(fā)送來傳或者在本機(jī)直接讀取原圖處理。6.2 提取結(jié)果中混入紅章或紅色簽字跡紅色在亮度上和黑色很接近如果只用亮度通道紅章會全部被當(dāng)簽名提出來。解決辦法有三層第一層是在HSV空間中抑制紅色的范圍具體參數(shù)是H在0-10和170-180之間S大于60V大于80將這部分像素強(qiáng)制置為背景第二層是加大顏色通道篩選的比例讓藍(lán)、黑筆跡的得分權(quán)重更高第三層是連通域形態(tài)過濾紅章通常是圓形、面積大簽名是細(xì)長條、面積相對分散可以用面積和長寬比把章體輪廓篩掉。需要提醒的是如果紅章和黑色簽名實(shí)際重疊那無論怎么調(diào)都分不開必須人工處理。我在工具里加了“輸出中間mask”的功能便于使用者快速判斷是哪一步出了問題。6.3 藍(lán)筆提取出來顏色發(fā)灰不像圓珠筆原色因?yàn)橥该鞅尘癙NG保存時(shí)我保留了RGB通道的顏色但顏色通道的原始值經(jīng)過了顏色空間轉(zhuǎn)換和再轉(zhuǎn)換會有輕微的色彩偏移。偏差來源是BGR轉(zhuǎn)LAB再轉(zhuǎn)回BGR時(shí)精度雖然有損但肉眼其實(shí)分不出來真正影響顏色的是你把灰度圖跟原圖做了位運(yùn)算。如果你在提取后又對結(jié)果做了二次二值化那藍(lán)筆自然變成灰黑色。我的建議是只要不是特別需要純黑色的輸出就保留原始RGB通道不要做二值化。如果需要純黑色版本直接疊加一個(gè)“簽名形狀”的純黑mask即可不要拿灰度圖來充當(dāng)顏色。6.4 輸出PNG后四周有大片透明區(qū)域透明底PNG打開后畫布還是原來整張圖的大小只不過背景透明了。放在白底頁面上沒問題但如果要貼到印章軟件或Word里最好先裁剪到簽名區(qū)域的最小包圍框。用cv2.boundingRect找到輪廓的外接矩形然后裁切再保存。def trim_signature(image, mask): # 找到mask中白色區(qū)域的外接矩形 contours, _ cv2.findContours(mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) if len(contours) 0: return image, mask x, y, w, h cv2.boundingRect(np.vstack(contours)) # 適當(dāng)向外擴(kuò)幾個(gè)像素避免筆跡被切到 pad 5 x max(0, x - pad) y max(0, y - pad) w min(image.shape[1] - x, w 2 * pad) h min(image.shape[0] - y, h 2 * pad) return image[y:yh, x:xw], mask[y:yh, x:xw]6.5 光照不均勻?qū)е乱话牒灻麤]提取出來這是手機(jī)拍照模式下最常見的問題。我在自適應(yīng)閾值里把窗口大小調(diào)小從15改成11偏移量C從10改成6能大幅改善暗部筆跡丟失的問題。但窗口太小紙張紋理又容易被誤判出來所以需要找到一個(gè)平衡點(diǎn)。我在工具里加了一個(gè)調(diào)試選項(xiàng)可以實(shí)時(shí)顯示閾值處理后的中間結(jié)果一邊看一邊調(diào)比盲調(diào)參數(shù)直觀得多。6.6 如何驗(yàn)證提取質(zhì)量你應(yīng)該建立一個(gè)簡單的質(zhì)量驗(yàn)證套路同時(shí)看三樣?xùn)|西——原圖、提取后的透明PNG、以及把透明PNG疊在白底和黑底兩種背景上的效果。疊黑底能看出有沒有殘留的白色紙邊疊白底能看出有沒有變灰的痕跡。如果黑底上全是白邊說明邊緣羽化不夠如果白底上簽名變淺說明原圖的暗淡筆跡被誤刪了。7. 工具的擴(kuò)展思考從批量處理到桌面應(yīng)用搞定了單張圖片提取我馬上遇到了新需求一個(gè)法務(wù)部門的朋友一次性發(fā)來一百多張簽好名的合同掃描件要逐張?zhí)崛∪缓髿w檔。這個(gè)需求直接催生了工具的兩個(gè)擴(kuò)展功能批量處理和批量改名。批量處理非常簡單文件夾里循環(huán)處理保持輸入文件名即可。真正麻煩的是輸出命名我建議使用項(xiàng)目編號 頁碼的規(guī)則例如“合同編號_第3頁簽名.png”這樣后期檢索的時(shí)候才不迷糊。另一個(gè)很實(shí)用的擴(kuò)展是把工具做成一個(gè)簡單的圖形界面。如果你不想寫代碼可以直接用現(xiàn)成的桌面工具很多圖像處理軟件里就有摳圖功能但如果有一定編程基礎(chǔ)用Python的PySimpleGUI寫一個(gè)幾十行的界面框架把模式選擇、批量處理、輸出目錄三個(gè)控件加上工具就基本可以交付給別人用了。代碼層面我最終把整個(gè)工具封裝成了命令行版本支持這樣的調(diào)用方式python sign_extract.py --mode scan --input ./scans/ --output ./results/配合模式參數(shù)可以靈活應(yīng)對不同場景。在批量處理場景下我建議加入一個(gè)失敗日志把每一張?zhí)幚懋惓5脑驅(qū)戇M(jìn)日志文件。由于簽名提取不是100%完美批量處理時(shí)備份原圖再操作是最穩(wěn)妥的做法任何覆蓋原文件的操作都不建議開啟。8. 寫在最后的實(shí)操心得做這個(gè)簽名提取工具的過程讓我重新認(rèn)識了一件事圖像處理里很多時(shí)候不是你選的算法不夠先進(jìn)而是你沒有針對你的特定對象把細(xì)節(jié)做好。簽名的提取難度遠(yuǎn)低于人臉分割但實(shí)際工程里“好用”的程度反而取決于那些文檔里不會寫的小細(xì)節(jié)閉運(yùn)算的核用橢圓還是矩形自適應(yīng)閾值的窗口是11還是15連通域面積閾值的比例尺是多少。每一個(gè)參數(shù)背后都有真實(shí)的圖像在使用中出現(xiàn)過問題。我個(gè)人最終穩(wěn)定使用的配置是掃描件用Otsu全局閾值 3x3閉運(yùn)算手機(jī)拍照用自適應(yīng)閾值窗口15偏移量10 顏色通道限制復(fù)雜背景用高嚴(yán)格模式 手工二次確認(rèn)。用這套配置試過各種圓珠筆、簽字筆、鉛筆鉛筆亮度反差小效果稍差但可用大多數(shù)情況下都能一次提取到位。還有一個(gè)建議如果你只是偶爾提取一兩張簽名直接用現(xiàn)成的工具最快。但如果你經(jīng)常跟合同、掃描件打交道強(qiáng)烈建議自己維護(hù)這么一個(gè)小工具把常見的干擾項(xiàng)處理邏輯都沉淀進(jìn)去隨著踩坑次數(shù)增多它會變得越來越順手效率提升是立竿見影的。