[OpenCV] 동전 검출 및 자동 계산기

토이 프로젝트를 진행하면서 기록을 해두고 싶어서 포스트잇 방식으로 씁니다.
관련 코드는 아래 링크를 참고하여 만들었습니다.
『OpenCV 4로 배우는 컴퓨터 비전과 머신 러닝』

2022.05.07

OpenCV를 공부도 하고 글로도 정리하는데 머리속으로 잘 들어 오지 않아 간단한 토이 프로젝트를 해보기로 함.
OpenCV 관련 네이버 카페를 찾아보니 동전을 검출하여 자동으로 계산해 주는 프로젝트가 보여서 시작.
참고 네이버 카페

2022.05.09

대략적으로 개발 방향만 정함. 

  1. USB 캠으로 실시간 화면 표시.
  2. 원검출로 동전 위치 검출
  3. 검출된 동전의 금액 검출
  4. 동전의 금액을 합쳐서 화면 표시

USB 1 프레임을 그레이스케일로 변환한 뒤 adaptiveThreshold 함수를 사용하니 아래와 같이 노이즈 발생하여 그레이스케일에서 블러를 적용한 뒤 adapttiveThreshold 사용.

블러 적용 전(좌측), 후(우측)

블러 적용 전(좌측), 후(우측)

블러의 경우 양방향 필터로 사용하였고, 사용해도 노이즈가 존재하긴 함. 우선 무시하고 원 검출 진행.
노이즈는 무시하고 허프 원 검출로 진행하려 했으나 노이즈때문에 원 검출이 제대로 안된다.
노이즈 제거를 위해 모폴로지 열기를 해봤으나 이미지만 더 이상해짐.

원본 영상이 너무 안좋은 것 같아 깔끔하게 하기 위해 흰색 종이에 동전을 올림.

이렇게 하니 형광등 반사도 적어지고 노이즈도 많이 줄음. (이래서 카페 글에 하얀 종이 위에 동전을 올렸구나..!)
왠지 원 검출이 잘 될거 같다. HoughCircles 함수로 원 검출.

노이즈 최소화

허프 원 검출

Mat src_gray;
cvtColor(src, src_gray, COLOR_BGR2GRAY);

vector<Vec3f> circles;
double param1 = (double)g_iParam1;
double param2 = (double)g_iParam2;
HoughCircles(src_gray, circles, HOUGH_GRADIENT, g_Dp, g_minDist, param1, param2, g_min_radius, g_max_radius);

cvtColor(src_gray, dst, COLOR_GRAY2BGR);
for (Vec3f c : circles)
{
	Point center(cvRound(c[0]), cvRound(c[1]));
	int radius = cvRound(c[2]);

	circle(dst, center, radius, Scalar(0, 0, 255), 2, LINE_AA);
}

원 검출은 잘 되는데 기분 탓인지 허프 원 검출은 뭔가 속도도 느린거 같고, 검출한 원의 크기가 늘었다 줄었다 하고있다.
뭔가 맘에 안들음.
다른 방식으로 원 검출을 시도해봐야겠다. findContours 사용하기 위해 THRESH_BINARY_INV으로 변경.

생각보다 너무 깔끔하게 잘 나온다. 시간이 늦어서 findContours는 나중에…

Mat src_gray;
cvtColor(src, src_gray, COLOR_BGR2GRAY);

// blur 처리
Mat src_blur;
//medianBlur(src_gray, src_blur, 5);
bilateralFilter(src_gray, src_blur, -1, 10, 5);
//GaussianBlur(src_gray, src_blur, Size(), 3);

Mat dst_noise;
//threshold(src_blur, dst_noise, g_iThreshold, 255, THRESH_BINARY_INV);
int iSize = g_iThreshold;
if (iSize % 2 == 0)
	iSize--;
if (iSize < 3)
	iSize = 3;
adaptiveThreshold(src_blur, dst_noise, 255, ADAPTIVE_THRESH_GAUSSIAN_C, /*THRESH_BINARY*/THRESH_BINARY_INV, iSize, 10);
cvtColor(dst_noise, dst, COLOR_GRAY2BGR);

2022.05.10

findContour를 하기 전에 이진화 이미지를 조금 더 깔끔하게 하는 방법이 없을까 고민함.
05.09에 흰 종이를 댄것 처럼, 원본 영상에 수정을 하면 좋을 것 같아서, 히스토그램 평활화 적용.
평활화 전에 양방향 필터 적용. 결과적으로 깔끔하게 추출이 된다.

Mat frame;
bRead = video.read(frame);
if (!bRead)
	break;

// 히스토그램 평활화
Mat frame_gray, frame_blur, frame_hist;
cvtColor(frame, frame_gray, COLOR_BGR2GRAY);
bilateralFilter(frame_gray, frame_blur, -1, 10, 5); // 필터 적용
equalizeHist(frame_blur, frame_hist); // 평활화

