Android Commencer


Exemple

RenderScript est un framework permettant des calculs parallèles hautes performances sur Android. Les scripts que vous écrivez seront exécutés en parallèle sur tous les processeurs disponibles (par ex. CPU, GPU, etc.), ce qui vous permettra de vous concentrer sur la tâche que vous souhaitez réaliser au lieu de la planifier et de l’exécuter.

Les scripts sont écrits dans un langage C99 (C99 étant une ancienne version du standard du langage de programmation C). Pour chaque script, une classe Java est créée qui vous permet d'interagir facilement avec RenderScript dans votre code Java.

Mise en place de votre projet

Il existe deux manières différentes d’accéder à RenderScript dans votre application, avec les bibliothèques Android Framework ou la bibliothèque de support. Même si vous ne souhaitez pas cibler les périphériques avant le niveau 11 de l'API, vous devez toujours utiliser l'implémentation de la bibliothèque de support car elle garantit la compatibilité des périphériques sur de nombreux périphériques différents. Pour utiliser l'implémentation de la bibliothèque de support, vous devez utiliser au moins les outils de construction version 18.1.0 !

Configurez maintenant le fichier build.gradle de votre application:

android {
    compileSdkVersion 24
    buildToolsVersion '24.0.1'

    defaultConfig {
        minSdkVersion 8
        targetSdkVersion 24

        renderscriptTargetApi 18
        renderscriptSupportModeEnabled true
    }
}
  • renderscriptTargetApi : Cela doit être défini sur le niveau de l'API le plus ancien de la version qui fournit toutes les fonctionnalités RenderScript dont vous avez besoin.
  • renderscriptSupportModeEnabled : active l'utilisation de l'implémentation RenderScript de la bibliothèque de support.

Comment fonctionne RenderScript

Un script Render typique se compose de deux éléments: les noyaux et les fonctions. Une fonction est exactement ce que cela ressemble - elle accepte une entrée, fait quelque chose avec cette entrée et renvoie une sortie. Un noyau est l'endroit d'où vient la puissance réelle de RenderScript.

Un noyau est une fonction exécutée sur chaque élément d'une Allocation . Une Allocation peut être utilisée pour transmettre des données comme un Bitmap ou un tableau d' byte à un RenderScript et elles sont également utilisées pour obtenir un résultat d'un noyau. Les noyaux peuvent soit prendre une Allocation en entrée et une autre en sortie, soit modifier les données dans une seule Allocation .

Vous pouvez écrire vos noyaux, mais il existe également de nombreux noyaux prédéfinis que vous pouvez utiliser pour effectuer des opérations courantes telles qu'un flou d'image gaussien.

Comme déjà mentionné pour chaque fichier RenderScript, une classe est générée pour interagir avec elle. Ces classes commencent toujours par le préfixe ScriptC_ suivi du nom du fichier RenderScript. Par exemple, si votre fichier RenderScript est appelé example la classe Java générée s'appellera ScriptC_example . Tous les scripts prédéfinis commencent par le préfixe Script - par exemple, le script gaussien de l'image est appelé ScriptIntrinsicBlur .

Écrire votre premier script RenderScript

L'exemple suivant est basé sur un exemple sur GitHub. Il effectue une manipulation d'image de base en modifiant la saturation d'une image. Vous pouvez trouver le code source ici et le vérifier si vous voulez jouer avec lui-même. Voici un aperçu rapide de ce à quoi le résultat est censé ressembler:

image démo

RenderScript Boilerplate

Les fichiers RenderScript résident dans le dossier src/main/rs de votre projet. Chaque fichier a l'extension de fichier .rs et doit contenir deux instructions #pragma en haut:

#pragma version(1)
#pragma rs java_package_name(your.package.name)
  • #pragma version(1) : Ceci peut être utilisé pour définir la version de RenderScript que vous utilisez. Actuellement, il n'y a que la version 1.

  • #pragma rs java_package_name(your.package.name) : Ceci peut être utilisé pour définir le nom du paquet de la classe Java générée pour interagir avec ce RenderScript particulier.

