Zig-zag e RLE: transformando o mar de zeros em bytes de verdade
Olá leitor, seja muito bem vindo de volta a mais uma etapa da nossa jornada aqui no Portal da Micilini! 😊
Este é o décimo segundo artigo da nossa série de ~25, e ele é aquele momento mágico em que a gente para de "brincar com matriz" e começa a transformar tudo em bytes de verdade que vão pro arquivo. 📁✨
Deixa eu te situar 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-10): blocos 8×8, DCT (pixels → frequências), e quantização (onde a perda acontece). 🌊
No fim do artigo 10, a gente ficou com um bloco quantizado assim, cheio de zeros:
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 0Esses zeros ainda estão ocupando espaço na memória. 🤨
Ter muitos zeros não é o mesmo que comprimir, a gente precisa transformar essa "abundância de zeros" em economia de bytes real.
E é exatamente isso que vamos fazer hoje, com duas técnicas que trabalham em dupla: a varredura em zig-zag e o RLE (Run-Length Encoding).
Pega o café ☕ e bora!
O problema: os zeros estão espalhados em 2D
Olha de novo pro bloco quantizado...
Repara onde estão os zeros: eles se concentram no canto inferior direito, formando um "triângulo" de zeros. Mas eles estão espalhados numa matriz 2D.
Se a gente simplesmente lesse o bloco linha por linha (o que chamamos de raster scan), a sequência ficaria assim:
Raster scan (linha por linha):
-26, -3, -6, 2, 2, -1, 0, 0, 0, -2, -4, 1, 1, 0, 0, 0, -3, 1, 5, -1, -1, 0, 0, 0, ...Percebe o problema? 🕵️
Os zeros ficam intercalados com os valores, de modo que toda hora aparece um "0, 0" no meio, depois volta valor, depois zero de novo... Isso é péssimo pro RLE, porque o RLE brilha quando tem longas sequências contíguas do mesmo valor.
A gente precisa de uma forma de ler o bloco que junte todos os zeros no final, numa sequência longa e ininterrupta.
E aí é que entra a genialidade do zig-zag. 💡
A varredura em zig-zag: seguindo as frequências
Lembra do artigo 9, quando aprendemos que a DCT organiza as frequências de um jeito específico?
- Canto superior esquerdo = baixas frequências (onde mora quase toda a energia).
- Canto inferior direito = altas frequências (que a quantização quase sempre zerou).
A varredura em zig-zag percorre o bloco na diagonal, indo do canto superior esquerdo até o inferior direito, seguindo as frequências crescentes.
Ela zigue-zagueia entre as diagonais, e é daí que vem o nome. 🐍
O efeito é lindo: como ela vai das baixas frequências (valores grandes) pras altas (valores quase todos zero), a sequência resultante naturalmente coloca os zeros no final, todos juntinhos! 🎯

