Capture: reduce conversions between Mat/Bitmap/ByteArray
This commit is contained in:
@@ -23,6 +23,7 @@ import androidx.test.ext.junit.runners.AndroidJUnit4
|
|||||||
import androidx.test.platform.app.InstrumentationRegistry
|
import androidx.test.platform.app.InstrumentationRegistry
|
||||||
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.detectDocumentQuad
|
import org.fairscan.imageprocessing.detectDocumentQuad
|
||||||
import org.fairscan.imageprocessing.scaledTo
|
import org.fairscan.imageprocessing.scaledTo
|
||||||
import org.junit.Assert.assertEquals
|
import org.junit.Assert.assertEquals
|
||||||
@@ -48,26 +49,24 @@ class DocumentDetectionTest {
|
|||||||
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")
|
||||||
val bitmap = BitmapFactory.decodeStream(inputStream)
|
val bitmap = BitmapFactory.decodeStream(inputStream)
|
||||||
var outputBitmap: Bitmap? = null
|
var outputJpeg: ByteArray? = null
|
||||||
|
|
||||||
val segmentationResult = runBlocking {
|
val segmentationResult = runBlocking {
|
||||||
segmentationService.runSegmentationAndReturn(bitmap, 0)
|
segmentationService.runSegmentationAndReturn(bitmap)
|
||||||
}
|
}
|
||||||
if (segmentationResult != null) {
|
if (segmentationResult != null) {
|
||||||
val mask = segmentationResult.segmentation
|
val mask = segmentationResult.segmentation
|
||||||
val quad = detectDocumentQuad(mask, false)
|
val quad = detectDocumentQuad(mask, ImageSize(bitmap.width, bitmap.height),false)
|
||||||
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 = extractDocumentFromBitmap(bitmap, resizedQuad, 0, mask).page
|
outputJpeg = extractDocumentFromBitmap(bitmap, resizedQuad, 0, mask).pageJpeg
|
||||||
val file = File(context.getExternalFilesDir(null), imageFileName)
|
val file = File(context.getExternalFilesDir(null), imageFileName)
|
||||||
FileOutputStream(file).use {
|
file.writeBytes(outputJpeg)
|
||||||
outputBitmap.compress(Bitmap.CompressFormat.JPEG, 95, it)
|
|
||||||
}
|
|
||||||
Log.i("DocumentDetectionTest", "Image saved to ${file.absolutePath}")
|
Log.i("DocumentDetectionTest", "Image saved to ${file.absolutePath}")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (outputBitmap == null) {
|
if (outputJpeg == null) {
|
||||||
fail("Failed to extract document from image $imageFileName")
|
fail("Failed to extract document from image $imageFileName")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -109,7 +109,7 @@ class MainViewModel(val imageRepository: ImageRepository, launchMode: LaunchMode
|
|||||||
fun handleImageCaptured(capturedPage: CapturedPage) {
|
fun handleImageCaptured(capturedPage: CapturedPage) {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
imageRepository.add(
|
imageRepository.add(
|
||||||
compressJpeg(capturedPage.page, 75),
|
capturedPage.pageJpeg,
|
||||||
compressJpeg(capturedPage.source, 90),
|
compressJpeg(capturedPage.source, 90),
|
||||||
capturedPage.metadata,
|
capturedPage.metadata,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ package org.fairscan.app.domain
|
|||||||
import android.graphics.Bitmap
|
import android.graphics.Bitmap
|
||||||
|
|
||||||
data class CapturedPage(
|
data class CapturedPage(
|
||||||
val page: Bitmap,
|
val pageJpeg: ByteArray,
|
||||||
val source: Bitmap,
|
val source: Bitmap,
|
||||||
val metadata: PageMetadata,
|
val metadata: PageMetadata,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -285,7 +285,7 @@ private fun CameraScreenScaffold(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
if (cameraUiState.captureState is CaptureState.CapturePreview) {
|
if (cameraUiState.captureState is CaptureState.CapturePreview) {
|
||||||
val page = cameraUiState.captureState.capturedPage.page
|
val page = bitmap(cameraUiState.captureState.capturedPage.pageJpeg)
|
||||||
CapturedImage(page.asImageBitmap(), thumbnailCoords)
|
CapturedImage(page.asImageBitmap(), thumbnailCoords)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -533,10 +533,10 @@ fun CameraScreenPreviewWithProcessedImage() {
|
|||||||
val p = Point(0 , 0)
|
val p = Point(0 , 0)
|
||||||
val quad = Quad(p, p, p, p)
|
val quad = Quad(p, p, p, p)
|
||||||
ScreenPreview(CaptureState.CapturePreview(
|
ScreenPreview(CaptureState.CapturePreview(
|
||||||
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"),
|
||||||
debugImage("gallica.bnf.fr-bpt6k5530456s-1.jpg"),
|
bitmap(debugImage("gallica.bnf.fr-bpt6k5530456s-1.jpg")),
|
||||||
PageMetadata(quad, R0, false))))
|
PageMetadata(quad, R0, false))))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -559,7 +559,7 @@ private fun ScreenPreview(captureState: CaptureState, rotationDegrees: Float = 0
|
|||||||
contentAlignment = Alignment.TopCenter
|
contentAlignment = Alignment.TopCenter
|
||||||
) {
|
) {
|
||||||
Image(
|
Image(
|
||||||
debugImage("uncropped/img01.jpg").asImageBitmap(),
|
bitmap(debugImage("uncropped/img01.jpg")).asImageBitmap(),
|
||||||
modifier=Modifier.rotate(rotationDegrees),
|
modifier=Modifier.rotate(rotationDegrees),
|
||||||
contentDescription = null
|
contentDescription = null
|
||||||
)
|
)
|
||||||
@@ -591,9 +591,9 @@ private fun ScreenPreview(captureState: CaptureState, rotationDegrees: Float = 0
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun debugImage(imgName: String): Bitmap {
|
private fun debugImage(imgName: String): ByteArray {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
return context.assets.open(imgName).use { input ->
|
return context.assets.open(imgName).readBytes()
|
||||||
BitmapFactory.decodeStream(input)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun bitmap(jpeg: ByteArray): Bitmap = BitmapFactory.decodeByteArray(jpeg, 0, jpeg.size)
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ import org.fairscan.imageprocessing.ImageSize
|
|||||||
import org.fairscan.imageprocessing.Mask
|
import org.fairscan.imageprocessing.Mask
|
||||||
import org.fairscan.imageprocessing.Quad
|
import org.fairscan.imageprocessing.Quad
|
||||||
import org.fairscan.imageprocessing.detectDocumentQuad
|
import org.fairscan.imageprocessing.detectDocumentQuad
|
||||||
|
import org.fairscan.imageprocessing.encodeJpeg
|
||||||
import org.fairscan.imageprocessing.extractDocument
|
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
|
||||||
@@ -218,27 +219,14 @@ fun extractDocumentFromBitmap(
|
|||||||
val isColored = isColoredDocument(bgr, mask, quad)
|
val isColored = isColoredDocument(bgr, mask, quad)
|
||||||
val maxPixels = ExportQuality.BALANCED.maxPixels
|
val maxPixels = ExportQuality.BALANCED.maxPixels
|
||||||
val page = extractDocument(bgr, quad, rotationDegrees, isColored, maxPixels)
|
val page = extractDocument(bgr, quad, rotationDegrees, isColored, maxPixels)
|
||||||
val outBgr = page
|
val pageJpeg = encodeJpeg(page, ExportQuality.BALANCED.jpegQuality)
|
||||||
bgr.release()
|
bgr.release()
|
||||||
val outBitmap = toBitmap(outBgr)
|
page.release()
|
||||||
outBgr.release()
|
|
||||||
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(outBitmap, source, metadata)
|
return CapturedPage(pageJpeg, 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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun rotateBitmap(source: Bitmap, angle: Float): Bitmap {
|
fun rotateBitmap(source: Bitmap, angle: Float): Bitmap {
|
||||||
|
|||||||
@@ -316,14 +316,11 @@ fun enhanceGrayscaleImage(img: Mat): Mat {
|
|||||||
val denoised = Mat()
|
val denoised = Mat()
|
||||||
Imgproc.bilateralFilter(stretched8u, denoised, 9, 20.0, 10.0)
|
Imgproc.bilateralFilter(stretched8u, denoised, 9, 20.0, 10.0)
|
||||||
|
|
||||||
val finalBgr = Mat()
|
|
||||||
Imgproc.cvtColor(denoised, finalBgr, Imgproc.COLOR_GRAY2BGR)
|
|
||||||
|
|
||||||
// -- Cleanup -----------
|
// -- Cleanup -----------
|
||||||
gray.release(); imgFloat.release(); logImg.release()
|
gray.release(); imgFloat.release(); logImg.release()
|
||||||
blur.release(); logBlur.release(); diff.release()
|
blur.release(); logBlur.release(); diff.release()
|
||||||
retinex.release(); result8u.release()
|
retinex.release(); result8u.release()
|
||||||
stretched8u.release(); denoised.release()
|
stretched8u.release();
|
||||||
|
|
||||||
return finalBgr
|
return denoised
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user