边缘检测之模板算子

2020-09-21  本文已影响0人  飞羽田海

对应GPUImage中的GPUImageSobelEdgeDetectionFilterGPUImagePrewittEdgeDetectionFilter两个类。

模板算子

边缘是图像中像素灰度值突变的结果,也就是不连续的像素,对于这些像素突变的地方,它的微积分运算中,一阶导数表现为极值点,二阶导数表现为过零点。因此,我们可以用微分算子来计算图像的边缘像素点,而这些微分算子,通常可以通过小区域的模板卷积来近似计算,这种小区域模板就是边缘检测的模板算子,这种模板卷积计算边缘像素的方法就叫做模板算子法。

1-1.png
Prewitt算子

Prewitt算子又叫交叉微分算子,通过计算中心像素周围邻域的差分来定位边缘像素。它的水平和垂直方向模板如下:

H=\begin{Bmatrix} -1 & +0 & +1\\ -1 & 0 & 1 \\ -1 & 0 & 1 \\ \end{Bmatrix} \\ V=\begin{Bmatrix} -1 & -1 & -1\\ 0 & 0 & 0 \\ 1 & 1 & 1 \end{Bmatrix}
对于图1-1中的像素点 P_5{(i,j)},Prewitt算子在水平和垂直方向上的表示为:

D_x = P_3 + P_6 + P_9 - P_1 - P_4 - P_7 \\ D_y = P_7 + P_8 + P_9 - P_1 - P_2 - P_3

写成公式则为:

D_x = P_{(i + 1,j-1)} + P_{(i+1,j)} + P_{(i+1,j+1)} - P_{(i-1,j-1)} - P_{(i-1,j)} - P_{(i-1,j+1)} \\ D_y = P_{(i-1,j+1)} + P_{(i,j+1)} + P_{(i+1,j+1)} - P_{(i-1,j-1)} - P_{(i,j-1)} - P_{(i+1,j-1)} \\ D_{(i,j)} = \sqrt{D_x^2 + D_y^2}

D_{(i,j)} 即为点P_5的梯度,表示图像在点{(i,j)}处的最大变化率,亦是该点的边缘像素值。

Sobel算子

Sobel算子常用的是3 * 3大小的模板算子,它在prewitt算子的基础上,考虑了中心像素与周围像素距离的关系,周围邻域像素距离中心像素越近,影响越大,因此,权重越大。Sobel算子常用水平和垂直方向模板分别如下:

H=\begin{Bmatrix} -1 & -2 & -1 \\ 0 & 0 & 0 \\ 1 & 2 & +1 \\ \end{Bmatrix} \\ V=\begin{Bmatrix} -1 & 0 & 1 \\ -2 & 0 & 2 \\ -1 & +0 & +1 \end{Bmatrix}

对于图1-1中的像素点 P_5{(i,j)},Sobel算子在水平和垂直方向上的表示为:

D_x = P_1 + 2 * P_2 + P_3 - P_7 - 2 * P_8 - P_9 \\ D_y = P_3 + 2 * P_6 + P_9 - P_1 - 2 * P_4 - P_7
写成公式:

D_x = P_{(i - 1,j-1)} + 2 * P_{(i,j - 1)} + P_{(i+1,j-1)} - P_{(i-1,j+1)} - 2*P_{(i,j + 1)} - P_{(i+1,j+1)} \\ D_y = P_{(i + 1,j-1)} + 2 * P_{(i+1,j)} + P_{(i+1,j+1)} - P_{(i-1,j-1)} - 2*P_{(i-1,j)} - P_{(i-1,j+1)} \\ D_{(i,j)} = \sqrt{D_x^2 + D_y^2}



