728x90

기존 블로그 에서 산업 자동화에 활용되는 머신 비전용 렌즈인 Telecentric Lens, Macro Lens, Line Scan Lens를 소개했습니다. 아래 링크를 참고해 보시면 좋을 듯 합니다.

2025.06.29 - [영상처리 도구] - 머신 비전 Machine Vision을 위한 카메라 렌즈 종류와 선택(1)

 

아래는 이 외에 머신 비전에서 활용되는 렌즈들 입니다. 활용 분야에 따라 렌즈의 적절한 선택은 효율성이나 비용 측면에서 더 유리할 수 있습니다.

 

고정 초점형 렌즈 (Fixed Focal Lens)

초점 거리가 고정되어 있어 화각이 일정하고 줌이 불가능한 렌즈를 의미합니다. 하지만 광학 성능이 우수하고 구조가 단순하여 PCB 패턴, 바코드, 약품 포장 라벨 검사 등등 산업용 검사나 머신 비전 분야에 널리 사용됩니다. 

 

명칭 그대로 고정 초점이기 때문에 해상도와 왜곡 보정에 유리하고 구조가 간단하여 유지보수가 용이합니다. 가격 측면에서도 효율성이 있어 산업용으로 많이 활용됩니다. 당연히 초점 거리 변경이 필요하면 렌즈 자체를 교체해야 하며 촬영 물체들의 높낮이가 변동이 있으면 사용이 불가 합니다.

 

무왜곡 렌즈 (Zero-Distortion Lens 또는 Ultra Low Distortion Lens)

광학 이미지에서 발생할 수 있는 기하학적 왜곡 Geometric Distortion을 최대한 제거하나 최소화한 렌즈를 말합니다. 다시 말해 왜곡률을 극한까지 줄여 실제 대상의 형태를 왜곡 없이 촬영하는 렌즈이며, 산업 검사와 비전 시스템 등 정확한 기하학적 재현이 요구되는 분야에 필수로 사용됩니다.

 

왜곡률의 경우 일반 렌즈 1~3%, 광각 렌즈 3~10% 이상, 무왜곡 렌즈 0.1% 이하로 볼 수 있으며, 광학 측정 장비나 생물학 영상, 문자 스캔, 실물 재현을 위한 건축물 및 문화재 촬영에 사용될 수 있습니다. 렌즈 설계 복잡성으로 가격이 비싸며 화각 FOV가 제한적일 수 있습니다. 

 

Pericentric 렌즈

360도 시야 확보용 렌즈로 렌즈 하나로 물체의 상단과 측면, 일부 바닥까지 한번에 볼 수 있도록 설계된 렌즈 입니다. 특히 원형 물체나 튜브용 물체의 외곽을 한번에 검사해야 할 때 활용됩니다.

 

일반 렌즈와 다르게 광선이 센서 중심을 향해 굴절 되도록 설계되어 센서가 보는 시야는 렌즈를 중심으로 물체를 감싸는 형태가 됩니다. 결과론적으로 물체의 위와 측면을 동시에 이미지화 할 수 있으며, 병뚜껑 밀봉 상태나 병과 캔의 레이블 위치, 화장품 튜브 외곽 인쇄 품질 검사 등에 사용됩니다.

 

그림에서와 같이 플라스틱 통의 뚜껑을 기준으로 촬영을 하면 플라스틱 측면에 라벨까지 이미지로 확인 할 수 있습니다. 그림에 WD는 Working Distance를 의미합니다.

 

 

전동식 렌즈 (Motorized Lens)

명칭에서와 같이 전동식으로 초점, 줌, 조리개 등을 자동으로 조절할 수 있는 렌즈 입니다. 시스템 명령이나 소프트웨어를 통해 원격으로 제어할 수도 있습니다. 따라서 넓은 지역을 감시하면서 특정 지점을 확대해 볼 수 있는 감시 시스템이나 다양한 거리의 물체 인식을 필요로 하는 로봇 비전에서 활용될 수 있습니다. 요즘 골목마다 설치 되어 있는 CCTV가 전동식 렌즈의 한 예 입니다.

 

원격 제어 가능함과 자동화 시스템의 연동으로 정밀한 제어에 장점을 가지며 전원 의존성이나 비용, 설정 복잡성 등은 단점으로 생각할 수 있습니다.

728x90
728x90

