diff --git a/app/src/main/java/org/fairscan/app/MainActivity.kt b/app/src/main/java/org/fairscan/app/MainActivity.kt
index b6590ec..51e83da 100644
--- a/app/src/main/java/org/fairscan/app/MainActivity.kt
+++ b/app/src/main/java/org/fairscan/app/MainActivity.kt
@@ -253,6 +253,7 @@ class MainActivity : ComponentActivity() {
},
onResetExportDirClick = { settingsViewModel.setExportDirUri(null) },
onExportFormatChanged = { format -> settingsViewModel.setExportFormat(format) },
+ onExportQualityChanged = { quality -> settingsViewModel.setExportQuality(quality) },
onBack = nav.back,
)
}
diff --git a/app/src/main/java/org/fairscan/app/data/ImageRepository.kt b/app/src/main/java/org/fairscan/app/data/ImageRepository.kt
index 7b92d16..fc4ab42 100644
--- a/app/src/main/java/org/fairscan/app/data/ImageRepository.kt
+++ b/app/src/main/java/org/fairscan/app/data/ImageRepository.kt
@@ -146,6 +146,11 @@ class ImageRepository(
return if (file.exists()) file else null
}
+ fun getSourceFor(id: String): ByteArray? {
+ val file = File(sourceDir, id)
+ return if (file.exists()) file.readBytes() else null
+ }
+
fun getThumbnail(id: String): ByteArray? {
val thumbFile = getThumbnailFile(id)
if (!thumbFile.exists()) {
diff --git a/app/src/main/java/org/fairscan/app/domain/ExportPreparation.kt b/app/src/main/java/org/fairscan/app/domain/ExportPreparation.kt
new file mode 100644
index 0000000..ac49821
--- /dev/null
+++ b/app/src/main/java/org/fairscan/app/domain/ExportPreparation.kt
@@ -0,0 +1,121 @@
+/*
+ * 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 .
+ */
+package org.fairscan.app.domain
+
+import org.fairscan.app.data.ImageRepository
+import org.fairscan.imageprocessing.extractDocument
+import org.fairscan.imageprocessing.resizeForMaxPixels
+import org.fairscan.imageprocessing.scaledTo
+import org.opencv.core.Mat
+import org.opencv.core.MatOfByte
+import org.opencv.core.MatOfInt
+import org.opencv.imgcodecs.Imgcodecs
+
+fun jpegsForExport(
+ imageRepository: ImageRepository,
+ exportQuality: ExportQuality
+): Sequence {
+
+ val imageIds = imageRepository.imageIds()
+ val baseJpegs = imageIds.asSequence().mapNotNull { id -> imageRepository.getContent(id) }
+ return when (exportQuality) {
+ ExportQuality.BALANCED -> baseJpegs
+ ExportQuality.LOW -> baseJpegs.mapNotNull { jpeg ->
+ resizeJpegBytesForMaxPixels(
+ jpegBytes = jpeg,
+ maxPixels = exportQuality.maxPixels.toDouble(),
+ jpegQuality = exportQuality.jpegQuality
+ )
+ }
+ ExportQuality.HIGH -> {
+ imageIds.asSequence().mapNotNull { id ->
+ val sourceJpegBytes = imageRepository.getSourceFor(id)
+ val pageMetadata = imageRepository.getPageMetadata(id)
+ if (sourceJpegBytes != null && pageMetadata != null)
+ prepareJpegForHigh(sourceJpegBytes, pageMetadata, ExportQuality.HIGH)
+ else
+ imageRepository.getContent(id)
+ }
+ }
+ }
+}
+
+fun resizeJpegBytesForMaxPixels(
+ jpegBytes: ByteArray,
+ maxPixels: Double,
+ jpegQuality: Int
+): ByteArray? {
+ val decoded = decodeJpeg(jpegBytes)
+ if (decoded == null)
+ return null
+
+ val resized = resizeForMaxPixels(decoded, maxPixels)
+ val outJpegBytes = encodeJpeg(resized, jpegQuality)
+
+ decoded.release()
+ resized.release()
+ return outJpegBytes
+}
+
+fun prepareJpegForHigh(
+ sourceJpegBytes: ByteArray,
+ pageMetadata: PageMetadata,
+ exportQuality: ExportQuality,
+): ByteArray? {
+
+ val decoded = decodeJpeg(sourceJpegBytes)
+ if (decoded == null)
+ return null
+
+ val quad = pageMetadata.normalizedQuad.scaledTo(1,1,decoded.width(), decoded.height())
+ val page = extractDocument(
+ decoded,
+ quad,
+ pageMetadata.rotationDegrees,
+ pageMetadata.isColored,
+ exportQuality.maxPixels)
+ val outJpegBytes = encodeJpeg(page, exportQuality.jpegQuality)
+
+ decoded.release()
+ page.release()
+ return outJpegBytes
+}
+
+fun decodeJpeg(jpegBytes: ByteArray): Mat? {
+ val src = MatOfByte(*jpegBytes)
+ val decoded = Imgcodecs.imdecode(src, Imgcodecs.IMREAD_COLOR)
+ src.release()
+ if (decoded.empty()) {
+ decoded.release()
+ return null
+ }
+ return decoded
+}
+
+fun encodeJpeg(mat: Mat, jpegQuality: Int): ByteArray? {
+ val params = MatOfInt(Imgcodecs.IMWRITE_JPEG_QUALITY, jpegQuality.coerceIn(0, 100))
+ val encoded = MatOfByte()
+ val ok = Imgcodecs.imencode(".jpg", mat, encoded, params)
+ params.release()
+
+ if (!ok) {
+ encoded.release()
+ return null
+ }
+
+ val result = encoded.toArray()
+ encoded.release()
+ return result
+}
diff --git a/app/src/main/java/org/fairscan/app/domain/ExportQuality.kt b/app/src/main/java/org/fairscan/app/domain/ExportQuality.kt
new file mode 100644
index 0000000..8b0c534
--- /dev/null
+++ b/app/src/main/java/org/fairscan/app/domain/ExportQuality.kt
@@ -0,0 +1,33 @@
+/*
+ * 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 .
+ */
+package org.fairscan.app.domain
+
+enum class ExportQuality(
+ val jpegQuality: Int,
+ val maxPixels: Long
+) {
+ LOW(
+ jpegQuality = 60,
+ maxPixels = 1_000_000
+ ),
+ BALANCED(
+ jpegQuality = 75,
+ maxPixels = 2_000_000
+ ),
+ HIGH(
+ jpegQuality = 90,
+ maxPixels = 5_000_000
+ )
+}
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 0574ca6..41489ba 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
@@ -32,11 +32,13 @@ import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.fairscan.app.AppContainer
import org.fairscan.app.domain.CapturedPage
+import org.fairscan.app.domain.ExportQuality
import org.fairscan.app.domain.PageMetadata
import org.fairscan.imageprocessing.Mask
import org.fairscan.imageprocessing.Quad
import org.fairscan.imageprocessing.detectDocumentQuad
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
@@ -196,13 +198,15 @@ fun extractDocumentFromBitmap(
val bgr = Mat()
Imgproc.cvtColor(rgba, bgr, Imgproc.COLOR_RGBA2BGR) // CV_8UC4 → CV_8UC3
rgba.release()
- val page = extractDocument(bgr, quad, rotationDegrees, mask)
- val outBgr = page.image
+ val isColored = isColoredDocument(bgr, mask, quad)
+ val maxPixels = ExportQuality.BALANCED.maxPixels
+ val page = extractDocument(bgr, quad, rotationDegrees, isColored, maxPixels)
+ val outBgr = page
bgr.release()
val outBitmap = toBitmap(outBgr)
outBgr.release()
val normalizedQuad = quad.scaledTo(source.width, source.height, 1, 1)
- val metadata = PageMetadata(normalizedQuad, rotationDegrees, page.pageAnalysis.isColored)
+ val metadata = PageMetadata(normalizedQuad, rotationDegrees, isColored)
return CapturedPage(outBitmap, source, metadata)
}
diff --git a/app/src/main/java/org/fairscan/app/ui/screens/export/ExportViewModel.kt b/app/src/main/java/org/fairscan/app/ui/screens/export/ExportViewModel.kt
index 7927df9..386c46d 100644
--- a/app/src/main/java/org/fairscan/app/ui/screens/export/ExportViewModel.kt
+++ b/app/src/main/java/org/fairscan/app/ui/screens/export/ExportViewModel.kt
@@ -21,6 +21,7 @@ import android.net.Uri
import android.os.Build
import android.os.Environment
import android.provider.MediaStore
+import android.util.Log
import androidx.annotation.RequiresApi
import androidx.core.net.toUri
import androidx.documentfile.provider.DocumentFile
@@ -41,6 +42,8 @@ import org.fairscan.app.AppContainer
import org.fairscan.app.RecentDocument
import org.fairscan.app.data.FileManager
import org.fairscan.app.data.ImageRepository
+import org.fairscan.app.domain.ExportQuality
+import org.fairscan.app.domain.jpegsForExport
import org.fairscan.app.ui.screens.settings.ExportFormat
import java.io.File
import java.io.FileInputStream
@@ -64,16 +67,16 @@ class ExportViewModel(container: AppContainer, val imageRepository: ImageReposit
private val _events = MutableSharedFlow()
val events = _events.asSharedFlow()
- private suspend fun generatePdf(): ExportResult.Pdf = withContext(Dispatchers.IO) {
- val imageIds = imageRepository.imageIds()
- val jpegs = imageIds.asSequence()
- .mapNotNull { id -> imageRepository.getContent(id) }
+ private suspend fun generatePdf(
+ exportQuality: ExportQuality
+ ): ExportResult.Pdf = withContext(Dispatchers.IO) {
+ val jpegs = jpegsForExport(imageRepository, exportQuality)
val pdf = fileManager.generatePdf(jpegs)
return@withContext ExportResult.Pdf(pdf.file, pdf.sizeInBytes, pdf.pageCount)
}
suspend fun generatePdfForExternalCall(): ExportResult.Pdf {
- return generatePdf()
+ return generatePdf(ExportQuality.BALANCED)
}
private val _uiState = MutableStateFlow(ExportUiState())
@@ -91,21 +94,21 @@ class ExportViewModel(container: AppContainer, val imageRepository: ImageReposit
cancelPreparation()
preparationJob = viewModelScope.launch {
+ val exportQuality = settingsRepository.exportQuality.first()
exportFormat = settingsRepository.exportFormat.first()
_uiState.update { it.copy(format = exportFormat) }
try {
+ val t1 = System.currentTimeMillis()
val result = if (exportFormat == ExportFormat.JPEG) {
- val jpegFiles = imageRepository.imageIds()
- .mapNotNull { id -> imageRepository.getFileFor(id) }
- .map { f -> f.copyTo(File(preparationDir, f.name), overwrite = true) }
- val sizeInBytes = jpegFiles.sumOf { it.length() }
- ExportResult.Jpeg(jpegFiles, sizeInBytes)
+ generateJpegs(exportQuality)
} else {
- generatePdf()
+ generatePdf(exportQuality)
}
_uiState.update {
it.copy(isGenerating = false, result = result)
}
+ val t2 = System.currentTimeMillis()
+ Log.i("Export", "Generation time: ${t2-t1} ms")
} catch (e: Exception) {
val message = "Failed to prepare $exportFormat export"
logger.e("FairScan", message, e)
@@ -119,6 +122,20 @@ class ExportViewModel(container: AppContainer, val imageRepository: ImageReposit
}
}
+ private suspend fun generateJpegs(
+ exportQuality: ExportQuality
+ ): ExportResult.Jpeg = withContext(Dispatchers.IO) {
+ val jpegs = jpegsForExport(imageRepository, exportQuality)
+ val timestamp = System.currentTimeMillis()
+ val files = jpegs.mapIndexed { index, bytes ->
+ val file = File(preparationDir, "$timestamp-${index + 1}.jpg")
+ file.writeBytes(bytes)
+ file
+ }.toList()
+ val sizeInBytes = files.sumOf { it.length() }
+ ExportResult.Jpeg(files, sizeInBytes)
+ }
+
fun cancelPreparation() {
preparationJob?.cancel()
_uiState.value = ExportUiState()
diff --git a/app/src/main/java/org/fairscan/app/ui/screens/settings/SettingsRepository.kt b/app/src/main/java/org/fairscan/app/ui/screens/settings/SettingsRepository.kt
index 183f6f4..322d159 100644
--- a/app/src/main/java/org/fairscan/app/ui/screens/settings/SettingsRepository.kt
+++ b/app/src/main/java/org/fairscan/app/ui/screens/settings/SettingsRepository.kt
@@ -20,6 +20,7 @@ import androidx.datastore.preferences.core.stringPreferencesKey
import androidx.datastore.preferences.preferencesDataStore
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
+import org.fairscan.app.domain.ExportQuality
private val Context.dataStore by preferencesDataStore(name = "fairscan_settings")
@@ -27,6 +28,7 @@ class SettingsRepository(private val context: Context) {
private val EXPORT_DIR_URI = stringPreferencesKey("export_dir_uri")
private val EXPORT_FORMAT = stringPreferencesKey("export_format")
+ private val EXPORT_QUALITY = stringPreferencesKey("export_quality")
val exportDirUri: Flow =
context.dataStore.data.map { prefs ->
@@ -42,6 +44,16 @@ class SettingsRepository(private val context: Context) {
}
}
+ val exportQuality: Flow =
+ context.dataStore.data.map { prefs ->
+ when (prefs[EXPORT_QUALITY]) {
+ "LOW" -> ExportQuality.LOW
+ "HIGH" -> ExportQuality.HIGH
+ "BALANCED", null -> ExportQuality.BALANCED
+ else -> ExportQuality.BALANCED
+ }
+ }
+
suspend fun setExportDirUri(uri: String?) {
context.dataStore.edit { prefs ->
if (uri == null) {
@@ -57,6 +69,12 @@ class SettingsRepository(private val context: Context) {
prefs[EXPORT_FORMAT] = format.name
}
}
+
+ suspend fun setExportQuality(quality: ExportQuality) {
+ context.dataStore.edit { prefs ->
+ prefs[EXPORT_QUALITY] = quality.name
+ }
+ }
}
enum class ExportFormat(val mimeType: String) {
diff --git a/app/src/main/java/org/fairscan/app/ui/screens/settings/SettingsScreen.kt b/app/src/main/java/org/fairscan/app/ui/screens/settings/SettingsScreen.kt
index 385deb0..344e972 100644
--- a/app/src/main/java/org/fairscan/app/ui/screens/settings/SettingsScreen.kt
+++ b/app/src/main/java/org/fairscan/app/ui/screens/settings/SettingsScreen.kt
@@ -50,6 +50,7 @@ import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.core.net.toUri
import org.fairscan.app.R
+import org.fairscan.app.domain.ExportQuality
import org.fairscan.app.ui.components.BackButton
import org.fairscan.app.ui.theme.FairScanTheme
@@ -60,6 +61,7 @@ fun SettingsScreen(
onChooseDirectoryClick: () -> Unit,
onResetExportDirClick: () -> Unit,
onExportFormatChanged: (ExportFormat) -> Unit,
+ onExportQualityChanged: (ExportQuality) -> Unit,
onBack: () -> Unit,
) {
BackHandler { onBack() }
@@ -76,6 +78,7 @@ fun SettingsScreen(
onChooseDirectoryClick,
onResetExportDirClick,
onExportFormatChanged,
+ onExportQualityChanged,
modifier = Modifier.padding(paddingValues))
}
}
@@ -86,6 +89,7 @@ private fun SettingsContent(
onChooseDirectoryClick: () -> Unit,
onResetExportDirClick: () -> Unit,
onExportFormatChanged: (ExportFormat) -> Unit,
+ onExportQualityChanged: (ExportQuality) -> Unit,
modifier: Modifier = Modifier,
) {
val context = LocalContext.current
@@ -118,6 +122,26 @@ private fun SettingsContent(
Spacer(Modifier.height(32.dp))
+ Text("Export quality", style = MaterialTheme.typography.titleLarge)
+
+ ExportQuality.entries.forEach { quality ->
+ Row(verticalAlignment = Alignment.CenterVertically) {
+ RadioButton(
+ selected = uiState.exportQuality == quality,
+ onClick = { onExportQualityChanged(quality) },
+ )
+ Text(
+ when (quality) {
+ ExportQuality.LOW -> "Low (smaller files)"
+ ExportQuality.BALANCED -> "Balanced"
+ ExportQuality.HIGH -> "High (best quality)"
+ }
+ )
+ }
+ }
+
+ Spacer(Modifier.height(32.dp))
+
Text("Export format", style = MaterialTheme.typography.titleLarge)
Row(verticalAlignment = Alignment.CenterVertically) {
@@ -207,6 +231,7 @@ fun SettingsScreenPreview(uiState: SettingsUiState) {
onChooseDirectoryClick = {},
onResetExportDirClick = {},
onExportFormatChanged = {},
+ onExportQualityChanged = {},
onBack = {}
)
}
diff --git a/app/src/main/java/org/fairscan/app/ui/screens/settings/SettingsViewModel.kt b/app/src/main/java/org/fairscan/app/ui/screens/settings/SettingsViewModel.kt
index a9dbec7..5533ae8 100644
--- a/app/src/main/java/org/fairscan/app/ui/screens/settings/SettingsViewModel.kt
+++ b/app/src/main/java/org/fairscan/app/ui/screens/settings/SettingsViewModel.kt
@@ -19,10 +19,12 @@ import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch
import org.fairscan.app.AppContainer
+import org.fairscan.app.domain.ExportQuality
data class SettingsUiState(
val exportDirUri: String? = null,
val exportFormat: ExportFormat = ExportFormat.PDF,
+ val exportQuality: ExportQuality = ExportQuality.BALANCED,
)
class SettingsViewModel(container: AppContainer) : ViewModel() {
@@ -32,10 +34,12 @@ class SettingsViewModel(container: AppContainer) : ViewModel() {
val uiState = combine(
repo.exportDirUri,
repo.exportFormat,
- ) { dir, format ->
+ repo.exportQuality,
+ ) { dir, format, quality ->
SettingsUiState(
exportDirUri = dir,
exportFormat = format,
+ exportQuality = quality,
)
}.stateIn(
viewModelScope,
@@ -54,4 +58,10 @@ class SettingsViewModel(container: AppContainer) : ViewModel() {
repo.setExportFormat(format)
}
}
+
+ fun setExportQuality(quality: ExportQuality) {
+ viewModelScope.launch {
+ repo.setExportQuality(quality)
+ }
+ }
}
diff --git a/evaluation/src/main/java/org/fairscan/evaluation/ColorDetectionEvaluator.kt b/evaluation/src/main/java/org/fairscan/evaluation/ColorDetectionEvaluator.kt
index 90493cc..b94845c 100644
--- a/evaluation/src/main/java/org/fairscan/evaluation/ColorDetectionEvaluator.kt
+++ b/evaluation/src/main/java/org/fairscan/evaluation/ColorDetectionEvaluator.kt
@@ -14,9 +14,9 @@
*/
package org.fairscan.evaluation
-import org.fairscan.imageprocessing.ExtractedDocument
import org.fairscan.imageprocessing.detectDocumentQuad
import org.fairscan.imageprocessing.extractDocument
+import org.fairscan.imageprocessing.isColoredDocument
import org.fairscan.imageprocessing.scaledTo
import org.opencv.imgcodecs.Imgcodecs
import java.io.File
@@ -60,11 +60,11 @@ object ColorDetectionEvaluator {
val quad = detectDocumentQuad(mask, isLiveAnalysis = false)
?.scaledTo(mask.width, mask.height, mat.width(), mat.height())
- val extracted: ExtractedDocument = if (quad != null) {
- extractDocument(mat, quad, 0, mask)
- } else continue
+ if (quad == null) continue
+ val isColored = isColoredDocument(mat, mask, quad)
+ val extracted = extractDocument(mat, quad, 0, isColored, 2_000_000)
- val detected = extracted.pageAnalysis.isColored
+ val detected = isColored
nbProcessedImages++
@@ -72,7 +72,7 @@ object ColorDetectionEvaluator {
Imgcodecs.imwrite(inputOut.absolutePath, mat)
val outputOut = File(outputDir, "${imgName}_output.jpg")
- Imgcodecs.imwrite(outputOut.absolutePath, extracted.image)
+ Imgcodecs.imwrite(outputOut.absolutePath, extracted)
results += ColorResult(
imgName,
diff --git a/evaluation/src/main/java/org/fairscan/evaluation/DatasetEvaluator.kt b/evaluation/src/main/java/org/fairscan/evaluation/DatasetEvaluator.kt
index bff721a..a74ebfe 100644
--- a/evaluation/src/main/java/org/fairscan/evaluation/DatasetEvaluator.kt
+++ b/evaluation/src/main/java/org/fairscan/evaluation/DatasetEvaluator.kt
@@ -17,6 +17,7 @@ package org.fairscan.evaluation
import org.fairscan.imageprocessing.Mask
import org.fairscan.imageprocessing.detectDocumentQuad
import org.fairscan.imageprocessing.extractDocument
+import org.fairscan.imageprocessing.isColoredDocument
import org.fairscan.imageprocessing.scaledTo
import org.opencv.core.Mat
import org.opencv.imgcodecs.Imgcodecs
@@ -71,7 +72,8 @@ object DatasetEvaluator {
?.scaledTo(mask.width, mask.height, inputMat.width(), inputMat.height())
val corrected: Mat? = if (quad != null) {
- extractDocument(inputMat, quad = quad, rotationDegrees = 0, mask).image
+ val isColored = isColoredDocument(inputMat, mask, quad)
+ extractDocument(inputMat, quad = quad, rotationDegrees = 0, isColored, 2_000_000)
} else null
val inputOut = File(outputDir, "${e.name}_input.jpg")
diff --git a/imageprocessing/src/main/java/org/fairscan/imageprocessing/ColorDetection.kt b/imageprocessing/src/main/java/org/fairscan/imageprocessing/ColorDetection.kt
index 511393d..1ea50b9 100644
--- a/imageprocessing/src/main/java/org/fairscan/imageprocessing/ColorDetection.kt
+++ b/imageprocessing/src/main/java/org/fairscan/imageprocessing/ColorDetection.kt
@@ -25,7 +25,6 @@ import org.opencv.core.Size
import org.opencv.imgproc.Imgproc
import org.opencv.imgproc.Imgproc.fillConvexPoly
import kotlin.math.roundToInt
-import kotlin.math.sqrt
fun isColoredDocument(
img: Mat,
@@ -97,18 +96,6 @@ fun isColoredDocument(
return proportion > proportionThreshold
}
-private fun resizeForMaxPixels(img: Mat, maxPixels: Double): Mat {
- val origPixels = img.width() * img.height()
- if (origPixels <= maxPixels) {
- return img.clone()
- }
- val scale = sqrt(maxPixels / origPixels)
- val size = Size(img.width() * scale, img.height() * scale)
- val resizedImg = Mat()
- Imgproc.resize(img, resizedImg, size, 0.0, 0.0, Imgproc.INTER_AREA)
- return resizedImg
-}
-
private fun chroma(a: Mat, b: Mat): Mat {
val aFloat = Mat()
val bFloat = Mat()
diff --git a/imageprocessing/src/main/java/org/fairscan/imageprocessing/DocumentDetection.kt b/imageprocessing/src/main/java/org/fairscan/imageprocessing/DocumentDetection.kt
index 83f6421..ca9b755 100644
--- a/imageprocessing/src/main/java/org/fairscan/imageprocessing/DocumentDetection.kt
+++ b/imageprocessing/src/main/java/org/fairscan/imageprocessing/DocumentDetection.kt
@@ -32,15 +32,6 @@ interface Mask {
fun toMat(): Mat
}
-data class PageAnalysis(
- val isColored: Boolean,
-)
-
-data class ExtractedDocument(
- val image: Mat,
- val pageAnalysis: PageAnalysis,
-)
-
fun detectDocumentQuad(mask: Mask, isLiveAnalysis: Boolean, minQuadAreaRatio: Double = 0.02): Quad? {
val mat = mask.toMat()
val (biggest: MatOfPoint2f?, area) = biggestContour(mat)
@@ -125,8 +116,9 @@ fun extractDocument(
inputMat: Mat,
quad: Quad,
rotationDegrees: Int,
- mask: Mask,
-): ExtractedDocument {
+ isColored: Boolean,
+ maxPixels: Long,
+): Mat {
val widthTop = norm(quad.topLeft, quad.topRight)
val widthBottom = norm(quad.bottomLeft, quad.bottomRight)
val targetWidth = (widthTop + widthBottom) / 2
@@ -153,27 +145,11 @@ fun extractDocument(
val outputSize = Size(targetWidth, targetHeight)
Imgproc.warpPerspective(inputMat, outputMat, transform, outputSize)
- val resized = resize(outputMat, 1500.0)
- val isColored = isColoredDocument(inputMat, mask, quad)
+ val resized = resizeForMaxPixels(outputMat, maxPixels.toDouble())
val enhanced = enhanceCapturedImage(resized, isColored)
val rotated = rotate(enhanced, rotationDegrees)
- return ExtractedDocument(rotated, PageAnalysis(isColored))
-}
-
-fun resize(original: Mat, targetMax: Double): Mat {
- val origSize = original.size()
- if (max(origSize.width, origSize.height) < targetMax)
- return original;
- var targetWidth = targetMax
- var targetHeight = origSize.height * targetWidth / origSize.width
- if (origSize.width < origSize.height) {
- targetHeight = targetMax
- targetWidth = origSize.width * targetHeight / origSize.height
- }
- val result = Mat()
- Imgproc.resize(original, result, Size(targetWidth, targetHeight), 0.0, 0.0, Imgproc.INTER_AREA)
- return result
+ return rotated
}
fun rotate(input: Mat, degrees: Int): Mat {
diff --git a/imageprocessing/src/main/java/org/fairscan/imageprocessing/Utils.kt b/imageprocessing/src/main/java/org/fairscan/imageprocessing/Utils.kt
new file mode 100644
index 0000000..9ef6d49
--- /dev/null
+++ b/imageprocessing/src/main/java/org/fairscan/imageprocessing/Utils.kt
@@ -0,0 +1,32 @@
+/*
+ * 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 .
+ */
+package org.fairscan.imageprocessing
+
+import org.opencv.core.Mat
+import org.opencv.core.Size
+import org.opencv.imgproc.Imgproc
+import kotlin.math.sqrt
+
+fun resizeForMaxPixels(img: Mat, maxPixels: Double): Mat {
+ val origPixels = img.width() * img.height()
+ if (origPixels <= maxPixels) {
+ return img.clone()
+ }
+ val scale = sqrt(maxPixels / origPixels)
+ val size = Size(img.width() * scale, img.height() * scale)
+ val resizedImg = Mat()
+ Imgproc.resize(img, resizedImg, size, 0.0, 0.0, Imgproc.INTER_AREA)
+ return resizedImg
+}