Análise de imagens com Python: contando e medindo sementes

23 de nov. de 2024 · Evilasio Anisio

Análise de imagens com Python: contando e medindo sementes

Umas das maiores ameaças dentro de um laboratório são as tarefas monótonas. Repetitivas e longas, são as mais propensas ao erro humano. Entretanto, essas atividades são as mais importantes pois estruturam os dados e garantem a consistência de experimentos.

A contagem de sementes é um exemplo clássico de tarefa repetitiva. Feita manualmente, consome muito tempo, atenção e energia. Felizmente, com Python, é possível transformar esse processo em algo mais rápido, preciso e menos cansativo.

Visão Computacional

Essa é uma área da computação que permite que os computadores “vejam” e interpretem o nosso mundo. Essa é uma tecnologia aplicada em diversos segmentos industriais - é, por exemplo, a base dos carros autônomos.

A tecnologia LiDAR (Light Detection and Ranging) utiliza feixes de laser para medir distâncias e objetos em movimentos em tempo real.

Ela funciona emitindo pulsos de infravermelho, que ricocheteiam nos objetos ao redor e retornam para o sensor. O sensor então mede o tempo que cada pulso demorou para retornar.

bd60838c-7100-4248-b849-b1b70ceacc29
Imagem construída a partir de pulsos gerados pelo sensor LiDAR.

Mas não precisamos de todo esse aparato tecnológico para contar e medir sementes. Precisamos apenas fotografar nossas sementes e aplicar algoritmos de processamento de imagem.

Príncipios da análise de imagens

Tudo começa com a fotografia. Para garantir resultados confiáveis, precisamos ter imagens de alta resolução e um certo controle do ambiente. A iluminação deve ser o mais uniforme possível para evitar sombras que possam confundir nosso algoritmo. O fundo deve ter contraste suficiente para destacar as sementes.

Idealmente, escolhemos uma superfície neutra, de cor branca ou preta, e reta. Dependendo do tamanho dos objetos que queremos selecionar, uma câmera de smartphone é suficiente. Mas podemos utilizar imagens de microscopia também.

Tamanho digital e tamanho real

Calcular as dimensões de um objeto a partir de uma imagem é possível, mas há dois desafios: primeiro, as medidas iniciais serão dadas em pixels, não em unidades reais como centímetros ou milímetros. Segundo, a dimensão calculada pode variar com o ângulo e a distância da câmera em relação ao objeto.

Convertendo Pixels para Unidades Reais

A conversão de pixels para unidades físicas requer uma escala de referência, que pode ser criada usando um objeto de dimensões conhecidas, como uma régua ou marcador, posicionado no mesmo plano do objeto a ser medido. Geralmente, criamos essa referência desenhando um quadrado de 1x1 cm em um dos cantos do fundo. Com isso, podemos calcular a relação entre o número de pixels e a unidade real.

Dcm=Dobjeto (pixels)Dmarcador(pixels)×Dmarcador (cm)D_{cm} = \frac{D_{\text{objeto (pixels)}}}{D_{\text{marcador(pixels)}}} \times D_{\text{marcador (cm)}}

