20201025のAndroidに関する記事は6件です。

【Android】【OpenCV】OCRの前処理のための画像処理(二値化・角度補正)

はじめに

Androidネイティブアプリで、カード状のものを写真で撮影してOCRを実行したかったのですが、
OCRの前処理として角度補正などの画像処理を行う必要がありました。

画像処理といえばOpenCV、ということで情報を探してみると、ちょうどやりたいこととぴったりな記事を発見しました。
OpenCVを使って免許証を角度補正(射影変換)する-二値化の閾値も自動で決定-

こちらの記事はPythonで書かれていたので、Android(Kotlin)向けに書き換えてみました。
※ PythonもOpenCVもほぼ初心者のため、誤りがあったらすみません。

環境

  • Android Studio 3.6.2
  • Kotlin 1.3.61
  • OpenCV 4.5.0

Android StudioにOpenCVを導入する手順については、以下の記事を参考にさせていただきました。
Android StudioでOpenCVを使う

ソースコード

さっそくソースコードです。
処理内容についての解説は元記事をご参照いただければと思います。

Sample.kt
import android.graphics.Bitmap
import org.opencv.android.Utils
import org.opencv.core.*
import org.opencv.imgproc.Imgproc
import kotlin.math.pow
import kotlin.math.roundToInt

object Sample {

    /**
     * 画像からカードを切り出す
     * @param bitmap 元画像
     * @return 切り出し後の画像
     */
    fun trimCard(bitmap: Bitmap): Bitmap? {
        // Bitmap -> Matに変換
        val imageMatOriginal = Mat()
        Utils.bitmapToMat(bitmap, imageMatOriginal)

        ////////////////////////////////
        // 画像を二値化する
        ////////////////////////////////
        val imageMat = imageMatOriginal.clone()
        // グレースケール化
        Imgproc.cvtColor(imageMat, imageMat, Imgproc.COLOR_RGB2GRAY)
        // 二値化の閾値算出
        val flatMat = arrayListOf<Double>()
        for (i in 0 until imageMat.rows()) {
            for (j in 0 until imageMat.cols()) {
                flatMat.add(imageMat[i, j][0])
            }
        }
        var thresholdValue = 100
        val cardLuminancePercentage = 0.2
        val numberThreshold = imageMat.width() * imageMat.height() * cardLuminancePercentage
        for (diffLuminance in 0 until 100) {
            val count = flatMat.count { it > (200 - diffLuminance).toDouble() }
            if (count >= numberThreshold) {
                thresholdValue = 200 - diffLuminance
                break
            }
        }
        println("** threshold: $thresholdValue **")
        // 二値化
        Imgproc.threshold(imageMat, imageMat, thresholdValue.toDouble(), 255.0, Imgproc.THRESH_BINARY)

        ////////////////////////////////
        // カードの輪郭を抽出する
        ////////////////////////////////
        val contours = ArrayList<MatOfPoint>()
        Imgproc.findContours(imageMat, contours, Mat(), Imgproc.RETR_TREE, Imgproc.CHAIN_APPROX_SIMPLE)
        // 面積が最大のものを選択
        val cardCnt = MatOfPoint2f()
        contours.maxBy { Imgproc.contourArea(it) }?.convertTo(cardCnt, CvType.CV_32F)

        ////////////////////////////////
        // 射影変換
        ////////////////////////////////
        // 輪郭を凸形で近似
        val epsilon = 0.1 * Imgproc.arcLength(cardCnt, true)
        val approx = MatOfPoint2f()
        Imgproc.approxPolyDP(cardCnt, approx, epsilon, true)

        if (approx.rows() != 4) {
            // 角が4つじゃない場合(四角形でない場合)は検出失敗としてnullを返す
            return null
        }

        // カードの横幅
        val cardImageLongSide = 2400.0
        val cardImageShortSide = (cardImageLongSide * (5.4 / 8.56)).roundToInt().toDouble()

        val line1Len = (approx[1, 0][0] - approx[0, 0][0]).pow(2) + (approx[1, 0][1] - approx[0, 0][1]).pow(2)
        val line2Len = (approx[3, 0][0] - approx[2, 0][0]).pow(2) + (approx[3, 0][1] - approx[2, 0][1]).pow(2)
        val line3Len = (approx[2, 0][0] - approx[1, 0][0]).pow(2) + (approx[2, 0][1] - approx[1, 0][1]).pow(2)
        val line4Len = (approx[0, 0][0] - approx[3, 0][0]).pow(2) + (approx[0, 0][1] - approx[3, 0][1]).pow(2)
        val targetLine1 = if (line1Len > line2Len) line1Len else line2Len
        val targetLine2 = if (line3Len > line4Len) line3Len else line4Len

        val cardImageWidth: Double
        val cardImageHeight: Double
        if (targetLine1 > targetLine2) {
            // 縦長
            cardImageWidth = cardImageShortSide
            cardImageHeight = cardImageLongSide
        } else {
            // 横長
            cardImageWidth = cardImageLongSide
            cardImageHeight = cardImageShortSide
        }

        val src = Mat(4, 2, CvType.CV_32F)
        for (i in 0 until 4) {
            src.put(i, 0, *(approx.get(i, 0)))
        }
        val dst = Mat(4, 2, CvType.CV_32F)
        dst.put(0, 0, 0.0, 0.0)
        dst.put(1, 0, 0.0, cardImageHeight)
        dst.put(2, 0, cardImageWidth, cardImageHeight)
        dst.put(3, 0, cardImageWidth, 0.0)

        val projectMatrix = Imgproc.getPerspectiveTransform(src, dst)

        val transformed = imageMatOriginal.clone()
        Imgproc.warpPerspective(imageMatOriginal, transformed, projectMatrix, Size(cardImageWidth, cardImageHeight))

        if (cardImageHeight > cardImageWidth) {
            // 縦長の場合は90度回転させる
            Core.rotate(transformed, transformed, Core.ROTATE_90_CLOCKWISE)
        }

        val newBitmap = Bitmap.createBitmap(transformed.width(), transformed.height(), Bitmap.Config.ARGB_8888)
        Utils.matToBitmap(transformed, newBitmap)
        return newBitmap
    }
}

