[PYTHON] Créez une application de reconnaissance d'image qui discrimine les nombres écrits à l'écran avec Android (PyTorch Mobile) [implémentation Android]

Application à créer cette fois

Créez une application de reconnaissance d'image qui reconnaît les nombres écrits sur l'écran avec Pytorch Mobile et kotlin. ** Créez toutes les fonctions du modèle et d'Android pour la reconnaissance d'image à partir de zéro. ** ** Il sera divisé en deux parties, ** Création de modèle (Python) ** et ** Implémentation Android (kotlin) **.

Ce projet de studio Android Github: https://github.com/SY-BETA/NumberRecognitionApp/tree/master

Si vous n'avez pas encore créé de modèle avec python, [Créez une application de reconnaissance d'image qui détermine les nombres écrits à l'écran avec Android (PyTorch Mobile) [Création de réseau]](https://qiita.com/YS-BETA/items / 077b5b8d3163fb7de800) Veuillez le faire. Ou si vous êtes un ingénieur Android qui ne possède pas d'environnement python ou si vous êtes fatigué de créer des modèles, nous avons répertorié des modèles formés. Téléchargez le modèle entraîné depuis Github: https://github.com/SY-BETA/CNN_PyTorch/blob/master/CNNModel.pt.

Que faire cette fois, cette ↓

Flux de création

  1. Téléchargez MNIST (* Il est nécessaire de changer le nombre de canaux à 3 canaux)
  2. Créez un modèle CNN simple avec python (PyTorch)
  3. Former le modèle
  4. Enregistrez le modèle
  5. Implémentation de la possibilité de dessiner des images sur Android
  6. Implémentez le modèle sur Android pour la propagation vers l'avant

Que faire à ce moment

Faire 5 et 6 Maintenant que le modèle a été créé, nous pourrons l'inférer sur Android en utilisant pytorch mobile et implémenter la possibilité d'écrire des nombres à l'écran.

Dépendances

Ajout de ce qui suit à gradle (à compter du 25 janvier 2020)

dependencies {
    implementation 'org.pytorch:pytorch_android:1.4.0'
    implementation 'org.pytorch:pytorch_android_torchvision:1.4.0'
}

Créer une mise en page

Définir surfaceView pour écrire des caractères キャプチcvxbxャ.PNG

fichier xml ↓

activity_main.xml


<androidx.constraintlayout.widget.ConstraintLayout 
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <FrameLayout
        android:id="@+id/frameLayout"
        android:layout_width="230dp"
        android:layout_height="230dp"
        android:layout_marginStart="24dp"
        android:layout_marginTop="24dp"
        android:layout_marginEnd="24dp"
        android:layout_marginBottom="24dp"
        android:background="@android:color/darker_gray"
        app:layout_constraintBottom_toTopOf="@+id/sampleImg"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/text1">

        <SurfaceView
            android:id="@+id/surfaceView"
            android:layout_width="match_parent"
            android:layout_height="match_parent" />
    </FrameLayout>

    <Button
        android:id="@+id/resetBtn"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="réinitialiser"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toStartOf="@+id/inferBtn"
        app:layout_constraintHorizontal_bias="0.5"
        app:layout_constraintStart_toStartOf="parent" />

    <Button
        android:id="@+id/inferBtn"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="inférence"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintHorizontal_bias="0.5"
        app:layout_constraintStart_toEndOf="@+id/resetBtn" />

    <TextView
        android:id="@+id/text1"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginStart="16dp"
        android:layout_marginTop="24dp"
        android:text="Le nombre écrit est"
        android:textSize="40sp"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <TextView
        android:id="@+id/resultNum"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginStart="8dp"
        android:text="?"
        android:textAppearance="@style/TextAppearance.AppCompat.Body2"
        android:textColor="@color/colorAccent"
        android:textSize="55sp"
        app:layout_constraintBottom_toBottomOf="@+id/text1"
        app:layout_constraintStart_toEndOf="@+id/text1"
        app:layout_constraintTop_toTopOf="@+id/text1" />

    <ImageView
        android:id="@+id/sampleImg"
        android:layout_width="100dp"
        android:layout_height="100dp"
        app:layout_constraintBottom_toTopOf="@+id/resetBtn"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:srcCompat="@mipmap/ic_launcher_round" />

    <TextView
        android:id="@+id/textView"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Après le redimensionnement 28 × 28 ↓"
        app:layout_constraintBottom_toTopOf="@+id/sampleImg"
        app:layout_constraintEnd_toEndOf="@+id/sampleImg"
        app:layout_constraintStart_toStartOf="@+id/sampleImg" />
