Píxel retroproyectado a rayos 3D en coordenadas mundiales usando el método pseudoinverso

Para la proyección en perspectiva con matrices de cámara dadas y rotación y traslación, podemos calcular la coordenada de píxel 2D de un punto 3D.

ingrese la descripción de la imagen aquí

utilizando la matriz de proyección,

PAG = k [ R | t ]

dónde k es matriz de cámara intrínseca, R es rotación t es traducción. La proyección es simple multiplicación de matrices. X = PAG X . Libro de Zisserman , pág. 161 sugiere usar 3 × 4 matriz de proyección y toma de pseudoinversa. Entonces uno calcularía X que definió hasta la escala que luego puede interpretarse como el rayo que comienza desde el centro de la cámara y va hasta el infinito. Rápidamente codifiqué esto, tomé Z como profundidad, así que traduje la cámara en Y dirección (hasta 1 metro), y después de recuperar X volteado Y , Z para trazar (la mayoría de las matemáticas geométricas proyectivas parecen estar construidas para hacer Z profundidad),

K = [[ 282.363047,      0.,          166.21515189],
     [   0.,          280.10715905,  108.05494375],
     [   0.,            0.,            1.        ]]
K = np.array(K)
R = np.eye(3)
t = np.array([[0],[1],[0]])
P = K.dot(np.hstack((R,t)))

import scipy.linalg as lin

x = np.array([300,300,1])
X = np.dot(lin.pinv(P),x)
X = X / X[3] 
from mpl_toolkits.mplot3d import Axes3D
w = 20
f = plt.figure()
XX  = X[:]; XX[1] = X[2]; XX[2] = X[1]
ax = f.gca(projection='3d')
ax.quiver(0, 0, 1., XX[:3][0], XX[:3][1], XX[:3][2],color='red')
ax.set_xlim(0,10);ax.set_ylim(0,10);ax.set_zlim(0,10)
ax.quiver(0., 0., 1., 0, 5., 0.,color='blue')
ax.set_xlabel("X")
ax.set_ylabel("Y")
ax.set_zlabel("Z")
ax.set_title(str(x[0])+","+str(x[1]))
ax.set_xlim(-w,w);ax.set_ylim(-w,w);ax.set_zlim(-w,w)

ax.view_init(elev=29, azim=-30)
fout = 'test_%s_01.png' % (str(x[0])+str(x[1]))
plt.savefig(fout)
ax.view_init(elev=29, azim=-60)
fout = 'test_%s_02.png' % (str(x[0])+str(x[1]))
plt.savefig(fout)

Estas imágenes a continuación son el resultado (la flecha azul muestra el vector normal perpendicular al plano de la imagen, las imágenes muestran todas las combinaciones x=10,300 y=10,300):

ingrese la descripción de la imagen aquí ingrese la descripción de la imagen aquí ingrese la descripción de la imagen aquí ingrese la descripción de la imagen aquí ingrese la descripción de la imagen aquí ingrese la descripción de la imagen aquí ingrese la descripción de la imagen aquí ingrese la descripción de la imagen aquí

Doy el diagrama de cámara/rayo para cada píxel desde dos ángulos diferentes.

¿Estos resultados parecen sensatos? 10,10 y 200,200 parecían extraños, jugué un poco con los signos, si traduzco usando -1 negativo y usando -Z después de X calc., ¿las cosas mejoran un poco?

t = np.array([[0],[-1],[0]])
..
XX  = X[:]; XX[1] = X[2]; XX[2] = -X[1]

No sé por qué es eso.