一部元記事から改変している箇所もありますが、ほぼ同じ処理内容になっていると思います。

余談

PythonでもOpenCVをさわってみようと思いインストールしたのですが、
import numpy でエラーが発生してしまいました。
結果としては、Pythonのバージョンを3.9.0から3.8.6に変更したら動きました。
なぜかはよくわかりませんが、、、

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

[memo]ActivityResultContractでActivity間通信を行う

準備

build.gradle(app)
    implementation("androidx.activity:activity-ktx:1.2.0-alpha06")
    implementation("androidx.fragment:fragment-ktx:1.3.0-alpha06")

結果を受けとるActivity or Fragment側

xxx.kt
    private val launcher: ActivityResultLauncher<Intent> = registerForActivityResult(
        ActivityResultContracts.StartActivityForResult()
    ) {
        if (it.resultCode == Activity.RESULT_OK) {
            doSomething()
        }
    }

※公式ドキュメントではprepareCallとあるが、ライブラリバージョンアップ時にregisterForActivityResult()にメソッド名が変更されている

画面遷移時

xxx.kt
val intent = Intent(this, yyy::class.java)
launcher.launch(intent)

結果を送るActivity or Fragment側

yyy.kt
    // fragmentから呼び出す場合はrequireActivity or getActivityでActivityのインスタンスを取得する
    setResult(Activity.RESULT_OK)

値を渡したい場合は第二引数にIntentを渡す

Intentを作らなくても型安全に値を渡すことも可能

ActivityResultContractを使ったActivity間のデータ受け渡し

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

#19 Kotlin Koans Conventions/Invoke 解説

1 はじめに

Kotlin公式リファレンスのKotlin Koans/Invokeの解説記事です。

Kotlin Koansを通してKotlinを学習される人の参考になれば幸いです。

ただし、リファレンスを自力で読む力を養いたい方は、
すぐにこの記事に目を通さないで下さい!

一度各自で挑戦してから、お目通し頂ければと思います:fist:

2 Invoke関数

Invoke関数は、関数の呼び出しが通常の呼び出しより簡易になるという特徴を持ち、()だけで参照することができます。

つまり、

通常の関数呼び出しならばa.invoke()のようになるところが、

a()で呼び出すことができます。

ただし、本文のほうにも注意喚起がありますが、

あまり使いすぎるとコードの可読性が低下してしまうので注意が必要です。

3 Conventions/Invokeの解説

Kotlin Koans Conventions/Invokeの解説です。
随時本サイトの内容を引用させていただきます。

本文とコードを見てみましょう。

Objects with invoke() method can be invoked as a function.

You can add invoke extension for any class, but it's better not to overuse it:

fun Int.invoke() { println(this) }

1() //huh?..

Implement the function Invokable.invoke() so it would count a number of invocations.

Invoke
class Invokable {
    var numberOfInvocations: Int = 0
        private set
    operator fun invoke(): Invokable {
        TODO()
    }
}

