NSView, NSImage et optimisation

yqnnyqnn Membre
06:09 modifié dans API AppKit #1
Bonsoir,

Je m'arrache les cheveux depuis quelques jours sur un problème qui, je l'espère, a une solution  ::)


J'ai réalisé une sous-classe de NSView dont le but est d'afficher une image (un peu à  la manière de NSImageView).
Cette vue contient une autre vue qui affiche des tracés (de type NSBezierPath).
Tout ceci afin d'afficher des annotations sur une image (à  la manière de la fonction Annotations de Aperçu.app).


Maintenant passons aux choses sérieuses.
La première vue affiche l'image "im" de cette manière :
[im drawInRect:rect fromRect:NSZeroRect operation:NSCompositeCopy fraction:1.];

Cette seule ligne de code est très rapide quand les conditions suivantes sont remplies :
-  [im isDataRetained] est à  NO
-  [im size] est égal à  rect
- ce n'est pas le premier appel à  "drawInRect:"

Par contre, si les deux première conditions sont remplies, le premier à  appel "drawInRect:" après allocation de l'image est très long (et dépend de la taille donnée à  l'image par setSize:, juste après l'allocation).



Pour avoir un affichage rapide, j'ai trouvé uniquement deux solutions avec chacune de gros défauts. Dans chacune des solutions, je m'arrange pour qu'à  chaque affichage la condition ([im size] == rect) soit remplie :
1 - Je garde la même NSImage, avec [im isDataRetained] à  NO : les resize successifs détériorent de plus en plus la qualité de l'image.
2 - A chaque resize, je réalloue l'image avant de redimensionner. Cette fois-ci, pas de perte de qualité, mais le redimensionnement est très lent.


A contrario, si  [im isDataRetained] est à  YES, le fait d'avoir [im size] égal à  rect ne change rien du tout : le temps d'affichage est constant quelque soit [im size] (et ce temps est très supérieur à  celui des deux cas précédents).


Ce que je ne comprend pas, c'est pourquoi le simple fait de passer [im isDataRetained] à  YES diminue énormément les performances de l'affichage ?


En bref je souhaite souvent comment faire pour qu'une image soit redimensionnée rapidement quand l'utilisateur change la taille de sa fenêtre, et qu'une fois le redimensionnement effectué l'affichage d'annotations au dessus de cette image soit rapide (comme c'est le cas dans Apercu.app).


Merci d'avance.

