Quantização: onde a compressão com perdas finalmente acontece
Olá leitor, seja muito bem vindo de volta a mais uma etapa da nossa jornada aqui no Portal da Micilini! 😊
Este é o décimo artigo da nossa série de ~25, e se o artigo anterior (a DCT) foi o coração do CODEC, este aqui é a alma da compressão. É neste artigo que o nosso CODEC finalmente vai jogar dados fora de propósito, de um jeito tão inteligente que o seu olho quase não vai perceber. 🪄
Deixa eu te lembrar rapidinho onde estamos na jornada:
- Fase 1 (artigos 1-3): cores, RGB, YCbCr e Chroma Subsampling. 🎨
- Fase 2 (artigos 4-7): fundamentos de compressão — entropia, RLE, Huffman e LZ77. 📦
- Fase 3 (artigos 8-9): dividimos a imagem em blocos 8×8 e aplicamos a DCT, que transformou pixels em frequências. 🌊
No fim do artigo 9, eu bati muito nessa tecla: a DCT sozinha não comprime nada.
Ela só reorganizou a energia do bloco, empurrando tudo o que importa pro canto superior esquerdo. Lembra daquele resultado?
No nosso bloco de teste, 97% da energia ficou em apenas 16 dos 64 coeficientes. 🤯
Pois é. Agora chegou a hora de usar essa reorganização a nosso favor. Pega um café bem reforçado ☕, porque este artigo é longo, detalhado e cheio de exemplos. Bora!
Onde paramos: A DCT preparou o terreno
Só pra recolocar você nos trilhos, esses foram os coeficientes DCT do nosso bloco de teste (o famoso bloco clássico do JPEG que usamos no artigo 9):
Coeficientes DCT (canto sup. esq. = DC):
-415.38 -30.19 -61.20 27.24 56.12 -20.10 -2.39 0.46
4.47 -21.86 -60.76 10.25 13.15 -7.09 -8.54 4.88
-46.83 7.37 77.13 -24.56 -28.91 9.93 5.42 -5.65
-48.53 12.07 34.10 -14.76 -10.24 6.30 1.83 1.95
12.12 -6.55 -13.20 -3.95 -1.88 1.75 -2.79 3.14
-7.73 2.91 2.38 -5.94 -2.38 0.94 4.30 1.85
-1.03 0.18 0.42 -2.42 -0.88 -3.02 4.12 -0.66
-0.17 0.14 -1.07 -4.19 -1.17 -0.10 0.50 1.68Repare no padrão: os números grandes (em módulo) estão amontoados no topo-esquerda, e conforme você anda pra baixo e pra direita, eles vão ficando minúsculos: , 1.68, -0.66, 0.46... 👀0.14
Esses coeficientes minúsculos de alta frequência são detalhes tão finos que o olho humano mal registra. E aqui vem a pergunta de ouro deste artigo:
E se a gente simplesmente... arredondasse esses coeficientes pequenos pra zero? 🤔
É exatamente isso que a quantização faz. Só que de um jeito bem mais esperto do que "arredondar tudo igual".
O que é quantização?
Quantizar, na essência, é reduzir a quantidade de valores possíveis que um número pode assumir. É pegar uma escala contínua e transformá-la numa escada com poucos degraus.
Aqui vai uma analogia do mundo real: imagina que você vai anotar a altura de várias pessoas. 📏
- Sem quantização: você anota tudo com precisão milimétrica, por exemplo: Fulano de tal tem 1,73m, Ciclano de tal tem 1,68m, e aquele outro ali tem 1,72m...
- Com quantização grosseira: você arredonda tudo pro decímetro mais próximo, ou seja, pega a idade de todo mundo e converte para 1,7m, 1,7m, 1,7m...
No segundo caso, você perdeu informação (não dá mais pra saber se a pessoa tinha 1,68m ou 1,72m), mas ganhou algo enorme: em vez de você possuir infinitos valores possíveis, agora você só tem alguns poucos ("degraus").
E como nós sabemos, valores repetidos comprimem MUITO bem. 🎯
Sendo assim, podemos dizer que a matemática da quantização é ridículamente simples, observe:
valor_quantizado = arredondar( valor_original / passo )Onde passo é o tamanho do degrau. Quanto maior o passo, mais grosseira a quantização (mais perda, mais compressão).
Exemplo com passo = 10:
- 56.12 / 10 = 5.612 → arredonda pra 6
- 0.46 / 10 = 0.046 → arredonda pra 0 ✨ (virou zero!)
- -415.38 / 10 = -41.538 → arredonda pra -42
Viu como os números pequenos viram zero e os grandes sobrevivem (com menos precisão)?
É essa a mágica da quantização. E agora vamos aprender um pouco mais sobre o pulo do gato do JPEG. 🐈
A tabela de quantização: um passo diferente pra cada frequência
É importante ressaltar que o JPEG não usa um passo único pra todos os coeficientes, fazer uma atrocidade dessas seria burrice demais, até porque a gente já sabe que:
- 👁️ O olho é muito sensível às baixas frequências (topo-esquerda) → precisamos de passos pequenos ali, pra preservar precisão.
- 🙈 O olho é pouco sensível às altas frequências (baixo-direita) → podemos usar passos grandes ali, jogando quase tudo fora.
Então, em vez de um passo único, o JPEG usa uma matriz 8×8 de passos, chamada de tabela de quantização (ou matriz Q). Cada coeficiente é dividido pelo seu próprio passo, na sua posição correspondente.

