效果图:
从左往右分别为原图、灰度图、进行边缘检测后的图
先说说Sobel边缘检测算法:
Sobel算子是计算机视觉领域的一种重要处理方法。主要用于获得数字图像的一阶梯度,常见的应用和物理意义是边缘检测。在技术上,它是一个离散的一阶差分算子,用来计算图像亮度函数的一阶梯度之近似值。在图像的任何一点使用此算子,将会产生该点对应的梯度矢量或是其法矢量。
该算子包含两组3x3的矩阵,分别为横向及纵向,将之与图像作平面卷积,即可分别得出横向及纵向的亮度差分近似值。如果以A代表原始图像,Gx及Gy分别代表经横向及纵向边缘检测的图像,其卷积因子和公式如下:
Sobel的卷积因子:
公式:
图像的每一个像素的横向及纵向梯度近似值可用以下的公式结合,来计算梯度的大小:
具体计算如下:
getPixel()方法是获取bitmap图象x, y位置的像素
可以看到上面我用到了Math.sqrt(2)这是对2进行开方是因为我用了sobel的另一种形式
Sobel算子另一种形式是各向同性Sobel(IsotropicSobel)算子,也有两个,一个是检测水平边缘的,另一个是检测垂直边缘的。各向同性Sobel算子和普通Sobel算子相比,它的位置加权系数更为准确,在检测不同方向的边沿时梯度的幅度一致。将Sobel算子矩阵中的所有2改为根号2,就能得到各向同性Sobel的矩阵。
知道了sobel算子的算法,接下来就开始实现:
Sobel.java部分代码:
@Override
public void detection(Bitmap originalBitmap) {
//原图
this.originalBitmap = originalBitmap;
//将原图灰度化
this.temp = BitmapUtil.toGrayscale(this.originalBitmap);
//图片的宽高
int w = temp.getWidth();
int h = temp.getHeight();
//存放灰度图个像素点的数值
mmap = new int[w * h];
//存放计算后各对应点的数值
tmap = new double[w * h];
//获取灰度图各像素点的数值,并赋给mmap数组
temp.getPixels(mmap, 0, temp.getWidth(), 0, 0, temp.getWidth(),
temp.getHeight());
//保存数值最大的数
max = -999;
//进行计算
for (int i = 0; i < w; i++) {
for (int j = 0; j < h; j++) {
//计算横向数值
double gx = GX(i, j, temp);
//计算纵向数值
double gy = GY(i, j, temp);
//进行开方处理
tmap[j * w + i] = Math.sqrt(gx * gx + gy * gy);
//保存最大值
if (max < tmap[j * w + i]) {
max = tmap[j * w + i];
}
}
}
}
/**
*
* 根据设定的阙值获取处理后的图片
*
* @param a
* @param b
* @param c
*
*/
public void getBitmap(double a, double b, double c){
//如果阙值为0,返回灰度图
if(a == 0 && b == 0 && c == 0){
iMain.setBitmap(temp);
return ;
}
int w = temp.getWidth();
int h = temp.getHeight();
//存放处理后的图象各像素点的数组
int[] cmap = new int[w * h];
//筛选计算
for (int i = 0; i < w; i++) {
for (int j = 0; j < h; j++) {
if (tmap[j * w + i] > max * a) {
//如果大于阙值max*a,则保存灰度图该点的像素
cmap[j * w + i] = mmap[j * w + i];
} else if (tmap[j * w + i] > max * b) {
//否则如果大于阙值max*b,则保存灰度图该点的像素+50,(变淡)
cmap[j * w + i] = getColor(mmap[j * w + i], 50);
} else if (tmap[j * w + i] > max * c) {
//否则如果大于阙值max*c,则保存灰度图该点的像素+80,(变得更淡)
cmap[j * w + i] = getColor(mmap[j * w + i], 80);
} else {
//否则该点为白色
cmap[j * w + i] = Color.WHITE;
}
}
}
//将筛选出来的结果生成bitmap
Bitmap bm = Bitmap.createBitmap(cmap, temp.getWidth(), temp.getHeight(),
Bitmap.Config.ARGB_8888);
iMain.setBitmap(bm);
}
可以看到这里我用了三个阙值,原来的算法是只用了一个阙值,但是我自己测试后,发现一个阙值处理出来的图片边缘还是不够平滑,所以写多两个阙值
颜色处理方法:
/**
* 处理颜色
* @param color
* @param value value为负数时颜色加深,为正数时颜色变淡
* @return
*/
private int getColor(int color, int value) {
int cr, cg, cb;
cr = (color & 0x00ff0000) >> 16;
cg = (color & 0x0000ff00) >> 8;
cb = color & 0x000000ff;
cr += value;
cg += value;
cb += value;
if(cr > 255){
cr = 255;
}
if(cg > 255){
cg = 255;
}
if(cb > 255){
cb = 255;
}
if(cr < 0){
cr = 0;
}
if(cg < 0){
cg = 0;
}
if(cb < 0){
cb = 0;
}
return Color.argb(255, cr, cg, cb);
}
由于代码较多,这里就一一展示了,具体可以看看我的代码:
Github地址: https://github.com/smileysx/EdgeDetection
这里几次用到了两个for循环,一张图片两个for循环,处理时间可想而知。
所以我在里面处理图片的时候开启了一个子线程去处理。
后面会说下用RenderScript处理图象,能更快处理图象。
效果图:
代码地址:
Github地址: https://github.com/smileysx/EdgeDetection
参考文章:
1. Android自动手绘,圆你儿时画家梦!