해당 포스트는 "열혈강의 영상처리 프로그래밍" 책의 내용을 요약한 것이다.



ex) 편리한 멤버 함수 구현하기

 

여기서 구현하는 함수들은 OpenCV를 비롯한 대부분 영상 처리 라이브러리에도 공통으로 포함된 기능

 

 

CMyImage intImage;
CMyImage floatImage;
intImage = floatImage;

위의 코드를 실행할 시 컴파일러가 알아서 두 클래스 사이의 변환을 해주지 않으므로 오류가 발생한다. 대입 연산자의 경우 같은 클래스끼리만 가능하다. 그래서 자유롭게 타입을 변환시켜줄 수 있는 변환 생성자를 따로 정의해야 한다.

 

 

//변환 생성자
	template <typename From>  

CMyImage(const CMyImage<From>& myImage) { m_nChannels = myImage.GetChannel(); m_nHeight = myImage.GetHeight(); m_nWidth = myImage.GetWidth(); m_nWStep = ((m_nWidth*m_nChannels*sizeof(T)+3)&~3)/sizeof(T);//(1) m_pImageData = new T[m_nHeight*m_nWStep]; int nWStep = myImage.GetWStep(); if (sizeof(T)==1) //(2) { for (int r=0 ; r < m_nHeight ; r++) { T* pDst = GetPtr(r); From* pSrc = myImage.GetPtr(r); for (int c=0 ; c < nWStep ; c++) { pDst[c] = (T)CLIP(pSrc[c]); //(3) } } } else { for (int r=0 ; r < m_nHeight ; r++) { T* pDst = GetPtr(r); From* pSrc = myImage.GetPtr(r); for (int c=0 ; c < nWStep ; c++) { pDst[c] = (T)pSrc[c]; } } } }

(1)번 코드의 경우 데이터 형식 변화로 인해서 다시 계산해야 한다. 그 이유는 만약 CIntImage를 CByteImage로 바꾼다고 할 경우, CIntImage는 sizeof(int)가 무조건 4이기 때문에 dummy데이터가 나오지 않는다. 하지만 CByteImage는 CIntImage에서 받은 m_nWidth가 3일 경우 행당 dummy 데이터가 1개 있게 된다. 다라서 다시 계산해야 한다.

(2)번 코드의 경우 원본 데이터형이 int나 double형이고 반환의 결과 영상이 BYTE형이면 0에서 255사이를 벗어나는 값은 전혀 엉뚱한 값으로 변환된다. 따라서 (3)번 코드 CLIP() 매크로를 사용해야 한다.

 

#define CLIP(x) (x < 0)? 0 : x > 255 ? 255 : x

그리고 m_pImageData를 복사 생성자에서는 memcpy()함수를 사용했는 데 여기서는 for문을 이용하여 순환하면서 한 픽셀씩 직접 변환을 수행하였다. 그 이유는 픽셀의 데이터형에 따라 메모리에 저장되는 모습이 완전히 다르기 때문이다. 예를 들어, 같은 픽셀 값 5라고 해도 BYTE형에서는 0x05로 저장되지만 int형으로는 0x0000000005로 저장된다. 따라서 memcpy() 함수를 사용할 수 없다.

 

 

 

 

CByteImage image(640, 480);
BYTE* pData = image.GetPtr();
pData[0] = 1;

위 코드는 원하는 픽셀의 위치를 지정하여 새로운 값을 넣는 예다. 연속해서 많은 픽셀을 읽고 쓰는 경우라면 위와 같이 포인터를 직접 받아서 계산하는 것이 효율적(다음에 배움)이지만 한 개 또는 소수의 픽셀만을 다루는 경우는 포인터를 받아오는 것이 효율적이지 않고 코드를 작성하기가 성가시다. 따라서 밑에와 같은 별도의 함수를 작성한다.

#include <assert.h> 
	inline T& GetAt(int x, int y, int c=0) const
	{
		assert(x >= 0 && x < m_nWidth && y >= 0 && y < m_nHeight);
		return m_pImageData[m_nWStep*y + m_nChannels*x + c];
	}

위 함수는 자주 실행할 수 있고 코드의 길이가 짧아 inline함수로 정의했다. 또한, 회색조 영상의 채널 순서 c값은 항상 0이므로 기본 매개변수로 0을 지정해 줬다. assert() 함수는 디버깅할 때 내부 조건에 만족 안 될 시 오류를 발생시킨다. 실제 프로그램을 실행할 때는 해당 코드는 사라진다. 위 함수는 참조자를 반환해서 반환된 픽셀 값을 변경할 수도 있다. 

 

 

 

영상 처리 알고리즘을 구현하다 보면 알고리즘 수행 도중에 임시 값 또는 결과값을 저장할 공간으로 새로운 영상을 만들어 전체 픽셀 값을 특정 상수로 초기화 해야 한다. 방법으로는 memset()함수와 픽셀 하나하나 직접 방문하면서 값을 입력하는 방법이다. memset()의 경우 속도는 훨씬 빠르지만 초기화가 바이트 단위로 이루어져 초기화할 것이 0인 경우와 영상의 데이터 형식이 BYTE형인 경우만 사용 가능하다. 예를 들어, int형 변수에 1을 memset하면 0x01010101이 된다. 나머지의 경우 for문을 통해 직접 방문하면서 값을 입력해야 한다. 다음 코드가 픽셀 값을 특정 상수로 초기화 하는 함수이다.

 

    void SetConstValue(T val)
	{
		if (val == 0)   // 초기화할 값이 0인 경우
		{
			memset(m_pImageData, 0, m_nWStep*m_nHeight*sizeof(T));
			return;
		}

		if (sizeof(T)==1)  // 영상의 데이터 형식이 BYTE형인 경우
		{
			memset(m_pImageData, val, m_nWStep*m_nHeight);
		}
		else  // 나머지 경우들
		{
			T* pData = m_pImageData;
			for (int r=0 ; r < m_nHeight ; r++)
			{
				for (int c=0 ; c < m_nWidth ; c++)
				{
					pData[c] = val;
				}
				pData += m_nWStep;
			}
		}
	}

 

 

아래 코드는 영상의 데이터가 유효한지를 검사하는 함수로 m_pImageData 포인터 변수가 NULL값을 갖는지 검사하면 된다.

	bool IsEmpty() const
	{
		return m_pImageData? false : true;
	}

해당 포스트는 "열혈강의 영상처리 프로그래밍" 책의 내용을 요약한 것이다.



ex3) 필수 멤버 함수 구현하기

 

