使用CATransform3D将矩形图像转化为四边形

44
我有一张图片和四个点的集合(描述了一个四边形Q)。我想将此图像转换为适合四边形Q的形状,Photoshop称这种变换为"Distort"。但根据这个四边形的源头(图像在空间中的视角移动),实际上是一个尺度、旋转和透视矩阵的组合。
我想知道是否可以使用CATransform3D 4x4矩阵实现这一目标。你有什么提示吗?我已经尝试使用这四个点构建16个方程(A’ = A x u),但没有成功:我不确定应该使用什么作为z,z',w和w'系数...
以下图片展示了我想要做的事情: Transforming a rectangle image into a quadrilateral using a CATransform3D 下面是一些点的示例:
276.523, 236.438,   517.656, 208.945,   275.984, 331.285,   502.23,  292.344
261.441, 235.059,   515.09,  211.5,     263.555, 327.066,   500.734, 295
229.031, 161.277,   427.125, 192.562,   229.16, 226,        416.48,  256

4
我认为在这个问题上,你不会找到比KennyTM在类似问题(iPhone图片拉伸(倾斜))的回答更好的答案了。他在那里提出的数学应该足以通过将CATransform3D应用于CALayer来创建任意四边形。 - Brad Larson
1
@BradLarson,出于某种奇怪的原因,KennyTM的答案似乎不起作用。我们得到了一个3D矩阵,但是当它被传递给CoreAnimation时,它会产生虚假的结果... - MonsieurDart
从四边形到矩形的反向转换怎么样? - Cherpak Evgeny
@CherpakEvgeny,我认为cvFindHomography() OpenCV函数也可以用于这种方式。 - MonsieurDart
@BradLarson和其他人,KennyTM五年前的惊人代码在实践中确实有点麻烦,因为ABCD与ABDC排序、翻译等方面存在一些混乱...谢天谢地,下面的JoshRL做了所有的艰苦工作,并基于KTM的数学创建了一个完美的插入类。现在有一个Swift版本,它运行得非常完美,并经过了很多测试 https://dev59.com/mWox5IYBdhLWcg3wDgEz#18606029 - Fattie
8个回答

39

我在iOS上创建了一个工具包用于这个目的:https://github.com/hfossli/AGGeometryKit/


确保你的锚点是左上角(CGPointZero)。

+ (CATransform3D)rectToQuad:(CGRect)rect
                     quadTL:(CGPoint)topLeft
                     quadTR:(CGPoint)topRight
                     quadBL:(CGPoint)bottomLeft
                     quadBR:(CGPoint)bottomRight
{
    return [self rectToQuad:rect quadTLX:topLeft.x quadTLY:topLeft.y quadTRX:topRight.x quadTRY:topRight.y quadBLX:bottomLeft.x quadBLY:bottomLeft.y quadBRX:bottomRight.x quadBRY:bottomRight.y];
}

+ (CATransform3D)rectToQuad:(CGRect)rect
                    quadTLX:(CGFloat)x1a
                    quadTLY:(CGFloat)y1a
                    quadTRX:(CGFloat)x2a
                    quadTRY:(CGFloat)y2a
                    quadBLX:(CGFloat)x3a
                    quadBLY:(CGFloat)y3a
                    quadBRX:(CGFloat)x4a
                    quadBRY:(CGFloat)y4a
{
    CGFloat X = rect.origin.x;
    CGFloat Y = rect.origin.y;
    CGFloat W = rect.size.width;
    CGFloat H = rect.size.height;

    CGFloat y21 = y2a - y1a;
    CGFloat y32 = y3a - y2a;
    CGFloat y43 = y4a - y3a;
    CGFloat y14 = y1a - y4a;
    CGFloat y31 = y3a - y1a;
    CGFloat y42 = y4a - y2a;

    CGFloat a = -H*(x2a*x3a*y14 + x2a*x4a*y31 - x1a*x4a*y32 + x1a*x3a*y42);
    CGFloat b = W*(x2a*x3a*y14 + x3a*x4a*y21 + x1a*x4a*y32 + x1a*x2a*y43);
    CGFloat c = H*X*(x2a*x3a*y14 + x2a*x4a*y31 - x1a*x4a*y32 + x1a*x3a*y42) - H*W*x1a*(x4a*y32 - x3a*y42 + x2a*y43) - W*Y*(x2a*x3a*y14 + x3a*x4a*y21 + x1a*x4a*y32 + x1a*x2a*y43);

    CGFloat d = H*(-x4a*y21*y3a + x2a*y1a*y43 - x1a*y2a*y43 - x3a*y1a*y4a + x3a*y2a*y4a);
    CGFloat e = W*(x4a*y2a*y31 - x3a*y1a*y42 - x2a*y31*y4a + x1a*y3a*y42);
    CGFloat f = -(W*(x4a*(Y*y2a*y31 + H*y1a*y32) - x3a*(H + Y)*y1a*y42 + H*x2a*y1a*y43 + x2a*Y*(y1a - y3a)*y4a + x1a*Y*y3a*(-y2a + y4a)) - H*X*(x4a*y21*y3a - x2a*y1a*y43 + x3a*(y1a - y2a)*y4a + x1a*y2a*(-y3a + y4a)));

    CGFloat g = H*(x3a*y21 - x4a*y21 + (-x1a + x2a)*y43);
    CGFloat h = W*(-x2a*y31 + x4a*y31 + (x1a - x3a)*y42);
    CGFloat i = W*Y*(x2a*y31 - x4a*y31 - x1a*y42 + x3a*y42) + H*(X*(-(x3a*y21) + x4a*y21 + x1a*y43 - x2a*y43) + W*(-(x3a*y2a) + x4a*y2a + x2a*y3a - x4a*y3a - x2a*y4a + x3a*y4a));

    const double kEpsilon = 0.0001;

    if(fabs(i) < kEpsilon)
    {
        i = kEpsilon* (i > 0 ? 1.0 : -1.0);
    }

    CATransform3D transform = {a/i, d/i, 0, g/i, b/i, e/i, 0, h/i, 0, 0, 1, 0, c/i, f/i, 0, 1.0};

    return transform;
}

