iosquartz-graphicscgcontextcgaffinetransform

CGContext transformation, to extract rotated rect


My intention is to write a function which can extract a rotated rectangular portion of a larger image. That rotated rectangular portion is described with a center point (from 0,0 to full image width/height) a size, and a rotation angle.

I worry that I'm not understanding something about CGContexts. I know that after a rotation, my coordinate space is rotated.

This function always works if the angle is 0 and appears to be correct if the angle is small. However, as the angle increases, everything is offset improperly.

I am calculating the top-left corner with an affine transform similar to the image transformation to avoid any inaccuracy. I have also tried further changing the top-left corner by applying the image transformation to it, as well as the inverse of the image transformation (just as a random thought).

Will this approach work? Am I missing a space transformation somewhere?

Note: It was extremely easy to black out the image area by drawing the full image and then drawing a black rectangle at the correct position with the right size and rotation. Given this, I know my inputs are correct.

Visualizations:

This represents the full image, and the rotated rect I am trying to sample: This represents the full image, and the rotated rect I am trying to sample

This image represents the desired output of this function: This image represents the desired output of this function

- (UIImage*) croppedImage:(UIImage*) image center:(CGPoint) rotationCenter size:(CGSize) size rotation:(CGFloat) rotation
{
    CGFloat fullWidth = image.size.width;
    CGFloat fullHeight = image.size.height;
    CGRect imageRect = CGRectMake(0, 0, fullWidth, fullHeight);

    //must convert UI space to CG space, in this case that just means adjusting the y coordinate
    rotationCenter.y = fullHeight - rotationCenter.y;

    CGFloat width = size.width;
    CGFloat height = size.height;

    int flags = kCGBitmapByteOrderDefault | kCGImageAlphaPremultipliedLast;
    CGContextRef btx = CGBitmapContextCreate(NULL, width, height, 8, 0, CGColorSpaceCreateDeviceRGB(), flags);
    CGContextSetAllowsAntialiasing(btx, YES);
    CGContextSetShouldAntialias(btx, YES);

    //use a transformation to rotate at the center of the request area
    CGAffineTransform imageTransform = CGAffineTransformIdentity;
    imageTransform = CGAffineTransformTranslate(imageTransform, rotationCenter.x, rotationCenter.y);
    imageTransform = CGAffineTransformRotate(imageTransform, rotation);
    imageTransform = CGAffineTransformTranslate(imageTransform, -rotationCenter.x, -rotationCenter.y);

    //use a transformation to determine the top-left coordinate of the rect
    CGAffineTransform ptTransform = CGAffineTransformIdentity;
    ptTransform = CGAffineTransformTranslate(ptTransform, rotationCenter.x, rotationCenter.y);
    ptTransform = CGAffineTransformRotate(ptTransform, rotation);
    ptTransform = CGAffineTransformTranslate(ptTransform, -width/2.0, -height/2.0);

    //this gets me the position of the top-left corner of the image no matter the rotation
    CGPoint topleft  = CGPointApplyAffineTransform(CGPointZero, ptTransform);

    CGContextConcatCTM(btx, imageTransform);

    //move the image away from the origin, to align origin with where i want my image sampled from
    CGContextTranslateCTM(btx, -topleft.x,
                                -topleft.y);

    //Close... but wrong
    CGContextDrawImage(btx, imageRect, image.CGImage);

    CGImageRef img = CGBitmapContextCreateImage(btx);
    UIImage* uiimage =  [UIImage imageWithCGImage:img scale:image.scale orientation:image.imageOrientation];

    CGContextRelease(btx);
    CGImageRelease(img);

    return uiimage;
}

Solution

  • What you are describing, I believe, is this sort of thing:

    enter image description here

    That is an actual running iOS app, so you can see what I've done: in the first picture, I've drawn in blue the rectangle I intend to capture, and shown the result in an image view; in the second picture, I've captured it, and shown that result in an image view.

    As you can see by looking carefully at the edges of the second image, we've captured the blue rectangle contents exactly.

    So now I suppose you want to know how I did that? The blue rectangle was drawn using three pieces of information: the top left of the rectangle topLeft, the size of the rectangle size, and the rotation rot. To simplify things, I have disregarded your talk about the "center"; I have rotated around the top left.

    Then the second image was captured by creating a graphics context of size size, rotating it the negative of rot, and drawing the full original image at the negative of topLeft. The actual drawing part is just two lines of code (sorry, I write and think in Swift these days, but I'm sure you can translate):

    CGContextRotateCTM(context, -rot)
    im.drawAtPoint(CGPointMake(-topLeft.x, -topLeft.y))
    

    So I guess my answer ultimately is: this is very easy and works just as you would expect, if you will turn convert your original description of what you want to do so that there is a topLeft instead of a center.