← 실계산 조각 여섯 편
진짜 DCT · 표준 양자화 테이블(ITU T.81) · 압축은 전부 화면에서 실행

JPEG — 사진이 1/10로 줄어도 멀쩡한 이유

JPEG은 픽셀을 저장하지 않습니다 — 사진을 8×8 조각으로 잘라 각 조각을 64가지 물결의 합으로 다시 쓰고(DCT), 눈이 둔감한 잔물결의 정밀도를 깎아서(양자화) 수많은 0을 만든 뒤, 0을 공짜로 세는 방법(런렝스)으로 적습니다. 아래는 그 전 과정을 브라우저에서 실제로 실행하는 화면입니다 — 품질 슬라이더를 내리면 어느 물결부터 버려지는지, 사진 아무 데나 클릭하면 그 조각의 주파수 해부도가 보입니다.

사진 클릭 = 그 8×8 조각 해부

인코더 데스크

압축비 (추정★)
화질 PSNR
살아남은 계수
0이 된 계수
원본 크기
압축 크기 (추정★)
크기 추정★ = 지그재그 순회에서 0의 연속은 건너뛰고, 남은 계수만 (자릿수 비트+부호)로 합산 — 실제 JPEG의 허프만 부호를 단순화한 셈법이다. PSNR·계수 개수·복원 화면은 근사가 아니라 실행 결과다.

한 조각이 지나가는 길

JPEG 는 사진을 8×8 조각으로 잘라 하나씩 처리합니다. 위 화면에서 조각을 하나 골라(사진을 누르면 바뀝니다) 그 조각에서 무슨 일이 벌어지는지 세워 놓고 보겠습니다.

품질 고른 조각 살아남은 계수 버려진 계수 이 조각 오차

왼쪽은 픽셀, 오른쪽은 계수입니다 — 같은 8×8 격자지만 다른 물건입니다. 왼쪽 칸의 밝기가 픽셀 값이고, 오른쪽 막대의 높이는 그 주파수 성분의 크기입니다. 오른쪽 왼앞 구석(노란색)이 DC — 조각 전체의 평균 밝기이고, 오른뒤로 갈수록 잔무늬(고주파)입니다.

버리는 자리가 정해져 있습니다. 양자화표(ITU-T T.81 부속서 K)는 고주파 구석일수록 나누는 수가 큽니다. 그러니 구석부터 0이 됩니다 — 화면에서 검붉게 죽는 막대가 그것입니다. 품질 손잡이를 내리면 죽는 범위가 안쪽으로 번집니다.

품질을 내리면 이렇게 죽습니다. 조각 (15, 20) 에서 살아남는 계수는 Q95 33개Q75 17개Q50 12개Q25 10개Q10 3개Q2 1개(DC 하나만) 입니다. 같은 순서로 이 조각의 오차는 1.55 → 3.90 → 5.73 → 6.62 → 9.89 → 18.54. Q2 에서 남는 한 개가 DC — 조각 전체의 평균 밝기 하나만 남으니 그 조각은 단색 사각형이 됩니다. 저품질 JPEG 의 그 네모난 얼룩이 이것입니다. (손잡이를 그 값에 맞추면 그대로 나옵니다.)

변환 자체는 무손실입니다. 화면이 매 프레임 idct8(dct8(f))를 원본과 견주어 왕복 오차를 찍습니다 — 1e-9 를 넘으면 ‘★어긋남’이 뜹니다. 사진이 상하는 건 DCT 가 아니라 그 다음의 나눗셈입니다. 이 구분이 이 조각의 전부입니다.

계산을 여기서 다시 짜지 않았습니다. 위 인코더가 쓰는 DCT·양자화표·사진을 그대로 불러 씁니다. 베껴 적으면 언젠가 두 화면이 다른 말을 하게 됩니다.

풀고 있는 방정식

