Skip to content

Commit 1c5179d

Browse files
authored
Merge pull request #555 from code-payments/chore/improve-code-parsing-success
chore(scan): scan full static image at various scan qualities to improve parse success rate
2 parents 401a33d + 9248a50 commit 1c5179d

5 files changed

Lines changed: 93 additions & 50 deletions

File tree

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,34 @@
11
package com.kik.kikx.kikcodes
22

33
import com.kik.kikx.models.ScannableKikCode
4+
import com.kik.scan.KikCode
5+
import com.kik.scan.Scanner.ScanResult
6+
7+
sealed class ScanQuality(val headerValue: Int) {
8+
data object Low : ScanQuality(0)
9+
data object Medium : ScanQuality(3)
10+
data object High : ScanQuality(8)
11+
data object Best : ScanQuality(10)
12+
13+
companion object {
14+
private val values = listOf(Low, Medium, High, Best)
15+
16+
fun iterator(): Iterator<ScanQuality> {
17+
return values.iterator()
18+
}
19+
}
20+
}
21+
22+
open class ScannerError(override val message: String) : Exception(message)
423

524
interface KikCodeScanner {
6-
class NoKikCodeFoundException : Exception("No Kik Code found in image buffer")
25+
class NoKikCodeFoundException : ScannerError("No Kik Code found in image buffer")
26+
class FailedToParseCodeException(val scanResult: ScanResult) :
27+
ScannerError("Code found in image buffer, but failed to parse")
28+
29+
class UnsupportedKikCodeFoundException(val kikCode: KikCode) : ScannerError("Code found in unsupported")
730

8-
suspend fun scanKikCode(imageData: ByteArray, width: Int, height: Int): Result<ScannableKikCode>
31+
suspend fun scanKikCode(
32+
imageData: ByteArray, width: Int, height: Int, quality: ScanQuality = ScanQuality.Medium
33+
): Result<ScannableKikCode>
934
}

