20200223のAndroidに関する記事は9件です。

[kotlin]アンドロイドでリアルタイム画像認識アプリをつくる

今回やること

アンドロイドでカメラに映った映像(画像)をリアルタイムで画像認識するアプリを作る。
PyTorch Mobileを使って学習済みモデルをアンドロイドで動かす。

これ↓

依存関係

まずは依存関係(dependencies)を追加(2020年2月時点)
camera x と 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'

上の方のandroid{}ってあるところの一番最後に以下を追加

build.gradle
    compileOptions {
        sourceCompatibility JavaVersion.VERSION_1_8
        targetCompatibility JavaVersion.VERSION_1_8
    }

Camera Xの実装

依存関係を追加したら続いてCamera Xというアンドロイドで簡単にカメラを扱いやすくなるライブラリを利用して写真を撮る機能を実装する。

以下、公式のCamera Xのチュートリアルを実装していく。詳細は他の記事でも上がっていたりするので省略してコードのみ。

マニフェスト

パーミッションの許可

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

レイアウト

カメラを起動するボタンとプレビュー表示用のtextureView等を配置
キャプチ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="推論結果"
            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="スコア"
            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="カメラ起動"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent" />

</androidx.constraintlayout.widget.ConstraintLayout>

use case

Camera X ではプレビュー、画像キャプチャ、画像解析の3つのuse caseが提供されている。今回はプレビューと画像解析を使っていく。use caseに合わせることでコードがかき分けやすくなる。
ちなみに可能な組み合わせは以下の通り。(公式ドキュメントより)

ccキャプチャ.PNG

プレビューuse caseを実装

Camera Xのuse caseのプレビューまで実装していく。
ほぼチュートリアルと同様の内容。

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)

        // カメラ起動
        activateCameraBtn.setOnClickListener {
            if (allPermissionsGranted()) {
                viewFinder.post { startCamera() }
            } else {
                ActivityCompat.requestPermissions(
                    this, REQUIRED_PERMISSIONS, REQUEST_CODE_PERMISSIONS
                )
            }
        }

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

    private fun startCamera() {

        //プレビュー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()
        }

        /**後でここに画像解析useCaseの実装をする**/ 

        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)

        //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
    }
}

モデルと分類クラスの用意

今回は学習済みのresnet18を使う。
※python pytorchの環境がある方は以下のコードをpythonで実行、ない方はgithubからresnet.ptをダウンロードしてください。

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")

うまく実行できると同じ階層にresnet.ptというファイルが生成される。この学習済みresnet18を使って画像認識していく。

ダウンロードしたモデルをandroid studio のassetフォルダに入れる。(デフォルトでは存在しないのでresフォルダとかを右クリック->新規->フォルダ-> Assetフォルダで作れる)

推論した後にクラス名に変換するためにImageNetクラスをファイルに書いておく。新しくImageNetClasses.ktとかを作って、その中にImageNetの1000クラスを書いておく。
長すぎるのでgithubからコピペしてください。

ImageNetClasses.kt
class ImageNetClasses {
    var IMAGENET_CLASSES = arrayOf(
        "tench, Tinca tinca",
        "goldfish, Carassius auratus",
         //略(githubからコピペしてください)
        "ear, spike, capitulum",
        "toilet tissue, toilet paper, bathroom tissue"
    )
}

画像解析use caseの作成

つづいてCamera Xのuse caseの画像解析を実装していく。
新しくImageAnalyze.ktというファイルを作って画像認識の処理を行う。

流れ的にはモデルをロードして画像解析 use caseでプレビューの画像をpytorch mobileで使えるようにテンソルに変換し先ほどassetフォルダからロードしたモデルに通してその結果を取得する感じ。

あとは、viewに推論結果を反映させるためにインターフェースとかカスタムリスナとかを書いている。(この辺、正しい書き方がイマイチ分からず、我流なのでスマートな書き方があれば教えてください。)

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

    private lateinit var listener: OnAnalyzeListener    // Viewを更新するためのカスタムリスナ
    private var lastAnalyzedTimestamp = 0L
    //ネットワークモデルのモデルのロード
    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.5秒ごとに推論する
            lastAnalyzedTimestamp = currentTimestamp

            // テンソルに変換 (imageのformat調べてみたらYUV_420_888とかいうのだった)
            val inputTensor = TensorImageUtils.imageYUV420CenterCropToFloat32Tensor(
                image.image,
                rotationDegrees,
                224,
                224,
                TensorImageUtils.TORCHVISION_NORM_MEAN_RGB,
                TensorImageUtils.TORCHVISION_NORM_STD_RGB
            )
            // 学習済みモデルで推論する
            val outputTensor = resnet.forward(IValue.from(inputTensor)).toTensor()
            val scores = outputTensor.dataAsFloatArray

            var maxScore = 0F
            var maxScoreIdx = 0
            for (i in scores.indices) { //スコアが最大のインデックスを取得
                if (scores[i] > maxScore) {
                    maxScore = scores[i]
                    maxScoreIdx = i
                }
            }

            // スコアからカテゴリ名を取得
            val inferredCategory = ImageNetClasses().IMAGENET_CLASSES[maxScoreIdx]
            listener.getAnalyzeResult(inferredCategory, maxScore)  // Viewを更新
        }
    }

    //// assetファイルからパスを取得する関数
    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
    }
}

画像がImageProxyとかいうよくわからない型で戸惑ったがformat調べるとYUV_420_888とかでbitmapに変換しなきゃダメかなとか思ってたけど、pytorch mobileにYUV_420からテンソルに変換するメソッドがあり、放り込むだけで簡単に推論できた。

ちなみにコード見た方は思ったかもしれないですが、リアルタイムといってますが、0.5秒刻みです..

画像解析use caseを組み込む

先ほど作ったImageAnalyzeクラスをCamera Xにuse caseとして導入し、最後にImageAnalyzeクラスのインターフェースを無名オブジェクト使ってMainActivityで実装し、viewを更新できるようにして完成。

以下のコードをonCreateの最後に追加する。(上の方で「/後でここに画像解析useCaseの実装をする/ 」とコメントしてあったところ)

