UIImage: redimensionner, puis recadrer


187

Cela fait littéralement des jours que je frappe mon visage contre celui-ci et même si je sens constamment que je suis juste au bord de la révélation, je ne peux tout simplement pas atteindre mon objectif.

J'ai pensé, à l'avance dans les phases conceptuelles de ma conception, qu'il serait trivial de saisir une image de l'appareil photo ou de la bibliothèque de l'iPhone, de la réduire à une hauteur spécifiée, en utilisant une fonction équivalente à l' option Aspect Fill de UIImageView (entièrement dans le code), puis rognez tout ce qui ne rentre pas dans un CGRect passé.

Obtenir l'image originale de la caméra ou de la bibliothèque était trivial. Je suis choqué de voir à quel point les deux autres étapes se sont révélées difficiles.

L'image ci-jointe montre ce que j'essaie d'accomplir. Quelqu'un aurait-il la gentillesse de me tenir la main? Chaque exemple de code que j'ai trouvé jusqu'à présent semble briser l'image, être à l'envers, ressembler à de la merde, sortir des limites ou tout simplement ne pas fonctionner correctement.


7
Le lien est rompu avec votre image.

1
Une chose dont nous sommes choqués est pourquoi Apple ne l'a pas ajouté à UIImagePickerController - c'est trop difficile ;-)
Tim

Réponses:


246