(si ce n'est pas assez clair, je peux faire un petit projet xCode pour illustrer le problème).

Réponses

  • CéroceCéroce Membre, Modérateur
    06:09 modifié #2
    Pour essayer de répondre à  ta question:
    - Quand les rectangles source et destination n'ont pas les mêmes dimensions, il est nécessaire de redimensionner l'image, en utilisant une interpolation, ce qui est une opération coûteuse en temps machine. Il est donc logique de conserver l'image redimensionnée en cache.
    - D'autres opérations sont coûteuses: par exemple, afficher une image en 256 couleurs indexées sur ton écran en 32 bits/pixel => là  encore, NSImage met sûrement l'image en cache.
    - Méfie-toi de la résolution écrite dans ton fichier graphique. Si elle est > 72 ppp, NSImage réduit l'image affichée.

    - Dans la doc de la méthode setDataRetained:
    Data retention is also useful if you plan to resize an image frequently; otherwise, resizing occurs on a cached copy of the image, which can lose image quality during successive scaling operations. With data retention enabled, the image is resized from the original source data.


    Voici qui explique les problèmes de qualité.


    NSImage possède une grosse qualité: l'universalité. Tu peux créer une NSImage aussi bien à  partir d'un fichier bitmap que vectoriel et elle se débrouille pour l'afficher.

    Simplement, comme tu le soulignes, son inconvénient, c'est qu'on ne sait pas trop comment la classe travaille, et elle ne semble jamais fonctionner comme on s'y attend. Pour mon projet, j'ai décidé de ne pas du tout utiliser la classe NSImage, et à  la place, j'ai créé un équivalent qui utilise des CGImage. Je suis sûr que c'est ce que fait Aperçu. Note que les CGImage ne gèrent que les bitmaps, mais il existe également des CGPDFImage (voir le guide Quartz 2D).
    Le seul inconvénient de ma technique est que tout l'AppKit (par exemple NSImageWell) utilise des NSImage, mais on peut créer une NSImage à  partir d'une CGImage.
  • yqnnyqnn Membre
    06:09 modifié #3
    Merci pour cette réponse  :-*

    dans 1239005126:

    Pour mon projet, j'ai décidé de ne pas du tout utiliser la classe NSImage, et à  la place, j'ai créé un équivalent qui utilise des CGImage. Je suis sûr que c'est ce que fait Aperçu. Note que les CGImage ne gèrent que les bitmaps, mais il existe également des CGPDFImage (voir le guide Quartz 2D).


    Je pense que je vais m'orienter dans cette voie. Est-ce difficile à  faire ? Qu'elle doc faut il lire ?
  • NoNo Membre
    avril 2009 modifié #4
    Il y a un truc que j'ai jamais tenté de faire mais qui pourrait fonctionner :
    c'est d'utiliser directement les NSImageRep sans passer par NSImage.

    Voici quelques avantages apparents de NSImageRep :
    - méthodes pour afficher directement à  l'écran,
    - méthodes qui renvoient le type de sous-classe spécifique à  utiliser en fonction du type d'image,
    - création aisée d'une NSImage à  partir du NSImageRep,
    - ça reste du cocoa.

    Je répète, je n'ai jamais tenté, mais cela peut être une bonne alternative à  CGImage.
    D'ailleurs, je vois CGImage plus comme une image-representation que comme une NSImage (qui est un container complexe pouvant stocker différents types d'images).
  • CéroceCéroce Membre, Modérateur
    06:09 modifié #5
    En effet, NSImageRep est une alternative, mais d'après mes souvenirs, ça n'était pas très confortable à  utiliser, ça conservait le même côté opaque que NSImage. D'ailleurs, pour passer d'une CGImage à  une NSImage, on passe par une NSBitmapImageRep (qui hérite de NSImageRep).

    @yqnn:
    La doc est Quartz 2D Programming guide. Ici, nous parlerons plutôt de Core Graphics: c'est un synonyme de Quartz 2D.
    Core Graphics est un ensemble d'API en langage C, mais les API sont orientées-objet dans l'esprit, et sont très propres. Même si tu décides de rester sur les classes de Cocoa, il peut être intéressant de lire la doc pour savoir comment ça marche sous le capot.
  • CéroceCéroce Membre, Modérateur
    06:09 modifié #6
    Je te file ma classe qui enrobe une CGImage.


  • CéroceCéroce Membre, Modérateur
    06:09 modifié #7
    Et la méthode pour passer d'une CGImage à  une NSImage:

    + (NSImage*) NSImageWithCGImage:(CGImageRef)cgImage<br />{<br />	// Determine the size of the CGImage<br />	NSSize size = {<br />		CGImageGetWidth(cgImage),<br />		CGImageGetHeight(cgImage)<br />	};<br />	<br />	// Create the NSImage from the CGImage<br />	NSBitmapImageRep* rep = [[NSBitmapImageRep alloc] initWithCGImage:cgImage];<br />	NSImage* nsImage = [[NSImage alloc] initWithSize:size];<br />	[nsImage addRepresentation:rep];<br />	<br />	[rep release];<br />	[nsImage autorelease];<br />	<br />	return nsImage;<br />}
    

  • schlumschlum Membre
    06:09 modifié #8
    Et en changeant juste le contexte graphique avec un "scale" de manière à  ce que le rectangle d'affichage corresponde toujours à  la dimension de l'image (et ce sans changer l'image du coup...) ?
  • CéroceCéroce Membre, Modérateur
    06:09 modifié #9
    Non, ce n'est pas une bonne idée, au final il faut quand même faire correspondre les pixels de l'image à  ceux de l'écran. Tu ne peux pas échapper au redimensionnement !
  • schlumschlum Membre
    06:09 modifié #10
    dans 1239030380:

    Non, ce n'est pas une bonne idée, au final il faut quand même faire correspondre les pixels de l'image à  ceux de l'écran. Tu ne peux pas échapper au redimensionnement !


    Ben ça il faut le faire quelque soit la technique hein  :)
    C'est juste pour pouvoir utiliser NSImage sans bidouiller ses données internes, le " redimensionnement " étant fait de manière rapide par Quartz.
  • yqnnyqnn Membre
    avril 2009 modifié #11
    dans 1239018543:

    Je te file ma classe qui enrobe une CGImage.

    J'ai essayé et ça marche vraiment bien, merci  :adios!:

    Par contre, je n'arrive pas à  récupérer les pixels de l'image.
    J'ai essayé de faire un truc du genre :
    NSBitmapImageRep * image = [[NSBitmapImageRep alloc] initWithCGImage:cgImage];<br />[image getPixel:a atX:x y:y];<br />
    

    Ca fonctionne, mais c'est affreusement lent (beaucoup plus lent que si le NSBitmapImageRep était récupéré d'une NSImage).
    J'ai aussi essayé de passer par "bitmapData" de NSBitmapImageRep, mais là  le pointeur renvoyé ne semble pas pointer sur le premier pixel de l'image  :why?:
    EDIT : en fait je m'étais planté, la deuxième méthode fonctionne ^^


    dans 1239028939:

    Et en changeant juste le contexte graphique avec un "scale" de manière à  ce que le rectangle d'affichage corresponde toujours à  la dimension de l'image (et ce sans changer l'image du coup...) ?


    Je ne comprend pas bien à  quoi correspond le contexte graphique.
    Cela consiste à  faire un setBounds: sur la vue ?
  • schlumschlum Membre
    06:09 modifié #12
    Oui, un setBounds fait effectivement un scale...
  • CéroceCéroce Membre, Modérateur
    06:09 modifié #13
    Pour comprendre l'histoire du scale, c'est en rapport avec la matrice de transformation courante ("Current Transform Matrix"). Ils en parlent en détail dans Quartz 2D Programming Guide, ainsi que dans Cocoa Drawing Guide et View Programming Guide.

    Il s'agit d'une matrice qui est appliquée à  chaque point pour convertir les coordonnées du contexte graphique (= en général ta vue), vers le périphérique d'affichage (= en général, ton écran).


    N'utilise pas getPixel:atX:y:. C'est forcément très lent.
    Je te renvoie vers cet article que j'ai écrit, qui t'expliquera comment accéder rapidement à  la bitmap.

    L'article utilise un NSImageRep, mais tu peux accéder à  la bitmap de la CGImage en utilisant la fonction CGDataProviderCopyData(). Une note technique appelée Getting the pixel data from a CGImage object explique comment obtenir un pointeur sur la bitmap.

    Autre remarque: je ne sais pas ce que tu souhaites faire de ta bitmap, mais si c'est pour une opération courante (flou gaussien, contraste, désaturation, etc.), intéresse-toi à  Core Image, qui est très performant, et reste simple à  utiliser.
  • yqnnyqnn Membre
    avril 2009 modifié #14
    Merci beaucoup pour toutes ces précisions  o:)


    dans 1239085808:

    Je te renvoie vers cet article que j'ai écrit, qui t'expliquera comment accéder rapidement à  la bitmap.

    L'article utilise un NSImageRep, mais tu peux accéder à  la bitmap de la CGImage en utilisant la fonction CGDataProviderCopyData(). Une note technique appelée Getting the pixel data from a CGImage object explique comment obtenir un pointeur sur la bitmap.

    Merci, l'article est vraiment très intéressant. J'ai découvert l'existence des row bytes, mais étrangement mon code fonctionnait sans en tenir compte.
    Par contre, comment savoir sur combien d'octets les pixels sont codés (et le type d'encodage : G, RGB, RGBA, CMYKA ...) ? Est ce qu'il y a mieux que bitmapFormat et/ou bitsPerPixel ?



    dans 1239085808:

    Autre remarque: je ne sais pas ce que tu souhaites faire de ta bitmap, mais si c'est pour une opération courante (flou gaussien, contraste, désaturation, etc.), intéresse-toi à  Core Image, qui est très performant, et reste simple à  utiliser.

    Je m'en sert pour faire de la détection de formes, je doute que Core Image sache faire cela ^^.
    Mais je devrais avoir besoin de CI dans un avenir proche, je reviendrai certainement ici pour demander pourquoi ça ne marche pas.
  • CéroceCéroce Membre, Modérateur
    06:09 modifié #15
    dans 1239148729:

    J'ai découvert l'existence des row bytes, mais étrangement mon code fonctionnait sans en tenir compte.

    Ce n'est pas forcément étrange:
    - Les row bytes sont inutilisés, alors les modifier n'a aucune conséquence. Ainsi, si tu parcours toute la bitmap avec un pointeur, tu as très bien pu les modifier sans t'en rendre compte.
    - Parfois, il n'y a pas de row bytes. Par exemple, j'imagine que c'est le cas pour les machines qui ne possèdent pas de mémoire vidéo dédiée.
    - Pour certaines dimensions d'image (certainement toutes celles qui sont multiples d'une puissance de 2, comme avec OpenGL), il n'est pas nécessaire d'aligner la bitmap.
    - ça peut marcher chez toi, et pas chez les autres.

    dans 1239148729:

    Par contre, comment savoir sur combien d'octets les pixels sont codés (et le type d'encodage : G, RGB, RGBA, CMYKA ...) ? Est ce qu'il y a mieux que bitmapFormat et/ou bitsPerPixel ?

    Je ne sais pas trop. Tu peux peut-être appeler -[NSBitmapImageRep CGImage] et utiliser les fonctions de Core Gaphics pour connaà®tre le format. Cependant, pour simplifier, tu as sans doute intérêt à  n'utiliser dans ton algo de reconnaissance de forme qu'un seul format de bitmap.
  • yqnnyqnn Membre
    06:09 modifié #16
    dans 1239172102:
    Cependant, pour simplifier, tu as sans doute intérêt à  n'utiliser dans ton algo de reconnaissance de forme qu'un seul format de bitmap.


    L'image étant fournie par l'utilisateur, cela n'est pas possible.
    Mais j'ai finalement trouvé un code assez élégant pour résoudre ce problème : http://forums.macrumors.com/showthread.php?t=143722

    Je cite :
    - (BOOL) manipulateImageRep:(NSBitmapImageRep*)rep {<br />&nbsp; &nbsp; int Bpr, spp;<br />&nbsp; &nbsp; unsigned byte *data;<br />&nbsp; &nbsp; int x, y, w, h;<br /><br />&nbsp; &nbsp; if ([rep bitsPerSample] != 8) {<br />&nbsp; &nbsp; &nbsp; &nbsp; // usually each color component is stored<br />&nbsp; &nbsp; &nbsp; &nbsp; // using 8 bits, but recently 16 bits is occasionally<br />&nbsp; &nbsp; &nbsp; &nbsp; // also used ... you might want to handle that here<br />&nbsp; &nbsp; &nbsp; &nbsp; return NO;<br />&nbsp; &nbsp; }<br />&nbsp; &nbsp; <br />&nbsp; &nbsp; Bpr = [rep bytesPerRow];<br />&nbsp; &nbsp; spp = [rep samplesPerPixel];<br />&nbsp; &nbsp; data = [rep bitmapData];<br />&nbsp; &nbsp; w = [rep pixelsWide];<br />&nbsp; &nbsp; h = [rep pixelsHigh];<br /><br />&nbsp; &nbsp; for (y=0; y&lt;h; y++) {<br />&nbsp; &nbsp; &nbsp; &nbsp; unsigned byte *p = data + Bpr*y;<br />&nbsp; &nbsp; &nbsp; &nbsp; for (x=0; x&lt;w; x++) {<br />&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; // do your thing<br />&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; // p[0] is red, p[1] is green, p[2] is blue, p[3] can be alpha if spp==4<br />&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; p += spp;<br />&nbsp; &nbsp; &nbsp; &nbsp; }<br />&nbsp; &nbsp; }<br />&nbsp; &nbsp; return YES;
    
  • CéroceCéroce Membre, Modérateur
    avril 2009 modifié #17
    Je te proposais de convertir toutes les images vers le même format (par exemple RGB 24 bits/pixel), même si le fichier source était en 48 bits/pixels, ou en 256 niveaux de gris. Ce qui te permet de n'avoir qu'un seul algo.

    Le code que tu donnes ne résout rien. Si la bitmap n'est pas en 8 bits/composante, il ne traite pas l'image. Point (et en plus, il ne prend pas en compte les octets inutilisés).
  • yqnnyqnn Membre
    06:09 modifié #18
    dans 1239433105:

    Je te proposais de convertir toutes les images vers le même format (par exemple RGB 24 bits/pixel), même si le fichier source était en 48 bits/pixels, ou en 256 niveaux de gris. Ce qui te permet de n'avoir qu'un seul algo.

    Ah désolé, je n'avais pas bien compris. En effet ce serait une bien meilleure solution. La conversion doit se faire avec : "representationUsingType:properties:"  (de NSBitmapImageRep) ?

Connectez-vous ou Inscrivez-vous pour répondre.