2차원 웨이블릿 변환은 이미지 및 신호 처리에서 필터링, 압축, 특징 추출에 중요한 역할을 한다. 여기서는 Haar 웨이블릿을 중심으로 2차원 이산 웨이블릿 변환(DWT)의 구현 방법을 설명하고, 이를 C++로 구현하는 방법을 제시하겠다. 기본적으로 2차원 웨이블릿 변환은 이미지의 행과 열에 각각 1차원 웨이블릿 변환을 적용하는 방식으로 진행된다. 이를 통해 이미지는 저주파 및 고주파 성분으로 분해된다.
2차원 웨이블릿 변환의 개요
2차원 웨이블릿 변환은 먼저 이미지의 행을 기준으로 변환을 수행한 후, 열을 기준으로 또 한 번 변환을 수행하여 고주파와 저주파 성분을 분리한다. 이 과정에서 이미지 \mathbf{I}가 크기 M \times N일 때, 2차원 변환 결과는 다음과 같은 네 가지 성분으로 나뉜다.
- LL 성분 (저주파 - 저주파): 원본 이미지의 저해상도 버전으로, 주요 정보가 포함된 성분
- LH 성분 (저주파 - 고주파): 수평 방향의 고주파 성분을 나타내며, 엣지 정보가 포함
- HL 성분 (고주파 - 저주파): 수직 방향의 고주파 성분을 나타내며, 엣지 정보가 포함
- HH 성분 (고주파 - 고주파): 대각선 방향의 고주파 성분
이 성분들은 행렬 \mathbf{I}의 각 사분면에 배치되며, 이미지의 다양한 특징을 추출하거나 압축하는 데 사용된다.
수학적 정의
주어진 이미지 \mathbf{I}의 2차원 웨이블릿 변환은 다음과 같이 정의할 수 있다. 먼저 행별로 1차원 웨이블릿 변환을 적용하여 중간 행렬 \mathbf{I}'을 얻고, 이어서 열별로 1차원 웨이블릿 변환을 적용하여 최종 변환 행렬 \mathbf{W}을 생성한다.
- 행 변환: 각 행 \mathbf{I}_i에 대해 1차원 변환을 수행하여 중간 행렬 \mathbf{I}'의 각 행을 계산한다.
여기서 \psi는 웨이블릿 함수이다.
- 열 변환: \mathbf{I}'의 각 열에 대해 1차원 변환을 수행하여 최종 행렬 \mathbf{W}를 생성한다.
이 과정을 통해, 최종 변환 행렬 \mathbf{W}는 \mathbf{LL}, \mathbf{LH}, \mathbf{HL}, \mathbf{HH} 사분면에 각기 다른 주파수 성분을 포함하게 된다.
C++ 코드 구성
C++에서 2차원 웨이블릿 변환을 구현할 때, 다음과 같은 단계로 진행할 수 있다.
- 1차원 웨이블릿 변환 함수 작성: 이미지의 각 행과 열에 대해 1차원 웨이블릿 변환을 적용하는 함수.
- 행렬 분해 및 결합: 변환 결과를 각 성분에 맞게 분해하고, 변환 후 이미지 크기에 맞추어 결합.
- 2차원 변환 함수 작성: 이미지의 행과 열 모두에 1차원 변환을 적용하여 최종 변환 행렬 생성.
1차원 웨이블릿 변환 함수 예제
void waveletTransform1D(std::vector<double>& data) {
int n = data.size();
std::vector<double> temp(n);
while (n > 1) {
n /= 2;
for (int i = 0; i < n; i++) {
temp[i] = (data[2 * i] + data[2 * i + 1]) / 2.0; // 저주파 성분
temp[n + i] = (data[2 * i] - data[2 * i + 1]) / 2.0; // 고주파 성분
}
std::copy(temp.begin(), temp.begin() + 2 * n, data.begin());
}
}
위 코드에서는 입력 벡터 data
에 대해 1차원 웨이블릿 변환을 수행하여 저주파 성분과 고주파 성분을 계산한다. 이 함수는 이미지의 각 행과 열에 대해 호출될 수 있다.
2차원 웨이블릿 변환 함수 작성
이제 1차원 변환을 이용하여 2차원 이미지에 웨이블릿 변환을 적용하는 함수를 작성한다. 이미지의 각 행과 열에 대해 1차원 변환을 순차적으로 수행하여 최종적으로 2차원 웨이블릿 변환 결과를 얻을 수 있다.
2차원 웨이블릿 변환 함수 예제
#include <vector>
void waveletTransform2D(std::vector<std::vector<double>>& image) {
int rows = image.size();
int cols = image[0].size();
// 각 행에 대해 1차원 웨이블릿 변환 적용
for (int i = 0; i < rows; i++) {
waveletTransform1D(image[i]);
}
// 열을 추출하여 1차원 변환 적용
std::vector<double> colData(rows);
for (int j = 0; j < cols; j++) {
// 각 열의 데이터를 추출
for (int i = 0; i < rows; i++) {
colData[i] = image[i][j];
}
// 열 데이터에 1차원 웨이블릿 변환 적용
waveletTransform1D(colData);
// 변환 결과를 다시 이미지에 반영
for (int i = 0; i < rows; i++) {
image[i][j] = colData[i];
}
}
}
위 함수 waveletTransform2D
에서는 먼저 각 행에 대해 1차원 변환을 수행한 후, 각 열에 대해 동일한 변환을 수행하여 최종적으로 2차원 변환을 완료한다.
결과 구조화 및 LL, LH, HL, HH 성분 추출
2차원 변환을 수행한 후에는 변환 결과인 \mathbf{LL}, \mathbf{LH}, \mathbf{HL}, \mathbf{HH} 성분을 각각의 사분면에 위치시킨다. 이를 통해 주파수 성분을 분석하거나 노이즈 제거, 이미지 압축 등에 사용할 수 있다.
이때 변환 결과 \mathbf{W}의 사분면은 다음과 같이 나눌 수 있다.
- \mathbf{LL} 성분: 상위 좌측 사분면에 위치
- \mathbf{LH} 성분: 상위 우측 사분면에 위치
- \mathbf{HL} 성분: 하단 좌측 사분면에 위치
- \mathbf{HH} 성분: 하단 우측 사분면에 위치
이를 코드로 나타내면 다음과 같다.
std::vector<std::vector<double>> extractSubbands(const std::vector<std::vector<double>>& image) {
int rows = image.size() / 2;
int cols = image[0].size() / 2;
std::vector<std::vector<double>> LL(rows, std::vector<double>(cols));
std::vector<std::vector<double>> LH(rows, std::vector<double>(cols));
std::vector<std::vector<double>> HL(rows, std::vector<double>(cols));
std::vector<std::vector<double>> HH(rows, std::vector<double>(cols));
// LL 추출
for (int i = 0; i < rows; i++) {
for (int j = 0; j < cols; j++) {
LL[i][j] = image[i][j];
}
}
// LH 추출
for (int i = 0; i < rows; i++) {
for (int j = cols; j < 2 * cols; j++) {
LH[i][j - cols] = image[i][j];
}
}
// HL 추출
for (int i = rows; i < 2 * rows; i++) {
for (int j = 0; j < cols; j++) {
HL[i - rows][j] = image[i][j];
}
}
// HH 추출
for (int i = rows; i < 2 * rows; i++) {
for (int j = cols; j < 2 * cols; j++) {
HH[i - rows][j - cols] = image[i][j];
}
}
// 성분들을 반환할 구조체나 클래스에 저장하는 방법도 가능하지만, 예제에서는 단순 반환으로 설명
return {LL, LH, HL, HH};
}
이 함수 extractSubbands
는 이미지의 변환된 데이터를 입력받아, 각 성분을 별도의 행렬로 분리하여 반환한다. 이를 통해 각 성분에 대해 후처리를 적용하거나 분석할 수 있다.
Haar 웨이블릿을 활용한 기본 변환 예제
앞서 구현한 함수를 사용하여, Haar 웨이블릿을 이용한 기본 2차원 웨이블릿 변환을 수행해보겠다. Haar 웨이블릿은 가장 간단한 형태의 웨이블릿으로, 빠르게 구현할 수 있으며 이미지의 전반적인 특징을 추출하는 데 유용하다. Haar 웨이블릿 변환을 통해 이미지의 다중 해상도 분석을 수행할 수 있다.
Haar 웨이블릿을 활용한 2차원 변환 예제
Haar 웨이블릿을 이용한 2차원 변환에서는 이미지의 각 행과 열에 대해 저주파 성분과 고주파 성분을 반복적으로 추출하여 변환을 수행한다. 이때, Haar 웨이블릿은 간단히 평균과 차이를 구하는 방식으로 고주파 및 저주파 성분을 계산하므로 효율적인 처리가 가능한다.
Haar 웨이블릿을 적용한 이미지 변환 예제 코드
다음은 이미지를 2차원 Haar 웨이블릿으로 변환하는 전체 예제 코드이다.
#include <vector>
#include <iostream>
void printMatrix(const std::vector<std::vector<double>>& matrix) {
for (const auto& row : matrix) {
for (double val : row) {
std::cout << val << " ";
}
std::cout << std::endl;
}
}
int main() {
// 예제 이미지 데이터 (4x4 행렬)
std::vector<std::vector<double>> image = {
{255, 255, 255, 255},
{255, 255, 255, 255},
{0, 0, 0, 0},
{0, 0, 0, 0}
};
std::cout << "Original Image:" << std::endl;
printMatrix(image);
// 2차원 웨이블릿 변환 수행
waveletTransform2D(image);
std::cout << "\nWavelet Transformed Image:" << std::endl;
printMatrix(image);
// LL, LH, HL, HH 성분 추출
auto subbands = extractSubbands(image);
std::cout << "\nLL Subband:" << std::endl;
printMatrix(subbands[0]);
std::cout << "\nLH Subband:" << std::endl;
printMatrix(subbands[1]);
std::cout << "\nHL Subband:" << std::endl;
printMatrix(subbands[2]);
std::cout << "\nHH Subband:" << std::endl;
printMatrix(subbands[3]);
return 0;
}
이 예제는 4 \times 4 이미지 데이터에서 2차원 웨이블릿 변환을 수행하고, 변환 후 \mathbf{LL}, \mathbf{LH}, \mathbf{HL}, \mathbf{HH} 성분을 각각 추출한다. 이를 통해 이미지의 다양한 주파수 성분을 분석할 수 있으며, 변환된 각 성분의 결과를 출력한다.
성능 향상을 위한 최적화 방법
2차원 웨이블릿 변환은 이미지의 크기에 따라 계산량이 증가하므로, 효율적인 메모리 관리 및 연산 최적화가 중요하다. 다음은 성능을 향상시키기 위한 몇 가지 최적화 방법이다.
- 메모리 사용 최적화: 중간 변환 결과를 저장하는 임시 메모리를 최소화하여 불필요한 메모리 할당을 줄이다.
- 병렬 처리: 각 행과 열에 대한 1차원 변환은 독립적이므로 병렬 처리가 가능한다. OpenMP와 같은 라이브러리를 사용하여 다중 스레드로 처리할 수 있다.
- 고정된 웨이블릿 필터: Haar 웨이블릿과 같은 고정 필터를 사용하는 경우, 연산을 하드코딩하여 계산 효율을 높일 수 있다.
OpenMP를 사용한 병렬 처리 예제
OpenMP를 통해 행렬의 각 행과 열에 대한 변환을 병렬로 수행할 수 있다. 다음은 병렬 처리된 waveletTransform2D
의 예제이다.
#include <omp.h>
void waveletTransform2DParallel(std::vector<std::vector<double>>& image) {
int rows = image.size();
int cols = image[0].size();
// 각 행에 대해 1차원 웨이블릿 변환을 병렬 처리
#pragma omp parallel for
for (int i = 0; i < rows; i++) {
waveletTransform1D(image[i]);
}
// 각 열에 대해 1차원 웨이블릿 변환을 병렬 처리
#pragma omp parallel for
for (int j = 0; j < cols; j++) {
std::vector<double> colData(rows);
for (int i = 0; i < rows; i++) {
colData[i] = image[i][j];
}
waveletTransform1D(colData);
for (int i = 0; i < rows; i++) {
image[i][j] = colData[i];
}
}
}
이 코드는 OpenMP의 #pragma omp parallel for
지시어를 사용하여 각 행과 열에 대해 병렬로 1차원 변환을 수행한다. 이를 통해 성능을 크게 향상할 수 있다.
코드 구현 시 유의 사항
- 경계 처리: 이미지의 크기가 2의 거듭제곱이 아닌 경우, 경계 처리에 신경 써야 한다. 변환을 수행하기 전에 이미지의 크기를 조정하거나, 경계 영역을 처리하는 조건문을 추가해야 한다.
- 정확도 확인: 특히 고주파 성분을 포함한 \mathbf{LH}, \mathbf{HL}, \mathbf{HH} 성분은 노이즈나 외부 간섭에 민감할 수 있으므로, 변환 결과의 정확성을 테스트하는 것이 중요하다.
- 필터 유형: Haar 웨이블릿 외에도 다양한 웨이블릿 필터(예: Daubechies, Symlet)를 사용할 수 있으며, 필터의 종류에 따라 변환 결과가 달라질 수 있다.