Mostrar imágenes combinadas juntas sin corte usando warpAffine

Estoy tratando de unir dos imágenes usando una plantilla que coincida encontrar 3 conjuntos de puntos que paso a cv2.getAffineTransform() obtener una matriz warp que paso a cv2.warpAffine() para alinear mis imágenes.

Sin embargo, cuando me uno a mis imágenes, la mayoría de mi imagen afín no se muestra. Intenté utilizar diferentes técnicas para seleccionar puntos, cambié el orden o los argumentos, etc., pero solo puedo obtener un delgado deslizamiento de la imagen afín que se mostrará.

¿Podría alguien decirme si mi enfoque es válido y sugerirme dónde podría estar cometiendo un error? Cualquier adivinación sobre qué podría estar causando el problema sería muy apreciada. Gracias por adelantado.

Este es el resultado final que obtengo. Aquí están las imágenes originales ( 1 , 2 ) y el código que uso:

EDITAR: Aquí están los resultados de la variable trans

 array([[ 1.00768049e+00, -3.76690353e-17, -3.13824885e+00], [ 4.84461775e-03, 1.30769231e+00, 9.61912797e+02]]) 

Y aquí están los puntos pasados ​​a cv2.getAffineTransform : unified_pair1

 array([[ 671., 1024.], [ 15., 979.], [ 15., 962.]], dtype=float32) 

unified_pair2

 array([[ 669., 45.], [ 18., 13.], [ 18., 0.]], dtype=float32) 

 import cv2 import numpy as np def showimage(image, name="No name given"): cv2.imshow(name, image) cv2.waitKey(0) cv2.destroyAllWindows() return image_a = cv2.imread('image_a.png') image_b = cv2.imread('image_b.png') def get_roi(image): roi = cv2.selectROI(image) # spacebar to confirm selection cv2.waitKey(0) cv2.destroyAllWindows() crop = image_a[int(roi[1]):int(roi[1]+roi[3]), int(roi[0]):int(roi[0]+roi[2])] return crop temp_1 = get_roi(image_a) temp_2 = get_roi(image_a) temp_3 = get_roi(image_a) def find_template(template, search_image_a, search_image_b): ccnorm_im_a = cv2.matchTemplate(search_image_a, template, cv2.TM_CCORR_NORMED) template_loc_a = np.where(ccnorm_im_a == ccnorm_im_a.max()) ccnorm_im_b = cv2.matchTemplate(search_image_b, template, cv2.TM_CCORR_NORMED) template_loc_b = np.where(ccnorm_im_b == ccnorm_im_b.max()) return template_loc_a, template_loc_b coord_a1, coord_b1 = find_template(temp_1, image_a, image_b) coord_a2, coord_b2 = find_template(temp_2, image_a, image_b) coord_a3, coord_b3 = find_template(temp_3, image_a, image_b) def unnest_list(coords_list): coords_list = [a[0] for a in coords_list] return coords_list coord_a1 = unnest_list(coord_a1) coord_b1 = unnest_list(coord_b1) coord_a2 = unnest_list(coord_a2) coord_b2 = unnest_list(coord_b2) coord_a3 = unnest_list(coord_a3) coord_b3 = unnest_list(coord_b3) def unify_coords(coords1,coords2,coords3): unified = [] unified.extend([coords1, coords2, coords3]) return unified # Create a 2 lists containing 3 pairs of coordinates unified_pair1 = unify_coords(coord_a1, coord_a2, coord_a3) unified_pair2 = unify_coords(coord_b1, coord_b2, coord_b3) # Convert elements of lists to numpy arrays with data type float32 unified_pair1 = np.asarray(unified_pair1, dtype=np.float32) unified_pair2 = np.asarray(unified_pair2, dtype=np.float32) # Get result of the affine transformation trans = cv2.getAffineTransform(unified_pair1, unified_pair2) # Apply the affine transformation to original image result = cv2.warpAffine(image_a, trans, (image_a.shape[1] + image_b.shape[1], image_a.shape[0])) result[0:image_b.shape[0], image_b.shape[1]:] = image_b showimage(result) cv2.imwrite('result.png', result) 

