From 08cf8e5ff17c1db4304277f6487713e5655acf1a Mon Sep 17 00:00:00 2001
From: Enliven26 <16521443@mahasiswa.itb.ac.id>
Date: Sun, 12 May 2024 12:28:06 +0700
Subject: [PATCH] feat: additional logic for omr

---
 .../k2_9/omrekap/omr/ContourOMRHelperTest.kt  |  10 +-
 app/src/main/assets/omr_config.json           |   4 +-
 .../omrekap/utils/omr/ContourOMRHelper.kt     | 147 +++++++++++++++++-
 3 files changed, 154 insertions(+), 7 deletions(-)

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
index df80cec..b765084 100644
--- a/app/src/androidTest/java/com/k2_9/omrekap/omr/ContourOMRHelperTest.kt
+++ b/app/src/androidTest/java/com/k2_9/omrekap/omr/ContourOMRHelperTest.kt
@@ -29,7 +29,7 @@ class ContourOMRHelperTest {
 		appContext = InstrumentationRegistry.getInstrumentation().targetContext
 
 		// Load the image resource as a Bitmap
-		val imageMat = Utils.loadResource(appContext, R.raw.test)
+		val imageMat = Utils.loadResource(appContext, R.raw.test2)
 		val templateLoader = CircleTemplateLoader(appContext, R.raw.circle_template)
 
 		// Convert if image is not grayscale
@@ -55,6 +55,8 @@ class ContourOMRHelperTest {
 			config.templateMatchingOMRHelperConfig.setTemplate(templateLoader)
 
 			helper = ContourOMRHelper(config.contourOMRHelperConfig)
+
+			helper.appContext = appContext
 		}
 	}
 
@@ -76,8 +78,8 @@ class ContourOMRHelperTest {
 		SaveHelper.saveImage(appContext, imgSecond, "test", "test_contour_omr_second")
 		SaveHelper.saveImage(appContext, imgThird, "test", "test_contour_omr_third")
 
-		assert(resultFirst == 172)
-		assert(resultSecond == 24)
-		assert(resultThird == 2)
+		assert(resultFirst == 87)
+		assert(resultSecond == 91)
+		assert(resultThird == 22)
 	}
 }
