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

View File

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

View File

@@ -14,12 +14,13 @@
*/
package org.fairscan.app.domain
import org.fairscan.imageprocessing.ColorMode
import org.fairscan.imageprocessing.Quad
data class PageMetadata(
val normalizedQuad: Quad,
val baseRotation: Rotation,
val isColored: Boolean,
val autoColorMode: ColorMode,
)
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.fakeDocument
import org.fairscan.app.ui.theme.FairScanTheme
import org.fairscan.imageprocessing.ColorMode
import org.fairscan.imageprocessing.Point
import org.fairscan.imageprocessing.Quad
@@ -539,7 +540,7 @@ fun CameraScreenPreviewWithProcessedImage() {
CapturedPage(
debugImage("gallica.bnf.fr-bpt6k5530456s-1.jpg"),
CompletableDeferred(Jpeg(ByteArray(0))),
PageMetadata(quad, R0, false))))
PageMetadata(quad, R0, ColorMode.COLOR))))
}
@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.detectDocumentQuad
import org.fairscan.imageprocessing.extractDocument
import org.fairscan.imageprocessing.isColoredDocument
import org.fairscan.imageprocessing.autoColorMode
import org.fairscan.imageprocessing.scaledTo
import org.opencv.android.Utils
import org.opencv.core.Mat
@@ -230,16 +230,16 @@ fun extractDocumentFromBitmap(
val bgr = Mat()
Imgproc.cvtColor(rgba, bgr, Imgproc.COLOR_RGBA2BGR) // CV_8UC4 → CV_8UC3
rgba.release()
val isColored = isColoredDocument(bgr, mask, quad)
val colorMode = autoColorMode(bgr, mask, quad)
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)
bgr.release()
page.release()
val normalizedQuad = quad.scaledTo(source.width, source.height, 1, 1)
val baseRotation = Rotation.fromDegrees(rotationDegrees)
val metadata = PageMetadata(normalizedQuad, baseRotation, isColored)
val metadata = PageMetadata(normalizedQuad, baseRotation, colorMode)
val sourceJpegDeferred = viewModelScope.async(Dispatchers.IO) {
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.R270
import org.fairscan.app.domain.Rotation.R90
import org.fairscan.imageprocessing.ColorMode
import org.fairscan.imageprocessing.Point
import org.fairscan.imageprocessing.Quad
import org.junit.Rule
@@ -43,7 +44,7 @@ class ImageRepositoryTest {
private val testScope = TestScope()
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 {
if (_filesDir == null) {
@@ -83,7 +84,7 @@ class ImageRepositoryTest {
assertThat(metadata).isNotNull()
assertThat(metadata!!.normalizedQuad).isEqualTo(quad1)
assertThat(metadata.baseRotation).isEqualTo(metadata1.baseRotation)
assertThat(metadata.isColored).isEqualTo(metadata1.isColored)
assertThat(metadata.autoColorMode).isEqualTo(metadata1.autoColorMode)
}
@Test
@@ -253,7 +254,9 @@ class ImageRepositoryTest {
listOf(true, false).forEach { isColored ->
val metadata = PageV2("1", 0, 0, quad, isColored).toMetadata()
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
import org.fairscan.imageprocessing.ColorMode
import org.fairscan.imageprocessing.detectDocumentQuad
import org.fairscan.imageprocessing.extractDocument
import org.fairscan.imageprocessing.isColoredDocument
import org.fairscan.imageprocessing.autoColorMode
import org.fairscan.imageprocessing.scaledTo
import org.fairscan.imageprocessing.toImageSize
import org.opencv.imgcodecs.Imgcodecs
@@ -62,10 +63,10 @@ object ColorDetectionEvaluator {
?.scaledTo(mask.width, mask.height, mat.width(), mat.height())
if (quad == null) continue
val isColored = isColoredDocument(mat, mask, quad)
val extracted = extractDocument(mat, quad, 0, isColored, 2_000_000)
val colorMode = autoColorMode(mat, mask, quad)
val extracted = extractDocument(mat, quad, 0, colorMode, 2_000_000)
val detected = isColored
val detected = colorMode == ColorMode.COLOR
nbProcessedImages++

View File

@@ -17,7 +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.autoColorMode
import org.fairscan.imageprocessing.scaledTo
import org.fairscan.imageprocessing.toImageSize
import org.opencv.core.Mat
@@ -74,8 +74,8 @@ object DatasetEvaluator {
?.scaledTo(mask.width, mask.height, inputMat.width(), inputMat.height())
val corrected: Mat? = if (quad != null) {
val isColored = isColoredDocument(inputMat, mask, quad)
extractDocument(inputMat, quad = quad, rotationDegrees = 0, isColored, 2_000_000)
val colorMode = autoColorMode(inputMat, mask, quad)
extractDocument(inputMat, quad = quad, rotationDegrees = 0, colorMode, 2_000_000)
} else null
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.extractDocument
import org.fairscan.imageprocessing.isColoredDocument
import org.fairscan.imageprocessing.autoColorMode
import org.fairscan.imageprocessing.scaledTo
import org.fairscan.imageprocessing.toImageSize
import org.opencv.core.MatOfInt
@@ -65,13 +65,13 @@ object ExportQualityEvaluator {
continue
}
val isColored = isColoredDocument(sourceMat, mask, quad)
val colorMode = autoColorMode(sourceMat, mask, quad)
for (quality in qualities) {
for (maxPixels in maxPixelsList) {
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 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 kotlin.math.roundToInt
fun isColoredDocument(
fun autoColorMode(
img: Mat,
mask: Mask,
quad: Quad,
@@ -34,7 +34,7 @@ fun isColoredDocument(
proportionThreshold: Double = 0.0003,
luminanceMin: Double = 40.0,
luminanceMax: Double = 180.0
): Boolean {
): ColorMode {
// Work on a reasonable size (for correct performance)
val resizedImg = resizeForMaxPixels(img, 1024.0 * 768.0)
@@ -90,10 +90,13 @@ fun isColoredDocument(
restrictedMask.release()
docMask.release()
if (totalPixels == 0) return false
if (totalPixels == 0) return ColorMode.GRAYSCALE
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 {

View File

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

View File

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