블로그 이미지
devtang
Instagram : @taebr0 devtaehyeong@gmail.com

calendar

1 2 3 4
5 6 7 8 9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28 29 30 31

Notice

2021. 1. 5. 20:41 개인 프로젝트

안녕하세요. 요새 코로나도 심해지는 와중에 건강 조심하시기 바랍니다.

집에서 코딩하는 것을 추천드려요~

 

 

 

이번 게시물에서는 딥러닝을 이용하지 않고

OpenCV를 이용하여 자동차 번호판 글꼴의 숫자를 인식하는 프로젝트를 작성합니다.

아직 자동차 번호판을 곧바로 인식하기엔 어려움이 있기 때문에 자동차 번호판의 숫자 글꼴을 인식하는 프로젝트로 진행하였습니다.

 

딥러닝을 이용해보신적이 있으시다면 숫자인식은 어떻게보면 "Hello World"같은 예제일정도로 쉬운 일입니다.

숫자나 어떤 이미지를 인식한다고 하면, 딥러닝에서는 데이터에서 특징을 추출하여 학습을 하여 인식을합니다.

그러나 OpenCV 라이브러리만을 이용하여 인식한다면 꽤나 생각해야할 부분이 많습니다..

본 게시물에서 진행한 숫자인식은 이런방법으로도 접근할 수 있구나라고 생각하고 봐주시면 감사하겠습니다.

 

제가 진행한 프로젝트의 흐름도는 다음과 같습니다.

우선 간단하게 요약하자면 숫자인식을 하기 위해 이미지 전처리, 관심영역 추출, 히스토그램 생성의 과정을 거칩니다.

 

표준 숫자 0~9 까지에 대해 위의 세과정을 거쳐서 표본을 만든후에

테스트할 숫자를 넣어 위와 같은 과정을 거쳐 표본 히스토그램의 픽셀수와 테스트 이미지 히스토그램의 픽셀수를 비교하여 가장 적게 차이나는 히스토그램을 가진 숫자를 인식된 숫자로 판정합니다. 

 

이미지 전처리

 

#define _CRT_SECURE_NO_WARNINGS

#include <opencv2/core.hpp>
#include <opencv2/highgui.hpp>
#include <opencv2/opencv.hpp>
#include <iostream>

using namespace std;
using namespace cv;

int main()
{
	Mat original_image; //image read(grayscale)
	Mat binary_image; //binary image


	original_image = imread("1.jpg", IMREAD_GRAYSCALE); //grayscale
	threshold(original_image, binary_image, 127, 255, THRESH_BINARY); //threshold

	imshow("grayscale", original_image);
	imshow("binary", binary_image);
	waitKey(0);
}

OpenCV를 이용하면 간단하게 이미지를 불러와서 그레이스케일 변환과 이진화를 진행할 수 있습니다.

 

이진화를 진행할때 threshold 값을 정해주어야 합니다. 그림에서 보이는 것과 같이 0이라는 숫자에

이미지의 quality가 좋지않아 noise가 발생되어있는데 0과 255 두개로만 픽셀을 깔끔하게 표현하기 위해서는

0의 윤곽쪽을 제외하고는 모두 255(흰색)으로 만들어줍니다. 

 

입력된 기본 original image를 그레이스케일 변환과 이진화를 진행한 사진입니다.

컬러 이미지를 흑백으로 전환하여 1채널(0~255)의 값으로 변경후 숫자 이미지의 픽셀을 0255로만 이루어지게 이진화를 거쳐 연산에 효율성과 인식율을 향상할 수 있었습니다.

 

이렇게 전처리를 진행함으로써 이미지의 픽셀에 접근하여 연산하는 과정 및 픽셀을 확인하는 과정을 효율적으로

진행할 수 있습니다. 

 

관심 영역 추출

이제 이진화까지의 과정을 거쳐서 0과 255로만 구성된 이미지를 얻었습니다.

다음 단계는 숫자 이외의 불필요한 배경들을 제거하여 숫자의 해당하는 픽셀의 연산에 조금더 정확하고 효율적으로

진행할 수 있도록 관심 영역의 x축 최소,최대 y축 최소,최대 좌표를 구하여 자르는 과정입니다. 

아래의 그림을 보면 이해가 쉽게 될 것 같습니다.

 

 

그림의 가로축을 X, 세로축 Y으로 놓았을때 좌측 상하단 우측 상하단을 순차적으로 돌면서 0의 라인에 해당하는

최솟값 최대값을 찾아냅니다. X-MIN ,X-MAX, Y-MIN , Y-MAX 값만 찾아내면 그값을 기준으로 자르면 되니까요.

 

#define _CRT_SECURE_NO_WARNINGS

#include <opencv2/core.hpp>
#include <opencv2/highgui.hpp>
#include <opencv2/opencv.hpp>
#include <iostream>

using namespace std;
using namespace cv;

int main()
{
	Mat original_image; //image read
	Mat binary_image; //binary image
	Mat roi_image; //roi image

	int x_count[200] = { 0, };
	int y_count[200] = { 0, };
	int x_min = 0, x_max = 0, y_min = 0, y_max = 0;
	int status = 0, pixel_count = 0;

	original_image = imread("0.jpg", IMREAD_GRAYSCALE); //grayscale
	threshold(original_image, binary_image, 127, 255, THRESH_BINARY); //threshold


 /************************** ROI 좌표 찾기 *****************************/
	for (int x = 0; x < binary_image.rows; x++) //x좌표 최솟값
	{
		for (int y = 0; y < binary_image.cols; y++)
		{
			if (binary_image.at<uchar>(y, x) == 0)
			{
				x_min = x;
				status = 1;
				break;
			}
			if (status == 1)
				break;
		}
	}
	status = 0;

	for (int y = binary_image.cols - 1; y >= 0; y--) //x좌표 최댓값
	{
		for (int x = binary_image.rows - 1; x >= 0; x--)
		{
			if (binary_image.at<uchar>(y, x) == 0)
			{
				x_max = x;
				y_max = y;
				status = 1;
				break;
			}
			if (status == 1)
				break;
		}
	}
	status = 0;

	for (int y = 0; y < binary_image.cols; y++) //y좌표 최솟값
	{
		for (int x = 0; x < binary_image.rows; x++)
		{
			if (binary_image.at<uchar>(y, x) == 0)
			{
				y_min = y;
				status = 1;
				break;
			}
			if (status == 1)
				break;
		}
	}
	status = 0;

	for (int x = binary_image.rows - 1; x >= 0; x--) //y좌표 최댓값
	{
		for (int y = binary_image.cols - 1; y >= 0; y--)
		{
			if (binary_image.at<uchar>(y, x) == 0)
			{
				if (y >= y_max)
					y_max = y;
				if (x >= x_max)
					x_max = x;
				status = 1;
				break;
			}
			if (status == 1)
				break;
		}
	}
	status = 0;
/************************** ROI 좌표 찾기 *****************************/

	roi_image = binary_image(Rect(Point(x_min, y_min), Point(x_max, y_max)));
	resize(roi_image, roi_image, Size(200, 200), 0, 0, INTER_LINEAR);

	rectangle(binary_image, Rect(Point(x_min, y_min), Point(x_max, y_max)), Scalar(0, 0, 100), 1, 4, 0);

	imshow("original", original_image);
	imshow("bin", binary_image);
	imshow("roi_image", roi_image);

	waitKey(0);
}

본 코드에는 나와있지 않지만 테스트겸 line을 그려주는 함수를 이용한 사진입니다.

 

위의 코드를 숫자 0을 입력하여 얻은 이미지입니다.

좌표가 올바르게 찾아졌는지 rectangle 함수를 이용하여 관심영역에 사각형을 그려보았습니다.

 

좌상하단 , 우상하단 = 4번의 연산이 필요합니다.

이중 for문을 이용하여 간단하게(?) 구현할 수 있습니다.

좌표를 찾았을때 break문을 걸어 탈출하며 flag를 바꾸는 방식으로 구현해보았습니다.

사실 코딩실력이 좋은편이 아니기 때문에 다소 난잡할 수 있으나,

구현적인 측면도 중요했기 떄문에 코드가 난잡하더라도 귀엽게 주니어 개발자를 봐주시면 감사하겠습니다.

	roi_image = binary_image(Rect(Point(x_min, y_min), Point(x_max, y_max)));
	resize(roi_image, roi_image, Size(200, 200), 0, 0, INTER_LINEAR);

 

주의할 코드부분은 관심영역을 자르고 나서 원본 사진의 해상도와 맞게 resize를 시켜주었습니다.

OpenCV에서는 resize 진행시 보간법(interpolation methods)을 지정해줄 수 있습니다. 
대표적으로 INTER_CUBIC ,INTER_LINEAR 등이 있습니다만, INTER_CUBIC을 사용하면 보다 더 선명한 이미지를 얻을수 있다고 합니다. 그러나 저는 INTER_LINEAR 방식으로 진행하였습니다.

 

 

히스토그램 생성

관심영역이 추출된 이미지를 가지고 본격적으로 인식에 가장 필요한 히스토그램을 생성하는 파트입니다.

 

히스토그램이란?

 

히스토그램(Histogram)은 표로 되어 있는 도수 분포를 정보 그림으로 나타낸 것입니다.

그러나 이미지 히스토그램은 이야기가 조금 다릅니다.

 

이미지 히스토그램은 가로축(x축) 에는 이미지의 픽셀 값을 나타내는 좌표값이고, 세로축(y축)으로는 픽셀의 수를 나타내는 좌표값입니다. 히스토그램으로 이미지의 대비(Contrast)나 빛의 강도 등을 나타낼 수도 있기 때문에, 이미지에서 어떤 특징점이 어느정도 분포하고 있는지 알 수 있는 그래프라고 볼 수 있습니다.

 

0과 9에대한 히스토그램... 별 차이가 없다.

저는 본 프로젝트를 진행하면서, 그레이스케일 변환한 숫자에 대해 히스토그램을이용하여 숫자 0~9 까지의 히스토그램을 모두 그려보았으나 뚜렷한 특징을 찾을 수가 없었습니다. 따라서 아래의 그림과 같이 진행하였습니다.

 

 

 

 

x축좌표에 대한 0에 해당하는 픽셀 개수를 표현

 

y축좌표에 대한 0에 해당하는 픽셀 개수를 표현

 

가로, 세로 축 좌표를 기준으로 숫자의 픽셀(검정색)의 개수를 카운팅하여, 빈 검정색 이미지에 line함수로 픽셀의 개수만큼 그려주었습니다. 축의 기준을 나눠서 히스토그램을 생성한 이유는 기존 히스토그램 사용시에, 밝은 픽셀과 어두운 픽셀의 분포를 표시하기 때문에 입력되는 이미지의 숫자의 크기에 따라 오인식 하는 확률이 크기 때문에 위와 같이 진행했었습니다.

 

x축 좌표에 대한 히스토그램 을 보면 가로축은 x가 0부터 image의 rows까지 , 세로축은 픽셀의 개수를 나타내었고

y축 좌표에 대한 히스토그램 역시 보면 가로축은 y가 0부터 image의 cols까지, 세로축은 픽셀의 개수를 나타내었습니다.

 

