转载请注明本文出自SilenceDut的CSDN博客(http://blog.csdn.net/ls5222325/article/details/46441693),请尊重他人的辛勤劳动成果,谢谢!
最近实现了利用OpenCV在Android平台上通过摄像头拍摄道路图像提取道路线(两条直线的方程)并得到道路的中心线位置,反馈出道路信息和摄像头当前相对道路的信息可用于小车循迹,四旋翼飞行器循迹等。在这个过程中遇到了很多坑,又一步步的爬出,所以记录下以便以后回顾也希望能帮到需要的人。
之前也看过很多关于道路循迹的文章,但只是通过调用Canny等取边缘算子得到道路的边缘然后进行Hough变换得到边缘的直线,但存在的问题是得到的边缘直线有很多条,而且只是显示出直线,后续无法做任何事情,因为得到道路线的目的是为了后续的循迹,所以这种做法的没有太大的意义。先上张效果图。
实现
首先进行OpenCV的配置,配置的过程看这里http://blog.csdn.net/nailperry/article/details/42834413,写的很详细了,免Cygwin的。
具体的主要实现部分代码如下:
public class CannySurfaceView extends SurfaceView implements SurfaceHolder.Callback, Runnable{
private static final String TAG = "CannySurfaceView::SurfaceView";
private SurfaceHolder mHolder;
private VideoCapture mCamera;
private Mat rgbMat;
private Mat mShow;
private Mat lines ;
private List mLines = new ArrayList();
private Line[] twoLines = new Line[2];
public CannySurfaceView(Context context) {
super(context);
mHolder = getHolder();
mHolder.addCallback(this);
}
public boolean openCamera() {
Log.i(TAG, "openCamera");
synchronized (this) {
releaseCamera();
mCamera = new VideoCapture(Highgui.CV_CAP_ANDROID);
if (!mCamera.isOpened()) {
mCamera.release();
mCamera = null;
Log.e(TAG, "Failed to open native camera");
return false;
}
}
return true;
}
public void releaseCamera() {
Log.i(TAG, "releaseCamera");
synchronized (this) {
if (mCamera != null) {
mCamera.release();
mCamera = null;
}
}
}
public void setupCamera(int width, int height) {
Log.i(TAG, "setupCamera("+width+", "+height+")");
if (mCamera != null && mCamera.isOpened()) {
List sizes = mCamera.getSupportedPreviewSizes();
int mFrameWidth = width;
int mFrameHeight = height;
// selecting optimal camera preview size
{
double minDiff = Double.MAX_VALUE;
for (Size size : sizes) {
if (Math.abs(size.height - height) < minDiff) {
mFrameWidth = (int) size.width;
mFrameHeight = (int) size.height;
minDiff = Math.abs(size.height - height);
}
}
}
mCamera.set(Highgui.CV_CAP_PROP_FRAME_WIDTH, mFrameWidth);
mCamera.set(Highgui.CV_CAP_PROP_FRAME_HEIGHT, mFrameHeight);
}
}
@Override
public void surfaceCreated(SurfaceHolder holder) {
rgbMat = new Mat();
mShow = new Mat();
lines = new Mat();
openCamera();
(new Thread(this)).start();
}
@Override
public void surfaceChanged(SurfaceHolder holder, int format, int width,
int height) {
setupCamera(width, height);
}
@Override
public void surfaceDestroyed(SurfaceHolder holder) {
releaseCamera();
}
protected Bitmap processFrame(VideoCapture capture) {
/*******因为在AndroidMainfext.xml中将屏幕旋转所以坐标系同样需要变换****
(0,0)**************>Y
*
*
*
*
*
*
X
//**********************************************************************/
int threshold = 150;
int minLineSize = 50;
int lineGap = 35;
double minYgap = 30;
int number=0;
double average_x1=0;
double average_y1=0;
double average_x2=0;
double average_y2=0;
String mMsg ="";
//将之前的信息清空,这里很重要,之前未清除导致严重BUG
mLines.clear();
//************************//
Mat mErodeElement = Imgproc.getStructuringElement(Imgproc.MORPH_RECT, new Size(10,25));
Mat mDilateElement = Imgproc.getStructuringElement(Imgproc.MORPH_RECT, new Size(10,25));
//从摄像头得到图像,为Mat矩阵形式
capture.retrieve(rgbMat, Highgui.CV_CAP_ANDROID_COLOR_FRAME_RGBA);
Log.i(TAG,rgbMat.cols()+"??"+rgbMat.rows());
//将摄像头得到的RGB图像转换为灰度图像,方便后续的其他算法处理
Imgproc.cvtColor(rgbMat, rgbMat, Imgproc.COLOR_BGRA2GRAY, 4);
//进行高斯滤波
Imgproc.GaussianBlur(rgbMat, rgbMat, new Size(5,5), 2.2,2);
//形态学中的腐蚀膨胀变换去除干扰的直线边缘,根据不同情况来设置结构元素mDilateElement和mErodeElement
Imgproc.dilate(rgbMat, rgbMat, mDilateElement);
Imgproc.erode(rgbMat, rgbMat, mErodeElement);
// 利用Canny算子得到道路边缘
Imgproc.Canny(rgbMat, rgbMat,50, 50);
//mShow是用来显示的RGB图像
Imgproc.cvtColor(rgbMat, mShow, Imgproc.COLOR_GRAY2BGRA, 4);
//利用Hough变换得到直线组,HoughLinesP是HoughLines的改进版,得到的直线组是以起始点和终点坐标来表示
Imgproc.HoughLinesP(rgbMat, lines, 1, Math.PI/180, threshold,minLineSize,lineGap);
for (int x = 0; x < lines.cols(); x+=1)
{
double[] vec = lines.get(0, x);
if(vec!=null) {
double x1 = vec[0],
y1 = vec[1],
x2 = vec[2],
y2 = vec[3];
Point start = new Point(x1, y1);
Point end = new Point(x2, y2);
if(!(Math.abs(x2-x1)new Line(start, end) ;
mLines.add(mSingleline);
number++;
average_x1+=x1;
average_y1+=y1;
average_x2+=x2;
average_y2+=y2;
Core.line(mShow, start, end, new Scalar(255,0,0,255), 3);
}
}
}
if(number!=0) {
average_x1/=number;
average_y1/=number;
average_x2/=number;
average_y2/=number;
double angle = MathUtils.getAngle(mLines);
// Log.i(TAG,"angle"+angle);
if(Math.abs(angle)<10) {
Line midLine = new Line(new Point(average_x1,average_y1),new Point(average_x2,average_y2));
mLines.add(midLine);
Collections.sort(mLines, new SortedPoint());
twoLines = MathUtils.getTwoLines(mLines,mLines.lastIndexOf(midLine));
if(twoLines[0].start!=null&&twoLines[0].end!=null&&twoLines[1].start!=null&&twoLines[1].end!=null) {
Point pStart = new Point((twoLines[0].start.x+twoLines[1].start.x)/2, (twoLines[0].start.y+twoLines[1].start.y)/2);
Point pEnd = new Point((twoLines[0].end.x+twoLines[1].end.x)/2, (twoLines[0].end.y+twoLines[1].end.y)/2);
midLine = new Line(pStart, pEnd);
//Log.i(TAG,number+"::::::"+average_x1+":"+average_y1+";"+average_x2+":"+average_y2);
Core.line(mShow, twoLines[0].start, twoLines[0].end, new Scalar(255,255,0,100), 15);
Core.line(mShow, twoLines[1].start, twoLines[1].end, new Scalar(255,255,0,100), 15);
Core.line(mShow, midLine.start, midLine.end, new Scalar(255,0,255,100), 15);
double distance = (midLine.start.y+midLine.end.y)/2-360;
if(distance<0) {
mMsg = "在道路中心线左侧"+(int)(-distance)+"个单位,需要向右侧调整";
}else {
mMsg = "在道路中心线右侧"+(int)distance+"个单位,需要向左侧调整";
}
// Core.putText(mShow,mMsg, new Point(0, 360), 2, 2, new Scalar(255,255,255,0),1);
}else {
mMsg = "道路消失";
// Core.putText(mShow,mMsg, new Point(0, 360), 2, 2, new Scalar(255,255,255,0),1);
}
}else {
if(angle>0) {
mMsg = "偏向左侧"+(int)angle+"度,需要向右侧偏转";
}else {
mMsg = "偏向右侧"+(int)(-angle)+"度,需要向左侧偏转";
}
//Core.putText(mShow,mMsg, new Point(0, 360), 3, 2, new Scalar(255,255,0,0),2);
}
sendDataToPC(mMsg);
}
Bitmap bmp = Bitmap.createBitmap(rgbMat.cols(), rgbMat.rows(), Bitmap.Config.ARGB_8888);
try {
Utils.matToBitmap(mShow, bmp);
return bmp;
} catch(Exception e) {
Log.e("org.opencv.samples.tutorial2", "Utils.matToBitmap() throws an exception: " + e.getMessage());
bmp.recycle();
return null;
}
}
@Override
public void run() {
while (true) {
Bitmap bmp = null;
if (mCamera == null)
break;
if (!mCamera.grab()) {
Log.e(TAG, "mCamera.grab() failed");
break;
}
long startTime=System.currentTimeMillis(); //获取开始时间
bmp = processFrame(mCamera);
if (bmp != null) {
Canvas canvas = mHolder.lockCanvas();
if (canvas != null) {
canvas.drawBitmap(bmp, (canvas.getWidth() - bmp.getWidth()) , (canvas.getHeight() - bmp.getHeight()) , null);
mHolder.unlockCanvasAndPost(canvas);
}
bmp.recycle();
long endTime=System.currentTimeMillis(); //获取结束时间
Log.i("程序运行时间: ","程序运行时间: "+(endTime-startTime)+"ms");
}
//Log.i(TAG, "Finishing processing thread");
}
if (rgbMat != null)
rgbMat.release();
if (mShow != null)
mShow.release();
rgbMat = null;
mShow = null;
}
//将得到的结果发送到PC,通过UDP方式,也可通过串口等方式来发送控制信息用来实现车循迹和飞行器循迹。
private void sendDataToPC (String data) {
DatagramSocket ds = null;
data="$"+data+"@";
try {
ds = new DatagramSocket(10000);
} catch (SocketException e) {
e.printStackTrace();
}
byte[] buf = data.getBytes();
DatagramPacket dp;
try {
dp = new DatagramPacket(buf,buf.length,InetAddress.getByName("172.25.10.1"),15000);
ds.send(dp);
ds.close();
} catch (Exception e) {
e.printStackTrace();
}
}
}
关键点
capture.retrieve(rgbMat, Highgui.CV_CAP_ANDROID_COLOR_FRAME_RGBA);
原因是如果通过Android 系统API调用摄像头所取得的视频流格式就是YUV420SP格式,而一般OpenCV算法所对应的颜色空间是RGB格式。所以需要把YUV420SP的数据通过数学运算得到每个像素点的RGB编码,存入Bitmap对象。这种方法效率极低,一张480x320分辨率的图片有20万个字节,因此运算需要经过20万次循环, 单张图片还好说,如果是连续图片构成的视频流是不可能完成这种需求的。而通过调用OpenCV的VideoCapture中的retrieve方法指定相应的颜色空间通道可直接得到RGB类型的视频流,这也是为什么采用OpenCV的API调用摄像头的原因。
Imgproc.HoughLinesP(rgbMat, lines, 1, Math.PI/180, threshold,minLineSize,lineGap);
采用HoughlineP是因为得到的直线是以坐标点来表示,这样可以结合当前的手机分辨率来合适的在屏幕上绘制出直线而Houghlines得到的(r,θ)表示,做种绘制可能会出出现越界无法绘出的情况,难以判断得到的结果正确性。
实现思路
上面的代码中的滤波和形态学变换看注释应该能理解。这里主要说一下得到道路边缘直线组后的处理过程。经过滤波、形态学变换的一系列操作可以将很多噪声和干扰线去除,剩下取到的直线基本上都是道路的边缘线了,这里的思路是先将所以得道路线分为左右两组,而前面已经介绍直线是以起始坐标点来表示的,因此分组后的直线组也就是左右道路的点集,然后利用最小二乘法进行直线拟合就能得到左右两条道路线了。所以分组是重要点也是难点,因为摄像头相对于道路的偏转角度是不确定的。此处应先排除偏转角度的干扰,所以首先要做的是对当前摄像头相对道路角度进行以此判断,如果角度偏转过大,应先校正角度,在进行左右的调整。因为在角度已经偏离很大的情况下再调整左右只会南辕北辙,在这里进行判断。
double angle = MathUtils.getAngle(mLines);
if(Math.abs(angle)<10) {
...
...
}
在角度偏转较小的情况下进行边缘线分组。首先求得所有直线起始坐标点的平均坐标点(所有直线的起始坐标点的平均值),然后将此坐标点加入到直线组中,然后对所有直线根据平均横坐标(或纵坐标,由屏幕的偏转方向确定)进行排序,由角度偏转较小的条件可知平均坐标点之前的和之后的正好分割成两组直线,然后利用List.indexof()确定平均坐标点的下标,这样就成功的分割了直线组。后面的事情就简单了。
实时显示和反馈
这里自定义一个SurfaceView的子类用来实时显示处理的结果,在这个类中开启一个线程,已进入此类线程就开始运行,然后对得到的视频信息进行处理,得到的图像再重新绘制显示出来,绘制的时候要注意越界的问题。
反馈才是要进行处理的目的,反馈的信息用于来进行其他运动物体(小车,飞行器等)的调整。
详细代码开源:https://github.com/AsanBrother/AutoTracking,欢迎fork和star。代码中还有很多不完善的地方,慢慢在改正。