Reconhecimento simples de placas Opencv

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:
Insira a descrição da imagem aqui

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:
Insira a descrição da imagem aqui
(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: Insira a descrição da imagem aqui
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:
Insira a descrição da imagem aqui

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:
Insira a descrição da imagem aqui
Insira a descrição da imagem aqui

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)
Insira a descrição da imagem aqui
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:
Insira a descrição da imagem aqui
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:
Insira a descrição da imagem aqui
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

Acho que você gosta

Origin blog.csdn.net/weixin_43960499/article/details/102773588
Recomendado
Clasificación