Solución:
TLDR; Use StereoSGBM (Semi Global Block Matching) para imágenes con bordes más suaves y use un filtrado posterior si lo desea aún más suave
OP no proporcionó imágenes originales, así que estoy usando Tsukuba
del conjunto de datos de Middlebury.
Resultado con StereoBM regular
Resultado con StereoSGBM (sintonizado)
El mejor resultado que pude encontrar en la literatura.
Consulte la publicación aquí para obtener más detalles.
Ejemplo de filtrado de publicaciones (ver enlace a continuación)
Teoría / Otras consideraciones de la pregunta de OP
Las grandes áreas negras de sus imágenes rectificadas calibradas me llevarían a creer que para ellas, la calibración no se hizo muy bien. Hay una variedad de razones por las que podría estar en juego, tal vez la configuración física, tal vez la iluminación cuando realizó la calibración, etc., pero hay muchos tutoriales de calibración de la cámara para eso y tengo entendido que está pidiendo una forma de obtener un mapa de profundidad mejor con una configuración no calibrada (esto no es 100% claro, pero el título parece respaldar esto y creo que eso es lo que la gente vendrá aquí a intentar encontrar).
Su enfoque básico es correcto, pero los resultados definitivamente pueden mejorarse. Esta forma de mapeo de profundidad no se encuentra entre las que producen mapas de la más alta calidad (especialmente si no están calibrados). La mayor mejora probablemente provendrá del uso de un algoritmo de coincidencia estéreo diferente. La iluminación también puede tener un efecto significativo. La imagen de la derecha (al menos a simple vista) parece estar menos iluminada, lo que podría interferir con la reconstrucción. Primero puede intentar iluminarlo al mismo nivel que el otro, o recopilar nuevas imágenes si es posible. De ahora en adelante, asumiré que no tiene acceso a las cámaras originales, por lo que consideraré recopilar nuevas imágenes, alterar la configuración o realizar la calibración para estar fuera de alcance. (Si tiene acceso a la configuración y las cámaras, le sugiero que verifique la calibración y utilice un método calibrado, ya que esto funcionará mejor).
Usaste StereoBM
para calcular su disparidad (mapa de profundidad) que funciona, pero StereoSGBM
es mucho más adecuado para esta aplicación (maneja mejor los bordes más suaves). Puedes ver la diferencia a continuación.
Este artículo explica las diferencias con más profundidad:
La coincidencia de bloques se enfoca en imágenes de alta textura (piense en la imagen de un árbol) y la coincidencia de bloques semi-global se enfocará en la coincidencia de niveles de subpíxeles e imágenes con texturas más suaves (piense en una imagen de un pasillo).
Sin ningún parámetro intrínseco explícito de la cámara, detalles específicos sobre la configuración de la cámara (como distancia focal, distancia entre las cámaras, distancia al sujeto, etc.), una dimensión conocida en la imagen o movimiento (para usar la estructura del movimiento), puede solo obtenga reconstrucción 3D hasta una transformación proyectiva; tampoco tendrá un sentido de escala ni necesariamente de rotación, pero aún puede generar un mapa de profundidad relativa. Es probable que sufra algunas distorsiones de barril y otras que podrían eliminarse con la calibración adecuada de la cámara, pero puede obtener resultados razonables sin ella siempre que las cámaras no sean terribles (el sistema de lentes no esté demasiado distorsionado) y estén bien configuradas. cerca de la configuración canónica (lo que básicamente significa que están orientados de manera que sus ejes ópticos estén lo más paralelos posible y sus campos de visión se superpongan lo suficiente). Sin embargo, esto no parece ser el problema de los OP, ya que logró obtener imágenes bien rectificadas con el método sin calibrar.
Procedimiento básico
- Encuentre al menos 5 puntos bien emparejados en ambas imágenes que pueda usar para calcular la Matriz Fundamental (puede usar cualquier detector y comparador que desee, mantuve FLANN pero usé ORB para hacer la detección ya que SIFT no está en la versión principal de OpenCV para 4.2.0)
- Calcule la matriz fundamental, F, con
findFundamentalMat
- Anula las distorsiones de tus imágenes con
stereoRectifyUncalibrated
ywarpPerspective
- Calcular la disparidad (mapa de profundidad) con
StereoSGBM
Los resultados son mucho mejores:
Coincide con ORB y FLANN
Imágenes sin distorsiones (izquierda, luego derecha)
Disparidad
StereoBM
Este resultado se parece a los problemas de los OP (manchas, huecos, profundidades incorrectas en algunas áreas).
StereoSGBM (sintonizado)
Este resultado se ve mucho mejor y utiliza aproximadamente el mismo método que el OP, menos el cálculo de disparidad final, lo que me hace pensar que el OP vería mejoras similares en sus imágenes, si se hubieran proporcionado.
Post filtrado
Hay un buen artículo sobre esto en los documentos de OpenCV. Recomiendo mirarlo si necesita mapas realmente fluidos.
Las fotos de ejemplo de arriba son el fotograma 1 de la escena. ambush_2
en el conjunto de datos MPI Sintel.
Código completo (probado en OpenCV 4.2.0):
import cv2
import numpy as np
import matplotlib.pyplot as plt
imgL = cv2.imread("tsukuba_l.png", cv2.IMREAD_GRAYSCALE) # left image
imgR = cv2.imread("tsukuba_r.png", cv2.IMREAD_GRAYSCALE) # right image
def get_keypoints_and_descriptors(imgL, imgR):
"""Use ORB detector and FLANN matcher to get keypoints, descritpors,
and corresponding matches that will be good for computing
homography.
"""
orb = cv2.ORB_create()
kp1, des1 = orb.detectAndCompute(imgL, None)
kp2, des2 = orb.detectAndCompute(imgR, None)
############## Using FLANN matcher ##############
# Each keypoint of the first image is matched with a number of
# keypoints from the second image. k=2 means keep the 2 best matches
# for each keypoint (best matches = the ones with the smallest
# distance measurement).
FLANN_INDEX_LSH = 6
index_params = dict(
algorithm=FLANN_INDEX_LSH,
table_number=6, # 12
key_size=12, # 20
multi_probe_level=1,
) # 2
search_params = dict(checks=50) # or pass empty dictionary
flann = cv2.FlannBasedMatcher(index_params, search_params)
flann_match_pairs = flann.knnMatch(des1, des2, k=2)
return kp1, des1, kp2, des2, flann_match_pairs
def lowes_ratio_test(matches, ratio_threshold=0.6):
"""Filter matches using the Lowe's ratio test.
The ratio test checks if matches are ambiguous and should be
removed by checking that the two distances are sufficiently
different. If they are not, then the match at that keypoint is
ignored.
https://stackoverflow.com/questions/51197091/how-does-the-lowes-ratio-test-work
"""
filtered_matches = []
for m, n in matches:
if m.distance < ratio_threshold * n.distance:
filtered_matches.append(m)
return filtered_matches
def draw_matches(imgL, imgR, kp1, des1, kp2, des2, flann_match_pairs):
"""Draw the first 8 mathces between the left and right images."""
# https://docs.opencv.org/4.2.0/d4/d5d/group__features2d__draw.html
# https://docs.opencv.org/2.4/modules/features2d/doc/common_interfaces_of_descriptor_matchers.html
img = cv2.drawMatches(
imgL,
kp1,
imgR,
kp2,
flann_match_pairs[:8],
None,
flags=cv2.DrawMatchesFlags_NOT_DRAW_SINGLE_POINTS,
)
cv2.imshow("Matches", img)
cv2.imwrite("ORB_FLANN_Matches.png", img)
cv2.waitKey(0)
def compute_fundamental_matrix(matches, kp1, kp2, method=cv2.FM_RANSAC):
"""Use the set of good mathces to estimate the Fundamental Matrix.
See https://en.wikipedia.org/wiki/Eight-point_algorithm#The_normalized_eight-point_algorithm
for more info.
"""
pts1, pts2 = [], []
fundamental_matrix, inliers = None, None
for m in matches[:8]:
pts1.append(kp1[m.queryIdx].pt)
pts2.append(kp2[m.trainIdx].pt)
if pts1 and pts2:
# You can play with the Threshold and confidence values here
# until you get something that gives you reasonable results. I
# used the defaults
fundamental_matrix, inliers = cv2.findFundamentalMat(
np.float32(pts1),
np.float32(pts2),
method=method,
# ransacReprojThreshold=3,
# confidence=0.99,
)
return fundamental_matrix, inliers, pts1, pts2
############## Find good keypoints to use ##############
kp1, des1, kp2, des2, flann_match_pairs = get_keypoints_and_descriptors(imgL, imgR)
good_matches = lowes_ratio_test(flann_match_pairs, 0.2)
draw_matches(imgL, imgR, kp1, des1, kp2, des2, good_matches)
############## Compute Fundamental Matrix ##############
F, I, points1, points2 = compute_fundamental_matrix(good_matches, kp1, kp2)
############## Stereo rectify uncalibrated ##############
h1, w1 = imgL.shape
h2, w2 = imgR.shape
thresh = 0
_, H1, H2 = cv2.stereoRectifyUncalibrated(
np.float32(points1), np.float32(points2), F, imgSize=(w1, h1), threshold=thresh,
)
############## Undistort (Rectify) ##############
imgL_undistorted = cv2.warpPerspective(imgL, H1, (w1, h1))
imgR_undistorted = cv2.warpPerspective(imgR, H2, (w2, h2))
cv2.imwrite("undistorted_L.png", imgL_undistorted)
cv2.imwrite("undistorted_R.png", imgR_undistorted)
############## Calculate Disparity (Depth Map) ##############
# Using StereoBM
stereo = cv2.StereoBM_create(numDisparities=16, blockSize=15)
disparity_BM = stereo.compute(imgL_undistorted, imgR_undistorted)
plt.imshow(disparity_BM, "gray")
plt.colorbar()
plt.show()
# Using StereoSGBM
# Set disparity parameters. Note: disparity range is tuned according to
# specific parameters obtained through trial and error.
win_size = 2
min_disp = -4
max_disp = 9
num_disp = max_disp - min_disp # Needs to be divisible by 16
stereo = cv2.StereoSGBM_create(
minDisparity=min_disp,
numDisparities=num_disp,
blockSize=5,
uniquenessRatio=5,
speckleWindowSize=5,
speckleRange=5,
disp12MaxDiff=2,
P1=8 * 3 * win_size ** 2,
P2=32 * 3 * win_size ** 2,
)
disparity_SGBM = stereo.compute(imgL_undistorted, imgR_undistorted)
plt.imshow(disparity_SGBM, "gray")
plt.colorbar()
plt.show()
Puede haber varios problemas posibles que resulten en una baja calidad Depth Channel
y Disparity Channel
lo que nos lleva a una secuencia estéreo de baja calidad. Aquí hay 6 de esos problemas:
Posible problema I
- Fórmula incompleta
Como una palabra uncalibrated
implica, stereoRectifyUncalibrated
El método de instancia calcula una transformación de rectificación por usted, en caso de que no conozca o no pueda conocer los parámetros intrínsecos de su par estéreo y su posición relativa en el entorno.
cv.StereoRectifyUncalibrated(pts1, pts2, fm, imgSize, rhm1, rhm2, thres)
dónde:
# pts1 –> an array of feature points in a first camera
# pts2 –> an array of feature points in a first camera
# fm –> input fundamental matrix
# imgSize -> size of an image
# rhm1 -> output rectification homography matrix for a first image
# rhm2 -> output rectification homography matrix for a second image
# thres –> optional threshold used to filter out outliers
Y tu método se ve de esta manera:
cv2.StereoRectifyUncalibrated(p1fNew, p2fNew, F, (2048, 2048))
Entonces, no tiene en cuenta tres parámetros: rhm1
, rhm2
y thres
. Si un threshold > 0
, todos los pares de puntos que no cumplen con un epipolar la geometría se rechaza antes de calcular las homografías. De lo contrario, todos los puntos se consideran inliers. Esta fórmula se ve así:
(pts2[i]^t * fm * pts1[i]) > thres
# t –> translation vector between coordinate systems of cameras
Por lo tanto, creo que pueden aparecer inexactitudes visuales debido a un cálculo de fórmula incompleto.
Puede leer Calibración de la cámara y Reconstrucción 3D en un recurso oficial.
Posible problema II
- Distancia Interaxial
Un robusto interaxial distance
entre las lentes de la cámara izquierda y derecha deben not greater than 200 mm
. Cuando el interaxial distance
es más grande que el interocular
distancia, el efecto se llama hyperstereoscopy
o hyperdivergence
y resulta no solo en una exageración profunda en la escena, sino también en las molestias físicas del espectador. Lea el informe técnico sobre creación de películas estereoscópicas de Autodesk para obtener más información sobre este tema.
Posible problema III
- Modo de cámara paralelo vs toed-in
Se produjeron inexactitudes visuales Disparity Map
puede ocurrir debido a un cálculo incorrecto del modo de cámara. Muchos estereógrafos prefieren Toe-In camera mode
pero Pixar, por ejemplo, prefiere Parallel camera mode
.
Posible problema IV
- Alineamiento vertical
En estereoscopía, si se produce un desplazamiento vertical (incluso si una de las vistas se desplaza 1 mm hacia arriba), se arruina una experiencia estéreo sólida. Entonces, antes de generar Disparity Map
debe asegurarse de que las vistas izquierda y derecha de su par estéreo estén alineadas en consecuencia. Consulte el documento técnico estereoscópico en tecnicolor sobre 15 problemas comunes en estéreo.
Matriz de rectificación estéreo:
┌ ┐
| f 0 cx tx |
| 0 f cy ty | # use "ty" value to fix vertical shift in one image
| 0 0 1 0 |
└ ┘
Aquí está un StereoRectify
método:
cv.StereoRectify(cameraMatrix1, cameraMatrix2, distCoeffs1, distCoeffs2, imageSize, R, T, R1, R2, P1, P2, Q=None, flags=CV_CALIB_ZERO_DISPARITY, alpha=-1, newImageSize=(0, 0)) -> (roi1, roi2)
Posible problema V
- Distorsión de la lente
La distorsión de la lente es un tema muy importante en la composición estéreo. Antes de generar un Disparity Map
debe anular la distorsión de las vistas izquierda y derecha, después de esto, generar un canal de disparidad y, a continuación, volver a distorsionar ambas vistas.
Posible problema VI
- Canal de profundidad de baja calidad sin suavizado
Para crear una alta calidad Disparity Map
necesitas izquierda y derecha Depth Channels
que debe ser generado previamente. Cuando trabaja en un paquete 3D, puede renderizar un canal de profundidad de alta calidad (con bordes nítidos) con solo un clic. Pero generar un canal de profundidad de alta calidad a partir de una secuencia de video no es fácil porque el par estéreo tiene que moverse en su entorno para producir datos iniciales para el futuro algoritmo de profundidad a partir del movimiento. Si no hay movimiento en un cuadro, el canal de profundidad será extremadamente pobre.
También,
Depth
canal en sí tiene un inconveniente más: sus bordes no coinciden con los bordes del RGB porque no tiene anti-aliasing.
Fragmento de código de canal de disparidad:
Aquí me gustaría representar un enfoque rápido para generar un Disparity Map
:
import numpy as np
import cv2 as cv
from matplotlib import pyplot as plt
imageLeft = cv.imread('paris_left.png', 0)
imageRight = cv.imread('paris_right.png', 0)
stereo = cv.StereoBM_create(numDisparities=16, blockSize=15)
disparity = stereo.compute(imageLeft, imageRight)
plt.imshow(disparity, 'gray')
plt.show()