From f49ddb6e81b847069ef6565cb5c346a4521a1300 Mon Sep 17 00:00:00 2001
From: Michael Utama <13521137@std.stei.itb.ac.id>
Date: Sun, 19 May 2024 09:10:52 +0000
Subject: [PATCH] improve corner detection technique

---
 .../data/view_models/ImageDataViewModel.kt    |  11 +-
 .../java/com/k2_9/omrekap/utils/CropHelper.kt |  95 ++++++++++++++++--
 .../omrekap/utils/omr/ContourOMRHelper.kt     |  11 +-
 .../views/activities/PreviewActivity.kt       |   2 +-
 app/src/main/res/raw/mark16.png               | Bin 0 -> 203 bytes
 app/src/main/res/raw/mark20.png               | Bin 0 -> 167 bytes
 6 files changed, 103 insertions(+), 16 deletions(-)
 create mode 100644 app/src/main/res/raw/mark16.png
 create mode 100644 app/src/main/res/raw/mark20.png

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 1ba7b58..bfa8ed2 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
@@ -94,11 +94,12 @@ class ImageDataViewModel : ViewModel() {
 				for ((section, value) in it) {
 					stringKeyResult[pageContent[section]!!] = value
 
-					annotatedImage = ImageAnnotationHelper.annotateOMR(
-						annotatedImage,
-						contourOMRHelper.getSectionPosition(section),
-						value
-					)
+					annotatedImage =
+						ImageAnnotationHelper.annotateOMR(
+							annotatedImage,
+							contourOMRHelper.getSectionPosition(section),
+							value,
+						)
 					Log.d("Result", "${pageContent[section]}: $value")
 				}
 			}
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 03324a2..57b8e46 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
@@ -4,15 +4,18 @@ import android.graphics.Bitmap
 import android.util.Log
 import com.k2_9.omrekap.data.models.CornerPoints
 import org.opencv.android.Utils
+import org.opencv.core.Core
 import org.opencv.core.CvType
 import org.opencv.core.Mat
 import org.opencv.core.MatOfPoint2f
 import org.opencv.core.Point
+import org.opencv.core.Size
 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.min
 import kotlin.math.pow
 import kotlin.math.sqrt
 
@@ -54,9 +57,12 @@ object CropHelper {
 			throw Exception("Pattern not loaded!")
 		}
 
-		val imgGray = img.clone()
+		var imgGray = img.clone()
 		cvtColor(img, imgGray, COLOR_BGR2GRAY)
 
+		// do local normalization here
+		imgGray = localNormalize(imgGray)
+
 		val resultMatrix =
 			Mat(
 				img.height() - pattern.height() + 1,
@@ -81,10 +87,22 @@ object CropHelper {
 		)
 
 		val pointsList: MutableList<PointsAndWeight> = mutableListOf()
-
-		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]))
+		val diagonalLength = (resultMatrix.height().toDouble().pow(2) * resultMatrix.width().toDouble().pow(2)).pow(1 / 2)
+
+		for (i in 0 until resultMatrix.height() step 2) {
+			for (j in 0 until resultMatrix.width() step 2) {
+				pointsList.add(
+					PointsAndWeight(
+						i,
+						j,
+						resultMatrix.get(i, j)[0] *
+							getWeight(
+								(
+									min(i, resultMatrix.height() - i).toDouble().pow(2) * min(j, resultMatrix.width() - j).toDouble().pow(2)
+								).pow(1 / 2) / diagonalLength,
+							),
+					),
+				)
 			}
 		}
 
