如何使用Swift根据3点应用仿射变换

问题描述

对于两个CGImage,我有三对相应的点。我想找到这三对之间的仿射变换,并将此变换应用于第二个CGImage。 以下代码使用C ++和OpenCV完成。如何在Swift和CoreGraphics中将其本地应用?

Mat warp_mat( 2,3,CV_32FC1 );
 warp_mat = getAffineTransform( srcTri,dstTri );

// Set the dst image the same type and size as src
 Mat  warp_dst = Mat::zeros( srcImg.rows,srcImg.cols,srcImg.type() );

/// Apply the Affine Transform just found to the src image
warpAffine( srcImg,warp_dst,warp_mat,warp_dst.size(),INTER_LINEAR );

解决方法

对于2D 中的 3 个点的特定情况,有一个名为单纯仿射映射 (SAM) 的解决方案,我觉得实现起来很有趣。它有几个优点:

  • 它可以仅使用 CoreGraphics 从头开始​​实现(尽管我编写了一些简单的扩展)
  • 可以手动计算仿射变换
  • 要执行的计算相对较少
  • 无论您是否想了解数学,按照作者的示例编写算法都很简单
  • 代码可以轻松移植到其他框架

将这种技术用于真实世界的图像应用程序存在一些问题,但更多信息请参见下文。

SAM 论文是 “初学者的单形映射指南”,作者是 Tymchyshyn 和 Khlevniuk,不到两年前上传到 ResearchGate:

https://www.researchgate.net/publication/332410209_Beginner%27s_guide_to_mapping_simplexes_affinely

来自同一作者的“工作簿”论文提供了示例和逐步计算:

https://www.researchgate.net/publication/332971934_Workbook_on_mapping_simplexes_affinely

SAM 公式依赖于计算 4x4 矩阵的行列式除以 3x3 矩阵的行列式。 4x4矩阵的元素包括向量(代表点)和标量。

下面的表达式产生仿射变换。 abc 是第一组 3 个点的二维向量。 def 是第二组 3 点的二维向量。 axay 是点/向量 a 的标量。我们求解 pq 以及一个翻译组件。我会引导您返回“工作簿”文件以获得正确的描述。

          |0  d   e   f   |
          |p  ax  bx  cx  |       // p called "x1" in the SAM paper
      det |q  ay  by  cy  |       // q called "x2" in the SAM paper
          |1  1   1   1   |
 (-1) ----------------------
             |ax  bx  cx  |      // ax = point A,x component
         det |ay  by  cy  |
             |1   1   1   |

下面的内容可能是过多的代码,但它已准备好粘贴到 XCode 12 playground 并运行。最后有测试函数和使用这些测试函数的例子。

sam() 函数是关键。其他代码只是为了演示 SAM 技术。我建议您重写代码以适合您的目的——在您重构时,请随时在您家的隐私中向我和我的 Swift 新手代码发誓。我包含了冗长的注释,希望代码可以独立存在。 SAM 论文的链接包含在 SAM 功能的评论中。

将整个代码块复制到 XCode 12 playground 中,运行它,看看您是否能理解打印语句。


import CoreGraphics
import Foundation       //for NumberFormatter,used in debug functions

// MARK: - Simple Affine Map function for 2D and supporting types

