Не так давно мы начали пару проектов, в которых необходима оптическая система с каналом дальности, и решили для этого использовать Kinect v2. Поскольку проекты реализуются на Python, то для начала нужно было заставить работать Kinect из Python, а затем откалибровать его, так как Kinect из коробки вносит некоторые геометрические искажения в кадры и дает сантиметровые ошибки в определении глубины.

До этого я никогда не имел дела ни с компьютерным зрением, ни с OpenCV, ни с Kinect. Исчерпывающую инструкцию, как со всем этим хозяйством работать, мне найти тоже не удалось, так что в итоге пришлось порядком повозиться. И я решил, что будет не лишним систематизировать полученный опыт в этой статье. Быть может, она окажется небесполезной для какого-нибудь страждущего, а еще нам нужна популярная статья для галочки в отчетности.

image

Минимальные системные требования: Windows 8 и выше, Kinect SDK 2.0, USB 3.0.

Таблица I. Характеристики Kinect v2:
Разрешение RGB камеры, пикс. 1920 x 1080
Разрешение инфракрасной (ИК) камеры, пикс. 512 x 424
Углы обзора RGB камеры, ? 84.1 x 53.8
Углы обзора ИК камеры, ? 70.6 x 60.0
Диапазон измерений дальности, м. 0.6 — 8.01
Частота съемки RGB камеры, Гц 30
Частота съемки ИК камеры, Гц 30

1
Информация различается от источника к источнику. Тут говорят о 0.5–4.5 м., по факту я получал ~0.6-8.0 м.

Таким образом, передо мной стояли следующие задачи:

  1. завести Kinect на Python;
  2. откалибровать RGB и ИК камеры;
  3. реализовать возможность совмещения кадров RGB и ИК;
  4. откалибровать канал глубины.

А теперь подробно остановимся на каждом пункте.

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 камере, так и на датчике глубины. Поскольку ИК камера является одновременно и приемником датчика глубины, то мы можем использовать ИК кадры для калибровки, а затем результаты калибровки использовать для устранения искажений с кадров глубины.

Калибровка камеры осуществляется с целью узнать внутренние параметры камеры, а именно — матрицу камеры и коэффициенты дисторсии.

image

Матрицей камеры называется матрица вида:

где

(с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, построенной после устранения дисторсии с использованием параметров камеры.

image

Стандартная процедура калибровки камеры состоит из следующих шагов:

1) cделать 20-30 фотографий с разными положениями объекта с известной геометрией шахматной доски;

image

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.

