에듀테크 프로젝트 대시보드
이산 코사인 변환과 JPEG
사진 파일이 원본의 1/10 크기로 줄어드는데도 눈에는 거의 똑같아 보이는 이유는? JPEG은 사진을 8×8 조각으로 자른 뒤, 각 조각을 여러 줄무늬(코사인 파동)의 합으로 바꾼다(DCT). 그리고 사람 눈이 둔감한 잔잔한 무늬 성분을 뭉뚝하게 버린다(양자화). 아래에서 직접 조각 하나를 그려 넣고 화질을 조절해 보자.
손실이 일어나는 단계는 딱 하나, ④ 양자화다. 나머지는 전부 되돌릴 수 있다. 화질(quality) 슬라이더가 바로 이 양자화 표의 세기를 조절한다.
모든 8×8 블록은 아래 64개 흑백 무늬를 적당한 밝기로 섞어 더한 것으로 정확히 표현된다. 왼쪽 위는 평평한 무늬(평균 밝기 = DC), 오른쪽·아래로 갈수록 촘촘한 줄무늬(고주파)다.
왼쪽 캔버스를 클릭·드래그하면 픽셀 밝기가 바뀐다(프리셋 버튼으로 시작해도 된다). 화질 슬라이더를 내리면 살아남는 계수 개수와 오차가 어떻게 변하는지 보라.
RMSE는 픽셀당 평균 밝기 오차(0~255 척도). 사람 눈은 대략 RMSE 3 이하면 차이를 거의 못 느낀다. 압축률은 "0이 아닌 계수 비율"로 매우 거칠게 추정한 값이다.
JPEG 표준이 정한 밝기용 양자화 표다. 각 DCT 계수를 대응하는 값으로 나눠 반올림한다. 오른쪽 아래(고주파)로 갈수록 나누는 수가 커서 거의 다 0이 된다 — 사람 눈이 고주파에 둔감하다는 실험 결과를 표에 새겨 넣은 것이다.
# 1D DCT-II 기저 (N=8) — 2D는 행에 한 번, 열에 한 번 def dct2(block): # block: 8x8, 값은 -128 시프트됨 F = zeros(8, 8) for u in range(8): for v in range(8): s = 0 for x in range(8): for y in range(8): s += block[x][y] * cos((2*x+1)*u*pi/16) * cos((2*y+1)*v*pi/16) F[u][v] = 0.25 * C(u) * C(v) * s # C(0)=1/√2, 그 외 1 return F quant = np.round(F / (Q_TABLE * scale)) # ← 유일한 손실 지점 # 복원: quant * (Q_TABLE*scale) 후 idct2, +128
| 현상 | 원인 |
|---|---|
| 격자무늬 블록 경계 (blocking) | 8×8 블록마다 DC가 조금씩 달라 계단처럼 보임 — 화질 낮을 때 심함 |
| 글자·경계 주변 잔물결 (ringing) | 급격한 모서리 = 고주파인데 고주파 계수를 버려서 생기는 깁스 현상 |
| 색 번짐 | Cb·Cr을 절반 해상도로 다운샘플(4:2:0)했기 때문 |
| 여러 번 저장할수록 열화 | 저장할 때마다 재양자화 — 손실이 누적됨 |