MainActivity.kt
        // 画像解析useCaseの実装
        val analyzerConfig = ImageAnalysisConfig.Builder().apply {
            setImageReaderMode(
                ImageAnalysis.ImageReaderMode.ACQUIRE_LATEST_IMAGE
            )
        }.build()

        //インスタンス
        val imageAnalyzer = ImageAnalyze(applicationContext)
        //推論結果を表示
        imageAnalyzer.setOnAnalyzeListener(object : ImageAnalyze.OnAnalyzeListener {
            override fun getAnalyzeResult(inferredCategory: String, score: Float) {
                // メインスレッド以外からviewの変更をする
                viewFinder.post {
                    inferredCategoryText.text = "推論結果: $inferredCategory"
                    inferredScoreText.text = "スコア: $score"
                }
            }
        })
        val analyzerUseCase = ImageAnalysis(analyzerConfig).apply {
            setAnalyzer(executor, imageAnalyzer)
        }

        // useCaseはプレビューと画像解析
        CameraX.bindToLifecycle(this, preview, analyzerUseCase)  // use caseに画像解析を追加

完成!!
ここまでうまく実装出来た方は冒頭のアプリが完成しているはず。いろいろ遊んでみてください。

おわり

今回のコードはgithubに挙げているので適宜参照してください。

Camera X ほんとに便利! pytroch mobileとかと組み合わせて、簡単に画像解析を行ったりできる。少々処理で重くなるのはしょうがないけど。
モデルさえ用意できればカメラ使っていろんな画像認識系のアプリが簡単に作れそう。やっぱり、転移学習とかしてそのモデル使ってなんかアプリ作るのが手っ取り早いのかな。

なんか機械学習系のアプリ作ってリリースしてみたい...
近日中にサンプルアプリ作って出す予定です。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[kotlin]アンドロイドでカメラで撮った写真を画像認識するアプリをつくる

今回やること

アンドロイドで写真を撮って保存しその写真の表示と画像分類を行い、分類結果を表示する簡単な画像認識アプリを作る。

これ↓

カメラを起動し撮影し、
撮った写真を画面に表示して

撮った写真を画像認識する

今回使うライブラリとかキーワード等

・ Python PyTorch Mobile
・ Android Camera X
・ resnet18
・ kotlin

去年出たものばっか...

依存関係

まずは依存関係(dependencies)を追加(2020年2月時点)
camera x と 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'

上の方のandroid{}ってあるところの一番最後に以下を追加

build.gradle
    compileOptions {
        sourceCompatibility JavaVersion.VERSION_1_8
        targetCompatibility JavaVersion.VERSION_1_8
    }

Camera Xの実装

依存関係を追加したら続いてCamera Xというアンドロイドで簡単にカメラを扱いやすくなるライブラリを利用して写真を撮る機能を実装する。

以下、公式のCamera Xのチュートリアルを実装していく。詳細は他の記事でも上がっていたりするので省略してコードのみ。

マニフェスト

パーミッションの許可

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

カメラで写真を撮る機能を実装

カメラで写真を撮って保存する機能を付ける。チュートリアルに沿ってカメラのプレビュー、カメラのキャプチャーまで。
チュートリアルの内容とほぼ同じなのでコードのみ載せておきます。

レイアウト


撮った写真を表示する場所、カメラのプレビュー表示場所、カメラ起動ボタン、キャプチャーボタン、推論ボタンを適当に設置する。

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">

    <Button
        android:id="@+id/capture_button"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginTop="2dp"
        android:text="撮影"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintHorizontal_bias="0.25"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/frameLayout" />

    <Button
        android:id="@+id/activateCamera"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="カメラ起動"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintHorizontal_bias="0.25"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/capture_button" />

    <ImageView
        android:id="@+id/capturedImg"
        android:layout_width="500px"
        android:layout_height="500px"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        app:srcCompat="@mipmap/ic_launcher_round" />

    <FrameLayout
        android:id="@+id/frameLayout"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginTop="8dp"
        android:background="@android:color/holo_blue_bright"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/capturedImg">

        <TextureView
            android:id="@+id/view_finder"
            android:layout_width="500px"
            android:layout_height="500px" />
    </FrameLayout>

    <Button
        android:id="@+id/inferBtn"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginStart="32dp"
        android:text="推論"
        app:layout_constraintBottom_toBottomOf="@+id/capture_button"
        app:layout_constraintStart_toEndOf="@+id/capture_button"
        app:layout_constraintTop_toTopOf="@+id/capture_button" />

    <TextView
        android:id="@+id/resultText"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginTop="4dp"
        android:text="推論結果"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintHorizontal_bias="0.31"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/activateCamera" />

</androidx.constraintlayout.widget.ConstraintLayout>

MainActivity

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

class MainActivity : AppCompatActivity(), LifecycleOwner {

    private var imgData: Bitmap? = null   // 保存した画像データ格納変数

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        viewFinder = findViewById(R.id.view_finder)

        // カメラ起動
        activateCamera.setOnClickListener {
            if (allPermissionsGranted()) {
                viewFinder.post { startCamera() }
            } else {
                ActivityCompat.requestPermissions(
                    this, REQUIRED_PERMISSIONS,REQUEST_CODE_PERMISSIONS
                )
            }
        }

