From e3ada11c8c2881b08cc5e2de6960b698210c074d Mon Sep 17 00:00:00 2001 From: Pierre-Yves Nicolas <6371790+pynicolas@users.noreply.github.com> Date: Sat, 14 Jun 2025 14:39:55 +0200 Subject: [PATCH] Basic postprocessing with OpenCV --- .../org/mydomain/myscan/DocumentDetection.kt | 13 ++- .../java/org/mydomain/myscan/MainViewModel.kt | 1 - .../org/mydomain/myscan/PostProcessing.kt | 88 +++++++++++++++++++ 3 files changed, 98 insertions(+), 4 deletions(-) create mode 100644 app/src/main/java/org/mydomain/myscan/PostProcessing.kt diff --git a/app/src/main/java/org/mydomain/myscan/DocumentDetection.kt b/app/src/main/java/org/mydomain/myscan/DocumentDetection.kt index 73e1c0b..e7647d0 100644 --- a/app/src/main/java/org/mydomain/myscan/DocumentDetection.kt +++ b/app/src/main/java/org/mydomain/myscan/DocumentDetection.kt @@ -91,10 +91,17 @@ fun extractDocument(originalBitmap: Bitmap, quad: Quad): Bitmap { val inputMat = Mat() Utils.bitmapToMat(originalBitmap, inputMat) val outputMat = Mat() - Imgproc.warpPerspective(inputMat, outputMat, transform, Size(maxWidth.toDouble(), maxHeight.toDouble())) + val outputSize = Size(maxWidth.toDouble(), maxHeight.toDouble()) + Imgproc.warpPerspective(inputMat, outputMat, transform, outputSize) - val outputBitmap = createBitmap(maxWidth, maxHeight) - Utils.matToBitmap(outputMat, outputBitmap) + val enhanced = enhanceCapturedImage(outputMat) + + return toBitmap(enhanced, maxWidth, maxHeight) +} + +private fun toBitmap(mat: Mat, width: Int, height: Int): Bitmap { + val outputBitmap = createBitmap(width, height) + Utils.matToBitmap(mat, outputBitmap) return outputBitmap } diff --git a/app/src/main/java/org/mydomain/myscan/MainViewModel.kt b/app/src/main/java/org/mydomain/myscan/MainViewModel.kt index 768cacd..42724f1 100644 --- a/app/src/main/java/org/mydomain/myscan/MainViewModel.kt +++ b/app/src/main/java/org/mydomain/myscan/MainViewModel.kt @@ -18,7 +18,6 @@ import android.content.Context import android.graphics.Bitmap import android.graphics.BitmapFactory import android.graphics.Matrix -import android.graphics.pdf.PdfDocument import androidx.camera.core.ImageProxy import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider diff --git a/app/src/main/java/org/mydomain/myscan/PostProcessing.kt b/app/src/main/java/org/mydomain/myscan/PostProcessing.kt new file mode 100644 index 0000000..c5c69c2 --- /dev/null +++ b/app/src/main/java/org/mydomain/myscan/PostProcessing.kt @@ -0,0 +1,88 @@ +/* + * 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.mydomain.myscan + +import android.util.Log +import org.opencv.core.Core +import org.opencv.core.CvType +import org.opencv.core.Mat +import org.opencv.core.MatOfDouble +import org.opencv.core.Scalar +import org.opencv.core.Size +import org.opencv.imgproc.Imgproc + +fun enhanceCapturedImage(img: Mat): Mat { + return if (isColoredDocument(img)) { + Log.i("PostProcessing", "color document") + val result = Mat() + Core.convertScaleAbs(img, result, 1.2, 10.0) + result + } else { + Log.i("PostProcessing", "grayscale document") + val gray = correctLighting(img) + val contrastedGray = enhanceContrastGray(gray) + val result = Mat() + Imgproc.cvtColor(contrastedGray, result, Imgproc.COLOR_GRAY2BGR) + result + } +} + +fun isColoredDocument(img: Mat, threshold: Double = 4.0): Boolean { + val lab = Mat() + Imgproc.cvtColor(img, lab, Imgproc.COLOR_BGR2Lab) + val channels = ArrayList() + Core.split(lab, channels) + + val aStd = MatOfDouble() + val bStd = MatOfDouble() + Core.meanStdDev(channels[1], MatOfDouble(), aStd) + Core.meanStdDev(channels[2], MatOfDouble(), bStd) + + val result = (aStd.toArray()[0] + bStd.toArray()[0]) / 2.0 + return result > threshold +} + +// TODO the radius should depend on the image size +fun correctLighting(img: Mat, radius: Int = 100): Mat { + val gray = Mat() + Imgproc.cvtColor(img, gray, Imgproc.COLOR_BGR2GRAY) + + val blur = Mat() + val kernelSize = 2 * radius + 1 + Imgproc.GaussianBlur(gray, blur, Size(kernelSize.toDouble(), kernelSize.toDouble()), 0.0) + + val normalized = Mat() + Core.divide(gray, blur, normalized, 255.0) + return normalized +} + +fun enhanceContrastGray(img: Mat): Mat { + val flat = img.reshape(0, 1) + val sorted = Mat() + Core.sort(flat, sorted, Core.SORT_ASCENDING) + + val totalPixels = sorted.cols() + val pLow = sorted[0, (totalPixels * 0.01).toInt()][0] + val pHigh = sorted[0, (totalPixels * 0.95).toInt()][0] + + val result = Mat(img.size(), img.type()) + img.convertTo(result, CvType.CV_32F) + Core.subtract(result, Scalar(pLow), result) + Core.multiply(result, Scalar(255.0 * 1.03 / (pHigh - pLow)), result) + Core.min(result, Scalar(255.0), result) + Core.max(result, Scalar(0.0), result) + result.convertTo(result, CvType.CV_8U) + return result +}