本文详解JavaScript中基于Sobel算子的图像边缘梯度计算原理。通过一阶微分近似x与y方向导数,构建3x3及5x5卷积核,结合边界处理与绝对值转换,实现高效的边缘检测功能。
边缘梯度计算原理
在前一篇关于图像膨胀与腐蚀函数的文章中,我们探讨了形态学处理的基础。本文将重点介绍如何计算图像的边缘梯度,这是图像边缘检测的核心步骤。
图像的边缘
从数学角度来看,图像边缘是如何被定义的?

在图像边缘区域,相邻像素的灰度值通常会发生显著变化。在数学表达中,导数用于衡量这种变化的速率。梯度值的剧烈波动往往预示着图像内容发生了显著改变。
为了更直观地理解,假设我们有一张一维灰度图像。下图中灰度值的“跃升”直观地表示了边缘的存在:

通过应用一阶微分求导,我们可以更清晰地观察到边缘处的“跃升”现象(在图中表现为高峰值):

基于上述分析,我们可以得出结论:通过定位那些梯度值显著大于其邻域像素的点,即可有效找到图像边缘。
近似梯度
以3x3的内核尺寸为例进行说明。
首先,计算x方向的近似导数:

接着,计算y方向的近似导数:

最后,综合计算总的梯度值:

当然,梯度计算也可以采用其他等效的数学表达形式:

Sobel算子实现
具体实现代码如下:
var Sobel = function(__src, __xorder, __yorder, __size, __borderType, __dst){
(__src && (__xorder ^ __yorder)) || error(arguments.callee, IS_UNDEFINED_OR_NULL/* {line} */);
if(__src.type && __src.type === "CV_GRAY"){
var kernel1,
kernel2,
height = __src.row,
width = __src.col,
dst = __dst || new Mat(height, width, CV_16I, 1),
dstData = dst.data
size = __size || 3;
switch(size){
case 1:
size = 3;
case 3:
if(__xorder){
kernel = [-1, 0, 1,
-2, 0, 2,
-1, 0, 1
];
}else if(__yorder){
kernel = [-1, -2, -1,
, 0, 0,
, 2, 1
];
}
break;
case 5:
if(__xorder){
kernel = [-1, -2, 0, 2, 1,
-4, -8, 0, 8, 4,
-6,-12, 0,12, 6,
-4, -8, 0, 8, 4,
-1, -2, 0, 2, 1
];
}else if(__yorder){
kernel = [-1, -4, -6, -4, -1,
-2, -8,-12, -8, -2,
, 0, 0, 0, 0,
, 8, 12, 8, 2,
, 4, 6, 4, 1
];
}
break;
default:
error(arguments.callee, UNSPPORT_SIZE/* {line} */);
}
GRAY216IC1Filter(__src, size, height, width, kernel, dstData, __borderType);
}else{
error(arguments.callee, UNSPPORT_DATA_TYPE/* {line} */);
}
return dst;
};本实现仅支持内核大小为3和5的Sobel算子,主要原因是当内核尺寸达到7或更大时,计算过程会变得相对缓慢。
该函数输出一个单通道、16位有符号整数的矩阵数据。
内部滤波器处理代码如下:
function GRAY216IC1Filter(__src, size, height, width, kernel, dstData, __borderType){
var start = size >> 1;
var withBorderMat = copyMakeBorder(__src, start, start, 0, 0, __borderType);
var mData = withBorderMat.data,
mWidth = withBorderMat.col;
var i, j, y, x, c;
var newValue, nowX, offsetY, offsetI;
for(i = height; i--;){
offsetI = i * width;
for(j = width; j--;){
newValue = 0;
for(y = size; y--;){
offsetY = (y + i) * mWidth;
for(x = size; x--;){
nowX = x + j;
newValue += (mData[offsetY + nowX] * kernel[y * size + x]);
}
}
dstData[j + offsetI] = newValue;
}
}
}通过将卷积核与矩阵数据传递给该滤波器进行处理,即可完成边缘梯度的计算。
将该滤波器独立封装的原因在于,它可以被其他类似的边缘计算函数复用,例如Laplacian(拉普拉斯)算子和Scharr算子。
转为无符号8位整数结果转换与合并
由于Sobel算子计算结果通常为16位有符号整数,无法直接显示为图像,因此我们需要一个函数将其转换为无符号8位整数矩阵。convertScaleAbs函数会对每个元素取绝对值,并将其放入Int8Array数组中。在赋值过程中,大于255的数值会自动截断为255,小于0的数值会自动转为0,因此无需额外编写函数来处理这些边界情况。
具体实现代码如下:
function convertScaleAbs(__src, __dst){
__src || error(arguments.callee, IS_UNDEFINED_OR_NULL/* {line} */);
var height = __src.row,
width = __src.col,
channel = __src.channel,
sData = __src.data;
if(!__dst){
if(channel === 1)
dst = new Mat(height, width, CV_GRAY);
else if(channel === 4)
dst = new Mat(height, width, CV_RGBA);
else
dst = new Mat(height, width, CV_8I, channel);
}else{
dst = __dst;
}
var dData = dst.data;
var i, j, c;
for(i = height; i--;){
for(j = width * channel; j--;){
dData[i * width * channel + j] = Math.abs(sData[i * width * channel + j]);
}
}
return dst;
}按比例合并值
此外,我们还需要一个函数将x方向的梯度计算值与y方向的梯度计算值进行叠加合并。
具体实现代码如下:
var addWeighted = function(__src1, __alpha, __src2, __beta, __gamma, __dst){
(__src1 && __src2) || error(arguments.callee, IS_UNDEFINED_OR_NULL/* {line} */);
var height = __src1.row,
width = __src1.col,
alpha = __alpha || 0,
beta = __beta || 0,
channel = __src1.channel,
gamma = __gamma || 0;
if(height !== __src2.row || width !== __src2.col || channel !== __src2.channel){
error(arguments.callee, "Src2 must be the same size and channel number as src1!"/* {line} */);
return null;
}
if(!__dst){
if(__src1.type.match(/CV\_\d+/))
dst = new Mat(height, width, __src1.depth(), channel);
else
dst = new Mat(height, width, __src1.depth());
}else{
dst = __dst;
}
var dData = dst.data,
s1Data = __src1.data,
s2Data = __src2.data;
var i;
for(i = height * width * channel; i--;)
dData[i] = __alpha * s1Data[i] + __beta * s2Data[i] + gamma;
return dst;
};该函数逻辑较为简单,主要作用是将两个矩阵的对应元素按照固定的比例进行加权相加。
效果图