Question

How should I interpret CoreGraphics CGAffineTransform into human readable, meaningful format?

I’m looking to take something like:

NSLog(@"naturalSize %@, appliedSize %@, transformationMatrix %@",
      NSStringFromCGSize(clipVideoTrack.naturalSize),
      NSStringFromCGSize(CGSizeApplyAffineTransform(clipVideoTrack.naturalSize, clipVideoTrack.preferredTransform)),
      NSStringFromCGAffineTransform(clipVideoTrack.preferredTransform));

naturalSize {1920, 1080}, appliedSize {-1080, 1920}, transformationMatrix [0, 1, -1, 0, 1080, 0]

The end result of the above matrix transformation is taking this Landscape Right Video and transforming into this Portrait Up Video

I would be glad to be able to break it down into steps, human readable form so that one can look and understand what the transformation is actually doing.

Something like (not sure I’m getting the steps right):

 0. Will use upper left corner for video export of width 1080, height 1920.
 1. Will move the video -1080(left) on the x axis 
 2. Will move the video 1920(down) on the y axis
 3. Will rotate 90deg clockwise from bottom right corner 

I would appreciate for points to a code that does this, or an implementation or an explanation. I attempting to learn and understand how exactly is the transformation matrix in the context of AVFoundation is functioning.

Was it helpful?

Solution

It turns out that in most cases you can make a decent description about an affine transform since it's quite constrained. Doing the same for 3D transforms is much harder :(

Note that I still won't be able to tell you the different transforms matrices that were concatenated, only the end result. I'm also ignoring shearing since there is no provided function for creating such a transform.


I wrote a function that does a decent job of figuring out what the affine transform does.

You will see in a number of places that I write if (fabs(foo - bar) < FLT_EPSILON) instead of just if (foo == bar). This is to protect myself for floating point (im)precision in the comparison.

The other notable thing to point out is the way I'm figuring out the rotation angle. For a pure rotation I could just have used asin(b), but if the transform is also scaled, then that result will be incorrect. Instead I divide b by a and use arctan to calculate the angle.

There is a decent amount of comments in the code so you should be able to follow along, mostly by just reading it.

NSString *affineTransformDescription(CGAffineTransform transform)
{
    // check if it's simply the identity matrix
    if (CGAffineTransformIsIdentity(transform)) {
        return @"Is the identity transform";
    }
    // the above does't catch things like a 720° rotation so also check it manually
    if (fabs(transform.a  - 1.0) < FLT_EPSILON &&
        fabs(transform.b  - 0.0) < FLT_EPSILON &&
        fabs(transform.c  - 0.0) < FLT_EPSILON &&
        fabs(transform.d  - 1.0) < FLT_EPSILON &&
        fabs(transform.tx - 0.0) < FLT_EPSILON &&
        fabs(transform.ty - 0.0) < FLT_EPSILON) {
        return @"Is the identity transform";
    }

    // The affine transforms is built up like this:

    // a b tx
    // c d ty
    // 0 0 1

    // An array to hold all the different descirptions, charasteristics of the transform.
    NSMutableArray *descriptions = [NSMutableArray array];

    // Checking for a translation
    if (fabs(transform.tx) > FLT_EPSILON) { // translation along X
        [descriptions addObject:[NSString stringWithFormat:@"Will move %.2f along the X axis",
                                 transform.tx]];
    }
    if (fabs(transform.ty) > FLT_EPSILON) { // translation along Y
        [descriptions addObject:[NSString stringWithFormat:@"Will move %.2f along the Y axis",
                                 transform.ty]];
    }


    // Checking for a rotation
    CGFloat angle = atan2(transform.b, transform.a); // get the angle of the rotation. Note this assumes no shearing!
    if (fabs(angle) < FLT_EPSILON || fabs(angle - M_PI) < FLT_EPSILON) {
        // there is a change that there is a 180° rotation, in that case, A and D will and be negative.
        BOOL bothAreNegative  = transform.a < 0.0 && transform.d < 0.0;

        if (bothAreNegative) {
            angle = M_PI;
        } else {
            angle = 0.0; // this is not considered a rotation, but a negative scale along one axis.
        }
    }

    // add the rotation description if there was an angle
    if (fabs(angle) > FLT_EPSILON) {
        [descriptions addObject:[NSString stringWithFormat:@"Will rotate %.1f° degrees",
                                 angle*180.0/M_PI]];
    }


    // Checking for a scale (and account for the possible rotation as well)
    CGFloat scaleX = transform.a/cos(angle);
    CGFloat scaleY = transform.d/cos(angle);


    if (fabs(scaleX - scaleY) < FLT_EPSILON && fabs(scaleX - 1.0) > FLT_EPSILON) {
        // if both are the same then we can format things a little bit nicer
        [descriptions addObject:[NSString stringWithFormat:@"Will scale by %.2f along both X and Y",
                                 scaleX]];
    } else {
        // otherwise we look at X and Y scale separately
        if (fabs(scaleX - 1.0) > FLT_EPSILON) { // scale along X
            [descriptions addObject:[NSString stringWithFormat:@"Will scale by %.2f along the X axis",
                                     scaleX]];
        }

        if (fabs(scaleY - 1.0) > FLT_EPSILON) { // scale along Y
            [descriptions addObject:[NSString stringWithFormat:@"Will scale by %.2f along the Y axis",
                                     scaleY]];
        }
    }

    // Return something else when there is nothing to say about the transform matrix
    if (descriptions.count == 0) {
        return @"Can't easilly be described.";
    }

    // join all the descriptions on their own line
    return [descriptions componentsJoinedByString:@",\n"];
}

