Skip to content
Snippets Groups Projects
Commit 2c7fb897 authored by Michael Utama's avatar Michael Utama
Browse files

merge from dev

parents 37e5f341 7bb0871b
1 merge request!58improve corner detection technique
Showing
with 214 additions and 51 deletions
......@@ -53,8 +53,5 @@ jobs:
- name: Run unit tests
run: ./gradlew test
- name: Run integration tests
run: ./gradlew connectedAndroidTest
- name: Code formatting
run: ./gradlew spotlessCheck
......@@ -15,6 +15,9 @@
</p>
</div>
[![forthebadge](https://forthebadge.com/images/badges/made-with-kotlin.svg)](https://forthebadge.com) [![forthebadge](https://forthebadge.com/images/badges/built-for-android.svg)](https://forthebadge.com) [![forthebadge](https://forthebadge.com/images/badges/built-with-love.svg)](https://forthebadge.com)
[![Android](https://img.shields.io/badge/Android-%233DDC84.svg?&style=for-the-badge&logo=android&logoColor=white)](https://developer.android.com/) [![Kotlin](https://img.shields.io/badge/Kotlin-%230095D5.svg?&style=for-the-badge&logo=kotlin&logoColor=white)](https://kotlinlang.org/) [![Gradle](https://img.shields.io/badge/Gradle-%2302303A.svg?&style=for-the-badge&logo=gradle&logoColor=white)](https://gradle.org/) [![OpenCV](https://img.shields.io/badge/OpenCV-%23opencv.svg?&style=for-the-badge&logo=opencv&logoColor=white)](https://opencv.org/)
<!-- TABLE OF CONTENTS -->
<details>
<summary>Table of Contents</summary>
......@@ -32,13 +35,42 @@
<li><a href="#installation">Installation</a></li>
</ul>
</li>
<li><a href="#how-to-use">How To Use</a></li>
<li><a href="#Development">Development</a></li>
<li><a href="#usage">Usage</a></li>
<li><a href="#contact">Contact</a></li>
<li><a href="#contributors">Contributors</a></li>
<li><a href="#acknowledgments">Acknowledgments</a></li>
</ol>
</details>
# About The Project
OMRekap adalah aplikasi rekapitulasi pemilihan umum yang dibuat untuk memudahkan proses rekapitulasi suara pada pemilihan umum.
Aplikasi ini dibuat menggunakan bahasa pemrograman Kotlin dan menggunakan OpenCV untuk mendeteksi dan mengenali lembar suara.
<p align="right">(<a href="#readme-top">back to top</a>)</p>
# Features
1. Mengambil foto kertas plano menggunakan kamera 📷 atau menggunakan galeri 🖼
2. Melihat hasil foto yang diambil pada halaman berikutnya
3. Mendeteksi dan mengenali lembar suara
4. Menampilkan hasil rekapitulasi suara
5. Menyimpan hasil rekapitulasi suara dalam bentuk foto 🖼 dan JSON 📃
![Application](screenshots/screenshot.png)
<p align="right">(<a href="#readme-top">back to top</a>)</p>
# How To Use
1. Navigasi ke folder apk
2. Unduh file apk lalu pindahkan ke perangkat android
3. Buka file apk di perangkat android
4. Install aplikasi
5. Buka aplikasi
Alternatif lain untuk menjalankan aplikasi:
1. Clone repository https://gitlab.informatika.org/k-02-09/omrekap.git
2. Buka project menggunakan Android Studio
3. Run aplikasi menggunakan emulator atau perangkat android
# Development
### Clone the repository
```bash
......@@ -64,20 +96,22 @@ git clone https://gitlab.informatika.org/k-02-09/omrekap
<p align="right">(<a href="#readme-top">back to top</a>)</p>
# Built With
* [![Android](https://img.shields.io/badge/Android-%233DDC84.svg?&style=flat&logo=android&logoColor=white)](https://developer.android.com/)
* [![Kotlin](https://img.shields.io/badge/Kotlin-%230095D5.svg?&style=flat&logo=kotlin&logoColor=white)](https://kotlinlang.org/)
* [![Gradle](https://img.shields.io/badge/Gradle-%2302303A.svg?&style=flat&logo=gradle&logoColor=white)](https://gradle.org/)
* [![OpenCV](https://img.shields.io/badge/OpenCV-%23opencv.svg?&style=flat&logo=opencv&logoColor=white)](https://opencv.org/)
# About The Project
OMRekap adalah aplikasi rekapitulasi pemilihan umum yang dibuat untuk memudahkan proses rekapitulasi suara pada pemilihan umum.
Aplikasi ini dibuat menggunakan bahasa pemrograman Kotlin dan menggunakan OpenCV untuk mendeteksi dan mengenali lembar suara.
# Contributors
<table>
<tbody>
<tr>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Altair1618"><img src="https://github.com/Altair1618" width="100px;" alt="Farhan Nabil Suryono"/><br /><sub><b>Farhan Nabil Suryono</b></sub></a><br /><a href="https://github.com/codesandbox/codesandbox-client/issues?q=author%3ACompuIves" title="Bug reports">🐛</a> <a href="https://github.com/codesandbox/codesandbox-client/commits?author=CompuIves" title="Code">💻</a> <a href="#design-CompuIves" title="Design">🎨</a> <a href="https://github.com/codesandbox/codesandbox-client/commits?author=CompuIves" title="Documentation">📖</a> <a href="#infra-CompuIves" title="Infrastructure (Hosting, Build-Tools, etc)">🚇</a> <a href="https://github.com/codesandbox/codesandbox-client/pulls?q=is%3Apr+reviewed-by%3ACompuIves" title="Reviewed Pull Requests">👀</a> <a href="https://github.com/codesandbox/codesandbox-client/commits?author=CompuIves" title="Tests">⚠️</a> <a href="#tool-CompuIves" title="Tools">🔧</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Enliven26"><img src="https://github.com/Enliven26" width="100px;" alt="Johanes Lee"/><br /><sub><b>Johanes Lee</b></sub></a><br /><a href="https://github.com/codesandbox/codesandbox-client/issues?q=author%3ACompuIves" title="Bug reports">🐛</a> <a href="https://github.com/codesandbox/codesandbox-client/commits?author=CompuIves" title="Code">💻</a> <a href="#design-CompuIves" title="Design">🎨</a> <a href="https://github.com/codesandbox/codesandbox-client/commits?author=CompuIves" title="Documentation">📖</a> <a href="#infra-CompuIves" title="Infrastructure (Hosting, Build-Tools, etc)">🚇</a> <a href="https://github.com/codesandbox/codesandbox-client/pulls?q=is%3Apr+reviewed-by%3ACompuIves" title="Reviewed Pull Requests">👀</a> <a href="https://github.com/codesandbox/codesandbox-client/commits?author=CompuIves" title="Tests">⚠️</a> <a href="#tool-CompuIves" title="Tools">🔧</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/dhanikanovlisa"><img src="https://github.com/dhanikanovlisa" width="100px;" alt="Dhanika Novlisariyanti"/><br /><sub><b>Dhanika Novlisariyanti</b></sub></a><br /><a href="https://github.com/codesandbox/codesandbox-client/issues?q=author%3ACompuIves" title="Bug reports">🐛</a> <a href="https://github.com/codesandbox/codesandbox-client/commits?author=CompuIves" title="Code">💻</a> <a href="#design-CompuIves" title="Design">🎨</a> <a href="https://github.com/codesandbox/codesandbox-client/commits?author=CompuIves" title="Documentation">📖</a> <a href="#infra-CompuIves" title="Infrastructure (Hosting, Build-Tools, etc)">🚇</a> <a href="https://github.com/codesandbox/codesandbox-client/pulls?q=is%3Apr+reviewed-by%3ACompuIves" title="Reviewed Pull Requests">👀</a> <a href="https://github.com/codesandbox/codesandbox-client/commits?author=CompuIves" title="Tests">⚠️</a> <a href="#tool-CompuIves" title="Tools">🔧</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Genvictus"><img src="https://github.com/Genvictus" width="100px;" alt="Johann Christian Kandani"/><br /><sub><b>Johann Christian Kandani</b></sub></a><br /><a href="https://github.com/codesandbox/codesandbox-client/issues?q=author%3ACompuIves" title="Bug reports">🐛</a> <a href="https://github.com/codesandbox/codesandbox-client/commits?author=CompuIves" title="Code">💻</a> <a href="#design-CompuIves" title="Design">🎨</a> <a href="https://github.com/codesandbox/codesandbox-client/commits?author=CompuIves" title="Documentation">📖</a> <a href="#infra-CompuIves" title="Infrastructure (Hosting, Build-Tools, etc)">🚇</a> <a href="https://github.com/codesandbox/codesandbox-client/pulls?q=is%3Apr+reviewed-by%3ACompuIves" title="Reviewed Pull Requests">👀</a> <a href="https://github.com/codesandbox/codesandbox-client/commits?author=CompuIves" title="Tests">⚠️</a> <a href="#tool-CompuIves" title="Tools">🔧</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Michaelu670"><img src="https://github.com/Michaelu670" width="100px;" alt="Michael Utama"/><br /><sub><b>Michael Utama</b></sub></a><br /><a href="https://github.com/codesandbox/codesandbox-client/issues?q=author%3ACompuIves" title="Bug reports">🐛</a> <a href="https://github.com/codesandbox/codesandbox-client/commits?author=CompuIves" title="Code">💻</a> <a href="#design-CompuIves" title="Design">🎨</a> <a href="https://github.com/codesandbox/codesandbox-client/commits?author=CompuIves" title="Documentation">📖</a> <a href="#infra-CompuIves" title="Infrastructure (Hosting, Build-Tools, etc)">🚇</a> <a href="https://github.com/codesandbox/codesandbox-client/pulls?q=is%3Apr+reviewed-by%3ACompuIves" title="Reviewed Pull Requests">👀</a> <a href="https://github.com/codesandbox/codesandbox-client/commits?author=CompuIves" title="Tests">⚠️</a> <a href="#tool-CompuIves" title="Tools">🔧</a></td>
</tbody>
</table>
<p align="right">(<a href="#readme-top">back to top</a>)</p>
# Usage
# Contact
# Acknowledgments
* [OpenCV](https://opencv.org/)
* [Kotlin](https://kotlinlang.org/)
* [Android](https://developer.android.com/)
# Acknowledgments
\ No newline at end of file
<p align="right">(<a href="#readme-top">back to top</a>)</p>
\ No newline at end of file
......@@ -29,7 +29,7 @@ class ContourOMRHelperTest {
appContext = InstrumentationRegistry.getInstrumentation().targetContext
// Load the image resource as a Bitmap
val imageMat = Utils.loadResource(appContext, R.raw.test)
val imageMat = Utils.loadResource(appContext, R.raw.test2)
val templateLoader = CircleTemplateLoader(appContext, R.raw.circle_template)
// Convert if image is not grayscale
......@@ -55,6 +55,8 @@ class ContourOMRHelperTest {
config.templateMatchingOMRHelperConfig.setTemplate(templateLoader)
helper = ContourOMRHelper(config.contourOMRHelperConfig)
helper.appContext = appContext
}
}
......@@ -76,8 +78,8 @@ class ContourOMRHelperTest {
SaveHelper.saveImage(appContext, imgSecond, "test", "test_contour_omr_second")
SaveHelper.saveImage(appContext, imgThird, "test", "test_contour_omr_third")
assert(resultFirst == 172)
assert(resultSecond == 24)
assert(resultThird == 2)
assert(resultFirst == 87)
assert(resultSecond == 91)
assert(resultThird == 22)
}
}
......@@ -2,13 +2,13 @@
"omrConfigs": {
"102": {
"contents": {
"FIRST": "Anis",
"SECOND": "Bowo",
"THIRD": "Janggar"
"FIRST": "Anies Rasyid Baswedan",
"SECOND": "Prabowo Subianto",
"THIRD": "Ganjar Pranowo"
},
"contourOMRHelperConfig": {
"darkIntensityThreshold": 200,
"darkPercentageThreshold": 0.9,
"darkIntensityThreshold": 150,
"darkPercentageThreshold": 0.7,
"maxAspectRatio": 1.5,
"maxRadius": 25,
"minAspectRatio": 0.5,
......
......@@ -6,7 +6,16 @@ import org.opencv.core.MatOfByte
import org.opencv.imgcodecs.Imgcodecs
import java.io.InputStream
/**
* Load the circle template image
* @param appContext application context
* @param resId resource id of the circle template image
*/
class CircleTemplateLoader(private val appContext: Context, private val resId: Int) {
/**
* Load the template image
* @return template image
*/
fun loadTemplateImage(): Mat {
val inputStream: InputStream = appContext.resources.openRawResource(resId)
val byteArray = inputStream.readBytes()
......
package com.k2_9.omrekap.data.configs.omr
/**
* Configuration for the OMR helper
* @param omrCropper cropper for the OMR section
* @param minRadius minimum radius of the circle
* @param maxRadius maximum radius of the circle
* @param minAspectRatio minimum aspect ratio of the circle
* @param maxAspectRatio maximum aspect ratio of the circle
* @param darkPercentageThreshold threshold for the percentage of dark pixels in the circle
* @param darkIntensityThreshold threshold for the intensity of dark pixels in the circle
*/
class ContourOMRHelperConfig(
omrCropper: OMRCropper,
minRadius: Int,
......
......@@ -3,7 +3,16 @@ package com.k2_9.omrekap.data.configs.omr
import org.opencv.core.Mat
import org.opencv.core.Rect
/**
* Cropper for OMR section
* @param config configuration for the cropper
*/
class OMRCropper(val config: OMRCropperConfig) {
/**
* Crop the image to the section
* @param section section to be cropped
* @return cropped image
*/
fun crop(section: OMRSection): Mat {
val (x, y) = config.getSectionPosition(section)
val (width, height) = config.omrSectionSize
......@@ -13,6 +22,11 @@ class OMRCropper(val config: OMRCropperConfig) {
return Mat(config.image, roi)
}
/**
* Get the position of the section
* @param section section to get the position
* @return position of the section
*/
fun sectionPosition(section: OMRSection): Rect {
val (x, y) = config.getSectionPosition(section)
val (width, height) = config.omrSectionSize
......
......@@ -2,6 +2,12 @@ package com.k2_9.omrekap.data.configs.omr
import org.opencv.core.Mat
/**
* Configuration for the cropper
* @param image image to be cropped
* @param omrSectionSize size of the OMR section
* @param omrSectionPosition position of the OMR section
*/
class OMRCropperConfig(
image: Mat?,
val omrSectionSize: Pair<Int, Int>,
......@@ -39,10 +45,19 @@ class OMRCropperConfig(
}
}
/**
* Get the position of the section
* @param section section to get the position
* @return position of the section
*/
fun getSectionPosition(section: OMRSection): Pair<Int, Int> {
return omrSectionPosition[section]!!
}
/**
* Set the image
* @param image image to be set
*/
fun setImage(image: Mat) {
require(omrSectionSize.first <= image.width() && omrSectionSize.second <= image.height()) {
"OMR section size must be less than or equal to the image size"
......
package com.k2_9.omrekap.data.configs.omr
/**
* Configuration for the OMR helper
* @param omrCropper cropper for the OMR section
*/
open class OMRHelperConfig(
val omrCropper: OMRCropper,
)
package com.k2_9.omrekap.data.configs.omr
/**
* Enum for OMR section
*/
enum class OMRSection {
FIRST,
SECOND,
......
......@@ -2,6 +2,12 @@ package com.k2_9.omrekap.data.configs.omr
import org.opencv.core.Mat
/**
* Configuration for the OMR helper using template matching
* @param omrCropper cropper for the OMR section
* @param templateLoader loader for the template image
* @param similarityThreshold threshold for the similarity between the template and the cropped image
*/
class TemplateMatchingOMRHelperConfig(
omrCropper: OMRCropper,
templateLoader: CircleTemplateLoader?,
......
......@@ -2,6 +2,13 @@ package com.k2_9.omrekap.data.models
import org.opencv.core.Point
/**
* Data class for corner points of a document
* @param topLeft top left corner point
* @param topRight top right corner point
* @param bottomRight bottom right corner point
* @param bottomLeft bottom left corner point
*/
data class CornerPoints(
val topLeft: Point,
val topRight: Point,
......
......@@ -3,6 +3,13 @@ package com.k2_9.omrekap.data.models
import android.graphics.Bitmap
import java.time.Instant
/**
* Data class for saving image data
* @param rawImage raw image
* @param annotatedImage image with annotations
* @param data map of candidate names and their vote counts
* @param timestamp timestamp of the image
*/
data class ImageSaveData(
val rawImage: Bitmap,
var annotatedImage: Bitmap,
......
......@@ -11,6 +11,12 @@ data class OMRBaseConfiguration(
val omrConfigs: Map<String, OMRConfigurationParameter>,
)
/**
* Configuration parameters for OMR detection
* @param contents map of OMR section and candidate name
* @param contourOMRHelperConfig configuration for contour-based OMR detection
* @param templateMatchingOMRHelperConfig configuration for template matching-based OMR detection
*/
data class OMRConfigurationParameter(
val contents: Map<OMRSection, String>,
val contourOMRHelperConfig: ContourOMRHelperConfig,
......
......@@ -8,7 +8,14 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import java.io.IOException
/** Repository for loading OMR configurations from JSON */
object OMRConfigRepository {
/**
* Load OMR configurations from JSON file
* @param context application context
* @return OMRBaseConfiguration object
*/
suspend fun loadConfigurations(context: Context): OMRBaseConfiguration? {
val jsonString =
withContext(Dispatchers.IO) {
......@@ -23,12 +30,22 @@ object OMRConfigRepository {
}
}
/**
* Get the JSON string of the OMR configuration
* @param omrBaseConfiguration OMR configuration object
* @return JSON string of the configuration
*/
fun printConfigurationJson(omrBaseConfiguration: OMRBaseConfiguration): String {
val jsonString = OMRJsonConfigLoader.toJson(omrBaseConfiguration)
Log.d("JSONConfigRepo", jsonString)
return jsonString
}
/**
* Read the JSON configuration file
* @param context application context
* @return JSON string of the configuration
*/
private fun readConfigString(context: Context): String? {
val inputStream = context.assets.open("omr_config.json")
return try {
......
......@@ -3,13 +3,26 @@ package com.k2_9.omrekap.data.repository
import com.google.gson.Gson
import com.k2_9.omrekap.data.models.OMRBaseConfiguration
/**
* JSON configuration loader for OMR configurations
*/
object OMRJsonConfigLoader {
private val gson = Gson()
/**
* Parse JSON string to OMRBaseConfiguration object
* @param jsonString JSON string
* @return OMRBaseConfiguration object
*/
fun parseJson(jsonString: String): OMRBaseConfiguration? {
return gson.fromJson(jsonString, OMRBaseConfiguration::class.java)
}
/**
* Convert OMRBaseConfiguration object to JSON string
* @param omrBaseConfiguration OMRBaseConfiguration object
* @return JSON string
*/
fun toJson(omrBaseConfiguration: OMRBaseConfiguration): String {
return gson.toJson(omrBaseConfiguration)
}
......
......@@ -21,10 +21,18 @@ import org.opencv.core.Mat
import org.opencv.imgproc.Imgproc
import java.time.Instant
/**
* ViewModel for image data processing in Result page
*/
class ImageDataViewModel : ViewModel() {
private val _data = MutableLiveData<ImageSaveData?>()
val data = _data as LiveData<ImageSaveData?>
/**
* Process the image data with OMR detection
* @param data image data to be processed
* @param circleTemplateLoader loader for circle template
*/
fun processImage(
data: ImageSaveData,
circleTemplateLoader: CircleTemplateLoader,
......
......@@ -10,10 +10,17 @@ import com.k2_9.omrekap.utils.PreprocessHelper
import kotlinx.coroutines.launch
import java.time.Instant
/**
* ViewModel for image data processing in Preview page
*/
class PreviewViewModel : ViewModel() {
private val _data = MutableLiveData<ImageSaveData>()
val data = _data as LiveData<ImageSaveData>
/**
* Preprocess the image data
* @param img image to be preprocessed
*/
fun preprocessImage(img: Bitmap) {
viewModelScope.launch {
val data = ImageSaveData(img, img, mapOf(), Instant.now())
......
......@@ -74,6 +74,11 @@ object AprilTagHelper {
return (idList to corners)
}
/**
* Annotate the detected april tag and its ID to the image
* @param imageBitmap image to be annotated
* @return annotated image in OpenCV's Mat type
*/
fun annotateImage(imageBitmap: Bitmap): Mat {
val res = getAprilTagId(imageBitmap)
val cornerPoints = res.second
......@@ -81,6 +86,11 @@ object AprilTagHelper {
return ImageAnnotationHelper.annotateAprilTag(prepareImage(imageBitmap), cornerPoints, ids)
}
/**
* Prepare detector with given dictionary
* @param detectorDictionary dictionary to be used for detection
* @return prepared ArucoDetector
*/
private fun prepareDetector(detectorDictionary: Dictionary): ArucoDetector {
// initialize detector parameters
val detectorParameters = DetectorParameters()
......@@ -88,6 +98,11 @@ object AprilTagHelper {
return ArucoDetector(detectorDictionary, detectorParameters)
}
/**
* Prepare image for ArucoDetector with preprocessing
* @param imageBitmap image to be prepared
* @return prepared image in OpenCV's Mat type
*/
private fun prepareImage(imageBitmap: Bitmap): Mat {
// transform to OpenCV Mat data
val imageMat = Mat()
......
......@@ -19,6 +19,9 @@ import kotlin.math.min
import kotlin.math.pow
import kotlin.math.sqrt
/**
* Helper class for cropping image based on corner points detection
*/
object CropHelper {
private const val UPPER_LEFT: Int = 0
private const val UPPER_RIGHT: Int = 1
......@@ -28,10 +31,8 @@ object CropHelper {
private lateinit var pattern: Mat
/**
* Uses OpenCV module, remember OpenCVLoader.initLocal() has been run before
* load corner pattern
*
* @param patternBitmap corner pattern in Bitmap
* Load corner pattern image
* @param patternBitmap pattern image in Bitmap
*/
fun loadPattern(patternBitmap: Bitmap) {
// Load only if pattern hasn't been loaded
......@@ -46,16 +47,9 @@ object CropHelper {
}
/**
* Uses OpenCV module, remember OpenCVLoader.initLocal() has been run before
*
* todo @exception if corner found are bad
*
* Initialize corner pattern first using [CropHelper.loadPattern]
*
*
* @param img Mat that has been scaled, in grayscale (CV_8UC1)
*
* @return CornerPoints if four corners are found
* Detect corner points in the image
* @param img image to be processed
* @return corner points
*/
fun detectCorner(img: Mat): CornerPoints {
// If pattern hasn't been loaded, throw exception
......@@ -137,7 +131,6 @@ object CropHelper {
needed[corner] = false
needChange--
val pointFromIt = Point(it.y.toDouble(), it.x.toDouble())
Log.d("Corner", "Difference: ${it.weight}; corner: $corner")
when (corner) {
UPPER_LEFT -> {
upperLeftPoint = pointFromIt
......@@ -168,14 +161,10 @@ object CropHelper {
}
/**
* Uses OpenCV module, remember OpenCVLoader.initLocal() has been run before
*
* Crop an image based on four corners, with some padding set by #mult
*
* @param img image to crop
* @param points corner points to crop image
*
* @return cropped image
* Transform image based on corner points into a rectangle
* @param img image to be processed
* @param points corner points of the image
* @return tilted corrected image
*/
fun fourPointTransform(
img: Mat,
......
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment