From 90287b3389030befec81c42706d563ba033d8ab0 Mon Sep 17 00:00:00 2001 From: Pierre-Yves Nicolas <6371790+pynicolas@users.noreply.github.com> Date: Tue, 24 Mar 2026 16:45:44 +0100 Subject: [PATCH] Compress source JPEG during capture preview rather than later --- .../app/domain/DocumentDetectionTest.kt | 8 +++--- .../java/org/fairscan/app/MainViewModel.kt | 23 ++++------------ .../org/fairscan/app/domain/CapturedPage.kt | 4 +-- .../app/ui/screens/camera/CameraScreen.kt | 3 ++- .../app/ui/screens/camera/CameraViewModel.kt | 27 +++++++++++++++---- 5 files changed, 36 insertions(+), 29 deletions(-) diff --git a/app/src/androidTest/java/org/fairscan/app/domain/DocumentDetectionTest.kt b/app/src/androidTest/java/org/fairscan/app/domain/DocumentDetectionTest.kt index cadf1c2..39d7052 100644 --- a/app/src/androidTest/java/org/fairscan/app/domain/DocumentDetectionTest.kt +++ b/app/src/androidTest/java/org/fairscan/app/domain/DocumentDetectionTest.kt @@ -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}") diff --git a/app/src/main/java/org/fairscan/app/MainViewModel.kt b/app/src/main/java/org/fairscan/app/MainViewModel.kt index 520110a..6f8c872 100644 --- a/app/src/main/java/org/fairscan/app/MainViewModel.kt +++ b/app/src/main/java/org/fairscan/app/MainViewModel.kt @@ -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() - } - } } diff --git a/app/src/main/java/org/fairscan/app/domain/CapturedPage.kt b/app/src/main/java/org/fairscan/app/domain/CapturedPage.kt index 2b67335..4734db8 100644 --- a/app/src/main/java/org/fairscan/app/domain/CapturedPage.kt +++ b/app/src/main/java/org/fairscan/app/domain/CapturedPage.kt @@ -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, val metadata: PageMetadata, ) diff --git a/app/src/main/java/org/fairscan/app/ui/screens/camera/CameraScreen.kt b/app/src/main/java/org/fairscan/app/ui/screens/camera/CameraScreen.kt index ca76038..7b13f03 100644 --- a/app/src/main/java/org/fairscan/app/ui/screens/camera/CameraScreen.kt +++ b/app/src/main/java/org/fairscan/app/ui/screens/camera/CameraScreen.kt @@ -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)))) } diff --git a/app/src/main/java/org/fairscan/app/ui/screens/camera/CameraViewModel.kt b/app/src/main/java/org/fairscan/app/ui/screens/camera/CameraViewModel.kt index 66a960e..43fbc29 100644 --- a/app/src/main/java/org/fairscan/app/ui/screens/camera/CameraViewModel.kt +++ b/app/src/main/java/org/fairscan/app/ui/screens/camera/CameraViewModel.kt @@ -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 {