Compress source JPEG during capture preview rather than later
This commit is contained in:
@@ -15,12 +15,14 @@
|
|||||||
package org.fairscan.app.domain
|
package org.fairscan.app.domain
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.graphics.Bitmap
|
|
||||||
import android.graphics.BitmapFactory
|
import android.graphics.BitmapFactory
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import androidx.test.core.app.ApplicationProvider
|
import androidx.test.core.app.ApplicationProvider
|
||||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||||
import androidx.test.platform.app.InstrumentationRegistry
|
import androidx.test.platform.app.InstrumentationRegistry
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.SupervisorJob
|
||||||
import kotlinx.coroutines.runBlocking
|
import kotlinx.coroutines.runBlocking
|
||||||
import org.fairscan.app.ui.screens.camera.extractDocumentFromBitmap
|
import org.fairscan.app.ui.screens.camera.extractDocumentFromBitmap
|
||||||
import org.fairscan.imageprocessing.ImageSize
|
import org.fairscan.imageprocessing.ImageSize
|
||||||
@@ -32,7 +34,6 @@ import org.junit.Test
|
|||||||
import org.junit.runner.RunWith
|
import org.junit.runner.RunWith
|
||||||
import org.opencv.android.OpenCVLoader
|
import org.opencv.android.OpenCVLoader
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.io.FileOutputStream
|
|
||||||
|
|
||||||
@RunWith(AndroidJUnit4::class)
|
@RunWith(AndroidJUnit4::class)
|
||||||
class DocumentDetectionTest {
|
class DocumentDetectionTest {
|
||||||
@@ -45,6 +46,7 @@ class DocumentDetectionTest {
|
|||||||
val segmentationService = ImageSegmentationService(context) { _, _, _ -> }
|
val segmentationService = ImageSegmentationService(context) { _, _, _ -> }
|
||||||
segmentationService.initialize()
|
segmentationService.initialize()
|
||||||
OpenCVLoader.initLocal()
|
OpenCVLoader.initLocal()
|
||||||
|
val scope = CoroutineScope(SupervisorJob() + Dispatchers.Unconfined)
|
||||||
|
|
||||||
listOf("img01.jpg", "img02.jpg", "img03.jpg").forEach { imageFileName ->
|
listOf("img01.jpg", "img02.jpg", "img03.jpg").forEach { imageFileName ->
|
||||||
val inputStream = context.assets.open("uncropped/$imageFileName")
|
val inputStream = context.assets.open("uncropped/$imageFileName")
|
||||||
@@ -60,7 +62,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)
|
||||||
outputJpeg = extractDocumentFromBitmap(bitmap, resizedQuad, 0, mask).pageJpeg
|
outputJpeg = extractDocumentFromBitmap(bitmap, resizedQuad, 0, mask, scope).pageJpeg
|
||||||
val file = File(context.getExternalFilesDir(null), imageFileName)
|
val file = File(context.getExternalFilesDir(null), imageFileName)
|
||||||
file.writeBytes(outputJpeg)
|
file.writeBytes(outputJpeg)
|
||||||
Log.i("DocumentDetectionTest", "Image saved to ${file.absolutePath}")
|
Log.i("DocumentDetectionTest", "Image saved to ${file.absolutePath}")
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ import androidx.lifecycle.ViewModel
|
|||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import kotlinx.collections.immutable.persistentListOf
|
import kotlinx.collections.immutable.persistentListOf
|
||||||
import kotlinx.collections.immutable.toImmutableList
|
import kotlinx.collections.immutable.toImmutableList
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.asCoroutineDispatcher
|
import kotlinx.coroutines.asCoroutineDispatcher
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.SharingStarted
|
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.NavigationState
|
||||||
import org.fairscan.app.ui.Screen
|
import org.fairscan.app.ui.Screen
|
||||||
import org.fairscan.app.ui.state.DocumentUiModel
|
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
|
import java.util.concurrent.Executors
|
||||||
|
|
||||||
class MainViewModel(val imageRepository: ImageRepository, launchMode: LaunchMode): ViewModel() {
|
class MainViewModel(val imageRepository: ImageRepository, launchMode: LaunchMode): ViewModel() {
|
||||||
@@ -125,10 +122,13 @@ class MainViewModel(val imageRepository: ImageRepository, launchMode: LaunchMode
|
|||||||
|
|
||||||
fun handleImageCaptured(capturedPage: CapturedPage) {
|
fun handleImageCaptured(capturedPage: CapturedPage) {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
|
val sourceJpeg = withContext(Dispatchers.IO) {
|
||||||
|
capturedPage.sourceJpeg.await()
|
||||||
|
}
|
||||||
val pages = withContext(repositoryDispatcher) {
|
val pages = withContext(repositoryDispatcher) {
|
||||||
imageRepository.add(
|
imageRepository.add(
|
||||||
capturedPage.pageJpeg,
|
capturedPage.pageJpeg,
|
||||||
compressJpeg(capturedPage.source, 90),
|
sourceJpeg,
|
||||||
capturedPage.metadata,
|
capturedPage.metadata,
|
||||||
)
|
)
|
||||||
imageRepository.pages()
|
imageRepository.pages()
|
||||||
@@ -136,17 +136,4 @@ class MainViewModel(val imageRepository: ImageRepository, launchMode: LaunchMode
|
|||||||
_pages.value = pages
|
_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()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,10 +15,10 @@
|
|||||||
|
|
||||||
package org.fairscan.app.domain
|
package org.fairscan.app.domain
|
||||||
|
|
||||||
import android.graphics.Bitmap
|
import kotlinx.coroutines.Deferred
|
||||||
|
|
||||||
data class CapturedPage(
|
data class CapturedPage(
|
||||||
val pageJpeg: ByteArray,
|
val pageJpeg: ByteArray,
|
||||||
val source: Bitmap,
|
val sourceJpeg: Deferred<ByteArray>,
|
||||||
val metadata: PageMetadata,
|
val metadata: PageMetadata,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -86,6 +86,7 @@ import androidx.compose.ui.unit.dp
|
|||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
import kotlinx.collections.immutable.toImmutableList
|
import kotlinx.collections.immutable.toImmutableList
|
||||||
|
import kotlinx.coroutines.CompletableDeferred
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import org.fairscan.app.MainViewModel
|
import org.fairscan.app.MainViewModel
|
||||||
import org.fairscan.app.R
|
import org.fairscan.app.R
|
||||||
@@ -536,7 +537,7 @@ fun CameraScreenPreviewWithProcessedImage() {
|
|||||||
bitmap(debugImage("uncropped/img01.jpg")),
|
bitmap(debugImage("uncropped/img01.jpg")),
|
||||||
CapturedPage(
|
CapturedPage(
|
||||||
debugImage("gallica.bnf.fr-bpt6k5530456s-1.jpg"),
|
debugImage("gallica.bnf.fr-bpt6k5530456s-1.jpg"),
|
||||||
bitmap(debugImage("gallica.bnf.fr-bpt6k5530456s-1.jpg")),
|
CompletableDeferred(ByteArray(0)),
|
||||||
PageMetadata(quad, R0, false))))
|
PageMetadata(quad, R0, false))))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -17,10 +17,11 @@ package org.fairscan.app.ui.screens.camera
|
|||||||
import android.graphics.Bitmap
|
import android.graphics.Bitmap
|
||||||
import android.graphics.Matrix
|
import android.graphics.Matrix
|
||||||
import androidx.camera.core.ImageProxy
|
import androidx.camera.core.ImageProxy
|
||||||
import androidx.core.graphics.createBitmap
|
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.async
|
||||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
@@ -42,7 +43,6 @@ import org.fairscan.imageprocessing.extractDocument
|
|||||||
import org.fairscan.imageprocessing.isColoredDocument
|
import org.fairscan.imageprocessing.isColoredDocument
|
||||||
import org.fairscan.imageprocessing.scaledTo
|
import org.fairscan.imageprocessing.scaledTo
|
||||||
import org.opencv.android.Utils
|
import org.opencv.android.Utils
|
||||||
import org.opencv.core.CvType
|
|
||||||
import org.opencv.core.Mat
|
import org.opencv.core.Mat
|
||||||
import org.opencv.imgproc.Imgproc
|
import org.opencv.imgproc.Imgproc
|
||||||
|
|
||||||
@@ -165,7 +165,8 @@ class CameraViewModel(appContainer: AppContainer): ViewModel() {
|
|||||||
val quad = detectDocumentQuad(mask, originalSize, isLiveAnalysis = false)
|
val quad = detectDocumentQuad(mask, originalSize, isLiveAnalysis = false)
|
||||||
if (quad != null) {
|
if (quad != null) {
|
||||||
val resizedQuad = quad.scaledTo(mask.width, mask.height, source.width, source.height)
|
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
|
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 {
|
sealed class CaptureState {
|
||||||
open val frozenImage: Bitmap? = null
|
open val frozenImage: Bitmap? = null
|
||||||
|
|
||||||
@@ -209,7 +223,7 @@ sealed class CaptureState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun extractDocumentFromBitmap(
|
fun extractDocumentFromBitmap(
|
||||||
source: Bitmap, quad: Quad, rotationDegrees: Int, mask: Mask
|
source: Bitmap, quad: Quad, rotationDegrees: Int, mask: Mask, viewModelScope: CoroutineScope
|
||||||
): CapturedPage {
|
): CapturedPage {
|
||||||
val rgba = Mat()
|
val rgba = Mat()
|
||||||
Utils.bitmapToMat(source, rgba)
|
Utils.bitmapToMat(source, rgba)
|
||||||
@@ -226,7 +240,10 @@ fun extractDocumentFromBitmap(
|
|||||||
val normalizedQuad = quad.scaledTo(source.width, source.height, 1, 1)
|
val normalizedQuad = quad.scaledTo(source.width, source.height, 1, 1)
|
||||||
val baseRotation = Rotation.fromDegrees(rotationDegrees)
|
val baseRotation = Rotation.fromDegrees(rotationDegrees)
|
||||||
val metadata = PageMetadata(normalizedQuad, baseRotation, isColored)
|
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 {
|
fun rotateBitmap(source: Bitmap, angle: Float): Bitmap {
|
||||||
|
|||||||
Reference in New Issue
Block a user