第5章 停车场车位检测 5.1 概述 随着国人生活水平的提高,汽车已经进入千家万户,停车难的问题也越来越突出。在寻找停车位时,能够实时提供车位信息的停车场大受欢迎,本章将给出一个根据图像自动检测车位的案例。 5.1.1 案例描述 本案例采用一张停车场的俯视图,如图5-1所示。这是一个有着150个车位的停车场,车位共有6排,每排25个,车位用白线标出。 图5-1 停车场俯视图 5.1.2 案例分析 一般来讲,停车场的图像是从固定的监控摄像头采集的,因此各车位的位置都是已知 的。在本案例中车位上的白线比较整齐,车位大小也基本一致,因此即使不知道车位坐标也可以直接从图像中提取车位信息。 提取这些白线的方法类似于第4章中提取围棋棋盘格子线的方法,不过现实中的白线不可能像计算机生成的图像那样整齐划一,因此在进行模板匹配前需要先进行边缘检测,对边缘图像的质量要求也较高,Canny边缘检测将是较为理想的选择。通过这一步希望提取出如图5-2所示的线条,根据这些线条就可以计算出每个停车位的位置。 图5-2?希望提取的线条 车位坐标确定后可以根据颜色信息来判断车位上是否有车,此时需要区分有车和无车时的颜色特征。 无车时车位的特征主要是以下两条: (1)颜色均一。 (2)底色为较深的灰色。 有车时的特征较为复杂,在大多数情况下颜色并不均匀,例如车窗处的颜色会与车身明显不同,但是少数情况下车体颜色均匀也是可能的,此时就需要通过色彩值是否接近深灰色进行判断。技术上实现上述区分并不难,可以借鉴第2章中魔方色块的识别方法。 5.2 总体设计 5.2.1 系统需求 本案例只需OpenCV,不需要任何第三方库。 5.2.2 总体思路及流程 根据上述分析,本案例的总体流程如下: (1)Canny边缘检测。 (2)模板匹配提取水平和垂直的线条。 (3)过滤重复线条并验证。 (4)颜色判断。 (5)判断有车无车并输出结果。 5.3 停车位车位检测的实现 5.3.1 Canny边缘检测 在本案例中需要通过Canny边缘检测提取较为清晰的边缘图像,相关代码在runCanny()函数中,其中的关键代码如下: Imgproc.cvtColor(src, gray, Imgproc.COLOR_BGR2GRAY); Imgproc.Canny(gray, canny, 50, 200, 3, false); 经过Canny边缘检测后的结果如图5-3所示。从图中可以看出,有车的车位有车体轮廓的线条,而无车处则比较干净,根据此特征其实也可以判断是否有车,不过既然通过颜色特征已可区别也就无此必要了。 图5-3 Canny边缘检测结果 5.3.2 模板匹配 接下来通过模板匹配找出水平和垂直的直线,同样分为matchLineH()函数和matchLineV()函数。该函数的基本思想和围棋盘面识别类似,但是为了能匹配到所有有用的线条,模板图像的大小设为100×3像素。模板中的直线如果太短,则会导致匹配结果中出现有箭头等标志的线条,如果太长,则可能错过重要的线条。由于匹配出的线条中y坐标相同的情况较多,代码中用pt数组保存结果,检测到线条的坐标用1标记,未检测到的则标为0。 5.3.3 过滤及验证 模板匹配时通过数组标记的方法避免坐标相同的线条重复出现,但即便如此也无法避免坐标相近的线条重复出现,例如y坐标为7和8处可能都检测出线条,此时需要将重复的线条过滤掉,程序中用combineLine()函数来过滤。该函数的原理非常简单,如果相邻的两个坐标值非常接近,则直接跳过后面的坐标,这样重复值就被过滤掉了。过滤后还需要对有关线条的数量和距离进行简单验证,相关代码见checkLines()函数。通过验证后可以将所有车位绘制出来,具体代码见drawBlocks()函数,绘制结果如图5-4所示。 图5-4 验证后车位的绘制结果 5.3.4 颜色识别 下一步是本案例的关键:通过颜色特征判断车位上是否有车。检测原理在5.1.2节已经介绍过,具体代码见carColor()函数。该函数中的代码与魔方色块的颜色识别类似,函数中的rec参数代表车位的矩形区域。函数通过RGB模式识别颜色,在此模式下灰色的R、G、B值非常接近。如果R、G、B值中任一个值与均值相差较大,则认为该颜色不是灰色。为了识别深灰色,可以将均值限制在较低范围。颜色是否均一可以通过标准差来判断,与魔方案例中类似。 5.3.5 车位检测 上述carColor()函数可判断某区域内是否停有车辆,对所有车位进行检测后即可获得结果,程序中通过testAll()函数完成此任务,识别的结果如图5-5所示。 图5-5 车位中是否停车的检测结果 5.4 完整代码 最后,给出本案例的完整代码: //第5章/ParkingLot.java import org.opencv.core.*; import org.opencv.highgui.HighGui; import org.opencv.imgcodecs.Imgcodecs; import org.opencv.imgproc.Imgproc; public class ParkingLot { public static int left; public static int right; public static void main(String[] args) { System.loadLibrary(Core.NATIVE_LIBRARY_NAME); //读取图像并提取边缘 Mat src = Imgcodecs.imread("Parking.png"); Mat binary = runCanny(src); //获取水平和垂直方向的直线 int[] p0 = matchLineH(binary); int[] py = combineLine(p0); int[] p1 = matchLineV(binary); int[] px = combineLine(p1); //验证车位边界值是否适当 boolean pass = checkLines(px, py); if (!pass) { System.out.println("未找到适合的车位边界,请确认后重试!"); System.exit(0); } //绘制所有车位 left = px[0]; right = px[1]; drawBlocks(src, py); //测试所有车位是否有车并绘制测试结果 testAll(src, py); System.exit(0); } public static Mat runCanny(Mat src) { //对图像进行边缘检测 Mat gray = new Mat(); Mat canny = new Mat(); Imgproc.cvtColor(src, gray, Imgproc.COLOR_BGR2GRAY); Imgproc.Canny(gray, canny, 50, 200, 3, false); //显示检测结果 HighGui.imshow("Canny", canny); HighGui.waitKey(0); return canny; } public static int[] matchLineH(Mat src) { //在白色背景上画一条水平的黑线 Mat template = new Mat(3, 100, CvType.CV_8UC1, new Scalar(255)); Point pt1 = new Point(0, 1); Point pt2 = new Point(99, 1); Imgproc.line(template, pt1, pt2, new Scalar(0), 1); //模板匹配 Mat result = new Mat(); Imgproc.matchTemplate(src,template,result,Imgproc.TM_CCOEFF_NORMED); //搜索匹配值>0.7的直线,标记y坐标的值 int rows = src.rows(); int[] pt = new int[rows]; for (int y = 0; y < result.rows(); y++) { for (int x = 0; x < result.cols(); x++) { double p = result.get(y, x)[0]; //匹配值 if (p > 0.7) { pt[y] = 1; } } } return pt; } public static int[] matchLineV(Mat src) { //在白色背景上画一条垂直的黑线 Mat template = new Mat(100, 3, CvType.CV_8UC1, new Scalar(255)); Point pt1 = new Point(1, 0); Point pt2 = new Point(1, 99); Imgproc.line(template, pt1, pt2, new Scalar(0), 1); //模板匹配 Mat result = new Mat(); Imgproc.matchTemplate(src,template,result, Imgproc.TM_CCOEFF_NORMED); //搜索匹配值>0.7的直线,标记x坐标的值 int cols = src.cols(); int[] pt = new int[cols]; for (int y = 0; y < result.rows(); y++) { for (int x = 0; x < result.cols(); x++) { double p = result.get(y, x)[0]; //匹配值 if (p > 0.7) { pt[x] = 1; } } } return pt; } public static int[] combineLine(int[] p0) { int last = -5; int num = p0.length; int[] p1 = new int[num]; int n = 0; //有效数组长度 for (int i = 0; i < num; i++) { if (p0[i] == 1) { if (i - last > 3) { //坐标接近的只取1个 p1[n] = i; n++; last = i; } } } //复制数组(数组长度已确定) int[] p = new int[n]; for (int i = 0; i < n; i++) { p[i] = p1[i]; } return p; } public static boolean checkLines(int[] px, int[] py) { if (px.length!=2) return false; if (py.length!=12) return false; int rows = py.length / 2; if (px[1] - px[0] < 300) //车位不够宽 return false; for (int i = 0; i < rows; i++) { if (py[2*i+1] - py[2*i] < 30) //车位不够高 return false; } return true; } public static void drawBlocks(Mat src, int[] py) { //将原图像复制到draw中 Mat draw = new Mat(); src.copyTo(draw); Scalar color = new Scalar(0, 0, 255); double width = (right - left) / 25.0; //将所有车位用矩形标出 for (int i = 0; i < 25; i++) { int x = left + (int) (width * i); int rows = py.length / 2; for (int j = 0; j < rows; j++) { Rect rec=new Rect(x, py[2*j],(int) width, py[2*j+1]- py[2*j]); Imgproc.rectangle(draw, rec, color, 3); } } //在屏幕上显示所有车位 HighGui.imshow("Blocks", draw); HighGui.waitKey(0); } public static boolean carColor(Mat bgr, Rect rec) { //截取车位大小区域 Mat sub = new Mat(); Mat roi = bgr.submat(rec); roi.copyTo(sub); //获取均值和标准差 MatOfDouble matMean = new MatOfDouble(); MatOfDouble matStd = new MatOfDouble(); Core.meanStdDev(sub, matMean, matStd); double std = matStd.get(0, 0)[0]; //标准差 double b = matMean.get(0, 0)[0]; double g = matMean.get(1, 0)[0]; double r = matMean.get(2, 0)[0]; double avg = (b + g + r) / 3.0; if (std > 50) //色彩不均匀的 return true; if ((avg < 30) || (avg > 90)) //色彩太亮或太暗的 return true; if ((Math.abs(b - avg) > 5) || (Math.abs(g - avg) > 5) || (Math.abs(r - avg) > 5)) return true; //非灰色系的 return false; } public static void testAll(Mat src, int[] py) { Mat draw = new Mat(); src.copyTo(draw); Scalar color = new Scalar(0, 0, 255); double width = (right - left) / 25.0; for (int i = 0; i < 25; i++) { int x = left + (int) (width * i); int rows = py.length / 2; for (int j = 0; j < rows; j++) { Rect rec = new Rect(x + 3, py[2 * j] + 5, (int) width - 6, py[2 * j + 1] - py[2 * j] - 10); if (carColor(src, rec)) { Imgproc.rectangle(draw, rec, color, 3); } } } HighGui.imshow("Cars", draw); HighGui.waitKey(0); } } 74 67