Compress source JPEG during capture preview rather than later

This commit is contained in:
Pierre-Yves Nicolas
2026-03-24 16:45:44 +01:00
parent fa619da867
commit 90287b3389
5 changed files with 36 additions and 29 deletions

View File

@@ -15,12 +15,14 @@
package org.fairscan.app.domain
import android.content.Context
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.util.Log
import androidx.test.core.app.ApplicationProvider
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.runBlocking
import org.fairscan.app.ui.screens.camera.extractDocumentFromBitmap
import org.fairscan.imageprocessing.ImageSize
@@ -32,7 +34,6 @@ import org.junit.Test
import org.junit.runner.RunWith
import org.opencv.android.OpenCVLoader
import java.io.File
import java.io.FileOutputStream
@RunWith(AndroidJUnit4::class)
class DocumentDetectionTest {
@@ -45,6 +46,7 @@ class DocumentDetectionTest {
val segmentationService = ImageSegmentationService(context) { _, _, _ -> }
segmentationService.initialize()
OpenCVLoader.initLocal()
val scope = CoroutineScope(SupervisorJob() + Dispatchers.Unconfined)
listOf("img01.jpg", "img02.jpg", "img03.jpg").forEach { imageFileName ->
val inputStream = context.assets.open("uncropped/$imageFileName")
@@ -60,7 +62,7 @@ class DocumentDetectionTest {
if (quad != null) {
val resizedQuad =
quad.scaledTo(mask.width, mask.height, bitmap.width, bitmap.height)
outputJpeg = extractDocumentFromBitmap(bitmap, resizedQuad, 0, mask).pageJpeg
outputJpeg = extractDocumentFromBitmap(bitmap, resizedQuad, 0, mask, scope).pageJpeg
val file = File(context.getExternalFilesDir(null), imageFileName)
file.writeBytes(outputJpeg)
Log.i("DocumentDetectionTest", "Image saved to ${file.absolutePath}")

View File

@@ -20,6 +20,7 @@ import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.asCoroutineDispatcher
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
@@ -35,10 +36,6 @@ import org.fairscan.app.domain.PageViewKey
import org.fairscan.app.ui.NavigationState
import org.fairscan.app.ui.Screen
import org.fairscan.app.ui.state.DocumentUiModel
import org.fairscan.imageprocessing.encodeJpeg
import org.opencv.android.Utils
import org.opencv.core.Mat
import org.opencv.imgproc.Imgproc
import java.util.concurrent.Executors
class MainViewModel(val imageRepository: ImageRepository, launchMode: LaunchMode): ViewModel() {
@@ -125,10 +122,13 @@ class MainViewModel(val imageRepository: ImageRepository, launchMode: LaunchMode
fun handleImageCaptured(capturedPage: CapturedPage) {
viewModelScope.launch {
val sourceJpeg = withContext(Dispatchers.IO) {
capturedPage.sourceJpeg.await()
}
val pages = withContext(repositoryDispatcher) {
imageRepository.add(
capturedPage.pageJpeg,
compressJpeg(capturedPage.source, 90),
sourceJpeg,
capturedPage.metadata,
)
imageRepository.pages()
@@ -136,17 +136,4 @@ class MainViewModel(val imageRepository: ImageRepository, launchMode: LaunchMode
_pages.value = pages
}
}
private fun compressJpeg(bitmap: Bitmap, quality: Int): ByteArray {
val rgba = Mat()
Utils.bitmapToMat(bitmap, rgba)
val bgr = Mat()
Imgproc.cvtColor(rgba, bgr, Imgproc.COLOR_RGBA2BGR)
rgba.release()
return try {
encodeJpeg(bgr, quality)
} finally {
bgr.release()
}
}
}

View File

@@ -15,10 +15,10 @@
package org.fairscan.app.domain
import android.graphics.Bitmap
import kotlinx.coroutines.Deferred
data class CapturedPage(
val pageJpeg: ByteArray,
val source: Bitmap,
val sourceJpeg: Deferred<ByteArray>,
val metadata: PageMetadata,
)

View File

@@ -86,6 +86,7 @@ import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.delay
import org.fairscan.app.MainViewModel
import org.fairscan.app.R
@@ -536,7 +537,7 @@ fun CameraScreenPreviewWithProcessedImage() {
bitmap(debugImage("uncropped/img01.jpg")),
CapturedPage(
debugImage("gallica.bnf.fr-bpt6k5530456s-1.jpg"),
bitmap(debugImage("gallica.bnf.fr-bpt6k5530456s-1.jpg")),
CompletableDeferred(ByteArray(0)),
PageMetadata(quad, R0, false))))
}

View File

@@ -17,10 +17,11 @@ package org.fairscan.app.ui.screens.camera
import android.graphics.Bitmap
import android.graphics.Matrix
import androidx.camera.core.ImageProxy
import androidx.core.graphics.createBitmap
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
@@ -42,7 +43,6 @@ import org.fairscan.imageprocessing.extractDocument
import org.fairscan.imageprocessing.isColoredDocument
import org.fairscan.imageprocessing.scaledTo
import org.opencv.android.Utils
import org.opencv.core.CvType
import org.opencv.core.Mat
import org.opencv.imgproc.Imgproc
@@ -165,7 +165,8 @@ class CameraViewModel(appContainer: AppContainer): ViewModel() {
val quad = detectDocumentQuad(mask, originalSize, isLiveAnalysis = false)
if (quad != null) {
val resizedQuad = quad.scaledTo(mask.width, mask.height, source.width, source.height)
result = extractDocumentFromBitmap(source, resizedQuad, rotationDegrees, mask)
result = extractDocumentFromBitmap(
source, resizedQuad, rotationDegrees, mask, viewModelScope)
}
}
return@withContext result
@@ -196,6 +197,19 @@ class CameraViewModel(appContainer: AppContainer): ViewModel() {
}
}
private fun compressJpeg(bitmap: Bitmap, quality: Int): ByteArray {
val rgba = Mat()
Utils.bitmapToMat(bitmap, rgba)
val bgr = Mat()
Imgproc.cvtColor(rgba, bgr, Imgproc.COLOR_RGBA2BGR)
rgba.release()
return try {
encodeJpeg(bgr, quality)
} finally {
bgr.release()
}
}
sealed class CaptureState {
open val frozenImage: Bitmap? = null
@@ -209,7 +223,7 @@ sealed class CaptureState {
}
fun extractDocumentFromBitmap(
source: Bitmap, quad: Quad, rotationDegrees: Int, mask: Mask
source: Bitmap, quad: Quad, rotationDegrees: Int, mask: Mask, viewModelScope: CoroutineScope
): CapturedPage {
val rgba = Mat()
Utils.bitmapToMat(source, rgba)
@@ -226,7 +240,10 @@ fun extractDocumentFromBitmap(
val normalizedQuad = quad.scaledTo(source.width, source.height, 1, 1)
val baseRotation = Rotation.fromDegrees(rotationDegrees)
val metadata = PageMetadata(normalizedQuad, baseRotation, isColored)
return CapturedPage(pageJpeg, source, metadata)
val sourceJpegDeferred = viewModelScope.async(Dispatchers.IO) {
compressJpeg(source, 90)
}
return CapturedPage(pageJpeg, sourceJpegDeferred, metadata)
}
fun rotateBitmap(source: Bitmap, angle: Float): Bitmap {