728x90

이미지를 다루다 보면 다양한 포맷들을 볼 수 있습니다. 실생활에서도 비트맵 Bitmap 이미지를 많이 사용하기도 하며 MFC와 C# 윈폼에서 화면에 그려주는 이미지가 비트맵이기도 합니다.

MFC (Microsoft Foundation Class) 라이브러리를 이용하여 비트맵을 만들어 화면에 출력하거나 파일로 저장하는 과정을 살펴 보겠습니다. 단계별로 보면, 메모리 DC 생성을 하고 비트맵 객체 생성, 그리기 작업을 수행합니다. 이후 비트맵 이미지를 저장하거나 화면에 출력하고 리소스 해제를 하면 끝입니다.

 

GDI (Graphics Device Interface) 그리기

memDC.FillSolidRect(0, 0, width, height, RGB(255, 255, 255)); // 배경 흰색

memDC.TextOutW(10, 10, _T("Hello Bitmap!!!")); // 텍스트 출력 

화면에 출력 또는 저장

pDC->BitBlt(0, 0, width, height, &memDC, 0, 0, SRCCOPY);  // 화면 출력

 

CImage image; // 저장

image.Attach((HBITMAP)bitmap.Detach()); // CBitmap → HBITMAP → CImage

image.Save(_T("output.bmp")); 

리소스 해제

memDC.SelectObject(pOldBitmap); // 원래 비트맵으로 복원

bitmap.DeleteObject(); // 리소스 해제

memDC.DeleteDC(); // 리소스 해제

 

OnDraw 함수를 이용한 전체 코드는 아래와 같습니다. 이미지를 화면에 출력시 Device Context(pDC)의 BitBlt 함수를 이용하는 것을 볼 수 있습니다. 함수 별 활용법을 알아두면 실무 개발에 많은 도움이 될 수 있습니다.

 

void CMyView::OnDraw(CDC* pDC)
{
    int width = 100;
    int height = 100;

    CDC memDC;
    memDC.CreateCompatibleDC(pDC);

    CBitmap bitmap;
    bitmap.CreateCompatibleBitmap(pDC, width, height);
    CBitmap* pOldBitmap = memDC.SelectObject(&bitmap);

    // 배경 및 텍스트 출력
    memDC.FillSolidRect(0, 0, width, height, RGB(200, 220, 255));
    memDC.TextOutW(10, 10, _T("Hello Bitmap!!!"));

    // 화면에 출력
    pDC->BitBlt(0, 0, width, height, &memDC, 0, 0, SRCCOPY);

   
    // 리소스 정리
    memDC.SelectObject(pOldBitmap);
    memDC.DeleteDC();
}

 

 

추가로 비트맵 헤더 정보는 아래와 같고 실제 프로그래밍 시 각 요소에 접근이 필요할 때가 있습니다. 압축 설정에는 여러 종류가 있으며 일반적으로 무압축 BI_RGB를 설정 합니다.

 

typedef struct tagBITMAPINFOHEADER {
  DWORD biSize;
  LONG  biWidth;
  LONG  biHeight;
  WORD  biPlanes;
  WORD  biBitCount;
  DWORD biCompression;
  DWORD biSizeImage;
  LONG  biXPelsPerMeter;
  LONG  biYPelsPerMeter;
  DWORD biClrUsed;
  DWORD biClrImportant;
} BITMAPINFOHEADER, *LPBITMAPINFOHEADER, *PBITMAPINFOHEADER;

 

 

728x90
728x90

MFC를 활용하여 GUI(User Interface) 구현 시, DC, CDC, HDC, HWND 등을 정확히 이해 못하고 활용할 때가 많습니다. Window GUI Programming에 핵심적인 개념으로 명확히 이해하고 있으면 MFC 기반 어플리케이션 개발이 쉬워질 수 있습니다.

 

  • DC (Device Context) - 그리기 작업을 수행하기 위한 핸들 또는 구조체
  • CDC - MFC에서 제공하는 HDC를 C++ Class 형태로 래핑한 Class
  • HWND - 윈도우 핸들 Handle, MFC에서는 CWnd Class가 HWND를 래핑

 1. DC: Device Context

윈도우에서 화면, 프린터 등 출력 장치에 텍스트나 그래픽을 출력할 때 사용되며, HDC라는 핸들로 윈도우 API 함수 GetDC와 ReleaseDC를 통해 가져오고 해제합니다. 메모리를 위한 DC는 CreateCompatibleDC 함수가 있습니다.

 

HDC hdc = ::GetDC(hWnd); // HDC: Handle to Device Context

TextOut(hdc, 30, 30, _T(”Device Context: DC !!!”), 10);

::ReleaseDC(hWnd, hdc);

 

2. CDC

HDC를 객체지향적으로 래핑한 MFC 클래스 입니다. 메서드를 통해 구조화된 방식으로 그리기 작업을 쉽게 할 수 있으며, HDC보다 더 다양한 멤버 함수를 제공합니다. MFC에서는 화면, 프린터, 메모리 등 다양한 DC를 파생 클래스로 나눠서 지원 합니다. 예를 들어 CClientDC는 클라이언트 영역 DC, CPaintDC는 WM_PAINT 메시지 핸들링, CWindowDC는 윈도우 전체 영역 그림, 그리고 CMemDC 등이 있습니다.

 

