Chroma Subsampling: o truque que corta dados pela metade (4:4:4, 4:2:2, 4:2:0)
Olá leitor, seja muito bem vindo de volta a mais uma etapa da nossa jornada aqui no Portal da Micilini 😊
Este é o terceiro e último artigo da Fase 1 (Fundamentos Visuais) da nossa série de 20 artigos, onde estamos construindo todo o conhecimento teórico necessário para, no final, criar o nosso próprio CODEC de imagens do zero, 100% autoral.
No artigo anterior, a gente aprendeu que o espaço de cor YCbCr separa a informação de brilho (luminância Y) da informação de cor (crominância Cb e Cr). E vimos que essa separação é genial porque nosso olho é muito mais sensível ao brilho do que à cor.
Mas ficou uma promessa no ar: eu disse que só essa separação já permite cortar 33% a 50% dos dados sem que o olho perceba. E também mencionei aqueles números misteriosos: 4:4:4, 4:2:2, 4:2:0...
Pois bem, chegou a hora de entender exatamente como isso funciona. E te adianto: quando você terminar de ler este artigo, vai olhar pra qualquer configuração de vídeo que tenha esses números e saber exatamente o que cada um significa.
Então pega seu café ☕, sente-se confortavelmente, e vamos fechar a Fase 1 com chave de ouro!
Antes de tudo, qual é a ideia geral?
Vamos recapitular rapidamente. No artigo anterior, a gente viu que depois de converter uma imagem de RGB pra YCbCr, ficamos com 3 canais, são eles:
- Y (luminância) → carrega o brilho, os contornos, os detalhes finos. E ficamos sabendo que o olho ama esse canal.
- Cb (crominância azul) → carrega o desvio de cor na direção do azul/amarelo. Aqui o nosso olho é preguiçoso pra perceber detalhes nessa parte.
- Cr (crominância vermelha) → carrega o desvio de cor na direção do vermelho/ciano. Mesma preguiça do olho rs
E vimos que quando separamos esses canais de uma foto real (lembra daquela foto de praia com os surfistas?), o canal Y ficou nítido e detalhado (130 KB), enquanto os canais Cb e Cr ficaram suaves e borrados (50 KB cada).

Agora, a pergunta natural é: se o olho não percebe detalhes finos de cor, por que gastar a mesma quantidade de dados pra armazenar Cb e Cr que a gente gasta pra armazenar Y?
É como se você estivesse montando uma mala pra viajar. Você tem 3 categorias de itens:
🧳 Documentos (passaporte, carteira, RG) → Essenciais. Sem eles, você não embarca. Precisam estar todos ali, sem faltar nenhum.
🧳 Roupas → Importantes, mas se faltar uma meia ou uma camiseta, ninguém vai notar.
🧳 Acessórios (bijuterias, cinto extra, lenço decorativo) → Legais de ter, mas se você cortar pela metade, a viagem continua perfeitamente.
A luminância Y são os documentos, e como bem sabemos, eles precisam estar completos. A crominância Cb e Cr são os acessórios, que dá pra reduzir sem problema algum, pois é possível continuar a viagem completamente sem eles, ou com poucos deles.
Chroma Subsampling é exatamente esse ato de "reduzir os acessórios". É a técnica de diminuir a resolução dos canais de crominância enquanto mantém a luminância intacta.
E a notação 4:4:4, 4:2:2, 4:2:0 é a forma de dizer quanto de crominância você está mantendo.
Entendendo a notação J:a:b
Antes de falar de cada esquema específico, preciso te ensinar como ler essa notação. Até porque, à primeira vista, a notação "4:2:0" parece uma contagem de algum timer sem sentido 😅
A notação J:A:B, descreve como a crominância é amostrada em relação à luminância, dentro de um bloco de referência de pixels.
Mas para isso, precisamos entender o que cada letra significa:
J → É a largura do bloco de referência, medida em pixels. Na prática, J é sempre 4. Ou seja, estamos sempre olhando pra um bloco de 4 pixels de largura.
a → É o número de amostras de crominância na primeira linha desse bloco de 4 pixels.
b → É o número de amostras de crominância na segunda linha desse bloco de 4 pixels.
Parece abstrato? E é aqui que a mente de muita gente dá um verdadeiro "tela azul" 🤯.
Se você acompanhou os artigos anteriores com atenção, sua cabeça deve estar fritando com a seguinte dúvida: "Peraí, William... a gente viu que 1 pixel tem 3 lâmpadinhas físicas (R, G, B) e pesa 3 bytes. Se a gente joga metade da cor fora, isso significa que alguma lâmpada do meu monitor vai ficar apagada? O YCbCr não encaixa nessa conta!"
NÃO! Nenhuma lâmpada fica apagada. E é agora que você vai dar o maior salto de conhecimento sobre CODECs.
Para entender o Chroma Subsampling (e a notação J:a:b), você precisa, a partir de agora, separar duas coisas na sua cabeça: o Mundo Físico e o Mundo Digital.
🖥️ O Mundo Físico (O seu Monitor)
Esqueça o YCbCr aqui.
O seu monitor, a tela do seu celular ou a sua TV são pedaços de hardware "burros". Eles só entendem uma única coisa na vida: voltagem para acender lâmpadas Vermelhas, Verdes e Azuis (RGB).
Na tela física, não existe economia. Se a imagem tem 8 pixels, as 24 lâmpadinhas (subpixels) vão acender. O hardware sempre, obrigatoriamente, vai trabalhar com RGB.
📁 O Mundo Digital (O Arquivo e o CODEC)
O CODEC não é a sua tela.
Ele é apenas o "correio" que transporta a imagem da internet até o seu aparelho. E como todo bom correio, ele quer carregar o menor peso possível.
Por isso, antes de enviar a imagem, o CODEC pega as informações daquelas lâmpadas RGB e transforma em uma fórmula matemática pura, que é o YCbCr.
No exato momento em que entramos no passo do J:a:b, as lâmpadinhas físicas deixaram de existir para o CODEC. Ele não enxerga luz, ele enxerga apenas tabelas de números, camadas e bloquinhos de dados.
E o que isso significa? Significa que se você imaginou o J:a:b dessa forma:

