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

이미지에서 우리가 원하는 영역을 찾거나 추출할 때 필요한 방법이 이진화 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

C 언어는 1970년 초 벨 연구소(Bell Lab.)에서 유닉스 시스템의 운영체제를 만들기 위해 개발된 프로그래밍 언어 입니다. 역사가 오래되기도 했지만 현재까지 폭넓게 사용되는 언어이기도 합니다. C++ C에 확장판으로 객체지향 프로그래밍을 지원하기 위해 개발된 언어이며, C#은 마이크로 소프트사(Microsoft)에서 C++ 언어를 기반으로 닷넷 프레임워크(.NET Framework)의 응용 프로그램 개발을 위한 언어 입니다. 유사한 성격에 JAVA 언어 진영에 대응하기 위한 목적도 있다고 하죠.

 

C 프로그래밍 언어에 역사에서처럼 C 프로그래밍 문법을 잘 알면 C#까지 익히는데 많은 시간이 필요하지 않습니다. 프로그래밍 언어 습득은 전공과도 연관성이 있겠지만 업무와도 사용에 지속성에서 상당한 연관이 있습니다. 외국어를 습득하는 과정처럼 사용하면 사용할수록 느는 게 프로그래밍 언어도 같습니다.

 

 

프로그래밍 기술에서는 C 언어에 문법을 기본적으로 알고 있다는 가정하에 영상처리 기술들을 확인해보고 검증해보는 도구로 마이크로 소프트사의 Visual Studio에 포함된 MFC(Microsoft Foundation Classes)를 사용할 예정이고 간혹 C#에 대한 이야기도 할 예정입니다. 개발 환경 설치나 기본적인 내용들은 많은 블로그에 잘 나와 있으니 프로그래밍 이야기에서는 다양한 프로젝트들을 진행하면서 문제 해결이나 유용하게 사용했던 코드들에 대한 이야기를 해 볼까 합니다.

728x90

+ Recent posts