        viewFinder.addOnLayoutChangeListener { _, _, _, _, _, _, _, _, _ ->
            updateTransform()
        }
        /**あとで画像分類するコードをここに追加**/
    }

    private val executor = Executors.newSingleThreadExecutor()
    private lateinit var viewFinder: TextureView

    private fun startCamera() {
        // プレビューuse case 作成
        val previewConfig = PreviewConfig.Builder().apply {
            setTargetResolution(Size(viewFinder.width, viewFinder.height)) // 680, 480
        }.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()
        }

        //キャプチャーuse case作成
        val imageCaptureConfig = ImageCaptureConfig.Builder()
            .apply {
                setCaptureMode(ImageCapture.CaptureMode.MIN_LATENCY)
            }.build()

        val imageCapture = ImageCapture(imageCaptureConfig)

        // 写真撮影
        capture_button.setOnClickListener {
            val file = File(
                externalMediaDirs.first(),
                "${System.currentTimeMillis()}.jpg"
            )

            imageCapture.takePicture(file, executor,
                object : ImageCapture.OnImageSavedListener {
                    override fun onError(
                        imageCaptureError: ImageCapture.ImageCaptureError,
                        message: String,
                        exc: Throwable?
                    ) {
                        val msg = "Photo capture failed: $message"
                        Log.e("CameraXApp", msg, exc)
                        viewFinder.post {
                            Toast.makeText(baseContext, msg, Toast.LENGTH_SHORT).show()
                        }
                    }

                    override fun onImageSaved(file: File) {
                        // 保存したファイルデータをビットマップとして取得
                        // ()Matrix を使って90度回転させて表示
                        val inputStream = FileInputStream(file)
                        val bitmap = BitmapFactory.decodeStream(inputStream)
                        val bitmapWidth = bitmap.width
                        val bitmapHeight = bitmap.height
                        val matrix = Matrix()
                        matrix.setRotate(90F, bitmapWidth / 2F, bitmapHeight / 2F)
                        val rotatedBitmap = Bitmap.createBitmap(
                            bitmap,
                            0,
                            0,
                            bitmapWidth,
                            bitmapHeight,
                            matrix,
                            true
                        )

                        imgData = rotatedBitmap  // 推論用に画像を格納
                        // 撮影した写真を表示
                        //メインスレッド以外からviewの変更
                        viewFinder.post {
                            capturedImg.setImageBitmap(rotatedBitmap)
                        }
                        val msg = "Photo capture succeeded: ${file.absolutePath}"
                        viewFinder.post {
                            Toast.makeText(baseContext, msg, Toast.LENGTH_SHORT).show()
                        }

                    }
                })
        }
        // プレビューとキャプチャーuse case
        CameraX.bindToLifecycle(this, preview, imageCapture)
    }

    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)

        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
    }
}

ここまで出来たら写真を撮って、その写真を画面に表示させることができるはず。
(自分の環境のせいなのかコードが悪いせいなのか分からないけど、写真撮ってから撮った写真が表示されるまで結構ラグがあります。)

公式からはCamera Xの use caseとしてプレビュー、キャプチャー、画像解析の3つのuse caseが提供されているが、今回はプレビューとキャプチャーの組み合わせを使う。
ちなみにuse case のサポートされている組み合わせは以下の通り。(公式ドキュメント)

 ccキャプチャ.PNG

画像認識の実装

モデルをダウンロードする

今回は学習済みのモデルを使って推論する。
※python,PyTorchの環境がない方は自分のgithubからresnet.ptをダウンロードしてここを読み飛ばしてください。
python PyTorchの環境がある方は以下を自分の環境で実行してモデルをダウンロードしてください。

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")

うまく実行できると同じ階層にresnet.ptというファイルが生成される。(後でこれをandroid studioのフォルダに入れる)
この学習済みresnet18を使って画像認識していく。

モデルを使って推論する

assetフォルダ

まずは先ほどダウンロードしたモデルをandroid studioのフォルダに放り込む。
放り込む場所はassetフォルダ (デフォルトでは存在しないのでresフォルダとかを右クリック->新規->フォルダ-> Assetフォルダで作れる)

次にassetフォルダからパスを取得する関数を作る
以下をMainActivity.ktの一番下とかに加える

MainActivity.kt
 //assetファイルのパス取得
    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
        }
    }

推論

画像分類するクラスを取得できるようにImage Netの1000クラスを参照できるようにしておく。
新しくImageNetCategory.ktとかを作ってそこにクラス名を書く。(長すぎるのでgithubからコピペしてください)

ImageNetCategory.kt
class ImageNetCategory {
    var IMAGENET_CLASSES = arrayOf(
        "tench, Tinca tinca",
        "goldfish, Carassius auratus",

          //略(githubからコピペしてください)

        "ear, spike, capitulum",
        "toilet tissue, toilet paper, bathroom tissue"
    )
}

つづいてメインの推論の部分を実装する。
MainActivity.ktのonCreateの最後の部分に以下を追加する。

MainActivity.kt
  // ネットワークモデルのロード
        val resnet = Module.load(getAssetFilePath(this, "resnet.pt"))

        /**推論**/
        inferBtn.setOnClickListener {
            //撮影した写真を224×224にリサイズする
            val imgDataResized = Bitmap.createScaledBitmap(imgData!!, 224, 224, true)
            // ビットマップからテンソルに変換
            val inputTensor = TensorImageUtils.bitmapToFloat32Tensor(
                imgDataResized,
                TensorImageUtils.TORCHVISION_NORM_MEAN_RGB,
                TensorImageUtils.TORCHVISION_NORM_STD_RGB
            )

            //フォワードプロパゲーション
            val outputTensor = resnet.forward(IValue.from(inputTensor)).toTensor()
            val scores = outputTensor.dataAsFloatArray

            var maxScore = 0F
            var maxScoreIdx = 0
            for (i in scores.indices) {
                if (scores[i] > maxScore) {
                    maxScore = scores[i]
                    maxScoreIdx = i
                }
            }

            // 推論結果のインデックスからカテゴリー名に変換
            val inferCategory = ImageNetCategory().IMAGENET_CLASSES[maxScoreIdx]
            resultText.text = "推論結果:${inferCategory}" 
        }

これだけで画像認識ができる。
いろんな写真撮ったりモデルを交換してみて遊んでみてください。

おわり

今回のコードはgithubに挙げていますので適宜参照してください。
本当はVGG-16とか載せようとしたんだけどout of memoryとかになってめんどそうだと思ったから断念。いろいろ転移学習させたモデルを載せても面白そう。
あと、Camera Xを使えば簡単にカメラの機能を使えて便利だなって思った。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Android x86でDetecting Android-x86から進まない時の対処法

はじめに

VMwareなどの仮想マシンにAndroid x86のISOイメージを使ってインストールした際に、最後の最後で「Detecting Android-x86...」の画面かた進まなくなることがあります。
その対処法を紹介していきます。

※筆者のAndroid x86は「Android7.1 x86」です。

成功した方法

筆者が実際に成功した方法は仮想マシンの設定の変更です。
・インターネット関連の設定を「NAT」から「ブリッジ」にする。
・グラフィック関連の設定を「3Dアクセラレーション」を有効化し、画面のサイズを下げる。

※「3Dアクセラレーション」を有効化すると注意が表示される場合があります。

最後に

この方法の他にもいろんな方法で成功例がありますが、必ず成功するとは限りません。ぜひいろんな方法を試してみてください。

