2차원 웨이블릿 변환은 이미지 및 신호 처리에서 필터링, 압축, 특징 추출에 중요한 역할을 한다. 여기서는 Haar 웨이블릿을 중심으로 2차원 이산 웨이블릿 변환(DWT)의 구현 방법을 설명하고, 이를 C++로 구현하는 방법을 제시하겠다. 기본적으로 2차원 웨이블릿 변환은 이미지의 행과 열에 각각 1차원 웨이블릿 변환을 적용하는 방식으로 진행된다. 이를 통해 이미지는 저주파 및 고주파 성분으로 분해된다.

2차원 웨이블릿 변환의 개요

2차원 웨이블릿 변환은 먼저 이미지의 행을 기준으로 변환을 수행한 후, 열을 기준으로 또 한 번 변환을 수행하여 고주파와 저주파 성분을 분리한다. 이 과정에서 이미지 \mathbf{I}가 크기 M \times N일 때, 2차원 변환 결과는 다음과 같은 네 가지 성분으로 나뉜다.

  1. LL 성분 (저주파 - 저주파): 원본 이미지의 저해상도 버전으로, 주요 정보가 포함된 성분
  2. LH 성분 (저주파 - 고주파): 수평 방향의 고주파 성분을 나타내며, 엣지 정보가 포함
  3. HL 성분 (고주파 - 저주파): 수직 방향의 고주파 성분을 나타내며, 엣지 정보가 포함
  4. HH 성분 (고주파 - 고주파): 대각선 방향의 고주파 성분

이 성분들은 행렬 \mathbf{I}의 각 사분면에 배치되며, 이미지의 다양한 특징을 추출하거나 압축하는 데 사용된다.

수학적 정의

주어진 이미지 \mathbf{I}의 2차원 웨이블릿 변환은 다음과 같이 정의할 수 있다. 먼저 행별로 1차원 웨이블릿 변환을 적용하여 중간 행렬 \mathbf{I}'을 얻고, 이어서 열별로 1차원 웨이블릿 변환을 적용하여 최종 변환 행렬 \mathbf{W}을 생성한다.

  1. 행 변환: 각 행 \mathbf{I}_i에 대해 1차원 변환을 수행하여 중간 행렬 \mathbf{I}'의 각 행을 계산한다.
\mathbf{I}'_{i,j} = \sum_{k} \mathbf{I}_{i,k} \cdot \psi(k - j)

여기서 \psi는 웨이블릿 함수이다.

  1. 열 변환: \mathbf{I}'의 각 열에 대해 1차원 변환을 수행하여 최종 행렬 \mathbf{W}를 생성한다.
\mathbf{W}_{i,j} = \sum_{k} \mathbf{I}'_{k,j} \cdot \psi(k - i)

이 과정을 통해, 최종 변환 행렬 \mathbf{W}\mathbf{LL}, \mathbf{LH}, \mathbf{HL}, \mathbf{HH} 사분면에 각기 다른 주파수 성분을 포함하게 된다.

C++ 코드 구성

C++에서 2차원 웨이블릿 변환을 구현할 때, 다음과 같은 단계로 진행할 수 있다.

  1. 1차원 웨이블릿 변환 함수 작성: 이미지의 각 행과 열에 대해 1차원 웨이블릿 변환을 적용하는 함수.
  2. 행렬 분해 및 결합: 변환 결과를 각 성분에 맞게 분해하고, 변환 후 이미지 크기에 맞추어 결합.
  3. 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}의 사분면은 다음과 같이 나눌 수 있다.

이를 코드로 나타내면 다음과 같다.

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. 메모리 사용 최적화: 중간 변환 결과를 저장하는 임시 메모리를 최소화하여 불필요한 메모리 할당을 줄이다.
  2. 병렬 처리: 각 행과 열에 대한 1차원 변환은 독립적이므로 병렬 처리가 가능한다. OpenMP와 같은 라이브러리를 사용하여 다중 스레드로 처리할 수 있다.
  3. 고정된 웨이블릿 필터: 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차원 변환을 수행한다. 이를 통해 성능을 크게 향상할 수 있다.

코드 구현 시 유의 사항