源碼地址:github.com/weiruifeng/…
數字圖像處理(Digital Image Processing)是指用計算機進行的處理。提及數字圖像處理你們都會想到C++有不少庫和算法,MATLAB的方便,但自從有了canvas,JavaScript能夠對圖像進行像素級的操做,甚至還能夠直接處理圖像的二進制原始數據。javascript
利用 fileReader 和 canvas 配合獲取圖像html
<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的數據java
HTMLCanvasElement提供一個 toDataURL 方法,此方法在保存圖片的時候很是有用。它返回一個包含被類型參數規定的圖像表現格式的數據連接。 數據連接的格式爲git
data:[<mediatype>][;base64],<data>
複製代碼
mediatype 是個 MIME 類型的字符串,例如 "image/jpeg" 表示 JPEG 圖像文件。若是被省略,則默認值爲 text/plain;charset=US-ASCIIgithub
經過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)可以讓用戶改變圖像數據佔據的灰度範圍,能夠看做是 從像素到像素 的複製操做。 若是輸入圖像爲,輸出圖像爲
),則點運算可表示爲:canvas
其中放 被稱爲灰度變換函數,它描述了輸入灰度值和輸出灰度值之間的轉換關係。一旦灰度變換函數肯定,該點運算就徹底被肯定下來了。數組
點運算通常操做有灰度均衡化、線性變換、閾值變換、窗口變換、灰度拉伸等。瀏覽器
灰度直方圖用於統計一幅灰度圖像的像素點(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]];
}
}
複製代碼
灰度的線性變換就是將圖像中全部的點的灰度按照線性灰度變換函數進行變換,該線性灰度變換函數是一個一維線性函數:
灰度變換方程爲:
式中參數爲線性函數的斜率,
爲線性函數在
軸的截距,
表示輸入圖像的灰度,
表示輸出圖像的灰度。
灰度圖像有如下規律:
/** * 該函數用來對圖像灰度 * @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度,繼續勇一樣的方法繼續搜索下一個黑點,直到返回最初多邊界點爲止。
輪廓跟蹤算法示意圖以下:
種子填充算法是圖形學中的算法,是輪廓提取算法的逆運算。
種子填充算法首先假定封閉輪廓線內某點是已知的,而後算法開始搜索與種子點相鄰且位於輪廓線內的點。若是相鄰點不在輪廓內,那麼就到達輪廓線的邊界;若是相鄰點位於輪廓線以內,那麼這一點就成爲新的種子點,而後繼續搜索下去。
算法流程以下:
對於第三步中四連通區域和八連通區域,解釋以下:
四連通區域中各像素在水平和垂直四個方向上是連通的。八連通區域各像素在水平、垂直及四個對角線方向都是連通的。
本文對前端進行數字圖像處理作了一個基礎的講解,主要針對獲取圖像數據、保存圖像、點運算、幾何處理、圖像加強、數字形態學和邊緣檢測輪廓提取作了一個簡單的分析和實現,並無算法作很深的研究。