Lendo uma imagem de verdade: o formato PPM e os canais de cor

Lendo uma imagem de verdade: o formato PPM e os canais de cor

Olá leitor, seja muito bem vindo de volta a mais uma etapa da nossa jornada aqui no Portal da Micilini! 😊

Este é o décimo quinto artigo da nossa série, e o segundo da Fase 4. No artigo anterior, a gente deu vida ao N.148i e criou o seu cabeçalho, aquela etiqueta de 16 bytes que abre o nosso arquivo. 🏷️

Mas a caixa ainda está vazia! 📦 Pra encher ela, a gente precisa primeiro conseguir trazer uma imagem de verdade pra dentro do nosso programa. E é exatamente isso que vamos fazer hoje.

No fim deste artigo, você vai pegar uma foto sua (um .png, .jpg ou o que for), converter pra um formato que o nosso programa entende, carregar ela em C, e separar nos três canais de cor Y, Cb e Cr, prontinhos pra alimentar todo o pipeline que a gente construiu na Fase 3.

Pega o café ☕ e vem comigo, que hoje tem foto de verdade! 🚀

O nosso primeiro problema: ler um JPG é difícil demais (por enquanto)

A primeira coisa que passa na cabeça é: "beleza, então vamos abrir um .jpg em C e pronto!"

Só que aí mora uma pegadinha, para abrir um .jpg, o nosso programa precisaria descomprimir um JPEG inteiro.

Ou seja, fazer Huffman ao contrário, dezig-zag, dequantização, IDCT... Tudo aquilo que a gente passou a Fase 3 inteira aprendendo, só que na ordem inversa. 🤯

E o mesmo vale pro .png (que precisaria de DEFLATE), pro .webp, pro .heic... Todos são formatos comprimidos, cada um com a sua própria complexidade.

Seria como precisar saber montar um motor pra conseguir dirigir, e seguir por esse caminho não faz sentido neste momento.

O que a gente quer agora é o mais simples possível: um formato onde os pixels estão ali, crus, sem compressão nenhuma, prontos pra serem lidos.

Dito isso, nada melhor do que usar o formato PPM.

O PPM: o formato mais simples do mundo

O PPM (Portable Pixmap) é um formato de imagem criado lá nos anos 80 com um objetivo bem específico: ser tão simples que qualquer programa consiga ler e escrever sem esforço.

Ele faz parte de uma categoria de outros formatos, são eles:

  • PBM (Portable Bitmap): imagens em preto e branco puro (1 bit por pixel). ⬛⬜
  • PGM (Portable Graymap): imagens em tons de cinza (1 byte por pixel). 🌫️
  • PPM (Portable Pixmap): imagens coloridas RGB (3 bytes por pixel). 🌈

O PPM é perfeito pra gente por três motivos:

É facílimo de ler: o cabeçalho é texto puro e os pixels vêm logo em seguida, sem compressão.

É RGB24 puro: exatamente o formato que a gente estudou lá no artigo 1!

Qualquer ferramenta converte pra ele: daqui a pouco eu te mostro como.

A desvantagem? Os arquivos são enormes, porque não tem compressão nenhuma. Mas isso não é problema: a nossa missão é justamente pegar essa imagem gigante e comprimir com o N.148i.

Esse vai ser o ponto de partida ideal. 😄

A anatomia de um arquivo PPM

Aqui vem a parte mais legal, e ela conecta diretamente com aquela conversa de texto x binário do artigo passado.

O PPM é um formato híbrido: e isso significa que o cabeçalho é escrito em texto legível, e os pixels vêm logo depois em binário puro. 🤝

Um arquivo PPM tem esta cara:

P6
320 240
255
[aqui começam os bytes crus dos pixels: R,G,B,R,G,B,R,G,B...]

Onde:

  • P6: é o magic number do PPM binário (lembra do conceito do artigo 14? É a "impressão digital" do formato).
  • 320 240: a largura e a altura da imagem, em pixels.
  • 255: o maxval, ou seja, o maior valor que um canal pode ter. Com 255, cada canal ocupa 1 byte (0 a 255), que é o nosso caso.

E depois do 255 vem um único byte de espaço em branco (geralmente um \n), e logo na sequência começam os pixels crus.

Os pixels vêm entrelaçados (interleaved), na ordem R, G, B, R, G, B... percorrendo a imagem da esquerda pra direita, de cima pra baixo. Exatamente como vimos o funcionamento do RGB24 do artigo 1!

P3 e P6: as duas versões do PPM

Uma coisa importante: o PPM tem duas variantes, e a diferença é justamente aquilo que discutimos no artigo passado.

P3 (ASCII): onde os pixels também são escritos como texto, dessa forma:

P3
2 2
255
255 0 0    0 255 0
0 0 255    255 255 255

Isso é algo totalmente legível por humanos, bonitinho... mas gigantesco!

Onde cada pixel gasta vários caracteres em vez de 3 bytes.

P6 (binário): os pixels são bytes crus. Muito menor e muito mais rápido de ler.

Pro nosso codec, vamos usar sempre o P6. É o padrão que as ferramentas geram por default, e é o que faz sentido pra um programa ler, sendo assim, se algum dia você abrir um PPM e vir números soltos em texto, sabe que caiu num P3 😉

Convertendo a sua imagem para PPM

Beleza, agora a parte prática: você tem uma foto .png, .jpg, .jpeg ou .webp e precisa transforma-la num formato .ppm.

Dito isso, vou te mostrar várias formas, e peço para que você escolha a que for mais confortável 😉

Opção 1 — ImageMagick (a mais recomendada) ⭐

O ImageMagick é um canivete suíço de imagens, e converte praticamente qualquer coisa.

Para instalar no Windows, baixe o instalador no site oficial: https://imagemagick.org/script/download.php.

Durante a instalação, deixe marcada a opção de adicionar ao PATH (assim o comando funciona em qualquer terminal).

Já no Linux, abra o terminal e digite o seguinte comando:

sudo apt install imagemagick

No Mac, se você tem o Homebrew, basta executar no terminal:

brew install imagemagick

Para usar o ImageMagick, certifique-se de que você abriu o terminal em um local aonde existe a sua foto chamada de foto.png ou foto.jpg ou foto.web, e execute: (funciona igual nos três sistemas operacionais):

magick foto.png example.ppm

No caso acima, ele vai pegar a foto.png e vai converter para example.ppm.

E funciona com qualquer formato de entrada, vejamos:

magick foto.jpg example.ppm
magick foto.jpeg example.ppm
magick foto.webp example.ppm

💡 Em versões mais antigas do ImageMagick, o comando é convert em vez de magick. Se o magick não funcionar, tente convert foto.png example.ppm.

Opção 2 — FFmpeg 🎬

Se você já tem o FFmpeg instalado (muita gente tem, por causa de vídeo), ele também resolve numa linha:

ffmpeg -i foto.png example.ppm

Instalação: no Linux, sudo apt install ffmpeg; no Mac, brew install ffmpeg; no Windows, baixe em https://ffmpeg.org/download.html.

Opção 3 — GIMP (sem terminal, tudo no mouse) 🖱️

Se você prefere não mexer com terminal, o GIMP (que é gratuito) resolve numa boa:

  • Abra a sua imagem no GIMP.
  • Vá em Arquivo → Exportar como...
  • No nome do arquivo, escreva example.ppm (a extensão é o que define o formato).
  • Clique em Exportar.
  • Vai aparecer uma janelinha perguntando o formato de dados: escolha Binário (não "ASCII"!). ✅
  • Confirme.

Aquela escolha "Binário" é justamente o P6 que a gente quer. Se escolher ASCII, você vai gerar um P3 e o nosso futuro programa não vai conseguir interpretar.

Uma dica: comece com uma imagem pequena 🐣

Pra esse primeiro teste, eu recomendo usar uma imagem pequena, tipo 320×240 ou 640×480 pixels. Motivos:

  • O arquivo PPM fica menor (uma foto de celular de 12 megapixels vira um PPM de ~36 MB! 😱).
  • Fica mais fácil de conferir os resultados.

Se quiser redimensionar junto com a conversão, o ImageMagick faz tudo de uma vez:

magick foto.jpg -resize 320x240 example.ppm

Colocando o arquivo na pasta certa

Agora, um passo simples mas essencial: o nosso programa vai procurar a imagem num lugar específico, com um nome específico. Então vamos deixar tudo no lugar.

Lembra da nossa estrutura de pastas do artigo 14? O arquivo convertido tem que ir pra pasta images, com o nome example.ppm:

n148i/
├── src/
│   ├── main.c          ← o nosso programa
│   ├── header.c
│   └── header.h
├── images/
│   └── example.ppm     ← 👈 SUA IMAGEM CONVERTIDA VAI AQUI!
└── output/
    └── image.n148i

E existem duas formas de você fazer esse tipo de coisa:

Pelo mouse: simplesmente arraste o example.ppm pra dentro da pasta images. Se o arquivo tiver outro nome, renomeie pra example.ppm.

Pelo terminal: você pode já converter direto pra pasta certa. Estando dentro da pasta n148i:

magick foto.jpg images/example.ppm

Para conferir se deu tudo certo, e que ele é um arquivo P6 de verdade, dá para espiar o comecinho dele, se você estiver usando Linux ou Mac, basta abrir o terminal dentro da pasta n148i e rodar:

head -c 20 images/example.ppm

Você deve ver algo como P6, seguido da largura, altura e o 255. Se aparecer P3, você gerou a versão ASCII e precisa converter de novo escolhendo binário.  

⚠️ Um detalhe chato: algumas ferramentas (o GIMP, por exemplo) adicionam uma linha de comentário no cabeçalho, começando com #, tipo # Created by GIMP. Isso é permitido pelo formato! Por isso o nosso leitor vai precisar saber ignorar comentários, já já você vai ver como.  

Escrevendo o leitor de PPM em C

Agora sim, o código! Ler um PPM é basicamente: ler o cabeçalho (que é texto), descobrir o tamanho, e depois engolir todos os bytes de pixel de uma vez.

Mas tem duas armadilhas no cabeçalho que precisamos tratar com carinho:

Armadilha 1: os espaços em branco são livres. O formato permite que os números do cabeçalho sejam separados por espaço, quebra de linha, tab, ou uma combinação disso. Então não dá pra assumir que sempre vai ser "uma linha pra cada coisa". Precisamos de uma função que pule qualquer quantidade de espaço em branco.

Armadilha 2: os comentários. Como falei acima, pode aparecer uma linha começando com # no meio do cabeçalho, e ela precisa ser ignorada inteira.

Pra resolver as duas de uma vez, vamos criar uma função skip_whitespace_and_comments() que a gente chama antes de ler cada número.

Dito isso, chegou a hora de escrever o nosso código, sendo assim, abra o arquivo n148i/src/main.c e coloque isto:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

// ============================================================
// DATA STRUCTURES
// ============================================================

typedef struct {
    int width;
    int height;
    unsigned char *pixels;      // RGB interleaved: R,G,B,R,G,B...
} Image;

typedef struct {
    int width;
    int height;
    unsigned char *y;           // luminance
    unsigned char *cb;          // blue-difference chroma
    unsigned char *cr;          // red-difference chroma
} YCbCrImage;

// ============================================================
// PPM HEADER PARSING HELPERS
// ============================================================
//
// The PPM header is plain text, and the format allows any amount
// of whitespace between the numbers, plus '#' comment lines.
// This helper eats all of that so we land exactly on the next
// real piece of content.

static void skip_whitespace_and_comments(FILE *f) {
    int c;
    for (;;) {
        c = fgetc(f);
        if (c == '#') {                         // comment: skip the whole line
            while (c != '\n' && c != EOF) c = fgetc(f);
        } else if (c == ' ' || c == '\t' || c == '\n' || c == '\r') {
            continue;                            // whitespace: keep going
        } else {
            ungetc(c, f);                        // real content: put it back
            return;
        }
    }
}

// Reads one decimal number from the header.
// NOTE: this also consumes the single character that ends the
// number (usually a newline), which is exactly the separator the
// format expects before the pixel data starts.
static int read_number(FILE *f) {
    skip_whitespace_and_comments(f);
    int value = 0, c, digits = 0;
    while ((c = fgetc(f)) != EOF && c >= '0' && c <= '9') {
        value = value * 10 + (c - '0');
        digits++;
    }
    if (digits == 0) return -1;
    return value;
}

// ============================================================
// LOAD A BINARY PPM (P6) FILE
// ============================================================