</androidx.constraintlayout.widget.ConstraintLayout>

Créer un CustomSurfaceView

Utilisez surfaceView pour dessiner. Pour cela, créez une classe qui hérite de «SurfaceView» et «SurfaceHolder.Callback» et contrôle «surfaceView». MNIST, qui sont les données entraînées du modèle, était une ligne blanche sur fond noir, donc je pourrai dessiner avec cette couleur.

constructeur

Une variable qui contient divers états. Copiez et collez correctement ok

DrawSurfaceView.kt


class DrawSurfaceView : SurfaceView, SurfaceHolder.Callback {

    private var surfaceHolder: SurfaceHolder? = null
    private var paint: Paint? = null
    private var path: Path? = null
    var color: Int? = null
    var prevBitmap: Bitmap? = null  /**Bitmap pour contenir l'image écrite**/
    private var prevCanvas: Canvas? = null
    private var canvas: Canvas? = null

    var width: Int? = null
    var height: Int? = null

    constructor(context: Context, surfaceView: SurfaceView, surfaceWidth: Int, surfaceHeight: Int) : super(context) {
        // surfaceHolder
        surfaceHolder = surfaceView.holder

        ///taille de la surface
        width = surfaceWidth
        height = surfaceHeight

        ///Rappeler
        surfaceHolder!!.addCallback(this)

        ///Paramètres de peinture
        paint = Paint()
        color = Color.WHITE  //Écrivez avec une ligne blanche
        paint!!.color = color as Int
        paint!!.style = Paint.Style.STROKE
        paint!!.strokeCap = Paint.Cap.ROUND
        paint!!.isAntiAlias = false
        paint!!.strokeWidth = 50F
    }
}

Assurez-vous d'inclure la largeur et la hauteur du fichier de disposition surfaceView lors de la création de cette instance avec MainActivity.

Classe de données

Créez une classe de données qui enregistre le chemin et la couleur lors du dessin.

DrawSurfaceView.kt


    ////Enregistre les informations de classe de chemin et les informations de couleur pour ce chemin
    data class pathInfo(
        var path: Path,
        var color: Int
    )

Implémentation d'interface et méthodes d'initialisation

Créer une méthode pour initialiser le canevas et le bitmap avec l'implémentation

DrawSurfaceView.kt


override fun surfaceCreated(holder: SurfaceHolder?) {
        /// bitmap,initialisation du canevas
        initializeBitmap()
    }

    override fun surfaceChanged(holder: SurfaceHolder?, format: Int, width: Int, height: Int) {
    }

    override fun surfaceDestroyed(holder: SurfaceHolder?) {
        ///Recycler le bitmap(Prévention des fuites de mémoire)
        prevBitmap!!.recycle()
    }

    ///Initialisation du bitmap et du canevas
    private fun initializeBitmap() {
        if (prevBitmap == null) {
            prevBitmap = Bitmap.createBitmap(width!!, height!!, Bitmap.Config.ARGB_8888)
        }

        if (prevCanvas == null) {
            prevCanvas = Canvas(prevBitmap!!)
        }
        //Sur fond noir
        prevCanvas!!.drawColor(Color.BLACK)
    }

Cette fois, Bitmap se recycle lorsque surfaceView est détruite. Si vous laissez le bitmap tel quel, il y a un risque de fuite de mémoire, donc recyclez-le lorsqu'il n'est plus utilisé.

Méthode de dessin

Créer une fonction pour dessiner sur le campus

DrawSurfaceView.kt


 /////Fonction pour dessiner
    private fun draw(pathInfo: pathInfo) {
        ///Verrouiller et obtenir une toile
        canvas = Canvas()
        canvas = surfaceHolder!!.lockCanvas()

        ////Toile transparente
        canvas!!.drawColor(0, PorterDuff.Mode.CLEAR)

        ///Dessinez le bitmap précédent sur le canevas
        canvas!!.drawBitmap(prevBitmap!!, 0F, 0F, null)

        ////Tracer le chemin
        paint!!.color = pathInfo.color
        canvas!!.drawPath(pathInfo.path, paint!!)

        ///Ouvrir
        surfaceHolder!!.unlockCanvasAndPost(canvas)
    }

    ///Appelez une fonction pour chaque action lorsque vous touchez l'écran
    fun onTouch(event: MotionEvent): Boolean {
        when (event.action) {
            MotionEvent.ACTION_DOWN -> touchDown(event.x, event.y)
            MotionEvent.ACTION_MOVE -> touchMove(event.x, event.y)
            MotionEvent.ACTION_UP -> touchUp(event.x, event.y)
        }
        return true
    }

    /////Contient le point à dessiner dans la classe de chemin
    ///    ACTION_Traitement au moment de DOWN
    private fun touchDown(x: Float, y: Float) {
        path = Path()
        path!!.moveTo(x, y)
    }

    ///    ACTION_Traitement au moment de MOVE
    private fun touchMove(x: Float, y: Float) {
        path!!.lineTo(x, y)
        draw(pathInfo(path!!, color!!))
    }

    ///    ACTION_Traitement au moment de UP
    private fun touchUp(x: Float, y: Float) {
        path!!.lineTo(x, y)
        draw(pathInfo(path!!, color!!))
        prevCanvas!!.drawPath(path!!, paint!!)
    }