CClientDC dc(this); // this는 CWnd 포인터

dc.TextOut(30, 30, _T(”Device Context: DC !!!”));

 

3. HWND: Window Handle

윈도우에서 각 창을 고유하게 식별하기 위한 ID로 보면 됩니다. MFC에서는 CWnd 클래스가 HWND를 래핑합니다. GetSafeHwnd()로 HWND를 가져올 수 있습니다.

 

// HWND > HDC > CDC

HWND hWnd = this->GetSafeHwnd();

HDC hdc = ::GetDC(hWnd); // HDC: Handle to Device Context

TextOut(hdc, 30, 30, _T(”Device Context: DC !!!”), 10);

::ReleaseDC(hWnd, hdc);

 

정리하면 HWND는 윈도우 창 식별자로 특정 창에 접근할 때 사용하며, HDC는 디바이스 컨텍스트 핸들로 GDI로 그림 그릴때 사용합니다. CDC는 MFC의 DC 클래스로 그리기 함수 및 리소스를 관리합니다. 이 세가지가 맞물려서 MFC 기반 앱에서 시각적 요소들을 처리합니다. 비유하면, HWND는 화면위에 창문, HDC는 그 창문에 그림을 그릴 수 있게 해주는 붓, CD는 그 붓을 잘 다뤄주는 도구함이라고 생각해 볼 수 있습니다. 

 

추가로 메모리 DC를 활용하면 화면 깜빡임 없는 출력이 가능합니다. 더블 버퍼링 Double-Buffering 기법이라고도 합니다.

 

CDC memDC;
memDC.CreateCompatibleDC(&dc);

CBitmap bitmap;
bitmap.CreateCompatibleBitmap(&dc, 100, 100);

CBitmap* pOldBitmap = memDC.SelectObject(&bitmap);
memDC.FillSolidRect(0, 0, 100, 100, RGB(255, 255, 255));
memDC.TextOutW(10, 10, _T("Off-screen drawing"));

dc.BitBlt(0, 0, 100, 100, &memDC, 0, 0, SRCCOPY);
memDC.SelectObject(pOldBitmap);
728x90
728x90

C++/MFC를 활용하여 이미지 처리 알고리즘 등 User Interface 및 Display 목적으로 코드 구현 시 파일 오픈 또는 외부 프로그램을 실행시켜야 할 때가 있습니다. 이때 사용할 수 있는 함수가 Window API로 ShellExecute 가 있습니다. 

 

MSDN에는 아래와 같이 정의 되어 있고, 3가지 이용할 수 있는 방법이 나와 있습니다.

 

HINSTANCE ShellExecute(

HWND hwnd,

LPCTSTR lpOperation,

LPCTSTR lpFile,

LPCTSTR lpParameters,

LPCTSTR lpDirectory,

INT nShowCmd

);

 

1) ShellExecute(handle, NULL, path_to_folder, NULL, NULL, SW_SHOWNORMAL);

2) ShellExecute(handle, “open”, path_to_folder, NULL, NULL, SW_SHOWNORMAL);

3) ShellExecute(handle, “explore”, path_to_folder, NULL, NULL, SW_SHOWNORMAL);

 

웹 주소 연결하려 웹을 열고 임의의 프로그램을 활성화하는 예제 입니다. 인터넷 웹을 활성화하는 방법은 두가지로 구분되며 HTTP 프로토콜을 사용 유무에 있습니다.

[Web(Internet)]

방법 #1.

HWND hWnd = ::FindWindow(NULL, “IEFrame”);

::ShellExecute(hWnd, “Open”, “www.kkk.co.kr”, NULL, NULL, SW_SHOW)

 

방법 #2.

