terça-feira, 3 de julho de 2012

Algoritmos de halftone

Neste artigo vou apresentar alguns algoritmos de halftone e as características de cada um.

O termo halftone diz respeito a uma técnica de simulação de tons contínuos de cor usando apenas duas cores, geralmente as cores branca e preta para simular tons de cinza. Essa técnica pode ser aplicada a qualquer dispositivo gráfico binário (onde cada pixel só pode apresentar dois valores, tal como um LED aceso ou apagado). Há também aplicações em que uma imagem colorida pode ser representada utilizando-se apenas LEDs RGB. Nesse caso, aplica-se a técnica de halftone para cada uma das componentes da imagem.

Um pouco mais de teoria

Os algoritmos de halftone tem a capacidade converter uma imagem qualquer para uma imagem composta de apenas dois valores, de forma a gerar a menor distorção possível para o olho humano. As primeiras aplicações foram utilizadas na indústria gráfica para imprimir imagens em jornais, de forma que os tons de cinza eram representados por círculos de tinta preta. Se a região da imagem fosse mais escura, os círculos eram maiores, se fosse mais clara, menores.

Digitalmente, tudo o que podemos fazer é decidir se um determinado pixel será pintado ou não. Para isso, existem vários algoritmos de halftone, cada um com diferentes características.

O primeiro algoritmo que vou apresentar é o mais simples possível: decisão por threshold. Este algoritmo é tão simples que dificilmente será aplicado na prática, mas vou mostrar o seu efeito para tornar este artigo mais didático.
Basicamente, o que a função halftone abaixo faz é pintar o pixel de branco se este for maior que 127 ou pintar de preto caso contrário.

void halftone1(uint8_t *img, size_t w, size_t h)
{
        size_t l;
        size_t c;
        size_t i;

        for (l = 0; l < h; l++) {
                for (c = 0; c < w; c++) {
                        i = (l * w) + c;
                        if (img[i] > 127)
                                img[i] = 255;
                        else
                                img[i] = 0;
                }
        }
}

Poderíamos melhorar um pouco esta técnica fazendo com que a análise seja realizada em um conjunto de quatro pixels por vez. Dessa forma, a decisão de como pintar os pixels será tomada baseada em um número maior de intervalos, nesse caso 5 intervalos possíveis. A soma dos valores dos pixels originais fará com que um dos padrões apresentados na figura abaixo seja escolhido.

Este método foi implementado na função halftone2 no código abaixo.

void halftone2(uint8_t *img, size_t w, size_t h)
{
        uint32_t l;
        uint32_t c;
        uint32_t i;
        int32_t val;

        for (l = 0; l < h - 1; l += 2) {
                for (c = 0; c < w - 1; c += 2) {
                        i = (l * w) + c;
                        val = (int32_t) img[i];
                        val += (int32_t) img[i + 1];
                        val += (int32_t) img[i + w];
                        val += (int32_t) img[i + w + 1];
                        if (val > (1 * 1024) / 8)
                                img[i] = 255;
                        else
                                img[i] = 0;
                        if (val > (3 * 1024) / 8)
                                img[i + w + 1] = 255;
                        else
                                img[i + w + 1] = 0;
                        if (val > (5 * 1024) / 8)
                                img[i + w] = 255;
                        else
                                img[i + w] = 0;
                        if (val > (7 * 1024) / 8)
                                img[i + 1] = 255;
                        else
                                img[i + 1] = 0;
                }
        }
}

Porém, estes métodos apresentados resultarão em erros de quantização. Digamos que um pixel recebe o valor 255 e o valor na imagem original em escala de cinza era 200. Então houve um erro de 55, e isso afetará a qualidade final da imagem.

O algoritmo de error diffusion faz com que o erro de quantização geral na imagem seja nulo. Isso é possível pois para cada pixel processado, o erro gerado é acumulado para o pixel seguinte. A função halftone3 mostra a implementação básica de um algoritmo de error diffusion.

void halftone3(uint8_t *img, size_t w, size_t h)
{
        uint32_t l;
        uint32_t c;
        uint32_t i;
        int32_t pe;     /* propagated error */
        int32_t te;     /* total error */

        pe = 0;
        te = 0;

        for (l = 1; l < h; l++) {
                for (c = 1; c < w; c++) {
                        i = (l * w) + c;
                        te = (int32_t) img[i] + pe;
                        if (te > 127)
                                img[i] = 255;
                        else
                                img[i] = 0;
                        pe = te - img[i];
                }
        }
}

Como todo método simples, este algoritmo também possui uma versão um pouco mais sofisticada. Imagine que, ao processar um determinado pixel, o erro no processo de quantização seja distribuído para os pixels vizinhos e não apenas acumulado no pixel seguinte. Essa distribuição do erro é feita de acordo com uma matriz de distribuição. Para exemplificar o método utilizei a matriz de Floyd e Steinberg.

O código do algoritmo é o seguinte:

