5.18. Histogramas e estatísticas

Além das operações que alteram os pixels de uma imagem, a classe Image traz uma família de métodos que os medem – resumem a distribuição dos valores dos pixels, retornam o brilho médio e mediano, encontram o ponto de corte ideal entre pixels escuros e claros, relatam a dispersão dos canais de cor. As medições alimentam aplicações de duas maneiras: como entradas para o código que decide qual limiar usar, qual ganho definir, qual a aparência do perfil tonal da cena; e como sinais de diagnóstico – “a cena está clara o suficiente?” – sobre os quais uma aplicação pode agir sem tomar uma decisão sobre qualquer pixel específico.

O ponto de partida para quase toda medição é o histograma.

5.18.1. O histograma

Um histograma de uma imagem é uma contagem de quantos pixels têm cada valor de brilho possível. Para uma imagem em escala de cinza, isso é uma lista de contagens indexada pelos valores 0 a 255. Para uma imagem colorida, são três dessas listas – uma por canal.

get_histogram() computa um:

h = img.get_histogram()

O objeto retornado é um resultado histogram que expõe as listas de bins por canal e algumas consultas de alto nível sobre elas. As contagens dos bins são normalizadas de modo que somem 1.0 – o histograma descreve o perfil da distribuição em vez da contagem absoluta de pixels, o que torna as medições comparáveis entre imagens de tamanhos diferentes.

Para imagens em escala de cinza, o histograma tem um canal de bins, disponível como h.bins() (ou equivalentemente h[0]). Para imagens RGB565, o histograma é computado no espaço de cores LAB introduzido na página de limiarização binária, com três canais de bins disponíveis como h.l_bins(), h.a_bins(), h.b_bins() (ou h[0], h[1], h[2]). LAB é a mesma escolha que os métodos de limiar e rastreamento usam; os histogramas concordam com os limiares sobre em qual espaço a cor está sendo medida.

5.18.2. Bins e a contagem de bins

O histograma padrão tem um bin por valor de pixel possível – 256 bins para um canal de 8 bits. Às vezes essa é uma resolução mais fina do que a aplicação precisa. Um classificador que só se importa com o perfil aproximado da distribuição pode ser melhor servido por uma contagem de bins menor – 32 ou até 8 bins – que tanto roda mais rápido quanto produz um resultado mais limpo contra o ruído. A palavra-chave bins (e as por canal l_bins, a_bins, b_bins para cor) define a contagem:

h = img.get_histogram(bins=32)

A delimitação por ROI e limiar funciona da mesma forma que em todos os outros métodos de medição. Passe um roi para confinar o histograma a um retângulo de pixels; passe uma lista thresholds para incluir apenas os pixels que correspondem a essas faixas. A forma com limiar é o que torna “computar o histograma apenas dos pixels correspondentes” uma operação de uma única chamada – um padrão comum quando uma aplicação quer caracterizar a textura de uma região já detectada sem ter que percorrer os pixels por conta própria.

Um histograma em escala de cinza desenhado como uma linha de barras ao longo da faixa de brilho de 0 a 255. A distribuição tem dois picos -- um pico escuro menor e um pico claro maior -- separados por um vale claro. Três linhas verticais estão sobrepostas: o limiar de Otsu no vale, a média deslocada em direção ao pico claro maior e a mediana mais à direita, onde a contagem cumulativa de pixels atinge a metade.

Um histograma em escala de cinza com três medições de resumo sobrepostas: o limiar de Otsu (o ponto de corte que melhor separa os agrupamentos escuro e claro), a média e a mediana. Cada medição diz algo diferente sobre a mesma distribuição.

5.18.3. Estatísticas

Um histograma é uma descrição da prevalência de cada valor; estatísticas são os resumos numéricos derivados dele. O objeto statistics retornado por get_statistics() traz o conjunto padrão:

  • mean – a média aritmética dos valores dos pixels.

  • median – o valor abaixo do qual está a metade dos pixels.

  • mode – o valor único mais comum.

  • stdev – o desvio padrão, uma medida da dispersão em torno da média.

  • min e max – os valores de pixel mais claro e mais escuro presentes.

  • lq e uq – os pontos de corte do quartil inferior e superior.