ShellExecute(NULL, “Open”, “http://www.kkk.co.kr/aaa”, NULL, NULL, SW_SHOW) 

[프로그램 (실행파일 .exe)]

ShellExecute(NULL, NULL, “program.exe”, NULL, NULL, SW_SHOWNORMAL)

 

ShellExecute 함수를 찾다보면 ShellExecuteEx 함수를 볼 수 있습니다. ShellExecuteEx도 웹 및 문서 열기, 애플리케이션 실행, 프린터 명령 등 다양한 작업을 윈도우 쉘을 통해 수행할 수 있게 해주며, ShellExecute 함수 보다 더 많은 기능을 제공합니다. 간략히 ShellExecuteEx 함수 사용법은 아래와 같습니다.

 

> SHELLEXECUTEINFO 를 설정 및 초기화하고 Size 설정

> ShellExecuteEx의 멤버 변수들 설정: __TEXT(SzFileName)에서 SzFileName은 초기 설정되어 있는 Path가 바뀔수 있기 때문에 그 실행 프로그램 위치를 고정시켜 줄 필요가 있고, Path가 변할일이 없으면 “program.exe”으로 직접 넣어 주면 됩니다.

// 예제 #1.
SHELLEXECUTEINFO Eexe;
ZeroMemory(&Eexe, sizeof(SHELLEXECUTEINFO);
Eexe.cbSize = sizeof(SHELLEXECUTEINFO);
Eexe.lpFile = __TEXT(SzFileName);
Eexe.nShow = SW_SHOWMAXIMIZED;
Eexe.lpVerb = __TEXT(”open”);
ShellExecuteEx(&Eexe);

 

// 예제 #2.
SHELLEXECUTEINFO Eexe= { 0 };
Eexe.cbSize = sizeof(SHELLEXECUTEINFO);
Eexe.fMask = SEE_MASK_NOCLOSEPROCESS;
Eexe.hwnd = NULL;
Eexe.lpVerb = L"open"; // L"runas" (관리자 권한 실행)
Eexe.lpFile = L"notepad.exe"; // 실행할 파일
Eexe.lpParameters = L""; 
Eexe.lpDirectory = NULL; // 디렉터리
Eexe.nShow = SW_SHOW;
Eexe.hInstApp = NULL;

if (ShellExecuteEx(&Eexe)) { // 성공적으로 실행됨    
    if (Eexe.hProcess != NULL) {
        // 실행된 프로세스가 종료될 때까지 대기
        WaitForSingleObject(Eexe.hProcess, INFINITE);
        CloseHandle(Eexe.hProcess);
    }
} else {
    MessageBox(NULL, L"실행 실패", L"오류", MB_OK | MB_ICONERROR);
}

// 오류처리
if (!ShellExecuteEx(&Eexe)) {
    DWORD err = GetLastError();
    wprintf(L"실패, 오류 코드: %lu\n", err);
}

 

728x90
728x90

이미지 처리에서 알고리즘 구현 시 픽셀 단위 처리나 필터 처리 등이 빈번하게 사용됩니다. 이는 이미지의 데이터 형식이 2차원 행렬 구조이기 때문에 그렇습니다. 영상처리 라이브러리인 OpenCV에서는 이미지 처리를 위해 데이터 타입 Mat을 제공합니다. CV의 여러 함수들 내부에서도 Mat 형식을 통한 처리가 기본이며, CV를 세부처리에 활용하기 위해서는 Mat 활용 방법을 정확히 알고 있어야 합니다.

 

행렬 Matrix와 배열 Array의 차이는 행렬은 X좌표와 Y좌표로 이루어진 2차원 배열을 의미하며, 배열은 1차원, 2차원, 3차원 모두를 포함합니다. 행렬 연산 관련 항목들을 찾다 보면 조밀 행렬 Dense Matrix와 희소 행렬 Sparse Matrix 형식을 볼 수 있습니다. 조밀 행렬은 배열의 모든 값이 의미 있는 값으로 채워져 있는 배열이며, 희소 행렬에서는 “0”이 아닌 값들을 의미 있는 값으로 보게 됩니다. 다시 말해 메모리 공간 확보 시 조밀 행렬은 전체 배열을 할당 해야하지만 희소 행렬은 “0”이 아닌 값만 메모리 할당을 하게 되어 메모리 절약을 할 수 있습니다.

 

Mat 형식은 Create 메서드로 객체 값을 할당 할 수도 있지만 생성자 여러 개로 오버로드 되어 있습니다. 메서드 및 예제는 C/C# 구현 예입니다. 

[Create 메서드]

Create( MatType type, params int[] sizes )

Create( Size size, MatType type )

Create( int rows, int cols, MatType type )

[ 생성자 ]

Mat()

Mat( int rows, int cols, MatType type )

Mat( int rows, int cols, MatType type, Scalar s )

Mat( Size size, MatType type )

Mat( Size size, MatType type, Scalar s )

 

기본적인 사용법은 아래와 같습니다. “CV_8UC3”은 8 bits Unsigned Char의 3채널을 의미합니다.

 

Mat M = new Mat();

예제 1) M.Create( MatType.CV_8UC3, new int[] { 240, 320 } );

예제 2) M.Create( new Size(320, 240), MatType.CV_8UC3 );

예제 3) M.Create( 240, 320, MatType.CV_8UC3 );

 

예제 4) Mat M( 240, 320, MatType.CV_8UC3 );

예제 5) Mat M( new Size(320, 240), MatType.CV_8UC3 );

 

Mat 클래스에서는 아래와 같은 기본 행렬 표현식을 사용할 수 있게 정적 메서드를 제공합니다. 기본 행렬 표현식은 단일 채널에 값을 할당하므로 주의해서 사용하면 됩니다.

[ 정적 메서드 ]

Mat.Eye( int rows, int cols, MatType type ) : 단위 행렬

Mat.Ones( int rows, int cols, MatType type ) : “1”로 채워진 행렬

Mat.Zeros( int rows, int cols, MatType type ) : “0”으로 채워진 행렬

Mat.Diag( Mat d ) : 대각행렬

 

예제) Mat M = Mat.Eye( new Size(3,3), MatType.CV_64FC1 ); // 64 bits double 1채널

 

Mat 데이터를 생성하고 처리했으면 행렬 요소에 접근 방법을 알아야합니다. 요소에 접근해서 값을 가져올때는 At() 또는 Get() 메서드를 사용하고, 값을 설정 할때는 Set() 메서드를 사용할 수 있습니다. 아래는 2차원 행렬 접근시 사용 예입니다. 행렬의 블럭 단위로 요소에 접근할 수 있는 메서드들 중에는 M.Row.Get, M.Col.Get, M.Row.Set, M.Col.Set 도 있습니다. 

[ 픽셀 접근 ]

