Save about 400ms at capture by delegating the rotation to OpenCV

This commit is contained in:
Pierre-Yves Nicolas
2025-06-19 19:24:01 +02:00
parent 453a8b3fb0
commit 7056393f56
4 changed files with 23 additions and 15 deletions

View File

@@ -57,7 +57,7 @@ class DocumentDetectionTest {
if (quad != null) { if (quad != null) {
val resizedQuad = val resizedQuad =
quad.scaledTo(mask.width, mask.height, bitmap.width, bitmap.height) quad.scaledTo(mask.width, mask.height, bitmap.width, bitmap.height)
outputBitmap = extractDocument(bitmap, resizedQuad) outputBitmap = extractDocument(bitmap, resizedQuad, 0)
val file = File(context.getExternalFilesDir(null), imageFileName) val file = File(context.getExternalFilesDir(null), imageFileName)
FileOutputStream(file).use { FileOutputStream(file).use {
outputBitmap.compress(Bitmap.CompressFormat.JPEG, 95, it) outputBitmap.compress(Bitmap.CompressFormat.JPEG, 95, it)

View File

@@ -15,7 +15,9 @@
package org.mydomain.myscan package org.mydomain.myscan
import android.graphics.Bitmap import android.graphics.Bitmap
import androidx.core.graphics.createBitmap
import org.opencv.android.Utils import org.opencv.android.Utils
import org.opencv.core.Core
import org.opencv.core.Mat import org.opencv.core.Mat
import org.opencv.core.MatOfPoint import org.opencv.core.MatOfPoint
import org.opencv.core.MatOfPoint2f import org.opencv.core.MatOfPoint2f
@@ -24,7 +26,6 @@ import org.opencv.imgproc.Imgproc
import kotlin.math.abs import kotlin.math.abs
import kotlin.math.max import kotlin.math.max
import kotlin.math.sqrt import kotlin.math.sqrt
import androidx.core.graphics.createBitmap
fun detectDocumentQuad(mask: Bitmap): Quad? { fun detectDocumentQuad(mask: Bitmap): Quad? {
val mat = Mat() val mat = Mat()
@@ -65,7 +66,7 @@ fun detectDocumentQuad(mask: Bitmap): Quad? {
return createQuad(vertices) return createQuad(vertices)
} }
fun extractDocument(originalBitmap: Bitmap, quad: Quad): Bitmap { fun extractDocument(originalBitmap: Bitmap, quad: Quad, rotationDegrees: Int): Bitmap {
val widthTop = norm(quad.topLeft, quad.topRight) val widthTop = norm(quad.topLeft, quad.topRight)
val widthBottom = norm(quad.bottomLeft, quad.bottomRight) val widthBottom = norm(quad.bottomLeft, quad.bottomRight)
val maxWidth = max(widthTop, widthBottom).toInt() val maxWidth = max(widthTop, widthBottom).toInt()
@@ -96,11 +97,23 @@ fun extractDocument(originalBitmap: Bitmap, quad: Quad): Bitmap {
val enhanced = enhanceCapturedImage(outputMat) val enhanced = enhanceCapturedImage(outputMat)
return toBitmap(enhanced, maxWidth, maxHeight) return toBitmap(rotate(enhanced, rotationDegrees))
} }
private fun toBitmap(mat: Mat, width: Int, height: Int): Bitmap { fun rotate(input: Mat, degrees: Int): Mat {
val outputBitmap = createBitmap(width, height) val output = Mat()
when ((degrees % 360 + 360) % 360) {
0 -> input.copyTo(output)
90 -> Core.rotate(input, output, Core.ROTATE_90_CLOCKWISE)
180 -> Core.rotate(input, output, Core.ROTATE_180)
270 -> Core.rotate(input, output, Core.ROTATE_90_COUNTERCLOCKWISE)
else -> throw IllegalArgumentException("Only 0, 90, 180, 270 degrees are supported")
}
return output
}
private fun toBitmap(mat: Mat): Bitmap {
val outputBitmap = createBitmap(mat.cols(), mat.rows())
Utils.matToBitmap(mat, outputBitmap) Utils.matToBitmap(mat, outputBitmap)
return outputBitmap return outputBitmap
} }

View File

@@ -17,7 +17,6 @@ package org.mydomain.myscan
import android.content.Context import android.content.Context
import android.graphics.Bitmap import android.graphics.Bitmap
import android.graphics.BitmapFactory import android.graphics.BitmapFactory
import android.graphics.Matrix
import androidx.camera.core.ImageProxy import androidx.camera.core.ImageProxy
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.ViewModelProvider
@@ -109,25 +108,19 @@ class MainViewModel(
private suspend fun processCapturedImage(imageProxy: ImageProxy): Bitmap? = withContext(Dispatchers.IO) { private suspend fun processCapturedImage(imageProxy: ImageProxy): Bitmap? = withContext(Dispatchers.IO) {
var corrected: Bitmap? = null var corrected: Bitmap? = null
val bitmap = imageProxy.toBitmap().rotate(imageProxy.imageInfo.rotationDegrees) var bitmap = imageProxy.toBitmap()
val segmentation = imageSegmentationService.runSegmentationAndReturn(bitmap, 0) val segmentation = imageSegmentationService.runSegmentationAndReturn(bitmap, 0)
if (segmentation != null) { if (segmentation != null) {
val mask = segmentation.segmentation.toBinaryMask() val mask = segmentation.segmentation.toBinaryMask()
val quad = detectDocumentQuad(mask) val quad = detectDocumentQuad(mask)
if (quad != null) { if (quad != null) {
val resizedQuad = quad.scaledTo(mask.width, mask.height, bitmap.width, bitmap.height) val resizedQuad = quad.scaledTo(mask.width, mask.height, bitmap.width, bitmap.height)
corrected = extractDocument(bitmap, resizedQuad) corrected = extractDocument(bitmap, resizedQuad, imageProxy.imageInfo.rotationDegrees)
} }
} }
return@withContext corrected return@withContext corrected
} }
fun Bitmap.rotate(degrees: Int): Bitmap {
if (degrees == 0) return this
val matrix = Matrix().apply { postRotate(degrees.toFloat()) }
return Bitmap.createBitmap(this, 0, 0, width, height, matrix, true)
}
fun addPage(bitmap: Bitmap, quality: Int = 75) { fun addPage(bitmap: Bitmap, quality: Int = 75) {
val resized = resizeImage(bitmap) val resized = resizeImage(bitmap)
val outputStream = ByteArrayOutputStream() val outputStream = ByteArrayOutputStream()

View File

@@ -131,6 +131,7 @@ fun CameraScreen(
pageCount = viewModel.pageCount(), pageCount = viewModel.pageCount(),
liveAnalysisState, liveAnalysisState,
onCapture = { onCapture = {
Log.i("MyScan", "Pressed <Capture>")
viewModel.liveAnalysisEnabled = false viewModel.liveAnalysisEnabled = false
showPageDialog.value = true showPageDialog.value = true
isProcessing.value = true isProcessing.value = true
@@ -140,6 +141,7 @@ fun CameraScreen(
viewModel.processCapturedImageThen(imageProxy) { viewModel.processCapturedImageThen(imageProxy) {
isProcessing.value = false isProcessing.value = false
viewModel.liveAnalysisEnabled = true viewModel.liveAnalysisEnabled = true
Log.i("MyScan", "Capture process finished")
} }
} else { } else {
Log.e("MyScan", "Error during image capture") Log.e("MyScan", "Error during image capture")