F(u,v) = ¼ C(u)C(v) ΣΣ f(x,y) · cos((2x+1)uπ/16) · cos((2y+1)vπ/16) DCT-II. 8×8 픽셀 64개를 64가지 물결의 배합비 64개로 바꾼다. 개수가 그대로라 버리는 게 없다 — 손실이 아니라 좌표계를 바꾼 것뿐이다. u=v=0인 첫 항이 평균(평평한 성분), 오른쪽 아래로 갈수록 잔물결.
F̂(u,v) = round( F(u,v) / Q(u,v) ) 버리는 순간은 여기 한 곳뿐이다. 규격 테이블 Q로 나누고 반올림한다. Q는 잔물결일수록 큰 수(왼쪽 위 16 → 오른쪽 아래 99)여서, 눈이 둔한 성분부터 0이 된다. 되돌릴 때는 다시 Q를 곱하는데, 반올림에서 잘린 만큼은 영영 돌아오지 않는다.
s = Q<50 ? 5000/Q : 200−2Q  ·  Q = floor((기본표·s + 50) / 100) 품질 슬라이더가 하는 일 전부. Q50이면 s=100이라 규격 기본표 그대로, Q10이면 s=500이라 나누는 수가 5배 — 그만큼 더 많이 0이 된다(IJG 관행).
PSNR = 10·log₁₀( 255² / MSE ) 복원본과 원본의 차이를 dB로. 30dB 근처면 눈으로 구분이 어렵고, 20dB 아래면 확실히 뭉개진 게 보인다. 이 페이지는 모든 픽셀의 차이를 실제로 재서 이 값을 낸다.

조각 하나가 압축되는 5단계

STEP 1

8×8로 자르고 가운데로 옮긴다

0~255인 밝기에서 128을 빼 −128~127로 만든다. 0을 중심에 두면 배합비가 0 근처로 모여 나중에 더 많이 0이 된다.

f[i] = gray[블록 안 픽셀] − 128
STEP 2

64개 물결의 배합비를 구한다

이 페이지는 빠른 알고리즘이 아니라 정의식 그대로 4,096번 곱한다 — 느리지만 속이지 않는다.

for (u,v) {
  s = ΣΣ f[x,y]*COS[u][x]*COS[v][y]
  F[u,v] = 0.25*C(u)*C(v)*s
}
STEP 3

규격 테이블로 나눈다

여기서만 정보가 준다. Q50 기준 왼쪽 위는 16으로, 오른쪽 아래는 99로 나눈다 — 사람 눈을 실측해 만든 표가 규격에 박혀 있다.

q = round(F[i] / qt[i])
// 대부분의 q 가 0 이 된다
STEP 4

지그재그로 훑어 0을 뭉친다

왼쪽 위(굵은 성분)에서 오른쪽 아래(잔물결)로 대각선 순회하면 0이 뒤쪽에 길게 이어진다. 그 연속을 개수로 적으면 끝 — 압축의 이익은 전부 여기서 실현된다.

for (i of 지그재그순서) {
  if (q === 0) { run++; continue; }
  bits += 자릿수(q) + 부호 + run길이
}
STEP 5

되돌린다 — 잘린 만큼은 안 온다

다시 Q를 곱하고 역DCT. 반올림에서 사라진 값은 복구되지 않고, 그 차이가 격자 자국과 모기 노이즈로 남는다.

g[i] = q * qt[i]        // 역양자화
idct8(g, f) ; v = f[i] + 128
RENDER

오차가 어디에 남는지

매끈한 하늘은 배합비 몇 개면 충분해 거의 안 깨진다. 반면 탑의 돌쌓기·지붕 널처럼 급한 변화는 잔물결이 많이 필요한데 그게 먼저 버려진다 — 그래서 경계 주변이 지저분해진다.

차이 = 복원 − 원본
MSE = 평균(차이²) → PSNR

두 품질 나란히 — 같은 사진, 같은 자리

Q12 · 많이 버린 쪽
8×8 격자 자국과 경계의 지저분함이 보인다.
Q85 · 조금 버린 쪽
같은 사진, 같은 자리를 4배 확대한 것.

