[Из песочницы] Калибровка Kinect v2 с помощью OpenCV на Python
Не так давно мы начали пару проектов, в которых необходима оптическая система с каналом дальности, и решили для этого использовать Kinect v2. Поскольку проекты реализуются на Python, то для начала нужно было заставить работать Kinect из Python, а затем откалибровать его, так как Kinect из коробки вносит некоторые геометрические искажения в кадры и дает сантиметровые ошибки в определении глубины.
До этого я никогда не имел дела ни с компьютерным зрением, ни с OpenCV, ни с Kinect. Исчерпывающую инструкцию, как со всем этим хозяйством работать, мне найти тоже не удалось, так что в итоге пришлось порядком повозиться. И я решил, что будет не лишним систематизировать полученный опыт в этой статье. Быть может, она окажется небесполезной для какого-нибудь страждущего, а еще нам нужна популярная статья для галочки в отчетности.
Минимальные системные требования: Windows 8 и выше, Kinect SDK 2.0, USB 3.0.
Таблица I. Характеристики Kinect v2:
Разрешение RGB камеры, пикс. | 1920×1080 |
Разрешение инфракрасной (ИК) камеры, пикс. | 512×424 |
Углы обзора RGB камеры, º | 84.1×53.8 |
Углы обзора ИК камеры, º | 70.6×60.0 |
Диапазон измерений дальности, м. | 0.6 — 8.01 |
Частота съемки RGB камеры, Гц | 30 |
Частота съемки ИК камеры, Гц | 30 |
Таким образом, передо мной стояли следующие задачи:
- завести Kinect на Python;
- откалибровать RGB и ИК камеры;
- реализовать возможность совмещения кадров RGB и ИК;
- откалибровать канал глубины.
А теперь подробно остановимся на каждом пункте.
1. Kinect v2 и Python
Как я уже говорил, до этого я с компьютерным зрением дел не имел, но до меня доходили слухи, что без библиотеки OpenCV тут никуда. А поскольку в ней есть целый модуль для калибровки камер, то первым делом я собрал OpenCV с поддержкой Python 3 под Windows 8.1. Тут не обошлось без некоторой мороки, обычно сопровождающей сборку open-sourсe проектов на Windows, но все прошло без особых сюрпризов и в целом в рамках инструкции от разработчиков.
С Kinect-ом же пришлось повозиться несколько подольше. Официальный SDK поддерживает интерфейсы только для C#, С++ и JavaScript. Если зайти с другой стороны, то можно увидеть, что OpenCV поддерживает ввод с 3D камер, но камера должна быть совместима с библиотекой OpenNI. OpenNI поддерживает Kinect, а вот сравнительно недавний Kinect v2 — нет. Впрочем, добрые люди написали драйвер для Kinect v2 под OpenNI. Он даже работает и позволяет полюбоваться на видео с каналов устройства в NiViewer, но при использовании с OpenCV вылетает с ошибкой. Впрочем, другие добрые люди написали Python-обертку над официальным SDK. На ней я и остановился.
2. Калибровка камер
Камеры не идеальны, искажают картинку и нуждаются в калибровке. Чтобы использовать Kinect для измерений, было бы неплохо устранить эти геометрические искажения как на RGB камере, так и на датчике глубины. Поскольку ИК камера является одновременно и приемником датчика глубины, то мы можем использовать ИК кадры для калибровки, а затем результаты калибровки использовать для устранения искажений с кадров глубины.
Калибровка камеры осуществляется с целью узнать внутренние параметры камеры, а именно — матрицу камеры и коэффициенты дисторсии.
Матрицей камеры называется матрица вида:
где
(сu, cv) — координаты принципиальной точки (точки пересечения оптической оси с плоскостью изображения, в идеальной камере находиться точно в центре изображения, в реальных немного смещена от центра);
fu, fv — фокусное расстояние f, измеренное в ширине и высоте пикселя.
Существуют два основных вида дисторсии: радиальная дисторсия и тангенциальная дисторсия.
Радиальная дисторсия — искажение изображения в результате неидеальности параболической формы линзы. Искажения, вызванные радиальной дисторсией, равны 0 в оптическом центре сенсора и возрастают к краям. Как правило, радиальная дисторсия вносит наибольший вклад в искажение изображения.
Тангенциальная дисторсия — искажения изображения, вызванные погрешностями в установки линзы параллельно плоскости изображения.
Для устранение дисторсии координаты пикселей можно пересчитать с помощью следующего уравнения:
где (u, v) — первоначальное расположение пикселя,
(ucorrected, vcorrected) — расположение пикселя после устранения геометрических искажений,
k1, k2, k3 — коэффициенты радиальной дисторсии,
p1, p2 — коэффициенты тангенциальной дисторсии,
r2=u2+v2.
Точность измерения параметров камеры (коэффициенты дисторсии, матрица камеры) определяется средней величиной ошибки перепроэцирования (ReEr, Reprojection Error). ReEr — расстояние (в пикселях) между проекцией P' на плоскость изображения точки P на поверхности объекта, и проекцией P'' этой же точки P, построенной после устранения дисторсии с использованием параметров камеры.
Стандартная процедура калибровки камеры состоит из следующих шагов:
1) cделать 20–30 фотографий с разными положениями объекта с известной геометрией шахматной доски;
2) определить ключевые точки объекта на изображении;
found, corners = cv2.findChessboardCorners(img, #изображение
PATTERN_SIZE,#сколько ключевых точек, в нашем случае 6x8
flags)#параметры поиска точек
3) найти такие коэффициенты дисторсии которые минимизирует ReEr.
ReEr, camera_matrix, dist_coefs, rvecs, tvecs = cv2.calibrateCamera(obj_points,#координаты ключевых точек в системе координат объекта
#(х', y', z'=0)
img_points,#в системе координат изображения (u,v)
(w, h),#размер изображения
None,#можно использовать уже известную матрицу камеры
None, #можно использовать уже известные коэффициенты дисторсии
criteria = criteria,#критерии окончания минимизации ReEr
flags = flags)#какие коэффициенты дисторсии мы хотим получить
В нашем случае, для RGB камеры среднее значение ReEr составило 0.3 пикселя, а для ИК камеры — 0.15. Результаты устранения дисторсии:
img = cv2.undistort(img, camera_matrix, dist_coefs)
3. Совмещение кадров с двух камер
Для того чтобы получить для пикселя как глубину (Z координату), так и цвет, для начала необходимо перейти из пиксельных координат на кадре глубины в трехмерные координаты ИК камеры [2]:
где (x1, y1, z1) — координаты точки в системе координат ИК камеры,
z1 — результат возвращаемый датчиком глубины,
(u1, v1) — координаты пикселя на кадре глубины,
c1, u, c1, v — координаты оптического центра ИК камеры,
f1, u, f1, v — проекции фокусного расстояния ИК камеры.
Затем нужно перейти из системы координат ИК камеры к системе координат RGB камеры. Для этого требуется переместить начало координат с помощью вектора переноса T и повернуть систему координат с помощью матрицы вращения R:
После чего нужно перейти из трехмерной системы координат RGB камеры к пиксельным координатам RGB кадра:
Таким образом, после всех этих преобразований, мы можем получить для пикселя (u1, v1) кадра глубины значение цвета соответствующего пикселя RGB кадра (u2, v2).
Как видно на результирующей картинке, изображение местами двоится. Такой же эффект можно наблюдать и при использовании класса CoordinateMapper из официального SDK. Впрочем, если на изображении нас интересует только человек, то можно воспользоваться bodyIndexFrame (поток Kinect, позволяющий узнать, какие пиксели относятся к человеку, а какие к фону) для выделения области интереса и устранения двоения.
Для определения матрицы вращения R и вектора переноса T необходимо провести совместную калибровку двух камер. Для этого нужно сделать 20–30 фотографий объекта с известной геометрий в различных положениях как RGB, так и ИК камерой, лучше при этом не держать объект в руках, чтобы исключить возможность его смещения между снятием кадров разными камерами. Затем нужно воспользоваться функцией stereoCalibrate из библиотеки OpenCV. Данная функция определяет позицию каждой из камер относительно калибровочного объекта, а затем находит такое преобразование из системы координат первой камеры в систему координат второй камеры, которое обеспечивает минимизацию ReEr.
retval, cameraMatrix1, distCoeffs1, cameraMatrix2, distCoeffs2, R, T, E, F = cv2.stereoCalibrate(pattern_points, #координаты ключевых
#точек в системе координат объекта (х', y', z'=0)
ir_img_points,#в системе координат ИК камеры (u1, v1)
rgb_img_points, #в системе координат RGB камеры (u2, v2)
irCamera['camera_matrix'],#матрица камеры ИК (брать из calibrateCamera),
irCamera['dist_coefs'], #коэф. дис. ИК камеры (брать из calibrateCamera)
rgbCamera['camera_matrix'], #матрица RGB камеры (брать из calibrateCamera)
rgbCamera['dist_coefs'], #коэф. дис. RGB камеры (брать из calibrateCamera)
image_size) #размер изображения ИК камеры (в пикселях)
И в итоге мы получили ReEr = 0.23.
4. Калибровка канала глубины
Датчик глубины Kinect возвращает глубину (именно глубину, т.е. Z-координату, а не расстояние) в мм. Но насколько точны эти значения? Судя по публикации [2], ошибка может cоставлять 0.5–3 см в зависимости от дистанции, так что есть смысл провести калибровку канала глубины.
Эта процедура заключается в том, чтобы найти систематическую ошибку Kinect (разницу между эталонной глубиной и глубиной, выдаваемой сенсором) в зависимости от расстояния до объекта. А для этого необходимо знать эталонную глубину. Наиболее очевидный путь — расположить плоский объект параллельно плоскости камеры и измерить расстояние до него линейкой. Постепенно сдвигая объект и делая серию измерений на каждом расстоянии, можно найти среднюю ошибку для каждой из дистанций. Но, во-первых, это не очень удобно, во-вторых, найти идеально плоский объект относительно больших размеров и обеспечить параллельность его расположения относительно плоскости камеры сложнее, чем может показаться на первый взгляд. Поэтому в качестве эталона, относительно которого будет рассчитываться ошибка, мы решили взять глубину, определяемую по известной геометрии объекта.
Зная геометрию объекта (например размеры клеток шахматной доски) и расположив его строго параллельно плоскости камеры можно определить глубину до него следующим образом:
где f — фокусное расстояние,
d — расстояние между проекциями ключевых точек на матрице камеры,
D — расстояние между ключевыми точками объекта,
Z — расстояние от центра проекции камеры до объекта.
В случае если объект расположен не строго параллельно, а под некоторым углом к плоскости камеры, глубину можно определить на основе решения задачи Perspective-n-Point (PnP) [3]. Решению этой проблемы посвящен ряд алгоритмов, реализованных в библиотеке OpenCV, которые позволяют найти преобразование |R, T| между системой координат калибровочного объекта и системой координат камеры, а значит, и определить глубину с точностью до параметров камеры.
retval, R, T = cv2.solvePnP(obj_points[:, [0, 5, 42, 47]],#крайние точки в координатах объекта
img_points[:, [0, 5, 42, 47]], #крайние точки в координатах изображения
rgbCameraMatrix,#матрица камеры
rgbDistortion,#коэффициенты дисторсии
flags= cv2.SOLVEPNP_UPNP)#метод решения PnP
R, jacobian = cv2.Rodrigues(R)#переходим от вектора вращения к матрице вращения
for j in range(0, numberOfPoints): # цикл по ключевым точкам
point = numpy.dot(rgb_obj_points[j], R.T) + T.T # Важно! В документации нигде об этом не сказано,
#но по итогам экспериментов с модельными изображениями, выяснилось, что нужно транспонировать матрицу вращения
computedDistance[j] = point[0][2] * 1000 # Z-координата в мм
Для калибровки канала глубины мы произвели серию съемок калибровочного объекта на расстояниях ~0.7–2.6 м с шагом ~7 cм. Калибровочный объект располагался в центре кадра параллельно плоскости камеры, на сколько это возможно сделать «на глазок». На каждом расстоянии делался один снимок RGB камерой и 100 снимков датчиком глубины. Данные с датчика усреднялись, а расстояние, определенное по геометрии объекта на основе RGB кадра, принималось за эталон. Средняя ошибка в определении глубины датчиком Kinect на данной дистанции определилась следующем образом:
где z iRGB — расстояние до i-й ключевой точки по геометрии,
z idepth — усредненное по 100 кадрам расстояние до i-й ключевой точки по данным датчика глубины,
N — количество ключевых точек на объекте (в нашем случае 48).
Затем мы получили функцию ошибки от расстояния путем интерполяции полученных результатов.
На рисунке ниже показано распределение ошибок до и после коррекции на калибровочных кадрах. Всего было сделано 120000 измерений (25 дистанций, 100 кадров глубины на каждой, 48 ключевых точек на объекте). Ошибка до коррекции составила 17±9.95 мм (среднее ± стандартное отклонение), после — 0.45±8.16 мм.
Затем было сделано 25 тестовых кадров (RGB и глубина) калибровочного объекта в различных положениях. Всего 1200 измерений (25 кадров, 48 ключевых точек на каждом). Ошибка до коррекции составила 7.41±6.32 мм (среднее ± стандартное отклонение), после — 3.12±5.50 мм. На рисунке ниже представлено распределение ошибок до и после коррекции на тестовых кадрах.
Заключение
Таким образом, мы устранили геометрические искажения RGB камеры и датчика глубины, научились совмещать кадры и улучшили точность определения глубины. Код этого проекта можно найти тут. Надеюсь, он окажется небесполезным.
Исследование выполнено за счет гранта Российского научного фонда (проект №15–19–30012)
Список источников
1. Kramer J. Hacking the Kinect / Apress. 2012. P. 130
2. Lachat E. et al. First Experiences With Kinect V2 Sensor for Close Range 3D Modelling // International Archives of the Photogrammetry, Remote Sensing and Spatial Information Sciences. 2015.
3. Gao X.S. et al. Complete solution classification for the perspective-three-point problem // IEEE Transactions on Pattern Analysis and Machine Intelligence. Vol. 25. N 8. 2003. P. 930–943.