Entropia cruzada em imagens

A entropia representa o valor médio da informação (grau de incerteza) associada às probabilidades dos objetos de um evento (variável aleatória discreta ou continua ou processo). Nos processos onde há perda de informação, há uma situação igual aos processos que ganham entropia.

Teoria da Informação

A Teoria da informação é um ramo da teoria da probabilidade e da matemática estatística que lida com sistemas de comunicação, transmissão de dados, criptografia, codificação, teoria do ruído, correção de erros, compressão de dados, etc.

“O problema fundamental da comunicação é reproduzir em um dado ponto, exata ou aproximadamente, uma mensagem produzida em outro ponto.”

Claude Shannon (1916-2001)

O engenheiro e matemático Claude Shannon introduziu conceitos primordiais que deram origem à teoria da informação em 1948, no trabalho “A Mathematical Theory of Communication” (publicado na revista “Bell System Technical Journal”).

No processo de desenvolvimento de uma teoria da comunicação que pudesse ser aplicada por engenheiros eletricistas para projetar sistemas de telecomunicação melhores, Shannon definiu uma medida chamada de entropia. Em Termodinâmica, entropia é a medida do grau de desorganização (aleatoriedade) das partículas em um determinado sistema. Assim, quanto maior a desordem, maior a alteração do estado do sistema e, consequentemente, maior será o grau de entropia.

Entropia

Em Teoria da Informação, a entropia é definida como sendo uma forma de medir o grau médio de incerteza a respeito de fontes de informação, o que consequentemente permite a quantificação da informação presente que flui no sistema. Ou seja, quanto mais incerto é o resultado de um experimento aleatório, maior é a informação que se obtém ao observar a sua ocorrência.

Matematicamente, a entropia da informação é definida como:

\(H(p)=-\sum\limits_{x\in\mathbb{R}}p(x)logp(x)\)

onde o logaritmo é na base 2 e determina o grau de “caoticidade” da distribuição de probabilidade e pode ser usada para determinar a capacidade do canal necessária para transmitir a informação.

Esse modelo teórico para a comunicação envolvendo mensagens digitais parte do pressuposto de que uma mensagem transmitida por um receptor é invariavelmente recebida com ruídos, ou seja, quando uma informação passa por um canal de comunicação ela sofre distorções e chega ao receptor precisando ser submetida a um processo de decodificação. O sinal pode ser recuperado utilizando-se um código corretor de erros adequado.

A entropia cruzada (“Cross-Entropy”) é comumente usada para quantificar a diferença (ou perda) entre duas distribuições de probabilidade (por exemplo, uma verdadeira e outra estimada). Ela é representada por:

\(
H(p,q)=-\sum\limits_{x\in\mathbb{R}}p(x)logq(x)
\)

A minimização de entropia cruzada é frequentemente usada na otimização e na estimativa da probabilidade de eventos raros. Seu valor mínimo é conhecido como Entropia Cruzada Mínima (MCE, Minimum Cross-Entropy).

Li & Lee (1993) propuseram um critério para encontrar o limiar ótimo para distinguir entre o fundo e o primeiro plano (ou seja, binarizada) de uma imagem. O problema de seleção de limiares é resolvido minimizando a entropia cruzada entre a imagem e sua versão segmentada. A entropia cruzada é formulada em uma base pixel a pixel entre as duas imagens e um algoritmo computacionalmente empregando histograma é desenvolvido nesse trabalho, sem fazer suposições a priori sobre a distribuição da população.

A maneira de encontrar esse limite era tentar todos os limites possíveis e depois escolher aquele com a menor entropia cruzada. Até que Li & Tam (1998) implementaram um novo método iterativo para encontrar mais rapidamente o ponto ótimo usando a inclinação da entropia cruzada.

O algoritmo de limiarização MCE seleciona um limiar minimizando a entropia cruzada entre a imagem original e sua imagem segmentada (Li et al., 2011). Após o limiar ótimo ser determinado de acordo, ele é usado para distinguir elementos entre duas classes. Pixels cujos valores de razão normalizados são menores que esse limiar são classificados como uma classe; caso contrário, outra classe.