두 칸은 같은 사진을 두 품질로 각각 압축해 복원한 결과이고, 같은 구역(탑 꼭대기가 있는 128×128)을 4배로 확대한 것입니다. 위 슬라이더와 무관하게 Q12·Q85로 고정돼 있어 언제 봐도 같은 조건이며, 아래 숫자는 압축비 — 화질을 무엇과 바꾸는지가 그 두 숫자 사이에 있습니다.

여섯 가지 궁금증

1

색부터 버린다

눈은 밝기엔 예민하고 색엔 둔하다. 그래서 JPEG은 시작부터 색 해상도를 가로세로 절반으로 버린다(4:2:0★) — 아무도 눈치 못 채는 75% 할인. 이 화면은 그 다음 단계인 밝기 채널의 압축을 보여준다.

2

8×8 조각과 64개의 물결

DCT는 조각 하나를 "평평한 성분 + 점점 잔물결" 64개 파도의 배합비로 다시 쓴다. 수학적으로 손실이 전혀 없는 좌표 바꾸기다 — 이 페이지의 검증기는 양자화 없이 되돌리면 원본과 오차 10⁻⁸ 이하임을 실제로 확인한다.

3

버리는 순간은 단 한 곳

양자화 — 각 물결의 배합비를 표준 테이블 숫자로 나눠 반올림한다. 잔물결일수록 큰 수로 나눠서 대부분 0이 된다. 눈이 둔감한 것부터 버리도록 사람 눈을 실측해 만든 테이블이 규격(ITU T.81)에 박혀 있다 — Q50에서 이 화면이 쓰는 테이블이 바로 그것.

4

0은 공짜다

양자화가 만든 0의 바다를 지그재그로 훑으면 0이 길게 이어진다 — "0이 열두 개" 라고 한 마디로 적으면 끝(런렝스). 남은 소수의 숫자만 짧은 부호로 적는다(허프만). 압축의 이익은 전부 여기서 실현된다.

5

저품질 사진이 깨지는 모양의 정체

품질을 바닥까지 내려보라 — 8×8 격자 자국(조각 경계가 드러남)과 글자·edge 주변의 모기 노이즈(잔물결을 버려서 생기는 링잉)가 나타난다. 인터넷에서 본 "짤 열화"의 정체가 이 두 가지다.

6

WebP·HEIC·AVIF는 뭐가 다른가

철학은 같다(주파수로 바꿔 눈이 둔한 것부터 버림). 다른 건 비디오 코덱의 예측 기술을 가져온 것 — 옆 블록으로 지금 블록을 예측하고 차이만 적는다(WebP=VP8, HEIC=HEVC, AVIF=AV1★). 같은 화질에 절반 크기가 그렇게 나온다.

이 화면은 이 네 종목 이야기입니다● 시세 불러오는 중

기준일을 100으로 놓고 그린 선입니다. 통화가 서로 달라 지수로 겹쳤습니다.

같은 기술의 서로 다른 칸입니다. 주가가 이 기술 때문이라고는 말하지 않습니다.

버리는 것이 어디인지 세워서 본다

JPEG 는 8×8 칸을 주파수로 바꾼 뒤 높은 쪽을 버립니다. 아래는 그 8×8 계수를 막대 높이로 세운 것입니다 — 왼쪽 뒤가 낮은 주파수, 오른쪽 앞이 높은 주파수입니다.

8×8 주파수 계수끌어서 회전 · 휠로 확대 · 손 떼면 다시 돈다
살아남는 낮은 주파수버려지는 높은 주파수
에너지가 왼쪽 뒤 구석에 몰립니다 — 이게 JPEG 가 되는 이유입니다. 나머지 대부분은 0에 가까워서 버려도 눈에 잘 안 띕니다.

품질을 낮춘다는 것은 더 많은 막대를 0으로 만드는 일입니다. 앞쪽(높은 주파수)부터 사라지므로 가장자리와 잔무늬가 먼저 뭉개집니다 — 위 판에서 보이는 그 뭉개짐입니다.

계수 값은 전형적인 사진 블록을 흉내 낸 모형입니다 — 특정 사진의 실제 값이 아닙니다.