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 ↓
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.
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'
}
Définir surfaceView pour écrire des caractères
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>
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.
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
.
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
)
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é.
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!!)
}
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.
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.
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.
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.
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
Recommended Posts