UIImage: Redimensionar, luego Recortar

187

He estado golpeándome la cara con este literalmente durante días, y aunque siento constantemente que estoy al borde de la revelación, simplemente no puedo lograr mi objetivo.

Pensé, con anticipación en las fases conceptuales de mi diseño, que sería un asunto trivial tomar una imagen de la cámara o biblioteca del iPhone, reducirla a una altura específica, usando una función equivalente a la opción de Relleno de aspecto de UIImageView (en su totalidad en el código) y, a continuación, recortar fuera algo que no encajaba dentro de un CGRect pasado.

Obtener la imagen original de la cámara o biblioteca fue trivial. Me sorprende lo difícil que han resultado ser los otros dos pasos.

La imagen adjunta muestra lo que estoy tratando de lograr. ¿Alguien podría ser tan amable de sostener mi mano? Todos los ejemplos de código que he encontrado hasta ahora parecen romper la imagen, estar al revés, parecer basura, salir de los límites o, de lo contrario, simplemente no funcionan correctamente.

Carson C.
fuente
77
El enlace está roto a su imagen.
1
Una cosa que nadie nos sorprende es por qué Apple no lo ha agregado a UIImagePickerController: es demasiado difícil ;-)
Tim

Respuestas:

246

Necesitaba lo mismo: en mi caso, elegir la dimensión que se ajuste una vez escalada y luego recortar cada extremo para ajustar el resto al ancho. (Estoy trabajando en horizontal, por lo que es posible que no haya notado ninguna deficiencia en el modo vertical). Aquí está mi código: es parte de una categoría en UIImage. El tamaño de destino en mi código siempre se establece en el tamaño de pantalla completa del dispositivo.

@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;
}
Jane Sales
fuente
1
curiosamente funciona en el simulador pero en el dispositivo recibo el ExecBadAccess ..
Sorin Antohi
55
El problema con el mal ejecutivo se produce porque las funciones UIImage no son seguras para subprocesos. Es por eso que a veces se bloquea, a veces no
Erik
1
Este código funcionó muy bien para mí, pero estaba borroso en la retina. La combinación de este código con el comentario a continuación hizo que todo fuera perfecto: stackoverflow.com/questions/603907/…
MaxGabriel
23
Use UIGraphicsBeginImageContextWithOptions (targetSize, YES, 0.0); para hacer una buena imagen en la retina también.
Borut Tomazin
1
Esto funciona para mí en modo vertical, pero no horizontal. La altura se estira en el paisaje. ¿Alguna idea de por qué?
Darren
77

Una publicación anterior contiene código para un método para cambiar el tamaño de tu UIImage. La porción relevante es la siguiente:

+ (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 cuanto al recorte, creo que si altera el método para usar un tamaño diferente para la escala que para el contexto, la imagen resultante debe recortarse a los límites del contexto.

Brad Larson
fuente
3
No tiene SENTIDO de por qué esto corrige la orientación de la imagen, pero lo hace y, por lo tanto, solucionó mi problema con la cámara que no devuelve la orientación correcta en el originalImage. Gracias.
Brenden el
10
Descubrí que cambiar el tamaño de una imagen en un dispositivo de retina parecía borroso. Para mantener la claridad, he modificado la primera línea a la siguiente: UIGraphicsBeginImageContextWithOptions(newSize, 1.0f, 0.0f);. (explicado aquí: stackoverflow.com/questions/4334233/… )
johngraham el
Gira la imagen, ¡pero no la recorta correctamente! ¿Por qué tiene tantos votos?
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;
}

Propongo este. ¿No es ella una belleza? ;)

Cualquiera
fuente
1
Este es bueno, si elimina primero la declaración if, actúa como AspectFill.
RaffAl
7

Aqui tienes. Este es perfecto ;-)

EDITAR: vea el comentario a continuación - "No funciona con ciertas imágenes, falla con: CGContextSetInterpolationQuality: error de contexto inválido 0x0"

// 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;
}
cdstamper
fuente
1
Luce realmente hermosa :) Me gusta la calidad de la Interpolación aquí
Kostiantyn Sokolinskyi
No funciona con ciertas imágenes, falla con: CGContextSetInterpolationQuality: error de contexto inválido 0x0
RaffAl
6

