Binarização Adaptativa de Imagens com o Algoritmo de Otsu em Python

Dominando a Binarização de Imagens com o Algoritmo de Otsu

No vasto campo do processamento de imagens, a binarização é uma etapa fundamental, especialmente para tarefas como reconhecimento de caracteres, segmentação de objetos ou análise de documentos. O desafio comum reside na escolha de um limiar adequado que consiga separar de forma eficaz o objeto de interesse do fundo da imagem. Ajustar esse valor manualmente é inviável para grandes volumes de dados ou em condições de iluminação variáveis. Limiares fixos, por sua vez, falham miseravelmente quando as características da imagem mudam.

É aqui que o Algoritmo de Otsu, também conhecido como método da máxima variância interclasses, emerge como uma solução elegante e automática. Longe de ser uma técnica misteriosa, o Otsu é um método estatístico adaptativo que determina um limiar global ideal para a binarização. Para desenvolvedores que lidam com digitalizações de documentos, imagens microscópicas de células ou inspeção industrial, dominar o Otsu significa resolver um problema central de pré-processamento em questão de minutos, com poucas linhas de código. Neste artigo, focaremos na implementação prática em Python e OpenCV, desvendando seu uso sem nos aprofundarmos nas complexas derivações matemáticas.

O Princípio Fundamental do Algoritmo de Otsu

Imagine uma imagem em tons de cinza, onde a intensidade dos pixels varia de 0 (preto puro) a 255 (branco puro). Nosso objetivo é "dividir" essa imagem em duas regiões distintas: uma representando o primeiro plano (o objeto de interesse) e outra o fundo. Essa "divisão" é definida por um valor de limiar. Pixels com intensidade abaixo do limiar se tornam pretos (0), e aqueles acima se tornam brancos (255) – esse é o processo de binarização de imagem.

A questão crucial é: qual é o melhor valor para esse limiar?

A genialidade do algoritmo de Otsu reside em sua abordagem. Em vez de adivinhações, ele permite que os dados da imagem falem por si. Sua ideia central é simples: encontrar um limiar que maximize a diferença (ou variância) entre as duas classes de pixels resultantes (objeto e fundo). Quanto maior a "variância interclasses", mais distintas e homogêneas internamente serão as duas categorias, indicando uma separação mais eficaz.

É importante notar que Otsu é um método de limiar global. Ele assume que um único limiar pode segmentar toda a imagem de forma eficiente. Isso o torna excepcionalmente eficaz para imagens com iluminação uniforme e bom contraste entre o primeiro plano e o fundo, como documentos digitalizados ou certas imagens médicas.

Para visualizar o problema que o Otsu resolve, observe um histograma típico de tons de cinza. Uma imagem ideal para o processamento com Otsu geralmente exibirá uma distribuição bimodal – ou seja, dois picos distintos. Um pico representa o fundo (geralmente mais claro ou mais escuro), e o outro representa o primeiro plano.


import cv2
import numpy as np
import matplotlib.pyplot as plt

# Gerar uma imagem simulada com distribuição bimodal
np.random.seed(42)
dados_fundo = np.random.normal(loc=70, scale=10, size=(200, 300)).astype(np.uint8) # Fundo, média 70
dados_objeto = np.random.normal(loc=180, scale=15, size=(100, 200)).astype(np.uint8) # Objeto, média 180

imagem_bimodal = np.zeros((300, 500), dtype=np.uint8)
imagem_bimodal[50:250, 100:400] = dados_fundo
imagem_bimodal[150:250, 200:400] = dados_objeto

# Calcular e plotar o histograma
histograma, bins = np.histogram(imagem_bimodal.flatten(), 256, [0, 256])

plt.figure(figsize=(10, 4))
plt.subplot(1, 2, 1)
plt.imshow(imagem_bimodal, cmap='gray')
plt.title('Imagem Bimodal Simulada')
plt.axis('off')