「Detecting Android-x86...」の画面が消えてカーソルのみの画面になれば起動します。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Firebaseでお手軽サーバレスアプリ(Realtime Database+CloudFunctions+Cloud Messaging+Android(kotlin)+iOS(swift))その2

はじめに

今回はデータを構造化して書き込むのと
書き込んだデータを取得し表示する所までを実装します。

データ構造

"log"をキーとし自動採番で"name"と"message"と"timestamp"の文字列を追加していく構造とします。

コードを追加

データクラス

data class Data(
    var Name: String? ="",
    var Message: String? ="",
    var Timestamp: String?= ""
)

データの書き込み

前回追加したコードを修正します

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

        val database = FirebaseDatabase.getInstance()
        val myRef = database.getReference("log")

        fab.setOnClickListener {
            val newData = Data("user1","オレオレ", System.currentTimeMillis().toString())
            myRef.push().setValue(newData)
        }
    }

データを表示する部分を追加します

データが追加される毎に自動的に更新されます

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

        val database = FirebaseDatabase.getInstance()
        val myRef = database.getReference("log")

        fab.setOnClickListener {
            val newData = Data("user1","オレオレ", System.currentTimeMillis().toString())
            myRef.push().setValue(newData)
        }

        myRef.addChildEventListener(object : ChildEventListener {
            override fun onChildAdded(dataSnapshot: DataSnapshot, p1: String?) {
                var currentText = textView.text.toString()
                val getData = dataSnapshot.getValue(Data::class.java)
                if(getData != null) {
                    currentText += "\n"
                    currentText += getData.Timestamp
                    currentText += " "
                    currentText += getData.Name
                    currentText += " "
                    currentText += getData.Message
                    textView.text = currentText
                }
            }
            override fun onCancelled(p0: DatabaseError) {
                println(p0.message)
            }
            override fun onChildMoved(p0: DataSnapshot, p1: String?) {
                println("A child removed !!!!!!!!!!!!!!!!!!")
            }
            override fun onChildChanged(p0: DataSnapshot, p1: String?) {
                println("A child changed !!!!!!!!!!!!!!!!!!")
            }
            override fun onChildRemoved(p0: DataSnapshot) {
                println("A child removed !!!!!!!!!!!!!!!!!!")
            }
        })
    }

Screenshot_20200223-135601.png

data.PNG

まとめ

データの書き込みと表示までできました。
このアプリを複数台のスマホで動かせば、書き込みと同時に表示されデータが常に同期されることを確認できると思います。簡単ですね。

このシリーズについて

その1:全体像とFirebaseの準備

その2:Androidのクライアントアプリからデータの登録と表示まで

その3:iOSのクライアントアプリからデータの登録と表示まで

[その4:準備中]

[その5:準備中]

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

ndroid でもとりあえず Ubuntu のデスクトップ環境を使いたい(Termux 版 - LXDEで日本語入力)

はじめに

Android でもとりあえず Ubuntu のデスクトップ環境を使いたい(Termux 版)
の続きです。
Ubuntu の LXDEデスクトップ環境で日本語入力を行うための設定を記載します。
fcitx-mozc はどうしても動作させることができなかった(オンになっているはずなのに、日本語が入力できない。。。)ので、fcitx-anthy を使用しています。

インストール

まずは日本語フォントおよび Anthy (日本語をキーボードから入力するためのツール)のインストールを行います。
dbus-x11 は入力ツールの不足分を補完します。
Ubuntu の LXDEデスクトップ環境を立ち上げてから、以下のコマンドを LXTerminal 等のシェルで実行して下さい。

apt install -y fonts-noto
apt install -y fcitx-anthy
apt install -y dbus-x11

設定

続いて入力ツールの設定ダイアログで設定を行います。
以下のコマンドでダイアログを開いて下さい。

fcitx-configtool

もしダイアログが立ち上がらなかったら以下のコマンドを実行してからもう一度、ダイアログを立ち上げて下さい。

fcitx-autostart

入力ツールのダイアログを表示したら以下の手順で Anthy を使えるように設定してください。

  1. Input Method タブを選択
  2. +ボタンを押す
  3. Only Show Current Language のチェックボックスからチェックを外す
  4. チェックボックスの下の入力欄に Anthy と入力する
  5. Anthy が上のボックスに出るので選択し、OKボタンを押す
  6. Anthy が上から2番目の位置になるよう ↑ や ↓ で移動する
  7. 続いて、Global Config タブを選択
  8. Trigger Input Method で日本語と英語を切り替えるボタンを設定する(任意です。ネットで見ると 右ALT や Shift+SPACE など好みで設定しているようです。)
  9. ダイアログの右上のxを押してダイアログを閉じる

続いて設定ファイルに環境変数とfcitxの起動コマンドを記載します。
以下のコマンドを実行して設定ファイルを編集します。

vim ~/.bashrc

以下を .bashrc ファイル内の適当な位置に記載します。
私は一番下に記載しました。

export GTK_IM_MODULE=fcitx
export QT_IM_MODULE=fcitx
export XMODIFIERS=@im=fcitx

fcitx > /dev/null 2>&1

最後に Termux を終了し起動し直します。
Android の通知領域に Termux の文字が2行出ているので上の方をタップします。
左下に Exit の文言が出ますので、Exit をタップします。
再び Termux + XSDL でLXDEデスクトップ環境を立ち上げるとAnthyで日本語入力できるようになります。

なお私の環境では Termux を再起動しないと Anthy で日本語入力出来ませんでした。
.bashrc を souce コマンドで読み込んでも駄目だったので、何か起動時に行わないといけないことがあるのかもしれません。

参考サイト

ChromebookのLinux(Crostini)の日本語入力をAnthyで構築

Chromebook C101PAでVSCodeを使う。(Crostini使用)

fcitxで「正しくfcitxに接続できません。」エラーが発生したときの対応

Chrombook C101PAにVisual Studio Codeをインストールしてみる

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Android でもとりあえず Ubuntu のデスクトップ環境を使いたい(Termux 版 - LXDEで日本語入力)

はじめに

Android でもとりあえず Ubuntu のデスクトップ環境を使いたい(Termux 版)
の続きです。
Ubuntu の LXDEデスクトップ環境で日本語入力を行うための設定を記載します。
fcitx-mozc はどうしても動作させることができなかった(オンになっているはずなのに、日本語が入力できない。。。)ので、fcitx-anthy を使用しています。

