OpenCVの簡単なナンバープレート認識

OpenCV ナンバー プレート認識

概要

この記事でのナンバー プレート認識はいくつかのステップに分かれています:
1. 画像の前処理
(1) グレースケール画像への変換
(2) ガウス フィルターの実行
(3) バイナリ画像への変換
(4) エッジ検出
(5) 形態学的処理

2. ナンバープレートを求める
(1) 前処理画像から各部の輪郭を求める
(2) 輪郭の外接長方形を取得する
(3) 長さと幅の条件からその長方形をナンバープレートとして決定する

3. 文字の切り出し
(1) ナンバープレートの前処理
(2) 枠線やリベットの除去
(3) 垂直投影法による文字の切り出し

4. 機械学習による文字認識

この記事で処理したサンプル画像は次のとおりです。

各ステップについては、以下で詳しく説明します。
ここに画像の説明を挿入します

1. 画像の前処理

前処理関数は次のとおりです。


```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);

ここでは、単に cvtcolor 関数を呼び出してグレースケール画像に変換します。

(2) ガウスフィルタリングを行う

	Mat gauss;
	GaussianBlur(gray, gauss, Size(5, 5), 0, 0);

単純なガウス フィルタリング。エッジがぼやけすぎないようにカーネルを大きすぎる必要はありません。

(3) バイナリイメージに変換

Mat thres_hold;
	threshold(median, thres_hold, 100, 255, THRESH_BINARY);

ここでの二値化の目的は、前景と背景を大まかに区別し、ナンバープレートを強調することです。後続のエッジ検出のために干渉アイテムを減らします。

(4) エッジ検出

Mat canny_edge;
Canny(thres_hold, canny_edge, 50, 100, 5);

Canny 検出オペレータの高閾値と低閾値 (3 番目と 4 番目のパラメータ) の比率は、一般に 2:1 または 3:1 であり、検出されたエッジ画像は次のようになります

ここに画像の説明を挿入します

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);

ここでは、形態学的クロージング操作と形態学的オープニング操作が使用されます。手順は、操作コアのサイズを定義する -> コアを定義する -> 対応する操作を実行することです。
ここでの終了操作の目的は、アウトラインの切れ目を埋め、切れたエッジを接続して全体を形成することです。コアが大きいほど、エッジによって接続された全体も大きくなります。
クロージング操作の結果:ここに画像の説明を挿入します
オープン操作の目的は、全体といくつかの小さな部分のラインを削除することです。コアが大きいほど、その効果はより顕著になります。
オープン操作の結果:
ここに画像の説明を挿入します

2. ナンバープレートの抽出

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) 前処理画像から各部位の輪郭を求める

//寻找轮廓
vector<vector<Point>> contours;  //定义轮廓点向量
vector<Vec4i> hierarchy;   //轮廓的索引
findContours(input, contours, hierarchy, RETR_TREE, CHAIN_APPROX_SIMPLE);//提取轮廓

(ここでは、入力は前処理された画像です)
2 番目のパラメーター contours は、見つかった輪郭を格納する OutputArrayofArrays タイプです。輪郭はもともと点の集合、つまりベクトルがあるので、輪郭はたくさんあります。したがって、 vector<vertor>
の 3 番目のパラメーター階層はOutputArrays 型であり、画像のトポロジー情報が含まれています。各輪郭 contours[i] には 4 つの階層要素、hierarchy[i][0] ~
hierarchy[i][3] が含まれており、それぞれ次の輪郭、前の輪郭、親輪郭、および埋め込まれた輪郭のインデックス番号を表します。 。したがって、階層の各要素は Vec4i タイプになります。

(2) 輪郭の外接長方形を求め、縦横の条件からナンバープレートの長方形と判定する

    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;
}

RotatedRect 型の minrect を定義して四角形を格納し、minAreaRect 関数で輪郭の外接四角形を minrect に格納します。
For ループは各囲み四角形を参照し、囲み四角形のアスペクト比がフィルター条件 (たとえば、2<y:x<5) を満たす場合、ナンバー プレートとして識別され、オリジナルでは赤枠でマークされます。画像。
結果を以下に示します。
ここに画像の説明を挿入します
ここに画像の説明を挿入します

3. 文字の分割

ここで、まず垂直投影文字分割の原理を説明する必要があります。
文字グラフィックスを 2 値化すると、画像は背景が黒、文字が白の画像になります。このとき、画像全体を走査し、各列の白いピクセルの数を数えます。次に、各列の白いピクセルの数のヒストグラムを描画します。
(下図のように、横軸が各列、縦軸がその列の白ピクセルの数です)
ここに画像の説明を挿入します
このとき、白ピクセルのない列が文字間の区切り線となるので、入力するだけで済みます。それがどの列であるかがわかります。

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) ナンバープレート画像の前処理

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);

このプロセスには次の内容が含まれます。
1. サイズ変更機能を使用してナンバー プレートを拡大し、その後の観察と処理を容易にするために、拡大された幅と高さをメモします。
2. グレースケール画像に変換します。
3. メディアン フィルタリング。塩コショウノイズの干渉を防ぎます。
4.二値化。ここでは、OTSU アルゴリズム (最大クラス間差分アルゴリズム) が使用されます。OTSU アルゴリズムの利点は、しきい値を自分で設定する必要がなく、アルゴリズムが画像に基づいて自動的にしきい値を計算するため、3 番目のパラメーターは意味がありません。ただし、前景と背景が逆になる場合もあります。
前処理画像:
ここに画像の説明を挿入します
この時点のナンバープレートには文字だけでなく枠線やリベットもあり、その後の文字の垂直投影分割に影響を与えることがわかります。したがって、次のステップは、これらの干渉アイテムを除去することです

(2) 境界線とリベットを削除する
まず最初に説明しますが、これは各列の白いピクセルを格納するために使用される配列です。配列要素の値は動的であるため、最初に new を使用して配列を作成し、完了したら delete を使用して配列を解放する必要があります。

#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]++; //统计白色像素的数量
	}
}

ここで境界線とリベットを削除する原則は次のとおりです。行を例に挙げます。各行で発生する白と黒のトランジションの数を数えます。これは、文字領域ではトランジションの数が多くなる必要がある一方、背景と境界線の領域ではトランジションの数が少ないためです。しきい値を設定するだけで、しきい値よりも遷移が少ない行は白ピクセルの計算にカウントされません。列についても同様です。

/*去除竖直的边框和铆钉*/
		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;
		}
		/*去除竖直的边框和铆钉*/

行動を例に挙げてみましょう。最初の行 j = 0 から最後から 2 番目の行 j = width - 1 まで。
この行の色が次の行の色と異なる場合、変更の数は +1 されます。

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;
}

(3) 投影図を描き、分割します。

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; //删除动态数组

描画された投影図は次のとおりです。
ここに画像の説明を挿入します
ここでのアルゴリズムは、 メソッドを使用して高さが 0 であるかどうかを判断し、bool 変数を使用して文字領域かどうかをマークし、start と end を使用して文字領域に入る列の数を記録します。キャラクターエリア。
次に、特定の文字範囲の画像を文字配列に格納します。

 if (end - start >= 6)
		   {
    
    
			   Mat image = expansion(Range(0, height), Range(start, end));
			   character.push_back(image); //push.back适用于vector类型 在数组尾部添加一个数据
		   }

4. 機械学習による文字認識

勉強中、後で対処します

おすすめ

転載: blog.csdn.net/weixin_43960499/article/details/102773588