@@ -93,9 +111,22 @@ object CropHelper {
 		pointsList.forEach {
 			if (needChange == 0) return@forEach
 
-			val corner = nearWhichCorner(it.x, it.y, resultMatrix.height(), resultMatrix.width(), limFrac = 0.1F)
+			val corner = nearWhichCorner(it.x, it.y, resultMatrix.height(), resultMatrix.width(), limFrac = 0.6F)
 			if (corner == -1) return@forEach
 
+			if (it.weight > 0.45) {
+				// Corner not found, throw exception
+				val exceptionMessage =
+					"Not all corners found: {" +
+						(if (needed[0]) "Upper left," else "") +
+						(if (needed[1]) "Upper right," else "") +
+						(if (needed[2]) "Lower right," else "") +
+						(if (needed[3]) "Lower left," else "") +
+						"}"
+				// throw NotFoundException(exceptionMessage);
+				Log.e("Corner", exceptionMessage)
+			}
+
 			if (needed[corner]) {
 				needed[corner] = false
 				needChange--
@@ -238,4 +269,56 @@ object CropHelper {
 			else -> -1
 		}
 	}
+
+	/**
+	 * Apply local normalization to an image
+	 * source: https://stackoverflow.com/questions/43240604/python-local-normalization-in-opencv
+	 * @see - https://bigwww.epfl.ch/demo/ip/demos/local-normalization/
+	 *
+	 * @param img input matrix
+	 * @return local normalized img
+	 */
+	private fun localNormalize(img: Mat): Mat {
+		// convert img to CV_32F
+		val gray = Mat()
+		img.convertTo(gray, CvType.CV_32F, 1.0 / 255.0)
+
+		val blur = Mat()
+		Imgproc.GaussianBlur(gray, blur, Size(0.0, 0.0), 2.0, 2.0)
+
+		val num = Mat()
+		Core.subtract(gray, blur, num)
+
+		val numSquared = Mat()
+		Core.multiply(num, num, numSquared)
+		val blur2 = Mat()
+		Imgproc.GaussianBlur(numSquared, blur2, Size(0.0, 0.0), 20.0, 20.0)
+
+		val den = Mat()
+		Core.sqrt(blur2, den)
+
+		val div = Mat()
+		Core.divide(num, den, div)
+
+		Core.normalize(div, div, 0.0, 1.0, Core.NORM_MINMAX)
+
+		// Convert back to uint8
+		val result = Mat()
+		div.convertTo(result, CvType.CV_8U, 255.0)
+
+		return result
+	}
+
+	/**
+	 * Weight for a point
+	 * Far from corner means bigger weight
+	 *
+	 * Smaller weight are more likely to be chosen as corner
+	 *
+	 * @param normDistance distance from nearest corner divided by diagonal
+	 * @return weight
+	 */
+	private fun getWeight(normDistance: Double): Double {
+		return 1 + 1 * normDistance.pow(1.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 43fdba7..c60aaea 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
@@ -170,15 +170,19 @@ class ContourOMRHelper(private val config: ContourOMRHelperConfig) : OMRHelper(c
 	 * @param radius radius of the circle
 	 * @return perfect circle contour
 	 */
-	private fun getPerfectCircle(x: Double, y: Double, radius: Double): MatOfPoint {
-		val numPoints = 100  // Adjust as needed
+	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])));
+			circleContour.push_back(MatOfPoint(Point(circleX[i], circleY[i])))
 		}
 
 		return circleContour
@@ -388,7 +392,6 @@ class ContourOMRHelper(private val config: ContourOMRHelperConfig) : OMRHelper(c
 
 			Utils.matToBitmap(display, bitmap)
 			SaveHelper.saveImage(appContext!!, bitmap, "test", "lol.png")
-
 		}
 
 		return filteredContours
diff --git a/app/src/main/java/com/k2_9/omrekap/views/activities/PreviewActivity.kt b/app/src/main/java/com/k2_9/omrekap/views/activities/PreviewActivity.kt
index 240353e..bd37f1d 100644
--- a/app/src/main/java/com/k2_9/omrekap/views/activities/PreviewActivity.kt
+++ b/app/src/main/java/com/k2_9/omrekap/views/activities/PreviewActivity.kt
@@ -50,7 +50,7 @@ class PreviewActivity : AppCompatActivity() {
 		val bitmapOptions = BitmapFactory.Options()
 		bitmapOptions.inPreferredConfig = Bitmap.Config.ALPHA_8
 		bitmapOptions.inScaled = false
-		val cornerPatternBitmap: Bitmap = BitmapFactory.decodeResource(resources, R.raw.corner_pattern, bitmapOptions)
+		val cornerPatternBitmap: Bitmap = BitmapFactory.decodeResource(resources, R.raw.mark20, bitmapOptions)
 
 		CropHelper.loadPattern(cornerPatternBitmap)
 
diff --git a/app/src/main/res/raw/mark16.png b/app/src/main/res/raw/mark16.png
new file mode 100644
index 0000000000000000000000000000000000000000..e34e5106f50567a9baecca15a4ba92bd1c58670f
GIT binary patch
literal 203
zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`jKx9jP7LeL$-D$|SkfJR9T^xl
z_H+M9WCij$3p^r=85o30K$!7fntTONFu~KsF~q{Zwa1a`fP=uHZ~y1tt137plEnK&
z%=Xd0n*oOpZ(P{cp6txaf4ogfu8-H$Mn*cT!ouvzhk~3dO@8*fx*ue`)@xe4b~i`g
jws%Svw~z(m&et;3EUKM9ZSglPpj`}}u6{1-oD!M<W!6Wf

literal 0
HcmV?d00001

diff --git a/app/src/main/res/raw/mark20.png b/app/src/main/res/raw/mark20.png
new file mode 100644
index 0000000000000000000000000000000000000000..7c65cbf6ea9c87669d1c11dec0d6d510b7ac3561
GIT binary patch
literal 167
zcmeAS@N?(olHy`uVBq!ia0vp^A|TAc1|)ksWqE-VV{wqX6T`Z5GB1G~mUKs7M+SzC
z{oH>NS%G}c0*}aI1_q%L5N5oWCSL&*wDELt46*P}PDx1k!GDD1;Q#+Fn#l=GmqM5q
zvjy?F@h*`uU@B$^jBJ=FIAtYA^F@z^GCfirmJ2pD^6)TRuH;av(rLL5G>5^{)z4*}
HQ$iB}hD<F3

literal 0
HcmV?d00001

-- 
GitLab