J'avais besoin de la même chose - dans mon cas, choisir la dimension qui correspond une fois mise à l'échelle, puis recadrer chaque extrémité pour adapter le reste à la largeur. (Je travaille en paysage, donc je n'aurais peut-être pas remarqué de lacunes en mode portrait.) Voici mon code - il fait partie d'une catégorie sur UIImage. La taille cible dans mon code est toujours définie sur la taille plein écran de l'appareil.

@implementation UIImage (Extras)

#pragma mark -
#pragma mark Scale and crop image

- (UIImage*)imageByScalingAndCroppingForSize:(CGSize)targetSize
{
    UIImage *sourceImage = self;
    UIImage *newImage = nil;    
    CGSize imageSize = sourceImage.size;
    CGFloat width = imageSize.width;
    CGFloat height = imageSize.height;
    CGFloat targetWidth = targetSize.width;
    CGFloat targetHeight = targetSize.height;
    CGFloat scaleFactor = 0.0;
    CGFloat scaledWidth = targetWidth;
    CGFloat scaledHeight = targetHeight;
    CGPoint thumbnailPoint = CGPointMake(0.0,0.0);

    if (CGSizeEqualToSize(imageSize, targetSize) == NO) 
    {
        CGFloat widthFactor = targetWidth / width;
        CGFloat heightFactor = targetHeight / height;

        if (widthFactor > heightFactor) 
        {
            scaleFactor = widthFactor; // scale to fit height
        }
        else
        {
            scaleFactor = heightFactor; // scale to fit width
        }

        scaledWidth  = width * scaleFactor;
        scaledHeight = height * scaleFactor;

        // center the image
        if (widthFactor > heightFactor)
        {
            thumbnailPoint.y = (targetHeight - scaledHeight) * 0.5; 
        }
        else
        {
            if (widthFactor < heightFactor)
            {
                thumbnailPoint.x = (targetWidth - scaledWidth) * 0.5;
            }
        }
    }   

    UIGraphicsBeginImageContext(targetSize); // this will crop

    CGRect thumbnailRect = CGRectZero;
    thumbnailRect.origin = thumbnailPoint;
    thumbnailRect.size.width  = scaledWidth;
    thumbnailRect.size.height = scaledHeight;

    [sourceImage drawInRect:thumbnailRect];

    newImage = UIGraphicsGetImageFromCurrentImageContext();

    if(newImage == nil)
    {
        NSLog(@"could not scale image");
    }

    //pop the context to get back to the default
    UIGraphicsEndImageContext();

    return newImage;
}

1
curieusement, cela fonctionne dans le simulateur mais sur l'appareil, je reçois ExecBadAccess ..
Sorin Antohi

5
Le problème avec l'exécution incorrecte se produit, car les fonctions UIImage ne sont pas thread-safe. C'est pourquoi il se bloque parfois, parfois pas
Erik

1
Ce code fonctionnait très bien pour moi, mais il était flou sur la rétine. La combinaison de ce code avec le commentaire ci-dessous a rendu tout parfait: stackoverflow.com/questions/603907/…
MaxGabriel

23
Utilisez UIGraphicsBeginImageContextWithOptions (targetSize, YES, 0.0); pour rendre l'image agréable sur la rétine également.
Borut Tomazin

1
Cela fonctionne pour moi en mode Portrait, mais pas en mode paysage. La hauteur est étirée en paysage. Une idée pourquoi?
Darren

77

Un article plus ancien contient le code d'une méthode pour redimensionner votre UIImage. La partie pertinente est la suivante:

+ (UIImage*)imageWithImage:(UIImage*)image 
               scaledToSize:(CGSize)newSize;
{
   UIGraphicsBeginImageContext( newSize );
   [image drawInRect:CGRectMake(0,0,newSize.width,newSize.height)];
   UIImage* newImage = UIGraphicsGetImageFromCurrentImageContext();
   UIGraphicsEndImageContext();

   return newImage;
}

En ce qui concerne le recadrage, je crois que si vous modifiez la méthode pour utiliser une taille différente pour la mise à l'échelle que pour le contexte, votre image résultante doit être découpée dans les limites du contexte.


3
Cela ne donne aucun sens à la raison pour laquelle cela corrige l'orientation de l'image, mais c'est le cas et a donc résolu mon problème avec la caméra ne retournant pas la bonne orientation dans le originalImage. Merci.
Brenden

10
J'ai trouvé que le redimensionnement d'une image sur un appareil Retina semblait flou. Pour maintenir la clarté, j'ai modifié la première ligne à ce qui suit: UIGraphicsBeginImageContextWithOptions(newSize, 1.0f, 0.0f);. (expliqué ici: stackoverflow.com/questions/4334233/… )
johngraham

Il fait pivoter l'image - mais ne la recadre pas correctement! Pourquoi y a-t-il autant de votes?
Dejell

18
+ (UIImage *)scaleImage:(UIImage *)image toSize:(CGSize)targetSize {
    //If scaleFactor is not touched, no scaling will occur      
    CGFloat scaleFactor = 1.0;

    //Deciding which factor to use to scale the image (factor = targetSize / imageSize)
    if (image.size.width > targetSize.width || image.size.height > targetSize.height)
        if (!((scaleFactor = (targetSize.width / image.size.width)) > (targetSize.height / image.size.height))) //scale to fit width, or
            scaleFactor = targetSize.height / image.size.height; // scale to fit heigth.

    UIGraphicsBeginImageContext(targetSize); 

    //Creating the rect where the scaled image is drawn in
    CGRect rect = CGRectMake((targetSize.width - image.size.width * scaleFactor) / 2,
                             (targetSize.height -  image.size.height * scaleFactor) / 2,
                             image.size.width * scaleFactor, image.size.height * scaleFactor);

    //Draw the image into the rect
    [image drawInRect:rect];

    //Saving the image, ending image context
    UIImage *scaledImage = UIGraphicsGetImageFromCurrentImageContext();
    UIGraphicsEndImageContext();

    return scaledImage;
}

Je propose celui-ci. N'est-elle pas une beauté? ;)


1
Celui-ci est bon, si vous supprimez d'abord l'instruction if, elle agit comme AspectFill.
RaffAl


7

Voici. Celui-ci est parfait ;-)

EDIT: voir le commentaire ci-dessous - "Ne fonctionne pas avec certaines images, échoue avec: CGContextSetInterpolationQuality: erreur de contexte 0x0 non valide"

