[原创] 【树莓派4B测评】+OPENCV 简单车牌定位

29447945   2020-9-21 19:04 楼主

近年来,汽车车牌识别(License Plate Recognition)已经越来越受到人们的重视。特别是在智能交通系统中,汽车牌照识别发挥了巨大的作用。汽车牌照的自动识别技术是把处理图像的方法与计算机的软件技术相连接在一起,以准确识别出车牌牌照的字符为目的,将识别出的数据传送至交通实时管理系统,以最终实现交通监管的功能。在车牌自动识别系统中,从汽车图像的获取到车牌字符处理是一个复杂的过程,主要分为四个阶段:图像获取、车牌定位、字符分割以及字符识别。

 

灰度化:

在车牌识别中我们需要将图像转化为灰度图像,这样有利于后续步骤的开展,如Soble算子只能作用于灰度图像。

灰度化,在RGB模型中,如果R=G=B时,则彩色表示一种灰度颜色,其中R=G=B的值叫灰度值,因此,灰度图像每个像素只需一个字节存放灰度值(又称强度值、亮度值),灰度范围为0-255。

Opencv中函数

void cvtColor(InputArray src,  OutputArray dst,  int code,  int dstCn=0 )

参数详解:

 src:输入图像:8位无符号的16位无符号(cv_16uc…)或单精度浮点。
 dst:的大小和深度src.

code:输出图像颜色空间转换的代码。

dstCn:目标图像中的信道数;如果参数为0,则从SRC和代码自动导出信道的数目。

Mat Grayscale(Mat &img) {
    Mat out;
    cvtColor(img, out, COLOR_RGB2GRAY);

    return out;
}

 

高斯模糊:

车牌识别中利用高斯模糊将图片平滑化,去除干扰的噪声对后续图像处理的影响。

高斯模糊(GaussianBlur()),也叫高斯平滑。

周边像素的平均值,所谓"模糊",可以理解成每一个像素都取周边像素的平均值。

Mat Gaussian(Mat &img) {
    Mat out;
    GaussianBlur(img, out, Size(3, 3),
        0, 0, BORDER_DEFAULT);
    return out;

}

Sobel算子(X方向):

车牌定位的核心算法,水平方向上的边缘检测,检测出车牌区域。

主要用于获得数字图像的一阶梯度,常见的应用和物理意义是边缘检测。在技术上,它是一个离散的一阶差分算子,用来计算图像亮度函数的一阶梯度之近似值。在图像的任何一点使用此算子,将会产生该点对应的梯度矢量或是其法矢量。

该算子包含两组3x3的矩阵,分别为横向及纵向,将之与图像作平面卷积,即可分别得出横向及纵向的亮度差分近似值。如果以A代表原始图像,Gx及Gy分别代表经横向及纵向边缘检测的图像,其公式如下:

图像的每一个像素的横向及纵向梯度近似值可用以下的公式结合,来计算梯度的大小。

可用以下公式计算梯度方向。

在以上例子中,如果以上的角度Θ等于零,即代表图像该处拥有纵向边缘,左方较右方暗。

 

OpenCV中函数:

void Sobel(InputArray src, OutputArray dst, int ddepth, int xorder, int yorder, int ksize=3, double scale=1, double delta=0, int borderType=BORDER_DEFAULT )

参数:
src: 源图像。
dst:相同大小和相同数量的通道的目标图像。
ddepth:目标图像的深度。
xorder:阶导数的X.
yorder:阶导数的Y.
ksize:扩展Sobel算子–大小。它必须是1, 3, 5,或者7。
scale:计算衍生值的可选刻度因子。默认情况下,不应用缩放。看到getderivkernels()详情。
delta :可选的delta值,在将它们存储在DST之前添加到结果中。
bordertype:像素外推方法。

convertScaleAbs()——先缩放元素再取绝对值,最后转换格式为8bit型。

Mat Sobel(Mat &img) {

Mat out;
		Mat grad_x, grad_y;
  Mat abs_grad_x, abs_grad_y;

  	int scale = 1;
  int delta = 0;
	int ddepth = CV_16S;
	int th = 100;

	
  Sobel( img, grad_x, ddepth, 1, 0, 3, scale, delta, BORDER_DEFAULT );
  //Scharr( grad_x, grad_x, ddepth, 1, 0, scale, delta, BORDER_DEFAULT );
  
  //Sobel( img, grad_y, ddepth, 0, 1, 3, scale, delta, BORDER_DEFAULT );
  //Scharr( grad_y, grad_y, ddepth, 0, 1, scale, delta, BORDER_DEFAULT );
  convertScaleAbs( grad_x, out );
//   convertScaleAbs( grad_y, abs_grad_y );

//   addWeighted( abs_grad_x, 0.5, abs_grad_y, 0.5, 0, out );

    // Mat out;
    // Mat grad_x, grad_y;
    // Mat abs_grad_x, abs_grad_y;

    //X方向
    //Sobel(img, grad_x, CV_16S, 1, 0, 3, 1, 1, BORDER_DEFAULT);
    //convertScaleAbs(grad_x, abs_grad_x);
    // Sobel(img, grad_x, CV_16S, 1, 0, 1, 3, 1, BORDER_DEFAULT);
    // convertScaleAbs(img, out);

    //Y方向
    //Sobel(img, grad_y, CV_16S, 0, 1, 3, 1, 1, BORDER_DEFAULT);
    //convertScaleAbs(grad_y, abs_grad_y);
    //convertScaleAbs(img, out);

    //合并
    //addWeighted(abs_grad_x, 0.5, abs_grad_y, 0.5, 0, out);

    return out;
}

二值化:

进一步对图像进行处理,强化目标区域,弱化背景。

图像的二值化,就是将图像上的像素点的灰度值设置为0或255,也就是将整个图像呈现出明显的只有黑和白的视觉效果。

OpenCV中函数

double threshold(InputArray src, OutputArray dst, double thresh, double maxVal, int thresholdType)

参数:
src:源阵列(单通道,32位浮点8位)。
dst:相同大小和类型的目标数组。
thresh:门限阈值。
Maxval:最大值使用的thresh_binary和thresh_binary_inv阈值类型。
thresholdtype:阈值型,如下。

 THRESH_BINARY  当前点值大于阈值时,取Maxval,也就是第四个参数,下面再不说明,否则设置为0

 THRESH_BINARY_INV 当前点值大于阈值时,设置为0,否则设置为Maxval

 THRESH_TRUNC 当前点值大于阈值时,设置为阈值,否则不改变

 THRESH_TOZERO 当前点值大于阈值时,不改变,否则设置为0

 THRESH_TOZERO_INV  当前点值大于阈值时,设置为0,否则不改变

Mat TwoValued(Mat &img) {
    Mat out;
    threshold(img, out, 0, 255, THRESH_BINARY|THRESH_OTSU);
    //threshold(img, out, 100, 255, CV_THRESH_BINARY);

    return out;
}

闭操作:

闭操作可以将目标区域连成一个整体,便于后续轮廓的提取。

闭操作可使轮廓线更光滑,但与开操作相反的是,闭操作通常消弥狭窄的间断和长细的鸿沟,消除小的空洞,并填补轮廓线中的断裂。

使用结构元素B对集合A进行闭操作,定义为

这个公式表明,使用结构元素B对集合A的闭操作就是用B对A进行膨胀,然后用B对结果进行腐蚀。

 

OpenCV中函数

void morphologyEx(InputArray src, OutputArray dst, int op, InputArray element, Point anchor=Point(-1,-1), int iterations=1, int borderType=BORDER_CONSTANT, const Scalar& borderValue=morphologyDefaultBorderValue() )

