Skip to content

Commit

Permalink
Remove extra step of converting back to spherical
Browse files Browse the repository at this point in the history
  • Loading branch information
kylecorry31 committed Jan 1, 2024
1 parent 03dea0c commit 4cb1db0
Show file tree
Hide file tree
Showing 6 changed files with 416 additions and 33 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -2,22 +2,20 @@ package com.kylecorry.trail_sense.shared.camera

import android.graphics.RectF
import android.opengl.Matrix
import com.kylecorry.andromeda.camera.ar.CameraAnglePixelMapper
import com.kylecorry.andromeda.camera.ar.LinearCameraAnglePixelMapper
import com.kylecorry.andromeda.camera.ar.SimplePerspectiveCameraAnglePixelMapper
import com.kylecorry.andromeda.core.units.PixelCoordinate
import com.kylecorry.andromeda.sense.orientation.IOrientationSensor
import com.kylecorry.andromeda.sense.orientation.OrientationUtils
import com.kylecorry.sol.math.SolMath
import com.kylecorry.sol.math.SolMath.real
import com.kylecorry.sol.math.SolMath.toDegrees
import com.kylecorry.sol.math.SolMath.toRadians
import com.kylecorry.sol.math.Vector3
import com.kylecorry.sol.math.geometry.Size
import com.kylecorry.sol.units.Coordinate
import com.kylecorry.sol.units.Distance
import com.kylecorry.trail_sense.tools.augmented_reality.AugmentedRealityView
import kotlin.math.asin
import com.kylecorry.trail_sense.tools.augmented_reality.mapper.CameraAnglePixelMapper
import com.kylecorry.trail_sense.tools.augmented_reality.mapper.LinearCameraAnglePixelMapper
import com.kylecorry.trail_sense.tools.augmented_reality.mapper.SimplePerspectiveCameraAnglePixelMapper
import kotlin.math.atan2
import kotlin.math.cos
import kotlin.math.sin
Expand Down Expand Up @@ -115,16 +113,14 @@ object AugmentedRealityUtils {
val d = distance.coerceIn(minDistance, maxDistance)

// Negate the rotation of the device
val spherical = toRelative(bearing, elevation, d, rotationMatrix)
val world = toRelative(bearing, elevation, d, rotationMatrix)

val mapper = mapperOverride ?: defaultMapper

return mapper.getPixel(
spherical.first,
spherical.second,
world,
rect,
fov,
d
fov
)
}