Esta es una versión de la respuesta de Jane Sales en Swift. ¡Salud!

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
}
Ishan Handa
fuente
2

Modifiqué el código de Brad Larson. Aspecto llenará la imagen en un rectángulo dado.

-(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;
}
Seunghoon
fuente
1
Probado con otras dos imágenes, retrato y paisaje. Esto NO está realizando el relleno de aspecto.
RaffAl
2

La versión de Xamarin.iOS para la respuesta aceptada sobre cómo cambiar el tamaño y luego recortar UIImage (Aspecto de relleno) está debajo

    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;
    }
Alex Sorokoletov
fuente
2

Convertí guía de Sam Wirch a rápida y funcionó bien para mí, aunque hay algunos muy leve "aplastar" en la imagen final que no podía resolver.

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 alguien quiere la versión objetiva c, está en su sitio web.

William T.
fuente
2

Descubrí que el Swift 3 publicado por Evgenii Kanvets no escala la imagen de manera uniforme.

Aquí está mi versión Swift 4 de la función que no aplasta la imagen:

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
} 
Chris Capener
fuente
Tuve que establecer el escalado a 1.0 UIGraphicsBeginImageContextWithOptionspero otros funcionaron perfectamente. ¡Gracias!
maxmzd
1

El siguiente código simple funcionó para mí.

[imageView setContentMode:UIViewContentModeScaleAspectFill];
[imageView setClipsToBounds:YES];
William Li
fuente
Su código funciona bien para los UIImageViewobjetos. Sin embargo, esta pregunta se trata de escalar el UIImageobjeto en sí.
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];

entonces puedes tomar capturas de pantalla de tu imagen con esto

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);
Khaled
fuente
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;
}

Debajo del ejemplo de trabajo: Imagen izquierda - (imagen de origen); Imagen derecha con escala x2

ingrese la descripción de la imagen aquí

Si desea escalar la imagen pero conservar su marco (proporciones), llame al método de esta manera:

[yourImage imageScale:2.0f cropForSize:CGSizeZero];
David
fuente
0

Esta pregunta parece haberse puesto a descansar, pero en mi búsqueda de una solución que pudiera entender más fácilmente (y escrita en Swift), llegué a esto (también publicado en: ¿Cómo recortar el UIImage? )


Quería poder recortar desde una región en función de una relación de aspecto y escalar a un tamaño basado en un límite externo. Aquí está mi variación:

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

Hay un par de cosas que encontré confusas, las preocupaciones separadas de recortar y cambiar el tamaño. El recorte se maneja con el origen del rect que pasa a drawInRect, y la escala se maneja por la porción de tamaño. En mi caso, necesitaba relacionar el tamaño del rectificador de recorte en la fuente, con mi rect de salida de la misma relación de aspecto. El factor de escala es entonces salida / entrada, y esto debe aplicarse a drawRect (pasado a drawInRect).

Una advertencia es que este enfoque asume efectivamente que la imagen que está dibujando es más grande que el contexto de la imagen. No he probado esto, pero creo que puede usar este código para manejar el recorte / zoom, pero definiendo explícitamente el parámetro de escala como el parámetro de escala mencionado anteriormente. Por defecto, UIKit aplica un multiplicador basado en la resolución de la pantalla.

Finalmente, debe tenerse en cuenta que este enfoque UIKit tiene un nivel más alto que los enfoques CoreGraphics / Quartz y Core Image, y parece manejar los problemas de orientación de la imagen. También vale la pena mencionar que es bastante rápido, después de ImageIO, de acuerdo con esta publicación aquí: http://nshipster.com/image-resizing/

Chris Conover
fuente
0

Versión rápida:

    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
}
Roman Barzyczak
fuente
1
Pequeña función agradable, pero estoy bastante seguro de que esto solo estira la imagen a un tamaño particular, no mantiene la relación de aspecto y recorta ...
William T.
Sí, tiene razón, esto es solo para extender la imagen a un tamaño paritcular.
Roman Barzyczak
0

Aquí hay una versión de Swift 3 de la guía de Sam Wirch para Swift publicada por 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
    }

}
Yevhenii Kanivets
fuente