// Resizes the image according to the given content mode, taking into account the image's orientation
- (UIImage *)resizedImageWithContentMode:(UIViewContentMode)contentMode imageToScale:(UIImage*)imageToScale bounds:(CGSize)bounds interpolationQuality:(CGInterpolationQuality)quality {
    //Get the size we want to scale it to
    CGFloat horizontalRatio = bounds.width / imageToScale.size.width;
    CGFloat verticalRatio = bounds.height / imageToScale.size.height;
    CGFloat ratio;

    switch (contentMode) {
        case UIViewContentModeScaleAspectFill:
            ratio = MAX(horizontalRatio, verticalRatio);
            break;

        case UIViewContentModeScaleAspectFit:
            ratio = MIN(horizontalRatio, verticalRatio);
            break;

        default:
            [NSException raise:NSInvalidArgumentException format:@"Unsupported content mode: %d", contentMode];
    }

    //...and here it is
    CGSize newSize = CGSizeMake(imageToScale.size.width * ratio, imageToScale.size.height * ratio);


    //start scaling it
    CGRect newRect = CGRectIntegral(CGRectMake(0, 0, newSize.width, newSize.height));
    CGImageRef imageRef = imageToScale.CGImage;
    CGContextRef bitmap = CGBitmapContextCreate(NULL,
                                                newRect.size.width,
                                                newRect.size.height,
                                                CGImageGetBitsPerComponent(imageRef),
                                                0,
                                                CGImageGetColorSpace(imageRef),
                                                CGImageGetBitmapInfo(imageRef));

    CGContextSetInterpolationQuality(bitmap, quality);

    // Draw into the context; this scales the image
    CGContextDrawImage(bitmap, newRect, imageRef);

    // Get the resized image from the context and a UIImage
    CGImageRef newImageRef = CGBitmapContextCreateImage(bitmap);
    UIImage *newImage = [UIImage imageWithCGImage:newImageRef];

    // Clean up
    CGContextRelease(bitmap);
    CGImageRelease(newImageRef);

    return newImage;
}

1
Look really beautiful :) J'aime InterpolationQuality here
Kostiantyn Sokolinskyi

Ne fonctionne pas avec certaines images, échoue avec: CGContextSetInterpolationQuality: erreur de contexte 0x0 non valide
RaffAl

6

Ceci est une version de la réponse de Jane Sales dans Swift. À votre santé!

public func resizeImage(image: UIImage, size: CGSize) -> UIImage? {
    var returnImage: UIImage?

    var scaleFactor: CGFloat = 1.0
    var scaledWidth = size.width
    var scaledHeight = size.height
    var thumbnailPoint = CGPointMake(0, 0)

    if !CGSizeEqualToSize(image.size, size) {
        let widthFactor = size.width / image.size.width
        let heightFactor = size.height / image.size.height

        if widthFactor > heightFactor {
            scaleFactor = widthFactor
        } else {
            scaleFactor = heightFactor
        }

        scaledWidth = image.size.width * scaleFactor
        scaledHeight = image.size.height * scaleFactor

        if widthFactor > heightFactor {
            thumbnailPoint.y = (size.height - scaledHeight) * 0.5
        } else if widthFactor < heightFactor {
            thumbnailPoint.x = (size.width - scaledWidth) * 0.5
        }
    }

    UIGraphicsBeginImageContextWithOptions(size, true, 0)

    var thumbnailRect = CGRectZero
    thumbnailRect.origin = thumbnailPoint
    thumbnailRect.size.width = scaledWidth
    thumbnailRect.size.height = scaledHeight

    image.drawInRect(thumbnailRect)
    returnImage = UIGraphicsGetImageFromCurrentImageContext()

    UIGraphicsEndImageContext()

    return returnImage
}

2

J'ai modifié le code de Brad Larson. Il remplira l'aspect de l'image dans le rect donné.

-(UIImage*) scaleAndCropToSize:(CGSize)newSize;
{
    float ratio = self.size.width / self.size.height;

    UIGraphicsBeginImageContext(newSize);

    if (ratio > 1) {
        CGFloat newWidth = ratio * newSize.width;
        CGFloat newHeight = newSize.height;
        CGFloat leftMargin = (newWidth - newHeight) / 2;
        [self drawInRect:CGRectMake(-leftMargin, 0, newWidth, newHeight)];
    }
    else {
        CGFloat newWidth = newSize.width;
        CGFloat newHeight = newSize.height / ratio;
        CGFloat topMargin = (newHeight - newWidth) / 2;
        [self drawInRect:CGRectMake(0, -topMargin, newSize.width, newSize.height/ratio)];
    }

    UIImage* newImage = UIGraphicsGetImageFromCurrentImageContext();
    UIGraphicsEndImageContext();

    return newImage;
}

1
Testé avec deux autres images, portrait et paysage. Ceci n'effectue PAS le remplissage d'aspect.
RaffAl

2