Expand Down Expand Up @@ -155,28 +151,21 @@ object AugmentedRealityUtils {
* @param elevation The elevation in degrees (rotation around the x axis)
* @param distance The distance in meters
*/
private fun toEastNorthUp(
fun toEastNorthUp(
bearing: Float,
elevation: Float,
distance: Float
): Vector3 {
val thetaRad = elevation.toRadians()
val phiRad = bearing.toRadians()
val elevationRad = elevation.toRadians()
val bearingRad = bearing.toRadians()

val cosTheta = cos(thetaRad)
val x = distance * cosTheta * sin(phiRad) // East
val y = distance * cosTheta * cos(phiRad) // North
val z = distance * sin(thetaRad) // Up
val cosElevation = cos(elevationRad)
val x = distance * cosElevation * sin(bearingRad) // East
val y = distance * cosElevation * cos(bearingRad) // North
val z = distance * sin(elevationRad) // Up
return Vector3(x, y, z)
}

private fun toSpherical(vector: Vector3): Vector3 {
val r = vector.magnitude()
val theta = asin(vector.z / r).toDegrees().real(0f)
val phi = atan2(vector.x, vector.y).toDegrees().real(0f)
return Vector3(r, theta, phi)
}

/**
* Converts a geographic spherical coordinate to a relative spherical coordinate in the AR coordinate system.
* @return The relative spherical coordinate (bearing, inclination)
Expand All @@ -186,23 +175,20 @@ object AugmentedRealityUtils {
elevation: Float,
distance: Float,
rotationMatrix: FloatArray
): Pair<Float, Float> {
): Vector3 {
// Convert to world space
val worldVector = toEastNorthUp(bearing, elevation, distance)

// Rotate
val rotated = synchronized(worldVectorLock) {
return synchronized(worldVectorLock) {
tempWorldVector[0] = worldVector.x
tempWorldVector[1] = worldVector.y
tempWorldVector[2] = worldVector.z
tempWorldVector[3] = 1f
Matrix.multiplyMV(tempWorldVector, 0, rotationMatrix, 0, tempWorldVector, 0)
Vector3(tempWorldVector[0], tempWorldVector[1], tempWorldVector[2])
// Swap y and z to convert to AR coordinate system
Vector3(tempWorldVector[0], tempWorldVector[2], tempWorldVector[1])
}

// Convert back to spherical
val spherical = toSpherical(rotated)
return spherical.z to spherical.y
}

}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,6 @@ import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleEventObserver
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.findViewTreeLifecycleOwner
import com.kylecorry.andromeda.camera.ar.CalibratedCameraAnglePixelMapper
import com.kylecorry.andromeda.camera.ar.CameraAnglePixelMapper
import com.kylecorry.andromeda.canvas.CanvasView
import com.kylecorry.andromeda.canvas.TextAlign
import com.kylecorry.andromeda.canvas.TextMode
Expand All @@ -39,6 +37,8 @@ import com.kylecorry.trail_sense.shared.sensors.SensorService
import com.kylecorry.trail_sense.shared.text
import com.kylecorry.trail_sense.shared.textDimensions
import com.kylecorry.trail_sense.shared.views.CameraView
import com.kylecorry.trail_sense.tools.augmented_reality.mapper.CalibratedCameraAnglePixelMapper
import com.kylecorry.trail_sense.tools.augmented_reality.mapper.CameraAnglePixelMapper
import com.kylecorry.trail_sense.tools.augmented_reality.position.ARPoint
import kotlinx.coroutines.Dispatchers
import java.time.Duration
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,219 @@
package com.kylecorry.trail_sense.tools.augmented_reality.mapper

import android.graphics.Rect
import android.graphics.RectF
import com.kylecorry.andromeda.camera.ICamera
import com.kylecorry.andromeda.core.units.PixelCoordinate
import com.kylecorry.sol.math.SolMath.toRadians
import com.kylecorry.sol.math.Vector2
import com.kylecorry.sol.math.Vector3
import com.kylecorry.sol.math.geometry.Size
import kotlin.math.cos
import kotlin.math.max
import kotlin.math.sin

/**
* A camera angle pixel mapper that uses the intrinsic calibration of the camera to map angles to pixels.
* @param camera The camera to use
* @param fallback The fallback mapper to use if the camera does not have intrinsic calibration
* @param applyDistortionCorrection Whether to apply distortion correction. Defaults to false.
*/
class CalibratedCameraAnglePixelMapper(
private val camera: ICamera,
private val fallback: CameraAnglePixelMapper = LinearCameraAnglePixelMapper(),
private val applyDistortionCorrection: Boolean = false
) : CameraAnglePixelMapper {

private var calibration: FloatArray? = null
private var preActiveArray: Rect? = null
private var activeArray: Rect? = null
private var distortion: FloatArray? = null
private val linear = LinearCameraAnglePixelMapper()

private var zoom: Float = 1f
private var lastZoomTime = 0L
private val zoomRefreshInterval = 20L

override fun getAngle(
x: Float,
y: Float,
imageRect: RectF,
fieldOfView: Size
): Vector2 {
// TODO: Figure out how to do this
return fallback.getAngle(x, y, imageRect, fieldOfView)
}

private fun getCalibration(): FloatArray? {
if (calibration == null) {
val calibration = camera.getIntrinsicCalibration(true) ?: return null
val rotation = camera.sensorRotation.toInt()
this.calibration = if (rotation == 90 || rotation == 270) {
floatArrayOf(
calibration[1],
calibration[0],
calibration[3],
calibration[2],
calibration[4]
)
} else {
calibration
}
}
return calibration
}

private fun getPreActiveArraySize(): Rect? {
if (preActiveArray == null) {
val activeArray = camera.getActiveArraySize(true) ?: return null
val rotation = camera.sensorRotation.toInt()
this.preActiveArray = if (rotation == 90 || rotation == 270) {
Rect(activeArray.top, activeArray.left, activeArray.bottom, activeArray.right)
} else {
activeArray
}
}
return preActiveArray
}

private fun getActiveArraySize(): Rect? {
if (activeArray == null) {
val activeArray = camera.getActiveArraySize(false) ?: return null
val rotation = camera.sensorRotation.toInt()
this.activeArray = if (rotation == 90 || rotation == 270) {
Rect(activeArray.top, activeArray.left, activeArray.bottom, activeArray.right)
} else {
activeArray
}
}
return activeArray
}

private fun getDistortion(): FloatArray? {
if (distortion == null) {
distortion = camera.getDistortionCorrection()
}
return distortion
}

private fun getZoom(): Float {
if (System.currentTimeMillis() - lastZoomTime < zoomRefreshInterval) {
return zoom
}
zoom = camera.zoom?.ratio?.coerceAtLeast(0.05f) ?: 1f
lastZoomTime = System.currentTimeMillis()
return zoom
}

override fun getPixel(
angleX: Float,
angleY: Float,
imageRect: RectF,
fieldOfView: Size,
distance: Float?
): PixelCoordinate {
// TODO: Factor in pose translation (just add it to the world position?)
val world = toCartesian(angleX, angleY, distance ?: 1f)
return getPixel(world, imageRect, fieldOfView)
}

override fun getPixel(world: Vector3, imageRect: RectF, fieldOfView: Size): PixelCoordinate {
// Point is behind the camera, so calculate the linear projection
if (world.z < 0) {
return linear.getPixel(world, imageRect, fieldOfView)
}

val calibration = getCalibration()
val preActiveArray = getPreActiveArraySize() ?: getActiveArraySize()
val activeArray = getActiveArraySize()
val distortion = getDistortion()

if (calibration == null || preActiveArray == null || activeArray == null) {
return fallback.getPixel(world, imageRect, fieldOfView)
}

val fx = calibration[0]
val fy = calibration[1]
val cx = calibration[2]
val cy = calibration[3]

// Get the pixel in the pre-active array
val preX = fx * (world.x / world.z) + cx
val preY = fy * (world.y / world.z) + cy

// Correct for distortion
val corrected = if (applyDistortionCorrection && distortion != null) {
undistort(preX, preY, preActiveArray, cx, cy, distortion)
} else {
Vector2(preX, preY)
}

// Translate to the active array
val activeX = corrected.x - activeArray.left
val activeY = corrected.y - activeArray.top

// The y axis is inverted (TODO: Why?)
val invertedY = activeArray.height() - activeY

// Unzoom the output image
val zoom = getZoom()
val rectLeft = imageRect.centerX() - zoom * imageRect.width() / 2f
val rectWidth = zoom * imageRect.width()
val rectTop = imageRect.centerY() - zoom * imageRect.height() / 2f
val rectHeight = zoom * imageRect.height()

// Scale to the output image dimensions
val pctX = activeX / activeArray.width()
val pctY = invertedY / activeArray.height()
val pixelX = pctX * rectWidth + rectLeft
val pixelY = pctY * rectHeight + rectTop

return PixelCoordinate(pixelX, pixelY)
}

private fun undistort(
x: Float,
y: Float,
activeArray: Rect,
cx: Float,
cy: Float,
distortion: FloatArray
): Vector2 {
val sizeX = max(cx - activeArray.left, activeArray.right - cx)
val sizeY = max(cy - activeArray.top, activeArray.bottom - cy)

val normalizedX = (x - cx) / sizeX
val normalizedY = (y - cy) / sizeY

val rSquared = normalizedX * normalizedX + normalizedY * normalizedY

val radialDistortion =
1 + distortion[0] * rSquared + distortion[1] * rSquared * rSquared + distortion[2] * rSquared * rSquared * rSquared

val xc =
normalizedX * radialDistortion + distortion[3] * (2 * normalizedX * normalizedY) + distortion[4] * (rSquared + 2 * normalizedX * normalizedX)
val yc =
normalizedY * radialDistortion + distortion[3] * (rSquared + 2 * normalizedX * normalizedY) + distortion[4] * (2 * normalizedY * normalizedY)

return Vector2(xc * sizeX + cx, yc * sizeY + cy)
}

private fun toCartesian(
bearing: Float,
altitude: Float,
radius: Float
): Vector3 {
val altitudeRad = altitude.toRadians()
val bearingRad = bearing.toRadians()
val cosAltitude = cos(altitudeRad)
val sinAltitude = sin(altitudeRad)
val cosBearing = cos(bearingRad)
val sinBearing = sin(bearingRad)

// X and Y are flipped
val x = sinBearing * cosAltitude * radius
val y = cosBearing * sinAltitude * radius
val z = cosBearing * cosAltitude * radius
return Vector3(x, y, z)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package com.kylecorry.trail_sense.tools.augmented_reality.mapper

import android.graphics.RectF
import com.kylecorry.andromeda.core.units.PixelCoordinate
import com.kylecorry.sol.math.Vector2
import com.kylecorry.sol.math.Vector3
import com.kylecorry.sol.math.geometry.Size

interface CameraAnglePixelMapper {
/**
* Get the real world angles of the pixel.
* @param x The x pixel coordinate
* @param y The y pixel coordinate
* @param imageRect The image rect
* @param fieldOfView The field of view of the camera
* @return The angle (negative is left or below center)
*/
fun getAngle(
x: Float,
y: Float,
imageRect: RectF,
fieldOfView: Size
): Vector2

/**
* Get the pixel coordinate of the real world angle.
* @param angleX The horizontal angle (negative is left of center)
* @param angleY The vertical angle (negative is below center)
* @param imageRect The image rect
* @param fieldOfView The field of view of the camera
* @param distance The distance to the object in meters (optional)
* @return The pixel coordinate
*/
fun getPixel(
angleX: Float,
angleY: Float,
imageRect: RectF,
fieldOfView: Size,
distance: Float? = null
): PixelCoordinate

fun getPixel(
world: Vector3,
imageRect: RectF,
fieldOfView: Size,
): PixelCoordinate
}
Loading

0 comments on commit 4cb1db0

Please sign in to comment.