Fuentes: Enfoque basado en los consejos recibidos aquí , este tutorial y este ejemplo de los documentos.

12 de julio Edición:

Esta publicación inspiró un repository de GitHub que proporciona funciones para realizar esta tarea; uno para warpAffine() acolchado warpAffine() y otro para warpPerspective() acolchado warpPerspective() . Horquilla la versión Python o la versión C ++ .


Las transformaciones cambian la ubicación de los píxeles

Lo que hace cualquier transformación es tomar coordenadas de puntos (x, y) y asignarlas a nuevas ubicaciones (x', y') :

 s*x' h1 h2 h3 x s*y' = h4 h5 h6 * y s h7 h8 1 1 

donde s es un factor de escala. Debe dividir las nuevas coordenadas por el factor de escala para recuperar las ubicaciones de píxeles adecuadas (x', y') . Técnicamente, esto solo es cierto para las homografías: (3, 3) matrices de transformación; no es necesario escalar para transformaciones afines (ni siquiera es necesario utilizar coordenadas homogéneas … pero es mejor mantener esta discusión general).

Luego, los valores reales de los píxeles se mueven a esas nuevas ubicaciones, y los valores de color se interpolan para ajustarse a la nueva cuadrícula de píxeles. Entonces durante este proceso, estas nuevas ubicaciones se graban en algún punto. Necesitaremos esas ubicaciones para ver dónde se mueven los píxeles, en relación con la otra imagen. Comencemos con un ejemplo fácil y veamos dónde se asignan los puntos.

Supongamos que su matriz de transformación simplemente cambia los píxeles hacia la izquierda por diez píxeles. La traducción es manejada por la última columna; la primera fila es la traducción en x segunda fila es la traducción en y . Entonces tendríamos una matriz de identidad, pero con -10 en la primera fila, tercera columna. ¿Dónde se mapearía el píxel (0,0) ? Con suerte, (-10,0) si la lógica tiene algún sentido. Y de hecho, lo hace:

 transf = np.array([[1.,0.,-10.],[0.,1.,0.],[0.,0.,1.]]) homg_pt = np.array([0,0,1]) new_homg_pt = transf.dot(homg_pt)) new_homg_pt /= new_homg_pt[2] # new_homg_pt = [-10. 0. 1.] 

¡Perfecto! Entonces podemos averiguar dónde se mapean todos los puntos con un poco de álgebra lineal. Tendremos que obtener todos los puntos (x,y) y ponerlos en una gran matriz para que cada punto se encuentre en su propia columna. Vamos a pretender que nuestra imagen es solo 4x4 .

 h, w = src.shape[:2] # 4, 4 indY, indX = np.indices((h,w)) # similar to meshgrid/mgrid lin_homg_pts = np.stack((indX.ravel(), indY.ravel(), np.ones(indY.size))) 

Estos lin_homg_pts tienen ahora todos los puntos homogéneos:

 [[ 0. 1. 2. 3. 0. 1. 2. 3. 0. 1. 2. 3. 0. 1. 2. 3.] [ 0. 0. 0. 0. 1. 1. 1. 1. 2. 2. 2. 2. 3. 3. 3. 3.] [ 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1.]] 

Entonces podemos hacer multiplicación de matrices para obtener el valor mapeado de cada punto. Para simplificar, sigamos con la homografía anterior.

 trans_lin_homg_pts = transf.dot(lin_homg_pts) trans_lin_homg_pts /= trans_lin_homg_pts[2,:] 

Y ahora tenemos los puntos transformados:

 [[-10. -9. -8. -7. -10. -9. -8. -7. -10. -9. -8. -7. -10. -9. -8. -7.] [ 0. 0. 0. 0. 1. 1. 1. 1. 2. 2. 2. 2. 3. 3. 3. 3.] [ 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1.]] 

Como podemos ver, todo está funcionando como se esperaba: solo hemos cambiado los valores x , por -10 .

Los píxeles se pueden desplazar fuera de los límites de la imagen

