Python으로 배우는 DSP/Part 1. 신호의 기초와 디지털화

[08] 양자화(Quantization): 비트 깊이(Bit-depth)에 따른 오차 분석

multimedia 2026. 3. 28. 10:00
반응형

들어가며

샘플링이 시간축에서의 디지털화라면, 오늘 다룰 양자화(Quantization)는 진폭(Amplitude) 축에서의 디지털화입니다.

현실 세계의 아날로그 신호는 무한한 정밀도를 가진 연속적인 값을 가지지만, 컴퓨터는 메모리의 한계로 인해 이를 0과 1로 이루어진 유한한 비트(Bit)로 표현해야만 합니다. 이처럼 무한한 경우의 수를 유한한 단계(Level)로 매핑하는 과정에서 필연적으로 **오차(Error)**가 발생하게 됩니다.

이번 시간에는 비트 깊이(Bit-depth)가 신호의 품질에 어떤 영향을 미치는지, 그리고 이때 발생하는 양자화 오차와 SQNR의 관계를 Python 시뮬레이션을 통해 시각적으로 확인해 보겠습니다.

 

1. 양자화

아날로그 신호를 디지털로 변환(ADC: Analog-to-Digital Conversion)하는 과정은 크게 두 단계로 나뉩니다.

    • 샘플링(Sampling): 시간축으로 연속 신호를 일정한 시간 간격마다 값을 읽어 이산 수열로 변환하는 작업입니다. 
    • 양자화(Quantization): 아날로그 신호의 진폭을 유한한 디지털 레벨로 나누는 과정입니다.

양자화는 이산신호 $x[n]$을 아래 식처럼 유한한 레벨로 매핑합니다.

$$x_q[n] = Q(x[n])$$

일반적인 균일 양자화(Uniform Quantization)에서는, 신호가 표현될 수 있는 최대 범위를 $[x_{\min}, x_{\max}]$로 잡고 이를 $2^n$개의 레벨로 나눕니다. 여기서 비트 깊이(Bit-depth), 또는 양자화 비트 수($n$)는 아날로그 신호의 진폭을 몇 개의 디지털 레벨로 나눌 것인지를 결정합니다. 

    • 비트 수(비트 깊이): $n$
    • 레벨 수: $L = 2^n$
    • 양자화 간격(step size):
    $$\Delta = \frac{x_{\max} - x_{\min}}{2^n}$$

비트 깊이가 클수록 아날로그 신호의 진폭을 더 잘게 쪼개어 세밀하게 표현할 수 있으므로 원본 신호에 더 가까워집니다. 하지만 그만큼 데이터를 저장하고 전송하는 데 필요한 용량과 대역폭이 기하급수적으로 증가합니다.

 

2. 양자화 오차

아날로그 신호의 원래 값과 디지털로 변환된 양자화 값 사이의 차이를 양자화 오차(Quantization Error) 또는 양자화 잡음(Quantization Noise)이라고 부릅니다.

진폭을 제한된 단계로 반올림(또는 버림)하여 할당하기 때문에, 변환된 디지털 신호 파형을 확대해 보면 부드러운 곡선이 아니라 층이 지는 계단 현상(Staircase effect)을 띠게 됩니다. 비트 깊이가 낮을수록 이 계단 현상이 두드러지며, 오디오에서는 백색 잡음(White Noise) 형태의 '치익'하는 소리로, 이미지에서는 색상이 부드럽게 이어지지 않고 등고선처럼 보이는 밴딩(Color Banding) 현상으로 나타납니다.

양자화된 값 $x_q[n]$은 다음과 같이 표현됩니다.

$$x_q[n] = \Delta \cdot \text{round}\left(\frac{x[n]}{\Delta}\right)$$

그리고 양자화 오차 $e[n]$은:

$$e[n] = x_q[n] - x[n], \quad -\frac{\Delta}{2} \le e[n] < \frac{\Delta}{2}$$

즉, 양자화 오차는 $\pm\Delta/2$ 범위에 균일하게 분포한다고 가정합니다. 이것이 균일 양자화(Uniform Quantization)의 핵심 가정이며, 이 가정 위에서 SQNR 공식이 유도됩니다.

아래 코드는 동일한 사인파를 생성한 뒤, 각각 비트 깊이를 변화시키며 양자화했을 때 파형이 어떻게 변형되고 양자화 오차가 얼마나 커지는지 보여줍니다.

import numpy as np
import matplotlib.pyplot as plt

# 한글 폰트 설정 (macOS: AppleGothic, Windows: Malgun Gothic)
plt.rcParams['font.family'] = 'Malgun Gothic'
plt.rcParams['axes.unicode_minus'] = False


