44

I have an image and a set of four points (describing a quadrilateral Q). I want to transform this image so that it is fits the quadrilateral Q. Photoshop calls this transformation "Distort." But according to the source of this quadrilateral (the perspective of the image moving in space), it is in fact the combination of a scale, a rotation and a perspective matrix.

I am wondering if this is possible using a CATransform3D 4x4 matrix. Do you have any hints on how to do that? I've tried to take the four points and build 16 equations (out of A' = A x u) but it did not work: I'm not sure of what I should use as z, z', w and w' coefficients…

The following picture shows what I want to do:

Transforming a rectangle image into a quadrilateral using a CATransform3D

Here are some examples of points:

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
MonsieurDart
  • 6,005
  • 2
  • 43
  • 45
  • 4
    I don't think you're going to find a better answer to this than KennyTM's on the similar question [iPhone image stretching (skew)](http://stackoverflow.com/questions/2351586/iphone-image-stretching-skew). The math he presents there should be enough to create an arbitrary quadrilateral as a result of applying a CATransform3D to a CALayer. – Brad Larson Feb 27 '12 at 20:45
  • 1
    @BradLarson, for some odd reason, the answer of KennyTM does not seem to work. We get a 3D matrix, but it gives dummy results when given to CoreAnimation… – MonsieurDart Mar 05 '12 at 11:00
  • What about doing reverse transform from quadrilateral to rect? – Cherpak Evgeny May 28 '13 at 16:21
  • @CherpakEvgeny, I think the `cvFindHomography()` OpenCV function would also work for that way. – MonsieurDart Sep 11 '13 at 11:25
  • @BradLarson and others, KennyTM's amazing code from 5 yrs ago is definitely a bit of a chore to get to work in practice since there is some muddle with ABCD versus ABDC ordering, translation, and the like ... thank goodness, JoshRL below did all the hard work and created a flawless drop-in class based on KTM's math. There's now a Swift version which works perfectly and is tested a lot http://stackoverflow.com/a/18606029/294884 – Fattie May 23 '16 at 18:27

8 Answers8

39

I've created a kit for doing this on iOS: https://github.com/hfossli/AGGeometryKit/


Make sure your anchor point is top left (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;
}

I take no credit for this code. All I did was scouring the internet and put together various incomplete answers.

hfossli
  • 22,616
  • 10
  • 116
  • 130
  • Sorry if this seems obvious, but how can I apply this to an **array of point** inside the rect ? – Benoît Lahoz Mar 05 '13 at 17:47
  • I have a NSArray filled with points (x, y) that defines a polygon. Then I have a bounding box, and I would like to apply this king of transformation to all my points while dragging the corner. – Benoît Lahoz Mar 06 '13 at 01:25
  • Great ! Thank you ! And if it is concave ? I could split the polygon in convex ones, then apply the transform and reunify them ? – Benoît Lahoz Mar 10 '13 at 11:22
  • Sorry but : which value am I supposed to use for my x and y (c and b on CGAffine) ? – Benoît Lahoz Mar 10 '13 at 11:23
  • Yes, I can use the bounding box. – Benoît Lahoz Mar 11 '13 at 07:25
  • Sorry for the late response, I'm struggling with another thing in my code before I can implement your solution :-) I'll tell you soon ! – Benoît Lahoz Mar 21 '13 at 23:12
  • 1
    Excellent, I needed a Java solution, but this allows itself to port in an hour - thank You! – Rekin Jan 03 '14 at 11:31
  • @Rekin would you care to share? :D – hfossli May 09 '14 at 08:47
  • 1
    for 2016 just scroll down for a drop in **Swift** class for this ... http://stackoverflow.com/a/18606029/294884 ... based on all the old great answers here – Fattie May 23 '16 at 18:28
  • 1
    Any thoughts on how to do the opposite? Given the same rectangle and quadrilateral inputs, how can I create a CATransform3D to transform the quadrilateral into the rectangle? I would use it to transform an image with perspective, rotation and scale to look like the eye view was perpendicular to the viewer. – VTPete Aug 31 '20 at 00:33
