[PYTHON] [kotlin] Créez une application de reconnaissance d'images en temps réel sur Android

Que faire cette fois

Créez une application qui reconnaît l'image (image) capturée par la caméra sur Android en temps réel. Exécutez le modèle entraîné sur Android à l'aide de PyTorch Mobile.

Ce ↓

L'exemple d'application que j'ai créé est répertorié en bas, veuillez donc y jeter un œil si vous le souhaitez.

Dépendances

Tout d'abord, ajoutez des dépendances (à partir de février 2020) appareil photo x et pytorch mobile

build.gradle


  def camerax_version = '1.0.0-alpha06'
    implementation "androidx.camera:camera-core:${camerax_version}"
    implementation "androidx.camera:camera-camera2:${camerax_version}"
    implementation 'org.pytorch:pytorch_android:1.4.0'
    implementation 'org.pytorch:pytorch_android_torchvision:1.4.0'

Ajoutez ce qui suit à la fin du ** android {} ** supérieur

build.gradle


    compileOptions {
        sourceCompatibility JavaVersion.VERSION_1_8
        targetCompatibility JavaVersion.VERSION_1_8
    }

Implémentation de la caméra X

Après avoir ajouté la dépendance, nous allons implémenter la fonction pour prendre une photo en utilisant ** Camera X **, une bibliothèque qui facilite la manipulation de la caméra sur Android.

Ci-dessous, nous allons mettre en œuvre le [Tutoriel] officiel de Camera X (https://codelabs.developers.google.com/codelabs/camerax-getting-started/#0). Les détails sont mentionnés dans d'autres articles, alors omettez-les et codez simplement.

Manifeste

Octroi de permission

<uses-permission android:name="android.permission.CAMERA" />

Disposition

Disposer un bouton pour démarrer la caméra et une vue de texture pour l'affichage de l'aperçu キャプチccaャ.PNG

activity_main.xml


<?xml version="1.0" encoding="utf-8"?>
<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">

    <TextureView
        android:id="@+id/view_finder"
        android:layout_width="0dp"
        android:layout_height="0dp"
        android:layout_marginBottom="16dp"
        app:layout_constraintBottom_toTopOf="@+id/activateCameraBtn"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <androidx.constraintlayout.widget.ConstraintLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:alpha="0.7"
        android:animateLayoutChanges="true"
        android:background="@android:color/white"
        app:layout_constraintEnd_toEndOf="@+id/view_finder"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="@+id/view_finder">

        <TextView
            android:id="@+id/inferredCategoryText"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_marginStart="8dp"
            android:layout_marginTop="16dp"
            android:layout_marginEnd="8dp"
            android:text="Résultat d'inférence"
            android:textSize="18sp"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent" />

        <TextView
            android:id="@+id/inferredScoreText"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginStart="24dp"
            android:layout_marginTop="16dp"
            android:text="But"
            android:textSize="18sp"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toBottomOf="@+id/inferredCategoryText" />
    </androidx.constraintlayout.widget.ConstraintLayout>

    <Button
        android:id="@+id/activateCameraBtn"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginBottom="16dp"
        android:text="Activation de la caméra"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent" />

</androidx.constraintlayout.widget.ConstraintLayout>

use case Camera X propose trois cas d'utilisation: ** aperçu, capture d'image et analyse d'image **. Cette fois, nous utiliserons l'aperçu et l'analyse d'image. La correspondance avec le cas d'utilisation facilite le tri du code. À propos, les combinaisons possibles sont les suivantes. (De document officiel)

cc capture.PNG

Cas d'utilisation de l'aperçu implémenté

Nous implémenterons jusqu'à l'aperçu du cas d'utilisation de Camera X. Presque le même contenu que Tutorial.

MainActivity.kt



private const val REQUEST_CODE_PERMISSIONS = 10
private val REQUIRED_PERMISSIONS = arrayOf(Manifest.permission.CAMERA)

class MainActivity : AppCompatActivity(), LifecycleOwner {
    private val executor = Executors.newSingleThreadExecutor()
    private lateinit var viewFinder: TextureView

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

        viewFinder = findViewById(R.id.view_finder)

        //Activation de la caméra
        activateCameraBtn.setOnClickListener {
            if (allPermissionsGranted()) {
                viewFinder.post { startCamera() }
            } else {
                ActivityCompat.requestPermissions(
                    this, REQUIRED_PERMISSIONS, REQUEST_CODE_PERMISSIONS
                )
            }
        }

        viewFinder.addOnLayoutChangeListener { _, _, _, _, _, _, _, _, _ ->
            updateTransform()
        }
    }

    private fun startCamera() {

        //Implémentation de l'aperçu useCase
        val previewConfig = PreviewConfig.Builder().apply {
            setTargetResolution(Size(viewFinder.width, viewFinder.height))
        }.build()

        val preview = Preview(previewConfig)

        preview.setOnPreviewOutputUpdateListener {
            val parent = viewFinder.parent as ViewGroup
            parent.removeView(viewFinder)
            parent.addView(viewFinder, 0)
            viewFinder.surfaceTexture = it.surfaceTexture
            updateTransform()
        }

        /**Nous implémenterons l'analyse d'image useCase ici plus tard.**/ 

        CameraX.bindToLifecycle(this, preview)
    }

    private fun updateTransform() {
        val matrix = Matrix()
        val centerX = viewFinder.width / 2f
        val centerY = viewFinder.height / 2f

        val rotationDegrees = when (viewFinder.display.rotation) {
            Surface.ROTATION_0 -> 0
            Surface.ROTATION_90 -> 90
            Surface.ROTATION_180 -> 180
            Surface.ROTATION_270 -> 270
            else -> return
        }
        matrix.postRotate(-rotationDegrees.toFloat(), centerX, centerY)

        //Reflété dans textureView
        viewFinder.setTransform(matrix)
    }

    override fun onRequestPermissionsResult(
        requestCode: Int, permissions: Array<String>, grantResults: IntArray
    ) {
        if (requestCode == REQUEST_CODE_PERMISSIONS) {
            if (allPermissionsGranted()) {
                viewFinder.post { startCamera() }
            } else {
                Toast.makeText(
                    this,
                    "Permissions not granted by the user.",
                    Toast.LENGTH_SHORT
                ).show()
                finish()
            }
        }
    }

    private fun allPermissionsGranted() = REQUIRED_PERMISSIONS.all {
        ContextCompat.checkSelfPermission(
            baseContext, it
        ) == PackageManager.PERMISSION_GRANTED
    }
}