# 원본 신호 생성 (1kHz 사인파, 샘플링 44100Hz)
fs = 44100
t = np.linspace(0, 0.005, int(fs * 0.005), endpoint=False)
x = np.sin(2 * np.pi * 1000 * t)  # 진폭 범위 [-1, +1]

def quantize(signal, n_bits):
    """균일 양자화: n_bits 비트로 양자화"""
    levels = 2 ** n_bits
    delta = 2.0 / levels  # 진폭 범위 [-1, +1] 기준
    x_q = delta * np.round(signal / delta)
    # 클리핑: 범위 초과 방지
    x_q = np.clip(x_q, -1.0, 1.0 - delta)
    return x_q

bit_depths = [2, 4, 8, 16]

fig, axes = plt.subplots(len(bit_depths), 2, figsize=(14, 10))
fig.suptitle('비트 깊이별 양자화 오차 비교', fontsize=14)

for i, n in enumerate(bit_depths):
    x_q = quantize(x, n)
    error = x_q - x

    axes[i, 0].plot(t * 1000, x, 'b-', alpha=0.5, label='원본', linewidth=1)
    axes[i, 0].step(t * 1000, x_q, 'r-', alpha=0.8, label=f'{n}-bit 양자화', linewidth=1)
    axes[i, 0].set_title(f'{n}-bit: {2**n}개 레벨, Δ = {2/2**n:.4f}')
    axes[i, 0].legend(fontsize=8)
    axes[i, 0].set_ylabel('진폭')

    axes[i, 1].plot(t * 1000, error, 'g-', linewidth=0.8)
    axes[i, 1].axhline(2/(2**(n+1)), color='r', linestyle='--', alpha=0.5, label=f'+Δ/2 = {1/2**n:.4f}')
    axes[i, 1].axhline(-2/(2**(n+1)), color='r', linestyle='--', alpha=0.5, label=f'-Δ/2')
    axes[i, 1].set_title(f'{n}-bit 양자화 오차')
    axes[i, 1].legend(fontsize=8)
    axes[i, 1].set_ylabel('오차')

for ax in axes[-1]:
    ax.set_xlabel('시간 (ms)')

plt.tight_layout()
plt.show()

코드를 실행하여 그래프를 살펴보면 8-bit 경우에는 원본과 거의 일치하여 오차(빨간선)가 미미하지만, 4-bit에서는 신호(파란선)의 계단 모양이 확연해지고, 2-bit(총 4단계)로 떨어지면 신호의 형태가 매우 거칠게 찌그러지며 양자화 오차가 걷잡을 수 없이 증폭되는 것을 시각적으로 확인할 수 있습니다.

 

3. SQNR: 양자화 품질의 정량적 척도

오차가 얼마나 작은지를 신호 대 양자화 잡음비(SQNR, Signal-to-Quantization Noise Ratio)로 정량화합니다.

$$SQNR = 10 \log_{10} \frac{P_{\text{signal}}}{P_{\text{noise}}}$$

균일 양자화 오차 $e[n]$이 $[-\Delta/2, +\Delta/2]$에서 균일 분포(Uniform Distribution)를 따른다고 가정합니다.

오차의 분산(= 잡음 전력)은:

$$P_{\text{noise}} = \sigma_e^2 = \frac{\Delta^2}{12}$$

신호가 진폭 $A$인 정현파라면 신호 전력은:

$$P_{\text{signal}} = \frac{A^2}{2}$$

$n$-bit 양자화에서 $\Delta = \frac{2A}{2^n}$ 이므로 ($A$는 풀스케일 진폭):

$$P_{\text{noise}} = \frac{(2A/2^n)^2}{12} = \frac{4A^2}{12 \cdot 2^{2n} } = \frac{A^2}{3 \cdot 2^{2n}}$$

따라서 SQNR은:

$$\begin{aligned} \text{SQNR} &= 10 \log_{10} \frac{A^2/2}{A^2/(3 \cdot 2^{2n})} = 10 \log_{10} \frac{3 \cdot 2^{2n}}{2} \\ &= 10 \log_{10} 3 + 2n \cdot 10 \log_{10} 2 - 10 \log_{10} 2 \\ &= 4.77 + 2n \times 3.0103 - 3.0103 \\ &\approx 4.77 - 3.01 + 6.02n \\ &= 6.02n + 1.76 \; [\text{dB}] \end{aligned}$$

이 식이 주는 직관적 의미는 비트 수 또는 비트 깊이가 1 증가할 때마다 SQNR은 약 6.02 dB가 개선됩니다. 6 dB는 전력비로 약 4배(진폭비로 2배) 개선에 해당합니다. 예시를 살펴보면 다음과 같습니다.

  • 8-bit: $6.02 \cdot 8 + 1.76 \approx 49.9$ dB
  • 10-bit: $\approx 62.0$ dB
  • 12-bit: $\approx 74.0$ dB
  • 16-bit: $\approx 98.1$ dB
  • 24-bit: $\approx 146.2$ dB

