Processamento de Imagens em C

Construa sua própria E/S PPM/PGM, conversão para tons de cinza, filtros de convolução e detecção de bordas — com o mínimo de bibliotecas externas.

Representando imagens

Uma imagem digital é um array 2D de intensidades de pixel:
Veja a imersão em arrays multidimensionais — o layout linha-a-linha de C encaixa naturalmente em buffers de imagem.

Ler/escrever PPM & PGM (sem biblioteca)

PGM (Portable Graymap) e PPM (Portable Pixmap) usam cabeçalhos minúsculos seguidos de dados brutos de pixel — perfeito para aprender.

Formato do cabeçalho PGM

P5 # número mágico (P5 binário, P2 ASCII)
4 3 # largura altura
255 # valor máximo
<largura × altura bytes de pixel>

Escrever

#include <stdio.h>

int save_pgm(const char *path, int w, int h, const unsigned char *data) {
    FILE *fp = fopen(path, "wb");
    if (!fp) return -1;
    fprintf(fp, "P5\n%d %d\n255\n", w, h);
    fwrite(data, 1, (size_t)w * h, fp);
    fclose(fp);
    return 0;
}

Ler

unsigned char *load_pgm(const char *path, int *w, int *h) {
    FILE *fp = fopen(path, "rb");
    if (!fp) return NULL;
    char magic[3];
    int maxval;
    if (fscanf(fp, "%2s %d %d %d", magic, w, h, &maxval) != 4) {
        fclose(fp); return NULL;
    }
    fgetc(fp);   // pula a quebra de linha única após o cabeçalho
    size_t n = (size_t)(*w) * (*h);
    unsigned char *data = malloc(n);
    fread(data, 1, n, fp);
    fclose(fp);
    return data;
}

PPM (colorido, 3 bytes por pixel)

fprintf(fp, "P6\n%d %d\n255\n", w, h);
fwrite(rgb_data, 3, (size_t)w * h, fp);   // ordem RGB
Obtendo imagens de teste: no macOS/Linux use ImageMagick (convert input.png output.pgm), ou exporte como PGM/PPM no GIMP ou Preview.app.

Tons de cinza / inverter / limiarização

RGB → tons de cinza

Luminância perceptiva: Y = 0.299 R + 0.587 G + 0.114 B (ITU-R BT.601).
void to_gray(int w, int h,
             const unsigned char *rgb,
             unsigned char *gray) {
    for (int i = 0; i < w * h; i++) {
        int r = rgb[3*i + 0];
        int g = rgb[3*i + 1];
        int b = rgb[3*i + 2];
        gray[i] = (unsigned char)(0.299 * r + 0.587 * g + 0.114 * b);
    }
}

Inverter

void invert(int w, int h, unsigned char *img) {
    for (int i = 0; i < w * h; i++)
        img[i] = 255 - img[i];
}

Limiarização (binarizar)

void binarize(int w, int h, unsigned char *img, int threshold) {
    for (int i = 0; i < w * h; i++)
        img[i] = (img[i] >= threshold) ? 255 : 0;
}
Escolhendo um limiar: um valor fixo de 128 geralmente funciona, mas o método de Otsu escolhe um automaticamente a partir do histograma.

Convolução (blur / nitidez)

Uma convolução multiplica cada pixel e seus vizinhos por um kernel (uma grade de pesos) e soma os resultados. É a base de quase todo filtro de imagem.

Box blur (média 3×3)

1/9
1/9
1/9
1/9
1/9
1/9
1/9
1/9
1/9

Nitidez

0
-1
0
-1
5
-1
0
-1
0

Convolução genérica 3×3

void convolve3(int w, int h,
               const unsigned char *src,
               unsigned char *dst,
               const double k[3][3]) {
    for (int y = 0; y < h; y++) {
        for (int x = 0; x < w; x++) {
            double sum = 0;
            for (int dy = -1; dy <= 1; dy++) {
                for (int dx = -1; dx <= 1; dx++) {
                    int ny = y + dy, nx = x + dx;
                    if (ny < 0) ny = 0;
                    if (ny >= h) ny = h - 1;
                    if (nx < 0) nx = 0;
                    if (nx >= w) nx = w - 1;
                    sum += src[ny * w + nx] * k[dy + 1][dx + 1];
                }
            }
            if (sum < 0) sum = 0;
            if (sum > 255) sum = 255;
            dst[y * w + x] = (unsigned char)sum;
        }
    }
}

double blur[3][3] = {
    {1/9.0, 1/9.0, 1/9.0},
    {1/9.0, 1/9.0, 1/9.0},
    {1/9.0, 1/9.0, 1/9.0}
};
convolve3(w, h, src, dst, blur);
Não faça no mesmo lugar: escrever no mesmo buffer que você está lendo corrompe o resultado. Mantenha src e dst separados.

Detecção de bordas Sobel

Calcule gradientes horizontais e verticais com dois kernels e depois pegue a magnitude.

Kernels Gx / Gy de Sobel

Gx:
-1
0
1
-2
0
2
-1
0
1
Gy:
-1
-2
-1
0
0
0
1
2
1

Implementação

#include <math.h>