int load_ppm(const char *path, Image *img) {
    FILE *f = fopen(path, "rb");     // "rb" = read binary
    if (!f) {
        printf("Could not open '%s'. Is the file in the images/ folder?\n", path);
        return 0;
    }

    // 1. Check the magic number: it must be "P6"
    char magic[3] = {0};
    if (fread(magic, 1, 2, f) != 2) {
        printf("File is too small to be a PPM.\n");
        fclose(f); return 0;
    }
    if (magic[0] != 'P' || magic[1] != '6') {
        printf("Not a binary PPM (P6). Found '%s'.\n", magic);
        printf("Tip: if it says P3, re-export choosing the BINARY option.\n");
        fclose(f); return 0;
    }

    // 2. Read width, height and maxval from the text header
    int width  = read_number(f);
    int height = read_number(f);
    int maxval = read_number(f);

    if (width <= 0 || height <= 0) {
        printf("Invalid dimensions in header.\n");
        fclose(f); return 0;
    }
    if (maxval != 255) {
        printf("Only maxval 255 is supported (found %d).\n", maxval);
        fclose(f); return 0;
    }

    // 3. Read all the raw pixel bytes at once
    long pixel_count = (long) width * height;
    long byte_count  = pixel_count * 3;          // 3 bytes per pixel (R, G, B)

    unsigned char *pixels = (unsigned char *) malloc(byte_count);
    if (!pixels) {
        printf("Out of memory.\n");
        fclose(f); return 0;
    }

    if (fread(pixels, 1, byte_count, f) != (size_t) byte_count) {
        printf("Pixel data ended too early. Is the file truncated?\n");
        free(pixels); fclose(f); return 0;
    }

    fclose(f);

    img->width  = width;
    img->height = height;
    img->pixels = pixels;
    return 1;
}

// ============================================================
// RGB -> YCbCr CONVERSION
// ============================================================
//
// Same formulas we saw back in article 2 (JPEG / ITU-R BT.601).
// Y holds the brightness, Cb and Cr hold the colour information.

static unsigned char clamp_byte(double v) {
    if (v < 0.0)   return 0;
    if (v > 255.0) return 255;
    return (unsigned char)(v + 0.5);     // round to nearest
}

int convert_to_ycbcr(Image *img, YCbCrImage *out) {
    long n = (long) img->width * img->height;

    out->width  = img->width;
    out->height = img->height;
    out->y  = (unsigned char *) malloc(n);
    out->cb = (unsigned char *) malloc(n);
    out->cr = (unsigned char *) malloc(n);

    if (!out->y || !out->cb || !out->cr) {
        printf("Out of memory.\n");
        return 0;
    }

    for (long i = 0; i < n; i++) {
        double r = img->pixels[i * 3 + 0];
        double g = img->pixels[i * 3 + 1];
        double b = img->pixels[i * 3 + 2];

        out->y[i]  = clamp_byte( 0.299000*r + 0.587000*g + 0.114000*b);
        out->cb[i] = clamp_byte(-0.168736*r - 0.331264*g + 0.500000*b + 128.0);
        out->cr[i] = clamp_byte( 0.500000*r - 0.418688*g - 0.081312*b + 128.0);
    }
    return 1;
}

// ============================================================
// SAVE A SINGLE CHANNEL AS A GRAYSCALE PGM (P5) FILE
// ============================================================
//
// PGM is the grayscale cousin of PPM: same idea, but 1 byte per
// pixel instead of 3. Perfect for looking at one channel alone.

int save_pgm(const char *path, unsigned char *data, int width, int height) {
    FILE *f = fopen(path, "wb");
    if (!f) { printf("Could not create '%s'\n", path); return 0; }

    fprintf(f, "P5\n%d %d\n255\n", width, height);   // text header
    fwrite(data, 1, (long) width * height, f);       // raw pixel bytes

    fclose(f);
    return 1;
}

// ============================================================
// MAIN
// ============================================================

int main() {
    Image img;
    if (!load_ppm("../images/example.ppm", &img)) return 1;

    long pixel_count = (long) img.width * img.height;

    printf("=== PPM loaded successfully ===\n");
    printf("Size:        %d x %d pixels\n", img.width, img.height);
    printf("Pixels:      %ld\n", pixel_count);
    printf("Pixel data:  %ld bytes (%.1f KB)\n\n",
           pixel_count * 3, (pixel_count * 3) / 1024.0);

    printf("First 3 pixels (RGB):\n");
    for (int i = 0; i < 3; i++) {
        printf("  pixel %d: R=%3d G=%3d B=%3d\n", i,
               img.pixels[i*3+0], img.pixels[i*3+1], img.pixels[i*3+2]);
    }
    printf("\n");

    YCbCrImage ycc;
    if (!convert_to_ycbcr(&img, &ycc)) { free(img.pixels); return 1; }

    printf("Same 3 pixels after RGB -> YCbCr:\n");
    for (int i = 0; i < 3; i++) {
        printf("  pixel %d: Y=%3d Cb=%3d Cr=%3d\n", i,
               ycc.y[i], ycc.cb[i], ycc.cr[i]);
    }
    printf("\n");

    // Save each channel so we can actually look at them
    save_pgm("../output/channel_y.pgm",  ycc.y,  img.width, img.height);
    save_pgm("../output/channel_cb.pgm", ycc.cb, img.width, img.height);
    save_pgm("../output/channel_cr.pgm", ycc.cr, img.width, img.height);
    printf("Channels saved to output/: channel_y.pgm, channel_cb.pgm, channel_cr.pgm\n");

    // Always give the memory back
    free(img.pixels);
    free(ycc.y); free(ycc.cb); free(ycc.cr);
    return 0;
}