/************************** ROI 좌표 찾기 *****************************/

	roi_image = binary_image(Rect(Point(x_min, y_min), Point(x_max, y_max)));
	resize(roi_image, roi_image, Size(200, 200), 0, 0, INTER_LINEAR);

	Mat x_hist_image = Mat::zeros(roi_image.rows, roi_image.cols, CV_8U);
	Mat y_hist_image = Mat::zeros(roi_image.rows, roi_image.cols, CV_8U);
	Mat total_hist_image = Mat::zeros(roi_image.cols, roi_image.rows * 2, CV_8U);
	/**************************   histogram    ****************************/
	for (int y = 0; y < roi_image.rows; y++) //y축 히스토그램
	{
		pixel_count = 0;
		for (int x = 0; x < roi_image.cols; x++)
		{
			if (roi_image.at<uchar>(x, y) == 0)
			{
				pixel_count++;
			}
		}
		y_count[y] = pixel_count;

	}



	for (int x = 0; x < roi_image.cols; x++) //x축 히스토그램
	{
		pixel_count = 0;
		for (int y = 0; y < roi_image.rows; y++)
		{
			if (roi_image.at<uchar>(x, y) == 0)
			{
				pixel_count++;
			}
		}
		x_count[x] = pixel_count;
	}
	for (int x = 0; x < roi_image.rows; x++) //x축 히스토그램 그리기
		line(x_hist_image, Point(x, roi_image.rows), Point(x, roi_image.rows - x_count[x]), Scalar(255, 255, 255), 0);

	for (int y = 0; y < roi_image.cols; y++) //y축 히스토그램 그리기
		line(y_hist_image, Point(y, roi_image.cols), Point(y, roi_image.cols - y_count[y]), Scalar(255, 255, 255), 0);

	/* 통합 히스토그램 그리기 */
	for (int x= 0; x < roi_image.rows; x++) 
		line(total_hist_image , Point(x + 200, roi_image.rows), Point(x+200, roi_image.rows - x_count[x]), Scalar(255, 255, 255), 0);
	for (int y = 0; y < roi_image.cols; y++)
		line(total_hist_image, Point(y, roi_image.cols), Point(y, roi_image.cols - y_count[y]), Scalar(255, 255, 255), 0);

	/**************************   histogram    ****************************/

	imshow("x_hist", x_hist_image);
	imshow("y_hist", y_hist_image);
	imshow("original", original_image);
	imshow("bin", binary_image);
	imshow("roi_image", roi_image);
	imshow("total", total_hist_image);
	waitKey(0);
}

코드 중 가로 세로축 히스토그램을 그리는 코드의 일부입니다.

위의 코드 역시 이중 for문을 이용하여 이미지의 행 ,열을 훑어 연산하면서 픽셀의 카운트를 진행하였습니다.

테스트할 이미지의 데이터와 비교할 표준 숫자 데이터 0~9까지의 히스토그램을 모두 생성하여 저장하였습니다.

 

그림과 같이 각 숫자마다 모두 다른 히스토그램을 확인할 수 있습니다.

이제 표준 데이터의 히스토그램을 모두 저장하였으니,

테스트할 숫자 이미지 역시 지금까지 진행했던 방식으로 히스토그램을 생성합니다.

 

테스트할 숫자 데이터는 인터넷에서 자동차 번호판의 숫자를 찾아서 아래 그림과 같이 크기와 위치가 다르게 저장하여 진행하였습니다.

 

/* compare */
	for (int i = 0; i < 10; i++)
	{
		for (int j = 0; j < 10; j++)
		{
			for (int y = 0; y < total_hist_image[i].cols; y++) //y축 히스토그램
			{
				for (int x = 0; x < total_hist_image[i].rows; x++)
				{
					if (total_hist_image[i].at<uchar>(x, y) != test_total_hist_image[j].at<uchar>(x, y))
						test_check_sum[i]++;
				}
			}
			if (check_sum[i] <= min)
				min = test_check_sum[i];
		}
		if (test_check_sum[i] == min)
			min = i;
		detect_num[i] = min;
	}
    
    /* compare */

표준 히스토그램 데이터와의 픽셀차이를 계산하는 코드중 일부입니다.

 

본 코드가 관심영역 추출이 올바르게 되는지, 글자의 크기에 영향을 미치지 않는지 테스트를 진행해보았습니다.

 

youtu.be/1li7GSmIwqk

 

 

테스트할 숫자 이미지가 입력이 되면 표준 숫자 데이터 0~9 를 각각 비교하면서 픽셀차이가 가장 적은 숫자를

인식하는 숫자로 판정하였습니다. 위의 사진과 같이 똑같은 글꼴이지만 위치와 숫자의 크기를 달리 하여도 표준 히스토그램의 특징과 뚜렷하게 다른점이 거의 없습니다.

 

테스트 숫자의 픽셀이 관심영역을 추출하면서 resize되어

글꼴이 깨져도 숫자의 본 모습은 크게 바뀌지 않아서 그런것 같습니다.

한계점..

 