diff --git a/app/src/main/assets/omr_config.json b/app/src/main/assets/omr_config.json
index bc4515a..e164111 100644
--- a/app/src/main/assets/omr_config.json
+++ b/app/src/main/assets/omr_config.json
@@ -7,8 +7,8 @@
         "THIRD": "Janggar"
       },
       "contourOMRHelperConfig": {
-        "darkIntensityThreshold": 200,
-        "darkPercentageThreshold": 0.9,
+        "darkIntensityThreshold": 150,
+        "darkPercentageThreshold": 0.7,
         "maxAspectRatio": 1.5,
         "maxRadius": 25,
         "minAspectRatio": 0.5,
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 b5a4d9c..fd04515 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
@@ -1,22 +1,30 @@
 package com.k2_9.omrekap.utils.omr
 
+import android.content.Context
 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 com.k2_9.omrekap.utils.SaveHelper
 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.Point
 import org.opencv.core.Rect
 import org.opencv.core.Scalar
 import org.opencv.imgproc.Imgproc
+import kotlin.math.cos
+import kotlin.math.floor
+import kotlin.math.max
+import kotlin.math.sin
 
 class ContourOMRHelper(private val config: ContourOMRHelperConfig) : OMRHelper(config) {
 	private var currentSectionGray: Mat? = null
 	private var currentSectionBinary: Mat? = null
+	public var appContext: Context? = null
 
 	private fun createContourInfo(contour: Mat): ContourInfo {
 		val rect = Imgproc.boundingRect(contour)
@@ -129,6 +137,29 @@ class ContourOMRHelper(private val config: ContourOMRHelperConfig) : OMRHelper(c
 		return darkestRow
 	}
 
+	private fun getPerfectCircle(x: Double, y: Double, radius: Double): MatOfPoint {
+		val numPoints = 100  // Adjust as needed
+		val theta = DoubleArray(numPoints) { it * 2 * Math.PI / numPoints }
+		val circleX = DoubleArray(numPoints) { x + radius * cos(theta[it]) }
+		val circleY = DoubleArray(numPoints) { y + radius * sin(theta[it]) }
+
+		val circleContour = MatOfPoint()
+		for (i in 0 until numPoints) {
+			circleContour.push_back(MatOfPoint(Point(circleX[i], circleY[i])));
+		}
+
+		return circleContour
+	}
+
+	private fun replaceWithPerfectCircle(contour: MatOfPoint): MatOfPoint {
+		val rect = Imgproc.boundingRect(contour)
+		val centroidX = rect.x + rect.width.toDouble() / 2
+		val centroidY = rect.y + rect.height.toDouble() / 2
+		val radius = maxOf(rect.width.toDouble(), rect.height.toDouble()) / 2
+
+		return getPerfectCircle(centroidX, centroidY, radius)
+	}
+
 	private fun compareAll(contours: List<MatOfPoint>): Int {
 		// Sort contours by column and then by row
 		val contoursSorted = contours.sortedBy { Imgproc.boundingRect(it).x }
@@ -158,6 +189,99 @@ class ContourOMRHelper(private val config: ContourOMRHelperConfig) : OMRHelper(c
 		}
 		return getCombinedNumbers(darkestRows.map { it ?: 0 })
 	}
+	private fun completeMissingContours(contours: List<MatOfPoint>): List<MatOfPoint> {
+		val sortedContours = contours.sortedBy { Imgproc.boundingRect(it).y }
+		val columnMap = Array(3) { mutableListOf<MatOfPoint>() }
+		val rectColumnMap = Array(3) { mutableListOf<Rect>() }
+		val sortedRects = sortedContours.map { Imgproc.boundingRect(it) }
+
+		fun getColumnIndex(index: Int): Int {
+			return floor((sortedRects[index].x.toDouble() / config.omrCropper.config.omrSectionSize.first.toDouble()) * 3.0).toInt()
+		}
+
+		for ((idx, rect) in sortedRects.withIndex()) {
+			val columnIndex = getColumnIndex(idx)
+			columnMap[columnIndex].add(contours[idx])
+			rectColumnMap[columnIndex].add(rect)
+		}
+
+		val averageX = DoubleArray(3)
+
+		for ((idx, columns) in columnMap.withIndex()) {
+			if (columns.isEmpty()) {
+				// no contour in this column, skip entirely
+				return contours
+			}
+			averageX[idx] = rectColumnMap[idx].sumOf { it.x + it.width / 2.0 } / columns.size
+		}
+
+		val result = mutableListOf<MatOfPoint>()
+		val fillRecord = booleanArrayOf(false, false, false)
+		var ySum = 0.0
+		var radiusSum = 0.0
+		var lowestY = -1
+
+		var idx = 0
+
+		fun getLowestY(index: Int) = sortedRects[index].y + sortedRects[index].height
+
+		while (idx < sortedContours.size) {
+			val contour = sortedContours[idx]
+			val columnIndex = getColumnIndex(idx)
+			val currentLowestY = getLowestY(idx)
+
+			if (fillRecord[columnIndex] || (lowestY != -1 && sortedRects[idx].y > lowestY)) {
+				val nonFilledColumn = (0 until 3).filter { !fillRecord[it] }
+				val filledCount = 3 - nonFilledColumn.size
+
+				if (filledCount == 0) {
+					lowestY = currentLowestY
+					continue
+				}
+
+				val y = ySum / filledCount
+				val radius = radiusSum / filledCount
+
+				for (i in nonFilledColumn) {
+					val x = averageX[i]
+					result.add(getPerfectCircle(x, y, radius))
+					fillRecord[i] = true
+				}
+				lowestY = currentLowestY
+			} else {
+				result.add(contour)
+				ySum += sortedRects[idx].y + sortedRects[idx].height.toDouble() / 2
+				radiusSum += max(sortedRects[idx].width.toDouble(), sortedRects[idx].height.toDouble()) / 2
+				fillRecord[columnIndex] = true
+				idx++
+				lowestY = max(lowestY, currentLowestY)
+			}
+
+			val allFilled = fillRecord.all { it }
+
+			if (allFilled) {
+				fillRecord.fill(false)
+				ySum = 0.0
+				radiusSum = 0.0
+			}
+		}
+
+		val nonFilledColumn = (0 until 3).filter { !fillRecord[it] }
+		val filledCount = 3 - nonFilledColumn.size
+
+		if (filledCount > 0) {
+			val y = ySum / filledCount
+			val radius = radiusSum / filledCount
+
+			for (i in nonFilledColumn) {
+				val x = averageX[i]
+				result.add(getPerfectCircle(x, y, radius))
+				fillRecord[i] = true
+			}
+		}
+
+		return result
+	}
 
 	private fun getAllContours(): List<MatOfPoint> {
 		// Find circle contours in cropped OMR section
@@ -184,7 +308,7 @@ class ContourOMRHelper(private val config: ContourOMRHelperConfig) : OMRHelper(c
 			val maxAR = config.maxAspectRatio
 
 			if (rect.width in minLength..maxLength && rect.height in minLength..maxLength && ar >= minAR && ar <= maxAR) {
-				filteredContours.add(contour)
+				filteredContours.add(replaceWithPerfectCircle(contour))
 			} else {
 				Log.d(
 					"ContourOMRHelper",
@@ -193,6 +317,27 @@ class ContourOMRHelper(private val config: ContourOMRHelperConfig) : OMRHelper(c
 			}
 		}
 
+		if (filteredContours.size < 30) {
+			Log.d(
+				"ContourOMRHelper",
+				"Detected ${filteredContours.size} contours, attempting to complete missing contours",
+			)
+			val completedContours = completeMissingContours(filteredContours)
+			Log.d(
+				"ContourOMRHelper",
+				"Completed missing contours, now have ${completedContours.size} contours",
+			)
+
+			val display = currentSectionGray!!.clone()
+			Imgproc.drawContours(display, completedContours, -1, Scalar(255.0), -1)
+
+			val bitmap = Bitmap.createBitmap(display.cols(), display.rows(), Bitmap.Config.ARGB_8888)
+
+			Utils.matToBitmap(display, bitmap)
+			SaveHelper.saveImage(appContext!!, bitmap, "test", "lol.png")
+
+		}
+
 		return filteredContours
 	}
 
-- 
GitLab