Il y a un autre #pragma vous devriez normalement définir dans chacun de vos fichiers RenderScript et il est utilisé pour définir la précision en virgule flottante. Vous pouvez définir la précision en virgule flottante sur trois niveaux différents:

  • #pragma rs_fp_full : C'est le paramètre le plus strict avec la plus grande précision et c'est aussi la valeur par défaut si rien n'est spécifié. Vous devriez l'utiliser si vous avez besoin d'une précision élevée en virgule flottante.
  • #pragma rs_fp_relaxed : Ceci garantit non seulement une précision en virgule flottante aussi élevée, mais sur certaines architectures cela permet un tas d'optimisations qui peuvent accélérer l'exécution de vos scripts.
  • #pragma rs_fp_imprecise : Ceci garantit encore moins de précision et devrait être utilisé si la précision en virgule flottante n'a pas vraiment d'importance pour votre script.

La plupart des scripts peuvent simplement utiliser #pragma rs_fp_relaxed sauf si vous avez vraiment besoin d'une précision élevée en virgule flottante.

Variables globales

Maintenant, tout comme en code C, vous pouvez définir des variables globales ou des constantes:

const static float3 gMonoMult = {0.299f, 0.587f, 0.114f};

float saturationLevel = 0.0f;

La variable gMonoMult est de type float3 . Cela signifie que c'est un vecteur composé de 3 nombres flottants. L'autre variable float appelée saturationValue n'est pas constante, vous pouvez donc la définir à l'exécution à une valeur qui vous plait. Vous pouvez utiliser des variables comme celle-ci dans vos noyaux ou fonctions et elles constituent donc un autre moyen de fournir des entrées ou des sorties à vos scripts Render. Pour chaque variable non constante, une méthode getter et setter sera générée sur la classe Java associée.

Graines

Mais maintenant, commençons à implémenter le noyau. Pour les besoins de cet exemple, je ne vais pas expliquer les calculs utilisés dans le noyau pour modifier la saturation de l'image, mais plutôt comment implémenter un noyau et comment l'utiliser. À la fin de ce chapitre, j'expliquerai rapidement ce que fait le code dans ce noyau.

Noyaux en général

Regardons d'abord le code source:

uchar4 __attribute__((kernel)) saturation(uchar4 in) {
    float4 f4 = rsUnpackColor8888(in);
    float3 dotVector = dot(f4.rgb, gMonoMult);
    float3 newColor = mix(dotVector, f4.rgb, saturationLevel);
    return rsPackColorTo8888(newColor);
}

Comme vous pouvez le voir, cela ressemble à une fonction C normale avec une exception: le __attribute__((kernel)) entre le type de retour et le nom de la méthode. C'est ce qui dit à RenderScript que cette méthode est un noyau. Une autre chose que vous pouvez remarquer est que cette méthode accepte un paramètre uchar4 et renvoie une autre valeur uchar4 . uchar4 est - comme la variable float3 nous avons parlé dans le chapitre précédent - un vecteur. Il contient 4 valeurs uchar qui ne sont que des valeurs d'octet comprises entre 0 et 255.

Vous pouvez accéder à ces valeurs individuelles de différentes manières, par exemple, in.r renverrait l'octet qui correspond au canal rouge d'un pixel. Nous utilisons un uchar4 puisque chaque pixel est composé de 4 valeurs - r pour le rouge, g pour le vert, b pour le bleu et a pour alpha - et vous pouvez y accéder avec ce raccourci. RenderScript vous permet également de prendre n'importe quel nombre de valeurs d'un vecteur et de créer un autre vecteur avec elles. Par exemple, in.rgb renverrait une valeur uchar3 contenant uniquement les parties rouge, verte et bleue du pixel sans la valeur alpha.

A l'exécution, RenderScript appellera cette méthode du noyau pour chaque pixel d'une image, ce qui explique pourquoi la valeur de retour et le paramètre ne représentent qu'une seule valeur uchar4 . RenderScript exécutera plusieurs de ces appels en parallèle sur tous les processeurs disponibles, raison pour laquelle RenderScript est si puissant. Cela signifie également que vous n'avez pas à vous soucier des threads ou de la sécurité des threads, vous pouvez simplement implémenter ce que vous voulez faire pour chaque pixel et RenderScript se charge du reste.