Conhecendo a tabela padrão do JPEG (luminância)
O comitê que criou o JPEG fez estudos com testes visuais em humanos pra descobrir a sensibilidade do olho a cada frequência, e cristalizou isso numa tabela recomendada (a famosa tabela do Anexo K da especificação).
Vamos dar uma olhadinha nela:
Tabela de quantização padrão — Luminância (Y):
16 11 10 16 24 40 51 61
12 12 14 19 26 58 60 55
14 13 16 24 40 57 69 56
14 17 22 29 51 87 80 62
18 22 37 56 68 109 103 77
24 35 55 64 81 104 113 92
49 64 78 87 103 121 120 101
72 92 95 98 112 100 103 99Repare na história que os números acima nos contam:
- Topo-esquerda = 16 (o passo do coeficiente DC). É um dos menores. O DC é sagrado, quase não mexemos nele.
- Baixo-direita = 99 (frequência máxima). Um passo gigante. Qualquer coeficiente de alta frequência que não for enorme vira zero na hora.
- O crescimento não é perfeitamente uniforme, porque a sensibilidade do olho não é uniforme, pois tem algumas irregularidades que vieram direto dos testes com humanos. 🧑🔬
Por que cor tem tabela própria (e é mais agressiva)?
Lembra lá do artigo 3, quando falamos de Chroma Subsampling?
Naquela época a gente já explorou que o olho humano enxerga muito mais detalhe em brilho (luminância) do que em cor (crominância).
A quantização usa esse mesmo princípio de novo! Os canais Cb e Cr (cor) usam uma tabela separada e muito mais agressiva, observe:
Tabela de quantização padrão — Crominância (Cb, Cr):
17 18 24 47 99 99 99 99
18 21 26 66 99 99 99 99
24 26 56 99 99 99 99 99
47 66 99 99 99 99 99 99
99 99 99 99 99 99 99 99
99 99 99 99 99 99 99 99
99 99 99 99 99 99 99 99
99 99 99 99 99 99 99 99Olha só que interessante: a partir de certo ponto, é 99 pra tudo. 😲 A cor de alta frequência é praticamente aniquilada, porque o olho simplesmente não sente falta.
É por isso que num JPEG você pode destruir bastante a informação de cor sem que a imagem pareça ruim, porem... se você mexer demais no brilho estraga tudo na hora.
Entendendo o fato de qualidade (aquele slider de 1 a 100)
"Peraí Will, mas e aquele controle de qualidade que aparece quando eu salvo um JPEG? De onde ele sai?" 🤨
Ótima pergunta! Aquele número de 1 a 100 é simplesmente um multiplicador aplicado na tabela de quantização. Ele escala todos os passos pra cima ou pra baixo:
- Qualidade alta (ex: 90): multiplica a tabela por um fator pequeno → passos pequenos → pouca perda, arquivo maior. 💎
- Qualidade baixa (ex: 10): multiplica a tabela por um fator grande → passos enormes → muita perda, arquivo minúsculo. 🗑️
A fórmula que a libjpeg (a biblioteca de referência) usa é esta:
se qualidade < 50:
fator = 5000 / qualidade
senão:
fator = 200 - qualidade * 2
Q_escalada[i][j] = (Q_base[i][j] * fator + 50) / 100
(limitado ao range de 1 a 255)Um detalhe curioso: qualidade 50 é o ponto neutro. Nela, , então fator = 200 - 100 = 100. Ou seja, na qualidade 50 você usa a tabela padrão exatamente como ela é. 🎯Q_escalada = Q_base * 100 / 100 = Q_base

