NSView, NSImage et optimisation
yqnn
Membre
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 :
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).
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).
Connectez-vous ou Inscrivez-vous pour répondre.
Réponses
- 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:
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.
Je pense que je vais m'orienter dans cette voie. Est-ce difficile à faire ? Qu'elle doc faut il lire ?
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).
@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.
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.
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 :
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 ^^
Je ne comprend pas bien à quoi correspond le contexte graphique.
Cela consiste à faire un setBounds: sur la vue ?
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.
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 ?
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.
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.
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.
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 :
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).
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) ?