Tenga en cuenta que estas ubicaciones de píxeles son negativas, están fuera de los límites de la imagen. Si hacemos algo un poco más complejo y giramos la imagen 45 grados, obtendremos algunos valores de píxeles fuera de nuestros límites originales. Sin embargo, no nos importan todos los valores de píxel, solo necesitamos saber qué tan lejos están los píxeles más alejados que están fuera de las ubicaciones de píxeles de la imagen original, de modo que podamos rellenar la imagen original muy lejos, antes de mostrar la imagen deformada en ella. .

 theta = 45*np.pi/180 transf = np.array([ [ np.cos(theta),np.sin(theta),0], [-np.sin(theta),np.cos(theta),0], [0.,0.,1.]]) print(transf) trans_lin_homg_pts = transf.dot(lin_homg_pts) minX = np.min(trans_lin_homg_pts[0,:]) minY = np.min(trans_lin_homg_pts[1,:]) maxX = np.max(trans_lin_homg_pts[0,:]) maxY = np.max(trans_lin_homg_pts[1,:]) # minX: 0.0, minY: -2.12132034356, maxX: 4.24264068712, maxY: 2.12132034356, 

Así que vemos que podemos obtener ubicaciones de píxeles fuera de nuestra imagen original, tanto en la dirección negativa como en la positiva. El valor x mínimo no cambia porque cuando una homografía aplica una rotación, lo hace desde la esquina superior izquierda. Ahora, una cosa a tener en cuenta aquí es que he aplicado la transformación a todos los píxeles de la imagen. Pero esto es realmente innecesario, simplemente puede urdir los cuatro puntos de esquina y ver dónde aterrizan.

Relleno de la imagen de destino

Tenga en cuenta que cuando llama a cv2.warpAffine() debe ingresar el tamaño de destino. Estos valores de píxeles transformados hacen referencia a ese tamaño. Entonces, si un píxel se asigna a (-10,0) , no aparecerá en la imagen de destino. Eso significa que tendremos que hacer otra homografía con traducciones que cambien todas las ubicaciones de píxeles sean positivas, y luego podemos rellenar la matriz de imágenes para compensar nuestro cambio. También tendremos que rellenar la imagen original en la parte inferior y la derecha si la homografía mueve los puntos a posiciones más grandes que la imagen, también.

En el ejemplo reciente, el valor mínimo x es el mismo, por lo que no necesitamos desplazamiento horizontal. Sin embargo, el valor mínimo ha disminuido en aproximadamente dos píxeles, por lo que debemos desplazar la imagen dos píxeles hacia abajo. Primero, creemos la imagen de destino acolchada.

 pad_sz = list(src.shape) # in case three channel pad_sz[0] = np.round(np.maximum(pad_sz[0], maxY) - np.minimum(0, minY)).astype(int) pad_sz[1] = np.round(np.maximum(pad_sz[1], maxX) - np.minimum(0, minX)).astype(int) dst_pad = np.zeros(pad_sz, dtype=np.uint8) # pad_sz = [6, 4, 3] 

Como podemos ver, la altura aumentó del original en dos píxeles para dar cuenta de ese cambio.

Agregue traducción a la transformación para cambiar todas las ubicaciones de píxeles a positivas

Ahora, necesitamos crear una nueva matriz de homografía para traducir la imagen deformada en la misma cantidad en la que nos desplazamos. Y para aplicar ambas transformaciones — el original y este nuevo cambio — tenemos que componer las dos homografías (para una transformación afín, simplemente puede agregar la traducción, pero no para una homografía). Además, debemos dividir por la última entrada para asegurarnos de que las escalas sigan siendo adecuadas (nuevamente, solo para homografías):

 anchorX, anchorY = 0, 0 transl_transf = np.eye(3,3) if minX < 0: anchorX = np.round(-minX).astype(int) transl_transf[0,2] -= anchorX if minY < 0: anchorY = np.round(-minY).astype(int) transl_transf[1,2] -= anchorY new_transf = transl_transf.dot(transf) new_transf /= new_transf[2,2] 

