Original Article: Friday Q&A 2012-08-31: Obtaining and Interpreting Image Data
Author: Mike Ash

Q&A de Sexta 31-08-2012: Obtendo e interpretando dados de imagem
por Mike Ash  

Cocoa oferece algumas ótimas abstrações para trabalhar com imagens. A NSImage permite que você trate uma imagem como um blob opaco que você pode simplesmente desenhar onde você quer. A imagem principal envolve muito processamento de imagem em uma API fácil de usar que o deixa de se preocupar com a forma como os pixels individuais são representados. No entanto, às vezes você realmente quer apenas obter os dados de pixel em bruto no código. Scott Luther sugeriu o tópico de hoje: buscar e manipular esses dados de pixel em bruto.

Teoria
A representação de imagem mais simples é um bitmap simples. Esta é uma série de bits, um por pixel, indicando se é preto ou branco. A matriz contém linhas de pixels uma após a outra, de modo que o número total de bits é igual à largura da imagem multiplicada pela altura. Aqui está um exemplo de bitmap de um rosto sorridente:

    0 0 0 0 0 0 0 0
    0 0 0 0 0 0 0 0
    0 0 1 0 0 1 0 0
    0 0 0 0 0 0 0 0
    0 0 0 0 0 0 0 0
    0 1 0 0 0 0 1 0
    0 0 1 1 1 1 0 0
    0 0 0 0 0 0 0 0

Puro preto e branco não é um meio muito expressivo, é claro, e acessar bits individuais em uma matriz é um pouco complicado. Vamos mover um passo para usar um byte por pixel, o que permite a escala de cinza (podemos ter zero ser preto, 255 ser branco e os números entre diferentes tons de cinza) e facilita o acesso aos elementos também.

Mais uma vez, usaremos uma série de bytes com linhas seqüenciais. Aqui está um exemplo de código para alocar memória para a imagem:

    uint8_t *AllocateImage(int width, int height)
    {
        return malloc(width * height);
    }

Para chegar a um pixel específico em (x, y), temos que mover para baixo linhas y, então, através dessa linha, por pixeis x. Como as linhas são definidas sequencialmente, nós nos movemos para baixo linhas y movendo-se através da matriz por bytes y * width. O índice para um pixel particular é então x + y * width. Com base nisso, aqui estão duas funções para obter e definir um pixel em escala de cinza em uma coordenada particular:

    uint8_t ReadPixel(uint8_t *image, int width, int x, int y)
    {
        int index = x + y * width;
        return image[index];
    }

    void SetPixel(uint8_t *image, int width, int x, int y, uint8_t value)
    {
        int index = x + y * width;
        image[index] = value;
    }

Grayscale ainda não é tão interessante em muitos casos, e queremos poder representar a cor. A maneira típica de representar pixels coloridos é com uma combinação de três valores para componentes vermelhos, verdes e azuis. Todos os zeros resultam em preto, com outros valores misturando as três cores juntas para formar qualquer cor que seja necessária. É típico usar 8 bits por cor, o que resulta em 24 bits por pixel. Às vezes, eles são embalados juntos, e às vezes eles são preenchidos com um extra 8 pedaços de vazio para dar 32 bits por pixel, que é melhor trabalhar, já que os computadores geralmente são bons em manipular 32-valores de bits.

Transparência, ou alfa, também pode ser útil para representar em uma imagem. 8 bits de transparência se encaixam bem na 8 pedaços de preenchimento em um 32 bit pixel e usando 32 bit pixels segurando vermelho, verde, azul e alfa é provavelmente o formato de pixel mais comum atualmente em uso.

Existem duas maneiras de empacotar esses pixels juntos. A maneira comum é apenas executá-los todos juntos em seqüência, então você teria um byte de vermelho, um byte de verde, um byte de azul e um byte de alfa ao lado um do outro. Então você teria vermelho, verde, azul e alfa para o próximo pixel, e assim por diante. Cada pixel ocupa quatro bytes de memória contígua.

Também é possível armazenar cada cor em um pedaço de memória separado. Cada pedaço é chamado de avião, e este formato é chamado de "plano". Neste caso, você tem essencialmente três ou quatro regiões (dependendo da existência de alfa) de memória, cada uma delas descrita exatamente como os pixels do exemplo de escala de cinza acima. A cor do pixel é uma combinação dos valores de todos os planos. Isso às vezes pode ser mais conveniente para trabalhar, mas muitas vezes é mais lento, devido a uma localidade de referência ruim, e muitas vezes mais complexo para trabalhar, por isso é um formato muito menos comum.

A única coisa a descobrir é como as cores são ordenadas. O pedido de RGBA (vermelho, verde, azul, e alfa) é o mais comum no Mac, mas as ordens como ARGB e BGRA aparecem ocasionalmente também. Não há motivos específicos para escolher um sobre o outro, além da compatibilidade ou da velocidade. Para evitar conversões de formato caras, é melhor combinar o formato usado pelo que você vai desenhar, salvar ou carregar, quando possível.

