背景
最近一个项目有很多需求涉及到了图片处理相关的技术,比如图像边缘检测及区域裁剪,灰度及黑白增强效果,滤镜,人脸检测及美化等,这些功能想纯靠Android原生api实现太难了,而且效率也不行,所以就想通过OpenCV来实现,这里通过几篇博客记录下其实现过程
OpenCV
OpenCV 的全称是 Open Source Computer Vision Library,是一个开放源代码的计算机视觉库。OpenCV 是最初由英特尔公司发起并开发,以 BSD 许可证授权发行,可以在商业和研究领域中免费使用,现在美国 Willow Garage 为 OpenCV 提供主要的支持。OpenCV 可用于开发实时的图像处理、计算机视觉以及模式识别程序,目前在工业界以及科研领域广泛采用
其中 BSD 协议是一个非常宽松的协议。简而言之,用户可以修改 OpenCV 的源代码,可以将 OpenCV 嵌入到自己的软件中,可以将包含 OpenCV的软件销售,可以用于商业产品,也可以用于科研领域。BSD 协议并不具有“传染性”,如果你的软件中使用了 OpenCV,你不需要公开代码。你可以对 OpenCV做任何操作,协议对用户的唯一约束是要在软件的文档或者说明中注明使用了
OpenCV,并附上 OpenCV 的协议。正是因为这个协议的存在,让更多的人从OpenCV中受益。
说了这么多你可能还是有点迷糊OpenCV是啥,说白了它就是一堆 C 和 C++语言的源代码文件,这些源代码文件中实现了许多常用的计算机视觉算法,而不需要开发者重复造轮子,直接使用就可以了。例如 C 接口函数 cvCanny()实现了 Canny 边缘提取算法。可以直接将这些源代码添加到我们自己的软件项目中,而不需要自己再去写代码实现 Canny 算法
开发环境
我这里使用的环境如下:
- Android Studio是3.5.2
- CMake是3.10
- NDK是20.1
- OpenCV是3.4.1
OpenCV直接在官网找对应的版本下载
下载下来是一个压缩包
其中各个目录含义:
- apk:里面包含每个CPU架构的OpenCv代理apk
- samples:官方给的样例demo,但是坑爹的都是Eclipse版本的,不过里面也有对应的apk,可以安装看看样例效果
- sdk:这个目录里就是我们接下来需要使用的
其中
- etc里是训练好的级联分类器数据
- java里是我们待会要导入的module,也就是OpenCV Android SDK
- native里是编译好的动态库和静态库文件,以及JNI层开发需要的头文件和CMake文件
导入SDK
直接在Android Studio中新建一个工程,就默认配置就行了
接下来操作如下图所示
将刚才那个sdk目录里的java目录导入进来,这时候你的工程目录可能是这样的
其中openCVLibrary341就是刚才导入的java目录,也就是OpenCV为Android开发的SDK,最好将这个module下的build.gradle中的编译参数改成跟app module一样,否则可能报错;最后把openCVLibrary341这个module添加为app module的依赖
接下来需要把动态库给添加进来,否则光java代码是用不了的;在openCVLibrary348 module下面直接新建jniLibs,然后将如下图所示的文件拷贝到该目录下
当然了,你可以选你需要的Cpu架构的动态库,最后工程如图
验证灰度处理
到此环境已经搭建好了,接下来我们验证下是否搭建成功了,验证的功能很简单,就是先加载一张资源图,然后通过OpenCV SDK做灰度处理
MainActivity代码如下
ImageView imageView;
Bitmap bitmap;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
boolean b = OpenCVLoader.initDebug();
if (b) {
Log.i("MainActivity", "Succese");
} else {
Log.i("MainActivity","Failed");
}
imageView = findViewById(R.id.img);
}
public void load(View view) {
bitmap = BitmapFactory.decodeResource(getResources(), R.mipmap.un);
imageView.setImageBitmap(bitmap);
}
public void gray(View view){
Mat src = new Mat();
Utils.bitmapToMat(bitmap,src);
Mat dst = new Mat();
Imgproc.cvtColor(src,dst,Imgproc.COLOR_RGBA2GRAY);
Utils.matToBitmap(dst, bitmap);
imageView.setImageBitmap(bitmap);
}
其中OpenCVLoader.initDebug()是加载OpenCV中的动态库
这里要注意到gray方法中有一个Mat对象,它是什么呢?在OpenCV中,Mat即为图片,所有对图片的操作都会转成对Mat对象的操作,你可以将其理解成一个多维矩阵,它是存储图像信息的类,Java层的Bitmap传到Native层都会将其转换成Mat再进行后续处理,下面会再介绍
所以上面是先通过Utils.bitmapToMat(bitmap,src)方法先将Bitmap转成Mat,然后通过Imgproc.cvtColor将图片的颜色格式从RGBA转成GRAY,也就是转成灰度,最后再将Mat转成Bitmap
如果Mat对象不使用了,需要释放掉
src.release();
dst.release();
效果如图
二值化
说完灰度处理,再看看图像识别中经常用到的二值化操作,在OpenCV中图像二值化或者叫图像阈值操作,原理就是将图像上的像素点的灰度值根据与阈值的比较设置为0或者255,这样使整个图像呈现出明显的黑白效果(这也就意味着在二值化之前通常要做灰度处理)
在没用OpenCV之前,用Android api实现二值化如下
public static Bitmap setBlackWhite(Bitmap source){
int width = source.getWidth();
int height = source.getHeight();
Bitmap binary = source.copy(Bitmap.Config.RGB_565,true);
for (int x=0; x<width; x++) {
for (int y=0; y<height; y++) {
int pixel = source.getPixel(x, y);
int red = (pixel >> 16) & 0xFF;
int green = (pixel >> 8) & 0xFF;
int blue = pixel & 0xFF;
int gray = (int) ((float) red * 0.213 + (float) green * 0.715 + (float) blue * 0.072);
if(gray <= 150){
gray = 0;
} else{
gray = 255;
}
pixel = 0xff000000 | (gray << 16) | (gray << 8) | gray;
binary.setPixel(x,y,pixel);
}
}
return binary;
}
但是这种实现效率很低,特别是图片分辨率比较高的时候,花费的时间太久了
在其OpenCV SDK中提供了Imgproc.threshold方法来实现二值化
public static double threshold(Mat src, Mat dst, double thresh, double maxval, int type)
其参数如下
- src:输入图像
- dst:输出图像
- thresh:阈值
- maxval:最大值
- type:阈值的类型
阈值的类型有如下几种
- THRESH_BINARY(二进制阈值)
- THRESH_BINARY_INV(反二进制阈值)
- THRESH_TRUNC(截断阈值)
- THRESH_TOZERO(0阈值)
- THRESH_TOZERO_INV(反0阈值)
这几个阈值作用如下:
- THRESH_BINARY:当前像素点的灰度值 > thresh ? 当前像素点值为 maxval :反之为0
- THRESH_BINARY_INV: 当前像素点的灰度值 > thresh ? 当前像素点值为0 :反之为maxval
- THRESH_TRUNC: 当前像素点灰度值 > thresh ? 当前像素点值设定为thresh :反之保持不变
- THRESH_TOZERO: 当前像素点灰度值 > thresh ? 当前像素点值保持不变 :其他情况为0
- THRESH_TOZERO_INV: 当前像素点灰度值 > thresh ? 当前像素点值设定为0 :其他情况保持不变
如何使用呢?看下面
public void binary(View view){
Imgproc.threshold(src,dst,160,250,Imgproc.THRESH_BINARY);
Utils.matToBitmap(dst,bitmap);
photo.setImageBitmap(bitmap);
}
效果如图
腐蚀
在二值化操作后,我们通常会为了让目标区域更加明显,这里我们想让文字更加突出,可以使用OpenCV里的腐蚀操作,其实与腐蚀操作相对应的还有膨胀,其中
- 膨胀操作:膨胀是求局部最大值的操作,原理是将图像与卷积核进行卷积;从图像直观看来,就是将图像光亮部分放大,黑暗部分缩小
- 腐蚀操作:腐蚀是求局部最小值,它也是需要图像与卷积核进行卷积;从图像直观看来,效果图拥有比原图更小的高亮区域,黑暗部分也就放大了
它们两都涉及一个卷积操作,其操作过程如下图
OpenCV SDK提供了Imgproc.erode方法实现腐蚀
public static void erode(Mat src, Mat dst, Mat kernel)
- src:输入图像
- dst:输出图像
- kernel:卷积核
前面两个参数好理解,第三个参数卷积核什么意思呢?首先得理解图像中的卷积操作,其实网上有很多专业的解释,我这里就粗鄙的解释下:
大家都知道图像是二维数组,这里为了方便起见,以一维数组举例,有一个原始数据[1,2,3,4,5],另外有一个卷积核,也是一个一维数组[10,20],那么卷积后的数组第一个值就是1 * 10+2 * 20,第二个值是2 * 10+3 * 20,第三个值是3 * 10+4 * 20,第四个值是4 * 10+5 * 20,看到规律了吧,其实就是错一位对应项相乘,然后求和
而图像是二维数组, 无非就是两个二维矩阵错位相乘再求和
对图像做卷积操作其实就是利用卷积核(卷积模板)在图像上滑动,在滑动一个步长的过程中,将图像像素点的灰度值与卷积核上对应值相乘得到的所有结果相加作为卷积核中间像素对应图像上像素的灰度值,重复该过程,直到滑动完图像所有像素
看下图
上图中最左边为图像二位矩阵,中间是3 * 3的卷积核矩阵,卷积核内共有九个数值,对应右上角的计算过程(有9行计算,其每一行都是图像像素值与卷积核值的对应项相乘),最终计算结果-8代替了原图像中对应位置处的1;这样卷积核沿着水平方向以一个像素点为步长滑动(当X方向滑动结束,再往下平移继续沿X方向滑动),直到滑动完所有像素点
这时候应该就能理解erode方法第三个参数的意思了,我们一般使用Imgproc.getStructuringElement获取一个卷积核,如果传null,表示使用参考点位于中心的3x3的核
接下来看看具体实现代码
public void binary(View view){
Imgproc.threshold(src,dst,160,250,Imgproc.THRESH_BINARY);
Imgproc.erode(dst,src,Imgproc.getStructuringElement(Imgproc.MORPH_RECT,new Size(2,2)));
Utils.matToBitmap(src,bitmap);
photo.setImageBitmap(bitmap);
}
其中getStructuringElement方法有两个参数
- 第一个参数MORPH_RECT表示矩形的卷积核,当然还可以选择Imgproc.MORPH_CROSS(交叉形)、Imgproc.MORPH_ELLIPSE(椭圆形)
- Size是卷积核大小
看看效果
可以看到在经过腐蚀操作后,文字在视觉上变得更加突出了
腐蚀方法在C++里还有几个重载方法,参数巨多
public static void erode(Mat src, Mat dst, Mat kernel, Point anchor, int iterations, int borderType, Scalar borderValue)
- src:输入图
- dst:输出图
- kernel:腐蚀操作的核
- anchor:锚的位置,默认值为(-1,-1),表示锚位于中心
- iterations:迭代使用腐蚀的次数,默认为1
- borderType:推断外部像素的某种边界模式,默认值为BORDER_DEFAULT
- borderValue:当边界为常数时的边界值,有默认值,一般不去管它。
编译OpenCV
如果直接使用官方提供的SDK和动态库,这样你的apk体积可能会变得很大,因为最终打包出来的.so文件很大
所以就需要你自己去编译OpenCV源码了
OpenCV的模块有很多
平时开发肯定是用不了这么多的,通常的图像处理基本只需要个core和imgproc 模块,这里因为篇幅原因,编译部分就不在继续叙述了,后面有时间单独写一篇,编译结束使用C++代码实现上面的几个功能很简单
//java
private static native void nativeEnhance(Bitmap srcBitmap,Bitmap outBitmap,int threshold,int maxThre);
//native
static void native_enhance(JNIEnv *env, jclass type, jobject srcBitmap,jobject outBitmap,jint threshold,jint maxThre){
Mat srcBitmapMat;
cv::bitmap_to_mat(env,srcBitmap,srcBitmapMat);
Mat bwDataMat(srcBitmapMat.rows, srcBitmapMat.cols, CV_8UC3);
cv::cvtColor(srcBitmapMat,bwDataMat,COLOR_RGBA2GRAY);
cv::threshold(bwDataMat,srcBitmapMat,threshold,maxThre,THRESH_BINARY);
Mat element = cv::getStructuringElement(MORPH_RECT, Size(2, 2));
cv::erode(srcBitmapMat, bwDataMat, element,Point(-1,-1),1,BORDER_CONSTANT,morphologyDefaultBorderValue());
cv::mat_to_bitmap(env,srcBitmapMat,outBitmap);
}
Mat
重点介绍下Mat对象,因为在OpenCV里做图像处理需要经常跟它打交道
Mat是OpenCV中用于存储图像信息的类,与Android中用于存储图像信息的Bitmap类似,我们都知道Bitmap有图像深度和通道数的概念;
Mat同样也有,其图像位深度如下
- CV_8U : 无符号8位整型 取值范围 0 - 255
- CV_8S : 有符号8位整型 取值范围 -128 - 127
- CV_16U : 无符号16位整型 取值范围 0 - 65535
- CV_16S : 有符号16位整型 取值范围 -32768 - 32767
- CV_32S : 有符号32位整型 取值范围 -2147483648 - 2147483647
- CV_32F : 单精度浮点数 取值范围 0.0 - 1.0
- CV_64F : 双精度浮点数 取值范围 0.0 - 1.0
其中U表示无符号,S表示符号整形,F表示浮点型
Bitmap有一个Config配置类,定义了几种像素存储方式
- ALPHA_8
- ARGB_4444
- ARGB_8888
- RGB_565
比如RGB_565表示一个像素拥有RGB三个通道占16位,Mat同样也有对应的通道概念,在CvType类中可以看到
CV_8UC1 = CV_8UC(1), CV_8UC2 = CV_8UC(2), CV_8UC3 = CV_8UC(3), CV_8UC4 = CV_8UC(4),
CV_8SC1 = CV_8SC(1), CV_8SC2 = CV_8SC(2), CV_8SC3 = CV_8SC(3), CV_8SC4 = CV_8SC(4),
CV_16UC1 = CV_16UC(1), CV_16UC2 = CV_16UC(2), CV_16UC3 = CV_16UC(3), CV_16UC4 = CV_16UC(4),
CV_16SC1 = CV_16SC(1), CV_16SC2 = CV_16SC(2), CV_16SC3 = CV_16SC(3), CV_16SC4 = CV_16SC(4),
CV_32SC1 = CV_32SC(1), CV_32SC2 = CV_32SC(2), CV_32SC3 = CV_32SC(3), CV_32SC4 = CV_32SC(4),
CV_32FC1 = CV_32FC(1), CV_32FC2 = CV_32FC(2), CV_32FC3 = CV_32FC(3), CV_32FC4 = CV_32FC(4),
CV_64FC1 = CV_64FC(1), CV_64FC2 = CV_64FC(2), CV_64FC3 = CV_64FC(3), CV_64FC4 = CV_64FC(4);
这些数据类型公式是
CV_<bit_depth>(S|U|F)C<number_of_channels>
她们的组成很容易理解,就拿我们经常使用的CV_8UC3(8位无符号整型3通道)来说,CV表示计算机视觉,8U表示8位无符号整形,3表示3通道(在OpenCv中3通道的顺序是GBR,不像Android是RGB)
至于Bitmap和Mat之间的转换,SDK中提供了一个工具类org.opencv.android.Utils,该类提供了几个静态方法
以bitmapToMat(Bitmap bmp, Mat mat, boolean unPremultiplyAlpha)方法为例,作用是将bitmap转成mat
- 第一个参数:待转换的Bitmap,支持ARGB_8888 和 RGB_565 像素类型,输出的Mat类型默认是CV_8UC4类型,大小和Bitmap一样,通道顺序为RGBA
- 第三个参数:当Bitmap像素类型为ARGB_8888时,决定是否保留透明度属性(该设置对于RGB_565类型Bitmap无效)
其它方法类似,就不再叙述了