Tag Archives: OpenCV

OpenCV 4로 배우는 컴퓨터 비전과 머신 러닝/ OpenCV 주요 기능

  • (각 함수의 상세한 파라미터에 대해서는 이전의 <OpenCV로 배우는 영상처리 및 응용>에서 정리하였으므로 생략)

카메라와 동영상 파일 다루기

VideoCapture 클래스

  • 동영상이란 일련의 정지 영상을 압축하여 파일로 저장한 형태
    • 이때 동영상에 저장되어 있는 정지 영상을 프레임(frame)이라고 함.
    • 동영상을 처리하는 작업은 동영상에서 프레임을 추출한 후, 각각의 프레임에 영상 처리 기법을 적용하는 형태로 이루어짐.
  • OpenCV에서는 VideoCapture라는 클래스를 이용하여 카메라 또는 동영상 파일로부터 정지 영상 프레임을 받아올 수 있음.
  • VideoCapture 클래스에서 동영상 파일을 불러오려면 처음 VideoCapture 객체를 생성할 때 동영상 파일 이름을 지정하거나 기본 생성자로 VideoCpature 객체를 생성한 후 VideoCapture::open() 멤버 함수를 호출해야 함.
  • 하나의 동영상 파일 대신 일련의 숫자로 구분되는 이름의 정지 영상 파일을 가지고 있고, 이 파일을 불러오고 싶을 때에도 VideoCapture 클래스를 이용할 수 있음.
    • 예컨대 img0001.jpg, img0002.jpg, img0003.jpg 라는 파일이 있을 때 filename 인자에 ‘img%04d.jpg”라고 입력하면 일련의 영상 파일을 차례로 불러올 수 있음.
    • 또한 filename 인자에 ‘protocol://host:port/script_name?script_params|auth’ 형태의 비디오 스트림 URL을 지정하여 인터넷 동영상을 사용할 수도 있음.
  • apiPreference 인자에는 아래 표와 같은 VideoCaptureAPIs 열거형 상수 중 하나를 사용하여 동영상 파일을 불러오는 방법을 지정할 수 있음.
    • 대부분의 경우 apiPreference 인자를 생략하거나 기본값인 CAP_ANY를 지정하는데, 이 경우 시스템이 알아서 적절한 방법을 선택하여 사용 함.
열거형 상수 설명
CAP_ANY 자동선택
CAP_V4L, CAP_V4L2 V4L/V4L2(리눅스)
CAP_FIREWIRE, CAP_IEEE1394 IEEE 1394 드라이버
CAP_DSHOW 다이렉트쇼(DirectShow)
CAP_PVAPI PvAPI, Prosilica GigE SDK
CAP_OPENNI OpenNI
CAP_MSMF 마이크로소프트 미디어 파운데이션
CAP_GSTREAMER GStreamer
CAP_FFMPEG FFMPEG 라이브러리
CAP_IMAGES OpenCV에서 지원하는 일련의 영상 파일
CAP_OPENCV_MJPEG OpenCV에 내장된 MotionJPEG 코덱

 

  • 컴퓨터에 연결된 카메라 장치를 열 때에도 VideoCapture 생성자 혹은 VideoCapture::open() 함수를 이용하는데, 이때 함수의 인자에 문자열이 아닌 정수 값을 전달 함.
    • 카메라 장치를 사용하려 할 때 VideoCapture 클래스의 생성자 혹은 VideoCapture::open() 함수에 전달하는 정수 값 index는 다음과 같은 형태로 구성됨
index = camera_id + domain_offset_id
  • 만일 컴퓨터에 한 대의 카메라만 연결되어 있다면 이 카메라의 camera_id는 0이 된다.
    • 만일 두 대 이상의 카메라가 연결되어 있다면 각각의 카메라는 0보다 같거나 큰 정수를 ID로 갖는다.
  • domain_offset_id는 카메라 장치를 사용하는 방식을 표현하는 정수 값이며 VideoCaptureAPIs 열거형 상수 중 하나를 지정한다.
    • 대부분의 경우 domain_offset_id는 자동 선택을 의미하는 0(CAP_ANY)을 사용하기 때문에 index 값은 결국 camera_id와 같은 값으로 설정한다.
  • 카메라 또는 동영상 파일 열기를 수행한 후에는 VideoCapture::isOpened() 함수를 이용하여 열기 작업이 성공적으로 수행되었는지를 확인하는 것이 좋다.
  • 카메라 장치 또는 동영상 파일의 사용이 끝나면 VideoCapture::release()를 호출하여 사용하던 자원을 해제해야 한다.
    • 참고로 VideoCapture 클래스의 소멸자에도 VideoCapture::release() 함수와 마찬가지로 사용하고 있던 자원을 모두 해제하는 코드가 들어가 있어서 VideoCapture 객체가 소멸할 때 자동으로 열려 있던 카메라 장치 또는 동영상 파일이 닫히게 된다.
  • VideoCapture 클래스를 이용하여 카메라 또는 동영상 파일을 정상적으로 열었다면 그 후에 공통의 멤버 함수를 사용하여 프레임을 받아올 수 있다.
    • VideoCapture 클래스에서 한 프레임을 받아 오기 위해서는 VideoCapture::operator >>() 연산자 재정의 함수 또는 VideoCapture::read() 함수를 사용한다.
  • >> 연산자 재정의와 VideoCapture::read() 함수는 모두 카메라 또는 동영상 파일로부터 다음 프레임을 받아 와서 Mat 클래스 형식의 변수 image에 저장한다.
    • 사실 >> 연산자 재정의는 함수 내부에 명시적으로 VideoCapture::read() 함수를 호출하는 형태로 구성되어 있기 때문에 그 둘은 완전히 같다.
VideoCapture cap(0);

Mat frame1, frame2;
cap >> frame1; // 1st frame
cap.read(frame2); // 2nd frame
  • 현재 열려 있는 카메라 장치 또는 동영상 파일로부터 여러 정보를 받아 오기 위해서는 VideoCapture::get() 함수를 사용한다.
    • VideoCapture::get() 함수는 인자로 지정한 속성 ID(propID)에 해당하는 속성 값을 반환한다.
    • VideoCapture::get() 함수의 인자로 지정할 수 있는 속성 ID는 VideoCaptureProperties 열거형 상수 중 하나를 지정할 수 있으며, 자주 사용되는 상수를 아래 표에 정리.
    • VideoCapture::get() 함수는 속성을 double 타입으로 반환한다.
VideoCaptureProperties 열거형 상수 설명
CAP_PROP_POS_MSEC 비디오 파일에서 현재 위치(밀리초 단위)
CAP_PROP_POS_FRAMES 현재 프레임 위치 (0-기반)
CAP_PROP_POS_AVI_RATIO [0, 1] 구간으로 표현한 동영상 프레임의 상대적 위치(0: 시작, 1: 끝)
CAP_PROP_FRAME_WIDTH 비디오 프레임의 가로 크기
CAP_PROP_FRAME_HEIGHT 비디오 프레임의 세로 크기
CAP_PROP_FPS 초당 프레임 수
CAP_PROP_FOURCC fourcc 코드(코덱을 표현하는 정수 값)
CAP_PROP_FRAME_COUNT 비디오 파일의 전체 프레임 수
CAP_PROP_BRIGHTNESS (카메라에서 지원하는 경우) 밝기 조절
CAP_PROP_CONTRAST (카메라에서 지원하는 경우)  명암비 조절
CAP_PROP_SATURATION (카메라에서 지원하는 경우) 채도 조절
CAP_PROP_HUE (카메라에서 지원하는 경우) 색상 조절
CAP_PROP_GAIN (카메라에서 지원하는 경우) 감도 조절
CAP_PROP_EXPOSURE (카메라에서 지원하는 경우) 노출 조절
CAP_PROP_ZOOM (카메라에서 지원하는 경우) 줌 조절
CAP_PROP_FOCUS (카메라에서 지원하는 경우) 초점 조절
VideoCapture cap(0);

int w = cvRound(cap.get(CAP_PROP_FRAME_WIDTH));
int h = cvRound(cap.get(CAP_PROP_FRAME_HEIGHT));
  • VideoCapture::get() 함수와 반대로 현재 열려 있는 카메라 또는 비디오 파일 재생과 관련된 속성 값을 설정할 때는 VideoCapture::set() 함수를 사용한다.
    • VideoCapture::set() 함수의 속성 ID도 위의 표에 정리한 VideoCaptureProperties 열거형 상수를 지정한다.
    • 만일 video.mp4 파일을 열어서 100번째 프레임으로 이동하려면 다음과 같은 코드를 작성하면 된다.
VideoCapture cap("video.mp4");
cap.set(CAP_PROP_POS_FRAMES, 100);

카메라 입력 처리하기

  • (앞서 나온 내용을 이용한 예제 코드와 설명이라 생략)

동영상 파일 처리하기

  • (앞서 나온 내용을 이용한 예제 코드와 설명이라 생략)

동영상 파일 저장하기

  • (앞서 나온 내용을 이용한 예제 코드와 설명이라 생략)

다양한 그리기 함수

직선 그리기

  • line()은 영상 위에 직선을 그리는 함수
    • 영상 위에 pt1 좌표부터 pt2 좌표까지 직선을 그린다.
    • 이때 선의 색상, 밝기는 color로 지정할 수 있고, thickness를 이용하여 두께를 지정할 수 있다.
    • lineType인자는 그리는 방식을 지정할 수 있는데, LineTypes 열거형 상수 중 하나를 지정할 수 있다.
LineTypes 설명
FILLED -1 내부를 채움(직선 그리기 함수에는 사용 불가)
LINE_4 4 4방향 연결
LINE_8 8 8방향 연결
LINE_AA 18 안티에일리어싱

 

  • 화살표 형태의 직선을 그려야 하는 경우에는 arrowedLine()을 이용하면 된다.
    • arrowedLine() 함수는 영상 위에 pt1 좌표부터 pt2 좌표까지 직선을 그리고 끝점인 pt2에 화살표 모양의 직선 두 개를 추가로 그린다.
    • 이때 화살표 모양의 직선 길이는 arrowedLine() 함수의 마지막 인자인 tipLength를 이용하여 조절할 수 있다.
  • drawMarker()는 직선 그리기 함수를 이용하여 다양한 모양의 마커를 그린다.
    • drawMarker() 함수는 img 영상의 position 좌표에 color 색상을 이용하여 마커를 그리는데, 마커의 종류는 markerType 인자로 지정할 수 있다. 기본값으로는 십자가 모양의 MARKER_CROSS 가 지정되어 있다.
MarkerTypes 설명
MARKER_CROSS 십자가 모양 (+)
MARKER_TILTED_CROSS 45도 회전된 십자가 모양 (x)
MARKER_STAR 별 모양 (*)
MARKER_DIAMOND 마름모 모양
MARKER_SQUARE 정사각형 모양
MARKER_TRIANGLE_UP 위로 뾰족한 삼각형
MARKER_TRIANGLE_DOWN 아래로 뾰족한 삼각형