インストール

まずは日本語フォントおよび Anthy (日本語をキーボードから入力するためのツール)のインストールを行います。
dbus-x11 は入力ツールの不足分を補完します。
Ubuntu の LXDEデスクトップ環境を立ち上げてから、以下のコマンドを LXTerminal 等のシェルで実行して下さい。

apt install -y fonts-noto
apt install -y fcitx-anthy
apt install -y dbus-x11

設定

続いて入力ツールの設定ダイアログで設定を行います。
以下のコマンドでダイアログを開いて下さい。

fcitx-configtool

もしダイアログが立ち上がらなかったら以下のコマンドを実行してからもう一度、ダイアログを立ち上げて下さい。

fcitx-autostart

入力ツールのダイアログを表示したら以下の手順で Anthy を使えるように設定してください。

  1. Input Method タブを選択
  2. +ボタンを押す
  3. Only Show Current Language のチェックボックスからチェックを外す
  4. チェックボックスの下の入力欄に Anthy と入力する
  5. Anthy が上のボックスに出るので選択し、OKボタンを押す
  6. Anthy が上から2番目の位置になるよう ↑ や ↓ で移動する
  7. 続いて、Global Config タブを選択
  8. Trigger Input Method で日本語と英語を切り替えるボタンを設定する(任意です。ネットで見ると 右ALT や Shift+SPACE など好みで設定しているようです。)
  9. ダイアログの右上のxを押してダイアログを閉じる

続いて設定ファイルに環境変数とfcitxの起動コマンドを記載します。
以下のコマンドを実行して設定ファイルを編集します。

vim ~/.bashrc

以下を .bashrc ファイル内の適当な位置に記載します。
私は一番下に記載しました。

export GTK_IM_MODULE=fcitx
export QT_IM_MODULE=fcitx
export XMODIFIERS=@im=fcitx

fcitx > /dev/null 2>&1

最後に Termux と XSDLを終了し起動し直します。
Android の通知領域に Termux の文字が2行出ているので上の方をタップします。
左下に Exit の文言が出ますので、Exit をタップして Termux を終了します。
XSDL は Android の各ランチャーの手順に従って終了してください。
再び Termux + XSDL でLXDEデスクトップ環境を立ち上げるとAnthyで日本語入力できるようになります。

なお私の環境では Termux を再起動しないと Anthy で日本語入力出来ませんでした。
.bashrc を souce コマンドで読み込んでも駄目だったので、何か起動時に行わないといけないことがあるのかもしれません。

参考サイト

ChromebookのLinux(Crostini)の日本語入力をAnthyで構築

Chromebook C101PAでVSCodeを使う。(Crostini使用)

fcitxで「正しくfcitxに接続できません。」エラーが発生したときの対応

Chrombook C101PAにVisual Studio Codeをインストールしてみる

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Android でもとりあえず Ubuntu のデスクトップ環境を使いたい(Termux 版 - デスクトップ環境で日本語入力)

はじめに

Android でもとりあえず Ubuntu のデスクトップ環境を使いたい(Termux 版)
の続きです。
Ubuntu の LXDE、XFCEデスクトップ環境で日本語入力を行うための設定を記載します。
fcitx-mozc はどうしても動作させることができなかった(オンになっているはずなのに、日本語が入力できない。。。)ので、fcitx-anthy を使用しています。

インストール

まずは日本語フォントおよび Anthy (日本語をキーボードから入力するためのツール)のインストールを行います。
dbus-x11 は入力ツールの不足分を補完します。
Ubuntu の LXDE または XFCE デスクトップ環境を立ち上げてから、以下のコマンドを LXTerminal 等のシェルで実行して下さい。

apt install -y fonts-noto
apt install -y fcitx-anthy
apt install -y dbus-x11

設定

続いて入力ツールの設定ダイアログで設定を行います。
以下のコマンドでダイアログを開いて下さい。

fcitx-configtool

もしダイアログが立ち上がらなかったら以下のコマンドを実行してからもう一度、ダイアログを立ち上げて下さい。

fcitx-autostart

入力ツールのダイアログを表示したら以下の手順で Anthy を使えるように設定してください。

  1. Input Method タブを選択
  2. + ボタンを押す
  3. Only Show Current Language のチェックボックスからチェックを外す
  4. チェックボックスの下の入力欄に Anthy と入力する
  5. Anthy が上のボックスに出るので選択し、OKボタンを押す
  6. Anthy が上から2番目の位置になるよう、Anthy を選択してから + ボタンの右側にある ↑ ボタンや ↓ ボタンで移動する
  7. 続いて、Global Config タブを選択
  8. Trigger Input Method で日本語と英語を切り替えるボタンを設定する(※)
  9. ダイアログの右上のxを押してダイアログを閉じる

※任意です。ネットで見ると 右ALT や Shift+SPACE など好みで設定しているようです。
Android Studio を使っている人は、デフォルトで設定されている Ctrl+SPACE を別の設定に変えましょう。
Ctrl+SPACE は Android Studio のコード補完のショートカットになります。

続いて設定ファイルに環境変数とfcitxの起動コマンドを記載します。
以下のコマンドを実行して設定ファイルを編集します。

vim ~/.bashrc

以下を .bashrc ファイル内の適当な位置に記載します。
私は一番下に記載しました。

export GTK_IM_MODULE=fcitx
export QT_IM_MODULE=fcitx
export XMODIFIERS=@im=fcitx

fcitx > /dev/null 2>&1

最後に Termux と XSDLを終了し起動し直します。
Android の通知領域に Termux の文字が2行出ているので上の方をタップします。
左下に Exit の文言が出ますので、Exit をタップして Termux を終了します。
XSDL は Android の各ランチャーの手順に従って終了してください。
再び Termux + XSDL でデスクトップ環境を立ち上げるとAnthyで日本語入力できるようになります。

なお私の環境では Termux を再起動しないと Anthy で日本語入力出来ませんでした。
.bashrc を souce コマンドで読み込んでも駄目だったので、何か起動時に行わないといけないことがあるのかもしれません。

参考サイト

ChromebookのLinux(Crostini)の日本語入力をAnthyで構築

Chromebook C101PAでVSCodeを使う。(Crostini使用)

