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



비트맵 파일 읽기와 쓰기

 

 

비트맵 파일의 읽기와 쓰기에서 주의할 점은 비트맵 파일 안에서 영상은 상하가 반전되어 저장된다. 위 그림에서와 같이 프로그램 내부에서는 왼쪽 상단부터 시작해서 행 우선 배열에 따라 픽셀의 배열이 이루어지나 BMP 파일 안에서는 영상의 왼쪽 하단부터 시작해서 행 우선 배열에 따라 저장되 상하 좌표가 바뀐다.

 

ex) 영상 데이터 클래스 구현하기 포스트 코드에서 나왔던 LoadImage() 와 SaveImage()를 구현할 것이다.

 

 

bool LoadImage(const char* filename) 
{ 
assert(sizeof(T) == 1); // 일반적인 비트맵 파일은 1byte이기 때문에 BYTE형의 영상인 경우에만 동작할 수 있도록 한다. 

if (strcmp(".BMP", &filename[strlen(filename) - 4])) //확장자가 BMP인 비트맵 파일이 맞는 지 확인
{ 
FILE* pFile = NULL; 
fopen_s(&pFile, filename, "rb"); // 바이너리 읽기 모드 
if (!pFile) 
return false; 

BITMAPFILEHEADER fileHeader; 

if (!fread(&fileHeader, sizeof(BITMAPFILEHEADER), 1, pFile)) 
{ 
fclose(pFile); 
return false; 
} 

if (fileHeader.bfType != 0x4D42) // 'BM' 문자 검사 
{ 
fclose(pFile); 
return false; 
} 

BITMAPINFOHEADER infoHeader; 
if (!fread(&infoHeader, sizeof(BITMAPINFOHEADER), 1, pFile)) 
{ 
fclose(pFile); 
return false; 
} 

if (infoHeader.biBitCount != 8 && infoHeader.biBitCount != 24) //회색조 영상 또는 트루 컬러영상만을 읽을 수 있도록 한다.
{ 
fclose(pFile); 
return false; 
} 

if (m_nWidth != infoHeader.biWidth && m_nHeight != infoHeader.biHeight 
&& m_nChannels != infoHeader.biBitCount / 8) 
{ 
if (m_pImageData) 
delete[] m_pImageData; 

m_nChannels = infoHeader.biBitCount / 8; 
m_nHeight = infoHeader.biHeight; 
m_nWidth = infoHeader.biWidth; 
m_nWStep = (m_nWidth*m_nChannels * sizeof(T) + 3)&~3; 

m_pImageData = new T[m_nHeight*m_nWStep]; 
} 

fseek(pFile, fileHeader.bfOffBits, SEEK_SET); //실제 픽셀 데이터 위치를 가리킨다.

int r; 
for (r = m_nHeight - 1; r >= 0; r--) //비트맵 파일은 상하가 바뀌므로 m_nHeight-1부터 접근
{ 
if (!fread(&m_pImageData[r*m_nWStep], sizeof(BYTE), m_nWStep, pFile)) 
{ 
fclose(pFile); 
return false; 
} 
} 

fclose(pFile); 

return true; 
} 
else 
{ 
return false; 
} 
}

비트맵 파일의 헤더정보를 읽는 과정과 저장할 픽셀 데이터에 관한 영상 정보를 읽는 과정, 픽셀 데이터를 읽는 과정으로 구성된다.

 

 

bool SaveImage(const char* filename) 
{ 
assert(sizeof(T) == 1); // 일반적인 비트맵 파일은 1byte이기 때문에 BYTE형의 영상인 경우에만 동작할 수 있도록 한다.  

if (strcmp(".BMP", &filename[strlen(filename) - 4])) 
{ 
FILE* pFile = NULL; 
fopen_s(&pFile, filename, "wb"); 
if (!pFile) 
return false; 

BITMAPFILEHEADER fileHeader; 
fileHeader.bfType = 0x4D42; // 'BM' 
//(1)
fileHeader.bfSize = sizeof(BITMAPFILEHEADER) + sizeof(BITMAPINFOHEADER) + m_nWStep*m_nHeight + (m_nChannels == 1) * 1024; 
fileHeader.bfReserved1 = 0; 
fileHeader.bfReserved2 = 0;
//(2) 
fileHeader.bfOffBits = sizeof(BITMAPFILEHEADER) + sizeof(BITMAPINFOHEADER) + (m_nChannels == 1) * 256 * sizeof(RGBQUAD); 

fwrite(&fileHeader, sizeof(BITMAPFILEHEADER), 1, pFile); 

BITMAPINFOHEADER infoHeader; 
infoHeader.biSize = sizeof(BITMAPINFOHEADER); 
infoHeader.biWidth = m_nWidth; 
infoHeader.biHeight = m_nHeight; 
infoHeader.biPlanes = 1; 
infoHeader.biBitCount = m_nChannels * 8; 
infoHeader.biCompression = BI_RGB; 
infoHeader.biSizeImage = m_nWStep*m_nHeight; 
infoHeader.biClrImportant = 0; 
infoHeader.biClrUsed = 0; 
infoHeader.biXPelsPerMeter = 0; 
infoHeader.biYPelsPerMeter = 0; 

fwrite(&infoHeader, sizeof(BITMAPINFOHEADER), 1, pFile); 

if (m_nChannels == 1)  //(3)
{ 
for (int l = 0; l<256; l++) 
{ 
RGBQUAD GrayPalette = { l, l, l, 0 }; 
fwrite(&GrayPalette, sizeof(RGBQUAD), 1, pFile); 
} 
} 

int r; 
for (r = m_nHeight - 1; r >= 0; r--) 
{ 
fwrite(&m_pImageData[r*m_nWStep], sizeof(BYTE), m_nWStep, pFile); 
} 

fclose(pFile); 
return true; 
} 
else 
{ 
return false; 
} 
} 