void halftone4(int16_t *img, size_t w, size_t h)
{
        uint32_t l;
        uint32_t c;
        uint32_t i;
        int pe;         /* propagated error */
        int oldp;
        int newp;

        for (l = 1; l < h - 1; l++) {
                for (c = 1; c < w - 1; c++) {
                        i = (l * w) + c;
                        oldp = img[i];
                        if (oldp > 127)
                                newp = 255;
                        else
                                newp = 0;
                        img[i] = (int16_t) newp;
                        pe = oldp - newp;
                        img[i + 1] = (int16_t) ((int) img[i + 1] + (7 * pe) / 16);
                        img[i + w - 1] = (int16_t) ((int) img[i + w - 1] + (3 * pe) / 16);
                        img[i + w] = (int16_t) ((int) img[i + w] + (5 * pe) / 16);
                        img[i + w + 1] = (int16_t) ((int) img[i + w + 1] + (1 * pe) / 16);
                }
        }
}

Há uma diferença importante entre a função halftone4 e as demais. Nesse último caso, o erro gerado em cada pixel será somado nos pixels da vizinhança. Como as imagens que estou trabalhando possuem resolução de 8 bits (uint8_t), pode acontecer de o valor do pixel somado aos erros extrapolarem os limites da variável. Por isso, a função halftone4 recebe a imagem como int16_t, a qual deve ser convertida antes da chamada dessa função.

Resultados

Para mostrar o efeito de cada algoritmo, utilizei os programas disponibilizados neste artigo para aplicar estas funções a imagens quaisquer.

Abaixo está o efeito de cada algoritmo aplicado a uma imagem de rampa de tons de cinza.

teste123

rampa de tons de cinza

halftone1: threshold

halftone2: célula de threshold

halftone3: error diffusion

halftone4: matriz de Floyd e Steinberg

Para ter uma ideia mais prática, apliquei também estes algoritmos no R2D2 e gerei as imagens abaixo:

imagem original colorida

halftone1: threshold

halftone2: célula de threshold

halftone3: error diffusion

halftone4: matriz de Floyd e Steinberg

Conclusão

Se você precisa transformar uma imagem em zeros e uns, qualquer um desses algoritmos funciona. Se você precisa de qualidade na imagem final apenas aplicar o threshold não será o suficiente. Já a célula de threshold pode dar um efeito bom, mas quanto maior for a célula usada (nesse caso usei 4 pixels), menor ficará a resolução aparente da imagem. Os algoritmos de error diffusion irão manter a luminosidade aparente da imagem. O error diffusion simples causa mais granulosidade. Para obter o melhor resultado a recomendação é aplicar uma matriz de distribuição como na função halftone4, mas vai demandar mais linhas de código e mais tempo para processar.

sábado, 2 de junho de 2012

Trabalhando com imagens em tons de cinza

Este artigo abre o blog Processamento Digital de Imagens em C com o objetivo de oferecer ferramentas simples para manipulação de imagens.

Para os próximos artigos, pretendo abordar tópicos variados sobre processamento de imagens. Em cada trabalho, vou utilizar algoritmos e exemplos de implementações em C visando documentar as minhas experiências e também atender as necessidades reportadas pelos leitores.

O objetivo desse artigo é apresentar duas ferramentas que irei utilizar futuramente: a primeira é um conversor de imagem JPEG para tons de cinza. A segunda ferramenta faz o contrário: converte uma imagem crua (raw em tons de cinza) para JPEG.
Assim, poderemos pegar um JPEG, gerar um arquivo manipulável dessa imagem, fazer alterações e gerar uma nova imagem em JPEG.Para exemplificar o uso destas ferramentas, fiz também um programa que abre uma imagem em raw a aplica um filtro de média.

O código foi testado no Ubuntu 12.04, mas acredito que não será muito difícil recompilar tudo em uma distribuição linux diferente.

Conversão de JPEG para tons de cinza

Para facilitar o trabalho, utilizei a biblioteca libjpeg. Disponibilizei aqui um pacote com os fontes do meu programa.
A biblioteca libjpeg poderá ser instalada no Ubuntu com o seguinte comando:

sudo apt-get install libjpeg-dev

Ao descompactar o arquivo jpeg_tools.tar.bz2, você terá acesso a quatro diretórios: build guardará os arquivos compilados, em src está o código do programa que aplica um filtro de média na imagem em raw, em utils estão os códigos dos conversores para JPEG e em images há uma imagem de exemplo. Para compilar, basta digitar "make".

O arquivo "utils/jpeg2raw.c" é quem faz toda a mágica. A função jpeg_decompress abre um arquivo JPEG no espaço de cores RGB e extrai a luminância (imagem em tons de cinza), que volta para a função main através da struct raw_img. Por fim, a luminância é gravada em um arquivo especificado pelo usuário.

O trexo de código abaixo mostra a conversão de RGB para tons de cinza.

