数字图像处理-前端实现

源码地址:github.com/weiruifeng/…
数字图像处理(Digital Image Processing)是指用计算机进行的处理。提及数字图像处理你们都会想到C++有不少库和算法,MATLAB的方便,但自从有了canvas,JavaScript能够对图像进行像素级的操做,甚至还能够直接处理图像的二进制原始数据。javascript

获取数据和保存图片

获取数据

利用 fileReadercanvas 配合获取图像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)可以让用户改变图像数据占据的灰度范围,能够看做是 从像素到像素 的复制操做。 若是输入图像为A(x,y),输出图像为B(x,y),则点运算可表示为:canvas

B(x,y)=f[A(x,y)]

其中放f(D) 被称为灰度变换函数,它描述了输入灰度值和输出灰度值之间的转换关系。一旦灰度变换函数肯定,该点运算就彻底被肯定下来了。数组

点运算通常操做有灰度均衡化、线性变换、阈值变换、窗口变换、灰度拉伸等。浏览器

灰度直方图

概述

灰度直方图用于统计一幅灰度图像的像素点(0~255)的个数或者比例,从图形上来讲,灰度直方图就是一个二维图,横坐标表示灰度值(灰度级别),纵坐标表示具备各个灰度值或者灰度级别的像素在图像中出现的次数或者几率。

直方图.png

直方图图片.jpg

代码