Fonction de réinitialisation de la toile

Une méthode pour initialiser le bitmap dessiné

DrawSurfaceView.kt


    ///méthode de réinitialisation
    fun reset() {
        ///Initialisation et toile effacées
        initializeBitmap()
        canvas = surfaceHolder!!.lockCanvas()
        canvas?.drawColor(0, PorterDuff.Mode.CLEAR)
        surfaceHolder!!.unlockCanvasAndPost(canvas)
    }

Ceci termine DrawSurfaceView. Si vous implémentez ceci avec MainActivity.kt, vous pouvez implémenter la fonction pour dessiner une image.

Implémenter le DrawSurfaceView.kt créé

Obtenez la taille de drawSurfaceView de la disposition, créez une instance de DrawSurfaceView et implémentez-la. En outre, la méthode du bouton de réinitialisation peut être appelée.

MainActivity.kt


class MainActivity : AppCompatActivity() {

    var surfaceViewWidth: Int? = null
    var surfaceViewHeight: Int? = null
    var drawSurfaceView:DrawSurfaceView? = null

    ///Fonction d'extension
    //Obtenir la taille de surfaceView après la création de la vue à l'aide de ViewTreeObserver
    private inline fun <T : View> T.afterMeasure(crossinline f: T.() -> Unit) {
        viewTreeObserver.addOnGlobalLayoutListener(object :
            ViewTreeObserver.OnGlobalLayoutListener {
            override fun onGlobalLayout() {
                if (width > 0 && height > 0) {
                    viewTreeObserver.removeOnGlobalLayoutListener(this)
                    f()
                }
            }
        })
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        
        ///Utiliser ViewTreeObserber
        ///Obtenir la taille de surfaceView après la génération de surfaceView
        surfaceView.afterMeasure {
            surfaceViewWidth = surfaceView.width
            surfaceViewHeight = surfaceView.height
            ////DrawrSurfaceView et instanciation
            drawSurfaceView = DrawSurfaceView(
                applicationContext,
                surfaceView,
                surfaceViewWidth!!,
                surfaceViewHeight!!
            )
            ///Ensemble d'auditeurs
            surfaceView.setOnTouchListener { v, event -> drawSurfaceView!!.onTouch(event) }
        }

        ///Bouton de réinitialisation
        resetBtn.setOnClickListener {
            drawSurfaceView!!.reset()   ///Appelez la méthode d'initialisation bitmap
            sampleImg.setImageResource(R.color.colorPrimaryDark)
            resultNum.text = "?"
        }
    }
}

Si vous pouvez le faire bien, vous devriez pouvoir dessiner sur l'écran.

Si quelque chose ne va pas, copiez et collez tout depuis Github. Github: https://github.com/SY-BETA/NumberRecognitionApp/tree/master

J'utiliserai enfin PyTorch Mobile à partir du prochain.

Implémentation de la reconnaissance d'image avec PyTorch Mobile

Charger le modèle entraîné

Créez un dossier de ressources dans votre projet. (Vous pouvez le faire en cliquant avec le bouton droit sur l'application sur le côté gauche de l'interface utilisateur-> Nouveau-> Dossier-> dossier des actifs) Créez une application de reconnaissance d'image qui discrimine les nombres écrits sur l'écran avec android (PyTorch Mobile) [Créer un réseau] ou ajoutez le modèle appris téléchargé au début.