Tá errado! o J:a:b, nunca poderia ser representado assim... pois os pixels com as 3 lampadinhas funciona em nível de HARDWARE (seu monitor), e o YCbCr funciona A NÍVEL DE SOFTWARE! (formulas matemáticas, que posteriormente serão convertidas para RGB).
Sendo assim, o YCbCr é uma formula matemática de compressão de dados, e NÃO TEM COMO IMAGINAR ELA, do mesmo modo como FIZEMOS COM A REPRESENTAÇÃO ACIMA.
Se você não conseguir fazer essa separação, nunca vai entender o funcionamento real do YCbCr.
Para entender o J:a:b, precisamos imaginar eles como formulas matemáticas puras, e a melhor maneira de visualizar isso, é entendendo a metáfora da forminha de gelo 🧊

Para você visualizar como a cor é reduzida no arquivo (sem apagar lâmpadas na tela), pense no nosso bloco J de referência (4 pixels em cima e 4 embaixo = 8 pixels no total) como uma forminha de gelo com 8 espaços.
- A Camada do Brilho (Luminância - Y): O CODEC vai lá e coloca uma pedrinha de gelo com o valor de brilho exato dentro de cada um dos 8 espaços. O brilho é individual, afinal, ele desenha os contornos da imagem.
- A Camada da Cor (Crominância - Cb/Cr): Aqui o CODEC fica preguiçoso para economizar dados. Em vez de colocar uma pedrinha de cor individual em cada espaço, ele usa "blocos esticados". Ele diz: "Pixel 1 e Pixel 2, eu anotei o brilho individual de vocês, mas na hora da cor, vocês vão compartilhar a mesma anotação, ok?"
Ainda não entendeu? Então vamos desmitificar ainda mais o exemplo da ilustração acima...
A Forminha e o "J":

A forminha inteira representa o nosso bloco de referência.
Como a regra diz que o J vale 4, a nossa forminha sempre terá 4 espaços de largura. Como ela tem duas linhas, no total ela tem 8 espaços. Como é representado na ilustração acima.
As Pedrinhas de Gelo (O Brilho - Y):
Em cada um dos 8 espaços dessa forminha, o CODEC coloca uma pedrinha de gelo.
Uma pode ser gelo totalmente transparente (branco/brilho máximo), a do lado pode ser um gelo mais turvo (cinza), e outra pode ser um gelo escuro (preto). São 8 pedrinhas independentes. Onde o brilho de uma não interfere na outra.
A Cor (Crominância - Cb/Cr) e as letras "a" e "b":

Aqui vai um ponto importante: Nós não "adicionamos cores nas linhas a e b".
Na verdade, "a" e "b" não são lugares físicos. Eles são apenas números que ditam as regras de quantas "amostras de cor" o CODEC tem permissão para usar.
Tá, eu sei, ficou complexo demais para entender, mas continue seguindo o fluxo 🤓
Para visualizar a cor na nossa metáfora, imagine que a cor não é líquida, mas sim bloquinhos de gelatina colorida e transparente que nós vamos colocar por cima das pedrinhas de gelo.
Nesse caso, as regras "a" e "b" entram da seguinte forma:
- A letra "a" é a regra para a linha de cima da forminha. Ela diz: "Quantos bloquinhos de gelatina colorida eu posso usar na fileira de cima?"
- A letra "b" é a regra para a linha de baixo. Ela diz: "Quantos bloquinhos de gelatina colorida eu posso usar na fileira de baixo?"
Dito isso, estamos prontos para entendermos a próxima fase.
4:4:4 — Sem Subsampling (crominância completa)
Vamos começar pelo caso mais simples, onde nenhuma crominância é descartada.
O 4:4:4 significa:
- J = 4: bloco de referência de 4 pixels de largura;
- a = 4: a primeira linha tem 4 amostras de crominância (uma pra cada pixel)
- b = 4: a segunda linha também tem 4 amostras de crominância (uma pra cada pixel)
Ou seja: cada pixel tem sua própria informação de cor. Nada é compartilhado, nada é descartado.
Ele segue a representação que fizemos no exemplo anterior usando a metáfora dos blocos de gelo:

Cada pixel tem 3 valores (Y, Cb, Cr), igualzinho ao RGB (R, G, B). Nenhuma economia de dados.
Então pra que o 4:4:4 existe, se não economiza nada?
Ele é usado em situações onde a qualidade é prioridade absoluta e tamanho de arquivo não importa. Como nesses casos abaixo:
- Edição profissional de vídeo: quando você está editando um filme no DaVinci Resolve ou no Premiere Pro e vai aplicar correção de cor pesada, precisa de toda a informação de crominância pra não ter artefatos.
- Transmissão de conteúdo médico: em imagens de ressonância magnética ou raio-X, qualquer perda de dados pode esconder um diagnóstico.
- Produção de efeitos visuais (VFX): quando o estúdio precisa recortar objetos com chroma key (tela verde), ter crominância completa facilita muito o recorte.
💡 Curiosidade: Se você já configurou a saída de vídeo de um PC gamer ou de um console, talvez já tenha visto a opção "4:4:4 Chroma" nas configurações de HDMI. Isso garante que textos pequenos na tela fiquem nítidos, porque monitores configurados em 4:2:0 podem borrar letras finas (já que a crominância é reduzida). Se você usa o PC na TV como monitor, ligar o 4:4:4 faz diferença.
Vamos ver como isso funciona na prática do Arquivo Digital....
No formato 4:4:4 (sem nenhum tipo de economia):
- A regra "a" vale 4. Então, na linha de cima, o CODEC recorta 4 bloquinhos pequenos de gelatina vermelha/azul e coloca exatamente um em cima de cada pedrinha de gelo.
- A regra "b" vale 4. Sendo assim, na linha de baixo, a mesma coisa. 4 bloquinhos pequenos de gelatina.
- Resultado: 8 pedrinhas de gelo + 8 bloquinhos de gelatina. Cada pixel tem sua cor exata. Nenhuma economia.
4:2:2 — Metade da crominância horizontal
Agora vamos ao primeiro nível de subsampling real.
O 4:2:2 significa:
- J = 4: bloco de referência de 4 pixels de largura;
- a = 2: a primeira linha tem 2 amostras de crominância (em vez de 4);
- b = 2: a segunda linha também tem 2 amostras de crominância;
Ou seja: a cada 4 pixels numa linha, só guardamos 2 amostras de cor. Cada amostra de cor é compartilhada entre 2 pixels vizinhos horizontalmente.

Percebe o que aconteceu? Os dois pixels da primeira fileira compartilham a mesma informação de cor. E os outros dois da primeira fileira também compartilham. E assim por diante.
Cada pixel ainda tem sua própria luminância (brilho) individual, os contornos e detalhes continuam nítidos. Mas a cor é "compartilhada" entre pares de pixels vizinhos.
Na prática, é como se cada par de pixels dissesse:
"Eu (pixel 1) tenho meu brilho próprio, e ele (pixel 2) tem o brilho dele. Mas em termos de cor, a gente combinou de usar a mesma."
E quanto isso economiza?
No 4:4:4, tínhamos 24 valores para 8 pixels. No 4:2:2, temos 16 valores para 8 pixels.
Economia: 24 → 16 = redução de 33% no volume de dados!
E o olho humano praticamente não percebe a diferença, porque a resolução de brilho continua a mesma (cada pixel tem seu Y individual), e a resolução de cor caiu só pela metade na horizontal.
Vamos ver como isso funciona na prática do Arquivo Digital:
No formato 4:2:2, vulgo economia inteligente...
- A regra "a" vale 2. Opa! O CODEC só tem permissão para usar 2 bloquinhos de gelatina para cobrir os 4 espaços da linha de cima. O que ele faz? Ele pega uma gelatina amarela e estica ela para cobrir a pedrinha de gelo 1 e a pedrinha 2 ao mesmo tempo. Depois, pega uma gelatina azulada e estica sobre a pedrinha 3 e 4.]
- A regra "b" vale 2. Na linha de baixo, ele faz a mesma coisa. Estica 2 bloquinhos de gelatina para cobrir os 4 buracos.
- Resultado: 8 pedrinhas de gelo (detalhes perfeitos), mas cobertas por apenas 4 blocos de gelatina esticados. Nós dividimos o peso da cor pela metade no arquivo!
Onde o 4:2:2 é usado?
- Broadcast profissional de TV: a maioria das emissoras de TV transmite em 4:2:2. É o padrão do ProRes 422 e do DNxHR, formatos que editores de vídeo profissionais usam no dia a dia.
- Câmeras profissionais: câmeras como a Sony A7S III e a Canon C70 gravam internamente em 4:2:2 10-bit. É o equilíbrio ideal entre qualidade e tamanho de arquivo pra produção audiovisual.
Dito isso, vamos dar uma olhada agora no terceiro formato 🤗
4:2:0 — Metade horizontal E metade vertical
Agora vem o esquema mais agressivo e, ao mesmo tempo, o mais usado no mundo. Se existe um rei do Chroma Subsampling, é o 4:2:0.
O 4:2:0 significa:
- J = 4: bloco de referência de 4 pixels de largura
- a = 2: a primeira linha tem 2 amostras de crominância
- b = 0: a segunda linha tem 0 amostras de crominância (ela usa as mesmas da primeira linha!)
Aqui é onde a economia fica pesada. Além de compartilhar crominância horizontalmente (como o 4:2:2), o 4:2:0 também compartilha verticalmente. Cada amostra de cor serve pra um bloco de 2×2 pixels.

