Saltar al contenido

Medición de imágenes de diámetro de agujeros en piezas metálicas, fotografiadas con cámara monocromática telecéntrica con opencv

Solución:

Sabemos dos cosas sobre estas imágenes:

  1. Los objetos son oscuros, sobre un fondo brillante.
  2. Los agujeros son todos círculos y queremos medir todos los agujeros.

Entonces, todo lo que tenemos que hacer es detectar agujeros. En realidad, esto es bastante trivial:

  1. umbral (el fondo se convierte en el objeto, ya que es brillante)
  2. eliminar objetos de borde

lo que queda son los agujeros. No se incluirán los agujeros que toquen el borde de la imagen. Ahora podemos medir fácilmente estos agujeros. Dado que asumimos que son circulares, podemos hacer tres cosas:

  1. Cuente los píxeles del objeto, esta es una estimación no sesgada del área. A partir del área determinamos el diámetro del agujero.
  2. Detecte contornos, encuentre el centroide, luego use, por ejemplo, la distancia media de los puntos del contorno al centroide como radio.
  3. Normalice las intensidades de la imagen para que la iluminación de fondo tenga una intensidad de 1 y el objeto con los orificios tenga una intensidad de 0. La integral sobre las intensidades de cada orificio es un subpíxel: estimación de precisión del área (ver en la parte inferior para una explicación rápida de este método).

Este código de Python, usando DIPlib (soy un autor) muestra cómo hacer estos tres enfoques:

import PyDIP as dip
import numpy as np

img = dip.ImageRead('geriausias.bmp')
img.SetPixelSize(dip.PixelSize(dip.PhysicalQuantity(1,'um'))) # Usually this info is in the image file
bin, thresh = dip.Threshold(img)
bin = dip.EdgeObjectsRemove(bin)
bin = dip.Label(bin)
msr = dip.MeasurementTool.Measure(bin, features=['Size','Radius'])
print(msr)
d1 = np.sqrt(np.array(msr['Size'])[:,0] * 4 / np.pi)
print("method 1:", d1)
d2 = np.array(msr['Radius'])[:,1] * 2
print("method 2:", d2)

bin = dip.Dilation(bin, 10) # we need larger regions to average over so we take all of the light
                            # coming through the hole into account.
img = (dip.ErfClip(img, thresh, thresh/4, "range") - (thresh*7/8)) / (thresh/4)
msr = dip.MeasurementTool.Measure(bin, img, features=['Mass'])
d3 = np.sqrt(np.array(msr['Mass'])[:,0] * 4 / np.pi)
print("method 3:", d3)

Esto da la salida:

  |       Size |                                            Radius | 
- | ---------- | ------------------------------------------------- | 
  |            |        Max |       Mean |        Min |     StdDev | 
  |      (µm²) |       (µm) |       (µm) |       (µm) |       (µm) | 
- | ---------- | ---------- | ---------- | ---------- | ---------- | 
1 |  6.282e+04 |      143.9 |      141.4 |      134.4 |      1.628 | 
2 |  9.110e+04 |      171.5 |      170.3 |      168.3 |     0.5643 | 
3 |  6.303e+04 |      143.5 |      141.6 |      133.9 |      1.212 | 
4 |  9.103e+04 |      171.6 |      170.2 |      167.3 |     0.6292 | 
5 |  6.306e+04 |      143.9 |      141.6 |      126.5 |      2.320 | 
6 |  2.495e+05 |      283.5 |      281.8 |      274.4 |     0.9805 | 
7 |  1.176e+05 |      194.4 |      193.5 |      187.1 |     0.6303 | 
8 |  1.595e+05 |      226.7 |      225.3 |      219.8 |     0.8629 | 
9 |  9.063e+04 |      171.0 |      169.8 |      167.6 |     0.5457 | 

method 1: [282.8250363  340.57242408 283.28834869 340.45277017 283.36249824
 563.64770132 386.9715443  450.65294139 339.70023023]
method 2: [282.74577033 340.58808144 283.24878097 340.43862835 283.1641869
 563.59706479 386.95245928 450.65392268 339.68617582]
