Store "source" (unprocessed captured image)
This commit is contained in:
@@ -319,7 +319,7 @@ class MainActivity : ComponentActivity() {
|
||||
LaunchedEffect(Unit) {
|
||||
cameraViewModel.events.collect { event ->
|
||||
when (event) {
|
||||
is CameraEvent.ImageCaptured -> viewModel.handleImageCaptured(event.jpegBytes)
|
||||
is CameraEvent.ImageCaptured -> viewModel.handleImageCaptured(event.page)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,9 +27,11 @@ import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
import org.fairscan.app.data.ImageRepository
|
||||
import org.fairscan.app.domain.CapturedPage
|
||||
import org.fairscan.app.ui.NavigationState
|
||||
import org.fairscan.app.ui.Screen
|
||||
import org.fairscan.app.ui.state.DocumentUiModel
|
||||
import java.io.ByteArrayOutputStream
|
||||
|
||||
class MainViewModel(val imageRepository: ImageRepository, launchMode: LaunchMode): ViewModel() {
|
||||
|
||||
@@ -67,14 +69,18 @@ class MainViewModel(val imageRepository: ImageRepository, launchMode: LaunchMode
|
||||
}
|
||||
|
||||
fun movePage(id: String, newIndex: Int) {
|
||||
viewModelScope.launch {
|
||||
imageRepository.movePage(id, newIndex)
|
||||
_pageIds.value = imageRepository.imageIds()
|
||||
}
|
||||
}
|
||||
|
||||
fun deletePage(id: String) {
|
||||
viewModelScope.launch {
|
||||
imageRepository.delete(id)
|
||||
_pageIds.value = imageRepository.imageIds()
|
||||
}
|
||||
}
|
||||
|
||||
fun startNewDocument() {
|
||||
_pageIds.value = persistentListOf()
|
||||
@@ -93,8 +99,19 @@ class MainViewModel(val imageRepository: ImageRepository, launchMode: LaunchMode
|
||||
return bytes?.let { BitmapFactory.decodeByteArray(it, 0, it.size) }
|
||||
}
|
||||
|
||||
fun handleImageCaptured(jpegBytes: ByteArray) {
|
||||
imageRepository.add(jpegBytes)
|
||||
fun handleImageCaptured(capturedPage: CapturedPage) {
|
||||
viewModelScope.launch {
|
||||
imageRepository.add(
|
||||
compressJpeg(capturedPage.page, 75),
|
||||
compressJpeg(capturedPage.source, 90)
|
||||
)
|
||||
_pageIds.value = imageRepository.imageIds()
|
||||
}
|
||||
}
|
||||
|
||||
private fun compressJpeg(bitmap: Bitmap, quality: Int): ByteArray {
|
||||
val outputStream = ByteArrayOutputStream()
|
||||
bitmap.compress(Bitmap.CompressFormat.JPEG, quality, outputStream)
|
||||
return outputStream.toByteArray()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.serialization.json.Json
|
||||
import java.io.File
|
||||
|
||||
const val SOURCE_DIR_NAME = "sources"
|
||||
const val SCAN_DIR_NAME = "scanned_pages"
|
||||
const val THUMBNAIL_DIR_NAME = "thumbnails"
|
||||
|
||||
@@ -28,6 +29,10 @@ class ImageRepository(
|
||||
private val thumbnailSizePx: Int,
|
||||
) {
|
||||
|
||||
private val sourceDir: File = File(scanRootDir, SOURCE_DIR_NAME).apply {
|
||||
if (!exists()) mkdirs()
|
||||
}
|
||||
|
||||
private val scanDir: File = File(scanRootDir, SCAN_DIR_NAME).apply {
|
||||
if (!exists()) mkdirs()
|
||||
}
|
||||
@@ -76,11 +81,12 @@ class ImageRepository(
|
||||
|
||||
fun imageIds(): ImmutableList<String> = fileNames.toImmutableList()
|
||||
|
||||
fun add(bytes: ByteArray) {
|
||||
fun add(pageBytes: ByteArray, sourceBytes: ByteArray? = null) {
|
||||
val fileName = "${System.currentTimeMillis()}.jpg"
|
||||
val file = File(scanDir, fileName)
|
||||
file.writeBytes(bytes)
|
||||
file.writeBytes(pageBytes)
|
||||
writeThumbnail(file)
|
||||
sourceBytes?.let { File(sourceDir, fileName).writeBytes(sourceBytes) }
|
||||
fileNames.add(fileName)
|
||||
saveMetadata()
|
||||
}
|
||||
@@ -158,6 +164,9 @@ class ImageRepository(
|
||||
scanDir.listFiles()?.forEach {
|
||||
file -> file.delete()
|
||||
}
|
||||
sourceDir.listFiles()?.forEach {
|
||||
file -> file.delete()
|
||||
}
|
||||
saveMetadata() // "empty" json file
|
||||
}
|
||||
}
|
||||
|
||||
23
app/src/main/java/org/fairscan/app/domain/CapturedPage.kt
Normal file
23
app/src/main/java/org/fairscan/app/domain/CapturedPage.kt
Normal file
@@ -0,0 +1,23 @@
|
||||
/*
|
||||
* Copyright 2025 Pierre-Yves Nicolas
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify it
|
||||
* under the terms of the GNU General Public License as published by the Free
|
||||
* Software Foundation, either version 3 of the License, or (at your option)
|
||||
* any later version.
|
||||
* This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
* more details.
|
||||
* You should have received a copy of the GNU General Public License along with
|
||||
* this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.fairscan.app.domain
|
||||
|
||||
import android.graphics.Bitmap
|
||||
|
||||
data class CapturedPage(
|
||||
val page: Bitmap,
|
||||
val source: Bitmap
|
||||
)
|
||||
@@ -84,6 +84,7 @@ import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.coroutines.delay
|
||||
import org.fairscan.app.MainViewModel
|
||||
import org.fairscan.app.R
|
||||
import org.fairscan.app.domain.CapturedPage
|
||||
import org.fairscan.app.ui.Navigation
|
||||
import org.fairscan.app.ui.Screen
|
||||
import org.fairscan.app.ui.components.CameraPermissionState
|
||||
@@ -235,7 +236,8 @@ private fun CameraScreenScaffold(
|
||||
)
|
||||
}
|
||||
if (cameraUiState.captureState is CaptureState.CapturePreview) {
|
||||
CapturedImage(cameraUiState.captureState.processed.asImageBitmap(), thumbnailCoords)
|
||||
val page = cameraUiState.captureState.capturedPage.page
|
||||
CapturedImage(page.asImageBitmap(), thumbnailCoords)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -461,7 +463,9 @@ fun CameraScreenPreview() {
|
||||
fun CameraScreenPreviewWithProcessedImage() {
|
||||
ScreenPreview(CaptureState.CapturePreview(
|
||||
debugImage("uncropped/img01.jpg"),
|
||||
debugImage("gallica.bnf.fr-bpt6k5530456s-1.jpg")))
|
||||
CapturedPage(
|
||||
debugImage("gallica.bnf.fr-bpt6k5530456s-1.jpg"),
|
||||
debugImage("gallica.bnf.fr-bpt6k5530456s-1.jpg"))))
|
||||
}
|
||||
|
||||
@Preview(showBackground = true, widthDp = 640, heightDp = 320)
|
||||
|
||||
@@ -31,6 +31,7 @@ import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.fairscan.app.AppContainer
|
||||
import org.fairscan.app.domain.CapturedPage
|
||||
import org.fairscan.imageprocessing.Mask
|
||||
import org.fairscan.imageprocessing.Quad
|
||||
import org.fairscan.imageprocessing.detectDocumentQuad
|
||||
@@ -40,10 +41,9 @@ import org.opencv.android.Utils
|
||||
import org.opencv.core.CvType
|
||||
import org.opencv.core.Mat
|
||||
import org.opencv.imgproc.Imgproc
|
||||
import java.io.ByteArrayOutputStream
|
||||
|
||||
sealed interface CameraEvent {
|
||||
data class ImageCaptured(val jpegBytes: ByteArray) : CameraEvent
|
||||
data class ImageCaptured(val page: CapturedPage) : CameraEvent
|
||||
}
|
||||
|
||||
class CameraViewModel(appContainer: AppContainer): ViewModel() {
|
||||
@@ -88,7 +88,7 @@ class CameraViewModel(appContainer: AppContainer): ViewModel() {
|
||||
_captureState.value = CaptureState.Capturing(frozenImage)
|
||||
}
|
||||
|
||||
private fun onCaptureProcessed(captured: Bitmap?) {
|
||||
private fun onCaptureProcessed(captured: CapturedPage?) {
|
||||
val current = _captureState.value
|
||||
_captureState.value = when {
|
||||
current is CaptureState.Capturing && captured != null ->
|
||||
@@ -117,23 +117,25 @@ class CameraViewModel(appContainer: AppContainer): ViewModel() {
|
||||
fun onImageCaptured(imageProxy: ImageProxy?) {
|
||||
if (imageProxy != null) {
|
||||
viewModelScope.launch {
|
||||
val image = processCapturedImage(imageProxy)
|
||||
val source = imageProxy.toBitmap()
|
||||
val processed = processCapturedImage(source, imageProxy.imageInfo.rotationDegrees)
|
||||
imageProxy.close()
|
||||
onCaptureProcessed(image)
|
||||
onCaptureProcessed(processed?.let { CapturedPage(processed, source) })
|
||||
}
|
||||
} else {
|
||||
onCaptureProcessed(null)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun processCapturedImage(imageProxy: ImageProxy): Bitmap? = withContext(Dispatchers.IO) {
|
||||
private suspend fun processCapturedImage(
|
||||
bitmap: Bitmap,
|
||||
rotationDegrees: Int
|
||||
): Bitmap? = withContext(Dispatchers.IO) {
|
||||
var corrected: Bitmap? = null
|
||||
val bitmap = imageProxy.toBitmap()
|
||||
val segmentation = imageSegmentationService.runSegmentationAndReturn(bitmap, 0)
|
||||
if (segmentation != null) {
|
||||
val mask = segmentation.segmentation
|
||||
var quad = detectDocumentQuad(mask, isLiveAnalysis = false)
|
||||
val rotationDegrees = imageProxy.imageInfo.rotationDegrees
|
||||
if (quad == null) {
|
||||
val now = System.currentTimeMillis()
|
||||
lastSuccessfulLiveAnalysisState?.timestamp?.let {
|
||||
@@ -157,14 +159,11 @@ class CameraViewModel(appContainer: AppContainer): ViewModel() {
|
||||
return@withContext corrected
|
||||
}
|
||||
|
||||
fun addProcessedImage(quality: Int = 75) {
|
||||
fun addProcessedImage() {
|
||||
val current = _captureState.value
|
||||
if (current is CaptureState.CapturePreview) {
|
||||
val outputStream = ByteArrayOutputStream()
|
||||
current.processed.compress(Bitmap.CompressFormat.JPEG, quality, outputStream)
|
||||
val jpegBytes = outputStream.toByteArray()
|
||||
viewModelScope.launch {
|
||||
_events.emit(CameraEvent.ImageCaptured(jpegBytes))
|
||||
_events.emit(CameraEvent.ImageCaptured(current.capturedPage))
|
||||
}
|
||||
}
|
||||
_captureState.value = CaptureState.Idle
|
||||
@@ -184,7 +183,7 @@ sealed class CaptureState {
|
||||
data class CaptureError(override val frozenImage: Bitmap) : CaptureState()
|
||||
data class CapturePreview(
|
||||
override val frozenImage: Bitmap,
|
||||
val processed: Bitmap,
|
||||
val capturedPage: CapturedPage,
|
||||
) : CaptureState()
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user