ARKit Project 2D point to captured image coordinate system - ios

I would like to project ARPlaneAnchor point to captured image coordinate system.
At this moment i am able to get plane point and project it to view coordinate system by method
func translateTransform(_ x: Float, _ y: Float, _ z: Float) -> float4x4 {
var tf = float4x4(diagonal: SIMD4<Float>(repeating: 1))
tf.columns.3 = SIMD4<Float>(x: x, y: y, z: z, w: 1)
return tf
}
extension ARPlaneAnchor {
func worldPoints() -> (SCNVector3, SCNVector3, SCNVector3, SCNVector3) {
let worldTransform = transform * translateTransform(center.x, 0, center.z)
let width = extent.x
let height = extent.z
let topLeft = worldTransform * translateTransform(-width / 2.0, 0, -height / 2.0)
let topRight = worldTransform * translateTransform(width / 2.0, 0, -height / 2.0)
let bottomLeft = worldTransform * translateTransform(-width / 2.0, 0, height / 2.0)
let bottomRight = worldTransform * translateTransform(width / 2.0, 0, height / 2.0)
let pointTopLeft = SCNVector3(
x: topLeft.columns.3.x,
y: topLeft.columns.3.y,
z: topLeft.columns.3.z
)
let pointTopRight = SCNVector3(
x: topRight.columns.3.x,
y: topRight.columns.3.y,
z: topRight.columns.3.z
)
let pointBottomLeft = SCNVector3(
x: bottomLeft.columns.3.x,
y: bottomLeft.columns.3.y,
z: bottomLeft.columns.3.z
)
let pointBottomRight = SCNVector3(
x: bottomRight.columns.3.x,
y: bottomRight.columns.3.y,
z: bottomRight.columns.3.z
)
return (
pointTopLeft,
pointTopRight,
pointBottomLeft,
pointBottomRight
)
}
And then by projectPoint convert 3D point to 2D like in example
guard let point = arPlane?.worldPoints().0 else { return }
let pp = self.sceneView.session.currentFrame?.camera.projectPoint(SIMD3.init(point), orientation: .landscapeRight, viewportSize: self.view.bounds.size)
But how to convert this point to capturedImage coordinate system?

Related

Decomposing CGAffineTransform in rotate, scale, and translate components

I am trying to break CGAffineTransform into rotate, scale, and translate components as follows:
public extension CGAffineTransform {
func rotationRadians() -> CGFloat {
return atan2(b, a)
}
func translation() -> CGPoint {
return CGPoint(x: tx, y: ty)
}
func scaleXY() -> CGPoint {
let scalex = sqrt(a * a + c * c)
let scaley = sqrt(d * d + b * b)
return CGPoint(x: scalex, y: scaley)
}
}
Now I try this code on random transform by breaking it and then constructing it back from the same components, but the answer never comes out to be same, no matter what order I use in concatenation. Not sure what I am doing wrong.
let randomTransform = __CGAffineTransformMake(2.078, 3.459, 1.676, 0, 591, 397)
let rotationRadians = randomTransform.rotationRadians()
let scaleXY = randomTransform.scaleXY()
let translation = randomTransform.translation()
let translationTransform = CGAffineTransform(translationX: translation.x, y: translation.y)
let scalingTransform = CGAffineTransform(scaleX: scaleXY.x, y: scaleXY.y)
let rotationTransform = CGAffineTransform(rotationAngle: rotationRadians)
var newTransform = rotationTransform.concatenating(scalingTransform).concatenating(translationTransform)
NSLog("New transform \(newTransform), original \(randomTransform)")
Here is the output from console:
New transform CGAffineTransform(a: 1.374790977280978, b: 2.965084308709465, c: -2.2884513909600113, d: 1.7812793274062642, tx: 591.0, ty: 397.0), original CGAffineTransform(a: 2.078, b: 3.459, c: 1.676, d: 0.0, tx: 591.0, ty: 397.0)
What you're trying to do is basically impossible, although it might be approachable in some instances using some sophisticated mathematical techniques. See http://callumhay.blogspot.com/2010/10/decomposing-affine-transforms.html for an example.

UICollectionViewLayoutAttributes and transform

I am using the below to modify UICollectionViewLayoutAttributes. Method 1 works but not Method 2, anyone can help explain the difference?
guard let newAttrs = attributes?.copy() as? UICollectionViewLayoutAttributes,
let imageViewInfo = imageViewInfo else { return nil }
let center = imageViewInfo.center
//Method 1
newAttrs.transform = CGAffineTransform(scaleX: scale, y: scale)
let x = (newAttrs.center.x-center.x) * scale + igCenter.x
let y = (newAttrs.center.y-center.y) * scale + igCenter.y
newAttrs.center = CGPoint(x: x, y: y)
//Method 2
newAttrs.transform = CGAffineTransform(translationX: -center.x, y: -center.y)
.scaledBy(x: scale, y: scale)
.translatedBy(x: igCenter.x, y: igCenter.y)
https://developer.apple.com/documentation/uikit/uiview/1622459-transform
to change position of the view/cell, modify 'center' instead.

ARCamera.projectPoint swift implementation

I am trying to implement an ARCamera.projectPoint function using projectionMatrix and viewMatrix from the camera.
Going to create something like this:
let position = simd_float3(x: 1, y: 2, z: 3)
let position4 = simd_float4(x: position.x, y: position.y, z: position.z, w: 1)
let projectionMatrix = frame.camera.projectionMatrix(for: .landscapeRight, viewportSize: frame.camera.imageResolution, zNear: 0.001, zFar: 1000.0)
let viewMatrix = frame.camera.viewMatrix(for: .landscapeRight)
let projection = position4 * projectionMatrix * viewMatrix
let arkitProjection = frame.camera.projectPoint(position, orientation: .landscapeRight, viewportSize: frame.camera.imageResolution)
assert(projection.x == Float(arkitProjection.x))
assert(projection.y == Float(arkitProjection.y))
But I don't know how to implement it correctly. Hope for you help.
You were very close! What you have in projection is actually a "clip space" position (also note I fixed the order of the matrix multiplication). You then need to convert that to a "normalized device coordinate" (which has coordinates from -1 to +1 in xyz dimensions) using "perspective divide". Then finally you need to linear map it to the position in the image based on the image size.
let position = simd_float3(x: 1, y: 2, z: 3)
let position4 = simd_float4(x: position.x, y: position.y, z: position.z, w: 1)
let projectionMatrix = frame.camera.projectionMatrix(for: .landscapeRight, viewportSize: frame.camera.imageResolution, zNear: 0.001, zFar: 1000.0)
let viewMatrix = frame.camera.viewMatrix(for: .landscapeRight)
let clipSpacePosition = projectionMatrix * viewMatrix * position4
let normalizedDeviceCoordinate = clipSpacePosition / clipSpacePosition.w
let projection = CGPoint(
x: (CGFloat(normalizedDeviceCoordinate.x) + 1) * (frame.camera.imageResolution.width / CGFloat(2)),
y: ((-CGFloat(normalizedDeviceCoordinate.y) + 1)) * (frame.camera.imageResolution.height / CGFloat(2))
)
let arkitProjection = frame.camera.projectPoint(position, orientation: .landscapeRight, viewportSize: frame.camera.imageResolution)
assert(abs(projection.x - arkitProjection.x) < 0.01)
assert(abs(projection.y - arkitProjection.y) < 0.01)
Further reading:
http://www.opengl-tutorial.org/beginners-tutorials/tutorial-3-matrices/#the-projection-matrix (generally useful info about clip space and projection)
https://developer.apple.com/documentation/arkit/tracking_and_visualizing_faces (search "perspective divide")
https://learnopengl.com/Getting-started/Coordinate-Systems

Creating a curly bracket curve from two points

I'm trying to create a curly bracket in Swift, from two points. The idea works fine, with a straight line, because it's currently not dynamic in anyway. My issue lies in finding the dynamic control points and center depending on the location of p1 and p2 points.
This is my current code:
override func viewDidLoad() {
super.viewDidLoad()
let path = UIBezierPath()
let p1 = CGPointMake(100, 100)
let p2 = CGPointMake(300, 100)
let c1 = CGPointMake(150, 80)
let c2 = CGPointMake(250, 80)
var midPoint = midPointForPoints(p1, p2: p2)
var midP1 = midPoint
midP1.x -= 10
var midP2 = midPoint
midP2.x += 10
midPoint.y -= 20
path.moveToPoint(p1)
path.addQuadCurveToPoint(midP1, controlPoint: c1)
path.addLineToPoint(midPoint)
path.addLineToPoint(midP2)
path.addQuadCurveToPoint(p2, controlPoint: c2)
let shape = CAShapeLayer()
shape.lineWidth = 5
shape.strokeColor = UIColor.redColor().CGColor
shape.fillColor = UIColor.clearColor().CGColor
shape.path = path.CGPath
self.view.layer.addSublayer(shape)
}
func midPointForPoints(p1: CGPoint, p2: CGPoint)->CGPoint{
let deltaX = (p1.x + p2.x)/2
let deltaY = (p1.y + p2.y)/2
let midPoint = CGPointMake(deltaX, deltaY)
return midPoint
}
This doesen't take the degrees of the points into account, so if I were to create the two points as:
let p1 = CGPointMake(100, 100)
let p2 = CGPointMake(300, 300)
It would not find the proper control points and midpoint.
Hope someone can help me in the right direction. The idea is of course in the end to just know the two points (p1, p2) and dynamically create every other points, I just typed in values for the moment, to make it easier for myself. I've added images of the issue to better show you.
First create a path for a brace that starts at (0, 0) and ends at (1, 0). Then apply an affine transformation that moves, scales, and rotates the path to span your designed endpoints. It needs to transform (0, 0) to your start point and (1, 0) to your end point. Creating the transformation efficiently requires some trigonometry, but I've done the homework for you:
extension UIBezierPath {
class func brace(from start: CGPoint, to end: CGPoint) -> UIBezierPath {
let path = self.init()
path.move(to: .zero)
path.addCurve(to: CGPoint(x: 0.5, y: -0.1), controlPoint1: CGPoint(x: 0, y: -0.2), controlPoint2: CGPoint(x: 0.5, y: 0.1))
path.addCurve(to: CGPoint(x: 1, y: 0), controlPoint1: CGPoint(x: 0.5, y: 0.1), controlPoint2: CGPoint(x: 1, y: -0.2))
let scaledCosine = end.x - start.x
let scaledSine = end.y - start.y
let transform = CGAffineTransform(a: scaledCosine, b: scaledSine, c: -scaledSine, d: scaledCosine, tx: start.x, ty: start.y)
path.apply(transform)
return path
}
}
Result:
Here's the entire Swift playground I used to make the demo:
import UIKit
import PlaygroundSupport
extension UIBezierPath {
class func brace(from start: CGPoint, to end: CGPoint) -> UIBezierPath {
let path = self.init()
path.move(to: .zero)
path.addCurve(to: CGPoint(x: 0.5, y: -0.1), controlPoint1: CGPoint(x: 0, y: -0.2), controlPoint2: CGPoint(x: 0.5, y: 0.1))
path.addCurve(to: CGPoint(x: 1, y: 0), controlPoint1: CGPoint(x: 0.5, y: 0.1), controlPoint2: CGPoint(x: 1, y: -0.2))
let scaledCosine = end.x - start.x
let scaledSine = end.y - start.y
let transform = CGAffineTransform(a: scaledCosine, b: scaledSine, c: -scaledSine, d: scaledCosine, tx: start.x, ty: start.y)
path.apply(transform)
return path
}
}
class ShapeView: UIView {
override class var layerClass: Swift.AnyClass { return CAShapeLayer.self }
lazy var shapeLayer: CAShapeLayer = { self.layer as! CAShapeLayer }()
}
class ViewController: UIViewController {
override func loadView() {
let view = UIView(frame: CGRect(x: 0, y: 0, width: 600, height: 200))
view.backgroundColor = .white
for (i, handle) in handles.enumerated() {
handle.autoresizingMask = [ .flexibleTopMargin, .flexibleTopMargin, .flexibleBottomMargin, .flexibleRightMargin ]
let frame = CGRect(x: view.bounds.width * 0.1 + CGFloat(i) * view.bounds.width * 0.8 - 22, y: view.bounds.height / 2 - 22, width: 44, height: 44)
handle.frame = frame
handle.shapeLayer.path = CGPath(ellipseIn: handle.bounds, transform: nil)
handle.shapeLayer.lineWidth = 2
handle.shapeLayer.lineDashPattern = [2, 6]
handle.shapeLayer.lineCap = kCALineCapRound
handle.shapeLayer.strokeColor = UIColor.blue.cgColor
handle.shapeLayer.fillColor = nil
view.addSubview(handle)
let panner = UIPanGestureRecognizer(target: self, action: #selector(pannerDidFire(panner:)))
handle.addGestureRecognizer(panner)
}
brace.shapeLayer.lineWidth = 2
brace.shapeLayer.lineCap = kCALineCapRound
brace.shapeLayer.strokeColor = UIColor.black.cgColor
brace.shapeLayer.fillColor = nil
view.addSubview(brace)
setBracePath()
self.view = view
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
setBracePath()
}
private let handles: [ShapeView] = [
ShapeView(),
ShapeView()
]
private let brace = ShapeView()
private func setBracePath() {
brace.shapeLayer.path = UIBezierPath.brace(from: handles[0].center, to: handles[1].center).cgPath
}
#objc private func pannerDidFire(panner: UIPanGestureRecognizer) {
let view = panner.view!
let offset = panner.translation(in: view)
panner.setTranslation(.zero, in: view)
var center = view.center
center.x += offset.x
center.y += offset.y
view.center = center
setBracePath()
}
}
let vc = ViewController()
PlaygroundPage.current.liveView = vc.view
The key to the problem is when the figure is rotated your base vectors will rotate. When your figure is axis-aligned your base vectors are u (1, 0) and v (0, 1).
So when you are performing midPoint.y -= 20 you can see it as the same as midPoint.x -= v.x * 20; midPoint.y -= v.y * 20 where v is (0, 1). The results are the same, check for yourself.
This implementation will do what your code does, only axis independent.
let path = UIBezierPath()
let p1 = CGPointMake(100, 100)
let p2 = CGPointMake(300, 100)
let o = p1.plus(p2).divide(2.0) // origo
let u = p2.minus(o) // base vector 1
let v = u.turn90() // base vector 2
let c1 = o.minus(u.times(0.5)).minus(v.times(0.2)) // CGPointMake(150, 80)
let c2 = o.plus(u.times(0.5)).minus(v.times(0.2)) // CGPointMake(250, 80)
var midPoint = o.minus(v.times(0.2))
var midP1 = o.minus(u.times(0.2))
var midP2 = o.plus(u.times(0.2))
Note: I set the factors to match the initial values in your implementation.
Also added this CGPoint extension for convenience. Hope it helps.
extension CGPoint {
public func plus(p: CGPoint) -> (CGPoint)
{
return CGPoint(x: self.x + p.x, y: self.y + p.y)
}
public func minus(p: CGPoint) -> (CGPoint)
{
return CGPoint(x: self.x - p.x, y: self.y - p.y)
}
public func times(f: CGFloat) -> (CGPoint)
{
return CGPoint(x: self.x * f, y: self.y * f)
}
public func divide(f: CGFloat) -> (CGPoint)
{
return self.times(1.0/f)
}
public func turn90() -> (CGPoint)
{
return CGPoint(x: -self.y, y: x)
}
}

Strange behavior with SceneKit Node

I created a circular array objects, no problems.
When I rotate there appears to be a sinc or gaussian function at the center.
The camera is a z 60, radius of structure is 30.
Initial view, no artifacts
Rotated 90 deg up, no artifacts
Rotated 180 deg, artifact appears in center of object
Continued rotation, artifact is still there.
The code for the object is here
class func Build(scene: SCNScene) -> SCNNode {
let radius: Double = 30.0
let numberOfStrands: Int = 24
//Create the base chromosomes.
let baseChromosome = SCNBox(width: 4.0, height: 24, length: 1.0, chamferRadius: 0.5)
let baseChromosomes = DNA.buildCircleOfObjects(baseChromosome, numberOfItems: numberOfStrands, radius: radius)
baseChromosomes.position = SCNVector3(x: 0, y: 0.5, z: 0)
return baseChromosomes
}
class func buildCircleOfObjects(_geometry: SCNGeometry, numberOfItems: Int, radius: Double) -> SCNNode {
var x: Double = 0.0
var z: Double = radius
let theta: Double = (M_PI) / Double(numberOfItems / 2)
let incrementalY: Double = (M_PI) / Double(numberOfItems) * 2
let nodeCollection = SCNNode()
nodeCollection.position = SCNVector3(x: 0, y: 0.5, z: 0)
for index in 1...numberOfItems {
x = radius * sin(Double(index) * theta)
z = radius * cos(Double(index) * theta)
let node = SCNNode(geometry: _geometry)
node.position = SCNVector3(x: Float(x), y: 0, z:Float(z))
let rotation = Float(incrementalY) * Float(index)
node.rotation = SCNVector4(x: 0, y: 1, z: 0, w: rotation)
nodeCollection.addChildNode(node)
}
return nodeCollection
}
}
Using these settings
cameraNode.camera?.xFov = 60
cameraNode.camera?.yFov = 60
cameraNode.camera?.zFar = 1000
cameraNode.camera?.zNear = 0.01
the artifacts disappeared. I think this is a problem with zFar, it should have clipped the back surface uniformly not like a lens aberration.
Falling out of Viewing frustum may be the one to cause most problem. However there is another common cause by misunderstanding the behavior of SCNView's allowsCameraControl, that is when you Pinch to zoom in or zoom out (change the camera's fieldOfView), but the pointOfView's position reminds the same.
Your nodes still stand BEHIND the pointOfView, however small the camera's zNear is. Hence you see them got clipped. The code below may help you in this case, at least avoid the problem.
sceneView.pointOfView!.simdPosition
= float3(0, 0, -scene.rootNode.boundingSphere.radius)

Resources