A matemática: quantizar e dequantizar
A matemática é simples e pode se resumir em apenas duas operações, uma sendo o inverso da outra.
Quantização (no encoder): divide cada coeficiente pelo passo da sua posição e arredonda:
Q(u,v) = arredondar( DCT(u,v) / TabelaQ(u,v) )Dequantização (no decoder): multiplica de volta:
DCT_recuperado(u,v) = Q(u,v) × TabelaQ(u,v)Parece que uma desfaz a outra, né? Mas não desfaz! ⚠️ E é aqui que mora toda a perda.
⚠️ Atenção: aqui a perda é IRREVERSÍVEL
Essa é a diferença mais importante entre o artigo 9 e este.
No artigo 9, a gente provou que a DCT → IDCT é lossless: você ia e voltava sem perder nenhum bit. A DCT é uma via de mão dupla perfeita. 🔄✅
A quantização não é. Olha o que acontece com o coeficiente DC do nosso bloco na qualidade 50:
Original DCT: -415.38
Quantiza: -415.38 / 16 = -25.96 → arredonda pra -26
Dequantiza: -26 × 16 = -416.00Você percebeu? O virou -415.38. Aquele -416.00 se perdeu pra sempre. 💀.38
Não tem como recuperar, porque o arredondamento jogou fora a informação de "quão longe" o valor estava do degrau mais próximo.
E com os coeficientes pequenos é ainda mais dramático:
Original DCT: 0.46
Quantiza: 0.46 / 61 = 0.0075 → arredonda pra 0
Dequantiza: 0 × 61 = 0.00O virou 0.46 e nunca mais volta... sumiu. 👻0
É por isso que a quantização é a etapa lossy do JPEG. A DCT reorganiza (sem perda), a quantização descarta (com perda), e é justamente por descartar que ela comprime. A arte está em descartar só o que o olho não sente falta. 🎨
Exemplo completo passo a passo
Agora vamos fazer o processo inteiro, cell por cell, com o nosso bloco na qualidade 50. Como vimos, na qualidade 50 a tabela escalada é igual à tabela base:
Passo 1: os coeficientes DCT (do artigo 9), e a tabela Q (qualidade 50):
Coeficientes DCT: Tabela Q (qualidade 50):
-415.38 -30.19 -61.20 ... 16 11 10 ...
4.47 -21.86 -60.76 ... 12 12 14 ...
-46.83 7.37 77.13 ... 14 13 16 ...
... ...Passo 2: dividir e arredondar, célula por célula. Vou mostrar algumas contas pra você acompanhar o que está acontecendo:
posição (0,0): -415.38 / 16 = -25.96 → -26
posição (0,1): -30.19 / 11 = -2.74 → -3
posição (0,2): -61.20 / 10 = -6.12 → -6
posição (0,4): 56.12 / 24 = 2.34 → 2
posição (0,7): 0.46 / 61 = 0.008 → 0 ✨
posição (2,2): 77.13 / 16 = 4.82 → 5
posição (7,7): 1.68 / 99 = 0.017 → 0 ✨Fazendo isso pros 64 coeficientes, o resultado é:
Coeficientes QUANTIZADOS (qualidade 50):
-26 -3 -6 2 2 -1 0 0
0 -2 -4 1 1 0 0 0
-3 1 5 -1 -1 0 0 0
-3 1 2 -1 0 0 0 0
1 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0OLHA ISSO! 😱 De 64 coeficientes, 44 viraram zero (69% do bloco!). Sobrou um cantinho pequeno de valores no topo-esquerda, e o resto é um oceano de zeros. 🌊
E aqui está o segredo da compressão: uma sequência gigante de zeros comprime absurdamente bem com RLE + Huffman (as técnicas da Fase 2, lembra?).
E nós iremos usar exatamente isso nos próximos dois artigos. 📦