현재까지 영상 처리 순서는
원본 영상 -> 이진화 -> 양방향 필터 -> 히스토그램 평활화 -> 이진화
그리고 findContour로 이진화 영상에서 객체를 검출.

area는 원의 면적이며, ratio는 0~1 사이의 실수값을 가지며, 1에 가까울 수록 원에 가깝다.
내 경우 area는 500 ~ 4000 정도의 값을 가지며, ratio는 0.02 ~ 0.9 정도의 값을 가짐
객체 검출을 위해 ratio 값은 0으로 해놓고, 작은 것들은 검출 되지 않게 150 이하 area는 객체로 검출 하지 않음

결과적으로는 맞게 한 것 같다.

Mat src_gray;
cvtColor(src, src_gray, COLOR_BGR2GRAY);

vector<vector<Point>> contours;
findContours(src_gray, contours, RETR_EXTERNAL, CHAIN_APPROX_NONE);

cvtColor(src_gray, dst, COLOR_GRAY2BGR);
for (vector<Point> &pts : contours)
{
	vector<Point> approx;
	approxPolyDP(pts, approx, arcLength(pts, true) * 0.02, true);

	int vtc = (int)approx.size();
	if (4 < vtc)
	{
		double len = arcLength(pts, true);
		double area = contourArea(pts);
		double ratio = 4. * CV_PI * area / (len * len);

		double area_min = (double)g_area;
		double ratio_min = (double)g_ratio * 0.1;
		if (ratio_min < ratio  && area_min < area)
		{
			Rect rc = boundingRect(pts);
            output.push_back(rc);
			rectangle(dst, rc, Scalar(0,0,255), 1);
		}
	}
}

퇴근후 찾아보니 다른 USB 캠이 있다. 실행 해보니 이전 영상보다 더 깔끔하고 좋음.
90도 위에서 찍을수 있는 카메라라서 더 장확하게 원으로 검출 된다.
검출된 원의 크기로 금액 계산이 가능할 것같다.

500원, 100원, 50원 10원 순서대로 검출

동전 금액별로 면적을 지정해서 화면에 총 금액을 표시한다.

dst = src.clone();
if (output.size() <= 0)
	return false;

int sum_coin = 0;
for (int number = 0 ; number < output.size() ; number++)
{
	vector<Point> pts = output.at(number);
	Rect rc = boundingRect(pts);
	rectangle(dst, rc, Scalar(0, 0, 255), 1);

	int area = cvRound(contourArea(pts));
	int coin = 0;
	if (g_i100max <= area && area < g_i500max)
		coin = 500;
	else if (g_i50max <= area && area < g_i100max)
		coin = 100;
	else if (g_i10max <= area && area < g_i50max)
		coin = 50;
	else if (g_area <= area && area < g_i10max)
		coin = 10;
	else
		coin = 0;

	String strCoin = format("%d", coin);
	putText(dst, strCoin, Point(rc.x, rc.y), FONT_HERSHEY_SIMPLEX, 1, Scalar(0, 0, 255));

	sum_coin += coin;
}
String strSumCoin = format("%d", sum_coin);
putText(dst, strSumCoin, Point(50, 50), FONT_HERSHEY_SIMPLEX, 2, Scalar(0, 0, 0));

imshow("dst", dst);

실행해서 확인해본 결과 검출 잘 된다.

100원, 500원을 순서대로 놓음

초반에 정했던 개발 방향을 정리하자면 아래와 같다.

  1. USB 캠으로 실시간 화면 표시.
    – 원본 영상에서 최대한 노이즈 제거, Threshold로 이진화
  2. 원검출로 동전 위치 검출
    – contour로 객체 검출
  3. 검출된 동전의 금액 검출
    – 검출된 객체의 면적으로 금액 검출
  4. 동전의 금액을 합쳐서 화면 표시
    – 검출된 금액 및 합계 화면 표시

아무래도 3번이 객체의 면적으로 계산하다 보니 외화나 크기가 큰 10원같은 것들은 검출이 안된다.

1000루피아, 10원을 순서대로 놓음

진행하면서 느끼는 결론

  • 원본 영상이 깔끔해야함
  • 객체 검출 전 단계가 생각보다 손이 많이 감, 임계값 등은 조절할 수 있게 만들어야 편할 듯
  • 생각보다 특정 상황에서 속도가 느림
  • 객체 검출하는 것도 힘들고, 검출한게 무엇인지 판별하는 것도 힘들고.

객체 검출 방식 중 머신러닝과 딥러닝을 통해 검출하는 방식이 있다고 듣긴 했는데, 더 공부해서 해봐야겠다.