/** * 统计数据(针对灰度图像) * @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},让n_i表示灰度i出现的次数,这样图像中灰度为 i 的像素的出现几率是:

p_x(i)=p(x=i)=\frac {n_i}{n}       0 \leq i < L (1)

L 是图像中全部的灰度数(一般为256),n是图像中全部的像素数,p_x(i) 其实是像素值为 i 的图像的直方图,归一化到 [0,1]

把对应于p_x的累积分布函数,定义为:

cdf_x(i)=\sum_{j=1}^{n}p_x(j) (2)

是图像的累计归一化直方图。

咱们建立一个形式为y=T(x)的转换,对于原始图像中的每一个值它就产生一个y,这样y的累计几率函数就能够在全部值范围内进行线性化,转换公式定义为:

cdf_y(y) = yK (3)

对于常数K,图像处理中是256。

y=T(x)带入(3)得:

cdf_y(y)=cdf_y(T(k))=cdf_x(k) (4)

将(3)(4)计算得:

y=cdf_x(k) / K (5)

公式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(x)是一个一维线性函数:

f(x) = fA * x + fB

灰度变换方程为:

D_B = f(D_A) = fA * D_A + fB

式中参数fA为线性函数的斜率,fB 为线性函数在y轴的截距,D_A表示输入图像的灰度,D_B表示输出图像的灰度。

灰度图像有如下规律:

  • fA > 1时,输出图像当对比度将增大;当fA < 1时,输出图像当对比度将减少;
  • fA = 1fB不等于0时,操做仅使全部像素当灰度值上移或下移,其效果是使整个图像更暗或更亮;
  • 若是fA < 0,暗区域将变亮,亮区域将变暗,点运算完成了图像求补运算;
  • 若是fA = 0,fB = 0时,输出图像和输入图像相同;
  • 若是fA = 1,fB = 255时,输出图像的灰度正好反转;

代码

/** * 该函数用来对图像灰度 * @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 。

f(x)=\begin{cases}0,\quad x< T \\\\255,\quad x \geq T\end{cases}

代码

/** * 该函数用来对图像进行阈值变换 * @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 。

灰度窗口变换的变换函数表达式以下:

f(x)=\begin{cases}0,\quad x< L \\\\x, \quad L\leq x\leq U \\\\ 255, \quad x > U\end{cases}

式中,L表示窗口的下限,U表示窗口的上限。

灰度的窗口变换能够用来去除背景是浅色,物体是深色的图片背景。

代码

/** * 该函数用来对图像进行窗口变换。只有在窗口范围内对灰度保持不变 * @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;
            }
        }
    }
}
复制代码

灰度拉伸变换函数

灰度拉伸与灰度线性变换有点相似,不一样之处在于灰度拉伸不是彻底的线性变换,而是分段进行线性变换。

函数表达式以下:

f(x)=\begin{cases}\frac{y_1}{x_1}x\quad x< x_1 \\\\ \frac{y_2 - y_1}{x_2 - x_1}(x - x_1) + y_1 \quad x_1\leq x\leq x_2 \\\\ \frac{255 - y_2}{255 - x_2}(x - x_2) + y_2 \quad x > x_2\end{cases}

灰度变换函数如图:

灰度拉伸变换函数.png

代码

/** * 该函数用来对图像进行灰度拉伸 * 该函数的运算结果是将原图在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中并无为镜像变换专门提供方法,但没必要紧张,至此咱们依然还没有接触到像素级的操做。在上一节中介绍了图像缩放的相关内容,其中讲到scalewidthscaleheight的绝对值大于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);
    复制代码

图像加强

图像加强是为了将图像中感兴趣的部分有选择的突出,而衰减其次要信息,从而提升图像的可读性。常见的目的有突出目标的轮廓,衰减各类噪声等。

图像加强技术一般有两类方法:空间域法和频率域法。空间域法主要在空间域中对图像像素灰度值直接进行运算处理。本章只介绍空间域法。

空间域法等图像加强技术能够用下式来描述:

g(x,y) = f(x,y) * h(x,y)

其中f(x,y)是处理前的图像,g(x, y)表示处理后的图像,h(x,y)为空间运算函数。

图像的灰度修正

图像的灰度修正是根据图像不一样的降质现象而采用不一样的修正方法。常见的方法参考点运算里面的方法。

模版操做

模版是一个矩阵方块,模版操做可看做是加权求和的过程,使用到的图像区域中的每一个像素分别于矩阵方块中的每一个元素对应相乘,全部乘积之和做为区域中心像素的新值,是数字图像处理中常常用到的一种运算方式,图像的平滑、锐化、细化以及边缘检测都要用到模版操做。

例如:有一种常见的平滑算法是将原图中一个像素的灰度值和它周围临近八个像素的灰度值相加,而后将求得的平均值(除以9)做为新图中该像素的灰度值。表示以下:

\frac{1}{9}\begin{bmatrix} 1& 1 & 1 \\\\1 & 1* & 1 \\\\ 1 & 1 & 1 \end{bmatrix}

使用模版处理图像时,要注意边界问题,由于用模版在处理边界时会报错,经常使用的处理办法有:

  • 忽略边界像素,即处理后的像素将丢掉这些像素。
  • 保留原边界像素,即复制边界像素处处理后的图像。

经常使用模版

  • 低通滤波器

    \begin{bmatrix} 1& 1 & 1 \\\\1 & 1* & 1 \\\\ 1 & 1 & 1 \end{bmatrix} *  \frac{1}{9} \begin{bmatrix} 1& 1 & 1 \\\\1 & 2* & 1 \\\\ 1 & 1 & 1 \end{bmatrix} * \frac{1}{10} \begin{bmatrix} 1& 2 & 1 \\\\2 & 4* & 2 \\\\ 1 & 2 & 1 \end{bmatrix} * \frac{1}{16}

  • 高通滤波器

    \begin{bmatrix} 0& -1 & 0 \\\\ -1 & 5 & -1 \\\\ 0 & -1 & 0 \end{bmatrix} \begin{bmatrix} -1& -1 & -1 \\\\-1 & 9 & -1 \\\\ -1 & -1 & -1 \end{bmatrix} \begin{bmatrix} 1& -2 & 1 \\\\-2 & 5 & -2 \\\\ 1 & -2 & 1 \end{bmatrix}

  • 平移和差分边缘检测

    \begin{bmatrix} 0& 0 & 0 \\\\ -1 & 1 & 0 \\\\ 0 & 0 & 0 \end{bmatrix} \begin{bmatrix} 0& -1 & 0 \\\\ 0 & 1 & 0 \\\\ 0 & 0 & 0 \end{bmatrix} \begin{bmatrix} -1& 0 & 0 \\\\ 0 & 1 & 0 \\\\ 0 & 0 & 0 \end{bmatrix}

  • 匹配滤波边缘检测

    \begin{bmatrix} -1& -1 & -1 & -1 & -1 \\\\ 0 & 0 & 0 & 0 & 0 \\\\ 1 & 1 & 1 & 1 & 1 \end{bmatrix} \begin{bmatrix} -1& 0 & 1 \\\\ -1 & 0 & 1 \\\\ -1 & 0 & 1 \\\\ -1 & 0 & 1 \\\\ -1 & 0 & 1 \end{bmatrix}

  • 边缘检测

    \begin{bmatrix} -1& 0 & -1 \\\\ 0 & 4 & 0 \\\\ -1 & 0 & -1 \end{bmatrix} \begin{bmatrix} -1& -1 & -1 \\\\ -1 & 8 & -1 \\\\ -1 & -1 & -1 \end{bmatrix} \begin{bmatrix} -1& -1 & -1 \\\\ -1 & 9 & -1 \\\\ -1 & -1 & -1 \end{bmatrix} \begin{bmatrix} 1& -2 & 1 \\\\ -2 & 4 & -2 \\\\ 1 & -2 & 1 \end{bmatrix}

  • 梯度方向检测

\begin{bmatrix} 1& 1 & 1 \\\\ 1 & -2 & 1 \\\\ -1 & -1 & -1 \end{bmatrix} \begin{bmatrix} 1& 1 & 1 \\\\ -1 & -2 & 1 \\\\ -1 & -1 & 1 \end{bmatrix} \begin{bmatrix} -1& 1 & 1 \\\\ -1 & -2 & 1 \\\\ -1 & 1 & 1 \end{bmatrix} \begin{bmatrix} -1& -1 & 1 \\\\ -1 & -2 & 1 \\\\ 1 & 1 & 1 \end{bmatrix}

\begin{bmatrix} -1& -1 & -1 \\\\ 1 & -2 & 1 \\\\ 1 & 1 & 1 \end{bmatrix} \begin{bmatrix} 1& -1 & -1 \\\\ 1 & -2 & -1 \\\\ 1 & 1 & 1 \end{bmatrix} \begin{bmatrix} 1& 1 & 1 \\\\ 1 & -2 & -1 \\\\ 1 & 1 & -1 \end{bmatrix} \begin{bmatrix} 1& 1 & 1 \\\\ 1 & -2 & -1 \\\\ 1 & -1 & -1 \end{bmatrix}

代码

/** * 模版操做 * @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];
            }
        }
    }
}
复制代码

代码中处理边界使用的是保留原边界像素。

平滑和锐化

平滑的思想是经过一点和周围几个点的运算来去除忽然变化的点,从而滤掉必定的噪声,但图像有必定程度的模糊,经常使用的模版是低通滤波器的模版。

锐化的目的是使模糊的图像变得更加清晰起来。图像的模糊实质就是图像受到平均或积分运算形成的,所以能够对图像进行逆运算如微分运算来使图像清晰话。从频谱角度来分析,图像模糊的实质是其高频份量被衰减,于是能够经过高通滤波操做来清晰图像。锐化处理也会将图片的噪声放大,所以,通常是先去除或减轻噪声后再进行锐化处理。

图像锐化通常有两种方法:微积分和高通滤波。高通滤波法能够参考高通滤波模版。微分锐化介绍一下拉普拉斯锐化。

梯度锐化

设图像为f(x, y),定义f(x, y) 在点(x, y)处的梯度矢量\overrightarrow{G}[f(x, y)]为:

\overrightarrow{G}[f(x, y)] = \begin{bmatrix} \frac{\partial f}{\partial x} \\ \frac{\partial f}{\partial y} \\ \end{bmatrix}

梯度有两个重要的性质:

梯度的方向在函数f(x, y) 最大变化率方向上

梯度的幅度用G[f(x, y)]表示,其值为:

G[f(x, y)] = \sqrt{\begin{pmatrix} \frac{\partial f}{\partial x} \\ \end{pmatrix} ^2 + \begin{pmatrix} \frac{\partial f}{\partial y} \\ \end{pmatrix} ^2}

由此式可得出这样的结论:梯度的数值就是f(x, y)在其最大变化率方向上的单位距离所增长的量。

对于离散的数字图像,上式能够改写成:

G[f(x, y)] = \sqrt{\begin{bmatrix} f(i, j) - f(i + 1, j) \end{bmatrix} ^2 + \begin{bmatrix} f(i, j) - f(i, j + 1) \end{bmatrix} ^2}

为了计算方便,也能够采用下面的近似计算公式:

G[f(x, y)] = \begin{vmatrix} f(i, j) - f(i + 1, j) \end{vmatrix} + \begin{vmatrix} f(i, j) - f(i, j + 1) \end{vmatrix}

这种梯度法又称为水平垂直差分法,还有一种是交叉地进行差分计算,称为罗伯特梯度法:

G[f(x, y)] = \sqrt{\begin{bmatrix} f(i, j) - f(i + 1, j + 1) \end{bmatrix} ^2 + \begin{bmatrix} f(i + 1, j) - f(i, j + 1) \end{bmatrix} ^2}

采用绝对差算法近似为:

G[f(x, y)] = \begin{vmatrix} f(i, j) - f(i + 1, j + 1) \end{vmatrix} + \begin{vmatrix} f(i + 1, j) - f(i, j + 1) \end{vmatrix}

因为在图像变化缓慢的地方梯度很小,因此图像会显得很暗,一般的作法是给一个阈值\Delta,若是G[f(x, y)]小于该阈值\Delta,则保持原灰度值不变;若是大于或等于阈值\Delta,则赋值为G[f(x, y)]

g(x, y) = \begin{cases} G[f(x, y)]  & (G[f(x, y)]  \geq \Delta) \\ f(x, y), & (G[f(x, y)] < \Delta) \end{cases}

基于水平垂直差分法的算法代码以下:

/** * 该函数用来对图像进行梯度锐化 * @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;
                }
            }
        }
    }
}
复制代码

拉普拉斯锐化

咱们知道,一个函数的一阶微分描述了函数图像的增加或下降,二阶微分描述的则是图像变化的速度,如急剧增加或降低仍是平缓的增加或降低。拉普拉斯运算也是偏导数运算的线性组合,并且是一种各向同性的线性运算。

\nabla ^2 {f}为拉普拉斯算子,则:

\nabla{f} = \frac{\partial ^2 f}{\partial x ^2}  + \frac{\partial ^ 2 f}{\partial y ^2}

对于离散数字图像f(i, j),其一阶偏导数为:

\begin{cases} \frac{\partial f(i, j)}{\partial x}  = \nabla _ x{f(i,j)} =  f(i, j) - f(i - 1, j)  \\  \frac{\partial f(i, j)}{\partial y}  = \nabla _ y{f(i,j)} =  f(i, j) - f(i, j - 1) \end{cases}

则其二阶偏导数为:

\begin{cases} \frac{\partial ^2 f(i, j)}{\partial x ^2}  = \nabla _ x{f(i + 1,j)} -  \nabla _ x{f(i,j)}=  f(i + 1, j) + f(i - 1, j) - 2f(i, j)  \\ \frac{\partial ^2 f(i, j)}{\partial y ^2}  = \nabla _ y{f(i, j + 1)} -  \nabla _ y{f(i,j)}=  f(i, j + 1) + f(i, j - 1) - 2f(i, j) \end{cases}

因此,拉普拉斯算子\nabla ^2 {f}为:

\nabla ^2 {f} = \frac{\partial ^2 f}{\partial x ^2} + \frac{\partial ^2 f}{\partial y ^2} =  f(i + 1, j) + f(i - 1, j) + f(i, j + 1) + f(i, j - 1) - 4f(i, j)

对于扩散现象引发的图像模糊,能够用下式来进行锐化:

g(i, j) = f(i, j) - k\tau \nabla ^2 {f(i, j)}

这里k\tau是与扩散效应有关的系数。该系数取值要合理,若是k\tau过大,图像轮廓边缘会产生过冲;反之若是k\tau太小,锐化效果就不明显。

若是令k\tau = 1,则变换公式为:

g(i, j) =  5f(i, j) - f(i + 1, j) - f(i - 1, j) - f(i, j + 1) - f(i, j - 1)

这样变能够获得一个模版矩阵:

\begin{bmatrix} 0 & -1 & 0 \\\\ -1 & 5* & -1 \\\\ 0 & -1 & 0 \end{bmatrix}

其实,咱们经过经常使用的拉普拉斯锐化模版还有另一种形式:

\begin{bmatrix} -1 & -1 & -1 \\\\ -1 & 9* & -1 \\\\ -1 & -1 & -1 \end{bmatrix}

代码参考模版中的代码。

中值滤波

原理

中值滤波是一种非线性数字滤波器技术,通常采用一个含有奇数个点的滑动窗口,将窗口中个点灰度值的中值来代替定点(通常是窗口的中心点)的灰度值。对于奇数个元素,中值是指按大小排序后,中间的数值,对于偶数个元素,中值是指排序后中间两个灰度值的平均值。

中值滤波是图像处理中的一个经常使用步骤,它对于斑点噪声和椒盐噪声来讲尤为有用。

代码

/** * 中值滤波 * @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 };

复制代码

图像形态学

形态学的理论基础是集合论。数学形态学提出了一套独特的变换和运算方法。下面咱们来看看最基本的 几种数学形态学运算。

对一个给定的目标图像X和一个结构元素S,想象一下将S在图像上移动。在每个当前位置xS[x]只有三中可能的状态:

  1. S[x] \subseteq X
  2. S[x] \subseteq X ^C
  3. S[x] \bigcap XS[x] \bigcap X^C均不为空

如图所示:

status.png

第一种状况说明S[x]X相关最大;第二种状况说明S[x]X不相关;而第三种状况说明S[x]X只是部分相关。

腐蚀和膨胀

原理

当知足条件1的点x的全体构成结构元素与图像的最大相关点集,咱们称这个点集为SX的腐蚀,当知足条件1和2的点x的全体构成元素与图像的最大相关点集,咱们称这个点集为SX的膨胀。简单的说,腐蚀能够看做是将图像X中每个与结构元素S全等的子集S[x]收缩为点x,膨胀则是将X中的每个点X扩大为S[x]

腐蚀与膨胀的操做是用一个给定的模版对图像X进行集合运算,如图所示:

Erosion.png

代码

代码为针对二值图像进行的腐蚀和膨胀算法。

/** * 说明: * 该函数用于对图像进行腐蚀运算。 * 结构元素为水平方向或垂直方向的三个点,中间点位于原点; * 或者由用户本身定义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快速并行细化算法):

一幅图像中的一个3*3区域,对各点标记名称P1,P2,···P9,其中P1位于中心。如图所示:

thin.png

若是P1=1(即黑点),下面四个条件若是同时知足,则删除P1(P1=0)

  • 2 \leq N(p1) \leq 6
  • S(p1) = 1
  • p2 \times p4 \times p6 = 0
  • p4 \times p6 \times p8 = 0

其中N(p1)p1的非零邻点的个数,S(p1)是以p2p3,···,p9为序时这些点的值从01变化的次数。

对图像中的每个点重复这一步骤,直到全部的点都不可删除为止。

代码

/** * 说明: * 该函数用于对图像进行细化运算 * 要求目标图像为只有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边缘检测算子是一种利用局部差分算子寻找边缘的算子。它由下式给出:

    g(x, y) = [\sqrt{f(x, y)} - \sqrt{f(x + 1, y + 1)}] ^ 2 + [\sqrt{f(x, y + 1)} - \sqrt{f(x + 1, y)}] ^ 2

    其中,f(x, y)是具备整数像素坐标的输入图像,平方根运算使该处理相似于在人类视觉系统中发生的过程。

  • Sobel边缘算子

    \begin{bmatrix} -1& -2 & -1 \\\\ 0 & 0 & 0 \\\\ 1 & 2 & 1 \end{bmatrix} \begin{bmatrix} -1& 0 & 1 \\\\ -2 & 0 & 2 \\\\ -1 & 0 & 1 \end{bmatrix}

    上面两个卷积核造成了Sobel边缘算子,图像中的每一个点都用这两个核作卷积,一个核对一般的垂直边缘影响最大,而另外一个对水平边缘影响最大。两个卷机的最大值做为该点的输出位。

  • Prewitt边缘算子

    \begin{bmatrix} -1& -1 & -1 \\\\ 0 & 0 & 0 \\\\ 1 & 1 & 1 \end{bmatrix} \begin{bmatrix} -1& 0 & 1 \\\\ -1 & 0 & 1 \\\\ -1 & 0 & 1 \end{bmatrix}

    上面两个卷积核造成了Prewitt边缘算子,和使用Sobel算子的方法同样,图像中的每一个点都是用这两个核进行卷积,取最大值做为输出。Prewitt算子也产生一幅边缘幅度图像。

  • Krisch边缘算子

    \begin{bmatrix} +5& +5 & +5 \\\\ -3 & 0 & -3 \\\\ -3 & -3 & -3 \end{bmatrix} \begin{bmatrix} -3& +5 & +5 \\\\ -3 & 0 & +5 \\\\ -3 & -3 & -3 \end{bmatrix} \begin{bmatrix} -3& -3 & +5 \\\\ -3 & 0 & +5 \\\\ -3 & -3 & +5 \end{bmatrix} \begin{bmatrix} -3& -3 & -3 \\\\ -3 & 0 & +5 \\\\ -3 & +5 & +5 \end{bmatrix}
    \begin{bmatrix} -3& -3 & -3 \\\\ -3 & 0 & -3 \\\\ +5 & +5 & +5 \end{bmatrix} \begin{bmatrix} -3& -3 & -3 \\\\ +5 & 0 & -3 \\\\ +5 & +5 & -3 \end{bmatrix} \begin{bmatrix} +5& -3 & -3 \\\\ +5 & 0 & -3 \\\\ +5 & -3 & -3 \end{bmatrix} \begin{bmatrix} +5& +5 & -3 \\\\ +5 & 0 & -3 \\\\ -3 & -3 & -3 \end{bmatrix}

    上面的8个卷积核组成了Kirsch边缘算子。图像中的每一个点都用8个掩模进行卷积,每一个掩模都对某个特定边缘方向做出最大响应。全部8个方向中的最大值做为边缘幅度图像的输出。最大响应掩模的序号构成了边缘方向的编号。

  • 高斯-拉普拉斯算子

    拉普拉斯算子是对二维函数进行运算的二阶导数算子。一般使用的拉普拉斯算子以下:

    \begin{bmatrix} 0& -1 & 0 \\\\ -1 & 4 & -1 \\\\ 0 & -1 & 0 \end{bmatrix} \begin{bmatrix} -1& -1 & -1 \\\\-1 & 8 & -1 \\\\ -1 & -1 & -1 \end{bmatrix}

各边缘检测算子对比

算子 优缺点比较
Roberts 对具备陡峭的低噪声的图像处理效果较好,但利用Roberts算子提取边缘的结果是边缘比较粗,所以边缘定位鄙视很准确。
Sobel 对灰度渐变和噪声较多的图像处理效果比较好,Sobel算子对边缘定位比较准确。
Prewit 对灰度渐变和噪声较多的图像处理效果较好
Kirsch 对灰度渐变和噪声较多的图像处理效果较好
高斯-拉普拉斯 对图像中的 阶段性边缘点定位准确,对噪声很是敏感,丢失一部分边缘的方向信息,形成一些不连续的边缘检测。

轮廓提取与轮廓跟踪

轮廓提取和轮廓跟踪的目的都是获取图像的外部轮廓特征。二值图像轮廓提取的算法很是简单,就是掏空内部点:若是原图中一点为黑,且它的8个相邻点都是黑色时(此时该点是内部点),则将该点删除。用形态学的内容就是用一个九个点的结构元素对原图进行腐蚀,再用原图像减去腐蚀图像。

图像轮廓提取图像对比:

轮廓提取.png

轮廓跟踪就是经过顺序找出边缘点来跟踪出边界。首先按照从左到右,从下到上的顺序搜索,找到的第一个黑点必定是最左下方的边界点,记为A。它的右、右上、上、左上四个邻点中至少有一个是边界点,记为B。从B开始找起,按右、右上、上、左、左上、左下、下、右下的顺序找相邻点中的边界点C。若是C就是A点,则代表已经转了一圈,程序结束;不然从C点继续找,直到找到A为止。判断是否是边界点很容易:若是它的上下左右四个邻点都不是黑点则它即为边界点。

这种方法须要对每一个边界像素周围的八个点进行判断,计算量比较大。还有一种跟踪准则:

首先按照上述方法找到最左下方的边界点。以这个边界点开始,假设已经沿顺时针方向环绕整个图像一圈找到了全部的边界点。因为边界是连续的,因此每个边界点均可以用这个边界点对前一个边界点所张的角度来表示。所以可使用下面的跟踪准则:从第一个边界点开始,定义初始的搜索方向为沿左上方;若是左上方的点是黑点,则为边界点,不然在搜索方向的基础上逆时针旋转90度,继续勇一样的方法继续搜索下一个黑点,直到返回最初多边界点为止。

轮廓跟踪算法示意图以下:

轮廓跟踪1.png

种子填充

种子填充算法是图形学中的算法,是轮廓提取算法的逆运算。

种子填充算法首先假定封闭轮廓线内某点是已知的,而后算法开始搜索与种子点相邻且位于轮廓线内的点。若是相邻点不在轮廓内,那么就到达轮廓线的边界;若是相邻点位于轮廓线以内,那么这一点就成为新的种子点,而后继续搜索下去。

算法流程以下:

  • 种子像素压入堆栈;
  • 当堆栈非空时,从堆栈中推出一个像素,并将该像素设置成所要的值;
  • 对于每一个与当前像素相邻的四连通或八连通像素,进行上述两部份内容的测试;
  • 若所测试的像素在区域内没有被填充过,则将该像素压入堆栈

对于第三步中四连通区域和八连通区域,解释以下:

四连通区域中各像素在水平和垂直四个方向上是连通的。八连通区域各像素在水平、垂直及四个对角线方向都是连通的。

总结

本文对前端进行数字图像处理作了一个基础的讲解,主要针对获取图像数据、保存图像、点运算、几何处理、图像加强、数字形态学和边缘检测轮廓提取作了一个简单的分析和实现,并无算法作很深的研究。

源码地址:github.com/weiruifeng/…

相关文章
相关标签/搜索