http://habrahabr.ru/post/272629/
Не так давно мы начали пару проектов, в которых необходима оптическая система с каналом дальности, и решили для этого использовать Kinect v2. Поскольку проекты реализуются на Python, то для начала нужно было заставить работать Kinect из Python, а затем откалибровать его, так как Kinect из коробки вносит некоторые геометрические искажения в кадры и дает сантиметровые ошибки в определении глубины.
До этого я никогда не имел дела ни с компьютерным зрением, ни с OpenCV, ни с Kinect. Исчерпывающую инструкцию, как со всем этим хозяйством работать, мне найти тоже не удалось, так что в итоге пришлось порядком повозиться. И я решил, что будет не лишним систематизировать полученный опыт в этой статье. Быть может, она окажется небесполезной для какого-нибудь страждущего,
а еще нам нужна популярная статья для галочки в отчетности.
Минимальные системные требования: 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 м.
Таким образом, передо мной стояли следующие задачи:
- завести 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.