M.At( i, j ) // 행렬 M의 i, j 위치 요소, (row, col>

M.Get( i, j ) // 행렬 M의 i, j 위치 요소

M.Set( i, j, value ) // 행렬 M의 i, j 위치 요소를 value로 설정 

 

예제) M.At(1, 1) // MatTyped이 CV_64FC1이면 type은 double

728x90
728x90

앞에 블로그에서 설명한 것 처럼 푸리에 변환은 시간 영역의 신호를 주파수 영역으로 변환할 수 있는 이론입니다. 푸리에 변환 기본을 아래 블로그로 들어 가시면 참고 할 수 있습니다.

 

2025.06.01 - [영상처리 기술] - 영상처리 푸리에 변환 Fourier Transform 이해와 기본 (Python)

 

영상처리 영역에서는 이미지 평면을 주파수 평면으로 변환하여 분석할 수 있는 도구로 사용할 수 있습니다. 실무에서는 속도와 구현 이점 때문에 고속 푸리에 변환 FFT를 사용합니다. Python, C/C++, Matlab 등 모두에서 이미지 처리시 사용하는 함수 명칭이 “fft” 로 되어 있는 이유가 그 이점 때문에 그렇습니다. 푸리에 변환과 같은 이론들을 테스트 해 볼 수 있는 라이브러리들이 많아지고 있습니다만 단계별 처리 과정이 어떻게 되는지 이해를 하고 활용 또는 응용을 한다면 더 효과 적으로 사용할 수 있습니다.

 

2D Discrete Fourier Transform(DFT) 수식은 아래와 같습니다. F[k,l]은 주파수 영역, f[m,n]은 이미지 평면을 나타냅니다. 

 

푸리에 변환을 통해 얻을 수 있는 값들은 스펙트럼 Spectrum과 페이즈 Phase 정보 입니다. 변환된 수식에서 F[k,l]의 크기를 Spectrum 또는 Magnitude를 의미하고, 각도를 Phase로 부릅니다. 일반적으로 이미지 처리에서 활용하는 정보는 Spectrun(Magnitude)입니다. 

 

이미지를 활용하여 주파수 영역의 스펙트럼을 구하는 순서를 살펴 보면, 

1) 이미지 Width와 Height가 각 2^N에 맞게 조정 후 Zero Padding  

2) 이미지 X 방향 푸리에 변환하고 그 결과에 Y방향으로 푸리에 변환

3) 각 모서리에 집중되어 있는 저주파 영역 DC 정보를 중심으로 이동 시키기 위한 DC Shift

4) 주파수 영역은 그림에서 상단 두 번째 이미지 결과 형태

 

 

 

FFT에서는 2에 거듭제곱에 비례한 신호 또는 픽셀 수를 이용하여 짝수와 홀수로 나누어 변환을 수행합니다. 이를 Butterfly Operation이라고 하며, 처리 속도를 향상 시킬수 있고 하드웨어적인 구현에도 이점을 가질 수 있습니다. 따라서 이미지에 대해서도 2에 거듭제곱에 비례하게 크기를 조절 후 변환하게 됩니다. 예를 들어 500X500 크기를 갖는 이미지라면 2의 N 승배인 512X512로 크기 조절 및 Zero Padding 후 처리하게 됩니다.

 

그림에서 하단 첫번째 처럼 주파수 영역 중심 부분을 필터링하고 역변환하면 하단 두 번째 이미지 결과를 얻는데, 저주파 영역을 마스크 처리 했으니 고주파 특성의 에지영역들만 남길 수 있습니다. 이러한 처리를 Lowpass Filtering 하며, 여러 필터링 방법들을 이용하여 이미지 처리에 응용할 수 있습니다.

728x90
728x90

실무에서 영상처리 알고리즘을 구현할 때 관심 영역이나 물체의 외곽선을 탐색하고 추출할 상황이 많습니다. 물체의 대략적인 위치 확인을 위해 탐색할 때도 있지만 세부적인 외곽선이 필요한 상황도 알고리즘 중간 중간 필요할 때가 있습니다. 이때 사용할 수 있는 함수가 OpenCV의 “findConturs” 함수 이며 파이썬 또는 C/C++에서 아래 형식으로 적용해 볼 수 있습니다. 출력에 “contours”는 검출된 윤곽선을 저장한 포인트 리스트 이며, “hierachy”는 윤곽선의 계층 구조를 저장할 배열 입니다.

 

C/C++ 활용

void cv::findContours( 

           InputArray image,                            // 8-bit 그레이 또는 이진 이미지 

           OutputArrayOfArrays contours,          // (출력) contours

           OutputArray hierarchy,                     // (출력) hierarchy

           int mode,                                       // (아래 설명)

           int method,                                    // (아래 설명)

           Point           offset = Point() )           // (옵션) 설정된 offset 값 만큼 이동

 

Python 활용

contours, hierarchy = cv.findContours( image, mode, method[, contours[, hierarchy[, offset]]]) 

 

실무 활용시에는 “mode”와 “method”를 알고리즘에 맞게 적절히 설정할 줄 알아야 원하는 결과를 얻을 수 있으며, 일반적으로 입력 이미지는 이진 binary 이미지를 사용합니다. “mode”에서 윤곽선을 정의할 때 물체가 한 덩어리로 될 수 있지만, 구멍이 있는 영역들도 존재하기 때문에 계층 구조로 윤곽선을 추출 할 수 있게 설정할 수 있습니다. “method”는 좌표 리스트들을 어떻게 근사화해서 추출 할 지에 대한 설정 이며, 3)과 4)의 CHAIN_APPROX는 1989는 PAMI에 발표된 “On the detection of dominant points on digital curves” 논문에 기반한 방법이니 참조해 보시면 좋을 듯 합니다.

 

모드 mode

1) RETR_EXTERNAL (Python: cv.RETR_EXTERNAL): 최외곽선 검출

2) RETR_LIST (Python: cv.RETR_LIST): 모든 윤곽선 검출

3) RETR_CCOMP (Python: cv.RETR_CCOMP): 2-Level 계측 구조로 윤곽선 검출

4) RETR_TREE (Python: cv.RETR_TREE): 모든 윤곽선을 계층 구조로 검출

 

메소드 method

1) CHAIN_APPROX_NONE (Python: cv.CHAIN_APPROX_NONE): 모든 포인트에 대한 좌표를 추출

2) CHAIN_APPROX_SIMPLE (Python: cv.CHAIN_APPROX_SIMPLE): 수평, 수직, 대각 방향의 끝 좌표만 추출, 예를 들어 물체가 사각형이라면 4개의 끝점만 추출.

3) CHAIN_APPROX_TC89_L1 (Python: cv.CHAIN_APPROX_TC89_L1)

4) CHAIN_APPROX_TC89_KCOS (Python: cv.CHAIN_APPROX_TC89_KCOS)

 

아래는 파이썬 코드 예이며, 응용에서는 외곽선을 추출하고 “contourArea”와 “arcLength” 함수를 이용하여 물체나 ROI의 면적과 둘레를 구할 수 있습니다.

 

import cv2
import numpy as np

# 1. 흑백 배경에 흰색 사각형 
img = np.zeros((500, 500, 3), dtype=np.uint8)
cv2.rectangle(img, (50, 50), (350, 350), (255, 255, 255), -1) 

# 2. 이진화
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
ret, thresh = cv2.threshold(gray, 127, 255, cv2.THRESH_BINARY)

# 3. 윤곽선 찾기
contours, _ = cv2.findContours(thresh, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
#contours, hierarchy = cv2.findContours(thresh, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)

# 4. 면적, 둘레 구하기
for i, contour in enumerate(contours):
    area = cv2.contourArea(contour) #면적
    perimeter = cv2.arcLength(contour, True) #둘레
    
    print(f"{area}")
    print(f"{perimeter}")
    
    cv2.drawContours(img, [contour], -1, (0, 0, 255), 3)

# 5. 결과 출력
cv2.imshow('Contours with Area & Perimeter', img)
cv2.waitKey(0)
cv2.destroyAllWindows()
728x90
728x90

이미지 처리에서 영역을 분할하는 방법들은 다양합니다. OpenCV에서 제공하는 기능 중 floodFill도 영역을 분할 하는 방법들 중에 하나 입니다. 응용에서는 물체 추적에 전처리나 배경 제거, 물체의 영역 채우기 등에 활용됩니다.

 

아래는 파이썬에서 활용하는 함수 형태와 C/C++에서 사용하는 함수 예 입니다. C/C++에서 활용하는 함수는 두 가지 형태인데 mask 유무가 차이 입니다. 

 

(Python) retval, image, mask, rect = cv2.floodFill(image, mask, seedPoint, newVal,

                                                                                 loDiff=None, upDiff=None, flags=None)

 

(C/C++) int floodFill(InputOutputArray image, Point seedPoint, Scalar newVal,

                                  Rect* rect = 0, Scalar loDiff = Scalar(), Scalar upDiff = Scalar(), int flags = 4);

 

(C/C++) int floodFill(InputOutputArray image, InputOutputArray mask, Point seedPoint, 

                    Scalar newVal, Rect* rect = 0, Scalar loDiff = Scalar(), Scalar upDiff = Scalar(), int flags = 4);

 

파라미터 설명은 아래와 같으며 실무 적용에서는 mask 활용할 때와 아닐 때만 유의하면 됩니다. 

 

1) image: 입력 이미지이자 처리 대상 이미지이며 그레이 이미지와 칼라 이미지 적용 가능합니다.

2) mask: 어떤 부분을 채울 때 채우기 작업을 제한하는 역할을 합니다. 예를 들어 영역의 에지를 추출한 Binary 이미지라면 “0”인 부분만 특정 값으로 채울 수 있습니다. 8-bit 그레이 이미지로 정의해야 하며, 원본 이미지보다 가로와 세로가 2 픽셀씩 커야 합니다. 추가로 마스크의 테두리는 “1”로 채우며, 동일한 마스크를 여러번 사용해도 채워진 영역이 겹치지 않도록 할 수 있습니다.

3) seedPoint: 값을 변경하는 시작 좌표(x, y) 입니다.

4) newVal: 변경할 픽셀값 또는 색상 입니다. 칼라라면 (255, 10, 10) 형태가 됩니다.

5) loDiff: 변경할 픽셀값의 하한값 입니다.

6) upDiff: 변경할 픽셀값의 상한값 입니다.

7) flag: 변경 처리 조건을 지정합니다.

 

flag에는 아래 두 가지가 있으며, 실제 적용 시에는 픽셀 연결 방식인 4방향 또는 8방향과 조합해서 사용할 수 있습니다.

 

(flag)

1) FLOODFILL_FIXED_RANGE: 정의된 상한 및 하한 픽셀값 비교

2) FLOODFILL_MASK_ONLY: 입력 이미지는 변화 없으며 mask를 업데이트

(connectivity) 상하좌우 4방향과 대각 포함한 8방향으로 비교

 

아래 예는 mask를 사용하지 않는 파이썬 및 C/C++ 코드 입니다. “flag” 부분을 살펴보면 “flags=4 | cv2.FLOODFILL_FIXED_RANGE”과 “4 | FLOODFILL_FIXED_RANGE”로 정의 되었으며, 4방향 + 정의된 상한 및 하한 픽셀값 비교 로 동일한 의미로 적용되었음을 알 수 있습니다. 

 

import cv2
import numpy as np

# 이미지 로드 및 복사
img = cv2.imread('image.jpg')
im_floodfill = img.copy()

# 마스크 생성
h, w = img.shape[:2]
mask = np.zeros((h+2, w+2), np.uint8)

# 위치 및 변환값
seed_point = (125, 125)
new_val = (255, 0, 0)

cv2.floodFill(im_floodfill, mask, seed_point, new_val,
              loDiff=(5, 5, 5), upDiff=(5, 5, 5),
              flags=4 | cv2.FLOODFILL_FIXED_RANGE)

cv2.imshow("Filled Image", im_floodfill)
cv2.waitKey(0)
cv2.destroyAllWindows()

 

#include <opencv2/opencv.hpp>
using namespace cv;

int main() {
    
    // 이미지 로드
    Mat img = imread("image.jpg");
   
    // 마스크 생성
    Mat mask = Mat::zeros(img.rows + 2, img.cols + 2, CV_8UC1);

    // 위치 및 변환값
    Point seedPoint(125, 125);
    Scalar fillColor(255, 0, 0);  

   floodFill(img, mask, seedPoint, fillColor,
              0,              // rect 포인터 (필요 없으면 0)
              Scalar(5,5,5),  // loDiff
              Scalar(5,5,5),  // upDiff
              4 | FLOODFILL_FIXED_RANGE);  // flags

    // 결과 출력
    imshow("Flood Filled", img);
    waitKey(0);
    return 0;
}
728x90
728x90

시간 영역의 신호를 주파수 영역으로 변환하여 처리나 해석하는 이론이 푸리에 변환 Fourier Transform 입니다. 신호처리 분야에서 주파수 영역 분석을 위한 중요한 이론이기도 합니다. 영상처리에서는 이미지를 2차원 신호로 보고 푸리에 변환을 이용할 수 있으며, 이미지 공간 정보를 주파수 성분으로 바꾸어 분석할 수 있습니다. 분석을 통한 응용은 이미지 필터링, 압축, 복원등에 활용됩니다.

 

푸리에 변환은 이론적으로 4가지 형식으로 구분됩니다. 영상처리에 활용되는 기법은 이산 푸리에 변환 DFT (Discrete Fourier Transform) 입니다. 아래는 2차원 이산 푸리에 변환 수식이며 오일러 공식과 결합된 형태 입니다.

 

 

여기서 f(x,y)는 원본 이미지의 픽셀 값, F(u,v)는 주파수 평면에서의 값, M과 N은 이미지 크기인 Width와 Height 입니다. 추가로 푸리에 변환 요소에는 각 주파수 성분의 세기를 나타내는 크기 Magnitude와 성분의 위치 정보를 나타내는 위상 Phase 정보가 있으며, 위상 정보는 이미지의 구조를 유지하는데 중요한 요소입니다.

 

이미지를 푸리에 변환하면 최종 주파수 영역에 중심에는 저주파, 바깥쪽에는 고주파로 구성됩니다. 고주파는 이미지에서 물체의 윤곽선과 같은 에지 Edge 성분들을 의미하고 저주파는 물체의 전반적인 형태를 나타냅니다. 따라서 응용에서는 이러한 특징을 이용하여 저역통과 Low-pass 필터링 및 고역통과 High-pass 필터링을 통해 이미지의 경계 강조나 블러 효과를 낼 수 있습니다. 이미지 압축에서는 고주파 성분이 적으면 적을 수록 작은 정보만으로 이미지 재구성이 가능해지며, 대표적으로 JPEG 손실 압축 알고리즘에서 푸리에 이론의 일종인 이산 코사인 변환 DCT(Discrete Cosine Transform)을 사용합니다.

 

코드를 통해 푸리에 활용 방법을 확인해 보겠습니다. 실무에서는 C/C++, Python, Matlab 등 알고리즘 구현시 FFT (Fast Fourier Transform)을 이용합니다. DFT의 계산 복잡도 때문에 FFT를 활용하게 되며, 예를 들어 샘플 수가 N=1024일 때 DFT는 약 100만번의 연산이 필요한 반면 FFT는 10,000번의 연산이 필요합니다. 아래와 같이 파이썬 Numpy에서는 “np.fft.fft2” 함수를 사용하여 이미지 평면을 주파수 평면으로 변환하며, “np.fft.fftshift” 함수를 통해 저주파 성분을 중심으로 이동 시킵니다. 반대로 주파수 평면에서 이미지 평면으로 변환 시, “np.fft.ifftshift” > “np.fft.ifft2” 함수 순으로 적용할 수 있습니다. 

 

FFT의 이론 및 응용에 대해서는 추후 예와 함께 좀더 상세히 다루도록 하겠습니다.

 

import cv2
import numpy as np

# 이미지 불러오기 (그레이스케일)
img = cv2.imread('lena.jpg', cv2.IMREAD_GRAYSCALE)

f = np.fft.fft2(img) # 푸리에 FFT 변환
fshift = np.fft.fftshift(f)  # 중심 이동

magnitude_spectrum = 20 * np.log(np.abs(fshift)) # 주파수 스펙트럼 확인

f_ishift = np.fft.ifftshift(fshift_filtered) # 중심 복원
img_ifft = np.fft.ifft2(f_ishift) # 역 FFT 적용

# 실수 이미지 변환
img_re = np.abs(img_ifft)
728x90
728x90

이미지에서 우리가 원하는 영역을 찾거나 추출할 때 필요한 방법이 이진화 Binarization 입니다. 물체 탐색, 물체 추적, 이미지 분할 등 알고리즘을 구현할 때 필수적으로 사용해야하는 기법이 이진화 기술들입니다. 이진화 방법에는 다양한 기술들이 있으며, OpenCV 에서 제공하는 기술들 중 하나인 적응적 이진화 기술 Adaptive Thresholding Method에 대해 알아보겠습니다.

 

이진화 방법은 크게 전역적 처리 Global Processing과 지역적 처리 방법 Local Processing 방법으로 구분할 수 있습니다. OpenCV에서 제공하는 “adaptiveThreshold” 방법은 지역적 처리 방법과 적응적 방법이 합쳐진 이진화 방법입니다. Python에서는 아래와 같이 “cv2.adaptiveThreshold” 명으로 적용해 볼 수 있으며, 입력 변수는 아래와 같습니다.

 

[Python] dst = cv2.adaptiveThreshold(src, maxValue, adaptiveMethod, thresholdType, blockSize, C)

(입력 변수 설명)

- src: 입력 이미지 (Gray Scale Image)

- maxValue: 임계값 Threshold을 초과한 픽셀에 적용할 값 (일반적으로 255)

- adaptiveMethod: 1) cv2.ADAPTIVE_THRESH_MEAN_C, 2) cv2.ADAPTIVE_THRESH_GAUSSIAN_C

- thresholdType: 1) cv2.THRESH_BINARY, 2) cv2.THRESH_BINARY_INV

- blockSize: 픽셀 임계값을 계산할 영역 크기 (홀수: 3, 5, 7 ...)

- C: 계산된 임계값에서 빼는 상수 (일반적으로 0 또는 2~10)

 

여기서 “cv2.ADAPTIVE_THRESH_MEAN_C”는 “blockSize”가 3이라면 3x3 크기 영역 안에 평균 값에서 “C”를 뺀 값을 임계값으로 사용하게 됩니다. “cv2.ADAPTIVE_THRESH_GAUSSIAN_C”는 3x3 크기 영역 안에 가우시안 가중 평균값에서 “C”를 뺀 값을 임계값으로 사용합니다. 앞에서 언급했듯이 “blockSize”는 3은 3x3, 5는 5x5의 블럭을 의미하며 블럭 크기가 작아질 수록 잡음 Noise에 민감해지고 너무 크면 지역적 처리의 이점이 줄어들 수 있으니 설정 시 고민이 필요할 수 있습니다.

 

C/C++ 코드를 사용한다면 아래와 같이 사용할 수 있습니다. 파이썬 함수와 다르게 결과 이미지는 입력 변수와 함께 설정해야 합니다. (dst = 결과 이미지)

[C/C++] adaptiveThreshold(src, dst, maxValue, adaptiveMethod, thresholdType, blockSize, C)

 

지역적이면서 적응적 이진화 방법은 이미지에 조명이 균일하지 않거나 배경이 복잡하여 명암이 불균일한 경우 유용할 수 있으며, 문서 이미지에서 문자 추출 시에도 잘 활용될 수 있습니다. 아래는 파이썬과 C/C++에서 “adaptiveThreshold”를 활용한 예이니 참고해서 보면 좋을 듯 합니다.

 

import cv2

# 그레이 이미지 읽기
img = cv2.imread('grayimage.jpg', cv2.IMREAD_GRAYSCALE)

# 가우시안 적응형 이진화
thresh = cv2.adaptiveThreshold(
    img,
    255,
    cv2.ADAPTIVE_THRESH_GAUSSIAN_C,
    cv2.THRESH_BINARY,
    11,
    2
)

cv2.imshow("Adaptive Threshold", thresh)
cv2.waitKey(0)
cv2.destroyAllWindows()

 

#include <opencv2/opencv.hpp>
using namespace cv;

