Introduce ColorMode enum

This commit is contained in:
Pierre-Yves Nicolas
2026-03-29 14:48:16 +02:00
parent 3681d5771d
commit 7d01493477
12 changed files with 47 additions and 33 deletions

View File

@@ -33,6 +33,7 @@ import org.fairscan.app.domain.PageMetadata
import org.fairscan.app.domain.PageViewKey import org.fairscan.app.domain.PageViewKey
import org.fairscan.app.domain.Rotation import org.fairscan.app.domain.Rotation
import org.fairscan.app.domain.ScanPage import org.fairscan.app.domain.ScanPage
import org.fairscan.imageprocessing.ColorMode
import org.fairscan.imageprocessing.Point import org.fairscan.imageprocessing.Point
import org.fairscan.imageprocessing.Quad import org.fairscan.imageprocessing.Quad
import java.io.File import java.io.File
@@ -151,7 +152,7 @@ class ImageRepository(
quad = metadata.normalizedQuad.toSerializable(), quad = metadata.normalizedQuad.toSerializable(),
baseRotationDegrees = metadata.baseRotation.degrees, baseRotationDegrees = metadata.baseRotation.degrees,
manualRotationDegrees = Rotation.R0.degrees, manualRotationDegrees = Rotation.R0.degrees,
isColored = metadata.isColored isColored = metadata.autoColorMode == ColorMode.COLOR
) )
) )
saveMetadata() saveMetadata()
@@ -313,6 +314,6 @@ fun PageV2.toMetadata(): PageMetadata? {
return PageMetadata( return PageMetadata(
quad.toQuad(), quad.toQuad(),
Rotation.fromDegrees(baseRotationDegrees), Rotation.fromDegrees(baseRotationDegrees),
isColored if (isColored) ColorMode.COLOR else ColorMode.GRAYSCALE
) )
} }

View File

@@ -98,7 +98,7 @@ private fun prepareJpegForHigh(
decoded, decoded,
quad, quad,
pageMetadata.baseRotation.add(manualRotation).degrees, pageMetadata.baseRotation.add(manualRotation).degrees,
pageMetadata.isColored, pageMetadata.autoColorMode,
exportQuality.maxPixels exportQuality.maxPixels
) )
return Jpeg.fromMat(page, exportQuality.jpegQuality) return Jpeg.fromMat(page, exportQuality.jpegQuality)

View File

@@ -14,12 +14,13 @@
*/ */
package org.fairscan.app.domain package org.fairscan.app.domain
import org.fairscan.imageprocessing.ColorMode
import org.fairscan.imageprocessing.Quad import org.fairscan.imageprocessing.Quad
data class PageMetadata( data class PageMetadata(
val normalizedQuad: Quad, val normalizedQuad: Quad,
val baseRotation: Rotation, val baseRotation: Rotation,
val isColored: Boolean, val autoColorMode: ColorMode,
) )
data class ScanPage( data class ScanPage(

View File

@@ -104,6 +104,7 @@ import org.fairscan.app.ui.components.pageCountText
import org.fairscan.app.ui.dummyNavigation import org.fairscan.app.ui.dummyNavigation
import org.fairscan.app.ui.fakeDocument import org.fairscan.app.ui.fakeDocument
import org.fairscan.app.ui.theme.FairScanTheme import org.fairscan.app.ui.theme.FairScanTheme
import org.fairscan.imageprocessing.ColorMode
import org.fairscan.imageprocessing.Point import org.fairscan.imageprocessing.Point
import org.fairscan.imageprocessing.Quad import org.fairscan.imageprocessing.Quad
@@ -539,7 +540,7 @@ fun CameraScreenPreviewWithProcessedImage() {
CapturedPage( CapturedPage(
debugImage("gallica.bnf.fr-bpt6k5530456s-1.jpg"), debugImage("gallica.bnf.fr-bpt6k5530456s-1.jpg"),
CompletableDeferred(Jpeg(ByteArray(0))), CompletableDeferred(Jpeg(ByteArray(0))),
PageMetadata(quad, R0, false)))) PageMetadata(quad, R0, ColorMode.COLOR))))
} }
@Preview(showBackground = true, widthDp = 640, heightDp = 320) @Preview(showBackground = true, widthDp = 640, heightDp = 320)