fcitxで「正しくfcitxに接続できません。」エラーが発生したときの対応

Chrombook C101PAにVisual Studio Codeをインストールしてみる

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

MediaCodecで動画をExtract, Decode, Encode, Muxしてみる

概要

Android SDK に含まれる Media Codec を用いて動画の Extract(分離), Decode, Encode, Mux(結合) し再度動画ファイルとして出力してみる

処理の全体像

動画を入力したら Extract, Decode, Encode, Mux の処理を無限ループで回していく。各処理を無限ループで回しつつ一部のデータを切り出して少しずつ並列に処理することで効率を上げる作りになっている

    fun doExtractDecodeEncodeMux() {
        videoDecoder.start()
        videoEncoder.start()
        audioDecoder.start()
        audioEncoder.start()

        while (!isMuxEnd) {
            if (!isVideoExtractEnd) { isVideoExtractEnd = extract(videoExtractor, videoDecoder) }
            if (!isAudioExtractEnd) { isAudioExtractEnd = extract(audioExtractor, audioDecoder) }
            if (!isVideoDecodeEnd) { isVideoDecodeEnd = decode(videoDecoder, videoEncoder) }
            if (!isAudioDecodeEnd) { isAudioDecodeEnd = decode(audioDecoder, audioEncoder) }
            if (!isVideoEncodeEnd) {
                isVideoEncodeEnd = encode(videoEncoder, {
                    outputVideoFormat = it
                    outputVideoTrackIdx = muxer.addTrack(it)
                }, { outputBuffer, outputBufferInfo ->
                    muxer.writeSampleData(outputVideoTrackIdx, outputBuffer, outputBufferInfo)
                })
            }
            if (!isAudioEncodeEnd) {
                isAudioEncodeEnd = encode(audioEncoder, {
                    outputAudioFormat = it
                    outputAudioTrackIdx = muxer.addTrack(it)
                }, { outputBuffer, outputBufferInfo ->
                    muxer.writeSampleData(outputAudioTrackIdx, outputBuffer, outputBufferInfo)
                })
            }
        }
        ...
    }

Extract

Extract は MediaExtractorクラスで行う。入力元動画のファイルパスを設定しておく。 Video用とAudio用をそれぞれ用意する必要がある

    private val videoExtractor = MediaExtractor()
    private val audioExtractor = MediaExtractor()

    init {
        videoExtractor.setDataSource(inputFilePath)
        videoTrackIdx = getVideoTrackIdx(videoExtractor)
        if (videoTrackIdx == -1) {
            Logger.e("video not found")
            throw RuntimeException("video not found")
        }
        videoExtractor.selectTrack(videoTrackIdx)
        ...
        Logger.e("inputVideoFormat: $inputVideoFormat")

        audioExtractor.setDataSource(inputFilePath)
        audioTrackIdx = getAudioTrackIdx(audioExtractor)
        if (audioTrackIdx == -1) {
            Logger.e("audio not found")
            throw RuntimeException("audio not found")
        }
        audioExtractor.selectTrack(audioTrackIdx)
        ...
        Logger.e("inputAudioFormat: $inputAudioFormat")

        ...
    }

    private fun getAudioTrackIdx(extractor: MediaExtractor): Int {
        for (idx in 0 until extractor.trackCount) {
            val format = extractor.getTrackFormat(idx)
            val mime = format.getString(MediaFormat.KEY_MIME)
            if (mime?.startsWith("audio") == true) {
                return idx
            }
        }
        return -1
    }

    private fun getVideoTrackIdx(extractor: MediaExtractor): Int {
        for (idx in 0 until extractor.trackCount) {
            val format = extractor.getTrackFormat(idx)
            val mime = format.getString(MediaFormat.KEY_MIME)
            if (mime?.startsWith("video") == true) {
                return idx
            }
        }
        return -1
    }

Extract処理では MediaExtractorreadSampleData() でデータを読み込み Decoder の inputBuffer に送る。最後まで読み込んだら送信時の flag に最後のデータであることを示す MediaCodec.BUFFER_FLAG_END_OF_STREAM をセットして送る。同時に isExtractEnd フラグをセットし、無限ループでこの処理が呼ばれないようにする

    private fun extract(extractor: MediaExtractor, decoder: MediaCodec): Boolean {
        var isExtractEnd = false
        val inputBufferIdx = decoder.dequeueInputBuffer(CODEC_TIMEOUT_IN_US)
        if (inputBufferIdx >= 0) {
            val inputBuffer = decoder.getInputBuffer(inputBufferIdx) as ByteBuffer
            val sampleSize = extractor.readSampleData(inputBuffer, 0)
            if (sampleSize > 0) {
                decoder.queueInputBuffer(inputBufferIdx, 0, sampleSize, extractor.sampleTime, extractor.sampleFlags)
            } else {
                Logger.e("isExtractEnd = true")
                isExtractEnd = true
                decoder.queueInputBuffer(inputBufferIdx, 0, 0, 0, MediaCodec.BUFFER_FLAG_END_OF_STREAM)
            }

            if (!isExtractEnd) {
                extractor.advance()
            }
        }
        return isExtractEnd
    }

Decode

Decodeは MediaCodec クラスで行う。 MediaCodec.createDecoderByType() で生成し、 configure() で各種パラメータをセットする。今回は入力と出力で同じ動画なので入力元動画のパラメータをそのままセットしている

    private val videoDecoder: MediaCodec
    private val audioDecoder: MediaCodec

    init {
        ...
        inputVideoFormat = videoExtractor.getTrackFormat(videoTrackIdx)
        ...
        videoDecoder = MediaCodec.createDecoderByType(inputVideoMime)
        videoDecoder.configure(inputVideoFormat, null, null, 0)

        ...
        inputAudioFormat = audioExtractor.getTrackFormat(audioTrackIdx)
        ...
        audioDecoder = MediaCodec.createDecoderByType(inputAudioMime)
        audioDecoder.configure(inputAudioFormat, null, null, 0)
        ...
    }

Decode処理では、Decoder の outputBuffer からデコードされたデータを取り出し今度は Encoder の inputBuffer へ送る

今回は入力動画と出力動画が同じものなのでそのまま decode 結果を encode して送ってしまうが、このタイミングで生のデータを加工し例えば動画にフィルターをかけたり、音声データのサンプリングレートを変換したりなどの面白いことが行える