/// Calculate the affine transform from one set of three 2D points (the "from" triangle) to another set of three 2D points (the "to" triangle).
/// Throws an error if one of the sets of points is colinear.
/// The transform is calculated using the 2D expression of the Simplex Affine Map [SAM] by V. B. Tymchyshyn and A. V. Khlevniuk.
/// https://www.researchgate.net/publication/332410209_Beginner%27s_guide_to_mapping_simplexes_affinely
///
/// The workbook from the authors provides numerous examples showing step-by-step calculation for simplexes in N dimensions.
/// https://www.researchgate.net/publication/332971934_Workbook_on_mapping_simplexes_affinely\
///
/// A triangle is a simplex in 2D space,and hence works using the SAM scheme.
/// https://en.wikipedia.org/wiki/Simplex
///
/// The matrix elements include a mix of vectors and scalars. For this code,use letter names corresponding to the vectors and scalars in the SAM paper.
/// a,b,c are 2D points in the "from" triangle
/// d,e,f are 2D points in the "to" triangle
///
///          |0  d   e   f   |
///          |p  ax  bx  cx  |       // p called "x1" in the SAM paper
///      det |q  ay  by  cy  |       // q called "x2" in the SAM paper
///          |1  1   1   1   |
/// (-1) ----------------------
///             |ax  bx  cx  |      // ax = point A,x component
///         det    |ay  by  cy  |
///             |1   1   1   |
///
/// For image processing applications this technique may not be appropriate if point finding is not sufficiently repeatable.
/// This mapping will be sensitive to noise / inaccuracy in finding any of the "from" or "to" points: any triangle can map to any other triangle!
/// For point registration,typically more than 3 point correspondences are used. The transform may be found
/// using a least squares fitting,and often by eliminating outlier points. Related topics include Singular Value Decomposition (SVD) and
/// Random Sampling and Consensus (RANSAC).
func sam(from: Triangle,to: Triangle) throws -> CGAffineTransform {
    if from.colinear() {
        throw SamError.colinearPoints(description: "Points in 'from' triangle are colinear. Can not calculate SAM transform.")
    }
    
    if to.colinear() {
        throw SamError.colinearPoints(description: "Points in 'to' triangle are colinear. Can not calculate SAM transform.")
    }
    
    let a = from.vector1
    let b = from.vector2
    let c = from.vector3
    
    let d = to.vector1
    let e = to.vector2
    let f = to.vector3

    // Determinant of 4x4 in numerator of SAM formula
    // Reduce to p,q,and translation. (p,q) here are (x1,x2) in the SAM paper.
    // We can find the determinant starting with any row or any column,so we make it easy to isolate p ("x1") and q ("x2")
    
    // +0 * (...)
    
    // -p ( [d] (by - cy) - [e] (ay - cy) + [f] (ay - by) )
    let p = CGFloat(-1.0) * (d * (b.dy - c.dy) - e * (a.dy - c.dy) + f * (a.dy - b.dy))
    
    // +q ( [d] (bx - cx) - [e] (ax - cx) + [f] (ax - bx) )
    let q = d * (b.dx - c.dx) - e * (a.dx - c.dx) + f * (a.dx - b.dx)
    
    // -1  ( [d] (bx * cy - by * cx) - [e] (ax * cy - ay * cx) + [f] * (ax * by - ay * bx) )
    let translation =  CGFloat(-1.0) * (d * (b.dx * c.dy - b.dy * c.dx) - e * (a.dx * c.dy - a.dy * c.dx) + f * (a.dx * b.dy - a.dy * b.dx))
    
    // Determinant of 3x3 used in denominator of SAM formula
    let denominator = determinant(from)
    
    let pd = CGFloat(-1.0) * p / denominator
    let qd = CGFloat(-1.0) * q / denominator
    let td = CGFloat(-1.0) * translation / denominator
    
    // plug values into CoreGraphics affine transform
    //  |a  b   0|   =  |pd.x   pd.y    0|
    //  |c  d   0|      |qd.x   qd.y    0|
    //  |tx ty  1|      |td.x   td.y    1|
    
    return CGAffineTransform(a: pd.dx,b: pd.dy,c: qd.dx,d: qd.dy,tx: td.dx,ty: td.dy)
}

enum SamError: Error {
    case colinearPoints(description: String)
}

/// Three points nominally defining a triangle,but possibly colinear.
struct Triangle {
    var point1: CGPoint
    var point2: CGPoint
    var point3: CGPoint
    