Un píxel no sería un rayo sino un cono o una pirámide. Pero el "centro" de ese cono/pirámide en algún sentido será, por supuesto, un rayo.
Una pirámide... lo siento, ¿cómo es eso posible? ¿Porque un píxel es una especie de forma rectangular, y eso se expande en una pirámide?
Un píxel es cómo la cámara muestrea el píxel que depende de la electrónica de los elementos CCD y cosas por el estilo. A menos que sepamos eso, no podemos decir exactamente qué forma tenía. Pero sí, puedes aproximarlo con el rayo central del píxel.
Aunque creo que sería difícil para la mayoría de las personas decir si los resultados son correctos. ¿Qué son las flechas rojas y azules? Tenga en cuenta que muchos en este sitio pueden no saber mucho de python.
Bien, entonces la flecha azul es normal al plano de imagen de la cámara. La flecha roja es la parte inicial del rayo que comienza en el centro de la cámara, atraviesa el píxel y apunta hacia el mundo exterior, una parte del rayo retroproyectado que estoy buscando.
Para empezar, tome puntos conocidos en el sistema de coordenadas universales . Proyéctelos en la imagen (supongo que puede hacerlo con éxito), luego retroalimente esos puntos a través de su código de inversión para ver si obtiene el resultado correcto.
Además, no recomiendo usar este método cuando esté disponible uno más estable numéricamente (descrito en el mismo lugar del libro). El número de condición de PAG PAG T porque su ejemplo es muy grande, por lo que multiplicar por su inverso puede generar inestabilidad numérica. Por otra parte, el número de condición de METRO no es terriblemente grande, y es varios órdenes de magnitud más pequeño que el de PAG PAG T , en cualquier caso.
¿No puedes simplemente rotar la trama en lugar de voltear el y - y z - hachas? Esa es una transformación de cambio de orientación, por lo que introducirá cambios de signo que solo enturbiarán las aguas para usted.
@amd sobre la estabilidad numérica: la mayoría de los paquetes numéricos usarían SVD para implementar el pseudoinverso, en lugar de calcular el producto escalar con la transposición, así que creo que estoy seguro allí...
Es posible que haya mencionado el problema general que está tratando de resolver (que describe en su respuesta eliminada) en primer lugar. Si está tratando de mapear desde la imagen a un plano de coordenadas mundiales conocido, hay formas muy sencillas de construir esa transformación directamente que puede encontrar buscando aquí. Uno reciente es math.stackexchange.com/q/2215736/265466 , que podría no aplicarse directamente a su situación, pero describe una forma sencilla de crear ese mapa.
Finalmente, en lugar de girar los ejes, ¿por qué no rotas la cámara para que apunte en la dirección que deseas? eso es lo que R matriz es para, después de todo.

Respuestas (3)

En lugar de tratar de depurar su código y verificar todas esas asignaciones inversas, describiré una forma de verificar sus propios resultados de manera objetiva. Si no tiene una buena idea de cuáles deberían ser los resultados, entonces realmente no veo cómo puede saber si son o no "razonables".

Asumiendo que no hay sesgo en la cámara, la matriz k tiene la forma

k = [ s X 0 C X 0 s y C y 0 0 1 ] .
Los valores a lo largo de la diagonal son X - y y - factores de escala, y ( C X , C y ) son las coordenadas de la imagen del eje de la cámara, que se supone normal al plano de la imagen ( z = 1 por convención). Entonces, en este sistema de coordenadas, el vector de dirección para un punto ( X , y ) en la imagen esta ( X C X , y C y , 1 ) y para obtener el vector de dirección correspondiente en el sistema de coordenadas de la cámara (externa), divida por los factores de escala respectivos: ( ( X C X ) / s X , ( y C y ) / s y , 1 ) . Esto es exactamente lo que obtienes al aplicar k 1 , que se encuentra fácilmente que es
k 1 = [ 1 / s X 0 C X / s X 0 1 / s y C y / s y 0 0 1 ]
usando tu método favorito. Finalmente, para transformar este vector en coordenadas mundiales, aplique R 1 , que es solo R 's transponer ya que es una rotación. El rayo resultante, por supuesto, se origina en la posición de la cámara en coordenadas mundiales. Debería ser sencillo codificar esta cascada explícitamente, después de lo cual puede compararla con los resultados que obtiene con cualquier otro método con el que esté experimentando.

