正 文
2013年12月,SRP项目管理系统开始接受学生报名。登录系统后,我对系统上的项目做了筛选和简单的调查后,便通过邮件联系了老师,最终成为了林老师的项目《基于机器视觉的汽车安全算法研究》中三名成员中的一员。
这是我第一次参加老师的项目,一开始我便对这个项目充满了热情。这个项目涉及的知识是我从来没有了解过的,即便是最基础的C++语言。在此,我也非常感谢林老师给予了我这个宝贵的机会,让我接触到了这个项目以及这个领域的知识。但是由于小组的项目时间安排不合理和分工不明确,我们的项目不得不延期结题,在此我也表示非常的抱歉。
在SRP项目《基于机器视觉的汽车安全算法研究》中,我主要负责的是图像处理算法的安卓移植。下面就我的工作做一个简单的总结。
(一) 项目简介
本项目是老师的SRP项目《基于机器视觉的汽车安全算法研究》。本项目主要在嵌入式系统上开发驾驶辅助系统软件,实时检测对前方车辆的定位,进行安全判别。项目的目标是构造基于计算机视觉的驾驶辅助系统、实现基于车辆检测的优化算法。最终小组将系统移植到Android平台上运行,通过对手机摄像头所采集的视频进行处理分析,来进行图像前方车辆检测,从而为车辆驾驶员提供前方车流信息以及为超车提供建议信息,同时也为智能紧急避障提供信息。
图像处理算法的算法流程图如图1所示:
图1 项目的整体算法流程图
首先用手机摄像头获取道路前方的视觉信息,打开系统的跟踪功能,系统将自动处理摄像头的视觉信息,自动跟踪前方车辆,并经过处理相邻视频帧信息后,获得前方车辆的相对距离和瞬时速度。如果车辆相对位置以及相对速度超过预警值时,还能自动报警,降低车祸的发生概率。这种车辆跟踪系统使用单个摄像头就能对车辆进行连续的监控而不会丢失目标保证了跟踪的连续性。
(二) 算法移植原理
现阶段大部分的图像处理算法都是用C++或者MATLAB语言编写的,但是这两种语言都不能直接在安卓平台上运行,所以我们需要对其进行封装成库再在安卓程序中进行调用,这就是算法的安卓移植。要将算法移植到android端,需要将C++文件通过CDT工具编译成.so文件(动态链接库文件),再通过NDK工具进行编译,在安卓程序中便可以通过调用.so动态库文件来实现相应的算法。
图像处理都需要使用到opencv包。Opencv是包含了许多图像处理运算函数的库,一般运行在WINDOWS平台,开发语言为C++。安卓程序开发者使用opencv有两种方式。一种是使用opencv的java版本的API,但是这种方式不是通过本地调用实现的,算法必须都是用java代码实现的,考虑到用java实现图像处理的算法难度较大,因此第一种方法并不适合在本项目中使用。另外一种方式就是使用opencv的c++版本的API,将本地c++代码编译成.so链接库,然后在安卓开发中通过 NDK工具对其进行调用。在本项目中,我们使用的是第二种方法。
(三) 环境的搭建
在本项目中,我们所使用的安卓开发平台是Eclipse,需要配置的环境比较复杂,下面就几个比较重要的步骤进行描述。
(1)下载并安装好必要的开发工具以及工具包,根据官方教程文档配置好环境变量。本项目在需要安装配置的工具有:Eclipse,OPENCV For Android, Android NDK,CDT,Android SDK, ADT等。
(2)在项目中新建一个jni文件,用于放置该项目的所有cpp代码。在jni文件夹下建立一个"ImgFun.cpp"的文件。其接口格式如下:
1. extern "C" {
2. JNIEXPORT jintArray JNICALL Java_com_testopencv_haveimgfun_LibImgFun_ImgFun(
3. JNIEnv* env, jobject obj, jintArray buf, int w, int h);
4. JNIEXPORT jintArray JNICALL Java_com_testopencv_haveimgfun_LibImgFun_ImgFun(
5. JNIEnv* env, jobject obj, jintArray buf, int w, int h) {
6. jint *cbuf;
7. cbuf = env->GetIntArrayElements(buf,NULL);
8. if (cbuf == NULL) {
9. return 0;
10. }
11. Mat myimg(h, w, CV_8UC4,(unsigned char*) cbuf);
12. IplImageimage=IplImage(myimg);
13. IplImage* SRC =change4channelTo3InIplImage(&image);
14. IplImage*pSrc=cvCreateImage(cvGetSize(SRC),IPL_DEPTH_8U,1);
15. cvCvtColor(SRC,pSrc,CV_BGR2GRAY);
16. //*********在这里添加C++算法接口程序部分***********//
17. //********在这里添加相应的算法操作**********//
18. return destImg; //返回处理后的图像数据
19. }
(3)配置好opencv的目录。如果不配置eclipse工程的包含目录,是找不到opencv头文件的。
(4)新建两个Mk配置文件:在jni下新建两个文件"Android.mk"文件和"Application.mk"文件,这两个文件事实上就是简单的Makefile文件。使用NDK进行编译的时候,需要使用Android.mk和Application.mk两个文件。
具体的配置文件如下:
Android.mk文件:
1. LOCAL_PATH := $(call my-dir)
2. include $(CLEAR_VARS)
3. OPENCV_LIB_TYPE:=STATIC
4. ifeq ("$(wildcard $(OPENCV_MK_PATH))","")
5. #try to load OpenCV.mk from default install location
6. include E:\java\OpenCV-2.4.5-android-sdk\sdk\native\jni\OpenCV.mk
7. else
8. include $(OPENCV_MK_PATH)
9. endif
10. LOCAL_MODULE := ImgFun
11. LOCAL_SRC_FILES := ImgFun.cpp
12. include $(BUILD_SHARED_LIBRARY)
Application.mk文件:
1. APP_STL:=gnustl_static
2. APP_CPPFLAGS:=-frtti -fexceptions
3. APP_ABI:=armeabi armeabi-v7a
(四) 安卓端算法流程以及核心步骤描述
本项目中,算法的安卓移植过程如图2所示:
图2 安卓移植过程流程图
大部分情况下,android摄像头模块不仅预览拍照这么简单,而是需要在预览视频的时候,能够做出一些检测,比如最常见的人脸检测,在未按下拍照按钮前,就检测出人脸然后矩形框标示出来,再按拍照。在本项目中,我在Activity里继承PreviewCallback这个接口。调用方法是:
1. Publicclass RectPhoto extends Activity implements SurfaceHolder.Callback, previewCallback{};
继承这个方法后,会自动重载这个函数:public voidonPreviewFrame(byte[] data, Camera camera) {}这个函数里的data就是实时预览帧视频。一旦程序调用PreviewCallback接口,就会自动调用onPreviewFrame这个函数。调用PreviewCallback的方法有三种方式调用这个回调。所谓回调就是当条件满足时,自动触发调用这个函数。这三种方法分别是:setPreviewCallback(), setOneShotPreviewCallback(), setPreviewCallbackWithBuffer(), 本项目是使用了第二种方式。如果Activity继承了PreviewCallback这个接口,只需调用这个函数
Camera.setOneShotPreviewCallback(this);
就可以了。程序会自动调用主类Activity里的onPreviewFrame函数。如果Camera.setOneShotPreviewCallback()这个函数是在主类Activity里的内部类如class A里面,里面的参数应写为Camera.setOneShotPreviewCallback(YourActivity.this)。当然这里,也可以定义一个变量,如Camera.PreviewCallback mPreviewCallback,在调用的时候用Camera.setOneShotPreviewCallback(mPreviewCallback)来完成。
按照理论,我们只要在onPreviewFrame()这个函数里写我们的处理程序就可以了。但通常不这么做,因为处理实时预览帧视频的算法可能比较复杂,耗时比较多可能会导致线程堵塞,这就需要借助AsyncTask开启一个线程在后台处理数据。在本项目中,我们定义一个FaceTask来进行图像实时处理的算法。
截取后台FaceTask类处理数据的代码片段(注意这里的代码不完整,只给出了每个类中的核心操作)
1)后台调用C++算法的程序
20. /*自定义的FaceTask类,开启一个线程分析数据*/
21. private class FaceTask extendsAsyncTask
22. {
23. private byte[] mData;
24. public FaceTask(byte[] data)
25. {
26. this.mData = data;
27. }
28. //构造函数
29. @Override
30. protected VoiddoInBackground(Void... params) {
31. size =camera.getParameters().getPreviewSize(); //获取预览大小
32. current =System.currentTimeMillis();
33. w = size.width; //宽度
34. h = size.height;
35. //视频流处理
36. final YuvImage image =new YuvImage(mData, ImageFormat.NV21, w, h, null); ByteArrayOutputStream os = newByteArrayOutputStream(mData.length);
37. byte[] tmp =os.toByteArray();
38. Bitmap bm =BitmapFactory.decodeByteArray(tmp, 0,tmp.length);
39. int[] pix = new int[w * h];
40. bm.getPixels(pix, 0, w, 0, 0, w, h);
41. myHandler.sendEmptyMessage(0x1233);
42. int[] resultInt =LibImgFun.ImgFun(pix, w, h); //调用C++程序接口
43. performance =System.currentTimeMillis() - current; //计算图像处理耗时时间
44. resultImg =Bitmap.createBitmap(w, h, Config.RGB_565); //返回的是一张图片的一个数组
45. resultImg.setPixels(resultInt, 0,w, 0, 0, w, h);
46. myHandler.sendEmptyMessage(0x1233);
47. return null;
48. }
2)预览视频帧时触发算法的后台运行
//周期性地触发取帧并处理图片。
49. class ScanThread implements Runnable{
50. public void run() {
51. while(!Thread.currentThread().isInterrupted()){
52. try {
53. if(null !=camera && isPreview)
54. {
55. traffic.schedule(new TimerTask()
56. {
57. @Override
58. public void run()
59. {
60. // 开始检测
61. camera.setOneShotPreviewCallback(MainActivity.this);
62. }
63. }, 0, 500); }
64. } catch(InterruptedException e) {
65. e.printStackTrace(); //异常处理
66. Thread.currentThread().interrupt();
67. }
68. }
69. }
3)Handler定时处理信息即画出车辆位置的矩形框,解决在异步任务中不可改变UI的问题
//Handler定时处理信息
70. final Handler myHandler = newHandler()
71. {
72. @Override
73. public voidhandleMessage(Message msg)
74. {
75. // 如果该消息是本程序所发送的
76. if (msg.what == 0x1233)
77. {
78. Toast.makeText(getApplicationContext(),"图像处理耗时"+ String.valueOf(performance) +" 毫秒",Toast.LENGTH_SHORT).show();
79. //不允许在异步任务中改变UI,所以需要一个handler来发数据在UI上画出矩形。
80. Matrix m =new Matrix();
81. Bitmapbm = Bitmap.createBitmap(resultImg, 0, 0, resultImg.getWidth(), resultImg.getHeight(), m, true);
82. imgView.setImageBitmap(bm);
83. resultImg = null;//释放内存
84. }
85. }
86. };
这三个小点都是安卓算法移植比较重要的部分。在接口处理上,我们使用了
int[] resultInt = LibImgFun.ImgFun(pix,w, h); //进行C++算法的调用
来获取从C++端传回来的位图数据,C++和JAVA的接口也是困扰我很久的问题,也是影响到项目拖期的重要原因,接口协调得不好将会导致整个项目的失败或者程序性能的恶化。因此这里的数据格式和接口的程序编写都要队员之间的默契合作。
(五) 算法移植结果截图
本人在项目中参与的是算法的安卓移植,下面仅仅对安卓端的结果进行截图展示。整个项目的成果可以参考其他组员的结果截图。
手机摄像头摄取到的原始图像如图3所示:
图3 手机摄像头摄取到的原始图像
在安卓端,前方车辆识别结果如图4所示:
图4 安卓端前方车辆识别图像
(六) 项目遇到的困难以及收获
这个SRP项目历时一年半,期间我们遇到了很多困难与困惑,但在老师耐心的指导和队友们的互相鼓励下,这些问题陆陆续续地得到了解决。项目中我遇到的主要问题有:
1. JNI环境的搭建以及OPENCV库在JAVA上的调用
2. 对视频流的处理。
3. JAVA代码和C++代码的衔接,即图片的传送接口问题。
4. 编写C++主函数代码,以适应NDK的调用。
这个期间我也遇到了很多其他的问题,这些问题通过查找书籍和百度找到了相应的解决方法。在这个项目中,我也养成了一个比较好的习惯,对于一些典型的错误,我都将它们以及对应的解决方法中记录在有道云笔记中。当项目结题的时候,每每翻阅这些项目笔记,对于项目依旧能够清晰地想起相关的细节。项目结束后,我将一些比较好的方法以及博文整理在我的CSDN博客中,这是我的博客链接:
http://write.blog.csdn.net/postlist
整理项目的资料和经典问题的解决方法逐渐变成了我的习惯,真心感谢这个项目让我培养了这种项目总结的方法。
回顾这一年来自己在这个项目的投入,投入的时间不少,但由于时间的不连续性和自己安排时间的不合理性,项目不得不延期结题,这里我也表示很遗憾。但是回首过去这一年在学科竞赛和SRP项目的收获,我才意识到竞赛和项目对于电信这个专业是多么的重要。同时,这个项目也警戒了我要合理安排时间,遇到困难不要畏缩而是要坚定地解决它们。总而言之,这个项目教给我最多的不是算法研究或者移植,更多的是培养了我的独立思考、总结归纳以及团队合作……
(七) 参考文献
[1]闫巧云,基于单目视觉与多特征的前方车辆检测算法研究[EB/0L] ,中南大学,. 2012
[2] 刘志强,基于单目视觉的车辆碰撞预警系统[EB/0L]江苏大学, 2007
[3] Android(安卓)开发通过NDK调用JNI,使用opencv做本地c++代码开发配置方法 [EB/0L].
http://blog.csdn.net/watkinsong/article/details/9849973
[4] Android摄像头:只拍摄SurfaceView预览界面特定区域内容(矩形框) [EB/0L].
http://blog.csdn.net/yanzi1225627/article/details/8580034
[6] Android开发:实时处理摄像头预览帧视频------浅析PreviewCallback,onPreviewFrame,AsyncTask的综合应用 [EB/0L].
http://blog.csdn.net/yanzi1225627/article/details/8605061
[7] Android NDK 如何使用自己的共享库 [EB/0L].
http://blog.csdn.net/vrix/article/details/7096276
[8]NDK编译后的文件如何加载到Android项目 [EB/0L].
http://blog.csdn.net/vrix/article/details/5693138
[9]NDK动态库的调用 [EB/0L].
http://blog.csdn.net/watkinsong/article/details/9924169
[10] eclipse里配置Android ndk环境,用eclipse编译.so文件[EB/0L].
http://blog.csdn.net/yanzhibo/article/details/7726997