diff --git a/.idea/compiler.xml b/.idea/compiler.xml index b86273d..fb7f4a8 100644 --- a/.idea/compiler.xml +++ b/.idea/compiler.xml @@ -1,6 +1,6 @@ - + \ No newline at end of file diff --git a/.idea/gradle.xml b/.idea/gradle.xml index 639c779..59e2c85 100644 --- a/.idea/gradle.xml +++ b/.idea/gradle.xml @@ -11,6 +11,7 @@ diff --git a/.idea/kotlinc.xml b/.idea/kotlinc.xml index bb44937..8ad8c86 100644 --- a/.idea/kotlinc.xml +++ b/.idea/kotlinc.xml @@ -1,6 +1,6 @@ - \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml index b2c751a..d852bbf 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -1,6 +1,6 @@ - + diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 8712f8c..4207016 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -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) diff --git a/app/src/androidTest/java/org/fairscan/app/domain/DocumentDetectionTest.kt b/app/src/androidTest/java/org/fairscan/app/domain/DocumentDetectionTest.kt index 5d1625f..8b17548 100644 --- a/app/src/androidTest/java/org/fairscan/app/domain/DocumentDetectionTest.kt +++ b/app/src/androidTest/java/org/fairscan/app/domain/DocumentDetectionTest.kt @@ -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 diff --git a/app/src/main/java/org/fairscan/app/domain/ImageSegmentation.kt b/app/src/main/java/org/fairscan/app/domain/ImageSegmentation.kt index 2cf34d4..fec3804 100644 --- a/app/src/main/java/org/fairscan/app/domain/ImageSegmentation.kt +++ b/app/src/main/java/org/fairscan/app/domain/ImageSegmentation.kt @@ -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 diff --git a/app/src/main/java/org/fairscan/app/ui/screens/camera/CameraPreview.kt b/app/src/main/java/org/fairscan/app/ui/screens/camera/CameraPreview.kt index 1686420..0016fa5 100644 --- a/app/src/main/java/org/fairscan/app/ui/screens/camera/CameraPreview.kt +++ b/app/src/main/java/org/fairscan/app/ui/screens/camera/CameraPreview.kt @@ -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 diff --git a/app/src/main/java/org/fairscan/app/ui/screens/camera/CameraUiState.kt b/app/src/main/java/org/fairscan/app/ui/screens/camera/CameraUiState.kt index ceed28b..0b137b8 100644 --- a/app/src/main/java/org/fairscan/app/ui/screens/camera/CameraUiState.kt +++ b/app/src/main/java/org/fairscan/app/ui/screens/camera/CameraUiState.kt @@ -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( 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 0883928..c02aa30 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 @@ -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 +} diff --git a/build.gradle.kts b/build.gradle.kts index dd1d7df..353741e 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -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 { diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 848a9f9..c1c9800 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -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" } diff --git a/imageprocessing/.gitignore b/imageprocessing/.gitignore new file mode 100644 index 0000000..796b96d --- /dev/null +++ b/imageprocessing/.gitignore @@ -0,0 +1 @@ +/build diff --git a/imageprocessing/build.gradle.kts b/imageprocessing/build.gradle.kts new file mode 100644 index 0000000..a961b5c --- /dev/null +++ b/imageprocessing/build.gradle.kts @@ -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) +} diff --git a/app/src/main/java/org/fairscan/app/domain/DocumentDetection.kt b/imageprocessing/src/main/java/org/fairscan/imageprocessing/DocumentDetection.kt similarity index 87% rename from app/src/main/java/org/fairscan/app/domain/DocumentDetection.kt rename to imageprocessing/src/main/java/org/fairscan/imageprocessing/DocumentDetection.kt index 7db33d6..6078447 100644 --- a/app/src/main/java/org/fairscan/app/domain/DocumentDetection.kt +++ b/imageprocessing/src/main/java/org/fairscan/imageprocessing/DocumentDetection.kt @@ -12,15 +12,11 @@ * You should have received a copy of the GNU General Public License along with * this program. If not, see . */ -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? @@ -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) } diff --git a/app/src/main/java/org/fairscan/app/domain/Geometry.kt b/imageprocessing/src/main/java/org/fairscan/imageprocessing/Geometry.kt similarity index 98% rename from app/src/main/java/org/fairscan/app/domain/Geometry.kt rename to imageprocessing/src/main/java/org/fairscan/imageprocessing/Geometry.kt index 8209444..ebd8aa9 100644 --- a/app/src/main/java/org/fairscan/app/domain/Geometry.kt +++ b/imageprocessing/src/main/java/org/fairscan/imageprocessing/Geometry.kt @@ -12,7 +12,7 @@ * You should have received a copy of the GNU General Public License along with * this program. If not, see . */ -package org.fairscan.app.domain +package org.fairscan.imageprocessing import kotlin.math.atan2 import kotlin.math.hypot diff --git a/app/src/main/java/org/fairscan/app/domain/PostProcessing.kt b/imageprocessing/src/main/java/org/fairscan/imageprocessing/PostProcessing.kt similarity index 97% rename from app/src/main/java/org/fairscan/app/domain/PostProcessing.kt rename to imageprocessing/src/main/java/org/fairscan/imageprocessing/PostProcessing.kt index 65af518..da09e42 100644 --- a/app/src/main/java/org/fairscan/app/domain/PostProcessing.kt +++ b/imageprocessing/src/main/java/org/fairscan/imageprocessing/PostProcessing.kt @@ -12,9 +12,8 @@ * You should have received a copy of the GNU General Public License along with * this program. If not, see . */ -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() diff --git a/app/src/main/java/org/fairscan/app/domain/quad/AdaptiveThreshold.kt b/imageprocessing/src/main/java/org/fairscan/imageprocessing/quad/AdaptiveThreshold.kt similarity index 99% rename from app/src/main/java/org/fairscan/app/domain/quad/AdaptiveThreshold.kt rename to imageprocessing/src/main/java/org/fairscan/imageprocessing/quad/AdaptiveThreshold.kt index 4be20a1..d59f5e5 100644 --- a/app/src/main/java/org/fairscan/app/domain/quad/AdaptiveThreshold.kt +++ b/imageprocessing/src/main/java/org/fairscan/imageprocessing/quad/AdaptiveThreshold.kt @@ -12,7 +12,7 @@ * You should have received a copy of the GNU General Public License along with * this program. If not, see . */ -package org.fairscan.app.domain.quad +package org.fairscan.imageprocessing.quad import org.opencv.core.Mat import org.opencv.core.CvType diff --git a/app/src/main/java/org/fairscan/app/domain/quad/MinAreaRect.kt b/imageprocessing/src/main/java/org/fairscan/imageprocessing/quad/MinAreaRect.kt similarity index 97% rename from app/src/main/java/org/fairscan/app/domain/quad/MinAreaRect.kt rename to imageprocessing/src/main/java/org/fairscan/imageprocessing/quad/MinAreaRect.kt index 2e42dcf..28fa9c5 100644 --- a/app/src/main/java/org/fairscan/app/domain/quad/MinAreaRect.kt +++ b/imageprocessing/src/main/java/org/fairscan/imageprocessing/quad/MinAreaRect.kt @@ -12,9 +12,9 @@ * You should have received a copy of the GNU General Public License along with * this program. If not, see . */ -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 diff --git a/app/src/main/java/org/fairscan/app/domain/quad/RightAngles.kt b/imageprocessing/src/main/java/org/fairscan/imageprocessing/quad/RightAngles.kt similarity index 98% rename from app/src/main/java/org/fairscan/app/domain/quad/RightAngles.kt rename to imageprocessing/src/main/java/org/fairscan/imageprocessing/quad/RightAngles.kt index b79cccd..c32c6d8 100644 --- a/app/src/main/java/org/fairscan/app/domain/quad/RightAngles.kt +++ b/imageprocessing/src/main/java/org/fairscan/imageprocessing/quad/RightAngles.kt @@ -12,9 +12,9 @@ * You should have received a copy of the GNU General Public License along with * this program. If not, see . */ -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 diff --git a/app/src/test/java/org/fairscan/app/domain/GeometryTest.kt b/imageprocessing/src/test/java/org/fairscan/imageprocessing/GeometryTest.kt similarity index 98% rename from app/src/test/java/org/fairscan/app/domain/GeometryTest.kt rename to imageprocessing/src/test/java/org/fairscan/imageprocessing/GeometryTest.kt index a5dfc1c..fa7a17d 100644 --- a/app/src/test/java/org/fairscan/app/domain/GeometryTest.kt +++ b/imageprocessing/src/test/java/org/fairscan/imageprocessing/GeometryTest.kt @@ -12,7 +12,7 @@ * You should have received a copy of the GNU General Public License along with * this program. If not, see . */ -package org.fairscan.app.domain +package org.fairscan.imageprocessing import org.assertj.core.api.Assertions.assertThat import org.assertj.core.api.Assertions.assertThatThrownBy diff --git a/settings.gradle.kts b/settings.gradle.kts index 968b2fd..8d6c3aa 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -21,4 +21,4 @@ dependencyResolutionManagement { rootProject.name = "FairScan" include(":app") - \ No newline at end of file +include(":imageprocessing")