Para uma imagem RGB565, as formas por canal (l_mean, a_median, b_mode, e assim por diante) entregam as mesmas medições canal a canal.

A maioria desses números surge em contextos específicos. mean e stdev juntos dão uma estimativa de ruído: uma cena que deveria ser uniforme tem stdev pequeno, enquanto um sensor ruidoso dá à mesma cena um stdev maior. min e max dão o contraste da imagem: quanto mais próximos estiverem, mais plana é a cena; quanto mais distantes, maior a faixa dinâmica com que o algoritmo tem que trabalhar. median é o centro robusto quando a distribuição tem outliers (alguns poucos pixels muito brilhantes não puxam a mediana da forma como puxam a média). mode é o valor único mais comum, útil para encontrar o nível de fundo de uma imagem cujo fundo cobre a maioria dos pixels.

get_statistics() executa a passagem do histograma internamente e depois o resume; passar os mesmos argumentos thresholds e roi de um histograma computado anteriormente produz as estatísticas para o mesmo conjunto de pixels.

5.18.4. Percentis e consultas de CDF

O objeto histogram expõe um método get_percentile() que transforma uma fração em um valor de pixel – o valor abaixo do qual está a fração solicitada de pixels. h.get_percentile(0.5) é a mediana; h.get_percentile(0.05) e h.get_percentile(0.95) juntos dão um min/max robusto que ignora os 5% inferiores e superiores como outliers.

Essa é a forma que uma aplicação usa quando quer caracterizar a faixa de valores dos pixels sem deixar que um punhado de pixels claros ou escuros isolados distorça a resposta. O min/max robusto dos percentis 5 e 95 também é a entrada natural para uma passagem de alongamento de contraste – o remapeamento por pixel que as Correções tonais abordam.

5.18.5. O método de Otsu

Os histogramas respondem a outra pergunta que vale a pena destacar por si só: dada uma imagem cujos pixels se dividem em um agrupamento “escuro” e um “claro”, qual é o ponto de corte entre eles? A página de limiarização já nomeou o mecanismo por seu resultado – um único limiar global que a aplicação pode entregar a binary() – mas adiou o como. O como é o método de Otsu, e ele vive no histograma.

A intuição: uma imagem com um primeiro plano e um fundo bem definidos tem dois agrupamentos em seu histograma de brilho, com um vale entre eles. O lugar certo para limiarizar é o fundo do vale – o valor onde os dois agrupamentos estão melhor separados. O método de Otsu pesquisa cada ponto de corte possível e escolhe aquele em que as variâncias intra-agrupamento são as menores (o que é o mesmo que dizer que a variância entre agrupamentos é a maior), e o resultado é a divisão binária ideal para a distribuição daquela imagem específica.

O objeto histogram expõe Otsu através de get_threshold:

h = img.get_histogram()
t = h.get_threshold()

O objeto threshold retornado tem atributos value (para escala de cinza) ou l_value / a_value / b_value (para cor) que carregam o ponto de corte escolhido. Alimentar o resultado de volta diretamente em binary() dá um limiar global autoajustável cujo ponto de corte é escolhido pela própria imagem:

img.binary([(t.value, 255)])

Esse padrão não resolve o problema de iluminação desigual que o limiar adaptativo baseado em filtro resolve; o que ele resolve é a pergunta “em que valor devo cortar?” quando a limiarização global já é a abordagem correta. Para uma cena cuja distinção entre primeiro plano e fundo é bem definida, o valor que Otsu escolhe geralmente está dentro de algumas poucas unidades do que um humano escolheria a olho nu.

5.18.6. Computando sobre uma imagem de diferença

Um detalhe útil sobre get_histogram() e get_statistics(): ambos aceitam uma palavra-chave difference que recebe outra Image e computa o histograma (ou as estatísticas) da diferença por pixel entre a fonte e essa imagem, sem alocar uma imagem de diferença separada. Essa é a maneira barata de perguntar “quanto a cena mudou desde o quadro de referência?” sem pagar por uma chamada explícita a difference() para produzir uma imagem cujo único propósito é ser medida. Para um script de detecção de movimento que roda continuamente, a economia se acumula.