void sobel(int w, int h,
           const unsigned char *src,
           unsigned char *dst) {
    static const int Gx[3][3] = {{-1,0,1},{-2,0,2},{-1,0,1}};
    static const int Gy[3][3] = {{-1,-2,-1},{0,0,0},{1,2,1}};

    for (int y = 1; y < h - 1; y++) {
        for (int x = 1; x < w - 1; x++) {
            int sx = 0, sy = 0;
            for (int dy = -1; dy <= 1; dy++) {
                for (int dx = -1; dx <= 1; dx++) {
                    int v = src[(y + dy) * w + (x + dx)];
                    sx += v * Gx[dy + 1][dx + 1];
                    sy += v * Gy[dy + 1][dx + 1];
                }
            }
            double mag = sqrt((double)sx * sx + (double)sy * sy);
            if (mag > 255) mag = 255;
            dst[y * w + x] = (unsigned char)mag;
        }
    }
}
Compilar: gcc -O2 sobel.c -o sobel -lm. Rode num PGM e abra a saída em qualquer visualizador de imagens — só as bordas vão aparecer.

Histograma & aumento de contraste

Conta quantos pixels caem em cada intensidade. Útil para ajuste de contraste, limiarização e análise.
void histogram(int w, int h,
               const unsigned char *img,
               int hist[256]) {
    for (int i = 0; i < 256; i++) hist[i] = 0;
    for (int i = 0; i < w * h; i++) hist[img[i]]++;
}

void print_histogram(const int hist[256]) {
    int maxv = 0;
    for (int i = 0; i < 256; i++) if (hist[i] > maxv) maxv = hist[i];
    for (int i = 0; i < 256; i += 4) {
        int sum = 0;
        for (int k = 0; k < 4; k++) sum += hist[i + k];
        int bars = maxv ? sum * 40 / maxv : 0;
        printf("%3d |", i);
        for (int b = 0; b < bars; b++) putchar('#');
        putchar('\n');
    }
}

Aumento linear de contraste

void stretch_contrast(int w, int h, unsigned char *img) {
    int lo = 255, hi = 0;
    for (int i = 0; i < w * h; i++) {
        if (img[i] < lo) lo = img[i];
        if (img[i] > hi) hi = img[i];
    }
    if (hi == lo) return;
    for (int i = 0; i < w * h; i++) {
        img[i] = (unsigned char)((img[i] - lo) * 255 / (hi - lo));
    }
}

stb_image para PNG/JPG

A forma mais fácil de ler PNG/JPG/BMP/GIF em C é a stb_image — uma biblioteca de arquivo único, domínio público e header-only.

Download

$ curl -O https://raw.githubusercontent.com/nothings/stb/master/stb_image.h $ curl -O https://raw.githubusercontent.com/nothings/stb/master/stb_image_write.h

Exemplo mínimo: PNG → PGM

#define STB_IMAGE_IMPLEMENTATION
#include "stb_image.h"
#include <stdio.h>
#include <stdlib.h>

int main(int argc, char **argv) {
    int w, h, ch;
    unsigned char *data = stbi_load(argv[1], &w, &h, &ch, 1);
    // último arg = 1: força canal único (tons de cinza)
    if (!data) { fprintf(stderr, "falha ao carregar\n"); return 1; }

    FILE *fp = fopen("out.pgm", "wb");
    fprintf(fp, "P5\n%d %d\n255\n", w, h);
    fwrite(data, 1, (size_t)w * h, fp);
    fclose(fp);

    stbi_image_free(data);
    return 0;
}
Gravando PNG: inclua stb_image_write.h e chame stbi_write_png("out.png", w, h, 1, data, w);

Desafios

Desafio 1: Gere um PGM de tabuleiro de damas
Gere um tabuleiro preto-e-branco de 400×400 (quadrados de 50 pixels) e salve como PGM.
Desafio 2: Ferramenta CLI de tons de cinza
Construa ./togray input.ppm output.pgm: leia um PPM P6, converta pela fórmula de luminância e grave um PGM.
Desafio 3: Blur gaussiano
Defina um kernel gaussiano 5×5 (pesos que decaem a partir do centro) para um blur mais suave que o filtro box.
Desafio 4: Mosaico
Tire a média de cada bloco N×N e preencha todo pixel do bloco com a média. Compare N = 1, 4, 16 e 32.
Desafio 5: Método de Otsu
Escolha automaticamente o limiar ótimo de binarização maximizando a variância entre classes no histograma.

Veja também

Quiz de Revisão

Teste seu entendimento desta aula!

Q1. Que informação um pixel de uma imagem colorida típica guarda?

Um valor de intensidade de 0 a 255 para cada R, G e B
Um único inteiro com sinal
Um único caractere ASCII

Cores de 24 bits usam 8 bits (0–255) para cada um: vermelho, verde e azul. Um formato de 32 bits adiciona um canal alfa (transparência).

Q2. Qual é uma fórmula padrão para converter uma imagem colorida para tons de cinza?

0.299*R + 0.587*G + 0.114*B
(R+G+B)^2
R * G * B

Os pesos correspondem à sensibilidade do olho humano — é a conversão padrão ITU-R BT.601.

Q3. Qual filtro é comumente usado para detecção de bordas?

Filtros de derivada como Laplaciano ou Sobel
Filtro de média
Matriz identidade

Bordas são mudanças bruscas de brilho, então kernels de convolução que aproximam derivadas as destacam.