🐎

ラムダ式で配列を宣言すると実行速度が超絶早くなる件(OpenCV in Android)

2023/03/20に公開

はじめに

Androidでリアルタイム画像処理をしようとしました.
リアルタイム画像処理って実行速度が超重要です.
例えば画像一枚の処理に1秒かかると1fpsの映像が表示されます.
0.1秒で処理すれば10fpsに,0.02秒で処理すれば50fpsに.(表示の時間や他の処理があるためここまでうまくいきませんが)
今回はOpenCVのremap関数を使う時に必要になる二つのMat型を作る速度を比較します.

他にもこんな記事を書いてます!
KotlinでDoubleArrayとArray<Double>の処理速度比較をする

環境

kotlin: 1.8.0
OpenCV: 4.7.0-dev
端末: Google Pixel7

目次

  1. remap関数と必要になる二つのMat型とは
  2. 1:2重forループで一個ずつputする.
  3. 2:Mat.putって実は配列をputできるんですよ!
  4. 3:配列宣言にラムダ式を使う
  5. まとめ
  6. ソースコード

そもそもremap関数と必要になる二つのMat型とは

remap関数の解説

おすすめ参考記事:OpenCVのremapを使って局所Affine変換
remap関数はそれぞれの画素を移動させます.
引数は以下の通りです.

  • src:入力画像
  • dst:出力画像
  • map1:x座標の変換マップ(この記事ではmapXと呼ぶ)
  • map2:y座標の変換マップ(この記事ではmapYと呼ぶ)
  • interpolation:変換に使用される補間方法。例えばImgproc.INTER_LINEARは線形補間を表します。
  • borderMode:変換領域の外側のピクセルの処理方法。例えばCore.BORDER_CONSTANTは定数を使って境界を埋めることを表します。
  • borderValue:境界値の色。例えばScalar.all(0.0)は、全ての要素が0のスカラー値を表します。

例えば以下の5*5の画像を考えます.

元画像

0 1 2 3 4
0 0,0 1,0 2,0 3,0 4,0
1 0,1 1,1 2,1 3,1 4,1
2 0,2 1,2 2,2 3,2 4,2
3 0,3 1,3 2,3 3,3 4,3
4 0,4 1,4 2,4 3,4 4,4

この画像を斜め右下にずらします.
この時,画素をどこに移動するかmapXとmapYで表します.
「X方向に1ずらす」というmapXを作ってみましょう
mapXの要素はx+1になります.

mapX

0 1 2 3 4
0 1 2 3 4 5
1 1 2 3 4 5
2 1 2 3 4 5
3 1 2 3 4 5
4 1 2 3 4 5

このmapXの中身は,その画素の移動先(x座標)を示します.
これを適応すると

mapX適応後

0 1 2 3 4
0 0,0 1,0 2,0 3,0
1 0,1 1,1 2,1 3,1
2 0,2 1,2 2,2 3,2
3 0,3 1,3 2,3 3,3
4 0,4 1,4 2,4 3,4

mapYも同じです.
今度はY方向に画像を2倍の大きさにします.
mapYの要素はy*2になります.

mapY

0 1 2 3 4
0 0 0 0 0 0
1 2 2 2 2 2
2 4 4 4 4 4
3 6 6 6 6 6
4 8 8 8 8 8

これを適応すると

mapY適応後

0 1 2 3 4
0 0,0 1,0 2,0 3,0
1
2 0,1 1,1 2,1 3,1
3
4 0,2 1,2 2,2 3,2

このように画素を移動できるのがremap関数です.
画像を拡大すると空白ができますが,引数に入れた補完方法で補完されます.
今回はmapXとmapYを生成する速度を競います.

レギュレーション

  • 作成するmapXとmapYの大きさは1920*1920とする.
  • 最終的にMat()が二つ作成する.
  • Mat.createやremap関数は計測外.あくまでmapXとmapYの生成速度を競う.
  • 要素には適当な計算式の結果を入れる.

1:2重forループで一個ずつputする.

  • 最も理解しやすく簡単に書ける方法.
一個ずつputする
val TAG = "ExecutionSpeedCheck"
val cols = 1920
val rows = 1920
val size = Size(cols.toDouble(), rows.toDouble())