有了公式我们就可以使用代码来实现了。下面是C代码实现的Sobel算子算法:
    // stride = 4 * width 
     for (int j = 0; j < height; j ++) {
          for (int k = 0; k < width; k ++) {
               if (j < 1 || k < 1 || k > width -2 || j > height -2) {
                    ptr[0] = ptr[1] = ptr[2];
                }else{
                    //H  -p1 - 2 * p2 - p3 + p7 + 2 * p8 + p9
                     //V  -p7 - 2 * p4 - p1 + p9 + 2 * p6 + p3
                    int pos = k * 4 + j * stride;
                     // tempData 是图片数据
                    int dx = tempData[pos - 4 + stride] + 2 * tempData[pos + stride] + tempData[pos + 4 + stride] - tempData[pos - 4 - stride] - 2 * tempData[pos - stride] - tempData[pos + 4 - stride];
                    int dy = tempData[pos + 4 - stride] + 2 * tempData[pos + 4] + tempData[pos + 4 + stride] - tempData[pos - 4 + stride] - 2 * tempData[pos - 4] - tempData[pos - 4 - stride];
                    ptr[0] = _CLIP3(sqrt(dx * dx + dy * dy), 0, 255);
                    pos++;
                    dx = tempData[pos - 4 + stride] + 2 * tempData[pos + stride] + tempData[pos + 4 + stride] - tempData[pos - 4 - stride] - 2 * tempData[pos - stride] - tempData[pos + 4 - stride];
                    dy = tempData[pos + 4 - stride] + 2 * tempData[pos + 4] + tempData[pos + 4 + stride] - tempData[pos - 4 + stride] - 2 * tempData[pos - 4] - tempData[pos - 4 - stride];
                    ptr[1] = _CLIP3(sqrt(dx * dx + dy * dy), 0, 255);
                    pos++;
                    dx = tempData[pos - 4 + stride] + 2 * tempData[pos + stride] + tempData[pos + 4 + stride] - tempData[pos - 4 - stride] - 2 * tempData[pos - stride] - tempData[pos + 4 - stride];
                    dy = tempData[pos + 4 - stride] + 2 * tempData[pos + 4] + tempData[pos + 4 + stride] - tempData[pos - 4 + stride] - 2 * tempData[pos - 4] - tempData[pos - 4 - stride];
                    ptr[2] = _CLIP3(sqrt(dx * dx + dy * dy), 0, 255);
                }
                ptr += 4;
            }
        }

static inline unsigned char _CLIP3(unsigned char min,unsigned char max,unsigned char v){
    
    char ret = v;
    if (ret < min) { ret = min; }
    if (ret > max) { ret = max; }
    return ret;
}
Shader

下面来看看在GPUImage中是如何使用Shader来实现算子算法的。其实用Shader来实现性能更好一些,因为顶点着色器和片元着色器本质就是一个循环结构,不需要显式的循环遍历。下面是GPUImage中使用Shader实现的Prewitt算子:

     // 为什么下面都在获取纹理转像素后的红色通道的值?因为Prewitt或Sobel算子都是作用于灰度图的,
     // 这里不光可以使用红色通道,使用绿色通道或是蓝色通道都可以。只要是单一的通道即可。

     // 左下点 相当于P7
     float bottomLeftIntensity = texture2D(inputImageTexture, bottomLeftTextureCoordinate).r;
      // 右上点 相当于P3
     float topRightIntensity = texture2D(inputImageTexture, topRightTextureCoordinate).r;
      // 左上点 相当于P1
     float topLeftIntensity = texture2D(inputImageTexture, topLeftTextureCoordinate).r;
      // 右下点 相当于P9
     float bottomRightIntensity = texture2D(inputImageTexture, bottomRightTextureCoordinate).r;
     // 左点  相当于P4
     float leftIntensity = texture2D(inputImageTexture, leftTextureCoordinate).r;
     // 右点 相当于P6
     float rightIntensity = texture2D(inputImageTexture, rightTextureCoordinate).r;
     // 下点 相当于P8
     float bottomIntensity = texture2D(inputImageTexture, bottomTextureCoordinate).r;
     // 上点 相当于P2
     float topIntensity = texture2D(inputImageTexture, topTextureCoordinate).r;
     // 水平方向 h = p7 + p8 + p9 - p1 - p2 - p3
     float h = -topLeftIntensity - topIntensity - topRightIntensity + bottomLeftIntensity + bottomIntensity + bottomRightIntensity;
     // 垂直方向 v = p3 + p6 + p9 - p1 - p4 - p7
     float v = -bottomLeftIntensity - leftIntensity - topLeftIntensity + bottomRightIntensity + rightIntensity + topRightIntensity;
      // 梯度
     float mag = length(vec2(h, v)) * edgeStrength;
     gl_FragColor = vec4(vec3(mag), 1.0);

要注意的是,在Shader中因为纹理坐标的范围为0.0 ~ 1.0,需要对纹理的宽度和高度进行归一化操作。

  glUniform1f(texelWidthUniform, 1.0/(float)width);
  glUniform1f(texelHeightUniform, 1.0/(float)height);

Sobel模板算子的Shader实现也大同小异。
效果如下:

原图.jpg prewitt模板.png Sobel模板.png

如果想进一步了解Shader与图像的关系,可以看下《Graphics Shaders: Theory and Practice》这本书(GPUImage里面的有些filter也是参考的此书)。此外这本书的中文版我也上传到了网盘,需要的可以下载:传送门,提取码:u8tl。

上一篇下一篇

猜你喜欢

热点阅读