본 프로젝트에서 진행한 숫자인식 방법은 가로 세로축을 기준으로 픽셀의 개수를 측정하였기 때문에 숫자가 조금만 회전이 되어도 오인식 하는 경우가 많았습니다..

 

회전이나 글꼴 차이에 해당하는 경우의 수는 고려하지 않고 반영하였기 때문에 본프로젝트는 추후에 더욱 좋은 구상을 통해 보완해야할 것 같습니다. 추후에 더욱 좋은 구상을 통해 보완하게 된다면 비교적 무거운 딥러닝을 사용하지 않고도 자동차 번호판을 인식할 수 있는 프로그램을 만들 수 있기 때문에 제한된 하드웨어 성능내에서 효율적인 시스템을 만들수 있을 것 같습니다.

 

본 프로젝트는 자동차 번호판 숫자 인식을 위한 프로젝트의 일부로 진행하였습니다.

막상 딥러닝을 이용할때는 몰랐는데 딥러닝의 필요성을 알게되는 프로젝트 중 하나였습니다. 영상처리에 대한 공부를 진행하면서 숫자 인식을 위해 진행했던 부분들이 흥미로운점이 많았던 것 같습니다.

 

긴 게시물 읽어주셔서 감사합니다.

 

posted by devtang
2020. 2. 28. 23:57 Jetson Nano

현재 Jetson nano에 깔려있는 CUDA 10.0 , JetPack 4.3 , OpenCV 3.4 버전을 기준으로 작성하였습니다.

 

 

 

YOLO ?

YOLO(You Only Look Once)는 이미지 내의 bounding box와 class probability를 single regression problem으로 간주하여, 이미지를 한 번 보는 것으로 객체의 종류와 위치를 추측합니다. 아래와 같이 single convolutional network를 통해 multiple bounding box에 대한 class probablility를 계산하는 방식입니다.

YOLO의 대한 자세한 원리와 설명은 아래 사이트를 참조해주세요.

 

https://github.com/zeuseyera/darknet-kr/blob/master/2_YOLO/yolo.md

 

 

zeuseyera/darknet-kr

:kr: 다크넷(darknet): C로 작성한 신경망 공개소스. Contribute to zeuseyera/darknet-kr development by creating an account on GitHub.

github.com

 

 

우선 YOLO 버전으로는 크게 두가지가 있습니다.

 

https://pjreddie.com/darknet/

 

Darknet: Open Source Neural Networks in C

Nightmare Use Darknet's black magic to conjure ghosts, ghouls, and wild badgermoles. But be warned, ye who enter here: no one is safe in the land of nightmares.

pjreddie.com

https://github.com/AlexeyAB/darknet

 

AlexeyAB/darknet

Windows and Linux version of Darknet Yolo v3 & v2 Neural Networks for object detection (Tensor Cores are used) - AlexeyAB/darknet

github.com

라즈베리파이나 젯슨 나노 보드에서 AlexeyAB 버전이 원활히 구동되는 것을 확인했습니다. 

두 버전의 차이는 알아보니 pjreddie버전이 원래 yolo v3의 저자가 만든 버전이고, AlexeyAB버전은 

아직까지도 활발한 업데이트를 하고 있으며 연구 목적(학습할때 mapping,손실율 등) 을 알 수 있습니다.

저는 AlexeyAB버전으로 설치하였습니다.

 

우선 기본적으로 업데이트를 한번 진행해줍니다.

sudo apt-get update
sudo apt-get upgrade

또한 YOLO는 실시간 객체인식 시스템 중 하나로 꽤 무겁기 때문에 GPU를 이용하여 돌려야 합니다.

Jetson nano는 초기 OS설치시에 CUDA가 기본적으로 설치가 되어 있기 때문에 설치할 필요가 없으며

경로만 export해줍니다. 

export PATH=/usr/local/cuda-10.0/bin${PATH:+:${PATH}}

export LD_LIBRARY_PATH=/usr/local/cuda-10.0/lib64${LD_LIBRARY_PATH:+:${LD_LIBRARY_PATH}}

 

darknet 을 받아줍니다.