En este caso concreto, R es solo la matriz de identidad, por lo que no hay nada más que hacer una vez que tenga el vector de dirección en las coordenadas de la cámara. Tenemos

s X = 282.363047 s y = 280.10715905 C X = 166.21515189 C y = 108.05494375
por lo que la transformación de interno a externo es aproximadamente
X X / 282.363 0.589 y y / 280.107 0.386.
Aplicando esto al punto ( 20 , 20 ) de tu pregunta anterior da ( 0.518 , 0.314 , 1 ) , que concuerda con el vector de dirección calculado allí. Tomando ( 10 , 10 ) en cambio resulta en ( 0.553 , 0.350 , 1 ) , que luego puede verificar con lo que haya producido su código, y así sucesivamente.

Aparte de todo eso, hay un problema cuando se usa el método pseudoinverso descrito por Zisserman. Da la siguiente ecuación para el rayo mapeado hacia atrás:

X ( λ ) = PAG + X + λ C .
Tenga en cuenta que el parámetro es un coeficiente de C , la posición de la cámara en coordenadas mundiales, no del resultado de mapear hacia atrás el punto de la imagen X . Convertido en coordenadas cartesianas, hay un factor de λ + k (para alguna constante k ) en el denominador, por lo que esta no es una parametrización lineal simple. Para extraer un vector de dirección de esto, deberá convertir PAG + X en coordenadas cartesianas y luego restar C .

Para ilustrar, aplicar PAG + a ( 10 , 10 , 1 ) produce ( 0.553 , 0.175 , 1.0 , 0.175 ) , entonces el rayo es ( 0.553 , t 0.175 , 1.0 , t 0.175 ) . En coordenadas cartesianas, el punto retroasignado es ( 3.161 , 1.0 , 5.713 ) y restando la posición de la cámara da ( 3.161 , 2.0 , 5.713 ) . Para comparar esto con el resultado conocido anterior, divida por la tercera coordenada: ( 0.553 , 0.350 , 1.0 ) , que está de acuerdo.

Actualización 2018.07.31: Para cámaras finitas, que es con lo que estás tratando, Zisserman sugiere una retroproyección más conveniente en el siguiente párrafo de la ecuación (6.14). La idea subyacente es que se descompone la matriz de la cámara como PAG = [ METRO pag 4 ] para que la retroproyección de un punto de imagen X corta al plano en el infinito en D = ( ( METRO 1 X ) T , 0 ) T . Esto le da el vector de dirección del rayo retroproyectado en coordenadas mundiales y, por supuesto, el centro de la cámara está en C ~ = METRO 1 pag 4 , es decir, el rayo retroproyectado es

X ~ ( m ) = METRO 1 pag 4 + m METRO 1 X = METRO 1 ( m X pag 4 ) .
Esta parametrización del rayo no sufre de la no linealidad mencionada anteriormente.

Nota rápida: tracé ( 3.161 , 2.0 , 5.713 ) y está apuntando a la esquina inferior derecha del plano de la imagen, mirando a través del centro de la cámara hacia +Z. Creo que tendré que jugar un poco con los signos para obtener la dirección a su forma final (la literatura dice que el modelo estenopeico niega Y , Z y los intercambia).
@BB_ML Bueno, si quiere terminar con un código de culto a la carga "jugando con los letreros", es asunto suyo. Las inversiones de signos causadas por la proyección deberían ser obvias después de mirar un poco el diagrama que incluiste en tus preguntas. Siempre va a haber uno en alguna parte porque el sistema de coordenadas estándar es diestro mirando hacia el origen, pero estamos mirando hacia otro lado cuando vemos la imagen proyectada. Eso se soluciona fácilmente apuntando la cámara hacia el negativo z -axis (que tiene más sentido para mí) o agregando un reflejo a la cadena de transformación.
¿Puede explicar por qué solo tenemos que multiplicar solo por el inverso de R para convertir las coordenadas de la cámara en coordenadas mundiales? ¿Por qué omitir el vector de traducción?