La version Xamarin.iOS pour une réponse acceptée sur la façon de redimensionner puis de rogner UIImage (Aspect Fill) est ci-dessous

    public static UIImage ScaleAndCropImage(UIImage sourceImage, SizeF targetSize)
    {
        var imageSize = sourceImage.Size;
        UIImage newImage = null;
        var width = imageSize.Width;
        var height = imageSize.Height;
        var targetWidth = targetSize.Width;
        var targetHeight = targetSize.Height;
        var scaleFactor = 0.0f;
        var scaledWidth = targetWidth;
        var scaledHeight = targetHeight;
        var thumbnailPoint = PointF.Empty;
        if (imageSize != targetSize)
        {
            var widthFactor = targetWidth / width;
            var heightFactor = targetHeight / height;
            if (widthFactor > heightFactor)
            {
                scaleFactor = widthFactor;// scale to fit height
            }
            else
            {
                scaleFactor = heightFactor;// scale to fit width
            }
            scaledWidth = width * scaleFactor;
            scaledHeight = height * scaleFactor;
            // center the image
            if (widthFactor > heightFactor)
            {
                thumbnailPoint.Y = (targetHeight - scaledHeight) * 0.5f;
            }
            else
            {
                if (widthFactor < heightFactor)
                {
                    thumbnailPoint.X = (targetWidth - scaledWidth) * 0.5f;
                }
            }
        }
        UIGraphics.BeginImageContextWithOptions(targetSize, false, 0.0f);
        var thumbnailRect = new RectangleF(thumbnailPoint, new SizeF(scaledWidth, scaledHeight));
        sourceImage.Draw(thumbnailRect);
        newImage = UIGraphics.GetImageFromCurrentImageContext();
        if (newImage == null)
        {
            Console.WriteLine("could not scale image");
        }
        //pop the context to get back to the default
        UIGraphics.EndImageContext();

        return newImage;
    }

2

J'ai converti le guide de Sam Wirch en swift et cela a bien fonctionné pour moi, bien qu'il y ait un très léger «écrasement» dans l'image finale que je n'ai pas pu résoudre.

func resizedCroppedImage(image: UIImage, newSize:CGSize) -> UIImage {
    var ratio: CGFloat = 0
    var delta: CGFloat = 0
    var offset = CGPointZero
    if image.size.width > image.size.height {
        ratio = newSize.width / image.size.width
        delta = (ratio * image.size.width) - (ratio * image.size.height)
        offset = CGPointMake(delta / 2, 0)
    } else {
        ratio = newSize.width / image.size.height
        delta = (ratio * image.size.height) - (ratio * image.size.width)
        offset = CGPointMake(0, delta / 2)
    }
    let clipRect = CGRectMake(-offset.x, -offset.y, (ratio * image.size.width) + delta, (ratio * image.size.height) + delta)
    UIGraphicsBeginImageContextWithOptions(newSize, true, 0.0)
    UIRectClip(clipRect)
    image.drawInRect(clipRect)
    let newImage = UIGraphicsGetImageFromCurrentImageContext()
    UIGraphicsEndImageContext()
    return newImage
}

Si quelqu'un veut la version objective c, c'est sur son site Web.


2

J'ai trouvé que le Swift 3 publié par Evgenii Kanvets ne redimensionnait pas uniformément l'image.

Voici ma version Swift 4 de la fonction qui n'écrase pas l'image:

static func resizedCroppedImage(image: UIImage, newSize:CGSize) -> UIImage? {

    // This function returns a newImage, based on image
    // - image is scaled uniformaly to fit into a rect of size newSize
    // - if the newSize rect is of a different aspect ratio from the source image
    //     the new image is cropped to be in the center of the source image
    //     (the excess source image is removed)

    var ratio: CGFloat = 0
    var delta: CGFloat = 0
    var drawRect = CGRect()

    if newSize.width > newSize.height {

        ratio = newSize.width / image.size.width
        delta = (ratio * image.size.height) - newSize.height
        drawRect = CGRect(x: 0, y: -delta / 2, width: newSize.width, height: newSize.height + delta)

    } else {

        ratio = newSize.height / image.size.height
        delta = (ratio * image.size.width) - newSize.width
        drawRect = CGRect(x: -delta / 2, y: 0, width: newSize.width + delta, height: newSize.height)

    }

    UIGraphicsBeginImageContextWithOptions(newSize, true, 0.0)
    image.draw(in: drawRect)
    let newImage = UIGraphicsGetImageFromCurrentImageContext()
    UIGraphicsEndImageContext()

    return newImage
} 