Por outro lado, o método de Minimum Cross-Entropy Local Thresholding, ou Minimum Cross-Entropy adaptive thresholding, é uma extensão do MCE, mas opera em blocos locais da imagem em vez de aplicar um único limite global. Em vez de calcular um único limite para toda a imagem, ele calcula limites locais em regiões menores da imagem. Isso permite que o método se adapte a variações locais de iluminação, contraste e ruído na imagem.

Os parâmetros comuns nesses métodos incluem:

  • Block Size (Tamanho do Bloco): Refere-se ao tamanho da janela ou bloco usado para calcular os limites locais. Uma imagem é dividida em blocos de tamanho específico, e um limite é calculado para cada bloco. O tamanho do bloco pode afetar a sensibilidade do método a variações locais na imagem. Blocos menores podem capturar detalhes finos, mas também podem ser mais sensíveis ao ruído. Blocos maiores podem suavizar variações locais, mas podem perder detalhes.
  • Constant (Constante): É um parâmetro que controla o ajuste fino dos limites calculados em relação à entropia cruzada. Essa constante pode ser usada para ajustar a sensibilidade do método à variação local de intensidade de pixels na imagem. Um valor mais alto pode levar a limites mais conservadores, enquanto um valor menor pode resultar em limites mais adaptativos.

Em resumo, enquanto o MCE determina um único limite global para toda a imagem, o Minimum Cross-Entropy Local Thresholding calcula múltiplos limites locais, tornando-o mais adaptável a variações locais na imagem. Os parâmetros como tamanho do bloco e constante são ajustados para controlar a sensibilidade e a precisão do método na segmentação adaptativa da imagem.

Histogramas

Os histogramas são contagens coletadas de dados organizados em um conjunto de intervalos predefinidos (“bins”). Por exemplo, uma imagem em tons de cinza, os valores de cada pixel pordem ser organizados em bins de 15 em 15, a partir de 0 até 255 (“range”). Um histograma pode ser apresentado em um gráfico tendo como eixos as classes (horizontal) e número de contagens dentro de cada classe (vertical).

Em python, existe o método “calcHist” dentro do pacote OpenCV que faz os histogramas, inclusive para cada canal do RGB – veja mais sobre o OpenCV e imagens no post Máscara em imagem no Python. Sua sintaxe é “cv2.calcHist(images, channels, mask, histSize, ranges[, hist[, accumulate]])”, onde:

  • images: lista de imagens como matrizes numpy (todas devem ser do mesmo tipo e tamanho);
  • channels: lista dos canais usados para calcular os histogramas;
  • mask: máscara opcional (matriz de 8 bits) do mesmo tamanho da imagem de entrada;
  • histSize: tamanhos de histograma em cada dimensão;
  • ranges: matriz das matrizes de dimensões dos limites do bin do histograma em cada dimensão;
  • hist: histograma de saída;
  • accumulate: sinalizador de acumulação, permite calcular um único histograma a partir de vários conjuntos de matrizes.

Ele retorna uma matriz de pontos de histograma de dtype float32. Veja o exemplo a seguir, usando uma imagem deixada como exemplo na biblioteca “skimage” (data, instalada como “conda install scikit-image”):

import matplotlib.pyplot as plt
import cv2
from skimage import data

# Load image
img = data.camera()
# Compute histogram
H = cv2.calcHist([img],[0],None,[256],[0,256])
# Plot image and histogram
fig, ax = plt.subplots(1, 2, figsize=(6, 3))
ax[0].imshow(img, cmap='gray')
ax[0].set_title('Image')
ax[0].set_axis_off()
ax[1].plot(H, color='k')
ax[1].set_title('Histogram')
fig.tight_layout()
plt.savefig('hist_grayscale.png', bbox_inches='tight')
Imagem e respectivo histograma, gerados pelo script (escala de cinza)
Imagem e respectivo histograma, gerados pelo script (escala de cinza)

A imagem está em escala de cinza e o histograma ao lado mostra o número de pixels para cada valor entre 0 (preto) e 255 (branco). Nota-se que tem um grande número de pontos escuros (abaixo de 50, correspondentes principalmente ao preto da roupa do fotógrafo) e outros dois picos cinzas: um mais claro (céu, por volta de 200) e outro menos (grama, por volta de 150).