También creé aquí los puntos de anclaje donde ubicaremos la imagen de destino en la matriz acolchada; se desplaza en la misma cantidad en que la homografía cambiará la imagen. Coloquemos la imagen de destino dentro de la matriz acolchada:

 dst_pad[anchorY:anchorY+dst_sz[0], anchorX:anchorX+dst_sz[1]] = dst 

Distorsionar con la nueva transformación en la imagen acolchada

Todo lo que queda por hacer es aplicar la nueva transformación a la imagen de origen (con el tamaño de destino acolchado), y luego podemos superponer las dos imágenes.

 warped = cv2.warpPerspective(src, new_transf, (pad_sz[1],pad_sz[0])) alpha = 0.3 beta = 1 - alpha blended = cv2.addWeighted(warped, alpha, dst_pad, beta, 1.0) 

Poniendolo todo junto

Vamos a crear una función para esto ya que estábamos creando bastantes variables que no necesitamos al final aquí. Para las entradas, necesitamos la imagen de origen, la imagen de destino y la homografía original. Y para las salidas, simplemente queremos la imagen de destino acolchada y la imagen deformada. Tenga en cuenta que en los ejemplos usamos una homografía de 3x3 así que es mejor que nos aseguremos de enviar las transformaciones de 3x3 en lugar de las deformaciones 2x3 afines o euclidianas. Puedes agregar la fila [0,0,1] a cualquier urdimbre afín en la parte inferior y estarás bien.

 def warpPerspectivePadded(img, dst, transf): src_h, src_w = src.shape[:2] lin_homg_pts = np.array([[0, src_w, src_w, 0], [0, 0, src_h, src_h], [1, 1, 1, 1]]) trans_lin_homg_pts = transf.dot(lin_homg_pts) trans_lin_homg_pts /= trans_lin_homg_pts[2,:] minX = np.min(trans_lin_homg_pts[0,:]) minY = np.min(trans_lin_homg_pts[1,:]) maxX = np.max(trans_lin_homg_pts[0,:]) maxY = np.max(trans_lin_homg_pts[1,:]) # calculate the needed padding and create a blank image to place dst within dst_sz = list(dst.shape) pad_sz = dst_sz.copy() # to get the same number of channels pad_sz[0] = np.round(np.maximum(dst_sz[0], maxY) - np.minimum(0, minY)).astype(int) pad_sz[1] = np.round(np.maximum(dst_sz[1], maxX) - np.minimum(0, minX)).astype(int) dst_pad = np.zeros(pad_sz, dtype=np.uint8) # add translation to the transformation matrix to shift to positive values anchorX, anchorY = 0, 0 transl_transf = np.eye(3,3) if minX < 0: anchorX = np.round(-minX).astype(int) transl_transf[0,2] += anchorX if minY < 0: anchorY = np.round(-minY).astype(int) transl_transf[1,2] += anchorY new_transf = transl_transf.dot(transf) new_transf /= new_transf[2,2] dst_pad[anchorY:anchorY+dst_sz[0], anchorX:anchorX+dst_sz[1]] = dst warped = cv2.warpPerspective(src, new_transf, (pad_sz[1],pad_sz[0])) return dst_pad, warped 

Ejemplo de ejecutar la función

Finalmente, podemos llamar a esta función con algunas imágenes y homografías reales y ver cómo se desarrolla. Tomaré prestado el ejemplo de LearnOpenCV :

 src = cv2.imread('book2.jpg') pts_src = np.array([[141, 131], [480, 159], [493, 630],[64, 601]], dtype=np.float32) dst = cv2.imread('book1.jpg') pts_dst = np.array([[318, 256],[534, 372],[316, 670],[73, 473]], dtype=np.float32) transf = cv2.getPerspectiveTransform(pts_src, pts_dst) dst_pad, warped = warpPerspectivePadded(src, dst, transf) alpha = 0.5 beta = 1 - alpha blended = cv2.addWeighted(warped, alpha, dst_pad, beta, 1.0) cv2.imshow("Blended Warped Image", blended) cv2.waitKey(0) 

Y terminamos con esta imagen deformada acolchada:

[Acolchado y deformado [1]

a diferencia de la típica urdimbre cortada que normalmente obtendrías.