1、前言
曾经写过一篇文章,介绍如何利用ESP32获取车牌数据上传至百度云平台识别车牌。不过这种方式既需要无线传输,还需要额外对车牌识别进行缴费。
本期我们探索如何利用STM32进行车牌识别的本地化部署。这里叠个甲,作者做这块纯属好奇,不涉及真正的应。
本期硬件采用STM32F4,TFTLCD以及OV2640显示屏,本来是想用H7的,但是由于不可抗力因素换为使用F4,摄像头之所以用OV2640,主要是比较便宜,其他型号手上暂时也没有。
2、步骤说明
如何实现STM32和摄像头(DCMIPP)的通讯这里不过多赘述,我们主要介绍一下实现车牌识别的步骤,这里我总结为三个部分:
1.车牌区域识别
2.字符分割
3.字符识别(尚未写完)
接下来我将逐一介绍其实现方式,以下是区域识别和字符分割的实现。
目前字符识别还没做完,所有的流程均有不同时间的延时以便展示。
3、区域识别
首先就是要如何识别出车牌的有效区域,我看许多人的做法是进行灰度化之后再进行二值化,再去检测每行中跳变点的个数之类的。
不过我觉得跳变点个数对照片的效果要求太理想了,而且单纯的灰度化的话引入的噪声又比较大。
因此我尝试着使用蓝色阈值+蓝色与红色差异值的方式来进行二值化,总而言之就是尽可能的提取出蓝色特征。
for (x = 0; x < RGB_X; x++) {
for (y = 0; y < RGB_Y; y++) {
color = rgb_buf[x][y];
R = (color >> 11) * 255 / 31; // 提取红色分量
G = ((color >> 5) & 0x3f) * 255 / 63; // 提取绿色分量
B = (color & 0x1f) * 255 / 31; // 提取蓝色分量
// 根据蓝色和红色、绿色的差异进行二值化处理
if (B > (R + G) * 0.8) { // 如果蓝色比红色和绿色的总和大一定比例
color = 0xffff; // 白色
} else {
color = 0x0000; // 黑色
}
// 更新图像缓冲区
rgb_buf[x][y] = color;
LCD->LCD_RAM = rgb_buf[x][y];
LCD_SetCursor(X_Offset + y, Y_Offset + x);
LCD_WriteRAM_Prepare();
}
}
将RGB三色提取出来后,比较蓝色和红绿值来进行二值化,这样子可以很好的提取出蓝色车牌我们需要的部分。
1
2
不过这个做法也有明显的缺点:就是只能识别蓝色的车牌~~
可以看到二值化后仍然存留着一些噪声,这里我们可以通过滤波降噪。
void MedianFilter(u16 (*input)[RGB_Y], u16 (*output)[RGB_Y])
{
u16 x, y;
u16 median[9]; // 用于存储3x3邻域的像素值
u16 temp;
// 处理内部像素(非边界像素)
for (x = 1; x < RGB_X - 1; x++) {
for (y = 1; y < RGB_Y - 1; y++) {
// 获取3x3邻域的像素值
median[0] = input[x-1][y-1];
median[1] = input[x-1][y];
median[2] = input[x-1][y+1];
median[3] = input[x][y-1];
median[4] = input[x][y];
median[5] = input[x][y+1];
median[6] = input[x+1][y-1];
median[7] = input[x+1][y];
median[8] = input[x+1][y+1];
// 对邻域像素值进行排序
for (u8 i = 0; i < 9; i++) {
for (u8 j = i + 1; j < 9; j++) {
if (median[i] > median[j]) {
temp = median[i];
median[i] = median[j];
median[j] = temp;
}
}
}
// 取中值作为当前像素的值
output[x][y] = median[4];
}
}
}
采用3x3的中值滤波算法降噪,可以有效的降低噪声。
为了找到车牌区域,我们可以扫描每行白点最多的一行作为基准,自上,自下分别寻找这个最大值80%的行为哪一行,即确定车牌的上下行。
之后在此基础上,对列也进行这种操作,从右至左和从左至右找到车牌的左右区间。
4、字符分割
得到了车牌区域我们就可以考虑如何分割字符了,这里我采用的策略是从右到左去测量空白间隙。
即利用各字符之间的空隙,这里正好也是因为这个川字,所以考虑从右到左,因为这样子可以避免去处理川字中间的几个空隙。
/*
寻找分割线
*/
col_threshold = max_col_white * 80 / 100;
for(int i = right_col;i>left_col;i--)
{
if(YuzhiFlag)//右边缘
{
if(col_white_counts[i]<max_col_white * 70 / 100)//左边缘
{
line[number++] = i;
YuzhiFlag = !YuzhiFlag;
}
}
else
{
if(col_white_counts[i]>max_col_white * 90 / 100)//最后一个字符
{
line[number++] = i;
YuzhiFlag = !YuzhiFlag;
}
}
}
int zifunumber = 0;
POINT_COLOR = RED;
for(int i = 0;i<number;i++)
{
if(((line[i]-line[i+1])>(right_col-left_col)*5/100)&&zifunumber<6)
{
box.zifu[zifunumber][0] = line[i];
box.zifu[zifunumber][1] = line[i+1];
LCD_DrawLine(box.zifu[zifunumber][0]+65,top_row+225,box.zifu[zifunumber][0]+65,bottom_row+225);
LCD_DrawLine(box.zifu[zifunumber][1]+65,top_row+225,box.zifu[zifunumber][1]+65,bottom_row+225);
zifunumber++;
i++;
delay_ms(1000);
}
else if(number>=6)
{
box.zifu[zifunumber][0] = line[i];
box.zifu[zifunumber][1] = left_col+3;
LCD_DrawLine(box.zifu[zifunumber][0]+65,top_row+225,box.zifu[zifunumber][0]+65,bottom_row+225);
LCD_DrawLine(box.zifu[zifunumber][1]+65,top_row+225,box.zifu[zifunumber][1]+65,bottom_row+225);
break;
delay_ms(1000);
}
}
这样子就可以实现分割字符的目的了。
5、字符识别
字符识别这两天做,大体应该是采用模板识别的策略,利用分割出来的字符和模板的匹配程度实现字符的识别。
这几天有空在钻研一下。