J'ai dû mettre la mise à l'échelle à 1,0 UIGraphicsBeginImageContextWithOptionsmais d'autres fonctionnaient parfaitement. Merci!
maxmzd

1

Le code simple suivant a fonctionné pour moi.

[imageView setContentMode:UIViewContentModeScaleAspectFill];
[imageView setClipsToBounds:YES];

Votre code fonctionne bien pour les UIImageViewobjets. Cependant, cette question concerne la mise à l'échelle de l' UIImageobjet lui-même.
Rickster

1
scrollView = [[UIScrollView alloc] initWithFrame:CGRectMake(0.0,0.0,ScreenWidth,ScreenHeigth)];
    [scrollView setBackgroundColor:[UIColor blackColor]];
    [scrollView setDelegate:self];
    [scrollView setShowsHorizontalScrollIndicator:NO];
    [scrollView setShowsVerticalScrollIndicator:NO];
    [scrollView setMaximumZoomScale:2.0];
    image=[image scaleToSize:CGSizeMake(ScreenWidth, ScreenHeigth)];
    imageView = [[UIImageView alloc] initWithImage:image];
    UIImageView* imageViewBk = [[UIImageView alloc] initWithImage:[UIImage imageNamed:@"background.png"]];
    [self.view addSubview:imageViewBk];
    CGRect rect;
    rect.origin.x=0;
    rect.origin.y=0;
    rect.size.width = image.size.width;
    rect.size.height = image.size.height;

    [imageView setFrame:rect];

    [scrollView setContentSize:[imageView frame].size];
    [scrollView setMinimumZoomScale:[scrollView frame].size.width / [imageView frame].size.width];
    [scrollView setZoomScale:[scrollView minimumZoomScale]];
    [scrollView addSubview:imageView];

    [[self view] addSubview:scrollView];

alors vous pouvez prendre des captures d'écran de votre image par ceci

float zoomScale = 1.0 / [scrollView zoomScale];
CGRect rect;
rect.origin.x = [scrollView contentOffset].x * zoomScale;
rect.origin.y = [scrollView contentOffset].y * zoomScale;
rect.size.width = [scrollView bounds].size.width * zoomScale;
rect.size.height = [scrollView bounds].size.height * zoomScale;

CGImageRef cr = CGImageCreateWithImageInRect([[imageView image] CGImage], rect);

UIImage *cropped = [UIImage imageWithCGImage:cr];

CGImageRelease(cr);

0
- (UIImage*)imageScale:(CGFloat)scaleFactor cropForSize:(CGSize)targetSize
{
    targetSize = !targetSize.width?self.size:targetSize;
    UIGraphicsBeginImageContext(targetSize); // this will crop

    CGRect thumbnailRect = CGRectZero;

    thumbnailRect.size.width  = targetSize.width*scaleFactor;
    thumbnailRect.size.height = targetSize.height*scaleFactor;
    CGFloat xOffset = (targetSize.width- thumbnailRect.size.width)/2;
    CGFloat yOffset = (targetSize.height- thumbnailRect.size.height)/2;
    thumbnailRect.origin = CGPointMake(xOffset,yOffset);

    [self drawInRect:thumbnailRect];

    UIImage *newImage  = UIGraphicsGetImageFromCurrentImageContext();

    if(newImage == nil)
    {
        NSLog(@"could not scale image");
    }

    UIGraphicsEndImageContext();

    return newImage;
}

Ci-dessous l'exemple de travail: Image de gauche - (image d'origine); Image de droite avec échelle x2

entrez la description de l'image ici

Si vous souhaitez mettre l'image à l'échelle tout en conservant son cadre (proportions), appelez la méthode de cette façon:

[yourImage imageScale:2.0f cropForSize:CGSizeZero];

0

