Projeto Determinação de Tamanho de Grãos

Um projeto da IEEE Computer Society UFABC + Electronics Packaging Society UFABC

Esse projeto consiste na utilização de técnicas de Processamento de Imagens para realizar a determinação de tamanho de grão para imagens de micrografia de metais. Essa tarefa é atualmente realizada manualmente por todos os estudantes de Engenharia na UFABC, em especial os de Engenharia de Materias. O objetivo final é a criação de um site que realize essa tarefa automaticamente e com precisão.


Grain Size Analysis Project

IEEE Computer Society UFABC + IEEE Electronics Packaging Society UFABC

This project utilizes Digital Image Processing techniques to perform grain size analysis in metal micrograph images. This task is currently done manually by Engineering Students in the Federal University of ABC (UFABC), especially Material Engineering students. The final objective is creating a website that performs this task precisely and automatically.


Para utilizar este projeto, clique em show widgets e aguarde.

Click show widgets and wait.

#nbi:hide_in
#Bibliotecas
import cv2
import numpy as np
import matplotlib.pyplot as plt

from io import BytesIO
from PIL import Image
from IPython.display import FileLink

import ipywidgets as widgets
from ipywidgets import interact
import nbinteract as nbi
import time

show = True
binder = True
n_real = 0
#nbi:hide_in
def getCircle(n):
    '''kernel has size NxN'''
    # xx and yy are 200x200 tables containing the x and y coordinates as values
    # mgrid is a mesh creation helper
    xx, yy = np.mgrid[:n,:n]
    # circles contains the squared distance to the (100, 100) point
    # we are just using the circle equation learnt at school
    circle = (xx - np.floor(n/2)) ** 2 + (yy - np.floor(n/2)) ** 2
    circle = circle<=np.max(circle)*.5
    circle = np.uint8(circle)
    return circle

