JPEG은 픽셀을 저장하지 않습니다 — 사진을 8×8 조각으로 잘라 각 조각을 64가지 물결의 합으로 다시 쓰고(DCT), 눈이 둔감한 잔물결의 정밀도를 깎아서(양자화) 수많은 0을 만든 뒤, 0을 공짜로 세는 방법(런렝스)으로 적습니다. 아래는 그 전 과정을 브라우저에서 실제로 실행하는 화면입니다 — 품질 슬라이더를 내리면 어느 물결부터 버려지는지, 사진 아무 데나 클릭하면 그 조각의 주파수 해부도가 보입니다.
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·양자화표·사진을 그대로 불러 씁니다. 베껴 적으면 언젠가 두 화면이 다른 말을 하게 됩니다.
0~255인 밝기에서 128을 빼 −128~127로 만든다. 0을 중심에 두면 배합비가 0 근처로 모여 나중에 더 많이 0이 된다.
f[i] = gray[블록 안 픽셀] − 128
이 페이지는 빠른 알고리즘이 아니라 정의식 그대로 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
}
여기서만 정보가 준다. Q50 기준 왼쪽 위는 16으로, 오른쪽 아래는 99로 나눈다 — 사람 눈을 실측해 만든 표가 규격에 박혀 있다.
q = round(F[i] / qt[i])
// 대부분의 q 가 0 이 된다
왼쪽 위(굵은 성분)에서 오른쪽 아래(잔물결)로 대각선 순회하면 0이 뒤쪽에 길게 이어진다. 그 연속을 개수로 적으면 끝 — 압축의 이익은 전부 여기서 실현된다.
for (i of 지그재그순서) { if (q === 0) { run++; continue; } bits += 자릿수(q) + 부호 + run길이 }
다시 Q를 곱하고 역DCT. 반올림에서 사라진 값은 복구되지 않고, 그 차이가 격자 자국과 모기 노이즈로 남는다.
g[i] = q * qt[i] // 역양자화
idct8(g, f) ; v = f[i] + 128
매끈한 하늘은 배합비 몇 개면 충분해 거의 안 깨진다. 반면 탑의 돌쌓기·지붕 널처럼 급한 변화는 잔물결이 많이 필요한데 그게 먼저 버려진다 — 그래서 경계 주변이 지저분해진다.
차이 = 복원 − 원본 MSE = 평균(차이²) → PSNR
두 칸은 같은 사진을 두 품질로 각각 압축해 복원한 결과이고, 같은 구역(탑 꼭대기가 있는 128×128)을 4배로 확대한 것입니다. 위 슬라이더와 무관하게 Q12·Q85로 고정돼 있어 언제 봐도 같은 조건이며, 아래 숫자는 압축비 — 화질을 무엇과 바꾸는지가 그 두 숫자 사이에 있습니다.
눈은 밝기엔 예민하고 색엔 둔하다. 그래서 JPEG은 시작부터 색 해상도를 가로세로 절반으로 버린다(4:2:0★) — 아무도 눈치 못 채는 75% 할인. 이 화면은 그 다음 단계인 밝기 채널의 압축을 보여준다.
DCT는 조각 하나를 "평평한 성분 + 점점 잔물결" 64개 파도의 배합비로 다시 쓴다. 수학적으로 손실이 전혀 없는 좌표 바꾸기다 — 이 페이지의 검증기는 양자화 없이 되돌리면 원본과 오차 10⁻⁸ 이하임을 실제로 확인한다.
양자화 — 각 물결의 배합비를 표준 테이블 숫자로 나눠 반올림한다. 잔물결일수록 큰 수로 나눠서 대부분 0이 된다. 눈이 둔감한 것부터 버리도록 사람 눈을 실측해 만든 테이블이 규격(ITU T.81)에 박혀 있다 — Q50에서 이 화면이 쓰는 테이블이 바로 그것.
양자화가 만든 0의 바다를 지그재그로 훑으면 0이 길게 이어진다 — "0이 열두 개" 라고 한 마디로 적으면 끝(런렝스). 남은 소수의 숫자만 짧은 부호로 적는다(허프만). 압축의 이익은 전부 여기서 실현된다.
품질을 바닥까지 내려보라 — 8×8 격자 자국(조각 경계가 드러남)과 글자·edge 주변의 모기 노이즈(잔물결을 버려서 생기는 링잉)가 나타난다. 인터넷에서 본 "짤 열화"의 정체가 이 두 가지다.
철학은 같다(주파수로 바꿔 눈이 둔한 것부터 버림). 다른 건 비디오 코덱의 예측 기술을 가져온 것 — 옆 블록으로 지금 블록을 예측하고 차이만 적는다(WebP=VP8, HEIC=HEVC, AVIF=AV1★). 같은 화질에 절반 크기가 그렇게 나온다.
기준일을 100으로 놓고 그린 선입니다. 통화가 서로 달라 지수로 겹쳤습니다.
JPEG 는 8×8 칸을 주파수로 바꾼 뒤 높은 쪽을 버립니다. 아래는 그 8×8 계수를 막대 높이로 세운 것입니다 — 왼쪽 뒤가 낮은 주파수, 오른쪽 앞이 높은 주파수입니다.
품질을 낮춘다는 것은 더 많은 막대를 0으로 만드는 일입니다. 앞쪽(높은 주파수)부터 사라지므로 가장자리와 잔무늬가 먼저 뭉개집니다 — 위 판에서 보이는 그 뭉개짐입니다.
계수 값은 전형적인 사진 블록을 흉내 낸 모형입니다 — 특정 사진의 실제 값이 아닙니다.