Плохая карта несоответствий с использованием StereoBM в OpenCV

Я собрал стереокамеру, и у меня проблемы с ее использованием, чтобы создать хорошую карту несоответствий. Здесь приведен пример двух исправленных изображений и карты несоответствий, которые я создал с ними:

Rectified images with disparity map

Как вы можете видеть, результаты довольно плохие. Изменение настроек StereoBM не сильно меняется.

Настройка

  • Обе камеры являются одной и той же моделью и подключаются к компьютеру с USB.
  • Они прикреплены к жесткой деревянной доске, чтобы они не двигались. Я выровнял их как мог, но, конечно, это не идеально. Они не могут двигаться, поэтому их позиции во время и после калибровки одинаковы.
  • Я откалибровал стереопары с использованием OpenCV и использую класс OpenCV StereoBM для создания карты несоответствий.
  • Возможно, это не так актуально, но я кодирую в Python.

Проблемы, которые я мог себе представить

Я делаю это в первый раз, поэтому я далек от того, чтобы быть экспертом, но я предполагаю, что проблема заключается в калибровке или в стереоректификации, а не в вычислении карты несоответствия. Я пробовал все перестановки настроек для StereoBM и, хотя я получаю разные результаты, они все похожи на карту несоответствий, показанную выше: Патчи черно-белого.

Эта идея также подтверждается тем фактом, что, как я понимаю, стереоректификация должна выровнять все точки на каждом изображении так, чтобы они были связаны прямой (в моем случае горизонтальной) линией. Если я рассмотрю обе исправленные изображения рядом друг с другом, то сразу же очевидно, что это не так. Соответствующие точки намного выше на правой картинке, чем слева. Я не уверен, является ли калибровка или исправление проблемой.

Код

Фактический код обернут в объекты - в случае, если вам интересно увидеть его целиком, он доступен в GitHub. Вот упрощенный пример того, что на самом деле выполняется (конечно, в реальном коде я откалибрую, используя не более двух изображений):

import cv2
import numpy as np

## Load test images
# TEST_IMAGES is a list of paths to test images
input_l, input_r = [cv2.imread(image, cv2.CV_LOAD_IMAGE_GRAYSCALE)
                    for image in TEST_IMAGES]
image_size = input_l.shape[:2]

## Retrieve chessboard corners
# CHESSBOARD_ROWS and CHESSBOARD_COLUMNS are the number of inside rows and
# columns in the chessboard used for calibration
pattern_size = CHESSBOARD_ROWS, CHESSBOARD_COLUMNS
object_points = np.zeros((np.prod(pattern_size), 3), np.float32)
object_points[:, :2] = np.indices(pattern_size).T.reshape(-1, 2)
# SQUARE_SIZE is the size of the chessboard squares in cm
object_points *= SQUARE_SIZE
image_points = {}
ret, corners_l = cv2.findChessboardCorners(input_l, pattern_size, True)
cv2.cornerSubPix(input_l, corners_l,
                 (11, 11), (-1, -1),
                 (cv2.TERM_CRITERIA_MAX_ITER + cv2.TERM_CRITERIA_EPS,
                  30, 0.01))
image_points["left"] = corners_l.reshape(-1, 2)
ret, corners_r = cv2.findChessboardCorners(input_r, pattern_size, True)
cv2.cornerSubPix(input_r, corners_r,
                 (11, 11), (-1, -1),
                 (cv2.TERM_CRITERIA_MAX_ITER + cv2.TERM_CRITERIA_EPS,
                  30, 0.01))
image_points["right"] = corners_r.reshape(-1, 2)

## Calibrate cameras
(cam_mats, dist_coefs, rect_trans, proj_mats, valid_boxes,
 undistortion_maps, rectification_maps) = {}, {}, {}, {}, {}, {}, {}
criteria = (cv2.TERM_CRITERIA_MAX_ITER + cv2.TERM_CRITERIA_EPS,
            100, 1e-5)
flags = (cv2.CALIB_FIX_ASPECT_RATIO + cv2.CALIB_ZERO_TANGENT_DIST +
         cv2.CALIB_SAME_FOCAL_LENGTH)
(ret, cam_mats["left"], dist_coefs["left"], cam_mats["right"],
 dist_coefs["right"], rot_mat, trans_vec, e_mat,
 f_mat) = cv2.stereoCalibrate(object_points,
                              image_points["left"], image_points["right"],
                              image_size, criteria=criteria, flags=flags)
(rect_trans["left"], rect_trans["right"],
 proj_mats["left"], proj_mats["right"],
 disp_to_depth_mat, valid_boxes["left"],
 valid_boxes["right"]) = cv2.stereoRectify(cam_mats["left"],
                                           dist_coefs["left"],
                                           cam_mats["right"],
                                           dist_coefs["right"],
                                           image_size,
                                           rot_mat, trans_vec, flags=0)
for side in ("left", "right"):
    (undistortion_maps[side],
     rectification_maps[side]) = cv2.initUndistortRectifyMap(cam_mats[side],
                                                           dist_coefs[side],
                                                           rect_trans[side],
                                                           proj_mats[side],
                                                           image_size,
                                                           cv2.CV_32FC1)

## Produce disparity map
rectified_l = cv2.remap(input_l, undistortion_maps["left"],
                        rectification_maps["left"],
                        cv2.INTER_NEAREST)
rectified_r = cv2.remap(input_r, undistortion_maps["right"],
                        rectification_maps["right"],
                        cv2.INTER_NEAREST)
cv2.imshow("left", rectified_l)
cv2.imshow("right", rectified_r)
block_matcher = cv2.StereoBM(cv2.STEREO_BM_BASIC_PRESET, 0, 5)
disp = block_matcher.compute(rectified_l, rectified_r, disptype=cv2.CV_32F)
cv2.imshow("disparity", disp)

Что здесь не так?

Ответ 1

Оказалось, что проблема заключалась в визуализации, а не в самих данных. Где-то я читал, что cv2.reprojectImageTo3D потребовал, чтобы карта различий была как значения с плавающей запятой, поэтому я запрашивал cv2.CV_32F из block_matcher.compute.

Чтение документации OpenCV более тщательно привело меня к мысли, что я думал об этом по ошибке, и я действительно хотел бы работать с целыми числами, чем с плавающей точкой для скорости, но документация для cv2.imshow не была ясно, что он делает с 16-битными целыми знаками (по сравнению с 16-битным без знака), поэтому для визуализации я оставляю значения как плавающие.

Документация cv2.imshow показывает, что 32-битные значения с плавающей запятой считаются между 0 и 1, поэтому они умножаются на 255. 255 - точка насыщения, при которой пиксель отображается как белый. В моем случае это предположение породило двоичную карту. Я вручную масштабировал его до диапазона 0-255, а затем разделил его на 255, чтобы скрыть тот факт, что OpenCV делает то же самое. Я знаю, это ужасная операция, но я делаю это только для того, чтобы настроить мой StereoBM офлайн, поэтому производительность некритична. Решение выглядит так:

# Other code as above
disp = block_matcher.compute(rectified_l, rectified_r, disptype=cv2.CV_32F)
norm_coeff = 255 / disp.max()
cv2.imshow("disparity", disp * norm_coeff / 255)

Тогда карта несоответствий выглядит нормально.