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")