도형 그리기

  • rectangle()은 사각형을 그리는 함수
    • rectangle(0 함수 인자 중 thickness는 도형 외곽선의 두께를 지정하는데, 만일 thickness에 -1 을 지정하거나 FILLED 열거형 상수를 지정하면 내부를 채운 사각형을 그린다.
  • circle()은 원을 그리는 함수
    • 원을 그리기 위해서는 원의 중심점 좌표와 반지름을 지정해야 한다.
  • ellipse()는 타원을 그리는 함수. 타원을 그리는 방식은 원을 그리는 방식보다 복잡하다.
    • ellipse() 함수는 다양한 형태의 타원 또는 타원의 일부인 호를 그릴 수 있다.
    • 타원의 크기는 axes 인자를 통해 지정하는데, axes 인자는 size 자료형을 사용하며, x축 방향 타원과 반지름과 y축 방향 반지름을 지정한다.
    • angle에 0이 아닌 값을 전달하면 회전된 타원을 그릴 수 있다.
    • startAngle과 endAngle 인자를 적절하게 이용하면 호를 그리는 용도로도 사용할 수 있다.
    • 예컨대 startAngle에 0을 지정하고 endAngle에 360을 지정하면 완전한 타원을 그리지만 startAngle에 0을 지정하고 endAngle에 180을 지정하면 타원에 반에 해당하는 호를 그린다.
    • thickness는 타원 외곽선 두께를 나타내는데, -1 또는 FILLED를 지정하면 내부를 채운 타원이나 호를 그린다.
  • polylines() 함수는 임의의 다각형을 그리는 함수
    • polylines()에는 다각형의 꼭지점 좌표를 전달해야 하며, 꼭지점 좌표는 vector<Point> 자료형에 저장하여 전달한다.

문자열 출력하기

  • 영상 위에 정해진 폰트로 문자열을 출력하려면 putText() 함수를 이용하면 된다.
    • putText() 함수는 img 영상의 org 위치에 text로 지정된 문자열을 출력한다.
    • 이때 사용할 폰트는 fontFace 인자로 지정할 수 있고, faceScale 인자를 이용하여 폰트 크기를 조절할 수 있다.
    • fontFace 인자에는 HersheyFonts 열거형 상수 값을 지정할 수 있다. HersheyFonts 열거형 중 FONT_ITALIC 상수는 논리합 연산자(|)를 이용하여 다른 상수와 함께 사용한다.
HersheyFonts 설명
FONT_HERSHEY_SIMPLEX 일반 크기의 산세리프 폰트
FONT_HERSHEY_PLAIN 작은 크기의 산세리프 폰트
FONT_HERSHEY_DUPLEX 일반 크기의 산세리프 폰트 (FONT_HERSHEY_SIMPLEX 보다 복잡)
FONT_HERSHEY_COMPLEX 일반 크기의 세리프 폰트
FONT_HERSHEY_TRIPLEX 일반 크기의 세리프 폰트 (FONT_HERSHEY_COMPLEX보다 복잡)
FONT_HERSHEY_COMPLEX_SMALL FONT_HERSHEY_COMPLEX 보다 작은 폰트
FONT_HERSHEY_SCRIPT_SIMPLEX 필기체 스타일의 폰트
FONT_HERSHEY_SCRIPT_COMPLEX 필기체 스타일의 폰트(FONT_HERSHEY_SCRIPT_SIMPLEX 보다 복잡한 형태)
FONT_ITALIC 이탤릭체를 위한 플래그

 

  • OpenCV는 문자열 출력을 위해 필요한 사각형 영역 크기를 가늠할 수 있는 getTextSize() 함수를 제공하는데, 이 함수를 잘 이용하면 문자열이 한쪼긍로 치우치지 않고 적당한 위치에 출력되도록 설정할 수 있다.
    • putText(0 함수를 이용하여 특정 위치 좌표에 문자열을 출력하면, 보통 문자열 길이와 크기에 따라 문자열이 차지하는 영역 크기가 달라지기 때문에 문자열이 한쪽에 치우쳐서 나타날 수 있다. 그러나 getTextSize() 함수가 반환하는 문자열 영역 크기 정보를 이용하면 문자열 출력 위치를 적절하게 조절할 수 있다.
Mat img(200, 640, CV_8UC3, Scalar(255, 255, 255));

const String text = "Hello, OpenCV";
int fontFace = FONT_HERSHEY_TRIPLEX;
double fontScale = 2.0;
int thickness = 1;

Size sizeText = getTextSize(text, fontFace, fontScale, thickness, 0);
Size sizeImg = img.size();

// 텍스트의 크기를 이용해서 적절한 위치를 계산
Point org((sizeImg.width - sizeText.width) / 2, (sizeImg.Height + sizeText.height) / 2);

putText(img, text, org, fontFace, fontScale, Scalar(255, 0, 0), thickness);

이벤트 처리

키보드 이벤트 처리

  • waitKey() 함수는 키보드 입력을 처리하는 OpenCV의 기본 함수
    • waitKey() 함수는 delay에 해당하는 밀리초 시간 동안 키 입력을 기다리다가 키 입력이 있으면 해당 키의 아스키 코드(ASCII code) 값을 반환한다. 만일 지정 시간 동안 키 입력이 없었으면 -1을 반환한다.
  • Window 운영체제에서 waitKey()는 일반적인 키보드 입력은 처리할 수 있지만, 함수키(F1, F2 등) 또는 화살표키 등 특수키 입력은 처리하지 못한다. 만일 키보드의 특수 키에 대한 처리를 하고 싶다면 waitKey() 대신 waitKeyEx()를 사용하면 된다.

마우스 이벤트 처리

  • OpenCV 에서 마우스 이벤트를 처리하려면 먼저 마우스 콜백 함수를 등록하고, 이후 마우스 콜백 함수에 마우스 이벤트를 처리하는 코드를 추가해야 한다.
    • OpenCv에서 특정 창에 마우스 콜백 함수를 등록할 때는 setMouseCallback() 함수를 사용한다.
    • setMouseCallback() 함수는 winname 창에서 마우스 이벤트가 발생하면 onMouse로 등록된 콜백 함수가 자동으로 호출되도록 설정한다.
    • userdata 인자에는 사용자가 마우스 콜백 함수에 전달하고 싶은 데이터를 void* 형식으로 전달할 수 있다.
    • 마우스 콜백 함수는 다음과 같이 정의되어 있다.
typedef void (*MouseCallback)(int event, int x, int y, int flags, void* userdata);
  • 마우스 콜백함수의 event 인자에는 MouseEventTypes로 정의된 열거형 상수가 전달된다.
MouseEventTypes 설명
EVENT_MOUSEMOVE 0 마우스가 창 위에서 움직임
EVENT_LBUTTONDOWN 1 마우스 왼쪽 버튼 누름
EVENT_RBUTTONDOWN 2 마우스 오른쪽 버튼 누름
EVENT_MBUTTONDOWN 3 마우스 가운데 버튼 누름
EVENT_LBUTTONUP 4 마우스 왼쪽 버튼 뗌
EVENT_RBUTTONUP 5 마우스 오른쪽 버튼 뗌 
EVENT_MBUTTONUP 6 마우스 가운데 버튼 뗌
EVENT_LBUTTONDCLICK 7 마우스 왼쪽 버튼 더블 클릭
EVENT_RBUTTONDCLICK 8 마우스 오른쪽 버튼 더블 클릭 
EVENT_MBUTTONDCLICK 9 마우스 가운데 버튼 더블 클릭
EVENT_MOUSEWHEEL 10 마우스 휠을 앞으로 돌림
EVENT_MOUSEHWHEEL 11 마우스 휠을 좌우로 돌림

 

  • 마우스 콜백함수의 flags 인자에는 MouseEventFlags 열거형 상수의 논리합이 전달된다.
MouseEventFlags 설명
EVENT_FLAG_LBUTTON 1 마우스 왼쪽 버튼이 눌려 있음
EVENT_FLAG_RBUTTON 2 마우스 오른쪽 버튼이 눌려 있음
EVENT_FLAG_MBUTTON 4 마우스 가운데 버튼이 눌려 있음
EVENT_FLAG_CTRLKEY 8 ctrl 키가 눌려있음
EVENT_FLAG_SHIFTKEY 16 shift 키가 눌려있음
EVENT_FLAG_ALTKEY 32 alt 키가 눌려있음

트랙바 사용하기

  • OpenCV에는 Window, Linux, Mac OS에서 공통으로 사용할 수 있는 트랙바(trackbar)라는 인터페이스를 제공함.
    • 트랙바는 슬라이더 컨트롤(slider control)이라고도 부르며, 영상 출력 창에 부착되어 프로그램 동작 중에 사용자가 지정된 범위 안의 값을 선택할 수 있음.
    • 트랙바는 사용자가 지정한 영상 출력 창의 상단에 부착되며, 필요한 경우 창 하나에 여러 개의 트랙바를 생성할 수 있음. 각각의 트랙바에는 고유한 이름을 지정해야 하며, 이 이름은 트랙바 왼쪽에 나타남.
    • 트랙바 위치는 사용자가 마우스를 이용하여 이동시킬 수 있고, 트랙바의 현재 위치는 트랙바 이름 옆에 함께 표시 됨.
  • OpenCV에서 트랙바를 생성하려면 createTrackbar() 함수를 이용하면 된다.
    • createTrackbar(0 함수는 winname 이름의 창에 trackbarname 이름의 트랙바를 부착하고, 트랙바가 움직일 때마다 onChange에 해당하는 트랙바 콜백 함수가 호출되도록 설정함.
    • 사용자가 트랙바 콜백 함수에 전다랗고 싶은 데이터가 있다면 userdata 인자를 통해 void* 형식으로 전달할 수 있음.
    • onChange에 지정하는 트랙바 콜백 함수는 트랙바 위치가 변경될 때 자동으로 호출되는 함수로 다음과 같이 정의되어 있음.
typedef void (*TrackbarCallback)(int pos, void* userdata);
  • 트랙바 콜백 함수의 첫 번째 인자에는 현재 트랙바의 위치 정보가 전달되고, 두 번째 인자에는 createTrackbar() 함수에서 지정한 사용자 데이터 포인터 값이 전달 됨.
  • 트랙바를 생성한 후 트랙바의 현재 위치를 알고 싶다면 getTrackbarPos() 함수를 사용하면 된다.
  • 프로그램 동작 중에 트랙바 위치를 강제로 옮기고 싶다면 setTrackbarPos()를 사용하면 된다.

OpenCV 데이터 파일 입출력

  • OpenCV에서 제공하는 FileStorage 클래스는 Mat 클래스 객체 뿐만 아니라 일반적인 C/C++ 자료형 데이터를 XML, YAML, JSON 등 파일 형식으로 저장하는 기능을 제공한다.

FileStorage 클래스

  • OpenCV에서 데이터 파일 입출력은 FileStorage가 담당하는데, FileStorage 클래스는 데이터의 파일 입출력 기능을 캡슐화하여 지원하는 클래스이다.
  • FileStorage 클래스를 이용하여 OpenCV 데이터를 저장하거나 읽어오려면 먼저 FileStorage 클래스를 생성한 후 FileStorage::open() 함수를 이용하여 실제 사용할 파일을 열어야 한다.
    • FileStorage::open() 함수의 첫 번째 인자 filename에는 데이터 파일 이름을 지정하는데, FileStorage 클래스는 XML, YAML, JSON 형식의 파일 입출력을 지원하며, 사용할 파일 형식은 filename의 확장자에 의해 자동으로 결정된다.
    • 만약 파일 이름 뒤에 .gz를 추가하면 데이터 파일을 압축하여 저장한다. 예컨대 filename을 “mydata.xml.gz”로 설정하면 XML 파일 형식으로 데이터를 저장한 후 gzip 형식으로 압축한다.
    • 두 번째 인자 flags는 파일 열기 모드를 결정하는데, FileStorage::mode 열거형 상수를 지정할 수 있다.
FileStorage::mode 열거형 상수 설명
FileStorage::READ 읽기 모드
FileStorage::WRITE 쓰기 모드 (새로 생성)
FileStorage::APPEND 추가로 쓰기 모드
FileStorage::MEMORY 논리합 연산자(|)를 이용하여 FileStorage::READ 또는 FileStorage::WRITE 상수와 함께 사용될 경우, 실제 파일 입출력 대신 메모리 버퍼를 이용한 입출력을 수행함

 

  • FileStorage::open() 이후 파일이 정상적으로 열렸는지 확인하는 함수는 FileStorage::isOpened()이다.
  • 일반적으로 FileStorage 클래스를 이용하여 파일에 데이터를 저장할 때는 << 연산자 재정의 함수를 사용하고, 파일로부터 데이터를 읽어올 때는 >> 연산자 재정의 함수를 사용한다.
  • FileStorage 객체를 이용하여 파일 입출력 작업이 완료되면 FileStorage::release() 함수를 호출해서 객체를 해제해야 한다.

데이터 파일 저장하기

  • 예제 코드
String name = "Jane";
int age = 10;
Point pt1(100, 200);
vector<int> scores = { 80, 90, 50 };
Mat mat1 = (Mat_<float>(2, 2) << 1.0f, 1.5f, 2.0f, 3.2f);

FileStorage fs("mydata.json", FileStorage::WRITE);

if (!fs.isOpened())
{
cerr << "File open failed!" << endl;
return;
}

fs << "name" << name;
fs << "age" << age;
fs << "point" << pt1;
fs << "scores" << scores;
fs << "data" << mat1;

fs.release();

데이터 파일 불러오기

  • 예제 코드
String name;
int age;
Point pt1;
vector<int> scores;
Mat mat1;

FileStorage fs("mydata.json", FileStorage::READ);

if (!fs.isOpened())
{
cerr << "File open failed!" << endl;
return;
}

fs["name"] >> name;
fs["age"] >> age;
fs["point"] >> pt1;
fs["scores"] >> scores;
fs["data"] >> mat1;

fs.release();

유용한 OpenCV 기능

마스크 연산

  • OpenCV에서는 임의의 모양을 갖는 ROI를 설정하기 위하여 일부 행렬 연산 함수에 대하여 마스크 연산을 지원한다.
    • 마스크 연산을 지원하는 OpenCV 함수는 보통 입력 영상과 크기가 같고 깊이가 CV_8U인 마스크 영상을 함께 인자로 전달 받는다.
    • 마스크 영상이 주어질 경우, 마스크 영상의 픽셀값이 0이 아닌 좌표에 대해서만 연산이 수행된다.
    • 일반적으로 마스크 영상은 사람의 눈으로도 구분이 쉽도록 픽셀 값이 0 또는 255로 구성된 흑백 영상이 사용된다.
  • Mat::setTo() 함수는 마스크 연산을 지원하는 함수로 함수의 두 번째 인자 mask에 마스크 영상을 지정할 수 있다.
    • 기본값으로 설정되어 있는 noArray()를 mask 인자로 지정하면 입력 행렬의 모든 원소 값을 value 값으로 설정하고 적절한 마스크 영상을 mask 인자로 지정하면 특정 영역에 대해서만 픽셀 값을 설정할 수 있다. 이때 마스크 영상은 Mat::setTo()를 호출하는 대상 행렬과 크기가 같아야 한다.
Mat src = imread("lenna.bmp", IMREAD_COLOR);
Mat mask = imread("mask_smile.bmp", IMREAD_GRAYSCALE);

if (src.empty() || mask.empty())
{
cerr << "Image load failed!" << endl;
return;
}

src.setTo(Scalar(0, 255, 255), mask);
  • 마스크 연산을 지원하는 또 다른 함수로 Mat::copyTo() 라는 함수가 있다.
    • 마스크 연산을 지원하는 Mat::copyTo() 함수는 mask 영상의 픽셀 값이 0이 아닌 위치에서만 *this 행렬 원소 값을 행렬 m으로 복사한다.
    • 만약 Mat::copyTo() 함수를 호출하는 *this 행렬과 인자로 전달된 m 행렬이 서로 크기 또는 타입이 같지 않을 경우, Mat::copyTo() 함수 내부에서 m.create() 함수를 호출하여 대상 영상 m을 새롭게 생성한 후 마스크 영상을 고려하여 픽셀 값을 복사한다.
    • 만약 *this 행렬과 m 행렬이 서로 크기와 타입이 같다면 m 행렬 원소 값을 그대로 유지한 상태에서 *this 행렬의 픽셀 값을 복사한다.
Mat src = imread("airplane.bmp", IMREAD_COLOR);
Mat mask = imread("mask_plane.bmp", IMREAD_GRAYSCALE);
Mat dst = imread("field.bmp", IMREAD_COLOR);

if (src.empty() || mask.empty())
{
cerr << "Image load failed!" << endl;
return;
}

src.copyTo(dst, mask);

연산 시간 측정

  • OpenCV 라이브러리는 정밀한 시간 측정 방법을 제공한다. 본래 특정 프로그램의 동작 시간을 측정하는 C/C++ 소스 코드 작성 방법은 운ㄷ영 체제마다 각기 다르지만 OpenCV 라이브러리를 이용하면 운영 체제에 상관 없이 통일된 인터페이스 함수를 사용하여 연산 시간을 측정할 수 있다.
  • OpenCV에서는 getTickCount() 함수와 getTickFrequency() 함수를 사용하여 특정 연산의 수행 시간을 측정한다.
    • getTickCount() 함수는 컴퓨터 시스템의 특정 시점부터 현재까지 발생한 틱(tick) 횟수를 반환한다. 여기서 틱 횟수는 컴퓨터 시스템에서 발생하는 클럭처럼 매우 빠르게 증가하는 성능 측정 계수를 의미하며, 컴퓨터의 성능에 따라 빠르게 증가할 수도 있고, 느리게 증가할 수도 있다.
    • 틱 횟수 차이 값은 사용하고 있는 컴퓨터의 시스템 성능에 따라 다르게 측정되므로 실제 연산 시간을 알아내기 위해서는 틱 횟수 차이를 시스템의 틱 주파수(tick frequency)로 나누는 작업이 동반되어야 한다.
    • 틱 주파수란 1초 동안 발생하는 틱 횟수를 의미하며 getTickFrequency()를 이용하여 시스템의 틱 주파수를 구할 수 있다.
int64 t1 = getTickCount();

my_func(); // do something

int64 t2 = getTickCount();
double ms = (t2 - t1) * 1000 / getTickFrequency();
  • getTickCount()와 getTickFrequency() 를 조합하는 것이 번거롭기 때문에 OpenCV 3.2.0 버전부터 연산 시간 측정을 위한 TickMeter 라는 이름의 클래스를 새롭게 제공한다.
    • 사용법은 아래와 같다.
TickMeter tm;
tm.start();

my_func(); // do something

tm.stop();
double ms = tm.getTimeMilli();

유용한 OpenCV 함수 사용법

sum() 함수와 mean() 함수

  • OpenCV에서 Mat 행렬의 원소 합을 구하고 싶을 때는 sum() 함수를 사용하고, 평균을 구하고 싶을 때는 mean() 함수를 사용한다.
    • 이 두 함수는 4채널 이하의 행렬에 대해서만 동작하며, 합과 평균을 Scalar 타입으로 반환한다.
  • mean() 함수는 마스크 연산을 지원하므로 필요한 경우 mask 영상을 지정하여 특정 영역의 원소 평균을 구할 수도 있다.
Mat img = imread("lenna.bmp", IMREAD_GRAYSCALE);

int sumVal = (int)sum(img)[0];
int meanVal = (int)mean(img)[0];

minMaxLoc() 함수

  • minMaxLoc() 함수는 행렬 또는 영상에서 최솟값, 최댓값, 그리고 최솟값과 최댓값의 위치를 찾을 때 사용한다.
    • minMaxLoc() 함수는 마스크 연산을 지원하므로 행렬 일부 영역에서의 최솟값, 최댓값과 그 위치를 구할 수 있다.
double minVal, maxVal;
Point minPos, maxPos;
minMaxLoc(img, &minVal, &maxVal, &minPos, &maxPos);

normalize() 함수

  • 행렬의 노름(norm) 값을 정규화하거나 원소 값 범위를 특정 범위로 정규화할 때 normalize() 함수를 사용한다.
  • normalize() 함수는 norm_type 인자에 따라 동작이 결정된다.
    • norm_type이 NORM_INF, NORM_L1, NORM_L2인 경우에는 \|dst\|_{L_{p}} = alpha(p = Inf, 1, 2) 수식을 만족하도록 입력 행렬 원소 값의 크기를 조정한다. 

\|dst\|_{L_{\infty}} = max_{i} |dst_{i}| = alpha

\|dst\|_{L_{1}} = \sum_{i} |dst_{i}| = alpha

\|dst\|_{L_{2}} = \sqrt{\sum_{i} dst_{i}^{2}} = alpha

  • 만약 norm_type 인자가 NORM_MINMAX인 경우에는 src 행렬의 최솟값이 alpha, 최댓값이 beta가 되도록 모든 원소 값 크기를 조절한다.
  • 많은 OpenCV 예제 코드에서 NORM_MINMAX 타입으로 normalize() 함수를 사용하고 있으며, 특히 실수로 구성된 행렬을 그레이스케일 영상 형태로 변환하고자 할 때 normalize() 함수를 사용하면 유용하다.
Mat src = Mat_<float>({1, 5}, {-1.f, -0.5f, 0.f, 0.5f, 1.f});

Mat dst;
normalize(src, dst, 0, 255, NORM_MINMAX, CV_8UC1);

cvRound() 함수

  • OpenCV에서 실수 갑의 반올림 연산을 위해 cvRound() 함수를 제공한다.
  • 이와 더불어 실수의 올림을 수행할 때는 cvCeil() 함수를 사용하고, 내림을 수행할 때는 cvFloor() 함수를 사용한다.

OpenCV 4로 배우는 컴퓨터 비전과 머신 러닝/ OpenCV 주요 클래스

  • (각 자료형의 상세한 파라미터에 대해서는 이전의 <OpenCV로 배우는 영상처리 및 응용>에서 정리하였으므로 생략)

기본 자료형 클래스

Point_ 클래스

  • Point_ 클래스는 2차원 평면 위에 있는 점의 좌표를 표현하는 템플릿 클래스
  • 2차원 좌표를 나타내는 x와 y라는 이름의 멤버변수를 갖고 있다.
// 생성 및 초기화
Point pt1; // pt1 = (0, 0)
pt1.x = 5; pt1.y = 10; // pt1 = (5, 10)
Point pt2(10, 30); // pt2 = (10, 30)

// 연산
Point pt3 = pt1 + pt2; // pt3 = [15, 40]
Point pt4 = pt1 * 2; // pt4 = [10, 20]
int d1 = pt1.dot(pt2); // d1 = 350
bool b1 = (pt1 == pt2); // b1 = false

Size_ 클래스

  • 영상 또는 사각형의 크기를 표현하는 클래스.
  • 사각형 영역의 가로와 세로 크기를 나타내는 width, height 멤버 변수를 갖고 있다.
// 생성 및 초기화
Size sz1, sz2(10, 20);  // sz1 = [0 x 0], sz2 = [10 x 20]
sz1.width = 5; sz1.height = 10;  // sz1 = [5 x 10]

// 연산
Size sz3 = sz1 + sz2;  // sz3 = [15 x 30]
Size sz4 = sz1 * 2;  // sz4 = [10 x 20]
int area1 = sz4.area();  // area1 = 200

Rect_ 클래스

  • 사각형의 위치와 크기를 표현하는 클래스
  • 사각형의 좌측 상단 점의 좌표를 나타내는 x, y와 가로 세로 크기를 나타내는 width, height 멤버 변수를 갖고 있다.
// 생성 및 초기화
Rect rc1;  // rc1 = [0 x 0 from (0, 0)]
Rect rc(10, 10, 60, 40);  // rc2 = [60 x 40 from (10, 10)]

// 연산
Rect rc3 = rc1 + Size(50, 40);  // rc3 = [50 x 40 from (0, 0)]
Rect rc4 = rc2 + Point(10, 10);  // rc4 = [60 x 40 from (20, 20)]

// 논리연산
Rect rc5 = rc3 & rc4; // rc5 = [30 x 20 from (10, 10)]
Rect rc6 = rc3 | rc4; // rc6 = [80 x 60 from (0, 0)]

RotatedRect 클래스

  • 회전된 사각형을 표현하는 클래스
  • 사각형의 중심 좌표를 나타내는 center, 가로 및 세로 크기를 나타내는 size, 회전 각도를 나타내는 angle 멤버 변수를 갖고 있다.
// 생성 및 초기화
RotatedRect rr1(Point2f(40, 30), Size2f(40, 20), 30.f);  // 아래 그림 a 참조

// 사각형의 바운딩 영역 확인
Rect br = rr1.boundingRect();  // 아래 그림 b 참조

Range 클래스

  • 범위 또는 구간을 표현하는 클래스.
  • 범위의 시작과 끝을 나타내는 start와 end 멤버 변수를 갖고 있다.
// 생성 및 초기화
Range r1(0, 10);

String 클래스

  • 문자열을 다루는 클래스
  • 본래 OpenCV에서는 자체적인 String을 정의하여 사용하였음. String 클래스는 std::string 클래스와 완전히 호환되도록 설계되어서 std::string 클래스를 다루는 방식과 유사하게 사용할 수 있었음.
  • 그러다가 OpenCV 4.0 버전부터는 자체적인 String 클래스를 삭제하고 C++ 표준 라이브러리의 std::string 클래스를 String 클래스로 재정의함.
// 생성 및 초기화
String str1 = "Hello";
String str2 = "world";
String str3 = str1 + " " + str2;  // str3 = "Hello world";

bool ret = (str2 == "WORLD");  // ret = false, == 연산자는 대소문자를 구분하므로

Mat 클래스

Mat 클래스 개요

  • Mat 클래스는 일반적인 2차원 행렬뿐만 아니라 고차원 행렬을 표현할 수 있으며, 한 개 이상의 채널(channel)을 가질 수 있음.
  • Mat 클래스에는 정수, 실수, 복소수 등으로 구성된 행렬 또는 벡터(vector)를 저장할 수 있고, 그레이스케일 또는 컬러 영상을 저장할 수도 있음.
  • 경우에 따라 벡터 필드(vector field), 포인트 클라우드(point cloud), 텐서(tensor), 히스토그램(histogram) 등 정보를 저장하는 용도로 사용 됨.
  • Mat 클래스에서 행렬이 어떤 자료형을 사용하는지에 대한 정보를 깊이(depth)라고 하는데, OpenCV에서 Mat 행렬의 깊이는 다음과 같은 형식의 매크로 상수를 이용하여 표현 함.
CV_<bit_depth>{U|S|F}
  • 깊이 표현 매크로 상수 형식의 처음에 나타나는 CV_는 OpenCV를 나타내는 접두사
  • 그 뒤의 <bit_depth>에는 8, 16, 32, 64의 숫자를 지정할 수 있으며, 이는 원소 값 하나의 비트 수를 나타냄
  • 그 다음의 {U|S|F} 부분에는 U, S, F 세 문자 중 하나를 지정할 수 있는데, U는 부호 없는 정수형, S는 부호 있는 정수형, F는 부동 소수형을 의미함
  • OpenCV 라이르러리는 행렬의 깊이 표현을 위해 다음과 같은 매크로 상수를 정의하여 사용함
#define CV_8U  0  // uchar, unsigned char
#define CV_8S  1  // schar, signed char
#define CV_16U 2  // ushort, unsigned short
#define CV_16S 3  // signed short
#define CV_32S 4  // int
#define CV_32F 5  // float
#define CV_64F 6  // double
#define CV_16F 7  // float16_t
  • Mat 행렬 원소는 하나의 값을 가질 수도 있고, 여러 개의 구성된 값을 가질 수도 있다.
  • Mat 행렬 원소를 구성하는 각각의 값을 채널(channel)이라고 한다.
    • 즉, Mat 행렬은 하나의 채널을 가질 수도 있고, 여러 개의 채널을 가질 수도 있다.
    • 이때 하나의 행렬을 구성하는 각 채널은 모두 같은 자료형을 사용해야 함.
  • OpenCV에서는 Mat 행렬의 깊이 정보와 채널 수 정보를 합쳐서 Mat 객체의 타입(type)이라고 부른다. OpenCV 행렬의 타입은 다음과 같은 형식의 매크로 상수로 표현한다.
CV_<bit_depth>{U|S|F}C(<number_of_channels>)
  • 즉, Mat 행렬의 깊이 표현 매크로 뒤에 C1, C3 같은 채널 정보가 추가로 붙어 있는 형태이다.
    • 예컨대 CV_8UC1 타입은 8비트 unsigned char 자료형을 사용하고 채널이 한 개인 행렬 또는 영상을 의미한다.
    • B, G, R 세 개의 색상 성분을 가지고 있는 컬러 영상은 unsigned char 자료형을 사용하고 세 개의 채널을 가지고 있기 때문에 CV_8UC 타입이다.
    • 복소수처럼 두 개의 실수 값을 사용하는 행렬은 CV_32FC2 타입으로 만들 수 있다.

행렬의 생성과 초기화

// 생성 및 초기화
Mat img1;
Mat img2(480, 640, CV_8UC1);
Mat img3(480, 640, CV_8UC3);  // unsigned char, 1-channel
Mat img4(Size(640, 480), CV_8UC3);  // unsigned char, 3-channels
  • 행렬의 크기와 타입을 지정하여 Mat 객체를 생성하는 경우 Mat 행렬의 모든 원소는 쓰레기값(garbage value)이라고 부르는 임의의 값으로 채워지게 된다. 고로 모든 원소 값을 특정 값으로 초기화하여 사용하는 것이 안전하다.
Mat img5(480, 640, CV_8UC1, Scalar(128));  // initial values, 128
Mat img6(480, 640, CV_8UC3, Scalar(0, 0, 255));  // initial values, red
  • 새로운 행렬을 생성할 때 모든 원소 값을 0으로 초기화하는 경우가 많기 때문에 OpenCV에서는 모든 원소가 0으로 초기화된 행렬을 만드는 함수로써 Mat::zeros()를 제공한다.
Mat mat1 = Mat::zeros(3, 3, CV_32SC1);  // 0's matrix
  • 비슷하게 행렬의 모든 원소가 1로 초기화된 행렬을 생성하려면 Mat::ones()를 이용하면 되고, 단위 행렬(identity matrix)를 생성하려면 Mat:eye() 함수를 이용하면 된다.
Mat mat2 = Mat::ones(3, 3, CV_32FC1);  // 1's matrix
Mat mat3 = Mat::eye(3, 3, CV_32FC1);  // identity matrix

mat1 = \left[ \begin{array}{rrr} 0 & 0 & 0 \\ 0 & 0 & 0 \\ 0 & 0 & 0 \end{array} \right]

mat2 = \left[ \begin{array}{rrr} 1 & 1 & 1 \\ 1 & 1 & 1 \\ 1 & 1 & 1 \end{array} \right]

mat3 = \left[ \begin{array}{rrr} 1 & 0 & 0 \\ 0 & 1 & 0 \\ 0 & 0 & 1 \end{array} \right]

  • Mat 객체를 생성할 떄, 행렬 원소를 저장할 메모리 공간을 새로 할당하는 것이 아니라 기존에 이미 할당되어 있는 메모리 공간의 데이터를 행렬 원소 값으로 사용할 수 있다.
    • 이는 외부 메모리 공간을 참조하는 방식이기 때문에 객체 생성이 빠르다는 장점이 있다. 이러한 용도의 Mat 클래스 생성자 형식은 다음과 같다.
float data[] = { 1, 2, 3, 4, 5, 6 };
Mat mat4(2, 3, CV_32FC1, data);
  • 외부 배열을 행렬 원소 값으로 사용하는 경우 외부 배열 크기와 생성할 행렬 원소 개수는 같아야 하고, 서로 사용한느 자료형도 같아야 한다.
    • 위와 같은 코드의 경우 mat4 행렬의 1행은 data 배열의 처음 세 개의 원소로 채워지고, 2행은 data 배열의 나머지 원소로 채워진다.

mat4 = \left[ \begin{array}{rrr} 1 & 2 & 3 \\ 4 & 5 & 6 \end{array} \right]

  • 사용자가 지정한 원소 값을 이용하여 Mat 객체를 생성하는 방법 중에 Mat_ 클래스를 사용하는 방법도 종종 사용된다. Mat_ 클래스는 Mat 클래스를 상속하여 만든 템플릿 클래스로 Mat_ 클래스 객체와 Mat 객체는 상호 변환이 가능하다.
    • 그런데 Mat_ 클래스는 << 연산자와 , 를 이용하여 간단하게 행렬 원소 값을 설정하는 인터페이스를 제공한다.
    • 그래서 일단 Mat_ 객체를 만들어서 << 연산자로 행렬을 지정한 후 이를 Mat 객체로 변환하여 사용하기도 한다.
Mat_<float> mat5_(2, 3);
mat5_ << 1, 2, 3, 4, 5, 6;
Mat mat5 = mat5_;

// 다음과 같이 한 줄로도 표현 가능
Mat mat5 = (Mat_<float>(2, 3) << 1, 2, 3, 4, 5, 6);

mat5 = \left[ \begin{array}{rrr} 1 & 2 & 3 \\ 4 & 5 & 6 \end{array} \right]

  • OpenCV 4.0에서는 C++ 11의 초기화 리스트(initializer list)를 이용한 행렬 초기화 방법을 사용할 수 있다.
    • Mat 클래스 또는 Mat_ 클래스의 생성자에 행렬 크기와 초깃값을 중괄호를 이용한 리스트 형태로 전달하는 방식.
    • 다만 생성된 Mat 객체의 타입을 명시적으로 지정하기 위해 Mat_ 클래스 형식으로 생성한 후 Mat 타입으로 변경하는 것이 좋다.
Mat mat6 = Mat_<float>({2, 3}, {1, 2, 3, 4, 5, 6});
  • 비어 있는 Mat 객체 또는 이미 생성된 Mat 객체에 새로운 행렬을 할당하려면 Mat 클래스의 Mat::create() 멤버 함수를 사용할 수 있다.
    • 이미 행렬 데이터가 할당되어 있는 Mat 객체에서 Mat::create() 함수를 호출할 경우, 만약 Mat::create() 함수의 인자로 지정한 행렬 크기와 타입이 기존 행렬과 모두 같으면 Mat::create() 함수는 별다른 동작을 하지 않고 그대로 함수를 종료한다.
    • 반면 새로 만들 행렬의 크기 또는 타입이 기존 행렬과 다른 경우, Mat::create() 함수는 일단 기존 메모리 공간을 해제한 후 새로운 행렬 데이터 저장을 위한 메모리 공간을 할당한다.
mat4.create(256, 256, CV_8UC3);  // 256 x 256, uchar, 3-channels
mat5.create(4, 4, CV_32FC1);  // 4 x 4, float, 1-channels
  • Mat:create() 함수는 새로 만든 행렬의 원소 값을 초기화하는 기능이 없기 때문에 행렬을 생성한 후 행렬 전체를 초기화하고 싶다면 = 연산자 재정의 또는 Mat::setTo() 멤버 함수를 이용하여 행렬 전체 원소 값을 한꺼번에 설정할 수 있다.
    • Mat:setTo() 함수는 두 개의 인자를 가지고 있지만 두 번째 인자 mask는 기본값을 가지고 있으므로 생략할 수 있다.
mat4 = Scalar(255, 0, 0);
mat5.setTo(1.f); 

행렬의 복사

  • Mat 클래스 객체에 저장된 영상 또는 행렬을 복사하는 가장 간단한 방법은 복사 생성자 또는 대입 연산자를 사용하는 방법이다.
Mat img1 = imread("dog.bmp");

Mat img2 = img1;  // 복사 생성자 - 얕은 복사

Mat img3;
img3 = img1; // 대입 연산자 - 얕은 복사
  • 만일 복사본 영상을 새로 생성할 때 픽셀 데이터를 공유하는 것이 아니라 메모리 공간을 새로 할당하여 픽셀 데이터 전체를 복사하고 싶다면 Mat::clone() 또는 Mat::copyTo() 함수를 사용해야 한다.
    • Mat::clone() 함수는 자기 자신과 동일한 Mat 객체를 완전히 새로 만들어서 반환한다. 
    • Mat::copyTo() 함수는 인자로 전달된 m 행렬에 자기 자신을 복사한다. 만약 Mat::copyTo() 함수를 호출한 행렬과 인자로 전달된 행렬 m이 서로 크기와 타입이 같으면 원소 값 복사만 수행한다.
    • 반면 서로 크기 또는 타입이 다르면 Mat::copyTo() 함수 내부에서 행렬 m을 새로 생성한 후 픽셀 값을 복사한다.
Mat img4 = img1.clone();  // 깊은 복사

Mat img5;
img1.copyTo(img5);  // 깊은 복사

부분 행렬 추출

  • Mat 클래스로 정의된 행렬에서 특정 사각형 영역의 부분 행렬을 추출하고 싶을 때는 Mat 클래스에 저으이된 괄호 연산자 재정의를 사용한다.
Mat img1 = imread("cat.bmp");
Mat img2 = img1(Rect(220, 120, 340, 240)); // 220, 120 좌표부터 340 x 240 크기만큼의 사각형 부분 영상 추출
  • 부분 영상 추출은 얕은 복사이기 때문에 부분 영상을 추출한 후 부분 영상의 픽셀 값을 수정하면 원본 영상의 픽셀 값도 함께 변경 됨.
  • 영상의 반전은 Mat 클래스 타입의 변수 앞에 ~ 연산자를 붙이는 방식으로 할 수 있다.
img2 = ~img2;
  • Mat 클래스의 부분 영상 참조 기능은 입력 영상에 사각형 모양의 관심 영역 (ROI, Region Of Interest)을 설정하는 용도로 사용할 수 있다. 
    • ROI는 영상의 전체 영역 중에서 특정 영역에 대해서만 영상 처리를 수행할 때 설정하는 영역을 의미한다.
    • 사각형이 아닌 임의의 모양의 ROI를 설정하고 싶은 경우에는 마스크 연산을 응용할 수 있다.
  • 만약 독립된 메모리 영역을 확보하여 부분 영상을 추출하고자 한다면 괄호 연산자 뒤에 Mat::clone() 함수를 사용하면 된다.
Mat img3 = img1(Rect(220, 120, 340, 240)).clone();
  • Mat 행렬에서 특정 범위의 행을 추출하고자 할 때는 Mat::rowRange()를 이용하며 특정 열을 추출하고자 할 때는 Mat:colRange() 함수를 사용할 수 있다.
    • 행 또는 열의 범위는 두 개의 int 값으로 지정할 수도 있고, Range 클래스 객체를 이용하여 지정할 수도 있다.
    • 하나의 행 또는 열을 추출하여 1행짜리 또는 1열짜리 행렬을 만들고자 할때는 Mat::row() 또는 Mat::col() 함수를 사용할 수 있다.
  • Mat::rowRange(), Mat::colRange(), Mat::row(), Mat::col() 함수들은 모두 부분 행렬을 얕은 복사 형태로 반환한다.
    • 따라서 깊은 복사를 수행하려면 Mat::clone() 함수를 함께 사용해야 한다.

행렬의 원소 값 참조

Mat::at() 함수 사용 방법

  • OpenCV에서 제공하는 가장 직관적인 행렬 원소 접근 방법은 Mat::at() 멤버 함수를 사용하는 방법이다.
    • Mat::at() 함수는 보통 행과 열을 나타내는 두 개의 정수를 인자로 받아 해당 위치의 행렬 원소 값을 참조 형식으로 반환한다.
    • Mat::at() 함수는 템플릿 함수로 정의되어 있기 때문에 Mat::at() 함수를 사용할 때는 행렬 원소 자료형을 명시적으로 지정해야 한다.
Mat mat1 = Mat::zeros(3, 4, CV_8UC1);

for (int j = 0; j < mat1.rows; j++)
{
for (int i = 0; i < mat.cols; i++)
{
mat1.at<uchar>(j, i)++;
}
}
  • 위 코드에서 Mat::at() 함수가 참조하는 행렬 원소 위치는 아래 그림과 같다.

Mat::ptr() 함수 사용 방법

  • Mat:ptr() 함수는 Mat 행렬에서 특정 행의 첫 번째 원소 주소를 반환한다.
    • Mat::ptr() 함수도 템플릿으로 정의되어 있기 때문에 행렬 원소의 자료형을 명시적으로 지정해야 한다.
    • Mat::ptr() 함수는 지정한 자료형의 포인터를 반환하며, 이 포인터를 이용하여 지정한 행의 원소에 접근할 수 있다.
for (int j = 0; j < mat1.rows; j++)
{
uchar* p = mat1.ptr<uchar>(j);

for (int i = 0; i < mat.cols; i++)
{
p[i]++;
}
}

MatIterator_ 반복자 사용 방법

  • Mat 클래스와 함께 사용할 수 있는 반복자 클래스 이름은 MatIterator_ 이다.
    • MatIterator_ 클래스는 템플릿으로 정의된 클래스이므로 Mat 행렬 타입에 맞는 자료형을 명시하여 사용해야 한다.
    • MatIterator_ 클래스를 사용하는 방식은 C++의 반복자 사용 방법과 유사하다. Mat::begin() 함수를 이용하여 행렬의 첫 번째 원소 위치를 얻을 수 있고, Mat::end() 함수를 이용하여 마지막 원소 바로 다음 위치를 얻을 수 있다.
  • MatIterator_ 반복자를 사용하면 행렬의 가로, 세로 크기에 상관없이 모든 원소를 안전하게 방문할 수 있지만, Mat::ptr() 보다 느리고, Mat::at() 처럼 임의의 위치에 자유롭게 접근할 수 없기 때문에 사용성이 높지 않은 편이다.
for (MatIterator_<uchar> it = mat1.begin<uchar>(); it != mat1.end<uchar>(); ++it)
{
(*it)++;
}

행렬 정보 참조하기

  • (Mat 클래스의 멤버 변수들 설명. 생략)

행렬 연산

// mat1과 mat2 행렬 사이의 덧셈과 뺄셈 연산
mat3 = mat1 + mat2;
mat3 = mat1 - mat2;

// mat1 행렬의 각 원소와 슼라라 s1 사이의 덧셈 및 뺄셈 연산
mat3 = mat1 + s1;
mat3 = mat1 - s1;
mat3 = s1 + mat1;
mat3 = s1 - mat1;

// mat1 행렬의 각 원소에 -1을 곱함
mat3 = -mat1;

// mat1과 mat2 행렬의 곱셈 연산
mat3 = mat1 * mat2;

// mat1 행렬의 각 원소에 실수 d1을 곱함
mat3 = mat1 * d1;
mat3 = d1 * mat1;

// mat1과 mat2 행렬의 같은 위치 원소끼리 나눗셈
mat3 = mat1 / mat2;

// mat1 행렬의 각 원소와 실수 d1끼리 나눗셈 연산
mat3 = mat1 / d1;
mat3 = d1 / mat1;
  • 위의 연산 중 * 연산은 행렬의 수학적 곱셈 연산을 의미하며, 만일 두 행렬의 같은 위치에 있는 원소끼리 곱셈 연산을 수행하려면 Mat::mul() 함수를 사용해야 한다.
  • 행렬의 역행렬은 Mat::inv() 함수를 이용해서 구할 수 있다.
    • Mat::inv() 함수는 method 인자를 통해 역행렬 계산 방법을 지정할 수 있다.
    • 역행렬이 존재하는 일반적인 행렬이라면 가우스 소거법을 사용하는 DECOMP_LU를 사용할 수 있으며, 이 값은 기본값으로 지정되어 있으므로 생략할 수 있다.
    • 역행렬이 존재하지 않는 경우 DECOMP_SVD를 지정하면 특잇값 분해(singular value decomposition) 방법을 이용하여 의사 역행렬(pseudo-inverse matrix)를 구할 수 있다.
    • DECOMP_EIG와 DECOMP_CHOLESKY는 각각 고윳값 분해와 촐레스키(Cholesky) 분해에 의한 역행렬 계산을 의미한다.
  • 행렬의 전치행렬은 Mat::t()를 이용하여 구할 수 있다.

크기 및 타입 변환 함수

  • 행렬의 타입을 변경할 때는 Mat::convertTo() 함수를 사용한다.
    • Mat::convertTo() 함수는 행렬 원소의 타입을 다른 타입으로 변경하고 추가적으로 모든 원소에 일정한 값을 더하거나 곱할 수 있다.
    • Mat::convertTo() 함수에 의해 생성되는 출력 행렬 m의 원소 값은 다음 수식에 의해 결정된다.

m(x, y) = saturate_cast<rtype>(alpha \times (*this)(x, y) + beta)

  • 일반적으로 영상은 픽셀 값을 uchar 자료형을 이용하는데, 만일 일련의 복잡한 연산을 수행해야 하는 경우 연산의 정확도를 높이기 위해 픽셀 값을 float, double 같은 실수형으로 변환하여 내부 연산을 수행해야 하는 경우가 있을 수 있다.
  • 이러한 경우에 Mat::convertTo() 함수를 사용하여 CV_8UC1 타입의 행렬을 CV_32FC1 타입으로 변경할 수 있다.
Mat img1 = imread("lenna.bmp", IMREAD_GRAYSCALE);

Mat img1f;
img1.convertTo(img1f, CV_32FC1);
  • Mat::reshape() 함수는 주어진 행렬의 크기 또는 채널 수를 변경한다.
    • Mat::reshape() 함수는 행렬 원소 데이터를 복사하여 새로운 행렬을 만드는 것이 아니라 하나의 행렬 원소 데이터를 같이 참조하는 행렬을 반환한다. 그러므로 Mat::reshape() 함수에 의해 반환된 행렬 원소 값을 변경하면 원본의 값도 함께 바뀌게 된다.
uchar data1[] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12 };
Mat mat1(3, 4, CV_8UC1, data1);
Mat mat2 = mat1.reshape(0, 1);

// result
// mat1: [ 1, 2, 3, 4;
// 5, 6, 7, 9;
// 9, 10, 11, 12]
// mat2: [ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12 ]
  • 행렬의 모양이 아니라 단순히 행렬의 행 크기를 변경하고 싶을 때는 Mat::resize() 함수를 사용하면 된다.
    • Mat::resize() 함수는 행렬의 행 개수를 변경하는데, 입력된 개수가 기존 행 개수보다 작으면 아래쪽 행을 제거하고, 기존 행렬의 행 개수보다 크면 아래쪽에 행을 추가한다. 이때 추가하는 행 원소의 초기값을 지정할 수 있다.
mat1.resize(5, 100);

// result
// mat1: [ 1, 2, 3, 4;
// 5, 6, 7, 9;
// 9, 10, 11, 12;
// 100, 100, 100, 100;
// 100, 100, 100, 100]
  • 이미 존재하는 행렬에 원소 데이터를 추가하고 싶을 때는 Mat::push_back() 함수를 이용할 수 있다.
    • Mat::push_back() 함수 인자로 _Tp& 또는 std::vector<_Tp>& 타입을 사용할 경우, *this 행렬은 1열짜리 행렬이어야 한다.
    • 만약 Mat_<_Tp>& 또는 Mat& 타입을 인자로 사용할 경우에는 *this 행렬과 인자로 전달된 m 행렬의 열 개수가 같아야 한다.
Mat mat3 = Mat::ones(1, 4, CV_8UC1) * 255;
mat1.push_back(mat3);

// result
// mat1: [ 1, 2, 3, 4;
// 5, 6, 7, 9;
// 9, 10, 11, 12;
// 100, 100, 100, 100;
// 100, 100, 100, 100;
// 255, 255, 255, 255]
  • Mat::push_back()과 반대로 맨 아래에 있는 행을 제거할 때는 Mat::pop_back() 함수를 이용하면 된다.

Vec과 Scalar 클래스

Vec 클래스

  • 벡터는 같은 자료형을 가진 원소 몇 개로 구성된 데이터 형식이다.
// 생성 및 초기화
Vec<uchar, 3> p1, p2(0, 0, 255);
  • 매번 Vec<uchar, 3> 형태로 입력하는 것은 번거롭기 때문에, OpenCV에서는 자주 사용되는 자료형과 개수에 대한 Vec 클래스 템플릿의 이름을 재정의를 제공한다.
    • OpenCV 에서 제공하는 Vec 클래스 템플릿의 이름 재정의는 다음 형식을 따른다.
Vec<num-of-data>{b|s|w|i|f|d}
  • <num-of-data> 위치에는 2, 3, 4 등 숫자를 지정할 수 있고, {b|s|w|i|f|d} 부분에는 b, s, w, i, f, d 문자 중 하나를 지정한다.
    • OpenCV 라이브러리에 정의된 Vec 클래스의 이름 재정의는 다음과 같다.
typedef Vec<uchar, 2> Vec2b;
typedef Vec<uchar, 3> Vec3b;
typedef Vec<uchar, 4> Vec4b;

typedef Vec<short, 2> Vec2s;
typedef Vec<short, 3> Vec3s;
typedef Vec<short, 4> Vec4s;

typedef Vec<ushort, 2> Vec2w;
typedef Vec<ushort, 3> Vec3w;
typedef Vec<ushort, 4> Vec4w;

typedef Vec<int, 2> Vec2i;
typedef Vec<int, 3> Vec3i;
typedef Vec<int, 4> Vec4i;
typedef Vec<int, 6> Vec3i;
typedef Vec<int, 8> Vec4i;

typedef Vec<float, 2> Vec2f;
typedef Vec<float, 3> Vec3f;
typedef Vec<float, 4> Vec4f;
typedef Vec<float, 6> Vec4f;

typedef Vec<double, 2> Vec2d;
typedef Vec<double, 3> Vec3d;
typedef Vec<double, 4> Vec4d;
typedef Vec<double, 6> Vec4d;
  • Vec 클래스는 [ ] 연산자가 재정의되어 있기 때문에 [ ] 연산자를 이용하여 멤버 변수 val 배열에 쉽게 접근할 수 있다.

Scalar 클래스

  • Scalar 클래스는 4채널 이하의 영상에서 픽셀 값을 표현하는 용도로 자주 사용된다.
    • Scalar 클래스는 Scalar_ 라는 이름의 클래스 템플릿 이름 재정의이며, Scalar_ 클래스는 Vec 클래스를 상속받아 만들어졌다.
    • Scalar 클래스는 보통 네 개 이하의 채널을 갖는 영상의 픽셀 값을 표현하는 용도로 사용된다.
      • 그레이스케일 영상의 경우 Scalar 클래스의 첫 번째 원소가 픽셀 밝기를 표현하고 나머지 세 개는 0으로 설정된다.
      • 트루컬러 영상의 경우, 처음 세 개 원소가 B, G, R 색상 성분 값을 표현하고 네 번째 원소는 보통 0으로 설정된다.
      • 간혹 PNG 파일 처럼 투명도를 표현하는 알파 채널이 있는 경우 Scalar 클래스의 네 번째 원소를 이용하기도 한다.
Scalar(밝기)
Scalar(파란색, 녹색, 빨간색)
Scalar(파란색, 녹색, 빨간색, 투명도)

InputArray와 OutputArray 클래스

  • InputArray 클래스는 주로 OpenCV 함수의 입력으로 사용되고, OutputArray 클래스는 OpenCV 함수의 출력으로 사용되는 인터페이스 클래스이다.

InputArray 클래스

  • InputArray 클래스는 Mat, vector<T> 등 다양한 타입을 표현할 수 있는 인터페이스 클래스로서 OpenCV 함수의 입력 인자 자료형으로 주로 사용된다.
    • _InputArray 클래스는 Mat, Mat_<T>, Matx<T, m, n>, vector<T>, vector<vector<T>>, vector<Mat>, vector<Mat_<T>, UMat, vector<UMat>, double 같은 다양한 타입으로부터 생성될 수 있는 인터페이스 클래스이다.
    • _InputArray 클래스는 OpenCV 라이브러리 내부에서 코드 구현 편의상 사용되며, 사용자가 명시적으로 _InputArray 클래스의 인스턴스 또는 변수를 생성하여 사용하는 것을 금지하고 있다.
  • 만약 OpenCV에서 제공하는 함수처럼 사용자 저으이 함수에서 Mat 객체뿐만 아니라 vector<T> 타입의 객체를 한꺼번에 전달받을 수 있게 만들고 싶다면 사용자 저으이 함수 인자에 InputArray 타입을 사용할 수 있다.
    • 그리고 실제 함수 본문에서는 _InputArray 클래스의 멤버 함수인 _InputArray::getMat() 함수를 사용하여 Mat 객체 타입 형태로 변환해서 사용해야 한다.

OutputArray 클래스

  • _OutputArray 클래스는 클래스 계층적으로 _InputArray 클래스를 상속받아 만들어졌다. 그러므로 _OutputArray 클래스도 Mat 또는 vector<T> 같은 타입의 객체로부터 생성될 수 있다.
  • 다만 _OutputArray 클래스는 새로운 행렬을 생성하는 _OutputArray:create() 함수가 추가적으로 정의되어 있다.
    • 그래서 OpenCV의 많은 영상 처리 함수는 결과 영상을 저장할 새로운 행렬을 먼저 생성한 후, 영상 처리 결과를 저장하는 형태로 구현되어 있다.
  • OutputArray 클래스도 InputArray와 마찬가지로 사용자가 직접 OutputArray 타입의 변수를 생성해서 사용하면 안 된다.
    • OutputArray 타입으로 정의된 OpenCV 함수의 인자에는 Mat 또는 vector<T> 같은 타입의 변수를 전달하는 형태로 코드를 작성해야 한다.
  • 영상에 그림을 그리는 몇몇 OpenCV 함수는 입력 영상 자체를 변경하여 다시 출력으로 반환하는 경우가 있으며, 이러한 함수는 InputOutputArray 클래스 타입의 인자를 사용한다.
    • 이 클래스는 입력과 출력의 역할을 동시에 수행할 때 사용된다.

OpenCV 4로 배우는 컴퓨터 비전과 머신 러닝/ OpenCV 설치와 기초 사용법

OpenCV 개요와 설치

OpenCV 개요

  • OpenCV는 오픈 소스로 개발되고 있는 컴퓨터 비전 및 머신 러닝 라이브러리로 Open Source Computer Vision Library의 약자.
  • OpenCV는 2500개가 넘는 최신 컴퓨터 비전 알고리즘과 머신 러닝 알고리즘을 포함하고 있음.
    • 기본적인 영상 파일 입출력, 영상의 화질 향상, 객체 검출과 인식, 추적, 3차원 비전 문제 등의 기능을 제공하고, K 최근접 이웃 알고리즘(kNN, k-Nearest Neighbor)과 서포트 벡터 머신(SVM, Support Vector Machine) 같은 머신 러닝 알고리즘도 제공 함.
    • 최근에는 딥러닝(deep learning)으로 알려져 있는 심층 신경망(DNN, Deep Neural Network) 모델을 실행하는 기능도 제공되고 있음.
  • OpenCV는 태생부터 실시간 처리를 고려하여 만들어졌기 때문에 다양한 하드웨어 플랫폼에서 매우 빠르게 동작함.
    • OpenCV는 기본적으로 C/C++ 언어로 작성되었지만 Python, Java, Matlab, JavaScript 등과도 인터페이스도 제공함.
    • OpenCV는 Windows, Linux, MacOS 등 운영 체제를 지원하고 안드로이드와 iOS 같은 모바일 환경도 지원 함.
    • OpenCV 기능은 대부분 병렬 처리로 동작하며 MMX, SSE, AVX, NEON 등 CPU 특화 명령어도 지원 함. 또한 오래전부터 CUDA와 OpenCL을 통한 GPU 활용을 지원하고 있음.

OpenCV 역사

  • OpenCV는 1999년 인텔에서 개발된 IPL(Image Primitive Library)을 기반으로 만들어졌음.
  • 이후 2000년 일반에 공개되어 오픈 소스로서 개발이 진행되었고, 2006년에 OpenCV 1.0 버전이 정식으로 배포되었음.
    • OpenCV 1.0은 C 언어를 기반으로 구현되었기 때문에 많은 알고리즘이 구조체와 함수로 구현되었으며 영상 데이터는 IPlImage라는 이름의 구조체로 표현되었음.
  • 2009년에 OpenCV 2.0이 발표되었는데, 이때는 C++ 인터페이스를 채택하였고 Mat 이라는 이름의 클래스를 사용하여 영상 데이터를 표현하기 시작했음.
  • 2015년 6월에 OpenCV 3.0이 발표되었는데, 이때 OpenCV 프로젝트 구조가 크게 개선되었고 전반적인 성능이 향상 됨. 
    • OpenCV 사용성을 크게 확대한 T-API (Transparent API)를 지원하기 시작했고, 유료로 사용해야 했던 인텔 IPP(Intergrated Performance Primitives) 라이브러리 일부를 무료로 사용할 수 있게 되었음.
    • 2017년 발표된 OpenCV 3.3 버전에서는 최근 각광받고 있는 심층 신경망을 지원하는 DNN 모듈이 기본 소스에 포함되기 시작하였음. 또한 AVX/AVX2/SSE4.x 최적화가 추가되었고 C++ 11 문법을 지원하기 시작함.
  • 2018년 11월에 OpenCV 4.0이 발표되었는데, OpenCV 4.0의 가장 큰 변화는 C++ 11의 필수 지원. 람다 표현식(lambda expression)을 사용할 수 있게 됨.
    • 이 외에도 DNN 모듈 기능이 강화되어 AlexNet, Inception v2, Resnet, VGG 같은 영상 분류기 뿐만 아니라 Mask-RCNN, tiny YOLO 같은 최신 딥러닝 네트워크 구조를 지원함. QR 코드를 검출하고 해석하는 기능도 새롭게 제공 됨.
    • OpenCV 4.0에서는 오래된 C API 지원이 종료되어 더는 IplImage 구조체를 사용할 수 없게 됨.

OpenCV 모듈

  • OpenCV 라이브러리는 다수의 모듈(module)로 구성되어 있음.
    • OpenCV의 핵심 클래스와 함수는 core 모듈에 있으며, 영상 출력 기능은 highgui 모듈에 포함되어 있음.
모듈 이름 설명
core 행렬, 벡터 등 OpenCV 핵심 클래스와 연산 함수
calib3d 카메라 캘리브레이션과 3차원 재구성
dnn 심층 신경망 기능
features2d 2차원 특징 추출과 특징 벡터 기술, 매칭 방법
flann 다차원 공간에서 빠른 최근방 이웃 검색
highgui 영상의 화면 출력, 마우스 이벤트 처리 등 사용자 인터페이스
imgcodecs 영상 파일 입출력
imgproc 필터링, 기하학적 변환, 색 공간 변환 등 영상 처리 기능
ml 통계적 분류, 회기 등 머신 러닝 알고리즘
objdetect 얼굴, 보행자 검출 등 객체 검출
photo HDR, 잡음 제거 등 사진 처리 기능
stitching 영상 이어 붙이기
video 옵티컬 플로우, 배경 차분 등 동영상 처리 기술
videoio 동영상 파일 입출력
world 여러 OpenCV 모듈을 포함하는 하나의 통합 모듈

 

OpenCV 4로 배우는 컴퓨터 비전과 머신 러닝/ 컴퓨터 비전과 영상의 이해

컴퓨터 비전 개요

  • 1960년대 미국에서 인공위성으로부터 전송된 달 표면 사진의 잡음을 제거하는 작업을 수행하였는데 이것이 디지털 영상처리의 시초라고 알려져 있음.
  • 1966년 MIT에서 “The Summer Vision Project”라는 제목으로 카메라와 컴퓨터를 연결하여 카메라가 바라보는 장면을 컴퓨터가 인식하는 시도를 하였고, 이것을 컴퓨터 비전의 시초로 보는 사람들도 있음.
  • 1970년대에서 1990년대까지는 영상에서 객체의 윤곽 또는 에지 정보를 추출하거나 코너 점 검출, 모양 정보 분석, 텍스쳐 분석 등의 기본적인 영상 특징 분석 방법과 이를 응용한 3차원 구조 분석, 움직임 정보 추출, 얼굴 검출 및 인식 등의 고수준 연구가 진행 됨
  • 2000년대 들어와서는 실시간 얼굴 검출, 크기 불변 특징점 검출 및 매칭 등의 기술이 개발 됨.
    • 실시간 얼굴 검출 기술은 현재 대부분의 디지털 카메라에탑재되어 얼굴에 자동으로 초점을 맞춰주는 용도로 활용 되고 있음.
  • 2010년대 들어와서는 딥러닝(deep learning) 기술이 크게 발전하면서 컴퓨터 비전의 가능성과 활용 영역이 확대되어 가고 있음.
  • 컴퓨터 비전에 활용되는 관련 지식들은 아래 이미지 참조

영상의 구조와 표현방법

영상의 획득과 표현방법

  • 빛이 피사체에 부딪혀 반사되고, 그 반사된 빛이 카메라 렌즈를 통해 카메라 내부로 들어오게 됨. 실제 카메라에는 여러 렌즈를 복합적으로 사용하여 좋은 화질의 사진을 얻을 수 있도록 설계 됨.
  • 렌즈는 카메라 바깥으로부터 들어온 빛을 굴절시켜 이미지 센서로 모아주는 역할을 함.
  • 이미지 센서는 빛을 전기적 신호로 변환하는 포토 다이오드(photodiode)가 2차원 평면 상에 배열되어 있는 장치로 렌즈에서 모인 빛이 이미지 센서에 닿으면 이미지 센서에 포함된 포토 다이오드가 빛을 전기적 신호로 변환 함.
  • 빛을 많이 받은 포토 다이오드는 큰 신호를 생성하고 빛을 적게 받은 포토 다이오는 작은 크기의 신호를 생성함으로써 명암이 있는 2차원 영상을 구성 함.
  • 포토 다이오드에서 생성된 전기적 신호는 아날로그-디지털 변환기(ADC, Analog-Digital Convertor)를 거쳐 디지털 신호로 바뀌게 되고, 이 디지털 신호는 다시 카메라의 ISP(Image Signal Processor) 장치로 전달 됨.
  • ISP 장치는 화이트밸런스 조정, 색 보정, 잡음 제거 드으이 기본적인 처리를 수행한 후 2차원 디지털 영상을 생성 함.
  • 이렇게 구성된 영상은 곧바로 컴퓨터로 전송되거나 JPG, TIFF 등의 영상 파일 형식으로 변환되어 저장됨.

  • 영상을 구성하는 최소 단위는 픽셀(pixel). 픽셀은 사진(picture)와 요소(element)에서 유래하였으며, 화소라고도 부름.
  • 영상은 픽셀이 바둑판처럼 균일한 격자 형태로 배열되어 있는 형태로 표현 됨. (아래 이미지 참조)
  • 영상을 표현하는 2차원 xy 좌표계에서 x 좌표는 왼쪽에서 오른쪽으로 증가하고, y 좌표는 위에서 아래로 증가함 (0, 0이 좌상단)

  • 영상을 수식으로 설명할 때는 보통 함수의 형태를 사용함. 즉, x 좌표와 y 좌표를 입력 받고 해당 위치에서의 픽셀 값을 출력으로 내보내는 함수 형태로 영상을 표현할 수 있음. 
    • 예컨대 위 그림에 나타난 영상을 f라 표기할 때, (x, y) 좌표의 픽셀 값은 f(x, y)로 표현 됨.
  • 영상은 2차원 평면 위에 픽셀 값이 나열된 형태이기 때문에 영상을 2차원 행렬로 표현할 수 있음. 실제로 몇몇 영상 처리 알고리즘은 행렬 이론을 이용하여 컴퓨터 비전 문제를 해결하기도 함.
  • 행의 개수가 M이고 열의 개수가 N인 행렬 A는 아래와 같이 표현 함.
    • 아래 행렬에서 소문자 a_{ij} 는 j 번째 행, i 번째 열에 위치한 행렬 원소를 나타냄. 만일 영상을 나타내는 것이라면 해당 위치의 픽셀을 의미 함.

A = \left[ \begin{array}{rrrr} a_{11} & a_{12} & ... & a_{1n} \\ a_{21} & a_{22} & ... & a_{2n} \\ ... \\ a_{m1}  & a_{m2} & ... & a_{mn} \end{array} \right]

OpenCV로 배우는 영상 처리 및 응용/ 영상 처리 응용 사례 II

SVM을 이용한 차량 번호 검출 프로그램

SVM(Support Vector Machine)의 개념

  • 아래 그림 왼쪽과 같이 흰색 원과 검은색 원이 학습용 데이터로 주어졌고, 새로운 데이터가 입력되었다고 하자.
    • 일반적으로는 각 그룹 내에서 데이터 간 거리를 측정하여 중심점(center)를 구한다. 그리고 두 중심점의 중간에서 최적의 분리 경계면을 구한다. 이 판별 경계를 기준으로 새로운 데이터에 대하여 분류할 수 있다. 이런 방식을 선형 판별법이라고 한다.
  • SVM은 데이터들을 분리하는 분리 경계면 중에서 각 분류 데이터들과의 거리(margin)가 가장 먼 분리 경계면을 찾아냄으로써 데이터를 분리하는 방법이다.
    • 즉 분리 경계와 실제 데이터들 사이의 여유(margin)이 가장 크도록 분리 경계를 설정하는 것이다.
    • 이 여유 공간을 설정함으로써 새로운 데이터에 대한 판별의 정확도를 높이며, 일반화 오류를 줄인다.
  • SVM에서는 각 그룹의 중심점이 아닌 두 그룹 사이의 경계에 있는 데이터(support vector)에 초점을 맞춘다.
    • 즉, 흰색과 검은색의 각 그룹의 경계에서 먼저 H1과 H2 선을 긋고, 그 선 안에서 가운데 새로운 선을 그어 최적의 분리 경계면을 정한다.
    • 여기서 H1과 H2를 구하는 방식은 무한히 존재할 수 있다. 다만 두 선분 사이에 학습 데이터가 존재하지 않는다는 점과 두 선분 사이의 거리가 최대가 된다는 제약 조건을 둠으로써 SVM의 분리 경계면이 하나로 정해지도록 한다.
    • 아래 그림에서 선형 판별법은 test 데이터를 검은색으로 분류하지만, SVM은 흰색으로 분류하게 된다.
  • (이하 예제 코드 설명 내용 생략)

OpenCV로 배우는 영상 처리 및 응용/ 영상 처리 응용 사례 Ⅰ

2차원 히스토그램을 이용한 이미지 검색

  • 영상 검색하는 방법은 크게 문자기반 검색방법(Text-Based Image Retrieval, TBIR)과 내용기반 검색 방법(Content-Based Image Retrieval, CBIR)로 구분할 수 있다.
    • 이중 내용기반 검색방법은 영상의 색상, 질감, 모양과 같은 해당 영상 데이터의 특징을 자동으로 추출하여 검색에 이용한다.

2차원 히스토그램

  • 크기가 다른 영상들에서 히스토그램을 계산하면 영상간의 히스토그램 빈도값이 동등하지 않게 된다. 즉, 크기가 큰 영상은 화소의 개수가 많아서 각 빈도값이 커지므로 영상간의 동등한 비교를 위해 계산된 히스토그램을 0~1 사이의 값으로 정규화한다.
  • 아래 그림과 같이 2차원 히스토그램의 빈도 데이터는 색상(hue)와 채도(saturation)를 축으로 2차원의 좌표를 구성하고, 각각의 좌표에 색상과 채도를 연관지어 빈도가 계산된다.
  • 이와 같은 2차원 히스토그램을 그림으로 표현하면, 2차원 행렬에서 x 좌표는 채도를 y 좌표는 색상을 나타내도록 하고, 빈도값을 그 좌표에서의 밝기로 지정할 수 있을 것이다.

유사도 비교

  • OpenCV에서 두 영상의 히스토그램이 얼마나 비슷한지를 비교해주는 함수가 cv::compareHist()이다.
    • 비교 방식 옵션 중에서 CV_COMP_CORREL를 지정하면 다음의 수식으로 계산된 값을 반환한다. 이 수식은 상호상관 함수의 수식을 응용한 것으로서 두 히스토그램의 비슷한 정도를 나타낸다.

d(H_{1}, H_{2}) = {\sum_{i}(H_{1}(i) - \bar{H}_{1}) (H_{2}(i) - \bar{H}_{2}) \over \sqrt{\sum_{i} (H_{1}(i) - \bar{H}_{1})^{2} \cdot (H_{2}(i) - \bar{H}_{2})^{2}}}

하르 분류기를 이용한 얼굴검출 및 성별 분류

하르 기반 분류기

  • 2001년 Viola와 Jones는 객체 검출 분야에서 가장 대표적인 방법론으로 인정받는 논문 “Rapid Object Detection Using a Boosted Cascade of Simple Features”를 발표했다.
    • 이 논문에서 얼굴과 얼굴이 아닌 것의 차이를 효율적으로 보여줄 수 있는 하르 유사 특징(Haar-like features)을 이용한 방법을 제안하였다.
    • 하르 유사 특징은 하르 웨이브릿(Haar wavelet)과 유사하기 때문에 붙여진 이름으로서 아래 그림과 같이 위치, 모양, 크기에 따라 다양한 형태로 구성되어 있다. 그 특징값은 흰색 영역의 화소값의 합과 검음색 직사각형 영역의 화소값의 합의 차로 정의된다.
  • 하르 기반 캐스케이드 분류기는 하르 유사 특징과 같은 매우 단순한 특징들을 조합하여 객체를 찾아낸다. 캐스케이드(cascade)란 용어에서 알 수 있듯, 여러 개의 검출기를 순차적으로 사용한다.
    • 처음에 간단한 검출기를 적용하고, 진행할 수록 복잡한 검출기를 적용한다.
    • 따라서 단순 검출기를 통과한 후보에만 시간이 많이 걸리는 강력한 검출기가 적용되기 때문에 검출 속도를 크게 향상시킬 수 있다.
  • OpenCV의 캐스케이드 분류기는 1,000개 이상의 얼굴 영상과 10,000개 이상의 얼굴이 아닌 영상을 사용하여 학습되었다.
    • 이 과정은 일반적인 멀티코어 CPU를 장착한 컴퓨터에서 최대 1주일 정도의 시간이 소요되는데, 다행히 OpenCV에서 미리 학습된 다양한 검출기를 제공한다.
    • 따라서 필요한 종류의 Cascade 분류기 파일을 로드함녀 검출기를 사용하여 분류할 수 있다. 이를 통해 전면 얼굴, 옆면 얼굴, 눈, 코 등을 검출할 수 있다.
    • OpenCV의 \sources\data\harrcascades\ 폴더에 xml 파일로 된 다양한 하르 기반 검출기가 있다.
  • (이하 예제 코드 설명 내용 생략)

OpenCV로 배우는 영상 처리 및 응용/ 영상 분할 및 특징 처리

허프 변환

  • 직선 검출 방법 중 가장 널리 사용되는 것은 허프 변환(Hough transform)이다.
    • 허프 변환은 영상 내의 선, 원 뿐만 아니라 임의의 형태를 지닌 물체를 감지해 내는 대표적인 기술로서 데이터 손실 및 왜곡이 포함된 영상에서도 직선을 잘 추출한다.

허프 변환의 좌표계

  • 허프 변환은 아래의 수식과 같이 직교 좌표계로 표현되는 영상의 에지 점들을 오른쪽 식의 극좌표계로 옮겨 검출하고자 하는 물체의 파라미터(\rho, \theta )를 추출하는 방법이다.

y = ax + b \leftrightarrow \rho = x \cdot cos \theta + y \cdot sin \theta

  • 직교 좌표계에서는 수직선일 경우 기울기가 무한대가 될 수 있으며, 검출되는 직선의 간격이 동일하지 않아서 검출 속도와 정밀도에서 문제가 될 수 있다.
    • 반면 극좌표계를 사용하면 직선이 원점에서 떨어진 수직 거리(\rho )와 이 수직선이 x축과 이루는 각도(\theta )를 이용해서 직선을 표현할 수 있다.
    • 또한 직선의 수직거리와 각도를 일정한 간격으로 검출함으로써 정밀한 직선 검출이 가능하다.
  • 직교 좌표계에서 하나의 직선상에 놓인 좌표들을 허프 변환 좌표계로 표시하면 아래 그림과 같다.
    • 직선 A 상에 검출된 3개의 지점 a_{1}, a_{2}, a_{3} 가 있다고 가정하자.
    • 먼저 직선 A는 \rho_{1} = x \cdot cos \theta_{1} + y \cdot sin \theta_{1} 로 표현할 수 있다.
    • 이 수식에 따라 직선 A는 허프 변환 좌표계에서 한 점 (\rho_{1}, \theta_{1} )의 좌표로 나타난다.
    • 또한 영상 좌표에서 3개의 지점(a_{1}, a_{2}, a_{3} )은 허프 변환 좌표(\rho_{1}, \theta_{1} )를 같이 공유한다.
  • 또한 직교 좌표계에서 한 점은 허프 변환 좌표계에서는 곡선으로 표현된다. 
    • 즉, a_{1} 좌표에서 그려질 수 있는 수많은 직선이 존재하며, 많은 (\rho_{i}, \theta_{j} )로 표현되어, 결과적으로 아래 그림의 b에서 a_{1} 곡선을 이룬다.

허프 변환의 전체 과정

  • 허프 변환은 다음과 같은 세부적인 과정을 거쳐서 수행된다.
    • 허프 변환 좌표계에서 행렬 구성
    • 영상 내 모든 화소의 직선 여부 검사
    • 직선 인지 좌표에 대한 허프 변환 누적 행렬 구성
    • 허프 누적 행렬의 지역 최대값 선정
    • 임계값 이상인 누적값(직선) 선별
    • 직선 (\rho_{i}, \theta_{j} )을 누적값 기준으로 내림차순 정렬
  • 먼저 영상의 크기에 맞게 허프 변환 좌표계를 위한 행렬을 구성한다. 이 행렬은 검출된 직선을 좌표(\rho, \theta )로 표시해서 값을 누적하기 위한 것이다.
    • \rho 는 -\rho_{max} 에서 +\rho_{max} 까지의 범위를 가지고, \theta 0 ~ \pi 까지의 범위를 가지며, 다음의 수식으로 계산된다.
    • 여기서 height, width는 원본 영상의 크기이며, acc_h, acc_w는 허프 누적 행렬의 크기이다.

-\rho_{max} \leq \rho \leq \rho_{max}, \rho_{max} = height + width

acc_h = {\rho_{max} \times 2 \over \Delta \rho}

0 \leq \theta \leq \theta_{max}, \theta_{max} = \pi

acc_w = {\pi \over \Delta \theta}

  • 따라서 허프 행렬의 크기는 거리 간격(\Delta \rho )과 각도 간격(\Delta \theta )에 따라 조절되며, 거리 간격과 각도 간격이 작을수록 행렬의 크기가 커져서 해상도가 높아진다.
    • 예컨대 각도 간격을 2도로 하면 가로(\theta ) 방향으로 90개의 원소가 필요하며, 1도 간격으로 하면, 180개의 원소가 필요하다.

허프 누적 행렬 구성

  • 다음으로 영상 내의 모든 좌표에서 직선인지 여부를 점검한다. 허프 변환에 입력되는 영상은 먼저 이진화나 모폴로지와 같은 다양한 전처리를 수행하여 잡음을 제거하고 직선 성분을 검출한다.
    • 일반적으로 캐니 에지 검출과 같이 이진 영상을 허프 변환의 입력 영상으로 사용한다. 따라서 입력 영상에서 0보다 큰 화소를 직선으로 간주한다.
  • 다음의 hough_coord() 함수는 영상 공간에서 직선으로 인지된 좌표(x, y)를 허프 변환 좌표계로 변환하여 곡선을 구성하는 알고리즘 이다.
void hough_coord(Mat image, Mat& acc_mat, double rho, double theta)
{
  int acc_h = (image.rows + image.cols) * 2 / rho;
  int acc_w = CV_PI / theta;
  acc_mat = Mat(acc_h, acc_w, CV_32S, Scalar(0));

  for (int y = 0; y < image.rows; y++)
  {
    for (int x = 0; x < image.cols; x++)
    {
      Point pt(x, y);

      if (image.at<uchar>(pt) > 0)
      {
        for (int t = 0; t < acc_w; t++)
        {
          double radian = t * theta;
          float r = pt.x * cos(radian) + pt.y * sin(radian);
          r = cvRound(r / rho + acc_mat.rows / 2);
          acc_mat.at<int>(r, t)++;
        }
      }
    }
  }
}
  • 입력 영상의 좌표를 조회하는 2중 반복문 내에서 화소값이 0보다 크면 직선으로 인지한다. 
    • 직선으로 인지되면, 다시 0 ~ acc_w 까지의 반복문을 구성하고, 각도 간격(\Delta \theta )만큼 증가한다. 즉, 0~180도까지 증가한다.
    • 이 과정에서 입력된 좌표(pt)의 x, y와 각도(\theta_{t} )를 허프 변환 수식에 대입해서 \rho_{t} 를 계산한다.
    • 거리와 각도를 좌표로 사용해 허프 누적행렬의 해당 원소값을 1 증가한다.
  • 결과적으로 직선으로 인지된 입력 영상의 각 좌표들에 대해 0~180도로 회전하며 허프 변환 수식에 따라 거리와 각도로 좌표를 계산하고, 이 좌표에 해당하는 허프 누적행렬의 원소에 값을 누적한다.
  • 아래 그림은 3개의 좌표가 있는 에지 영상으로 허프 누적행렬을 구성한 예이다.
    • 영상에서 3개 좌표에 대해 허프 누적행렬은 3개의 곡선이 구성된다. 3개의 곡선이 겹치는 좌표 (\rho_{t1}, \theta_{t1} )에서 누적값이 3으로 가장 많다. 또한 이 \rho_{t1}, \theta_{t1} 좌표는 영상의 3점을 잇는 직선이 된다.
    • 따라서 허프 누적행렬에서 원소의 값이 특정값 이상인 좌표들을 검색하면, 특정값 이상인 길이를 갖는 직선들을 \rho_{t}, \theta_{t} 좌표로 찾을 수 있다.
    • 또한 이 좌표로 허프 변환 수식을 다음과 같이 변경하여 직선을 구성할 수 있다.

\rho = x \cdot cos \theta + y \cdot sin \theta \to y = - {cos \theta \over sin \theta} x + {\rho \over sin \theta}

허프 누적 행렬의 지역 최대값 선정

  • 계산된 누적 행렬에서 좌표들을 반환받으면 위 그림과 같이 한 곳에 비슷한 직선이 여러 개 선택되어 반환될 수 있다. 이 경우 정확한 하나의 직선을 찾지 못하게 된다.
    • 또한 비슷한 위치, 비슷한 각도에서 여러 개의 직선이 검출되어서 다른 지점에 있는 직선들을 검출하지 못할 수도 있다.
  • 이 문제를 해결하는 방법으로 마스크를 이용한 방법이 있다. 허프 누적행렬을 작은 블록으로 나누고 각 블록에서 가장 큰 값만을 유지시키고 나머지 값은 제거하는 것이다.
    • 여기서 나누어진 블록을 마스크(mask)라 부르며, 마스크 내에서 가장 큰 값을 지역 최대값 (local maximum)이라 한다.
  • 아래 그림은 허프 누적행렬에 3 x 7 크기의 마스크를 적용하여 지역 최대값을 구한 예이다. 두 개의 점선 사각형으로 마스크를 표시해서 예시를 나타내었다.
    • 누적행렬 좌상단의 예시 1은 17이 가장 큰 값으로 유지되고, 마스크 주위의 나머지 값은 0으로 제거된다. 같은 방식으로 우하단의 예시 2는 11이 가장 큰 값으로 주위의 9, 10이 제거된다.
    • 이렇게 함으로써 비슷한 위치의 직선을 하나만 검출해서 직선 검출의 정확도를 높일 수 있다..
  • 다음은 허프 누적행렬에서 마스크 내의 지역 최대값만을 유지시키는 함수 acc_mask()를 구현한 것이다.
void acc_mask(Mat acc_mat, Mat& acc_dst, Size size, int thresh)
{
  acc_dst = Mat(acc_mat.size(), CV_32S, Scalar(0));
  Point h_m = size / 2;

  for (int r = h_m.y; r < acc_mat.rows - h_m.y; r++)
  {
    for (int t = h_m.x; t < acc_mat.cols - h_m.x; t++)
    {
      Point center = Point(t, r) - h_m;
      int c_value = acc_mat.at<int>(center);

      if (c_value >= thresh)
      {
        double maxVal = 0;

        for (int u = 0; u < size.height; u++)
        {
          for (int v = 0; v < size.width; v++)
          {
            Point start = center + Point(v, u);

            if (start != center && acc_mat.at<int>(start) > maxVal)
            {
              maxVal = acc_mat.at<int>(start);
            }
          }
        }

        Rect rect(center, size);

        if (c_value > maxVal)
        {
          acc_dst.at<int>(center) = c_value;
          acc_mat(rect).setTo(0);
        }
      }
    }
  }
}

임계값 이상인 누적값(직선) 선별

  • 이제 직선들을 가져와 보자. 다음의 thres_lines() 함수는 중복이 제거된 누적행렬의 원소 중에서 임계값보다 큰 값을 선별하여 직선들을 lines 행렬에 저장한다.
    • 여기서 하나의 직선(line)은 3개의 원소를 갖는 Matx13f 형으로 선언하여 간편하게 초기화 한다.
    • 누적행렬의 좌표(r, t)를 수직거리(rho)와 각도(radian)로 계산하여 두 개의 원소를 구성하고, 세 번째 원소로 누적값(value)을 지정한다.
void thres_lines(Mat acc_dst, Mat& lines, double _rho, double theta, int thresh)
{
  for (int r = 0; r < acc_dst.rows; r++)
  {
    for (int t = 0; t < acc_dst.cols; t++)
    {
      float value = (float)acc_dst.at<int>(r, t);

      if (value >= thresh)
      {
        float rho = (float)((r - acc_dst.rows / 2) * _rho);
        float radian = (float)(t * theta);
        Matx13f line(rho, radian, value);
        lines.push_back((Mat)line);
      }     
    }
  }
}

직선을 누적값 기준으로 내림차순 정렬

  • 다음은 선별된 직선들 중에서 긴 직선이 먼저 선택되도록 하기 위해 누적값이 큰 직선부터 저장해야 한다.
    • 이것은 직선들(line)을 누적값 기준으로 내림차순 정렬함으로써 쉽게 구현할 수 있다.
    • 정렬 알고리즘은 직접 구현하지 않고 OpenCV에서 제공하는 cv::sortIdx() 함수를 이용하여 다음의 sort_lines() 함수로 구현한다.
    • cv:sortIdx() 함수는 행렬 원소를 정렬하고, 정렬 원소의 원본 인덱스를 반환한다.
void sort_lines(Mat lines, vector<Vec2f>& s_lines)
{
  Mat acc = lines.col(2), idx;
  sortIdx(acc, idx, SORT_EVERY_COLUMN + SORT_DESCENDING);

  for (int i = 0; i < idx.rows; i++)
  {
    int id = idx.at<int>(i);
    float rho = lines.at<float>(id, 0);
    float radian = lines.at<float>(id, 1);
    s_lines.push_back(Vec2f(rho, radian));
  }
}
  • 앞서 thres_lines() 함수에서 반환받은 직선들(lines)은 수직거리, 각도, 누적값이 저장된 행렬이다.
    • 이 행렬에서 2번열(0번 열부터 시작)이 누적값들이기 때문에 Mat::col() 함수로 2번 열만 가져와 acc 행렬에 저장한다.
    • 그리고 cv::sortIdx() 함수로 acc 행렬을 행단위 내림차순(SORT_EVERY_COLUMN + SORT_DESCENDING) 정렬을 한다. 그러면 두 번째 인수(idx)로 정렬값에 대한 원본 인덱스가 반환되기 때문에 idx에 내림차순으로 정렬된 원본 인덱스가 저장된다.
  • idx 행렬의 전체 원소를 조회해서 원본 인덱스를 id에 저장한다. 그리고 원본 인덱스(id)로 lines 행렬(직선들)에서 한 원소를 가져온다. 여기서 lines 행렬에서 0번 열이 수직거리(rho)이며, 1번 열이 각도(radian)이다.
    • 그리고 수직거리와 각도를 Vec2f 형으로 선언해서 s_lines 벡터에 추가한다. 그러면 s_lines 벡터에 누적값에 따라서 내림차순으로 정렬된 직선 좌표가 저장된다.

코너 검출

  • 영상 내에는 에지나 직선과 같은 다양한 특징 정보들이 있다. 그러나 직선 정보는 영상 구조 파악 및 객체 검출에는 도움이 되지만, 영상 매칭에는 큰 도움이 되지 않는다.
    • 또한 에지는 강도와 방향 정보만 가지므로 영상 매칭을 하기엔 정보가 부족하다.
  • 에지나 직선처럼 영상 처리에 중요한 특징 정보로 사용되는 꼭지점 혹은 코너라 부르는 특징점이 있다. 이 코는 아래 그림과 같이 영상에서 경계가 만나는 지점의 특정한 모양을 갖는 곳을 가리킨다.
    • 이 코너 정보들 중에서 영상의 왜곡에도 불변하는 특징을 가진 지점들이 영상 매칭에 유용하게 사용될 수 있다. 여기서는 코너 검출기 중 하나인 해리스(Harris) 코너 검출기의 원리에 대해 알아볼 것이다.
  • 코너 점은 위 그림과 같이 모든 방향에서 영상의 밝기 변화가 커야 한다.  A, C 지점은 모든 방향으로 밝기의 변화가 크다. 그러나 B, D의 경우는 모든 방향으로 밝기 변화가 거의 없다.
    • 이 아이디어는 모라벡(moravec)에 의해서 다음 식과 같이 영상 변화량(SSD: Sum of Squared Difference)으로 정리된다. 여기서 w(x, y)는 지정된 크기의 윈도우로서 해당 범위만 1의 값을 갖는 마스크이다.

E(u, v) = \sum_{y} \sum_{x} w(x, y) \cdot (I(x+u, y+v) - I(x, y))^{2}

  • 영상 변화량(SSD)은 현재 화소에서 u, v 방향으로 이동했을 때의 밝기 변화량의 제곱을 합한 것이기 때문에, 모든 방향에서 밝기 변화가 커야 큰 값을 갖게 된다.
    • 모라벡은 구현을 위해서 (u, v)를 (1, 0), (1, 1), (0, 1), (-1, 1)의 4개 방향으로 한정시켜서 SSD를 구하고, 그 중에서 최소값을 해당 픽셀의 영상 변화량으로 지정해서 ‘특징 가능성’ 값으로 결정한다.
  • 모라벡 알고리즘은 마스크가 0과 1의 값만 갖는 이진 윈도우를 사용하여 노이즈에 취약하다. 또한 4개 방향으로 한정 시켰기 때문에 45도 간격의 에지만 고려할 수 있다.
    • 이런 문제를 해결하여 좀 더 정밀한 코너 검출을 위해 해리스는 다음과 같은 개선 방안을 제시했다.
    • 먼저 이진 윈도우인 w(u, v) 대신 점진적으로 변화하는 가우시안 마스크 G(x, y)를 적용하여 다음과 같은 수식으로 확장한다. 이를 통해 노이즈에 대한 민감도를 낮춘다.

E(u, v) = \sum_{y} \sum_{x} G(x, y) \cdot (I(x+u, y+v) - I(x, y))^{2}

  • 또한 4개 방향에 대해서만 코너를 검출하는 것을 모든 방향에서 검출할 수 있도록 미분을 도입하여 다음과 같이 변경한다.

I(x+u, y+v) \cong I(x, y) + vd_{y}(x, y) + ud_{x} (x, y) \\ E(u, v) \cong \sum_{y} \sum_{x} G(x, y) \cdot (vd_{y}(x, y) + ud_{x}(x, y))^{2}

  • 여기서 특징 가능성을 직접 계산하는 대신 위의 수식을 다음과 같이 행렬 M의 식으로 정리한다.

E(u, v) \cong \sum_{y} \sum_{x} G(x, y) \cdot (vd_{y}(x, y) + ud_{x}(x, y))^{2} \\ = \sum_{y} \sum_{x} G(x, y) \cdot (v^{2}d_{y}^{2} + u^{2} d_{x}^{2} + 2vud_{x}d_{y}) \\ = \sum_{y} \sum_{x} G(x, y) \cdot (u, v) \left( \begin{array}{rr} d_{x}^{2} & d_{x} d_{y} \\ d_{x} d_{y} & d_{y} \end{array} \right) \left( \begin{array}{r} u \\ v \end{array} \right) \\ = (u v) M \left( \begin{array}{r} u \\ v \end{array} \right) \\ M = \sum_{y} \sum_{x} G(x, y)  \left( \begin{array}{rr} d_{x}^{2} & d_{x} d_{y} \\ d_{x} d_{y} & d_{y} \end{array} \right)

  • 모라벡 알고리즘은 (u, v)를 변화시켜 E(u, v) 맵을 구해서 코너 여부를 판단하는 반면, 해리스 알고리즘은 위 수식의 행렬 M에서 고유벡터를 구하면 경계선 방향에 수직인 벡터 두 개를 얻을 수 있다.
    • 이 행렬 M의 고유값 (\lambda_{1}, \lambda_{2} )으로 다음의 식과 같이 코너 응답 함수(corner response function)를 계산한다. 여기서 k는 상수값으로 일반적으로 0.04~0.06 정도가 적당하다.

R = \lambda_{1}, \lambda_{2} - k \cdot (\lambda_{1} + \lambda_{2})^{2}

  • 다만 고유값을 계산하려면 고유값 분해의 복잡한 과정을 거쳐야 하기 때문에 해리스 검출기에서는 직접 고유값을 구하지 않고 다음과 같이 행렬식(det)과 대각합(trace)를 통해서 코너 응답 함수로 사용한다.

M = \left( \begin{array}{rr} d_{x}^{2} & d_{x} d_{y} \\ d_{x} d_{y} & d_{y} \end{array} \right) = \left( \begin{array}{rr} a & c \\ c & b \end{array} \right)

R = det(M) - k \cdot trace(M)^{2} = (ab - c^{2}) - k \cdot (a + b)^{2}

  • 해리스 코너 검출 방법은 영상의 평행이동, 회전 변환에는 불변(invariant)하는 특징이 있고, 어파인(affine) 변환이나 조명(illumination) 변화에도 어느 정도 강인성이 있다. 하지만 영상의 크기(scale) 변화에는 영향을 받는다.
  • 실제 해리스 코너 검출기를 구현하려면 다음과 같은 과정을 거친다.
    • 소벨 마스크로 미분 행렬 계산 (dx, dy )
    • 미분 행렬의 곱 계산 (dx^{2}, dy^{2}, dxy )
    • 곱 행렬에 가우시안 마스크 적용
    • 코너 응답합수 C = det(M) - k \cdot trace(M)^{2} 계산
    • 비최대치 억제

k-최근접 이웃 분류기

k-최근접 이웃 분류기의 이해

  • 최근접 이웃 알고리즘은 기존에 가지고 있는 데이터들을 일정한 규칙에 의해 분류된 상태에서 새로운 입력 데이터의 종류를 예측하는 분류 알고리즘이다.
    • 이 방법은 기존의 학습된 여러 클래스의 샘플들을 각각 좌표로 표시하여 두고, 새로운 미지으 샘플이 입력될 때, 학습 클래스의 샘플들과 새 샘플의 거리가 가장 가까운(nearest) 클래스로 분류한다.
    • 여기서 ‘가장 가까운 거리’는 미지의 샘플과 학습 클래스 샘플간의 유사도가 가장 높은 것을 의미한다. 이 거리 측정 방법은 샘플의 형태와 종류에 따라 다양한데, 대표적으로 유클리드 거리(euclidean distance), 해밍 거리(hamming distance), 차분 절대값 등을 이용한다.
  • 최근접 이웃 방법 중에서 가장 많이 사용되는 것은 학습된 클래스들에서 여러 개(k개)의 이웃을 선출하고 이를 이용하여 미지의 샘플들을 분류하는 방법이다.
    • 즉, 미지의 샘플과 가까운 이웃으로 선출된 여러 개의 클래스 샘플들 중에서 가장 많은 수를 가진 클래스로 미지의 샘플들을 분류하는 방법이다. 이러한 분류 과정을 k-최근접 이웃 분류(k-Nearest Neighbors: k-NN)라고 한다.

k-NN 응용

  • 숫자 영상에서 숫자를 인식하는 예제. 아래 5단계를 따른다.
    • 학습 영상의 생성 
    • 학습 영상에서 셀 영상 분리
    • 숫자 객체 위치 검색 및 분리
    • 숫자 객체 중앙 배치 및 크기 정규화
    • 최종 예제 프로그램
    • (위 내용 중 3, 4 번째 항목만 내용 정리)

숫자 객체 위치 검색 및 분리

  • 각 셀에서 숫자 객체가 동일한 위치에 놓여야 학습의 효과와 분류의 정확도가 높아진다. 따라서 아래 그림과 같이 숫자객체를 셀 영역의 중앙에 위치시킨다.
    • 이를 위해 먼저 숫자객체의 위치를 바르게 찾고, 숫자객체를 분리해야 한다.
  • 숫자 객체의 위치 인식은 아래 그림과 같이 투영(projection) 히스토그램을 활용한다.
    • 투영은 다음 식과 같이 영상의 화소값들을 수직 혹은 수평으로 합산하여 나타내는 히스토그램이다.
    • OpenCV에서 제공하는 cv:reduce() 함수가 행렬 원소를 가로방향 혹은 세로 방향을 감축하기 때문에 투영의 수식과 일치한다. 따라서 이 함수를 이용해서 투영을 구현한다.

hsto_v(x) = \sum_{y=0}^{h-1} f(x, y) \\ hsto_v(y) = \sum_{x=0}^{w-1} f(x, y)

숫자 객체 중앙 배치 및 크기 정규화

  • 다음은 숫자객체를 셀 영역의 중앙에 위치시켜야 한다.
    • 먼저 아래 그림의 오른쪽 그림에서 숫자객체의 가로와 세로 길이 중 긴것을 택하여 그 크기(big)로 정방영상(square)을 만든다.
    • 다음으로 정방영상과 숫자객체의 크기 차이(dx, dy)를 계산한다.
    • 이것은 두 행렬(square, number) 크기의 차분으로 구할 수 있다.
    • 이때 중앙사각형의 시작위치(start)는 이 차분을 좌우, 상하로 양분해야 하기 때문에 2로 나누어준다.
    • 다음으로 시작위치(start)에서 숫자객체 크기로 사각형을 만들면 정방영상의 중앙을 가리키는 중앙사각형(middle)을 만들 수 있다.
    • 그리고 정방영상에서 중앙사각형을 관심영역으로 참조한 후 이 영역에 숫자객체(number)를 복사하면 숫자 객체가 셀 영역의 중앙에 배치된다.

영상 워핑과 영상 모핑

  • 영상 워핑은 하나의 영상에서 비선형적인 특정한 규칙에 따라 입력 영상을 재추출(resampling)하여 영사으이 형태를 변형시키는 기술이다.
    • 이 기술은 나사(NASA)에서 인공위성이나 우주선으로부터 전송된 영상이 렌즈의 변형이나 신호의 왜곡 등으로 인해 일그러지는 경우가 많아서 이를 복원하는 용도로 처음 사용되었다.
    • 영상을 여러 다른 방향으로 늘이거나 크기를 조절하는 기법으로 순수한 스케일링과 달리 크기 변화의 정도가 영상 전체에 대해 균일하지 않은 것이 특징이다.
    • 특히 고무판 위에 여상이 있는 것과 같이 임의의 형태로 구부리는 것과 같은 효과를 낸다는 의미에서 고무 시트 변환(Rubber Sheet Transform)이라고도 한다.
    • 영상 워핑 기술은 렌즈 왜곡 보정, 스테레오 영상 정합, 파노라마 영상 합성 등에 사용될 수 있다.
  • 영상 모핑은 조지 루카스가 설립한 특수 효과 전문회사인 ILM이 개발한 기법으로 변형(metamorphosis)란 단어에서 유래되었다.
    • 이것은 하나의 영상에서 형체가 전혀 다른 영상으로 변하도록 하는 기법을 말한다. 즉, 아래 그림과 같이 두 개의 서로 다른 영상 사이의 변화하는 과정을 서서히 나타내는 것이다.
    • 워핑의 기법을 이용해서 모핑을 수행할 수 있다.
  • 영상 워핑의 종류와 방법은 다양하게 있다. 다음의 규칙에 따라 원본 영상의 좌표를 목적 영상의 좌표로 재배치하면 워핑 영상이 완성 된다.

x' = x + ratio \cdot (pt2.x - pt1.x) \\ y' = y 

ratio \Rightarrow x < pt1.x \Rightarrow {x \over pt1.x},  x > pt1.x \Rightarrow {width - x \over width = pt1.x}

  • 일반적으로 카메라로 찍은 영상은 여러 이유에 의해 왜곡된다. 여기서 왜곡되는 요인을 카메라 외부 파라미터와 내부 파라미터로 구분할 수 있다.
    • 카메라는 3차원인 세상의 실세계 영상을 2차원의 평면 영상으로 맺히게 하기 때문에 기하학적인 왜곡이 발생하게 된다. 이것은 카메라 외부 파라미터에 의한 왜곡에 해당하며, 대표적으로 원근 투시 왜곡이 있다.
    • 또한 캡쳐된 영상은 렌즈, 초점거리, 렌즈와 이미지 센서가 이루는 각 등과 같은 카메라 내부의 기구적인 부분에 의해 상당한 영향을 받는데, 이러한 요인을 내부 파라미터 요인이라 한다.
  • 영상 좌표로부터 실세계의 3차원 좌표를 계산하거나 실세계의 3차원 좌표를 영상에 투영된 위치로 계산해야 하는 경우가 있다. 이때 카메라 내부 요인을 제거해야만 보다 정확한 좌표의 계산이 가능하다.
    • 여기서 내부 요인의 파라미터 값을 구하는 과정을 카메라 캘리브레이션(camera calibration)이라 한다.
    • 카메라 영상은 3차원 공간상의 점들을 2차원 영상 평면에 투영함으로써 얻어지는데, 핀홀(pinhole) 카메라 모델에서 이러한 변환 관계는 다음과 같이 모델링 한다.

s \left( \begin{array}{rrr} u \\ v \\ 1 \end{array} \right) = A \cdot M \cdot \left[ \begin{array}{rrrr} X \\ Y \\ Z \\ 1 \end{array} \right] \\ = \left[ \begin{array}{rrr} f_{x} & 0 & C_{x} \\ 0 & f_{y} & C_{y} \\ 0 & 0 & 1  \end{array} \right] \cdot \left[ \begin{array}{rrrr} r_{11} & r_{12} & r_{13} & t_{1} \\ r_{21} & r_{22} & r_{23} & t_{2} \\ r_{31} & r_{32} & r_{33} & t_{3} \end{array} \right] \cdot \left[ \begin{array}{rrrr} X \\ Y \\ Z \\ 1 \end{array} \right]

  • 여기서 X, Y, Z는 실세계의 3차원 좌표이며, u, v는 2차원 영상 평면에 투영된 좌표이다.
    • A는 카메라 내부 파라미터(intrinsic parameters)이고, M은 카메라 외부 파라미터이다.
    • 카메라 내부 파라미터의 요인으로는 초점거리(focal length)와 주점(principal point) 등이 있다.
  • 초점거리(f_{x}, f_{y} )는 아래 그림에서와 같이 렌즈에서 이미지 센서까지의 거리를 말한다.
    • 보통의 디지털 카메라에서는 mm 단위로 표현되지만 카메라 모델에서는 픽셀(pixel) 단위로 표현된다.
    • 초점거리가 두 개의 값으로 표현되는 이유는 이미지 센서의 가로 방향과 세로 방향의 셀 간격이 다를 수 있기 때문이다.
  • 주점(C_{x}, C_{y} )은 카메라 렌즈의 중심에서 이미지 센서에 내린 수선의 영상 좌표로서 일반적으로 말하는 영상 중심점과는 다른 의미이다.
    • 예컨대 카메라 조립과정에서 오차로 인해 렌즈와 이미지 센서가 수평이 어긋나면 주점과 영상중심은 다른 값을 가질 수 있다.

OpenCV로 배우는 영상 처리 및 응용/ 변환영역 처리

  • 영상을 데이터로 표현하는 것은 크게 두 가지 영역으로 나뉘는데, 앞 장까지 했던 화소값이 직접 표현된 공간영역(spatial domain)과 다른 하나가 우주 공간과 같은 변환영역(transform domain)
  • 변환영역은 직교변환에 의해 얻어진 영상 데이터의 다른 표현이다.
    • 여기서는 화소값이 직접 표현되는 것이 아니고 변환계수(coeffcient)로 표현된다.
    • 대표적인 변환은 DCT(Discrete Cosine Transform)와 DFT(Discrete Fourier Transform)이 있는데, 그 중 오래되었고 잘 알려진 것이 DFT인 이산 푸리에 변환이다.
    • 푸리에 변환은 시간(혹은 공간) 영역에서 주파수 영역으로의 변환으로 ‘모든 파형은 단순한 정형파의 합으로 합성되어 질 수 있다’라는 개념에 기초한 해석학적인 방법

공간 주파수의 이해

  • 일반적으로 주파수라는 말은 아래 그림과 같이 1초 동안 진동하는 횟수로 정의한다. 
    • 라디오 방송 채널이나 휴대폰의 통신 대역에서 사용하는 헤르츠(Hz)는 주파수를 표현하는 단위이다.
  • 그러나 이것은 전파라는 신호에 국한된 표현이라 할 수 있다. 아날로그 신호를 디지털화 하는 과정에서 시간단위로 샘플링하는 횟수를 지정할 때에 Hz는 단위와 함께 샘플링 주파수라는 표현을 사용한다.
    • 또한 우리가 공부하는 영상처리에서도 공간 주파수(spatial frequency)라는 개념을 사용한다.

  • 따라서 좀 더 확장된 의미에서 주파수는 이벤트가 주기적으로 발생하는 빈도라고 할 수 있다.
  • 영상에서 화소 밝기의 변화의 정도를 파형의 형태로 그려보면 아래 그림과 같이 표현할 수 있다. 신호의 주파수와 같은 의미가 되는 것이다.
  • 이렇게 확장된 의미를 영상 신호에 적용하면 영상에서의 주파수는 공간상에서 화소 밝기의 변화율이라 할 수 있다.
    • 이런 의미에서 공간 주파수라는 표현을 사용한다.
    • 공간 주파수는 밝기가 얼마나 빨리 변화하는가에 따라 고주파 영역과 저주파 영역으로 분류한다.
  • 아래 그림은 고주파 포함 영역과 저주파 포함 영역을 설명한다.
    • 상단 부분을 보면 화소 밝기가 거의 변화가 없거나 점진적으로 변화하는 것을 볼 수 있는데 이런 부분은 대부분 저주파 성분을 가진 저주파 공간 영역이라 한다.
    • 반면 하단 부분은 화소의 밝기가 급변하는 것을 볼 수 있는데, 이런 부분은 변화가 거의 없는 저주파 성분 위에 변화가 심한 고주파 성분이 포함되어 있는 고주파 공간 영역이라 할 수 있다.
  • 저주파 공간 영역은 보통 영상에서 배경 부분이나 객체의 내부에 많이 있으며, 고주파 공간 영역은 경계부분이나 객체의 모서리 부분에 많이 있다.

  • 변환을 통하여 영상을 주파수 영역별로 분리할 수 있으면, 각 주파수 영역별 처리가 가능할 것이다.
    • 예컨대 경계부분에 많은 고주파 성분을 제거하여 영상을 생성하면 경계가 흐려진 영상을 만들 수 있고, 저주파 성분을 제거하고 고주파 성분만을 취하여 영상을 만든다면 경계나 모서리만 포함하는 영상 즉, 에지 영상이 만들어질 것이다.
  • 일반적인 영상은 공간 영역상에서 저주파 성분과 고주파 성분이 혼합하여 있기 때문에 저주파 영역과 고주파 영역을 분리해서 선별적으로 처리하기란 쉬운 일이 아니다. 따라서 변환영역의 처리가 필요하다.
  • 변환영역, 즉 주파수 영역에서의 영상처리는 아래 그림과 같은 과정을 거친다.
    • 먼저 영상이 입력되면 주파수 영역으로 변환하며,
    • 주파수 변환으로 얻어진 계수의 특정 주파수 영역에 원하는 영상 처리를 적용한다.
    • 마지막으로 처리가 적용된 후에는 다시 주파수 역변환을 통해 공간영역의 영상으로 변환해서 출력 영상을 생성한다.

이산 푸리에 변환

  • 푸리에 변환은 신호나 영상을 주파수 영역으로 변환하는 가장 일반적인 방법으로 다음의 전제를 기본으로 한다.
    • 주기를 가진 신호는 정현파/여현파의 합으로 표현할 수 있다.
  • 여기서 정현파/여현파는 모든 파형 중에 가장 순수한 파형을 말하는 것으로 사인(sin), 코사인(cos) 함수로 된 신호를 말한다. 즉, 사인 또는 코사인 함수의 선형 조합으로 특정 주기의 신호를 구성할 수 있다.
  • 이 전제를 바꾸어 말하면 아래 그림과 같이 주기를 갖는 신호 g(t) 는 여러 개의 사인 및 코사인 함수 g_{f}(t) 로 분리되는 것이다.
    • 여기서 분리된 신호 g_{1}(t), g_{2}(t), g_{3}(t) 는 기저 함수(basis function)가 되며, 기저 함수에 곱해지는 값 0.3, 0.7, -0.5가 주파수의 계수가 된다.
  • 이 계수가 각 주파수 성분의 크기에 해당하며, 신호를 주파수로 변환하는 것은 각 주파수의 기저 함수들에 대한 계수를 찾는 것이다. 또한 주파수 영역에서의 역변환은 각 기저함수와 그 계수들로부터 원본 신호를 재구성하는 것이다.
  • 주파수 변환을 수식으로 표현하면 다음과 같다. 여기서 g_{f}(t) f 주파수에 대한 기저함수이며 G(t) 는 각 기저함수의 계수이다.

g(t) = \int_{-\infty}^{\infty} G(f) \cdot g_{f}(t)df

  • 연속신호에 대한 변환이기 떄문에 -무한대에서 +무한대까지를 적분하여 모든 주파수에 대한 기저함수와 그 계숟르의 선형 조합이 된다.
    • 따라서 연속신호를 주파수 영역으로의 변환은 존재하는 모든 기저함수에 대해 그 계수인 G(t) 를 구하는 것이다.
  • 기저 함수를 어떻게 정하는가에 따라 주파수 영역 변환의 방법이 달라진다.
    • 일반적으로 다음과 같이 사인이나 코사인 함수를 기저함수로 사용한다.이것은 가장 대표적인 방법인 푸리에 변환에서 사용하는 기저함수이다.
    • 또한 푸리에 변환에서는 허수를 이용한 함수를 기저함수로 사용한다.

g_{f}(t) = cos(2 \pi ft) + j \cdot sin(2 \pi ft) = e^{j 2 \pi ft}

  • 이 기저함수로 원본 신호를 나타내면 다음과 같아. 이것은 원본 신호를 만드는 것이기에 푸리에 역변환에 대한 수식이 된다.

g(t) = \int_{-\infty}^{\infty} G(f) \cdot e^{j 2 \pi ft} df

  • 원본 신호로부터 주파수의 계수 G(f) 를 얻는 식은 다음과 같이 유도된다. 이것은 1차원의 연속 신호에 대한 푸리에 변환이다.

G(f) = \int_{-\infty}^{\infty} g(t) \cdot e^{-j 2 \pi ft} dt

  • 이를 디지털 신호에 적용하려면 이산 푸리에 변환(DFT)을 사용해야 한다. 다음은 이산 푸리에 변환과 그 역변환의 수식이다.

G(k) = \sum_{n=0}^{N-1} g[n] \cdot e^{-j 2 \pi k {n \over N}}, (k = 0, ... N-1)

g[n] = {1 \over N} \sum_{n=0}^{N-1} G[k] \cdot e^{j 2 \pi k {n \over N}}, (n = 0, ... N-1)

  • 여기서 g[n] 은 디지털 신호이며, G(k) 는 주파수 k 에 대한 푸리에 변환 계수이다. 
    • 또한 k, n 은 신호의 원소개수(N)만큼 정수로 주어진다.
    • 연속 신호에서 적분기호가 이산신호에서는 합기호로 바뀐다.
  • 2차원 공간상의 영상에 이산 푸리에 변환을 적용하려면 다음 수식과 같이 1차원 이산 푸리에 변환을 가로방향과 세로방향으로 연속해서 두 번 적용해야 한다. 
    • 수식에서 괄호부분이 가로방향에 대한 1차원 푸리에 변환이다.

G(k, l) = \sum_{m=0}^{M-1} (\sum_{n=0}^{N-1} g[n, m] \cdot e^{-j 2 \pi k {n \over N}}) \cdot e^{-j 2 \pi t {m \over M}} \\ = \sum_{m=0}^{M-1} \sum_{n=0}^{N-1} g[n, m] \cdot e^{-j 2 \pi ({kn \over N} + {lm \over M})}

  • 다음 수식은 2차원 이산 푸리에 역변환에 대한 수식이다.

g[n, m] = {1 \over NM} \cdot \sum_{m=0}^{M-1} (\sum_{n=0}^{N-1} G(k, l) \cdot e^{j 2 \pi k {n \over N}}) \cdot e^{j 2 \pi l {m \over M}} \\ = {1 \over NM} \sum_{m=0}^{M-1} \sum_{n=0}^{N-1} G(k, l) \cdot e^{j 2 \pi ({kn \over N} + {lm \over M})}

  • 푸리에 변환의 기저함수에 허수를 이용했기 때문에 실수부와 함께 허수부에 대한 고려도 해야 한다.
    • 다음은 기저함수를 사인과 코사인 함수로 변경하여 1차원 푸리에 변환을 수식으로 나타내면 다음과 같다.

G(k) = \sum_{n=0}^{N-1} g[n] \cdot (cos(-2 \pi k {n \over N}) + j \cdot sin(-2 \pi k {n \over N}))

  • 그리고 실수부와 허수부를 구분하여 표현하면 다음과 같다. 푸리에 변환과 그 역변환이 사인과 코사인 함수에서 각도의 부호만 반대이다.

G(k)_{Re} = \sum_{n=0}^{N-1} g[n]_{Re} \cdot cos(-2 \pi k {n \over N}) - g[n]_{Im} \cdot sin(-2 \pi k {n \over N})

G(k)_{Im} = \sum_{n=0}^{N-1} g[n]_{Im} \cdot cos(-2 \pi k {n \over N}) + g[n]_{Re} \cdot sin(-2 \pi k {n \over N})

g[n]_{Re} = {1 \over N} \sum_{n=0}^{N-1} G(k)_{Re} \cdot cos(2 \pi k {n \over N}) - G(k)_{Im} \cdot sin(2 \pi k {n \over N})

g[n]_{Im} = {1 \over N} \sum_{n=0}^{N-1} G(k)_{Im} \cdot cos(2 \pi k {n \over N}) + G(k)_{Re} \cdot sin(2 \pi k {n \over N})

  • 푸리에 변환을 수행하면 복소수의 행렬이 결과로 생성된다. 이것을 영상으로 확인하기 위해서는 복수부의 실수부와 허수부를 벡터로 간주하여 다음의 수식과 같이 벡터의 크기를 구하면 된다. 이것을 주파수 스펙트럼이라 한다.
    • 또한 실수부와 허수부의 각도를 이용해서 주파수 위상을 계산할 수도 있다.

|G(k, l)| = \sqrt{Re(k, l)^{2} + Im(k, l)^{2}}

\theta (k, l) = tan^{-1} [ {Im(k, l) \over Re (k, l)} ]

  • 여기서 주파수 스펙트럼 영상은 저주파 영역의 계수값이 고주파 영역에 비해 상대적으로 너무 크다.
    • 이로 인해 계수값을 일반적인 방법으로 정규화해서 영상으로 표현하면 최저 주파 영역만 흰색으로 나타나고 나머지 영역은 거의 검은색으로 나타나서 고주파 영역의 계수를 영상으로 확인하기가 곤란하다.
    • 이런 문제를 해결하기 위해 계수값에 로그 함수를 먼저 적용하고 정규화한다.
void log_mag(Mat complex_mat, Mat& dst)
{
Mat planes[2];
split(complex_mat, planes); // 2채널 복소행렬 분리
magnitude(planes[0], planes[1], dst); // 크기 계산
log(dst + 1, dst);
normalize(dst, dst, 0, 255, CV_MINMAX); // 정규화 수행
dst.convertTo(dst, CV_8U);
}
  • 위의 log_mag() 함수는 DFT를 수행한 행렬에서 주파수 스펙트럼을 계산하고, 로그 함수를 적용한다.
    • 여기서 첫 번째 인수인 DFT 결과 행렬(complex_mat)은 실수부와 허수부를 갖는 2채널 행렬이다.
    • 따라서 cv::split() 함수로 2채널 행렬을 1채널 행렬 2개로 분리해서 크기를 구한다.
  • 또한 DFT 수행 후의 주파수 스펙트럼 영상은 저주파 영역이 영상의 모서리 부분에 위치하고 고주파 부분이 중심부에 있다.
    • 즉, 아래 그림의 왼쪽과 같이 푸리에 변환 영상에서 사각형의 각 모서리를 중심으로 원형의 밴드를 형성하여 주파수 영역이 분포한다. 
    • 이 때문에 해당 주파수 영역에서 어떤 처리를 하려면 상당한 불편함이 있다. (여기서 원형의 밴드는 이해를 돕기 위해 표현한 것으로 실제 DFT 스펙트럼 영상에서 나타나는 것은 아니다.)
  • 이 문제는 1사분면과 3사분면의 영상을 맞바꾸고, 2사분면과 4사분면의 영상을 맞바꿈으로서 해결할 수 있다.
    • 결과적으로 아래 그림의 오른쪽과 같이 영상의 중심이 최저주파 영역, 그리고 바깥쪽이 고주파 영역이 되며, 그림과 같이 원형의 밴드로 주파수 영역을 쉽게 구분할 수 있다.
    • 이런 과정을 셔플링(shuffling) 혹은 시프트(shift) 연산이라고 한다.
  • 다음의 shuffling() 함수는 입력 행렬에 셔플링을 수행해서 반환하는 함수이다.
    • q1~q4의 Rect 객체를 통해서 각 사분면의 영역을 지정한다.
    • 그리고 원본 행렬(mag_img)과 반환 행렬(dst) 모두에서 각 사분면을 관심영역으로 지정하여 참조한다.
    • Mat::copyTo() 함수를 통해서 참조된 원본 행렬을 참조된 반환 행렬로 복사하여 사분면의 맞교환을 수행한다.
void shuffling(Mat mag_img, Mat& dst)
{
int cx = mag_img.cols / 2;
int cy = mag_img.rows / 2;

Rect q1(cx, 0, cx, cy); // 1사분면 사각형
Rect q2(0, 0, cx, cy); // 2사분면 사각형
Rect q3(0, cy, cx, cy); // 3사분면 사각형
Rect q4(cx, cy, cx, cy); // 4사분면 사각형

dst = Mat(mag_img.size(), mag_img.type());
mag_img(q1).copyTo(dst(q3));
mag_img(q3).copyTo(dst(q1));
mag_img(q2).copyTo(dst(q4));
mag_img(q4).copyTo(dst(q2));
}
  • N x M 크기의 영상에서 2차원 푸리에 변환은 N2 x M2 만큼의 시간 복잡도를 요구한다.
    • 따라서 영상의 크기가 커지면 수행속도는 기하급수적으로 증가한다.
    • 이러한 이유에서 푸리에 변환을 빠르게 수행하는 알고리즘의 필요성이 대두되었다.

고속 푸리에 변환(FFT: Fast Fourier Transform)

  • 이산 푸리에 변환은 원본 신호의 한 원소에 곱해지는 기저 함수의 원소들을 원소 길이만큼 반복적으로 곱해야하기 때문에 신호가 커질수록 계산 속도는 기하급수적으로 증가한다.
    • 고속 푸리에 변환은 이 과정을 삼각함수의 주기성을 이용해 작은 단위로 분리해서 반복적으로 수행하고 합치도록 하여 효율성을 높이는 방법이다.
  • 다음은 삼각함수의 주기성을 이용하는 방법을 간단히 설명한다. 먼저 푸리에 변환 수식에서 짝수 번째 부분(2n)과 홀수 번쨰 부분(2n+1)을 분리하여 수식을 다음과 같이 정리한다.

G(k) = \sum_{n=0}^{L-1} g[2n] \cdot e^{-j 2 \pi k {2n \over 2L}} + \sum_{n=0}^{L-1} g[2n+1] \cdot e^{-j 2 \pi k {2n + 1 \over 2L}}

G(k) = \{ \sum_{n=0}^{L-1} g[2n] \cdot e^{-j 2 \pi k {n \over L}} \} + \{ \sum_{n=0}^{L-1} g[2n+1] \cdot e^{-j 2 \pi k {n \over L}} \} \cdot e^{-j 2 \pi k {1 \over 2L}}

  • 여기서 짝수 신호와 홀수 신호를 다음과 같이 지정해 보자

G_{even}(k) = \sum_{n=0}^{L-1} g[2n] \cdot e^{-j 2 \pi k {n \over L}}

G_{odd}(k) = \sum_{n=0}^{L-1} g[2n+1] \cdot e^{-j 2 \pi k {n \over L}} 

  • 그러면 푸리에 변환 수식은 다음과 같이 나타낼 수 있다.

G(k) = G_{even}(k) + G_{odd}(k) \cdot e^{-j 2 \pi k {1 \over 2L}}

  • 위 수식의 공통 지수부분에서 한 주기(L)를 더한 수식을 정리해 보자. 이것은 삼각 함수의 주기성으로 인해 다음과 같이 뒷부분 지수를 제거할 수 있다.

e^{-j 2 \pi (k+L) {n \over L}} \\ = e^{-j 2 \pi k {n \over L}} \cdot e^{-j 2 \pi  {Ln \over L}} \\ = e^{-j 2 \pi k {n \over L}} \cdot e^{-j 2 \pi n} \\ = e^{-j 2 \pi k {n \over L}}

  • 이 주기성을 이용하여 G(k+L)을 계산하면 다음과 같다.

G(k+L) = G_{even}(k+L) + G_{odd}(k+L) \cdot e^{-j 2 \pi (k+L) {1 \over 2L}} \\ = G_{even}(k) + G_{odd}(k) \cdot e^{-j 2 \pi (k+L) {1 \over 2L}}

  • 여기서 e^{-j 2 \pi (k+L) {1 \over 2L}} 의 지수를 분리하여 정리하면 다음과 같다.

e^{-j 2 \pi (k+L) {1 \over 2L}} \\ = e^{-j 2 \pi k {1 \over 2L}} \cdot e^{-j 2 \pi  {L \over 2L}} \\ = e^{-j 2 \pi k {1 \over 2L}} \cdot e^{-j \pi} \\ = -e^{-j 2 \pi k {1 \over 2L}}

  • 따라서 최종적으로 G(k+L)은 다음과 같다.

G(k+L) = G_{even}(k) - G_{odd}(k) \cdot e^{-j 2 \pi k {1 \over 2L}}

  • 이것은 G(k+L)의 값이 G(k)의 값들을 이용해서 구할 수 있다는 것이다.
  • 아래 그림은 삼각함수의 주기성이 어떻게 적용되어 반복계산을 줄이는지를 보인다.
    • 8개의 원소를 갖는 원본 신호 g[n]을 짝수 신호와 홀수 신호로 구분하여 푸리엔 변환을 한다.
    • 짝수 DFT 결과와 홀수 DFT 결과로 최종 변환 신호 G[n]을 구성하는 방법을 보인다.
  • 짝수 원소와 홀수 원소를 한 번 분리하는 것으로는 수행속도를 줄이지 못한다.
    • 각 그룹 내 원소들을 연속적으로 분리할 수 있을 것이다. 즉 아래 그림과 같이 짝수 원소 그룹내에서 짝수 원소와 홀수 원소를 다시 분리하고, 홀수 원소 그룹 내에서 짝수 원소와 홀수 원소를 분리한다.
    • 이렇게 연속적으로 분리하면 최종적으로 입력 신호를 2개 원소씩 묶을 수 있다.
  • 여기서 입력 신호에 대해 짝수부와 홀수부로 계속적으로 분리하여 최종적으로 두 원소만 갖게끔 신호를 재배열해야 한다. 이것을 스크램블(scramble)이라고 한다.
    • 스크램블은 보통 비트의 순서를 바꾸는 방법을 설명하지만 속도면에서 비효율적이며, 다음의 scramble() 함수로 구현한다.
Mat scramble(Mat signal)
{
Mat dst = signal.clone();

for (int i = 0, j = 0; i < dst.cols - 1; i++)
{
if (i > j)
{
swap(dst.at<Vec2f>(i), dst.at<Vec2f>(j));
}

int m = dst.cols >> 1;

while ((j >= m) && (m >= 2))
{
j -= m;
m >>= 1;
}

j += m;
}

return dst;
}
  • 다음은 버터플라이(butterfly) 과정이다. 이것은 스크램블 결과 원소에서 이웃한 두 원소에 대해 이산 푸리에 변환을 수행하는 것이다.
    • 여기서 Wk는 푸리에 변환의 기저함수인 삼각함수의 수식이다.
    • 버터플라이는 아래 그림과 같이 흐름도의 모양이 나비와 비슷해서 붙여진 이름이다.
  • 버터플라이 과정은 원본 신호 길이를 두 개 원소 신호로 분리하며, 분리 횟수만큼 연속적으로 반복한다.
    • 여기서 원본 신호를 연속적으로 짝수부와 홀수부로 분리하기 때문에 원본 신호의 원소 개수는 2의 자승이 되어야 한다.
    • 영상의 크기가 반드시 2의 자승이 되는 것이 아니기 때문에 이 문제를 해결하는 방법으로 원본 영상의 가로와 세로 크기를 2의 자승이 되게 넓히고, 빈 공간을 검은색(0)으로 채우는 방법을 사용한다. 이것을 영삽입(zero-padding)이라 한다.
  • 영삽입이 가능한 것은 원본 영상의 평행이동이 푸리에 변환 결과에 영향을 미치지 않기 때문이다.
    • 즉, 아래 그림의 오른쪽 영상에서 원본 영상이 어느 위치로 평행이동되든지 상관없이 푸리에 변환 결과에서 스펙트럼은 동일하다.
  • 다음의 zeropadding() 함수는 입력 영상에 영삽입을 수행해서 반환하는 함수이다. 그 과정은 2에 대한 로그함수인 log2() 함수로 영상의 가로와 세로가 2의 몇 승인지를 계산한다.
    • 계산된 승수는 소수점을 포함하며, ceil() 함수로 소수점 부분을 올림처리한다.
    • 그리고 다시 계산된 승수에 << 연산으로 2의 자승을 만든다.
    • 이렇게 하면 원본 영상보다 소수점 올림난큼 큰 2의 자승 크기(m, n)을 계산할 수 있다.
Mat zeropadding(Mat img)
{
int m = 1 << (int)ceil(log2(img.rows));
int n = 1 << (int)ceil(log2(img.cols));
Mat dst(m, n, img.type(), Scalar(0));

Rect rect(Point(0, 0), img.size());
img.copyTo(dst(rect));
dst.convertTo(dst, CV_32F);

return dst;
}

FFT를 이용한 주파수 영역 필터링

주파수 영역 필터링의 과정

  • 영상을 주파수 영역으로 변환하면 화소의 밝기가 서서히 변화하는 저주파 영역과 급격하게 변화하는 고주파 영역을 공간 영역에 비하여 쉽게 분리할 수 있다.
    • 이렇게 분리된 주파수 영역에 대해 각 주파수 영역을 강화하거나 약화하거나 혹은 제거하는 등의 처리를 통해 다양한 영상 처리를 할 수 있다.
  • 앞서 배운 내용으로 영상에 2차원 푸리에 변환을 수행할 수 있다. 이를 통해 아래 그림과 같이 영상을 주파수 영역으로 쉽게 변환할 수 있다.
    • 주파수 영역에서 필터링 과정은 푸리에 변환 계수에 필터 행렬을 원소간(element-wise)에 곱하여 수행된다.
    • 여기서 푸리에 변환 계수는 복소수이기 때문에 필터의 곱셈도 실수부와 허수부의 두 채널에 수행해야 한다.
    • 마지막으로 필터링된 푸리에 변환 계수를 푸리에 역변환(IFFT)함으로써 다시 공간영역의 영상으로 만들 수 있다.
  • 이러한 일련의 과정을 주파수 성분 조작이라 한다. 또한 필터를 어떻게 구성하느냐에 따라 저주파 통과 필터링, 고주파 통과 필터링, 대역 통과 필터링 등을 쉽게 구현할 수 있다.

저주파 및 고주파 통과 필터링

  • 저주파 통과 필터링은 DFT 변환 영역에서 저주파 영역의 계수들은 통과시키고, 그 외의 영역 즉, 고주파 영역의 계수는 차단하는 것을 말한다.
    • 아래 그림의 왼쪽처럼 푸리에 변환을 하고, 셔플링을 수행해서 주파수 스펙트럼 영상을 보면 중심 부분이 저주파 영역이며, 외곽으로 갈수록 고주파 영역이다. 여기서 원형의 점선은 주파수 밴드를 시각적으로 표현한 것이다.
    • 필터링은 주파수 계수에 필터 행렬의 원소가 곱해져서 수행된다. 따라서 저주파 통과 필터의 모양은 아래 그림의 중간 그림처럼 중심에서 지정된 반지름만큼 원형으로 1의 값을 갖게 하고, 외곽 부분을 0으로 지정하면 된다. 그림에서 흰색은 1의 값이며, 검은색은 0의 값이다.
    • 고주파 통과 필터링은 저주파 통과 필터와는 반대로 고주파 영역의 계수들을 통과시키고 저주파 영역의 계수들은 차단하는 것이다. 아래 그림의 오른쪽과 같이 중심에서 지정된 반지름 크기의 원형으로 0의 값을 갖게 하고 가장자리 부분을 1로 지정하면 된다.
    • 주파수 계수와 필터의 원소가 곱해지기 때문에 0의 값을 갖는 부분은 주파수 계수가 제거되어 차단되고, 1의 값을 갖는 부분은 그대로 유지되어 통과된다.

버터워스, 가우시안 필터링

  • 앞선 대역 통과 필터는 특정한 대역에서 급격하게 값을 제거하기 때문에 결과 영상의 화질이 좋지 못하다.
    • 특히 저주파 통과 필터링의 경우 영상에서 객체의 경계부분이 완만해지기는 하지만, 경계부분 주위로 잔물결 같은 무늬 (ringing pattern)가 나타나서 화질이 더욱 떨어진다.
    • 이 문제를 해결하는 것은 필터 원소의 값을 차단 주파수에서 급격하게 0으로 만들지 않고 완만한 경사를 이루도록 구성하면 된다.
    • 대표적으로 버터워즈 필터링(Butterworth filter)과 가우시안 필터(Gaussian filter)가 있다.
  • 가우시안 필터는 필터 우너소의 구성을 가우시안 함수의 수식 분포를 갖게 함으로써 차단 주파수 부분을 점진적으로 구성한 것이다.
    • 가우시안 함수의 수식에서 표준편차(\sigma )를 주파수를 차단할 반지름의 위치(R )로 간주하자.
    • 그러면 아래 그림의 오른쪽과 같이 포물선의 곡선을 갖는 주파수 공간 필터가 구성된다.
    • 또한 가우시안 함수의 수식은 {1 \over 2 \pi \sigma^{2}} 를 곱해야 하지만, 원소 값의 스케일을 최댓값이 1이 되도록 하기 위해 생략한다.

f(x, y) = exp(-{dx^{2} + dy^{2} \over 2 R^{2}}) \\ dx = x - center x \\ dy = y - center y

  • 버터워즈 필터는 다음의 수식으로 필터 원소의 구성이 가능하다. 여기서 차단 주파수 반지름 위치(R )와 지수의 승수인 n 값을 어떻게 지정하느냐에 따라 차잔 필터의 반지름과 포물선의 곡률이 달라진다.

f(x, y) = -{1 \over 1 + ({\sqrt{dx^{2} + dy^{2}} \over R})^{2n}} \\ dx = x - center x \\ dy = y - center y

이산 코사인 변환

  • 1974년 미국 텍사스 대학에서 라오 교수팀이 이산 코사인 변환(DCT: Discrete Cosine Transform)이라는 새로운 직교변환에 관한 논문을 발표하면서 멀티미디어 혁명이 시작되었다.
    • 라오 팀은 영상 신호의 에너지 집중 특서잉 뛰어나서 영상 압축에 효과적인 주파수 변환 방법을 찾는 것이 목표였고 그 결과가 바로 DCT인 것이다.
  • 이산 푸리에 변환(DFT)은 실수부에 코사인 함수가 곱해지며, 허수부에 사인 함수가 곱해져서 이루어진다. 반면 이산 코사인 변환은 이산 푸리에 변환에서 실수부만 취하고, 허수부분을 제외함으로써 코사인 함수만으로 구성된 직교 방법이다. 이를 이산 여현변환이라고도 한다.
  • 다음은 1차원 이산 코사인 변환의 수식이다.

F(k) = C(k) \cdot \sum_{n=0}^{N-1} g[n] \cdot cos ({(2n + 1) k \pi \over 2N})

g[n] = \sum_{k=0}^{N-1} C(k) \cdot F(k) \cdot cos ({(2n + 1) k \pi \over 2N})

$latex k = 0, 1, … N-1 \\ C(k) \Rightarrow k = 0 \Rightarrow \sqrt{1 / N}, k \neq 0 \Rightarrow \sqrt{2 / N} &s=2$

  • F(k) 는 주파수 영역 신호이며, g(k) 은 공간 영역의 신호이다.

F(k, l) = C(k) \cdot C(l) \cdot \sum_{n=0}^{N-1} \sum_{m=0}^{m-1} g[n, m] \cdot cos ({(2n + 1) k \pi \over 2N}) \cdot cos ({(2m + 1) l \pi \over 2M})

g(n, m) = \sum_{k=0}^{N-1} \sum_{l=0}^{m-1} C(k) \cdot C(l) \cdot F(k, l) \cdot cos ({(2n + 1) k \pi \over 2N}) \cdot cos ({(2m + 1) l \pi \over 2M})

k = 0, 1, ... N-1 \\ C(k) \Rightarrow k = 0 \Rightarrow \sqrt{1 / N}, k \neq 0 \Rightarrow \sqrt{2 / N}

l = 0, 1, ... , M-1 \\ C(l) \Rightarrow l = 0 \Rightarrow \sqrt{1 / N}, l \neq 0 \Rightarrow \sqrt{2 / N}

  • DCT는 일반적으로 전체 영상을 한 번에 변환시키는 것이 아니라 영상을 작은 블록으로 나누어서 블록 단위로 수행한다.
    • 이 블록의 크기를 키울수록 압축의 효율이 높아지지만, 변환의 구현이 어려워지고 속도도 느려진다.
    • 일반적으로 8 x 8 크기가 성능과 구현 용이성간의 상호보환(trade-off)되어 표준으로 사용된다.
  • 8 x 8 크기의 한 블록을 DCT 변환하면 아래 그림과 같이 64개의 주파수 계수가 구성된다. (0, 0) 위치를 DC 계수라 하며, 나머지 계수들을 AC 계수라 한다.
    • DC 계수는 공간 영역의 화소값의 평균에 해당하는 값으로서 에너지가 집중되어 있고, 영상의 주요 성분을 포함하고 있다.
    • 각 주파수 계수는 영상의 밝기 변화 특성을 나타낸다. 왼쪽 상단으로 갈수록 저주파 영역이며, 오른쪽 하단으로 갈수록 고주파 영역이다. 저주파 영역으로 갈수록 밝기 변화가 적으며, 고주파 영역으로 갈수록 밝기 변화의 정도가 증가한다.
  • 블록의 크기를 N x M이라 할 때, 코사인 함수 부분이 블록의 한 화소에서 N x M 만큼 계산하고, 블록의 모든 화소에서 코사인 함수를 계산하게 되면 O(N2 x M2)의 계산 복잡도가 된다.
    • 그리고 원본 영상을 N x M 크기의 블록으로 나누어 모든 블록에 DCT를 수행한다. 따라서 DCT 변환의 전체 계산복잡도는 다음의 수식과 같다.

DCT 변환의 계산 복잡도 = $latex O(N^{2} \times M^{2} \times Total_{block}) \\ Total_{block} = H_{block} \times W_{block} \\ H_{block} = {ImageHeight \over N} \\ W_{block} = {ImageWidth \over M} &s=2$

  • 512 x 512 영상에 8 x 8 블록으로 DCT 를 수행하면, 한 블록의 DCT 복잡도가 4,096번이며, 전체 블록의 개수가 4,096개이므로 16,777,216번의 코사인 함수를 계산해야 한다. 게다가 코사인 함수의 계산이 상대적으로 느리기 때문에 상당한 시간이 소요된다.
    • 다행히 DCT 정변환과 역변환에서 코사인 함수 부분이 동일하고 한 블록에 수행되는 모든 코사인 함수 값은 블록마다 반복적으로 사용되므로 블록에 사용되는 코사인 함수값들을 미리 계산에서 행렬에 저장해 두면 효율적이다.
    • (관련 코드 생략)

OpenCV로 배우는 영상 처리 및 응용/ 기하학 처리

  • 영상 처리에서 기하학 처리는 영상 내에 있는 기하학적인 대상의 공간적 배치를 변경하는 과정을 말한다.
    • 이것을 화소의 입장에서 보면 영상을 구성하는 화소들의 공간적 위치를 재배치하는 과정이라 할 수 있다.
    • 이러한 변환에는 회전, 크기 변경, 평행이동 등이 있다.

사상(Mapping)

  • 사상은 화소들의 배치를 변경할 때, 입력 영상의 좌표가 새롭게 배치될 해당 목적 영상의 좌표를 찾아서 화소값을 옮기는 과정을 말한다.
    • 사상에는 순방향 사상(forward mapping)과 역방향 사상(reverse mapping)의 두 가지 방식이 있다.
  • 순방향 사상은 원본 영상의 좌표를 중심으로 목적영상의 좌표를 계산하여 화소의 위치를 변환하는 방식이다.
    • 이 방식은 원본 영상과 목적 영상의 크기가 같을 때는 유용하지만 그렇지 않으면 홀(hole)이나 오버랩(overlap)의 문제가 발생할 수 있다.
    • 아래 그림에서 홀과 오버랩에 대해 나타내었는데, 홀은 입력 영상의 좌표들로 목적영상의 좌표를 만드는 과정에서 사상되지 않은 화소를 가리킨다. 보통 영상을 확대하거나 회전할 때 발생한다.
    • 반면 오버랩은 원본 영상의 여러 화소가 목적영상의 한 화소로 사상되는 것으로 영상을 축소할 때 주로 발생한다.
  • 이런 문제를 해결할 수 있는 방법이 역방향 사상이다. 역방향 사사은 목적 영상의 좌표를 중심으로 역변환을 계산하여 해당하는 입력 영상의 좌표를 찾아서 화소값을 가져오는 방식이다.
    • 아래 그램의 예시에서 입력영상의 하단 왼쪽 한 개의 화소가 목적 영상의 두 개 화소로 각각 사상된다. 이 경우에도 역방향 사상의 방식은 홀이나 오버랩은 발생하지 않는다.
    • 다만 입력영상의 한 화소를 목적영상의 여러 화소에서 사용하게 되면 결과 영상의 품질이 떨어질 수 있다.

크기 변경(Scaling)

  • 크기 변경은 입력 영상의 가로와 세로로 크기를 변경해서 목적영상을 만드는 방법으로 입력영상보다 변경하고자 하는 영상의 크기가 커지면 확대가 되고, 작아지면 축소가 된다.
  • 영상의 크기 변경은 비율을 이용해서 수행할 수 있다. 가로와 세로로 변경하고자 하는 비율을 지정하여 입력영상의 좌표에 곱하면 목적영상의 좌표를 계산할 수 있다.

x' = x \cdot ratio X \\ y' = y \cdot ratio Y

  • 목적영상의 크기를 지정해서 변경할 수도 있는데, 이것은 입력영상과 목적영상의 크기로 비율을 계산하고, 계산된 비율을 이용해서 목적영상의 좌표를 계산한다.

ratio X = {dst_{width} \over org_{width}}, ratio Y = {dst_{height} \over org_{height}}

보간(Interpolation)

  • 아래 그림은 입력영상을 두 배 확대할 때 목적영상의 화소 배치를 보인 것이다. 순방향 사상으로 목적영상의 화소를 찾은 경우에는 입력영상의 4개 화소는 쉽게 배치가 되지만, 목적영상에서 확대되는 나머지 화소들은 홀이 발생한다.
    • 이런 문제를 해결하는 방법으로 역방향 사상을 통해서 홀의 화소들을 입력영상에서 찾아서 목적영상의 화소에 대입함으로써 목적영상의 화질을 유지할 수 있다.
  • 이렇게 목적영상에서 홀의 화소들을 채우고 오버랩이 되지 않게 화소들을 배치하여 목적영상을 만드는 기법을 보간법이라한다.
    • 이렇나 보간법의 종류에는 최근접 이웃 보간법(nearest neighbor interpolation), 양선형 보간법(bilinear interpolation), 3차 회선 보간법(cubic convolution interpolation) 등이 있다.

최근접 이웃 보간법(Nearest Neighbor Interpolation)

  • 최근접 이웃 보간법은 목적영상을 만드는 과정에서 홀이 되어 할당 받지 못하는 화소들의 값을 찾을 때, 목적영상의 화소에 가장 가깝에 이웃한 입력영상의 화소값을 가져오는 방법이다.
  • 이 방법은 목적화소의 좌표를 반올림하는 간단한 알고리즘으로 비어있는 홀들을 채울 수 있어 쉽고 빠르게 목적영상의 품질을 높일 수 있다.
    • 다만 확대의 비율이 커지면 영상 내에서 경계선이나 모서리 부분에서 계단현상이 나타날 수 있다.

양선형 보간법(Bililnear Interpolation)

  • 영상을 확대할 때 확대비율이 커지면 최근접 이웃 보간법은 모자이크 현상 혹은 경계부근에서 계단현상이 나타나게 된다. 이러한 문제를 보완할 수 있는 방법이 양선형 보간법이다.
  • 여기서 선형의 의미는 중첩의 원리(superposition principle)가 적용되낟는 것이다. 
    • 쉽게 표현하면 직선의 특징을 가지고 있다는 것인데, 직선의 방정식을 예로 들 수 있다.
    • 아래 그림과 같이 두 개의 화소 값을 알고 있을 때 그 값으로 직선을 그리면, 직선의 선상에 위치한 중간 화소들의 값은 직선의 수식을 이용해서 쉽게 계산할 수 있다.
  • 양선형 보간법은 이와 같은 선형 보간을 두 번에 걸쳐서 수행하기에 붙여진 이름이다. 그 세부적인 방법은 아래 그림을 이용해 설명한다.
  • 먼저 목적영상의 화소(P)를 역변환으로 계산하여 가장 가까운 위치에 있는 입력영상의 4개 화소(A, B, C, D)를 가져온다.
    • (b) 그림과 같이 가져온 4개 화소를 두 개씩(AB, CD) 묶어서 화소값(P1, P2, P3, P4)으로 두 화소를 잇는 직선을 구성한다.
    • 다음으로 직선의 선상에서 목적영상의 화소의 좌표로 중간 위치를 찾고, 그 위치의 화소값(M1, M2)를 계산한다. 이때 중간 위치의 화소값은 기준 화소값 (P1, P2, P3, P4)과 거리 비율(α, 1-α)을 바탕으로 짓건의 수식을 이용해서 계산한다.
    • 마지막으로 구해진 중간 화소값(M1, M2)을 잇는 직선을 다시 구성하고, 두 개의 중간 화소값과 거리 비율(β, 1-β)을 바탕으로 직선의 수식을 이용해서 최종 화소값 (P)를 계산한다.
    • 이때 최종 화소값이 목적영상의 해당 좌표의 화소값이 된다.
  • 정확히는 세 번의 선형 보간을 수행하지만, 4개 화소값(P1, P2, P3, P4) 에 대해서 수행하는 선형 보간은 1차 보간으로 간주한다.
    • 그리고 중간 화소값(M1, M2)에 대해 수행하는 선형 보간을 2차 보간으로 간주하기 때문에 양선형 보간이라 한다.
    • 이를 수식으로 표현하면 다음과 같다.

M_{1} = \alpha \cdot B + (1-\alpha) \cdot A = A + \alpha \cdot (B - A) \\ M_{2} = \alpha \cdot D + (1-\alpha) \cdot C = C + \alpha \cdot (D - C) \\ P = \beta \cdot M_{2} + (1 - \beta) \cdot M_{1} = M_{1} + \beta \cdot (M_{2} - M_{1})

  • OpenCV에서는 보간이 필요한 함수들을 위해 아래와 같은 옵션값을 제공한다.
옵션 상수 설명
INTER_NEAREST 0 최근접 이웃 보간
INTER_LINEAR 1 양선형 보간(기본값)
INTER_CUBIC 2 바이큐빅 보간 – 4×4 이웃 화소 이용
INTER_AREA 3 픽셀 영역의 관계로 리샘플링
INTER_LANCZOS4 4 Lanczos 보간 – 8×8 이웃 화소 이용

평행이동(Translation)

  • 영상에서 원점 좌표는 좌상단이다. 평행이동은 영상의 원점을 기준으로 모든 화소를 동일하게 가로, 세로 방향으로 옮기는 것을 말한다.
    • 아래 그림은 최상단 오른쪽의 원점에서 가로 방향으로 dx, 세로 방향으로 dy 만큼 전체 영상의 모든 화소를 이동시키는 예이다.
    • 여기서 옮겨진 후에 입력영상의 범위를 벗어나는 부분(오른쪽 부분과 하단 부분)은 목적영상에서 제거된다.
    • 평행이동할 화소가 없는 부분인 상단과 왼쪽 부분은 0(검은색) 혹은 255(흰색)로 지정한다.
  • 평행이동을 수식으로 표현하면 다음과 같다.
    • 순방향 사상을 적용하면 입력영상의 화소(x, y)에서 이동할 화소 수만큼 가로 방향과 세로 방향으로 더해 주어서 목적영상의 화소(x’, y’) 위치를 정한다.

순방향 사상

x' = x + dx \\ y' = y + dy

역방향 사상

x = x' - dx \\ y = y' - dy

회전(Rotation)

  • 회전은 입력영상의 모든 화소를 영상의 원점을 기준으로 원하는 각도만큼 모든 화소에 대해 회전 변환을 시키는 것을 말한다.
    • 이것은 2차원 평면에서 회전 변환을 나타내는 행렬을 통해서 수식으로 표현할 수 있다.

순방향 사상

x' = x \cdot cos \theta - y \cdot sin \theta \\ y' = x \cdot sin \theta + y \cdot cos \theta

역방향 사상

x = x' \cdot cos \theta + y' \cdot sin \theta \\ y = - x' \cdot sin \theta + y' \cdot cos \theta

  • 목적영상의 모든 화소(x’, y’)에 대해 역방향 사상의 수식을 적용하여 입력화소를 계산하면 아래 그림과 같이 원점으로부터 시계 방향으로 정해진 각도만큼 회전된 영상이 생성된다.
    • 직교 좌표계에서 회전 변환은 반시계 방향으로 적용된다. 그러나 영상 좌표계에서는 y좌표가 하단으로 내려갈수록 증가하기 때문에 시계방향의 회전으로 표현됨에 유의한다.
  • 평행이동과 마찬가지로 목적영사으이 범위를 벗어나는 입력화소는 제거되며, 입력영상에서 찾지 못하는 화소는 검은색이나 흰색으로 지정한다.
  • 일반적으로 영상을 회전시킬 때 회전의 기준을 영상의 기준 원점인 좌상단으로 하지 않고, 물체의 중심(center X, center Y)으로 하는 경우가 많다.
    • 이런 경우에는 평행이동의 수식을 포함하여 회전 변환을 수행한다. 이것은 영상의 기준점을 원점으로 이동시킨 후 회전을 수행하고, 다시 기준점으로 이동하는 것이다.

x = (x' - cneter X) \cdot cos \theta + (y' - center Y) \cdot sin \theta + center X \\ y = -(x' - cneter X) \cdot sin \theta + (y' - center Y) \cdot cos \theta + center Y

행렬 연산을 통한 기하학 변환 – 어파인 변환(Affine Transform)

  • 앞서 기술한 기하학 변환들의 수식은 행렬식으로 표현 가능하다. 즉, 기하학 변환 수식이 행렬의 곱으로 표현되는 것이다. 각 변환식은 다음과 같은 수식으로 표현할 수 있다.

회전 

\left[ \begin{array}{rr} x' \\ y'  \end{array} \right] = \left[ \begin{array}{rr} cos \theta & - sin \theta \\ sin \theta & cos \theta \end{array} \right] \left[ \begin{array}{rr} x \\ y  \end{array} \right]

크기변경

\left[ \begin{array}{rr} x' \\ y'  \end{array} \right] = \left[ \begin{array}{rr} \alpha & 0 \\ 0 & \beta \end{array} \right] \left[ \begin{array}{rr} x \\ y  \end{array} \right]

평행이동

\left[ \begin{array}{rr} x' \\ y'  \end{array} \right] = \left[ \begin{array}{rr} x \\ y \end{array} \right] \left[ \begin{array}{rr} t_{x} \\ t_{y}  \end{array} \right]

  • 회전과 크기변경은 2×2 행렬로 표현이 가능하지만, 평행이동까지 포함하려면 2×3 행렬이 필요하다. 다음 수식과 같이 2×3 행렬로 변환 행렬을 구성하는 것을 어파인 변환이라 한다.

\left[ \begin{array}{rr} x' \\ y'  \end{array} \right] = \left[ \begin{array}{rrr} \alpha_{11} & \alpha_{12} & \alpha_{13} \\ \alpha_{21} & \alpha_{22} & \alpha_{23} \end{array} \right] \cdot \left[ \begin{array}{rrr} x \\ y \\ 1  \end{array} \right]

  • 어파인 변환은 변환 전과 변환 후의 두 어파인 공간 사이의 공선점을 보존하는 변환이다.
    • 따라서 변환 전에 직선은 변환 후에도 그대로 직선이며, 그 거리의 비도 유지된다. 또한 변환 전에 평행선도 변환 후에 평행선이 된다.
  • 어파인 변환을 수행하는 방법은 크게 2가지가 있다.
    • 하나는 회전 각도, 크기변경 비율, 평행이동의 정도를 지정해서 각각 변환 행렬을 구성한다. 그리고 각 변환 행렬을 행렬 곱으로 구성하면 하나의 변환 행렬을 만들 수 있다.
    • 각 행렬들을 곱하는 순서는 변환하고자는 방식에 따라서 달라질 수 있다.
    • 이때 2×3 크기의 어파인 행렬로 구성하면 행렬의 곱을 계산할 수 없기 때문에 다음 수식과 같이 3×3 크기의 행렬로 구성하여 행렬 곱을 수행한다.

어파인 변환행렬

\left[ \begin{array}{rrr} cos \theta & -sin' \theta & 0 \\ sin \theta & cos \theta & 0 \\ 0 & 0 & 1 \end{array} \right] \cdot \left[ \begin{array}{rrr} \alpha & 0 & 0 \\ 0 & \beta & 0 \\ 0 & 0 & 1 \end{array} \right] \cdot \left[ \begin{array}{rrr} 1 & 0 & t_{x} \\ 0 & 1 & t_{y} \\ 0 & 0 & 1 \end{array} \right]

  • 행렬 곱을 완성하면 3×3 행렬에서 마지막 행(0 0 1)을 삭제하면 최종적으로 2×3 형태의 어파인 행렬이 된다.
  • 다른 한 가지 방법으로 위 그림과 같이 변환 전인 입력영상의 좌표 3개 (x1, x2, x3)와 변환이 완료된 목적영상에서 상응하는 좌표 3개 (x’1, x’2, x’3)를 알면 두 좌표 (x -> x’) 사이를 변환해 주는 어파인 변환 행렬을 구할 수 있다.
  • 이렇게 행렬의 곱으로 기하학 변환을 적용하면 단순하면서도 쉽게 입력영상에 대한 변환이 가능하다.
    • OpenCV 에서도 어파인 변환을 수행할 수 있는 cv::wrapAffine() 함수를 제공한다. 이 함수는 지정된 어파인 변환 행렬을 적용하면 입력영상에 어파인 변환을 수행한 목적영상을 반환한다.
  • 또한 어파인 변환 행렬을 만드는 함수로는 cv::getAffineTransform()과 cv::getRotationMatrix2D()가 있다.
    • cv::getAffineTransform()는 변환 전의 좌표 3개와 변환 후의 좌표 3개를 지정하면 해당 변환을 수행해 줄 수 있는 어파인 행렬을 반환한다.
    • cv::getRotationMatrix2D()는 회전 변호나과 크기 변경ㅇㄹ 수행하는 어파인 행렬을 반환한다. 여기서 회전의 방향은 양수일 때 반시계 방향으로 회전하는 행렬을 반환한다.
반환형 이름 파라미터 내용
void wrapAffine

InputArray src,
OutputArray dst,
InputArray M,
Size dsize,
int flags,
int borderMode

입력영상에 어파인 변환을 수행해서 반환한다.
Mat getAffineTransform InputArray src,
OutputArray dst,
Point2f src[],
Point2f dst[]
3개의 좌표쌍을 입력하면 어파인 변환 행렬을 반환한다.
Mat getAffineTransform const Point2f src[],
const Point2f dst[]
 
Mat getRotationMatrix2D Point2f center,
double angle,
double scale
회전 변환과 크기 변경을 수행할 수 있는 어파인 행렬을 반환한다.

원근 투시(투영) 변환 (Perspective Projection Transformation)

  • 원근 투시 변환은 원근법을 영상 좌표계에서 표현하는 것이다. 원근 투시 변환은 아래 그림과 같이 3차원의 실세계의 좌표를 투영 스크린상의 2차원 좌표로 표현할 수 있도록 변환해 주는 것을 말한다.
  • 영상 처리에서 원근변환은 주로 2차원 영상을 다른 2차원 영상으로 변환할 때 사용한다.
    • 예컨대 카메라에서 입력 받은 영상에서 카메라 렌즈에 의한 왜곡을 보정할 수 있다. 그리고 원근감이 잘 표현된 2차원 영상을 변환하여 3차원 공간상의 거리를 측정하고자 할 때 사용될 수도 있다.
  • 원근 투영 변환을 사용할 때는 동차 좌표계(homogeneous coordinates)를 사용하는 것이 편리하다.
    • 동차 좌표계는 모든 항의 차수가 동일하기 때문에 붙여진 이름으로 n차원의 투영 공간을 n+1개의 좌표로 나타내는 좌표계이다. 좀 더 쉽게 말하면 직교 좌표인 (x, y)를 (x, y, 1)로 표현하는 것이다.
    • 이것을 일반화해서 표현하면 0이 아닌 상수 w에 대해 (x, y)를 (wx, wy, w)로 표현한다. 이렇게 되면 상수 w가 무한히 많기 때문에 (x, y)에 대한 동차 좌표 표현은 무한히 많이 존재하게 된다.
    • 거꾸로 동차 좌표계에서 한 점 (wx, wy, w)을 직교 좌표로 나타내면 각 원소를 w로 나누어저 (x/w, y/w)가 된다.
  • 원근변환을 수행할 수 있는 행렬은 다음과 같다. 이 수식으로 입력영상의 좌표를 목적영상의 좌표로 변환하면 원근변환이 수행된다.

w \cdot \left[ \begin{array}{rrr} x' \\ y' \\ 1\end{array} \right] = \left[ \begin{array}{rrr} \alpha_{11} & \alpha_{12} & \alpha_{13} \\ \alpha_{21} & \alpha_{22} & \alpha_{23} \\ \alpha_{31} & \alpha_{32} & \alpha_{33} \end{array} \right] \cdot \left[ \begin{array}{rrr} x \\ y \\ 1 \end{array} \right]

OpenCV로 배우는 영상 처리 및 응용/ 영역 처리

회선 (Convolution)

공간 영역의 개념과 회선

  • 영상 처리에서 ‘영역’에 대한 하나의 의미는 두 개의 다른 범위(domain)의 구분이다.
    • 하나는 공간 영역(spatial domain)이고, 다른 하나는 주파수 영역(frequency domain)이다.
      • 공간 영역은 영상들이 다루어지는 화소 공간을 의미하는데, x, y 차원의 2차원 공간을 말한다.
    • 다른 하나는 영역 기반 처리(area based processing)라는 표현에서 사용하는 영역이다.
      • 이는 화소 기반 처리와 상반되는 의미로서, 화소점 하나 하나의 개념이라기 보다는 화소가 모인 특정 범위(영역)의 화소 배열을 의미한다.
  • 화소 기반 처리가 화소값 각각에 대해 여러 가지 연산을 수행하는 것이라면, 공간 영역 기반 처리는 마스크(mask)라 불리는 규정된 영역을 기반으로 연산이 수행된다. 이러한 이유에서 공간 영역 기반 처리를 마스크 기반 처리라고도 한다.
  • 마스크 기반 처리는 마스크 내의 원소값과 공간 영역에 있는 입력 영상의 화소값들을 대응되게 곱하여 출력화소값을 계산하는 것을 말한다. 이러한 처리를 하는 과정을 모든 출력화소값에 대해 이동하면서 수행하는 것을 회선(convolution)이라고 한다.
    • 이때 입력 영상에 곱해지는 마스크는 커널(kernel), 윈도우(window), 필터(filter) 등의 용어로 불려진다.
  • 위 그림은 3×3 크기의 마스크로 회선을 수행하는 과정을 표현한 것으로, 출력 화소 O22는 대응되는 위치에 있는 입력화소 I22와 마스크 크기만큼의 주위 화소들을 이용해서 계산된다.
    • 즉 마스크의 각 원소가 같은 위치의 입력화소와 곱해지며, 이 곱한 값들을 모두 더해서 출력 화소가 계산된다.
    • 계속적으로 출력화소 O23은 입력 영상에서 마스크를 한 화소 오른쪽으로 이시킨 후에 대응되는 입력화소들과 곱하여 계산된다.
    • 같은 방법으로 입력 영상의 모든 화소에 대해 마스크를 이동시켜 곱하고, 그 값들을 모두 더해서 해당 위치의 출력화소가 계산된다.
  • 결과적으로 회선으로 생성되는 영상은 마스크의 원소 값에 따라 결정된다. 즉, 입력 영상의 각 화소의 위치에서 마스크의 크기의 주변 화소들을 마스크 원소의 비율만큼 반영하는 것이다.
    • 이 마스크의 원소를 어떻게 결정하느냐에 따라 결과 영상이 드라마틱하게 달라진다.

블러링(Blurring)

  • 블러링은 영상에서 화소값이 급격하게 변하는 부분들을 감소시켜 점진적으로 변하게 함으로써 영상이 전체적으로 부드러운 느낌이 나게 하는 기술이다.
    • 경우에 따라 스무딩(smoothing)이라고 하는 경우도 있다.
  • 화소값이 급격하게 변하는 것을 점진적으로 하는 방법은 회선을 이용한 필터링을 이용하는 것이다.
    • 마스크를 아래 그림과 같이 모든 원소의 값을 같게 구성하여 회선을 수행하면 블러링이 적용된다.
    • 이때 마스크의 전체 원소의 합은 1이 되어야 입력 영상의 밝기가 유지된다.
  • 아래 그림은 회선을 통한 블러링을 설명하기 위한 내용이다.
    • 아래 그림에서 O22와 O23의 계산에서 입력 영상 중 6개 화소가 동일하며, 나머지 3개 화소만 다르다.
    • 이웃하는 두 출력화소는 마스크 크기 내에서 입력화소의 2/3가 공통 부분이고 블러링 마스크의 원소 값이 모두 같기 때문에 입력화소가 같은 비율로 출력화소에 반영된다.
    • 따라서 입력화소의 공통부분이 같은 비율로 반영되기 떄문에 출력 영상에서 이웃하는 화소들이 비슷한 값을 갖게 된다.

샤프닝(sharpening)

  • 블러링이 이웃 화소의 차이를 감소시켜서 부드럽게 만드는 것이라면, 샤프닝은 이웃 화소끼리 차이를 크게 해서 날카로운 느낌이 나게 만드는 것이다.
    • 이렇게 함으로써 영상의 세세한 부분을 강조할 수 있으며 경계 부분에서 명암대비가 증가되는 효과를 낼 수 있다.
  • 입력 영상의 화소와 출력 영상의 화소가 마스크의 중심 위치에서 대응된다. 이 마스크의 중심 위치의 계수를 중심계수라고 한다.
    • 마스크 중심계수의 비중이 크면 출력 영상은 입력 영상의 형태를 유지하게 된다. 추가적으로 주변 계수들을 중심계수와 값의 차이를 크게 만들면 샤프닝이 수행된다.
    • 여기서 마스크 원소의 전체 합이 1이 되어야 입력 영상의 밝기가 손신 없이 출력 영상의 밝기로 유지된다. 마스크 원소의 합이 1보다 작으면 출력 영상의 밝기가 입력 영상보다 어두워지며, 1보다 크면 입력 영상보다 더 밝아진다.
    • 따라서 중심계수는 1보다 훨씬 크게 구성하며, 주변 화소는 비중을 감소시킬 수 있도록 음수 값을 갖게 한다. 그리고 전체 원소의 합은 1이 되도록 구성하면 샤프닝 필터가 완성된다. 아래 그림은 이와 같은 조건에 부합하는 샤프닝 마스크의 예이다.

에지(Edge) 검출

  • 영상처리에서 에지는 윤곽선 혹은 경계선의 의미를 갖는다. 이 윤곽선은 객체에서 크기, 위치, 모양을 인지할 수 있으며, 그 방향성을 탐지할 수 있다.
    • 따라서 에지 검출은 영상 처리에서 아주 중요하며 기본적인 처리 분야로 다루어진다.
  • 위 그림은 영상의 특정 지점(40, 250)에서 가로로 50개의 화소를 가져와서 그 화소값을 그래프로 나타낸 모습이다.
    • 그래프에서 원으로 표시한 부분들을 보면 화소값에서 급격하게 변하여 그래프가 꺾이는 것을 볼 수 있는데, 28번째 픽셀에서 화소값이 급격하게 낮아지며, 32번째 픽셀 주위에서는 다시 급격하게 높아진다.
    • 이렇게 화소값 그래프에서 급격하게 꺽이는 부분을 영상에서 보면 모서리나 윤곽선 혹은 경계 부분인 것을 알 수 있다.
  • 에지 검출(edge detection)이란 에지에 해당하는 화소를 찾는 과정으로 그 방법으로는 가장 간단하게 이웃하는 화소의 차분을 이용하여 그 차분이 특정 임계값 이상인 곳을 에지로 지정하는 것이다.
    • 또한 에지는 마스크를 이용하여 계산할 수도 있는데, 1차 미분 마스크나 2차 미분 마스크를 사용하여 회선을 수행하는 것이다.

차분 연산을 통한 에지 검출

  • 단순하고 빠른 에지 검출 방법으로 유사 연산자와 차 연산자를 이용한 방법이 있다.
    • 유사 연산자는 중심화소에서 각 방향의 주변 화소와 차분을 계산하고, 그 중에서 가장 큰 값을 출력화소로 결정하는 방법이다. 아래 그림에서 보듯 8방향의 차분을 계산해야 한다.
    • 반면 차 연산자는 중심화소를 배제하고 주변 화소의 상하 차분, 좌우 차분, 그리고 대각선 차분을 계산하고 그 중에서 가장 큰 값을 출력 화소로 결장하는 방법이다.
  • 회선의 방법과 유사하게 입력 영상의 해당 화소에 마스크를 위치시키지만 화소값과 마스크 원소를 곱하는 것이 아니라 마스크 범위의 입력 화소들 간에 차분을 계산한다.
    • 차 연산자의 경우 중심화소를 배제시켰기 때문에 4번의 차분만 계산하여 속도 면에서 유리하다.
  • 위 그림은 차 연산자를 이용한 에지 검출 예제이다. 여기서 반복을 통해서 원소의 대각선 방향 차분(start-end)을 구하기 위해 mask 벡터를 도입해서 그림과 같이 계산한다.

1차 미분 마스크

  • 영상의 특정 좌표에서 가로 방향(혹은 세로방향)으로 화소값들을 구성했을 때, 결과 그래프에서 밝기의 변화를 파악할 수 있다.
    • 에지가 화소의 밝기가 급격히 변하는 부분이기 때문에 함수의 변화율을 취하는 미분 연산을 이용해서 에지를 검출할 수 있다.
    • 영상에서 밝기의 변화율을 검출하는 방법은 밝기에 대한 기울기(gradient)를 계산하는 것이다. 현재 화소에서 밝기의 기울기를 계산하고, 이 기울기의 크기를 구하면 에지가 된다. 
    • 그러나 디지털 영상은 연속이 아닌 이산된 데이터이기 때문에 엄밀한 의미에서 미분 연산을 할 수 없다. 그래서 다음과 같은 수식으로 근사하여 계산한다.

G[f(x, y)] = \left[ \begin{array}{rr} G_{x} \\ G_{y}  \end{array} \right] = \left[ \begin{array}{rr} {\partial f(x, y) \over \delta x} \\ {\partial f(x, y) \over \delta y}  \end{array} \right]

G_{x} = {f(x + dx, y) - f(x, y) \over dx} \fallingdotseq f(x+1, y) - f(x, y), (dx = 1)

G_{y} = {f(x, y + dy) - f(x, y) \over dy} \fallingdotseq f(x, y+1) - f(x, y), (dy = 1)

G[f(x, y)] \fallingdotseq \sqrt{G_{x}^{2} + G_{y}^{2}} \approx |G_{x}| + |G_{y}|

\theta = tan^{-1}({G_{y} \over G_{x}})

  • 먼저 2차원 공간 상의 한 화소에서 수평 방향과 수직 방향으로 각각 미분한다. 이를 편미분이라 한다.
    • 그리고 각 방향의 편미분을 한 화소단위 (dx = 1, dy = 1)의 차분으로 근사한다.
    • 다음으로 각 방향의 차분을 이용해서 기울기의 크기를 계산한다. 이 크기가 에지의 강도가 된다.
    • 여기서 계산 복잡도를 줄이기 위해 제곱과 제곱근 대신, 절댓값을 사용하기도 한다. 또한 역탄젠트(arctan) 함수에 가로 방향과 세로 방향 차분을 적용하면 에지의 방향을 계산할 수도 있다.
  • 이러한 1차 미분 공식을 영상에 구현하는 쉬운 방법이 1차 미분 마스크로 회선을 적용하는 것이다.
    • 마스크의 중심 위치의 입력 화소가 f(x, y)일 때 주변 화소의 위치를 보면 위 그림과 같다.
    • 마스크 원소를 (a)와 같이 f(x, y), f(x, y+1) 위치에 -1과 1을 구성하여 회선을 수행하면, 회선의 내부 계산 수식이 f(x, y+1) – f(x, y) 이 되어서 y 방향 미분인 Gy와 같은 결과가 된다.
    • 또한 (b)와 같이 f(x, y), f(x+1, y) 위치에 -1과 1을 구성하여 회선을 수행하면, 회선 수식이 f(x+1, y) – f(x, y)이 되어서 x 방향 미분인 Gx가 적용된다.
    • 이렇게 회선의 수식을 이용해서 차분을 계산할 수 있도록 마스크의 원소를 구성하면 1차 미분 마스크가 된다. 이 마스크를 적용해서 입력 영상에 회선을 수행하면 에지 검출이 가능하다. 이때 마스크 계수의 합은 0이 되어야 한다.
  • 이 외에도 다양한 1차 미분 마스크가 있으며 대표적으로는 소벨(Sobel), 프리윗(Prewitt), 로버츠(Roberts) 등이 있다.

로버츠(Roberts) 마스크

  • 로버츠 마스크는 아래 그림과 같이 대각선 방향으로 1과 -1을 배치하여 구성된다. 나머지 원소의 값이 모두 0이어서 다른 1차 미분 마스크에 비해 수행 속도가 빠르다.
    • 그리고 한 번만 차분을 계산하기 때문에 차분의 크기가 작고, 이로 인해 경계가 확실한 에지만을 추출하며, 잡음에 매우 민감하다.

프리윗(Prewitt) 마스크

  • 프리윗 마스크는 로버츠 마스크의 단점을 보완하기 위해 고안되었다.
    • 먼저 수직 마스크를 보자. 원소의 배치가 수직 방향으로 구성되어서 수직 마스크라고 하며, 결과 영상에서 에지의 방향도 수직으로 나타난다.
    • 중심화소의 앞과 뒤 화소로 x 방향의 차분을 3번 계산하고, 다음 수평 마스크는 중심화소에서 위와 아래의 화소로 y 방향으로 차분을 3번 계산한다.
    • 최종적으로 수직 마스크의 회선 결과와 수평 마스크의 회선 결과에 대해서 크기(magnitude)로 결과 영상(에지 강도)을 생성한다.
    • 세 번의 차분을 합하기 때문에 로버츠 연산자에 비해 에지의 강도가 강하며, 수직과 수평 에지를 동등하게 찾는데 효과적이다.

소벨(Sobel) 마스크

  • 소벨 마스크는 에지 추출을 위한 가장 대표적인 1차 미분 연산자이다. 마스크의 구성은 프리윗 마스크와 유사하지만, 중심화소의 차분에 대한 비중을 2배로 키운 것이 특징이다.

2차 미분 마스크

  • 1차 미분 연산자는 밝기가 급격하게 변화하는 영역 뿐만 아니라 점진적으로 변화하는 부분까지 민감하게 에지를 검출하여 너무 많은 에지가 나타날 수 있다.
    • 이를 보완하기 위한 방법으로 1차 미분에서 한 번 더 미분을 하는 방법이 2차 미분 연산이 있다.
    • 2차 미분 연산자는 변화하는 영역의 중심에 위치한 에지만을 검출하며, 밝기가 점진적으로 변화되는 영역에 대해서는 반응을 보이지 않는다.
    • 대표적인 방법으로는 라플라시안(Laplacian), LoG(Laplacian of Gaussian), DoG(Difference of Gaussian) 등이 있다.

라플라시안(Laplacian) 에지 검출

  • 가장 대표적인 2차 미분 연산자로 라플라시안이 있다. 프랑스 수학자 라플라시안의 이름을 따서 지은 라플라시안은 함수 f에 대한 그래디언트의 발산으로 정의되며 수식으로 표현하면 다음과 같다.

\Delta f = \nabla^{2} f = \nabla \nabla f

  • 영상은 2차원 좌표계이기 때문에 2차원 직교좌표계에서 라플라시안의 수식은 다음과 같다.

\nabla^{2} f = {\partial^{2} f \over \partial x^{2}} + {\partial^{2} f \over \partial y^{2}}

  • 각 항을 디지털 영상의 화소로 근사하여 1차 미분한 결과에 한 번 더 미분을 수행하면 다음과 같이 정리할 수 있다.

{\partial^{2} f \over \partial x^{2}} = {\partial f(x+1, y) \over \partial x} - {\partial f(x, y) \over \partial x}

= [f(x+1, y) - f(x,y)] - [f(x,y) - f(x-1, y)]

= f(x+1, y) - 2 \cdot f(x, y) - f(x-1, y)

{\partial^{2} f \over \partial y^{2}} = {\partial f(x, y+1) \over \partial y} - {\partial f(x, y) \over \partial y}

= [f(x, y+1) - f(x,y)] - [f(x,y) - f(x, y-1)]

= f(x, y+1) - 2 \cdot f(x, y) - f(x, y-1)

  • 두 항을 더하면 라플라시안 마스크의 공식이 완성된다.

\nabla^{2} f(x, y) = f(x-1, y) + f(x+1, y) + f(x, y-1) + f(x, y+1) - 4 \cdot f(x, y)

  • 3 x 3 크기의 마스크를 예로 라플라시안 마스크 공식에 적용하면 중심화소를 4배로 하고 상하좌우 화소를 중심화소와 반대 부호를 갖게 구성한다. 또한 마스크 원소의 전체 합은 0이 되어야 한다.
  • 이런 방법으로 아래 그림 (a)와 같이 두 개의 마스크를 구성할 수 있으며, 4방향을 가지는 마스크가 된다.
    • 경우에 따라서는 (b)와 같이 8방향으로 늘려 보면 모든 방향의 에지를 검출하고자 할 때도 있다. 중심계수의 값을 더 크게 하고 8방향의 모든 값을 반대 부호가 되게 하면 된다.

LoG와 DoG

  • 라플라시안은 잡음에 민감한 단점이 있다. 그래서 잡음을 먼저 제거하고 라플라시안을 수행한다면 잡음에 강한 에지 검출이 가능할 것이다. 잡음 제거의 수단이 다양하게 있기에 미디언 필터링 혹은 최대/최솟값 필터링 등을 수행할 수 있다.
    • 그러나 이런 방법들은 비선형 공간 필터링이기 때문에 먼저 잡음 제거 필터링을 수행하고, 다시 라플라시안을 수행해야 한다. 바로 속도에서 문제가 있는 것이다.
  • 잡음을 제거하는 달느 방법으로 선형 공간 필터를 선택하여 회선을 하고, 그 후에 라플리사인 마스크로 회선하는 방법을 생각해 볼 수 있다. 이 경우 두 가지 모두 선형 필터링이기 때문에 다음 수식과 같이 단일의 마스크로 계산할 수 있다.
    • 여기서 Gσ는 가우시안 스무딩 마스크이며, *는 회선을 의미한다.

\Delta [G_{\sigma}(x, y) * f(x, y)] = [\Delta G_{\sigma}(x, y)] * f(x, y) = Log * f(x, y)

  • 이렇게 구성한 마스크를 LoG(Laplacian of Gaussian)라고 한다. LoG 마스크를 수식에 따라 풀면 다음과 같다.

LoG(x, y) = - {1 \over \pi \sigma^{4}} [1 - {x^{2} + y^{2} \over 2 \sigma^{2}}] \cdot e^{{-(x^{2} + y^{2}) \over 2 \sigma^{2}}}

  • 이 수식으로 마스크의 계수를 구성하고, 회선을 수행하면 잡음에 강한 에지를 검출할 수 있다.
    • 다만 수식에 따라 마스크 계수를 생성할 때 값의 범위가 너무 작은 관계로 전체 계수의 합이 0에 가까워지도록 스케일 조정이 필요하다.
  • LoG는 복잡한 공식에 의해 마스크를 생성해야 하며, 그에 따라 수행 시간도 많이 걸리게 된다. 이런 단점을 보완하여 LoG와 유사한 기능을 하면서 단순한 방법으로 구현하는 알고리즘이 바로 DoG(Difference of Gaussian)이다.
  • DoG는 가우시안 스무딩 필터링의 차이를 이용해서 에지를 검출하는 방식으로 공식은 아래와 같다.

DoG(x, y) = ({1 \over 2 \pi \sigma_{1}^{2}} \cdot e^{{-(x^{2} + y^{2}) \over 2 \sigma_{1}^{2}}}) - ({1 \over 2 \pi \sigma_{2}^{2}} \cdot e^{{-(x^{2} + y^{2}) \over 2 \sigma_{2}^{2}}}) (\sigma_{1} < \sigma_{2})

  • 두 개의 표준 편차를 이용해서 가우시안 마스크를 만들고 그 차이가 DoG 마스크가 된다.
    • 이 마스크로 회선을 수행하면 에지 검출이 가능하다. 여기서 각 표준 편차의 값을 조절함으로써 검출할 에지의 넓이를 조절할 수 있다.
    • 또한 좀 더 쉽게 DoG를 구현하는 방법은 두 개의 표준 편차로 가우시안 마스크를 생성하여 회선을 수행하고, 그 결과 행렬들의 차분을 계산하는 것이다.

캐니 에지 검출

  • 영상 내에서 잡음은 다른 부분과 경계를 이루는 경우가 많다. 그러다 보니 대부분의 에지 검출 방법이 이 잡음들을 에지로 검출하게 된다. 이런 문제를 보완하는 방법 중의 하나가 John F. Canny에 의해 개발된 캐니 에지(Canny Edge) 검출 방법이다.
  • 캐니 에지 알고리즘은 일반적으로 다음 네 단계의 알고리즘으로 구성되어 있다.
    1. 블러링을 통한 노이즈 제거(가우시안 블러링)
    2. 화소 기울기(gradiant)의 강도와 방향 검출 (소벨 마스크)
    3. 비최대치 억제(non-maximum suppression)
    4. 이력 임계값(hysteresis threshold)으로 에지 결정
  • 첫 단계에서 블러링은 5 x 5 크기의 가우시안 필터를 적용해서 수행한다. 여기서 블러링은 불필요한 잡음을 제거하기 위해서 수행하는 것이기 때문에 마스크의 크기를 다르게 하든지 혹은 다른 필터링을 적용해도 무관하다.
  • 다음으로 화소 기울기(gradient) 검출에는 가로 방향과 세로 방향의 소벨 마스크로 회선을 적용하고 회선이 완료된 행렬(Gx, Gy)를 이용해서 화소 기울기의 크기(magnitude)와 방향(direction)을 계산한다. 그리고 기울기의 방향은 4개 방향 (0, 45, 90, 135)로 근사하여 단순화 한다.
    • 여기서 아래 그림과 같이 기울기의 방향과 에지의 방향은 수직을 이루는 것에 유의하자.
  • 비최대치 억제(non-maximum suppression)라는 것은 현재 화소가 이웃하는 화소들보다 크면 에지로 보존하고 그렇지 않으면 에지가 아닌 것으로 간주해서 제거하는 것이다.
    • 먼저 에지의 방향에 있는 이웃 화소는 비교할 필요가 없기 때문에 아래 그림과 같이 기울기의 방향에 있는 두 개의 화소를 비교 대상으로 선택한다.
    • 그리고 현재 화소와 선택된 두 화소의 에지 강도를 비교하여 최대치가 아니면 억제되고, 최대치인 것만 에지로 결정한다.
  • 비최대치를 억제하여도 에지가 아닌 것이 에지로 결정된 경우가 많이 존재한다. 잘못된 에지를 제거하는 쉬운 방법 중 하나가 임계값을 설정하고, 에지의 강도가 이 임계값보다 작으면 에지에서 제외하는 것이다.
    • 그러나 이 방법은 임계값이 높으면 실제 에지도 제거될 수 있으며, 임계값이 낮으면 잘못된 에지를 제거하지 못하는 문제가 생길 수 있다.
  • 캐니 알고리즘은 잘못된 에지를 제거하고 정확한 에지만을 검출하여 에지가 끊어지는 것을 방지하는 방법으로 이력 임계값 방법(hysteresis thresholding)을 사용한다.
    • 이것은 두 개의 임계값 (Thigh, Tlow)을 사용해서 에지의 이력을 추적하여 에지를 결정하는 방법이다.
    • 이 방법은 각 화소에서 높은 임계값 (Thigh) 보다 크면 에지 추적을 시작한다.
    • 그리고 추적을 싲가하면 추적하지 않은 이웃 화소들을 대상으로 낮은 임계값 (Tlow) 보다 큰 화소를 에지로 결정하는 방식이다.
  • 아래 그림의 예시를 보면 A 부분은 높은 임계값보다 높아서 에지로 결정된다. C 부분은 높은 임계값보다 낮지만 에지 추적에 의해서 에지로 결정된다.
    • 반면 B 부분은 C 부분보다 에지의 강도는 높지만, 높은 임게값 보다 큰 이웃하는 부분이 없기 때문에 에지로 결정되지 않는다. 
    • 즉 낮은 임계값 이상인 에지들이 높은 임계값에서부터 연결되어 있다면 에지로 간주하는 것이다.

기타 필터링

최댓값/최솟값 필터링

  • 최댓값/최솟값 필터링은 입력 영상의 해당 화소(중심화소)에서 마스크로 씌워진 영역의 입력화소들을 가져와서 그 중에 최댓값 혹은 최솟값을 출력화소로 결정하는 방법이다.
  • 따라서 최댓값 필터링은 가장 큰 값인 밝은 색들로 출력화소가 구성되기 때문에 돌출되는 어두운 값이 제거되며, 전체적으로 밝은 영상이 된다.
    • 최솟값 필터링은 가장 작은 값들인 어두운 색들로 출력화소가 구성되기 때문에 돌출되는 밝은 값들이 제거되며, 전체적으로 어두운 영상이 된다.
    • 최댓값 필터링은 밝은 임펄스 잡음이 강조되며, 최솟값 필터링은 어두운 임펄스 잡음이 강조될 수 있다. 경우에 따라 높은 대조를 가진 영상에서 특징을 확대시키기 위한 기법으로 이용될 수 있다.

평균값 필터링

  • 평균값 필터링은 마스크로 씌워진 영역의 입력화소들을 가져와서 그 화소들의 평균을 구하여 출력화소로 지정하는 방법이다.
    • 마스크 영역의 화소값들을 평균하기 때문에 블러링의 효과가 나타난다.
  • 입력 영상의 상하좌우 끝부분에 있는 화소들은 마스크를 씌웠ㅇㄹ 때 입력화소가 존재하지 않는 화소들이 있다.
    • 3 x 3 마스크일 경우에는 상하좌우로 한 화소씩이 해당하며, 5 x 5 마스크일 경우에는 상하좌우 두 화소씩이 해당된다.
    • 이 경우 배열 참조가 잘못되어 오류가 발생하기 때문에 추가적인 방법을 적용해서 출력화소를 결정해야 한다.
    • OpenCV에서도 cv::filter2D(), cv::blur(), cv::boxFilter(), cv::sepFilter2D()와 같은 필터링을 수행하는 함수들에서 상하좌우 경계부분의 화소값을 결정하는 방법으로 borderType 이라는 옵션 상수를 다음과 같이 정의해 두었다.
옵션 상수 설명
BORDER_CONSTANT 0 특정 상수값으로 대체
BORDER_REPLICATE 1 계산 가능한 경계의 출력화소 하나만으로 대체
BORDER_REFLECT 2 계산 가능한 경계의 출력화소로부터 대칭되게 한 화소씩 지정
BORDER_WRAP 3 영상의 왼쪽 끝과 오른쪽 끝이 연결되어 있다고 가정하여 한 화소씩 가져와서 지정

미디언 필터링

  • 미디언 필터링은 중간값을 이용하기에 중간값 필터링이라고도 한다.
  • 중간값 필터링 과정은 아래 그림과 같다.
    • 먼저 입력 영상에서 해당 입력화소를 중심으로 마스크를 씌워 마스크 크기 내에 있는 입력화소들을 가져온다. 회선과는 다르게 마스크 계수는 필요하지 않고, 마스크의 크기만 필요하다.
    • 그리고 마스크 내에 있는 화소들을 크기 순으로 정렬한다.
    • 정렬된 화소값 중에서 중간 값을 취하여 출력 화소로 지정한다.
    • 이와 같은 과정을 마스크를 이동하며 모든 입력솨호에 대해 수행해서 출력영상을 생성한다.
  • 일정 영역에서 다른 화소들과 밝기가 심하게 차이가 나는 화소들은 임펄스(impulse noise) 잡음이나 소금-후추(salt & pepper) 잡음일 가능성이 높다.
    • 미디언 필터링 과정에서 마스크 영역 내의 심하게 차이가 나는 화소들은 정렬로 인해서 최하위 값이나 최상위 값이 된다. 따라서 중간 값만이 출력화소로 지정되고, 나머지 값들은 출력화소로 지정되지 않고 제거된다.
    • 이러한 이유로 미디언 필터링은 임펄스 잡음이나 소금-후추 잡음을 잘 제거해 준다. 또한 평균 필터를 이용한 필터링에 비하면 블러링 현상이 적다.
    • 다만 마스크의 크기가 커지면 잡음의 제거 성능은 향상되지만, 정렬 알고리즘을 수행해야 하는 부담 때문에 수행 시간이 기하급수적으로 증가한다.
  • 미디언 필터링은 보통 명암도 영상에서 효과적으로 수행된다.
    • RGB 컬러 공간에서는 3개 채널간의 상호 의존도가 매우 크다. 예컨대 한 채널에서는 특정 화소가 주위와 심한 차이를 보여서 제거되어도, 다른 채널에서는 같은 화소가 주위와 차이가 작아서 제거되지 않는 경우들이 생긴다. 이런 경우 RGB 조합이 맞지 않아서 오히려 잡음이 더 많아질 수도 있다.

가우시안 스무딩 필터링

  • 스무딩(smoothing)은 영상의 세세한 부분을 회선을 통해서 부드럽게 하는 기법으로 블러링과 같은 의미이다.
  • 스무딩 처리에 사용되는 대표적인 방법으로 가우시안 필터링이 있다. 가우시안 필터링은 가우시안 분포를 마스크의 계수로 사용해서 회선을 수행하는 것을 말한다.
    • 가우시안 분표는 정규 분포(normal distribution)으로 특정 값의 출현 비율을 그래프로 그렸을 때, 평균에서 가장 큰 수치를 가지며, 평균을 기준으로 좌우 대칭의 형태가 나타나고, 좌우 양끝으로 갈수록 급격하게 수치가 낮아지는 종 모양의 형태를 보인다.
    • 정규 분포를 평균(\mu )과 표준편차(\sigma )를 이용해서 함수식으로 표현하면 아래 수식과 같고, 이 수식에 따라 그래프를 그리면 아래 그림과 같다.
    • 그래프에서 표준 편차가 커지면 그래프의 폭이 넓어지고 표준편차가 작아지면 폭이 좁하진다.

N(\mu, \sigma)(x) = {1 \over \sigma \sqrt{2\pi}} exp(- {(x - \mu)^{2} \over 2 \sigma^{2}})

 

  • 가우시안 분포를 회선 마스크로 적용하려면 2차원으로 구성해야 한다. x, y 좌표를 축으로 2차원 가우시안 분포 함수를 구성하면 다음의 수식과 같고 이것을 그래프로 표현하면 아래 그림과 같다.

N(\mu, \sigma_{x}, \sigma_{y})(x, y) = {1 \over \sigma_{x} \sigma_{y} \sqrt{2\pi}} exp[- ({(x - \mu)^{2} \over 2 \sigma_{x}^{2}} + {(y - \mu)^{2} \over 2 \sigma_{y}^{2}})]

  • 이 가우시안 분포 값으로 마스크를 구성하여 회선을 수행하면 가우시안 스무딩을 수행할 수 있다.
    • 단, 마스크 계수의 전체 합이 1이 되어야 입력 영상의 밝기를 유지할 수 있다.
    • 여기서 표준편차를 변경하면 그래프에서 기둥의 폭을 조절할 수 있다. 즉, 마스크 계수의 구성을 표준편차로 조정하는 것이다.
    • 표준편차가 클수록 평균의 높이는 낮아지고, 폭이 넓어진다. 따라서 생성되는 마스크는 블러링 마스크와 유사하게 만들어진다. 결과 영상에 중심화소와 비슷한 비중으로 주변 화소가 반영되기 때문에 흐림의 정도가 심해진다.
    • 표준편차가 작아지면 구성된 마스크에서 중심계수의 값이 커지고, 주변 계수의 값은 작아진다. 이럴 경우 결과 영상에 중심화소의 비중이 커지기 때문에 흐림의 정도가 약화된다.

모폴로지(morphology)

  • 모폴로지는 형태학이라는 뜻으로서 영상 처리에서는 객체들의 형태(shape)를 분석하고 처리하는 기법을 말한다. 이 형태학적 처리를 활용하여 영상의 경계, 골격, 블록 등의 형태를 표현하는데 필요한 요소를 추출한다.
  • 영상 내에서 아주 작은 크기의 객체들은 잡음일 가능성이 높다. 이런 작은 크기의 객체는 그 크기를 더 깎아내서 제거하거나, 객체들을 분리하거나, 객체를 팽창하거나 할 때 필요한 것이 모폴로지이다.
  • 모폴로지는 객체의 형태를 변형시켜야 하기 때문에 주로 이진 영상에서 수행된다. 대표적인 연산 방법으로는 침식 연산과 팽창 연산이 있으며, 이 두 개를 결합한 닫힘 연산과 열림 연산이 있다.

침식 연산(erosion operation)

  • 침식 연산은 말 그대로 객체를 침식시키는 연산이다. 따라서 객체의 크기는 축소되고 배경은 확장된다. 객체의 크기가 축소되기 때문에 영상 내에 존재하는 잡음 같은 작은 크기의 객체들은 사라질 수도 있다.
  • 이러한 현상을 이용해서 소금-후추(salt & papper) 잡음과 같은 임펄스(impulse) 잡음들을 제거한다. 영상 내에서 객체의 돌출부를 감소시키기 때문에 서로 닿는 물체를 분리할 때도 유용하게 사용할 수 있다.
  • 위 그림은 이진 영상에서 침식 연산의 과정에 대한 내용이다. 
    • 입력 영상의 중심화소 주변에서 마스크 크기의 화소들을 가져와서 침식 마스크와 원소 간(element-wise)에 일치하는지를 비교한다. 여기서 침식 마스크의 원소가 1인 값에 대해서만 비교를 수행한다.
    • 그림의 상단과 같이 입력 영상의 화소와 마스크 원소가 한 개의 화소라도 일치하지 않으면 출력화소는 검은색인 0이 된다.
    • 반면 그 아래쪽의 그림과 같이 입력 영상의 화소와 마스크 원소와 비교해서 모두가 일치하면 출력화소는 흰색인 1이 지정된다.
    • 마스크의 크기와 원소의 구성은 입력 영상의 형태에 따라 사용자가 조정하여 더 나은 결과를 생성할 수 있다.
  • 아래 그림은 입력 영상에 값을 표시하고 침식 연산을 수행하여 결과 영상에 값을 표시한 예이다.
    • 입력된 이진 영상에서 객체로 인식되는 흰색의 블록들은 그 경계부분이 깎여져서 출력 영상이 만들어진다.

팽창 연산(dilation operation)

  • 팽창 연산은 객체를 팽창시키는 연산이다. 객체의 최외곽 화소를 확장시키는 기능을 하기 때문에 객체의 크기는 확대되고 배경은 축소된다. 또한 객체의 팽창으로 인해 객체 내부에 있는 빈 공간도 메워지게 된다.
  • 위 그림은 이진 영상에서 팽창 연산을 수행하는 과정을 나타낸 것이다.
    • 마스크 범위의 입력화소와 팽창 마스크의 원소 간(element-wise)에 일치하는 지를 비교한다. 팽창 마스크가 1인 원소와 해당 입력화소가 모두 불일치하면 출력화소로 0을 지정한다. 그리고 1개 화소라도 일치하게 되면 1이 출력화소로 결정된다.
  • 아래 그림은 팽창 연산의 결과를 예시한 것이다.
    • 팽창 연산 수행 결과로 객체의 외각이 확장되며 객체 내부의 빈 공간이 경계부분의 확장으로 인해 메워진다. 
    • 반면에 잡음으로 예상되는 작은 크기의 객체도 확장되는 것을 볼 수 있다.

열림 연산과 닫힘 연산

  • 열림 연산과 닫힘 연산은 모폴로지의 기본 연산인 침식 연산과 팽창 연산의 순서를 조합하여 수행한다.
  • 열림 연산(opening operator)은 침식 연산을 먼저 수행하고, 바로 팽창 연산을 수행한다.
    • 침식 연산으로 인해 객체는 축소되고, 배경 부분의 미세한 잡음들은 제거된다.
    • 다음으로 축소되었던 객체들이 팽창 연산으로 인해 다시 원래 크기로 돌아간다.
  • 아래 그림은 열림 연산의 과정을 예시한 것이다. 배경 부분의 잡음을 제거하면서 침식 연산으로 인한 객체 크기의 축소를 방지할 수 있다. 
    • 다만 돌출된 부분은 제거된 후 다시 원래 크기로 돌아가지 않는다.
  • 닫힘 연산(closing operator)은 팽창 연산을 먼저 수행하고, 다음으로 침식 연산을 수행한다.
    • 팽창 연산으로 객체가 확장되어서 객체 내부의 빈 공간이 메워진다.
    • 다음으로 침식 연산으로 확장되었던 객체의 크기가 원래대로 축소된다.
    • 최종 결과 영상을 보면 객체 내부의 비어있던 공간이 채워지며, 인접한 객체를 이어지게 하는 효과도 있다.
    • 모폴로지 연산은 한 번의 수행으로 결과 영상이 미흡할 경우에는 여러 번 반복적으로 수행할 수 있다.