數字影象處理-前端實現
原始碼地址: ofollow,noindex">github.com/weiruifeng/…
數字影象處理(Digital Image Processing)是指用計算機進行的處理。說起數字影象處理大家都會想到C++有很多庫和演算法,MATLAB的方便,但自從有了canvas,JavaScript可以對影象進行畫素級的操作,甚至還可以直接處理影象的二進位制原始資料。
獲取資料和儲存圖片
獲取資料
利用 fileReader 和 canvas 配合獲取影象
<canvas id="myCanvas">抱歉,您的瀏覽器還不支援canvas。</canvas> <input type="file" id="myFile" /> 複製程式碼
當用戶選擇圖片時
file.onchange = function(event) { const selectedFile = event.target.files[0]; const reader = new FileReader(); reader.onload = putImage2Canvas; reader.readAsDataURL(selectedFile); } function putImage2Canvas(event) { const img = new Image(); img.src = event.target.result; img.onload = function(){ myCanvas.width = img.width; myCanvas.height = img.height; var context = myCanvas.getContext('2d'); context.drawImage(img, 0, 0); const imgdata = context.getImageData(0, 0, img.width, img.height); // 處理imgdata } } 複製程式碼
其中,ImageData物件中儲存著canvas物件真實的畫素資料,包含3個只讀屬性: **width:**圖片寬度,單位是畫素 **height:**圖片高度,單位是畫素 **data:**Uint8ClampedArray型別的一維陣列,包含著RGBA格式的整型資料,範圍在0至255之間(包括255) **關係:**Uint8ClampedArray的length = 4 * width * height 數字影象處理應用的資料便是 ImageData.data 的資料
儲存圖片
HTMLCanvasElement 提供一個 toDataURL 方法,此方法在儲存圖片的時候非常有用。它返回一個包含被型別引數規定的影象表現格式的資料鏈接。 資料鏈接的格式為
data:[<mediatype>][;base64],<data> 複製程式碼
mediatype 是個 MIME 型別的字串,例如 "image/jpeg" 表示 JPEG 影象檔案。如果被省略,則預設值為 text/plain;charset=US-ASCII
通過HTML中a標籤的download屬性便可進行下載
downloadFile(fileName, url) { const aLink = document.createElement('a'); aLink.download = fileName; aLink.href = url; aLink.click(); } // 下載圖片 downloadFile(fileName, myCanvas.toDataURL()); 複製程式碼
點運算
點運算(Point Operation)能夠讓使用者改變影象資料佔據的灰度範圍,可以看作是 從畫素到畫素 的複製操作。 如果輸入影象為 ,輸出影象為 ),則點運算可表示為:
其中放 被稱為灰度變換函式,它描述了輸入灰度值和輸出灰度值之間的轉換關係。一旦灰度變換函式確定,該點運算就完全被確定下來了。
點運算一般操作有灰度均衡化、線性變換、閾值變換、視窗變換、灰度拉伸等。
灰度直方圖
概述
灰度直方圖用於統計一幅灰度影象的畫素點(0~255)的個數或者比例,從圖形上來說,灰度直方圖就是一個二維圖,橫座標表示灰度值(灰度級別),縱座標表示具有各個灰度值或者灰度級別的畫素在影象中出現的次數或者概率。


程式碼
/** * 統計資料(針對灰度影象) * @param data 原始資料 * @param strength 分份 * @returns {Array} */ function statistics(data, strength = 1) { const statistArr = []; for (let i = 0, len = data.length; i < len; i += 4) { const key = Math.round(data[i] / strength); statistArr[key] = statistArr[key] || 0; statistArr[key]++; } return statistArr; } 複製程式碼
通過直方圖可以看出一副影象的畫素分佈情況。
直方圖均衡化
概述
我們都知道,如果影象的對比度越大,圖片就會清晰醒目,對比度小,影象就會顯得灰濛濛的。所謂對比度,在灰色影象裡黑與白的比值,也就是從黑到白的漸變層次。比值越大,從黑到白的漸變層次就越多,從而色彩表現越豐富。
直方圖均衡化是影象處理領域中利用影象直方圖對對比度進行調整的方法。目的是使得影象的每個畫素值在影象上都一樣多,會使得背景和前景都太亮或者太暗的影象變得更加清晰。
理論基礎
考慮一個離散的灰度影象{x},讓 表示灰度 出現的次數,這樣影象中灰度為 的畫素的出現概率是:
是影象中所有的灰度數(通常為256), 是影象中所有的畫素數, 實際上是畫素值為 的影象的直方圖,歸一化到 。
把對應於 的累積分佈函式,定義為:
是影象的累計歸一化直方圖。
我們建立一個形式為 的轉換,對於原始影象中的每個值它就產生一個 ,這樣 的累計概率函式就可以在所有值範圍內進行線性化,轉換公式定義為:
對於常數 ,影象處理中是256。
將 帶入(3)得:
將(3)(4)計算得:
公式5便是原畫素與變換後的畫素點的關係。
程式碼
/** * 該函式用來對影象進行直方圖均衡 * @param data */ function inteEqualize(data) { // 灰度對映表 const bMap = new Array(256); // 灰度對映表 const lCount = new Array(256); for (let i = 0; i < 256; i++) { // 清零 lCount[i] = 0; } // 計算各個灰度值的計數(只針對灰度影象) for (let i = 0, len = data.length; i < len; i += 4) { lCount[data[i]]++; } // 計算灰度對映表 for (let i = 0; i < 256; i++) { let lTemp = 0; for (let j = 0; j < i; j++) { lTemp += lCount[j]; } // 計算對應的新灰度值 bMap[i] = Math.round(lTemp * 255 / (data.length / 4)); } // 賦值 for (let i = 0, len = data.length; i < len; i += 4) { data[i] = bMap[data[i]]; data[i + 1] = bMap[data[i + 1]]; data[i + 2] = bMap[data[i + 2]]; } } 複製程式碼
灰度的線性變換
理論基礎
灰度的線性變換就是將影象中所有的點的灰度按照線性灰度變換函式進行變換,該線性灰度變換函式 是一個一維線性函式:
灰度變換方程為:
式中引數 為線性函式的斜率, 為線性函式在 軸的截距, 表示輸入影象的灰度, 表示輸出影象的灰度。
灰度影象有以下規律:
- 當 時,輸出影象當對比度將增大;當f 時,輸出影象當對比度將減小;
- 當 且 不等於0時,操作僅使所有畫素當灰度值上移或下移,其效果是使整個影象更暗或更亮;
- 如果 ,暗區域將變亮,亮區域將變暗,點運算完成了影象求補運算;
- 如果 時,輸出影象和輸入影象相同;
- 如果 時,輸出影象的灰度正好反轉;
程式碼
/** * 該函式用來對影象灰度 * @param data * @param fA線性變換的斜率 * @param fB線性變換的截距 */ function linerTrans(data, fA, fB) { for (let i = 0, len = data.length; i < len; i += 4) { // 針對RGB三個進行轉換 for (let j = 0; j < 3; j++) { let fTemp = fA * data[i + j] + fB; if (fTemp > 255) { fTemp = 255; } else if (fTemp < 0) { fTemp = 0; } else { fTemp = Math.round(fTemp); } data[i + j] = fTemp; } } } 複製程式碼
灰度的閾值變換
理論基礎
灰度的閾值變換可以將一幅灰度影象轉換成黑白二值影象。由使用者提前設定一個閾值,如果影象中某畫素的灰度值小於該閾值,則將該畫素的灰度值設定為0,否則設定為255 。
程式碼
/** * 該函式用來對影象進行閾值變換 * @param data * @param bthre 閾值 */ function thresholdTrans(data, bthre) { for (let i = 0, len = data.length; i < len; i += 4) { // 針對RGB三個進行轉換 for (let j = 0; j < 3; j++) { if (data[i + j] < bthre) { data[i + j] = 0; } else { data[i + j] = 255; } } } } 複製程式碼
灰度的視窗變換
理論基礎
灰度的視窗變換限定一個視窗範圍,該視窗中的灰度值保持不變;小於該視窗下限的灰度值直接設定為0;大於該視窗上限的灰度值直接設定為255 。
灰度視窗變換的變換函式表示式如下:
式中, 表示視窗的下限, 表示視窗的上限。
灰度的視窗變換可以用來去除背景是淺色,物體是深色的圖片背景。
程式碼
/** * 該函式用來對影象進行視窗變換。只有在視窗範圍內對灰度保持不變 * @param data * @param bLow下限 * @param bUp上限 */ function windowTrans(data, bLow, bUp) { for (let i = 0, len = data.length; i < len; i += 4) { // 針對RGB三個進行轉換 for (let j = 0; j < 3; j++) { if (data[i + j] < bLow) { data[i + j] = 0; } else if (data[i + j] > bUp) { data[i + j] = 255; } } } } 複製程式碼
灰度拉伸變換函式
灰度拉伸與灰度線性變換有點類似,不同之處在於灰度拉伸不是完全的線性變換,而是分段進行線性變換。
函式表示式如下:
灰度變換函式如圖:

程式碼
/** * 該函式用來對影象進行灰度拉伸 * 該函式的運算結果是將原圖在x1和x2之間的灰度拉伸到y1和y2之間 * @param data * @param bx1灰度拉伸第一個點的X座標 * @param by1灰度拉伸第一個點的Y座標 * @param bx2灰度拉伸第二個點的X座標 * @param by2灰度拉伸第二個點的Y座標 */ function grayStretch(data, bx1, by1, bx2, by2) { // 灰度對映表 const bMap = new Array(256); for (let i = 0; i < bx1; i++) { // 防止分母為0 if (bx1 > 0) { // 線性變換 bMap[i] = Math.round(by1 * i / bx1); } else { bMap[i] = 0; } } for (let i = bx1; i < bx2; i++) { // 判斷bx1是否等於bx2(防止分母為0) if (bx2 !== bx1) { bMap[i] = Math.round((by2 - by1) * (i - bx1) / (bx2 - bx1)); } else { // 直接賦值為by1 bMap[i] = by1; } } for (let i = bx2; i < 256; i++) { // 判斷bx2是否等於256(防止分母為0) if (bx2 !== 255) { // 線性變換 bMap[i] = by2 + Math.round((255 - by2) * (i - bx2) / (255 - bx2)); } else { // 直接賦值為255 bMap[i] = 255; } } for (let i = 0, len = data.length; i < len; i += 4) { data[i] = bMap[data[i]]; data[i + 1] = bMap[data[i + 1]]; data[i + 2] = bMap[data[i + 2]]; } } 複製程式碼
影象的幾何變換
HTML5中的canvas有完善的影象處理介面,在對影象進行幾何變換時,我們可以直接使用canvas介面即可,下面簡單列舉幾個幾何變換的介面:
-
影象平移
context.translate(x, y); 複製程式碼
-
影象縮放
context.scale(scalewidth, scaleheight); 複製程式碼
-
映象變換
canvas 中並沒有為映象變換專門提供方法,但不必緊張,至此我們依然尚未接觸到畫素級的操作。在上一節中介紹了影象縮放的相關內容,其中講到 scalewidth 和 scaleheight 的絕對值大於1時為放大,小於1時為縮小,但並沒有提到其正負。
content.translate(myCanvas.width/2, myCanvas.height/2); content.scale(-1, 1); content.translate(myCanvas.width/2, myCanvas.height/2); content.drawImage(img, 10, 10); 複製程式碼
-
影象旋轉
context.rotate(angle); 複製程式碼
-
影象轉置
canvas 沒有為影象轉置專門提供方法,但我們可以利用旋轉和映象組合的方法實現影象轉置的目的。影象的轉置可以分解為水平翻轉後再順時針旋轉90°,或是垂直翻轉後再逆時針旋轉90°。下面我們利用順時針旋轉90°後再水平翻轉實現影象轉置的操作
context.translate(myCanvas.width/2, myCanvas.height/2); context.scale(-1, 1); context.rotate(90*Math.PI/180); context.translate(-myCanvas.width/2, -myCanvas.height/2); context.drawImage(img, 10, 10); 複製程式碼
影象增強
影象增強是為了將影象中感興趣的部分有選擇的突出,而衰減其次要資訊,從而提高影象的可讀性。常見的目的有突出目標的輪廓,衰減各種噪聲等。
影象增強技術通常有兩類方法:空間域法和頻率域法。空間域法主要在空間域中對影象畫素灰度值直接進行運算處理。本章只介紹空間域法。
空間域法等影象增強技術可以用下式來描述:
其中 是處理前的影象, 表示處理後的影象, 為空間運算函式。
影象的灰度修正
影象的灰度修正是根據影象不同的降質現象而採用不同的修正方法。常見的方法參考點運算裡面的方法。
模版操作
模版是一個矩陣方塊,模版操作可看作是加權求和的過程,使用到的影象區域中的每個畫素分別於矩陣方塊中的每個元素對應相乘,所有乘積之和作為區域中心畫素的新值,是數字影象處理中經常用到的一種運算方式,影象的平滑、銳化、細化以及邊緣檢測都要用到模版操作。
例如:有一種常見的平滑演算法是將原圖中一個畫素的灰度值和它周圍臨近八個畫素的灰度值相加,然後將求得的平均值(除以9)作為新圖中該畫素的灰度值。表示如下:
使用模版處理影象時,要注意邊界問題,因為用模版在處理邊界時會報錯,常用的處理辦法有:
- 忽略邊界畫素,即處理後的畫素將丟掉這些畫素。
- 保留原邊界畫素,即複製邊界畫素到處理後的影象。
常用模版
-
低通濾波器
-
高通濾波器
-
平移和差分邊緣檢測
-
匹配濾波邊緣檢測
-
邊緣檢測
-
梯度方向檢測
程式碼
/** * 模版操作 * @param data資料 * @param lWidth影象寬度 * @param lHeight影象高度 * @param tempObj模版資料 * @param tempObj.iTempW模版寬度 * @param tempObj.iTempH模版高度 * @param tempObj.iTempMX模版中心元素X座標 * @param tempObj.iTempMY模版中心元素Y座標 * @param tempObj.fpArray模版陣列 * @param tempObj.fCoef模版係數 */ function template(data, lWidth, lHeight, tempObj) { const { iTempW, iTempH, iTempMX, iTempMY, fpArray, fCoef } = tempObj; // 儲存原始資料 const dataInit = []; for (let i = 0, len = data.length; i < len; i++) { dataInit[i] = data[i]; } // 行(除去邊緣幾行) for (let i = iTempMY; i < lHeight - iTempMY - 1; i++) { // 列(除去邊緣幾列) for (let j = iTempMX; j < lWidth - iTempMX - 1; j++) { const count = (i * lWidth + j) * 4; const fResult = [0, 0, 0]; for (let k = 0; k < iTempH; k++) { for (let l = 0; l < iTempW; l++) { const weight = fpArray[k * iTempW + l]; const y = i - iTempMY + k; const x = j - iTempMX + l; const key = (y * lWidth + x) * 4; // 儲存畫素值 for (let i = 0; i < 3; i++) { fResult[i] += dataInit[key + i] * weight; } } } for (let i = 0; i < 3; i++) { // 乘上係數 fResult[i] *= fCoef; // 取絕對值 fResult[i] = Math.abs(fResult[i]); fResult[i] = fResult[i] > 255 ? 255 : Math.ceil(fResult[i]); // 將修改後的值放回去 data[count + i] = fResult[i]; } } } } 複製程式碼
程式碼中處理邊界使用的是保留原邊界畫素。
平滑和銳化
平滑的思想是通過一點和周圍幾個點的運算來去除突然變化的點,從而濾掉一定的噪聲,但影象有一定程度的模糊,常用的模版是低通濾波器的模版。
銳化的目的是使模糊的影象變得更加清晰起來。影象的模糊實質就是影象受到平均或積分運算造成的,因此可以對影象進行逆運算如微分運算來使影象清晰話。從頻譜角度來分析,影象模糊的實質是其高頻分量被衰減,因而可以通過高通濾波操作來清晰影象。銳化處理也會將圖片的噪聲放大,因此,一般是先去除或減輕噪聲後再進行銳化處理。
影象銳化一般有兩種方法:微積分和高通濾波。高通濾波法可以參考高通濾波模版。微分銳化介紹一下拉普拉斯銳化。
梯度銳化
設影象為 ,定義 在點 處的梯度向量 為:
梯度有兩個重要的性質:
梯度的方向在函式 最大變化率方向上
梯度的幅度用 表示,其值為:
由此式可得出這樣的結論:梯度的數值就是 在其最大變化率方向上的單位距離所增加的量。
對於離散的數字影象,上式可以改寫成:
為了計算方便,也可以採用下面的近似計算公式:
這種梯度法又稱為水平垂直差分法,還有一種是交叉地進行差分計算,稱為羅伯特梯度法:
採用絕對差演算法近似為:
由於在影象變化緩慢的地方梯度很小,所以影象會顯得很暗,通常的做法是給一個閾值 ,如果 小於該閾值 ,則保持原灰度值不變;如果大於或等於閾值 ,則賦值為 :
基於水平垂直差分法的演算法程式碼如下:
/** * 該函式用來對影象進行梯度銳化 * @param data資料 * @param lWidth寬度 * @param lHeight高度 * @param bThre閾值 */ function gradSharp(data, lWidth, lHeight, bThre) { // 儲存原始資料 const dataInit = []; for (let i = 0, len = data.length; i < len; i++) { dataInit[i] = data[i]; } for (let i = 0; i < lHeight - 1; i++) { for (let j = 0; j < lWidth - 1; j++) { const lpSrc = (i * lWidth + j) * 4; const lpSrc1 = ((i + 1) * lWidth + j) * 4; const lpSrc2 = (i * lWidth + j + 1) * 4; for (let i = 0; i < 3; i++) { const bTemp = Math.abs(dataInit[lpSrc + i] - dataInit[lpSrc1 + i]) + Math.abs(dataInit[lpSrc + i] - dataInit[lpSrc2 + i]); if (bTemp >= 255) { data[lpSrc + i] = 255; // 判斷是否大於閾值,對於小於情況,灰度值不變 } else if (bTemp >= bThre) { data[lpSrc + i] = bTemp; } } } } } 複製程式碼
拉普拉斯銳化
我們知道,一個函式的一階微分描述了函式影象的增長或降低,二階微分描述的則是影象變化的速度,如急劇增長或下降還是平緩的增長或下降。拉普拉斯運算也是偏導數運算的線性組合,而且是一種各向同性的線性運算。
設 為拉普拉斯運算元,則:
對於離散數字影象 ,其一階偏導數為:
則其二階偏導數為:
所以,拉普拉斯運算元 為:
對於擴散現象引起的影象模糊,可以用下式來進行銳化:
這裡 是與擴散效應有關的係數。該係數取值要合理,如果 過大,影象輪廓邊緣會產生過沖;反之如果 過小,銳化效果就不明顯。
如果令 ,則變換公式為:
這樣變可以得到一個模版矩陣:
其實,我們通過常用的拉普拉斯銳化模版還有另外一種形式:
程式碼參考模版中的程式碼。
中值濾波
原理
中值濾波是一種非線性數字濾波器技術,一般採用一個含有奇數個點的滑動視窗,將視窗中個點灰度值的中值來代替定點(一般是視窗的中心點)的灰度值。對於奇數個元素,中值是指按大小排序後,中間的數值,對於偶數個元素,中值是指排序後中間兩個灰度值的平均值。
中值濾波是影象處理中的一個常用步驟,它對於斑點噪聲和椒鹽噪聲來說尤其有用。
程式碼
/** * 中值濾波 * @param data資料 * @param lWidth影象寬度 * @param lHeight影象高度 * @param filterObj模版資料 * @param filterObj.iFilterW模版寬度 * @param filterObj.iFilterH模版高度 * @param filterObj.iFilterMX模版中心元素X座標 * @param filterObj.iFilterMY模版中心元素Y座標 */ function medianFilter(data, lWidth, lHeight, filterObj) { const { iFilterW, iFilterH, iFilterMX, iFilterMY } = filterObj; // 儲存原始資料 const dataInit = []; for (let i = 0, len = data.length; i < len; i++) { dataInit[i] = data[i]; } // 行(除去邊緣幾行) for (let i = iFilterMY; i < lHeight - iFilterH - iFilterMY - 1; i++) { for (let j = iFilterMX; j < lWidth - iFilterW - iFilterMX - 1; j++) { const count = (i * lWidth + j) * 4; const fResult = [[], [], []]; for (let k = 0; k < iFilterH; k++) { for (let l = 0; l < iFilterW; l++) { const y = i - iFilterMY + k; const x = j - iFilterMX + l; const key = (y * lWidth + x) * 4; // 儲存畫素值 for (let i = 0; i < 3; i++) { fResult[i].push(dataInit[key + i]); } } } // 將中值放回去 for (let w = 0; w < 3; w++) { data[count + w] = getMedianNum(fResult[w]); } } } } /** * 將陣列排序後獲取中間的值 * @param bArray * @returns {*|number} */ function getMedianNum(bArray) { const len = bArray.length; bArray.sort(); let bTemp = 0; // 計算中值 if ((len % 2) > 0) { bTemp = bArray[(len - 1) / 2]; } else { bTemp = (bArray[len / 2] + bArray[len / 2 - 1]) / 2; } return bTemp; } export { medianFilter }; 複製程式碼
影象形態學
形態學的理論基礎是集合論。數學形態學提出了一套獨特的變換和運算方法。下面我們來看看最基本的 幾種數學形態學運算。
對一個給定的目標影象 和一個結構元素 ,想象一下將 在影象上移動。在每一個當前位置 , 只有三中可能的狀態:
- 與 均不為空
如圖所示:

第一種情況說明 與 相關最大;第二種情況說明 與 不相關;而第三種情況說明 與 只是部分相關。
腐蝕和膨脹
原理
當滿足條件1的點 的全體構成結構元素與影象的最大相關點集,我們稱這個點集為 對 的腐蝕,當滿足條件1和2的點x的全體構成元素與影象的最大相關點集,我們稱這個點集為 對 的膨脹。簡單的說,腐蝕可以看作是將影象 中每一個與結構元素 全等的子集 收縮為點 ,膨脹則是將 中的每一個點 擴大為 。
腐蝕與膨脹的操作是用一個給定的模版對影象X進行集合運算,如圖所示:

程式碼
程式碼為針對二值影象進行的腐蝕和膨脹演算法。
/** * 說明: * 該函式用於對影象進行腐蝕運算。 * 結構元素為水平方向或垂直方向的三個點,中間點位於原點; * 或者由使用者自己定義3*3的結構元素。 * 要求目標影象為只有0和255兩個灰度值的灰度影象 * @param data影象資料 * @param lWidth原影象寬度(畫素數) * @param lHeight原影象高度(畫素數) * @param nMode腐蝕方式,0表示水平方向,1表示垂直方向,2表示自定義結構元素 * @param structure自定義的3*3結構元素 */ function erosionDIB(data, lWidth, lHeight, nMode, structure) { // 儲存原始資料 const dataInit = []; for (let i = 0, len = data.length; i < len; i++) { dataInit[i] = data[i]; } if (nMode === 0) { // 使用水平方向的結構元素進行腐蝕 for (let j = 0; j < lHeight; j++) { // 由於使用1*3的結構元素,為防止越界,所以不處理最左邊和最右邊的兩列畫素 for (let i = 1; i < lWidth - 1; i++) { const lpSrc = j * lWidth + i; for (let k = 0; k < 3; k++) { // 如果原影象中當前點自身或者左右如果有一個點不是黑色,則將目標影象中的當前點賦成白色 for (let n = 0; n < 3; n++) { const pixel = lpSrc + n - 1; data[lpSrc * 4 + k] = 0; if (dataInit[pixel * 4 + k] === 255) { data[lpSrc * 4 + k] = 255; break; } } } } } } else if (nMode === 1) { // 使用垂直方向的結構元素進行腐蝕 // 由於使用1*3的結構元素,為防止越界,所以不處理最上邊和最下邊的兩列畫素 for (let j = 1; j < lHeight - 1; j++) { for (let i = 0; i < lWidth; i++) { const lpSrc = j * lWidth + i; for (let k = 0; k < 3; k++) { // 如果原影象中當前點自身或者左右如果有一個點不是黑色,則將目標影象中的當前點賦成白色 for (let n = 0; n < 3; n++) { const pixel = (j + n - 1) * lWidth + i; data[lpSrc * 4 + k] = 0; if (dataInit[pixel * 4] === 255) { data[lpSrc * 4 + k] = 255; break; } } } } } } else { // 由於使用3*3的結構元素,為防止越界,所以不處理最左邊和最右邊的兩列畫素和最上邊和最下邊的兩列元素 for (let j = 1; j < lHeight - 1; j++) { for (let i = 1; i < lWidth - 1; i++) { const lpSrc = j * lWidth + i; for (let k = 0; k < 3; k++) { data[lpSrc * 4 + k] = 0; // 如果原影象中對應結構元素中為黑色的那些點中有一個不是黑色,則將目標影象中的當前點賦成白色 for (let m = 0; m < 3; m++) { for (let n = 0; n < 3; n++) { if (structure[m][n] === -1) { continue; } const pixel = lpSrc + ((2 - m) - 1) * lWidth + (n - 1); if (dataInit[pixel * 4] === 255) { data[lpSrc * 4 + k] = 255; break; } } } } } } } } /** * 說明: * 該函式用於對影象進行膨脹運算。 * 結構元素為水平方向或垂直方向的三個點,中間點位於原點; * 或者由使用者自己定義3*3的結構元素。 * 要求目標影象為只有0和255兩個灰度值的灰度影象 * @param data影象資料 * @param lWidth原影象寬度(畫素數) * @param lHeight原影象高度(畫素數) * @param nMode腐蝕方式,0表示水平方向,1表示垂直方向,2表示自定義結構元素 * @param structure自定義的3*3結構元素 */ function dilationDIB(data, lWidth, lHeight, nMode, structure) { // 儲存原始資料 const dataInit = []; for (let i = 0, len = data.length; i < len; i++) { dataInit[i] = data[i]; } if (nMode === 0) { // 使用水平方向的結構元素進行腐蝕 for (let j = 0; j < lHeight; j++) { // 由於使用1*3的結構元素,為防止越界,所以不處理最左邊和最右邊的兩列畫素 for (let i = 1; i < lWidth - 1; i++) { const lpSrc = j * lWidth + i; for (let k = 0; k < 3; k++) { // 如果原影象中當前點自身或者左右如果有一個點不是黑色,則將目標影象中的當前點賦成白色 for (let n = 0; n < 3; n++) { const pixel = lpSrc + n - 1; data[lpSrc * 4 + k] = 255; if (dataInit[pixel * 4 + k] === 0) { data[lpSrc * 4 + k] = 0; break; } } } } } } else if (nMode === 1) { // 使用垂直方向的結構元素進行腐蝕 // 由於使用1*3的結構元素,為防止越界,所以不處理最上邊和最下邊的兩列畫素 for (let j = 1; j < lHeight - 1; j++) { for (let i = 0; i < lWidth; i++) { const lpSrc = j * lWidth + i; for (let k = 0; k < 3; k++) { // 如果原影象中當前點自身或者左右如果有一個點不是黑色,則將目標影象中的當前點賦成白色 for (let n = 0; n < 3; n++) { const pixel = (j + n - 1) * lWidth + i; data[lpSrc * 4 + k] = 255; if (dataInit[pixel * 4] === 0) { data[lpSrc * 4 + k] = 0; break; } } } } } } else { // 由於使用3*3的結構元素,為防止越界,所以不處理最左邊和最右邊的兩列畫素和最上邊和最下邊的兩列元素 for (let j = 1; j < lHeight - 1; j++) { for (let i = 1; i < lWidth - 1; i++) { const lpSrc = j * lWidth + i; for (let k = 0; k < 3; k++) { data[lpSrc * 4 + k] = 255; // 如果原影象中對應結構元素中為黑色的那些點中有一個不是黑色,則將目標影象中的當前點賦成白色 for (let m = 0; m < 3; m++) { for (let n = 0; n < 3; n++) { if (structure[m][n] === -1) { continue; } const pixel = lpSrc + ((2 - m) - 1) * lWidth + (n - 1); if (dataInit[pixel * 4] === 0) { data[lpSrc * 4 + k] = 0; break; } } } } } } } } 複製程式碼
開運算和閉運算
我們知道, 腐蝕是一種消除邊界點,使邊界向內部收縮的過程,可以用來消除小且無意義的物體。而膨脹是將與物體接觸的所有背景點合併到該物體中,使邊界向外部擴張的過程,可以用來填補物體中的空洞。
先腐蝕後膨脹的過程稱為開運算。用來消除小物體、在纖細點處分離物體、平滑較大物體的邊界的同時並不明顯改變其面積;先膨脹後腐蝕的過程稱為閉運算。用來填充物體內細小空洞、連線鄰近物體、平滑其邊界的同時並不明顯改變其面積。
開運算和閉運算是腐蝕和膨脹的結合,因此程式碼可以參考腐蝕和膨脹的程式碼。
細化
細化就是尋找圖形、筆畫的中軸或骨架,以其骨架取代該圖形或筆劃。在文字識別或影象理解中,先對被處理的影象進行細化有助於突出和減少冗餘的資訊量。
下面是一個具體的細化演算法(Zhang快速並行細化演算法):
一幅影象中的一個 區域,對各點標記名稱 ,其中P1位於中心。如圖所示:

如果 (即黑點),下面四個條件如果同時滿足,則刪除 。
其中 是 的非零鄰點的個數, 是以 , ,···,p9為序時這些點的值從 到 變化的次數。
對影象中的每一個點重複這一步驟,直到所有的點都不可刪除為止。
程式碼
/** * 說明: * 該函式用於對影象進行細化運算 * 要求目標影象為只有0和255兩個灰度值的灰度影象 * @param data影象資料 * @param lWidth原影象寬度(畫素數) * @param lHeight原影象高度(畫素數) */ function thinDIB(data, lWidth, lHeight) { // 儲存原始資料 const dataInit = []; for (let i = 0, len = data.length; i < len; i++) { dataInit[i] = data[i]; } let bModified = true; const neighBour = [ [0, 0, 0], [0, 0, 0], [0, 0, 0] ]; while (bModified) { bModified = false; for (let j = 1; j < lHeight - 1; j++) { for (let i = 1; i < lWidth - 1; i++) { let bCondition1 = false; let bCondition2 = false; let bCondition3 = false; let bCondition4 = false; const lpSrc = j * lWidth + i; // 如果原影象中當前點為白色,則跳過 if (dataInit[lpSrc * 4]) { continue; } // 獲取當前點相鄰的3*3區域內畫素值,0代表白色,1代表黑色 const bourLength = 3; for (let m = 0; m < bourLength; m++) { for (let n = 0; n < bourLength; n++) { const pixel = lpSrc + ((2 - m) - 1) * lWidth + (n - 1); neighBour[m][n] = (255 - dataInit[pixel * 4]) ? 1 : 0; } } const borderArr = [neighBour[0][1], neighBour[0][0], neighBour[1][0], neighBour[2][0], neighBour[2][1], neighBour[2][2], neighBour[1][2], neighBour[0][2]]; let nCount1 = 0; let nCount2 = 0; for (let i = 0, len = borderArr.length; i < len; i++) { nCount1 += borderArr[i]; if (borderArr[i] === 0 && borderArr[(i + 1) % len] === 1) { nCount2++; } } // 判斷 2<= NZ(P1)<=6 if (nCount1 >= 2 && nCount1 <= 6) { bCondition1 = true; } // 判斷Z0(P1) = 1 if (nCount2 === 1) { bCondition2 = true; } // 判斷P2*P4*P8=0 if (borderArr[0] * borderArr[2] * borderArr[6] === 0) { bCondition3 = true; } // 判斷P2*P4*P6=0 if (borderArr[0] * borderArr[2] * borderArr[4] === 0) { bCondition4 = true; } for (let k = 0; k < 3; k++) { if (bCondition1 && bCondition2 && bCondition3 && bCondition4) { data[lpSrc * 4 + k] = 255; bModified = true; } else { data[lpSrc * 4 + k] = 0; } } } } if (bModified) { for (let i = 0, len = data.length; i < len; i++) { dataInit[i] = data[i]; } } } } 複製程式碼
邊緣、輪廓與填充
邊緣檢測
圖片的邊緣是影象的最基本特徵,所謂邊緣是指其周圍畫素灰度有階躍變化或屋頂變化的那些畫素的集合。邊緣的種類可以分為兩種:一種稱為階躍性邊緣,它兩邊的畫素的灰度值有著顯著的不同;另一種稱為屋頂狀邊緣,它位於灰度值從增加到減少到變化轉折點。
邊緣檢測運算元檢測每個畫素到鄰域並對灰度變化率進行量化,也包括方向的確定。大多數使用基於方向導數掩模求卷積的方法。下面是幾種常用的邊緣檢測運算元:
-
Roberts邊緣檢測運算元:
Roberts邊緣檢測運算元是一種利用區域性差分運算元尋找邊緣的運算元。它由下式給出:
其中,f(x, y)是具有整數畫素座標的輸入影象,平方根運算使該處理類似於在人類視覺系統中發生的過程。
-
Sobel邊緣運算元
上面兩個卷積核形成了Sobel邊緣運算元,影象中的每個點都用這兩個核做卷積,一個核對通常的垂直邊緣影響最大,而另一個對水平邊緣影響最大。兩個卷機的最大值作為該點的輸出位。
-
Prewitt邊緣運算元
上面兩個卷積核形成了Prewitt邊緣運算元,和使用Sobel運算元的方法一樣,影象中的每個點都是用這兩個核進行卷積,取最大值作為輸出。Prewitt運算元也產生一幅邊緣幅度影象。
-
Krisch邊緣運算元
上面的8個卷積核組成了Kirsch邊緣運算元。影象中的每個點都用8個掩模進行卷積,每個掩模都對某個特定邊緣方向作出最大響應。所有8個方向中的最大值作為邊緣幅度影象的輸出。最大響應掩模的序號構成了邊緣方向的編號。
-
高斯-拉普拉斯運算元
拉普拉斯運算元是對二維函式進行運算的二階導數運算元。通常使用的拉普拉斯運算元如下:
各邊緣檢測運算元對比
運算元 | 優缺點比較 |
---|---|
Roberts | 對具有陡峭的低噪聲的影象處理效果較好,但利用Roberts運算元提取邊緣的結果是邊緣比較粗,因此邊緣定位鄙視很準確。 |
Sobel | 對灰度漸變和噪聲較多的影象處理效果比較好,Sobel運算元對邊緣定位比較準確。 |
Prewit | 對灰度漸變和噪聲較多的影象處理效果較好 |
Kirsch | 對灰度漸變和噪聲較多的影象處理效果較好 |
高斯-拉普拉斯 | 對影象中的 階段性邊緣點定位準確,對噪聲非常敏感,丟失一部分邊緣的方向資訊,造成一些不連續的邊緣檢測。 |
輪廓提取與輪廓跟蹤
輪廓提取和輪廓跟蹤的目的都是獲取影象的外部輪廓特徵。二值影象輪廓提取的演算法非常簡單,就是掏空內部點:如果原圖中一點為黑,且它的8個相鄰點都是黑色時(此時該點是內部點),則將該點刪除。用形態學的內容就是用一個九個點的結構元素對原圖進行腐蝕,再用原影象減去腐蝕影象。
影象輪廓提取影象對比:

輪廓跟蹤就是通過順序找出邊緣點來跟蹤出邊界。首先按照從左到右,從下到上的順序搜尋,找到的第一個黑點一定是最左下方的邊界點,記為A。它的右、右上、上、左上四個鄰點中至少有一個是邊界點,記為B。從B開始找起,按右、右上、上、左、左上、左下、下、右下的順序找相鄰點中的邊界點C。如果C就是A點,則表明已經轉了一圈,程式結束;否則從C點繼續找,直到找到A為止。判斷是不是邊界點很容易:如果它的上下左右四個鄰點都不是黑點則它即為邊界點。
這種方法需要對每個邊界畫素周圍的八個點進行判斷,計算量比較大。還有一種跟蹤準則:
首先按照上述方法找到最左下方的邊界點。以這個邊界點開始,假設已經沿順時針方向環繞整個影象一圈找到了所有的邊界點。由於邊界是連續的,所以每一個邊界點都可以用這個邊界點對前一個邊界點所張的角度來表示。因此可以使用下面的跟蹤準則:從第一個邊界點開始,定義初始的搜尋方向為沿左上方;如果左上方的點是黑點,則為邊界點,否則在搜尋方向的基礎上逆時針旋轉90度,繼續勇同樣的方法繼續搜尋下一個黑點,直到返回最初多邊界點為止。
輪廓跟蹤演算法示意圖如下:

種子填充
種子填充演算法是圖形學中的演算法,是輪廓提取演算法的逆運算。
種子填充演算法首先假定封閉輪廓線內某點是已知的,然後演算法開始搜尋與種子點相鄰且位於輪廓線內的點。如果相鄰點不在輪廓內,那麼就到達輪廓線的邊界;如果相鄰點位於輪廓線之內,那麼這一點就成為新的種子點,然後繼續搜尋下去。
演算法流程如下:
- 種子畫素壓入堆疊;
- 當堆疊非空時,從堆疊中推出一個畫素,並將該畫素設定成所要的值;
- 對於每個與當前畫素相鄰的四連通或八連通畫素,進行上述兩部分內容的測試;
- 若所測試的畫素在區域內沒有被填充過,則將該畫素壓入堆疊
對於第三步中四連通區域和八連通區域,解釋如下:
四連通區域中各畫素在水平和垂直四個方向上是連通的。八連通區域各畫素在水平、垂直及四個對角線方向都是連通的。