val mapX1 = Mat()
val mapY1 = Mat()

mapX1.create(size, CvType.CV_32FC1)
mapY1.create(size, CvType.CV_32FC1)

Log.d(TAG,"1 start!")
for (i in 0 until rows) {
    for (j in 0 until cols) {
        val x = j.toDouble()
        val y = i.toDouble()
        val dx = x + sin(y/10.0)
        val dy = y + sin(x/10.0)
        mapX1.put(i, j, dx)
        mapY1.put(i, j, dy)
    }
}
Log.d(TAG,"1 end!")

結果

2重forループで一個ずつputする.
2023-03-20 13:46:30.842  6256-6256  ExecutionSpeedCheck     com.example.executionspeed           D  1 start!
2023-03-20 13:46:38.419  6256-6256  ExecutionSpeedCheck     com.example.executionspeed           D  1 end!
  • 7.577[s]
  • おっそ!

2:Mat.putって実は配列をputできるんですよ!

  • 先ほどのプログラムではputを1920×1920×2回実行した.
  • 実はputの第3引数には配列が入る.
配列をputする例
Mat.put(i, 0, FloatArray)
  • こいつを使えば1920×1920×2回ではなく1920×2回のputで済む.
配列をputする
val mapX2 = Mat()
val mapY2 = Mat()

mapX2.create(size, CvType.CV_32FC1)
mapY2.create(size, CvType.CV_32FC1)

Log.d(TAG,"2 start!")
for (i in 0 until rows) {

    val tmpX: MutableList<Float> = mutableListOf()
    val tmpY: MutableList<Float> = mutableListOf()

    for (j in 0 until cols) {
        val x = j.toDouble()
        val y = i.toDouble()
        val dx = x + sin(y/10.0)
        val dy = y + sin(x/10.0)
        tmpX.add(dx.toFloat())
        tmpX.add(dy.toFloat())
    }
    mapX1.put(i, 0, tmpX.toFloatArray())
    mapY1.put(i, 0, tmpY.toFloatArray())
}
Log.d(TAG,"2 end!")

結果

配列をputする.
2023-03-20 13:46:38.420  6256-6256  ExecutionSpeedCheck     com.example.executionspeed           D  2 start!
2023-03-20 13:46:38.863  6256-6256  ExecutionSpeedCheck     com.example.executionspeed           D  2 end!
  • 0.443[s]
  • めちゃめちゃ早くなった!
  • 約17倍早い実行速度に.

3:配列宣言にラムダ式を使う

  • さっきのプログラムからtmpXとtmpYを無くしてみる.
配列宣言にラムダ式を使った
val mapX3 = Mat()
val mapY3 = Mat()

mapX3.create(size, CvType.CV_32FC1)
mapY3.create(size, CvType.CV_32FC1)

Log.d(TAG,"3 start!")
for (i in 0 until rows) {
    mapX3.put(i, 0, FloatArray(cols) { j -> (j + sin(i/10.0)).toFloat() })
}
for (i in 0 until rows) {
    mapY3.put(i, 0, FloatArray(cols) { j -> (i + sin(j/10.0)).toFloat() })
}
Log.d(TAG,"3 end!")

待って、2重forループを2回やってない!!?

さっきまで2重forループ無いでmapXとmapYの計算を行っていた.
しかしラムダ式を使うということはmapXとmapY宣言を別々に書かなければならない.
一体どうなってしまうのか...

結果

配列宣言にラムダ式を使った
2023-03-20 13:46:38.864  6256-6256  ExecutionSpeedCheck     com.example.executionspeed           D  3 start!
2023-03-20 13:46:38.975  6256-6256  ExecutionSpeedCheck     com.example.executionspeed           D  3 end!
  • 0.111[s]
  • めっちゃ早い!!!

まとめ

  • 1の方法,一個づつput方法はあまりに遅い
  • 2の方法,配列をputする方法は1の方法と比べてかなり早い
  • 3の方法,ラムダ式を使うともっと早い

