stitching - panoramica opencv
Mostrar imágenes unidas sin cortar usando warpAffine (1)
12 de julio Editar:
Esta publicación inspiró un repositorio de GitHub que proporciona funciones para realizar esta tarea;
uno para
warpAffine()
acolchado
warpAffine()
y otro para
warpPerspective()
acolchado.
Bifurca la
versión de Python
o la
versión de C ++
.
Las transformaciones cambian la ubicación de los píxeles.
Lo que hace cualquier transformación es tomar sus coordenadas de punto
(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 algún 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 necesita escalar para transformaciones afines (ni siquiera necesita usar coordenadas homogéneas ... pero es mejor mantener esta discusión general).
Luego, los valores de píxeles reales 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 registran en algún momento. Necesitaremos esas ubicaciones para ver dónde se mueven realmente los píxeles, en relación con la otra imagen. Comencemos con un ejemplo sencillo y veamos dónde se asignan los puntos.
Suponga que su matriz de transformación simplemente desplaza los píxeles a la izquierda en diez píxeles.
La traducción es manejada por la última columna;
la primera fila es la traducción en
x
y la 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 asignan
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 esté en su propia columna.
Supongamos 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 cada punto homogéneo ahora:
[[ 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 una multiplicación matricial para obtener el valor mapeado de cada punto. Por simplicidad, 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 funciona como se esperaba: hemos cambiado los valores de
x
solo, 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 importa cada valor de píxel, solo necesitamos saber qué tan lejos están los píxeles más lejanos que están fuera de las ubicaciones de píxeles de la imagen original, para que podamos rellenar la imagen original tan 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,
Por lo tanto, vemos que podemos obtener ubicaciones de píxeles bien fuera de nuestra imagen original, tanto en la dirección negativa como en la positiva.
El valor mínimo de
x
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 deformar 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 desplacen
todas las
ubicaciones de píxeles para que sean positivas, y luego podemos rellenar la matriz de imagen para compensar nuestro desplazamiento.
También tendremos que rellenar la imagen original en la parte inferior y derecha si la homografía mueve puntos a posiciones más grandes que la imagen también.
En el ejemplo reciente, el valor min
x
es el mismo, por lo que no necesitamos desplazamiento horizontal.
Sin embargo, el valor min
y
ha reducido 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ó desde el 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 positivo
Ahora, necesitamos crear una nueva matriz de homografía para traducir la imagen deformada en la misma cantidad que cambiamos. Y para aplicar ambas transformaciones, la 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, necesitamos 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 para colocar la imagen de destino en la matriz acolchada; se desplaza en la misma cantidad que la homografía desplazará la imagen. Así que coloquemos la imagen de destino dentro de la matriz acolchada:
dst_pad[anchorY:anchorY+dst_sz[0], anchorX:anchorX+dst_sz[1]] = dst
Deformación con la nueva transformación en la imagen acolchada
Todo lo que nos 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
Creemos 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
3x3
así que mejor nos aseguramos de enviar transformaciones
3x3
en lugar de urdimbres afines o euclidianas
2x3
.
Simplemente puede agregar la fila
[0,0,1]
a cualquier urdimbre afín en la parte inferior y estará 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 y acolchada:
a diferencia de la deformación de corte típica que normalmente obtendrías.
Estoy tratando de unir 2 imágenes usando la coincidencia de plantillas para encontrar 3 conjuntos de puntos que paso a
cv2.getAffineTransform()
obtengo 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 afinada no se muestra. Intenté usar diferentes técnicas para seleccionar puntos, cambié el orden o los argumentos, etc., pero solo puedo obtener una pequeña muestra de la imagen afinada que se mostrará.
¿Alguien podría decirme si mi enfoque es válido y sugerirme dónde podría estar cometiendo un error? Cualquier suposición sobre lo que 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 here , este tutorial y este example de los documentos.