我不为这段代码负责。我所做的只是搜寻互联网并整合各种不完整的答案。


抱歉如果这看起来很明显,但我如何将其应用于矩形内的点数组? - Benoît Lahoz
抱歉回复晚了,我在实现你的解决方案之前遇到了另一个代码问题 :-) 我很快会告诉你! - Benoît Lahoz
1
太好了,我需要一个Java解决方案,但这个可以在一个小时内移植 - 谢谢! - Rekin
1
对于2016年,只需向下滚动以获取有关Swift类的详细信息... https://dev59.com/mWox5IYBdhLWcg3wDgEz#18606029 ... 基于这里所有伟大的旧答案。 - Fattie
1
有没有想法如何做相反的操作?给定相同的矩形和四边形输入,我如何创建一个CATransform3D将四边形转换为矩形?我会用它来转换具有透视、旋转和缩放的图像,使其看起来像眼睛视角垂直于观察者。 - VTPete
显示剩余5条评论

10

这是一个示例项目,应用了hfossli上面的答案中的代码,并创建了一个在UIView上设置框架并应用变换的类别,只需一次调用:

https://github.com/joshrl/FreeTransform

UIView+Quadrilateral 代码:

#import <UIKit/UIKit.h>
#import <QuartzCore/QuartzCore.h>

@interface UIView (Quadrilateral)

//Sets frame to bounding box of quad and applies transform
- (void)transformToFitQuadTopLeft:(CGPoint)tl topRight:(CGPoint)tr bottomLeft:(CGPoint)bl bottomRight:(CGPoint)br;

@end    

@implementation UIView (Quadrilateral)

- (void)transformToFitQuadTopLeft:(CGPoint)tl topRight:(CGPoint)tr bottomLeft:(CGPoint)bl bottomRight:(CGPoint)br
{

    NSAssert(CGPointEqualToPoint(self.layer.anchorPoint, CGPointZero),@"Anchor point must be (0,0)!");
    CGRect boundingBox = [[self class] boundingBoxForQuadTR:tr tl:tl bl:bl br:br];
    self.frame = boundingBox;

    CGPoint frameTopLeft = boundingBox.origin;
    CATransform3D transform = [[self class] rectToQuad:self.bounds
                                                quadTL:CGPointMake(tl.x-frameTopLeft.x, tl.y-frameTopLeft.y)
                                                quadTR:CGPointMake(tr.x-frameTopLeft.x, tr.y-frameTopLeft.y)
                                                quadBL:CGPointMake(bl.x-frameTopLeft.x, bl.y-frameTopLeft.y)
                                                quadBR:CGPointMake(br.x-frameTopLeft.x, br.y-frameTopLeft.y)];

    self.layer.transform = transform;

}

+ (CGRect)boundingBoxForQuadTR:(CGPoint)tr tl:(CGPoint)tl bl:(CGPoint)bl br:(CGPoint)br
{
    CGRect boundingBox = CGRectZero;

    CGFloat xmin = MIN(MIN(MIN(tr.x, tl.x), bl.x),br.x);
    CGFloat ymin = MIN(MIN(MIN(tr.y, tl.y), bl.y),br.y);
    CGFloat xmax = MAX(MAX(MAX(tr.x, tl.x), bl.x),br.x);
    CGFloat ymax = MAX(MAX(MAX(tr.y, tl.y), bl.y),br.y);

    boundingBox.origin.x = xmin;
    boundingBox.origin.y = ymin;
    boundingBox.size.width = xmax - xmin;
    boundingBox.size.height = ymax - ymin;

    return boundingBox;
}

