Reconhecimento de placas Opencv
Visão geral
O reconhecimento de placas neste artigo é dividido em várias etapas:
1. Pré-processamento de imagem
(1) Converter para imagem em escala de cinza
(2) Executar filtragem gaussiana
(3) Converter para imagem binária
(4) Detecção de borda
(5) Tratamento morfológico
2. Encontre a placa
(1) Encontre o contorno de cada parte na imagem pré-processada
(2) Obtenha o retângulo circunscrito do contorno
(3) Determine o retângulo como a placa através das condições de comprimento e largura
3. Segmentação de caracteres
(1) Pré-processamento de placas de veículos
(2) Remoção de bordas e rebites
(3) Método de projeção vertical para segmentar caracteres
4. Aprendizado de máquina para reconhecer personagens
As imagens de amostra processadas neste artigo são as seguintes:
Cada etapa é descrita detalhadamente abaixo:
1. Pré-processamento de imagem
A função de pré-processamento é a seguinte:
```cpp
Mat preprocessing(Mat input)
{
//转为灰度图
Mat gray;
cvtColor(input, gray, COLOR_BGR2GRAY, 1);
//进行高斯滤波
Mat gauss;
GaussianBlur(gray, gauss, Size(5, 5), 0, 0);
//阈值化操作
Mat thres_hold;
threshold(median, thres_hold, 100, 255, THRESH_BINARY);
//边缘检测
Mat canny_edge;
Canny(thres_hold, canny_edge, 50, 100, 5);
//形态学操作
int close_size = 5; //闭操作的核的大小
int open_size = 7; //开操作的核的大小
Mat element_close = getStructuringElement(MORPH_RECT, Size(1 + 2 * close_size, 1 + 2 * close_size));//定义闭操作的核
Mat element_open = getStructuringElement(MORPH_RECT, Size(1 + 2 * open_size, 1 + 2 * open_size));
Mat morph_close, morph_open;
morphologyEx(canny_edge, morph_close, MORPH_CLOSE, element_close);
morphologyEx(morph_close, morph_open, MORPH_OPEN, element_open);
return morph_open;
}
(1)转为灰度图
```cpp
Mat gray;
cvtColor(input, gray, COLOR_BGR2GRAY, 1);
Aqui, basta chamar a função cvtcolor para converter para imagem em tons de cinza
(2) Execute a filtragem gaussiana
Mat gauss;
GaussianBlur(gray, gauss, Size(5, 5), 0, 0);
Filtragem Gaussiana simples. O kernel não precisa ser muito grande para evitar que as bordas fiquem muito desfocadas.
(3) Converter para imagem binária
Mat thres_hold;
threshold(median, thres_hold, 100, 255, THRESH_BINARY);
O objetivo da binarização aqui é distinguir aproximadamente o primeiro e o segundo plano e destacar a placa do carro. Reduza os itens de interferência para posterior detecção de bordas.
(4) Detecção de bordas
Mat canny_edge;
Canny(thres_hold, canny_edge, 50, 100, 5);
A proporção de limites altos e baixos (terceiro e quarto parâmetros) do operador de detecção astuto é geralmente 2: 1 ou 3: 1. A
imagem de borda detectada é a seguinte:
(5) Processamento morfológico
int close_size = 5; //闭操作的核的大小
int open_size = 7; //开操作的核的大小
Mat element_close = getStructuringElement(MORPH_RECT, Size(1 + 2 * close_size, 1 + 2 * close_size));//定义闭操作核
Mat element_open = getStructuringElement(MORPH_RECT, Size(1 + 2 * open_size, 1 + 2 * open_size));//定义开操作核
Mat morph_close, morph_open;
morphologyEx(canny_edge, morph_close, MORPH_CLOSE, element_close);
morphologyEx(morph_close, morph_open, MORPH_OPEN, element_open);
Operações de fechamento morfológico e abertura morfológica são usadas aqui. As etapas são definir o tamanho do núcleo da operação -> definir o núcleo -> executar a operação correspondente.O
objetivo da operação de fechamento aqui é preencher as quebras no contorno e conectar as bordas quebradas em um todo. Quanto maior o núcleo, maior será o todo conectado pelas bordas.
Resultado da operação de fechamento:
O objetivo da operação de abertura é eliminar o todo e a linha de algumas peças pequenas. Quanto maior o núcleo, mais óbvio será o efeito.
Resultado da operação aberta:
2. Extração de matrícula
Mat find_the_plate(Mat input)
{
Mat output; //提取出来的车牌
//寻找轮廓
vector<vector<Point>> contours; //定义轮廓点向量
vector<Vec4i> hierarchy; //轮廓的索引
findContours(input, contours, hierarchy, RETR_TREE, CHAIN_APPROX_SIMPLE);//提取轮廓
Point2f rect_info[4]; //用于储存最小矩形的四个顶点
RotatedRect minrect; //返回的最小外接矩形
int width; //最小矩形的宽
int height; //最小矩形的高
for (int i = 0; i < contours.size(); i++)
{
minrect = minAreaRect(contours[i]);
width = minrect.size.width;
height = minrect.size.height;
minrect.points(rect_info);
for (int j = 0; j < 4; j++)
{
if ( width / height >= 2 && width / height <= 5) //利用长宽比筛选条件
{
output = src(Rect(rect_info[1].x, rect_info[1].y, width, height));//根据筛选出的矩形提取车牌,相当于创建ROI
line(src, rect_info[j], rect_info[(j + 1) % 4], Scalar(0, 0, 255));//在原图画出符合条件的矩形
}
}
}
return output;
}
(1) Encontre o contorno de cada parte na imagem pré-processada
//寻找轮廓
vector<vector<Point>> contours; //定义轮廓点向量
vector<Vec4i> hierarchy; //轮廓的索引
findContours(input, contours, hierarchy, RETR_TREE, CHAIN_APPROX_SIMPLE);//提取轮廓
(Aqui a entrada é a imagem pré-processada)
O segundo parâmetro contornos é do tipo OutputArrayofArrays, que armazena os contornos encontrados. Como o contorno é originalmente uma coleção de pontos, ou seja, existe um vetor, existem muitos contornos.
Portanto, a terceira hierarquia de parâmetros de vector<vertor> é do tipo OutputArrays e contém as informações topológicas da imagem. Cada contorno de contorno[i] contém 4 elementos de hierarquia, hierarquia[i][0]~
hierarquia[i][3], que representam respectivamente os números de índice do próximo contorno, do contorno anterior, do contorno pai e do contorno incorporado . Portanto, cada elemento da hierarquia é do tipo Vec4i.
(2) Obtenha o retângulo circunscrito do contorno e determine-o como o retângulo da placa através das condições de comprimento e largura
Point2f rect_info[4]; //用于储存最小矩形的四个顶点
RotatedRect minrect; //返回的最小外接矩形
int width; //最小矩形的宽
int height; //最小矩形的高
for (int i = 0; i < contours.size(); i++) //contours.size()轮廓的数量
{
minrect = minAreaRect(contours[i]);
width = minrect.size.width;
height = minrect.size.height;
minrect.points(rect_info);
for (int j = 0; j < 4; j++)
{
if ( width / height >= 2 && width / height <= 5) //利用长宽比筛选条件
{
output = src(Rect(rect_info[1].x, rect_info[1].y, width, height));//根据筛选出的矩形提取车牌,相当于创建ROI
line(src, rect_info[j], rect_info[(j + 1) % 4], Scalar(0, 0, 255));//在原图画出符合条件的矩形
}
}
}
return output;
}
Defina minrect do tipo RotatedRect para armazenar o retângulo, e a função minAreaRect armazena o retângulo circunscrito do contorno em minrect.
O loop For visita cada retângulo envolvente. Se a proporção do retângulo envolvente atender às condições de filtragem (por exemplo, 2<y:x<5),ele será identificado como uma placa de carro e marcado com uma moldura vermelha no original imagem.
O resultado está apresentado abaixo:
3. Segmentação de personagens
Aqui precisamos primeiro explicar o princípio da segmentação de caracteres de projeção vertical:
após binarizar os gráficos dos caracteres, a imagem se tornará uma imagem com fundo preto e caracteres brancos. Neste momento, percorra toda a imagem e conte o número de pixels brancos em cada coluna. Em seguida, desenhe um histograma do número de pixels brancos em cada coluna.
(Conforme mostrado na figura abaixo, o eixo horizontal é cada coluna e o eixo vertical é o número de pixels brancos na coluna)
Neste momento, a coluna sem pixels brancos é a linha divisória entre os caracteres, e você só precisa saiba qual é a coluna.
void character_division(Mat input)
{
Mat expansion; //车牌图片放大
resize(input, expansion, Size(input.cols * 3, input.rows * 3), 0, 0);
int width = expansion.size().width; //获取图像的长和宽
int height = expansion.size().height;
Mat gray;
cvtColor(expansion, gray, CV_BGR2GRAY, 1);
//中值滤波
medianBlur(gray, gray, 3); //核太大会使得二值化后的字变模糊
Mat thres_hold;
threshold(gray, thres_hold, 0,255, THRESH_OTSU);
/*
int dilate_size = 2; //开操作的核的大小
Mat element_open = getStructuringElement(MORPH_RECT, Size(1 + 2 * dilate_size, 1 + 2 * dilate_size));
Mat morph_dilate;
morphologyEx(thres_hold, morph_dilate, MORPH_DILATE, element_open);
*/
int pixelvalue; //每个像素的值
int * white_nums = new int[width](); //定义动态数组并初始化, 储存每一列的白色像素数量
//遍历每一个像素,去掉边框和铆钉,再统计剩下每一列白色像素的数量
for (int col = 0; col < width; col++)
{
/*去除竖直的边框和铆钉*/
int cols_convert_num = 0;//每一列黑白转变的次数
for (int i = 0; i < height - 1; i++)//遍历某一列的所有元素,计算转变次数
{
if (thres_hold.at<uchar>(i, col) != thres_hold.at<uchar>(i + 1, col))
cols_convert_num++;
}
if (cols_convert_num < cols_thres_value)
{
continue;
}
/*去除竖直的边框和铆钉*/
for (int row = 0; row < height; row++)
{
/*去除水平的边框和铆钉*/
int rows_convert_num = 0;//每一行黑白转变的次数
for (int j = 0; j < width - 1; j++)//遍历某一行的所有元素,计算转变次数
{
if (thres_hold.at<uchar>(row, j) != thres_hold.at<uchar>(row, j + 1))
rows_convert_num++;
}
if (rows_convert_num < rows_thres_value)
{
continue;
}
/*去除水平的边框和铆钉*/
pixelvalue = thres_hold.at<uchar>(row, col);
if (pixelvalue == 255)
white_nums[col]++; //统计白色像素的数量
}
}
//画出投影图
Mat verticalProjection(height,width, CV_8UC1, Scalar(0,0,0));
for (int i = 0; i < width; i++)
{
line(verticalProjection,Point(i, height),Point(i, height - white_nums[i]), Scalar(255,255,255));
}
//根据投影图进行分割
vector<Mat> character;
int character_num = 0; //字符数量
int start; //进入字符区的列数
int end; //退出字符区的列数
bool character_block = false;// 是否进入了字符区
for (int i = 0; i < width; i++)
{
if(white_nums[i] != 0 && character_block == false) //刚进入字符区
{
start = i;
character_block = true;
}
else if (white_nums[i] == 0 && character_block == true) //刚出来字符区
{
character_block = false;
end = i;
if (end - start >= 6)
{
Mat image = expansion(Range(0, height), Range(start, end));
character.push_back(image); //push.back适用于vector类型 在数组尾部添加一个数据
}
}
}
delete[] white_nums; //删除动态数组
imshow("pro", thres_hold);
imshow("projection", verticalProjection);
}
(1) Pré-processamento de imagens de placas
Mat expansion; //车牌图片放大
resize(input, expansion, Size(input.cols * 3, input.rows * 3), 0, 0);
int width = expansion.size().width; //获取图像的长和宽
int height = expansion.size().height;
Mat gray;
cvtColor(expansion, gray, CV_BGR2GRAY, 1);
//中值滤波
medianBlur(gray, gray, 3); //核太大会使得二值化后的字变模糊
Mat thres_hold;
threshold(gray, thres_hold, 0,255, THRESH_OTSU);
Este processo inclui:
1. Use a função de redimensionamento para ampliar a placa e observe a largura e altura ampliadas para facilitar a observação e o processamento subsequentes.
2. Converter para imagem em tons de cinza.
3. Filtragem de mediana. Evite a interferência do ruído do sal e da pimenta.
4. Binarização. O algoritmo OTSU (algoritmo de diferença máxima entre classes) é usado aqui. A vantagem do algoritmo OTSU é que você não precisa definir o limite sozinho. O algoritmo calculará automaticamente o limite com base na imagem, portanto o terceiro parâmetro não tem sentido. Mas às vezes o primeiro plano e o fundo são invertidos.
Imagem pré-processada:
Percebe-se que a placa neste momento não possui apenas caracteres, mas também bordas e rebites, o que terá impacto na posterior segmentação da projeção vertical dos caracteres. Então o próximo passo é remover esses itens de interferência .
(2) Remova bordas e rebites.
Deixe-me falar sobre isso primeiro, que é um array usado para armazenar os pixels brancos de cada coluna. Como os valores dos elementos da matriz são dinâmicos, você precisa usar new para criar a matriz primeiro e, em seguida, usar delete para liberar a matriz quando terminar.
#define rows_thres_value 10 //除掉车牌框时,行跳变的阈值
#define cols_thres_value 2 //除掉车牌框时,列跳变的阈值
int pixelvalue; //每个像素的值
int * white_nums = new int[width](); //定义动态数组并初始化, 储存每一列的白色像素数量
//遍历每一个像素,去掉边框和铆钉,再统计剩下每一列白色像素的数量
for (int col = 0; col < width; col++)
{
/*去除水平的边框和铆钉*/
int cols_convert_num = 0;//每一列黑白转变的次数
for (int i = 0; i < height - 1; i++)//遍历某一列的所有元素,计算转变次数
{
if (thres_hold.at<uchar>(i, col) != thres_hold.at<uchar>(i + 1, col))
cols_convert_num++;
}
if (cols_convert_num < cols_thres_value)
{
continue;
}
/*去除水平的边框和铆钉*/
for (int row = 0; row < height; row++)
{
/*去除竖直的边框和铆钉*/
int rows_convert_num = 0;//每一行黑白转变的次数
for (int j = 0; j < width - 1; j++)//遍历某一行的所有元素,计算转变次数
{
if (thres_hold.at<uchar>(row, j) != thres_hold.at<uchar>(row, j + 1))
rows_convert_num++;
}
if (rows_convert_num < rows_thres_value)
{
continue;
}
/*去除竖直的边框和铆钉*/
pixelvalue = thres_hold.at<uchar>(row, col);
if (pixelvalue == 255)
white_nums[col]++; //统计白色像素的数量
}
}
O princípio de remoção de bordas e rebites aqui é: Tome a linha como exemplo. Conte o número de transições de branco e preto que ocorrem em cada linha, pois na área do caractere o número de transições deve ser maior, enquanto o número de transições nas áreas de fundo e borda é menor. Basta definir um limite e as linhas com menos transições que o limite não serão contadas no cálculo dos pixels brancos. O mesmo vale para colunas.
/*去除竖直的边框和铆钉*/
int rows_convert_num = 0;//每一行黑白转变的次数
for (int j = 0; j < width - 1; j++)//遍历某一行的所有元素,计算转变次数
{
if (thres_hold.at<uchar>(row, j) != thres_hold.at<uchar>(row, j + 1))//本行与下一行颜色不同
rows_convert_num++;
}
if (rows_convert_num < rows_thres_value)
{
continue;
}
/*去除竖直的边框和铆钉*/
Tomemos o comportamento como exemplo. Da primeira linha j = 0, até a penúltima linha j = largura - 1.
Se a cor desta linha for diferente da cor da linha seguinte, o número de alterações será +1
if (thres_hold.at<uchar>(row, j) != thres_hold.at<uchar>(row, j + 1))//本行与下一行颜色不同
rows_convert_num++;
Se o número de transições for inferior ao limite, esta coluna não será incluída nas estatísticas.
if (rows_convert_num < rows_thres_value)
{
continue;
}
(3) Desenhe o diagrama de projeção e segmente-o.
Mat verticalProjection(height,width, CV_8UC1, Scalar(0,0,0));
for (int i = 0; i < width; i++)
{
line(verticalProjection,Point(i, height),Point(i, height - white_nums[i]), Scalar(255,255,255));
}
//根据投影图进行分割
vector<Mat> character;
int character_num = 0; //字符数量
int start; //进入字符区的列数
int end; //退出字符区的列数
bool character_block = false;// 是否进入了字符区
for (int i = 0; i < width; i++)
{
if(white_nums[i] != 0 && character_block == false) //刚进入字符区
{
start = i;
character_block = true;
}
else if (white_nums[i] == 0 && character_block == true) //刚出来字符区
{
character_block = false;
end = i;
if (end - start >= 6)
{
Mat image = expansion(Range(0, height), Range(start, end));
character.push_back(image); //push.back适用于vector类型 在数组尾部添加一个数据
}
}
}
delete[] white_nums; //删除动态数组
O diagrama de projeção desenhado é o seguinte:
O algoritmo aqui é: use o método para determinar se a altura é 0, use a variável bool para marcar se é a área do caractere e use início e fim para registrar o número de colunas que entram no área do personagem.
Em seguida, armazene a imagem de um determinado intervalo de caracteres na matriz de caracteres.
if (end - start >= 6)
{
Mat image = expansion(Range(0, height), Range(start, end));
character.push_back(image); //push.back适用于vector类型 在数组尾部添加一个数据
}
4. Aprendizado de máquina para reconhecer personagens
Estudando, tratarei disso mais tarde