딥러닝을 활용하여 자동차 번호판을 추출하고 인식(OCR)을 테스트 해 보았을 때 

추출은 잘 되지만 인식은 잘 되지 않는 경우가 많았다. 

그래서 어떻게 하면 더 간단하게 이미지를 변형할 수 있을까 생각을 하며 테스트를 해보았다

    

1. 추출된 번호판에 대해서 진행한다 

2. 번호판 사이 간격이 존재한다는 가정 

3. 번호판을 하나의 단어만을 유추 할 수 있도록 쪼개보자 

 

단순 테스트용으로 내용은 부실합니다 .ㅎㅎ 

 

 순서 

1. 이미지를 gray scale로 변환 

2. Blur를 적용하여 불필요한 엣지를 줄임 

3. 엣지를 찾고 

4. 외곽선을 검출하고 

5. 외곽선 결과를 4개의 점으로 받아 넓이가 큰 순서로 정렬

6. 가장큰 외곽선을 엣지추출한 이미지에서 추출

7. Blur와 closing을 통해   이미지에 노이즈를 줄임 

8. x, y 축 각각 좌표별 원소의 합을 구해 정보가 적은 영역을 찾기 

9. 정보가 적은 영역 제거

10. 다시 x,y축 각각 좌표별 원소의 합을 구해서 plot 해보기 

 

사용한 차량 번호는 없는 번호입니다 ~ 

 

1 ~ 2 - 이미지 전처리 

img = cv2.imread("plate1.jpg")
img2 = img.copy()
img = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
img = cv2.GaussianBlur(img, (5,5),0)

 

3. edge 추출

edge = cv2.Canny(img, 100,100)

4 ~ 5 외곽선 검출 하고 넓이로 정렬

contours, _ = cv2.findContours(edge.copy(), cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)

sorted_contours = sorted(contours, key=cv2.contourArea, reverse=True)

6. 가장 큰 외곽선 선택 하여 해당 영역만 추출

contour = sorted_contours[0]
xmin = contour[:][:,:,0].min()
ymin = contour[:][:,:,1].min()
xmax = contour[:][:,:,0].max()
ymax = contour[:][:,:,1].max()

plate = edge[ymin:ymax, xmin:xmax]

7. 노이즈 제거를 위해 blur 적용 후 closing

plate = cv2.GaussianBlur(plate, (5,5),0)
kernel =  cv2.getStructuringElement(cv2.MORPH_RECT,(3,3))
plate = cv2.morphologyEx(plate, cv2.MORPH_CLOSE, kernel)

8. x축, y축 원소의 개수 확인  

h,w = plate.shape
# x축, y축 각각 원소의 개수 카운팅
x_result = np.sum(plate,axis=0) // 255 
x_result = x_result[5:-5]
y_result = np.sum(plate,axis=1) // 255
y_result = y_result[5:-5]

plt.plot(x_result[5:-5] )
plt.plot(y_result[5:-5] )
plt.xlim([0,w])

노란선이 y축 파란선이 x 축

plot을 보면 이미지에서 아무 정보가 없는 부분의 원소의 합이 0에 가깝게 나온다는 것을 확인 할 수 있다. 

그렇다면 처음 ~ 0까지 정보와 맨뒤에서 0이 아닌 원소에 대해서찾는 다면 원하는 번호만 존재하는 영역을 뽑을 수 있겠다 

 

9. 정보가 적은 영역 제거 후 다시 원소의 개수 확인 

# y축에 대해서 값이 너무 작은 영역 제외
se_list = []
condition = y_result < (y_result.max() // 2)
for i, l in enumerate(condition):
    if l == False :
        se_list.append(i)
        break
for i, l in enumerate(condition[::-1]):
    if l == False :
        se_list.append(h - i)
        break

       
# 다시 x축 y축 각각 원소의 개수 확인
plate = plate[se_list[0] : se_list[1]]
x_result = np.sum(plate,axis=0) // 255
y_result = np.sum(plate,axis=1) // 255
plt.plot(x_result[5:-5] )
plt.plot(y_result[5:-5] )
plt.xlim([0,w])
cv2.imshow("qwe", plate)
plt.show()    

그래프에서 노란선은 y축에 대한 원소의 합 파란선은 x축에 대한 원소의 합이다. 

먼저 x 축에 대하여 보면

3의 정보가 있는 영역은 x축 0 ~ 50까지라고 볼 수 있다

8의 정보가 있는 영역은 x축 50 ~ 120까지라고 볼 수 있다 

이런식으로 각 7자리 영역에 대해서 유추를 해볼 수 있다. 

 

워낙 테스트용으로 만들어본 내용이라 실제 사용은 불가능 하다 

1. 외곽선 검출에서 가장 넓은 영역이 번호판이 아닐 수 있다.

2. 차량 번호판은 다양하기 때문에 각 작업에 다른 처리가 들어가야한다. 

3. 번호판이 기울어져 있다면 x 축에대해서 원소의 개수가없는 영역이 줄어들 것이다. 

 

 

(내용 추가)  

floodFill을 활용한 정보 부곽   

h,w = plate.shape
mask = np.zeros((h+2,w+2), np.uint8)
cv2.floodFill(plate, mask, (0,0), 255, 254)
plate = cv2.bitwise_not(plate)

번호판에서 0,0 위치부터 0에 해당하는 값을255로 변경하고 bitwise not으로 색상을 반전시켰다 

 

1. Image 결합 

#include <opencv2/opencv.hpp>
#include <iostream>
#include <vector>
#include <array>

using namespace std;

int main() {
	// cv namespace는 설명을 위해 사용하지 않음 
	cv::Mat img = cv::imread("img1.png");
	cv::Mat gray_img;
	cv::cvtColor(img, gray_img, cv::COLOR_BGR2GRAY); // gray.로 변환 

	cout << gray_img.size() << " channel : " << gray_img.channels() << endl; // 225x225x1
	cout << img.size() << " channel : " << img.channels() << endl; // 225x225x3

	array<cv::Mat, 3> arr = { gray_img, gray_img, gray_img };

	cv::Mat merge_img; 
	cv::merge(arr, merge_img); // ArrayOfArray를 합침 
	
	cout << merge_img.size() << " channel : " << merge_img.channels() << endl; //225x225x3

	cv::Mat concat_img;
	cv::hconcat(merge_img, img, concat_img); // 수평으로 결합 - 차원이 같아야함 
	//cv::vconcat(merge_img, img, concat_img); // 수직으로 결합 - 차원이 같아야함 

	cout << concat_img.size() << " channel : " << concat_img.channels() << endl; // 450x225x3

	cv::imshow("qwe", concat_img); 
	cv::waitKey(0);

	return 0;
}

  • Opencv에서 제공하는 concat 함수를 사용하여 이미지를 수직 또는 수평으로 결합할 수 있다 
    - vconcat, hconcat 
  • 흑백이미지와 color 이미지를 결합하기 위해서는 흑백이미지의 경우 채널이 1이기 때문에 먼저 3채널로 만든 뒤 결합하여야 한다. 그때 cv::merge 함수를 사용하여 생성하였다  
  • merge를 사용할 때 흑백 이미지에 대해서 array를 생성한 후 merge 하였는데, vertor를 사용하지 않고 array를 사용한 이유는 우리가 원하는 차원의 개수(3)를 알고있기 때문에 원소의 개수를 지정할 수 있는 array를 사용했다 

2. 외곽선 검출 (https://docs.opencv.org/3.4/df/d0d/tutorial_find_contours.html

#include <opencv2/opencv.hpp>
#include <opencv2/imgproc.hpp>
#include <vector>
#include <array>
#include <iostream>

using namespace std; 

cv::Mat gray_img;
const int max_thresh = 255; // threshold 최댓값 
int thresh = 100;
cv::RNG rng(54321); // Random Number Generator - 랜덤값 생성

void thresh_callback(int, void*);

int main() {
	const char* img_path = "milk.jpg";
	cv::Mat img = cv::imread(img_path); // 이미지 읽어오기

	cv::cvtColor(img, gray_img, cv::COLOR_BGR2GRAY); // gray scale로 변환 

	cv::blur(gray_img, gray_img, cv::Size(3, 3)); // 이미지에 블러 적용 3x3 커널사용

	const char* window_name = "origin"; // 보여줄 윈도우 이름 
	cv::namedWindow(window_name);  // 이미지를 보여줄 윈도우 생성 
	cv::imshow(window_name, img); // 이미지 표현 

	cv::createTrackbar("Canny thresh:", window_name, &thresh, max_thresh, thresh_callback); // 이미지에 수평으로 컨트롤 할 수 있는 바 생성 ( 마지막 파라미터는 onChange 함수로 파라미터를 (int, void*) 형태로 가지고 있어야함 
	thresh_callback(0,0); // trackbar에 따라 외곽선 검출 결과를 보여주는 함수 
	cv::waitKey(0);

	return 0;
}

void thresh_callback(int, void*)
{
	cv::Mat canny_output;
	cv::Canny(gray_img, canny_output, thresh, thresh * 2); // Canny 알고리즘을 이용해서 물체의 엣지(edge)를 검출
	vector<vector<cv::Point> > contours; // Point = {int, int} = vector int가 2개 있는 
	vector<cv::Vec4i> hierarchy; // Vec4i = {int, int, int, int} = vector int가 4개있는 

	cv::findContours(canny_output, contours, hierarchy, cv::RETR_TREE, cv::CHAIN_APPROX_SIMPLE); // binary 이미지에서 외곽선을 찾는다 

	cv::Mat drawing = cv::Mat::zeros(canny_output.size(), CV_8UC3);

	for (size_t i = 0; i < contours.size(); i++)
	{
		cv::Scalar color = cv::Scalar(rng.uniform(0, 256), rng.uniform(0, 256), rng.uniform(0, 256));
		cv::drawContours(drawing, contours, (int)i, color, 2, cv::LINE_8, hierarchy, 0);
	}
	imshow("Contours", drawing);
}
  • opencv docs에서 새로운 함수를 알게되었다 
     createTrackbar - 설정한 윈도우에 설정한 변수의 값을 변경할 수 있는 바가 생성된다 
  • Edge 추출시 blur를 활용하여 불필요한 edge가 덜 나오게 할 수 있다. 
  • cv::findContours - 외곽선을 검출한다 
    • (input image, output image, contours, hierarchy, int mode, int method, cv::Point offset=cv::Points())
    • mode 의 종류 
      cv2::RETR_EXTERNAL : 외곽선중 가장 바깥쪽 선만 리턴한다 
      cv2::RETR_LIST : 모든 외곽선을 찾지만 계층관계는 구성하지 않는다 
      cv2::RETR_CCOMP : 모든 외곽선을 찾으며 2단계의 계층관계를 구성한다 
      cv2::RETR_TREE : 모든 외곽선을 찾으며 모든 계층관계를 구성한다 
      여기서 계층이란 검출된 외곽선 안에 있는 또다른 외곽선을 뜻한다 
    • method 의 종류
      cv2::CHAIN_APPROX_NONE : 모든 point를 저장
      cv2::CHAIN_APPROX_SIMPLE : 4개의 point만을 저장하여 메모리를 절약 
      cv2::CHAIN_APPROX_TC89_L1 : Teh_Chin 연결 근사 알고리즘 L1버전을 적용하여 point 개수를 줄임 
      cv2::CHAIN_APPROX_TC89_KCOS : Teh_Chin 연결 근사 알고리즘 KCOS 버전을 적용하여 point 개수를 줄임 
      return 값으로 오는 contours의 shape를 찍어보면 각각 method마다 shape이 다른것을 볼 수 있다.
  • cv::Scalar 를 이용하여 외곽선을 그릴 때마다 다른 색상을 적용 
  • cv::drawContours
  • (InputOutputArray image,
    InputArrayOfArrays contours,
    int contourIdx,
    const cv:Scalar &color,
    int thickness=1, int lineType=8,
    cv::InputArray hierarchy=noArray(),
    int maxLevel = 2147483657,
    cv::Point offset = cv::Point())

python.을 활용하여 개발을 진행하던 중 Edge device를 사용하면서 문제가 발생했다 

속도가 너무 느리고 메모리가 부족하다는 점이였다. 

그래서 C++을 활용하여 개발을 진행하고자 이전에 python으로 진행했던 opencv 예제를 C++로 변환하여 다시 진행하려한다. 

2021.06.23 - [Machine Learning/Computer Vision] - (1) OpenCV python - 기초 ( imread, imshow, imwrite, cvtColor, resize, waitkey, destroyAllWindows)

 

(1) OpenCV python - 기초 ( imread, imshow, imwrite, cvtColor, resize, waitkey, destroyAllWindows)

CNN 모델에 학습을 시키기 전 하고자 하는 목표에 맞게 이미지를 전처리하여 학습을 시키면 더 좋은 효과를 볼 수 있다. 이미지에서 필요없는 부분이 있는지, 특정 영역만을 추출해도 되는지 개

house-of-e.tistory.com

 

 

1. cv::imread 

string img_path = "asd";
Mat img = imread(img_path, IMREAD_COLOR);
  • 지정된 경로에 이미지를 읽어오는 함수이다 
  • 파라미터 - (const cv::String &filename, int flags =1 ) 
  • 첫번째 파라미터는 파일 이름으로 String형태의 문자열을 입력받는다 
  • 두번째 파라미터는 load할 color의 종류로 cv:: 에 여러가지 종류의 color type이 enum으로 정의되어있다
  • enum  ImreadModes {
    IMREAD_UNCHANGED=-1, 
    IMREAD_GRAYSCALE = 0, 
    IMREAD_COLOR = 1 // 기본값 
    ....
    }

2. cv::imshow

imshow("window_name", img);
  • 설정한 Window name을 가지는 창을 띄운다 
  • 파라미터 - (const cv::String &winname, cv::InputArray mat) 
  • 첫번째 파라미터는 생성할 윈도우의 이름으로 String형태로 문자열을 입력받는다 
  • 두번째 파라미터는 윈도우에 표시할 이미지 데이터로cv::InputArray형태로 입력받는다 

3. cv::waitKey

waitKey(0)
  • 사용자의 키 입력을 설정한 시간만큼 대기한다. 만약 0이면 무한대로 대기한다 
  • 파라미터 - (int delay=0) 
  • 첫번째 파라미터는 대기할 시간을 의미한다 단위는 milliseconds 이다 

4. cv::cvtColor : header -> (opencv2/opencv.hpp)

Mat grayImage;
cvtColor(img, grayImage, COLOR_BGR2GRAY);
Mat HSVImage; 
cvtColor(img, grayImage, COLOR_BGR2HSV);

  • 파라미터 - (cv::InputArray src, cv::OutputArray dst, int code, int dstCn = 0 ) 
  • 첫번째 파라미터는 변경을 하려는 이미지 
  • 두번째 파라미터는 변경한 결과를 저장할 변수 
  • 세번째 파라미터는  변경을 할 색상이다. enum으로 cv::ColorConversionCodes에 작성되어있다 
  • 네번째 파라미터는 결과물의 차원 수이다. 기본값은 0으로 0일시 원본이미지와 코드에서 자동적으로 차원이결정된다  

5. cv::resize 

Mat resizedImg; 
resize(img, resizedImg, Size(640, 640));
  • 파라미터 - (cv::InputArray src, cv::OutputArray dst, cv::Size dsize, double fx =(0.0), double fy = (0.0), int interpolation = 1 ) 
  • 세번째 파라미터는 변경하고자 하는 크기이다. cv::Size형태로 전달해야한다 
  • 네번째 파라미터는 수평(x축)에 적용되는 배율의 값이다 
  • 다섯번째 파라미터는 수직(y축)에 적용되는 배율의 값이다
  • 여섯번째 파라미터는 사용할 보간법의 종류를 설정할 수 있다. enum으로 InterpolationFlags에 정의되어있다

 

이번 장에서는 openCV에서 어떤 타겟을 마치 정면에서 본 듯 만들 수 있는 투영 변환에 대한 글을 작성하겠다

이전 장에서도 이용했던 mouse_events를 이용한다 

import cv2 

class MouseEvents:
    def __init__(self) -> None:
        self.points = []

    def left_button_down(self, x, y, frame):
        self.points.append([x,y])  
        cv2.circle(frame, (x,y),5,(0,0,255),-1)
        cv2.imshow('origin',frame)
        print(x,y)      

    def right_button_down(self,x,y, frame):
        self.points.pop()
        print(x,y)      

    def onMouse(self, event, x, y, flags, img):
        if event == cv2.EVENT_LBUTTONDOWN:
            self.left_button_down(x,y, img)
        elif event == cv2.EVENT_RBUTTONDOWN:
            self.right_button_down(x,y,img)

1. 원근(투영) 변환 - perspective (projection) transform 

  • 원근  
     일반적으로 사람들이 2차원 화면에서 어떤것이 더 멀리 있는지 인식하는 방법은 원근법을 이용한 것이다 가까운 것은 크게, 멀리 있는 것은 작게, 자신의 초점이 있는 곳을 기준으로 한 점으로 모인다 
  • 투영이란 
    도형이나 입체를 다른 평면에 옮기는 일
  • 우리가 진행할 문제는 이미지에서 원하는 부분의 원근감을 없에는 문제이다. 

 

2. opencv perspective transform 

import cv2 
import numpy as np 

from mouse_events import MouseEvents

POINTS_CHECK = False
# 좌표점은 좌상->좌하-> 우상 -> 우하 
# 변경할 좌표를 4x2 행렬로 전환 
# 이동할 좌표를 설정
mouse_events = MouseEvents()
mouse_events2 = MouseEvents() 

path = 'images.jpg'
frame = cv2.imread(path)
height, width  = frame.shape[:2]
cv2.imshow('origin', frame)
if POINTS_CHECK != True:
    copy_frame = frame.copy()
    cv2.setMouseCallback('origin',mouse_events.onMouse, copy_frame)
    cv2.waitKey(0)
    # 원본 이미지에서 원하는 지점의 좌표
    source_coord = np.float32( mouse_events.points )
    # H와 W 는 새로 생성될 이미지(destination_coord)의 크기를 선택한 지점의 상하좌우 최대값으로 넣기 위해 구한다
    H = max(mouse_events.points[2][0],mouse_events.points[3][0]) - min(mouse_events.points[0][0],mouse_events.points[1][0])
    W = max(mouse_events.points[0][1], mouse_events.points[1][1], mouse_events.points[2][1], mouse_events.points[3][1] ) - min(mouse_events.points[0][1], mouse_events.points[1][1], mouse_events.points[2][1], mouse_events.points[3][1] )
    # 변환 이미지에서 원하는 지점의 좌표 ( (0,0) ~ (W,H)로 변환 )
    destination_coord = np.float32( [ [0,0], [0,H], [W,0], [W,H] ] )
    # 원본 -> 변환 미지수 8개를 구한다
    M = cv2.getPerspectiveTransform(source_coord, destination_coord)
    # 변환 -> 원본 미지수 8개를 구한다
    M2 = cv2.getPerspectiveTransform(destination_coord, source_coord)
	# 원본 -> 변환의 역행렬을 구한다 
    INV_M = np.linalg.pinv(M)
    
    
transformed = cv2.warpPerspective(frame, M, (W,H))
cv2.imshow('transformed', transformed)
  • opecv에서는 간단하게 원근변환을 할 수 있게 해주는 getPerspectiveTransform 함수와 warpPerspective 함수를 제공한다 
    getPerspectiveTransform은 이용을 하자면 미지수를 구하는 단순한 수학 계산이다. 사용하기 위해서는 해당 점이 이동할 위치를 알고 있어야 한다 또한 사용상 원하는 지점의 좌표를 선택할 때 좌상, 좌하, 우상, 우하 순서대로 입력해야 한다.

  • cv2.warpPerspective(frame, M, (W, H)) 
    첫 번째 파라미터로는 사용될 이미지의 데이터가 들어간다 
    두 번째 파라미터 M 은 getPerspectiveTranform을 통해 구한 변환 행렬이 들어간다 ( 아래 행렬식에서 P들이 M이 된다)
    세 번째 파라미터는 결과로 나오는 이미지의 높이, 넓이이다 

다음과 같이 M은 3x3의 행렬이다
미지수는 P11 ~ P32 8개

  • MouseCallback을 이용하여 원하는 영역의 꼭짓점 4개를 선택한 후 아무 키나 눌러 진행한다 

 

3. 역변환

  • 만약 변환된 이미지에서 특정 좌표가 원본 이미지에서 어느 지점인지 알고 싶다면 어떻게 할 수 있을까? 

  • 위 사진과 같이 변환된 이미지에서 파란 선의 좌표가 원본 이미지에서 어느 지점인지 알기 위해서는 이전에 구한 변환에 사용한 행렬의 역행렬을 구하여 변환 이미지에서 원하는 지점의 좌표와 행렬 곱해주면 된다 
if POINTS_CHECK != True:
    cv2.setMouseCallback('transformed',mouse_events2.onMouse, transformed)
    cv2.waitKey(0)
    line_coord = np.float32(mouse_events2.points)
    One = np.squeeze(INV_M @ np.expand_dims(np.append(line_coord[0], 1), axis=1))
    Two = np.squeeze(INV_M @ np.expand_dims(np.append(line_coord[1], 1), axis=1))
    One = One[:2] / One[2]
    Two = Two[:2] / Two[2]

    POINTS_CHECK = True

cv2.line(transformed, line_coord[0].astype(int), line_coord[1].astype(int), (255,0,0),2,1)
cv2.line(frame, One.astype(int), Two.astype(int), (255,0,0),2,1)

dst2 = cv2.warpPerspective(transformed, M2, (height, width))
cv2.imshow('transform2', dst2)

cv2.imshow('origin', frame)
cv2.imshow('transformed', transformed)
cv2.waitKey(0)
  • 행렬곱은 간단하게 @를 이용하여 구할 수 있다. 
  • 행렬의 길이가 서로 다르니 맞추기 위해 np.append로 값 1을 넣었다 그 후 하나의 차원을 추가해 3x1의 차원을 가지도록 만들었다 ( 3x3 @ 3x1 ) 
  • 위의 결과로 나온 One 은 wx, wy, w 형태이다 ( 위의 공식 참고 ) 그렇기 때문에 원본의 x, y를 구하기 위해서는 w를 구하여 나누어줘야 한다. 
  • M = P11 ~ P 32 
  • 원하는 지점 = x, y를 x, y, 1로 변경 
  • One = (wx`, wy`, w) 

위의 공식을 잘 이해하고 있다면 무리 없이 진행할 수 있다. 복잡한 수학 계산은 컴퓨터가 해주니 공식을 가지고 사용하면 된다! 

 

속도를 산출하는 과정에서 핀홀 카메라에서 기준선을 그리려 보니 원근에 의해 정확하게 차량의 이동 방향과 수평되는 선을 그리기가 어려워 bird-eye view를 통해 선을 그린 후 해당 선을 다시 원본 영상에 투영하려 했다

 

먼저 bird-eye view를 만들기 위해 opencv에서 제공해주는 getPerspectiveTransform을 이용하였다 

 

실제 적용은 도로에서 실행했지만 보기 좋은 예시 그림을 가지고 포스트를 올린다.

 

1. 원본사진에서 bird-eye view로 볼 부분의 4점을 선택한다. 

source_coord = np.float32( mouse_events.points )
H = max(mouse_events.points[2][0],mouse_events.points[3][0]) - min(mouse_events.points[0][0],mouse_events.points[1][0])
W = max(mouse_events.points[0][1], mouse_events.points[1][1], mouse_events.points[2][1], mouse_events.points[3][1] ) - min(mouse_events.points[0][1], mouse_events.points[1][1], mouse_events.points[2][1], mouse_events.points[3][1] )
destination_coord = np.float32( [ [0,0], [0,H], [W,0], [W,H] ] )
M = cv2.getPerspectiveTransform(source_coord, destination_coord)

2. opencv에서 제공해주는 cv2.getPerspectiveTransform과 cv2.warpPerspective를 이용하면 쉽게 계산결과를 볼 수 있다 

( getPerspectiveTransform에서는 순서대로 선정보를 입력해줘야한다 - 좌상, 좌하, 우상, 우하 )

M = cv2.getPerspectiveTransform(coord1, coord2) 

transformed = cv2.warpPerspective(image, M, (W, H))

transformed = cv2.warpPerspective(frame, M, (W,H))

계산결과가 잘 나왔다 

 

3. 계산 결과에 선을 그린다 

4. 이제 원본 이미지에 해당 선을 다시 그려야하는데 아무것도 모른상태로 함수를 이용하여 변환했기 때문에 역변환을 제공해주는 함수가 있는가 찾아봤지만 찾지 못했다 그래서 공식을 이용해서 역변환 하기로 하였다 

 

시점변환(perspective transform)의 공식은 다음과 같다 

여기서 우리가 알고 있는 것은 x', y', x, y 이다 x'과 y'은 변경시킬 좌표이고 x, y 는 원본에서의 좌표이다 

 

x',y'은 [0,0, 0,H, W,0, W,H] 가 되고 ( W : 넓이, H : 높이) 

x,y 는 마우스를 통해 찍은 점의 위치 대략적으로 [100,30 0,400 300,100 330,450] 이라고 하겠다 

 

getPerspectiveTransform을 이용하여 바꿀 위치들을 지정해주면(x', y') 위의 공식에서 h들에 대한 값이 return 된다  (이후부터 h들을 H라고 하겠다 )

그렇다면 [wx' wy' w] = H@[x y 1] 이 되고 우리가 구한 것은 w와 x' y'이 되었다 

이제 우리는 x' y' 이 있다면 역변환을 구할 수 있을것이다 

여기서 삽질을 정말 오래 했는데 w 를 보지 못하고 계속 [x' y' 1] 만을 이용하여 왜 생각대로 안되지 라고 한 3시간 찾아보았다 

어쨋든.... 

 

우리가 구할것은 이제 x' y' w 를 알고 있을 때 x y 를 구하는 것이다 

우리가 알고 있는 것은 x' y' w H 이니 이리저리 나누고 곱하고 해보자 

 

warpPerspective를 통해 나오는 값은 wx' wy' w 이다. 그러므로 원본 을 구하기 위해서는 w 를 모든 항에 나눠 x' y' 을 구해야 한다 그럼 끝나는 건데 w 를 생략해서 계속 고민하고 있었다니....

 

역행렬을 구할 때는 numpy 에 linalg.pinv 또는 linalg.inv를 이용하면 된다 pinv는 persuade inverse이다

첫번쨰 : 원본사진 두번째 : 변환 후 사진 세번째 : 역변환 사진

line_coord = np.float32(mouse_events2.points)
One = np.squeeze(INV_M @ np.expand_dims(np.append(line_coord[0], 1), axis=1))
Two = np.squeeze(INV_M @ np.expand_dims(np.append(line_coord[1], 1), axis=1))
One = One[:2] / One[2]
Two = Two[:2] / Two[2]

선이 잘 그려졌다! 

 

해당 코드는 깃헙에 업로드 되어있다. 

https://github.com/dldidfh/tistory_code/tree/master/%EC%9B%90%EA%B7%BC%ED%88%AC%EC%98%81

 

GitHub - dldidfh/tistory_code

Contribute to dldidfh/tistory_code development by creating an account on GitHub.

github.com

 

이번 장에서는 openCV에서 특정 색상을 추출할 때 사용되는 inRange를 이용해 차선을 추출하는 예제를 해보겠다 

이전 (9) setMouseCallback을 활용하여 색상의 값을 뽑고 해당 값과 비슷한 색상의 값들을 뽑아 보자

 

1. inRange

  • cv2.inRange(src, lower range, upper range, dst ) 
    - src : 목적이 되는 이미지 
    - lower range : 픽셀의 최솟값 ( 3채널이미지는 (x, x, x) 1 채널은 (x)  ) 
    - upper range : 픽셀의 최댓값
    lower range보다 크고 upper range보다 작은 값에 속하는 픽셀을 뽑아낸다 
import cv2 
import numpy as np 

origin_image = cv2.imread('inrange_image.jpg')

mask = cv2.inRange(origin_image, (0,100,0),(200,255,200))

range_image = cv2.bitwise_and(origin_image, origin_image, mask=mask)

mask = np.stack((mask,)*3, axis=-1)

image = np.concatenate((origin_image, mask, range_image), axis=1)
cv2.imshow('image', image)
cv2.waitKey(0)

 

  • 위의  inRange lower와 upper설정을 보면 값을 사전에 설정하여 입력하였다 
    하지만 위의 사진과 같이 특정 영역을 정확히 뽑아내지 못했다 . 
    inRange의 단점은 이것이다. 특정 객체를 뽑아내려면 해당 객체가 표현하는 색상을 알고 있어야 한다. 
    이럴 때 해당 객체의 색상을 알기 위해 이전 (9) setMouseCallback을 이용하여 색상을 알고 해당 색상에서 일정 범위 안에 해당하는 픽샐들을 뽑으면 객체를 뽑아내기 쉽다 

2. setMouseCallback을 이용한 inRange 

import cv2 
import numpy as np 

def get_RBG_in_image(event, x, y, flags, param):
    if event == cv2.EVENT_LBUTTONUP:
        print("마우스가 눌린 위치의 BGR값은 : {}".format(param['image'][y,x,:]))

    if event == cv2.EVENT_RBUTTONUP:
        print('마우스오른쪽 버튼이 눌린위치와 비슷한 색상을 가진 픽셀만 뽑기')
        threshold = 20
        value = param['image'][y,x,:]
        mask = cv2.inRange(param['image'],value - threshold, value + threshold )
        range_image = cv2.bitwise_and(param['image'], param['image'], mask=mask)
        cv2.imshow("range_image",range_image)
    return 


origin_image = cv2.imread('inrange_image.jpg')

param = {
    'image' : origin_image
}
cv2.imshow('image', origin_image)
cv2.setMouseCallback('image', get_RBG_in_image, param)
cv2.waitKey(0)

하얀색 방향선을 클릭했을 때 이미지
회색 도로를 클릭했을 떄 이미지

  • setMouseCallback을 설정하여 마우스 왼쪽을 클릭하면 해당 위치에 픽셀의 값을 print 한다 
  • 마우스 오른쪽을 클릭하면 해당 위치의 픽셀과 비슷한 (threashold를 설정) 값을 가지고 있는 픽셀만을 imshow 하였다 
  • 위와 같이 inRange를 이용하여 차량이 진행중에 차선을 검출할 수도 있다 - 조금만 응용하여 동영상에 접목하는 것을 각자 해보자!

이번 장에서는 openCV에서 나온 영상 또는 이미지에서 마우스를 이용하여 할 수 있는 작업에 대해서 알아보겠다

마우스를 이용하여 그림을 그리거나 특정 색상의 값을 얻을 수 있다 

 

1. 마우스 동작 

마우스의 동작의 종류는 3가지가 있다.  
마우스 오른쪽 버튼 동작, 왼쪽 버튼 동작, 가운데 버튼 동작 

이 3가지 동작마다 각각 눌렸을 때, 눌리고 올렸을 때, 더블클릭했을 때의 기능에 대해서 정의할 수 있다. 

  • EVENT_MOUSEMOVE : 마우스가 움직였을 때 
  • EVENT_LBUTTONDOWN : 마우스 왼쪽 버튼을 눌렀을 때 
  • EVENT_LBUTTONUP :  마우스 왼쪽 버튼을 올렸을 때 
  • EVENT_RBUTTONDOWN : 마우스 오른쪽 버튼을 눌렀을 때 
  • EVENT_RBUTTONUP : 마우스 오른쪽 버튼을 올렸을 때 
  • EVENT_MBUTTONDOWN : 마우스 가운데 버튼을 눌렀을 때 
  • EVENT_MBUTTONUP : 마우스 가운데 버튼을 올렸을 때 
  • EVENT_LBUTTONDBCLICK : 마우스 왼쪽 버튼을 두번 눌렀을 때
  • EVENT_RBUTTONDBCLICK : 마우스 오른쪽 버튼을 두번 눌렀을 때 
  • EVENT_MBUTTONDBCLICK : 마우스 가운데 버튼을 두분 눌렀을 때 

2. 마우스 콜백 함수의 파라미터 

def onMouse(event, x, y, flags, param):
    event = 위에서 정의한 마우스의 동작에 대한 감지 
    x, y = 해당 동작이 감지된 x, y 좌표값 ( 영상또는 이미지에 대한 )
    flags = 마우스 이벤트가 발생할 때의 특정 조건 (컨트롤, 쉬프트, 알트 등 키 조합 생성) 
    param = 파라미터로 전송될 값 (이미지 또는 특정 변수 전송)

 

import cv2 
import numpy as np 

class MouseGesture():
    def __init__(self) -> None:
        self.is_dragging = False 
        self.x0, self.y0, self.w0, self.h0 = -1,-1,-1,-1

    def on_mouse(self, event, x, y, flags, param):
        if event == cv2.EVENT_LBUTTONDOWN:
            print("왼쪽 버튼 눌림 \t좌표 : x : {} y : {}".format(x,y) )
        elif event == cv2.EVENT_LBUTTONUP:
            print("왼쪽 버튼 올림\t좌표 : x : {} y : {}".format(x,y) )
        elif event == cv2.EVENT_RBUTTONDOWN:
            print("오른쪽 버튼 눌림\t좌표 : x : {} y : {}".format(x,y) )
        elif event == cv2.EVENT_RBUTTONUP:
            print("오른쪽 버튼 올림\t좌표 : x : {} y : {}".format(x,y) )    
        elif event == cv2.EVENT_MBUTTONDOWN:
            print("가운데 버튼 내림\t좌표 : x : {} y : {}".format(x,y) )
        elif event == cv2.EVENT_MBUTTONUP:
            print("가운데 버튼 올림\t좌표 : x : {} y : {}".format(x,y) )
        # elif event == cv2.EVENT_MOUSEMOVE:
        #     # 마우스 움직임은 너무 많이 나와서 생략    
        #     print("마우스 움직임\t좌표 : x : {} y : {}".format(x,y) )
        elif event == cv2.EVENT_MOUSEHWHEEL:
            # 가로휠이 없는 마우스라 .... 
            print("마우스 가로 휠 \t좌표 : x : {} y : {}".format(x,y) )
        elif event == cv2.EVENT_MOUSEWHEEL:
            print("마우스 그냥 휠 \t좌표 : x : {} y : {}".format(x,y) )
            
        return 

image = cv2.imread('test.JPG')
window_name = 'mouse_callback'
mouse_class = MouseGesture()

cv2.imshow(window_name, image)
cv2.setMouseCallback(window_name, mouse_class.on_mouse, param=image)
cv2.waitKey(0)

3. 적용 예시 

  • 마우스를 클릭한 위치의 픽셀의 값 확인 
    import cv2 
    import numpy as np 
    
    class MouseGesture():
        def __init__(self) -> None:
            self.is_dragging = False 
            # 마우스 위치 값 임시 저장을 위한 변수 
            self.x0, self.y0, self.w0, self.h0 = -1,-1,-1,-1
    
        def on_mouse(self, event, x, y, flags, param):
            if event == cv2.EVENT_LBUTTONDOWN:
                value = param[y,x,:]
                print("왼쪽 버튼 눌림 \t x : {} y : {} 좌표의 픽셀값은 : {}".format(x,y, value) )
            return 
    
    image = cv2.imread('test.JPG')
    window_name = 'mouse_callback'
    mouse_class = MouseGesture()
    
    cv2.imshow(window_name, image)
    cv2.setMouseCallback(window_name, mouse_class.on_mouse, param=image)
    cv2.waitKey(0)​
  • 마우스로 그림 그리기 
    import cv2 
    import numpy as np 
    
    class MouseGesture():
        def __init__(self) -> None:
            self.is_dragging = False 
            # 마우스 위치 값 임시 저장을 위한 변수 
            self.x0, self.y0, self.w0, self.h0 = -1,-1,-1,-1
    
        def on_mouse(self, event, x, y, flags, param):
            if event == cv2.EVENT_LBUTTONDOWN:
                self.x0 = x
                self.y0 = y
                self.is_dragging = True
                print("사각형의 시작 좌표는 x : {} y : {}".format(x,y) )
            elif event == cv2.EVENT_LBUTTONUP:
                self.is_dragging = False
                cv2.rectangle(param['image'], (self.x0, self.y0), (x,y),(0,0,255),2)            
                cv2.imshow(param['window_name'], param['image'])
                print("사각형의 좌표는 ({}, {}), ({}, {})".format(self.x0,self.y0,x,y) )
            elif event == cv2.EVENT_MOUSEMOVE:
                if self.is_dragging:
                    temp_img = param['image'].copy()
                    cv2.rectangle(temp_img, (self.x0, self.y0), (x,y),(0,0,255),2)            
                    cv2.imshow(param['window_name'], temp_img)
            return 
    
    
    image = cv2.imread('test.JPG')
    window_name = 'mouse_callback'
    mouse_class = MouseGesture()
    param = {
        "image" : image,
        "window_name" : window_name
    }
    cv2.imshow(window_name, image)
    cv2.setMouseCallback(window_name, mouse_class.on_mouse, param=param)
    cv2.waitKey(0)​
  •  

이번 장에서는 차영상기법이 사용되는 유용한 기법인 배경 추출에 대해서 알아보겠다 

간단한 차영상부터 시작해서 OpenCV 함수 이용까지 다루어 보겠다 

 

 

1. 차영상 기법 

  • 차영상이란 어느 한 이미지와 다른 이미지와의 차이를 나타낸다 
  • Image subtraction과 pixel subtraction이 대표적이다 
  • 하나의 영상에서 첫 번째 프레임과 다음 프레임의 pixel 기준 차이를 생성해보겠다 

import cv2
import time
import numpy as np
from numpy.lib.function_base import diff

video_path = './cctv.mp4'
output_path = './background_extraction_output.mp4'
video = cv2.VideoCapture(video_path)

width = int(video.get(cv2.CAP_PROP_FRAME_WIDTH))
height = int(video.get(cv2.CAP_PROP_FRAME_HEIGHT))

# 프레임 정보를 저장해놓을 변수를 선언한다
first_frame = np.zeros((400,400,3))
second_frame = np.zeros((400,400,3))

# 영상에서 첫 프레임과 두번째 프레임을 저장한다 
for i in range(2):
    return_value, frame = video.read()
    frame = cv2.resize(frame,(400,400))
    if i == 0 : first_frame = frame
    if i == 1 : second_frame = frame

diff_image = second_frame - first_frame
concat_image = np.concatenate((first_frame,second_frame,diff_image),axis=1)

cv2.imshow('images',concat_image)
cv2.waitKey(0)
  • 위의 사진과 같이 하나의 고정된 영상장치에서 프레임간의 차이는 움직이는 객체를 표현하기 좋다 
  • 배경은 움직이지 않고 가만히 있다 보니 배경이 없어지고 움직이는 자동차에 대해서 픽셀의 값이 높게 나오는 것을 볼 수 있다 
  • 위의 예제는 3채널(BGR)의 차이를 보이다 보니 먼가 고장 난 TV처럼 보인다. 
  • GRAY scale로 변환한 뒤 다시 확인해보자
  • BGR보다는 더 보기 편해 보인다 하지만 눈으로 보기에 무엇인가 깔끔하지 않다.
  • 깔끔하지 않기에 깔끔하게 만들기 위해서는 전에 (7)장에서 학습한 Opening과 closing을 이용할 수 있지만 opencv에서는 여러 가지 함수를 이용해 더욱 깨끗한 차영상을 만들 수 있다 

 

2. opencv- createBackgroundSubtraction - 배경분리
  

import cv2
import numpy as np
import time

video_path = './cctv.mp4'
output_path = './background_subtraction_output.mp4'

video = cv2.VideoCapture(video_path)
width = int(video.get(cv2.CAP_PROP_FRAME_WIDTH))
height = int(video.get(cv2.CAP_PROP_FRAME_HEIGHT))
fps = int(video.get(cv2.CAP_PROP_FPS))
codec = cv2.VideoWriter_fourcc(*'mp4v')
out = cv2.VideoWriter(output_path, codec, fps, (width, height)) 

fgbg = cv2.createBackgroundSubtractorKNN(detectShadows=False)
# fgbg = cv2.bgsegm.createBackgroundSubtractorMOG(history=200,nmixtures=3,backgroundRatio=0.7, noiseSigma=0)
# fgbg = cv2.createBackgroundSubtractorMOG2(history=200,varThreshold=32,detectShadows=False)
# fgbg = cv2.bgsegm.createBackgroundSubtractorGMG(initializationFrames=20, decisionThreshold=0.5)

kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE,(7,7))
# kernel = cv2.getStructuringElement(cv2.MORPH_RECT,(7,7))
while(1):
    start = time.time()
    
    return_value, frame = video.read()
    # 비디오 프레임 정보가 있으면 계속 진행 
    if return_value:
        pass
    else : 
        print('비디오가 끝났거나 오류가 있습니다')
        break

    background_extraction_mask = fgbg.apply(frame)
    # background_extraction_mask = cv2.morphologyEx(background_extraction_mask,
    #                                  cv2.MORPH_CLOSE, kernel)
    background_extraction_mask = cv2.dilate(background_extraction_mask,kernel,iterations=1)

    background_extraction_mask = np.stack((background_extraction_mask,)*3, axis=-1)

    concat_image = np.concatenate((frame,background_extraction_mask), axis=1)

    cv2.imshow('background extraction video', concat_image)
    cv2.waitKey(0)
video.release()
cv2.destroyAllWindows()
  • opencv python 기본 패키지에는 MOG와 GMG가 없다 pip install opencv-contrib-python 가 필요하다
  • 우선 opencv에서 비디오를 읽어와 변환한 영상을 다시 저장할 때는 위의 예처럼 읽어온 video에서 영상의 해상도(height, width)를 불러오고 영상의 FPS, codec정보를 불러와 저장할 영상의 정보를 설정해 줘야 한다 ( 기존 영상의 FPS가 30이고 새로운 영상의 FPS를 10으로 설정하면 3배 빠르게 움직이고 영상의 길이가 길어진 것을 볼 수 있을 것이다) 
  • cv2.createBackgroundSubtractor  XXX  
    - 배경추출을 진행할 함수를 선언해준다 종류로는 MOG, MOG2, KNN, GMG 가있다.
    - 각각의 알고리즘에 대한 세부적인 내용은 구글링을 통해 학습하는 것이 좋을 것이다. 
    - MOG - Mixture of Gaussian - Gaussian Mixture Model - K개의 Gaussian model의 결합 
    - MOG2 - Adaptive Mixture of Gaussian - 픽셀에 대해 적절한 수의 Gaussian distribution을 선택 
    - KNN - K-Nearest Neighbor 최근접이웃 알고리즘 
    - GMG - Bayesian과 Kalmanfilter의 조합 
    - MOG와 GMG의 경우 현재 버전의 opencv 기본 패키지에서는 존재하지 않는다. 대부분 MOG2를 사용하는데 성능이 MOG와 GMG에 비해 좋아서 삭제한 거 같다 
    - KNN과 MOG중 자신이 하려는 부분에 맞는 알고리즘을 사용하는 것이 좋다. 이번 글에서는 KNN을 이용하였다 
    - KNN의 결과물에 dilate를 하여 움직이는 객체의 안쪽 부분의 noise를 줄였다 ( 객체 주변의 배경에 대한 노이즈가 늘어난다 일반적으로 dilate보단 opening과 closing을 추천한다 )

3. 원본 영상에 mask 적용 cv2.bitwise_and(src1, src2)

  • 위의 사진에서 KNN의 결괏값으로 나온 흑백으로는 실제 해당 위치에 어떤 객체가 있는지 판단하기 어렵다(classification) 그렇기에 실제 영상에 마스크를 적용하여 classification에 적용하기 좋은 영상으로 만들어보자

import cv2
import numpy as np
import os
import time
video_path = './cctv.mp4'
output_path = './mask_background_subtraction_output.mp4'
video = cv2.VideoCapture(video_path)

width = int(video.get(cv2.CAP_PROP_FRAME_WIDTH))
height = int(video.get(cv2.CAP_PROP_FRAME_HEIGHT))
fps = int(video.get(cv2.CAP_PROP_FPS))
codec = cv2.VideoWriter_fourcc(*'mp4v')

# codec = int(video.get(cv2.CAP_PROP_FOURCC))
print(codec, type(codec))

# # # codec = chr(codec&0xff) + chr((codec>>8)&0xff) + chr((codec>>16)&0xff) + chr((codec>>24)&0xff) 
# # # codec = cv2.VideoWriter_fourcc(*codec)
# # print(codec)
out = cv2.VideoWriter(output_path, codec, fps, (width, height)) 

fgbg = cv2.createBackgroundSubtractorKNN(detectShadows=False)
# fgbg = cv2.bgsegm.createBackgroundSubtractorMOG(history=200,nmixtures=3,backgroundRatio=0.7, noiseSigma=0)
# fgbg = cv2.createBackgroundSubtractorMOG2(history=200,varThreshold=32,detectShadows=False)
# fgbg = cv2.bgsegm.createBackgroundSubtractorGMG(initializationFrames=20, decisionThreshold=0.5)


# kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE,(7,7))
kernel = cv2.getStructuringElement(cv2.MORPH_RECT,(7,7))
time_cost = 0
fps_cost = 0
frame_num = 0
while(1):
    start = time.time()
    frame_num +=1 
    
    return_value, frame = video.read()
    # 비디오 프레임 정보가 있으면 계속 진행 
    if return_value:
        pass
    else : 
        print('비디오가 끝났거나 오류가 있습니다')
        break

    background_extraction_mask = fgbg.apply(frame)
    # background_extraction_mask = cv2.morphologyEx(background_extraction_mask,
    #                                  cv2.MORPH_CLOSE, kernel)
    background_extraction_mask = cv2.dilate(background_extraction_mask,kernel,iterations=1)

    background_extraction_mask = np.stack((background_extraction_mask,)*3, axis=-1)
    bitwise_image = cv2.bitwise_and(frame, background_extraction_mask)

    concat_image = np.concatenate((frame,bitwise_image), axis=1)

    time_temp = 1000*(time.time()-start)  
    if time.time()-start != 0.0:
        fps_temp = 1/(time.time()-start)  
    else:
        fps_temp=0.01

    time_cost = time_cost + time_temp
    fps_cost = fps_cost + fps_temp
    print('소요 시간 : {:.2f} ms \t 평균FPS : {:.2f}'.format(time_temp,fps_temp))

    cv2.imshow('background extraction video', concat_image)
    cv2.waitKey(0)
print('소요시간 평균 : {:.2f} ms\t 평균FPS : {:.2f}'.format(time_cost / frame_num, fps_cost/ frame_num))
video.release()
cv2.destroyAllWindows()
  • 2. 의 코드에서 bitwise_and를 하는 항목과 소요된 시간을 체크하는 코드가 추가되었다 
  • bitwise_and는 두 데이터 간의 AND작업을 진행한다 (0 and 0 = 0) (1 and 0 = 0) (1 and 1 = 1)
  • 기존의 영상은 3 채널 BGR이미지이기에 backgroundSubtraction의 결괏값에 np.stack을 이용하여 grayscale 2 채널 이미지를 3 채널로 만들어줬고 원본 영상과 3 채널이 된 배경추출 결괏값을 서로 and 해주었다
  • 배경추출 결괏값에서 배경은 0으로 표현이 되고 움직이는 객체는 255로 표현이 되기에 서로를 and 하면 원본 영상에서 움직이는 객체만 표현할 수 있다 
  • 그런데 예를 들어 255와 120을 and 하면 0이 나와야 하는 거 아니냐?라는 생각이 들 수 있다. 여기서 0이 아닌 120이 나오는 이유는 bitwise에서 답을 찾을 수 있다
  • bitwise는 bit의 기준에서 and를 한다 opencv는 기본적으로 uint8의 data type을 사용한다
  • bit기준으로 255 = 11111111     120 = 01111000으로 표현된다 그러므로 서로 bitwise and를 하면 01111000이 되기 때문에 255 and 120의 bitwise and는 120이 된다 

 

각 background subtraction을 자신이 하려는 작업에 맞게 사용하자 저자는 이미지에서 움직이는 객체를 표시하고 객체의 외곽선을 정확히 나눈다 보다는 객체의 내부에 noise를 지우기 위해 사용하였고 그렇기 때문에 배경에 대해서 noise가 늘어나는 것을 감수했다 또한 배경 추출의 처리속도를 고려하여 비교적 빠르고 내부 noise를 줄일 수 있는 KNN을 사용하였다. 

외곽선에 대해서 더 정확하게 나누고 싶다면 GMG의 parameter에서 initializationFrames를 높여가며 테스트를 해보는 것도 좋을 것이다 

 

https://github.com/dldidfh/tistory_code

 

GitHub - dldidfh/tistory_code

Contribute to dldidfh/tistory_code development by creating an account on GitHub.

github.com

 

+ Recent posts