Laplacian 算子
二维函数 f ( x , y ) f(x,y) f(x,y) 的 Laplacian(拉普拉斯)变换,由以下计算公式定义:
∇ 2 f ( x , y ) = ∂ 2 f ( x , y ) ∂ 2 x + ∂ 2 f ( x , y ) ∂ 2 y ≈ ∂ ( f ( x + 1 , y ) − f ( x , y ) ) ∂ x + ∂ ( f ( x + 1 , y ) − f ( x , y ) ) ∂ y ≈ f ( x + 1 , y ) − f ( x , y ) − ( f ( x , y ) − f ( x − 1 , y ) ) + f ( x , y + 1 ) − f ( x , y ) − ( f ( x , y ) − f ( x , y − 1 ) ) ≈ f ( x + 1 , y ) + f ( x − 1 , y ) + f ( x , y − 1 ) + f ( x , y + 1 ) − 4 f ( x , y ) \begin{aligned} \nabla^2 f(x,y) & = \frac{\partial^2 f(x, y)}{\partial^2x} + \frac{\partial^2 f(x, y)}{\partial^2y} \\ & \approx \frac{\partial (f(x+1, y) - f(x,y))}{\partial x} + \frac{\partial (f(x+1, y) - f(x,y))}{\partial y} \\ & \approx f(x+1, y) - f(x,y) - (f(x, y) - f(x-1, y)) \\ & \;\;\;\; + f(x, y+1) - f(x,y) - (f(x,y) - f(x, y-1)) \\ & \approx f(x+1, y) + f(x-1, y) + f(x, y-1) + f(x, y+1) - 4f(x,y) \end{aligned} ∇2f(x,y)=∂2x∂2f(x,y)+∂2y∂2f(x,y)≈∂x∂(f(x+1,y)−f(x,y))+∂y∂(f(x+1,y)−f(x,y))≈f(x+1,y)−f(x,y)−(f(x,y)−f(x−1,y))+f(x,y+1)−f(x,y)−(f(x,y)−f(x,y−1))≈f(x+1,y)+f(x−1,y)+f(x,y−1)+f(x,y+1)−4f(x,y)
将其推广到离散的二维数组(矩阵),即矩阵的拉普拉斯变换是矩阵与拉普拉斯核(以下两种表示都可以,只差一个负号)的卷积:
l 0 = ( 0 − 1 0 − 1 4 − 1 0 − 1 0 ) , l 0 − = ( 0 1 0 1 − 4 0 0 1 0 ) \mathbf{l}_{0} = \begin{pmatrix} 0 & -1 & 0\\ -1 & 4 & -1\\ 0 & -1 & 0 \end{pmatrix}, \;\;\; \mathbf{l}_{0^-} = \begin{pmatrix} 0 & 1 & 0\\ 1 & -4 & 0\\ 0 & 1 & 0 \end{pmatrix} l0=⎝⎛0−10−14−10−10⎠⎞,l0−=⎝⎛0101−41000⎠⎞
图像矩阵与拉普拉斯核的卷积本质上是计算任意位置的值与其在水平方向和垂直方向上四个相邻点平均值之间的差值(只是相差一个4的倍数)
Laplacian 边缘检测算子不像 Sobel 和 Prewitt 算子那样对图像进行了平滑处理,所以它会对噪声产生较大的响应,误将噪声作为边缘,并且得不到有方向的边缘。无法像 Sobel 和 Prewitt 算子那样单独得到水平方向、垂直方向或者其他固定方向上的边缘。拉普拉斯算子的优点是它只有一个卷积核,所以其计算成本比其他算子要低。
拉普拉斯算子还有一些常用的形式,如
l 1 = ( − 1 − 1 − 1 − 1 8 − 1 − 1 − 1 − 1 ) , l 2 = ( 2 − 1 2 − 1 − 4 − 1 2 − 1 2 ) , l 3 = ( 0 2 0 2 − 8 2 0 2 0 ) , l 4 = ( 2 0 2 0 − 8 0 2 0 2 ) , \mathbf{l}_{1} = \begin{pmatrix} -1 & -1 & -1\\ -1 & 8 & -1\\ -1 & -1 & -1 \end{pmatrix}, \;\;\; \mathbf{l}_{2} = \begin{pmatrix} 2 & -1 & 2\\ -1 & -4 & -1\\ 2 & -1 & 2 \end{pmatrix}, \;\;\; \mathbf{l}_{3} = \begin{pmatrix} 0 & 2 & 0\\ 2 & -8 & 2\\ 0 & 2 & 0 \end{pmatrix}, \;\;\; \mathbf{l}_{4} = \begin{pmatrix} 2 & 0 & 2\\ 0 & -8 & 0\\ 2 & 0 & 2 \end{pmatrix}, \;\;\; l1=⎝⎛−1−1−1−18−1−1−1−1⎠⎞,l2=⎝⎛2−12−1−4−12−12⎠⎞,l3=⎝⎛0202−82020⎠⎞,l4=⎝⎛2020−80202⎠⎞,
拉普拉斯核内所有值的核必须等于0,这样就使得在恒等灰度值区域不会产生错误的边缘,而且上述几种形式的拉普拉斯算子均是不可分离的。
Python实现
拉普拉斯边缘检测的实现步骤
-
图像矩阵与拉普拉斯核卷积
-
通过第一步得到的卷积结果,得到边缘的二值化显示。与 Sobel 和 Prewitt 对卷积结果取绝对值来衡量边缘强度不同,拉普拉斯通过一下方式对边缘进行灰度二值化:
e d g e ( r , c ) = { 255 , c o n v _ l a p > 0 0 , c o n v _ l a p ≤ 0 \mathbf{edge}(r,c) = \begin{cases} 255, & \mathbf{conv\_lap} > 0 \\ 0, & \mathbf{conv\_lap} \leq 0 \end{cases} edge(r,c)={ 255,0,conv_lap>0conv_lap≤0
除了可以通过阈值化得到边缘图,还可以通过以下定义得到水墨效果的边缘图。
a b s t r a c t i o n ( r , c ) = { 255 , c o n v _ l a p > 0 255 ∗ ( 1 − tanh ( c o n v _ l a p ( r , c ) ) ) , c o n v _ l a p ≤ 0 \mathbf{abstraction}(r,c) = \begin{cases} 255, & \mathbf{conv\_lap} > 0 \\ 255*(1-\tanh(\mathbf{conv\_lap}(r,c))), & \mathbf{conv\_lap} \leq 0 \end{cases} abstraction(r,c)={ 255,255∗(1−tanh(conv_lap(r,c))),conv_lap>0conv_lap≤0
OpenCV函数
void cv::Laplacian(InputArray src,
OutputArray dst,
int ddepth,
int ksize = 1,
double scale = 1,
double delta = 0,
int borderType = BORDER_DEFAULT
)
//Python:
dst =cv.Laplacian( src, ddepth[, dst[, ksize[, scale[, delta[, borderType]]]]]
参数 | 解释 |
---|---|
ksize | 卷积核大小,大小必须为正数和奇数。等于1时为 l 0 \mathbf{l}_0 l0 卷积核 |
scale | 比例系数 |
delta | 平移系数 |
borderType | 边界扩充类型 |
高斯拉普拉斯(LoG)边缘检测
拉普拉斯边缘检测算子没有对图像做平滑处理,会对噪声产生明显的响应,所以在用拉普拉斯核进行边缘检测时,首先要对图像进行高斯平滑处理,然后再与拉普拉斯核进行卷积运算。这里进行了两次卷积运算,那么有没有可能用一次卷积运算就可以实现类似的效果呢?可以的。可以利用二维高斯函数的拉普拉斯变换:
g a u s s ( x , y , σ ) = 1 2 π σ 2 exp ( − x 2 + y 2 2 σ 2 ) \mathbf{gauss}(x, y, \sigma) = \frac{1}{2\pi\sigma^2} \exp(-\frac{x^2+y^2}{2\sigma^2}) gauss(x,y,σ)=2πσ21exp(−2σ2x2+y2)
∇ 2 g a u s s ( x , y , σ ) = ∂ 2 g a u s s ( x , y , σ ) ∂ 2 x + ∂ 2 g a u s s ( x , y , σ ) ∂ 2 y = 1 2 π σ 2 ∂ ( − x σ 2 exp ( − x 2 + y 2 2 σ 2 ) ) ∂ x + 1 2 π σ 2 ∂ ( − y σ 2 exp ( − x 2 + y 2 2 σ 2 ) ) ∂ y = 1 2 π σ 4 ( x 2 σ 2 − 1 ) exp ( − x 2 + y 2 2 σ 2 ) + 1 2 π σ 4 ( y 2 σ 2 − 1 ) exp ( − x 2 + y 2 2 σ 2 ) = 1 2 π σ 4 ( x 2 + y 2 σ 2 − 2 ) exp ( − x 2 + y 2 2 σ 2 ) \begin{aligned} \nabla^2 \mathbf{gauss}(x, y, \sigma) & = \frac{\partial^2 \mathbf{gauss}(x, y, \sigma)}{\partial^2x} + \frac{\partial^2 \mathbf{gauss}(x, y, \sigma)}{\partial^2y} \\ & = \frac{1}{2\pi\sigma^2}\frac{\partial (-\frac{x}{\sigma^2} \exp(-\frac{x^2+y^2}{2\sigma^2}))}{\partial x} + \frac{1}{2\pi\sigma^2}\frac{\partial (-\frac{y}{\sigma^2} \exp(-\frac{x^2+y^2}{2\sigma^2}))}{\partial y} \\ & = \frac{1}{2\pi\sigma^4} (\frac{x^2}{\sigma^2} - 1) \exp(-\frac{x^2+y^2}{2\sigma^2}) + \frac{1}{2\pi\sigma^4} (\frac{y^2}{\sigma^2} - 1) \exp(-\frac{x^2+y^2}{2\sigma^2})\\ & = \frac{1}{2\pi\sigma^4} (\frac{x^2+y^2}{\sigma^2} - 2) \exp(-\frac{x^2+y^2}{2\sigma^2}) \end{aligned} ∇2gauss(x,y,σ)=∂2x∂2gauss(x,y,σ)+∂2y∂2gauss(x,y,σ)=2πσ21∂x∂(−σ2xexp(−2σ2x2+y2))+2πσ21∂y∂(−σ2yexp(−2σ2x2+y2))=2πσ41(σ2x2−1)exp(−2σ2x2+y2)+2πσ41(σ2y2−1)exp(−2σ2x2+y2)=2πσ41(σ2x2+y2−2)exp(−2σ2x2+y2)
通常称 ∇ 2 g a u s s ( x , y , σ ) \nabla^2 \mathbf{gauss}(x, y, \sigma) ∇2gauss(x,y,σ) 为高斯拉普拉斯(Laplician of Gaussian, LoG),这是高斯拉普拉斯边缘检测的基底。高斯拉普拉斯边缘检测的具体步骤如下:
-
构建窗口大小为 H × W H \times W H×W 、标准差为 σ \sigma σ 的 LoG 卷积核
L o G H × W = [ ∇ 2 g a u s s ( w − W − 1 2 , h − H − 1 2 , σ ) ] 0 ≤ h < H , 0 ≤ w < W \mathbf{LoG}_{H \times W} = [\nabla^2 \mathbf{gauss}(w - \frac{W-1}{2}, h - \frac{H-1}{2}, \sigma) ]_{0\leq h < H, 0\leq w < W} LoGH×W=[∇2gauss(w−2W−1,h−2H−1,σ)]0≤h<H,0≤w<W
其中 H,W 均为奇数且一般 H = W H=W H=W ,卷积核的锚点位置在 ( W − 1 2 , H − 1 2 ) (\frac{W-1}{2}, \frac{H-1}{2}) (2W−1,2H−1) -
图像矩阵与 L o G H × W \mathbf{LoG}_{H \times W} LoGH×W 核卷积结果记为 I _ C o n v _ L o G \mathbf{I\_Conv\_LoG} I_Conv_LoG
-
边缘二值化显示
e d g e ( r , c ) = { 255 , I _ C o n v _ L o G > 0 0 , I _ C o n v _ L o G ≤ 0 \mathbf{edge}(r,c) = \begin{cases} 255, & \mathbf{I\_Conv\_LoG} > 0 \\ 0, & \mathbf{I\_Conv\_LoG} \leq 0 \end{cases} edge(r,c)={ 255,0,I_Conv_LoG>0I_Conv_LoG≤0
这样高斯拉普拉斯边缘检测的效果与先进行高斯平滑,然后再进行拉普拉斯边缘检测的效果是类似的。
因为
∇ 2 g a u s s ( x , y , σ ) = 1 σ 2 [ ( x 2 σ 2 − 1 ) g a u s s ( x , σ ) ] g a u s s ( y , σ ) + 1 σ 2 [ ( y 2 σ 2 − 1 ) g a u s s ( y , σ ) ] g a u s s ( x , σ ) \nabla^2 \mathbf{gauss}(x, y, \sigma) = \frac{1}{\sigma^2}[(\frac{x^2}{\sigma^2} - 1) \mathbf{gauss}(x, \sigma)] \mathbf{gauss}(y, \sigma) + \frac{1}{\sigma^2}[(\frac{y^2}{\sigma^2} - 1) \mathbf{gauss}(y, \sigma)] \mathbf{gauss}(x, \sigma) ∇2gauss(x,y,σ)=σ21[(σ2x2−1)gauss(x,σ)]gauss(y,σ)+σ21[(σ2y2−1)gauss(y,σ)]gauss(x,σ)
所以高斯拉普拉斯卷积核可以分解为两个可分离的卷积核的和,其中一维高斯函数 g a u s s ( x , σ ) = 1 2 π σ exp ( − x 2 2 σ 2 ) \mathbf{gauss}(x, \sigma) = \frac{1}{\sqrt{2\pi \sigma}}\exp(-\frac{x^2}{2\sigma^2}) gauss(x,σ)=2πσ1exp(−2σ2x2),因此可以利用卷积的加法分配律和结合律减少执行时间。
C++实现
void getSeqLoGKernel(float sigma, int length, Mat& kernelX, Mat& kernelY)
{
//分配内存
kernelX.create(Size(length, 1) , CV_32FC1);
kernelY.create(Size(1, length) , CV_32FC1);
int center = (length - 1) / 2;
double sigma2 = pow(sigma, 2.0);
//构建可分离的高斯拉普拉斯核
for(int c = 0; c < length; c++)
{
float norm2 = pow(c - center, 2.0);
kernelX.at<float>(c, 0) = exp(-norm2 / (2 * sigma2));
kernelY.at<float>(0, c) = (norm2 / sigma2 - 1.0) * kernelX.at<float>(c, 0);
}
}
void conv2D(const Mat& src, Mat& kernel, Mat& dst, int ddepth, Point anchor=Point(-1, -1), int borderType=BORDER_DEFAULT)
{
//卷积运算第一步:卷积核逆时针翻转180
Mat kernelFlip;
flip(kernel, kernelFlip, -1);
//卷积运算第二步
filter2D(src, dst, ddepth, kernel, anchor, 0.0, borderType);
}
void sepConv2D_X_Y(const Mat& src, Mat& src_kerX_kerY, int ddepth, Mat& kernelX, Mat& kernelY,Point anchor=Point(-1, -1), int borderType=BORDER_DEFAULT)
{
// 输入矩阵与水平方向上的卷积核的卷积
Mat src_kerX;
conv2D(src, kernelX, src_kerX, ddepth, anchor, borderType);
// 垂直
conv2D(src_kerX, kernelY, src_kerX_kerY, ddepth, anchor, borderType);
}
void sepConv2D_Y_X(const Mat& src, Mat& src_kerY_kerX, int ddepth, Mat& kernelX, Mat& kernelY,Point anchor=Point(-1, -1), int borderType=BORDER_DEFAULT)
{
// 输入矩阵与水平方向上的卷积核的卷积
Mat src_kerY;
conv2D(src, kernelY, src_kerY, ddepth, anchor, borderType);
// 垂直
conv2D(src_kerY, kernelX, src_kerY_kerX, ddepth, anchor, borderType);
}
Mat LoG(const Mat& image, float sigma, int win)
{
Mat kernelX, kernelY;
// 得到两个分离卷积核
getSeqLoGKernel(sigma, win, kernelX, kernelY);
//先进行水平卷积,再进行垂直卷积
Mat covXY;
sepConv2D_X_Y(image, covXY, CV_32FC1, kernelX, kernelY);
//卷积核转置
Mat kernelX_T = kernelX.t();
Mat kernelY_T = kernelY.t();
// 先进行垂直卷积,再进行水平卷积
Mat covYX;
sepConv2D_Y_X(image, covYX, CV_32FC1, kernelX_T, kernelY_T);
//计算两个卷积结果的和,得到高斯拉普拉斯卷积
Mat LoGCov;
add(covXY, covYX, LoGCov);
return LoGCov;
}
int main()
{
Mat img = imread("img7.jpg", 0);
float sigma = 2;
int win = 13;
Mat log = LoG(img, sigma, win);
Mat edge;
threshold(log, edge, 0, 255, THRESH_BINARY);
imshow("edge", edge);
waitKey(0);
destroyAllWindows();
return 0;
}
对于高斯拉普拉斯核的尺寸,一般取 ( 6 ∗ σ + 1 ) × ( 6 ∗ σ + 1 ) (6*\sigma + 1) \times (6*\sigma + 1) (6∗σ+1)×(6∗σ+1) ,即大于 6 σ 6\sigma 6σ 的最小奇数,这样的到的边缘效果会比较好。从效果可以看出,随着标准差的增大,所得到的边缘的尺度也越来越大,越来越失去图像边缘的细节,显得更加粗略。
虽然高斯拉普拉斯核可分离,但是当核的尺寸较大时,计算量仍然很大,下面通过高斯差分近似高斯拉普拉斯,从而进一步减少计算量。
高斯差分(DoG)边缘检测
高斯拉普拉斯核高斯差分的关系
二维高斯函数对 σ \sigma σ 的一阶偏导数如下:
∂ g a u s s ( x , y , σ ) ∂ σ = − 1 π σ 3 exp ( − x 2 + y 2 2 σ 2 ) + x 2 + y 2 2 π σ 5 exp ( − x 2 + y 2 2 σ 2 ) = 1 2 π σ 3 ( x 2 + y 2 σ 2 − 2 ) exp ( − x 2 + y 2 2 σ 2 ) \begin{aligned} \frac{\partial \mathbf{gauss}(x, y, \sigma)}{\partial \sigma} & = -\frac{1}{\pi \sigma^3} \exp(-\frac{x^2+y^2}{2\sigma^2}) + \frac{x^2+y^2}{2\pi \sigma^5} \exp(-\frac{x^2+y^2}{2\sigma^2}) \\ & = \frac{1}{2\pi \sigma^3}(\frac{x^2+y^2}{\sigma^2} - 2) \exp(-\frac{x^2+y^2}{2\sigma^2}) \end{aligned} ∂σ∂gauss(x,y,σ)=−πσ31exp(−2σ2x2+y2)+2πσ5x2+y2exp(−2σ2x2+y2)=2πσ31(σ2x2+y2−2)exp(−2σ2x2+y2)
显然, ∂ g a u s s ( x , y , σ ) ∂ σ \frac{\partial \mathbf{gauss}(x, y, \sigma)}{\partial \sigma} ∂σ∂gauss(x,y,σ) 和高斯拉普拉斯 ∇ 2 g a u s s ( x , y , σ ) \nabla^2 \mathbf{gauss}(x, y, \sigma) ∇2gauss(x,y,σ) 有如下关系:
σ ∇ 2 g a u s s ( x , y , σ ) = ∂ g a u s s ( x , y , σ ) ∂ σ \sigma \nabla^2 \mathbf{gauss}(x, y, \sigma) = \frac{\partial \mathbf{gauss}(x, y, \sigma)}{\partial \sigma} σ∇2gauss(x,y,σ)=∂σ∂gauss(x,y,σ)
有根据一阶导数定义的到:
∂ g a u s s ( x , y , σ ) ∂ σ = lim k → 1 g a u s s ( x , y , k ∗ σ ) − g a u s s ( x , y , σ ) k ∗ σ − σ ≈ g a u s s ( x , y , σ ) − g a u s s ( x , y , k ∗ σ ) k ∗ σ − σ \begin{aligned} \frac{\partial \mathbf{gauss}(x, y, \sigma)}{\partial \sigma} & = \lim_{k\to1} \frac{\mathbf{gauss}(x, y, k*\sigma)-\mathbf{gauss}(x, y, \sigma)}{k * \sigma - \sigma} \\ & \approx \frac{\mathbf{gauss}(x, y, \sigma)-\mathbf{gauss}(x, y, k*\sigma)}{k * \sigma - \sigma} \end{aligned} ∂σ∂gauss(x,y,σ)=k→1limk∗σ−σgauss(x,y,k∗σ)−gauss(x,y,σ)≈k∗σ−σgauss(x,y,σ)−gauss(x,y,k∗σ)
根据上面两个公式,显然可以得到高斯拉普拉斯的近似值
∇ 2 g a u s s ( x , y , σ ) ≈ g a u s s ( x , y , k ∗ σ ) − g a u s s ( x , y , σ ) σ 2 ( k − 1 ) \nabla^2 \mathbf{gauss}(x, y, \sigma) \approx \frac{\mathbf{gauss}(x, y, k*\sigma)-\mathbf{gauss}(x, y, \sigma)}{\sigma^2(k-1)} ∇2gauss(x,y,σ)≈σ2(k−1)gauss(x,y,k∗σ)−gauss(x,y,σ)
该近似值常称为高斯差分(Difference of Gaussian,DoG)。当 k = 0.95 时,高斯拉普拉斯核高斯差分的值是近似相等的。
高斯差分是高斯差分边缘检测的基底,高斯差分边缘检测的步骤如下:
-
构建窗口大小为 H × W H \times W H×W 、标准差为 σ \sigma σ 的 LoG 卷积核
D o G H × W = [ D o G ( w − W − 1 2 , h − H − 1 2 , σ ) ] 0 ≤ h < H , 0 ≤ w < W \mathbf{DoG}_{H \times W} = [\mathbf{DoG}(w - \frac{W-1}{2}, h - \frac{H-1}{2}, \sigma) ]_{0\leq h < H, 0\leq w < W} DoGH×W=[DoG(w−2W−1,h−2H−1,σ)]0≤h<H,0≤w<W
其中 H,W 均为奇数且一般 H = W H=W H=W ,卷积核的锚点位置在 ( W − 1 2 , H − 1 2 ) (\frac{W-1}{2}, \frac{H-1}{2}) (2W−1,2H−1) -
图像矩阵与 D o G H × W \mathbf{DoG}_{H \times W} DoGH×W 核卷积结果记为 I _ C o n v _ D o G \mathbf{I\_Conv\_DoG} I_Conv_DoG
-
与拉普拉斯边缘检测相同的二值化显示
高斯差分核是两个归一化的高斯核的差,已知高斯核又是可分离的,所以真正在程序实现时,为了减少计算量,可以不用创建高斯差分核,而是根据卷积的加法分配律和结合律的性质,图像矩阵分别与两个高斯核卷积,然后做差,用来代替第一步核第二步操作。
C++实现
Mat gaussConv(const Mat& image, float sigma, int s)
{
//构建水平方向上的非归一化的高斯核
Mat xkernel = Mat::zeros(1, s, CV_32FC1);
// 中心位置
int cs = (s - 1) / 2;
//方差
float sigma2 = pow(sigma, 2.0);
for (int c = 0; c < s; c++)
{
float norm2 = pow(float(c - cs), 2.0);
xkernel.at<float>(0, c) = exp(-norm2 / (2 * sigma2));
}
//将 xkernel 转置,得到垂直方向上的卷积核
Mat ykernel = xkernel.t();
Mat gauConv;
sepConv2D_X_Y(image, gauConv, CV_32F, xkernel, ykernel);
gauConv.convertTo(gauConv, CV_32F, 1.0/sigma2);
return gauConv;
}
Mat DoG(const Mat& image, float sigma, int s, float k)
{
//与标准差为 sigma 的非归一化的高斯核卷积
Mat imageG = gaussConv(image, sigma, s);
//与标准差为 k*sigma 的非归一化的高斯核卷积
Mat imageGk = gaussConv(image, k*sigma, s);
//两个高斯卷积结果做差
Mat dog = imageGk - imageG;
return dog;
}
可以看到,与高斯拉普拉斯检测效果对比,差别很小。
Marr-Hildreth 边缘检测
高斯差分核高斯拉普拉斯是 Marr-Hildreth 边缘检测的基底。对于高斯差分和高斯拉普拉斯边缘检测,最后一步只是简单地进行阈值化处理,显然所得到的边缘很粗略,不够精准,Marr-Hildreth 边缘检测可以简单地理解为对高斯差分和高斯拉普拉斯检测到的边缘的细化,就像 Canny 对 Sobel、Prewitt检测到的边缘的细化一样。步骤如下:
- 构建窗口大小为 H × W H\times W H×W 的高斯拉普拉斯或者高斯差分卷积核
- 图像矩阵与 L o G \mathbf{LoG} LoG 核或者 D o G \mathbf{DoG} DoG 核卷积
- 通过第二步得到的卷积结果寻找零点位置,过零点位置即为边缘位置。
Marr-Hildreth 边缘检测只是将高斯差分和高斯拉普拉斯边缘检测最后一步的阈值化处理,改成了寻找过零点位置的操作。如下图所示,其中图a显示的是 f ( x ) = e x − 1 e x + 1 f(x) = \frac{e^x -1}{e^x+1} f(x)=ex+1ex−1 曲线,图b显示的是对 f ( x ) f(x) f(x) 的一阶导数曲线,相当于 Sobel 或 Prewitt 提到的差分运算, ∣ f ′ ( x ) ∣ |f'(x)| ∣f′(x)∣ 反映的是 f ( x ) f(x) f(x) 的变化率,等价于边缘强度的概念。对于该函数而言 ∣ f ′ ( x ) ∣ |f'(x)| ∣f′(x)∣ 在 x=0 处是最大的,那么对应到 f ( x ) f(x) f(x) 在 x=0 处的函数值变化率是最大的,即边缘强度最大处;而二阶导数 f ′ ′ ( x ) f''(x) f′′(x) 在 x=0 处的函数值是等于 0 的,即 x=0 就是 f ′ ′ ( x ) f''(x) f′′(x) 的过零点,显然二阶导数的过零点位置也对应到 f ( x ) f(x) f(x) 的变化率最大处,即边缘强度最大值。
对于连续函数 g ( x ) g(x) g(x) ,如果 g ( x 1 ) ∗ g ( x 2 ) < 0 g(x_1) * g(x_2) < 0 g(x1)∗g(x2)<0 ,即 g ( x 1 ) g(x_1) g(x1) 和 g ( x 2 ) g(x_2) g(x2) 异号,那么在 x 1 x_1 x1 和 x 2 x_2 x2 之间,一定存在 x 0 x_0 x0 使得 g ( x 0 ) = 0 g(x_0)=0 g(x0)=0 ,即 x 0 x_0 x0 为 g ( x ) g(x) g(x) 的零点,推广到二维函数,就是 Marr-Hildreth 寻找零点位置的主要思想。
常用的寻找零点的方式有两种
-
针对图像矩阵与高斯差分核(或者高斯拉普拉斯)的卷积结果,对每一个位置判断以该位置为中心的 3 × 3 3 \times 3 3×3 邻域内的上/下方向、左/右方向、左上/右下方向、右上/左下方向的值是否有异号出现,即如下四种情况
对于这四种情况,只要有一种情况出现异号,该位置 ( r , c ) (r,c) (r,c) 就是过零点,即为边缘点。
-
与第一种类似,只是首先计算左上、右上、左下、右下的 4 个 2 × 2 2\times 2 2×2 邻域内的均值
对于这四个邻域内的均值,只要任意两个均值是异号的,该位置就是过零点,即为边缘点。
C++实现
void zero_cross_default(const Mat& src, Mat& dst)
{
src.convertTo(src, CV_32FC1);
// 输入图像矩阵的高、宽
int rows = src.rows;
int cols = src.cols;
//零交叉点
for(int r = 1; r < rows - 2; r++)
{
for(int c = 1; c < cols - 2; c++)
{
// 上下方向
if(src.at<float>(r-1, c) * src.at<float>(r+1, c) < 0)
{
dst.at<uchar>(r, c) = 255;
continue;
}
// 左右方向
if(src.at<float>(r, c-1) * src.at<float>(r, c+1) < 0)
{
dst.at<uchar>(r, c) = 255;
continue;
}
// 左上 右下方向
if(src.at<float>(r-1, c-1) * src.at<float>(r+1, c+1) < 0)
{
dst.at<uchar>(r, c) = 255;
continue;
}
// 左下 右上方向
if(src.at<float>(r-1, c+1) * src.at<float>(r+1, c-1) < 0)
{
dst.at<uchar>(r, c) = 255;
continue;
}
}
}
}
void zero_cross_mean(const Mat& src, Mat& dst)
{
src.convertTo(src, CV_32FC1);
dst.convertTo(dst, CV_8UC1);
// 输入图像矩阵的高、宽
int rows = src.rows;
int cols = src.cols;
double minValue;
double maxValue;
//存储四个方向的均值
Mat temp(1, 4, CV_32FC1);
//零交叉点
for(int r = 1; r < rows - 1; r++)
{
for(int c = 1; c < cols - 1; c++)
{
// 左上
Mat left_top(src, Rect(c-1, r-1, 2, 2));
temp.at<float>(0, 0) = mean(left_top)[0];
// 右上
Mat right_top(src, Rect(c, r-1, 2, 2));
temp.at<float>(0, 1) = mean(right_top)[0];
// 左下
Mat left_bottom(src, Rect(c-1, r, 2, 2));
temp.at<float>(0, 2) = mean(left_bottom)[0];
// 右下
Mat right_bottom(src, Rect(c, r, 2, 2));
temp.at<float>(0, 3) = mean(right_bottom)[0];
minMaxLoc(temp, &minValue, &maxValue);
//最大值和最小值异号,高位置为过零点
if(minValue * maxValue < 0)
{
dst.at<uchar>(r, c) = 255;
}
}
}
}
Mat Marr_Hildreth(const Mat& image, int win, float sigma, int type)
{
// 高斯拉普拉斯
Mat dog = DoG(image, sigma, win, 1.05);
// 过零点
// Mat zeroCrossImage;
Mat zeroCrossImage = Mat::zeros(image.rows, image.cols, CV_8UC1);
if(type == 0)
{
zero_cross_default(dog, zeroCrossImage);
}
else
{
zero_cross_mean(dog, zeroCrossImage);
}
return zeroCrossImage;
}
上图显示了使用不同参数对原图进行 Marr-Hildreth 边缘检测的效果,相对于高斯差分和高斯拉普拉斯边缘检测相当于进行了细化操作,所得到的边缘更准确,而且产生了封闭的边缘,这是Canny 边缘检测做不到的。当然了,也比 Canny 更加耗时。