A ordem zig-zag oficial do JPEG
O JPEG define uma ordem exata pra essa varredura. Ela mapeia as 64 posições do bloco 8×8 nesta sequência (os números são os índices das posições visitadas, na ordem), vejamos:
Ordem de visita (posição do bloco 8×8):
0 → 1 → 8 → 16 → 9 → 2 → 3 → 10 →
17 → 24 → 32 → 25 → 18 → 11 → 4 → 5 →
12 → 19 → 26 → 33 → 40 → 48 → 41 → 34 →
27 → 20 → 13 → 6 → 7 → 14 → 21 → 28 →
35 → 42 → 49 → 56 → 57 → 50 → 43 → 36 →
29 → 22 → 15 → 23 → 30 → 37 → 44 → 51 →
58 → 59 → 52 → 45 → 38 → 31 → 39 → 46 →
53 → 60 → 61 → 54 → 47 → 55 → 62 → 63Traduzindo pra você visualizar o caminho: começa no , vai pro (0,0) à direita, desce na diagonal pra (0,1), desce mais pra (1,0), sobe na diagonal... e assim vai, sempre zigue-zagueando pelas diagonais. 🐍(2,0)
Essa tabela é fixa e universal, de tal modo que todo JPEG do mundo usa exatamente essa ordem. No código, ela vira um simples array de 64 índices. 📋
DC e AC no zig-zag
Um detalhe importante da organização: a primeira posição do zig-zag (índice 0) é sempre o coeficiente DC (aquele que representa a média do bloco, lembra do artigo 9?).
As outras 63 posições são os coeficientes AC, ordenados da frequência mais baixa pra mais alta.
Por que isso importa? Porque o DC e os AC são tratados de formas diferentes na compressão:
- O DC é codificado separadamente (no JPEG completo, ele usa uma técnica chamada DPCM, que veremos no próximo artigo).
- Os AC são justamente onde o RLE entra em ação, porque são eles que têm aquele mar de zeros no final.
Então, na prática: pegamos o zig-zag, separamos o DC (posição 0), e aplicamos RLE nos 63 coeficientes AC.
Aplicando no nosso bloco
Vamos passar o nosso bloco quantizado pela varredura em zig-zag. O resultado está abaixo:
Sequência zig-zag completa:
-26 -3 0 -3 -2 -6 2 -4 1 -3 1 1 5 1 2 -1
1 -1 2 0 0 0 0 0 -1 -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 0 0 0 0 0 0 0OLHA QUE MARAVILHA! 😍 Compara com o raster scan lá de cima. Agora todos os valores não-zero estão agrupados no começo, e a partir de certo ponto é zero até o fim do bloco, uma sequência longa e ininterrupta de zeros.
- DC =
(a primeira posição). ☀️-26 - AC = todo o resto, com os últimos ~38 valores sendo zero puro.
E é justamente essa cauda gigante de zeros que o RLE vai devorar.
RLE: comprimindo a cauda de zeros
O RLE (Run-Length Encoding) a gente já viu lá no artigo 5. A ideia base é: em vez de guardar "0, 0, 0, 0, 0", você guarda "cinco zeros".
Mas o RLE do JPEG tem um toque especial. Em vez de codificar valores repetidos genéricos, ele foca em contar os zeros que aparecem antes de cada valor não-zero.
Cada coeficiente AC não-zero vira um par, por exemplo:
(quantidade_de_zeros_antes, valor)Outro exemplo, se a sequência AC for , o RLE fica:-3, 0, -3, ...
-3 → (0, -3) // nenhum zero antes, valor -3
0, -3 → (1, -3) // um zero antes, valor -3Isso é perfeito pra sequência do zig-zag, porque os zeros isolados no meio viram só um "número de zeros" grudado no próximo valor.
O marcador EOB: a otimização matadora
Agora vem o truque mais esperto de todos. Depois do último coeficiente não-zero, o resto do bloco é tudo zero (aquela cauda gigante). Seria um desperdício codificar par por par até o fim.
Então o JPEG usa um marcador especial chamado EOB (End of Block — fim do bloco). Ele diz isso em um único símbolo:
"A partir daqui, é tudo zero até o fim do bloco. Pode parar." ✋No formato de pares, o EOB é representado como . Quando o decoder encontra ele, sabe que só precisa preencher o resto do bloco com zeros.(0, 0)
Essa é a otimização que faz o JPEG voar em imagens com áreas suaves: um bloco de céu liso pode ter só o DC e um EOB logo em seguida, w o bloco 8×8 inteiro (64 valores!) vira apenas 2 símbolos. 🤯
O marcador ZRL: quando os zeros são muitos
Tem um caso especial: e se aparecer uma sequência de mais de 15 zeros no meio do bloco (não no final)? 🤔
O par só consegue representar até 15 zeros de uma vez (porque o (run, valor) cabe em 4 bits: 0 a 15).run
Pra runs maiores, o JPEG usa o marcador ZRL (Zero Run Length), representado como , que significa "16 zeros seguidos, e ainda tem mais coisa depois".(15, 0)
Você emite quantos ZRLs precisar pra "consumir" os zeros em blocos de 16, e depois continua normalmente.
⚠️ Atenção para um detalhe fino: o ZRL é só pra zeros no meio do bloco. Se os zeros estão no final, você usa o EOB (que é mais barato). Nunca gaste ZRLs numa cauda de zeros que poderia ser um único EOB, isso é um erro clássico de implementação! 🐛
Exemplo completo passo a passo
Vamos aplicar o RLE na sequência AC do nosso bloco. Relembrando os 63 coeficientes AC em zig-zag (depois do DC = -26):
-3, 0, -3, -2, -6, 2, -4, 1, -3, 1, 1, 5, 1, 2, -1, 1, -1, 2, [5 zeros], -1, -1, [resto tudo zero]Agora percorremos, contando zeros antes de cada valor:
-3 → (0, -3) // 0 zeros antes
0,-3 → (1, -3) // 1 zero antes
-2 → (0, -2)
-6 → (0, -6)
2 → (0, 2)
-4 → (0, -4)
1 → (0, 1)
-3 → (0, -3)
1 → (0, 1)
1 → (0, 1)
5 → (0, 5)
1 → (0, 1)
2 → (0, 2)
-1 → (0, -1)
1 → (0, 1)
-1 → (0, -1)
2 → (0, 2)
[5 zeros], -1 → (5, -1) // 5 zeros antes deste -1!
-1 → (0, -1)
[resto tudo zero] → (EOB) // fim do bloco 🚩O resultado final:
(0,-3) (1,-3) (0,-2) (0,-6) (0,2) (0,-4) (0,1) (0,-3) (0,1) (0,1)
(0,5) (0,1) (0,2) (0,-1) (0,1) (0,-1) (0,2) (5,-1) (0,-1) (EOB)20 símbolos, para representar os 63 coeficientes AC. 🎉
E lembra: 44 daqueles 63 eram zero. Nós os representamos com um único (pros 5 zeros do meio) e um único (5,-1)EOB (pros ~38 zeros do final). 💪