    var x1: CGFloat { point1.x }
    var y1: CGFloat { point1.y }
    var x2: CGFloat { point2.x }
    var y2: CGFloat { point2.y }
    var x3: CGFloat { point3.x }
    var y3: CGFloat { point3.y }
    
    /// Point1 as a 2D vector
    var vector1: CGVector { CGVector(point1) }
    
    /// Point2 as a 2D vector
    var vector2: CGVector { CGVector(point2) }
    
    /// Point3 as a 2D vector
    var vector3: CGVector { CGVector(point3) }
    
    /// Return a Triangle after applying an affine transform to self.
    func applying(_ t: CGAffineTransform) -> Triangle {
        Triangle(
            point1: self.point1.applying(t),point2: self.point2.applying(t),point3: self.point3.applying(t)
        )
    }
    
    init(point1: CGPoint,point2: CGPoint,point3: CGPoint) {
        self.point1 = point1
        self.point2 = point2
        self.point3 = point3
    }
    
    init(x1: CGFloat,y1: CGFloat,x2: CGFloat,y2: CGFloat,x3: CGFloat,y3: CGFloat) {
        point1 = CGPoint(x: x1,y: y1)
        point2 = CGPoint(x: x2,y: y2)
        point3 = CGPoint(x: x3,y: y3)
    }
    
    /// Returns a (Bool,CGFloat) tuple indicating whether the points in the Triangle are colinear,and the angle between vectors tested.
    func colinear(degreesTolerance: CGFloat = 0.5) -> Bool {
        let v1 = vector2 - vector1
        let v2 = vector3 - vector2
        let radians = CGVector.angleBetweenVectors(v1: v1,v2: v2)
        
        if radians.isNaN {
            return true
        }
        
        var degrees = radiansToDegrees(radians)
        
        if degrees > 90 {
           degrees = 180 - degrees
        }

        // code to get around parsing error
        return 0 > degrees - degreesTolerance
    }
}

func degreesToRadians(_ degrees: CGFloat) -> CGFloat {
    degrees * CGFloat.pi / 180.0
}

func radiansToDegrees(_ radians: CGFloat) -> CGFloat {
    180.0 * radians / CGFloat.pi
}

extension CGVector {
    init(_ point: CGPoint) {
        self.init(dx: point.x,dy: point.y)
    }
    
    static func + (lhs: CGVector,rhs: CGVector) -> CGVector {
        CGVector(dx: lhs.dx + rhs.dx,dy: lhs.dy + rhs.dy)
    }

    static func - (lhs: CGVector,rhs: CGVector) -> CGVector {
        CGVector(dx: lhs.dx - rhs.dx,dy: lhs.dy - rhs.dy)
    }

    static func * (_ vector: CGVector,_ scalar: CGFloat) -> CGVector {
        CGVector(dx: vector.dx * scalar,dy: vector.dy * scalar)
    }
    
    static func * (_ scalar: CGFloat,_ vector: CGVector) -> CGVector {
        CGVector(dx: vector.dx * scalar,dy: vector.dy * scalar)
    }

    static func * (lhs: CGVector,rhs: CGVector) -> CGFloat {
        lhs.dx * rhs.dx + lhs.dy * rhs.dy
    }
    
    static func dotProduct(v1: CGVector,v2: CGVector) -> CGFloat {
        v1.dx * v2.dx + v1.dy * v2.dy
    }
    
    static func / (_ vector: CGVector,_ scalar: CGFloat) -> CGVector {
        CGVector(dx: vector.dx / scalar,dy: vector.dy / scalar)
    }

    static func / (_ scalar: CGFloat,_ vector: CGVector) -> CGVector {
        CGVector(dx: vector.dx / scalar,dy: vector.dy / scalar)
    }
    
    // Returns again between vectors in range 0 to 2 * pi [positive]
    // a * b = ||a|| ||b|| cos(theta)
    // theta = arc cos (a * b / ||a|| ||b||)
    static func angleBetweenVectors(v1: CGVector,v2: CGVector) -> CGFloat {
        acos( v1 * v2 / (v1.length() * v2.length()) )
    }
    