#Função de processamento principal
def processing(img, parameters):
    '''
    parameters:
    0 thresholdType = cv2.ADAPTIVE_THRESH_MEAN_C or cv2.ADAPTIVE_THRESH_GAUSSIAN_C
    1 blockSize = 99-299
    2 constant = 0-20
    3 kernelSize = [3-7]
    4 openingIt = 0-2
    5 erosionIt = 0-5
    6 contourMethod = cv2.CHAIN_APPROX_SIMPLE or cv2.CHAIN_APPROX_TC89_L1 or cv2.CHAIN_APPROX_TC89_KCOS 
    7 minArea = 15-50
    
    return img_contours, img_borders, img_colored, resultado, erro
    '''
    #Default parameters
    if parameters == None:
        parameters = [0,199,3,3,0,3,0,20]
    
    #Transformação do vetor de parâmetros em variáveis com nomes informativos
    if parameters[0] == 0: thresholdType = cv2.ADAPTIVE_THRESH_MEAN_C
    if parameters[0] == 1: thresholdType = cv2.ADAPTIVE_THRESH_GAUSSIAN_C
    blockSize =  parameters[1]
    constant =   parameters[2]
    kernelSize = parameters[3]
    openingIt =  parameters[4]
    erosionIt =  parameters[5]
    if parameters[6] == 0: contourMethod = cv2.CHAIN_APPROX_SIMPLE
    if parameters[6] == 1: contourMethod = cv2.CHAIN_APPROX_TC89_L1
    if parameters[6] == 2: contourMethod = cv2.CHAIN_APPROX_TC89_KCOS
    minArea =    parameters[7]
    
    #Conversão de uma imagem para outro sistema de cores
    img_gray = cv2.cvtColor(img,cv2.COLOR_BGR2GRAY)

    #Adaptivo
    img_thresh = cv2.adaptiveThreshold(img_gray, 255, thresholdType, cv2.THRESH_BINARY, blockSize, constant) 

    #Fechamento   
    kernel = getCircle(kernelSize)
    img_open = cv2.morphologyEx(img_thresh,cv2.MORPH_CLOSE,kernel, iterations = openingIt)
    img_open = cv2.erode(img_open, kernel, iterations=erosionIt)

    #Desenhando borda na imagem
    y,x = img_open.shape
    color = 0
    img_open[:,   0] = 0; img_open[:, x-1] = 0; img_open[0,   :] = 0; img_open[y-1, :] = 0

    #Gerando Lista de Contornos
    cv2MajorVersion = cv2.__version__.split(".")[0]
    if int(cv2MajorVersion) >= 4:
        contours, _= cv2.findContours(img_open,cv2.RETR_EXTERNAL, contourMethod)
    else:
        _, contours, _ = cv2.findContours(img_open,cv2.RETR_EXTERNAL, contourMethod)

    #Ordenando Lista de Contornos de acordo com a área
    contours = sorted(contours, key = cv2.contourArea, reverse = True)

    #Selecionando apenas contornos cuja área é maior que algum valor
    contours = [c for c in contours if cv2.contourArea(c)>minArea]

    #Desenhando Contornos na imagem original
    verde = (0,255,0)
    img_contours = cv2.drawContours(img.copy(), contours, -1, verde, 3)

    #Separando grãos das bordas
    faixa = 3
    n_borda = 0
    img_borders = np.int32(np.ones(img.shape))
    red = [0,255,0]
    blue = [255,0,0]
    for c in contours:
        (x_ini,y_ini,w,h) = cv2.boundingRect(c)
        x_end = x_ini+w; y_end = y_ini+h
        y_img, x_img = img_thresh.shape

        if 0<x_ini<faixa or 0<y_ini<faixa or x_img-faixa<x_end<x_img or y_img-faixa<y_end<y_img:
            n_borda +=1
            random_red = [np.random.randint(20, 235) for i in range(3)]
            random_red[0] = 255
            img_borders = cv2.fillPoly(img_borders, [c], random_red)
        else:
            random_blue = [np.random.randint(20, 235) for i in range(3)]
            random_blue[1] = 255
            img_borders = cv2.fillPoly(img_borders, [c], random_blue)

    #Preenchendo contornos
    img_colored = np.int32(np.ones(img.shape))
    img_out = img.copy()
    for c in contours:
        random_color = [np.random.randint(20, 235) for i in range(3)]
        img_colored = cv2.fillPoly(img_colored, [c], random_color)
        img_out = cv2.drawContours(img_out, [c], -1, random_color, 3)
    
    #resultados
    resultado = len(contours)-round(n_borda/2)
    
    return img_contours, img_borders, img_colored, img_out, resultado

Faça upload da imagem desejada.

Upload desired image.

#nbi:hide_in
#Upload da imagem
uploader = widgets.FileUpload()
display(uploader)

Marque a caixa abaixo para conferir que a imagem selecionada foi corretamente enviada.

Tick the box to make sure the image was correctly uploaded.

#nbi:hide_in
#Conversão da imagem e display inicial
img = np.ones([5,5])
def convert(mostrar):
    global img
    if mostrar:
        [uploaded_file] = list(uploader.value)
        binary = uploader.value[uploaded_file]['content']
        img = Image.open(BytesIO(binary))
        img = np.asarray(img)
        
        plt.figure(figsize=(10,10))
        plt.axis('off')
        plt.imshow(img, 'gray')
        return plt.show()
interact(convert, mostrar=False);

Marque a caixa abaixo para obter os resultados.

Tick the box for the results.

#nbi:hide_in
#Processamento principal
img_out = img.copy()
parametros = [1, 191, 3, 5, 0, 2, 0, 41]
def results(mostrar, avancado):
    global img,img_out
    if mostrar and not avancado:
        img_contours, img_borders, img_colored, img_out, resultado = processing(img, parametros)
        
        print("Resultado = {} grãos contados".format(resultado))
        if n_real>0: print("Erro = {} grãos".format(erro))
        
        plt.figure(figsize=(10,10))
        plt.axis('off')
        plt.imshow(img_out, 'gray')
        return plt.show()
    elif mostrar and avancado:
        img_contours, img_borders, img_colored, img_out, resultado = processing(img, parametros)
        
        print("Resultado = {} grãos contados".format(resultado))
        print("Parâmetros =", parametros)
        if n_real>0: print("Erro = {} grãos".format(erro))
        
        plt.figure(figsize=(20,20))
        plt.subplot(221).axis('off'); fig=plt.imshow(img_contours,'gray')
        plt.subplot(222).axis('off'); fig=plt.imshow(img_borders, 'gray')
        plt.subplot(223).axis('off'); fig=plt.imshow(img_colored, 'gray')
        plt.subplot(224).axis('off'); fig=plt.imshow(img_out,     'gray')
        plt.subplots_adjust(hspace = -0.25, wspace = 0.05, bottom = 0.1)
        return plt.show()