Lorsque vous appelez un noyau en Java, vous devez fournir deux variables d’ Allocation , l’une qui contient les données en entrée et l’autre qui reçoit la sortie. Votre méthode du noyau sera appelée pour chaque valeur de l’ Allocation entrée et écrira le résultat à l’ Allocation sortie.

Méthodes de RenderScript Runtime API

Dans le noyau ci-dessus quelques méthodes sont utilisées qui sont fournies hors de la boîte. RenderScript fournit beaucoup de ces méthodes et elles sont essentielles pour presque tout ce que vous allez faire avec RenderScript. Parmi celles-ci, il y a des méthodes pour effectuer des opérations mathématiques comme sin() et des méthodes auxiliaires comme mix() qui mélange deux valeurs selon d'autres valeurs. Mais il existe également des méthodes pour des opérations plus complexes avec des vecteurs, des quaternions et des matrices.

La référence officielle de RenderScript Runtime API est la meilleure ressource disponible si vous souhaitez en savoir plus sur une méthode particulière ou si vous recherchez une méthode spécifique qui effectue une opération commune telle que le calcul du produit scalaire d'une matrice. Vous pouvez trouver cette documentation ici .

Implémentation du noyau

Voyons maintenant les spécificités de ce que fait le noyau. Voici la première ligne du noyau:

float4 f4 = rsUnpackColor8888(in);

La première ligne appelle la méthode rsUnpackColor8888() qui transforme la valeur uchar4 valeur float4 . Chaque canal de couleur est également transformé dans la plage 0.0f - 1.0f0.0f correspond à une valeur d'octet de 0 et 1.0f à 255 . Le but principal de cette opération est de simplifier tous les calculs dans ce noyau.

float3 dotVector = dot(f4.rgb, gMonoMult);

Cette ligne suivante utilise la méthode intégrée dot() pour calculer le produit scalaire de deux vecteurs. gMonoMult est une valeur constante que nous avons définie ci-dessus. Puisque les deux vecteurs doivent avoir la même longueur pour calculer le produit scalaire et aussi parce que nous voulons simplement affecter les canaux de couleur et non le canal alpha d’un pixel, nous utilisons le raccourci .rgb pour obtenir un nouveau vecteur float3 canaux de couleur rouge, vert et bleu. Ceux d'entre nous qui se souviennent encore de la façon dont fonctionne le produit scalaire remarqueront rapidement que le produit scalaire ne devrait renvoyer qu'une valeur et non un vecteur. Pourtant, dans le code ci-dessus, nous float3 le résultat à un vecteur float3 . Ceci est encore une fonctionnalité de RenderScript. Lorsque vous attribuez un numéro à une dimension à un vecteur, tous les éléments du vecteur seront définis sur cette valeur. Par exemple, l'extrait suivant attribue 2.0f à chacune des trois valeurs du vecteur float3 :

float3 example = 2.0f;

Ainsi, le résultat du produit scalaire ci-dessus est attribué à chaque élément du vecteur float3 ci-dessus.

Maintenant vient la partie dans laquelle nous utilisons effectivement la variable globale saturationLevel pour modifier la saturation de l'image:

float3 newColor = mix(dotVector, f4.rgb, saturationLevel);

Cela utilise la méthode intégrée mix() pour mélanger la couleur originale avec le vecteur de produit scalaire que nous avons créé ci-dessus. La façon dont ils sont mélangés est déterminée par la variable globale saturationLevel . Ainsi, un saturationLevel de 0.0f ne donnera aucune partie des valeurs de couleur d'origine à la couleur résultante et consistera uniquement en des valeurs dans le dotVector ce qui dotVector une image en noir et blanc ou grisée. Une valeur de 1.0f fera que la couleur résultante sera complètement composée des valeurs de couleur d'origine et que les valeurs supérieures à 1.0f multiplieront les couleurs d'origine pour les rendre plus lumineuses et plus intenses.

return rsPackColorTo8888(newColor);

C'est la dernière partie du noyau. rsPackColorTo8888() transforme le vecteur float3 en une valeur uchar4 qui est ensuite renvoyée. Les valeurs d'octet résultantes sont bloquées dans une plage comprise entre 0 et 255; les valeurs flottantes supérieures à 1.0f entraînent une valeur d'octet de 255 et des valeurs inférieures à 0.0 entraînent une valeur d'octet de 0 .