El método pseudoinverso funciona, a continuación se muestra el ejemplo para el píxel 215,180 (la esquina superior izquierda de la imagen es (0,0)), el rayo de este píxel va hacia la parte inferior derecha desde el punto de vista de una persona que mira desde el centro de la cámara hacia el eje Y. Debido al modelo de cámara estenopeica / proyección en perspectiva, fueron necesarios algunos cambios en el eje (pude cambiar mientras trazaba, pero el código a continuación es parte de otro análisis que tuve que realizar en un espacio 3D familiar).

from PIL import Image

from mpl_toolkits.mplot3d import Axes3D
import scipy.linalg as lin

K = [[ 282.363047,      0.,          166.21515189],
     [   0.,          280.10715905,  108.05494375],
     [   0.,            0.,            1.        ]]
K = np.array(K)
R = np.eye(3)
t = np.array([[0],[1.],[0]])
P = K.dot(np.hstack((R,t)))
C = np.array([0., 0., 1.])
p1 = np.array([215, 180, 1.])

X = np.dot(lin.pinv(P),p1)
X = X / X[3]
XX  = np.copy(X)
XX[1] = X[2]; XX[2] = X[1]; XX[2] = -XX[2]
w = 10
f = plt.figure()
ax = f.gca(projection='3d')
xvec = C - XX[:3] 
xvec = -xvec
ax.quiver(C[0], C[1], C[2], xvec[0], xvec[1], xvec[2],color='red')
ax.set_xlim(0,10);ax.set_ylim(0,10);ax.set_zlim(0,10)
ax.quiver(0., 0., 1., 0, 5., 0.,color='blue')
ax.set_xlabel("X")
ax.set_ylabel("Y")
ax.set_zlabel("Z")
ax.set_xlim(-w,w);ax.set_ylim(-w,w);ax.set_zlim(-w,w)
ax.view_init(elev=5, azim=100)
plt.savefig('out1.png')
ax.view_init(elev=5, azim=50)
plt.savefig('out2.png')

ingrese la descripción de la imagen aquí ingrese la descripción de la imagen aquí

Dado que este es el primer y único resultado de Google que realmente ayuda, y tomó bastante tiempo crearlo, aquí está en Numpy

import numpy as np


def convert_nx2_to_homo_3xn(points_nx2: np.array):
    return np.hstack([points_nx2, np.ones((points_nx2.shape[0], 1))]).T


def convert_nx3_to_homo_4xn(points_nx3: np.array):
    return np.hstack([points_nx3, np.ones((points_nx3.shape[0], 1))]).T


def cast_2d_points_as_3d_rays(sub_pixels_nx2: np.array, proj_3x4: np.array):
    """
    see Harley & Zisserman pg 162, section 6.2.2, figure 6.14
    see https://math.stackexchange.com/a/597489/541203

    cast rays from camera center through the sub_pixels_nx2. Return camera center and ray directions.
    """
    m_3x3 = proj_3x4[:, :3]
    p4_3x1 = proj_3x4[:, 3]
    m_inv_3x3 = np.linalg.inv(m_3x3)

    # projection matrix to camera center
    camera_center_3x1 = np.expand_dims(-m_inv_3x3 @ p4_3x1, 1)
    camera_center_homo_4x1 = np.vstack([camera_center_3x1, 1])

    # projection matrix + pixel locations to ray directions
    sub_pixels_homo_3xn = convert_nx2_to_homo_3xn(sub_pixels_nx2)
    ray_directions_3xn = m_inv_3x3 @ sub_pixels_homo_3xn
    ray_directions_homo_4xn = convert_nx3_to_homo_4xn(ray_directions_3xn.T)
    ray_directions_homo_4xn[3, :] = 0  # this is a direction

    return ray_directions_homo_4xn, camera_center_homo_4x1

```