이미지 처리 및 분석을 통한 기술 적용 분야가 많아지고 있습니다. 그 중 컴퓨터 비전 Computer Vision의 한 부분인 머신 비전 분야가 있습니다. 머신 비전은 산업 자동화와 제조 라인의 검사, 품질 관리, 제어 등에 사용되며, 그 이미지를 취득하는 다양한 비전 카메라들이 있습니다. 현 블로그에서는 비전 카메라에 장착되는 렌즈에 특성을 알아보겠습니다. 

 

텔레센트릭 렌즈 (Telecentric Lens)

광학 중심이 물체나 제품쪽으로 이동하면서도 배율이 일정하게 유지되도록 설계된 특수 렌즈이며, 정밀한 측정 및 검사에 사용됩니다. 특성을 보면 배율이 일정하고 일반 렌즈에 비해 왜곡을 최소화합니다. 일반 렌즈는 조리개가 렌즈 중간에 위치한 반면 텔레세트릭 렌즈는 조리개가 주점(Primary principal point) 위에 배치되어 광선이 평행하게 입사 및 출사할 수 있는 구조적 특징을 가지고 있습니다.

 

이러한 구조적 특성에 의해 물체의 왜곡을 최소화하고 깊이 변화에 무관한 정확한 크기 측정이 가능하며 동일한 조건에서 반복 측정이 가능한 장점을 가지고 있습니다. 반면 고가에 렌즈가 길고 무거우며 조리개가 작아 광량 손실이 있는 단점과 검사 대상 물체 크기가 렌즈 직경보다 작아야 한다는 단점을 가집니다. 세부적으로 반도체 검사나 PCB 및 전자부품 측정, 3D 스캐닝 시스템 등에 활용됩니다.

 

종류로는 아래 세가지로 구분할 수 있습니다. 아래 그림은 Bi-Telecentric Lens 구성도와 일반 렌즈 및 텔레센트릭 간 촬영한 물체를 비교한 예입니다. 일반 렌즈에서는 원근에 따른 물체의 크기가 다르지만 텔레센트릭 렌즈에서는 같은 크기의 물체는 동일하게 촬영됩니다.

 

1) Object-space Telecentric Lens: 물체가 렌즈에 가깝거나 멀어도 배율이 일정

2) Image-space Telecentric Lens: 이미지 센서에 광선이 평행하게 입사, 이미지 센서 크기에 무관하게 균일한 이미지 품질 유지

3) Bi-Telecentric Lens: 물체 및 이미지 센서 모두 Telecentric. 

 

 

 

매크로 렌즈 (Macro Lens)

작은 사물 또는 제품 등을 크게 확대하여 촬영할 수 있도록 설계된 특수 렌즈이며, 주로 접사(Close-up photography)에 사용됩니다. 일반적으로 곤충이나 꽃, 작은 전자부품 및 인쇄패턴과 같은 미세한 부분 촬영에 적합합니다.

매크로 렌즈의 특성은 배율이 크며 촬영 대상의 왜곡을 최소화하고 얕은 심도(Depth of Field)를 가집니다. 초점 거리별 유형을 보통 50~60mm는 문서 등의 근거리 접사, 90~105mm는 곤충이나 꽃 접사 촬용에 사용되며 150~200mm는 원거리 접사로 산업용 검사에 활용됩니다.

 

라인 스캔 렌즈 (Line Scan Lens)

라인 스캔 카메라와 함께 사용하는 특수 렌즈로 한 줄(Line)의 이미지를 정밀하게 촬용하기 위해 설계된 렌즈입니다. 우리가 일반적으로 사용하는 영역 스캔 (Area Scan) 방식과는 다른 연속적으로 고속 이미지 취득에 최적화 되어 있습니다.

 

라인 스캔은 한번에 물체의 가로 또는 세로의 한 줄만 촬영이 되며, 대상이 이동하면서 연속 라인 촬영을 통해 2차원 이미지를 구성합니다. 문서를 스캔할 때 방식과 유사하다고 보면 됩니다. 라인 스캔 렌즈의 특징을 보면 미세 결함도 감지할 만큼 해상도가 높고 전체 라인에 걸쳐 왜곡 없이 균일합니다. 그리고 균일하게 밝기를 유할 수 있는 광량 균일성과 열이나 진동에 강건하게 장시간 연속 촬영이 가능합니다.

 

아래 그림에서와 같이 라인 스캔 렌즈로 촬영된 이미지는 일반적으로 X축이 시간으로 볼수 있이며, Y축이 라인 스캔의 라인 크기입니다.

 

728x90
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

+ Recent posts