To try it out I tested the output on a number of different transforms. This is the code I used to test it:

// identity
CGAffineTransform t = CGAffineTransformIdentity;
NSLog(@"identity: \n%@", affineTransformDescription(t));


// translation
t = CGAffineTransformMakeTranslation(10, 0);
NSLog(@"translate(10, 0): \n%@", affineTransformDescription(t));

t = CGAffineTransformMakeTranslation(0, 20);
NSLog(@"translate(0, 20): \n%@", affineTransformDescription(t));

t = CGAffineTransformMakeTranslation(2, -3);
NSLog(@"translate(2, -3): \n%@", affineTransformDescription(t));


// scale
t = CGAffineTransformMakeScale(2, 2);
NSLog(@"scale(2, 2): \n%@", affineTransformDescription(t));


t = CGAffineTransformMakeScale(-1, 3);
NSLog(@"scale(-1, 3): \n%@", affineTransformDescription(t));



// rotation
t = CGAffineTransformMakeRotation(M_PI/3.0);
NSLog(@"rotate(60 deg): \n%@", affineTransformDescription(t));

t = CGAffineTransformMakeRotation(M_PI);
NSLog(@"rotate(180 deg): \n%@", affineTransformDescription(t));

t = CGAffineTransformMakeRotation(4.0*M_PI);
NSLog(@"rotate(720 deg): \n%@", affineTransformDescription(t));

t = CGAffineTransformMakeRotation(3.0*M_PI);
NSLog(@"rotate(540 deg): \n%@", affineTransformDescription(t));



// concatenated transforms
// rotate & translate
t = CGAffineTransformMakeRotation(M_PI/3.0);
t = CGAffineTransformTranslate(t, 10, 20);
NSLog(@"rotate(60 deg), translate(10, 20): \n%@", affineTransformDescription(t));

t = CGAffineTransformMakeTranslation(10, 20);
t = CGAffineTransformRotate(t, M_PI/3.0);
NSLog(@"translate(10, 20), rotate(60 deg): \n%@", affineTransformDescription(t));

// rotate & scale
t = CGAffineTransformMakeRotation(M_PI/3.0);
t = CGAffineTransformScale(t, 2, 2);
NSLog(@"rotate(60 deg), scale(2, 2): \n%@", affineTransformDescription(t));

t = CGAffineTransformMakeScale(2, 2);
t = CGAffineTransformRotate(t, M_PI/3.0);
NSLog(@"scale(2, 2), rotate(60 deg): \n%@", affineTransformDescription(t));

// translate & scale
t = CGAffineTransformMakeTranslation(10, 20);
t = CGAffineTransformScale(t, 2, 2);
NSLog(@"translate(10, 20), scale(2, 2): \n%@", affineTransformDescription(t));

t = CGAffineTransformMakeScale(2, 2);
t = CGAffineTransformTranslate(t, 10, 20);
NSLog(@"scale(2, 2), translate(10, 20): \n%@", affineTransformDescription(t));

and the output from that test:

identity: 
  Is the identity transform
translate(10, 0): 
  Will move 10.00 along the X axis
translate(0, 20): 
  Will move 20.00 along the Y axis
translate(2, -3): 
  Will move 2.00 along the X axis,
  Will move -3.00 along the Y axis
scale(2, 2): 
  Will scale by 2.00 along both X and Y
scale(-1, 3): 
  Will scale by -1.00 along the X axis,
  Will scale by 3.00 along the Y axis
rotate(60 deg): 
  Will rotate 60.0° degrees
rotate(180 deg): 
  Will rotate 180.0° degrees
rotate(720 deg): 
  Is the identity transform
rotate(540 deg): 
  Will rotate 180.0° degrees
rotate(60 deg), translate(10, 20): 
  Will move -12.32 along the X axis,
  Will move 18.66 along the Y axis,
  Will rotate 60.0° degrees
translate(10, 20), rotate(60 deg): 
  Will move 10.00 along the X axis,
  Will move 20.00 along the Y axis,
  Will rotate 60.0° degrees
rotate(60 deg), scale(2, 2): 
  Will rotate 60.0° degrees,
  Will scale by 2.00 along both X and Y
scale(2, 2), rotate(60 deg): 
  Will rotate 60.0° degrees,
  Will scale by 2.00 along both X and Y
translate(10, 20), scale(2, 2): 
  Will move 10.00 along the X axis,
  Will move 20.00 along the Y axis,
  Will scale by 2.00 along both X and Y
scale(2, 2), translate(10, 20): 
  Will move 20.00 along the X axis,
  Will move 40.00 along the Y axis,
  Will scale by 2.00 along both X and Y

OTHER TIPS

To understand how affine transform matrices work you would be well served to read this excellent article.

As mentioned in the comments once a few transforms have been added together it becomes very difficult to work out the individual components. I have a small app on GitHub which allows you to stack up transforms to see the individual effects. This is for 3D transforms but the principles are the same.

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top