method 3: [282.74836803 340.56787463 283.24627163 340.39568372 283.31396961
 563.601641   386.89884807 450.62167913 339.68954136]

La imagen bin, después de llamar dip.Label, es una imagen entera en la que los píxeles del agujero 1 tienen todos el valor 1, los del agujero 2 tienen el valor 2, etc. Así que seguimos manteniendo la relación entre los tamaños medidos y los agujeros que eran. No me he molestado en hacer una imagen de marcado que muestre los tamaños en la imagen, pero esto se puede hacer fácilmente como ha visto en otras respuestas.

Debido a que no hay información de tamaño de píxel en los archivos de imagen, he impuesto 1 micrón por píxel. Es probable que esto no sea correcto, tendrá que hacer una calibración para obtener información sobre el tamaño de los píxeles.

Un problema aquí es que la iluminación de fondo es demasiado brillante, lo que genera píxeles saturados. Esto hace que los agujeros parezcan más grandes de lo que realmente son. Es importante calibrar el sistema de manera que la iluminación de fondo está cerca del máximo que puede grabar la cámara, pero no en ese máximo ni por encima. Por ejemplo, intente que la intensidad de fondo sea 245 o 250. El tercer método es el más afectado por la mala iluminación.

Para la segunda imagen, el brillo es muy bajo, dando una imagen más ruidosa de lo necesario. Necesitaba modificar la línea bin = dip.Label(bin) dentro:

bin = dip.Label(bin, 2, 500) # Imposing minimum object size rather than filtering

Quizás sea más fácil filtrar el ruido. El resultado fue:

  |       Size |                                            Radius | 
- | ---------- | ------------------------------------------------- | 
  |            |        Max |       Mean |        Min |     StdDev | 
  |      (µm²) |       (µm) |       (µm) |       (µm) |       (µm) | 
- | ---------- | ---------- | ---------- | ---------- | ---------- | 
1 |  4.023e+06 |      1133. |      1132. |      1125. |     0.4989 | 

method 1: [2263.24621554]
method 2: [2263.22724164]
method 3: [2262.90068056]

Explicación rápida del método n. ° 3

El método se describe en la tesis doctoral de Lucas van Vliet (Universidad Tecnológica de Delft, 1993), capítulo 6.

Piénselo de esta manera: la cantidad de luz que entra por el orificio es proporcional al área del orificio (en realidad está dada por ‘área’ x ‘intensidad de luz’). Al sumar toda la luz que entra por el agujero, conocemos el área del agujero. El código suma todas las intensidades de píxeles del objeto, así como algunos píxeles justo fuera del objeto (estoy usando 10 píxeles allí, la distancia a recorrer depende del desenfoque).

los erfclip La función se denomina función de “clip suave”, asegura que la intensidad dentro del orificio sea uniformemente 1, y la intensidad fuera del orificio sea uniformemente 0, y solo alrededor de los bordes deje valores de gris intermedios. En este caso particular, este clip suave evita algunos problemas con las compensaciones en el sistema de imágenes y estimaciones deficientes de la intensidad de la luz. En otros casos, es más importante evitar problemas con el color desigual de los objetos que se miden. También reduce la influencia del ruido.

Puede limitar la imagen y utilizar findContours para encontrar los contornos de los agujeros y luego ajustarlos a círculos con minEnclosingCircle. Se puede comprobar la cordura de los círculos ajustados comparándolos con el área del contorno.

import cv2 as cv
import math
import numpy as np
from matplotlib import pyplot as pl

gray = cv.imread('geriausias.bmp', cv.IMREAD_GRAYSCALE)
_,mask = cv.threshold(gray, 127, 255, cv.THRESH_BINARY)
contours,_ = cv.findContours(mask, cv.RETR_LIST, cv.CHAIN_APPROX_NONE)
contours = [contour for contour in contours if len(contour) > 15]
circles = [cv.minEnclosingCircle(contour) for contour in contours]
areas = [cv.contourArea(contour) for contour in contours]
radiuses = [math.sqrt(area / math.pi) for area in areas]