interact(results, mostrar=False, avancado=False);

Se desejar, realize o download da clicando com o botão direito.

If you wish, download the result image with right-click

#nbi:hide_in
#nbi:hide_out

#Donload da imagem
def download(switch):
    if switch:
        filename = './img_out.jpg'
        cv2.imwrite(filename,img_out)
        local_file = FileLink(filename, result_html_prefix="Click here to download: ")
        return local_file
    else:
        return 0
interact(download, switch=(0,1));

O código utilizado no processamento é mostrado abaixo:

The code is shown below:

Primeiramente a imagem é transformada em escala de cinza.

#Abrindo imagem
img = plt.imread('data/aço 1010 50x - corrigido (1).jpg')
print(img.shape)

#Conversão de uma imagem para outro sistema de cores
img_gray = cv2.cvtColor(img,cv2.COLOR_BGR2GRAY)

#Visualização de imagem individual
if show: plt.figure(figsize=(10,10)); plt.title("img_gray"); fig = plt.imshow(img_gray, 'gray')
(1342, 1600, 3)

Depois, a imagem passa por um filtro de limiarização que a tranforma em uma imagem em preto e branco.

Este é um filtro Adaptivo, o que significa que ele calcula o melhor nível de cinza para fazer a distinção entre o preto e o branco, para cada região da imagem.

O objetivo aqui é que cada grão seja uma célula branca, e as bordas pretas entre os grãos sejam preservadas.

#Adaptivo
img_thresh = cv2.adaptiveThreshold(img_gray, 255, cv2.ADAPTIVE_THRESH_MEAN_C, cv2.THRESH_BINARY, 199, 3) 
print("dtype=", img_thresh.dtype)
if show: plt.figure(figsize=(10,10)); plt.title("img_thresh"); fig = plt.imshow(img_thresh, 'gray')
dtype= uint8

Depois, um filtro chamado Erosão é aplicado para que as bordas pretas sejam realçadas.

#Fechamento
def getCircle(n):
    '''kernel has size NxN'''
    # xx and yy are 200x200 tables containing the x and y coordinates as values
    # mgrid is a mesh creation helper
    xx, yy = np.mgrid[:n,:n]
    # circles contains the squared distance to the (100, 100) point
    # we are just using the circle equation learnt at school
    circle = (xx - np.floor(n/2)) ** 2 + (yy - np.floor(n/2)) ** 2
    circle = circle<=np.max(circle)*.5
    circle = np.uint8(circle)
    return circle

kernel = getCircle(3)
img_open = cv2.morphologyEx(img_thresh,cv2.MORPH_CLOSE,kernel, iterations = 0)
img_eroded = cv2.erode(img_open, kernel, iterations=3)
print("dtype=", img_eroded.dtype)

if show: plt.figure(figsize= (20,20))
if show: plt.subplot(121); plt.title('img_thresh'); fig=plt.imshow(img_thresh, 'gray')
if show: plt.subplot(122); plt.title('img_eroded'); fig=plt.imshow(img_eroded, 'gray')
dtype= uint8
#nbi:hide_in
#nbi:hide_out
#Desenhando borda na imagem
y,x = img_eroded.shape
print(img_eroded.dtype)
color = 0
img_eroded[:,   0] = 0; img_eroded[:, x-1] = 0; img_eroded[0,   :] = 0; img_eroded[y-1, :] = 0