Passo 3: dequantizar (multiplicar de volta), pra ver o que o decoder recupera:
posição (0,0): -26 × 16 = -416 (era -415.38, perdemos 0.62)
posição (0,1): -3 × 11 = -33 (era -30.19, "erramos" 2.81)
posição (0,7): 0 × 61 = 0 (era 0.46, sumiu de vez)Passo 4: IDCT nos coeficientes dequantizados, pra voltar aos pixels. E aí comparamos com o bloco original pra medir o estrago. É aqui que entra o PSNR, que eu vou explicar já já. 📐
Múltiplas qualidades: vendo o trade-off
Para ver o resultado final do trade-off acompanhe abaixo:
Qualidade 90 -> zeros: 35/64 (55%) | erro max: 10.0 | PSNR: 40.10 dB
Qualidade 50 -> zeros: 44/64 (69%) | erro max: 14.6 | PSNR: 32.60 dB
Qualidade 10 -> zeros: 57/64 (89%) | erro max: 31.7 | PSNR: 25.69 dBLê essa tabela com carinho, porque ela resume o coração do JPEG: 💛
- Qualidade 90: só 55% de zeros, erro máximo de 10 níveis, PSNR alto (40 dB). Imagem quase perfeita, arquivo maior. 💎
- Qualidade 50: 69% de zeros, erro de ~15 níveis, PSNR médio (33 dB). O ponto de equilíbrio clássico. ⚖️
- Qualidade 10: 89% de zeros!, erro de até 32 níveis, PSNR baixo (26 dB). Arquivo minúsculo, mas os quadradinhos já aparecem. 🧱
Perceba a relação: mais zeros = mais compressão = menos PSNR = mais perda visível. É sempre esse cabo de guerra. O fator de qualidade é literalmente você escolhendo onde quer ficar nessa corda. 🪢
PSNR: como medir "quão boa" ficou a imagem
Como eu te disse anteriormente, agora chegou o momento de explicar o que é essa tal de PSNR rs
PSNR significa Peak Signal-to-Noise Ratio (relação sinal-ruído de pico). É a métrica mais clássica pra medir o quanto uma imagem comprimida se afastou da original. A fórmula é:
MSE = média de (pixel_original - pixel_reconstruído)² (o erro quadrático médio)
PSNR = 10 × log₁₀( 255² / MSE ) (resultado em decibéis, dB)Como interpretar o número: 📊
- PSNR alto (> 40 dB): diferença praticamente imperceptível. 🤩
- PSNR médio (30-40 dB): boa qualidade, perdas leves e aceitáveis. 🙂
- PSNR baixo (< 30 dB): perdas visíveis, artefatos começam a incomodar. 😕
- PSNR = ∞: imagem idêntica (MSE = 0, sem nenhuma perda). ♾️
Repara que o PSNR usa uma escala logarítmica (por causa do ), então cada poucos dB representam uma diferença grande de qualidade. É a mesma lógica dos decibéis do som. 🔊log₁₀
⚠️ Uma ressalva honesta: o PSNR mede diferença matemática, não diferença percebida pelo olho. Duas imagens com o mesmo PSNR podem parecer bem diferentes pro olho humano. Por isso existem métricas mais modernas como SSIM e VMAF, que tentam modelar a percepção.
O código completo em C
Hora de botar a mão na massa! Esse programa junta tudo: reaproveita a DCT do artigo 9, adiciona as tabelas de quantização, a escala por qualidade, o pipeline completo e o cálculo do PSNR.
Sendo assim, vamos criar uma pasta chamada codec-quant-test, e dentro dela um main.c:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <math.h>
#define BLOCK_SIZE 8
#define PI 3.14159265358979323846
typedef struct { double valores[BLOCK_SIZE][BLOCK_SIZE]; } Bloco8x8;
// ============================================================
// DCT DO ARTIGO 9 (tabelas de cosseno + versão separável)
// ============================================================
double tabela_cos[BLOCK_SIZE][BLOCK_SIZE];
double alpha[BLOCK_SIZE];
void inicializar_tabelas_dct(void) {
for (int freq = 0; freq < BLOCK_SIZE; freq++) {
alpha[freq] = (freq == 0) ? (1.0 / sqrt(2.0)) : 1.0;
for (int pos = 0; pos < BLOCK_SIZE; pos++)
tabela_cos[freq][pos] =
cos((2.0 * pos + 1.0) * freq * PI / (2.0 * BLOCK_SIZE));
}
}
void dct_1d(double in[BLOCK_SIZE], double out[BLOCK_SIZE]) {
for (int f = 0; f < BLOCK_SIZE; f++) {
double s = 0.0;
for (int p = 0; p < BLOCK_SIZE; p++) s += in[p] * tabela_cos[f][p];
out[f] = 0.5 * alpha[f] * s;
}
}
void idct_1d(double in[BLOCK_SIZE], double out[BLOCK_SIZE]) {
for (int p = 0; p < BLOCK_SIZE; p++) {
double s = 0.0;
for (int f = 0; f < BLOCK_SIZE; f++)
s += alpha[f] * in[f] * tabela_cos[f][p];
out[p] = 0.5 * s;
}
}
void dct_separavel(Bloco8x8 *e, Bloco8x8 *saida) {
double temp[BLOCK_SIZE][BLOCK_SIZE];
for (int lin = 0; lin < BLOCK_SIZE; lin++) {
double a[BLOCK_SIZE], b[BLOCK_SIZE];
for (int col = 0; col < BLOCK_SIZE; col++)
a[col] = e->valores[lin][col] - 128.0; // level shift
dct_1d(a, b);
for (int col = 0; col < BLOCK_SIZE; col++) temp[lin][col] = b[col];
}
for (int col = 0; col < BLOCK_SIZE; col++) {
double a[BLOCK_SIZE], b[BLOCK_SIZE];
for (int lin = 0; lin < BLOCK_SIZE; lin++) a[lin] = temp[lin][col];
dct_1d(a, b);
for (int lin = 0; lin < BLOCK_SIZE; lin++) saida->valores[lin][col] = b[lin];
}
}
void idct_separavel(Bloco8x8 *e, Bloco8x8 *saida) {
double temp[BLOCK_SIZE][BLOCK_SIZE];
for (int col = 0; col < BLOCK_SIZE; col++) {
double a[BLOCK_SIZE], b[BLOCK_SIZE];
for (int lin = 0; lin < BLOCK_SIZE; lin++) a[lin] = e->valores[lin][col];
idct_1d(a, b);
for (int lin = 0; lin < BLOCK_SIZE; lin++) temp[lin][col] = b[lin];
}
for (int lin = 0; lin < BLOCK_SIZE; lin++) {
double a[BLOCK_SIZE], b[BLOCK_SIZE];
for (int col = 0; col < BLOCK_SIZE; col++) a[col] = temp[lin][col];
idct_1d(a, b);
for (int col = 0; col < BLOCK_SIZE; col++)
saida->valores[lin][col] = b[col] + 128.0; // desfaz o level shift
}
}
// ============================================================
// TABELAS DE QUANTIZAÇÃO PADRÃO DO JPEG (Anexo K)
// ============================================================
//
// Luminância (Y): sensível ao olho, passos menores.
// Crominância (Cb, Cr): menos sensível, passos maiores (mais agressiva).
const int Q_LUMINANCIA[BLOCK_SIZE][BLOCK_SIZE] = {
{16, 11, 10, 16, 24, 40, 51, 61},
{12, 12, 14, 19, 26, 58, 60, 55},
{14, 13, 16, 24, 40, 57, 69, 56},
{14, 17, 22, 29, 51, 87, 80, 62},
{18, 22, 37, 56, 68,109,103, 77},
{24, 35, 55, 64, 81,104,113, 92},
{49, 64, 78, 87,103,121,120,101},
{72, 92, 95, 98,112,100,103, 99}
};
const int Q_CROMINANCIA[BLOCK_SIZE][BLOCK_SIZE] = {
{17, 18, 24, 47, 99, 99, 99, 99},
{18, 21, 26, 66, 99, 99, 99, 99},
{24, 26, 56, 99, 99, 99, 99, 99},
{47, 66, 99, 99, 99, 99, 99, 99},
{99, 99, 99, 99, 99, 99, 99, 99},
{99, 99, 99, 99, 99, 99, 99, 99},
{99, 99, 99, 99, 99, 99, 99, 99},
{99, 99, 99, 99, 99, 99, 99, 99}
};
// ============================================================
// ESCALA A TABELA CONFORME O FATOR DE QUALIDADE (1-100)
// ============================================================
//
// É a fórmula da libjpeg. Qualidade 50 = tabela base intacta.
void escalar_tabela(const int base[BLOCK_SIZE][BLOCK_SIZE], int qualidade,
int saida[BLOCK_SIZE][BLOCK_SIZE]) {
if (qualidade <= 0) qualidade = 1;
if (qualidade > 100) qualidade = 100;
int fator;
if (qualidade < 50)
fator = 5000 / qualidade;
else
fator = 200 - qualidade * 2;
for (int r = 0; r < BLOCK_SIZE; r++) {
for (int c = 0; c < BLOCK_SIZE; c++) {
int v = (base[r][c] * fator + 50) / 100;
if (v < 1) v = 1; // passo mínimo é 1
if (v > 255) v = 255; // passo máximo (JPEG baseline)
saida[r][c] = v;
}
}
}
// ============================================================
// QUANTIZAR E DEQUANTIZAR
// ============================================================
void quantizar(Bloco8x8 *coef, const int Q[BLOCK_SIZE][BLOCK_SIZE],
int saida[BLOCK_SIZE][BLOCK_SIZE]) {
for (int r = 0; r < BLOCK_SIZE; r++)
for (int c = 0; c < BLOCK_SIZE; c++)
saida[r][c] = (int) round(coef->valores[r][c] / Q[r][c]);
}
void dequantizar(int quant[BLOCK_SIZE][BLOCK_SIZE],
const int Q[BLOCK_SIZE][BLOCK_SIZE], Bloco8x8 *saida) {
for (int r = 0; r < BLOCK_SIZE; r++)
for (int c = 0; c < BLOCK_SIZE; c++)
saida->valores[r][c] = (double)(quant[r][c] * Q[r][c]);
}
// ============================================================
// MÉTRICAS
// ============================================================
int contar_zeros(int quant[BLOCK_SIZE][BLOCK_SIZE]) {
int z = 0;
for (int r = 0; r < BLOCK_SIZE; r++)
for (int c = 0; c < BLOCK_SIZE; c++)
if (quant[r][c] == 0) z++;
return z;
}
// PSNR entre o bloco original e o reconstruído.
double calcular_psnr(Bloco8x8 *orig, Bloco8x8 *rec, double *erro_max_out) {
double mse = 0.0, erro_max = 0.0;
for (int r = 0; r < BLOCK_SIZE; r++)
for (int c = 0; c < BLOCK_SIZE; c++) {
double d = rec->valores[r][c] - orig->valores[r][c];
mse += d * d;
if (fabs(d) > erro_max) erro_max = fabs(d);
}
mse /= (BLOCK_SIZE * BLOCK_SIZE);
*erro_max_out = erro_max;
if (mse == 0.0) return 999.0; // idêntico
return 10.0 * log10(255.0 * 255.0 / mse);
}
void imprimir_int(const char *titulo, int m[BLOCK_SIZE][BLOCK_SIZE]) {
printf("%s\n", titulo);
for (int r = 0; r < BLOCK_SIZE; r++) {
printf(" ");
for (int c = 0; c < BLOCK_SIZE; c++) printf("%5d", m[r][c]);
printf("\n");
}
printf("\n");
}
// ============================================================
// MAIN — Pipeline completo de quantização
// ============================================================
int main() {
inicializar_tabelas_dct();
// Bloco clássico do JPEG (o mesmo do artigo 9)
Bloco8x8 bloco = {{
{52, 55, 61, 66, 70, 61, 64, 73},
{63, 59, 55, 90,109, 85, 69, 72},
{62, 59, 68,113,144,104, 66, 73},
{63, 58, 71,122,154,106, 70, 69},
{67, 61, 68,104,126, 88, 68, 70},
{79, 65, 60, 70, 77, 68, 58, 75},
{85, 71, 64, 59, 55, 61, 65, 83},
{87, 79, 69, 68, 65, 76, 78, 94}
}};
printf("=== Quantizacao: onde a compressao com perdas acontece ===\n\n");
// 1. DCT
Bloco8x8 coef;
dct_separavel(&bloco, &coef);
// 2. Exemplo detalhado com qualidade 50
int Q50[BLOCK_SIZE][BLOCK_SIZE];
escalar_tabela(Q_LUMINANCIA, 50, Q50);
imprimir_int("Tabela de quantizacao Q (qualidade 50):", Q50);
int quant50[BLOCK_SIZE][BLOCK_SIZE];
quantizar(&coef, Q50, quant50);
imprimir_int("Coeficientes QUANTIZADOS (olha o mar de zeros!):", quant50);
printf("Zeros: %d de 64\n\n", contar_zeros(quant50));
// 3. Varredura de qualidades
printf("=== Efeito do fator de qualidade ===\n\n");
int qualidades[] = {90, 50, 10};
for (int i = 0; i < 3; i++) {
int q = qualidades[i];
int Qs[BLOCK_SIZE][BLOCK_SIZE];
escalar_tabela(Q_LUMINANCIA, q, Qs);
int quant[BLOCK_SIZE][BLOCK_SIZE];
quantizar(&coef, Qs, quant);
int zeros = contar_zeros(quant);
Bloco8x8 deq, rec;
dequantizar(quant, Qs, &deq);
idct_separavel(&deq, &rec);
double erro_max;
double psnr = calcular_psnr(&bloco, &rec, &erro_max);
printf("Qualidade %3d -> zeros: %2d/64 (%2.0f%%) | erro max: %5.1f | PSNR: %.2f dB\n",
q, zeros, 100.0 * zeros / 64.0, erro_max, psnr);
}
printf("\nMenor qualidade = mais zeros (mais compressao) e menor PSNR (mais perda).\n");
return 0;
}Pra compilar e rodar:
cd ~/codec-quant-test
gcc main.c -o quant -lm
./quantA saída desse programa deve ser a seguinte:
=== Quantizacao: onde a compressao com perdas acontece ===
Tabela de quantizacao Q (qualidade 50):
16 11 10 16 24 40 51 61
12 12 14 19 26 58 60 55
14 13 16 24 40 57 69 56
14 17 22 29 51 87 80 62
18 22 37 56 68 109 103 77
24 35 55 64 81 104 113 92
49 64 78 87 103 121 120 101
72 92 95 98 112 100 103 99
Coeficientes QUANTIZADOS (olha o mar de zeros!):
-26 -3 -6 2 2 -1 0 0
0 -2 -4 1 1 0 0 0
-3 1 5 -1 -1 0 0 0
-3 1 2 -1 0 0 0 0
1 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0
Zeros: 44 de 64
=== Efeito do fator de qualidade ===
Qualidade 90 -> zeros: 35/64 (55%) | erro max: 10.0 | PSNR: 40.10 dB
Qualidade 50 -> zeros: 44/64 (69%) | erro max: 14.6 | PSNR: 32.60 dB
Qualidade 10 -> zeros: 57/64 (89%) | erro max: 31.7 | PSNR: 25.69 dBTudo batendo com o que discutimos. O bloco denso virou um punhado de valores no canto e 44 zeros. E quanto mais baixa a qualidade, mais o bloco derrete em zeros. 🫠
Os artefatos de bloco: o preço de exagerar
Lembra que no artigo 8 eu já tinha avisado dos "quadradinhos"? Agora você entende de onde eles vêm de verdade.
Quando a quantização é muito agressiva (qualidade baixa), quase todos os coeficientes AC viram zero.
O bloco fica com pouquíssima informação, às vezes só o DC sobrevive! E quando só o DC sobrevive, o bloco inteiro vira uma cor sólida (lembra que o DC é a média do bloco?). 🎨
Aí, quando você tem vários blocos vizinhos virando cada um a sua "corzinha sólida", a fronteira entre eles fica visível, e você enxerga a grade de 8×8 na imagem, eles são os famosos blocking artifacts.