fun invokeTwice(invokable: Invokable) = invokable()()

invoke()関数を実装して、invoke()関数が呼ばれた回数をカウントできるようにしていきます。

invoable()()は、invokable()で一度invoke()関数を呼び出し、戻り値(Invokable型のインスタンス)が再度()でinvoke()関数を呼び出しています。

invoke()は呼び出されるたびに、呼び出された回数(numberOfInvocations)に1を加え、かつ、Invokableインスタンスを返せばよいので、

以下が解答となります。

Invoke
class Invokable {
    var numberOfInvocations: Int = 0
        private set
    operator fun invoke(): Invokable {
        numberOfInvocations ++ 
        return this
    }
}

fun invokeTwice(invokable: Invokable) = invokable()()

4 最後に

次回はKotlin Koans Collections/Introducionの解説をします:muscle:

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

#18 Kotlin Koans Conventions/Destructuring declarations 解説

1 はじめに

Kotlin公式リファレンスのKotlin Koans/Destructuring declarationsの解説記事です。

Kotlin Koansを通してKotlinを学習される人の参考になれば幸いです。

ただし、リファレンスを自力で読む力を養いたい方は、
すぐにこの記事に目を通さないで下さい!

一度各自で挑戦してから、お目通し頂ければと思います:fist:

2 Destructuring Declarations

Destructuring Declarations

Destructuring Declarationsとは、複数の変数を同時に宣言できる仕組みです。

以下では、nameageという2つの変数が同時に宣言されています。

val (name, age) = person

各要素は別々に利用することが可能です。

println(name)
println(age)

以下のように、componetN()関数を利用して参照することも可能です。

val name = person.component1()
val age = person.component2()

componetN()Nの部分は宣言時の順序と一致します。

componetN()は、dataクラスで自動的に宣言される関数です。

なので、dataクラスではDestructuring declarationsが利用できます。

3 Conventions/Destructuring declarationsの解説

Kotlin Koans Conventions/Destructuring declarationsの解説です。
随時本サイトの内容を引用させていただきます。

本文とコードを見てみましょう。

Read about destructuring declarations and make the following code compile by adding one word.

Destructuring_declarations
/* TODO */class MyDate(val year: Int, val month: Int, val dayOfMonth: Int)

fun isLeapDay(date: MyDate): Boolean {

    val (year, month, dayOfMonth) = date

    // 29 February of a leap year
    return year % 4 == 0 && month == 2 && dayOfMonth == 29
}

val (year, month, dayOfMonth) = dateの部分で、Destructuring declarationsを利用しようとしています。

ただ、このままでは利用できません。

dateはMyDate型のインスタンスですが、MyDateクラスがdataクラスでは無いからです。

したがって、MyDateクラスをdataクラスにすればよいので以下が解答になります。

Destructuring_declarations
data class MyDate(val year: Int, val month: Int, val dayOfMonth: Int)

fun isLeapDay(date: MyDate): Boolean {

    val (year, month, dayOfMonth) = date

    // 29 February of a leap year
    return year % 4 == 0 && month == 2 && dayOfMonth == 29
}

4 最後に

次回はKotlin Koans Conventions/Invokeの解説をします:muscle:

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

Flutter系の記事のまとめ

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

#17 Kotlin Koans Conventions/Operators overloading 解説

1 はじめに

Kotlin公式リファレンスのKotlin Koans/Operators overloadingの解説記事です。

Kotlin Koansを通してKotlinを学習される人の参考になれば幸いです。

ただし、リファレンスを自力で読む力を養いたい方は、
すぐにこの記事に目を通さないで下さい!

一度各自で挑戦してから、お目通し頂ければと思います:fist:

2 plus()関数/times()関数

いずれの関数もoperator修飾子がついており、

a + ba.plus(b)

a * ba.times(b)

を表現します。

Operator overloading

3 Conventions/Operators overloadingの解説

Kotlin Koans Conventions/Operators overloadingの解説です。
随時本サイトの内容を引用させていただきます。

本文とコードを見てみましょう。

Implement a kind of date arithmetic. Support adding years, weeks and days to a date. You could be able to write the code like this: date + YEAR * 2 + WEEK * 3 + DAY * 15.

At first, add an extension function 'plus()' to MyDate, taking a TimeInterval as an argument. Use an utility function MyDate.addTimeIntervals() declared in DateUtil.kt

Then, try to support adding several time intervals to a date. You may need an extra class.

Operators_overloading
import TimeInterval.*

data class MyDate(val year: Int, val month: Int, val dayOfMonth: Int)