3の方法では1つの宣言にかかる時間は単純計算で0.055[s]
2の方法では0.443[s]かかったので約10個以上同時にmapを生成しないのであればラムダ式を使って一個づつ宣言したほうが早いと思われる.

と,いうことで

基本的にはラムダ式を使うほうが早い

という結果になりました.

ソースコード

MatPutTest.kt, MainActivity.kt
MatPutTest
import android.util.Log
import org.opencv.core.CvType
import org.opencv.core.Mat
import org.opencv.core.Size
import kotlin.math.sin

class MatPutTest {

    fun speedCheck() {
        val TAG = "ExecutionSpeedCheck"
        val cols = 1920
        val rows = 1920
        val size = Size(cols.toDouble(), rows.toDouble())

        val mapX1 = Mat()
        val mapY1 = Mat()

        mapX1.create(size, CvType.CV_32FC1)
        mapY1.create(size, CvType.CV_32FC1)

        Log.d(TAG,"1 start!")
        for (i in 0 until rows) {
            for (j in 0 until cols) {
                val x = j.toDouble()
                val y = i.toDouble()
                val dx = x + sin(y/10.0)
                val dy = y + sin(x/10.0)
                mapX1.put(i, j, dx)
                mapY1.put(i, j, dy)
            }
        }
        Log.d(TAG,"1 end!")

        val mapX2 = Mat()
        val mapY2 = Mat()

        mapX2.create(size, CvType.CV_32FC1)
        mapY2.create(size, CvType.CV_32FC1)

        Log.d(TAG,"2 start!")
        for (i in 0 until rows) {

            val tmpX: MutableList<Float> = mutableListOf()
            val tmpY: MutableList<Float> = mutableListOf()

            for (j in 0 until cols) {
                val x = j.toDouble()
                val y = i.toDouble()
                val dx = x + sin(y/10.0)
                val dy = y + sin(x/10.0)
                tmpX.add(dx.toFloat())
                tmpX.add(dy.toFloat())
            }
            mapX1.put(i, 0, tmpX.toFloatArray())
            mapY1.put(i, 0, tmpY.toFloatArray())
        }
        Log.d(TAG,"2 end!")

        val mapX3 = Mat()
        val mapY3 = Mat()

        mapX3.create(size, CvType.CV_32FC1)
        mapY3.create(size, CvType.CV_32FC1)

        Log.d(TAG,"3 start!")
        for (i in 0 until rows) {
            mapX3.put(i, 0, FloatArray(cols) { j -> (j + sin(i/10.0)).toFloat() })
        }
        for (i in 0 until rows) {
            mapY3.put(i, 0, FloatArray(cols) { j -> (i + sin(j/10.0)).toFloat() })
        }
        Log.d(TAG,"3 end!")
    }

}
MainActivity
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.util.Log
import org.opencv.android.BaseLoaderCallback
import org.opencv.android.LoaderCallbackInterface
import org.opencv.android.OpenCVLoader
import org.opencv.core.Mat
import kotlin.math.sin

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

    }

    // openCV読み込んだら実行
    private val mLoaderCallback: BaseLoaderCallback = object : BaseLoaderCallback(this) {
        override fun onManagerConnected(status: Int) {
            when (status) {
                SUCCESS -> {
                    Log.i("OpenCV", "OpenCV loaded successfully")
                    // openCVが使えるか確認
                    var imageMat = Mat()
                    // 実行速度テスト!
                     MatPutTest().speedCheck()
                }
                else -> {
                    super.onManagerConnected(status)
                }
            }
        }
    }

    override fun onResume() {
        super.onResume()
        // OpenCVがAndroidより読み込みが遅いのでOpenCVを読み込んだらコールバックする
        if (!OpenCVLoader.initDebug()) {
            Log.d(
                "OpenCV",
                "Internal OpenCV library not found. Using OpenCV Manager for initialization"
            )
            OpenCVLoader.initAsync(OpenCVLoader.OPENCV_VERSION_3_0_0, this, mLoaderCallback)
        } else {
            Log.d("OpenCV", "OpenCV library found inside package. Using it!")
            mLoaderCallback.onManagerConnected(LoaderCallbackInterface.SUCCESS)
        }
    }
}

Discussion