- 投稿日:2020-10-25T23:38:29+09:00
【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.ktimport 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に変更したら動きました。
なぜかはよくわかりませんが、、、
- 投稿日:2020-10-25T22:43:09+09:00
[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.ktprivate val launcher: ActivityResultLauncher<Intent> = registerForActivityResult( ActivityResultContracts.StartActivityForResult() ) { if (it.resultCode == Activity.RESULT_OK) { doSomething() } }※公式ドキュメントではprepareCallとあるが、ライブラリバージョンアップ時にregisterForActivityResult()にメソッド名が変更されている
画面遷移時
xxx.ktval 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を作らなくても型安全に値を渡すことも可能
- 投稿日:2020-10-25T21:46:36+09:00
#19 Kotlin Koans Conventions/Invoke 解説
1 はじめに
Kotlin公式リファレンスのKotlin Koans/Invokeの解説記事です。
Kotlin Koansを通してKotlinを学習される人の参考になれば幸いです。
ただし、リファレンスを自力で読む力を養いたい方は、
すぐにこの記事に目を通さないで下さい!一度各自で挑戦してから、お目通し頂ければと思います
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.
Invokeclass 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インスタンスを返せばよいので、
以下が解答となります。
Invokeclass Invokable { var numberOfInvocations: Int = 0 private set operator fun invoke(): Invokable { numberOfInvocations ++ return this } } fun invokeTwice(invokable: Invokable) = invokable()()4 最後に
次回はKotlin Koans Collections/Introducionの解説をします
- 投稿日:2020-10-25T20:46:32+09:00
#18 Kotlin Koans Conventions/Destructuring declarations 解説
1 はじめに
Kotlin公式リファレンスのKotlin Koans/Destructuring declarationsの解説記事です。
Kotlin Koansを通してKotlinを学習される人の参考になれば幸いです。
ただし、リファレンスを自力で読む力を養いたい方は、
すぐにこの記事に目を通さないで下さい!一度各自で挑戦してから、お目通し頂ければと思います
2 Destructuring Declarations
Destructuring Declarationsとは、複数の変数を同時に宣言できる仕組みです。
以下では、
name
とage
という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_declarationsdata 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の解説をします
- 投稿日:2020-10-25T17:04:45+09:00
Flutter系の記事のまとめ
まとめ
- 自分のFlutter関連の記事を整理しました
- 自分が一番の読者なのでw
Bitfinexアプリ系
実際のアプリを通して1通りの機能を整理
- Flutter/DartでBitfinexのレンディングアプリを作る(Part1 インストールと画面作成/画面遷移)
- Flutter/DartでBitfinexのレンディングアプリを作る(Part2 stateful/データの永続化)
- Flutterの"Widget of the Week"がめっちゃ勉強になったので、まとめてみた
- Flutter/DartでBitfinexのレンディングアプリを作る(Part3 グラフ描画/ファイル分割/設計書/アイコン)
- Flutter/DartでBitfinexのレンディングアプリを作る(Part4 バックグラウンド/mbaas/テスト)
リリース系
- Flutterで作ったアプリをAndroidとiOSとWebに同時にリリースする(Android編)
- Flutterで作ったアプリをAndroidとiOSとWebに同時にリリースする(Webアプリ編)
- Flutterで作ったアプリをAndroidとiOSとWebに同時にリリースする(iOS編)
Widget系
その他系
- 投稿日:2020-10-25T16:58:50+09:00
#17 Kotlin Koans Conventions/Operators overloading 解説
1 はじめに
Kotlin公式リファレンスのKotlin Koans/Operators overloadingの解説記事です。
Kotlin Koansを通してKotlinを学習される人の参考になれば幸いです。
ただし、リファレンスを自力で読む力を養いたい方は、
すぐにこの記事に目を通さないで下さい!一度各自で挑戦してから、お目通し頂ければと思います
2 plus()関数/times()関数
いずれの関数もoperator修飾子がついており、
a + b
がa.plus(b)
a * b
がa.times(b)
を表現します。
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_overloadingimport 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.ktimport 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_overloadingoperator 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_overloadingoperator 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_overloadingclass 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_overloadingoperator fun MyDate.plus(timeIntervals: RepeatedTimeInterval) = addTimeIntervals(timeIntervals.timeInterval, timeIntervals.number)完成したコード全体は以下のようになります。
Operators_overloadingimport 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の解説をします