源码地址: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度,继续勇一样的方法继续搜索下一个黑点,直到返回最初多边界点为止。
轮廓跟踪算法示意图以下:
种子填充算法是图形学中的算法,是轮廓提取算法的逆运算。
种子填充算法首先假定封闭轮廓线内某点是已知的,而后算法开始搜索与种子点相邻且位于轮廓线内的点。若是相邻点不在轮廓内,那么就到达轮廓线的边界;若是相邻点位于轮廓线以内,那么这一点就成为新的种子点,而后继续搜索下去。
算法流程以下:
对于第三步中四连通区域和八连通区域,解释以下:
四连通区域中各像素在水平和垂直四个方向上是连通的。八连通区域各像素在水平、垂直及四个对角线方向都是连通的。
本文对前端进行数字图像处理作了一个基础的讲解,主要针对获取图像数据、保存图像、点运算、几何处理、图像加强、数字形态学和边缘检测轮廓提取作了一个简单的分析和实现,并无算法作很深的研究。