Rendez possible l'obtention du chemin à partir de ce dossier d'actifs. Ajoutez ce qui suit à ʻonCreate dans MainActivity.kt`.

MainActivity.kt


////Fonction pour obtenir le chemin du fichier d'actif
        fun assetFilePath(context: Context, assetName: String): String {
            val file = File(context.filesDir, assetName)
            if (file.exists() && file.length() > 0) {
                return file.absolutePath
            }
            context.assets.open(assetName).use { inputStream ->
                FileOutputStream(file).use { outputStream ->
                    val buffer = ByteArray(4 * 1024)
                    var read: Int
                    while (inputStream.read(buffer).also { read = it } != -1) {
                        outputStream.write(buffer, 0, read)
                    }
                    outputStream.flush()
                }
                return file.absolutePath
            }
        }

        ///Charger le modèle entraîné
        val module = Module.load(assetFilePath(this, "CNNModel.pt"))

Notez que le chargement d'images et de modèles à partir du dossier des actifs peut être assez fastidieux.

inférence

Effectuez une propagation vers l'avant lorsque le bouton d'inférence est enfoncé sur le modèle entraîné chargé. De plus, le résultat est acquis et affiché. Ajoutez ce qui suit à ʻonCreate dans MainActivity.kt`.

MainActivity.kt


         //Cliquez sur le bouton d'inférence
        inferBtn.setOnClickListener {
            //Image dessinée(Obtenir un bitmap)
            val bitmap = drawSurfaceView!!.prevBitmap!!
            //Redimensionner à la taille d'entrée du modèle formé créé
            val bitmapResized    = Bitmap.createScaledBitmap(bitmap,28, 28, true)

            ///Conversion et standardisation Tensol
            val inputTensor = TensorImageUtils.bitmapToFloat32Tensor(
                bitmapResized,
                TensorImageUtils.TORCHVISION_NORM_MEAN_RGB, TensorImageUtils.TORCHVISION_NORM_STD_RGB
            )

            ///Inférence et ses conséquences
            ///Propagation vers l'avant
            val outputTensor = module.forward(IValue.from(inputTensor)).toTensor()
            val scores = outputTensor.dataAsFloatArray

            //Afficher l'image redimensionnée
            sampleImg.setImageBitmap(bitmapResized)

            ///Variable pour stocker le score
            //Indice de score MAX=Chiffres prédits par la reconnaissance d'image(De la façon de faire un modèle)
            var maxScore: Float = 0F
            var maxScoreIdx = -1
            for (i in scores.indices) {
                Log.d("scores", scores[i].toString()) //Liste des scores de sortie à enregistrer(C'est intéressant de voir quel nombre est proche)
                if (scores[i] > maxScore) {
                    maxScore = scores[i]
                    maxScoreIdx = i
                }
            }

            //Afficher les résultats de l'inférence
            resultNum.text = "$maxScoreIdx"
        }

ʻLa taille de inputTensor` est ** (1, 3, 28, 28) ** Il est nécessaire de créer un modèle pour que cette taille soit l'entrée.

Si vous pouvez le faire, vous devriez avoir la première application! !! Écrivez des nombres, faites des prédictions et jouez avec eux.

fin

Dans l'ensemble, j'ai eu du mal à changer le nombre de canaux lors de la création du réseau et de l'ajustement de la taille d'entrée du réseau. Étant donné que l'implémentation sur Android n'est que propagation en avant, j'ai pensé qu'elle changerait selon que l'on peut créer ou non un réseau. De plus, PyTorch Mobile vient de sortir, mais j'ai été surpris qu'il ait été mis à niveau en environ deux semaines.

C'est amusant de pouvoir reconnaître les chiffres écrits à l'écran. Cette fois, c'était un numéro manuscrit dans MNIST, mais cela me semble intéressant si je laisse d'autres choses comme le transfert d'apprentissage.

Ce code est sur Github. Github: https://github.com/SY-BETA/NumberRecognitionApp/tree/master

Modèle CNN formé Github: https://github.com/SY-BETA/CNN_PyTorch/blob/master/CNNModel.pt

Créez une application de reconnaissance d'image qui discrimine les nombres écrits sur l'écran avec Android (PyTorch Mobile) [Création de réseau]

Recommended Posts

Créez une application de reconnaissance d'image qui discrimine les nombres écrits à l'écran avec Android (PyTorch Mobile) [implémentation Android]
Créez une application qui reconnaît les images en écrivant des chiffres à l'écran sur Android (PyTorch Mobile) [création de réseau CNN]
[kotlin] Trier les images sur Android (Pytorch Mobile)
À propos de l'itinéraire le plus court pour créer un modèle de reconnaissance d'image par apprentissage automatique et mettre en œuvre une application Android
[kotlin] Créez une application de reconnaissance d'images en temps réel sur Android
Créer une application à l'aide de l'API Spotify
[kotlin] Créez une application qui reconnaît les photos prises avec un appareil photo sur Android