View File

@@ -40,7 +40,7 @@ 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.extractDocument import org.fairscan.imageprocessing.extractDocument
import org.fairscan.imageprocessing.isColoredDocument import org.fairscan.imageprocessing.autoColorMode
import org.fairscan.imageprocessing.scaledTo import org.fairscan.imageprocessing.scaledTo
import org.opencv.android.Utils import org.opencv.android.Utils
import org.opencv.core.Mat import org.opencv.core.Mat
@@ -230,16 +230,16 @@ fun extractDocumentFromBitmap(
val bgr = Mat() val bgr = Mat()
Imgproc.cvtColor(rgba, bgr, Imgproc.COLOR_RGBA2BGR) // CV_8UC4 → CV_8UC3 Imgproc.cvtColor(rgba, bgr, Imgproc.COLOR_RGBA2BGR) // CV_8UC4 → CV_8UC3
rgba.release() rgba.release()
val isColored = isColoredDocument(bgr, mask, quad) val colorMode = autoColorMode(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, colorMode, maxPixels)
val pageJpeg = Jpeg.fromMat(page, ExportQuality.BALANCED.jpegQuality) val pageJpeg = Jpeg.fromMat(page, ExportQuality.BALANCED.jpegQuality)
bgr.release() bgr.release()
page.release() page.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, colorMode)
val sourceJpegDeferred = viewModelScope.async(Dispatchers.IO) { val sourceJpegDeferred = viewModelScope.async(Dispatchers.IO) {
compressJpeg(source, 90) compressJpeg(source, 90)
} }

View File