+ (CATransform3D)rectToQuad:(CGRect)rect
                     quadTL:(CGPoint)topLeft
                     quadTR:(CGPoint)topRight
                     quadBL:(CGPoint)bottomLeft
                     quadBR:(CGPoint)bottomRight
{
    return [self rectToQuad:rect quadTLX:topLeft.x quadTLY:topLeft.y quadTRX:topRight.x quadTRY:topRight.y quadBLX:bottomLeft.x quadBLY:bottomLeft.y quadBRX:bottomRight.x quadBRY:bottomRight.y];
}

+ (CATransform3D)rectToQuad:(CGRect)rect
                    quadTLX:(CGFloat)x1a
                    quadTLY:(CGFloat)y1a
                    quadTRX:(CGFloat)x2a
                    quadTRY:(CGFloat)y2a
                    quadBLX:(CGFloat)x3a
                    quadBLY:(CGFloat)y3a
                    quadBRX:(CGFloat)x4a
                    quadBRY:(CGFloat)y4a
{
    CGFloat X = rect.origin.x;
    CGFloat Y = rect.origin.y;
    CGFloat W = rect.size.width;
    CGFloat H = rect.size.height;

    CGFloat y21 = y2a - y1a;
    CGFloat y32 = y3a - y2a;
    CGFloat y43 = y4a - y3a;
    CGFloat y14 = y1a - y4a;
    CGFloat y31 = y3a - y1a;
    CGFloat y42 = y4a - y2a;

    CGFloat a = -H*(x2a*x3a*y14 + x2a*x4a*y31 - x1a*x4a*y32 + x1a*x3a*y42);
    CGFloat b = W*(x2a*x3a*y14 + x3a*x4a*y21 + x1a*x4a*y32 + x1a*x2a*y43);
    CGFloat c = H*X*(x2a*x3a*y14 + x2a*x4a*y31 - x1a*x4a*y32 + x1a*x3a*y42) - H*W*x1a*(x4a*y32 - x3a*y42 + x2a*y43) - W*Y*(x2a*x3a*y14 + x3a*x4a*y21 + x1a*x4a*y32 + x1a*x2a*y43);

    CGFloat d = H*(-x4a*y21*y3a + x2a*y1a*y43 - x1a*y2a*y43 - x3a*y1a*y4a + x3a*y2a*y4a);
    CGFloat e = W*(x4a*y2a*y31 - x3a*y1a*y42 - x2a*y31*y4a + x1a*y3a*y42);
    CGFloat f = -(W*(x4a*(Y*y2a*y31 + H*y1a*y32) - x3a*(H + Y)*y1a*y42 + H*x2a*y1a*y43 + x2a*Y*(y1a - y3a)*y4a + x1a*Y*y3a*(-y2a + y4a)) - H*X*(x4a*y21*y3a - x2a*y1a*y43 + x3a*(y1a - y2a)*y4a + x1a*y2a*(-y3a + y4a)));

    CGFloat g = H*(x3a*y21 - x4a*y21 + (-x1a + x2a)*y43);
    CGFloat h = W*(-x2a*y31 + x4a*y31 + (x1a - x3a)*y42);
    CGFloat i = W*Y*(x2a*y31 - x4a*y31 - x1a*y42 + x3a*y42) + H*(X*(-(x3a*y21) + x4a*y21 + x1a*y43 - x2a*y43) + W*(-(x3a*y2a) + x4a*y2a + x2a*y3a - x4a*y3a - x2a*y4a + x3a*y4a));

    const double kEpsilon = 0.0001;

    if(fabs(i) < kEpsilon)
    {
        i = kEpsilon* (i > 0 ? 1.0 : -1.0);
    }

    CATransform3D transform = {a/i, d/i, 0, g/i, b/i, e/i, 0, h/i, 0, 0, 1, 0, c/i, f/i, 0, 1.0};

    return transform;
}

@end

输入图片描述


1
注意,由于一些原因,我无法在现代Swift中完全使用Josh的精彩课程。只需向下滚动,https://dev59.com/mWox5IYBdhLWcg3wDgEz#39981054,我们制作并高度测试了Josh工作的完全可用的剪切和粘贴版本,您只需粘贴并使用,希望能帮助到某人! - Fattie

9
我们终于让这个工作起来了。我们尝试了几种不同的方法,但大多数都失败了。有些甚至在输入和输出相同时(例如来自KennyTM的那个),会检索到一个非标识矩阵……我们一定漏掉了什么。