#include <jpeglib.h>
/* ... */
static int jpeg_decompress(const char *file, struct raw_img *raw)
{
        /* ... */
        while (jdec.output_scanline < jdec.image_height) {
                jpeg_read_scanlines(&jdec, row_pointer, 1);
                for (i = 0; i < raw->width * raw->num_comp; i += 3) {
                        r = (float) row_pointer[0][i];
                        g = (float) row_pointer[0][i + 1];
                        b = (float) row_pointer[0][i + 2];
                        /* here convert to grayscale */
                        raw->img[j] = (uint8_t)
                                        (0.2126 * r + 0.7152 * g + 0.0722 * b);
                        j++;
                }
        }
        /* ... */
}

O restante do código é basicamente a configuração do libjpeg para descompactar a imagem em JPEG e a gravação do arquivo em raw.

Conversão de tons de cinza para JPEG

O processo inverso é semelhante e foi implementado no arquivo utils/raw2jpeg.c. A função jpeg_compress recebe uma imagem raw já aberta, faz a compressão para JPEG e salva no arquivo que foi especificado no parâmetro file.

static int jpeg_compress(const char *file, struct raw_img *raw)
{
        struct jpeg_compress_struct cinfo;
        struct jpeg_error_mgr jerr;
        JSAMPROW row_ptr[1];
        FILE* out_jpeg;
        uint8_t *image;
        int row_stride;
        int err;

        err = 0;
        image = raw->img;
        out_jpeg = fopen(file, "w+");
        if (out_jpeg == NULL) {
                printf("Could no open '%s' filen", file);
                err = -1;
                goto error;
        }


        jpeg_create_compress(&cinfo);
        jpeg_stdio_dest(&cinfo, out_jpeg);

        cinfo.image_width = raw->width;
        cinfo.image_height = raw->height;
        cinfo.input_components = IMAGE_NCHANNELS;
        cinfo.in_color_space = JCS_GRAYSCALE;
        cinfo.err = jpeg_std_error(&jerr);
        jpeg_set_defaults(&cinfo);
        jpeg_start_compress(&cinfo, TRUE);
        row_stride = raw->width * IMAGE_NCHANNELS;

        while (cinfo.next_scanline < cinfo.image_height) {
                row_ptr[0] = &image[cinfo.next_scanline * row_stride];
                jpeg_write_scanlines(&cinfo, row_ptr, 1);
        }

        jpeg_finish_compress(&cinfo);
        jpeg_destroy_compress(&cinfo);
        fclose(out_jpeg);

error:
        return err;
}

Como a imagem em raw não guarda informações sobre profundidade de cor e resolução da imagem, neste programa estou assumindo que cada pixel é representado por um byte (uint8_t) e a resolução deverá ser passada como parâmetro.

Resultado das transformações

Para gerar uma imagem em raw e depois converter para JPEG execute os seguintes comandos:

./build/jpeg2raw images/sapo_380x254.jpg images/grayscale.raw
./build/raw2jpeg images/grayscale.raw images/grayscale.jpeg 380 254

A imagem gerada em escala de cinza ficará no diretório images, junto com a imagem original colorida.

Imagem de arquivo pessoal

Filtro de média bidimensional

Como havia dito, fiz um outro programa que aplica um filtro de médias em uma imagem em tons de cinza. O código está em src/mean_filter_2d.c. A função mean_filter_2d aplica um filtro de médias 3x3, ou seja, cada píxel é calculado como a média dos nove pixels mais próximos:

static void mean_filter_2d(uint8_t *in, uint8_t *out, size_t w, size_t h)
{
        uint32_t l;
        uint32_t c;
        uint32_t i;
        uint32_t pix;

        for (l = 1; l < h; l++) {
                for (c = 1; c < w; c++) {
                        i = (l * w) + c;
                        pix = (uint32_t) in[i - w - 1];
                        pix += (uint32_t) in[i - w];
                        pix += (uint32_t) in[i - w + 1];
                        pix += (uint32_t) in[i - 1];
                        pix += (uint32_t) in[i];
                        pix += (uint32_t) in[i + 1];
                        pix += (uint32_t) in[i + w - 1];
                        pix += (uint32_t) in[i + w];
                        pix += (uint32_t) in[i + w + 1];
                        out[i] = (uint8_t) (pix / 9);
                }
        }
}

Este programa recebe como parâmetros uma imagem de entrada (em raw), uma imagem de saída (também em raw) e a resolução da imagem.

Resultado do filtro

Para aplicar o filtro na imagem gerada com o comando anterior e gerar um JPEG da imagem filtrada, digite:

./build/mean_filter_2d images/grayscale.raw images/mean.raw 380 254
./build/raw2jpeg images/mean.raw images/mean.jpeg 380 254

Para simplificar tudo isso, fiz o script run.sh que faz todo esse processo para uma imagem JPEG qualquer.

Nos próximos artigos vou trabalhar com outros tipos de filtros e o primeiro será halftoning.

Se você tem alguma sugestão, deixe um comentário!