web-dev-qa-db-fra.com

Le moyen le plus efficace de dessiner une partie d'une image dans iOS

Étant donné un UIImage et un CGRect, quelle est la manière la plus efficace (en mémoire et en temps) de dessiner la partie de l'image correspondant au CGRect (sans mise à l'échelle)?

Pour référence, voici comment je le fais actuellement:

- (void)drawRect:(CGRect)rect {
    CGContextRef context = UIGraphicsGetCurrentContext();
    CGRect frameRect = CGRectMake(frameOrigin.x + rect.Origin.x, frameOrigin.y + rect.Origin.y, rect.size.width, rect.size.height);    
    CGImageRef imageRef = CGImageCreateWithImageInRect(image_.CGImage, frameRect);
    CGContextTranslateCTM(context, 0, rect.size.height);
    CGContextScaleCTM(context, 1.0, -1.0);
    CGContextDrawImage(context, rect, imageRef);
    CGImageRelease(imageRef);
}

Malheureusement, cela semble extrêmement lent avec des images de taille moyenne et une fréquence setNeedsDisplay élevée. Jouer avec le cadre de UIImageView et clipToBounds produit de meilleurs résultats (avec moins de flexibilité).

36
hpique

Je suppose que vous faites cela pour afficher une partie d'une image à l'écran, car vous avez mentionné UIImageView. Et les problèmes d'optimisation doivent toujours être définis spécifiquement.


Trust Apple for Regular UI stuff)

En fait, UIImageView avec clipsToBounds est l'un des moyens les plus rapides/les plus simples d'archiver votre objectif si votre objectif est simplement de découper une région rectangulaire d'une image (pas trop grande). De plus, vous n'avez pas besoin d'envoyer de message setNeedsDisplay.

Ou vous pouvez essayer de mettre le UIImageView à l'intérieur d'un UIView vide et définir l'écrêtage dans la vue du conteneur. Avec cette technique, vous pouvez transformer librement votre image en définissant la propriété transform en 2D (mise à l'échelle, rotation, translation).

Si vous avez besoin d'une transformation 3D, vous pouvez toujours utiliser CALayer avec la propriété masksToBounds, mais l'utilisation de CALayer vous donnera très peu de performances supplémentaires généralement non considérables.

Quoi qu'il en soit, vous devez connaître tous les détails de bas niveau pour les utiliser correctement pour l'optimisation.


Pourquoi est-ce l'un des moyens les plus rapides?