Cette question semble avoir été mise au repos, mais dans ma quête d'une solution que je pourrais comprendre plus facilement (et écrite en Swift), j'y suis arrivé (également posté sur: Comment recadrer l'IUImage? )


Je voulais pouvoir recadrer à partir d'une région en fonction d'un rapport hauteur / largeur et redimensionner à une taille basée sur une étendue de délimitation externe. Voici ma variante:

import AVFoundation
import ImageIO

class Image {

    class func crop(image:UIImage, crop source:CGRect, aspect:CGSize, outputExtent:CGSize) -> UIImage {

        let sourceRect = AVMakeRectWithAspectRatioInsideRect(aspect, source)
        let targetRect = AVMakeRectWithAspectRatioInsideRect(aspect, CGRect(origin: CGPointZero, size: outputExtent))

        let opaque = true, deviceScale:CGFloat = 0.0 // use scale of device's main screen
        UIGraphicsBeginImageContextWithOptions(targetRect.size, opaque, deviceScale)

        let scale = max(
            targetRect.size.width / sourceRect.size.width,
            targetRect.size.height / sourceRect.size.height)

        let drawRect = CGRect(origin: -sourceRect.origin * scale, size: image.size * scale)
        image.drawInRect(drawRect)

        let scaledImage = UIGraphicsGetImageFromCurrentImageContext()
        UIGraphicsEndImageContext()

        return scaledImage
    }
}

Il y a quelques choses que j'ai trouvées déroutantes, les préoccupations distinctes du recadrage et du redimensionnement. Le recadrage est géré avec l'origine du rect que vous passez à drawInRect, et la mise à l'échelle est gérée par la partie de taille. Dans mon cas, je devais relier la taille du rect de recadrage sur la source, à mon rect de sortie du même rapport hauteur / largeur. Le facteur d'échelle est alors sortie / entrée, et cela doit être appliqué à drawRect (passé à drawInRect).

Une mise en garde est que cette approche suppose en fait que l'image que vous dessinez est plus grande que le contexte de l'image. Je n'ai pas testé cela, mais je pense que vous pouvez utiliser ce code pour gérer le recadrage / zoom, mais en définissant explicitement le paramètre d'échelle comme étant le paramètre d'échelle susmentionné. Par défaut, UIKit applique un multiplicateur basé sur la résolution de l'écran.

Enfin, il convient de noter que cette approche UIKit est de plus haut niveau que les approches CoreGraphics / Quartz et Core Image, et semble gérer les problèmes d'orientation de l'image. Il convient également de mentionner que c'est assez rapide, après ImageIO, selon ce post ici: http://nshipster.com/image-resizing/


0

Version Swift:

    static func imageWithImage(image:UIImage, newSize:CGSize) ->UIImage {
    UIGraphicsBeginImageContextWithOptions(newSize, true, UIScreen.mainScreen().scale);
    image.drawInRect(CGRectMake(0, 0, newSize.width, newSize.height))

    let newImage = UIGraphicsGetImageFromCurrentImageContext();

    UIGraphicsEndImageContext();
    return newImage
}

1
Jolie petite fonction mais je suis à peu près sûr que cela étend simplement l'image à une taille particulière, ne garde pas le rapport hauteur / largeur et le recadrage ...
William T.

Oui vous avez raison, ce n'est que pour les retouches de l'image à une taille paritculaire.
Roman Barzyczak

0

Voici une version Swift 3 du guide de Sam Wirch sur Swift posté par William T.

extension UIImage {

    static func resizedCroppedImage(image: UIImage, newSize:CGSize) -> UIImage? {
        var ratio: CGFloat = 0
        var delta: CGFloat = 0
        var offset = CGPoint.zero

        if image.size.width > image.size.height {
            ratio = newSize.width / image.size.width
            delta = (ratio * image.size.width) - (ratio * image.size.height)
            offset = CGPoint(x: delta / 2, y: 0)
        } else {
            ratio = newSize.width / image.size.height
            delta = (ratio * image.size.height) - (ratio * image.size.width)
            offset = CGPoint(x: 0, y: delta / 2)
        }

        let clipRect = CGRect(x: -offset.x, y: -offset.y, width: (ratio * image.size.width) + delta, height: (ratio * image.size.height) + delta)
        UIGraphicsBeginImageContextWithOptions(newSize, true, 0.0)
        UIRectClip(clipRect)
        image.draw(in: clipRect)
        let newImage = UIGraphicsGetImageFromCurrentImageContext()
        UIGraphicsEndImageContext()

        return newImage
    }

}
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.