使用 OpenCV 如下所示,我们得到了一个可以在CAAnimation图层上使用的CATransform3D

+ (CATransform3D)transformQuadrilateral:(Quadrilateral)origin toQuadrilateral:(Quadrilateral)destination {

    CvPoint2D32f *cvsrc = [self openCVMatrixWithQuadrilateral:origin]; 
    CvMat *src_mat = cvCreateMat( 4, 2, CV_32FC1 );
    cvSetData(src_mat, cvsrc, sizeof(CvPoint2D32f));

    CvPoint2D32f *cvdst = [self openCVMatrixWithQuadrilateral:destination]; 
    CvMat *dst_mat = cvCreateMat( 4, 2, CV_32FC1 );
    cvSetData(dst_mat, cvdst, sizeof(CvPoint2D32f));

    CvMat *H = cvCreateMat(3,3,CV_32FC1);
    cvFindHomography(src_mat, dst_mat, H);
    cvReleaseMat(&src_mat); 
    cvReleaseMat(&dst_mat); 

    CATransform3D transform = [self transform3DWithCMatrix:H->data.fl]; 
    cvReleaseMat(&H); 

    return transform; 
}

+ (CvPoint2D32f *)openCVMatrixWithQuadrilateral:(Quadrilateral)origin {

    CvPoint2D32f *cvsrc = (CvPoint2D32f *)malloc(4*sizeof(CvPoint2D32f)); 
    cvsrc[0].x = origin.upperLeft.x;
    cvsrc[0].y = origin.upperLeft.y;
    cvsrc[1].x = origin.upperRight.x;
    cvsrc[1].y = origin.upperRight.y;
    cvsrc[2].x = origin.lowerRight.x;
    cvsrc[2].y = origin.lowerRight.y;
    cvsrc[3].x = origin.lowerLeft.x;
    cvsrc[3].y = origin.lowerLeft.y; 
    return cvsrc; 
}

+ (CATransform3D)transform3DWithCMatrix:(float *)matrix {
    CATransform3D transform = CATransform3DIdentity; 

    transform.m11 = matrix[0];
    transform.m21 = matrix[1];
    transform.m41 = matrix[2];

    transform.m12 = matrix[3];
    transform.m22 = matrix[4];
    transform.m42 = matrix[5];

    transform.m14 = matrix[6];
    transform.m24 = matrix[7];
    transform.m44 = matrix[8];

    return transform; 
}

你能粘贴 transform3DWithCMatrix 方法吗? - user963395
1
为了使其正常工作,我必须使用正在使用变换的图层的坐标系中的坐标进行计算。我还必须将anchorPoint设置为左上角(相对于anchorPoint应用变换)。 - Ben Lings
1
另外,我将OpenCV代码转换为使用OpenCV 2方法的“cv :: getPerspectiveTransform”。由此函数和“cv :: findHomography”返回的元素矩阵类型均为double,而非上述的float。 - Ben Lings
哎呀!非常抱歉我忘记了 anchorPoint 的特殊性!非常感谢你提醒 doublefloat 返回值的区别,我之前并没有注意到。但是 cv::findHomographycv::getPerspectiveTransform 之间究竟有什么区别呢? - MonsieurDart

9
感谢JoshRL,以下是他的类的Swift版本。
这个版本已经完全调试,所有受到"太长在Swift中"问题影响的行都已经重新设计和测试。它在高流量生产环境中运行得非常完美。
使用起来非常简单。下面是如何在Swift中使用的示例。
2016年发布的Swift版本... 完整、可用的复制粘贴解决方案。
为2022年更新!直接插入带有当前语法等的代码。
// JoshQuadView in Swift
// from: https://dev59.com/mWox5IYBdhLWcg3wDgEz#18606029

// NB: JoshRL uses the ordering convention
// "topleft, topright, bottomleft, bottomright"
// which is different from "clockwise from topleft".

// Note: is not meant to handle concave.

import UIKit

class JoshQuadView: UIImageView { // or UIView, as preferred
    
    func transformToFitQuadTopLeft(tl: CGPoint, tr: CGPoint, bl: CGPoint, br: CGPoint) {
        guard self.layer.anchorPoint == .zero else {
            print("suck")
            return
        }
        let b = boundingBoxForQuadTR(tl, tr, bl, br)
        self.frame = b
        self.layer.transform = rectToQuad(bounds,
            .init(x: tl.x-b.origin.x, y: tl.y-b.origin.y),
            .init(x: tr.x-b.origin.x, y: tr.y-b.origin.y),
            .init(x: bl.x-b.origin.x, y: bl.y-b.origin.y),
            .init(x: br.x-b.origin.x, y: br.y-b.origin.y))
    }
    