Salve este arquivo, e antes de executá-lo, deixa eu destrinchar cada peça principal:

skip_whitespace_and_comments(): a nossa faxineira do cabeçalho. Ela vai comendo caracteres enquanto forem espaço, tab, quebra de linha ou comentário. Quando encontra algo "de verdade", devolve o caractere pro arquivo com ungetc() e sai. Esse ungetc é um truque bacana do C: ele "desengole" um caractere, colocando ele de volta na fila pra ser lido de novo.

read_number(): lê um número decimal do cabeçalho, dígito por dígito. Repara num detalhe sutil que está comentado no código: quando ela para de ler, o caractere que encerrou o número (geralmente o \n) já foi consumido. Isso é ótimo, porque é justamente o separador que o formato exige antes dos pixels, então quando terminamos de ler o maxval, estamos exatamente no primeiro byte de pixel.

💡 Essa é uma armadilha clássica de quem escreve leitor de PPM: colocar um fgetc() extra "pra pular o espaço" depois do maxval, e sem querer comer o primeiro byte da imagem. O resultado é uma imagem com as cores todas deslocadas de um byte, o que deixa tudo com um tom esquisito. Se um dia sua imagem sair com cores estranhas, desconfie disso!   

load_ppm(): junta tudo: confere o P6, lê largura/altura/maxval, aloca a memória necessária e engole todos os pixels com um único fread. Repara que a gente valida cada passo e dá uma mensagem de erro útil (inclusive sugerindo o binário se achar um P3). 🛡️

convert_to_ycbcr(): aqui a gente reencontra as fórmulas do artigo 2! São elas que transformam R, G, B em Y (brilho), Cb e Cr (cor). O clamp_byte() garante que o resultado sempre caiba em 0-255, porque a conta pode passar um pouquinho dos limites por arredondamento. 🎨

save_pgm(): essa é a cereja do bolo didático. Ela salva um canal isolado como um arquivo PGM (o primo em escala de cinza do PPM), pra você poder abrir e ver com os próprios olhos cada canal separado. 👀

Rodando o nosso programa

Com o example.ppm na pasta images e o código salvo, compile e rode como sempre:

cd n148i/src
gcc main.c -o n148i
./n148i

Usando uma imagem de teste de 320×240, o resultado que é muito similar a este:

=== PPM loaded successfully ===
Size:        320 x 240 pixels
Pixels:      76800
Pixel data:  230400 bytes (225.0 KB)

First 3 pixels (RGB):
  pixel 0: R=  0 G=  0 B=128
  pixel 1: R=  0 G=  0 B=128
  pixel 2: R=  1 G=  0 B=128

Same 3 pixels after RGB -> YCbCr:
  pixel 0: Y= 15 Cb=192 Cr=118
  pixel 1: Y= 15 Cb=192 Cr=118
  pixel 2: Y= 15 Cb=192 Cr=118

Channels saved to output/: channel_y.pgm, channel_cb.pgm, channel_cr.pgm

Os números vão ser diferentes com a sua imagem, claro, mas a estrutura é essa. 🎉

Vamos conferir uma dessas contas na mão, pra ver a matemática funcionando. O primeiro pixel é (R=0, G=0, B=128), um azul escuro. Aplicando a fórmula do Y:

Y = 0.299 × 0 + 0.587 × 0 + 0.114 × 128
Y = 0 + 0 + 14.59
Y = 15  (arredondando)

E deu exatamente os 15 que apareceram na tela! ✅

Repara como faz sentido: o azul contribui pouquíssimo pro brilho (só 11,4%), então um azul escuro tem luminância baixa, e é aquilo que a gente estudou no artigo 2 acontecendo na prática.

Vendo os canais com os seus próprios olhos

Agora a parte mais divertida. Vá até a pasta output e abra os três arquivos .pgm que o programa gerou.

É importante ressaltar que a maioria dos visualizadores de imagem abre PGM numa boa (o GIMP com certeza abre; se o seu visualizador não abrir, converta com magick channel_y.pgm channel_y.png).