# Render contours blue and circles green.
canvas = cv.cvtColor(mask, cv.COLOR_GRAY2BGR)
cv.drawContours(canvas, contours, -1, (255, 0, 0), 10)
for circle, radius_from_area in zip(circles, radiuses):
    if 0.9 <= circle[1] / radius_from_area <= 1.1:  # Only allow 10% error in radius.
        p = (round(circle[0][0]), round(circle[0][1]))
        r = round(circle[1])
        cv.circle(canvas, p, r, (0, 255, 0), 10)
cv.imwrite('geriausias_circles.png', canvas)

canvas_small = cv.resize(canvas, None, None, 0.25, 0.25, cv.INTER_AREA)
cv.imwrite('geriausias_circles_small.png', canvas_small)

geriausias_circles_small.png

Los círculos que pasan la prueba de cordura se muestran en verde en la parte superior de todos los contornos que se muestran en azul.

ingrese la descripción de la imagen aquí

ingrese la descripción de la imagen aquí


Aquí hay un enfoque

  • Convertir imagen a escala de grises y desenfoque gaussiano
  • Umbral adaptativo
  • Realice transformaciones morfológicas para suavizar / filtrar la imagen
  • Encuentra contornos
  • Encuentre el perímetro del contorno y realice una aproximación del contorno
  • Obtenga el rectángulo delimitador y el centroide para obtener el diámetro

Después de encontrar los contornos, realizamos una aproximación de contorno. La idea es que si el contorno aproximado tiene Tres vértices, entonces debe ser un triángulo. Del mismo modo, si tiene cuatro, debe ser un cuadrado o un rectángulo. Por lo tanto, podemos suponer que si tiene más de cierto número de vértices, entonces es un círculo.

Hay varias formas de obtener el diámetro, una forma de encontrar el rectángulo delimitador del contorno y usar su ancho. Otra forma es calcularlo a partir de las coordenadas del centroide.

import cv2

image = cv2.imread('1.bmp')

# Gray, blur, adaptive threshold
gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
blur = cv2.GaussianBlur(gray, (3,3), 0)
thresh = cv2.threshold(blur, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)[1]

# Morphological transformations
kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (5,5))
opening = cv2.morphologyEx(thresh, cv2.MORPH_OPEN, kernel)

# Find contours
cnts = cv2.findContours(opening, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)
cnts = cnts[0] if len(cnts) == 2 else cnts[1]

for c in cnts:
    # Find perimeter of contour
    perimeter = cv2.arcLength(c, True)
    # Perform contour approximation
    approx = cv2.approxPolyDP(c, 0.04 * perimeter, True)

    # We assume that if the contour has more than a certain
    # number of verticies, we can make the assumption
    # that the contour shape is a circle
    if len(approx) > 6:

        # Obtain bounding rectangle to get measurements
        x,y,w,h = cv2.boundingRect(c)

        # Find measurements
        diameter = w
        radius = w/2

        # Find centroid
        M = cv2.moments(c)
        cX = int(M["m10"] / M["m00"])
        cY = int(M["m01"] / M["m00"])

        # Draw the contour and center of the shape on the image
        cv2.rectangle(image,(x,y),(x+w,y+h),(0,255,0),4)
        cv2.drawContours(image,[c], 0, (36,255,12), 4)
        cv2.circle(image, (cX, cY), 15, (320, 159, 22), -1) 

        # Draw line and diameter information 
        cv2.line(image, (x, y + int(h/2)), (x + w, y + int(h/2)), (156, 188, 24), 3)
        cv2.putText(image, "Diameter: ".format(diameter), (cX - 50, cY - 50), cv2.FONT_HERSHEY_SIMPLEX, 3, (156, 188, 24), 3)

cv2.imwrite('image.png', image)
cv2.imwrite('thresh.png', thresh)
cv2.imwrite('opening.png', opening)

Aquí puedes ver las comentarios y valoraciones de los usuarios

Nos puedes corroborar nuestra misión ejecutando un comentario o valorándolo te damos la bienvenida.

¡Haz clic para puntuar esta entrada!
(Votos: 0 Promedio: 0)



Utiliza Nuestro Buscador

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *