이 장에서는 이산 웨이블릿 변환(DWT)을 C++로 구현하는 방법을 단계별로 설명한다. 이를 통해 독자는 웨이블릿 변환의 기초 원리를 이해할 뿐 아니라, 실제 코드로 이를 구현하는 능력을 갖추게 될 것이다. 구현 예제는 Haar 웨이블릿을 사용한 기본적인 1차원 DWT를 기반으로 하며, 필요한 경우 다차원 확장에 대한 지침도 제공한다.
DWT의 수학적 배경
이산 웨이블릿 변환은 주어진 신호 \mathbf{x}를 고주파 성분과 저주파 성분으로 분리하는 변환이다. 이를 위해 두 가지 필터가 사용된다: 저주파 통과 필터 \mathbf{h}와 고주파 통과 필터 \mathbf{g}. 신호 \mathbf{x}에 대한 DWT는 다음과 같이 정의된다:
여기서, \mathbf{cA}는 저주파 성분(approximation coefficients), \mathbf{cD}는 고주파 성분(detail coefficients)을 나타낸다. 이러한 변환은 반복적으로 적용되어 다중 해상도 분석(Multi-Resolution Analysis)을 수행할 수 있다.
C++ 구현을 위한 기본 구조
DWT를 구현하기 위해 다음과 같은 C++ 코드를 작성할 수 있다. 이 코드는 1차원 배열을 입력으로 받아 Haar 웨이블릿 필터를 적용하여 저주파와 고주파 성분을 계산한다.
1. 필요한 헤더 파일 및 라이브러리 선언
#include <iostream>
#include <vector>
#include <cmath>
필요한 기본 헤더 파일을 포함한다. 이 예제에서는 vector
를 사용하여 신호 데이터를 저장하며, cmath
를 사용하여 수학적 계산을 수행한다.
2. Haar 웨이블릿 필터 정의
Haar 웨이블릿의 필터는 다음과 같은 단순한 형태를 가진다:
이 필터들은 입력 신호의 저주파 성분과 고주파 성분을 계산하는 데 사용된다.
std::vector<double> low_pass_filter = {1 / std::sqrt(2), 1 / std::sqrt(2)};
std::vector<double> high_pass_filter = {1 / std::sqrt(2), -1 / std::sqrt(2)};
3. DWT 변환 함수 정의
DWT를 적용하는 핵심 함수는 다음과 같다. 이 함수는 입력 신호와 필터를 받아서 필터링 결과를 반환한다.
std::vector<double> apply_filter(const std::vector<double>& signal, const std::vector<double>& filter) {
std::vector<double> result(signal.size() / 2);
for (size_t i = 0; i < result.size(); ++i) {
result[i] = signal[2 * i] * filter[0] + signal[2 * i + 1] * filter[1];
}
return result;
}
이 함수는 입력 신호의 짝수 인덱스와 홀수 인덱스를 필터링하여 저주파 및 고주파 성분을 계산한다.
4. 전체 DWT 과정
void perform_dwt(const std::vector<double>& signal) {
std::vector<double> approximation = apply_filter(signal, low_pass_filter);
std::vector<double> detail = apply_filter(signal, high_pass_filter);
std::cout << "Approximation Coefficients: ";
for (const auto& value : approximation) {
std::cout << value << " ";
}
std::cout << std::endl;
std::cout << "Detail Coefficients: ";
for (const auto& value : detail) {
std::cout << value << " ";
}
std::cout << std::endl;
}
이 코드는 입력된 신호를 저주파 필터와 고주파 필터에 각각 적용하여 두 가지 결과를 출력한다.
5. 사용 예제
이제 구현한 DWT 함수가 실제로 어떻게 작동하는지 예제를 통해 확인해 보자. 예를 들어, 다음과 같은 신호가 있다고 가정하자:
C++ 코드에서 이 신호를 입력으로 하여 DWT를 수행하는 방법은 다음과 같다:
int main() {
std::vector<double> signal = {4, 6, 10, 12, 14, 8, 6, 4};
std::cout << "Original Signal: ";
for (const auto& value : signal) {
std::cout << value << " ";
}
std::cout << std::endl;
perform_dwt(signal);
return 0;
}
이 프로그램을 실행하면 입력 신호의 저주파 및 고주파 성분이 다음과 같이 출력된다:
Original Signal: 4 6 10 12 14 8 6 4
Approximation Coefficients: 7.07107 15.5563 15.5563 7.07107
Detail Coefficients: -1.41421 -1.41421 4.24264 1.41421
6. 단계별 설명
이 코드는 다음과 같은 절차로 작동한다:
- 신호 입력:
signal
벡터를 통해 신호 데이터를 입력받는다. - 저주파 및 고주파 필터링: 각각의 샘플 쌍에 대해 저주파 필터와 고주파 필터를 적용하여
apply_filter
함수를 사용해 필터링을 수행한다. - 결과 출력: 필터링된 결과를
approximation
과detail
벡터에 저장하고, 이를 화면에 출력한다.
이러한 방식으로, DWT를 사용하면 신호의 저주파 및 고주파 성분을 쉽게 분리할 수 있다. 이 과정은 다중 해상도 분석(Multi-Resolution Analysis, MRA)의 기본 단위가 된다.
7. 확장: 다중 레벨 DWT
위의 예제는 신호의 1단계(레벨 1) 변환만을 수행하였다. 하지만 다중 해상도 분석에서는 반복적으로 저주파 성분에 대해 변환을 적용하여 다중 레벨 분석을 수행할 수 있다. 이를 위해 저주파 성분에 대해 추가적으로 DWT를 수행하는 재귀 함수 형태로 확장할 수 있다.
void multi_level_dwt(std::vector<double> signal, int levels) {
for (int i = 0; i < levels; ++i) {
std::cout << "Level " << i + 1 << ":" << std::endl;
perform_dwt(signal);
signal = apply_filter(signal, low_pass_filter); // 다음 레벨은 저주파 성분으로 다시 변환
}
}
이 함수는 입력 신호에 대해 지정된 레벨 수만큼 반복적으로 DWT를 수행하여, 각 단계에서 저주파 성분을 다시 변환한다. 예를 들어, 3단계 변환을 수행하려면 다음과 같이 코드를 수정할 수 있다:
int main() {
std::vector<double> signal = {4, 6, 10, 12, 14, 8, 6, 4};
int levels = 3;
multi_level_dwt(signal, levels);
return 0;
}
이제 프로그램은 3단계의 DWT 결과를 연속적으로 출력하게 되며, 각 단계에서 점차적으로 신호의 세부 정보를 추출하고 분석할 수 있다.
8. 메모리 효율적인 구현
위에서 설명한 DWT 구현은 직관적이지만, 신호 길이가 커질수록 메모리 사용량이 증가할 수 있다. 이를 방지하기 위해, 원본 신호의 일부만을 사용하여 계산을 수행하고 나머지 데이터를 덮어쓰는 방식으로 메모리 효율적인 구현이 가능하다.
메모리 효율적 DWT 함수
다음 코드는 원본 신호의 메모리를 재활용하여, 추가적인 메모리 할당 없이 DWT를 수행하는 방법을 보여준다:
void in_place_dwt(std::vector<double>& signal) {
int n = signal.size();
std::vector<double> temp(n);
while (n > 1) {
n /= 2;
for (int i = 0; i < n; ++i) {
temp[i] = (signal[2 * i] * low_pass_filter[0] + signal[2 * i + 1] * low_pass_filter[1]);
temp[i + n] = (signal[2 * i] * high_pass_filter[0] + signal[2 * i + 1] * high_pass_filter[1]);
}
for (int i = 0; i < 2 * n; ++i) {
signal[i] = temp[i];
}
}
}
메모리 재사용 방식의 작동 원리
이 함수는 다음과 같은 방법으로 작동한다:
- 임시 벡터 생성: 신호 길이와 동일한 크기의 임시 벡터
temp
를 만든다. - 신호의 절반 크기만 변환: 저주파 및 고주파 성분을 각각 신호의 절반에 저장하여, 전체 신호 크기를 절반으로 줄인다.
- 신호 덮어쓰기: 계산된 결과를 원본 신호에 다시 저장하여 메모리를 절약한다.
- 반복 감소: 신호의 크기를 줄여가며 재귀적으로 변환을 수행한다.
이 구현은 기존 방식에 비해 메모리 사용량이 절반 수준으로 줄어들며, 대용량 신호 처리에 효과적이다.
9. 역변환(Inverse DWT) 구현
DWT의 또 다른 중요한 기능은 역변환(Inverse Discrete Wavelet Transform, IDWT)이다. 신호를 원래 상태로 복원하기 위해 IDWT는 필터 계수를 사용하여 역변환을 수행한다.
위 식은 저주파 성분과 고주파 성분을 결합하여 원래 신호를 복원하는 과정이다.
C++ 역변환 함수
std::vector<double> inverse_dwt(const std::vector<double>& approximation, const std::vector<double>& detail) {
size_t n = approximation.size();
std::vector<double> signal(2 * n);
for (size_t i = 0; i < n; ++i) {
signal[2 * i] = (approximation[i] * low_pass_filter[0] + detail[i] * high_pass_filter[0]);
signal[2 * i + 1] = (approximation[i] * low_pass_filter[1] + detail[i] * high_pass_filter[1]);
}
return signal;
}
10. 역변환 사용 예제
int main() {
std::vector<double> approximation = {7.07107, 15.5563, 15.5563, 7.07107};
std::vector<double> detail = {-1.41421, -1.41421, 4.24264, 1.41421};
std::vector<double> reconstructed_signal = inverse_dwt(approximation, detail);
std::cout << "Reconstructed Signal: ";
for (const auto& value : reconstructed_signal) {
std::cout << value << " ";
}
std::cout << std::endl;
return 0;
}
이 코드는 DWT로부터 얻은 저주파 및 고주파 성분을 사용하여 원래의 신호를 복원하며, 예상 결과는 다음과 같다:
Reconstructed Signal: 4 6 10 12 14 8 6 4
이로써, DWT 및 역변환을 통해 신호를 분해하고 복원하는 전체 과정이 구현되었다.