Комментарии (9)


  1. ZlodeiBaal
    08.12.2015 12:40
    +1

    В нашем случае, для RGB камеры среднее значение ReEr составило 0.3 пикселя, а для ИК камеры — 0.15

    Ну, то есть вы понимаете, что коррекция дисторсии для Киннекта — бессмысленна? Для примера возьмите фотоаппарат с таким же фокусным расстоянием, который даёт сырой кадр, без обработки. И посмотрите значение ReEr на нём. Там будут большие значения. 0.3 и 0.15 пикселя — значит всё уже скомпенсировано программно кинектом. И, скорее всего, значительно точнее, чем это можно сделать OpenCV. А 0.5 пикселя это наверняка будет погрешность для метода измерения дисторсии через OpenCV.

    Про калибровку расстояния вообще не понял. Судя по всему там системная ошибка. Вы:
    1) придумали тест, которым оцениваете расстояние
    2) скомпенсировали ошибку показанную данным тестом
    3) провели тот же самый тест
    4) подтвердили что ошибка теста устранена.

    Почему вы считаете, что это ошибка kinect, а не ошибка теста?
    Более того, я считаю, ошибка скорее всего вызвана именно той дисторсией, которая скомпенсирована для оптического потока самим киннектом. Я думаю что вы знаете, что исправление дисторсии — компенсирует прямые, но искажает расстояние. В расстояния вноситься дополнительная ошибка — > оптическая камера недостоверный инструмент измерения ошибки.

    Больше всего, конечно, удивляет фраза
    Исследование выполнено за счет гранта Российского научного фонда (проект №15-19-30012)

    Я надеюсь, что у нас сейчас государство не выделяет гранты на калибровку Кинекта, это более серьёзная какая-то работа была?


    1. BelBES
      08.12.2015 13:49
      +1

      Больше всего, конечно, удивляет фраза
      Исследование выполнено за счет гранта Российского научного фонда (проект №15-19-30012)

      Это не удивляет, а многое объясняет :-)
      А вообще по поводу второго кинекта хз, но для первого калибровка имела смысл и без неё методы 3D реконструкции сыпались.


      1. ZlodeiBaal
        08.12.2015 13:54

        Если бы калибровка была сделана по линейке — я бы поверил)
        Но калибровка по второй камере вызывает у меня морю вопросов))
        Правда, не удивлюсь, если погрешность реально есть.

        С Киннектом не игрался, только с RealSense. На втором проверял, расстояния с неплохой точностью определялись.


    1. tataraidze
      08.12.2015 13:53
      +1

      для RGB камеры среднее значение ReEr составило 0.3 пикселя, а для ИК камеры — 0.15

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

      Я надеюсь, что у нас сейчас государство не выделяет гранты на калибровку Кинекта, это более серьёзная какая-то работа была?

      Разумеется. Это скромная часть большего проекта, в котором нам нужна оптическая подсистема. С предварительными результатами за этом год по другим частям проекта можно ознакомиться тут и тут. Согласно правилам РНФ, любые публикации по результатам работ, выполненных при поддержке фонда, должны содержать ссылку на него.

      Почему вы считаете, что это ошибка kinect, а не ошибка теста?

      Ошибка Kinect-а может измеряться сантиметрами (см. 2 публикацию из списка источников). Определение расстояния по геометрии тоже неидеально, но дает уже миллиметровые ошибки. Для того чтобы в этом удостовериться, можно тем же способом определить расстояние по смоделированным изображением, когда мы знаем абсолютно точное расстояние до каждой точки.


      1. ZlodeiBaal
        08.12.2015 14:15
        +1

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

        Ну, рассматривать картинку плохого качества неинтересно. А единственные цифры говорят что дисторсии нет. Привели бы ошибку до и после.
        тут и тут

        Да, это куда интереснее, чем данная статья:)
        Может лучше было написать про разработанный агрегат?:)
        Определение расстояния по геометрии тоже неидеально, но дает уже миллиметровые ошибки.

        Вопрос не в этом. Вы делаете следующую последовательность действий: эксперимент «А»-> компенсация ошибки эксперимента — > повторный эксперимент «А».
        При этом сам эксперимент «А» не даёт прямого измерения, а только косвенное «I». И для этого измерения вы не проводите верификации.
        Вполне может произойти что вы подменяете одну систематическую ошибку другой.
        По хорошему нужно либо проверить метод измерения «I» и доказать что он корректный, либо после компенсации эксперимента верифицировать результат не экспериментом «A», а независимым экспериментом «B» содержащим прямое измерение.


        1. tataraidze
          08.12.2015 16:56

          Может лучше было написать про разработанный агрегат?:)

          Думаю, напишем чуть позже.

          Вполне может произойти что вы подменяете одну систематическую ошибку другой.

          В известном смысле так и есть, но ошибка определения по геометрии (метода «I») значительно меньше, чем ошибка Kinect-а. Я согласен, было бы лучше взять заведомо плоский объект, расположить его параллельно плоскости камеры и мерить до него дистанцию лазерным дальномером, как это делали Lachat E. et al. Но это заметно сложнее и затратнее, чем калиброваться по геометрии. Что касается корректности этого метода, то ее мы проверяли на модельных изображениях, и ошибка не превышала нескольких мм., что априори лучше того, что дает Kinect.


  1. ZlodeiBaal
    08.12.2015 14:14

    веткой промазал
    del


  1. iroln
    08.12.2015 20:34

    Исследование выполнено
    Калибровка стереопары не является самоцелью — это необходимость в любой задаче машинного зрения, где она применяется. Вы это делали в рамках какого-то исследования или «оценка точности Kinect и её калибровка» и есть само исследование?


    1. tataraidze
      08.12.2015 21:13

      В рамках конечно. Это не более чем заметка о решении технической задачи, которая встретилась во время проведения исследования.