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.
The CGAffineTransform type provides functions for creating, concatenating, and applying affine transformations. Affine transforms are represented by a 3 by 3 matrix: Because the third column is always (0,0,1) , the CGAffineTransform data structure contains values for only the first two columns.
An affine transformation allows a UIView to be translated, scaled, rotated or skewed. UIViews have a CGAffineTransform property called transform.
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
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With