SaveImag(0 함수와 반대 과정으로 비트맵 파일 헤더와 영상 정보 헤더를 생성하고 파일에 쓰는 과정, 픽셀 데이터를 파일에 쓰는 과정으로 구성된다. 회색조 영상은 팔레트 정보를 기록하는 과정도 있다. 또한, BMP파일에 픽셀 데이터를 쓸 때 상하 반대로 해서 써야한다.

(1)번 코드 fileHeader.bfSize = sizeof(BITMAPFILEHEADER) + sizeof(BITMAPINFOHEADER) + m_nWStep*m_nHeight + (m_nChannels == 1) * 1024; 는 비트맵 파일의 크기를 구하는 것으로 m_nChannels가 1이면 즉, 회색조 영상일 경우 팔레트 정보의 크기를 추가해 주는 코드이다. 1024 대신 (2)번 코드와 같이 256*sizeof(RGBQUAD)를 곱해줘도 된다.

(3)번 코드는 회색조 영상의 경우 팔레트 정보를 추가해 줘야 하기 때문에 구현한 코드이다. 

해당 포스트는 "자바 객체지향 디자인 패턴", "JAVA 언어로 배우는 디자인 패턴 입문" 책의 내용을 요약한 것이다.



2. Observer 패턴을 JDK API를 이용하지 않고 직접 구현하는 방법

 

ex)

 

<Observer>

public interface Display {
	public void display();
}

interface Observer{
	public void update(Observable o);
}

class KoreanDisplay implements Observer, Display{

	private Observable observable; // 등록될 Observable
	
	private float temperature;
	private float humidity;
	private float pressure;
	
	public KoreanDisplay(Observable observable) {
		this.observable = observable;
		observable.addObserver(this); //this(KoreaDisplay)를 옵저버로 등록
	}
	
	@Override
	public void display() {
		System.out.println("온도 : "+temperature);
		System.out.println("습도 : "+humidity);
		System.out.println("압력 : "+pressure);
	}

	
	/* 
	 * Observable에서 notifyObservers()를 호출할 시 해당 메소드가 호출되어 옵저버의 데이터가 갱신
	 */
	@Override
	public void update(Observable o) {
		if(o instanceof WeatherData){
			WeatherData weatherData = (WeatherData)o;
			this.temperature = weatherData.getTemperature();
			this.humidity = weatherData.getHumidity();
			this.pressure = weatherData.getPressure();
			display();
		}
	}
}

class EnglishDisplay implements Observer, Display{

	private Observable observable; // 등록될 Observable
	
	private float temperature;
	private float humidity;
	private float pressure;
	
	public EnglishDisplay(Observable observable) {
		this.observable = observable;
		observable.addObserver(this); //this(EnglishDisplay)를 옵저버로 등록
	}
	
	@Override
	public void display() {
		System.out.println("temperature : "+temperature);
		System.out.println("humidity : "+humidity);
		System.out.println("pressure : "+pressure);
	}

	
	/* 
	 * Observable에서 notifyObservers()를 호출할 시 해당 메소드가 호출되어 옵저버의 데이터가 갱신
	 */
	@Override
	public void update(Observable o) {
		if(o instanceof WeatherData){
			WeatherData weatherData = (WeatherData)o;
			this.temperature = weatherData.getTemperature();
			this.humidity = weatherData.getHumidity();
			this.pressure = weatherData.getPressure();
			display();
		}
	}
}

Observer는 JDK API를 사용했던 것과 거의 달라지는 게 없고 Observer 인터페이스만 변경했다.

 

<직접 구현한 Observable>

public interface Observable {
	public void addObserver(Observer o);     //옵저버 등록
	public void deleteObserver(Observer o);  //옵저버 제거
	public void notifyObservers();           //옵저버에 데이터 전송
}

class WeatherData implements Observable{

	private ArrayList observers;  //등록할 옵저버 리스트
	
	private float temperature;
	private float humidity;
	private float pressure;

	public WeatherData() {
		observers = new ArrayList();
	}
	
	/* 
	 * 옵저버 등록 메소드
	 */
	@Override
	public void addObserver(Observer o) {
		observers.add(o);
	}

	/* 
	 * 옵저버 제거 메소드
	 */
	@Override
	public void deleteObserver(Observer o) {
		int i = observers.indexOf(o);
		if(i>=0){
			observers.remove(i);
		}
	}

	/* 
	 * 옵저버에 데이터 전달
	 */
	@Override
	public void notifyObservers() {
		for (int i = 0; i < observers.size(); i++) {
			Observer observer = (Observer)observers.get(i);
			observer.update(this);
		}
	}
	
	public void setData(float temperature, float humidity, float pressure){
		this.temperature = temperature;
		this.humidity = humidity;
		this.pressure = pressure;
		notifyObservers();
	}

	public float getTemperature() {
		return temperature;
	}

	public float getHumidity() {
		return humidity;
	}

	public float getPressure() {
		return pressure;
	}
}

'자바 > 디자인패턴' 카테고리의 다른 글

퍼사드 패턴  (0) 2017.06.08
프로토타입 패턴  (0) 2017.06.08
컴퍼지트 패턴  (0) 2017.06.07
데코레이터(Decorator) 패턴  (0) 2017.06.06
옵저버(Observer) 패턴(1) - JDK API 활용  (0) 2017.06.06

+ Recent posts