plt.subplot(1, 2, 2)
plt.plot(histograma, color='black')
plt.title('Histograma de Intensidade (Picos Distintos)')
plt.xlabel('Intensidade do Pixel')
plt.ylabel('Contagem de Pixels')
plt.tight_layout()
plt.show()

Ao executar o código acima, você observará a imagem e seu histograma. A presença de dois "montes" ou picos no histograma é nítida. A missão do algoritmo de Otsu é identificar o "vale" entre esses picos, definindo-o como o limiar ótimo. Este ponto garante que os pixels dentro de cada classe sejam o mais semelhantes possível (baixa variância intraclasse) e que as classes sejam o mais diferentes possível entre si (alta variância interclasses).

Construindo o Algoritmo de Otsu do Zero

Com a compreensão conceitual estabelecida, vamos ver como traduzir essa ideia em código. Embora bibliotecas como OpenCV e Scikit-image ofereçam implementações prontas do Otsu, codificá-lo manualmente proporciona uma compreensão mais profunda de seu funcionamento interno. O processo envolve avaliar todos os possíveis valores de intensidade (de 0 a 255) como candidatos a limiar e selecionar aquele que otimiza a separação das classes.

Passos Detalhados para o Cálculo do Limiar

O fluxo do algoritmo pode ser dividido nas seguintes etapas:

  1. Cálculo do Histograma: Determinar a frequência de cada nível de intensidade de cinza (0-255) na imagem.
  2. Normalização para Probabilidades: Converter as contagens do histograma em probabilidades, dividindo pelo número total de pixels da imagem.
  3. Iteração de Limiares: Percorrer todos os possíveis valores de limiar T (de 0 a 254). Para cada T:
    • Determinar Pesos de Classe: O limiar T divide os pixels em duas classes. A classe 0 (pixels <= T) terá um peso w0 (probabilidade cumulativa até T). A classe 1 (pixels > T) terá um peso w1 (1 - w0).
    • Calcular Médias de Classe: Determinar a média de intensidade mu0 para a classe 0 e mu1 para a classe 1.
    • Calcular Variância Interclassses: Usar a fórmula simplificada sigma_b_squared = w0 * w1 * (mu0 - mu1)**2.
    • Atualizar Limiar Ótimo: Se a sigma_b_squared calculada for maior que a máxima já registrada, este T se torna o novo limiar ótimo.
  4. Retornar Limiar Selecionado: O valor de T que resultou na maior variância interclasses.

Implementação em Python com Explicação Linha a Linha

A seguir, apresentamos nossa implementação do algoritmo de Otsu em Python, com comentários detalhados.


import numpy as np

