Extract module imageprocessing (#76)

This commit is contained in:
pynicolas
2025-12-06 08:08:20 +01:00
committed by GitHub
parent 8ac844d8fd
commit 3d9d5565f1
22 changed files with 91 additions and 45 deletions

2
.idea/compiler.xml generated
View File

@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="CompilerConfiguration">
<bytecodeTargetLevel target="21" />
<bytecodeTargetLevel target="11" />
</component>
</project>

1
.idea/gradle.xml generated
View File

@@ -11,6 +11,7 @@
<set>
<option value="$PROJECT_DIR$" />
<option value="$PROJECT_DIR$/app" />
<option value="$PROJECT_DIR$/imageprocessing" />
</set>
</option>
</GradleProjectSettings>

2
.idea/kotlinc.xml generated
View File

@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="KotlinJpsPluginSettings">
<option name="version" value="2.1.0" />
<option name="version" value="2.2.21" />
</component>
</project>

2
.idea/misc.xml generated
View File

@@ -1,6 +1,6 @@
<project version="4">
<component name="ExternalStorageConfigurationManager" enabled="true" />
<component name="ProjectRootManager" version="2" languageLevel="JDK_21" default="true" project-jdk-name="jbr-21" project-jdk-type="JavaSDK">
<component name="ProjectRootManager" version="2" languageLevel="JDK_11" project-jdk-name="jbr-21" project-jdk-type="JavaSDK">
<output url="file://$PROJECT_DIR$/build/classes" />
</component>
<component name="ProjectType">

View File

@@ -88,8 +88,10 @@ android {
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
}
kotlinOptions {
jvmTarget = "11"
kotlin {
compilerOptions {
jvmTarget.set(org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_11)
}
}
buildFeatures {
compose = true
@@ -101,6 +103,10 @@ apply(from = "download-tflite.gradle.kts")
dependencies {
implementation(project(":imageprocessing")) {
exclude(group = "org.openpnp", module = "opencv")
}
implementation(libs.androidx.core.ktx)
implementation(libs.androidx.lifecycle.runtime.ktx)
implementation(libs.androidx.lifecycle.runtime.compose)

View File

@@ -22,6 +22,7 @@ import androidx.test.core.app.ApplicationProvider
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry
import kotlinx.coroutines.runBlocking
import org.fairscan.imageprocessing.scaledTo
import org.junit.Assert.assertEquals
import org.junit.Assert.fail
import org.junit.Test

View File

@@ -29,6 +29,7 @@ import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.withContext
import org.fairscan.app.data.Logger
import org.fairscan.imageprocessing.Mask
import org.opencv.core.CvType
import org.opencv.core.Mat
import org.tensorflow.lite.DataType
@@ -132,7 +133,11 @@ class ImageSegmentationService(private val context: Context, private val logger:
return maskFloats
}
data class Segmentation(private val probmap: FloatArray, val width: Int, val height: Int) {
data class Segmentation(
private val probmap: FloatArray,
override val width: Int,
override val height: Int
): Mask {
fun get(x: Int, y: Int): Float = probmap[y * width + x]
fun toBinaryMask(): Bitmap {
val bmp = createBitmap(width, height, Bitmap.Config.ARGB_8888)
@@ -144,7 +149,8 @@ class ImageSegmentationService(private val context: Context, private val logger:
bmp.setPixels(pixels, 0, width, 0, 0, width, height)
return bmp
}
fun toMat(): Mat {
override fun toMat(): Mat {
val mat = Mat(height, width, CvType.CV_32FC1)
mat.put(0, 0, probmap)
return mat

View File

@@ -53,8 +53,8 @@ import androidx.core.graphics.scale
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.compose.LocalLifecycleOwner
import com.google.common.util.concurrent.ListenableFuture
import org.fairscan.app.domain.Point
import org.fairscan.app.domain.scaledTo
import org.fairscan.imageprocessing.Point
import org.fairscan.imageprocessing.scaledTo
import org.fairscan.app.ui.components.CameraPermissionState
import java.util.concurrent.ExecutorService
import java.util.concurrent.Executors

View File

@@ -16,7 +16,7 @@ package org.fairscan.app.ui.screens.camera
import android.graphics.Bitmap
import androidx.compose.runtime.Immutable
import org.fairscan.app.domain.Quad
import org.fairscan.imageprocessing.Quad
@Immutable
data class LiveAnalysisState(

View File

@@ -17,6 +17,7 @@ package org.fairscan.app.ui.screens.camera
import android.graphics.Bitmap
import android.util.Log
import androidx.camera.core.ImageProxy
import androidx.core.graphics.createBitmap
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.Dispatchers
@@ -30,9 +31,12 @@ import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.fairscan.app.AppContainer
import org.fairscan.app.domain.detectDocumentQuad
import org.fairscan.app.domain.extractDocument
import org.fairscan.app.domain.scaledTo
import org.fairscan.imageprocessing.Quad
import org.fairscan.imageprocessing.detectDocumentQuad
import org.fairscan.imageprocessing.extractDocument
import org.fairscan.imageprocessing.scaledTo
import org.opencv.android.Utils
import org.opencv.core.Mat
import java.io.ByteArrayOutputStream
sealed interface CameraEvent {
@@ -143,7 +147,7 @@ class CameraViewModel(appContainer: AppContainer): ViewModel() {
}
if (quad != null) {
val resizedQuad = quad.scaledTo(mask.width, mask.height, bitmap.width, bitmap.height)
corrected = extractDocument(bitmap, resizedQuad, imageProxy.imageInfo.rotationDegrees)
corrected = extractDocumentFromBitmap(bitmap, resizedQuad, imageProxy.imageInfo.rotationDegrees)
}
}
return@withContext corrected
@@ -179,3 +183,15 @@ sealed class CaptureState {
val processed: Bitmap
) : CaptureState()
}
fun extractDocumentFromBitmap(originalBitmap: Bitmap, quad: Quad, rotationDegrees: Int): Bitmap {
val inputMat = Mat()
Utils.bitmapToMat(originalBitmap, inputMat)
return toBitmap(extractDocument(inputMat, quad, rotationDegrees))
}
private fun toBitmap(mat: Mat): Bitmap {
val outputBitmap = createBitmap(mat.cols(), mat.rows())
Utils.matToBitmap(mat, outputBitmap)
return outputBitmap
}

View File

@@ -5,6 +5,7 @@ plugins {
alias(libs.plugins.kotlin.compose) apply false
alias(libs.plugins.aboutLibrariesAndroid) apply false
alias(libs.plugins.license)
alias(libs.plugins.jetbrains.kotlin.jvm) apply false
}
license {

View File

@@ -14,6 +14,7 @@ datastore = "1.2.0"
documentfile = "1.1.0"
litert = "1.4.1"
opencv = "4.12.0"
opencv_java = "4.9.0-0"
assertj = "3.27.6"
pdfbox = "2.0.27.0"
zoomable = "2.9.0"
@@ -22,6 +23,7 @@ protobuf = "0.9.5"
protobufJavaLite = "4.33.1"
kotlinSerialization = "1.9.0"
reorderable = "3.0.0"
jetbrainsKotlinJvm = "2.2.21"
[libraries]
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
@@ -54,6 +56,7 @@ litert = { group = "com.google.ai.edge.litert", name = "litert", version.ref = "
litert-support = { group = "com.google.ai.edge.litert", name = "litert-support", version.ref = "litert" }
litert-metadata = { group = "com.google.ai.edge.litert", name = "litert-metadata", version.ref = "litert" }
opencv = { group="org.opencv", name="opencv", version.ref = "opencv" }
opencvjava = { group="org.openpnp", name="opencv", version.ref = "opencv_java" }
pdfbox = { group = "com.tom-roush", name = "pdfbox-android", version.ref = "pdfbox" }
zoomable = { group = "net.engawapg.lib", name = "zoomable", version.ref = "zoomable" }
reorderable = { module = "sh.calvin.reorderable:reorderable", version.ref = "reorderable" }
@@ -70,3 +73,4 @@ kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", versi
license = { id = "com.github.hierynomus.license", version.ref = "license" }
aboutLibrariesAndroid = { id = "com.mikepenz.aboutlibraries.plugin.android", version.ref = "aboutLibraries" }
protobuf = { id = "com.google.protobuf", version.ref = "protobuf" }
jetbrains-kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "jetbrainsKotlinJvm" }

1
imageprocessing/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
/build

View File

@@ -0,0 +1,19 @@
plugins {
id("java-library")
alias(libs.plugins.jetbrains.kotlin.jvm)
}
java {
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
}
kotlin {
compilerOptions {
jvmTarget = org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_11
}
}
dependencies {
implementation(libs.opencvjava)
testImplementation(kotlin("test"))
testImplementation(libs.assertj)
}

View File

@@ -12,15 +12,11 @@
* 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
package org.fairscan.imageprocessing
import android.graphics.Bitmap
import androidx.core.graphics.createBitmap
import org.fairscan.app.domain.ImageSegmentationService.Segmentation
import org.fairscan.app.domain.quad.detectDocumentQuadFromProbmap
import org.fairscan.app.domain.quad.findQuadFromRightAngles
import org.fairscan.app.domain.quad.minAreaRect
import org.opencv.android.Utils
import org.fairscan.imageprocessing.quad.detectDocumentQuadFromProbmap
import org.fairscan.imageprocessing.quad.findQuadFromRightAngles
import org.fairscan.imageprocessing.quad.minAreaRect
import org.opencv.core.Core
import org.opencv.core.CvType
import org.opencv.core.Mat
@@ -31,7 +27,13 @@ import org.opencv.imgproc.Imgproc
import kotlin.math.abs
import kotlin.math.max
fun detectDocumentQuad(mask: Segmentation, isLiveAnalysis: Boolean, minQuadAreaRatio: Double = 0.02): Quad? {
interface Mask {
val width: Int
val height: Int
fun toMat(): Mat
}
fun detectDocumentQuad(mask: Mask, isLiveAnalysis: Boolean, minQuadAreaRatio: Double = 0.02): Quad? {
val mat = mask.toMat()
val (biggest: MatOfPoint2f?, area) = biggestContour(mat)
var vertices: List<Point>?
@@ -114,7 +116,7 @@ fun refineMask(original: Mat): Mat {
return opened
}
fun extractDocument(originalBitmap: Bitmap, quad: Quad, rotationDegrees: Int): Bitmap {
fun extractDocument(inputMat: Mat, quad: Quad, rotationDegrees: Int): Mat {
val widthTop = norm(quad.topLeft, quad.topRight)
val widthBottom = norm(quad.bottomLeft, quad.bottomRight)
val targetWidth = (widthTop + widthBottom) / 2
@@ -137,8 +139,6 @@ fun extractDocument(originalBitmap: Bitmap, quad: Quad, rotationDegrees: Int): B
)
val transform = Imgproc.getPerspectiveTransform(srcPoints, dstPoints)
val inputMat = Mat()
Utils.bitmapToMat(originalBitmap, inputMat)
val outputMat = Mat()
val outputSize = Size(targetWidth.toDouble(), targetHeight.toDouble())
Imgproc.warpPerspective(inputMat, outputMat, transform, outputSize)
@@ -147,7 +147,7 @@ fun extractDocument(originalBitmap: Bitmap, quad: Quad, rotationDegrees: Int): B
val enhanced = enhanceCapturedImage(resized)
val rotated = rotate(enhanced, rotationDegrees)
return toBitmap(rotated)
return rotated
}
fun resize(original: Mat, targetMax: Double): Mat {
@@ -177,12 +177,6 @@ fun rotate(input: Mat, degrees: Int): Mat {
return output
}
private fun toBitmap(mat: Mat): Bitmap {
val outputBitmap = createBitmap(mat.cols(), mat.rows())
Utils.matToBitmap(mat, outputBitmap)
return outputBitmap
}
fun Point.toCv(): org.opencv.core.Point {
return org.opencv.core.Point(x, y)
}

View File

@@ -12,7 +12,7 @@
* 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
package org.fairscan.imageprocessing
import kotlin.math.atan2
import kotlin.math.hypot

View File

@@ -12,9 +12,8 @@
* 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
package org.fairscan.imageprocessing
import android.util.Log
import org.opencv.core.Core
import org.opencv.core.CvType
import org.opencv.core.Mat
@@ -25,12 +24,10 @@ import kotlin.math.max
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 = multiScaleRetinex(img)
val contrastedGray = enhanceContrastAuto(gray)
val result = Mat()

View File

@@ -12,7 +12,7 @@
* 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.quad
package org.fairscan.imageprocessing.quad
import org.opencv.core.Mat
import org.opencv.core.CvType

View File

@@ -12,9 +12,9 @@
* 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.quad
package org.fairscan.imageprocessing.quad
import org.fairscan.app.domain.Point
import org.fairscan.imageprocessing.Point
import kotlin.math.cos
import kotlin.math.sin

View File

@@ -12,9 +12,9 @@
* 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.quad
package org.fairscan.imageprocessing.quad
import org.fairscan.app.domain.Point
import org.fairscan.imageprocessing.Point
import kotlin.math.abs
import kotlin.math.acos
import kotlin.math.sqrt

View File

@@ -12,7 +12,7 @@
* 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
package org.fairscan.imageprocessing
import org.assertj.core.api.Assertions.assertThat
import org.assertj.core.api.Assertions.assertThatThrownBy

View File

@@ -21,4 +21,4 @@ dependencyResolutionManagement {
rootProject.name = "FairScan"
include(":app")
include(":imageprocessing")