Оценка положения камеры от гомографии или с помощью функции solvePnP()

Я пытаюсь создать статическую сцену дополненной реальности над фотографией с 4 определенными соответствиями между копланарными точками на плоскости и изображением.

Вот шаг за шагом:

  • Пользователь добавляет изображение с помощью камеры устройства. Предположим, что он содержит прямоугольник, захваченный с некоторой перспективой.
  • Пользователь определяет физический размер прямоугольника, лежащего в горизонтальной плоскости (YOZ с точки зрения SceneKit). Пусть предположим, что центр - это мировое происхождение (0, 0, 0), поэтому мы можем легко найти (x, y, z) для каждого угла.
  • Пользователь определяет координаты uv в системе координат изображения для каждого угла прямоугольника.
  • Сцена SceneKit создается с прямоугольником того же размера и видима с той же точки зрения.
  • Другие узлы могут быть добавлены и перемещены в сцене.

Поток

Я также измерил положение камеры iphone относительно центра бумаги формата А4. Таким образом, для этого снимка положение было (0, 14, 42,5), измеренное в см. Также мой iPhone был слегка разбит на стол (5-10 градусов)

Используя эти данные, я настроил SCNCamera, чтобы получить желаемую перспективу синей плоскости на третьем изображении:

let camera = SCNCamera()
camera.xFov = 66
camera.zFar = 1000
camera.zNear = 0.01

cameraNode.camera = camera
cameraAngle = -7 * CGFloat.pi / 180
cameraNode.rotation = SCNVector4(x: 1, y: 0, z: 0, w: Float(cameraAngle))
cameraNode.position = SCNVector3(x: 0, y: 14, z: 42.5)

Это даст мне ссылку на сравнение моего результата с.

Чтобы построить AR с помощью SceneKit, мне нужно:

  • Отрегулируйте SCNCamera fov, чтобы он соответствовал реальной камере fov.
  • Рассчитать положение и поворот камеры node с использованием 4 корневых точек между точками мира (x, 0, z) и точками изображения (u, v)

Уравнение

H - гомография; K - внутренняя матрица; [R | t] - Внешняя матрица

Я попробовал два подхода, чтобы найти матрицу преобразования для камеры: using solvePnP из OpenCV и ручной расчет из гомографии на основе 4 компланарных точек.

Ручной подход:

1. Узнайте о гомографии

Гомография

Этот шаг выполняется успешно, так как UV-координаты мирового происхождения выглядят правильно.

2. Внутренняя матрица

Чтобы получить внутреннюю матрицу iPhone 6, я использовал это приложение, которое дало мне следующий результат из 100 изображений 640 * 480 Разрешение:

Внутренний

Предполагая, что входное изображение имеет соотношение сторон 4: 3, я могу масштабировать вышеуказанную матрицу в зависимости от разрешения

Intrinsic2

Я не уверен, но это похоже на потенциальную проблему. Я использовал cv:: calibrationMatrixValues ​​для проверки fovx для вычисленной внутренней матрицы, и результат был ~ 50 °, а он должен быть близок к 60 °.

3. Матрица представления камеры

func findCameraPose(homography h: matrix_float3x3, size: CGSize) -> matrix_float4x3? {
    guard let intrinsic = intrinsicMatrix(imageSize: size),
        let intrinsicInverse = intrinsic.inverse else { return nil }

    let l1 = 1.0 / (intrinsicInverse * h.columns.0).norm
    let l2 = 1.0 / (intrinsicInverse * h.columns.1).norm
    let l3 = (l1+l2)/2

    let r1 = l1 * (intrinsicInverse * h.columns.0)
    let r2 = l2 * (intrinsicInverse * h.columns.1)
    let r3 = cross(r1, r2)

    let t = l3 * (intrinsicInverse * h.columns.2)

    return matrix_float4x3(columns: (r1, r2, r3, t))
}

Результат:

Результат

Поскольку я измерил приблизительное положение и ориентацию для этого конкретного изображения, я знаю матрицу преобразования, которая даст ожидаемый результат, и это совсем другое:

Конечный результат

Я также немного conserned около 2-3 элемента матрицы опорного вращения, которая является -9,1, в то время как она должна быть близка к нулю вместо этого, так как существует очень небольшое вращение.

Подход OpenCV:

В OpenCV есть функция solvePnP, поэтому я попытался использовать ее вместо того, чтобы изобретать колесо.

OpenCV в Objective-C ++:

typedef struct CameraPose {
    SCNVector4 rotationVector;
    SCNVector3 translationVector; 
} CameraPose;

+ (CameraPose)findCameraPose: (NSArray<NSValue *> *) objectPoints imagePoints: (NSArray<NSValue *> *) imagePoints size: (CGSize) size {

    vector<Point3f> cvObjectPoints = [self convertObjectPoints:objectPoints];
    vector<Point2f> cvImagePoints = [self convertImagePoints:imagePoints withSize: size];

    cv::Mat distCoeffs(4,1,cv::DataType<double>::type, 0.0);
    cv::Mat rvec(3,1,cv::DataType<double>::type);
    cv::Mat tvec(3,1,cv::DataType<double>::type);
    cv::Mat cameraMatrix = [self intrinsicMatrixWithImageSize: size];

    cv::solvePnP(cvObjectPoints, cvImagePoints, cameraMatrix, distCoeffs, rvec, tvec);

    SCNVector4 rotationVector = SCNVector4Make(rvec.at<double>(0), rvec.at<double>(1), rvec.at<double>(2), norm(rvec));
    SCNVector3 translationVector = SCNVector3Make(tvec.at<double>(0), tvec.at<double>(1), tvec.at<double>(2));
    CameraPose result = CameraPose{rotationVector, translationVector};

    return result;
}

+ (vector<Point2f>) convertImagePoints: (NSArray<NSValue *> *) array withSize: (CGSize) size {
    vector<Point2f> points;
    for (NSValue * value in array) {
        CGPoint point = [value CGPointValue];
        points.push_back(Point2f(point.x - size.width/2, point.y - size.height/2));
    }
    return points;
}

+ (vector<Point3f>) convertObjectPoints: (NSArray<NSValue *> *) array {
    vector<Point3f> points;
    for (NSValue * value in array) {
        CGPoint point = [value CGPointValue];
        points.push_back(Point3f(point.x, 0.0, -point.y));
    }
    return points;
}

+ (cv::Mat) intrinsicMatrixWithImageSize: (CGSize) imageSize {
    double f = 0.84 * max(imageSize.width, imageSize.height);
    Mat result(3,3,cv::DataType<double>::type);
    cv::setIdentity(result);
    result.at<double>(0) = f;
    result.at<double>(4) = f;
    return result;
}

Использование в Swift:

func testSolvePnP() {
    let source = modelPoints().map { NSValue(cgPoint: $0) }
    let destination = perspectivePicker.currentPerspective.map { NSValue(cgPoint: $0)}

    let cameraPose = CameraPoseDetector.findCameraPose(source, imagePoints: destination, size: backgroundImageView.size);    
    cameraNode.rotation = cameraPose.rotationVector
    cameraNode.position = cameraPose.translationVector
}

Вывод:

результат OpenCV Результат SolvePnP

Результат лучше, но далек от моих ожиданий.

Некоторые другие вещи, которые я также пробовал:

  • Этот вопрос очень похож, хотя я не понимаю, как принятый ответ работает без встроенных функций.
  • decomposeHomographyMat также не дал мне ожидаемого результата

Я действительно застрял в этой проблеме, поэтому любая помощь будет очень оценена.

Ответ 1

На самом деле я был в шаге от рабочего решения с OpenCV.

Моя проблема со вторым подходом заключалась в том, что я забыл преобразовать выходные данные из решения solvePnP обратно в систему координат SpriteKit. enter image description here

Обратите внимание, что входные данные (точки изображения и мира) действительно были правильно преобразованы в систему координат OpenCV (convertObjectPoints: и convertImagePoints:withSize: методов)

Итак, вот исправленный метод findCameraPose с напечатанными комментариями и промежуточными результатами:

+ (CameraPose)findCameraPose: (NSArray<NSValue *> *) objectPoints imagePoints: (NSArray<NSValue *> *) imagePoints size: (CGSize) size {

    vector<Point3f> cvObjectPoints = [self convertObjectPoints:objectPoints];
    vector<Point2f> cvImagePoints = [self convertImagePoints:imagePoints withSize: size];

    std::cout << "object points: " << cvObjectPoints << std::endl;
    std::cout << "image points: " << cvImagePoints << std::endl;

    cv::Mat distCoeffs(4,1,cv::DataType<double>::type, 0.0);
    cv::Mat rvec(3,1,cv::DataType<double>::type);
    cv::Mat tvec(3,1,cv::DataType<double>::type);
    cv::Mat cameraMatrix = [self intrinsicMatrixWithImageSize: size];

    cv::solvePnP(cvObjectPoints, cvImagePoints, cameraMatrix, distCoeffs, rvec, tvec);

    std::cout << "rvec: " << rvec << std::endl;
    std::cout << "tvec: " << tvec << std::endl;

    std::vector<cv::Point2f> projectedPoints;
    cvObjectPoints.push_back(Point3f(0.0, 0.0, 0.0));
    cv::projectPoints(cvObjectPoints, rvec, tvec, cameraMatrix, distCoeffs, projectedPoints);

    for(unsigned int i = 0; i < projectedPoints.size(); ++i) {
        std::cout << "Image point: " << cvImagePoints[i] << " Projected to " << projectedPoints[i] << std::endl;
    }


    cv::Mat RotX(3, 3, cv::DataType<double>::type);
    cv::setIdentity(RotX);
    RotX.at<double>(4) = -1; //cos(180) = -1
    RotX.at<double>(8) = -1;

    cv::Mat R;
    cv::Rodrigues(rvec, R);

    R = R.t();  // rotation of inverse
    Mat rvecConverted;
    Rodrigues(R, rvecConverted); //
    std::cout << "rvec in world coords:\n" << rvecConverted << std::endl;
    rvecConverted = RotX * rvecConverted;
    std::cout << "rvec scenekit :\n" << rvecConverted << std::endl;

    Mat tvecConverted = -R * tvec;
    std::cout << "tvec in world coords:\n" << tvecConverted << std::endl;
    tvecConverted = RotX * tvecConverted;
    std::cout << "tvec scenekit :\n" << tvecConverted << std::endl;

    SCNVector4 rotationVector = SCNVector4Make(rvecConverted.at<double>(0), rvecConverted.at<double>(1), rvecConverted.at<double>(2), norm(rvecConverted));
    SCNVector3 translationVector = SCNVector3Make(tvecConverted.at<double>(0), tvecConverted.at<double>(1), tvecConverted.at<double>(2));

    return CameraPose{rotationVector, translationVector};
}

Заметки:

  1. Матрица RotX означает поворот на 180 градусов вокруг оси x, который преобразует любой вектор из системы координат OpenCV в SpriteKit.

  2. Метод Родрига преобразует вектор вращения в матрицу вращения (3x3) и наоборот

Ответ 2

Привет @alexburtnik Я использовал ваш код здесь, но он, кажется, не работает должным образом, и когда я рисую с помощью SceneKit, объект плавает по всей поверхности моего маркера, как если бы он находился над плоскостью маркера, но это не так, когда я двигаюсь в более высокий угол к нему. Посмотрите, что я имею в виду здесь. Линии вокруг телефона сделаны с использованием рисования openCV, поэтому отслеживание хорошо.

Я использую маркеры Aruco для генерации rvec и tvec вместо solvePnP.

Я не могу найти, что я делаю неправильно... любая помощь приветствуется. Извините за публикацию в качестве ответа, но я не могу отформатировать код в комментариях :(

Mat rvec(3, 1, DataType<double>::type);
Mat tvec(3, 1, DataType<double>::type);

...
aruco::estimatePoseBoard(corners, markerIds, gridBoard, self.camMatrix, self.distCoeffs, rvec, tvec);
[self updateCameraProjection:payload withRotation:rvec andTranslation:tvec];
...

-(void) updateCameraProjection:(ArucoPayload *)payload withRotation:(Mat)rvec andTranslation:(Mat)tvec {

    cv::Mat RotX(3, 3, cv::DataType<double>::type);
    cv::setIdentity(RotX);
    RotX.at<double>(4) = -1;
    RotX.at<double>(8) = -1;

    cv::Mat R;
    cv::Rodrigues(rvec, R);

    R = R.t();
    Mat rvecConverted;
    Rodrigues(R, rvecConverted); 
    rvecConverted = RotX * rvecConverted;

    Mat tvecConverted = -R * tvec;
    tvecConverted = RotX * tvecConverted;

    payload.rotationVector = SCNVector4Make(rvecConverted.at<double>(0), rvecConverted.at<double>(1), rvecConverted.at<double>(2), norm(rvecConverted));
    payload.translationVector = SCNVector3Make(tvecConverted.at<double>(0), tvecConverted.at<double>(1), tvecConverted.at<double>(2));
}