enum class TimeInterval { DAY, WEEK, YEAR }

operator fun MyDate.plus(timeInterval: TimeInterval): MyDate = TODO()

fun task1(today: MyDate): MyDate {
    return today + YEAR + WEEK
}

fun task2(today: MyDate): MyDate {
    TODO("Uncomment") //return today + YEAR * 2 + WEEK * 3 + DAY * 5
}
DateUtil.kt
import java.util.Calendar

fun MyDate.addTimeIntervals(timeInterval: TimeInterval,number: Int):MyDate{
    val c = Calendar.getInstance()
    c.set(year,month,dayOfMonth)
    when(timeInterval){
        TimeInterval.DAY -> c.add(Calendar.DAY_OF_MONTH,number)
        TimeInterval.WEEK -> c.add(Calendar.WEEK_OF_MONTH,number)
        TimeInterval.YEAR -> c.add(Calendar.YEAR,number)
    }
    return MyDate(c.get(Calendar.YEAR),c.get(Calendar.MONTH),c.get(Calendar.DATE))  
}

task1()を呼び出したら、today + YEAR + WEEKが正確に呼び出しもとに返るように、
task2()を呼び出したら、today + YEAR * 2 + WEEK * 3 + DAY * 5が正確に呼び出しもとに返るように実装します。


task1()から考えましょう。

MyDate型のtodayがtoday.plus(YEAR)のようにplus()関数を呼び出します。戻り値がplus()関数に引数WEEKを渡して参照します。

plus()関数の戻り値の設計にDateUtil.ktファイルaddTimeIntervals()を利用するように指示があるので、

Operators_overloading
operator fun MyDate.plus(timeInterval: TimeInterval) = addTimeIntervals(timeInterval, 1)

のように設計します。

(※addTimeIntervals()は、引数として受け取ったTimeInterval型のプロパティ(DAY/WEEK/YEAR)にnumberの数を加えたものを、MyDate型のインスタンスのプロパティに代入して戻り値とします。)


次に、task2()について考えてみましょう。

TODO("Uncomment")//を削除した、return today + YEAR * 2 + WEEK * 3 + DAY * 5がtake2()の戻り値となります。

YEAR * 2は、YEAR.times(2)を意味するので、times()を実装する必要があります。

なので、

Operators_overloading
operator fun TimeInterval.times(number: Int)

のようにtimes()を設定します。

return today + YEAR * 2 + WEEK * 3 + DAY * 5を考えると、
todayが呼び出すplus()の戻り値ではaddTimeintervals(timeInterval,1)そのまま利用したいところですが、

YEAR * 2 (YEAR.times(2))は戻り値としてTimeInterval型を返すことができないので、

TimeInterval型とnumberをプロパティとして持つクラスを作成する必要があります。

したがって、

Operators_overloading
class RepeatedTimeInterval(val timeInterval: TimeInterval,val number: Int)
operator fun TimeInterval.times(number: Int) = RepeatedTimeInterval(this, number)

のようにtimes()と新たにRepeatedTimeIntervalクラスを実装します。

つまり、YEAR * 2はRepeatedTimeInterval型のインスタンスが返ってくることになります。

これに伴って、todayがplus()に渡す引数は既存のplus()(引数としてTimeInterval型を受け取る。)では受け取ることができないので、

引数としてRepeatedTimeInterval型のインスタンスを受け取るplus()を実装します。

Operators_overloading
operator fun MyDate.plus(timeIntervals: RepeatedTimeInterval) = addTimeIntervals(timeIntervals.timeInterval, timeIntervals.number)

完成したコード全体は以下のようになります。

Operators_overloading
import TimeInterval.*

data class MyDate(val year: Int, val month: Int, val dayOfMonth: Int)

enum class TimeInterval { DAY, WEEK, YEAR }

operator fun MyDate.plus(timeInterval: TimeInterval) = addTimeIntervals(timeInterval, 1)

class RepeatedTimeInterval(val timeInterval: TimeInterval, val number: Int)
operator fun TimeInterval.times(number: Int) = RepeatedTimeInterval(this, number)

operator fun MyDate.plus(timeIntervals: RepeatedTimeInterval) = addTimeIntervals(timeIntervals.timeInterval, timeIntervals.number)

fun task1(today: MyDate): MyDate {
    return today + YEAR + WEEK
}

fun task2(today: MyDate): MyDate {
    return today + YEAR * 2 + WEEK * 3 + DAY * 5
}

4 最後に

次回はKotlin Koans Conventions/Destructuring declarationsの解説をします:muscle:

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