UIView n'est qu'une fine couche au-dessus de CALayer qui est implémentée au-dessus de OpenGL qui est une interface pratiquement directe avec [~ # ~] gpu [~ # ~]. Cela signifie que UIKit est accéléré par le GPU.

Donc, si vous les utilisez correctement (je veux dire, dans les limites prévues), il fonctionnera aussi bien que la simple implémentation de OpenGL. Si vous n'utilisez que quelques images à afficher, vous obtiendrez des performances acceptables avec l'implémentation de UIView car elle peut obtenir une accélération complète du sous-jacent OpenGL (ce qui signifie l'accélération GPU).

Quoi qu'il en soit, si vous avez besoin d'une optimisation extrême pour des centaines de sprites animés avec des pixel shaders finement réglés comme dans une application de jeu, vous devez utiliser OpenGL directement, car CALayer manque de nombreuses options d'optimisation à des niveaux inférieurs. Quoi qu'il en soit, au moins pour l'optimisation des choses de l'interface utilisateur, il est incroyablement difficile d'être meilleur qu'Apple.


Pourquoi votre méthode est plus lente que UIImageView?

Ce que vous devez savoir, c'est tout sur l'accélération GPU. Dans tous les ordinateurs récents, les performances graphiques rapides sont obtenues uniquement avec le GPU. Ensuite, le point est de savoir si la méthode que vous utilisez est implémentée au-dessus du GPU ou non.

IMO, les méthodes de dessin CGImage ne sont pas implémentées avec le GPU. Je pense avoir lu à ce sujet dans la documentation d'Apple, mais je ne me souviens pas où. Je ne suis donc pas sûr de cela. Quoi qu'il en soit, je crois que CGImage est implémenté dans le processeur parce que,

  1. Son API semble avoir été conçue pour le processeur, comme l'interface d'édition de bitmap et le dessin de texte. Ils ne correspondent pas très bien à une interface GPU.
  2. L'interface de contexte bitmap permet un accès direct à la mémoire. Cela signifie que son stockage backend est situé dans la mémoire du processeur. Peut-être quelque peu différent sur l'architecture de mémoire unifiée (et aussi avec l'API Metal), mais de toute façon, l'intention de conception initiale de CGImage devrait être pour le CPU.
  3. Beaucoup ont récemment publié d'autres Apple APIs mentionnant explicitement l'accélération GPU. Cela signifie que leurs anciennes API ne l'étaient pas. S'il n'y a pas de mention spéciale, cela est généralement fait dans le CPU par défaut.

Il semble donc que cela se fasse dans le CPU. Les opérations graphiques effectuées dans le CPU sont beaucoup plus lentes que dans le GPU.

Le simple découpage d'une image et la composition des couches d'image sont des opérations très simples et peu coûteuses pour le GPU (par rapport au CPU), vous pouvez donc vous attendre à ce que la bibliothèque UIKit l'utilise car UIKit entier est implémenté au-dessus d'OpenGL.


À propos des limitations

L'optimisation étant une sorte de travail sur la micro-gestion, les chiffres spécifiques et les petits faits sont très importants. Quelle est la taille moyenne? OpenGL sur iOS limite généralement la taille de texture maximale à 1024x1024 pixels (peut-être plus grande dans les versions récentes). Si votre image est plus grande que cela, cela ne fonctionnera pas ou les performances seront considérablement dégradées (je pense que UIImageView est optimisé pour les images dans les limites).

Si vous avez besoin d'afficher des images énormes avec écrêtage, vous devez utiliser une autre optimisation comme CATiledLayer et c'est une tout autre histoire.

Et n'allez pas à OpenGL à moins que vous ne vouliez connaître tous les détails d'OpenGL. Il nécessite une compréhension complète des graphiques de bas niveau et au moins 100 fois plus de code.


À propos de Some Future

Bien que cela ne se produise pas très probablement, mais les trucs CGImage (ou toute autre chose) n'ont pas besoin d'être coincés uniquement dans le CPU. N'oubliez pas de vérifier la technologie de base de l'API que vous utilisez. Pourtant, les trucs GPU sont un monstre très différent du CPU, alors les gars de l'API les mentionnent généralement explicitement et clairement.

78
Eonil

Ce serait finalement plus rapide, avec beaucoup moins de création d'images à partir d'atlas Sprite, si vous pouviez définir non seulement l'image pour un UIImageView, mais aussi le décalage en haut à gauche à afficher dans ce UIImage. C'est peut-être possible.

Pendant ce temps, j'ai créé ces fonctions utiles dans une classe utilitaire que j'utilise dans mes applications. Il crée un UIImage à partir d'une partie d'un autre UIImage, avec des options pour faire pivoter, mettre à l'échelle et retourner en utilisant des valeurs UIImageOrientation standard à spécifier.

Mon application crée beaucoup d'images UII lors de l'initialisation, et cela prend nécessairement du temps. Mais certaines images ne sont pas nécessaires jusqu'à ce qu'un certain onglet soit sélectionné. Pour donner l'apparence d'une charge plus rapide, je pourrais les créer dans un thread séparé généré au démarrage, puis attendre jusqu'à ce que ce soit fait si cet onglet est sélectionné.

+ (UIImage*)imageByCropping:(UIImage *)imageToCrop toRect:(CGRect)aperture {
    return [ChordCalcController imageByCropping:imageToCrop toRect:aperture withOrientation:UIImageOrientationUp];
}

// Draw a full image into a crop-sized area and offset to produce a cropped, rotated image
+ (UIImage*)imageByCropping:(UIImage *)imageToCrop toRect:(CGRect)aperture withOrientation:(UIImageOrientation)orientation {

            // convert y coordinate to Origin bottom-left
    CGFloat orgY = aperture.Origin.y + aperture.size.height - imageToCrop.size.height,
            orgX = -aperture.Origin.x,
            scaleX = 1.0,
            scaleY = 1.0,
            rot = 0.0;
    CGSize size;

    switch (orientation) {
        case UIImageOrientationRight:
        case UIImageOrientationRightMirrored:
        case UIImageOrientationLeft:
        case UIImageOrientationLeftMirrored:
            size = CGSizeMake(aperture.size.height, aperture.size.width);
            break;
        case UIImageOrientationDown:
        case UIImageOrientationDownMirrored:
        case UIImageOrientationUp:
        case UIImageOrientationUpMirrored:
            size = aperture.size;
            break;
        default:
            assert(NO);
            return nil;
    }


    switch (orientation) {
        case UIImageOrientationRight:
            rot = 1.0 * M_PI / 2.0;
            orgY -= aperture.size.height;
            break;
        case UIImageOrientationRightMirrored:
            rot = 1.0 * M_PI / 2.0;
            scaleY = -1.0;
            break;
        case UIImageOrientationDown:
            scaleX = scaleY = -1.0;
            orgX -= aperture.size.width;
            orgY -= aperture.size.height;
            break;
        case UIImageOrientationDownMirrored:
            orgY -= aperture.size.height;
            scaleY = -1.0;
            break;
        case UIImageOrientationLeft:
            rot = 3.0 * M_PI / 2.0;
            orgX -= aperture.size.height;
            break;
        case UIImageOrientationLeftMirrored:
            rot = 3.0 * M_PI / 2.0;
            orgY -= aperture.size.height;
            orgX -= aperture.size.width;
            scaleY = -1.0;
            break;
        case UIImageOrientationUp:
            break;
        case UIImageOrientationUpMirrored:
            orgX -= aperture.size.width;
            scaleX = -1.0;
            break;
    }

    // set the draw rect to pan the image to the right spot
    CGRect drawRect = CGRectMake(orgX, orgY, imageToCrop.size.width, imageToCrop.size.height);

    // create a context for the new image
    UIGraphicsBeginImageContextWithOptions(size, NO, imageToCrop.scale);
    CGContextRef gc = UIGraphicsGetCurrentContext();

    // apply rotation and scaling
    CGContextRotateCTM(gc, rot);
    CGContextScaleCTM(gc, scaleX, scaleY);

    // draw the image to our clipped context using the offset rect
    CGContextDrawImage(gc, drawRect, imageToCrop.CGImage);

    // pull the image from our cropped context
    UIImage *cropped = UIGraphicsGetImageFromCurrentImageContext();

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

    // Note: this is autoreleased
    return cropped;
}
5
Scott Lahteine

Le moyen très simple de déplacer une grande image à l'intérieur IImageView comme suit.

Soit l'image de taille (100, 400) représentant 4 états d'une image les uns en dessous des autres. Nous voulons montrer la 2ème image ayant offsetY = 100 dans le carré IImageView de taille (100, 100). La solution est:

UIImageView *iView = [[UIImageView alloc] initWithFrame:CGRectMake(0, 0, 100, 100)];
CGRect contentFrame = CGRectMake(0, 0.25, 1, 0.25);
iView.layer.contentsRect = contentFrame;
iView.image = [UIImage imageNamed:@"NAME"];

Ici contentFrame est une trame normalisée par rapport à la taille réelle IImage. Donc, "0" signifie que nous commençons la partie visible de l'image à partir de la bordure gauche, "0,25" signifie que nous avons un décalage vertical de 100, "1" signifie que nous voulons afficher toute la largeur de l'image, et enfin, "0,25" signifie que nous voulons montrer seulement 1/4 partie de l'image en hauteur.

Ainsi, en coordonnées d'image locales, nous montrons le cadre suivant

CGRect visibleAbsoluteFrame = CGRectMake(0*100, 0.25*400, 1*100, 0.25*400)
or CGRectMake(0, 100, 100, 100);
3
malex

Plutôt que de créer une nouvelle image (qui est coûteuse car elle alloue de la mémoire), pourquoi ne pas utiliser CGContextClipToRect?

1
David Dunham

Le moyen le plus rapide consiste à utiliser un masque d'image: une image de la même taille que l'image à masquer mais avec un certain motif de pixels indiquant la partie de l'image à masquer lors du rendu ->

// maskImage is used to block off the portion that you do not want rendered
// note that rect is not actually used because the image mask defines the rect that is rendered
-(void) drawRect:(CGRect)rect maskImage:(UIImage*)maskImage {

    UIGraphicsBeginImageContext(image_.size);
    [maskImage drawInRect:image_.bounds];
    maskImage = UIGraphicsGetImageFromCurrentImageContext();
    UIGraphicsEndImageContext();

    CGImageRef maskRef = maskImage.CGImage;
    CGImageRef mask = CGImageMaskCreate(CGImageGetWidth(maskRef),
                                    CGImageGetHeight(maskRef),
                                    CGImageGetBitsPerComponent(maskRef),
                                    CGImageGetBitsPerPixel(maskRef),
                                    CGImageGetBytesPerRow(maskRef),
                                    CGImageGetDataProvider(maskRef), NULL, false);

    CGImageRef maskedImageRef = CGImageCreateWithMask([image_ CGImage], mask);
    image_ = [UIImage imageWithCGImage:maskedImageRef scale:1.0f orientation:image_.imageOrientation];

    CGImageRelease(mask);
    CGImageRelease(maskedImageRef); 
}
0
Adam Freeman