    func length() -> CGFloat {
        sqrt(self.dx * self.dx + self.dy * self.dy)
    }
}

/// Determinant of three points used in the denominator of the SAM formula.
/// x1   x2  x3
/// y1   y2  y3
/// 1    1   1
func determinant(_ t: Triangle) -> CGFloat {
    t.x1 * (t.y2 - t.y3) - t.x2 * (t.y1 - t.y3) + t.x3 * (t.y1 - t.y2)
}

//MARK: - Debug
extension NumberFormatter {
    func string(_ value: CGFloat,digits: Int,failText: String = "[?]") -> String {
        minimumFractionDigits = max(0,digits)
        maximumFractionDigits = minimumFractionDigits
        
        guard let s = string(from: NSNumber(value: Double(value))) else {
            return failText
        }
        
        return s
    }
    
    func string(_ point: CGPoint,digits: Int = 1,failText: String = "[?]") -> String {
        let sx = string(point.x,digits: digits,failText: failText)
        let sy = string(point.y,failText: failText)
        return "(\(sx),\(sy))"
    }
    
    func string(_ vector: CGVector,failText: String = "[?]") -> String {
        let sdx = string(vector.dx,failText: failText)
        let sdy = string(vector.dy,failText: failText)
        return "(\(sdx),\(sdy))"
    }
    
    func string(_ transform: CGAffineTransform,rotationDigits: Int = 2,translationDigits: Int = 1,failText: String = "[?]") -> String {
        let sa = string(transform.a,digits: rotationDigits)
        let sb = string(transform.b,digits: rotationDigits)
        let sc = string(transform.c,digits: rotationDigits)
        let sd = string(transform.d,digits: rotationDigits)
        let stx = string(transform.tx,digits: translationDigits)
        let sty = string(transform.ty,digits: translationDigits)

        var s = "a:  \(sa)   b: \(sb)   0"
        s += "\nc:  \(sc)   d: \(sd)   0"
        s += "\ntx: \(stx)   ty: \(sty)   1"
        return s
    }
}

let formatter = NumberFormatter()

/// Checks whether the three points in the Triangle are colinear.
/// Returns a test message spanning multiple lines.
func testColinearity(_ t: Triangle) -> String {
    let co = t.colinear()
    let st1 = formatter.string(t.point1,digits: 2)
    let st2 = formatter.string(t.point2,digits: 2)
    let st3 = formatter.string(t.point3,digits: 2)
    
    var s = "\(st1),\(st2),\(st3): points in triangle"
    s += "\n\(co): colinear"

    return s
}

/// Checks how close two points are. Used to test whether a transformed point (actual) is equivalent to the expected point.
/// Returns a test message spanning multiple lines.
func testCoincidence(actual: CGPoint,expected: CGPoint,pointTolerance: CGFloat = 0.1) -> String {
    let vector = CGVector(actual) - CGVector(expected)
    let distance = vector.length()
    let sd = formatter.string(distance,digits: 2)
    let sa = formatter.string(actual,digits: 2)
    let se = formatter.string(expected,digits: 2)
    
    return "\(sd) offset  from actual \(sa) to expected \(se)"
}

/// Calculates the affine transform and checks its validity.
/// Returns a test message spanning multiple lines.
func testSAM(from t1: Triangle,to t2: Triangle,pointTolerance: CGFloat = 0.1) -> String {
    var s = "from \(t1) to \(t2)"

    do {
        let transform = try sam(from: t1,to: t2)
        s += "\n" + formatter.string(transform)

        // apply transform to points in t1
        let a = t1.applying(transform)

        // get difference between transformed points (actual) and t2 points (expected)
        s += "\n" + testCoincidence(actual: a.point1,expected: t2.point1,pointTolerance: pointTolerance)
        s += "\n" + testCoincidence(actual: a.point2,expected: t2.point2,pointTolerance: pointTolerance)
        s += "\n" + testCoincidence(actual: a.point3,expected: t2.point3,pointTolerance: pointTolerance)

        return s
    }
    catch SamError.colinearPoints(let description) {
        return description
    }
    catch {
        return error.localizedDescription
    }
}