git clone https://github.com/AlexeyAB/darknet

cd darknet

wget https://pjreddie.com/media/files/yolov3.weights

darknet에서는 coco data(COCO: Common Object in COntext)와 학습해 놓은 weights 파일을 제공하고 있는데, 

약 80개정도 사물을 학습해놓았습니다. 기본 제공해주는 데이터 파일로써, YOLO를 처음 접했을때 돌려보는 가장 기본적인 demo정도로 생각하면 됩니다. 추가적으로 Tensorflow Lite버전처럼 경량화된 샘플인 yolov3-tiny.weights 파일도 설치해주면 좋습니다.

wget https://pjreddie.com/media/files/yolov3-tiny.weights

이제 YOLO를 build하기 위해 Makefile을 vi 명령어를 통해 약간 수정해줍니다.

sudo vi Makefile

들어가면

GPU=1
CUDNN=1
OPENCV=1

이 세개부분이 원래 0으로 되어있으므로, 위에 사진처럼 1로 바꿔줍니다. 0쪽으로 방향키버튼을 통해 이동후 

s 버튼을 눌러 1로 변경후 ESC, 방향키버튼으로 이동후 s 버튼을 눌러 1로 변경후 ESC를 해줍니다.

 

*vi 명령어 

 

 

변경이 완료됬으면 Shift + ; ( : 콜론)누르고 wq! (쓰고 나가기) 를통해 vi창을 나갑니다.

명령어창으로 돌아와서 컴파일을 진행합니다.

make

이제 YOLO 설치는 끝이 났습니다. 정상적으로 설치되었는지 구동해보도록 하겠습니다.

 

간단한 사진예제 먼저 구동해보겠습니다.

./darknet detect cfg/yolov3.cfg yolov3.weights data/dog.jpg

tiny 버전으로 구동하고 싶다면 아래 명령어를 입력해줍니다. lite한 버전이라 위의 명령어보다 처리속도가 빨라요.

./darknet detect cfg/yolov3-tiny.cfg yolov3-tiny.weights data/dog.jpg

개와 자전거,트럭이 인식이 잘되는 것을 볼 수 있습니다.

사진이 정상적으로 인식 되는것으로 보아 YOLO가 잘 설치된 것 같습니다.

 

YOLO는 웹캠을 통해 출력되는 영상을 실시간으로 인식할 수 있습니다.

숫자 0으로 실행이 안되면 1로 바꿔서 해주시면 됩니다.

./darknet detector demo cfg/coco.data cfg/yolov3.cfg yolov3.weights -c 0

마찬가지로 tiny 버전으로 구동하고 싶으면 아래 명령어를 입력해줍니다. 

./darknet detector demo cfg/coco.data cfg/yolov3-tiny.cfg yolov3-tiny.weights -c 0

웹캠으로 실시간 인식한 구동 영상입니다.

 

tiny 버전으로 구동

위의 동영상은 tiny버전으로 GPU사용하여 구동하였습니다. 13~14 프레임으로

평균 10프레임 초중반대를 뽑아내줍니다. 일반yolov3.weight파일로 진행시 1~4프레임 정도밖에 안나오는데

라즈베리파이에서는 엄두도 못내는 실시간 인식을 꽤 괜찮은 성능으로 구동시켜주네요..

 

지금까지 YOLO의 기본 설정방법 및 예제 실행에 대해 글을 작성해 보았습니다.

서투른 글 읽어주셔서 감사합니다.

 

참고사이트)

*YOLO를 처음 접할때 어려웠던 부분이 많은데 몇년전 게시물임에도 불구하고 꾸준히 댓글로 질문들에 답변해주시고 계십니다. 

https://pgmrlsh.tistory.com/

 

개발자 이상현

Instagram:sh_lee77 머신비전, YOLO, 영상처리, Deep Learning, 딥러닝

pgmrlsh.tistory.com

 

'Jetson Nano' 카테고리의 다른 글

[OpenCV] Jetson nano에 OpenCV 3.4 설치  (0) 2020.02.28
[NVIDIA]Jetson nano OS 설치  (8) 2020.02.28
posted by devtang
prev 1 next