参数:
src:源图像。
dst:相同大小和类型的目标图像。
element:内核类型    用getStructuringElement函数得到。
OP:
可以是以下形式之一的形态学操作的类型:
morph_open -开启操作
morph_close -闭合操作
morph_gradient -形态学梯度
morph_tophat“顶帽”
morph_blackhat -“黑帽”
iterations:侵蚀和膨胀的次数被应用。
bordertype–像素外推方法。
bordervalue–边界值在一个恒定的边界情况。默认值有特殊含义。

关注前4个参数即可,后面用默认参数。

Mat Close(Mat &img) {
    Mat out;
    //Mat element(5, 5, CV_8U, cv::Scalar(1));
    Mat element = getStructuringElement(MORPH_RECT, Size(17, 5));
    morphologyEx(img, out, cv::MORPH_CLOSE, element);

    return out;
}

取轮廓:

将前面处理的车牌目标区域提取出来。

相关函数:

查找轮廓:

void findContours(InputOutputArray image, OutputArrayOfArrays contours, OutputArray hierarchy, int mode, int method, Point offset=Point())

image: 输入的 8-比特、单通道图像. 非零元素被当成 1, 0 象素值保留为 0 - 从而图像被看成二值的。为了从灰度图像中得到这样的二值图像,可以使用 cvThreshold, cvAdaptiveThreshold 或 cvCanny. 本函数改变输入图像内容。 

storage :得到的轮廓的存储容器 
first_contour :输出参数:包含第一个输出轮廓的指针 
header_size :如果 method=CV_CHAIN_CODE,则序列头的大小 >=sizeof(CvChain),否则 >=sizeof(CvContour) . 
mode 
提取模式. 
CV_RETR_EXTERNAL - 只提取最外层的轮廓 
CV_RETR_LIST - 提取所有轮廓,并且放置在 list 中 
CV_RETR_CCOMP - 提取所有轮廓,并且将其组织为两层的 hierarchy: 顶层为连通域的外围边界,次层为洞的内层边界。 
CV_RETR_TREE - 提取所有轮廓,并且重构嵌套轮廓的全部 hierarchy 
method :
逼近方法 (对所有节点, 不包括使用内部逼近的 CV_RETR_RUNS). 
CV_CHAIN_CODE - Freeman 链码的输出轮廓. 其它方法输出多边形(定点序列). 
CV_CHAIN_APPROX_NONE - 将所有点由链码形式翻译(转化)为点序列形式 
CV_CHAIN_APPROX_SIMPLE - 压缩水平、垂直和对角分割,即函数只保留末端的象素点; 
CV_CHAIN_APPROX_TC89_L1, 
CV_CHAIN_APPROX_TC89_KCOS - 应用 Teh-Chin 链逼近算法. CV_LINK_RUNS - 通过连接为 1 的水平碎片使用完全不同的轮廓提取算法。仅有 CV_RETR_LIST 提取模式可以在本方法中应用. 
offset :
每一个轮廓点的偏移量. 当轮廓是从图像 ROI 中提取出来的时候,使用偏移量有用,因为可以从整个图像上下文来对轮廓做分析. 
函数 cvFindContours 从二值图像中提取轮廓,并且返回提取轮廓的数目。指针 first_contour 的内容由函数填写。它包含第一个最外层轮廓的指针,如果指针为 NULL,则没有检测到轮廓(比如图像是全黑的)。其它轮廓可以从 first_contour 利用 h_next 和 v_next 链接访问到。 在 cvDrawContours 的样例显示如何使用轮廓来进行连通域的检测。轮廓也可以用来做形状分析和对象识别 - 见CVPR2001 教程中的 squares 样例。该教程可以在 SourceForge 网站上找到。 

绘制轮廓:

void drawContours(InputOutputArray image, InputArrayOfArrays contours, int contourIdx, const Scalar& color, intthickness=1, int lineType=8, InputArray hierarchy=noArray(), int maxLevel=INT_MAX, Point offset=Point() )