    func boundingBoxForQuadTR(_ tl: CGPoint, _ tr: CGPoint, _ bl: CGPoint, _ br: CGPoint) -> CGRect {
        var b: CGRect = .zero
        let xmin: CGFloat = min(min(min(tr.x,tl.x),bl.x),br.x)
        let ymin: CGFloat = min(min(min(tr.y,tl.y),bl.y),br.y)
        let xmax: CGFloat = max(max(max(tr.x,tl.x),bl.x),br.x)
        let ymax: CGFloat = max(max(max(tr.y,tl.y),bl.y),br.y)
        b.origin.x = xmin
        b.origin.y = ymin
        b.size.width = xmax - xmin
        b.size.height = ymax - ymin
        return b
    }
    
    func rectToQuad(_ rect: CGRect,
                    _ topLeft: CGPoint, _ topRight: CGPoint, _ bottomLeft: CGPoint, _ bottomRight: CGPoint) -> CATransform3D {
        rectToQuadCalculation(rect,
                topLeft.x, topLeft.y,
                topRight.x, topRight.y,
                bottomLeft.x, bottomLeft.y,
                bottomRight.x, bottomRight.y)
    }
    
    func rectToQuadCalculation(_ rect: CGRect,
                    _ x1a: CGFloat, _ y1a: CGFloat,
                    _ x2a: CGFloat, _ y2a: CGFloat,
                    _ x3a: CGFloat, _ y3a: CGFloat,
                    _ x4a: CGFloat, _ y4a: CGFloat) -> CATransform3D {
    let XX = rect.origin.x
    let YY = rect.origin.y
    let WW = rect.size.width
    let HH = rect.size.height
        
    let y21 = y2a - y1a
    let y32 = y3a - y2a
    let y43 = y4a - y3a
    let y14 = y1a - y4a
    let y31 = y3a - y1a
    let y42 = y4a - y2a
    
    let a = -HH * (x2a*x3a*y14 + x2a*x4a*y31 - x1a*x4a*y32 + x1a*x3a*y42)
    let b = WW * (x2a*x3a*y14 + x3a*x4a*y21 + x1a*x4a*y32 + x1a*x2a*y43)
    
    let c0 = -HH * WW * x1a * (x4a*y32 - x3a*y42 + x2a*y43)
    let cx = HH * XX * (x2a*x3a*y14 + x2a*x4a*y31 - x1a*x4a*y32 + x1a*x3a*y42)
    let cy = -WW * YY * (x2a*x3a*y14 + x3a*x4a*y21 + x1a*x4a*y32 + x1a*x2a*y43)
    let c = c0 + cx + cy
    let d = HH * (-x4a*y21*y3a + x2a*y1a*y43 - x1a*y2a*y43 - x3a*y1a*y4a + x3a*y2a*y4a)
    let e = WW * (x4a*y2a*y31 - x3a*y1a*y42 - x2a*y31*y4a + x1a*y3a*y42)
    
    let f0 = -WW * HH * (x4a * y1a * y32 - x3a * y1a * y42 + x2a * y1a * y43)
    let fx = HH * XX * (x4a * y21 * y3a - x2a * y1a * y43 - x3a * y21 * y4a + x1a * y2a * y43)
    let fy = -WW * YY * (x4a * y2a * y31 - x3a * y1a * y42 - x2a * y31 * y4a + x1a * y3a * y42)
    let f = f0 + fx + fy
    let g = HH * (x3a * y21 - x4a * y21 + (-x1a + x2a) * y43)
    let h = WW * (-x2a * y31 + x4a * y31 + (x1a - x3a) * y42)
    
    let iy = WW * YY * (x2a * y31 - x4a * y31 - x1a * y42 + x3a * y42)
    let ix = HH * XX * (x4a * y21 - x3a * y21 + x1a * y43 - x2a * y43)
    let i0 = HH * WW * (x3a * y42 - x4a * y32 - x2a * y43)
    var i = i0 + ix + iy
    let kEpsilon: CGFloat = 0.0001
    if abs(i) < kEpsilon {
        i = kEpsilon * (i > 0 ? 1 : -1)
    }
        
    return CATransform3D(
        m11: a/i, m12: d/i, m13: 0, m14: g/i,
        m21: b/i, m22: e/i, m23: 0, m24: h/i,
        m31: 0, m32: 0, m33: 1, m34: 0,
        m41: c/i, m42: f/i, m43: 0, m44: 1.0)
    }
}

快速测试:

@IBOutlet var someImage: JoshQuadView!

    someImage.transformToFitQuadTopLeft(
        tl: CGPoint(x: 0, y: 0),
        tr: CGPoint(x: 400, y: 0),
        bl: CGPoint(x: 0, y: 400),
        br: CGPoint(x: 400, y: 400))

