iOS revierte la proyección de la cámara

Estoy tratando de estimar la posición de mi dispositivo relacionada con un código QR en el espacio. Estoy usando ARKit y el marco Vision, ambos introducidos en iOS11, pero la respuesta a esta pregunta probablemente no dependa de ellos.

Con el marco Vision, puedo obtener el rectángulo que limita un código QR en el marco de la cámara. Me gustaría hacer coincidir este rectángulo con la traducción y rotación del dispositivo necesarias para transformar el código QR desde una posición estándar.

Por ejemplo, si observo el marco:

*            *

    B
          C
  A
       D


*            *

mientras que si estuviera a 1 m del código QR, centrado en él, y suponiendo que el código QR tenga un lado de 10 cm, vería:

*            *


    A0  B0

    D0  C0


*            *

¿Cuál ha sido la transformación de mi dispositivo entre esos dos cuadros? Entiendo que un resultado exacto podría no ser posible, porque quizás el código QR observado es ligeramente no plano y estamos tratando de estimar una transformación afín en algo que no es uno perfectamente.

Supongo quesceneView.pointOfView?.camera?.projectionTransform es más útil que elsceneView.pointOfView?.camera?.projectionTransform?.camera.projectionMatrix dado que el último ya tiene en cuenta la transformación inferida del ARKit que no me interesa para este problema.

¿Cómo llenaría

func get transform(
  qrCodeRectangle: VNBarcodeObservation,
  cameraTransform: SCNMatrix4) {
  // qrCodeRectangle.topLeft etc is the position in [0, 1] * [0, 1] of A0

  // expected real world position of the QR code in a referential coordinate system
  let a0 = SCNVector3(x: -0.05, y: 0.05, z: 1)
  let b0 = SCNVector3(x: 0.05, y: 0.05, z: 1)
  let c0 = SCNVector3(x: 0.05, y: -0.05, z: 1)
  let d0 = SCNVector3(x: -0.05, y: -0.05, z: 1)

  let A0, B0, C0, D0 = ?? // CGPoints representing position in
                          // camera frame for camera in 0, 0, 0 facing Z+

  // then get transform from 0, 0, 0 to current position/rotation that sees
  // a0, b0, c0, d0 through the camera as qrCodeRectangle 
}

==== Editar ====

Después de probar varias cosas, terminé yendo a la estimación de la postura de la cámara usando la proyección openCV y el solucionador de perspectiva,solvePnP Esto me da una rotación y traducción que debería representar la pose de la cámara en el código QR referencial. Sin embargo, al usar esos valores y colocar objetos correspondientes a la transformación inversa, donde el código QR debe estar en el espacio de la cámara, obtengo valores desplazados inexactos y no puedo hacer que la rotación funcione:

// some flavor of pseudo code below
func renderer(_ sender: SCNSceneRenderer, updateAtTime time: TimeInterval) {
  guard let currentFrame = sceneView.session.currentFrame, let pov = sceneView.pointOfView else { return }
  let intrisics = currentFrame.camera.intrinsics
  let QRCornerCoordinatesInQRRef = [(-0.05, -0.05, 0), (0.05, -0.05, 0), (-0.05, 0.05, 0), (0.05, 0.05, 0)]

  // uses VNDetectBarcodesRequest to find a QR code and returns a bounding rectangle
  guard let qr = findQRCode(in: currentFrame) else { return }

  let imageSize = CGSize(
    width: CVPixelBufferGetWidth(currentFrame.capturedImage),
    height: CVPixelBufferGetHeight(currentFrame.capturedImage)
  )

  let observations = [
    qr.bottomLeft,
    qr.bottomRight,
    qr.topLeft,
    qr.topRight,
  ].map({ (imageSize.height * (1 - $0.y), imageSize.width * $0.x) })
  // image and SceneKit coordinated are not the same
  // replacing this by:
  // (imageSize.height * (1.35 - $0.y), imageSize.width * ($0.x - 0.2))
  // weirdly fixes an issue, see below

  let rotation, translation = openCV.solvePnP(QRCornerCoordinatesInQRRef, observations, intrisics)
  // calls openCV solvePnP and get the results

  let positionInCameraRef = -rotation.inverted * translation
  let node = SCNNode(geometry: someGeometry)
  pov.addChildNode(node)
  node.position = translation
  node.orientation = rotation.asQuaternion
}

Aquí está la salida:

donde A, B, C, D son las esquinas del código QR en el orden en que se pasan al programa.

El origen predicho permanece en su lugar cuando el teléfono gira, pero se desplaza desde donde debería estar. Sorprendentemente, si cambio los valores de las observaciones, puedo corregir esto:

  // (imageSize.height * (1 - $0.y), imageSize.width * $0.x)
  // replaced by:
  (imageSize.height * (1.35 - $0.y), imageSize.width * ($0.x - 0.2))

y ahora el origen previsto se mantiene firmemente en su lugar. Sin embargo, no entiendo de dónde provienen los valores de cambio.

Finalmente, intenté obtener una orientación fija relativamente al código QR referencial:

    var n = SCNNode(geometry: redGeometry)
    node.addChildNode(n)
    n.position = SCNVector3(0.1, 0, 0)
    n = SCNNode(geometry: blueGeometry)
    node.addChildNode(n)
    n.position = SCNVector3(0, 0.1, 0)
    n = SCNNode(geometry: greenGeometry)
    node.addChildNode(n)
    n.position = SCNVector3(0, 0, 0.1)

La orientación está bien cuando miro el código QR directamente, pero luego cambia por algo que parece estar relacionado con la rotación del teléfono:

Las preguntas pendientes que tengo son:

¿Cómo resuelvo la rotación?¿De dónde vienen los valores de cambio de posición?¿Qué relación simple verifican la rotación, la traducción, QRCornerCoordinatesInQRRef, las observaciones, las intrínsecas? ¿Es O ~ K ^ -1 * (R_3x2 | T) Q? Porque si es así, eso está fuera de lugar en un orden de magnitud.

Si eso es útil, aquí hay algunos valores numéricos:

Intrisics matrix
Mat 3x3
1090.318, 0.000, 618.661
0.000, 1090.318, 359.616
0.000, 0.000, 1.000

imageSize
1280.0, 720.0
screenSize
414.0, 736.0

==== Editar2 ====

Me di cuenta de que la rotación funciona bien cuando el teléfono permanece horizontalmente paralelo al código QR (es decir, la matriz de rotación es [[a, 0, b], [0, 1, 0], [c, 0, d]] ), independientemente de la orientación real del código QR:

Otra rotación no funciona.

Respuestas a la pregunta(2)

Su respuesta a la pregunta