void Contour(Mat &img, Mat &out) {
    RNG rng(12345);
	
    // vector< Mat > contours(1000);
    // vector<Vec4i> hierarchy(1000);
	vector<vector<Point>> contours;
    vector<Vec4i> hierarchy;
    findContours(img, contours, hierarchy, 3, 2, Point(0, 0));

// 	return ;
    vector< vector<Point> >::iterator itc = contours.begin();
    vector<RotatedRect> rects;
    int t = 0;
    while (itc != contours.end()) {
        //Create bounding rect of object
        RotatedRect mr = minAreaRect(Mat(*itc));
        //large the rect for more
        if (!verifySizes(mr)) {
            itc = contours.erase(itc);
        }
        else {
            ++itc;
            rects.push_back(mr);
        }
   }
	
    cv::Mat result;
    img.copyTo(result);
    for (int i = 0; i< contours.size(); i++)
    {
        drawContours(result, contours, i, Scalar(0, 0, 255), 2, 8, vector<Vec4i>(), 0, Point());
        //drawContours(result, contours, i, Scalar(255), 2);
    }
    //imshow("画轮廓", out);

    for (int i = 0; i < rects.size(); i++) {
        circle(result, rects[i].center, 3, Scalar(0, 255, 0), -1);

        float minSize = (rects[i].size.width < rects[i].size.height) ? rects[i].size.width : rects[i].size.height;
        //minSize = minSize - minSize*0.5;

        srand(time(NULL));
        Mat mask;
        mask.create(out.rows + 2, out.cols + 2, CV_8UC1);
        mask = Scalar::all(0);
        int loDiff = 30;
        int upDiff = 30;
        int connectivity = 4;
        int newMaskVal = 255;
        int NumSeeds = 10;
        Rect ccomp;
        int flags = connectivity + (newMaskVal << 8) + (1 << 16) + (1 << 17);

        for (int j = 0; j < NumSeeds; j++) {
            Point seed;
            seed.x = rects[i].center.x + rand() % (int)minSize - (minSize / 2);
            seed.y = rects[i].center.y + rand() % (int)minSize - (minSize / 2);
            circle(result, seed, 1, Scalar(0, 255, 255), -1);
            int area = floodFill(out, mask, seed, Scalar(255, 0, 0), &ccomp, Scalar(loDiff, loDiff, loDiff), Scalar(upDiff, upDiff, upDiff), flags);
        }
        //imshow("漫水填充", mask);

        vector<Point> pointsInterest;
        Mat_<uchar>::iterator itMask = mask.begin<uchar>();
        Mat_<uchar>::iterator end = mask.end<uchar>();
        for (; itMask != end; ++itMask)
            if (*itMask == 255)
                pointsInterest.push_back(itMask.pos());

        RotatedRect minRect = minAreaRect(pointsInterest);

        if (verifySizes(minRect)) {
            // rotated rectangle drawing
            Point2f rect_points[4]; minRect.points(rect_points);
            for (int j = 0; j < 4; j++)
                line(result, rect_points[j], rect_points[(j + 1) % 4], Scalar(0, 0, 255), 1, 8);

            //Get rotation matrix
            float r = (float)minRect.size.width / (float)minRect.size.height;
            float angle = minRect.angle;
            if (r < 1)
                angle = 90 + angle;
            Mat rotmat = getRotationMatrix2D(minRect.center, angle, 1);

            //Create and rotate image
            Mat img_rotated;
            warpAffine(out, img_rotated, rotmat, out.size(), 2);//实现旋转

            //Crop image
            Size rect_size = minRect.size;
            if (r < 1)
                swap(rect_size.width, rect_size.height);
            Mat img_crop;
            getRectSubPix(img_rotated, rect_size, minRect.center, img_crop);

            Mat resultResized;
            resultResized.create(33, 144, CV_8UC3);
            resize(img_crop, resultResized, resultResized.size(), 0, 0, INTER_CUBIC);;

            ////Equalize croped image
            Mat grayResult;
            cvtColor(resultResized, grayResult, COLOR_BGR2GRAY);// CV_RGB2GRAY
            blur(grayResult, grayResult, Size(3, 3));
            grayResult = histeq(grayResult);

            if (1) {
                stringstream ss(stringstream::in | stringstream::out);
                ss << "haha" << "_" << i << ".jpg";
                imwrite(ss.str(), grayResult);
            }

        }
    }
}

 