int main() {
    // 이미지 불러오기 (그레이스케일)
    Mat src = imread("grayimage.jpg", IMREAD_GRAYSCALE);
    Mat dst;
    // 적응형 이진화 적용
    adaptiveThreshold(
        src,              // 입력 이미지
        dst,              // 출력 이미지
        255,              // 최대값
        ADAPTIVE_THRESH_GAUSSIAN_C,  // 적응형 방법
        THRESH_BINARY,    // 임계값 타입
        7,               // 블록 크기 (홀수)
        2                 // C 상수
    );

    // 결과 출력
    imshow("Original", src);
    imshow("Adaptive Threshold", dst);
    waitKey(0);
    return 0;
}
728x90
728x90

보간법 Interpolation 은 영상처리에서 자주 활용됩니다. 이미지 확대나 변환 시 원본 이미지의 특성을 보전하고자 하는 해상도 개선 기법의 기본 입니다. 양선형 보간법 Bilinear Interpolation은 단어에서 같이 선형 보간법을 두번 반복하는 방법이며, 양선형 필터링 또는 양선형 맵핑 등으로도 언급됩니다. 현 블로그에서는 보간법의 이론적인 부분을 간략 다뤄보겠습니다.

 

양선형 보간 과정을 살펴보면, 그림에서 우리가 알고 싶은 점은 실수 좌표 (x, y)에 위치한 픽셀 값입니다. 이 실수 좌표는 정수 좌표 A, B, C, D의 픽셀 사이에 있으며, 이 정수 좌표의 픽셀 값을 이용하여 보간 할 수 있습니다.

 

 

 

4개의 좌표의 픽셀 값을 식으로 표현하면,

 

A = f(x1, y1), B = f(x2, y1), C = f(x1, y2), D = f(x2, y2)

보간 위치: (x, y), 여기서 x1 ≤ x ≤ x2, y1 ≤ y ≤ y2

 

계산 과정을 단계로 보면,

1) X 방향 보간

(A와 B 사이) R1=(x2−x)/(x2−x1)∗A+(x−x1)/(x2−x1)∗B

(C와 D 사이) R2=(x2−x)/(x2−x1)∗C+(x−x1)/(x2−x1)∗D

 

2) Y 방향 보간

(R1과 R2를 이용) P(x,y)=(y2−y)/(y2−y1)∗R1+(y−y1)/(y2−y1)∗R2

 

3) 하나의 공식으로 표현하면,

P(x,y)=A∗(x2−x)∗(y2−y)+B∗(x−x1)∗(y2−y)+C∗(x2−x)∗(y−y1)+D∗(x−x1)∗(y−y1)

여기서, 픽셀 간격이 “1”이라고 가정합니다. 참고로 “1”이 아닌 경우 (x2-x1)*(y2-y1)으로 정규화해주는 것이 일반적입니다.

 

단계별 공식 변환에서처럼 X축 방향과 Y축 방향으로 각각 적용한 방법이 양선형 보간법 입니다. 더 확장해보면 위 식은 다선형 다항식 Multilinear Polynomial 형태로 변환하여 문제를 풀 수 있습니다. 양선형 보간은 이차 다항식이 아니라 두 변수 일차 다항식으로 생각할 수 있으며, 어떤점 (x, y)에서의 값을 2차원이 아닌 일차 다항식 형태로 근사합니다.

 

하나의 공식으로 합쳐진 P(x, y)를 아래와 같이 좌표 (x, y) 기준 다항식으로 변환 할 수 있습니다.

f(x,y)=a0​+a1​x+a2​y+a3​xy

a0​=Ax2​y2​−Bx1​y2​−Cx2​y1​+Dx1​y1

a1​=−Ay2​+By2​−Cy1​+Dy1​

a2​=−Ax2​+Bx1​+Cx2​−Dx1​

a3=A+D−B−C

 

따라서 위 f(x,y) 식을 선형시스템으로 가정하여 우리가 알고 있는 4개의 위치 A, B, C, D를 행렬식으로 정리하여 선형대수로 계수 a0, a1, a2, a3를 구할 수 있습니다. 간략히 Ax=b 형태의 Matrix 계산으로 변환할 수 있으며, 주변 4개의 좌표를 이용한 양선형 보간법의 예를 파이썬 코드로 확인 할 수 있습니다. 계수 계산 시 np.lianlg,solve 함수를 사용합니다.

 

| 1 x1 y1 x1y1 |  |a0|      |f1|

| 1 x2 y1 x2y1 |  |a1|  =  |f2|

| 1 x1 y2 x1y2 |  |a2|      |f3|

| 1 x2 y2 x2y2 |  |a3|      |f4|

 

# Linear System Solution Ax=b
import numpy as np

# 4개 점의 좌표와 픽셀값
x1, x2 = 0, 1
y1, y2 = 0, 1
f1, f2, f3, f4 = A, B, C, D

# 계수 행렬 A
M = np.array([
    [1, x1, y1, x1*y1],
    [1, x2, y1, x2*y1],
    [1, x1, y2, x1*y2],
    [1, x2, y2, x2*y2],
])

# 함수값 벡터
f = np.array([f1, f2, f3, f4])

# 계수 a0, a1, a2, a3 계산
a = np.linalg.solve(M, f)

# 양선형 보간 함수
def bilinear_interp(x, y):
    return a[0] + a[1]*x + a[2]*y + a[3]*x*y
728x90

+ Recent posts