O que você vai ver ao abrir essas imagens:

  • channel_y.pgm: a sua imagem em preto e branco, e com uma qualidade surpreendentemente boa! É o canal de luminância, que carrega toda a estrutura e o detalhe da foto. 🌗
  • channel_cb.pgm e channel_cr.pgm: duas imagens acinzentadas e "borradas", bem menos definidas. São os canais de cor. 🎨

E aqui está a lição visual mais importante da série toda: repara como o canal Y sozinho já é praticamente a imagem inteira pro seu olho, enquanto os canais de cor parecem manchas suaves.

É por isso que o chroma subsampling (artigo 3) funciona tão bem, e é por isso que a tabela de quantização da crominância (artigo 10) pode ser tão agressiva. Você está literalmente vendo o motivo pelo qual a compressão de imagem funciona. 🤯

Cuidando da Memoria

Um detalhe de C que vale destacar, principalmente se você vem de linguagens como JavaScript ou Python: aqui a gente pede memória com malloc() e precisa devolver com free().

Repara que no fim do main a gente libera tudo:

free(img.pixels);
free(ycc.y); free(ycc.cb); free(ycc.cr);

Se você esquecer disso, o programa vai deixar memória "presa" (o famoso memory leak).

Num programinha que roda e fecha, isso é inofensivo, o sistema operacional limpa tudo no fim. Mas é um hábito importante de se ter, porque em programas maiores isso vira um problema sério. 🧹

O que vem por aí

Agora que já temos a imagem na memória, separada em três canais bonitinhos. O caminho está livre pro próximo passo, que é onde tudo se junta:

  • ✂️ Dividir cada canal em blocos 8×8 (artigo 8), tratando as bordas quando a imagem não é múltipla de 8.
  • 🔄 Passar cada bloco pelo pipeline completo: DCT → quantização → zig-zag → RLE → Huffman.
  • 📦 Gravar tudo isso logo depois do cabeçalho, formando o .n148i completo.

Ou seja: no próximo artigo, a gente finalmente comprime uma imagem inteira e gera o primeiro arquivo .n148i de verdade, com foto dentro!  

Repositório no GitHub

E sim, você pode acompanhar a evolução desse CODEC de imagens em um repositório no GitHub, commit por commit 👋

Segue o link abaixo do commit desse artigo, onde implementamos a leitura de arquivos PPM e a separação dos canais de cor:

Resumo

Recapitulando este artigo:

  • O PPM (Portable Pixmap) é um formato de imagem sem compressão, feito pra ser fácil de ler — perfeito pra alimentar o nosso codec.
  • Ele é híbrido: cabeçalho em texto legível (P6, largura, altura, maxval) seguido dos pixels em binário puro (RGB entrelaçado).
  • Existe o P3 (pixels em texto, enorme) e o P6 (pixels em binário). Sempre use o P6!
  • Dá pra converter qualquer .png, .jpg, .jpeg ou .webp pra PPM com ImageMagick (magick foto.png example.ppm), FFmpeg, ou pelo GIMP exportando em modo Binário.
  • O arquivo convertido vai na pasta images com o nome example.ppm.
  • O leitor precisa lidar com espaços em branco livres e linhas de comentário (#) no cabeçalho, daí a nossa função skip_whitespace_and_comments().
  • Cuidado com a armadilha do byte extra depois do maxval, que desloca todos os pixels e bagunça as cores.
  • Convertemos RGB →  YCbCr com as fórmulas do artigo 2, e salvamos cada canal como PGM pra poder visualizar.
  • Olhando os canais, dá pra ver que o Y carrega quase toda a informação visual, enquanto Cb e Cr são suaves, a base de todo o truque da compressão.

Saímos de "o codec tem um cabeçalho para o codec tem uma imagem de verdade carregada e separada em canais". A caixa está prestes a ser preenchida!

No próximo artigo, vamos pegar esses canais, fatiar em blocos 8×8, e rodar o pipeline completo da Fase 3 na imagem inteira, gerando finalmente um arquivo .n148i com uma foto comprimida dentro.

Prepara o café ☕, porque o próximo é o grande encontro de tudo o que a gente construiu até aqui.

Até a próxima! 👋

Criadores de Conteúdo

Foto do William Lima
William Lima
Fundador da Micilini

Inventor nato, escreve conteudos de programação para o portal da micilini.

Torne-se um MIC 🤖

Mais de 100 mic's já estão conectados na plataforma.