Por fin después de tanto luchar hemos dado con el resultado de este atolladero que tantos usuarios de nuestro sitio han presentado. Si tienes algo que aportar puedes aportar tu información.
Solución:
Primero, consulte /samples/c/squares.c en su distribución de OpenCV. Este ejemplo proporciona un detector cuadrado, y debería ser un buen comienzo sobre cómo detectar características similares a las esquinas. Luego, eche un vistazo a las funciones orientadas a características de OpenCV como cvCornerHarris () y cvGoodFeaturesToTrack ().
Los métodos anteriores pueden volver muchos características similares a las esquinas: la mayoría no serán las “true esquinas “que estás buscando. En mi aplicación, tenía que detectar cuadrados que habían sido rotados o sesgados (debido a la perspectiva). Mi canal de detección consistía en:
- Convertir de RGB a escala de grises (cvCvtColor)
- Suave (cvSmooth)
- Umbral (cvThreshold)
- Detectar bordes (cvCanny)
- Buscar contornos (cvFindContours)
- Contornos aproximados con características lineales (cvApproxPoly)
- Busque “rectángulos”, que eran estructuras que: tenían contornos poligonales que poseían 4 puntos, tenían un área suficiente, tenían bordes adyacentes de ~ 90 grados, tenían una distancia entre vértices “opuestos” de tamaño suficiente, etc.
El paso 7 era necesario porque una imagen ligeramente ruidosa puede producir muchas estructuras que parecen rectangulares después de la poligonalización. En mi aplicación, también tuve que lidiar con estructuras cuadradas que aparecían dentro o se superponían al cuadrado deseado. Encontré que la propiedad del área del contorno y el centro de gravedad son útiles para discernir el rectángulo adecuado.
A primera vista, para el ojo humano hay 4 esquinas. Pero en la visión por computadora, una esquina se considera un punto que tiene un gran cambio de gradiente en la intensidad en su vecindario. El vecindario puede ser un vecindario de 4 píxeles o un vecindario de 8 píxeles.
En la ecuación proporcionada para encontrar el gradiente de intensidad, se ha considerado para vecindad de 4 píxeles VER DOCUMENTACIÓN.
Aquí está mi enfoque para la imagen en cuestión. También tengo el código en Python:
path = r'C:Usersselwyn77DesktopStackcorner'
filename = 'env.jpg'
img = cv2.imread(os.path.join(path, filename))
gray = cv2.cvtColor(img,cv2.COLOR_BGR2GRAY) #--- convert to grayscale
Es una buena opción desenfocar siempre la imagen para eliminar los menos posibles cambios de degradado y conservar los más intensos. Opté por elegir el filtro bilateral que, a diferencia del filtro gaussiano, no difumina todos los píxeles del vecindario. Más bien difumina los píxeles que tienen una intensidad de píxel similar a la del píxel central. En resumen, conserva los bordes / esquinas de un cambio de gradiente alto pero difumina las regiones que tienen cambios de gradiente mínimos.
bi = cv2.bilateralFilter(gray, 5, 75, 75)
cv2.imshow('bi',bi)
Para un humano, no es una gran diferencia en comparación con la imagen original. Pero importa. Ahora encontrando posibles rincones:
dst = cv2.cornerHarris(bi, 2, 3, 0.04)
dst
devuelve un array (la misma forma 2D de la imagen) con valores propios obtenidos de la ecuación final mencionada AQUÍ.
Ahora se debe aplicar un umbral para seleccionar esas esquinas más allá de un cierto valor. Usaré el de la documentación:
#--- create a black image to see where those corners occur ---
mask = np.zeros_like(gray)
#--- applying a threshold and turning those pixels above the threshold to white ---
mask[dst>0.01*dst.max()] = 255
cv2.imshow('mask', mask)
Los píxeles blancos son regiones de posibles esquinas. Puedes encontrar muchos rincones vecinos entre sí.
Para dibujar las esquinas seleccionadas en la imagen:
img[dst > 0.01 * dst.max()] = [0, 0, 255] #--- [0, 0, 255] --> Red ---
cv2.imshow('dst', img)
(Los píxeles de color rojo son las esquinas, no tan visibles)
Para obtener un array de todos los píxeles con esquinas:
coordinates = np.argwhere(mask)
ACTUALIZAR
Variable coor
es un array de matrices. Convirtiéndolo en una lista de listas
coor_list = [l.tolist() for l in list(coor)]
Conversión de lo anterior a una lista de tuplas
coor_tuples = [tuple(l) for l in coor_list]
Tengo una forma fácil y bastante ingenua de encontrar las 4 esquinas. Simplemente calculé la distancia de cada esquina a cada otra esquina. Conservé aquellos rincones cuya distancia excedía cierto umbral.
Aquí está el código:
thresh = 50
def distance(pt1, pt2):
(x1, y1), (x2, y2) = pt1, pt2
dist = math.sqrt( (x2 - x1)**2 + (y2 - y1)**2 )
return dist
coor_tuples_copy = coor_tuples
i = 1
for pt1 in coor_tuples:
print(' I :', i)
for pt2 in coor_tuples[i::1]:
print(pt1, pt2)
print('Distance :', distance(pt1, pt2))
if(distance(pt1, pt2) < thresh):
coor_tuples_copy.remove(pt2)
i+=1
Antes de ejecutar el fragmento anterior coor_tuples
tenía todos los puntos de esquina:
[(4, 42),
(4, 43),
(5, 43),
(5, 44),
(6, 44),
(7, 219),
(133, 36),
(133, 37),
(133, 38),
(134, 37),
(135, 224),
(135, 225),
(136, 225),
(136, 226),
(137, 225),
(137, 226),
(137, 227),
(138, 226)]
Después de ejecutar el fragmento, me quedé con 4 esquinas:
[(4, 42), (7, 219), (133, 36), (135, 224)]
ACTUALIZACIÓN 2
Ahora todo lo que tienes que hacer es marcar estos 4 puntos en una copia de la imagen original.
img2 = img.copy()
for pt in coor_tuples:
cv2.circle(img2, tuple(reversed(pt)), 3, (0, 0, 255), -1)
cv2.imshow('Image with 4 corners', img2)
Aquí hay una implementación usando cv2.goodFeaturesToTrack()
para detectar esquinas. El enfoque es
- Convertir imagen a escala de grises
- Realice una detección de bordes astuta
- Detectar esquinas
- Opcionalmente, realice una transformación de perspectiva de 4 puntos para obtener una vista de arriba hacia abajo de la imagen
Usando esta imagen inicial,
Después de convertir a escala de grises, realizamos una astuta detección de bordes
Ahora que tenemos una imagen binaria decente, podemos usar cv2.goodFeaturesToTrack()
corners = cv2.goodFeaturesToTrack(canny, 4, 0.5, 50)
Para los parámetros, le damos la imagen astuta, establecemos el número máximo de esquinas en 4 (maxCorners
), utilice una calidad mínima aceptada de 0,5 (qualityLevel
), y establezca la distancia euclidiana mínima posible entre las esquinas devueltas en 50 (minDistance
). Aqui esta el resultado
Ahora que hemos identificado las esquinas, podemos realizar una transformación de perspectiva de 4 puntos para obtener una vista de arriba hacia abajo del objeto. Primero ordenamos los puntos en el sentido de las agujas del reloj y luego dibujamos el resultado en una máscara.
Nota: Podríamos haber encontrado contornos en la imagen de Canny en lugar de hacer este paso para crear la máscara, pero finge que solo teníamos los 4 puntos de esquina para trabajar.
A continuación, encontramos contornos en esta máscara y filtramos usando cv2.arcLength()
y cv2.approxPolyDP()
. La idea es que si el contorno tiene 4 puntos, entonces debe ser nuestro objeto. Una vez que tenemos este contorno, realizamos una transformación de perspectiva
Finalmente giramos la imagen en función de la orientación deseada. Aqui esta el resultado
Código para solo detectar esquinas
import cv2
image = cv2.imread('1.png')
gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
canny = cv2.Canny(gray, 120, 255, 1)
corners = cv2.goodFeaturesToTrack(canny,4,0.5,50)
for corner in corners:
x,y = corner.ravel()
cv2.circle(image,(x,y),5,(36,255,12),-1)
cv2.imshow('canny', canny)
cv2.imshow('image', image)
cv2.waitKey()
Código para detectar esquinas y realizar transformación de perspectiva
import cv2
import numpy as np
def rotate_image(image, angle):
# Grab the dimensions of the image and then determine the center
(h, w) = image.shape[:2]
(cX, cY) = (w / 2, h / 2)
# grab the rotation matrix (applying the negative of the
# angle to rotate clockwise), then grab the sine and cosine
# (i.e., the rotation components of the matrix)
M = cv2.getRotationMatrix2D((cX, cY), -angle, 1.0)
cos = np.abs(M[0, 0])
sin = np.abs(M[0, 1])
# Compute the new bounding dimensions of the image
nW = int((h * sin) + (w * cos))
nH = int((h * cos) + (w * sin))
# Adjust the rotation matrix to take into account translation
M[0, 2] += (nW / 2) - cX
M[1, 2] += (nH / 2) - cY
# Perform the actual rotation and return the image
return cv2.warpAffine(image, M, (nW, nH))
def order_points_clockwise(pts):
# sort the points based on their x-coordinates
xSorted = pts[np.argsort(pts[:, 0]), :]
# grab the left-most and right-most points from the sorted
# x-roodinate points
leftMost = xSorted[:2, :]
rightMost = xSorted[2:, :]
# now, sort the left-most coordinates according to their
# y-coordinates so we can grab the top-left and bottom-left
# points, respectively
leftMost = leftMost[np.argsort(leftMost[:, 1]), :]
(tl, bl) = leftMost
# now, sort the right-most coordinates according to their
# y-coordinates so we can grab the top-right and bottom-right
# points, respectively
rightMost = rightMost[np.argsort(rightMost[:, 1]), :]
(tr, br) = rightMost
# return the coordinates in top-left, top-right,
# bottom-right, and bottom-left order
return np.array([tl, tr, br, bl], dtype="int32")
def perspective_transform(image, corners):
def order_corner_points(corners):
# Separate corners into individual points
# Index 0 - top-right
# 1 - top-left
# 2 - bottom-left
# 3 - bottom-right
corners = [(corner[0][0], corner[0][1]) for corner in corners]
top_r, top_l, bottom_l, bottom_r = corners[0], corners[1], corners[2], corners[3]
return (top_l, top_r, bottom_r, bottom_l)
# Order points in clockwise order
ordered_corners = order_corner_points(corners)
top_l, top_r, bottom_r, bottom_l = ordered_corners
# Determine width of new image which is the max distance between
# (bottom right and bottom left) or (top right and top left) x-coordinates
width_A = np.sqrt(((bottom_r[0] - bottom_l[0]) ** 2) + ((bottom_r[1] - bottom_l[1]) ** 2))
width_B = np.sqrt(((top_r[0] - top_l[0]) ** 2) + ((top_r[1] - top_l[1]) ** 2))
width = max(int(width_A), int(width_B))
# Determine height of new image which is the max distance between
# (top right and bottom right) or (top left and bottom left) y-coordinates
height_A = np.sqrt(((top_r[0] - bottom_r[0]) ** 2) + ((top_r[1] - bottom_r[1]) ** 2))
height_B = np.sqrt(((top_l[0] - bottom_l[0]) ** 2) + ((top_l[1] - bottom_l[1]) ** 2))
height = max(int(height_A), int(height_B))
# Construct new points to obtain top-down view of image in
# top_r, top_l, bottom_l, bottom_r order
dimensions = np.array([[0, 0], [width - 1, 0], [width - 1, height - 1],
[0, height - 1]], dtype = "float32")
# Convert to Numpy format
ordered_corners = np.array(ordered_corners, dtype="float32")
# Find perspective transform matrix
matrix = cv2.getPerspectiveTransform(ordered_corners, dimensions)
# Return the transformed image
return cv2.warpPerspective(image, matrix, (width, height))
image = cv2.imread('1.png')
original = image.copy()
gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
canny = cv2.Canny(gray, 120, 255, 1)
corners = cv2.goodFeaturesToTrack(canny,4,0.5,50)
c_list = []
for corner in corners:
x,y = corner.ravel()
c_list.append([int(x), int(y)])
cv2.circle(image,(x,y),5,(36,255,12),-1)
corner_points = np.array([c_list[0], c_list[1], c_list[2], c_list[3]])
ordered_corner_points = order_points_clockwise(corner_points)
mask = np.zeros(image.shape, dtype=np.uint8)
cv2.fillPoly(mask, [ordered_corner_points], (255,255,255))
mask = cv2.cvtColor(mask, cv2.COLOR_BGR2GRAY)
cnts = cv2.findContours(mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
cnts = cnts[0] if len(cnts) == 2 else cnts[1]
for c in cnts:
peri = cv2.arcLength(c, True)
approx = cv2.approxPolyDP(c, 0.015 * peri, True)
if len(approx) == 4:
transformed = perspective_transform(original, approx)
result = rotate_image(transformed, -90)
cv2.imshow('canny', canny)
cv2.imshow('image', image)
cv2.imshow('mask', mask)
cv2.imshow('transformed', transformed)
cv2.imshow('result', result)
cv2.waitKey()
Si aceptas, puedes dejar un artículo acerca de qué le añadirías a este enunciado.