Obtendo dados de pixels
A classe Cocoa que contém e fornece dados de pixels é NSBitmapImageRep. Esta é uma subclasse de NSImageRep, que é uma classe abstrata para uma única "representação" de uma imagem. NSImage é um recipiente para um ou mais NSImageRep instâncias. No caso de mais de uma representação, eles podem representar diferentes tamanhos, resoluções, espaços de cores, etc., e NSImage escolherá o melhor para o contexto atual ao desenhar.

Dado que, ele parece que deve ser bastante fácil obter os dados da imagem de um NSImage: encontrar um NSBitmapImageRep em suas representações, então peça essa representação para seus dados de pixel.

Há dois problemas com isso. Primeiro, a imagem pode não ter uma NSBitmapImageRep. Existem tipos de representação que não são bitmaps. Por exemplo, um NSImage representar um PDF conterá dados vetoriais, não dados bitmap e usará um tipo diferente de representação de imagem. Em segundo lugar, mesmo que a imagem tenha um NSBitmapImageRep, não há como contar o formato de pixel dessa representação. Não é prático escrever código para lidar com todos os formatos de pixels possíveis, especialmente porque será difícil testar a maioria dos casos.

Há um monte de código lá fora que faz isso de qualquer maneira. Ele escapa com ele fazendo suposições sobre o conteúdo da NSImage e o formato de pixel do NSBitmapImageRep. Isso não é confiável, e deve ser evitado.

Como fazer você obter dados confiáveis de pixels, então? Você pode extrair um NSImage de forma confiável, e você pode extrair dentro de um NSBitmapImageRep usando a classe NSGraphicsContext, e você pode obter dados de pixel do NSBitmapImageRep. Encadecê-lo tudo em conjunto, e você pode obter dados de pixel.

Aqui está algum código para lidar com esta seqüência. A primeira coisa que faz é descobrir a largura e a altura do pixel da representação de bitmap. Isto não é necessariamente óbvio, como oNSImage size não precisa corresponder às dimensões do pixel. Este código usará size De qualquer forma, mas, dependendo da sua situação, você pode querer usar uma maneira diferente de descobrir o tamanho:

    NSBitmapImageRep *ImageRepFromImage(NSImage *image)
    {
        int width = [image size].width;
        int height = [image size].height;

        if(width < 1 || height < 1)
            return nil;

Em seguida, criamos o NSBitmapImageRep. Isso envolve o uso de um de fato longo método de inicialização que parece amedrontador, mas vou passar por todos os parâmetros em detalhes:

        NSBitmapImageRep *rep = [[NSBitmapImageRep alloc]
                                 initWithBitmapDataPlanes: NULL
                                 pixelsWide: width
                                 pixelsHigh: height
                                 bitsPerSample: 8
                                 samplesPerPixel: 4
                                 hasAlpha: YES
                                 isPlanar: NO
                                 colorSpaceName: NSCalibratedRGBColorSpace
                                 bytesPerRow: width * 4
                                 bitsPerPixel: 32]

Vejamos estes parâmetros um a um. O primeiro argumento, BitmapDataPlanes, permite que você especifique a memória onde os dados de pixels serão armazenados. Passagem NULL aqui, como este código faz, diz NSBitmapImageRep para alocar sua própria memória internamente, que geralmente é a maneira mais conveniente de lidar com isso.

Em seguida, o código especifica o número de pixels de largura e alta, que calculou anteriormente. Apenas passa esses valores para pixelsWide e pixelsHigh.

Agora começamos a entrar no formato de pixel real. Eu mencionei anteriormente que o RGBA de 32 bits (onde vermelho, verde, azul e alfa cada um aceita um byte e é colocado de forma contígua na memória) é um formato de pixel comum, e é isso que vamos usar. Como cada amostra é de um byte, o código passa 8 para bitsPerSample:. O samplesPerPixel: parâmetro refere-se ao número de componentes diferentes usados na imagem. Temos quatro componentes (R, G, B e A) e, portanto, o código 4 passa aqui.

O formato RGBA tem alfa, então passamos YES para hasAlpha. Nós não queremos um formato plano, então nós passamos NO por IsPlanar. Queremos um espaço de cores RGB, então passamos NSCalibratedRGBColorSpace.

Próximo, NSBitmapImageRep quer saber quantos bytes compõem cada linha da imagem. Isso é usado caso o preenchimento seja desejado. Às vezes, uma linha de imagem usa mais do que o número estritamente minimo de bytes, geralmente por motivos de desempenho, para manter as coisas bem alinhadas. Nós não queremos mexer com estofamento, então passamos o número mínimo de bytes necessários para uma linha de pixels, que é apenas width * 4.

Finalmente, ele pede o número de bits por pixel. Em 8 bits por componente e 4 componentes, isso é apenas 32.

Agora temos um NSBitmapImageRep com o formato que queremos, mas como desenhamos nisso? O primeiro passo é fazer uma NSGraphicsContext com:

        NSGraphicsContext *ctx = [NSGraphicsContext graphicsContextWithBitmapImageRep: rep];

Uma nota importante quando a solução de problemas: nem todos os parâmetros para uma NSBitmapImageRep são aceitáveis ao criar um NSGraphicsContext. Se esta linha se queixa de um formato não suportado, isso significa que um dos parâmetros usados para criar o NSBitmapImageRep não era para o gosto do sistema, então volte e verifique as.

O próximo passo é definir esse contexto como o atual contexto gráfico. Para garantir que não nos encontremos com qualquer outra atividade gráfica que possa estar acontecendo, primeiro salvamos o estado atual dos gráficos, para que possamos restaurá-lo mais tarde:

        [NSGraphicsContext saveGraphicsState];
        [NSGraphicsContext setCurrentContext: ctx];

Neste ponto, qualquer desenho que façamos vai entrar no nosso recém-cunhado NSBitmapImageRep. O próximo passo é simplesmente extrair a imagem.

        [image drawAtPoint: NSZeroPoint fromRect: NSZeroRect operation: NSCompositeCopy fraction: 1.0];

NSZeroRect é simplesmente um atalho conveniente que conta NSImage extrair toda a imagem.

Agora que a imagem é extraida, liberamos o contexto de gráficos para garantir que nenhuma dessas coisas ainda esteja em fila, restaure o estado dos gráficos e retorne o bitmap:

        [ctx flushGraphics];
        [NSGraphicsContext restoreGraphicsState];

        return rep;
    }

Usando esta técnica, você pode obter qualquer coisa que o Cocoa é capaz de atrair um 32-bit RGBA bitmap.

Interpretando dados de Pixel
Agora que temos os dados de pixels, o que nós fazemos com isso? Precisamente o que fazer com isso depende de você, mas vamos ver como realmente obter os dados de pixel.

Vamos começar definindo uma estrutura para representar um pixel individual:

    struct Pixel { uint8_t r, g, b, a; };

Isso se alinhará com os dados de pixels RGBA armazenados no NSBitmapImageRep. Nós podemos pegar um ponteiro para fora para usar:

    struct Pixel *pixels = (struct Pixel *)[rep bitmapData];

O acesso a um pixel específico em (x, y) funciona como o código de exemplo anterior para imagens em escala de cinza:

    int index = x + y * width;
    NSLog(@"Pixel at %d, %d: R=%u G=%u B=%u A=%u",
          x, y
          pixels[index].r,
          pixels[index].g,
          pixels[index].b,
          pixels[index].a);

Certifique-se de que x e y estão localizados dentro dos limites da imagem antes de fazer isso, ou então resultados divertidos podem resultar. Se você tiver sorte, as coordenadas fora dos limites irão travar.

Para iterar sobre todos os pixels na imagem, um simples par de loops fará:

    for(int y = 0; y < height; y++)
        for(int x = 0; x < width; x++)
        {
            int index = x + y * width;
            // Use pixels[index] here
        }

Observe como o y loop é o mais externo, embora x primeiro seria a ordem natural. Isso ocorre porque é muito mais rápido para iterar sobre os pixels na mesma ordem em que eles estão armazenados na memória, de modo que os pixels adjacentes sejam acessados sequencialmente. Colocando x no interior faz isso, eo código resultante é muito mais amigável para o cache e os controladores de memória que são criados para lidar com o acesso seqüencial.

Um compilador moderno provavelmente gerará um bom código para o acima, mas no caso de você ser paranóico e quiser certificar-se de que o compilador não gerará um índice de multiplicação e matriz para cada iteração de loop, você pode iterar usando a aritmética do ponteiro em vez disso:

    struct Pixel *cursor = pixels;
    for(int y = 0; y < height; y++)
        for(int x = 0; x < width; x++)
        {
            // Use cursor->r, cursor->g, etc.
            cursor++;
        }

Finally, note that this data is mutáveis. Se você desejar, você pode realmente modificar r, g, b, e a, e o NSBitmapImageRep refletirá as mudanças.

Conclusão
Lidar com dados de pixel em bruto não é algo que você costuma fazer, mas se você precisar, o Cocoa torna relativamente fácil. A técnica é uma pequena rotunda, mas atraindo uma NSBitmapImageRep com um formato de pixel escolhido, você pode obter dados de pixels no formato de sua escolha. Depois de ter esses dados de pixels, é uma questão simples de indexação nela para obter os valores de pixel individuais.

Por hoje é isso! Sexta-feira Q&A é conduzido por idéias de leitor como sempre, então, se você tiver alguma sugestão para tópicos que você gostaria de ver cobertos em uma parcela futura, por favor nos envie.

Gostou deste artigo? Eu estou vendendo livros inteiros cheios deles! Os volumes II e III estão agora fora! Eles estão disponíveis como ePub, PDF, impressão e em iBooks e Kindle. Clique aqui para obter mais informações.