diff --git a/.github/workflows/unit-test.yml b/.github/workflows/unit-test.yml index 621029c9755d123c91d49dfc106367fecfe12c48..c384c722d2b1ab4ec86e1b0bd01ee303f1350be9 100644 --- a/.github/workflows/unit-test.yml +++ b/.github/workflows/unit-test.yml @@ -53,5 +53,8 @@ jobs: - name: Run unit tests run: ./gradlew test + - name: Run integration tests + run: ./gradlew connectedAndroidTest + - name: Code formatting run: ./gradlew spotlessCheck diff --git a/.gitignore b/.gitignore index 23b69bbd44e661fc7e1807f42ec1c2b880f7a7e0..30103e291128e6b33a2355c31e49e55f4b4127dc 100644 --- a/.gitignore +++ b/.gitignore @@ -11,7 +11,8 @@ .DS_Store /build /captures -/opencv +/opencv/* +!/opencv/build.gradle .externalNativeBuild .cxx local.properties diff --git a/app/build.gradle.kts b/app/build.gradle.kts index d2328702c663978eb11947d7ce12cd417f5193a0..a0d2079cb042af49a7c244c4c5de753672e516d2 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -27,11 +27,11 @@ android { } } compileOptions { - sourceCompatibility = JavaVersion.VERSION_17 - targetCompatibility = JavaVersion.VERSION_17 + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 } kotlinOptions { - jvmTarget = "17" + jvmTarget = "1.8" } } @@ -53,10 +53,16 @@ dependencies { implementation("androidx.camera:camera-view:${cameraxVersion}") implementation("androidx.camera:camera-extensions:${cameraxVersion}") + // JSON +// implementation("com.squareup.moshi:moshi:1.15.1") +// implementation("com.squareup.moshi:moshi-adapters:1.15.1") +// implementation("com.squareup.moshi:moshi-kotlin:1.15.1") + implementation("com.google.code.gson:gson:2.10.1") + // OpenCV - implementation(project(":opencv")) + implementation("org.opencv:opencv:4.9.0") - // Test + // Test testImplementation("junit:junit:4.13.2") androidTestImplementation("androidx.test.ext:junit:1.1.5") androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1") diff --git a/app/src/androidTest/java/com/k2_9/omrekap/aprilTag/AprilTagConfigDetectionTest.kt b/app/src/androidTest/java/com/k2_9/omrekap/aprilTag/AprilTagConfigDetectionTest.kt new file mode 100644 index 0000000000000000000000000000000000000000..ff11227c2dd6ff000dbb2d534eb2920d2ce008dc --- /dev/null +++ b/app/src/androidTest/java/com/k2_9/omrekap/aprilTag/AprilTagConfigDetectionTest.kt @@ -0,0 +1,55 @@ +package com.k2_9.omrekap.aprilTag + +import android.util.Log +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.platform.app.InstrumentationRegistry +import com.google.gson.Gson +import com.k2_9.omrekap.R +import com.k2_9.omrekap.data.repository.OMRConfigRepository +import com.k2_9.omrekap.utils.omr.OMRConfigurationDetector +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import org.junit.Test +import org.junit.runner.RunWith +import org.opencv.android.OpenCVLoader +import org.opencv.android.Utils +import org.opencv.core.Mat +import org.opencv.imgproc.Imgproc + +@RunWith(AndroidJUnit4::class) +class AprilTagConfigDetectionTest { + @Test + fun test_detect() { + val appContext = InstrumentationRegistry.getInstrumentation().targetContext + OpenCVLoader.initLocal() + + val imageMat = Utils.loadResource(appContext, R.raw.example) + + val grayImageMat = Mat() + // transform to grayscale for ArucoDetector + Imgproc.cvtColor(imageMat, grayImageMat, Imgproc.COLOR_BGR2GRAY) + + CoroutineScope(Dispatchers.Default).launch { + OMRConfigurationDetector.loadConfiguration( + appContext + ) + val result = OMRConfigurationDetector.detectConfiguration(grayImageMat) + val gson = Gson() + Log.d("ConfigDetectionTestx", gson.toJson(result)) + val compare = OMRConfigRepository.loadConfigurations(appContext) + +// val resultHash = result!!.first.hashCode() +// val compareHash = compare!!.configs["102"].hashCode() +// Log.d("ConfigDetectionTestx1", resultHash.toString()) +// Log.d("ConfigDetectionTestx1", compareHash.toString()) +// assert(resultHash == compareHash) + + val resultJSONString = gson.toJson(result!!.first) + val compareJSONString = gson.toJson(compare!!.omrConfigs["102"]) + Log.d("ConfigDetectionTestx2", resultJSONString) + Log.d("ConfigDetectionTestx2", compareJSONString) + assert(resultJSONString == compareJSONString) + } + } +} diff --git a/app/src/androidTest/java/com/k2_9/omrekap/aprilTag/AprilTagHelperTest.kt b/app/src/androidTest/java/com/k2_9/omrekap/aprilTag/AprilTagHelperTest.kt new file mode 100644 index 0000000000000000000000000000000000000000..085f1dceb723da4045fa0940003c2f4f15bbff19 --- /dev/null +++ b/app/src/androidTest/java/com/k2_9/omrekap/aprilTag/AprilTagHelperTest.kt @@ -0,0 +1,30 @@ +package com.k2_9.omrekap.aprilTag + +import android.util.Log +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.platform.app.InstrumentationRegistry +import com.k2_9.omrekap.R +import com.k2_9.omrekap.utils.AprilTagHelper +import org.junit.Test +import org.junit.runner.RunWith +import org.opencv.android.OpenCVLoader +import org.opencv.android.Utils + +@RunWith(AndroidJUnit4::class) +class AprilTagHelperTest { + private val helper: AprilTagHelper = AprilTagHelper + + @Test + fun testAprilTagDetection() { + OpenCVLoader.initLocal() + + val appContext = InstrumentationRegistry.getInstrumentation().targetContext + + // Load the image resource as a Bitmap + val image = Utils.loadResource(appContext, R.raw.example) + + // Call the method to detect AprilTag + val result = helper.getAprilTagId(image) + Log.d("ContourOMRHelperTest", result.toString()) + } +} diff --git a/app/src/androidTest/java/com/k2_9/omrekap/omr/ContourOMRHelperTest.kt b/app/src/androidTest/java/com/k2_9/omrekap/omr/ContourOMRHelperTest.kt new file mode 100644 index 0000000000000000000000000000000000000000..186f40e5ef968b0a2fdb930162247038930571d7 --- /dev/null +++ b/app/src/androidTest/java/com/k2_9/omrekap/omr/ContourOMRHelperTest.kt @@ -0,0 +1,71 @@ +package com.k2_9.omrekap.omr + +import android.content.Context +import android.util.Log +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.platform.app.InstrumentationRegistry +import com.google.gson.Gson +import com.k2_9.omrekap.R +import com.k2_9.omrekap.data.configs.omr.ContourOMRHelperConfig +import com.k2_9.omrekap.data.configs.omr.OMRCropper +import com.k2_9.omrekap.data.configs.omr.OMRCropperConfig +import com.k2_9.omrekap.data.configs.omr.OMRSection +import com.k2_9.omrekap.utils.SaveHelper +import com.k2_9.omrekap.utils.omr.ContourOMRHelper +import org.junit.Test +import org.junit.runner.RunWith +import org.opencv.android.OpenCVLoader +import org.opencv.android.Utils + +@RunWith(AndroidJUnit4::class) +class ContourOMRHelperTest { + private var helper: ContourOMRHelper + private val appContext: Context + + init { + OpenCVLoader.initLocal() + + appContext = InstrumentationRegistry.getInstrumentation().targetContext + + // Load the image resource as a Bitmap + val image = Utils.loadResource(appContext, R.raw.example) + + val sectionPositions = + mapOf( + OMRSection.FIRST to Pair(780, 373), + OMRSection.SECOND to Pair(0, 0), + OMRSection.THIRD to Pair(0, 0), + ) + + val cropperConfig = + OMRCropperConfig( + image, + Pair(140, 220), + sectionPositions, + ) + + val cropper = OMRCropper(cropperConfig) + + val config = + ContourOMRHelperConfig( + cropper, + 12, + 30, + 0.5f, + 1.5f, + 0.9f, + 230, + ) + Log.d("ContourOMRHelperTest", Gson().toJson(config)) + helper = ContourOMRHelper(config) + } + + @Test + fun test_detect() { + val result = helper.detect(OMRSection.FIRST) + val imageAnnotated = helper.annotateImage(result) + Log.d("ContourOMRHelperTest", result.toString()) + assert(result == 172) + SaveHelper.saveImage(appContext, imageAnnotated, "test", "test_detect") + } +} diff --git a/app/src/androidTest/java/com/k2_9/omrekap/omr/OMRCropperTest.kt b/app/src/androidTest/java/com/k2_9/omrekap/omr/OMRCropperTest.kt new file mode 100644 index 0000000000000000000000000000000000000000..d7853f000a5f4654b9fc88ce073db32c48309c89 --- /dev/null +++ b/app/src/androidTest/java/com/k2_9/omrekap/omr/OMRCropperTest.kt @@ -0,0 +1,57 @@ +package com.k2_9.omrekap.omr + +import android.content.Context +import android.graphics.Bitmap +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.platform.app.InstrumentationRegistry +import com.k2_9.omrekap.R +import com.k2_9.omrekap.data.configs.omr.OMRCropper +import com.k2_9.omrekap.data.configs.omr.OMRCropperConfig +import com.k2_9.omrekap.data.configs.omr.OMRSection +import com.k2_9.omrekap.utils.SaveHelper +import org.junit.Test +import org.junit.runner.RunWith +import org.opencv.android.OpenCVLoader +import org.opencv.android.Utils + +@RunWith(AndroidJUnit4::class) +class OMRCropperTest { + private var cropper: OMRCropper + private var appContext: Context + + init { + OpenCVLoader.initLocal() + + appContext = InstrumentationRegistry.getInstrumentation().targetContext + + // Load the image resource as a Bitmap + val image = Utils.loadResource(appContext, R.raw.example) + + val sectionPositions = + mapOf( + OMRSection.FIRST to Pair(780, 375), + OMRSection.SECOND to Pair(0, 0), + OMRSection.THIRD to Pair(0, 0), + ) + + val config = + OMRCropperConfig( + image, + Pair(140, 225), + sectionPositions, + ) + + cropper = OMRCropper(config) + } + + @Test + fun test_crop() { + val result = cropper.crop(OMRSection.FIRST) + + val bitmap = Bitmap.createBitmap(result.cols(), result.rows(), Bitmap.Config.ARGB_8888) + Utils.matToBitmap(result, bitmap) + + SaveHelper.saveImage(appContext, bitmap, "test", "test_crop.png") + assert(result.width() == 140 && result.height() == 225) + } +} diff --git a/app/src/androidTest/java/com/k2_9/omrekap/omr/TemplateMatchingOMRHelperTest.kt b/app/src/androidTest/java/com/k2_9/omrekap/omr/TemplateMatchingOMRHelperTest.kt new file mode 100644 index 0000000000000000000000000000000000000000..ca8e81920f35f2859e138ba8316ed9b69d65f117 --- /dev/null +++ b/app/src/androidTest/java/com/k2_9/omrekap/omr/TemplateMatchingOMRHelperTest.kt @@ -0,0 +1,72 @@ +package com.k2_9.omrekap.omr + +import android.content.Context +import android.util.Log +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.platform.app.InstrumentationRegistry +import com.k2_9.omrekap.R +import com.k2_9.omrekap.data.configs.omr.CircleTemplateLoader +import com.k2_9.omrekap.data.configs.omr.OMRCropper +import com.k2_9.omrekap.data.configs.omr.OMRCropperConfig +import com.k2_9.omrekap.data.configs.omr.OMRSection +import com.k2_9.omrekap.data.configs.omr.TemplateMatchingOMRHelperConfig +import com.k2_9.omrekap.utils.SaveHelper +import com.k2_9.omrekap.utils.omr.TemplateMatchingOMRHelper +import org.junit.Test +import org.junit.runner.RunWith +import org.opencv.android.OpenCVLoader +import org.opencv.android.Utils +import org.opencv.core.Mat + +@RunWith(AndroidJUnit4::class) +class TemplateMatchingOMRHelperTest { + private var helper: TemplateMatchingOMRHelper + private val image: Mat + private val appContext: Context + + init { + OpenCVLoader.initLocal() + + appContext = InstrumentationRegistry.getInstrumentation().targetContext + + // Load the image resource + image = Utils.loadResource(appContext, R.raw.example) + + val sectionPositions = + mapOf( + OMRSection.FIRST to Pair(780, 373), + OMRSection.SECOND to Pair(0, 0), + OMRSection.THIRD to Pair(0, 0), + ) + + val cropperConfig = + OMRCropperConfig( + image, + Pair(140, 220), + sectionPositions, + ) + + val cropper = OMRCropper(cropperConfig) + + // Load the template image resource + val templateLoader = CircleTemplateLoader(appContext, R.raw.circle_template) + + val config = + TemplateMatchingOMRHelperConfig( + cropper, + templateLoader, + 0.7f, + ) + + helper = TemplateMatchingOMRHelper(config) + } + + @Test + fun test_detect() { + val result = helper.detect(OMRSection.FIRST) + val imgRes = helper.annotateImage(result) + Log.d("TemplateMatchingOMRHelperTest", result.toString()) + SaveHelper.saveImage(appContext, imgRes, "test", "test_detect") + assert(result == 172) + } +} diff --git a/app/src/androidTest/java/com/k2_9/omrekap/preprocess/CropHelperTest.kt b/app/src/androidTest/java/com/k2_9/omrekap/preprocess/CropHelperTest.kt new file mode 100644 index 0000000000000000000000000000000000000000..6d4164215a341bcdae247a6e4f569b0958c7a6b2 --- /dev/null +++ b/app/src/androidTest/java/com/k2_9/omrekap/preprocess/CropHelperTest.kt @@ -0,0 +1,65 @@ +package com.k2_9.omrekap.preprocess + +import android.content.Context +import android.graphics.Bitmap +import androidx.test.platform.app.InstrumentationRegistry +import com.k2_9.omrekap.R +import com.k2_9.omrekap.data.models.ImageSaveData +import com.k2_9.omrekap.utils.CropHelper +import com.k2_9.omrekap.utils.PreprocessHelper +import com.k2_9.omrekap.utils.SaveHelper +import org.junit.After +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 +import org.opencv.android.OpenCVLoader +import org.opencv.android.Utils +import org.opencv.core.CvType +import org.opencv.core.Mat + +@RunWith(JUnit4::class) +class CropHelperTest { + private val image: Mat + private val patternImage: Mat + private val imageBitmap: Bitmap + private val patternBitmap: Bitmap + private val appContext: Context + + private var imageSaveData: ImageSaveData + + init { + OpenCVLoader.initLocal() + + appContext = InstrumentationRegistry.getInstrumentation().targetContext + image = Utils.loadResource(appContext, R.raw.example, CvType.CV_8UC1) + patternImage = Utils.loadResource(appContext, R.raw.corner_pattern, CvType.CV_8UC4) + + patternBitmap = Bitmap.createBitmap(patternImage.width(), patternImage.height(), Bitmap.Config.ARGB_8888) + imageBitmap = Bitmap.createBitmap(image.width(), image.height(), Bitmap.Config.ARGB_8888) + Utils.matToBitmap(image, imageBitmap) + Utils.matToBitmap(patternImage, patternBitmap) + + CropHelper.loadPattern(patternBitmap) + + imageSaveData = ImageSaveData(imageBitmap, imageBitmap, mutableMapOf<String, Int>()) + } + + @Before + fun beforeEachTest() { + imageSaveData = ImageSaveData(imageBitmap, imageBitmap, mutableMapOf<String, Int>()) + } + + @Test + fun test_preprocess_and_crop() { + CropHelper.loadPattern(patternBitmap) + imageSaveData = PreprocessHelper.preprocessImage(imageSaveData) + + SaveHelper.saveImage(appContext, imageSaveData.rawImage, "test", "test_preprocess_raw") + SaveHelper.saveImage(appContext, imageSaveData.annotatedImage, "test", "test_preprocess_annotated") + } + + @After + fun clear() { + } +} diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 3b24980ecc0e1c924f6b18adae0f869483e404b9..c207413611a87f079d1cb7689ee7cd90ca813877 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -17,7 +17,7 @@ android:theme="@style/Theme.Omrekap" tools:targetApi="31"> <activity - android:name=".activities.HomeActivity" + android:name=".views.activities.HomeActivity" android:exported="true"> <intent-filter> <action android:name="android.intent.action.MAIN" /> @@ -25,11 +25,11 @@ <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity> - <activity android:name=".activities.ExpandImageActivity" /> - <activity android:name=".activities.CameraActivity" /> - <activity android:name=".activities.PreviewActivity" /> - <activity android:name=".activities.ResultFromCameraActivity" /> - <activity android:name=".activities.ResultFromGalleryActivity" /> + <activity android:name=".views.activities.ExpandImageActivity" /> + <activity android:name=".views.activities.CameraActivity" /> + <activity android:name=".views.activities.PreviewActivity" /> + <activity android:name=".views.activities.ResultFromCameraActivity" /> + <activity android:name=".views.activities.ResultFromGalleryActivity" /> <meta-data android:name="preloaded_fonts" android:resource="@array/preloaded_fonts" /> diff --git a/app/src/main/assets/omr_config.json b/app/src/main/assets/omr_config.json new file mode 100644 index 0000000000000000000000000000000000000000..7509badb09fb47ba40ca3f29ca4c74f98204369d --- /dev/null +++ b/app/src/main/assets/omr_config.json @@ -0,0 +1,64 @@ +{ + "omrConfigs": { + "102": { + "contourOMRHelperConfig": { + "darkIntensityThreshold": 230, + "darkPercentageThreshold": 0.9, + "maxAspectRatio": 1.5, + "maxRadius": 30, + "minAspectRatio": 0.5, + "minRadius": 12, + "omrCropper": { + "config": { + "image": null, + "omrSectionPosition": { + "FIRST": { + "first": 780, + "second": 373 + }, + "SECOND": { + "first": 0, + "second": 0 + }, + "THIRD": { + "first": 0, + "second": 0 + } + }, + "omrSectionSize": { + "first": 140, + "second": 220 + } + } + } + }, + "templateMatchingOMRHelperConfig": { + "similarityThreshold": 0.7, + "templateLoader": null, + "omrCropper": { + "config": { + "image": null, + "omrSectionPosition": { + "FIRST": { + "first": 780, + "second": 373 + }, + "SECOND": { + "first": 0, + "second": 0 + }, + "THIRD": { + "first": 0, + "second": 0 + } + }, + "omrSectionSize": { + "first": 140, + "second": 220 + } + } + } + } + } + } +} diff --git a/app/src/main/java/com/k2_9/omrekap/data/configs/omr/CircleTemplateLoader.kt b/app/src/main/java/com/k2_9/omrekap/data/configs/omr/CircleTemplateLoader.kt new file mode 100644 index 0000000000000000000000000000000000000000..f2deec73f6eedfca7503d262ac9a0c3c07b565a3 --- /dev/null +++ b/app/src/main/java/com/k2_9/omrekap/data/configs/omr/CircleTemplateLoader.kt @@ -0,0 +1,16 @@ +package com.k2_9.omrekap.data.configs.omr + +import android.content.Context +import org.opencv.core.Mat +import org.opencv.core.MatOfByte +import org.opencv.imgcodecs.Imgcodecs +import java.io.InputStream + +class CircleTemplateLoader(private val appContext: Context, private val resId: Int) { + fun loadTemplateImage(): Mat { + val inputStream: InputStream = appContext.resources.openRawResource(resId) + val byteArray = inputStream.readBytes() + val imgBuffer = MatOfByte(*byteArray) + return Imgcodecs.imdecode(imgBuffer, Imgcodecs.IMREAD_GRAYSCALE) + } +} diff --git a/app/src/main/java/com/k2_9/omrekap/data/configs/omr/ContourOMRHelperConfig.kt b/app/src/main/java/com/k2_9/omrekap/data/configs/omr/ContourOMRHelperConfig.kt new file mode 100644 index 0000000000000000000000000000000000000000..4053bca16b8d67e8257f02100f6df05c50644608 --- /dev/null +++ b/app/src/main/java/com/k2_9/omrekap/data/configs/omr/ContourOMRHelperConfig.kt @@ -0,0 +1,40 @@ +package com.k2_9.omrekap.data.configs.omr + +class ContourOMRHelperConfig( + omrCropper: OMRCropper, + minRadius: Int, + maxRadius: Int, + minAspectRatio: Float, + maxAspectRatio: Float, + darkPercentageThreshold: Float, + darkIntensityThreshold: Int, +) : OMRHelperConfig(omrCropper) { + var minRadius: Int + private set + var maxRadius: Int + private set + var minAspectRatio: Float + private set + var maxAspectRatio: Float + private set + var darkPercentageThreshold: Float + private set + var darkIntensityThreshold: Int + private set + + init { + require(minRadius >= 0) { "minRadius must be non-negative" } + require(maxRadius >= minRadius) { "maxRadius must be greater than or equal to minRadius" } + require(minAspectRatio >= 0.0f) { "minAspectRatio must be non-negative" } + require(maxAspectRatio >= minAspectRatio) { "maxAspectRatio must be greater than or equal to minAspectRatio" } + require(darkPercentageThreshold in 0.0f..1.0f) { "darkPercentageThreshold must be between 0 and 1" } + require(darkIntensityThreshold >= 0) { "darkIntensityThreshold must be non-negative" } + + this.minRadius = minRadius + this.maxRadius = maxRadius + this.minAspectRatio = minAspectRatio + this.maxAspectRatio = maxAspectRatio + this.darkPercentageThreshold = darkPercentageThreshold + this.darkIntensityThreshold = darkIntensityThreshold + } +} diff --git a/app/src/main/java/com/k2_9/omrekap/data/configs/omr/OMRCropper.kt b/app/src/main/java/com/k2_9/omrekap/data/configs/omr/OMRCropper.kt new file mode 100644 index 0000000000000000000000000000000000000000..c70a3d13f6759d9b7cb5995c6d07ff4ad5009163 --- /dev/null +++ b/app/src/main/java/com/k2_9/omrekap/data/configs/omr/OMRCropper.kt @@ -0,0 +1,15 @@ +package com.k2_9.omrekap.data.configs.omr + +import org.opencv.core.Mat +import org.opencv.core.Rect + +class OMRCropper(val config: OMRCropperConfig) { + fun crop(section: OMRSection): Mat { + val (x, y) = config.getSectionPosition(section) + val (width, height) = config.omrSectionSize + + val roi = Rect(x, y, width, height) + + return Mat(config.image, roi) + } +} diff --git a/app/src/main/java/com/k2_9/omrekap/data/configs/omr/OMRCropperConfig.kt b/app/src/main/java/com/k2_9/omrekap/data/configs/omr/OMRCropperConfig.kt new file mode 100644 index 0000000000000000000000000000000000000000..9467e226785d67a374ecbe8cdcc7fdaf755decc4 --- /dev/null +++ b/app/src/main/java/com/k2_9/omrekap/data/configs/omr/OMRCropperConfig.kt @@ -0,0 +1,48 @@ +package com.k2_9.omrekap.data.configs.omr + +import org.opencv.core.Mat + +class OMRCropperConfig( + image: Mat, + val omrSectionSize: Pair<Int, Int>, + omrSectionPosition: Map<OMRSection, Pair<Int, Int>>, +) { + var image: Mat + private set + get() = field.clone() + + // Check if all the sections are present + init { + + // Note: Top-left corner and height must be in the way so that the section is cropped with additional top padding and no bottom padding + // Top padding must have the same size as gap between circles inside the section + + require(omrSectionSize.first >= 0 && omrSectionSize.second >= 0) { + "OMR section size must be non-negative" + } + + require(omrSectionSize.first <= image.width() && omrSectionSize.second <= image.height()) { + "OMR section size must be less than or equal to the image size" + } + + require(omrSectionPosition.keys.containsAll(OMRSection.entries)) { + "All OMR sections must be present" + } + + require(omrSectionPosition.values.all { it.first >= 0 && it.second >= 0 }) { + "OMR section position must be non-negative" + } + + this.image = image.clone() + } + + private val omrSectionPosition: Map<OMRSection, Pair<Int, Int>> = omrSectionPosition.toMap() + + fun getSectionPosition(section: OMRSection): Pair<Int, Int> { + return omrSectionPosition[section]!! + } + + fun setImage(image: Mat) { + this.image = image + } +} diff --git a/app/src/main/java/com/k2_9/omrekap/data/configs/omr/OMRHelperConfig.kt b/app/src/main/java/com/k2_9/omrekap/data/configs/omr/OMRHelperConfig.kt new file mode 100644 index 0000000000000000000000000000000000000000..0022677d2746eec2256441f2115ce5caf0f7ff90 --- /dev/null +++ b/app/src/main/java/com/k2_9/omrekap/data/configs/omr/OMRHelperConfig.kt @@ -0,0 +1,5 @@ +package com.k2_9.omrekap.data.configs.omr + +open class OMRHelperConfig( + val omrCropper: OMRCropper, +) diff --git a/app/src/main/java/com/k2_9/omrekap/data/configs/omr/OMRSection.kt b/app/src/main/java/com/k2_9/omrekap/data/configs/omr/OMRSection.kt new file mode 100644 index 0000000000000000000000000000000000000000..d6f0ce8be6cbb4c13adda1c8c5760bfed2995b3b --- /dev/null +++ b/app/src/main/java/com/k2_9/omrekap/data/configs/omr/OMRSection.kt @@ -0,0 +1,7 @@ +package com.k2_9.omrekap.data.configs.omr + +enum class OMRSection { + FIRST, + SECOND, + THIRD, +} diff --git a/app/src/main/java/com/k2_9/omrekap/data/configs/omr/TemplateMatchingOMRHelperConfig.kt b/app/src/main/java/com/k2_9/omrekap/data/configs/omr/TemplateMatchingOMRHelperConfig.kt new file mode 100644 index 0000000000000000000000000000000000000000..68a481df431371f874a0fa60eb102113a000b742 --- /dev/null +++ b/app/src/main/java/com/k2_9/omrekap/data/configs/omr/TemplateMatchingOMRHelperConfig.kt @@ -0,0 +1,25 @@ +package com.k2_9.omrekap.data.configs.omr + +import org.opencv.core.Mat + +class TemplateMatchingOMRHelperConfig( + omrCropper: OMRCropper, + templateLoader: CircleTemplateLoader, + similarityThreshold: Float, +) : OMRHelperConfig(omrCropper) { + var template: Mat + private set + get() = field.clone() + + var similarityThreshold: Float + private set + + init { + require(similarityThreshold in 0.0..1.0) { + "similarity_threshold must be between 0 and 1" + } + + this.template = templateLoader.loadTemplateImage() + this.similarityThreshold = similarityThreshold + } +} diff --git a/app/src/main/java/com/k2_9/omrekap/models/CornerPoints.kt b/app/src/main/java/com/k2_9/omrekap/data/models/CornerPoints.kt similarity index 80% rename from app/src/main/java/com/k2_9/omrekap/models/CornerPoints.kt rename to app/src/main/java/com/k2_9/omrekap/data/models/CornerPoints.kt index da817ffcbff06acaa6b803bfcdb1f4d2f92f5d52..e3f8f5c8efd4dbf98b66475e340d3b026a7b652d 100644 --- a/app/src/main/java/com/k2_9/omrekap/models/CornerPoints.kt +++ b/app/src/main/java/com/k2_9/omrekap/data/models/CornerPoints.kt @@ -1,4 +1,4 @@ -package com.k2_9.omrekap.models +package com.k2_9.omrekap.data.models import org.opencv.core.Point diff --git a/app/src/main/java/com/k2_9/omrekap/models/ImageSaveData.kt b/app/src/main/java/com/k2_9/omrekap/data/models/ImageSaveData.kt similarity index 63% rename from app/src/main/java/com/k2_9/omrekap/models/ImageSaveData.kt rename to app/src/main/java/com/k2_9/omrekap/data/models/ImageSaveData.kt index 56e1391696c05dd06dd7998ff9a03a9bdaf2f0d5..adef620e1e5f1f49b207db68cb1d2ba0770c41c4 100644 --- a/app/src/main/java/com/k2_9/omrekap/models/ImageSaveData.kt +++ b/app/src/main/java/com/k2_9/omrekap/data/models/ImageSaveData.kt @@ -1,9 +1,9 @@ -package com.k2_9.omrekap.models +package com.k2_9.omrekap.data.models import android.graphics.Bitmap data class ImageSaveData( val rawImage: Bitmap, var annotatedImage: Bitmap, - var data: Map<String, Int>?, + var data: Map<String, Int>, ) diff --git a/app/src/main/java/com/k2_9/omrekap/data/models/OMRBaseConfiguration.kt b/app/src/main/java/com/k2_9/omrekap/data/models/OMRBaseConfiguration.kt new file mode 100644 index 0000000000000000000000000000000000000000..cc9cfa44f2d84e7c50a204dd4ac8c58d64ff553a --- /dev/null +++ b/app/src/main/java/com/k2_9/omrekap/data/models/OMRBaseConfiguration.kt @@ -0,0 +1,16 @@ +package com.k2_9.omrekap.data.models + +import com.k2_9.omrekap.data.configs.omr.ContourOMRHelperConfig +import com.k2_9.omrekap.data.configs.omr.TemplateMatchingOMRHelperConfig + +/** + * Scanned image's OMR detection template + */ +data class OMRBaseConfiguration( + val omrConfigs: Map<String, OMRConfigurationParameter> +) + +data class OMRConfigurationParameter( + val contourOMRHelperConfig: ContourOMRHelperConfig, + val templateMatchingOMRHelperConfig: TemplateMatchingOMRHelperConfig +) diff --git a/app/src/main/java/com/k2_9/omrekap/data/repository/OMRConfigRepository.kt b/app/src/main/java/com/k2_9/omrekap/data/repository/OMRConfigRepository.kt new file mode 100644 index 0000000000000000000000000000000000000000..69bee099f9d12b00e4ca7d566fd3a6b8eceb99b6 --- /dev/null +++ b/app/src/main/java/com/k2_9/omrekap/data/repository/OMRConfigRepository.kt @@ -0,0 +1,46 @@ +package com.k2_9.omrekap.data.repository + +import android.content.Context +import android.util.Log +import android.widget.Toast +import com.k2_9.omrekap.data.models.OMRBaseConfiguration +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import java.io.IOException + +object OMRConfigRepository { + suspend fun loadConfigurations(context: Context): OMRBaseConfiguration? { + val jsonString = withContext(Dispatchers.IO) { + readConfigString(context) + } + return if (jsonString == null) { + Toast.makeText(context, "Error! Unable to read configuration", Toast.LENGTH_SHORT) + .show() + null + } else { + OMRJsonConfigLoader.parseJson(jsonString) + } + } + + fun printConfigurationJson(omrBaseConfiguration: OMRBaseConfiguration): String { + val jsonString = OMRJsonConfigLoader.toJson(omrBaseConfiguration) + Log.d("JSONConfigRepo", jsonString) + return jsonString + } + + private fun readConfigString(context: Context): String? { + val inputStream = context.assets.open("omr_config.json") + return try { + val buffer = ByteArray(inputStream.available()) + inputStream.read(buffer) + Log.d("OMRConfigLoader", String(buffer)) + + String(buffer) + } catch (e: IOException) { + // Handle config read error + null + } finally { + inputStream.close() + } + } +} diff --git a/app/src/main/java/com/k2_9/omrekap/data/repository/OMRJsonConfigLoader.kt b/app/src/main/java/com/k2_9/omrekap/data/repository/OMRJsonConfigLoader.kt new file mode 100644 index 0000000000000000000000000000000000000000..793c0520e1f4b3e931ef9c9b3994e8af3d11897b --- /dev/null +++ b/app/src/main/java/com/k2_9/omrekap/data/repository/OMRJsonConfigLoader.kt @@ -0,0 +1,17 @@ +package com.k2_9.omrekap.data.repository + +import com.google.gson.Gson +import com.k2_9.omrekap.data.models.OMRBaseConfiguration + + +object OMRJsonConfigLoader { + private val gson = Gson() + + fun parseJson(jsonString: String): OMRBaseConfiguration? { + return gson.fromJson(jsonString, OMRBaseConfiguration::class.java) + } + + fun toJson(omrBaseConfiguration: OMRBaseConfiguration): String { + return gson.toJson(omrBaseConfiguration) + } +} diff --git a/app/src/main/java/com/k2_9/omrekap/view_models/ImageDataViewModel.kt b/app/src/main/java/com/k2_9/omrekap/data/view_models/ImageDataViewModel.kt similarity index 73% rename from app/src/main/java/com/k2_9/omrekap/view_models/ImageDataViewModel.kt rename to app/src/main/java/com/k2_9/omrekap/data/view_models/ImageDataViewModel.kt index d444843577ad162a5c2cd5aeed40be8231416a07..c2b93ce92a9476955782096493ae76ba338f6e04 100644 --- a/app/src/main/java/com/k2_9/omrekap/view_models/ImageDataViewModel.kt +++ b/app/src/main/java/com/k2_9/omrekap/data/view_models/ImageDataViewModel.kt @@ -1,21 +1,20 @@ -package com.k2_9.omrekap.view_models +package com.k2_9.omrekap.data.view_models import android.graphics.Bitmap import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import com.k2_9.omrekap.models.ImageSaveData +import com.k2_9.omrekap.data.models.ImageSaveData import kotlinx.coroutines.launch class ImageDataViewModel : ViewModel() { private val _data = MutableLiveData<ImageSaveData>() val data = _data as LiveData<ImageSaveData> - fun processImage(bitmap: Bitmap) { + fun processImage(data: ImageSaveData) { viewModelScope.launch { // TODO: Process the raw image using OMRHelper - val data = ImageSaveData(bitmap, bitmap, mapOf()) _data.value = data } } diff --git a/app/src/main/java/com/k2_9/omrekap/data/view_models/PreviewViewModel.kt b/app/src/main/java/com/k2_9/omrekap/data/view_models/PreviewViewModel.kt new file mode 100644 index 0000000000000000000000000000000000000000..ec0e7a301887f280044a1e1f9f6c5a234b9b1cbc --- /dev/null +++ b/app/src/main/java/com/k2_9/omrekap/data/view_models/PreviewViewModel.kt @@ -0,0 +1,22 @@ +package com.k2_9.omrekap.data.view_models + +import android.graphics.Bitmap +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.k2_9.omrekap.data.models.ImageSaveData +import com.k2_9.omrekap.utils.PreprocessHelper +import kotlinx.coroutines.launch + +class PreviewViewModel : ViewModel() { + private val _data = MutableLiveData<ImageSaveData>() + val data = _data as LiveData<ImageSaveData> + + fun preprocessImage(img: Bitmap) { + viewModelScope.launch { + val data = ImageSaveData(img, img, mapOf()) + _data.value = PreprocessHelper.preprocessImage(data) + } + } +} diff --git a/app/src/main/java/com/k2_9/omrekap/utils/AprilTagHelper.kt b/app/src/main/java/com/k2_9/omrekap/utils/AprilTagHelper.kt index 027e4d85d6ed4e25b16e5c13d47ceb00c8a571c6..bb1d90bd650facb8e1ac5d486c3f13a100420a2f 100644 --- a/app/src/main/java/com/k2_9/omrekap/utils/AprilTagHelper.kt +++ b/app/src/main/java/com/k2_9/omrekap/utils/AprilTagHelper.kt @@ -12,7 +12,7 @@ import org.opencv.objdetect.Objdetect private const val LOG_TAG = "AprilTagHelper" -class AprilTagHelper { +object AprilTagHelper { /** * Uses OpenCV module, remember OpenCVLoader.initLocal() has been run before * @@ -21,7 +21,7 @@ class AprilTagHelper { * @return List of possible IDs detected in the image, * returns empty list if no valid tag is found */ - suspend fun getAprilTagId(imageBitmap: Bitmap): List<String> { + fun getAprilTagId(imageBitmap: Bitmap): Pair<List<String>, List<Mat>> { val grayImageMat: Mat = prepareImage(imageBitmap) return getAprilTagId(grayImageMat) } @@ -36,7 +36,7 @@ class AprilTagHelper { * returns empty list if no valid tag is found */ - suspend fun getAprilTagId(imageMat: Mat): List<String> { + fun getAprilTagId(imageMat: Mat): Pair<List<String>, List<Mat>> { // TODO refactor to singleton pattern if initiation behavior is well known // TODO refactor AprilTag family to be read from config file val detector: ArucoDetector = @@ -45,10 +45,10 @@ class AprilTagHelper { ) // prepare output data containers - val corners: List<Mat> = ArrayList() + val corners: MutableList<Mat> = ArrayList() val idMat = Mat() // added in case needed in the future or for debugging purpose - val rejectedCandidates: List<Mat> = ArrayList() + val rejectedCandidates: MutableList<Mat> = ArrayList() // perform detection detector.detectMarkers(imageMat, corners, idMat, rejectedCandidates) @@ -59,12 +59,27 @@ class AprilTagHelper { val nId: Int = idMat.size().height.toInt() logDebug("found $nId IDs") for (i in 0..<nId) { - val id = idMat[i, 0][0].toInt() + val id = idMat[i, 0][0].toInt().toString() logDebug("detected tag with id: $id") - idList += id.toString() + idList.add(id) } - return idList + return (idList to corners) + } + + fun annotateImage(imageBitmap: Bitmap): Bitmap { + val res = getAprilTagId(imageBitmap) + val cornerPoints = res.second + val ids = (res.first)[0] + val annotatedImageMat = + ImageAnnotationHelper.annotateAprilTag(prepareImage(imageBitmap), cornerPoints, ids) + val annotatedImageBitmap = Bitmap.createBitmap( + annotatedImageMat.width(), + annotatedImageMat.height(), + Bitmap.Config.ARGB_8888 + ) + Utils.matToBitmap(annotatedImageMat, annotatedImageBitmap) + return annotatedImageBitmap } private fun prepareDetector(detectorDictionary: Dictionary): ArucoDetector { diff --git a/app/src/main/java/com/k2_9/omrekap/utils/CropHelper.kt b/app/src/main/java/com/k2_9/omrekap/utils/CropHelper.kt index 6ecae2fa024664f3f9b8e51853310cdd122aca34..b48ee52ec2e6d6f6f8825d511bace2516a6cf13d 100644 --- a/app/src/main/java/com/k2_9/omrekap/utils/CropHelper.kt +++ b/app/src/main/java/com/k2_9/omrekap/utils/CropHelper.kt @@ -1,61 +1,77 @@ package com.k2_9.omrekap.utils -import android.content.Context import android.graphics.Bitmap -import android.graphics.BitmapFactory import android.util.Log -import com.k2_9.omrekap.R -import com.k2_9.omrekap.models.CornerPoints +import com.k2_9.omrekap.data.models.CornerPoints import org.opencv.android.Utils import org.opencv.core.CvType import org.opencv.core.Mat import org.opencv.core.MatOfPoint2f import org.opencv.core.Point import org.opencv.imgproc.Imgproc +import org.opencv.imgproc.Imgproc.COLOR_BGR2GRAY +import org.opencv.imgproc.Imgproc.cvtColor import org.opencv.imgproc.Imgproc.getPerspectiveTransform import org.opencv.imgproc.Imgproc.warpPerspective import kotlin.math.pow import kotlin.math.sqrt -class CropHelper { - - fun detectCorner(image: Bitmap, caller: Context): CornerPoints { - // Convert to Matrix (Mat) - val imageMatrix = Mat(image.height, image.width, CvType.CV_8UC1) - Utils.bitmapToMat(image, imageMatrix) - - // Find corner pattern - val options = BitmapFactory.Options() - options.inPreferredConfig = Bitmap.Config.ARGB_8888 - val cornerPatternBitmap: Bitmap - = BitmapFactory.decodeResource(caller.resources, R.raw.corner_pattern, options) - val cornerPatternMatrix = Mat(cornerPatternBitmap.height, cornerPatternBitmap.width, CvType.CV_8UC1) - Utils.bitmapToMat(cornerPatternBitmap, cornerPatternMatrix) - - val resultMatrix = Mat( - image.height - cornerPatternBitmap.height + 1, - image.width - cornerPatternBitmap.width + 1, - CvType.CV_8UC1 - ) - Imgproc.matchTemplate(imageMatrix, cornerPatternMatrix, resultMatrix, Imgproc.TM_SQDIFF_NORMED) - var upperLeftPoint: Point = Point() - var upperRightPoint: Point = Point() - var lowerLeftPoint: Point = Point() - var lowerRightPoint: Point = Point() +object CropHelper { + private const val UPPER_LEFT: Int = 0 + private const val UPPER_RIGHT: Int = 1 + private const val LOWER_RIGHT: Int = 2 + private const val LOWER_LEFT: Int = 3 + + private lateinit var pattern: Mat + + fun loadPattern(patternBitmap: Bitmap) { + // Load only if pattern hasn't been loaded + if (::pattern.isInitialized) return + + this.pattern = Mat(patternBitmap.height, patternBitmap.width, CvType.CV_8UC1) + val cv8uc4pattern = Mat(patternBitmap.height, patternBitmap.width, CvType.CV_8UC1) + Utils.bitmapToMat(patternBitmap, cv8uc4pattern) + cvtColor(cv8uc4pattern, this.pattern, COLOR_BGR2GRAY) + + PreprocessHelper.preprocessPattern(this.pattern) + } + + fun detectCorner(img: Mat): CornerPoints { + // If pattern hasn't been loaded, throw exception + if (!::pattern.isInitialized) { + throw Exception("Pattern not loaded!") + } + + val imgGray = img.clone() + cvtColor(img, imgGray, COLOR_BGR2GRAY) + + val resultMatrix = + Mat( + img.height() - pattern.height() + 1, + img.width() - pattern.width() + 1, + CvType.CV_8UC1, + ) + + Imgproc.matchTemplate(imgGray, pattern, resultMatrix, Imgproc.TM_SQDIFF_NORMED) + + var upperLeftPoint = Point() + var upperRightPoint = Point() + var lowerLeftPoint = Point() + var lowerRightPoint = Point() val needed = mutableListOf(true, true, true, true) - var needChange: Int = 4 + var needChange = 4 - data class PointsAndWeight ( + data class PointsAndWeight( val x: Int, val y: Int, - val weight: Double + val weight: Double, ) val pointsList: MutableList<PointsAndWeight> = mutableListOf() - for (i in 0..<resultMatrix.height() step 4) { - for (j in 0..<resultMatrix.width() step 4) { + for (i in 0 until resultMatrix.height() step 4) { + for (j in 0 until resultMatrix.width() step 4) { pointsList.add(PointsAndWeight(i, j, resultMatrix.get(i, j)[0])) } } @@ -63,41 +79,52 @@ class CropHelper { pointsList.sortBy { it.weight } pointsList.forEach { - if (needChange == 0) { - return@forEach - } + if (needChange == 0) return@forEach + val corner = nearWhichCorner(it.x, it.y, resultMatrix.height(), resultMatrix.width(), limFrac = 0.1F) - if (corner == -1) { - return@forEach - } + if (corner == -1) return@forEach + if (needed[corner]) { needed[corner] = false needChange-- - val pointFromIt = Point(it.x.toDouble(), it.y.toDouble()) + val pointFromIt = Point(it.y.toDouble(), it.x.toDouble()) when (corner) { - UPPER_LEFT -> upperLeftPoint = pointFromIt - UPPER_RIGHT -> upperRightPoint = pointFromIt - LOWER_RIGHT -> lowerRightPoint = pointFromIt - LOWER_LEFT -> lowerLeftPoint = pointFromIt + UPPER_LEFT -> { + upperLeftPoint = pointFromIt + } + UPPER_RIGHT -> { + upperRightPoint = pointFromIt + upperRightPoint.x += pattern.height().toDouble() + } + LOWER_RIGHT -> { + lowerRightPoint = pointFromIt + lowerRightPoint.x += pattern.height().toDouble() + lowerRightPoint.y += pattern.width().toDouble() + } + LOWER_LEFT -> { + lowerLeftPoint = pointFromIt + lowerLeftPoint.y += pattern.width().toDouble() + } } } } - - Log.d("Corner", "type = ${resultMatrix.type()}, should be ${CvType.CV_8UC1}") - if (needChange > 0) { throw Exception("Not all corner points found!") } + + Log.d("Corner", CornerPoints(upperLeftPoint, upperRightPoint, lowerRightPoint, lowerLeftPoint).toString()) return CornerPoints(upperLeftPoint, upperRightPoint, lowerRightPoint, lowerLeftPoint) } fun fourPointTransform( - image: Bitmap, + img: Mat, points: CornerPoints, - ): Bitmap { - val mult = 0.03 + ): Mat { + // Multiplier for padding, can be adjusted + val mult = 0.01 + // Calculate new corner points val newTopLeft = Point( points.topLeft.x - (points.topRight.x - points.topLeft.x) * mult - (points.bottomLeft.x - points.topLeft.x) * mult, @@ -124,69 +151,36 @@ class CropHelper { val newPoints = CornerPoints(newTopLeft, newTopRight, newBottomRight, newBottomLeft) - val topWidth = - sqrt( - (newPoints.topRight.x - newPoints.topLeft.x).pow(2) + - (newPoints.topRight.y - newPoints.topLeft.y).pow(2), - ) - + // Calculate aspect ratio + val topWidth = sqrt((newPoints.topRight.x - newPoints.topLeft.x).pow(2) + (newPoints.topRight.y - newPoints.topLeft.y).pow(2)) val bottomWidth = - sqrt( - (newPoints.bottomRight.x - newPoints.bottomLeft.x).pow(2) + - (newPoints.bottomRight.y - newPoints.bottomLeft.y).pow(2), - ) - + sqrt((newPoints.bottomRight.x - newPoints.bottomLeft.x).pow(2) + (newPoints.bottomRight.y - newPoints.bottomLeft.y).pow(2)) val maxWidth = maxOf(topWidth, bottomWidth) - val leftHeight = - sqrt( - (newPoints.bottomLeft.x - newPoints.topLeft.x).pow(2) + - (newPoints.bottomLeft.y - newPoints.topLeft.y).pow(2), - ) - - val rightHeight = - sqrt( - (newPoints.bottomRight.x - newPoints.topRight.x).pow(2) + - (newPoints.bottomRight.y - newPoints.topRight.y).pow(2), - ) - + val leftHeight = sqrt((newPoints.bottomLeft.x - newPoints.topLeft.x).pow(2) + (newPoints.bottomLeft.y - newPoints.topLeft.y).pow(2)) + val rightHeight = sqrt((newPoints.bottomRight.x - newPoints.topRight.x).pow(2) + (newPoints.bottomRight.y - newPoints.topRight.y).pow(2)) val maxHeight = maxOf(leftHeight, rightHeight) val aspectRatio = maxWidth / maxHeight - val width = 800 + // Calculate new image size + val width = 540 val height = (width / aspectRatio).toInt() - val srcMatrix = - MatOfPoint2f( - newPoints.topLeft, - newPoints.topRight, - newPoints.bottomRight, - newPoints.bottomLeft, - ) - - val dstMatrix = - MatOfPoint2f( - Point(0.0, 0.0), - Point(width - 1.0, 0.0), - Point(0.0, height - 1.0), - Point(width - 1.0, height - 1.0), - ) + // Create source and destination matrix + val srcMatrix = MatOfPoint2f(newPoints.topLeft, newPoints.topRight, newPoints.bottomRight, newPoints.bottomLeft) + val dstMatrix = MatOfPoint2f(Point(0.0, 0.0), Point(width - 1.0, 0.0), Point(width - 1.0, height - 1.0), Point(0.0, height - 1.0)) + // Get perspective transform matrix val transformMatrix = getPerspectiveTransform(srcMatrix, dstMatrix) - val imageMatrix = Mat(image.height, image.width, CvType.CV_8UC1) - Utils.bitmapToMat(image, imageMatrix) + // Warp image + val result = Mat(height, width, CvType.CV_8UC1) + warpPerspective(img, result, transformMatrix, result.size()) - val resultMatrix = Mat(height, width, CvType.CV_8UC1) - warpPerspective(imageMatrix, resultMatrix, transformMatrix, resultMatrix.size()) - - val bitmapResult: Bitmap = Bitmap.createBitmap(resultMatrix.width(), resultMatrix.height(), Bitmap.Config.ARGB_8888) - Utils.matToBitmap(resultMatrix, bitmapResult) - return bitmapResult + return result } - /** * Get corner ID near enough to the point * @@ -200,11 +194,15 @@ class CropHelper { * else return -1 * */ - private fun nearWhichCorner(x: Int, y: Int, - height: Int, width: Int, - limX: Int? = null, - limY: Int? = null, - limFrac: Float = 0.1F): Int { + private fun nearWhichCorner( + x: Int, + y: Int, + height: Int, + width: Int, + limX: Int? = null, + limY: Int? = null, + limFrac: Float = 0.1F, + ): Int { // process limit val limitX: Int = limX ?: (limFrac / 2 * height).toInt() val limitY: Int = limY ?: (limFrac / 2 * width).toInt() @@ -222,11 +220,4 @@ class CropHelper { else -> -1 } } - - companion object { - const val UPPER_LEFT: Int = 0 - const val UPPER_RIGHT: Int = 1 - const val LOWER_RIGHT: Int = 2 - const val LOWER_LEFT: Int = 3 - } } diff --git a/app/src/main/java/com/k2_9/omrekap/utils/ImageAnnotationHelper.kt b/app/src/main/java/com/k2_9/omrekap/utils/ImageAnnotationHelper.kt new file mode 100644 index 0000000000000000000000000000000000000000..8a2cb6e34a0a7bccd9b732d492fd2a81495c6d0c --- /dev/null +++ b/app/src/main/java/com/k2_9/omrekap/utils/ImageAnnotationHelper.kt @@ -0,0 +1,90 @@ +package com.k2_9.omrekap.utils + +import com.k2_9.omrekap.data.models.CornerPoints +import org.opencv.core.Mat +import org.opencv.core.MatOfPoint +import org.opencv.core.Point +import org.opencv.core.Rect +import org.opencv.core.Scalar +import org.opencv.imgproc.Imgproc + +object ImageAnnotationHelper { + fun annotateCorner( + img: Mat, + cornerPoints: CornerPoints, + ): Mat { + val imgWithAnnotations = img.clone() + Imgproc.circle(imgWithAnnotations, cornerPoints.topLeft, 10, Scalar(0.0, 255.0, 0.0), 5) + Imgproc.circle(imgWithAnnotations, cornerPoints.topRight, 10, Scalar(0.0, 255.0, 0.0), 5) + Imgproc.circle(imgWithAnnotations, cornerPoints.bottomLeft, 10, Scalar(0.0, 255.0, 0.0), 5) + Imgproc.circle(imgWithAnnotations, cornerPoints.bottomRight, 10, Scalar(0.0, 255.0, 0.0), 5) + return imgWithAnnotations + } + + fun annotateAprilTag( + img: Mat, + cornerPoints: List<Mat>, + id: String, + ): Mat { + val imgWithAnnotations = img.clone() + if (id.isNotEmpty()) { + val points = + cornerPoints.map { mat -> + val x = mat.get(0, 0)[0] + val y = mat.get(1, 0)[0] + Point(x, y) + } + + // Draw ID and bounding box + Imgproc.putText(imgWithAnnotations, id, points[0], Imgproc.FONT_HERSHEY_SIMPLEX, 1.0, Scalar(0.0, 255.0, 0.0), 5) + Imgproc.polylines(imgWithAnnotations, listOf(MatOfPoint(*points.toTypedArray())), true, Scalar(0.0, 255.0, 0.0), 5) + } else { + val topLeft = Point(cornerPoints[0].get(0, 0)[0], cornerPoints[0].get(1, 0)[0]) + Imgproc.putText(imgWithAnnotations, "April Tag Not Detected", topLeft, Imgproc.FONT_HERSHEY_SIMPLEX, 1.0, Scalar(0.0, 255.0, 0.0), 5) + } + return imgWithAnnotations + } + + fun annotateTemplateMatchingOMR( + img: Mat, + cornerPoints: List<Rect>, + contourNumber: Int, + ): Mat { + val imgWithAnnotations = img.clone() + for (rect in cornerPoints) { + Imgproc.rectangle(imgWithAnnotations, rect.tl(), rect.br(), Scalar(0.0, 255.0, 0.0), 1) + } + Imgproc.putText( + imgWithAnnotations, + "$contourNumber", + cornerPoints[0].tl(), + Imgproc.FONT_HERSHEY_SIMPLEX, + 0.5, + Scalar(0.0, 255.0, 0.0), + 1, + ) + return imgWithAnnotations + } + + fun annotateContourOMR( + img: Mat, + cornerPoints: List<MatOfPoint>, + contourNumber: Int, + ): Mat { + val imgWithAnnotations = img.clone() + for (contour in cornerPoints) { + val rect = Imgproc.boundingRect(contour) + Imgproc.rectangle(imgWithAnnotations, rect.tl(), rect.br(), Scalar(0.0, 255.0, 0.0), 1) + } + Imgproc.putText( + imgWithAnnotations, + "$contourNumber", + Point(0.0, 20.0), + Imgproc.FONT_HERSHEY_SIMPLEX, + 0.5, + Scalar(0.0, 255.0, 0.0), + 1, + ) + return imgWithAnnotations + } +} diff --git a/app/src/main/java/com/k2_9/omrekap/utils/ImageSaveDataHolder.kt b/app/src/main/java/com/k2_9/omrekap/utils/ImageSaveDataHolder.kt new file mode 100644 index 0000000000000000000000000000000000000000..2f396779aefa96385121e8ab3ca48b759fc545b0 --- /dev/null +++ b/app/src/main/java/com/k2_9/omrekap/utils/ImageSaveDataHolder.kt @@ -0,0 +1,21 @@ +package com.k2_9.omrekap.utils + +import android.util.Log +import com.k2_9.omrekap.data.models.ImageSaveData + +object ImageSaveDataHolder { + private var imageSaveData: ImageSaveData? = null + + fun save(data: ImageSaveData) { + imageSaveData = data + } + + fun get(): ImageSaveData { + if (imageSaveData == null) { + Log.e("ImageSaveDataHolder", "ImageSaveData is null") + throw RuntimeException("ImageSaveData is null") + } + + return imageSaveData!! + } +} diff --git a/app/src/main/java/com/k2_9/omrekap/utils/PermissionHelper.kt b/app/src/main/java/com/k2_9/omrekap/utils/PermissionHelper.kt new file mode 100644 index 0000000000000000000000000000000000000000..70e9154a83d6f9a32ac8eab7e6eeb9993ad334d5 --- /dev/null +++ b/app/src/main/java/com/k2_9/omrekap/utils/PermissionHelper.kt @@ -0,0 +1,37 @@ +package com.k2_9.omrekap.utils + +import android.content.pm.PackageManager +import android.widget.Toast +import androidx.activity.result.contract.ActivityResultContracts +import androidx.appcompat.app.AppCompatActivity +import androidx.core.content.ContextCompat + +object PermissionHelper { + fun requirePermission( + activity: AppCompatActivity, + permission: String, + verbose: Boolean = true, + operation: () -> Unit, + ) { + if (ContextCompat.checkSelfPermission( + activity, + permission, + ) == PackageManager.PERMISSION_GRANTED + ) { + operation() + } else { + val requestPermissionLauncher = + activity.registerForActivityResult(ActivityResultContracts.RequestPermission()) { + isGranted: Boolean -> + if (isGranted) { + operation() + } else { + if (verbose) { + Toast.makeText(activity, "Permission denied", Toast.LENGTH_SHORT).show() + } + } + } + requestPermissionLauncher.launch(permission) + } + } +} diff --git a/app/src/main/java/com/k2_9/omrekap/utils/PreprocessHelper.kt b/app/src/main/java/com/k2_9/omrekap/utils/PreprocessHelper.kt new file mode 100644 index 0000000000000000000000000000000000000000..827b27a7e3d25adda0cdf6a5c4ac716c88159405 --- /dev/null +++ b/app/src/main/java/com/k2_9/omrekap/utils/PreprocessHelper.kt @@ -0,0 +1,76 @@ +package com.k2_9.omrekap.utils + +import android.graphics.Bitmap +import com.k2_9.omrekap.data.models.ImageSaveData +import org.opencv.android.Utils +import org.opencv.core.Core +import org.opencv.core.Mat +import org.opencv.core.Size +import org.opencv.imgproc.Imgproc + +object PreprocessHelper { + private const val FINAL_WIDTH = 540.0 + private const val FINAL_HEIGHT = 960.0 + + fun preprocessImage(data: ImageSaveData): ImageSaveData { + // Initialize Mats + val mainImageMat = Mat() + val annotatedImageMat = Mat() + + Utils.bitmapToMat(data.rawImage, mainImageMat) + Utils.bitmapToMat(data.annotatedImage, annotatedImageMat) + + // Preprocess both images + var mainImageResult = preprocessMat(mainImageMat) + var annotatedImageResult = preprocessMat(annotatedImageMat) + + // Get corner points + val cornerPoints = CropHelper.detectCorner(mainImageResult) + + // Annotate annotated image + // TODO: Call function to annotate image + annotatedImageResult = ImageAnnotationHelper.annotateCorner(annotatedImageResult, cornerPoints) + + // Crop both images + mainImageResult = CropHelper.fourPointTransform(mainImageResult, cornerPoints) + annotatedImageResult = CropHelper.fourPointTransform(annotatedImageResult, cornerPoints) + + // Re-resize both images + mainImageResult = resizeMat(mainImageResult) + annotatedImageResult = resizeMat(annotatedImageResult) + + // Convert Mats to Bitmaps + val mainImageBitmap = Bitmap.createBitmap(mainImageResult.width(), mainImageResult.height(), Bitmap.Config.ARGB_8888) + val annotatedImageBitmap = Bitmap.createBitmap(annotatedImageResult.width(), annotatedImageResult.height(), Bitmap.Config.ARGB_8888) + + Utils.matToBitmap(mainImageResult, mainImageBitmap) + Utils.matToBitmap(annotatedImageResult, annotatedImageBitmap) + + return ImageSaveData(mainImageBitmap, annotatedImageBitmap, data.data) + } + + private fun preprocessMat(img: Mat): Mat { + return img.apply { + resizeMat(this) + normalize(this) + } + } + + fun preprocessPattern(img: Mat): Mat { + return img.apply { + normalize(this) + } + } + + private fun resizeMat(img: Mat): Mat { + val resizedImg = Mat() + Imgproc.resize(img, resizedImg, Size(FINAL_WIDTH, FINAL_HEIGHT)) + return resizedImg + } + + private fun normalize(img: Mat): Mat { + val normalizedImg = Mat() + Core.normalize(img, normalizedImg) + return normalizedImg + } +} diff --git a/app/src/main/java/com/k2_9/omrekap/utils/SaveHelper.kt b/app/src/main/java/com/k2_9/omrekap/utils/SaveHelper.kt index 3c52b72656262157ba6a2c5a145d5d1b3fa73291..7d22fc16b18f8713c1caee7dc6d2cb0e228149d2 100644 --- a/app/src/main/java/com/k2_9/omrekap/utils/SaveHelper.kt +++ b/app/src/main/java/com/k2_9/omrekap/utils/SaveHelper.kt @@ -9,7 +9,7 @@ import android.os.Build import android.os.Environment import android.provider.MediaStore import androidx.annotation.RequiresApi -import com.k2_9.omrekap.models.ImageSaveData +import com.k2_9.omrekap.data.models.ImageSaveData import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import java.io.File @@ -19,34 +19,30 @@ import java.text.SimpleDateFormat import java.util.Date import java.util.Locale -class SaveHelper { +object SaveHelper { suspend fun save( context: Context, data: ImageSaveData, ) { val folderName: String = generateFolderName() - if (data.rawImage == null) { - throw RuntimeException("The raw image bitmap is null") - } - - if (data.annotatedImage == null || data.data == null) { - throw RuntimeException("Image has not been processed yet") - } +// TODO: Uncomment after implemented +// if (data.data.isEmpty()) { +// throw RuntimeException("Image has not been processed yet") +// } - if (data.rawImage!!.width <= 0 || data.rawImage!!.height <= 0) { + if (data.rawImage.width <= 0 || data.rawImage.height <= 0) { throw RuntimeException("The raw image bitmap is empty") } - if (data.annotatedImage!!.width <= 0 || data.rawImage!!.height <= 0) - { - throw RuntimeException("The annotated image bitmap is empty") - } + if (data.annotatedImage.width <= 0 || data.rawImage.height <= 0) { + throw RuntimeException("The annotated image bitmap is empty") + } withContext(Dispatchers.IO) { saveImage(context, data.rawImage, folderName, "raw_image.jpg") saveImage(context, data.annotatedImage, folderName, "annotated_image.jpg") - saveJSON(context, data.data!!, folderName, "data.json") + saveJSON(context, data.data, folderName, "data.json") } } @@ -66,7 +62,7 @@ class SaveHelper { return sdf.format(Date()) } - private fun saveImage( + fun saveImage( context: Context, image: Bitmap, folderName: String, diff --git a/app/src/main/java/com/k2_9/omrekap/utils/omr/ContourOMRHelper.kt b/app/src/main/java/com/k2_9/omrekap/utils/omr/ContourOMRHelper.kt new file mode 100644 index 0000000000000000000000000000000000000000..2ec887351e78e29105acad6d4d9c0bf7dec8278b --- /dev/null +++ b/app/src/main/java/com/k2_9/omrekap/utils/omr/ContourOMRHelper.kt @@ -0,0 +1,200 @@ +package com.k2_9.omrekap.utils.omr + +import android.graphics.Bitmap +import android.util.Log +import com.k2_9.omrekap.data.configs.omr.ContourOMRHelperConfig +import com.k2_9.omrekap.data.configs.omr.OMRSection +import com.k2_9.omrekap.utils.ImageAnnotationHelper +import org.opencv.android.Utils +import org.opencv.core.Core +import org.opencv.core.CvType +import org.opencv.core.Mat +import org.opencv.core.MatOfPoint +import org.opencv.core.Scalar +import org.opencv.imgproc.Imgproc + +class ContourOMRHelper(private val config: ContourOMRHelperConfig) : OMRHelper(config) { + private var currentSectionGray: Mat? = null + private var currentSectionBinary: Mat? = null + + private fun createContourInfo(contour: Mat): ContourInfo { + val rect = Imgproc.boundingRect(contour) + val centerX = rect.x + rect.width / 2 + val centerY = rect.y + rect.height / 2 + return ContourInfo(Pair(centerX, centerY), Pair(rect.width, rect.height)) + } + + private fun getContourInfo(filledContours: List<Mat>): List<ContourInfo?> { + val contourInfos = mutableListOf<ContourInfo?>() + val sortedContours = filledContours.sortedBy { Imgproc.boundingRect(it).x } + + // Sort the filled contours from left to right and get the center and size of each contour + for (contour in sortedContours) { + contourInfos.add(createContourInfo(contour)) + } + return filterContourInfos(contourInfos) + } + + private fun predictForFilledCircle(contours: List<MatOfPoint>): Int { + // Predict the number based on the filled circle contours + + val filledContours = mutableListOf<Mat>() + + for (contour in contours) { + val mask = Mat.zeros(currentSectionBinary!!.size(), CvType.CV_8UC1) + Imgproc.drawContours(mask, listOf(contour), -1, Scalar(255.0), -1) + + // Apply the mask to the binary image + val maskedBinary = Mat() + Core.bitwise_and(currentSectionBinary!!, currentSectionBinary!!, maskedBinary, mask) + + // Compute the total intensity (number of non-zero pixels) within the contour area + val totalIntensity = Core.countNonZero(maskedBinary) + + // Compute the total number of pixels within the contour area + val contourArea = Imgproc.contourArea(contour) + + // Compute the percentage of dark pixels inside the contour + val percentageDarkPixels = totalIntensity.toDouble() / contourArea + + if (totalIntensity > config.darkIntensityThreshold && + percentageDarkPixels >= config.darkPercentageThreshold + ) { + filledContours.add(contour) + } + } + + val contourInfos = getContourInfo(filledContours) + return contourInfosToNumbers(contourInfos) + } + + private fun getDarkestRow(colContours: List<MatOfPoint>): Int? { + // Initialize variables to store the darkest row and its intensity + var darkestRow: Int? = null + var darkestIntensity = 0.0 + + // Loop through contours in the column + for ((idx, contour) in colContours.withIndex()) { + // Construct a mask for the current contour + val mask = Mat.zeros(currentSectionBinary!!.size(), CvType.CV_8UC1) + Imgproc.drawContours(mask, listOf(contour), -1, Scalar(255.0), -1) + + // # Apply the mask to the binary image + val maskedBinary = Mat() + Core.bitwise_and(currentSectionBinary!!, currentSectionBinary!!, maskedBinary, mask) + + // Compute the total intensity (number of non-zero pixels) within the contour area + val totalIntensity = Core.countNonZero(maskedBinary) + + // Compute the total number of pixels within the contour area + val contourArea = Imgproc.contourArea(contour) + + // Compute the percentage of dark pixels inside the contour + val percentageDarkPixels = totalIntensity.toDouble() / contourArea + + // Update the darkest row if necessary + if (darkestIntensity < totalIntensity && + totalIntensity >= config.darkIntensityThreshold && + percentageDarkPixels >= config.darkPercentageThreshold + ) { + darkestIntensity = totalIntensity.toDouble() + darkestRow = idx + } + } + if (darkestRow == null) { + // If no darkest row is found, return null + Log.e("ContourOMRHelper", "No darkest row found") + } + return darkestRow + } + + private fun compareAll(contours: List<MatOfPoint>): Int { + // Sort contours by column and then by row + val contoursSorted = contours.sortedBy { Imgproc.boundingRect(it).x } + + // Initialize a list to store the darkest contour row for each column + val darkestRows = mutableListOf<Int?>() + + // Loop through each column + for (col in 0 until 3) { + // Get contours for the current column and sort by rows + val colContours = contoursSorted.subList(col * 10, (col + 1) * 10).sortedBy { Imgproc.boundingRect(it).y } + + val darkestRow = getDarkestRow(colContours) + + // Append the darkest row for the current column to the list + darkestRows.add(darkestRow) + } + + darkestRows.forEachIndexed { idx, darkestRow -> + if (darkestRow == null) { + Log.e("ContourOMRHelper", "No darkest row found for column ${idx + 1}. Assuming 0") + } + } + return getCombinedNumbers(darkestRows.map { it ?: 0 }) + } + + private fun getAllContours(): List<MatOfPoint> { + // Find circle contours in cropped OMR section + val contours = mutableListOf<MatOfPoint>() + val hierarchy = Mat() + Imgproc.findContours(currentSectionBinary!!, contours, hierarchy, Imgproc.RETR_EXTERNAL, Imgproc.CHAIN_APPROX_SIMPLE) + + // Initialize a list to store filtered contours + val filteredContours = mutableListOf<MatOfPoint>() + + // Filter contours based on aspect ratio and size + for (contour in contours) { + val rect = Imgproc.boundingRect(contour) + val ar = rect.width.toDouble() / rect.height.toDouble() + val minLength = config.minRadius + val maxLength = config.maxRadius + val minAR = config.minAspectRatio + val maxAR = config.maxAspectRatio + + if (rect.width in minLength..maxLength && rect.height in minLength..maxLength && ar >= minAR && ar <= maxAR) { + filteredContours.add(contour) + } else { + Log.d("ContourOMRHelper", "Contour with aspect ratio $ar and size ${rect.width} x ${rect.height} filtered out") + } + } + + return filteredContours + } + + fun annotateImage(contourNumber: Int): Bitmap { + var annotatedImg = currentSectionGray!!.clone() + val contours = getAllContours() + annotatedImg = ImageAnnotationHelper.annotateContourOMR(annotatedImg, contours, contourNumber) + + val annotatedImageBitmap = Bitmap.createBitmap(annotatedImg.width(), annotatedImg.height(), Bitmap.Config.ARGB_8888) + Utils.matToBitmap(annotatedImg, annotatedImageBitmap) + return annotatedImageBitmap + } + + override fun detect(section: OMRSection): Int { + val omrSectionImage = config.omrCropper.crop(section) + + // Convert image to grayscale + val gray = Mat() + Imgproc.cvtColor(omrSectionImage, gray, Imgproc.COLOR_BGR2GRAY) + + // Apply binary thresholding + val binary = Mat() + Imgproc.threshold(gray, binary, 0.0, 255.0, Imgproc.THRESH_BINARY_INV + Imgproc.THRESH_TRIANGLE) + + // Update states + currentSectionGray = gray + currentSectionBinary = binary + + val contours = getAllContours() + + return if (contours.size != 30) { + Log.d("ContourOMRHelper", "Some circles are not detected, considering only filled circles") + predictForFilledCircle(contours) + } else { + Log.d("ContourOMRHelper", "All 30 circles are detected") + compareAll(contours) + } + } +} diff --git a/app/src/main/java/com/k2_9/omrekap/utils/omr/OMRConfigurationDetector.kt b/app/src/main/java/com/k2_9/omrekap/utils/omr/OMRConfigurationDetector.kt new file mode 100644 index 0000000000000000000000000000000000000000..42b66317fd7972b1af6ba7ee74435284f4c8817e --- /dev/null +++ b/app/src/main/java/com/k2_9/omrekap/utils/omr/OMRConfigurationDetector.kt @@ -0,0 +1,57 @@ +package com.k2_9.omrekap.utils.omr + +import android.content.Context +import android.util.Log +import com.k2_9.omrekap.data.models.OMRBaseConfiguration +import com.k2_9.omrekap.data.models.OMRConfigurationParameter +import com.k2_9.omrekap.data.repository.OMRConfigRepository +import com.k2_9.omrekap.utils.AprilTagHelper +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import org.opencv.core.Mat + +object OMRConfigurationDetector { + private lateinit var loadedConfig: OMRBaseConfiguration + + /** + * Initialize and load the detection configuration data. + * Make sure to run this before detecting configurations + */ + suspend fun loadConfiguration(context: Context) { + loadedConfig = OMRConfigRepository.loadConfigurations(context) + ?: throw Exception("Failed to load OMR Configuration!") + } + + /** + * Detects the OMR configuration of an image to be processed. + * @param imageMat pre-processed image in gray or non color form + * @return Pair of OMR configuration and the image's tag corners that was used for configuration detector + */ + suspend fun detectConfiguration(imageMat: Mat): + Pair<OMRConfigurationParameter, Mat>? { + val configs = loadedConfig.omrConfigs + + var result: Pair<OMRConfigurationParameter, Mat>? = null + withContext(Dispatchers.Default) { + // get detected AprilTags + val (ids, cornersList) = AprilTagHelper.getAprilTagId(imageMat) + val nId = ids.size + for (i in 0..<nId) { + val id = ids[i] + if (id in configs) { + if (result == null) { + result = configs[id]!! to cornersList[i] + } else { + Log.e( + "OMRConfigurationDetector", + "Multiple tags detected, unable to determine configuration" + ) + result = null + break + } + } + } + } + return result + } +} diff --git a/app/src/main/java/com/k2_9/omrekap/utils/omr/OMRHelper.kt b/app/src/main/java/com/k2_9/omrekap/utils/omr/OMRHelper.kt new file mode 100644 index 0000000000000000000000000000000000000000..fd34cc0a7f39512a6666b5a43c9ac333d1c02ff7 --- /dev/null +++ b/app/src/main/java/com/k2_9/omrekap/utils/omr/OMRHelper.kt @@ -0,0 +1,49 @@ +package com.k2_9.omrekap.utils.omr + +import com.k2_9.omrekap.data.configs.omr.OMRHelperConfig +import com.k2_9.omrekap.data.configs.omr.OMRSection +import kotlin.math.floor + +abstract class OMRHelper(private val config: OMRHelperConfig) { + data class ContourInfo(val center: Pair<Int, Int>, val size: Pair<Int, Int>) + + protected fun getCombinedNumbers(numbers: List<Int>): Int { + // Combine the detected numbers into a single integer + return numbers.joinToString("").toInt() + } + + protected fun contourInfosToNumbers(contourInfos: List<ContourInfo?>): Int { + // TODO: consider gap height between circles + // Return the detected numbers based on the vertical position of the filled circles for each column + require(contourInfos.size == 3) + + val columnHeight = config.omrCropper.config.omrSectionSize.second // Define the column height based on your image + + val result = mutableListOf<Int>() + + for (contourInfo in contourInfos) { + if (contourInfo == null) { + // user might accidentally leave the column with no filled circle for 0 value + result.add(0) + } else { + // Detect number based on vertical position of the contour + val centerY = contourInfo.center.second + val h = contourInfo.size.second + + val columnIndex = floor(((centerY.toDouble() - h.toDouble() / 2.0) / columnHeight.toDouble()) * 10).toInt() + + result.add(columnIndex) + } + } + return getCombinedNumbers(result) + } + + protected fun filterContourInfos(contourInfos: List<ContourInfo?>): List<ContourInfo?> { + // TODO: Handle when 1 column has more than 1 filled circle + // TODO: Handle when no filled circle for each column (assume that the number is 0, with null as representation of the ContourInfo) + + return contourInfos + } + + abstract fun detect(section: OMRSection): Int +} diff --git a/app/src/main/java/com/k2_9/omrekap/utils/omr/TemplateMatchingOMRHelper.kt b/app/src/main/java/com/k2_9/omrekap/utils/omr/TemplateMatchingOMRHelper.kt new file mode 100644 index 0000000000000000000000000000000000000000..5cb0b42f94be857b7a94972cb6c17112f0362770 --- /dev/null +++ b/app/src/main/java/com/k2_9/omrekap/utils/omr/TemplateMatchingOMRHelper.kt @@ -0,0 +1,140 @@ +package com.k2_9.omrekap.utils.omr + +import android.graphics.Bitmap +import com.k2_9.omrekap.data.configs.omr.OMRSection +import com.k2_9.omrekap.data.configs.omr.TemplateMatchingOMRHelperConfig +import com.k2_9.omrekap.utils.ImageAnnotationHelper +import org.opencv.android.Utils +import org.opencv.core.Mat +import org.opencv.core.Point +import org.opencv.core.Rect +import org.opencv.imgproc.Imgproc +import kotlin.collections.ArrayList + +class TemplateMatchingOMRHelper(private val config: TemplateMatchingOMRHelperConfig) : OMRHelper(config) { + private var currentSectionGray: Mat? = null + private var currentSectionBinary: Mat? = null + + private fun getMatchRectangles(): List<Rect> { + // TODO: fix algorithm bug + + // Load the template image + val template = config.template + + // Apply binary thresholding to the template image + val templateBinary = Mat() + Imgproc.threshold(template, templateBinary, 0.0, 255.0, Imgproc.THRESH_BINARY_INV + Imgproc.THRESH_TRIANGLE) + + // Perform template matching + val result = Mat() + Imgproc.matchTemplate(currentSectionBinary, templateBinary, result, Imgproc.TM_CCOEFF_NORMED) + + // Set a threshold for template matching result + val threshold = config.similarityThreshold + + val locations = mutableListOf<Point>() + + // Iterate through the result matrix + for (y in 0 until result.rows()) { + for (x in 0 until result.cols()) { + val similarityScore = result.get(y, x)[0] + + if (similarityScore > threshold) { + // Add the location to the list + locations.add(Point(x.toDouble(), y.toDouble())) + } + } + } + + // Get the bounding rectangles for the matched locations + val matchedRectangles = ArrayList<Rect>() + for (point in locations) { + val locX = point.x.toInt() + val locY = point.y.toInt() + val rect = Rect(locX, locY, template.width(), template.height()) + matchedRectangles.add(rect) + } + + return matchedRectangles + } + + private fun getContourInfos(matchedRectangles: List<Rect>): List<ContourInfo?> { + // Initialize a set to keep track of added rectangles + val addedRectangles = mutableSetOf<Rect>() + + val contourInfos = mutableListOf<ContourInfo>() + + // Iterate through the rectangles + for (rect in matchedRectangles) { + val x = rect.x + val y = rect.y + val w = rect.width + val h = rect.height + + // Calculate the center of the rectangle + val centerX = x + w / 2 + val centerY = y + h / 2 + + // Check if the rectangle overlaps with any of the added rectangles + var overlap = false + for (addedRect in addedRectangles) { + if (centerX >= addedRect.x && centerX <= addedRect.x + addedRect.width && + centerY >= addedRect.y && centerY <= addedRect.y + addedRect.height + ) { + overlap = true + break + } + } + + // If the rectangle does not overlap, add it to contour_info and the set of added rectangles + if (!overlap) { + contourInfos.add(ContourInfo(Pair(centerX, centerY), Pair(w, h))) + addedRectangles.add(Rect(x, y, w, h)) + } + } + + // short by center_x + contourInfos.sortBy { it.center.first } + + return contourInfos.toList() + } + + fun annotateImage(contourNumber: Int): Bitmap { + val annotatedImg = currentSectionGray!!.clone() + val matchedRectangles = getMatchRectangles() + val res = ImageAnnotationHelper.annotateTemplateMatchingOMR(annotatedImg, matchedRectangles, contourNumber) + + // Convert the annotated Mat to Bitmap + val annotatedImageBitmap = + Bitmap.createBitmap( + res.width(), + res.height(), + Bitmap.Config.ARGB_8888, + ) + Utils.matToBitmap(res, annotatedImageBitmap) + return annotatedImageBitmap + } + + override fun detect(section: OMRSection): Int { + val omrSectionImage = config.omrCropper.crop(section) + + // Convert image to grayscale + val gray = Mat() + Imgproc.cvtColor(omrSectionImage, gray, Imgproc.COLOR_BGR2GRAY) + + // Apply binary thresholding + val binary = Mat() + Imgproc.threshold(gray, binary, 0.0, 255.0, Imgproc.THRESH_BINARY_INV + Imgproc.THRESH_TRIANGLE) + + // Update states + currentSectionGray = gray + currentSectionBinary = binary + + val matchedRectangles = getMatchRectangles() + + val contourInfos = getContourInfos(matchedRectangles) + val filteredContourInfos = filterContourInfos(contourInfos.toList()) + + return contourInfosToNumbers(filteredContourInfos.toList()) + } +} diff --git a/app/src/main/java/com/k2_9/omrekap/activities/CameraActivity.kt b/app/src/main/java/com/k2_9/omrekap/views/activities/CameraActivity.kt similarity index 86% rename from app/src/main/java/com/k2_9/omrekap/activities/CameraActivity.kt rename to app/src/main/java/com/k2_9/omrekap/views/activities/CameraActivity.kt index 96f4a19bdb2d0cebeb5d3e2f79f6f96ce8bce88f..a022ed336ee429da56682151d2a03b84293d7b52 100644 --- a/app/src/main/java/com/k2_9/omrekap/activities/CameraActivity.kt +++ b/app/src/main/java/com/k2_9/omrekap/views/activities/CameraActivity.kt @@ -1,9 +1,8 @@ -package com.k2_9.omrekap.activities +package com.k2_9.omrekap.views.activities import android.Manifest import android.content.Context import android.content.Intent -import android.content.pm.PackageManager import android.graphics.Bitmap import android.graphics.Matrix import android.media.AudioManager @@ -12,7 +11,6 @@ import android.os.Bundle import android.widget.ImageButton import android.widget.Toast import androidx.activity.OnBackPressedCallback -import androidx.activity.result.contract.ActivityResultContracts.RequestPermission import androidx.appcompat.app.AppCompatActivity import androidx.camera.core.CameraSelector import androidx.camera.core.ImageCapture @@ -21,9 +19,9 @@ import androidx.camera.core.ImageProxy import androidx.camera.view.CameraController import androidx.camera.view.LifecycleCameraController import androidx.camera.view.PreviewView -import androidx.core.content.ContextCompat import androidx.lifecycle.lifecycleScope import com.k2_9.omrekap.R +import com.k2_9.omrekap.utils.PermissionHelper import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext @@ -105,7 +103,7 @@ class CameraActivity : AppCompatActivity() { } captureButton.isEnabled = true - requirePermission(Manifest.permission.CAMERA) { + PermissionHelper.requirePermission(this, Manifest.permission.CAMERA, true) { cameraController = LifecycleCameraController(this) (cameraController as LifecycleCameraController).bindToLifecycle(this) cameraController.cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA @@ -113,34 +111,7 @@ class CameraActivity : AppCompatActivity() { } } - private fun requirePermission( - permission: String, - verbose: Boolean = true, - operation: () -> Unit, - ) { - if (ContextCompat.checkSelfPermission( - this, - permission, - ) == PackageManager.PERMISSION_GRANTED - ) { - operation() - } else { - val requestPermissionLauncher = - registerForActivityResult(RequestPermission()) { - isGranted: Boolean -> - if (isGranted) { - operation() - } else { - if (verbose) { - Toast.makeText(this, "Permission denied", Toast.LENGTH_SHORT).show() - } - } - } - requestPermissionLauncher.launch(permission) - } - } - - suspend fun saveImageOnCache(image: ImageProxy) { + fun saveImageOnCache(image: ImageProxy) { // get current day as sign val dateString = LocalDate.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd")) diff --git a/app/src/main/java/com/k2_9/omrekap/activities/ExpandImageActivity.kt b/app/src/main/java/com/k2_9/omrekap/views/activities/ExpandImageActivity.kt similarity index 96% rename from app/src/main/java/com/k2_9/omrekap/activities/ExpandImageActivity.kt rename to app/src/main/java/com/k2_9/omrekap/views/activities/ExpandImageActivity.kt index 0d8cf80e5b19eb70f596f15bbfeea8fa77d4dae7..a83ac5d4062c19ba9f5b60d2e5a9ca5a7bf37408 100644 --- a/app/src/main/java/com/k2_9/omrekap/activities/ExpandImageActivity.kt +++ b/app/src/main/java/com/k2_9/omrekap/views/activities/ExpandImageActivity.kt @@ -1,4 +1,4 @@ -package com.k2_9.omrekap.activities +package com.k2_9.omrekap.views.activities import android.net.Uri import android.os.Bundle diff --git a/app/src/main/java/com/k2_9/omrekap/activities/HomeActivity.kt b/app/src/main/java/com/k2_9/omrekap/views/activities/HomeActivity.kt similarity index 90% rename from app/src/main/java/com/k2_9/omrekap/activities/HomeActivity.kt rename to app/src/main/java/com/k2_9/omrekap/views/activities/HomeActivity.kt index d80ca0eb6d427c9d5b98e433249b6647fe7d0488..a20f2eb733970800acb9dbcbca8f3f8906c54353 100644 --- a/app/src/main/java/com/k2_9/omrekap/activities/HomeActivity.kt +++ b/app/src/main/java/com/k2_9/omrekap/views/activities/HomeActivity.kt @@ -1,10 +1,10 @@ -package com.k2_9.omrekap.activities +package com.k2_9.omrekap.views.activities import android.content.Intent import android.net.Uri import android.os.Bundle import androidx.fragment.app.Fragment -import com.k2_9.omrekap.fragments.HomePageFragment +import com.k2_9.omrekap.views.fragments.HomePageFragment class HomeActivity : MainActivity() { override fun getGalleryPreviewIntent(imageUri: Uri): Intent { diff --git a/app/src/main/java/com/k2_9/omrekap/activities/MainActivity.kt b/app/src/main/java/com/k2_9/omrekap/views/activities/MainActivity.kt similarity index 98% rename from app/src/main/java/com/k2_9/omrekap/activities/MainActivity.kt rename to app/src/main/java/com/k2_9/omrekap/views/activities/MainActivity.kt index bb81a4db2f3de125f8e3aacd64d2dae850ab9e2e..19e43edd1877166e248a1f943ea2f5052ebebab0 100644 --- a/app/src/main/java/com/k2_9/omrekap/activities/MainActivity.kt +++ b/app/src/main/java/com/k2_9/omrekap/views/activities/MainActivity.kt @@ -1,4 +1,4 @@ -package com.k2_9.omrekap.activities +package com.k2_9.omrekap.views.activities import android.content.Intent import android.net.Uri diff --git a/app/src/main/java/com/k2_9/omrekap/activities/PreviewActivity.kt b/app/src/main/java/com/k2_9/omrekap/views/activities/PreviewActivity.kt similarity index 62% rename from app/src/main/java/com/k2_9/omrekap/activities/PreviewActivity.kt rename to app/src/main/java/com/k2_9/omrekap/views/activities/PreviewActivity.kt index e53650fb017a1fd3a9a2458baecc87cbe3959363..7b813002e490e17f39645210df990fac0cf0c467 100644 --- a/app/src/main/java/com/k2_9/omrekap/activities/PreviewActivity.kt +++ b/app/src/main/java/com/k2_9/omrekap/views/activities/PreviewActivity.kt @@ -1,13 +1,23 @@ -package com.k2_9.omrekap.activities +package com.k2_9.omrekap.views.activities import android.content.Intent +import android.graphics.Bitmap +import android.graphics.BitmapFactory import android.net.Uri import android.os.Bundle import android.util.Log import android.widget.ImageButton +import androidx.activity.viewModels import androidx.appcompat.app.AppCompatActivity +import androidx.core.graphics.drawable.toBitmap +import androidx.lifecycle.Observer import com.github.chrisbanes.photoview.PhotoView import com.k2_9.omrekap.R +import com.k2_9.omrekap.data.models.ImageSaveData +import com.k2_9.omrekap.data.view_models.PreviewViewModel +import com.k2_9.omrekap.utils.CropHelper +import com.k2_9.omrekap.utils.ImageSaveDataHolder +import org.opencv.android.OpenCVLoader class PreviewActivity : AppCompatActivity() { companion object { @@ -16,6 +26,8 @@ class PreviewActivity : AppCompatActivity() { const val EXTRA_NAME_IS_FROM_CAMERA = "IS_FROM_CAMERA" } + private val viewModel: PreviewViewModel by viewModels() + private var imageUriString: String? = null private var isFromCamera: Boolean = false @@ -23,6 +35,8 @@ class PreviewActivity : AppCompatActivity() { super.onCreate(savedInstanceState) setContentView(R.layout.activity_preview) + OpenCVLoader.initLocal() + // display photo val photoView: PhotoView = findViewById(R.id.preview_content) @@ -33,8 +47,25 @@ class PreviewActivity : AppCompatActivity() { throw IllegalArgumentException("Image URI string is null") } + val bitmapOptions = BitmapFactory.Options() + bitmapOptions.inPreferredConfig = Bitmap.Config.ARGB_8888 + val cornerPatternBitmap: Bitmap = BitmapFactory.decodeResource(resources, R.raw.corner_pattern, bitmapOptions) + + CropHelper.loadPattern(cornerPatternBitmap) + photoView.setImageURI(Uri.parse(imageUriString)) + viewModel.preprocessImage(photoView.drawable.toBitmap()) + + // Observe Data + val preprocessImageObserver = + Observer<ImageSaveData> { newValue -> + photoView.setImageBitmap(newValue.annotatedImage) + ImageSaveDataHolder.save(newValue) + } + + viewModel.data.observe(this, preprocessImageObserver) + // set buttons action val acceptButton = findViewById<ImageButton>(R.id.accept_preview_button) val rejectButton = findViewById<ImageButton>(R.id.reject_preview_button) diff --git a/app/src/main/java/com/k2_9/omrekap/activities/ResultActivity.kt b/app/src/main/java/com/k2_9/omrekap/views/activities/ResultActivity.kt similarity index 74% rename from app/src/main/java/com/k2_9/omrekap/activities/ResultActivity.kt rename to app/src/main/java/com/k2_9/omrekap/views/activities/ResultActivity.kt index ea4c6924b4d128e93771416e77a99cb12a15d7a8..f6d5f7352a55d28f8202d56a2951c19b6d6ae054 100644 --- a/app/src/main/java/com/k2_9/omrekap/activities/ResultActivity.kt +++ b/app/src/main/java/com/k2_9/omrekap/views/activities/ResultActivity.kt @@ -1,8 +1,7 @@ -package com.k2_9.omrekap.activities +package com.k2_9.omrekap.views.activities import android.Manifest import android.content.Intent -import android.content.pm.PackageManager import android.graphics.Bitmap import android.net.Uri import android.os.Build @@ -10,16 +9,16 @@ import android.os.Bundle import android.os.PersistableBundle import android.util.Log import android.widget.Toast -import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.viewModels -import androidx.core.content.ContextCompat import androidx.fragment.app.Fragment import androidx.lifecycle.Observer import androidx.lifecycle.lifecycleScope -import com.k2_9.omrekap.fragments.ResultPageFragment -import com.k2_9.omrekap.models.ImageSaveData +import com.k2_9.omrekap.data.models.ImageSaveData +import com.k2_9.omrekap.data.view_models.ImageDataViewModel +import com.k2_9.omrekap.utils.ImageSaveDataHolder +import com.k2_9.omrekap.utils.PermissionHelper import com.k2_9.omrekap.utils.SaveHelper -import com.k2_9.omrekap.view_models.ImageDataViewModel +import com.k2_9.omrekap.views.fragments.ResultPageFragment import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job import kotlinx.coroutines.launch @@ -37,9 +36,12 @@ abstract class ResultActivity : MainActivity() { private var startSaveJob: Boolean = false private val omrHelperObserver = Observer<ImageSaveData> { newValue -> - if (newValue?.annotatedImage != null && newValue.data != null) { - saveFile() - } + saveFile() + +// TODO: save file when data is not empty after implemented +// if (newValue.data.isNotEmpty()) { +// saveFile() +// } } private lateinit var imageUriString: String @@ -64,10 +66,10 @@ abstract class ResultActivity : MainActivity() { } } - imageBitmap = SaveHelper().uriToBitmap(applicationContext, Uri.parse(imageUriString)) + imageBitmap = SaveHelper.uriToBitmap(applicationContext, Uri.parse(imageUriString)) if (viewModel.data.value == null) { - viewModel.processImage(imageBitmap) + viewModel.processImage(ImageSaveDataHolder.get()) viewModel.data.observe(this, omrHelperObserver) } } @@ -76,7 +78,7 @@ abstract class ResultActivity : MainActivity() { saveFileJob = lifecycleScope.launch(Dispatchers.IO) { startSaveJob = true - SaveHelper().save(applicationContext, viewModel.data.value!!) + SaveHelper.save(applicationContext, viewModel.data.value!!) startSaveJob = false withContext(Dispatchers.Main) { @@ -89,33 +91,6 @@ abstract class ResultActivity : MainActivity() { } } - private fun requirePermission( - permission: String, - verbose: Boolean = true, - operation: () -> Unit, - ) { - if (ContextCompat.checkSelfPermission( - this, - permission, - ) == PackageManager.PERMISSION_GRANTED - ) { - operation() - } else { - val requestPermissionLauncher = - registerForActivityResult(ActivityResultContracts.RequestPermission()) { - isGranted: Boolean -> - if (isGranted) { - operation() - } else { - if (verbose) { - Toast.makeText(this, "Permission denied", Toast.LENGTH_SHORT).show() - } - } - } - requestPermissionLauncher.launch(permission) - } - } - override fun getFragment(intent: Intent): Fragment { val fragment = ResultPageFragment() @@ -157,7 +132,7 @@ abstract class ResultActivity : MainActivity() { OpenCVLoader.initLocal() if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.Q) { - requirePermission(Manifest.permission.WRITE_EXTERNAL_STORAGE, false) {} + PermissionHelper.requirePermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE, false) {} } startSaveJob = savedInstanceState?.getBoolean("startSaveJob") ?: false diff --git a/app/src/main/java/com/k2_9/omrekap/activities/ResultFromCameraActivity.kt b/app/src/main/java/com/k2_9/omrekap/views/activities/ResultFromCameraActivity.kt similarity index 95% rename from app/src/main/java/com/k2_9/omrekap/activities/ResultFromCameraActivity.kt rename to app/src/main/java/com/k2_9/omrekap/views/activities/ResultFromCameraActivity.kt index 7d2633805d709c85531ac9862421c38dc8f7a62b..4213381b9b1bd9af2a8a2e0551456bf6a1386f36 100644 --- a/app/src/main/java/com/k2_9/omrekap/activities/ResultFromCameraActivity.kt +++ b/app/src/main/java/com/k2_9/omrekap/views/activities/ResultFromCameraActivity.kt @@ -1,4 +1,4 @@ -package com.k2_9.omrekap.activities +package com.k2_9.omrekap.views.activities import android.content.Intent import android.util.Log diff --git a/app/src/main/java/com/k2_9/omrekap/activities/ResultFromGalleryActivity.kt b/app/src/main/java/com/k2_9/omrekap/views/activities/ResultFromGalleryActivity.kt similarity index 95% rename from app/src/main/java/com/k2_9/omrekap/activities/ResultFromGalleryActivity.kt rename to app/src/main/java/com/k2_9/omrekap/views/activities/ResultFromGalleryActivity.kt index 9e3c402019e3c718ea9b2cb5e72b2a4e3a6b4adc..29baa0896d016fd98041b8ff410eaae30e6251cb 100644 --- a/app/src/main/java/com/k2_9/omrekap/activities/ResultFromGalleryActivity.kt +++ b/app/src/main/java/com/k2_9/omrekap/views/activities/ResultFromGalleryActivity.kt @@ -1,4 +1,4 @@ -package com.k2_9.omrekap.activities +package com.k2_9.omrekap.views.activities import android.content.Intent diff --git a/app/src/main/java/com/k2_9/omrekap/adapters/ResultAdapter.kt b/app/src/main/java/com/k2_9/omrekap/views/adapters/ResultAdapter.kt similarity index 96% rename from app/src/main/java/com/k2_9/omrekap/adapters/ResultAdapter.kt rename to app/src/main/java/com/k2_9/omrekap/views/adapters/ResultAdapter.kt index acdf2ca476dc40325e49e3802ef40ad0f1112518..44c70928f9c6ca6c59e6a39b695c21f388b09b43 100644 --- a/app/src/main/java/com/k2_9/omrekap/adapters/ResultAdapter.kt +++ b/app/src/main/java/com/k2_9/omrekap/views/adapters/ResultAdapter.kt @@ -1,4 +1,4 @@ -package com.k2_9.omrekap.adapters +package com.k2_9.omrekap.views.adapters import android.view.LayoutInflater import android.view.View import android.view.ViewGroup diff --git a/app/src/main/java/com/k2_9/omrekap/fragments/HomePageFragment.kt b/app/src/main/java/com/k2_9/omrekap/views/fragments/HomePageFragment.kt similarity index 96% rename from app/src/main/java/com/k2_9/omrekap/fragments/HomePageFragment.kt rename to app/src/main/java/com/k2_9/omrekap/views/fragments/HomePageFragment.kt index 989e973373faefe2b33cf1a62641ac32a877a05c..9f3ca4bf939bfea5de7eb227a7d2bf6660f76723 100644 --- a/app/src/main/java/com/k2_9/omrekap/fragments/HomePageFragment.kt +++ b/app/src/main/java/com/k2_9/omrekap/views/fragments/HomePageFragment.kt @@ -1,4 +1,4 @@ -package com.k2_9.omrekap.fragments +package com.k2_9.omrekap.views.fragments import android.os.Bundle import android.view.LayoutInflater diff --git a/app/src/main/java/com/k2_9/omrekap/fragments/ResultPageFragment.kt b/app/src/main/java/com/k2_9/omrekap/views/fragments/ResultPageFragment.kt similarity index 95% rename from app/src/main/java/com/k2_9/omrekap/fragments/ResultPageFragment.kt rename to app/src/main/java/com/k2_9/omrekap/views/fragments/ResultPageFragment.kt index f116e84912889773725b0388c10200f1309b7efa..03fcfb83762e7499f3b0177583edbaa3ba4c902c 100644 --- a/app/src/main/java/com/k2_9/omrekap/fragments/ResultPageFragment.kt +++ b/app/src/main/java/com/k2_9/omrekap/views/fragments/ResultPageFragment.kt @@ -1,4 +1,4 @@ -package com.k2_9.omrekap.fragments +package com.k2_9.omrekap.views.fragments import android.content.Intent import android.net.Uri @@ -15,9 +15,9 @@ import androidx.fragment.app.Fragment import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView import com.k2_9.omrekap.R -import com.k2_9.omrekap.activities.ExpandImageActivity -import com.k2_9.omrekap.activities.HomeActivity -import com.k2_9.omrekap.adapters.ResultAdapter +import com.k2_9.omrekap.views.activities.ExpandImageActivity +import com.k2_9.omrekap.views.activities.HomeActivity +import com.k2_9.omrekap.views.adapters.ResultAdapter /** * A simple [Fragment] subclass. diff --git a/app/src/main/res/layout/activity_expand_image.xml b/app/src/main/res/layout/activity_expand_image.xml index 90782a40b889e7e1aecbecce03b165a00a69db03..e8f9d90083e0215d1ed44711a7994efee1908744 100644 --- a/app/src/main/res/layout/activity_expand_image.xml +++ b/app/src/main/res/layout/activity_expand_image.xml @@ -6,7 +6,7 @@ android:layout_width="match_parent" android:layout_height="match_parent" android:background="#000000" - tools:context=".activities.ExpandImageActivity"> + tools:context=".views.activities.ExpandImageActivity"> <com.github.chrisbanes.photoview.PhotoView android:id="@+id/fullscreen_content" diff --git a/app/src/main/res/layout/activity_preview.xml b/app/src/main/res/layout/activity_preview.xml index 9bb9a82019712bf57500e7dad22ad1c03561f1c9..9288d53a08d45a2b3973b21c67aaf37d2d4b2b1e 100644 --- a/app/src/main/res/layout/activity_preview.xml +++ b/app/src/main/res/layout/activity_preview.xml @@ -6,7 +6,7 @@ android:layout_width="match_parent" android:layout_height="match_parent" android:background="@color/black" - tools:context=".activities.ExpandImageActivity"> + tools:context=".views.activities.ExpandImageActivity"> <com.github.chrisbanes.photoview.PhotoView android:id="@+id/preview_content" diff --git a/app/src/main/res/layout/fragment_home_page.xml b/app/src/main/res/layout/fragment_home_page.xml index d80a15eaacd18b241bb8278d4c631a2bc66339b9..012d4c8edda6f390f7f9c5a9efdb9a2490f6cc6b 100644 --- a/app/src/main/res/layout/fragment_home_page.xml +++ b/app/src/main/res/layout/fragment_home_page.xml @@ -5,7 +5,7 @@ android:layout_width="match_parent" android:layout_height="match_parent" android:background="@drawable/bg_gradient_main_background" - tools:context=".fragments.HomePageFragment"> + tools:context=".views.fragments.HomePageFragment"> <androidx.constraintlayout.widget.ConstraintLayout android:id="@+id/constraintLayout3" diff --git a/app/src/main/res/layout/fragment_result_page.xml b/app/src/main/res/layout/fragment_result_page.xml index dc0729b4696183f7635ebb8e1dbcb0bda3fd2a8a..ff672cee005d00f30e58a21d61cb113f453da858 100644 --- a/app/src/main/res/layout/fragment_result_page.xml +++ b/app/src/main/res/layout/fragment_result_page.xml @@ -5,7 +5,7 @@ xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" - tools:context=".fragments.ResultPageFragment"> + tools:context=".views.fragments.ResultPageFragment"> <androidx.constraintlayout.widget.ConstraintLayout android:id="@+id/top_layout" diff --git a/app/src/main/res/raw/circle_template.png b/app/src/main/res/raw/circle_template.png new file mode 100644 index 0000000000000000000000000000000000000000..a3dffd38d2ecf9363c09bfabd4658b86b0fb39d8 Binary files /dev/null and b/app/src/main/res/raw/circle_template.png differ diff --git a/app/src/main/res/raw/corner_pattern.png b/app/src/main/res/raw/corner_pattern.png index 24dae8a6c8a19eb3f2866c332928db22fabd14c7..89f086ed8d72f7e2db915abd223ec2342e9e4748 100644 Binary files a/app/src/main/res/raw/corner_pattern.png and b/app/src/main/res/raw/corner_pattern.png differ diff --git a/app/src/main/res/raw/example.jpg b/app/src/main/res/raw/example.jpg new file mode 100644 index 0000000000000000000000000000000000000000..3aadcb30503609955c737975b9a666156386f214 Binary files /dev/null and b/app/src/main/res/raw/example.jpg differ diff --git a/settings.gradle.kts b/settings.gradle.kts index df45e0a8a5da416aa30fbc3fee541f4f35f2c9ba..630f1222aeeed5f7ded006fa90bed2b9f3700888 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -16,4 +16,3 @@ dependencyResolutionManagement { rootProject.name = "omrekap" include(":app") -include(":opencv")