É por isso que CODECs mais modernos (e o nosso CODEC lá na Fase 5) vão usar deblocking filters pra suavizar essas fronteiras. Mas isso é papo pro artigo 23. 😉
Ou seja, tem muuuuuuito chão ainda meu amigo rs
A abordagem do nosso CODEC
Pro nosso CODEC, vamos começar exatamente como o JPEG faz:
- Duas tabelas de quantização (uma pra luminância, e crominância). ✅
- Fator de qualidade de 1 a 100 escalando as tabelas. ✅
- Quantização por divisão + arredondamento. ✅
Mas anota aí o gancho pro futuro: lá na Fase 5, o nosso CODEC vai além do JPEG com quantização adaptativa por segmento (artigo 19), em vez de uma tabela fixa pra imagem inteira, o encoder vai analisar cada região e redistribuir os "bits" pra onde o olho mais precisa.
É uma das principais razões do WebP ser 25-34% menor que o JPEG na mesma qualidade. 💪 Mas primeiro, precisamos dominar o básico. Uma coisa de cada vez! 🧗
Resumo: a compressão finalmente aconteceu
Recapitulando este artigo denso e importante:
- 🪜 Quantizar é reduzir a quantidade de valores possíveis, dividindo cada coeficiente por um "passo" e arredondando.
- 📊 O JPEG usa uma tabela de quantização 8×8: passos pequenos nas baixas frequências (preserva) e grandes nas altas (descarta), seguindo a sensibilidade do olho.
- 🎨 Crominância (Cb, Cr) usa uma tabela própria e mais agressiva, porque o olho enxerga menos detalhe de cor.
- 🎚️ O fator de qualidade (1-100) é só um multiplicador que escala a tabela. Qualidade 50 usa a tabela base intacta.
- ⚠️ Ao contrário da DCT, a quantização é irreversível o arredondamento joga informação fora pra sempre. É a etapa lossy do CODEC.
- 🌊 No nosso bloco de teste, a qualidade 50 transformou 44 dos 64 coeficientes em zero, e são esses zeros que vão permitir a compressão brutal nos próximos artigos.
- 📐 O PSNR mede a qualidade da reconstrução em dB: > 40 dB é quase perfeito, < 30 dB já mostra artefatos.
- 🧱 Quantização exagerada faz só o DC sobreviver, gerando os blocking artifacts (os quadradinhos).
Repara que agora temos, na mão, um bloco cheio de zeros. Mas ainda não comprimimos nada de verdade, os zeros ainda estão lá, ocupando espaço na memória. 🤨
No próximo artigo, vamos aprender a varredura em zig-zag e como ela reorganiza esses coeficientes numa sequência linear que agrupa todos os zeros no finais, o setup perfeito pra aplicar RLE. É a ponte entre "ter muitos zeros" e "realmente ocupar menos bytes".
Prepara mais um café ☕, porque a partir daqui a gente começa a transformar esses blocos em bytes de verdade num arquivo. A compressão que a gente vem construindo há 10 artigos finalmente vai virar tamanho de arquivo real!
Até o artigo 11!