Préparation du modèle et classe de classification

Cette fois, j'utiliserai le resnet18 formé.

import torch
import torchvision

model = torchvision.models.resnet18(pretrained=True)
model.eval()
example = torch.rand(1, 3, 224, 224)
traced_script_module = torch.jit.trace(model, example)
traced_script_module.save("resnet.pt")

S'il peut être exécuté avec succès, un fichier appelé resnet.pt sera généré dans la même hiérarchie. La reconnaissance d'image sera effectuée à l'aide de ce resnet18 formé.

Placez le modèle téléchargé dans le ** dossier d'actifs ** d'Android Studio. (Comme il n'existe pas par défaut, vous pouvez le créer en faisant un clic droit sur le dossier res-> Nouveau-> Dossier-> Dossier Asset)

Écrivez la classe ImageNet dans un fichier pour la convertir en nom de classe après avoir déduit. Créez un nouveau ** ImageNetClasses.kt ** et écrivez-y 1000 classes d'ImageNet. C'est trop long, alors copiez-le depuis github.

ImageNetClasses.kt


class ImageNetClasses {
    var IMAGENET_CLASSES = arrayOf(
        "tench, Tinca tinca",
        "goldfish, Carassius auratus",
         //Abréviation(Veuillez copier depuis github)
        "ear, spike, capitulum",
        "toilet tissue, toilet paper, bathroom tissue"
    )
}

Créer un cas d'utilisation d'analyse d'image

Ensuite, nous implémenterons l'analyse d'image pour le cas d'utilisation de Camera X. Créez un nouveau fichier appelé ImageAnalyze.kt et effectuez un traitement de reconnaissance d'image.

Dans le flux, chargez le modèle et utilisez le cas d'utilisation de l'analyse d'image pour convertir l'image d'aperçu en tenseur afin qu'elle puisse être utilisée avec pytorch mobile, puis transmettez le modèle chargé à partir du dossier d'actifs plus tôt pour obtenir le résultat.

Après cela, j'ai écrit une interface et un écouteur personnalisé pour refléter le résultat de l'inférence dans la vue. (Je ne sais pas comment écrire correctement ici, alors faites-moi savoir s'il existe une façon intelligente de l'écrire.)

ImageAnalyze.kt



class ImageAnalyze(context: Context) : ImageAnalysis.Analyzer {

    private lateinit var listener: OnAnalyzeListener    //Écouteur personnalisé pour la mise à jour de la vue
    private var lastAnalyzedTimestamp = 0L
    //Chargement d'un modèle de modèle de réseau
    private val resnet = Module.load(getAssetFilePath(context, "resnet.pt"))

    interface OnAnalyzeListener {
        fun getAnalyzeResult(inferredCategory: String, score: Float)
    }

    override fun analyze(image: ImageProxy, rotationDegrees: Int) {
        val currentTimestamp = System.currentTimeMillis()

        if (currentTimestamp - lastAnalyzedTimestamp >= 0.5) {  // 0.Infer toutes les 5 secondes
            lastAnalyzedTimestamp = currentTimestamp

            //Convertir en tenseur(Lorsque j'ai vérifié le format d'image, YUV_420_Il s'appelait 888)
            val inputTensor = TensorImageUtils.imageYUV420CenterCropToFloat32Tensor(
                image.image,
                rotationDegrees,
                224,
                224,
                TensorImageUtils.TORCHVISION_NORM_MEAN_RGB,
                TensorImageUtils.TORCHVISION_NORM_STD_RGB
            )
            //Inférer avec un modèle entraîné
            val outputTensor = resnet.forward(IValue.from(inputTensor)).toTensor()
            val scores = outputTensor.dataAsFloatArray

            var maxScore = 0F
            var maxScoreIdx = 0
            for (i in scores.indices) { //Obtenez l'index avec le score le plus élevé
                if (scores[i] > maxScore) {
                    maxScore = scores[i]
                    maxScoreIdx = i
                }
            }

            //Obtenir le nom de la catégorie à partir du score
            val inferredCategory = ImageNetClasses().IMAGENET_CLASSES[maxScoreIdx]
            listener.getAnalyzeResult(inferredCategory, maxScore)  //Mettre à jour la vue
        }
    }

    ////Fonction pour obtenir le chemin du fichier d'actif
    private fun getAssetFilePath(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
        }
    }

    fun setOnAnalyzeListener(listener: OnAnalyzeListener){
        this.listener = listener
    }
}