app/src/main/java/com/kik/kikx/kikcodes/implementation/KikCodeAnalyzer.kt

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import com.getcode.media.MediaScanner
1010
import com.getcode.util.toByteArray
1111
import com.getcode.utils.ErrorUtils
1212
import com.kik.kikx.kikcodes.KikCodeScanner
13+
import com.kik.kikx.kikcodes.ScannerError
1314
import com.kik.kikx.models.ScannableKikCode
1415
import dagger.hilt.android.qualifiers.ApplicationContext
1516
import kotlinx.coroutines.CoroutineScope
@@ -55,7 +56,7 @@ class KikCodeAnalyzer @Inject constructor(
5556

5657
}.onFailure { error ->
5758
when (error) {
58-
is KikCodeScanner.NoKikCodeFoundException -> Unit
59+
is ScannerError -> Unit
5960
else -> ErrorUtils.handleError(error)
6061
}
6162
imageProxy.close()
@@ -70,7 +71,7 @@ class KikCodeAnalyzer @Inject constructor(
7071
onCodeScanned(result)
7172
}.onFailure { error ->
7273
when (error) {
73-
is KikCodeScanner.NoKikCodeFoundException -> onNoCodeFound()
74+
is ScannerError -> onNoCodeFound()
7475
else -> ErrorUtils.handleError(error)
7576
}
7677
}

app/src/main/java/com/kik/kikx/kikcodes/implementation/KikCodeScannerImpl.kt

Lines changed: 14 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package com.kik.kikx.kikcodes.implementation
22

33
import android.util.Base64
44
import com.kik.kikx.kikcodes.KikCodeScanner
5+
import com.kik.kikx.kikcodes.ScanQuality
56
import com.kik.kikx.models.GroupInviteCode
67
import com.kik.kikx.models.ScannableKikCode
78
import com.kik.scan.GroupKikCode
@@ -12,10 +13,6 @@ import com.kik.scan.UsernameKikCode
1213

1314
class KikCodeScannerImpl : KikCodeScanner {
1415

15-
companion object {
16-
private const val SCAN_QUALITY = 3
17-
}
18-
1916
private fun KikCode.toModelKikCode(): ScannableKikCode {
2017
return when (this) {
2118
is GroupKikCode -> {
@@ -27,16 +24,23 @@ class KikCodeScannerImpl : KikCodeScanner {
2724
}
2825
is UsernameKikCode -> ScannableKikCode.UsernameKikCode(username, nonce, colour)
2926
is RemoteKikCode -> ScannableKikCode.RemoteKikCode(payloadId, colour)
30-
else -> throw Exception("Unsupported Kik code type")
27+
else -> throw KikCodeScanner.UnsupportedKikCodeFoundException(this)
3128
}
3229
}
3330

34-
override suspend fun scanKikCode(imageData: ByteArray, width: Int, height: Int): Result<ScannableKikCode> {
35-
val source = PlanarYUVLuminanceSource(imageData, width, height, 0, 0, width, height, false)
31+
override suspend fun scanKikCode(imageData: ByteArray, width: Int, height: Int, quality: ScanQuality): Result<ScannableKikCode> {
32+
val source = PlanarYUVLuminanceSource(imageData, width, height)
33+
34+
try {
35+
val scanResult = Scanner.scan(source.matrix, width, height, quality.headerValue)
36+
?: return Result.failure(KikCodeScanner.NoKikCodeFoundException())
37+
38+
val kikCode = KikCode.parse(scanResult.data)
39+
?: return Result.failure(KikCodeScanner.FailedToParseCodeException(scanResult))
40+
41+
val scannable = kikCode.toModelKikCode() // will throw UnsupportedKikCodeFoundException
3642

37-
return try {
38-
val scanResult = Scanner.scan(source.matrix, width, height, SCAN_QUALITY) ?: throw KikCodeScanner.NoKikCodeFoundException()
39-
runCatching { KikCode.parse(scanResult.data)?.toModelKikCode() ?: throw KikCodeScanner.NoKikCodeFoundException() }
43+
return Result.success(scannable)
4044
} catch (e: Exception) {
4145
return Result.failure(e)
4246
}

app/src/main/java/com/kik/kikx/kikcodes/implementation/PlanarYUVLuminanceSource.kt

Lines changed: 28 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -33,8 +33,16 @@ import kotlin.experimental.and
3333
* jmeyer: NOTE
3434
* This class used to extend LuminanceSource. It has been trimmed down to not require a ZXing import
3535
*/
36-
class PlanarYUVLuminanceSource(private val _yuvData: ByteArray, private val _dataWidth: Int, private val _dataHeight: Int, private val _left: Int, private val _top: Int, private val width: Int, private val height: Int, reverseHorizontal: Boolean) {
37-
36+
class PlanarYUVLuminanceSource(
37+
private val yuvData: ByteArray,
38+
private val width: Int,
39+
private val height: Int,
40+
private val left: Int = 0,
41+
private val top: Int = 0,
42+
private val dataWidth: Int = width,
43+
private val dataHeight: Int = height,
44+
reverseHorizontal: Boolean = false
45+
) {
3846
// If the caller asks for the entire underlying image, save the copy and give them the
3947
// original data. The docs specifically warn that result.length must be ignored.
4048
// If the width matches the full width of the underlying data, perform a single copy.
@@ -43,22 +51,22 @@ class PlanarYUVLuminanceSource(private val _yuvData: ByteArray, private val _dat
4351
get() {
4452
val width = width
4553
val height = height
46-
if (width == _dataWidth && height == _dataHeight) {
47-
return _yuvData
54+
if (width == dataWidth && height == dataHeight) {
55+
return yuvData
4856
}
4957

5058
val area = width * height
5159
val matrix = ByteArray(area)
52-
var inputOffset = _top * _dataWidth + _left
53-
if (width == _dataWidth) {
54-
System.arraycopy(_yuvData, inputOffset, matrix, 0, area)
60+
var inputOffset = top * dataWidth + left
61+
if (width == dataWidth) {
62+
System.arraycopy(yuvData, inputOffset, matrix, 0, area)
5563
return matrix
5664
}
57-
val yuv = _yuvData
65+
val yuv = yuvData
5866
for (y in 0 until height) {
5967
val outputOffset = y * width
6068
System.arraycopy(yuv, inputOffset, matrix, outputOffset, width)
61-
inputOffset += _dataWidth
69+
inputOffset += dataWidth
6270
}
6371
return matrix
6472
}
@@ -68,7 +76,7 @@ class PlanarYUVLuminanceSource(private val _yuvData: ByteArray, private val _dat
6876

6977
init {
7078

71-
if (_left + width > _dataWidth || _top + height > _dataHeight) {
79+
if (left + width > dataWidth || top + height > dataHeight) {
7280
// LogUtils.throwOrLog(IllegalArgumentException("Crop rectangle does not fit within image data."))
7381
}
7482
if (reverseHorizontal) {
@@ -85,29 +93,29 @@ class PlanarYUVLuminanceSource(private val _yuvData: ByteArray, private val _dat
8593
if (row == null || row.size < width) {
8694
row = ByteArray(width)
8795
}
88-
val offset = (y + _top) * _dataWidth + _left
89-
System.arraycopy(_yuvData, offset, row, 0, width)
96+
val offset = (y + top) * dataWidth + left
97+
System.arraycopy(yuvData, offset, row, 0, width)
9098
return row
9199
}
92100

93-
fun crop(left: Int, top: Int, width: Int, height: Int): PlanarYUVLuminanceSource {
94-
return PlanarYUVLuminanceSource(_yuvData, _dataWidth, _dataHeight, this._left + left, this._top + top, width, height, false)
101+
fun crop(left: Int = 0, top: Int = 0, width: Int, height: Int): PlanarYUVLuminanceSource {
102+
return PlanarYUVLuminanceSource(yuvData, dataWidth, dataHeight, this.left + left, this.top + top, width, height, false)
95103
}
96104

97105
fun renderCroppedGreyscaleBitmap(): Bitmap {
98106
val width = width
99107
val height = height
100108
val pixels = IntArray(width * height)
101-
val yuv = _yuvData
102-
var inputOffset = _top * _dataWidth + _left
109+
val yuv = yuvData
110+
var inputOffset = top * dataWidth + left
103111

104112
for (y in 0 until height) {
105113
val outputOffset = y * width
106114
for (x in 0 until width) {
107115
val grey = yuv[inputOffset + x] and 0xff.toByte()
108116
pixels[outputOffset + x] = -0x1000000 or grey * 0x00010101
109117
}
110-
inputOffset += _dataWidth
118+
inputOffset += dataWidth
111119
}
112120

113121
val bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888)
@@ -116,9 +124,9 @@ class PlanarYUVLuminanceSource(private val _yuvData: ByteArray, private val _dat
116124
}
117125

118126
private fun reverseHorizontal(width: Int, height: Int) {
119-
val yuvData = this._yuvData
127+
val yuvData = this.yuvData
120128
var y = 0
121-
var rowStart = _top * _dataWidth + _left
129+
var rowStart = top * dataWidth + left
122130
while (y < height) {
123131
val middle = rowStart + width / 2
124132
var x1 = rowStart
@@ -131,7 +139,7 @@ class PlanarYUVLuminanceSource(private val _yuvData: ByteArray, private val _dat
131139
x2--
132140
}
133141
y++
134-
rowStart += _dataWidth
142+
rowStart += dataWidth
135143
}
136144
}
137145
}

app/src/main/java/com/kik/kikx/kikcodes/implementation/StaticImageHelper.kt

Lines changed: 21 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,15 @@ import android.icu.text.DateFormat
77
import android.icu.text.SimpleDateFormat
88
import android.net.Uri
99
import android.os.Environment
10+
import com.getcode.BuildConfig
1011
import com.getcode.analytics.AnalyticsService
1112
import com.getcode.util.save
1213
import com.getcode.util.toByteArray
1314
import com.getcode.util.uriToBitmap
1415
import com.getcode.utils.TraceType
1516
import com.getcode.utils.timedTraceSuspend
1617
import com.kik.kikx.kikcodes.KikCodeScanner
18+
import com.kik.kikx.kikcodes.ScanQuality
1719
import com.kik.kikx.models.ScannableKikCode
1820
import dagger.hilt.android.qualifiers.ApplicationContext
1921
import kotlinx.coroutines.Dispatchers
@@ -32,11 +34,12 @@ class StaticImageHelper @Inject constructor(
3234
suspend fun analyze(uri: Uri): Result<ScannableKikCode> {
3335
val bitmap = context.uriToBitmap(uri)
3436
return if (bitmap != null) {
35-
detectCodeInImage(bitmap) {
37+
detectCodeInImage(bitmap) { image, quality ->
3638
scanner.scanKikCode(
37-
it.toByteArray(),
38-
it.width,
39-
it.height,
39+
image.toByteArray(),
40+
image.width,
41+
image.height,
42+
quality
4043
)
4144
}
4245
} else {
@@ -46,7 +49,7 @@ class StaticImageHelper @Inject constructor(
4649

4750
private suspend fun detectCodeInImage(
4851
bitmap: Bitmap,
49-
scan: suspend (Bitmap) -> Result<ScannableKikCode>
52+
scan: suspend (Bitmap, ScanQuality) -> Result<ScannableKikCode>
5053
): Result<ScannableKikCode> = withContext(Dispatchers.Default) {
5154
val destinationRoot =
5255
Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES)
@@ -63,7 +66,7 @@ class StaticImageHelper @Inject constructor(
6366
private suspend fun search(
6467
bitmap: Bitmap,
6568
destination: File,
66-
scan: suspend (Bitmap) -> Result<ScannableKikCode>,
69+
scan: suspend (Bitmap, ScanQuality) -> Result<ScannableKikCode>,
6770
): Result<ScannableKikCode> {
6871
return timedTraceSuspend(
6972
message = "analyzing image",
@@ -73,22 +76,24 @@ class StaticImageHelper @Inject constructor(
7376
analytics.photoScanned(result.isSuccess, time.inWholeMilliseconds)
7477
}
7578
) {
76-
// try scanning raw
77-
val raw = scan(bitmap)
78-
if (raw.isSuccess) {
79-
debugPrint("Code found raw")
80-
bitmap.recycle()
81-
return@timedTraceSuspend raw
82-
} else {
83-
debugPrint("No Code found via raw")
79+
// try scanning raw at various scan qualities
80+
for (quality in ScanQuality.iterator()) {
81+
val raw = scan(bitmap, quality)
82+
if (raw.isSuccess) {
83+
debugPrint("Code found raw using $quality")
84+
bitmap.recycle()
85+
return@timedTraceSuspend raw
86+
} else {
87+
debugPrint("No Code found via raw using $quality")
88+
}
8489
}
8590

8691
val zoomLevels = listOf(1.0)
8792
val result = slidingWindowSearch(
8893
bitmap = bitmap,
8994
destination = destination,
9095
zoomLevels = zoomLevels,
91-
scan = scan,
96+
scan = { scan(it, ScanQuality.Medium) },
9297
)
9398

9499
if (result.isSuccess) {
@@ -235,5 +240,5 @@ private fun debugPrint(message: String) {
235240
if (DEBUG) println(message)
236241
}
237242

238-
private const val DEBUG = true
243+
private val DEBUG = BuildConfig.DEBUG
239244
private const val SAVE_IMAGES = false

0 commit comments

Comments
 (0)