它将呈现普通的方形图像。
    someImage.transformToFitQuadTopLeft(
        tl: CGPoint(x: -50, y: -20),
        tr: CGPoint(x: 400, y: 0),
        bl: CGPoint(x: 0, y: 400),
        br: CGPoint(x: 400, y: 400))

它将弯曲掉左上角。


重要提醒。JoshRL 最初使用的顺序是 tl tr bl br。因此这里保持不变。在处理 verts 时,更常见的做法是按照顺时针方向使用 tl(即 tl tr br bl),所以请记住这一点!


在 Swift 中使用可拖动角落手柄:

假设您有一个容器视图 "QuadScreen"。

您想要拉伸的视图将是 JoshQuadView。将其放入场景中。将其连接到 IBOutlet,在此示例中为 "jqv"。

只需在场景中放置四个角落手柄(即图片),作为您的句柄图标的 PNG。将这些链接到四个句柄的 IBOutlets 上。代码完全处理这些句柄。 (按照代码中的注释轻松设置 storyboard。)

然后,只需一行代码即可完成拉伸:


class QuadScreen: UIViewController {
    // sit your JoshQuadView in this view
    @IBOutlet var jqv: JoshQuadView!
    
    // simply have four small subview views, "handles"
    // with an icon on them (perhaps a small circle)
    // and put those over the four corners of the jqv
    
    // NOTE numbered CLOCKWISE from top left here:
    @IBOutlet var handle1: UIView!
    @IBOutlet var handle2: UIView!
    @IBOutlet var handle3: UIView!
    @IBOutlet var handle4: UIView!
    
    // put a pan recognizer on each handle, action goes to here
    // (for the pan recognizers, set cancels-in-view as needed
    // if you, example, highlight them on touch in their class)
    
    @IBAction func dragHandle(p: UIPanGestureRecognizer!) {
        let tr = p.translationInView(p.view)
        p.view!.center.x += tr.x
        p.view!.center.y += tr.y
        p.setTranslation(.zero, inView: p.view)
        
        jqv.transformToFitQuadTopLeft( handle1.center, tr: handle2.center, bl: handle4.center, br: handle3.center)
        // it's that simple, there's nothing else to do
        p.setTranslation(.zero, inView: p.view)
    }
    
    override func viewDidLayoutSubviews() {
        // don't forget to do this....is critical.
        jqv.layer.anchorPoint = .zero
    }
}

有趣的是,对于Google来说,使用Android内置命令来重塑多边形非常容易。

Android

他们有一个用于重新塑造多边形的内置命令。这个出色的答案包含复制和粘贴代码:https://dev59.com/UGzXa4cB1Zd3GeqPRTgn#34667015


2
今天的英雄! - hbk
3
没问题,希望有所帮助。这是一个情况,页面上的所有其他代码在理论上都是正确的,但现在实际上不起作用了。你可以直接复制粘贴上面的代码。它花费了很长时间来完成,并且在生产中经过了广泛的测试。希望能帮到某个人!! - Fattie

3

如果你的新四边形是平行四边形,那么这被称为“剪切”,可以使用CGAffineTransform最轻松地完成。请参阅Jeff LaMarche的优秀文章CGAffineTransform 1.1

如果你的新四边形不是平行四边形,请参见以下问题以了解如何应用CATransform3D:iPhone图像拉伸(扭曲)