No exemplo acima vemos o seguinte:
- O Quadradão Amarelo: A gelatina amarela cobre os pixels 1 e 2 da linha de cima e "escorre" para cobrir os pixels 5 e 6 da linha de baixo. Ela forma um quadrado de 2x2 pixels. Todos esses 4 pixels herdam a mesma cor amarela.
- O Quadradão Azul: A gelatina azul faz a mesma coisa do outro lado. Cobre os pixels 3 e 4 da linha de cima e os pixels 7 e 8 da linha de baixo. Outro quadrado de 2x2 pixels dividindo a mesma cor azul.
Cada pixel continua com seu brilho individual, mas a cor é decidida por blocos de 4 pixels.
E quanto isso economiza?
No 4:4:4, tínhamos 24 valores para 8 pixels, já no 4:2:0, temos 12 valores para 8 pixels.
Economia: 24 → 12 = redução de 50% no volume de dados!
Metade! Você cortou metade da informação e o olho humano quase não percebe. Isso é absurdamente eficiente.
Mas pera William, se b = 0, isso não deveria significar "zero amostras na segunda linha"? 🤔
Boa observação! E sim, historicamente a notação é um pouco confusa. O b = 0 não significa literalmente "zero amostras". Ele é uma convenção que significa "a segunda linha reutiliza as amostras da primeira linha". É um caso especial da notação.
Se o b fosse literalmente zero, a segunda linha não teria informação de cor nenhuma, e os pixels ficariam em tons de cinza, o que obviamente não é o que acontece. O que realmente acontece é que a crominância é subamostrada tanto na horizontal quanto na vertical, resultando em 1/4 da resolução original de crominância.
Fazendo uma recapitulação do que vimos anteriormente no J:a:b:
- No 4:4:4 → crominância tem 100% da resolução
- No 4:2:2 → crominância tem 50% da resolução (metade horizontal)
- No 4:2:0 → crominância tem 25% da resolução (metade horizontal E metade vertical)
Onde o 4:2:0 é usado?
Em basicamente tudo que você consome no dia a dia, como por exemplo:
- JPEG: toda foto JPEG do seu celular usa 4:2:0 por padrão. Aquela selfie que você postou no Instagram? Usa 4:2:0.
- YouTube, Netflix, Prime Video: todo streaming de vídeo usa 4:2:0. Aquele filme 4K que você assiste na TV? Usa 4:2:0.
- H.264, H.265, VP9, AV1: todos os CODECs de vídeo modernos usam 4:2:0 como padrão.
- Blu-ray: os discos Blu-ray comerciais usam 4:2:0.
- Videoconferência: Zoom, Meet, Teams... tudo 4:2:0.
- Jogos gravados: quando você grava gameplay pelo OBS ou pelo GeForce Experience, o padrão é 4:2:0.
Basicamente, se é conteúdo de mídia pra consumo final, é 4:2:0. A economia de 50% é boa demais pra ser ignorada.
Fazendo as contas: quanto economizamos em cada esquema?
Vamos botar isso numa perspectiva bem concreta. Lembra do artigo 1, quando calculamos o tamanho de uma imagem Full HD (1920×1080) em RGB24?
1920 × 1080 × 3 bytes = 6.220.800 bytes ≈ 5,93 MBAgora, depois de converter pra YCbCr, ainda temos 3 canais com a mesma resolução, então o tamanho continua o mesmo: ~5,93 MB (isso é o 4:4:4).
Mas olha o que acontece quando aplicamos Chroma Subsampling....
Com 4:4:4 (sem subsampling):
Y: 1920 × 1080 = 2.073.600 amostras
Cb: 1920 × 1080 = 2.073.600 amostras
Cr: 1920 × 1080 = 2.073.600 amostras
Total: 6.220.800 bytes ≈ 5,93 MBCom 4:2:2 (metade horizontal):
Y: 1920 × 1080 = 2.073.600 amostras (100% - intacto)
Cb: 960 × 1080 = 1.036.800 amostras (50% - metade da largura)
Cr: 960 × 1080 = 1.036.800 amostras (50% - metade da largura)
Total: 4.147.200 bytes ≈ 3,95 MB
Economia: 33%Com 4:2:0 (metade horizontal E vertical):
Y: 1920 × 1080 = 2.073.600 amostras (100% - intacto)
Cb: 960 × 540 = 518.400 amostras (25% - metade da largura E da altura)
Cr: 960 × 540 = 518.400 amostras (25% - metade da largura E da altura)
Total: 3.110.400 bytes ≈ 2,97 MB
Economia: 50%Repare: de 5,93 MB pra 2,97 MB de tamanho, isso equivale a METADE do arquivo.
E conseguimos fazer isso, antes de qualquer outra técnica de compressão (DCT, quantização, Huffman...). O Chroma Subsampling sozinho já cortou o tamanho pela metade.
Agora imagine que depois desse corte, o CODEC ainda vai aplicar todas aquelas outras técnicas que vamos ver nos próximos artigos. A redução final fica absurda.
É por isso que eu disse antes que uma imagem Full HD que ocupa ~5,93 MB em RGB24 pode cair pra ~185 KB em JPEG. O Chroma Subsampling é o primeiro grande golpe, e as outras técnicas terminam o serviço.
Mas como exatamente o subsampling reduz os pixels de cor?
Beleza, agora você sabe que o 4:2:0 corta a crominância pela metade na horizontal e na vertical. Mas como isso é feito na prática?
Será que c CODEC simplesmente joga fora pixels de cor alternados?
Sim, ele faz quaaaaase isso, mas com um pouco mais de cuidado do que imaginamos 🤔
Existem basicamente duas estratégias que um CODEC pode aplicar quando faz o uso do subsampling:
Estratégia 1 — Amostragem direta (mais simples):
Você simplesmente pega uma amostra de crominância a cada 2 pixels (no 4:2:2) ou a cada bloco de 2×2 pixels (no 4:2:0) e descarta as outras.
É como se você tivesse uma grade de azulejos coloridos no chão. No 4:2:0, você olha pra cada grupo de 4 azulejos (2×2) e diz: "a cor desse grupo inteiro vai ser a cor do azulejo do canto superior esquerdo". Os outros 3 azulejos perdem sua cor individual e "herdam" a cor do primeiro.
Pra ficar mais claro, vamos pegar um exemplo concreto. Imagine esses 4 pixels vizinhos (2×2) com suas cores YCbCr:
┌──────────────────┬──────────────────┐
│ Y=180, Cb=90, │ Y=175, Cb=95, │
│ Cr=140 │ Cr=138 │
├──────────────────┼──────────────────┤
│ Y=170, Cb=88, │ Y=168, Cb=92, │
│ Cr=142 │ Cr=136 │
└──────────────────┴──────────────────┘No 4:2:0, o subsampling pegaria uma amostra de Cb e uma de Cr pra representar os 4 pixels. A mais simples: pegar os valores do pixel superior esquerdo:
Cb compartilhado = 90
Cr compartilhado = 140Agora os 4 pixels ficam assim:
┌──────────────────┬──────────────────┐
│ Y=180, Cb=90, │ Y=175, Cb=90, │ ← cada pixel mantém
│ Cr=140 │ Cr=140 │ seu Y individual,
├──────────────────┼──────────────────┤ mas compartilha
│ Y=170, Cb=90, │ Y=168, Cb=90, │ Cb e Cr com os
│ Cr=140 │ Cr=140 │ vizinhos
└──────────────────┴──────────────────┘Percebe? Os valores de Y continuam intocados (180, 175, 170, 168). O brilho de cada pixel é preservado individualmente. Mas o Cb e o Cr agora são iguais pros 4 pixels (90 e 140).
Estratégia 2 — Média dos vizinhos (mais suave):
Em vez de simplesmente pegar o valor de um pixel e copiar, você calcula a média dos valores de crominância dos pixels do bloco:
Cb médio = (90 + 95 + 88 + 92) / 4 = 91.25 ≈ 91
Cr médio = (140 + 138 + 142 + 136) / 4 = 139Isso produz um resultado mais suave, com menos "saltos" bruscos de cor nas bordas dos blocos. A maioria dos CODECs reais usa essa segunda estratégia (ou uma variação com filtros mais sofisticados).
🧠 Analogia: A primeira estratégia é como pedir pra 4 amigos decidirem a cor da camisa do grupo, e um deles simplesmente diz "essa vai ser a minha cor e ponto final". A segunda estratégia é como se todos votaressem e escolheressem a média. O resultado da votação tende a agradar mais a todo mundo (e produzir menos artefatos visuais).
Para entendermos melhor, pense no seguinte exemplo...
Estratégia 1 (O "Ditador"):
O Pixel 1 (canto superior esquerdo) vira pros outros 3 pixels do bloco e fala: "A minha gelatina é Cb=90. Então todo mundo aqui vai ter que engolir a gelatina 90 e acabou a conversa!". O coitado do Pixel 2, que queria a gelatina 95, perdeu totalmente a sua identidade 🤣
Estratégia 2 (O "Liquidificador" / Democracia):
O CODEC olha pros 4 pixels do bloco (que queriam as gelatinas 90, 95, 88 e 92) e diz: "Gente, pra ninguém sair muito no prejuízo, vou pegar as 4 gelatinas de vocês, jogar num liquidificador, bater tudo e ver que cor que dá".
O resultado da mistura deu 91.
É o famoso "meio-termo". Ninguém ganhou a cor exata que tinha no original, mas todo mundo recebeu uma cor que é muito parecida com o que queria. O 90 tá feliz com 91, o 92 tá feliz com 91, o 88 chegou perto...
Por que os CODECs (como o JPEG e vídeos MP4) preferem usar o liquidificador (a Média)?
Porque isso evita que a imagem fique "quadradona" (pixelada/com artefatos).
Se o CODEC usar a Estratégia 1 (do Ditador), a cor pode dar um salto muito brusco de um bloco 2x2 para o próximo bloco 2x2. Ficam aquelas manchas duras na imagem.
Quando ele usa a Estratégia 2 (Média), a cor se mistura com os vizinhos, criando uma transição muito mais suave e natural pro olho humano. As manchas duras somem e viram um "degradê".
Quando o Chroma Subsampling dá problema?
Agora, seria desonesto da minha parte dizer que o Chroma Subsampling é perfeito e nunca causa problemas.
Ele funciona maravilhosamente bem na maioria dos casos, mas existem cenários onde a redução de crominância se torna visível.
Problema 1 — Texto colorido sobre fundo contrastante
Esse é o caso clássico. Imagine um texto vermelho sobre fundo preto:
Fundo preto: Y ≈ 0, Cb = 128, Cr = 128
Texto vermelho: Y ≈ 76, Cb = 85, Cr = 255Cada letra do texto é formada por pixels vermelhos finos, de talvez 1 ou 2 pixels de largura. Quando o 4:2:0 agrupa esses pixels em blocos de 2×2 e compartilha a crominância, os pixels da borda da letra "vazam" a cor vermelha pro fundo, ou o fundo "invade" a cor da letra, como por exemplo:

O resultado é que as bordas do texto ficam com um "sangramento" de cor (chamado de chroma bleeding ou color fringing). As letras ficam com uma borda meio borrada, meio rosada, especialmente nos cantos e nas partes finas.
É por isso que se você conecta um PC na TV via HDMI e a TV está configurada em 4:2:0, o texto do sistema operacional pode parecer levemente borrado ou com cores "estranhas" nas bordas. Mudar pra 4:4:4 resolve isso imediatamente.
Problema 2 — Bordas nítidas entre cores saturadas
Imagine uma bandeira com listras finas alternando entre vermelho vivo e azul vivo. Cada listra tem 2-3 pixels de largura:

No 4:2:0, blocos de 2×2 pixels que caem na borda entre uma listra vermelha e uma azul vão receber uma crominância "média" que não é nem vermelha nem azul. O resultado é uma borda com uma cor estranha, tipo um rosa ou roxo que não deveria estar ali.
Em fotos do mundo real, isso raramente é um problema porque transições de cor naturais são graduais. Mas em gráficos sintéticos (logos, capturas de tela, interfaces de software), onde as bordas de cor são afiadas e retas, o 4:2:0 pode criar artefatos visíveis.
Problema 3 — Chroma key (tela verde)
Se você trabalha com edição de vídeo e precisa fazer recorte por tela verde, o 4:2:0 dificulta a vida. O chroma key funciona analisando a cor de cada pixel pra decidir o que é "fundo verde" e o que é "pessoa".
Com 4:2:0, a resolução de cor é 1/4 da resolução do brilho. Isso significa que as bordas entre a pessoa e o fundo verde ficam imprecisas no canal de crominância, resultando em um recorte com contornos serrilhados ou pedaços de verde "vazando" ao redor do cabelo e dos braços.

