Capture: reduce conversions between Mat/Bitmap/ByteArray

This commit is contained in:
Pierre-Yves Nicolas
2026-03-24 10:05:37 +01:00
parent ceceaf8792
commit 40314def4e
6 changed files with 24 additions and 40 deletions

View File

@@ -23,6 +23,7 @@ import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry
import kotlinx.coroutines.runBlocking
import org.fairscan.app.ui.screens.camera.extractDocumentFromBitmap
import org.fairscan.imageprocessing.ImageSize
import org.fairscan.imageprocessing.detectDocumentQuad
import org.fairscan.imageprocessing.scaledTo
import org.junit.Assert.assertEquals
@@ -48,26 +49,24 @@ class DocumentDetectionTest {
listOf("img01.jpg", "img02.jpg", "img03.jpg").forEach { imageFileName ->
val inputStream = context.assets.open("uncropped/$imageFileName")
val bitmap = BitmapFactory.decodeStream(inputStream)
var outputBitmap: Bitmap? = null
var outputJpeg: ByteArray? = null
val segmentationResult = runBlocking {
segmentationService.runSegmentationAndReturn(bitmap, 0)
segmentationService.runSegmentationAndReturn(bitmap)
}
if (segmentationResult != null) {
val mask = segmentationResult.segmentation
val quad = detectDocumentQuad(mask, false)
val quad = detectDocumentQuad(mask, ImageSize(bitmap.width, bitmap.height),false)
if (quad != null) {
val resizedQuad =
quad.scaledTo(mask.width, mask.height, bitmap.width, bitmap.height)
outputBitmap = extractDocumentFromBitmap(bitmap, resizedQuad, 0, mask).page
outputJpeg = extractDocumentFromBitmap(bitmap, resizedQuad, 0, mask).pageJpeg
val file = File(context.getExternalFilesDir(null), imageFileName)
FileOutputStream(file).use {
outputBitmap.compress(Bitmap.CompressFormat.JPEG, 95, it)
}
file.writeBytes(outputJpeg)
Log.i("DocumentDetectionTest", "Image saved to ${file.absolutePath}")
}
}
if (outputBitmap == null) {
if (outputJpeg == null) {
fail("Failed to extract document from image $imageFileName")
}
}

View File

@@ -109,7 +109,7 @@ class MainViewModel(val imageRepository: ImageRepository, launchMode: LaunchMode
fun handleImageCaptured(capturedPage: CapturedPage) {
viewModelScope.launch {
imageRepository.add(
compressJpeg(capturedPage.page, 75),
capturedPage.pageJpeg,
compressJpeg(capturedPage.source, 90),
capturedPage.metadata,
)

View File

@@ -18,7 +18,7 @@ package org.fairscan.app.domain
import android.graphics.Bitmap
data class CapturedPage(
val page: Bitmap,
val pageJpeg: ByteArray,
val source: Bitmap,
val metadata: PageMetadata,
)

View File

@@ -285,7 +285,7 @@ private fun CameraScreenScaffold(
)
}
if (cameraUiState.captureState is CaptureState.CapturePreview) {
val page = cameraUiState.captureState.capturedPage.page
val page = bitmap(cameraUiState.captureState.capturedPage.pageJpeg)
CapturedImage(page.asImageBitmap(), thumbnailCoords)
}
}
@@ -533,10 +533,10 @@ fun CameraScreenPreviewWithProcessedImage() {
val p = Point(0 , 0)
val quad = Quad(p, p, p, p)
ScreenPreview(CaptureState.CapturePreview(
debugImage("uncropped/img01.jpg"),
bitmap(debugImage("uncropped/img01.jpg")),
CapturedPage(
debugImage("gallica.bnf.fr-bpt6k5530456s-1.jpg"),
debugImage("gallica.bnf.fr-bpt6k5530456s-1.jpg"),
bitmap(debugImage("gallica.bnf.fr-bpt6k5530456s-1.jpg")),
PageMetadata(quad, R0, false))))
}
@@ -559,7 +559,7 @@ private fun ScreenPreview(captureState: CaptureState, rotationDegrees: Float = 0
contentAlignment = Alignment.TopCenter
) {
Image(
debugImage("uncropped/img01.jpg").asImageBitmap(),
bitmap(debugImage("uncropped/img01.jpg")).asImageBitmap(),
modifier=Modifier.rotate(rotationDegrees),
contentDescription = null
)
@@ -591,9 +591,9 @@ private fun ScreenPreview(captureState: CaptureState, rotationDegrees: Float = 0
}
@Composable
private fun debugImage(imgName: String): Bitmap {
private fun debugImage(imgName: String): ByteArray {
val context = LocalContext.current
return context.assets.open(imgName).use { input ->
BitmapFactory.decodeStream(input)
}
return context.assets.open(imgName).readBytes()
}
private fun bitmap(jpeg: ByteArray): Bitmap = BitmapFactory.decodeByteArray(jpeg, 0, jpeg.size)

View File

@@ -37,6 +37,7 @@ import org.fairscan.imageprocessing.ImageSize
import org.fairscan.imageprocessing.Mask
import org.fairscan.imageprocessing.Quad
import org.fairscan.imageprocessing.detectDocumentQuad
import org.fairscan.imageprocessing.encodeJpeg
import org.fairscan.imageprocessing.extractDocument
import org.fairscan.imageprocessing.isColoredDocument
import org.fairscan.imageprocessing.scaledTo
@@ -218,27 +219,14 @@ fun extractDocumentFromBitmap(
val isColored = isColoredDocument(bgr, mask, quad)
val maxPixels = ExportQuality.BALANCED.maxPixels
val page = extractDocument(bgr, quad, rotationDegrees, isColored, maxPixels)
val outBgr = page
val pageJpeg = encodeJpeg(page, ExportQuality.BALANCED.jpegQuality)
bgr.release()
val outBitmap = toBitmap(outBgr)
outBgr.release()
page.release()
val normalizedQuad = quad.scaledTo(source.width, source.height, 1, 1)
val baseRotation = Rotation.fromDegrees(rotationDegrees)
val metadata = PageMetadata(normalizedQuad, baseRotation, isColored)
return CapturedPage(outBitmap, source, metadata)
}
fun toBitmap(bgr: Mat): Bitmap {
require(bgr.type() == CvType.CV_8UC3)
val rgba = Mat()
Imgproc.cvtColor(bgr, rgba, Imgproc.COLOR_BGR2RGBA)
val bmp = createBitmap(bgr.cols(), bgr.rows(), Bitmap.Config.ARGB_8888)
Utils.matToBitmap(rgba, bmp)
rgba.release()
return bmp
return CapturedPage(pageJpeg, source, metadata)
}
fun rotateBitmap(source: Bitmap, angle: Float): Bitmap {

View File

@@ -316,14 +316,11 @@ fun enhanceGrayscaleImage(img: Mat): Mat {
val denoised = Mat()
Imgproc.bilateralFilter(stretched8u, denoised, 9, 20.0, 10.0)
val finalBgr = Mat()
Imgproc.cvtColor(denoised, finalBgr, Imgproc.COLOR_GRAY2BGR)
// -- Cleanup -----------
gray.release(); imgFloat.release(); logImg.release()
blur.release(); logBlur.release(); diff.release()
retinex.release(); result8u.release()
stretched8u.release(); denoised.release()
stretched8u.release();
return finalBgr
return denoised
}