@@ -26,6 +26,7 @@ import org.fairscan.app.domain.Rotation.R0
import org.fairscan.app.domain.Rotation.R180 import org.fairscan.app.domain.Rotation.R180
import org.fairscan.app.domain.Rotation.R270 import org.fairscan.app.domain.Rotation.R270
import org.fairscan.app.domain.Rotation.R90 import org.fairscan.app.domain.Rotation.R90
import org.fairscan.imageprocessing.ColorMode
import org.fairscan.imageprocessing.Point import org.fairscan.imageprocessing.Point
import org.fairscan.imageprocessing.Quad import org.fairscan.imageprocessing.Quad
import org.junit.Rule import org.junit.Rule
@@ -43,7 +44,7 @@ class ImageRepositoryTest {
private val testScope = TestScope() private val testScope = TestScope()
val quad1 = Quad(Point(.01, .02), Point(.1, .03), Point(.11, .12), Point(.03, .09)) val quad1 = Quad(Point(.01, .02), Point(.1, .03), Point(.11, .12), Point(.03, .09))
val metadata1 = PageMetadata(quad1, R90, true) val metadata1 = PageMetadata(quad1, R90, ColorMode.COLOR)
fun getFilesDir(): File { fun getFilesDir(): File {
if (_filesDir == null) { if (_filesDir == null) {
@@ -83,7 +84,7 @@ class ImageRepositoryTest {
assertThat(metadata).isNotNull() assertThat(metadata).isNotNull()
assertThat(metadata!!.normalizedQuad).isEqualTo(quad1) assertThat(metadata!!.normalizedQuad).isEqualTo(quad1)
assertThat(metadata.baseRotation).isEqualTo(metadata1.baseRotation) assertThat(metadata.baseRotation).isEqualTo(metadata1.baseRotation)
assertThat(metadata.isColored).isEqualTo(metadata1.isColored) assertThat(metadata.autoColorMode).isEqualTo(metadata1.autoColorMode)
} }
@Test @Test
@@ -253,7 +254,9 @@ class ImageRepositoryTest {
listOf(true, false).forEach { isColored -> listOf(true, false).forEach { isColored ->
val metadata = PageV2("1", 0, 0, quad, isColored).toMetadata() val metadata = PageV2("1", 0, 0, quad, isColored).toMetadata()
assertThat(metadata).isNotNull() assertThat(metadata).isNotNull()
assertThat(metadata!!.isColored).isEqualTo(isColored) assertThat(metadata!!.autoColorMode).isEqualTo(
if (isColored) ColorMode.COLOR else ColorMode.GRAYSCALE
)
} }
} }

View File

@@ -14,9 +14,10 @@
*/ */
package org.fairscan.evaluation package org.fairscan.evaluation
import org.fairscan.imageprocessing.ColorMode
import org.fairscan.imageprocessing.detectDocumentQuad import org.fairscan.imageprocessing.detectDocumentQuad
import org.fairscan.imageprocessing.extractDocument import org.fairscan.imageprocessing.extractDocument
import org.fairscan.imageprocessing.isColoredDocument import org.fairscan.imageprocessing.autoColorMode
import org.fairscan.imageprocessing.scaledTo import org.fairscan.imageprocessing.scaledTo
import org.fairscan.imageprocessing.toImageSize import org.fairscan.imageprocessing.toImageSize
import org.opencv.imgcodecs.Imgcodecs import org.opencv.imgcodecs.Imgcodecs
@@ -62,10 +63,10 @@ object ColorDetectionEvaluator {
?.scaledTo(mask.width, mask.height, mat.width(), mat.height()) ?.scaledTo(mask.width, mask.height, mat.width(), mat.height())
if (quad == null) continue if (quad == null) continue
val isColored = isColoredDocument(mat, mask, quad) val colorMode = autoColorMode(mat, mask, quad)
val extracted = extractDocument(mat, quad, 0, isColored, 2_000_000) val extracted = extractDocument(mat, quad, 0, colorMode, 2_000_000)
val detected = isColored val detected = colorMode == ColorMode.COLOR
nbProcessedImages++ nbProcessedImages++

View File

@@ -17,7 +17,7 @@ package org.fairscan.evaluation
import org.fairscan.imageprocessing.Mask import org.fairscan.imageprocessing.Mask
import org.fairscan.imageprocessing.detectDocumentQuad import org.fairscan.imageprocessing.detectDocumentQuad
import org.fairscan.imageprocessing.extractDocument import org.fairscan.imageprocessing.extractDocument
import org.fairscan.imageprocessing.isColoredDocument import org.fairscan.imageprocessing.autoColorMode
import org.fairscan.imageprocessing.scaledTo import org.fairscan.imageprocessing.scaledTo
import org.fairscan.imageprocessing.toImageSize import org.fairscan.imageprocessing.toImageSize
import org.opencv.core.Mat import org.opencv.core.Mat
@@ -74,8 +74,8 @@ object DatasetEvaluator {
?.scaledTo(mask.width, mask.height, inputMat.width(), inputMat.height()) ?.scaledTo(mask.width, mask.height, inputMat.width(), inputMat.height())
val corrected: Mat? = if (quad != null) { val corrected: Mat? = if (quad != null) {
val isColored = isColoredDocument(inputMat, mask, quad) val colorMode = autoColorMode(inputMat, mask, quad)
extractDocument(inputMat, quad = quad, rotationDegrees = 0, isColored, 2_000_000) extractDocument(inputMat, quad = quad, rotationDegrees = 0, colorMode, 2_000_000)
} else null } else null
val inputOut = File(outputDir, "${e.name}_input.jpg") val inputOut = File(outputDir, "${e.name}_input.jpg")

View File

@@ -16,7 +16,7 @@ package org.fairscan.evaluation
import org.fairscan.imageprocessing.detectDocumentQuad import org.fairscan.imageprocessing.detectDocumentQuad
import org.fairscan.imageprocessing.extractDocument import org.fairscan.imageprocessing.extractDocument
import org.fairscan.imageprocessing.isColoredDocument import org.fairscan.imageprocessing.autoColorMode
import org.fairscan.imageprocessing.scaledTo import org.fairscan.imageprocessing.scaledTo
import org.fairscan.imageprocessing.toImageSize import org.fairscan.imageprocessing.toImageSize
import org.opencv.core.MatOfInt import org.opencv.core.MatOfInt
@@ -65,13 +65,13 @@ object ExportQualityEvaluator {
continue continue
} }
val isColored = isColoredDocument(sourceMat, mask, quad) val colorMode = autoColorMode(sourceMat, mask, quad)
for (quality in qualities) { for (quality in qualities) {
for (maxPixels in maxPixelsList) { for (maxPixels in maxPixelsList) {
val outputMat = val outputMat =
extractDocument(sourceMat, quad, 0, isColored, maxPixels.toLong()) extractDocument(sourceMat, quad, 0, colorMode, maxPixels.toLong())
val outputFile = File(outputDir, "$imgName-$quality-$maxPixels.jpg") val outputFile = File(outputDir, "$imgName-$quality-$maxPixels.jpg")
val params = MatOfInt(Imgcodecs.IMWRITE_JPEG_QUALITY, quality) val params = MatOfInt(Imgcodecs.IMWRITE_JPEG_QUALITY, quality)

View File

@@ -26,7 +26,7 @@ import org.opencv.imgproc.Imgproc
import org.opencv.imgproc.Imgproc.fillConvexPoly import org.opencv.imgproc.Imgproc.fillConvexPoly
import kotlin.math.roundToInt import kotlin.math.roundToInt
fun isColoredDocument( fun autoColorMode(
img: Mat, img: Mat,
mask: Mask, mask: Mask,
quad: Quad, quad: Quad,
@@ -34,7 +34,7 @@ fun isColoredDocument(
proportionThreshold: Double = 0.0003, proportionThreshold: Double = 0.0003,
luminanceMin: Double = 40.0, luminanceMin: Double = 40.0,
luminanceMax: Double = 180.0 luminanceMax: Double = 180.0
): Boolean { ): ColorMode {
// Work on a reasonable size (for correct performance) // Work on a reasonable size (for correct performance)
val resizedImg = resizeForMaxPixels(img, 1024.0 * 768.0) val resizedImg = resizeForMaxPixels(img, 1024.0 * 768.0)
@@ -90,10 +90,13 @@ fun isColoredDocument(
restrictedMask.release() restrictedMask.release()
docMask.release() docMask.release()
if (totalPixels == 0) return false if (totalPixels == 0) return ColorMode.GRAYSCALE
val proportion = coloredPixels.toDouble() / totalPixels.toDouble() val proportion = coloredPixels.toDouble() / totalPixels.toDouble()
return proportion > proportionThreshold return if (proportion > proportionThreshold)
ColorMode.COLOR
else
ColorMode.GRAYSCALE
} }
private fun chroma(a: Mat, b: Mat): Mat { private fun chroma(a: Mat, b: Mat): Mat {

View File

@@ -154,7 +154,7 @@ fun extractDocument(
inputMat: Mat, inputMat: Mat,
quad: Quad, quad: Quad,
rotationDegrees: Int, rotationDegrees: Int,
isColored: Boolean, colorMode: ColorMode,
maxPixels: Long, maxPixels: Long,
): Mat { ): Mat {
val widthTop = norm(quad.topLeft, quad.topRight) val widthTop = norm(quad.topLeft, quad.topRight)
@@ -184,7 +184,7 @@ fun extractDocument(
Imgproc.warpPerspective(inputMat, warped, transform, outputSize) Imgproc.warpPerspective(inputMat, warped, transform, outputSize)
val resized = resizeForMaxPixels(warped, maxPixels.toDouble()) val resized = resizeForMaxPixels(warped, maxPixels.toDouble())
val enhanced = enhanceCapturedImage(resized, isColored) val enhanced = enhanceCapturedImage(resized, colorMode)
val rotated = rotate(enhanced, rotationDegrees) val rotated = rotate(enhanced, rotationDegrees)
warped.release() warped.release()

View File

@@ -25,11 +25,15 @@ import org.opencv.imgproc.Imgproc
import kotlin.math.max import kotlin.math.max
import kotlin.math.min import kotlin.math.min
fun enhanceCapturedImage(img: Mat, isColored: Boolean): Mat { enum class ColorMode {
return if (isColored) { COLOR,
multiScaleRetinexOnL(img) GRAYSCALE,
} else { }
enhanceGrayscaleImage(img)
fun enhanceCapturedImage(img: Mat, colorMode: ColorMode): Mat {
return when (colorMode) {
ColorMode.COLOR -> multiScaleRetinexOnL(img)
ColorMode.GRAYSCALE -> enhanceGrayscaleImage(img)
} }
} }