10

Here is a sample project which applies code from hfossli's answer above and creates a category on UIView which sets the frame and applies the transform in one call:

https://github.com/joshrl/FreeTransform

UIView+Quadrilateral code :

#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

enter image description here

Fattie
  • 27,874
  • 70
  • 431
  • 719
joshrl
  • 3,527
  • 1
  • 22
  • 12
  • 1
    note, I couldn't totally get Josh's amazing class to work for a few reasons with modern Swift. Just scroll down, http://stackoverflow.com/a/39981054/294884 we made and highly tested a fully working cut and paste version of Josh's work, you can just paste and use, hope it helps someone!! – Fattie Oct 18 '16 at 19:34
9

With 100% thanks to JoshRL, here's a Swift version of JoshRL's class.

This has been completely and totally debugged. Lines that suffer the "too long in Swift" issue have been refactored and destruction tested. It is working flawlessly in high-volume production.

Couldn't be easier to use. Example showing how to use in Swift below.

2016 Swift version... full, working, copy and paste solution

Refreshed for 2022 ! Drop in code with current syntax etc.

// JoshQuadView in Swift
// from: https://stackoverflow.com/a/18606029/294884

// 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)
    }
}

For a quick test:

@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))

It will render the normal square image.

    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))

It will bend away the top left corner.


Important reminder. JoshRL originally used the ordering tl tr bl br. So that is maintained here. It's more common to use tl then clockwise when dealing with verts (ie, tl tr br bl), so just bear it in mind!


To use in Swift with draggable corner handles:

say you have a container view "QuadScreen".

The view you want to stretch will be a JoshQuadView. Drop it in the scene. Connect it to the IBOutlet, "jqv" in the example here.

Simply put four corner-handles (ie, images) in the scene, being PNGs of your handle icons. Link those to the four IBOutlets for handles. The code just completely handles these handles. (Follow the comments in the code for how to easily set them up in storyboard.)

Then, it's just one line of code to do the stretching:


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
    }
}

As a curiosity, and for the sake of google, it's ridiculously easy to do this in

Android

they have a built-in command for reshaping polys. This excellent answer has copy and paste code: https://stackoverflow.com/a/34667015/294884

Fattie
  • 27,874
  • 70
  • 431
  • 719
  • 2
    hero of the day! – hbk Jun 27 '17 at 14:08
  • 3
    Right, hope it helps. it's a case where all the other code on this page is **theoretically correct, but doesn't actually work these days**. You can just copy and paste the code above. it took a huge time to finalize and, has been tested very extensively in production. Hope it helps someone!! – Fattie Jun 27 '17 at 14:11
9

We finally got this to work. We've tried several different methods, but most were failing. And some were even retrieving a non identity matrix when giving the same points as input and outputs (for example, the one from KennyTM… we must have been missing something there).

Using OpenCV as following, we get a CATransform3D ready to be used on a CAAnimation layer:

+ (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; 
}
Tomasz Stanczak
  • 12,796
  • 1
  • 30
  • 32
MonsieurDart
  • 6,005
  • 2
  • 43
  • 45
  • can you paste transform3DWithCMatrix method –  May 08 '12 at 19:42
  • 1
    To get this to work correctly, I had to do the calculation with coordinates in the coordinate system of the layer that was using the transform. I also had to set the `anchorPoint` to the top left corner (`transform` is applied relative to `anchorPoint`). – Ben Lings May 28 '12 at 11:46
  • 1
    Also, I converted the OpenCV code to use the OpenCV 2 method `cv::getPerspectiveTransform`. The elements matrix returned by both this function and `cv::findHomography` are of type `double` not `float` as shown above. – Ben Lings May 28 '12 at 12:03
  • OMG! I'm really sorry for having forgotten the `anchorPoint` specificity! And thanks for the `double` vs `float` return value, I didn't noticed that. But what exactly is the difference between `cv::findHomography` and `cv::getPerspectiveTransform`? – MonsieurDart May 28 '12 at 21:00