Para uma imagem RGB, deve-se fazer um loop para iterar os três canais, conforme segue:

import numpy as np
import matplotlib.pyplot as plt
import cv2
from skimage import data

def create_circular_mask(img):
    '''
    cv2.circle(image, center_coordinates, radius, color, thickness)
    '''
    mask = np.zeros_like(img)
    mask = cv2.cvtColor(mask, cv2.COLOR_BGR2GRAY)
    hh, ww = img.shape[:2]
    xc = hh // 2
    yc = ww // 2
    r = (xc+yc) // 2
    mask = cv2.circle(mask, (xc,yc), r, (255,255,255), -1)
    #cv2.imwrite('mask.png', mask)
    return mask

# Load image
img = data.retina()
# Create circular mask
mask = create_circular_mask(img)
# Plot image
fig, ax = plt.subplots(1, 2, figsize=(6, 3))
ax[0].imshow(img)
ax[0].set_title('Image')
ax[0].set_axis_off()
# Define colors to plot the histograms
colors = ('r','g','b')
for i,color in enumerate(colors):
    # Compute and plot histograms
    hist = cv2.calcHist([img],[i],mask,[256],[0,256])
    ax[1].plot(hist,color = color)
ax[1].set_title('Histogram')
fig.tight_layout()
plt.savefig('hist_rgb.png', bbox_inches='tight')
Imagem e respectivo histograma, gerados pelo script (cores RGB)
Imagem e respectivo histograma, gerados pelo script (cores RGB)

Considerando somente a variação em um canal, quanto menor o número, mais escura a cor fica, e quanto maior, mais sólida ela fica. Note que a cor mais sólida (com maiores valores) é no canal do vermelho, o que é intuitivo observando-se a figura. Para esse tipo de leitura, foram retirados os pixels pretos ao redor da imagem usando uma máscara circular – caso contrário, apareceriam muitas contagens em zero devido ao contorno preto original da imagem.

Obs.: caso carregue uma imagem RGB de um arquivo usando o método “imread”, o OpenCV trata as imagens como BGR, então deve-se usar o também o método cvtColor para converter para RGB.

Script de MCE

Dentro da parte de filtros do pacote scikit-image (módulo “skimage.filters”), o método “thresholding” permite calcular a entropia cruzada através da função “_cross_entropy”. O script a seguir primeiro calcula um vetor com limiares a partir dos valores máximo e mínimo de tons de cinza da imagem carregada. Juntamente com a própria imagem, esses valores entram como argumento para a função que calcula a entropia cruzada. Por fim, “np.argmin” calcula o menor valor do vetor de entropias calculadas, definindo o limiar ótimo para a segmentação.

import numpy as np
import matplotlib.pyplot as plt
from skimage import data
from skimage import filters
from skimage.filters.thresholding import _cross_entropy

camera = data.camera()
thresholds = np.arange(np.min(camera) + 1.5, np.max(camera) - 1.5)
entropies = [_cross_entropy(camera, t) for t in thresholds]
optimal_camera_threshold = thresholds[np.argmin(entropies)]

O valor obtido é a mínima entropia cruzada. No entanto, ele obtida por “força bruta”, já que o método “np.argmin” é muito custoso. O método “threshold_li” calcula o valor do limiar pelo método iterativo de Entropia Cruzada Mínima de Li & Tam (1998). Como parâmetros de entrada, ela recebe:

  • image (imagem) – obrigatório, funciona com imagens em escala de cinza ou RGB;
  • tolerance (tolerância) – indica quando concluir o cálculo quando a alteração no limite em uma iteração for menor que esse valor – por padrão, essa é a metade da menor diferença entre os valores de intensidade na imagem;
  • initial_guess (palpite inicial) – o método iterativo usa gradiente descendente para encontrar o limiar ideal. Se o histograma de intensidade da imagem contiver mais de dois modos (picos), a descida do gradiente pode ficar presa em um ótimo local (em vez do global). Uma estimativa inicial para a iteração pode ajudar o algoritmo a encontrar o limite globalmente ótimo. Um valor float define um ponto inicial específico, enquanto um “callable” (objeto semelhante a uma função) deve receber uma matriz de intensidades de imagem e retornar um valor float.
  • iter_callback – uma função que será chamada no limite a cada iteração do algoritmo.