1
由于某些原因,KennyTM提供的技术似乎不再起作用。我还发现了这个问题:[Return CATransform3D to map quadrilateral to quadrilateral][https://dev59.com/qF_Va4cB1Zd3GeqPX-j5#9135803],但它也会对转换后的图像产生奇怪的影响。以及我们自己的矩阵计算。 - MonsieurDart
1
没有看到任何代码,我的怀疑是您未能包括透视变换。https://dev59.com/ynRC5IYBdhLWcg3wSu97 - Rob Napier
1
谢谢Rob,我已经阅读了这个问题,它似乎对我的代码没有问题。我正在进行更多的调查(尝试使用OpenCV),并且我会回来在这里更好地描述我们面临的问题。感谢您的友善和宝贵的帮助。 - MonsieurDart

2

独立的锚点解决方案:

我很喜欢@joshrl的答案,他创建了一个“UIView+Quadrilateral”类别,使用了@hfossli上面提供的非常好的答案。但是,多次调用该类别以更改四边形会失败,并且代码需要将AnchorPoint设置为左上角。

我的解决方案(源自他们的解决方案):

  • 适用于任何AnchorPoint
  • 允许更改四边形

UIView+Quadrilateral.h:

#import <UIKit/UIKit.h>
#import <QuartzCore/QuartzCore.h>

@interface UIView (Quadrilateral)

//Sets frame to bounding box of quad and applies transform
- (void)transformToFitQuadTopLeft:(CGPoint)tl topRight:(CGPoint)tr bottomLeft:(CGPoint)bl bottomRight:(CGPoint)br;

@end    

UIView+Quadrilateral.m:

#import "UIView+Quadrilateral.h"

@implementation UIView (Quadrilateral)

- (void)transformToFitQuadTopLeft:(CGPoint)tl topRight:(CGPoint)tr bottomLeft:(CGPoint)bl bottomRight:(CGPoint)br
{
    CGRect boundingBox = [[self class] boundingBoxForQuadTR:tr tl:tl bl:bl br:br];
    self.layer.transform = CATransform3DIdentity; // keeps current transform from interfering
    self.frame = boundingBox;

    CGPoint frameTopLeft = boundingBox.origin;
    CATransform3D transform = [[self class] rectToQuad:self.bounds
                                                quadTL:CGPointMake(tl.x-frameTopLeft.x, tl.y-frameTopLeft.y)
                                                quadTR:CGPointMake(tr.x-frameTopLeft.x, tr.y-frameTopLeft.y)
                                                quadBL:CGPointMake(bl.x-frameTopLeft.x, bl.y-frameTopLeft.y)
                                                quadBR:CGPointMake(br.x-frameTopLeft.x, br.y-frameTopLeft.y)];

    //  To account for anchor point, we must translate, transform, translate
    CGPoint anchorPoint = self.layer.position;
    CGPoint anchorOffset = CGPointMake(anchorPoint.x - boundingBox.origin.x, anchorPoint.y - boundingBox.origin.y);
    CATransform3D transPos = CATransform3DMakeTranslation(anchorOffset.x, anchorOffset.y, 0.);
    CATransform3D transNeg = CATransform3DMakeTranslation(-anchorOffset.x, -anchorOffset.y, 0.);
    CATransform3D fullTransform = CATransform3DConcat(CATransform3DConcat(transPos, transform), transNeg);

    //  Now we set our transform
    self.layer.transform = fullTransform;
}

+ (CGRect)boundingBoxForQuadTR:(CGPoint)tr tl:(CGPoint)tl bl:(CGPoint)bl br:(CGPoint)br
{
    CGRect boundingBox = CGRectZero;

    CGFloat xmin = MIN(MIN(MIN(tr.x, tl.x), bl.x),br.x);
    CGFloat ymin = MIN(MIN(MIN(tr.y, tl.y), bl.y),br.y);
    CGFloat xmax = MAX(MAX(MAX(tr.x, tl.x), bl.x),br.x);
    CGFloat ymax = MAX(MAX(MAX(tr.y, tl.y), bl.y),br.y);

    boundingBox.origin.x = xmin;
    boundingBox.origin.y = ymin;
    boundingBox.size.width = xmax - xmin;
    boundingBox.size.height = ymax - ymin;

    return boundingBox;
}

+ (CATransform3D)rectToQuad:(CGRect)rect
                     quadTL:(CGPoint)topLeft
                     quadTR:(CGPoint)topRight
                     quadBL:(CGPoint)bottomLeft
                     quadBR:(CGPoint)bottomRight
{
    return [self rectToQuad:rect quadTLX:topLeft.x quadTLY:topLeft.y quadTRX:topRight.x quadTRY:topRight.y quadBLX:bottomLeft.x quadBLY:bottomLeft.y quadBRX:bottomRight.x quadBRY:bottomRight.y];
}

+ (CATransform3D)rectToQuad:(CGRect)rect
                    quadTLX:(CGFloat)x1a
                    quadTLY:(CGFloat)y1a
                    quadTRX:(CGFloat)x2a
                    quadTRY:(CGFloat)y2a
                    quadBLX:(CGFloat)x3a
                    quadBLY:(CGFloat)y3a
                    quadBRX:(CGFloat)x4a
                    quadBRY:(CGFloat)y4a
{
    CGFloat X = rect.origin.x;
    CGFloat Y = rect.origin.y;
    CGFloat W = rect.size.width;
    CGFloat H = rect.size.height;

    CGFloat y21 = y2a - y1a;
    CGFloat y32 = y3a - y2a;
    CGFloat y43 = y4a - y3a;
    CGFloat y14 = y1a - y4a;
    CGFloat y31 = y3a - y1a;
    CGFloat y42 = y4a - y2a;

    CGFloat a = -H*(x2a*x3a*y14 + x2a*x4a*y31 - x1a*x4a*y32 + x1a*x3a*y42);
    CGFloat b = W*(x2a*x3a*y14 + x3a*x4a*y21 + x1a*x4a*y32 + x1a*x2a*y43);
    CGFloat c = H*X*(x2a*x3a*y14 + x2a*x4a*y31 - x1a*x4a*y32 + x1a*x3a*y42) - H*W*x1a*(x4a*y32 - x3a*y42 + x2a*y43) - W*Y*(x2a*x3a*y14 + x3a*x4a*y21 + x1a*x4a*y32 + x1a*x2a*y43);

    CGFloat d = H*(-x4a*y21*y3a + x2a*y1a*y43 - x1a*y2a*y43 - x3a*y1a*y4a + x3a*y2a*y4a);
    CGFloat e = W*(x4a*y2a*y31 - x3a*y1a*y42 - x2a*y31*y4a + x1a*y3a*y42);
    CGFloat f = -(W*(x4a*(Y*y2a*y31 + H*y1a*y32) - x3a*(H + Y)*y1a*y42 + H*x2a*y1a*y43 + x2a*Y*(y1a - y3a)*y4a + x1a*Y*y3a*(-y2a + y4a)) - H*X*(x4a*y21*y3a - x2a*y1a*y43 + x3a*(y1a - y2a)*y4a + x1a*y2a*(-y3a + y4a)));

    CGFloat g = H*(x3a*y21 - x4a*y21 + (-x1a + x2a)*y43);
    CGFloat h = W*(-x2a*y31 + x4a*y31 + (x1a - x3a)*y42);
    CGFloat i = W*Y*(x2a*y31 - x4a*y31 - x1a*y42 + x3a*y42) + H*(X*(-(x3a*y21) + x4a*y21 + x1a*y43 - x2a*y43) + W*(-(x3a*y2a) + x4a*y2a + x2a*y3a - x4a*y3a - x2a*y4a + x3a*y4a));

    const double kEpsilon = 0.0001;

    if(fabs(i) < kEpsilon)
    {
        i = kEpsilon* (i > 0 ? 1.0 : -1.0);
    }

    CATransform3D transform = {a/i, d/i, 0, g/i, b/i, e/i, 0, h/i, 0, 0, 1, 0, c/i, f/i, 0, 1.0};

    return transform;
}

@end

上述类别非常简单而优雅,应该包含在每个工具箱中。感谢上述代码的最终来源。不应将任何功劳归于我。


你知道,John,我没有找到“多次调用失败”的问题。请注意,你可以在链接中下载JRL的示例项目:它完美地工作。每次角落处理移动后,你只需使用普通的superview坐标调用transformToFitQuadTopLeft即可。顺便提一下,Swift版本也完美运行。再次感谢!干杯 - Fattie
太棒了。我喜欢你的更改。我的解决方案也考虑到了更新 - 我不确定你指的是什么。请查看此演示中的视频 https://github.com/agens-no/AGGeometryKit。我可能会借鉴你的锚点偏移改进 :) - hfossli

1
使用内置的 Swift 矩阵数学:

https://github.com/paulz/PerspectiveTransform#swift-code-example

import PerspectiveTransform

let destination = Perspective(
CGPoint(x: 108.315837, y: 80.1687782),
CGPoint(x: 377.282671, y: 41.4352201),
CGPoint(x: 193.321418, y: 330.023027),
CGPoint(x: 459.781253, y: 251.836131)
)

// Starting perspective is the current overlay frame or could be another 4 points.
let start = Perspective(overlayView.frame)

// Caclulate CATransform3D from start to destination
overlayView.layer.transform = start.projectiveTransform(destination: destination)

0

@hfossli的回答(被接受和得到最多票数的回答)是计算最终转换矩阵,这是一种"神奇的代码",以任何方式都很复杂且难以读懂,我认为没有任何真正的理由。

你需要做以下转换:

平移 x 旋转 x 缩放

(顺序很重要-缩放必须在最右边,平移在最左边)。

然后反转矩阵。

(或者你可以通过反转顺序并进行相反的变换(向相反方向平移,以相反角度旋转,并按比例缩小)来计算反转矩阵。)

在iOS中,我想应该是这样的:

CATransform3D t = CATransform3DIdentity;
t = CATransform3DScale(t, .... )
t = CATransform3DRotate(t, ....)
t = CATransform3DTranslate(t, ....)
CATransform3D invertT = CATransform3DInvert(t);

在这里你需要用实际的缩放、旋转和平移来填写....


感谢David提供这个建议,但不幸的是,我认为这三种基本变换无法捕捉透视映射所需的一些变换。 - MonsieurDart
1
没错,如果你需要更复杂的转换,那么你应该使用你发布的单应性解决方案(我认为这就是被接受答案的神奇代码的解释)。 - Maverick Meerkat

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接