最後まで処理したら Extract 時と同様に送信時の flag に最後のデータであることを示す MediaCodec.BUFFER_FLAG_END_OF_STREAM をセットして送る。同時に isDecodeEnd フラグをセットし、無限ループでこの処理が呼ばれないようにする

    private fun decode(decoder: MediaCodec, encoder: MediaCodec): Boolean {
        var isDecodeEnd = false
        val decoderOutputBufferInfo = MediaCodec.BufferInfo()
        val decoderOutputBufferIdx = decoder.dequeueOutputBuffer(decoderOutputBufferInfo, CODEC_TIMEOUT_IN_US)

        if (decoderOutputBufferInfo.flags and MediaCodec.BUFFER_FLAG_END_OF_STREAM != 0) {
            Logger.e("isDecodeEnd = true")
            isDecodeEnd = true
        }
        if (decoderOutputBufferIdx >= 0) {
            val encoderInputBufferIdx = encoder.dequeueInputBuffer(CODEC_TIMEOUT_IN_US)
            if (encoderInputBufferIdx >= 0) {
                val decoderOutputBuffer = (decoder.getOutputBuffer(decoderOutputBufferIdx) as ByteBuffer).duplicate()
                decoderOutputBuffer.position(decoderOutputBufferInfo.offset)
                decoderOutputBuffer.limit(decoderOutputBufferInfo.offset + decoderOutputBufferInfo.size)

                val encoderInputBuffer = encoder.getInputBuffer(encoderInputBufferIdx)
                encoderInputBuffer?.position(0)
                encoderInputBuffer?.put(decoderOutputBuffer)

                val flags = if (isDecodeEnd) MediaCodec.BUFFER_FLAG_END_OF_STREAM else decoderOutputBufferInfo.flags

                encoder.queueInputBuffer(
                    encoderInputBufferIdx, 0,
                    decoderOutputBufferInfo.size,
                    decoderOutputBufferInfo.presentationTimeUs, flags
                )
                decoder.releaseOutputBuffer(decoderOutputBufferIdx, false)
            }
        }
        return isDecodeEnd
    }

Encode

Encode も MediaCodec クラスで行う。 configure() で各種パラメータをセットする。可能な限り入力元動画から取得したパラメータをセットしたが取得できないものがあるのでそこは別途指定していく。 configure() の第4引数は Decode の時とは違い MediaCodec.CONFIGURE_FLAG_ENCODE をセットする

    private val videoEncoder: MediaCodec
    private val audioEncoder: MediaCodec

    init {
        ...
        val inputVideoMime = inputVideoFormat.getString(MediaFormat.KEY_MIME)
        val width = inputVideoFormat.getInteger(MediaFormat.KEY_WIDTH)
        val height = inputVideoFormat.getInteger(MediaFormat.KEY_HEIGHT)
        encodeVideoFormat = MediaFormat.createVideoFormat(inputVideoMime, width, height).also {
            it.setInteger(MediaFormat.KEY_COLOR_FORMAT, MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420Flexible)
            it.setInteger(MediaFormat.KEY_BIT_RATE, 2000000)
            it.setInteger(MediaFormat.KEY_FRAME_RATE, inputVideoFormat.getInteger(MediaFormat.KEY_FRAME_RATE))
            it.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 10)
        }
        videoEncoder = MediaCodec.createEncoderByType(inputVideoMime)
        videoEncoder.configure(encodeVideoFormat, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE)

        val inputAudioMime = inputAudioFormat.getString(MediaFormat.KEY_MIME)
        val sampleRate = inputAudioFormat.getInteger(MediaFormat.KEY_SAMPLE_RATE)
        val channelCount = inputAudioFormat.getInteger(MediaFormat.KEY_CHANNEL_COUNT)
        encodeAudioFormat = MediaFormat.createAudioFormat(inputAudioMime, sampleRate, channelCount).also {
            it.setInteger(MediaFormat.KEY_AAC_PROFILE, MediaCodecInfo.CodecProfileLevel.AACObjectLC)
            it.setInteger(MediaFormat.KEY_BIT_RATE, inputAudioFormat.getInteger(MediaFormat.KEY_BIT_RATE))
        }
        audioEncoder = MediaCodec.createEncoderByType(inputAudioMime)
        audioEncoder.configure(encodeAudioFormat, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE)
        ...
    }

Encode処理では、Encoder の outputBuffer からエンコードされたデータを取り出し今度は Muxer へ送る

    private fun encode(
        encoder: MediaCodec,
        onOutputFormatChaned: (outputFormat: MediaFormat) -> Unit,
        writeEncodedData: (outputBuffer: ByteBuffer, outputBufferInfo: MediaCodec.BufferInfo) -> Unit
    ): Boolean {
        var isEncodeEnd = false
        val encoderOutputBufferInfo = MediaCodec.BufferInfo()
        val encoderOutputBufferIdx = encoder.dequeueOutputBuffer(encoderOutputBufferInfo, CODEC_TIMEOUT_IN_US)

        if (encoderOutputBufferIdx == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
            Logger.e("output format changed: ${encoder.outputFormat}")
            onOutputFormatChaned(encoder.outputFormat)
            if (outputVideoFormat != null && outputAudioFormat != null) {
                Logger.e("muxer start")
                muxer.start()
            }
            return isEncodeEnd
        }

        if (encoderOutputBufferInfo.flags and MediaCodec.BUFFER_FLAG_CODEC_CONFIG != 0) {
            encoder.releaseOutputBuffer(encoderOutputBufferIdx, false)
            return isEncodeEnd
        }

        outputVideoFormat?: return isEncodeEnd
        outputAudioFormat?: return isEncodeEnd

        if (encoderOutputBufferIdx >= 0) {
            val encoderOutputBuffer = encoder.getOutputBuffer(encoderOutputBufferIdx)
            if (encoderOutputBufferInfo.size > 0) {
                encoderOutputBuffer?.let {
                    writeEncodedData(it, encoderOutputBufferInfo)
                    encoder.releaseOutputBuffer(encoderOutputBufferIdx, false)
                }
            }
            Logger.e("presentationTimeUs: ${encoderOutputBufferInfo.presentationTimeUs}, encoder: ${encoder.codecInfo.name}")
        }

        if (encoderOutputBufferInfo.flags and MediaCodec.BUFFER_FLAG_END_OF_STREAM != 0) {
            Logger.e("isEncodeEnd = true")
            isEncodeEnd = true
        }
        return isEncodeEnd
    }

