25

Agrandissement bilinéaire avec NEON

mai

Entre deux version du compteur de cycles, j’ai eu envie de m’amuser un petit peu avec un exercice un peu plus ludique.
Donc aujourd’hui, petit halte dans le domaine du traitement d’images… domaine où normalement, NEON est supposé exceller.

Introduction

L’interpolation bilinéaire d’une image permet de redimensionner une image en conservant une qualité “correcte”.
L’algorithme est relativement simple et peut sans problème (au moins en partie) exploiter les capacités de calculs de NEON (sans quoi j’en parlerai pas ici !)
Je ne vais pas entrer dans le détail de l’algorithme. Ce n’est pas le but ici qui est plutôt de savoir jusqu’à quel point on peut optimiser ce type d’algorithme en utilisant NEON.

Je dirai juste que :

  • L’interpolation bilinéaire n’est rien d’autre qu’une interpolation linéaire réalisée sur les deux axes X et Y.
  • L’algorithme n’est pas tout a fait identique selon qu’on souhaite agrandir ou réduire une image.
  • L’algorithme peut être réalisé en une ou deux étapes (Une fois en hauteur et une fois en largeur).

Le version que nous allons traiter ici est la version qui permet d’agrandir une image.

Code Source en C

J’ai d’abord développé l’algorithme en C afin de pouvoir comparer !

J’ai voulu être fairplay!
J’ai essayé d’optimiser un peu l’algorithme en C!

  • Je n’ai pas utilisé de nombres flottants
  • J’ai redimensionné l’image en une seule passe (là ou NEON va sans doute en avoir besoin de 2)!
  • J’ai fais des accès mémoire 32 bits puis j’ai découpé les composantes R,G,B et A grâce a des décalages et des masques plutôt que de faire des accès mémoire pour chaque composante
void strech_c(unsigned int *bSrc, unsigned int *bDst, int wSrc, int hSrc, int wDst, int hDst)
{
	unsigned int wStepFixed16b, hStepFixed16b, wCoef, hCoef, x, y;
	unsigned int pixel1, pixel2, pixel3, pixel4;
	unsigned int hc1, hc2, wc1, wc2, offsetX, offsetY;
	unsigned int r, g, b, a;

	wStepFixed16b = ((wSrc - 1) < < 16) / (wDst - 1);
	hStepFixed16b = ((hSrc - 1) << 16) / (hDst - 1);

	hCoef = 0;

	for (y = 0 ; y < hDst ; y++)
	{
		offsetY = (hCoef >> 16);
		hc2 = (hCoef >> 9) & 127;
		hc1 = 128 - hc2;

		wCoef = 0;
		for (x = 0 ; x < wDst ; x++)
		{
			offsetX = (wCoef >> 16);
			wc2 = (wCoef >> 9) & 127;
			wc1 = 128 - wc2;

			pixel1 = *(bSrc + offsetY * wSrc + offsetX);
			pixel2 = *(bSrc + (offsetY + 1) * wSrc + offsetX);
			pixel3 = *(bSrc + offsetY * wSrc + offsetX + 1);
			pixel4 = *(bSrc + (offsetY + 1) * wSrc + offsetX + 1);

			r = ((((pixel1 >> 24) & 255) * hc1 + ((pixel2 >> 24) & 255) * hc2) * wc1 +
				(((pixel3 >> 24) & 255) * hc1 + ((pixel4 >> 24) & 255) * hc2) * wc2) >> 14;
			g = ((((pixel1 >> 16) & 255) * hc1 + ((pixel2 >> 16) & 255) * hc2) * wc1 +
				(((pixel3 >> 16) & 255) * hc1 + ((pixel4 >> 16) & 255) * hc2) * wc2) >> 14;
			b = ((((pixel1 >> 8) & 255) * hc1 + ((pixel2 >> 8) & 255) * hc2) * wc1 +
				(((pixel3 >> 8) & 255) * hc1 + ((pixel4 >> 8) & 255) * hc2) * wc2) >> 14;
			a = ((((pixel1 >> 0) & 255) * hc1 + ((pixel2 >> 0) & 255) * hc2) * wc1 +
				(((pixel3 >> 0) & 255) * hc1 + ((pixel4 >> 0) & 255) * hc2) * wc2) >> 14;

			*bDst++ = (r < < 24) + (g << 16) + (b << 8) + (a);

			wCoef += wStepFixed16b;
		}
		hCoef += hStepFixed16b;
	}
}