J'étais confus par le type d'image inconnu appelé ImageProxy, mais lorsque j'ai vérifié le format, j'ai pensé que je devais le convertir en bitmap avec YUV_420_888, mais pytorch mobile a une méthode pour convertir de YUV_420 en tenseur, et cela peut être facilement déduit simplement en le jetant. C'était.

En passant, si vous regardez le code, vous avez peut-être pensé qu'il était en temps réel, mais il est par incréments de 0,5 seconde.

Incorporer le cas d'utilisation de l'analyse d'image

Introduction de la classe ImageAnalyze créée précédemment dans Camera X en tant que cas d'utilisation, et enfin implémentation de l'interface de la classe ImageAnalyze dans MainActivity à l'aide d'un objet anonyme, et complétée afin que la vue puisse être mise à jour.

Ajoutez le code suivant à la fin de onCreate. (En haut, j'ai commenté "/ ** Je vais implémenter l'analyse d'image useCase ici ** /" plus tard)

MainActivity.kt



        //Mise en œuvre de l'analyse d'image useCase
        val analyzerConfig = ImageAnalysisConfig.Builder().apply {
            setImageReaderMode(
                ImageAnalysis.ImageReaderMode.ACQUIRE_LATEST_IMAGE
            )
        }.build()

        //exemple
        val imageAnalyzer = ImageAnalyze(applicationContext)
        //Afficher les résultats de l'inférence
        imageAnalyzer.setOnAnalyzeListener(object : ImageAnalyze.OnAnalyzeListener {
            override fun getAnalyzeResult(inferredCategory: String, score: Float) {
                //Changer la vue à partir d'un autre que le thread principal
                viewFinder.post {
                    inferredCategoryText.text = "Résultat d'inférence: $inferredCategory"
                    inferredScoreText.text = "But: $score"
                }
            }
        })
        val analyzerUseCase = ImageAnalysis(analyzerConfig).apply {
            setAnalyzer(executor, imageAnalyzer)
        }

        //useCase est un aperçu et une analyse d'image
        CameraX.bindToLifecycle(this, preview, analyzerUseCase)  //Ajout d'une analyse d'image au cas d'utilisation

Achevée! !! Ceux qui l'ont mis en œuvre avec succès jusqu'à présent devraient avoir terminé la demande au début. Veuillez jouer avec.

fin

Ce code est répertorié dans github, veuillez donc vous y référer le cas échéant.

Camera X Vraiment pratique! Vous pouvez facilement effectuer une analyse d'image en combinaison avec pytroch mobile. Il ne peut pas être aidé que le traitement le rend un peu plus lourd. Si vous pouvez préparer un modèle, vous pouvez facilement créer diverses applications de reconnaissance d'image à l'aide d'un appareil photo. Après tout, je me demande s'il est rapide de créer une application utilisant ce modèle comme l'apprentissage par transfert.

Je souhaite créer et publier une application d'apprentissage automatique ... ~~ Nous prévoyons de créer un exemple d'application dans un proche avenir. (En cours de révision) ~~

J'ai créé un exemple d'application

Je l'ai ajouté parce qu'il a réussi l'examen. J'ai essayé d'incorporer le contenu écrit dans cet article dans l'application. Il est publié sur le Play Store.

Si vous souhaitez en faire l'expérience rapidement ou si vous souhaitez le télécharger, nous vous serions reconnaissants de bien vouloir le télécharger.

Analyseur d'objets

Play Store: Object Analyzer Prise en charge de l'anglais et du japonais   

Pour être honnête, il y a une grande différence entre ce qui peut être jugé et ce qui ne peut pas être jugé ...

Recommended Posts

[kotlin] Créez une application de reconnaissance d'images en temps réel sur Android
[kotlin] Créez une application qui reconnaît les photos prises avec un appareil photo sur Android
Créer une salle de classe sur Jupyterhub
Reconnaissance d'image en temps réel sur les appareils mobiles à l'aide du modèle d'apprentissage TensorFlow
Créez une application de reconnaissance d'image qui discrimine les nombres écrits à l'écran avec Android (PyTorch Mobile) [implémentation Android]
Créer un environnement Python sur Mac (2017/4)
Implémenter l'application Django sur Hy
Créer un environnement Linux sur Windows 10
Créer un environnement python dans centos
Exécutez headless-chrome sur une image basée sur Debian
Créer une image de conteneur Docker avec JRE8 / JDK8 sur Amazon Linux
[Python] Créez un linebot pour écrire le nom et l'âge sur l'image
Créez une image factice avec Python + PIL.
Créez un environnement python sur votre Mac
Créer une application GUI simple en Python
Créer une application graphique avec Tkinter de Python
Créez une application Web simple avec Flask
Créer une application Python-GUI dans Docker (PySimpleGUI)
Créer une machine virtuelle Linux sous Windows
Reconnaissance d'image
Une histoire addictive lors de l'utilisation de tensorflow sur Android
Comment coder un drone en utilisant la reconnaissance d'image
Créer une application d'assistance technique à l'aide de PyLearn2
Créez une application de composition d'images avec Flask + Pillow
[Venv] Créer un environnement virtuel python sur Ubuntu
Essayez de créer une nouvelle commande sous Linux
Procédure de création d'application multi-plateforme avec kivy
Jusqu'à ce que vous créiez une nouvelle application dans Django
[Ubuntu] Installez Android Studio et créez un raccourci
Créer un environnement d'exécution Python sur IBM i
Créez une interface graphique sur le terminal à l'aide de curses
Pratique de développement d'applications Web: Créez une page de création d'équipe avec Django! (Expérience sur la page d'administration)