3

If your new quadrilateral is a parallelogram, then this is called "shear," and can be done most easily with CGAffineTransform. See Jeff LaMarche's excellent article, CGAffineTransform 1.1.

If your new quadrilateral is not a parallelogram, then see the following question for how to apply CATransform3D: iPhone image stretching (skew).

Community
  • 1
  • 1
Rob Napier
  • 286,113
  • 34
  • 456
  • 610
  • 1
    For some reason, the technique given by KennyTM does not seem to work anymore. I've also found this question: [Return CATransform3D to map quadrilateral to quadrilateral][http://stackoverflow.com/questions/9088882/return-catransform3d-to-map-quadrilateral-to-quadrilateral/9135803#9135803], but it also produces strange effects on the transformed image. As well as our own matrix calculation. – MonsieurDart Mar 05 '12 at 11:03
  • 1
    Without seeing any code, my suspicion would be that you are failing to include the perspective transform. http://stackoverflow.com/questions/347721/how-do-i-apply-a-perspective-transform-to-a-uiview – Rob Napier Mar 05 '12 at 14:41
  • 1
    Thanks Rob, I've read this question and it seems to be fine on my code. I'm doing more investigations (trying to use OpenCV) and I'll come back here to describe better the problem we are facing. Thanks for your kind and precious help. – MonsieurDart Mar 06 '12 at 14:31
2

ANCHOR POINT INDEPENDENT Solution:

I really like @joshrl answer where he makes a category "UIView+Quadrilateral" which uses @hfossli's most excellent answer above. However, multiple calls to the category to change the quadrilateral fails, and the code requires the AnchorPoint to be top-left.

My solution (derived from theirs):

  • Accounts for any AnchorPoint
  • Allows for changes to the quadrilateral

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

The above category is so simple and elegant, it ought to be included in every toolbox. THANK YOUs to the ultimate sources of the above code. No credit should be given to me.

John Fowler
  • 3,173
  • 3
  • 18
  • 31
  • you know FYI John I did not find the issue that "multiple calls fail". Note that you can just download JRL's example project on the link: it works perfectly. After each corner-handle move, you simply call `transformToFitQuadTopLeft` with plain superview coordinates and that's it. the Swift version also works perfectly FYI. Thanks again also! Cheers – Fattie May 23 '16 at 18:23
  • Awesome. I like your changes. My solution also accounts for updates – I'm not sure what you are referring to. See the videos in this demo https://github.com/agens-no/AGGeometryKit. I might borrow your anchor offset improvements :) – hfossli May 24 '16 at 07:38
1

Using built-in Swift matrix math:

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)
Paul Zabelin
  • 225
  • 2
  • 6
0

@hfossli answer, (the accepted and most voted answer) is calculating the final transform matrix, which is "magic code" that is complicated and unreadable in any way, and I think without any real reason.

What you need to do is the following transformations:

translation x rotation x scaling

(Order is important - you must have the scaling as the most right side, and translation most left).

And then invert the matrix.

(Or you could already calculate the inverted matrix by inverting the order, and doing the opposite transformations (translating in opposite direction, rotating in opposite angle, and scale in inverse size). )

In iOS I guess it will be something along the lines of:

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

where you fill .... with the actual scaling, rotation and translation needed.

Maverick Meerkat
  • 5,737
  • 3
  • 47
  • 66
  • Thanks David for this proposal, but unfortunately I think those three base transformations cannot capture some transformations needed by a perspective mapping. – MonsieurDart Jun 07 '19 at 11:25
  • 1
    Yeah, you're right. If you need more complex transformation then you should use the homography solution you posted (which I think is the explanation of the magic code of the accepted answer). – Maverick Meerkat Jun 15 '19 at 20:24