2 Au cours de nos expérimentations, en particulier concernant le débruitage par lignes de niveaux décrit dans la chapitre précédent, nous avons cherché à comparer les performances d'un certain nombre d'algorithmes de filtrage portés sur GPU. Comme nous l'avons dit dans le chapitre décrivant les diverses solutions proposées, il s'est avére que le filtre médian n'avait pas fait l'objet de beaucoup de publications. On recensait tout de même quelques implémentations intéressantes des algorithmes BVM et PCMF, ainsi que l'existence d'une solution commerciale libJacket/Arrayfire (se reporter au paragraphe \ref{sec-median}).
4 Les performances annoncées pour des fenêtres de petite taille comme le médian 3$\times$3 pouvaient atteindre jusqu'à 180 millions de pixels traités à la seconde dans le cas de Arrayfire. En regard du petit nombre d'opérations à effectuer pour sélectionner la valeur médiane dans une fenêtre 3$\times$3, il nous a intuitivement semblé que ces débits étaient très en deça des possibilités des GPU employés.
6 Un rapide prototypage a conforté cette idée et nous a conduit à chercher plus avant une technique d'implémentation du filtre médian qui exploite pleinement les capacités de nos GPU.
8 \section{Les transferts de données}
10 Le chapitre \ref{ch-GPU}, présentant l'architecture et les caractéristiques principales des GPU, donne également la liste et les spécificités des types de mémoire accessibles par un kernel parallèle. Lorsqu'il s'agit de stocker des volumes importants de données, comme les images d'entrée et de sortie, les alternatives sont assez limitées. En effet, la seule mémoire suffisamment importante est la mémoire externe, malheureusement la plus lente également. On dispose cependant de plusieurs modes d'accès, comme la déclaration de textures, qui offre un mécanisme de cache 2D permettant d'augmenter assez nettement les débits en lecture.
11 Dans le cadre de nos travaux, cette mémorisation sous forme de texture s'est montré la plus performante pour les images d'entrée.
13 Les images de sortie sont produites en mémoire globale standard, hors texture, puis copiées vers une zone de mémoire non paginée de l'hôte CPU. L'algorithme \ref{algo-median-memcpy} synthétise ces pratiques en introduisant aussi les notations pour la suite.
14 Cet emploi de mémoire non paginée apporte un gain de temps important dans les transferts ; il peut s'avérer limitant dans des situations requérant de très grands volumes de données. Les quantités de mémoire vive dont disposent les ordinateurs modernes (plusieurs Go) permettent cependant de traiter sans restriction des images de plusieurs centaines de millions de pixels. Nos essais ont été conduits avec des images d'au maximum 100MP.
17 %\SetNlSty{textbf}{}{:}
19 allocation et affectation en mémoire CPU \textbf{h\_img\_in}\;
20 allocation de mémoire CPU non-paginée \textbf{h\_img\_out}\;
21 allocation de mémoire globale GPU \textbf{d\_img\_out}\;
22 allocation de mémoir texture GPU \textbf{tex\_img\_in}\;
23 copie image de \textbf{h\_img\_in} vers \textbf{tex\_img\_in}\;
24 kernel\kl gridDim,blockDim\kr\tcc*[f]{sortie dans d\_img\_out}\;
25 copie image de sortie de \textbf{d\_img\_out} vers \textbf{h\_img\_out}\;
26 \caption{Gestion des transferts mémoire vers et depuis le GPU.}
27 \label{algo-median-memcpy}
31 Ces choix concernant les types de mémoire employés sont un facteur déterminant de la performance globale de l'implémentation. Cela sera confirmé par les mesures présentées à la fin de ce chapitre sur le filtre médian, mais une première expérience permet de s'en convaincre : le kernel médian 3$\times$3 d'Arrayfire, aimablement mis à disposition par l'un des développeurs, voit son débit global pratiquement doublé lorsqu'on adapte ses accès mémoire pour la combinaison texture/non-paginée que l'on vient de presenter.
33 Le tableau \ref{tab-median-memcpy} donne le détail des temps de transferts pour quelques tailles usuelles d'images de niveaux de gris, codés en 8 ou 16 bits, et compare les temps globaux avec ceux mesurés lorsque la simple mémoire globale est employée. L'impact du choix de la configuration mémoire y est rendu évident, avec des gains de 15\% à 75\% constatés.
36 \renewcommand{\arraystretch}{1.5}
39 \begin{tabular}{cccccc}
41 \shortstack{Dimension\\(pixels)} & \shortstack{Profondeur\\(bits)} & \shortstack{CPU $\rightarrow$ GPU\\(ms)}& \shortstack{GPU $\rightarrow$ CPU\\(ms)}& \shortstack{\textbf{Total}\\(ms) }&\shortstack{\textbf{Mém. globale}\\(ms) } \\
43 \multirow{2}*{512$\times$512} &8 &0.08 &0.06 &\textbf{0.14}&0.23 \\
44 &16 &0.14 &0.10 &\textbf{0.24}&0.42 \\
46 \multirow{2}*{1024$\times$1024}& 8 &0.24 &0.19 &\textbf{0.43}&0.81 \\
47 & 16 &0.45 &0.35 &\textbf{0.80}&1.23 \\
49 \multirow{2}*{2048$\times$2048}& 8 &0.85 &0.68 &\textbf{1.53}&2.15 \\
50 & 16& 1.59& 1.32&\textbf{2.91}&3.83 \\
52 \multirow{2}*{4096$\times$4096}& 8 &3.27 &2.61 &\textbf{5.88}&7.10 \\
53 & 16 &6.21 &5.21 &\textbf{11.42}&13.16 \\
56 \caption{Temps de transfert vers et depuis le GPU, en fonction de la dimension de l'image et de la profondeur de niveaux de gris. La colonne ``Mémoire globale'' donne les temps mesurés lorsque cette seule mémoire est employée.}
57 \label{tab-median-memcpy}
61 \section{Utilisation des registres}
63 En traitement d'image, les filtres médians sont beaucoup employés avec des tailles de fenêtres modestes pour du prétraitement, événtuellement itératif, ou bien avec de grandes fenêtres pour de l'estimation d'intensité d'arrière plan. Les taille intérmédiaires, disons de quelques dizaines de pixels, ne sont à notre connaissance pas employées.
65 Un filtre médian de petite taille ne réalise que peu d'opérations, sans complexité de surcroît, et doit donc atteindre des niveaux de performances élevés.
66 Le cadre général des traitements sur GPU présenté au paragraphe \ref{sec-bilateral} n'est alors plus pertinent, pour deux raisons :
68 \item La phase de préchargement des données nécessaires en mémoire partagée prend du temps et la lecture se faisant depuis la mémoire globale/texture, elle est soumise aux latences qui lui sont attachées.
69 \item l'utilisation en lecture/écriture des données en mémoire partagée, outre le fait qu'elle puisse être contraignante en terme de motif d'accès, n'atteint pas les débits permis par les registres individuels à la disposition des threads.
72 Il est ainsi clair que la chaîne de traitement la plus performante consiste à ne faire qu'une lecture en texture par pixel puis d'effectuer les calcul en registres. Les limites d'intérêt de ce schéma général sont le nombre de registres disponibles, par thread et par bloc. Si on dépasse ces limites, le compilateur déporte les variables en mémoire locale, très peu performante.
73 Sans aller au delà de la limite, l'utilisation de trop nombreux registres va mécaniquement limiter le nombre de thread effectivement exécutés en parallèle par le GPU et bien souvent grêver la performance du kernel. Un compromis est à définir entre la recherche de vitesse par l'emploi des registres et le ralentissement que provoque l'usage d'un trop grand nombre de registres.
75 Prenons l'exemple d'un kernel qui ferait usage d'un total de 20 registres et que l'on exécuterait par blocs de 128 threads. La limite des 63 threads par blocs n'est évidemment pas atteinte, ni celle des 32K par bloc avec seulement $128 \times 20 = 2560$ registres. Dans ce cas, le GPU pourra exécuter en parallèle $32K/2560 = 12$ blocs, soit 1536 threads, ce qui représente le maximum possible.
76 En revanche si le même kernel utilise maintenant 24 registres, le GPU ne pourra plus exécuter en parallèle que 1280 threads sur les 1536 physiquement possibles. Une perte de performance est alors à craindre.
78 De ce point de vue, l'architecture Fermi qui propose 63 registres par thread et dont le modèle C2070 est un représentant, représente une régression par rapport à la génération précédente avec par exemple le GPU C1060 et ses 128 registres par thread.
81 \subsection{La sélection de la valeur médiane}
83 Dans le cas des filtres médians à petite fenêtre, on peut envisager d'attribuer un registre par valeur à trier. Dans ce cas, un médian 3$\times$3 emploiera 9 registres par thread, et cette méthode pourra théoriquement s'appliquer jusqu'au médian 7$\times$7 sur C2070 et 11$\times$11 sur C1060.
84 Toutefois, la recherche de performance impose de rationnaliser l'utilisation des registres et nous nous sommes orientés vers une méthode de sélection qui permet de ne pas avoir recours à cette cardinalité de un registre pour un pixel de la fenêtre, l'algorithme dit \textit{forgetful selection} ou sélection par oubli.
86 Le principe de la sélection par oubli est illustré en figure \ref{fig-median-ffs3-a} par l'exemple de la sélection de la médiane parmi 9 valeurs. Plus généralement, il s'agit de
88 \item former une liste initiale de $R_n$ valeurs prises parmi les $n=k\times k$ valeurs de la fenêtre du filtre.
89 \item identifier, puis éliminer de la liste la plus petite et la plus grande valeur.
90 \item insérer dans la liste une nouvelle valeur parmi celles non encore intégrées.
91 \item reprendre au point 2, et ce jusqu'à ce qu'il ne reste plus de valeur non utilisée. La médiane est alors la valeur restant dans la liste.
94 Cet algorithme nécessite un nombre constant d'étapes égal à $\left(n - \lceil\frac{n}{2}\rceil\right)$, ce qui assure une charge quasi équivalente pour tous les threads, même si le nombre d'opérations requis par l'identification des extrema dépend des valeurs dans chaque liste. Cette variabilité n'est pas conjointe à des branches d'exécution divergentes et n'induit pas de perte de performances.
96 Nous avons par ailleurs choisi de fixer le nombre $R_n$ de valeurs figurant initialement dans la liste, comme le plus petit permettant de réaliser la sélection de la médiane. On obtient ce nombre limite en considérant qu'à chaque phase d'élimination des extrema, il faut garantir que la médiane globale n'est pas élimnée. Or, la définition de la médiane indique que dans la liste triée complète, on trouve autant de valeurs dont l'indice est supérieur à celui de la médiane que de valeurs dont l'indice est inférieur. Sachant que les fenêtres des filtres comportent toujours un nombre impair de valeurs, la condition suffisante pour garantir la sélection est donc que le nombre de valeurs non intégrées dans la liste initiale soit inférieur au nombre de valeurs d'indice supérieur (ou inférieur) à la médiane dans la liste complète triée, soit
97 $$R_{n}=\lceil \frac{n}{2}\rceil+1$$
99 Cette valeur de $R_n$ représente donc aussi le nombre minimum de registres nécessaire à la sélection par oubli et sa minimisation permet de reculer la limite de taille admissible pour le filtre médian avec 9$\times$9 pour le GPU C2070.
102 \subfigure[Étapes de la sélection par oubli pour un filtre 3$\times$3.]{\label{fig-median-ffs3-a}\includegraphics[height=7cm]{Chapters/chapter5/img/forgetful_selectionb.jpg}\qquad}
103 \subfigure[Première étape d'identification des extrema pour un filtre 5$\times$5.]{\label{fig-median-ffs3-b}\includegraphics[height=5cm]{Chapters/chapter5/img/bitonic.jpg}}
104 \caption{Application de la sélection de médiane par oubli. a) à une fenêtre de $3\times 3$ pixels. b) Maximisation de l'ILP pour l'identification des extrema. }
105 \label{fig-median-ffs3}
109 \subsection{Masquage des latences}
111 Les lectures en textures ainsi que les écritures en mémoire globale sont soumises à des latences que nous avons déjà détaillées au chapitre \ref{ch-GPU}. La mémoire texture bénéficie d'un cache permettant d'optimiser les lectures dans un voisinage à deux dimensions. Cela permet de réduire nettement les latences apparentes lors de l'accès aux éléments de la fenêtre du filtre. L'algorithme que nous proposons ne requiert qu'une lecture par élément de la fenêtre, dont la taille est assez petite pour que tous les éléments soient mis en cache. Aucune latence supeflue n'est donc générée à la lecture.
113 Un autre moyen de réduire la latence moyenne constatée d'une séquence d'instructions est d'augmenter le niveau d'ILP (Instruction Level Parallelism ou parralélisme d'instructions). On cherche pour cela à réduire autant que possible la dépendance entre instructions successives au sein d'un kernel, de sorte à ne pas forcer le vidage des pipelines. Nous avons appliqué ce principe à la phase d'identification des extrema de la liste en arrangeant les instructions élémentaires de permutation de sorte à éloigner au maximum les instructions inter-dépendantes. Dans l'exemple de la figure \ref{fig-median-ffs3-b}, on montre la séquence des permutations conditionnelles permettant l'identification des extrema lors de la première étape de sélection d'un filtre médian 5$\times$5. On retrouve donc les $R_n=14$ éléments de la liste initiale en haut de la figure, et la même liste au bas avec la valeur minimale à gauche et la valeur maximale à droite. Les séquences d'instructions indépendantes sont séparées par les lignes pointillées horizontales.
115 Enfin, il est possible de réduire aussi la latence moyenne d'accès à la mémoire globale en faisant en sorte que chaque thread produise non pas la valeur de sortie d'un seul, mais de plusieurs pixels et ce, par autant d'écritures immédiatement consécutives, seule la première de la série générant une latence. Pour que l'application de ce principe produise l'effet attendu, il faut tout de même garantir la contiguïté des accès par demi-warp, ce qui est le cas ici si les valeurs multiples issues par chaque thread sont également à des adresses consécutives en mémoire globale.
117 Nous faisons l'hypothèse que chaque thread traite deux pixels voisins et cela impose de gérer la superposition partielle des fenêtres du filtre. La méthode de sélection que nous avons choisie nous interdit en effet d'employer les techniques habituelles, comme la mise à jour incrémentale de l'histogramme des niveaux de gris. Au contraire, les sélections doivent être menées conjointement et cela est rendu possible par le fait que la liste initiale ne comporte que $R_n$ éléments, ce qui est toujours inférieur ou égal au nombre de valeurs partagées par les fenêtres associées à deux pixels voisins (sous-entendu : horizontalement). Deux fenêtres voisines partagent $S_n = n-k = n-\sqrt{n}$ éléments et, pour $n\ge 9$ :
118 $$ n-\sqrt{n} \ge \lceil\frac{n}{2}\rceil +1$$
122 \includegraphics[width=6cm]{Chapters/chapter5/img/median5_overlap.jpg}
123 \caption{Gestion des éléments communs aux fenêtres de deux pixels centraux voisins dans un filtre médian 5$\times$5. La liste initiale comprend les 14 premiers éléments communs, puis les 7 premières étapes de sélection sont conduites en commun avant que les 5 dernières le soient en parallèle, mais de manière disjointe.}
124 \label{fig-median-overlap}
127 Nous pouvons donc initialiser la liste de sélection avec $R_n$ valeurs choisies parmi les $S_n$ valeurs communes, puis mener les $\left(S_n-R_n+1\right)$ premières étapes de sélection permettant d'intégrer progressivement l'ensemble des valeurs communes à la liste. Il ne reste alors que les $k$ éléments propres à chaque fenêtre à intégrer dans deux séquences de sélection rendues distinctes mais réalisées en parallèle. Cette répartition des éléments pour un filtre médian 5$\times$5 est représentée à la figure \ref{fig-median-overlap}. Comme on le voit sur la figure, les $R_n$ premières valeurs sont simplement prises au début de la liste des valeurs communes, sans que cela ne génère des défauts de cache pour les fenêtres de taille supérieure (le cache de texture 2D contient environ 5Ko).
129 Chaque thread utilise plus de registres ($R_n+2k$) pour traiter deux pixels que pour un seul ($R_n$). Notons que cela ne modifie pas les capacités théoriques de traitement avec toujours une taille maximale de 9$\times$9 pour C2070. Il suffit alors de réduire le nombre de threads par bloc pour retrouver le niveau de parallélisme souhaité. Mais globalement, traiter deux pixels par threads permet d'utiliser $k+1$ registres de moins par paire de pixels par rapport au traitement par threads distincts. Cela nous permet donc finalement aussi un gain de parallélisme au niveau de chaque bloc.
131 Nous avons fait ici l'hypothèse d'un traitement de deux pixels par thread. Le passage à quatre pixels par thread signifie ne plus disposer que de $n-3k$ éléments communs aux quatre fenêtres, ce qui est inférieur à $R_n$ pour les tailles 3$\times$3 et 5$\times$5, et ne permet que deux étapes de sélection communes pour le 7$\times$7, ce qui ne permet pas de compenser le coût des copies nécessaires au dédoublement. Le cas du 9$\times$9 pourrait sembler plus pertinent, mais là encore les coûts inhérents aux sélections disjointes ne parviennent pas à être compensées par une séquence suffisamment importante de sélections communes aux quatre pixels. La solution deux pixels par thread s'avère la plus performante.
133 L'ensemble des choix que nous venons de décrire et qui ont présidé à l'élaboration de notre filtre médian GPU conduisent à adopter un style de codage assez inhabituel. En effet, les registres dont nous faisons un usage intensif ne sont en particulier pas indexables. Le code du filtre médian 3$\times$3 est reproduit au listing \ref{lst-median3} pour en illustrer ces implications pratiques.
135 \lstinputlisting[label={lst-median3},caption=Kernel réalisant un filtre médian 3$\times$3 en registres.]{Chapters/chapter5/code/median3-2pix.cu}
141 \subfigure[image 512$\times$512 pixels.]{\includegraphics[width=7cm]{Chapters/chapter5/img/comp512.png}}\qquad
142 \subfigure[image 4096$\times$4096 pixels.]{\includegraphics[width=7cm]{Chapters/chapter5/img/comp4k.png}}
143 \caption{Comparaison des débits (MP/s) atteints par notre implémentation notée PRMF, avec les principales solutions de référence. De gauche à droite : PCMF, BVM, PRMF, ArrayFire (impossible en 4096$\times$4096)}
144 \label{fig-median-comp}
147 Les valeurs présentées dans les tableaux \ref{tab-median-coutcpy}, \ref{tab-median-chronos} et la figure \ref{fig-median-comp} sont obtenues par moyennage du chronométrage de 1000 exécutions du même kernel, développé en variantes 8 et 16 bits de profondeur de niveau de gris.
149 La première analyse que nous pouvons en faire est la pertinence des choix faits quant aux transferts de données, qui représentent entre 13\% et 82\% du temps total d'exécution des configurations testées.
152 \renewcommand{\arraystretch}{1.5}
155 \begin{tabular}{ccccc}
157 {\bf Taille d'image}& {\bf Profondeur}& \textbf{3$\times$3} & \textbf{5$\times$5} & \textbf{7$\times$7} \\
159 \multirow{2}{*}{512$^2$} &8 bits&73\% &44\% &20\% \\
160 &16 bits&82\% &57\% &29\% \\
162 \multirow{2}{*}{1024$^2$}&8 bits&68\% &37\% &15\% \\
163 &16 bits&80\% &53\% &25\% \\
165 \multirow{2}{*}{2048$^2$}&8 bits&66\% &34\% &14\% \\
166 &16 bits&79\% &59\% &23\% \\
168 \multirow{2}{*}{4096$^2$}&8 bits&65\% &33\% &13\% \\
169 &16 bits&78\% &50\% &23\% \\
172 \caption{Pourcentage du temps d'exécution pris par les transferts de données en fonction de la taille de fenêtre du filtre, pour les profondeurs 8 and 16 bit sur GPU C2070.}
173 \label{tab-median-coutcpy}
177 Les valeurs du tableau \ref{tab-median-chronos} détaillent les débits de pixels réalisés par les différents kernels. Ils prennent en compte le temps d'exécution ainsi que les temps de transfert. Par ailleurs, afin d'évaluer le niveau de performance absolue de notre méthode, nous avons également mesuré le débit maximum effectif permis par le couple GPU/CPU. Ce débit maximum nous permet d'évaluer la pertinence d'éventuelles recherches postérieures qui chercheraient à améliorer encore les débits. La valeur de ce débit maximum est obtenue en exécutant un kernel ``identité'' qui n'effectue aucune opération mais se contente de faire les lectures et écritures en mémoire. Les débits ainsi mesurés sont regroupés dans le tableau \ref{tab-median-debitmax} où l'on constate en particulier que plus l'image est de grande dimensions, plus on peut espérer un débit élevé. On vérifie aussi notre intuition initiale avec des valeurs d'environ 2 milliards de pixels par seconde, à comparer aux moins de 2 millions de pixels par seconde permis par les implémentations de référence.
184 {\bf Taille d'image}& {$\mathbf{ T_8}$} & {$\mathbf{T_{16}}$} \\
186 512$\times$512 &1598 &975 \\
187 1024$\times$1024 &2101 &1200 \\
188 2048$\times$2048 &2359 &1308 \\
189 4096$\times$4096 &2444 &1335 \\
192 \caption{Débits maximum effectifs $T_8$ and $T_{16}$ (en MP/s), respectivement pour les variantes 8 et 16 bits sur C2070.}
193 \label{tab-median-debitmax}
199 \begin{tabular}{clccc}
201 {\bf Taille d'image}&\shortstack{{\bf $t$ : temps kernel }\\{\bf $T_x$ débit en prof. x }} & \textbf{3$\times$3} & \textbf{5$\times$5} & \textbf{7$\times$7} \\\midrule
202 \multirow{3}{*}{{512$\times$512}} &t (ms) &0.05 &0.19 &0.60 \\
203 &$T_{8}$ (Mpix/s)&1291 &773 &348 \\
204 &$T_{16}$ (Mpix/s)&865 &607 &307 \\
206 \multirow{3}{*}{{1024$\times$1024}}&t (ms) &0.20 &0.74 &2.39 \\
207 &$T_{8}$ (Mpix/s)&1644 &889 &371 \\
208 &$T_{16}$ (Mpix/s)&1045 &692 &329 \\
210 \multirow{3}{*}{{2048$\times$2048}}&t (ms) &0.79 &2.95 &9.53 \\
211 &$T_{8}$ (Mpix/s)&1805 &936 &379 \\
212 &$T_{16}$ (Mpix/s)&1130 &729 &338 \\
214 \multirow{3}{*}{{4096$\times$4096}}&t (ms) &3.17 &11.77 &38.06 \\
215 &$T_{8}$ (Mpix/s)&1854 &951 &382 \\
216 &$T_{16}$ (Mpix/s)&1151 &738 &340 \\
219 \caption{Performances des filtres médians rapides en fonction des tailles d'image et de fenêtre du filtre, en variantes 8 et 16 bits de profondeursur GPU C2070.}
220 \label{tab-median-chronos}
223 Les performances des kernels en variante 16 bits ne différent pas de celles en variante 8 bits, les seuls temps de transfert des données sont à l'origine des différences de performance globale.
225 Enfin, considérant que l'algorithme implémenté est proche de celui d'Arrayfire, nous avons mesuré les débits atteints par leurs kernels, modifiés pour adopter notre gestion mémoire. Cette manipulation permet au kernel Arrayfire 3$\times$3 de traiter 670~MP/s, soit 3,7 fois plus que la version commerciale. L'écart restant avec notre implémentation ($\times$2,7) étant à mettre au crédit de notre implémentation du kernel.
230 L'implémentation GPU du filtre médian que nous avons décrite permet de traiter environ jusqu'à 1854 millions de pixels à la seconde, soit aussi 900 images haute définition (1080p), et surclasse les solutions jusqu'alors proposées dans la littérature dont la plus performante ne débitait que 180 millions de pixels par seconde (voir \cite{sanchezICASSP12}). Cet important gain de vitesse est la conséquence de l'attention toute particulière que nous avons apportée à la gestion de la mémoire, tant du coté GPU que CPU, et qui nous a conduit à concevoir des kernels utilisant exclusivement des registres pour effectuer les opérations de sélection.
232 Le débit de pixels constaté approche cette fois le débit maximal effectif de la plateforme (2444~MP/s), ce qui ne laisse pas envisager la possibilité d'obtenir de nouveau un gain de cet ordre par d'autres techniques. Toutefois, notre algorithme appartient à la classe des solutions de filtrage médian par tri (incomplet) des valeurs et à ce titre, ses temps d'exécution sont fortement dépendants de la taille de la fenêtre de filtrage, comme le montrent les diagrammes de la figure \ref{fig-median-comp}. Il n'est donc pertinent que pour les petites tailles de fenêtre, les plus communément employées en traitement d'image.
234 Dans les grandes tailles de fenêtres, la plupart des solutions adoptent des méthodes approchées de détermination de la médiane. Les principes que nos avons appliqués peuvent alors apporter des gains de performance, ainsi qu'à beaucoup d'autres méthodes de calcul comme les filtres de convolution abordés dans le chapitre suivant.
236 Enfin la conclusion du prochain chapitre présentera l'outil en ligne que nous proposons et qui permet de générer les codes des kernels médians et de convolution.