Le filtrage bilinéaire consiste à trouver les 4 pixels "sources" les plus proches du pixel que l'on souhaite tracer.
Il calcule les coefficients de proximité de ces pixels (ici nommés hc1, hc2, wc1 et wc2).
La couleur du pixel de destination étant finalement une somme pondérée des 4 pixels les plus proches.

Encore une fois il s'agit ici de l'algorithme d'agrandissement d'une image. Si vous essayer de réduire une image avec cet algorithme, cela devrait fonctionner mais ne rendra pas un résultat très satisfaisant.

Voici le résultat. (L’agrandissement "au plus proche a été réalisé avec photoshop !)

Image source 128*100 Agrandissement au plus proche 320*248 Agrandissement bilinéaire 320*248

La version C permet le redimensionnement de l'image en 8.97 ms.

Interpolation avec NEON

NEON permet de faire des calculs sur plusieurs pixels (ou composantes à la fois). Néanmoins, cette possibilité nécessite qu'on puisse charger dans des registres NEON (et surtout dans les différents slots de ces registres) les données que l'on souhaite manipuler.
Pour faire simple, disons qu'il est aisé de traiter des données rangées consécutivement en mémoire. Il est par contre nettement plus compliqué d'aller cherche les données par-ci par-là afin de les faire entrer dans les registres NEON.

Tout ça pour dire qu'il va être assez facile de traiter l'interpolation dans le sens de la hauteur car on manipule dans ce cas deux lignes d'octets consécutifs. Par contre l'interpolation dans le sens de la largeur risque d'être assez compliquée. Elle pourrait même être plus lente de la version C, c'est pour dire !!!

Comme il est facile (et rapide) d'interpoler dans le sens de la hauteur et que l'interpolation bilinéaire peut être décomposée en deux interpolations linéaires, l'algorithme que je vais mettre en oeuvre va réaliser les 4 opérations suivantes:

  • Interpolation de l'image dans le sens de la hauteur
  • Transposition (rotation !!!) de l'image obtenue pour échanger les X et les Y
  • Interpolation de l'image dans le sens de la hauteur (à nouveau)
  • Transposition inverse de l'image obtenue pour la remettre dans le sens originale.

Alors autant le dire tout de suite, je n'ai pas encore testé. Je vais peut être avoir de mauvaises surprises. L'algorithme nécessite effectivement un buffer temporaire, pour y stocker l'image transposée. Hors les accès mémoires sont clairement un des points faibles de nos processeurs basse consommation.

Première version

Voici une première version d'un algorithme d'interpolation linéaire fonctionnant uniquement sur l'axe Y.

strech:
@ r0 = *src
@ r1 = *dst
@ r2 = width src
@ r3 = height src
@ r4 = width dst
@ r5 = height dst -> r12

	push			{r4-r11, lr}
	ldr			r4, [sp, #36]
	ldr			r5, [sp, #40]

	mov			r10, r0
	mov			r11, r1
	mov			r12, r5
	mov			r9, r2
	mov			r8, r3

	lsl			r0, r8, #16							@ height src * 65536
	sub			r0, r0, #0x10000					@ height src - 1
	sub			r1, r5, #1							@ height dst - 1

	bl				udiv
	mov			r1, r0								@ r1 = hStepFixed16b (fixed 16bit)
	mov			r0, #0								@ r0 = hCoef 

	b				.strech_loop
	.align		4
.strech_loop:
	lsr			r2, r0, #16							@ r2 = offsetY = (hCoef >> 16);
	ubfx			r3, r0, #9, #7						@ hc2 = (hCoef >> 9) & 127;
	mul			r2, r2, r9
	rsb			r4, r3, #128						@ hc1 = 128 - hc2; 

	vdup.u8		d31, r3
	vdup.u8		d30, r4

	add			r3, r10, r2, lsl #2				@ ptr Pixel Start
	add			r4, r3, r9, lsl #2				@ ptr Pixel Start + 1 line
	lsr			r5, r9, #2							@ nb 4 pixels blocs in oneline
.strech_copy_line:
	vld1.32		{q0}, [r3]!							@ load 4 pixel 1
	vld1.32		{q2}, [r4]!							@ load 4 pixel 2
	vmull.u8		q4, d0, d30							@ pixel1 * (1 - coef)
	vmlal.u8		q4, d4, d31							@ + pixel2 * coef
	vmull.u8		q5, d1, d30							@ pixel1 * (1 - coef)
	vmlal.u8		q5, d5, d31							@ + pixel2 * coef

	vqrshrn.u16 d16, q4, #7							@ reduce to 8 bit
	vqrshrn.u16 d17, q5, #7							@ reduce to 8 bit

	vst1.32		{q8}, [r11]!						@ write result
	subs			r5, r5, #1
	bne			.strech_copy_line 

	add			r0, r0, r1							@ hCoef += hStepFixed16b;
	subs			r12, r12, #1
	bne			.strech_loop 

	pop			{r4-r11, pc}

Quelques remarques concernant cette implémentation:

  • On a besoin d’une division (dont le code est juste après)
  • Le code ARM contenu entre “strech_loop” et “strech_copy_line” va s’exécuter en même temps que le code NEON. Son temps d’exécution devrait donc être nul (ou plus exactement transparent)
  • Pour chaque bloc de 4 pixels, il faut realiser 2 lectures et 1 écriture
  • Aucune optimisation n’a été faite sur ce code (vu qu’il va grandement changer)

Voila la fonction de Division (que j’ai prise sur le site d’ARM)…

udiv:
	mov			r3, r1								@ Put divisor in r3
	cmp			r3, r0, lsr #1						@ double it until
.udiv_j1:
	movls			r3, r3, lsl #1						@ 2 * r3 > r0
	cmp			r3, r0, lsr #1
	bls			.udiv_j1								@ the b means search backwards
	mov			r2, #0								@ initialize quotient
.udiv_j2:
	cmp			r0, r3								@ can we subtract r3?
	subcs			r0, r0, r3							@ if we can, do so
	adc			r2, r2, r2							@ double r2
	mov			r3, r3, lsr #1						@ halve r3,
	cmp			r3, r1								@ and loop until
	bhs			.udiv_j2								@ less than divisor
	mov			r1, r0
	mov			r0, r2
	mov			pc, lr

Et voilà le résultat

Image source 128*100 Agrandissement bilinéaire (axe Y) 128*248

Cette première version NEON non optimisée (mais loin de réaliser la même chose que la version C proposée précédemment) redimensionne l’image sur l’axe Y en 0.18 ms.
Le programme NEON s’exécute donc 49 fois plus vite. Mais il nous reste encore à voir le coût que va avoir les deux transpositions ainsi que la deuxième interpolation.
D’un autre coté on peut espérer pouvoir optimiser ce premier code NEON. Disons qu’un algorithme complet 10 à 15 fois plus rapide que le C serait un résultat correct et un objectif que je me fixe.

Transposition

La façon la plus rapide pour transposer une matrice 4×4 dont les données 32 bits (ici des pixels RGBA) sont contenus dans les registres q0, q1, q2 et q3 est celle-ci.

vtrn.32		q0, q1
vtrn.32		q2, q3
vswp			d1, d4
vswp			d3, d6

Ce code prend 6 cycles… C’est tout à fait acceptable.

Nous n’allons pas réaliser 4 passes distinctes sur notre image, cela serait trop coûteux en temps (encore qu’il faudrait tester). Nous allons essayer de réaliser l’interpolation en Y et la transposition dans la foulée.
Cela ne va pas être simple !!!
En effet, afin de pouvoir réaliser une transposition, il nous faut une matrice. C’est à dire non plus 4 pixels, mais 16 (4×4) !!! De plus ces pixels ne sont pas sur la même ligne mais sur 4 lignes différentes !
Comme il nous faut deux pointeurs source pour interpoler les pixels, et bien là, il va nous en falloir 8 ! Soit 8 registres ARM juste pour ça!
Idem pour l’écriture, il va nous faudrait idéalement 4 pointeurs et donc 4 registres ARM supplémentaires.
Donc clairement nous n’aurons pas assez de registres ARM disponibles. Il va falloir voir comment économiser ces précieuses ressources sans avoir recourt à des accès mémoires long et gourmand.

Interpolation et transposition en une seule passe

Pour économiser des registres nous allons utiliser la post incrémentation. Les deux accès en lecture sont toujours réalisés sur deux lignes consécutives. Donc nous pouvons utiliser le même pointeur pour faire les deux lectures. Il nous faut donc 4 pointeurs + 2 pointeurs d’offset. Ce qui nous fait 6 registres pour la lecture au lieu de 8.
Pour l’écriture c’est encore plus simple un seul registre suffit (en plus du registre contenant le pointeur du buffer de destination) !
Comme nous disposons au maximum de 15 registres (oui, on ne va pas pouvoir utiliser r15 !!!)
Il nous en reste 8 ! Avec 8 registres on va s’en sortir…

L’utilisation des registres va donc être la suivante

@r0 : hCoef
@r1 : hStepFixed16b
@r2 : working Src Ptr1
@r3 : working Src Ptr2
@r4 : working Src Ptr3
@r5 : working Src Ptr4
@r6 : trash register // nb Bloc of 4 pixel (used in strech_copy_line loop)
@r7 : trash register // working ptr Dest Image
@r8 : hSrc // offset Next DstLine
@r9 : wSrc // offset Next SrcLine
@r10 : ptr Source Image
@r11 : ptr Dest Image
@r12 : offset Previous SrcLine  + 16
@r13 : preload Ptr.
@r14 : hDst (used for strech_loop loop)

Comme nous allons avoir besoins de r13 (utilisé ici pour précharger le cache), nous ne pourrons pas utiliser la pile pour sauvegarder les registres. Ce n’est pas bien grave, nous utiliserons une zone data.

Afin d’obtenir des performances acceptables nous allons devoir nous imposer quelques contraintes.

  • nos buffers mémoire sont alignés sur des adresses multiple de 8. (personnellement j’utilise des adresses alignées sur des multiple de 16 !)
  • La résolution des images sont des multiples de 4 aussi bien en largeur qu’en hauteur. Une image peut donc être contenu dans une zone mémoire un peu plus grande que nécessaire. (personnellement j’utilise des multiple de 8. C’est la taille d’une DCT jpeg)
  • Nos pixels sont 32 bits, même si la composante alpha est inexistante. Là aussi, le surplus de bande passante nécessaire pourrait être compensé par la simplification des traitements

Voilà donc l’algorithme complet qui réalise une interpolation et une transposition en une seule passe.

strech:
@r0 : hCoef
@r1 : hStepFixed16b
@r2 : working Src Ptr1
@r3 : working Src Ptr2
@r4 : working Src Ptr3
@r5 : working Src Ptr4
@r6 : trash register // nb Bloc of 4 pixel (used in strech_copy_line loop)
@r7 : trash register // working ptr Dest Image
@r8 : hSrc // offset Next DstLine
@r9 : wSrc // offset Next SrcLine
@r10 : ptr Source Image
@r11 : ptr Dest Image
@r12 : offset Previous SrcLine  + 16
@r13 : preload Ptr.
@r14 : hDst (used for strech_loop loop)

	movw        r12, #:lower16:.strech_save_buffer
	movt        r12, #:upper16:.strech_save_buffer
	stmia			r12, {r4-r11, r13, lr}			@ saving all registers

	mov         r10, r0								@ r10 : ptr Source Image
	mov         r11, r1								@ r11 : ptr Dest Image
	ldr         r12, [sp, #4]						@ r12 : hDst
	mov			r9, r2								@ r9 : wSrc
	mov			r8, r3								@ r8 : hSrc

	sub			r0, r8, #1							@ hSrc - 1
	sub         r1, r12, #1							@ hDst - 1
	lsl         r0, r0, #16							@ hSrc * 65536

	bl          udiv
	mov         r1, r0                        @ r1 : hStepFixed16b (fixed 16bit)
	mov         r0, #0                        @ r0 : hCoef 

	mov         r14, r12								@ r14 : hDst
	lsl			r9, r9, #2							@ r9 : wSrc * 4 (nb byte wSrcLine). Will be used to jump to next Src line.
	lsl			r8, r14, #2							@ r8 : hDst * 4 (nb byte wDstLine after transpose). Will be used to jump to next Dst line
	rsb			r12, r9, #16						@ r12 : offset previous line + 16 bytes. will be used to go back to the previous and skip 16 bytes.

.strech_loop:
	lsr			r2, r0, #16							@ r2 = offsetY = (hCoef >> 16);
	ubfx			r6, r0, #9, #7						@ hc2 = (hCoef >> 9) & 127;
	mla			r2, r2, r9, r10					@ r2 : ptr Source 1
	rsb			r7, r6, #128						@ hc1 = 128 - hc2;
	add			r0, r0, r1
	vdup.u8		d31, r6
	vdup.u8		d30, r7

	lsr			r3, r0, #16							@ r2 = offsetY = (hCoef >> 16);
	ubfx			r6, r0, #9, #7						@ hc2 = (hCoef >> 9) & 127;
	mla			r3, r3, r9, r10					@ r3 : ptr Source 2
	rsb			r7, r6, #128						@ hc1 = 128 - hc2;
	add			r0, r0, r1
	vdup.u8		d29, r6
	vdup.u8		d28, r7

	lsr			r4, r0, #16							@ r2 = offsetY = (hCoef >> 16);
	ubfx			r6, r0, #9, #7						@ hc2 = (hCoef >> 9) & 127;
	mla			r4, r4, r9, r10					@ r4 : ptr Source 3
	rsb			r7, r6, #128						@ hc1 = 128 - hc2;
	add			r0, r0, r1
	vdup.u8		d27, r6
	vdup.u8		d26, r7

	lsr			r5, r0, #16							@ r2 = offsetY = (hCoef >> 16);
	ubfx			r6, r0, #9, #7						@ hc2 = (hCoef >> 9) & 127;
	mla			r5, r5, r9, r10					@ r5 : ptr Source 3
	rsb			r7, r6, #128						@ hc1 = 128 - hc2;
	add			r0, r0, r1
	vdup.u8		d25, r6
	vdup.u8		d24, r7

	add			r13, r2, r9, lsl #2				@ r13 : ptr on next 4 lines of pixel. Used for preload
	lsr			r6, r9, #4							@ nb bloc of 4 pixels
	mov			r7, r11								@ r7 : work Dest Ptr

.strech_copy_line:
	vld1.32		{q2}, [r2:128], r9				@ load 4 pixel (line 1 src 1) + jump to next line
	vld1.32		{q3}, [r2:128], r12				@ load 4 pixel (line 1 src 2) + jump to previous line + 16
	vld1.32		{q4}, [r3:128], r9				@ load 4 pixel (line 2 src 1) + jump to next line
	vld1.32		{q5}, [r3:128], r12				@ load 4 pixel (line 2 src 2) + jump to previous line + 16
	vld1.32		{q6}, [r4:128], r9				@ load 4 pixel (line 3 src 1) + jump to next line
	vld1.32		{q7}, [r4:128], r12				@ load 4 pixel (line 3 src 2) + jump to previous line + 16
	vld1.32		{q8}, [r5:128], r9				@ load 4 pixel (line 4 src 1) + jump to next line
	vld1.32		{q9}, [r5:128], r12				@ load 4 pixel (line 4 src 2) + jump to previous line + 16

	pld			[r13]
	add			r13, r13, #128

	vmull.u8		q0, d4, d30							@ pixel1 * (1 - coef)
	vmlal.u8		q0, d6, d31							@ + pixel2 * coef
	vmull.u8		q1, d5, d30							@ pixel1 * (1 - coef)
	vmlal.u8		q1, d7, d31							@ + pixel2 * coef

	vmull.u8		q2, d8, d28							@ pixel1 * (1 - coef)
	vmlal.u8		q2, d10, d29						@ + pixel2 * coef
	vmull.u8		q3, d9, d28							@ pixel1 * (1 - coef)
	vmlal.u8		q3, d11, d29						@ + pixel2 * coef

	vmull.u8		q4, d12, d26						@ pixel1 * (1 - coef)
	vmlal.u8		q4, d14, d27						@ + pixel2 * coef
	vmull.u8		q5, d13, d26						@ pixel1 * (1 - coef)
	vmlal.u8		q5, d15, d27						@ + pixel2 * coef

	vmull.u8		q6, d16, d24						@ pixel1 * (1 - coef)
	vmlal.u8		q6, d18, d25						@ + pixel2 * coef
	vmull.u8		q7, d17, d24						@ pixel1 * (1 - coef)
	vmlal.u8		q7, d19, d25						@ + pixel2 * coef

	vqrshrn.u16 d16, q0, #7							@ reduce to 8 bit
	vqrshrn.u16 d17, q4, #7							@ reduce to 8 bit
	vqrshrn.u16 d18, q2, #7							@ reduce to 8 bit
	vqrshrn.u16 d19, q6, #7							@ reduce to 8 bit
	vqrshrn.u16 d20, q1, #7							@ reduce to 8 bit
	vqrshrn.u16 d21, q5, #7							@ reduce to 8 bit
	vqrshrn.u16 d22, q3, #7							@ reduce to 8 bit
	vqrshrn.u16 d23, q7, #7							@ reduce to 8 bit

	vtrn.32		q8, q9								@ tranpose
	vtrn.32		q10, q11

	vst1.32		{q8}, [r7:128], r8				@ write result
	vst1.32		{q9}, [r7:128], r8				@ write result
	vst1.32		{q10}, [r7:128], r8				@ write result
	vst1.32		{q11}, [r7:128], r8				@ write result

	subs			r6, r6, #1
	bne			.strech_copy_line

	add			r11, r11, #16						@ r11 : translate for next lane
	subs			r14, r14, #4
	bne			.strech_loop

	movw        r12, #:lower16:.strech_save_buffer
	movt        r12, #:upper16:.strech_save_buffer
	ldmia			r12, {r4-r11, r13, pc}			@ reload all registers and return
Image source 128*100 Agrandissement & Transposition 248*128 Agrandissement Bilinéaire NEON 320*248

Lorsqu’on copie ce code dans le compteur de cycle, on pense qu’on va obtenir des performances tout à fait intéressantes. En fait, ce n’est pas vraiment le cas !

Pour redimensionner l’image, il faut appeler deux fois programme ci dessus…
Le redimensionnement s’exécute finalement en 0.83 ms – donc nettement plus lent que l’espoir que j’avais fondé !
La raison est probablement due aux “cache miss” ! Le fait de lire plusieurs lignes de donnée simultanément est fatal au Cortex.
De plus, notre image de base est très petite. Plus elle va être grosse plus la performance va chuter de façon importante.

Conclusion

Dans l’exemple utilisé ici la version assembleur va quand même 10.8 fois plus vite que la version C. Ce n’est pas négligeable ! Mais on était en droit de s’attendre à mieux !
Bon je ne m’avoue pas vaincu !!! On doit sans doute pouvoir optimiser un peu ce programme je vais continuer mes investigations (voir même tester d’autres approches. Pourquoi pas tenter l’algorithme simple passe).

MàJ du 27/05/2011: Bon! Il n’y a pas grand chose à faire. Je suis arrivé en réorganisant les instructions à 0.74ms (soit 12.1 fois plus rapide que le version C).
La version en une passe est extrêmement complexe à réaliser et elle va beaucoup moins vite que la version proposé ici.
J’ai également essayer de traiter 2 bloc de 4 * 4 pixels à chaque itérations, mais le résultat était légèrement inférieur !!!

J’écrierai un prochain post concernant l’interpolation bilinéaire utilisé dans le cadre de la réduction d’une image. La réduction demande plus de calcul et moins d’accès mémoire aléatoire. Donc tout pour que NEON soit nettement plus efficace. Je ne sais pas s’il la réduction d’image est plus rapide que l’agrandissement, mais je suis à peu prêt sûr que l’écart de performance entre le C et l’assembleur va être nettement plus grand.

 | Tags: ,

5 Responses to “Agrandissement bilinéaire avec NEON”

  1. [...] This post is a translation of « Agrandissement bilinéaire avec NEON » [...]

  2. ericb dit :

    Pff … c’est de mieux en mieux :-) Je crois qu’on s’est manqués sur IRC. Est-il possible de discuter par mail ?

    Sauf problème, je vais _enfin_ avoir un peu de temps bientôt, et j’ai toujours plein de questions.

    Merci :-)


    ericb

  3. schtruck dit :

    salut

    une version 16bits RGB565 serait elle envisageable?

  4. Etienne SOBOLE dit :

    sans doute, mais elle serait sans doute plus compliqué car il faudrait séparer les composantes…
    faudrait que je m’y penche a l’occasion.

  5. [...] y a quelques temps j’avais proposé un algorithme d’interpolation bilinéaire permettant l’agrandissement d’une image dont les performances quoique correctes m’avaient un peu [...]

Répondre

Human control : 7 + 2 =