Onde:

  • DcmD_{\text{cm}} é a dimensão do objeto em centímetros.
  • Dobjeto (pixels)D_{\text{objeto (pixels)}} é a dimensão do objeto medida em pixels.
  • Dmarcador (pixels)D_{\text{marcador (pixels)}} é a dimensão do marcador na imagem em pixels.
  • Dmarcador (cm)D_{\text{marcador (cm)}} é a dimensão real do marcador, medida em centímetros.
  • Corrigindo Variações de Ângulo e Distância

    A posição da câmera em relação ao objeto pode causar distorções nas dimensões calculadas. Para resolver isso, utilizam-se duas abordagens: calibração da câmera e correção de perspectiva.

    💡
    Utilizar um equipamento scanner irá garantir que distorções de perspectiva não aconteçam!

    Calibração da Câmera. A calibração determina a geometria da câmera ao medir fatores como distância focal e distorções da lente. O processo usa padrões conhecidos, como um tabuleiro de xadrez, para estabelecer a relação entre o espaço tridimensional (mundo real) e bidimensional (imagem). Ferramentas como OpenCV simplificam essa etapa, corrigindo as distorções e garantindo medições precisas.

    Correção de Perspectiva. Quando o objeto não está paralelo ao plano da câmera, a perspectiva altera suas dimensões aparentes. A solução é aplicar a transformação de homografia, que ajusta a imagem como se o objeto estivesse sendo visto perpendicularmente. Este processo requer pontos de referência ou o conhecimento da posição relativa entre câmera e objeto.

    Prancheta_1_kymqlw
    De modo geral, capture a imagem com a câmera perpendicular ao plano da folha de fundo.

    Para obter medições mais precisas, o ideal é tirar a foto com a câmera alinhada diretamente acima do objeto, sem inclinações. Isso evita distorções e facilita o cálculo das dimensões, reduzindo a necessidade de ajustes complicados depois. No final das contas, sua imagem deve parecer mais ou menos com a de baixo.

    seeds
    Sementes de uma Solanaceae. Utilizaremos esta imagem como amostra para nosso código.

    Biblioteca PlantCV

    PlantCV é um pacote de software de análise de imagem de código aberto, desenvolvido especificamente para ciência vegetal. Esta biblioteca permite medir características de plantas (fenótipos) a partir de imagens. Para melhor aproveitamento, recomenda-se utilizar o PlantCV com Jupyter Notebooks, embora funcione em scripts tradicionais.

    A instalação é direta e pode ser feita com o PyPi.

    python -m pip install plantcv

    Escrevendo o código

    Após instalar a biblioteca, crie uma nova pasta para armazenar todos os arquivos, o que tornará a execução do programa mais simples. Salve tanto a imagem quanto o script Python nesta pasta. Iremos começar importanto a biblioteca e criando uma classe de opções.

    from plantcv import plantcv as pcv
    
    class Options:
        def __init__(self):
            self.image = "images/seed_003.jpeg"  # a localização da imagem
            self.debug = "plot"  # utilizamos "plot" para gerar os gráficos de visualização do Jupyter Notebook
            
    args = Options()
    script.py

    Com a classe de opções carregada, definimos essas variáveis na biblioteca. Em seguida, modificamos alguns parâmetros de visualização dos gráficos para melhorar sua apresentação.

    pcv.params.debug = args.debug
    
    pcv.params.dpi = 300
    pcv.params.text_size = 2
    pcv.params.text_thickness = 2
    pcv.params.line_thickness = 10

    Começaremos carregando nossa imagem, e para isso, utilizaremos a função pcv.readimage()

    img, path, filename = pcv.readimage(
    	filename=args.image, mode="rgba"
    )

    A variável img guarda nossa imagem no espaço de cores RGB. Ao executar essa linha de código, a imagem carregada será exibida. Os eixos x e y do gráfico representam a posição de cada pixel na imagem.

    blog/sementes/e1da2196-eb76-4718-860a-73e87a60c01e

    Quando visualizamos a imagem, percebemos que ela ainda é apenas uma matriz de valores representando a intensidade das cores vermelho, verde e azul (RGB) em cada pixel. Para iniciar o processamento e a contagem das sementes, o primeiro passo é simplificar esses dados.

    Convertendo para Tons de Cinza

    Uma imagem em RGB contém três canais de cor, mas para muitas análises, esses detalhes não são necessários. Podemos converter a imagem para tons de cinza, onde cada pixel é representado por um único valor de intensidade. Isso facilita o processamento sem perder informações importantes sobre os contornos e as formas das sementes.

    O código para realizar essa conversão é simples:

    gray_img = pcv.rgb2gray_lab(rgb_img=img, channel="b")

    A função pcv.rgb2gray_lab permite converter uma imagem no espaço de cores RGB para o espaço de cores LAB e extrair apenas um dos canais LAB: L (luminosidade), A (componentes vermelho-verde) ou B (componentes azul-amarelo). O segundo argumento, channel, define qual desses canais será retornado.

    blog/sementes/a51dd0ee-19ff-4cd9-8c18-75b5d6e50064
    channel=”b”
    blog/sementes/f596a8b2-0f37-420e-8344-130398216870
    channel=”a”
    blog/sementes/4e9a1811-3ba5-4dab-9ba3-a0d4c24691e2
    channel=”l”

    Como podemos observar, o melhor contraste entre as sementes e o fundo é obtido escolhendo o canal B.

    Segmentando a imagem

    Com a imagem convertida para escala de cinza, onde cada pixel possui um valor entre 0 (preto) e 255 (branco), podemos avançar para a segmentação. A segmentação mais simples é feita por meio de limiarização (thresholding). Aqui, definimos um valor-limite: pixels com valores acima desse limite são considerados parte das sementes, enquanto os demais são descartados como fundo. A função pcv.visualize.histogram é especialmente útil para analisar a distribuição dos valores de intensidade de uma imagem ou de um canal específico. Ela gera um histograma que mostra a frequência de ocorrência de cada valor de pixel, o que ajuda a entender o contraste, a iluminação e a segmentação da imagem.

    hist = pcv.visualize.histogram(gray_img)

    No histograma mostrado, os picos mais claros (referentes às sementes) estão em torno de 130 a 135, enquanto os pixels mais escuros estão abaixo disso.

    Assim, um threshold próximo a 133 seria um bom ponto de corte.

    blog/sementes/visualization_dlbrnc

    Aplicaremos a limiarização utilizando a função pcv.threshold.binary, que realiza a separação entre fundo e objeto com base em um valor-limite definido. Essa função converte a imagem em uma binária, onde os pixels abaixo do limiar serão pretos (0), e os acima, brancos (255), destacando as sementes.

    img_threshold = pcv.threshold.binary(
    	gray_img,  # imagem em escala de cinza que será processada
    	threshold=133,  # O valor-limite para separar fundo e objeto (133 neste caso, com base no histograma)
    	object_type="light"  # Define se o objeto é "light" (claro em relação ao fundo) ou "dark" (escuro em relação ao fundo).
    )

    blog/sementes/8dee8403-5e64-45b8-9925-928eeecfbfd7

    Limpando a imagem

    Após realizar a limiarização, podem surgir buracos ou regiões pretas no interior das sementes devido a imperfeições na imagem ou variações de intensidade. Para corrigir isso, utilizaremos a função pcv.fill_holes, garantindo que cada semente seja representada como uma região sólida.

    filled_mask = pcv.fill_holes(img_threshold)

    Após preencher os buracos internos com pcv.fill_holes, é comum encontrar pequenas imperfeições nas bordas ou partículas desconectadas que não fazem parte das sementes. Para suavizar essas irregularidades, utilizaremos a função pcv.fill, que ajuda a eliminar pequenas lacunas e a unificar áreas de interesse.

    filtered_mask = pcv.fill(
    	bin_img=filled_mask,  # imagem binária que será processada
    	size=100  # Partículas com uma área menor que 100 pixels serão apagadas
    )

    Contando as sementes

    Agora que a imagem está refinada, com sementes bem definidas e ruídos eliminados, podemos aplicar a função pcv.create_labels. Essa função identifica e rotula cada objeto na imagem binária, permitindo a contagem e o acompanhamento individual de cada semente.

    seeds_label, n_seeds = pcv.create_labels(mask=filtered_mask)
    print(f"Número total de sementes: {n_seeds}")
    # >>> Número total de sementes: 58
    blog/sementes/66371410-48c3-4039-afa9-029d6caa0c2a

    Analisando tamanho de sementes

    Agora que obtivemos os objetos presentes na imagem, podemos utilizar algumas funções do PlantCV para obter informações interessantes. Uma dessas funções é a pcv.analyze.size, que calcula métricas relacionadas ao tamanho e à forma de cada objeto rotulado.

    pcv.params.sample_label = "seed"  # definimos sample_label para organizar nossas observações. Teremos seed_1, seed_2, ..., seed_58
    shape_img = pcv.analyze.size(
    	img=img, 
    	labeled_mask=seeds_label,  # utilizamos a máscara que obtivemos com pcv.create_labels
    	n_labels=n_seeds  # definimos também a quantidade de objetos na máscara
    )

    Os valores calculados pela função pcv.analyze.size, assim como por outras análises do PlantCV, são armazenados no dicionário pcv.outputs.observations. Esse dicionário organiza as métricas extraídas com base no rótulo de cada objeto analisado, o sample_label (por exemplo, seed_1).

    Acessando os Resultados

    Podemos obter a área, ainda em pixels, de cada semente identificada, iterando sobre os rótulos atribuídos a cada uma. Isso nos dá uma visão detalhada do tamanho de cada semente na imagem.

    Aqui está um exemplo prático usando um loop:

    for i in range(1, n_seeds + 1):
        seed_area_px = pcv.outputs.observations[f"seed_{i}"]["area"]["value"]
        print(f"Semente {i} - Área: {seed_area_px} px²")
    
    # >>> Semente 1 - Área: 532.0 px²
    # >>> Semente 2 - Área: 650.0 px²
    # >>> Semente 3 - Área: 644.0 px²
    # >>> ...
    # >>> Semente 58 - Área: 612.0 px

    A função pcv.analyze.size fornece várias métricas além da área, todas acessíveis pelo dicionário de observações.

  • area - área total ocupada pelo objeto, medida em pixels;
  • center_of_mass - coordenadas do centro de massa do objeto;
  • convex_hull_area - área do menor polígono convexo que pode envolver o objeto;
  • convex_hull_vertices - posição dos vértices que formam o envoltório convexo;
  • ellipse_angle - ângulo de rotação da elipse que melhor se ajusta ao objeto;
  • ellipse_center - coordenadas do centro da elipse ajustada;
  • ellipse_eccentricity - excentricidade da elipse, indica a deformação de um círculo perfeito;
  • ellipse_major_axis - comprimento do eixo maior da elipse ajustada;
  • ellipse_minor_axis - comprimento do eixo menor da elipse ajustada;
  • height - altura do objeto em pixels;
  • in_bounds - indica se o objeto está completamente dentro da imagem;
  • longest_path - o comprimento do caminho mais longo dentro do objeto;
  • object_in_frame - indica se o objeto está visível na imagem;
  • perimeter - comprimento total das bordas do objeto;
  • solidity - proporção entre a área do objeto e a área do seu envoltório convexo;
  • width - largura do objeto em pixels.
  • Convertendo pixels para centimetros com um marcador

    As dimensões obtidas pelas análises até aqui estão em pixels, uma unidade relativa que depende da resolução e da distância da câmera. Para convertê-las para centímetros, utilizamos um marcador de tamanho conhecido presente na imagem. Este marcador serve como referência para calcular a relação entre pixels e a unidade de medida desejada.

    💡
    Escolha um marcador com uma cor diferente dos objetos presentes na imagem, isso irá facilitar sua análise!

    Para isso, começaremos definindo uma área de interesse (region of interest, ou ROI) na nossa imagem original. Esta área é um retângulo que deve encapsular por completo o marcador que adicionamos.

    pcv.params.sample_label = "marker"  # sinalizamos que o objeto a ser analisado não é mais uma semente, mas um marcador
    roi = pcv.roi.rectangle(
    	img=img, 
    	x=0,  # a posição horizontal inicial do ROI
    	y=0,  # a posição vertical inicial do ROI
    	h=200,  # o comprimento do ROI
    	w=200  # a largura do ROI
    )

    Após definir o ROI, utilizamos a função pcv.report_size_marker_area para identificar o marcador e calcular sua área em pixels. Essa função detecta o marcador baseado na cor ou brilho (canal escolhido) e um limiar de segmentação:

    image = pcv.report_size_marker_area(
        img=img, 
        roi=roi, 
        marker='detect',     # Detecta o marcador automaticamente
        bg_color='dark',     # Especifica se o objeto é mais claro (light) ou escuro (dark) que o fundo
        thresh_channel='v',  # Escolhemos o canal para a limiarização. opções: 
    												 # h - hue (matiz): útil para distinguir cores específicas
    												 # s - saturation (saturação): destaca áreas com cores vibrantes
    												 # v - value (brilho): enfatiza áreas claras ou escuras
        thresh=130           # Limiar para segmentação. Similar ao que fizemos para detectar as sementes
    )
    blog/sementes/5efb7a08-21f2-4d11-bb4e-1b58065c83c0
    Marcador identificado com sucesso!

    Calculando a Relação de Conversão

    A partir da área detectada do marcador e seu tamanho físico conhecido, calculamos a relação entre pixels e centímetros. Isso nos permitirá converter dimensões de quaisquer objetos na imagem.

    marker_area_pixels = pcv.outputs.observations['marker']['marker_area']['value']
    marker_real_area_cm2 = 1  # No nosso exemplo, o marcador é um retângulo com 1x1cm
    pixels_per_cm2 = marker_area_pixels / marker_real_area_cm2
    
    # Relacionando áreas em pixels para cm²
    for i in range(1, n_seeds + 1):
        seed_area_px = pcv.outputs.observations[f"seed_{i}"]["area"]["value"]
        seed_area_cm2 = seed_area_px / pixels_per_cm2
        print(f"Semente {i} - Área: {seed_area_cm2:.3f} cm²")
        
    # >>> Semente 1 - Área: 0.025 cm²
    # >>> Semente 2 - Área: 0.030 cm²
    # >>> Semente 3 - Área: 0.030 cm²
    # >>> ...
    # >>> Semente 58 - Área: 0.029 cm²

    Com isso, transformamos dados relativos em informações precisas e utilizáveis em análises biológicas ou agrícolas. A automação da contagem e medição de sementes torna-se, assim, uma ferramenta essencial para otimizar o trabalho no laboratório, aumentando a eficiência e a confiabilidade dos resultados.

    Salvando os Resultados

    Após calcular as áreas das sementes e realizar outras medições, você pode salvar os resultados diretamente em um arquivo CSV utilizando a função do próprio PlantCV. Isso facilita o registro e a análise dos dados em ferramentas externas, como o Excel.

    # Salvar os resultados das observações em um arquivo CSV
    pcv.outputs.save_observations(filename='seeds_results.csv')

    A função save_observations() do PlantCV salva todas as medições e observações feitas, armazenando-as em um arquivo CSV de forma prática e automatizada.

    Assim, você garante que os dados coletados estejam bem organizados e prontos para análises futuras ou relatórios. Mas atenção: as dimensões ainda estarão em pixels! A transformação que fizemos não modificou os resultados no dicionário de observações.


    A automação da contagem e medição de sementes com visão computacional torna o processo mais rápido, preciso e menos propenso a erros. Usando o PlantCV, aprendemos a carregar, segmentar e medir sementes, convertendo dimensões de pixels para unidades reais com um marcador de referência.

    Esse método, embora técnico, oferece uma maneira mais eficiente e confiável de coletar dados, facilitando o nosso trabalho!