O código completo em C
Hora de codar! Este programa faz o pipeline completo: varredura em zig-zag, RLE encode (com EOB e ZRL), RLE decode, e a varredura inversa (tudo com verificação de roundtrip).
Cria uma pasta , e dentro dela um codec-zigzag-test:main.c
#include <stdio.h>
#include <string.h>
#define BLOCK_SIZE 8
// ============================================================
// A ORDEM ZIG-ZAG OFICIAL DO JPEG
// ============================================================
//
// Cada número é o índice (row-major, 0..63) da posição do
// bloco 8×8 que é visitada naquela ordem. É uma tabela fixa,
// igual em todo JPEG do mundo.
const int ZIGZAG[64] = {
0, 1, 8, 16, 9, 2, 3, 10,
17, 24, 32, 25, 18, 11, 4, 5,
12, 19, 26, 33, 40, 48, 41, 34,
27, 20, 13, 6, 7, 14, 21, 28,
35, 42, 49, 56, 57, 50, 43, 36,
29, 22, 15, 23, 30, 37, 44, 51,
58, 59, 52, 45, 38, 31, 39, 46,
53, 60, 61, 54, 47, 55, 62, 63
};
// ============================================================
// VARREDURA ZIG-ZAG (2D -> 1D) E SUA INVERSA (1D -> 2D)
// ============================================================
void zigzag_scan(int bloco[BLOCK_SIZE][BLOCK_SIZE], int saida[64]) {
for (int i = 0; i < 64; i++) {
int idx = ZIGZAG[i];
saida[i] = bloco[idx / 8][idx % 8];
}
}
void zigzag_inverso(int entrada[64], int bloco[BLOCK_SIZE][BLOCK_SIZE]) {
for (int i = 0; i < 64; i++) {
int idx = ZIGZAG[i];
bloco[idx / 8][idx % 8] = entrada[i];
}
}
// ============================================================
// RLE ESTILO JPEG
// ============================================================
//
// Cada coeficiente AC não-zero vira um par (run, valor), onde
// 'run' é a quantidade de zeros que vieram antes dele.
//
// Símbolos especiais:
// EOB = (0, 0) -> "daqui até o fim é tudo zero"
// ZRL = (15, 0) -> "16 zeros seguidos, e ainda tem mais"
//
// Importante: zeros no FINAL do bloco viram EOB (barato),
// nunca ZRL. ZRL é só pra runs longos no MEIO do bloco.
typedef struct { int run; int valor; } ParRLE;
int rle_encode(int zz[64], ParRLE pares[]) {
int n = 0, run = 0;
// Só os AC (posições 1..63). O DC (posição 0) é tratado à parte.
for (int i = 1; i < 64; i++) {
if (zz[i] == 0) {
run++;
} else {
// Se acumulou mais de 15 zeros, emite ZRLs de 16 em 16
while (run > 15) {
pares[n].run = 15;
pares[n].valor = 0; // ZRL
n++;
run -= 16;
}
pares[n].run = run;
pares[n].valor = zz[i];
n++;
run = 0;
}
}
// Se sobrou uma cauda de zeros, ela vira um único EOB
if (run > 0) {
pares[n].run = 0;
pares[n].valor = 0; // EOB
n++;
}
return n;
}
void rle_decode(ParRLE pares[], int n, int zz[64]) {
for (int i = 1; i < 64; i++) zz[i] = 0; // começa tudo zero
int pos = 1;
for (int p = 0; p < n; p++) {
// EOB: para de decodificar, resto já é zero
if (pares[p].run == 0 && pares[p].valor == 0) break;
// ZRL: pula 16 posições (16 zeros)
if (pares[p].run == 15 && pares[p].valor == 0) {
pos += 16;
continue;
}
// Par normal: pula 'run' zeros e escreve o valor
pos += pares[p].run;
if (pos < 64) {
zz[pos] = pares[p].valor;
pos++;
}
}
}
// ============================================================
// MAIN — Pipeline zig-zag + RLE completo
// ============================================================
int main() {
// Bloco quantizado do artigo 10 (qualidade 50)
int bloco[BLOCK_SIZE][BLOCK_SIZE] = {
{-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}
};
// 1. Varredura zig-zag
int zz[64];
zigzag_scan(bloco, zz);
printf("Sequencia zig-zag completa:\n");
for (int i = 0; i < 64; i++) {
printf("%4d", zz[i]);
if ((i + 1) % 16 == 0) printf("\n");
}
printf("\nDC = %d (posicao 0 do zig-zag)\n\n", zz[0]);
// 2. RLE dos coeficientes AC
ParRLE pares[64];
int n = rle_encode(zz, pares);
printf("RLE dos AC -> (run, valor):\n");
for (int p = 0; p < n; p++) {
if (pares[p].run == 0 && pares[p].valor == 0)
printf("(EOB)");
else if (pares[p].run == 15 && pares[p].valor == 0)
printf("(ZRL) ");
else
printf("(%d,%d) ", pares[p].run, pares[p].valor);
}
printf("\n\nTotal: %d simbolos RLE (contra 63 coeficientes AC brutos)\n\n", n);
// 3. Verificação: decodifica e compara com o bloco original
int zz_rec[64];
zz_rec[0] = zz[0]; // DC é preservado à parte
rle_decode(pares, n, zz_rec);
int bloco_rec[BLOCK_SIZE][BLOCK_SIZE];
zigzag_inverso(zz_rec, bloco_rec);
int ok = (memcmp(bloco, bloco_rec, sizeof(bloco)) == 0);
printf("Roundtrip (zig-zag + RLE -> decode -> bloco): %s\n",
ok ? "IDENTICO ao original!" : "ERRO!");
return 0;
}Pra compilar e rodar:
cd ~/codec-zigzag-test
gcc main.c -o zigzag
./zigzagA saída deverá ser a seguinte:
Sequencia zig-zag completa:
-26 -3 0 -3 -2 -6 2 -4 1 -3 1 1 5 1 2 -1
1 -1 2 0 0 0 0 0 -1 -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 0 0 0 0 0 0 0
DC = -26 (posicao 0 do zig-zag)
RLE dos AC -> (run, valor):
(0,-3) (1,-3) (0,-2) (0,-6) (0,2) (0,-4) (0,1) (0,-3) (0,1) (0,1) (0,5) (0,1) (0,2) (0,-1) (0,1) (0,-1) (0,2) (5,-1) (0,-1) (EOB)
Total: 20 simbolos RLE (contra 63 coeficientes AC brutos)
Roundtrip (zig-zag + RLE -> decode -> bloco): IDENTICO ao original!A varredura zig-zag agrupou os zeros no final, o RLE devorou eles com um e um (5,-1), e o roundtrip provou que não perdemos nada nessa etapa ✅EOB
Isso é importante frisar: o zig-zag e o RLE são operações lossless (sem perda).
A única etapa com perda no nosso CODEC continua sendo a quantização (artigo 10). Aqui a gente só reorganiza e empacota de forma reversível.
Quanto a gente comprimiu (e o que ainda falta)?
Vamos fazer as "contas de guardanapo".
- Antes: 64 coeficientes quantizados, cada um ocupando pelo menos 1 byte cru = 64 bytes por bloco (só do canal Y).
- Depois: 1 DC + 20 símbolos RLE. Cada símbolo é um par pequeno. Já é bem menos!
Mas atenção, essa é só parte da mágica. Repara que cada par ainda está "cru", pois o (run, valor)run e o ainda ocupam bits fixos. 🤔valor
O golpe final vem no próximo artigo, com o Huffman (lembra dele do artigo 6?).
Vamos atribuir códigos curtos aos pares mais comuns (tipo , que aparece o tempo todo) e códigos longos aos raros. Aí sim os 20 símbolos viram uns pouquíssimos bytes. 🎯(0, 1)
Então o pipeline de codificação de entropia do JPEG é uma escadinha:
Coeficientes quantizados (2D)
↓ zig-zag
Sequência 1D com zeros agrupados no final
↓ RLE
Pares (run, valor) + EOB
↓ Huffman (artigo 12)
Bytes de verdade no arquivo! 📁E sim, nós estamos no penúltimo degrau. 🪜
A abordagem do nosso CODEC
Pro nosso CODEC, vamos começar exatamente com esse esquema clássico do JPEG:
- Varredura em zig-zag com a tabela padrão. ✅
- RLE dos coeficientes AC com EOB e ZRL. ✅
- DC tratado à parte (no próximo artigo). ✅
Mas anota o gancho pro futuro: lá na Fase 5, quando formos falar de codificação aritmética / ANS (artigo 20), a gente vai ver que dá pra substituir o RLE+Huffman por algo ainda mais eficiente.
Formatos modernos como o WebP e o AVIF usam entropia mais avançada que o velho par RLE+Huffman do JPEG.
Mas, como sempre: uma coisa de cada vez. Primeiro dominamos o clássico. 🧗
Resumo
Recapitulando este artigo:
- Depois da quantização, os zeros ficam espalhados em 2D — ler linha por linha (raster) os intercala com valores, o que é ruim pro RLE.
- A varredura em zig-zag percorre o bloco na diagonal, das baixas frequências às altas, agrupando todos os zeros no final numa sequência longa e ininterrupta.
- A ordem zig-zag é uma tabela fixa de 64 índices, universal em todo JPEG.
- A posição 0 do zig-zag é o DC (tratado à parte); as 63 restantes são os AC (onde o RLE atua).
- O RLE do JPEG codifica cada AC não-zero como um par (zeros_antes, valor).
- O marcador EOB
diz "daqui até o fim é tudo zero", a otimização matadora pra áreas suaves.(0,0) - O marcador ZRL
representa 16 zeros no meio do bloco (nunca use pra cauda final, isso é EOB).(15,0) - Zig-zag e RLE são lossless: no nosso bloco, 63 coeficientes AC viraram 20 símbolos, com roundtrip idêntico.
No próximo artigo, vamos fechar o pipeline de codificação de entropia com o Huffman, atribuindo códigos de bits curtos aos símbolos mais frequentes, e finalmente transformando esses pares RLE em bytes reais gravados num arquivo.
Também vamos ver como o DC é codificado com DPCM (a diferença entre blocos vizinhos).
É o artigo onde tudo o que construímos há 11 capítulos finalmente vira um arquivo comprimido de verdade. A linha de chegada da Fase 3 está logo ali!
Prepara o café ☕, porque o próximo é o grande encontro de tudo.
Até o artigo 12!