if show: plt.figure(figsize= (10,10))
if show: plt.subplot(121); plt.title('img_eroded'); fig = plt.imshow(img_eroded[:100,:100], 'gray')
if show: plt.subplot(122); plt.title('img_eroded'); fig = plt.imshow(img_eroded[1200:,1400:], 'gray')
uint8

Finalmente, são gerados os contornos - conjuntos de pontos que representam os grãos.

A partir desses contornos, são feitos cálculos de área mínima para serem considerados como grãos.

A seguir pode-se ver todos os contornos individualmente desenhados sobre a imagem original.

#Gerando Lista de Contornos
cv2MajorVersion = cv2.__version__.split(".")[0]
if int(cv2MajorVersion) >= 4:
    contours, _= cv2.findContours(img_eroded,cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
else:
    _, contours, _ = cv2.findContours(img_eroded,cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)

#Ordenando Lista de Contornos de acordo com a área
contours = sorted(contours, key = cv2.contourArea, reverse = True)
print("n_contours_before =", len(contours))

#Selecionando apenas contornos cuja área é maior que algum valor
min_area = 20.0
contours = [c for c in contours if cv2.contourArea(c)>min_area]
print("n_contours_after =", len(contours))

#Desenhando Contornos na imagem original
verde = (0,255,0)
img_out = cv2.drawContours(img.copy(), contours, -1, verde, 3)

if show: plt.figure(figsize= (20,20))
if show: plt.subplot(121); plt.title('img_thresh'); fig=plt.imshow(img_thresh, 'gray')
if show: plt.subplot(122); plt.title('img_out'); fig=plt.imshow(img_out, 'gray')
n_contours_before = 431
n_contours_after = 234

No método aqui implementado, os grãos das bordas são contados pela metade.

Abaixo, os grãos das bordas são identificados e demarcados de cor diferente.

#Contagem de grãos na borda
faixa = 3
n_borda = 0
img_colored = np.int32(np.ones(img.shape))
red = [0,255,0]
blue = [255,0,0]
for c in contours:
    (x_ini,y_ini,w,h) = cv2.boundingRect(c)
    x_end = x_ini+w; y_end = y_ini+h
    y_img, x_img = img_thresh.shape
    
    if 0<x_ini<faixa or 0<y_ini<faixa or x_img-faixa<x_end<x_img or y_img-faixa<y_end<y_img:
        n_borda +=1
        random_red = [np.random.randint(20, 235) for i in range(3)]
        random_red[0] = 255
        img_colored = cv2.fillPoly(img_colored, [c], random_red)
    else:
        random_blue = [np.random.randint(20, 235) for i in range(3)]
        random_blue[1] = 255
        img_colored = cv2.fillPoly(img_colored, [c], random_blue)
print(n_borda)
if show: plt.figure(figsize=(20,20)); plt.title("img_colored"); fig = plt.imshow(img_colored, 'gray')
53

Para fins de visualização, cada grão recebe uma cor aleatória abaixo.

#Preenchendo contornos
img_colored = np.int32(np.ones(img.shape))
img_out = img.copy()
for c in contours:
    random_color = [np.random.randint(20, 235) for i in range(3)]
    img_colored = cv2.fillPoly(img_colored, [c], random_color)
    img_out = cv2.drawContours(img_out, [c], -1, random_color, 3)
    
if show: plt.figure(figsize=(20,20)); plt.title("img_colored"); fig = plt.imshow(img_colored, 'gray')
#Visualizando novamente
plt.figure(figsize= (20,20))
plt.subplot(121); plt.title('img'); fig=plt.imshow(img[-800:,-800:], 'gray')
plt.subplot(122); plt.title('img_out'); fig=plt.imshow(img_out[-800:,-800:], 'gray')

Por fim, os resultados são calculados.

#nbi:hide_in
print("Número de contornos =", len(contours))
print("Número de contornos nas bordas =", n_borda)

resultado = len(contours)-round(n_borda/2)
print("Resultado =", resultado, "grãos")
#erro = abs(n_real-resultado)
#print("erro =", erro)
Número de contornos = 234
Número de contornos nas bordas = 53
Resultado = 208 grãos