Et c'est toute l'implémentation du noyau. Il ne reste qu'une partie: comment appeler un noyau en Java.

Appeler RenderScript en Java

Les bases

Comme cela a déjà été mentionné ci-dessus pour chaque fichier RenderScript, une classe Java est générée qui vous permet d'interagir avec vos scripts. Ces fichiers ont le préfixe ScriptC_ suivi du nom du fichier RenderScript. Pour créer une instance de ces classes, vous devez d’abord disposer d’une instance de la classe RenderScript :

final RenderScript renderScript = RenderScript.create(context);

La méthode statique create() peut être utilisée pour créer une occurrence RenderScript partir d'un Context . Vous pouvez ensuite instancier la classe Java générée pour votre script. Si vous appelez le fichier RenderScript saturation.rs alors la classe s'appellera ScriptC_saturation :

final ScriptC_saturation script = new ScriptC_saturation(renderScript);

Sur cette classe, vous pouvez maintenant définir le niveau de saturation et appeler le noyau. Le setter qui a été généré pour la variable saturationLevel aura le préfixe set_ suivi du nom de la variable:

script.set_saturationLevel(1.0f);

Il y a aussi un getter préfixé par get_ qui vous permet d'obtenir le niveau de saturation actuellement défini:

float saturationLevel = script.get_saturationLevel();

Les noyaux que vous définissez dans votre script RenderScript sont préfixés par forEach_ suivi du nom de la méthode du noyau. Le noyau que nous avons écrit attend une Allocation entrée et une Allocation sortie comme paramètres:

script.forEach_saturation(inputAllocation, outputAllocation);

L' Allocation entrée doit contenir l'image d'entrée et, une fois la méthode forEach_saturation terminée, l'allocation de sortie contiendra les données d'image modifiées.