영상/센서/오디오에서 비트가 몇 비트냐는 결국 양자화로 인해 확보 가능한 SNR의 상한을 규정합니다.

다음 코드는 SQNR 측정 및 이론값을 비교하는 내용입니다.

import numpy as np
import matplotlib.pyplot as plt

# 한글 폰트 설정 (macOS: AppleGothic, Windows: Malgun Gothic)
plt.rcParams['font.family'] = 'Malgun Gothic'
plt.rcParams['axes.unicode_minus'] = False

fs = 44100
duration = 1.0
t = np.linspace(0, duration, int(fs * duration), endpoint=False)
x = np.sin(2 * np.pi * 1000 * t)  # 풀스케일 정현파

def quantize(signal, n_bits):
    levels = 2 ** n_bits
    delta = 2.0 / levels
    x_q = delta * np.round(signal / delta)
    return np.clip(x_q, -1.0, 1.0 - delta)

def compute_sqnr(x, x_q):
    signal_power = np.mean(x ** 2)
    noise_power = np.mean((x_q - x) ** 2)
    return 10 * np.log10(signal_power / noise_power)

bit_depths = range(1, 17)
sqnr_measured = []
sqnr_theory = []

for n in bit_depths:
    x_q = quantize(x, n)
    sqnr_measured.append(compute_sqnr(x, x_q))
    sqnr_theory.append(6.02 * n + 1.76)

plt.figure(figsize=(10, 5))
plt.plot(list(bit_depths), sqnr_theory, 'b--', label='이론값: 6.02n + 1.76 dB', linewidth=2)
plt.plot(list(bit_depths), sqnr_measured, 'ro-', label='측정값', markersize=6)
plt.xlabel('비트 깊이 (bits)')
plt.ylabel('SQNR (dB)')
plt.title('비트 깊이에 따른 SQNR: 이론값 vs 측정값')
plt.legend()
plt.grid(True, alpha=0.3)
plt.xticks(list(bit_depths))
plt.tight_layout()
plt.show()

실행 결과를 보면 측정값이 이론값과 거의 일치하는 것을 확인할 수 있습니다. 1~2 dB의 미세한 차이는 클리핑 처리와 부동소수점 연산의 영향입니다.

 

[Insight] 

교과서에서는 흔히 '비트를 늘려 화질/음질을 높이세요'라고 가르치지만, 현업 엔지니어의 세계는 리소스와의 치열한 싸움입니다.

  • 비디오/이미지 코덱: 수십 년간 8-bit(256레벨)가 업계 표준이었습니다. 하지만 최근 HDR(High Dynamic Range) 디스플레이가 대중화되면서, 어두운 암부의 미세한 밴딩(계단 현상)을 없애기 위해 진폭을 1024단계로 쪼개는 10-bit 코덱(HEVC 10-bit 등)이 표준으로 자리 잡고 있습니다. 겨우 2-bit를 늘렸을 뿐이지만, 앞서 수식에서 보셨듯 화질(SQNR)은 약 12 dB이나 상승합니다.
  • AI 및 NPU 환경: 흥미롭게도 최근 AI 분야에서는 화질 분야와 정반대로 '의도적인 비트 축소'가 핵심 트렌드입니다. 거대 언어 모델(LLM)을 디바이스 메모리에 올리기 위해 기존 32-bit 부동소수점 가중치를 INT8(8-bit) 심지어 INT2(2-bit) 정수형으로 양자화(Model Quantization)합니다. SQNR이 다소 희생되더라도, NPU(신경망 처리 장치)에서의 추론 속도와 메모리 효율을 극대화하는 것이 실무적으로 훨씬 더 유리하기 때문입니다.

결국 양자화란 '정밀도(Quality)'와 '자원(Resource)' 사이에서 가장 이상적인 균형점을 찾아내는 공학적 예술이라 할 수 있습니다.

 

다음 글 예고

지금까지 Part 1을 통해 아날로그 신호가 시간(샘플링)과 진폭(양자화) 측면에서 어떻게 디지털로 변환되는지 그 근간을 살펴보았습니다.

다음 Part 2부터는 신호 처리의 꽃이자, 시간 영역의 신호를 주파수 영역으로 분해하여 물리적 의미를 재해석하는 핵심 이론인 [09. 푸리에 변환(DFT/FFT): 시간에서 주파수로, 물리적 의미의 재해석]에 대해 본격적으로 알아보겠습니다.

 

📌 이전 연재 글 보기

 

반응형