Skip to content

Commit

Permalink
Merge pull request #555 from code-payments/chore/improve-code-parsing…
Browse files Browse the repository at this point in the history
…-success

chore(scan): scan full static image at various scan qualities to improve parse success rate
  • Loading branch information
bmc08gt authored Sep 10, 2024
2 parents 401a33d + 9248a50 commit 1c5179d
Show file tree
Hide file tree
Showing 5 changed files with 93 additions and 50 deletions.
29 changes: 27 additions & 2 deletions app/src/main/java/com/kik/kikx/kikcodes/KikCodeScanner.kt
Original file line number Diff line number Diff line change
@@ -1,9 +1,34 @@
package com.kik.kikx.kikcodes

import com.kik.kikx.models.ScannableKikCode
import com.kik.scan.KikCode
import com.kik.scan.Scanner.ScanResult

sealed class ScanQuality(val headerValue: Int) {
data object Low : ScanQuality(0)
data object Medium : ScanQuality(3)
data object High : ScanQuality(8)
data object Best : ScanQuality(10)

companion object {
private val values = listOf(Low, Medium, High, Best)

fun iterator(): Iterator<ScanQuality> {
return values.iterator()
}
}
}

open class ScannerError(override val message: String) : Exception(message)

interface KikCodeScanner {
class NoKikCodeFoundException : Exception("No Kik Code found in image buffer")
class NoKikCodeFoundException : ScannerError("No Kik Code found in image buffer")
class FailedToParseCodeException(val scanResult: ScanResult) :
ScannerError("Code found in image buffer, but failed to parse")

class UnsupportedKikCodeFoundException(val kikCode: KikCode) : ScannerError("Code found in unsupported")

suspend fun scanKikCode(imageData: ByteArray, width: Int, height: Int): Result<ScannableKikCode>
suspend fun scanKikCode(
imageData: ByteArray, width: Int, height: Int, quality: ScanQuality = ScanQuality.Medium
): Result<ScannableKikCode>
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import com.getcode.media.MediaScanner
import com.getcode.util.toByteArray
import com.getcode.utils.ErrorUtils
import com.kik.kikx.kikcodes.KikCodeScanner
import com.kik.kikx.kikcodes.ScannerError
import com.kik.kikx.models.ScannableKikCode
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.CoroutineScope
Expand Down Expand Up @@ -55,7 +56,7 @@ class KikCodeAnalyzer @Inject constructor(

}.onFailure { error ->
when (error) {
is KikCodeScanner.NoKikCodeFoundException -> Unit
is ScannerError -> Unit
else -> ErrorUtils.handleError(error)
}
imageProxy.close()
Expand All @@ -70,7 +71,7 @@ class KikCodeAnalyzer @Inject constructor(
onCodeScanned(result)
}.onFailure { error ->
when (error) {
is KikCodeScanner.NoKikCodeFoundException -> onNoCodeFound()
is ScannerError -> onNoCodeFound()
else -> ErrorUtils.handleError(error)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package com.kik.kikx.kikcodes.implementation

import android.util.Base64
import com.kik.kikx.kikcodes.KikCodeScanner
import com.kik.kikx.kikcodes.ScanQuality
import com.kik.kikx.models.GroupInviteCode
import com.kik.kikx.models.ScannableKikCode
import com.kik.scan.GroupKikCode
Expand All @@ -12,10 +13,6 @@ import com.kik.scan.UsernameKikCode

class KikCodeScannerImpl : KikCodeScanner {

companion object {
private const val SCAN_QUALITY = 3
}

private fun KikCode.toModelKikCode(): ScannableKikCode {
return when (this) {
is GroupKikCode -> {
Expand All @@ -27,16 +24,23 @@ class KikCodeScannerImpl : KikCodeScanner {
}
is UsernameKikCode -> ScannableKikCode.UsernameKikCode(username, nonce, colour)
is RemoteKikCode -> ScannableKikCode.RemoteKikCode(payloadId, colour)
else -> throw Exception("Unsupported Kik code type")
else -> throw KikCodeScanner.UnsupportedKikCodeFoundException(this)
}
}

override suspend fun scanKikCode(imageData: ByteArray, width: Int, height: Int): Result<ScannableKikCode> {
val source = PlanarYUVLuminanceSource(imageData, width, height, 0, 0, width, height, false)
override suspend fun scanKikCode(imageData: ByteArray, width: Int, height: Int, quality: ScanQuality): Result<ScannableKikCode> {
val source = PlanarYUVLuminanceSource(imageData, width, height)

try {
val scanResult = Scanner.scan(source.matrix, width, height, quality.headerValue)
?: return Result.failure(KikCodeScanner.NoKikCodeFoundException())

val kikCode = KikCode.parse(scanResult.data)
?: return Result.failure(KikCodeScanner.FailedToParseCodeException(scanResult))

val scannable = kikCode.toModelKikCode() // will throw UnsupportedKikCodeFoundException

return try {
val scanResult = Scanner.scan(source.matrix, width, height, SCAN_QUALITY) ?: throw KikCodeScanner.NoKikCodeFoundException()
runCatching { KikCode.parse(scanResult.data)?.toModelKikCode() ?: throw KikCodeScanner.NoKikCodeFoundException() }
return Result.success(scannable)
} catch (e: Exception) {
return Result.failure(e)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,16 @@ import kotlin.experimental.and
* jmeyer: NOTE
* This class used to extend LuminanceSource. It has been trimmed down to not require a ZXing import
*/
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) {

class PlanarYUVLuminanceSource(
private val yuvData: ByteArray,
private val width: Int,
private val height: Int,
private val left: Int = 0,
private val top: Int = 0,
private val dataWidth: Int = width,
private val dataHeight: Int = height,
reverseHorizontal: Boolean = false
) {
// If the caller asks for the entire underlying image, save the copy and give them the
// original data. The docs specifically warn that result.length must be ignored.
// If the width matches the full width of the underlying data, perform a single copy.
Expand All @@ -43,22 +51,22 @@ class PlanarYUVLuminanceSource(private val _yuvData: ByteArray, private val _dat
get() {
val width = width
val height = height
if (width == _dataWidth && height == _dataHeight) {
return _yuvData
if (width == dataWidth && height == dataHeight) {
return yuvData
}

val area = width * height
val matrix = ByteArray(area)
var inputOffset = _top * _dataWidth + _left
if (width == _dataWidth) {
System.arraycopy(_yuvData, inputOffset, matrix, 0, area)
var inputOffset = top * dataWidth + left
if (width == dataWidth) {
System.arraycopy(yuvData, inputOffset, matrix, 0, area)
return matrix
}
val yuv = _yuvData
val yuv = yuvData
for (y in 0 until height) {
val outputOffset = y * width
System.arraycopy(yuv, inputOffset, matrix, outputOffset, width)
inputOffset += _dataWidth
inputOffset += dataWidth
}
return matrix
}
Expand All @@ -68,7 +76,7 @@ class PlanarYUVLuminanceSource(private val _yuvData: ByteArray, private val _dat

init {

if (_left + width > _dataWidth || _top + height > _dataHeight) {
if (left + width > dataWidth || top + height > dataHeight) {
// LogUtils.throwOrLog(IllegalArgumentException("Crop rectangle does not fit within image data."))
}
if (reverseHorizontal) {
Expand All @@ -85,29 +93,29 @@ class PlanarYUVLuminanceSource(private val _yuvData: ByteArray, private val _dat
if (row == null || row.size < width) {
row = ByteArray(width)
}
val offset = (y + _top) * _dataWidth + _left
System.arraycopy(_yuvData, offset, row, 0, width)
val offset = (y + top) * dataWidth + left
System.arraycopy(yuvData, offset, row, 0, width)
return row
}

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

fun renderCroppedGreyscaleBitmap(): Bitmap {
val width = width
val height = height
val pixels = IntArray(width * height)
val yuv = _yuvData
var inputOffset = _top * _dataWidth + _left
val yuv = yuvData
var inputOffset = top * dataWidth + left

for (y in 0 until height) {
val outputOffset = y * width
for (x in 0 until width) {
val grey = yuv[inputOffset + x] and 0xff.toByte()
pixels[outputOffset + x] = -0x1000000 or grey * 0x00010101
}
inputOffset += _dataWidth
inputOffset += dataWidth
}

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

private fun reverseHorizontal(width: Int, height: Int) {
val yuvData = this._yuvData
val yuvData = this.yuvData
var y = 0
var rowStart = _top * _dataWidth + _left
var rowStart = top * dataWidth + left
while (y < height) {
val middle = rowStart + width / 2
var x1 = rowStart
Expand All @@ -131,7 +139,7 @@ class PlanarYUVLuminanceSource(private val _yuvData: ByteArray, private val _dat
x2--
}
y++
rowStart += _dataWidth
rowStart += dataWidth
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,15 @@ import android.icu.text.DateFormat
import android.icu.text.SimpleDateFormat
import android.net.Uri
import android.os.Environment
import com.getcode.BuildConfig
import com.getcode.analytics.AnalyticsService
import com.getcode.util.save
import com.getcode.util.toByteArray
import com.getcode.util.uriToBitmap
import com.getcode.utils.TraceType
import com.getcode.utils.timedTraceSuspend
import com.kik.kikx.kikcodes.KikCodeScanner
import com.kik.kikx.kikcodes.ScanQuality
import com.kik.kikx.models.ScannableKikCode
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.Dispatchers
Expand All @@ -32,11 +34,12 @@ class StaticImageHelper @Inject constructor(
suspend fun analyze(uri: Uri): Result<ScannableKikCode> {
val bitmap = context.uriToBitmap(uri)
return if (bitmap != null) {
detectCodeInImage(bitmap) {
detectCodeInImage(bitmap) { image, quality ->
scanner.scanKikCode(
it.toByteArray(),
it.width,
it.height,
image.toByteArray(),
image.width,
image.height,
quality
)
}
} else {
Expand All @@ -46,7 +49,7 @@ class StaticImageHelper @Inject constructor(

private suspend fun detectCodeInImage(
bitmap: Bitmap,
scan: suspend (Bitmap) -> Result<ScannableKikCode>
scan: suspend (Bitmap, ScanQuality) -> Result<ScannableKikCode>
): Result<ScannableKikCode> = withContext(Dispatchers.Default) {
val destinationRoot =
Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES)
Expand All @@ -63,7 +66,7 @@ class StaticImageHelper @Inject constructor(
private suspend fun search(
bitmap: Bitmap,
destination: File,
scan: suspend (Bitmap) -> Result<ScannableKikCode>,
scan: suspend (Bitmap, ScanQuality) -> Result<ScannableKikCode>,
): Result<ScannableKikCode> {
return timedTraceSuspend(
message = "analyzing image",
Expand All @@ -73,22 +76,24 @@ class StaticImageHelper @Inject constructor(
analytics.photoScanned(result.isSuccess, time.inWholeMilliseconds)
}
) {
// try scanning raw
val raw = scan(bitmap)
if (raw.isSuccess) {
debugPrint("Code found raw")
bitmap.recycle()
return@timedTraceSuspend raw
} else {
debugPrint("No Code found via raw")
// try scanning raw at various scan qualities
for (quality in ScanQuality.iterator()) {
val raw = scan(bitmap, quality)
if (raw.isSuccess) {
debugPrint("Code found raw using $quality")
bitmap.recycle()
return@timedTraceSuspend raw
} else {
debugPrint("No Code found via raw using $quality")
}
}

val zoomLevels = listOf(1.0)
val result = slidingWindowSearch(
bitmap = bitmap,
destination = destination,
zoomLevels = zoomLevels,
scan = scan,
scan = { scan(it, ScanQuality.Medium) },
)

if (result.isSuccess) {
Expand Down Expand Up @@ -235,5 +240,5 @@ private fun debugPrint(message: String) {
if (DEBUG) println(message)
}

private const val DEBUG = true
private val DEBUG = BuildConfig.DEBUG
private const val SAVE_IMAGES = false

0 comments on commit 1c5179d

Please sign in to comment.