//MARK: - Tests for Playground
print()
print("** Colinearity tests **")
let c1 = Triangle(x1: 1,y1: 1,x2: 2,y2: 2,x3: 3,y3: 3)
let sc1 = testColinearity(c1)
print(sc1)

print()
let c2 = Triangle(x1: 0,y1: 0,x2: 5,y2: 5,x3: 0.01,y3: 0.02)
let sc2 = testColinearity(c2)
print(sc2)

print()
let c3 = Triangle(x1: 1,y2: 3,y3: 2)
let sc3 = testColinearity(c3)
print(sc3)

print()
print("** Arbitrary translation example from SAM workbook **")
let a1 = Triangle(x1: 1,y3: 2)
let a2 = Triangle(x1: 3,y1: 2,x2: 1,x3: -2,y3: 1)
print(testColinearity(a1))
print()
print(testColinearity(a2))
print()
print(testSAM(from: a1,to: a2))

print()
print("** Pure Translation **")
let t1 = Triangle(x1: 0,x2: -2,x3: -5,y3: 3)
let t2 = Triangle(x1: 3,y1: -2,y2: 1,y3: 1)
print(testSAM(from: t1,to: t2))

print()
print("** Rotation and Translation **")
let r1 = Triangle(x1: 0,y2: 0,x3: 0,y3: 1)
let r2 = Triangle(x1: 6,y1: 5,x3: 6,y3: 4)
print(testSAM(from: r1,to: r2))

print()
print("** Pure Scaling **")
let s1 = Triangle(x1: 0,y3: 1)
let s2 = Triangle(x1: 0,y3: 5)
print(testSAM(from: s1,to: s2))

print()
print("** Test in which a triangle has colinear points")
let k1 = Triangle(x1: 1,y2: -2,x3: 5,y3: 5)
let k2 = Triangle(x1: -5,y1: 3,y2: 8,x3: -3,y3: 1)
print()
print(testColinearity(k1))
print()
print(testColinearity(k2))
print()
print(testSAM(from: k1,to: k2))


更多通用技术

您使用过 OpenCV,因此您可能知道 SIFTAKAZE 等算法可以使用数千个对应关系,它们依赖于 RANSAC 或其他技术来丢弃异常值,等等在。这些都是强大的技术。

只有 3 个点对可以很好地工作,但人们通常想要一种更通用的技术,可以接受任意长的对应列表,对它们进行魔法处理,并在存在噪声的情况下产生良好的变换。

最初我开始寻找依赖奇异值分解 (SVD)、RANSAC 等的原生 Swift 代码。虽然我在 iOS 框架中没有找到 SVD 函数,但有一个示例项目,其中包含一个 SVD 函数和支持类型。它使用 Accelerate 框架。有一天我可能会发布一个从这个项目中借用 SVD 函数的后续答案:

https://developer.apple.com/documentation/accelerate/denoising_an_image_using_linear_algebra

虽然我只是快速浏览了去噪项目,构建了它并运行了一次,但我明白代码下面的某个地方依赖(或至少可以运行)LAPACK

对于这个主题的新手,有关于更通用解决方案的 StackOverflow 帖子:

Finding translation and scale on two sets of points to get least square error in their distance?

最后,一篇维基百科文章总结了将一组点映射到另一组点的各种方法:

https://en.wikipedia.org/wiki/Point_set_registration

相关问答

错误1:Request method ‘DELETE‘ not supported 错误还原:...
错误1:启动docker镜像时报错:Error response from daemon:...
错误1:private field ‘xxx‘ is never assigned 按Alt...
报错如下,通过源不能下载,最后警告pip需升级版本 Requirem...