Une fois que vous avez une instance Allocation , vous pouvez copier des données depuis et vers ces Allocations en utilisant les méthodes copyFrom() et copyTo() . Par exemple, vous pouvez copier une nouvelle image dans votre entrée `Allocation en appelant:

inputAllocation.copyFrom(inputBitmap);

De la même façon, vous pouvez récupérer l'image de résultat en appelant copyTo() sur la sortie Allocation :

outputAllocation.copyTo(outputBitmap);

Création d'instances d'allocation

Il existe plusieurs façons de créer une Allocation . Une fois que vous avez une instance Allocation , vous pouvez copier les nouvelles données depuis et vers ces Allocations avec copyTo() et copyFrom() comme expliqué ci-dessus, mais pour les créer initialement, vous devez savoir avec quel type de données vous travaillez. Commençons par l' Allocation entrée:

Nous pouvons utiliser la méthode statique createFromBitmap() pour créer rapidement notre Allocation entrée à partir d'un Bitmap :

final Allocation inputAllocation = Allocation.createFromBitmap(renderScript, image);

Dans cet exemple, l'image d'entrée ne change jamais, nous n'avons donc jamais besoin de modifier l' Allocation entrée. Nous pouvons le réutiliser à chaque fois que saturationLevel change pour créer un nouveau Bitmap sortie.

Création de la sortie L' Allocation est un peu plus complexe. Nous devons d'abord créer ce qu'on appelle un Type . Un Type est utilisé pour indiquer à une Allocation avec quel type de données il a affaire. Généralement, on utilise la classe Type.Builder pour créer rapidement un Type approprié. Regardons d'abord le code:

final Type outputType = new Type.Builder(renderScript, Element.RGBA_8888(renderScript))
        .setX(inputBitmap.getWidth())
        .setY(inputBitmap.getHeight())
        .create();

Nous travaillons avec un Bitmap normal de 32 bits (ou 4 octets) par pixel avec 4 canaux de couleur. C'est pourquoi nous choisissons Element.RGBA_8888 pour créer le Type . Ensuite, nous utilisons les méthodes setX() et setY() pour définir la largeur et la hauteur de l'image de sortie sur la même taille que l'image d'entrée. La méthode create() crée alors le Type avec les paramètres que nous avons spécifiés.

Une fois que nous avons le bon Type nous pouvons créer la sortie Allocation avec la méthode statique createTyped() :

final Allocation outputAllocation = Allocation.createTyped(renderScript, outputType);

Maintenant, nous avons presque terminé. Nous avons également besoin d'un Bitmap sortie dans lequel nous pouvons copier les données de l' Allocation sortie. Pour ce faire, nous utilisons la méthode statique createBitmap() pour créer un nouveau Bitmap vide ayant la même taille et la même configuration que le Bitmap entrée.

final Bitmap outputBitmap = Bitmap.createBitmap(
        inputBitmap.getWidth(),
        inputBitmap.getHeight(),
        inputBitmap.getConfig()
);

Et avec cela, nous avons toutes les pièces du puzzle pour exécuter notre script Render.

Exemple complet

Maintenant, mettons tout cela ensemble dans un exemple:

// Create the RenderScript instance
final RenderScript renderScript = RenderScript.create(context);

// Create the input Allocation 
final Allocation inputAllocation = Allocation.createFromBitmap(renderScript, inputBitmap);

// Create the output Type.
final Type outputType = new Type.Builder(renderScript, Element.RGBA_8888(renderScript))
        .setX(inputBitmap.getWidth())
        .setY(inputBitmap.getHeight())
        .create();

// And use the Type to create am output Allocation
final Allocation outputAllocation = Allocation.createTyped(renderScript, outputType);

// Create an empty output Bitmap from the input Bitmap
final Bitmap outputBitmap = Bitmap.createBitmap(
        inputBitmap.getWidth(),
        inputBitmap.getHeight(),
        inputBitmap.getConfig()
);

// Create an instance of our script
final ScriptC_saturation script = new ScriptC_saturation(renderScript);

// Set the saturation level
script.set_saturationLevel(2.0f);

// Execute the Kernel
script.forEach_saturation(inputAllocation, outputAllocation);

// Copy the result data to the output Bitmap
outputAllocation.copyTo(outputBitmap);

// Display the result Bitmap somewhere
someImageView.setImageBitmap(outputBitmap);

Conclusion

Avec cette introduction, vous devriez être prêt à écrire vos propres noyaux RenderScript pour une manipulation simple des images. Cependant, il y a quelques choses à garder à l'esprit:

  • RenderScript ne fonctionne que dans les projets d'application : Actuellement, les fichiers RenderScript ne peuvent pas faire partie d'un projet de bibliothèque.
  • Attention à la mémoire : RenderScript est très rapide, mais il peut également nécessiter beaucoup de mémoire. Il ne devrait jamais y avoir plus d'une instance de RenderScript à tout moment. Vous devriez également réutiliser autant que possible. Normalement, il vous suffit de créer vos instances d' Allocation une fois et de les réutiliser ultérieurement. Il en va de même pour les Bitmaps sortie ou vos instances de script. Réutiliser autant que possible.
  • Faites votre travail en arrière-plan : à nouveau, RenderScript est très rapide, mais pas instantané. Tout noyau, en particulier les noyaux complexes, devrait être exécuté à partir du thread d'interface utilisateur dans un AsyncTask ou quelque chose de similaire. Cependant, la plupart du temps, vous n'avez pas à vous soucier des fuites de mémoire. Toutes les classes liées à RenderScript utilisent uniquement le Context application et ne provoquent donc pas de fuite de mémoire. Mais vous devez toujours vous inquiéter des choses habituelles comme la fuite de View , Activity ou de toute instance de Context que vous utilisez vous-même!
  • Utilisez des éléments intégrés : il existe de nombreux scripts prédéfinis qui effectuent des tâches telles que le flou, la fusion, la conversion et le redimensionnement des images. Et il y a beaucoup d'autres méthodes intégrées qui vous aident à implémenter vos noyaux. Les chances sont que si vous voulez faire quelque chose, il existe soit un script ou une méthode qui fait déjà ce que vous essayez de faire. Ne réinventez pas la roue.

Si vous voulez rapidement commencer à jouer avec du code, je vous recommande de regarder l'exemple de projet GitHub qui implémente l'exemple exact dont il est question dans ce tutoriel. Vous pouvez trouver le projet ici . Amusez-vous avec RenderScript!