def calcular_limiar_otsu(imagem_tons_cinza):
    """
    Calcula o limiar ótimo de uma imagem em tons de cinza usando o algoritmo de Otsu.
    
    Parâmetros:
    imagem_tons_cinza : array NumPy
        A imagem de entrada em tons de cinza (numpy.uint8).
        
    Retorna:
    int
        O limiar ótimo calculado pelo método de Otsu.
    """
    
    # 1. Obter o histograma da imagem
    # np.histogram é mais flexível que cv2.calcHist para este propósito.
    histograma_counts, bins = np.histogram(imagem_tons_cinza.flatten(), bins=256, range=[0, 256])
    
    # Normalizar o histograma para obter as probabilidades de cada nível de intensidade
    total_pixels = imagem_tons_cinza.size # Número total de pixels na imagem
    probabilidades = histograma_counts / total_pixels
    
    # Inicializar variáveis para armazenar a máxima variância interclasses e o limiar correspondente
    max_variancia_interclasses = 0.0
    limiar_otsu_otimo = 0
    
    # Calcular as somas cumulativas de probabilidades e médias de intensidade
    # Isso otimiza o cálculo repetitivo dentro do loop
    p_cum_soma = np.cumsum(probabilidades) # P0(T) = soma(probabilidades[0]...probabilidades[T])
    
    # Média cumulativa de intensidade: sum(i * P(i))
    # Para calcular a média da classe: (sum(i * P(i))) / P(T)
    i_multi_p = np.arange(256) * probabilidades
    media_cum_soma = np.cumsum(i_multi_p) # M0(T) = soma(i * probabilidades[i]) para i de 0 a T
    
    # Média global de intensidade da imagem
    media_global = media_cum_soma[255] # Corresponde a M(255)
    
    # 2. Iterar sobre todos os possíveis valores de limiar T (de 0 a 255)
    # A iteração vai até 255, mas para calcular as duas classes, o limiar T
    # precisa estar entre 0 e 254 para que ambas as classes (fundo e objeto)
    # tenham pixels (probabilidade não zero).
    for T in range(255): # O limiar T pode ser de 0 a 254
        
        # Pesos das classes
        peso_classe0 = p_cum_soma[T] # w0(T): probabilidade de pixels na classe 0 (<= T)
        peso_classe1 = 1.0 - peso_classe0 # w1(T): probabilidade de pixels na classe 1 (> T)
        
        # Evitar divisão por zero caso uma das classes esteja vazia
        if peso_classe0 == 0 or peso_classe1 == 0:
            continue
            
        # Médias das classes
        # mu0(T) = M0(T) / P0(T)
        media_classe0 = media_cum_soma[T] / peso_classe0 
        
        # mu1(T) = (Media_Global - M0(T)) / (1 - P0(T)) = (M(255) - M0(T)) / w1(T)
        media_classe1 = (media_global - media_cum_soma[T]) / peso_classe1
        
        # Calcular a variância interclasses (sigma_b_squared)
        # Formula: w0 * w1 * (mu0 - mu1)**2
        variancia_interclasses_atual = peso_classe0 * peso_classe1 * ((media_classe0 - media_classe1) ** 2)
        
        # 3. Atualizar o limiar ótimo se a variância atual for maior
        if variancia_interclasses_atual > max_variancia_interclasses:
            max_variancia_interclasses = variancia_interclasses_atual
            limiar_otsu_otimo = T
            
    return limiar_otsu_otimo

# --- Exemplo de Uso ---
# Carregar uma imagem de teste (ou usar a imagem_bimodal gerada anteriormente)
# Para fins de demonstração, usaremos a imagem_bimodal
imagem_teste = imagem_bimodal # Use a imagem simulada

# Calcular o limiar usando nossa função Otsu personalizada
limiar_personalizado = calcular_limiar_otsu(imagem_teste)
print(f"Limiar de Otsu calculado manualmente: {limiar_personalizado}")

# Binarizar a imagem usando o limiar calculado
imagem_binarizada_personalizada = (imagem_teste > limiar_personalizado).astype(np.uint8) * 255

# Comparar com a implementação de Otsu do OpenCV
# Nota: cv2.threshold retorna o limiar encontrado como 'ret'
ret, imagem_binarizada_opencv = cv2.threshold(imagem_teste, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)
print(f"Limiar de Otsu calculado pelo OpenCV: {int(ret)}")

# Exibir os resultados
plt.figure(figsize=(15, 5))

plt.subplot(1, 3, 1)
plt.imshow(imagem_teste, cmap='gray')
plt.title('Imagem Original')
plt.axis('off')

plt.subplot(1, 3, 2)
plt.imshow(imagem_binarizada_personalizada, cmap='gray')
plt.title(f'Binarizada (Otsu Customizado: {limiar_personalizado})')
plt.axis('off')

plt.subplot(1, 3, 3)
plt.imshow(imagem_binarizada_opencv, cmap='gray')
plt.title(f'Binarizada (OpenCV Otsu: {int(ret)})')
plt.axis('off')

plt.tight_layout()
plt.show()

Tags: otsu BinarizaçãoDeImagens ProcessamentoDeImagens Python OpenCV

Publicado em 7-16 06:40