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 index ff11227c2dd6ff000dbb2d534eb2920d2ce008dc..6d80b4578042cb41dfb8f26d04836ee111d1f8f9 100644 --- a/app/src/androidTest/java/com/k2_9/omrekap/aprilTag/AprilTagConfigDetectionTest.kt +++ b/app/src/androidTest/java/com/k2_9/omrekap/aprilTag/AprilTagConfigDetectionTest.kt @@ -6,7 +6,7 @@ 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 com.k2_9.omrekap.utils.omr.OMRConfigDetector import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch @@ -31,10 +31,10 @@ class AprilTagConfigDetectionTest { Imgproc.cvtColor(imageMat, grayImageMat, Imgproc.COLOR_BGR2GRAY) CoroutineScope(Dispatchers.Default).launch { - OMRConfigurationDetector.loadConfiguration( + OMRConfigDetector.loadConfiguration( appContext ) - val result = OMRConfigurationDetector.detectConfiguration(grayImageMat) + val result = OMRConfigDetector.detectConfiguration(grayImageMat) val gson = Gson() Log.d("ConfigDetectionTestx", gson.toJson(result)) val compare = OMRConfigRepository.loadConfigurations(appContext) 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 index 6d4164215a341bcdae247a6e4f569b0958c7a6b2..a5522c034fa3da52221a9fe5f25562bd6138de82 100644 --- a/app/src/androidTest/java/com/k2_9/omrekap/preprocess/CropHelperTest.kt +++ b/app/src/androidTest/java/com/k2_9/omrekap/preprocess/CropHelperTest.kt @@ -35,7 +35,11 @@ class CropHelperTest { 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) + 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) @@ -56,7 +60,12 @@ class CropHelperTest { imageSaveData = PreprocessHelper.preprocessImage(imageSaveData) SaveHelper.saveImage(appContext, imageSaveData.rawImage, "test", "test_preprocess_raw") - SaveHelper.saveImage(appContext, imageSaveData.annotatedImage, "test", "test_preprocess_annotated") + SaveHelper.saveImage( + appContext, + imageSaveData.annotatedImage, + "test", + "test_preprocess_annotated" + ) } @After 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 index 69bee099f9d12b00e4ca7d566fd3a6b8eceb99b6..fcbfe5a078cc889c20de1554bdbc76bc2c4adb58 100644 --- 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 @@ -33,7 +33,7 @@ object OMRConfigRepository { return try { val buffer = ByteArray(inputStream.available()) inputStream.read(buffer) - Log.d("OMRConfigLoader", String(buffer)) +// Log.d("OMRConfigLoader", String(buffer)) String(buffer) } catch (e: IOException) { diff --git a/app/src/main/java/com/k2_9/omrekap/data/view_models/ImageDataViewModel.kt b/app/src/main/java/com/k2_9/omrekap/data/view_models/ImageDataViewModel.kt index c2b93ce92a9476955782096493ae76ba338f6e04..83a090511274daa7e968e7207ade9a6474ee076d 100644 --- a/app/src/main/java/com/k2_9/omrekap/data/view_models/ImageDataViewModel.kt +++ b/app/src/main/java/com/k2_9/omrekap/data/view_models/ImageDataViewModel.kt @@ -1,12 +1,16 @@ 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.AprilTagHelper +import com.k2_9.omrekap.utils.omr.OMRConfigDetector import kotlinx.coroutines.launch +import org.opencv.android.Utils +import org.opencv.core.Mat +import org.opencv.imgproc.Imgproc class ImageDataViewModel : ViewModel() { private val _data = MutableLiveData<ImageSaveData>() @@ -14,7 +18,23 @@ class ImageDataViewModel : ViewModel() { fun processImage(data: ImageSaveData) { viewModelScope.launch { + val rawImage = data.rawImage + val imageMat = Mat() +// val annotatedImageMat = Mat() + Utils.bitmapToMat(rawImage, imageMat) + + // convert image to gray + val grayImageMat = Mat() + Imgproc.cvtColor(imageMat, grayImageMat, Imgproc.COLOR_BGR2GRAY) + + // load configuration + val (loadedConfig, id, corners) = OMRConfigDetector.detectConfiguration(grayImageMat)!! + + // annotate the detected AprilTag + val annotatedImage = AprilTagHelper.annotateImage(rawImage) + // TODO: Process the raw image using OMRHelper + data.annotatedImage = annotatedImage _data.value = 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 bb1d90bd650facb8e1ac5d486c3f13a100420a2f..4f1e9f8fc661413d40f00f86ebd4c9e2fa774bbd 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 @@ -61,6 +61,13 @@ object AprilTagHelper { for (i in 0..<nId) { val id = idMat[i, 0][0].toInt().toString() logDebug("detected tag with id: $id") + val cornerPoints = corners[i] + logDebug( + "with corners at: (${cornerPoints[0, 0][0]},${cornerPoints[0, 0][1]}), " + + "(${cornerPoints[0, 1][0]},${cornerPoints[0, 1][1]}) " + + "(${cornerPoints[0, 2][0]},${cornerPoints[0, 2][1]}) " + + "(${cornerPoints[0, 3][0]},${cornerPoints[0, 3][1]})" + ) idList.add(id) } 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 index 8a2cb6e34a0a7bccd9b732d492fd2a81495c6d0c..bed44fbe62849938b690da790792980cc049e30a 100644 --- a/app/src/main/java/com/k2_9/omrekap/utils/ImageAnnotationHelper.kt +++ b/app/src/main/java/com/k2_9/omrekap/utils/ImageAnnotationHelper.kt @@ -28,19 +28,46 @@ object ImageAnnotationHelper { ): Mat { val imgWithAnnotations = img.clone() if (id.isNotEmpty()) { + // points -> list<Point*s*>, inside list of points are corners of the detector val points = cornerPoints.map { mat -> - val x = mat.get(0, 0)[0] - val y = mat.get(1, 0)[0] - Point(x, y) + val points = ArrayList<Point>() + for (i in 0..<4) { + val x = mat.get(0, i)[0] + val y = mat.get(0, i)[1] + points.add(Point(x, y)) + } + points } // 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) + Imgproc.putText( + imgWithAnnotations, + id, + points[0][0], + Imgproc.FONT_HERSHEY_SIMPLEX, + 1.0, + Scalar(0.0, 255.0, 0.0), + 5 + ) + Imgproc.polylines( + imgWithAnnotations, + listOf(MatOfPoint(*points[0].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) + Imgproc.putText( + imgWithAnnotations, + "April Tag Not Detected", + topLeft, + Imgproc.FONT_HERSHEY_SIMPLEX, + 1.0, + Scalar(0.0, 255.0, 0.0), + 5 + ) } return imgWithAnnotations } 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 index 2ec887351e78e29105acad6d4d9c0bf7dec8278b..d155d1785fb7a25355f42e90f6d87e1f528ce140 100644 --- 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 @@ -118,7 +118,8 @@ class ContourOMRHelper(private val config: ContourOMRHelperConfig) : OMRHelper(c // 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 colContours = contoursSorted.subList(col * 10, (col + 1) * 10) + .sortedBy { Imgproc.boundingRect(it).y } val darkestRow = getDarkestRow(colContours) @@ -138,7 +139,13 @@ class ContourOMRHelper(private val config: ContourOMRHelperConfig) : OMRHelper(c // 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) + Imgproc.findContours( + currentSectionBinary!!, + contours, + hierarchy, + Imgproc.RETR_EXTERNAL, + Imgproc.CHAIN_APPROX_SIMPLE + ) // Initialize a list to store filtered contours val filteredContours = mutableListOf<MatOfPoint>() @@ -155,7 +162,10 @@ class ContourOMRHelper(private val config: ContourOMRHelperConfig) : OMRHelper(c 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") + Log.d( + "ContourOMRHelper", + "Contour with aspect ratio $ar and size ${rect.width} x ${rect.height} filtered out" + ) } } @@ -165,9 +175,14 @@ class ContourOMRHelper(private val config: ContourOMRHelperConfig) : OMRHelper(c 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) + annotatedImg = + ImageAnnotationHelper.annotateContourOMR(annotatedImg, contours, contourNumber) + + val annotatedImageBitmap = Bitmap.createBitmap( + annotatedImg.width(), + annotatedImg.height(), + Bitmap.Config.ARGB_8888 + ) Utils.matToBitmap(annotatedImg, annotatedImageBitmap) return annotatedImageBitmap } @@ -181,7 +196,13 @@ class ContourOMRHelper(private val config: ContourOMRHelperConfig) : OMRHelper(c // Apply binary thresholding val binary = Mat() - Imgproc.threshold(gray, binary, 0.0, 255.0, Imgproc.THRESH_BINARY_INV + Imgproc.THRESH_TRIANGLE) + Imgproc.threshold( + gray, + binary, + 0.0, + 255.0, + Imgproc.THRESH_BINARY_INV + Imgproc.THRESH_TRIANGLE + ) // Update states currentSectionGray = gray @@ -190,7 +211,10 @@ class ContourOMRHelper(private val config: ContourOMRHelperConfig) : OMRHelper(c val contours = getAllContours() return if (contours.size != 30) { - Log.d("ContourOMRHelper", "Some circles are not detected, considering only filled circles") + Log.d( + "ContourOMRHelper", + "Some circles are not detected, considering only filled circles" + ) predictForFilledCircle(contours) } else { Log.d("ContourOMRHelper", "All 30 circles are detected") 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/OMRConfigDetector.kt similarity index 62% rename from app/src/main/java/com/k2_9/omrekap/utils/omr/OMRConfigurationDetector.kt rename to app/src/main/java/com/k2_9/omrekap/utils/omr/OMRConfigDetector.kt index 42b66317fd7972b1af6ba7ee74435284f4c8817e..8f542416cee9391810d19ce1dee6b497e509bae2 100644 --- a/app/src/main/java/com/k2_9/omrekap/utils/omr/OMRConfigurationDetector.kt +++ b/app/src/main/java/com/k2_9/omrekap/utils/omr/OMRConfigDetector.kt @@ -6,32 +6,42 @@ 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.CoroutineScope import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import org.opencv.core.Mat -object OMRConfigurationDetector { +object OMRConfigDetector { private lateinit var loadedConfig: OMRBaseConfiguration + private var job: Job? = null /** * 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!") + fun loadConfiguration(context: Context) { + if (!this::loadedConfig.isInitialized) { + job = CoroutineScope(Dispatchers.IO).launch { + 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 + * @return Triple of OMR configuration, the ID of the detected AprilTag, + * and the image's tag corners that was used for configuration detector */ suspend fun detectConfiguration(imageMat: Mat): - Pair<OMRConfigurationParameter, Mat>? { + Triple<OMRConfigurationParameter, String, Mat>? { + job?.join().also { job = null } val configs = loadedConfig.omrConfigs - var result: Pair<OMRConfigurationParameter, Mat>? = null + var result: Triple<OMRConfigurationParameter, String, Mat>? = null withContext(Dispatchers.Default) { // get detected AprilTags val (ids, cornersList) = AprilTagHelper.getAprilTagId(imageMat) @@ -40,7 +50,7 @@ object OMRConfigurationDetector { val id = ids[i] if (id in configs) { if (result == null) { - result = configs[id]!! to cornersList[i] + result = Triple(configs[id]!!, id, cornersList[i]) } else { Log.e( "OMRConfigurationDetector", 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 index 5cb0b42f94be857b7a94972cb6c17112f0362770..d36e7ca5064e84aea973fc44323fcbe461350799 100644 --- 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 @@ -9,9 +9,9 @@ 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) { +class TemplateMatchingOMRHelper(private val config: TemplateMatchingOMRHelperConfig) : + OMRHelper(config) { private var currentSectionGray: Mat? = null private var currentSectionBinary: Mat? = null @@ -23,11 +23,22 @@ class TemplateMatchingOMRHelper(private val config: TemplateMatchingOMRHelperCon // 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) + 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) + Imgproc.matchTemplate( + currentSectionBinary, + templateBinary, + result, + Imgproc.TM_CCOEFF_NORMED + ) // Set a threshold for template matching result val threshold = config.similarityThreshold @@ -102,7 +113,11 @@ class TemplateMatchingOMRHelper(private val config: TemplateMatchingOMRHelperCon fun annotateImage(contourNumber: Int): Bitmap { val annotatedImg = currentSectionGray!!.clone() val matchedRectangles = getMatchRectangles() - val res = ImageAnnotationHelper.annotateTemplateMatchingOMR(annotatedImg, matchedRectangles, contourNumber) + val res = ImageAnnotationHelper.annotateTemplateMatchingOMR( + annotatedImg, + matchedRectangles, + contourNumber + ) // Convert the annotated Mat to Bitmap val annotatedImageBitmap = @@ -124,7 +139,13 @@ class TemplateMatchingOMRHelper(private val config: TemplateMatchingOMRHelperCon // Apply binary thresholding val binary = Mat() - Imgproc.threshold(gray, binary, 0.0, 255.0, Imgproc.THRESH_BINARY_INV + Imgproc.THRESH_TRIANGLE) + Imgproc.threshold( + gray, + binary, + 0.0, + 255.0, + Imgproc.THRESH_BINARY_INV + Imgproc.THRESH_TRIANGLE + ) // Update states currentSectionGray = gray diff --git a/app/src/main/java/com/k2_9/omrekap/views/activities/ResultActivity.kt b/app/src/main/java/com/k2_9/omrekap/views/activities/ResultActivity.kt index f6d5f7352a55d28f8202d56a2951c19b6d6ae054..fcdca172937c92f358445c6176915fe38f66a75b 100644 --- a/app/src/main/java/com/k2_9/omrekap/views/activities/ResultActivity.kt +++ b/app/src/main/java/com/k2_9/omrekap/views/activities/ResultActivity.kt @@ -18,6 +18,7 @@ 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.utils.omr.OMRConfigDetector import com.k2_9.omrekap.views.fragments.ResultPageFragment import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job @@ -129,10 +130,15 @@ abstract class ResultActivity : MainActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) + OMRConfigDetector.loadConfiguration(this) OpenCVLoader.initLocal() if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.Q) { - PermissionHelper.requirePermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE, false) {} + PermissionHelper.requirePermission( + this, + Manifest.permission.WRITE_EXTERNAL_STORAGE, + false + ) {} } startSaveJob = savedInstanceState?.getBoolean("startSaveJob") ?: false