//기본 생성자
    CMyImage(void)
	: m_nChannels(0)
	, m_nHeight(0)
	, m_nWidth(0)
	, m_nWStep(0)
	, m_pImageData(NULL)
	{}

기본 생성자에 의해 생성된 객체는 아무 내용을 담지 않으므로 멤버 변수들이 사용되는 일이 없어 아무 값을 넣어도 되지만 생성된 영상이 유효한지 확인할 때, 이들 멤버 변수값이 0인이 아닌지 확인하면 편리하다. 또한, m_pImageData의 경우 소멸자 안 메모리 해제 과정에서 NULL인지 아닌지 확인해야 하기 때문에 NULL로 초기화 해야 한다.

 

 

    CMyImage(int nWidth, int nHeight, int nChannels = 1)
	: m_nChannels(nChannels)
	, m_nHeight(nHeight)
	, m_nWidth(nWidth)
	, m_nWStep(((nWidth*nChannels*sizeof(T)+3)&~3)/sizeof(T))
	{
		m_pImageData = new T[m_nHeight*m_nWStep];
	}

영상 처리 프로그램 내부에서 컬러 영상보다 회색조 영상을 사용하는 빈도가 높아 채널 변수에 기본 매개변수 값으로 1로 설정

 

 

//복사 생성자
    CMyImage(const CMyImage& myImage)
	{
		m_nChannels = myImage.m_nChannels;
		m_nHeight	= myImage.m_nHeight;
		m_nWidth	= myImage.m_nWidth;
		m_nWStep	= myImage.m_nWStep;
		m_pImageData = new T[m_nHeight*m_nWStep];
		memcpy(m_pImageData, myImage.m_pImageData, m_nHeight*m_nWStep*sizeof(T));
	}

//대입 연산자
	CMyImage& operator=(const CMyImage& myImage) 
	{
		if (this == &myImage)
			return *this;

		m_nChannels = myImage.m_nChannels;
		m_nHeight	= myImage.m_nHeight;
		m_nWidth	= myImage.m_nWidth;
		m_nWStep	= myImage.m_nWStep;

		if (m_pImageData) 
			delete [] m_pImageData;

		if (myImage.m_pImageData != NULL)
		{
			m_pImageData = new T[m_nHeight*m_nWStep];
			memcpy(m_pImageData, myImage.m_pImageData, m_nHeight*m_nWStep*sizeof(T));
		}
		return *this;
	}

//소멸자
	~CMyImage(void) 
	{
		if (m_pImageData)
		{
			delete[] m_pImageData;
		}
	}

포인터 값을 멤버 변수로 가지고 있어 깊은 복사를 해야 하므로 복사 생성자와 대입 연산자를 재정의 해야 한다.

+ Recent posts