O método retorna o valor limite superior – todos os pixels com uma intensidade superior a este valor são considerados em primeiro plano, ou seja, verdadeiros. Como chute inicial, por padrão é dado a média, mas é possível definir uma função, como a “percentil_95” em que 95% da imagem tem valor menor do que o do valor em questão. Essa pode ser uma boa opção quando muitas imagens com intervalos diferentes precisam ser processadas. Veja como fica o script:

#!/usr/bin/env python3.9.12
# -*- Coding: UTF-8 -*-

import numpy as np
import matplotlib.pyplot as plt
import cv2
from skimage import data
from skimage import filters
from skimage.filters.thresholding import _cross_entropy

def quantile_95(image):
    return np.percentile(image, 95)

# Load image
img = data.camera()
# Create thresholds and entropies axis
thresholds = np.arange(np.min(img) + 1.5, np.max(img) - 1.5)
entropies = [_cross_entropy(img, t) for t in thresholds]
# Calculate optimum threshold
iter_thresholds = []
opt_threshold = filters.threshold_li(img, initial_guess=quantile_95,
                                      iter_callback=iter_thresholds.append)
iter_entropies = [_cross_entropy(img, t) for t in iter_thresholds]
print(len(iter_thresholds), 'examined, optimum:', opt_threshold)

fig, ax = plt.subplots(1, 3, figsize=(8, 3))
# Plot original image
ax[0].imshow(img, cmap='gray')
ax[0].set_title('image')
ax[0].set_axis_off()
# Plot thresholded image
ax[1].imshow(img > opt_threshold, cmap='gray')
ax[1].set_title('thresholded')
ax[1].set_axis_off()
# Plot all entropies and thresholds
ax[2].plot(thresholds, entropies)
ax[2].set_title('optimal threshold')
ax[2].set_xlabel('threshold')
ax[2].set_ylabel('cross entropy', color='C0')
ax[2].tick_params(axis='y', labelcolor='C0')
# Plot optimization path
ax[2].plot(iter_thresholds, iter_entropies)
ax[2].scatter(iter_thresholds, iter_entropies, c='C1')
# Plot optimal threshold
ax[2].vlines(opt_threshold,
             ymin=np.min(entropies) - 0.05 * np.ptp(entropies),
             ymax=np.max(entropies) - 0.05 * np.ptp(entropies))
# Save final figure
fig.tight_layout()
plt.savefig('mce_camera.png', bbox_inches='tight')
Imagem original, imagem com limiar e gráfico com valores de entropia cruzada em função de cada limiar (limiar ótimo assinalado com reta vertical; "caminho de otimização" em laranja)
Imagem original, imagem com limiar e gráfico com valores de entropia cruzada em função de cada limiar (limiar ótimo assinalado com reta vertical; “caminho de otimização” em laranja)

Para treinar uma rede neural, é necessário encontrar o erro entre as saídas calculadas e as saídas alvo desejadas. A medida de erro mais comum é chamada de erro quadrático médio (“Mean Square Error”). No entanto, alguns trabalhos sugerem o uso de entropia cruzada. Ela pode ser usada como uma medida de erro quando as saídas de uma rede podem ser pensadas como representando hipóteses independentes e as ativações dos nós podem ser entendidas como representando a probabilidade que cada uma das hipóteses pode ser verdadeira. Nesse caso, o vetor de saída representa uma distribuição de probabilidade, e a medida de erro (entropia cruzada) indica a distância entre o que a rede acredita que essa distribuição deve ser e o que realmente deveria ser.

Fontes

Compartilhe :)

Leave a Reply

O seu endereço de e-mail não será publicado. Campos obrigatórios são marcados com *

Esse site utiliza o Akismet para reduzir spam. Aprenda como seus dados de comentários são processados.