ここで注意しないといけないのが MediaCodec.INFO_OUTPUT_FORMAT_CHANGED を受け取り動画と音声両方の outputFormat が確定してからじゃないと Muxer にデータが書き込めないということである。 そのため両方が確定するまでは return している。outputFormat を受け取ったら Muxer の addTrack() を実行し、動画と音声両方を addTrack() 完了したら muxer.start() でMux処理を始めている

Mux

Muxは MediaMuxer クラスで行う。setOrientationHint() を設定しないと動画が反転することがあるようである。出力ファイルのパスを設定しておく

    private val muxer: MediaMuxer

    init {
        ...
        val videoMetaData = MediaMetadataRetriever()
        videoMetaData.setDataSource(inputFilePath)
        val degreeString = videoMetaData.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_ROTATION)
        val videoDegree = degreeString?.toInt() ?: 0
        muxer = MediaMuxer(outputFilePath, MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4)
        muxer.setOrientationHint(videoDegree)
    }

writeSampleData() を実行することで最終的に動画ファイルとして出力される

        while (!isMuxEnd) {
            ...
            if (!isVideoEncodeEnd) {
                isVideoEncodeEnd = encode(videoEncoder, {
                    outputVideoFormat = it
                    outputVideoTrackIdx = muxer.addTrack(it)
                }, { outputBuffer, outputBufferInfo ->
                    muxer.writeSampleData(outputVideoTrackIdx, outputBuffer, outputBufferInfo)
                })
            }
            if (!isAudioEncodeEnd) {
                isAudioEncodeEnd = encode(audioEncoder, {
                    outputAudioFormat = it
                    outputAudioTrackIdx = muxer.addTrack(it)
                }, { outputBuffer, outputBufferInfo ->
                    muxer.writeSampleData(outputAudioTrackIdx, outputBuffer, outputBufferInfo)
                })
            }
        }

まとめ

MediaCodecを使って動画をExtract, Decode, Encode, Muxし元動画と同じ動画を出力することができた :ok_woman:
今回は入力動画と出力動画が同じだったが、今度はデコード結果の生の動画データや音声データを加工し動画の一部を切り出したり音のサンプリングレートを変換したりなどの面白いことに挑戦してみたい ?

プロジェクトファイル一式

参考

Android Developer (MediaCodec)
上の和訳
Googleのサンプルコード (ExtractDecodeEditEncodeMuxTest.java)

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Andoird端末上で Linux を遊ぶ 1

AndroidにLinux環境を入れれるアプリ「UserLAnd」の備忘録です。

動作確認環境
- UserLand v2.6.5
- Ubuntu 18.04
- MIUI 11 (Android 10)
- Xiaomi Redmi K20 Pro / 非root状態

UserLAndとは

Android端末上でLinuxディストリビューションを扱えるようにしたアプリです。root作業は非特権ユーザが疑似的にroot作業を行える proot の仕組みを利用しているみたいです。Linuxに詳しくない自分には正直良く分かりません。

インストール~起動まで

初回起動時

  1. UserLAnd」をインストールして起動します。GUI画面が開きます。
  2. 画面内「Distribution」で動作させるLinuxを選びます。今回は Ubuntu を選択。
  3. 初回起動時に Username、Password、VNC_Password を設定する画面が表示されます。適宜入力して「Continue」をタップでインストールが開始します。
  4. インストール時間は端末や回線に依存するので、画面が切り替わるまでは待ちましょう。
  5. 初回起動では SSH、VNC、XSDL のどれで接続するか聞かれます。今回は SSH を選択。
  6. シェル画面に移行します。パスワードを入力してログインします。

終了と2回目以降の起動

  1. UserLAndのGUI画面より終了は行います。GUI画面を立ち上げます。
  2. 画面内「Distribution」で停止させるLinuxを長押し→「Stop App」とすると終了します。
  3. 再度起動したい場合、画面内「Distribution」で動作させるLinuxを選びます。
  4. 2回目以降の起動では接続設定やパスワード設定は聞かれません。
  5. 接続設定を変更したい場合、画面内Linuxを長押し→「App Info」から変更可能です。

Ubuntu 18.04 インストール直後

インストール直後なので、パッケージ更新しておきましょう。

$ sudo apt update
$ sudo apt upgrade

Ubuntu18.04からデフォルトで ifconfig が使えないようです。
個人的には不便なので net-tools を入れて ifconfig を使えるようにします。

$ sudo apt install net-tools

$ ifconfig
Warning: cannot open /proc/net/dev (Permission denied). Limited output.
lo: flags=73<UP,LOOPBACK,RUNNING>  mtu 65536
        inet 127.0.0.1  netmask 255.0.0.0
        unspec 00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00  txqueuelen 1000  (UNSPEC)

wlan0: flags=4163<UP,BROADCAST,RUNNING,MULTICAST>  mtu 1500
        inet 192.168.1.98  netmask 255.255.255.0  broadcast 192.168.1.255
        unspec 00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00  txqueuelen 3000  (UNSPEC)

ifconfig コマンドでIPアドレスを確認出来ます。SSHサーバ起動済みなので、以降は端末ソフトでPCから作業すると楽です。ポート番号は 2022 で、dropbear という軽量SSHサーバが立っています。

スマホのCPUやメモリに余裕がある場合はデスクトップ環境を入れても良いです。軽量デスクトップ環境の「LXDE」をサポートしているので、それを入れます。

$ sudo apt install lxde

とりあえず最低限の設定は以上です。
あと何回かは、自分のLinux学習も兼ねて記事を書いてみようかと思います。

UserLAnd特有の問題と思われるもの

UserLAnnd特有というか、proot 動作しているシステム全般かもしれない。

systemctl コマンドは失敗する。

以下エラーが出るため、systemctlコマンドは失敗します。
serviceコマンドの場合は上手くいきます。

$ sudo systemctl start apache2
System has not been booted with systemd as init system (PID 1). Can't operate.

$ sudo service apache2 start
 * Starting Apache httpd web server apache2
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む