最后主函数如下:


int main() {
    Mat img;
    Mat out;
    //Mat result;

    //载入图片
    img = imread("car.jpg");//, CV_LOAD_IMAGE_GRAYSCALE);
    img.copyTo(out);
    //imshow ("原始图", img);
    
    img = Grayscale(img);
    imshow("灰度化", img);

	img = Gaussian(img);
    imshow ("高斯模糊", img);

    img = Sobel(img);
    imshow("Sobel_X", img);

    img = TwoValued(img);
    imshow("二值化", img);

    img = Close(img);
    imshow("闭操作", img);

    Contour(img, out);
	

   	waitKey(0);

	destroyAllWindows();
	return 0;
}

然后编写cmake文件:

cmake_minimum_required(VERSION 2.8)
project( test )
find_package( OpenCV REQUIRED )
add_executable( test test.cpp )
target_link_libraries( test ${OpenCV_LIBS} )

最后camke 和make

pi@raspberrypi:~/Public/OPENCV/program/Image_Identification $ cmake .
-- The C compiler identification is GNU 8.3.0
-- The CXX compiler identification is GNU 8.3.0
-- Check for working C compiler: /usr/bin/cc
-- Check for working C compiler: /usr/bin/cc -- works
-- Detecting C compiler ABI info
-- Detecting C compiler ABI info - done
-- Detecting C compile features
-- Detecting C compile features - done
-- Check for working CXX compiler: /usr/bin/c++
-- Check for working CXX compiler: /usr/bin/c++ -- works
-- Detecting CXX compiler ABI info
-- Detecting CXX compiler ABI info - done
-- Detecting CXX compile features
-- Detecting CXX compile features - done
-- Found OpenCV: /usr/local (found version "4.4.0") 
-- Configuring done
-- Generating done
-- Build files have been written to: /home/pi/Public/OPENCV/program/Image_Identification
pi@raspberrypi:~/Public/OPENCV/program/Image_Identification $ make
Scanning dependencies of target test
[ 50%] Building CXX object CMakeFiles/test.dir/test.cpp.o
[100%] Linking CXX executable test
[100%] Built target test
pi@raspberrypi:~/Public/OPENCV/program/Image_Identification $ 

运行之后界面出现如下窗口:

image.png 然后在工程目录下可以看到多了3个图片:

image.png 打开图片,可以看到已经定位到了车牌了:

image.png 后续就需要把数字分离并识别了

附上代码:

Image_Identification.rar (196.23 KB)
(下载次数: 13, 2020-9-21 19:03 上传)

  • image.png

回复评论 (7)

厉害了,看着C++就头疼

默认摸鱼,再摸鱼。2022、9、28
点赞  2020-9-21 20:25

很棒的工作

点赞  2020-9-23 20:19

楼主厉害!,我也正在用树莓派玩OpenCV

QQ:252669569
点赞  2020-9-23 23:31
引用: lb8820265 发表于 2020-9-23 23:31 楼主厉害!,我也正在用树莓派玩OpenCV

树莓派加个摄像头玩opencv很方便,有什么进展发帖一起交流哦

点赞  2020-9-24 09:06
引用: IC爬虫 发表于 2020-9-23 20:19 很棒的工作

感谢支持

点赞  2020-9-24 09:07
引用: freebsder 发表于 2020-9-21 20:25 厉害了,看着C++就头疼

哈哈哈,看多了就不疼了

点赞  2020-9-24 09:07

膜拜一下啊

点赞  2020-9-24 14:34
电子工程世界版权所有 京B2-20211791 京ICP备10001474号-1 京公网安备 11010802033920号
    写回复