É por isso que estúdios profissionais de VFX gravam em 4:4:4 ou no mínimo 4:2:2 quando sabem que vão usar chroma key.
💡 Regra prática: Se o conteúdo é foto/vídeo do mundo real pra consumo final → 4:2:0 é perfeito. Se é texto, gráficos, ou material pra pós-produção profissional → use 4:2:2 ou 4:4:4.
Existe 4:1:1 e outros esquemas?
Sim! Embora 4:4:4, 4:2:2 e 4:2:0 sejam de longe os mais comuns, existem outros esquemas que vale a pena conhecer:
4:1:1 → Cada amostra de crominância é compartilhada entre 4 pixels horizontais (em vez de 2 como no 4:2:2).
A crominância tem apenas 25% da resolução horizontal, mas mantém 100% da vertical. Era usado pelo antigo formato DV (aquelas fitas de filmadora Mini-DV dos anos 2000). Hoje é raramente utilizado.
4:4:0 → A crominância mantém 100% da resolução horizontal, mas tem apenas 50% da resolução vertical. É o oposto do 4:1:1. Muito raro na prática.
4:1:0 → A crominância tem apenas 25% da resolução horizontal e 50% da vertical. Extremamente agressivo. Quase nunca usado.
Na prática, os 3 que você precisa conhecer são:
4:4:4 → qualidade máxima, zero economia
4:2:2 → boa qualidade, 33% de economia (produção profissional)
4:2:0 → qualidade aceitável, 50% de economia (consumo final)
Pro nosso CODEC autoral, nós vamos implementar o 4:2:0, que é o padrão da indústria pra CODECs de imagem como o JPEG por exemplo.
Implementando Chroma Subsampling em C
Agoraaa chegou o momento de colocarmos a mão na massa e você aprender na prática como isso funciona ✅
Vamos criar um programa em C que pega uma imagem YCbCr e aplica o subsampling 4:2:0.
A ideia é a seguinte:
- Ler uma imagem RGB24 bruta
- Converter pra YCbCr (usando o código do artigo anterior)
- Aplicar o subsampling 4:2:0 nos canais Cb e Cr
- Salvar o resultado
Com o seu terminal aberto, crie a pasta e entre dentro dela:
mkdir -p ~/chroma-sub-c
cd ~/chroma-sub-c
Agora dentro dessa pasta crie o seu novo main.c, com a seguinte código:
#include <stdio.h>
#include <stdlib.h>
#include <math.h>
// ============================================================
// ESTRUTURAS
// ============================================================
typedef struct {
unsigned char r, g, b;
} PixelRGB;
typedef struct {
unsigned char y, cb, cr;
} PixelYCbCr;
// ============================================================
// FUNÇÕES DE CONVERSÃO (do artigo anterior)
// ============================================================
PixelYCbCr rgb_para_ycbcr(PixelRGB rgb) {
PixelYCbCr p;
double y = 0.299 * rgb.r + 0.587 * rgb.g + 0.114 * rgb.b;
double cb = -0.169 * rgb.r - 0.331 * rgb.g + 0.500 * rgb.b + 128.0;
double cr = 0.500 * rgb.r - 0.419 * rgb.g - 0.081 * rgb.b + 128.0;
if (y < 0) y = 0; if (y > 255) y = 255;
if (cb < 0) cb = 0; if (cb > 255) cb = 255;
if (cr < 0) cr = 0; if (cr > 255) cr = 255;
p.y = (unsigned char) round(y);
p.cb = (unsigned char) round(cb);
p.cr = (unsigned char) round(cr);
return p;
}
PixelRGB ycbcr_para_rgb(PixelYCbCr p) {
PixelRGB rgb;
double r = p.y + 1.402 * (p.cr - 128.0);
double g = p.y - 0.344 * (p.cb - 128.0) - 0.714 * (p.cr - 128.0);
double b = p.y + 1.772 * (p.cb - 128.0);
if (r < 0) r = 0; if (r > 255) r = 255;
if (g < 0) g = 0; if (g > 255) g = 255;
if (b < 0) b = 0; if (b > 255) b = 255;
rgb.r = (unsigned char) round(r);
rgb.g = (unsigned char) round(g);
rgb.b = (unsigned char) round(b);
return rgb;
}
// ============================================================
// CHROMA SUBSAMPLING 4:2:0
// ============================================================
//
// A função abaixo recebe:
// - Um array de pixels YCbCr (resolução completa)
// - Largura e altura da imagem
//
// E produz:
// - canal_y[] → luminância completa (width × height amostras)
// - canal_cb[] → crominância azul subamostrada (width/2 × height/2 amostras)
// - canal_cr[] → crominância vermelha subamostrada (width/2 × height/2 amostras)
//
// A ideia é percorrer a imagem em blocos de 2×2 pixels.
// Para cada bloco, mantemos os 4 valores de Y individuais,
// mas calculamos a MÉDIA dos 4 valores de Cb (e de Cr)
// pra produzir 1 único valor de Cb e 1 único valor de Cr
// que será compartilhado pelo bloco inteiro.
//
// IMPORTANTE: a largura e a altura devem ser números pares.
// Se não forem, o último pixel "solto" é ignorado.
// Na prática, os CODECs reais fazem padding pra garantir
// que as dimensões sejam pares.
void subsample_420(
PixelYCbCr *imagem, // imagem YCbCr completa
int width,
int height,
unsigned char *canal_y, // saída: Y completo
unsigned char *canal_cb, // saída: Cb subamostrado
unsigned char *canal_cr // saída: Cr subamostrado
) {
int half_w = width / 2; // largura dos canais Cb/Cr
int half_h = height / 2; // altura dos canais Cb/Cr
// Primeiro, copia todos os valores de Y (sem alteração)
for (int i = 0; i < width * height; i++) {
canal_y[i] = imagem[i].y;
}
// Agora, percorre em blocos de 2×2 e calcula a média de Cb e Cr
for (int j = 0; j < half_h; j++) {
for (int i = 0; i < half_w; i++) {
// Índices dos 4 pixels do bloco 2×2 na imagem original
//
// O bloco fica assim na imagem:
//
// (2*i, 2*j) (2*i+1, 2*j)
// (2*i, 2*j+1) (2*i+1, 2*j+1)
//
// Em um array linear, o pixel (x, y) está na posição y*width + x
int idx_tl = (2 * j) * width + (2 * i); // top-left
int idx_tr = (2 * j) * width + (2 * i + 1); // top-right
int idx_bl = (2 * j + 1) * width + (2 * i); // bottom-left
int idx_br = (2 * j + 1) * width + (2 * i + 1); // bottom-right
// Calcula a média dos 4 valores de Cb do bloco
int soma_cb = imagem[idx_tl].cb
+ imagem[idx_tr].cb
+ imagem[idx_bl].cb
+ imagem[idx_br].cb;
// Calcula a média dos 4 valores de Cr do bloco
int soma_cr = imagem[idx_tl].cr
+ imagem[idx_tr].cr
+ imagem[idx_bl].cr
+ imagem[idx_br].cr;
// Armazena no array subamostrado
// A posição no array reduzido é j * half_w + i
canal_cb[j * half_w + i] = (unsigned char)((soma_cb + 2) / 4);
canal_cr[j * half_w + i] = (unsigned char)((soma_cr + 2) / 4);
// O (+2) antes de dividir por 4 é um truque de arredondamento.
// Sem ele, a divisão inteira sempre arredonda pra baixo.
// Com o +2, o resultado fica mais próximo do valor correto.
// Exemplo: (91 + 95 + 88 + 92) = 366
// Sem +2: 366 / 4 = 91 (arredondou pra baixo)
// Com +2: 368 / 4 = 92 (mais próximo da média real 91.5)
}
}
}
// ============================================================
// RECONSTRUÇÃO: desfazendo o subsampling pra poder visualizar
// ============================================================
//
// Essa função "estica" os canais Cb/Cr de volta pro tamanho
// original, duplicando cada amostra pra preencher o bloco 2×2.
// Depois combina com o Y e converte tudo de volta pra RGB.
void reconstruir_rgb(
unsigned char *canal_y, // Y completo (width × height)
unsigned char *canal_cb, // Cb subamostrado (width/2 × height/2)
unsigned char *canal_cr, // Cr subamostrado (width/2 × height/2)
int width,
int height,
PixelRGB *saida_rgb // saída: imagem RGB reconstruída
) {
int half_w = width / 2;
for (int j = 0; j < height; j++) {
for (int i = 0; i < width; i++) {
int idx = j * width + i;
// Para encontrar qual amostra de Cb/Cr corresponde
// a este pixel, basta dividir as coordenadas por 2
int ci = i / 2;
int cj = j / 2;
int cidx = cj * half_w + ci;
PixelYCbCr p;
p.y = canal_y[idx];
p.cb = canal_cb[cidx];
p.cr = canal_cr[cidx];
saida_rgb[idx] = ycbcr_para_rgb(p);
}
}
}
// ============================================================
// MAIN
// ============================================================
int main() {
int width = 100;
int height = 100;
int total = width * height;
int half_w = width / 2;
int half_h = height / 2;
// ----- 1. Criar imagem RGB de teste (degradê colorido) -----
PixelRGB *imagem_rgb = (PixelRGB *) malloc(total * sizeof(PixelRGB));
for (int j = 0; j < height; j++) {
for (int i = 0; i < width; i++) {
int idx = j * width + i;
imagem_rgb[idx].r = (unsigned char)((i * 255) / (width - 1));
imagem_rgb[idx].g = (unsigned char)((j * 255) / (height - 1));
imagem_rgb[idx].b = 128;
}
}
// Salva original pra comparação
FILE *f = fopen("original.rgb", "wb");
fwrite(imagem_rgb, sizeof(PixelRGB), total, f);
fclose(f);
// ----- 2. Converter RGB → YCbCr -----
PixelYCbCr *imagem_ycbcr = (PixelYCbCr *) malloc(total * sizeof(PixelYCbCr));
for (int i = 0; i < total; i++) {
imagem_ycbcr[i] = rgb_para_ycbcr(imagem_rgb[i]);
}
// ----- 3. Aplicar Chroma Subsampling 4:2:0 -----
unsigned char *canal_y = (unsigned char *) malloc(total);
unsigned char *canal_cb = (unsigned char *) malloc(half_w * half_h);
unsigned char *canal_cr = (unsigned char *) malloc(half_w * half_h);
subsample_420(imagem_ycbcr, width, height, canal_y, canal_cb, canal_cr);
// ----- 4. Mostrar a economia de dados -----
int tamanho_444 = total * 3; // Y + Cb + Cr, todos em resolução completa
int tamanho_420 = total + (half_w * half_h) + (half_w * half_h);
printf("=== Chroma Subsampling 4:2:0 ===\n\n");
printf("Imagem: %d x %d pixels\n", width, height);
printf("Total de pixels: %d\n\n", total);
printf("Tamanho em 4:4:4 (sem subsampling):\n");
printf(" Y: %d amostras\n", total);
printf(" Cb: %d amostras\n", total);
printf(" Cr: %d amostras\n", total);
printf(" Total: %d bytes\n\n", tamanho_444);
printf("Tamanho em 4:2:0 (com subsampling):\n");
printf(" Y: %d amostras (100%%)\n", total);
printf(" Cb: %d amostras (25%%)\n", half_w * half_h);
printf(" Cr: %d amostras (25%%)\n", half_w * half_h);
printf(" Total: %d bytes\n\n", tamanho_420);
printf("Economia: %d bytes (%.1f%%)\n\n",
tamanho_444 - tamanho_420,
(1.0 - (double)tamanho_420 / tamanho_444) * 100.0);
// ----- 5. Reconstruir a imagem RGB a partir do 4:2:0 -----
PixelRGB *reconstruida = (PixelRGB *) malloc(total * sizeof(PixelRGB));
reconstruir_rgb(canal_y, canal_cb, canal_cr, width, height, reconstruida);
// Salva a versão reconstruída pra comparação visual
f = fopen("reconstruida_420.rgb", "wb");
fwrite(reconstruida, sizeof(PixelRGB), total, f);
fclose(f);
printf("Arquivos gerados com sucesso!\n\n");
printf("Para comparar visualmente:\n");
printf(" ffplay -f rawvideo -pixel_format rgb24 -video_size 100x100 original.rgb\n");
printf(" ffplay -f rawvideo -pixel_format rgb24 -video_size 100x100 reconstruida_420.rgb\n");
// ----- Limpeza -----
free(imagem_rgb);
free(imagem_ycbcr);
free(canal_y);
free(canal_cb);
free(canal_cr);
free(reconstruida);
return 0;
}Pra compilar e rodar:
gcc main.c -o subsample -lm
./subsampleA saída esperada será algo assim:
=== Chroma Subsampling 4:2:0 ===
Imagem: 100 x 100 pixels
Total de pixels: 10000
Tamanho em 4:4:4 (sem subsampling):
Y: 10000 amostras
Cb: 10000 amostras
Cr: 10000 amostras
Total: 30000 bytes
Tamanho em 4:2:0 (com subsampling):
Y: 10000 amostras (100%)
Cb: 2500 amostras (25%)
Cr: 2500 amostras (25%)
Total: 15000 bytes
Economia: 15000 bytes (50.0%)50% de economia, exatamente como previmos! E se você abrir as duas imagens com o ffplay, vai ver que a diferença visual entre a original e a reconstruída é mínima.
Na imagem de 100×100 com degradê suave, praticamente imperceptível.
Vejamos alguns exemplos lado a lado:



Observação importante: No caso de imagens reais (fotos), a diferença é ainda mais difícil de perceber, porque fotos naturais têm transições de cor suaves. Onde você vai notar diferença é em bordas nítidas entre cores saturadas diferentes, como texto vermelho sobre fundo branco.
Resumo: o truque que corta dados pela metade
Vamos recapitular tudo o que aprendemos neste artigo:
1) Chroma Subsampling é a técnica de reduzir a resolução dos canais de crominância (Cb e Cr) enquanto mantém a luminância (Y) intacta.
2) A notação J:a:b descreve o esquema: J é a largura do bloco de referência (sempre 4), a é o número de amostras de cor na primeira linha, b é o número na segunda linha.
3) 4:4:4 → crominância completa, zero economia. Usado em edição profissional.
4) 4:2:2 → metade da resolução horizontal de cor, 33% de economia. Usado em broadcast e câmeras profissionais.
5) 4:2:0 → 1/4 da resolução de cor (metade horizontal E vertical), 50% de economia. Usado em JPEG, streaming, Blu-ray, basicamente tudo que é consumo final.
6) O subsampling funciona porque o olho humano é biologicamente mais sensível ao brilho do que à cor. Não é gambiarra, é exploração inteligente de uma limitação real do sistema visual.
7) O 4:2:0 pode causar problemas em texto colorido fino, bordas nítidas entre cores saturadas e chroma key, mas pra fotos e vídeos naturais é praticamente perfeito.
Implementar o subsampling em C é simples: percorra a imagem em blocos de 2×2, mantenha cada Y individual, e calcule a média dos Cb e Cr do bloco.
E com isso, fechamos a Fase 1 — Fundamentos Visuais! 🎉
Ao longo desses 3 artigos, construímos uma base sólida:
- Artigo 1: Entendemos que cor é luz, que pixels são trios RGB, e que uma imagem crua é uma sequência de bytes.
- Artigo 2: Aprendemos que o YCbCr separa brilho de cor, e que isso é a chave pra compressão inteligente.
- Artigo 3: Vimos que o Chroma Subsampling corta metade dos dados antes de qualquer outra técnica.
No próximo artigo, entramos na Fase 2 — Fundamentos de Compressão, começando com: O que é compressão? Lossless vs Lossy, entropia e redundância.
Até lá 🚀

