From 16a42af437883e4b1a8a434b8ba38df8fc1a60bf Mon Sep 17 00:00:00 2001 From: Enliven26 <16521443@mahasiswa.itb.ac.id> Date: Sun, 19 May 2024 12:26:51 +0700 Subject: [PATCH] refactor: javadocs --- .../data/configs/omr/CircleTemplateLoader.kt | 9 +++ .../configs/omr/ContourOMRHelperConfig.kt | 10 +++ .../omrekap/data/configs/omr/OMRCropper.kt | 14 ++++ .../data/configs/omr/OMRCropperConfig.kt | 15 ++++ .../data/configs/omr/OMRHelperConfig.kt | 4 ++ .../omrekap/data/configs/omr/OMRSection.kt | 3 + .../omr/TemplateMatchingOMRHelperConfig.kt | 6 ++ .../k2_9/omrekap/data/models/CornerPoints.kt | 7 ++ .../k2_9/omrekap/data/models/ImageSaveData.kt | 7 ++ .../data/models/OMRBaseConfiguration.kt | 6 ++ .../data/repository/OMRConfigRepository.kt | 17 +++++ .../data/repository/OMRJsonConfigLoader.kt | 13 ++++ .../data/view_models/ImageDataViewModel.kt | 8 +++ .../data/view_models/PreviewViewModel.kt | 7 ++ .../com/k2_9/omrekap/utils/AprilTagHelper.kt | 15 ++++ .../java/com/k2_9/omrekap/utils/CropHelper.kt | 18 +++++ .../omrekap/utils/ImageAnnotationHelper.kt | 37 ++++++++++ .../k2_9/omrekap/utils/ImageSaveDataHolder.kt | 11 +++ .../k2_9/omrekap/utils/PermissionHelper.kt | 10 +++ .../k2_9/omrekap/utils/PreprocessHelper.kt | 8 +++ .../java/com/k2_9/omrekap/utils/SaveHelper.kt | 32 +++++++++ .../omrekap/utils/omr/ContourOMRHelper.kt | 70 ++++++++++++++++++- .../omrekap/utils/omr/OMRConfigDetector.kt | 5 ++ .../com/k2_9/omrekap/utils/omr/OMRHelper.kt | 43 ++++++++++++ .../utils/omr/TemplateMatchingOMRHelper.kt | 19 +++++ 25 files changed, 392 insertions(+), 2 deletions(-) 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 index f2deec7..c1bac3f 100644 --- 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 @@ -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() 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 index 4053bca..911d92d 100644 --- 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 @@ -1,5 +1,15 @@ 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, 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 index 7ee53ff..66f5e63 100644 --- 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 @@ -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 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 index fabe926..9a9be96 100644 --- 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 @@ -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" 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 index 0022677..8a72269 100644 --- 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 @@ -1,5 +1,9 @@ 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, ) 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 index d6f0ce8..3fc6350 100644 --- 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 @@ -1,5 +1,8 @@ package com.k2_9.omrekap.data.configs.omr +/** + * Enum for OMR section + */ enum class OMRSection { FIRST, SECOND, 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 index f3d83c3..59d62fb 100644 --- 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 @@ -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?, diff --git a/app/src/main/java/com/k2_9/omrekap/data/models/CornerPoints.kt b/app/src/main/java/com/k2_9/omrekap/data/models/CornerPoints.kt index e3f8f5c..af17fa9 100644 --- a/app/src/main/java/com/k2_9/omrekap/data/models/CornerPoints.kt +++ b/app/src/main/java/com/k2_9/omrekap/data/models/CornerPoints.kt @@ -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, diff --git a/app/src/main/java/com/k2_9/omrekap/data/models/ImageSaveData.kt b/app/src/main/java/com/k2_9/omrekap/data/models/ImageSaveData.kt index e8186b4..0002067 100644 --- a/app/src/main/java/com/k2_9/omrekap/data/models/ImageSaveData.kt +++ b/app/src/main/java/com/k2_9/omrekap/data/models/ImageSaveData.kt @@ -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, 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 index f5cc6f3..040dab3 100644 --- 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 @@ -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, diff --git a/app/src/main/java/com/k2_9/omrekap/data/repository/OMRConfigRepository.kt b/app/src/main/java/com/k2_9/omrekap/data/repository/OMRConfigRepository.kt index 05dbebd..94433d8 100644 --- a/app/src/main/java/com/k2_9/omrekap/data/repository/OMRConfigRepository.kt +++ b/app/src/main/java/com/k2_9/omrekap/data/repository/OMRConfigRepository.kt @@ -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 { 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 index 9cf3d98..2a3d64f 100644 --- 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 @@ -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) } diff --git a/app/src/main/java/com/k2_9/omrekap/data/view_models/ImageDataViewModel.kt b/app/src/main/java/com/k2_9/omrekap/data/view_models/ImageDataViewModel.kt index 3f3696f..1ba7b58 100644 --- a/app/src/main/java/com/k2_9/omrekap/data/view_models/ImageDataViewModel.kt +++ b/app/src/main/java/com/k2_9/omrekap/data/view_models/ImageDataViewModel.kt @@ -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, 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 index 89451f9..6176644 100644 --- 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 @@ -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()) 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 ecd7aac..7a61a3b 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 @@ -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() 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 1ff85ab..03324a2 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 @@ -16,6 +16,9 @@ import org.opencv.imgproc.Imgproc.warpPerspective 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 @@ -24,6 +27,10 @@ object CropHelper { private lateinit var pattern: Mat + /** + * Load corner pattern image + * @param patternBitmap pattern image in Bitmap + */ fun loadPattern(patternBitmap: Bitmap) { // Load only if pattern hasn't been loaded if (::pattern.isInitialized) return @@ -36,6 +43,11 @@ object CropHelper { this.pattern = PreprocessHelper.preprocessPattern(this.pattern) } + /** + * 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 if (!::pattern.isInitialized) { @@ -117,6 +129,12 @@ object CropHelper { return CornerPoints(upperLeftPoint, upperRightPoint, lowerRightPoint, lowerLeftPoint) } + /** + * 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, points: CornerPoints, diff --git a/app/src/main/java/com/k2_9/omrekap/utils/ImageAnnotationHelper.kt b/app/src/main/java/com/k2_9/omrekap/utils/ImageAnnotationHelper.kt index 7ae0919..5aef245 100644 --- a/app/src/main/java/com/k2_9/omrekap/utils/ImageAnnotationHelper.kt +++ b/app/src/main/java/com/k2_9/omrekap/utils/ImageAnnotationHelper.kt @@ -8,7 +8,16 @@ import org.opencv.core.Rect import org.opencv.core.Scalar import org.opencv.imgproc.Imgproc +/** + * Helper class for annotating image with detected objects + */ object ImageAnnotationHelper { + /** + * Annotate the corner points of a document + * @param img image to be annotated + * @param cornerPoints corner points of the document + * @return image with annotations + */ fun annotateCorner( img: Mat, cornerPoints: CornerPoints, @@ -21,6 +30,13 @@ object ImageAnnotationHelper { return imgWithAnnotations } + /** + * Annotate the detected AprilTag + * @param img image to be annotated + * @param cornerPoints corner points of the AprilTag + * @param id ID of the AprilTag + * @return image with annotations + */ fun annotateAprilTag( img: Mat, cornerPoints: List<Mat>, @@ -76,6 +92,13 @@ object ImageAnnotationHelper { return imgWithAnnotations } + /** + * Annotate the detected vote count in the image gained from template matching + * @param img image to be annotated + * @param cornerPoints corner points of the detected object + * @param contourNumber number of the detected object + * @return image with annotations + */ fun annotateTemplateMatchingOMR( img: Mat, cornerPoints: List<Rect>, @@ -99,6 +122,13 @@ object ImageAnnotationHelper { return imgWithAnnotations } + /** + * Annotate the detected vote count in the image gained from contour detection + * @param img image to be annotated + * @param cornerPoints corner points of the detected object + * @param contourNumber number of the detected object + * @return image with annotations + */ fun annotateContourOMR( img: Mat, cornerPoints: List<MatOfPoint>, @@ -121,6 +151,13 @@ object ImageAnnotationHelper { return imgWithAnnotations } + /** + * Annotate the detected OMR result in the image + * @param img image to be annotated + * @param section section of the detected OMR + * @param result result of the detected OMR + * @return image with annotations + */ fun annotateOMR( img: Mat, section: Rect, 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 index 2f39677..2e6590e 100644 --- a/app/src/main/java/com/k2_9/omrekap/utils/ImageSaveDataHolder.kt +++ b/app/src/main/java/com/k2_9/omrekap/utils/ImageSaveDataHolder.kt @@ -3,13 +3,24 @@ package com.k2_9.omrekap.utils import android.util.Log import com.k2_9.omrekap.data.models.ImageSaveData +/** + * Decorator for ImageSaveData + */ object ImageSaveDataHolder { private var imageSaveData: ImageSaveData? = null + /** + * Set the ImageSaveData + * @param data ImageSaveData to be saved + */ fun save(data: ImageSaveData) { imageSaveData = data } + /** + * Get the ImageSaveData + * @return ImageSaveData + */ fun get(): ImageSaveData { if (imageSaveData == null) { Log.e("ImageSaveDataHolder", "ImageSaveData is null") 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 index 70e9154..32384e6 100644 --- a/app/src/main/java/com/k2_9/omrekap/utils/PermissionHelper.kt +++ b/app/src/main/java/com/k2_9/omrekap/utils/PermissionHelper.kt @@ -6,7 +6,17 @@ import androidx.activity.result.contract.ActivityResultContracts import androidx.appcompat.app.AppCompatActivity import androidx.core.content.ContextCompat +/** + * Helper class for handling permissions + */ object PermissionHelper { + /** + * Request permission from the user + * @param activity activity context + * @param permission permission to be requested + * @param verbose show toast message if permission is denied + * @param operation operation to be executed if permission is granted + */ fun requirePermission( activity: AppCompatActivity, permission: String, 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 index 6858fea..cef1316 100644 --- a/app/src/main/java/com/k2_9/omrekap/utils/PreprocessHelper.kt +++ b/app/src/main/java/com/k2_9/omrekap/utils/PreprocessHelper.kt @@ -9,10 +9,18 @@ import org.opencv.core.Size import org.opencv.imgproc.Imgproc import java.time.Instant +/** + * Helper class for preprocessing image + */ object PreprocessHelper { private const val FINAL_WIDTH = 900.0 private const val FINAL_HEIGHT = 1600.0 + /** + * Preprocess the image data + * @param data image data to be preprocessed + * @return preprocessed image data + */ fun preprocessImage(data: ImageSaveData): ImageSaveData { // Initialize Mats val mainImageMat = Mat() 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 793eec7..e612364 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 @@ -19,7 +19,15 @@ import java.text.SimpleDateFormat import java.util.Date import java.util.Locale +/** + * Helper class for saving images and JSON data + */ object SaveHelper { + /** + * Save the image and JSON data to the device + * @param context the application context + * @param data the image and JSON data to be saved + */ suspend fun save( context: Context, data: ImageSaveData, @@ -48,6 +56,12 @@ object SaveHelper { } } + /** + * Convert the selected file URI to a Bitmap + * @param context the application context + * @param selectedFileUri the URI of the selected file + * @return the converted Bitmap + */ fun uriToBitmap( context: Context, selectedFileUri: Uri, @@ -59,11 +73,22 @@ object SaveHelper { return image } + /** + * Generate a folder name based on the current date and time + * @return the generated folder name + */ private fun generateFolderName(): String { val sdf = SimpleDateFormat("yyyyMMdd_HHmmss", Locale.getDefault()) return sdf.format(Date()) } + /** + * Save the image to the Documents/OMRekap/folderName directory + * @param context the application context + * @param image the image to be saved + * @param folderName the folder name where the image will be saved + * @param fileName the file name of the image + */ fun saveImage( context: Context, image: Bitmap, @@ -79,6 +104,13 @@ object SaveHelper { } } + /** + * Save the JSON to the Documents/OMRekap/folderName directory + * @param context the application context + * @param data the JSON data to be saved + * @param folderName the folder name where the JSON will be saved + * @param fileName the file name of the JSON + */ private fun saveJSON( context: Context, data: Map<String, Int?>, diff --git a/app/src/main/java/com/k2_9/omrekap/utils/omr/ContourOMRHelper.kt b/app/src/main/java/com/k2_9/omrekap/utils/omr/ContourOMRHelper.kt index fd04515..43fdba7 100644 --- a/app/src/main/java/com/k2_9/omrekap/utils/omr/ContourOMRHelper.kt +++ b/app/src/main/java/com/k2_9/omrekap/utils/omr/ContourOMRHelper.kt @@ -21,11 +21,21 @@ import kotlin.math.floor import kotlin.math.max import kotlin.math.sin +/** + * Helper for Optical Mark Recognition (OMR) using contours + * @param config configuration for the OMR helper + */ class ContourOMRHelper(private val config: ContourOMRHelperConfig) : OMRHelper(config) { private var currentSectionGray: Mat? = null private var currentSectionBinary: Mat? = null public var appContext: Context? = null + /** + * Create information object about the contour + * @param center center of the contour + * @param size size of the contour + * @return ContourInfo object + */ private fun createContourInfo(contour: Mat): ContourInfo { val rect = Imgproc.boundingRect(contour) val centerX = rect.x + rect.width / 2 @@ -33,6 +43,12 @@ class ContourOMRHelper(private val config: ContourOMRHelperConfig) : OMRHelper(c return ContourInfo(Pair(centerX, centerY), Pair(rect.width, rect.height)) } + /** + * Filter contours based on the intensities and return the filtered contour infos + * @param contourInfos list of contour infos + * @param intensities list of intensities + * @return filtered list of contour infos + */ private fun getContourInfo( filledContours: List<Mat>, filledIntensities: List<Int>, @@ -57,6 +73,11 @@ class ContourOMRHelper(private val config: ContourOMRHelperConfig) : OMRHelper(c return filterContourInfos(contourInfos, sortedIntensities.map { it.toDouble() }) } + /** + * Predict the number based on the detected filled circle contours + * @param contours list of filled circle contours + * @return predicted number + */ private fun predictForFilledCircle(contours: List<MatOfPoint>): Int { // Predict the number based on the filled circle contours @@ -97,6 +118,11 @@ class ContourOMRHelper(private val config: ContourOMRHelperConfig) : OMRHelper(c return contourInfosToNumbers(contourInfos) } + /** + * Get the darkest row in the column + * @param colContours list of contours in the column + * @return index of the darkest row + */ private fun getDarkestRow(colContours: List<MatOfPoint>): Int? { // Initialize variables to store the darkest row and its intensity var darkestRow: Int? = null @@ -137,6 +163,13 @@ class ContourOMRHelper(private val config: ContourOMRHelperConfig) : OMRHelper(c return darkestRow } + /** + * Create a perfect circle contour + * @param x x-coordinate of the center + * @param y y-coordinate of the center + * @param radius radius of the circle + * @return perfect circle contour + */ private fun getPerfectCircle(x: Double, y: Double, radius: Double): MatOfPoint { val numPoints = 100 // Adjust as needed val theta = DoubleArray(numPoints) { it * 2 * Math.PI / numPoints } @@ -151,6 +184,11 @@ class ContourOMRHelper(private val config: ContourOMRHelperConfig) : OMRHelper(c return circleContour } + /** + * Replace the contour with a perfect circle + * @param contour contour to be replaced + * @return perfect circle contour + */ private fun replaceWithPerfectCircle(contour: MatOfPoint): MatOfPoint { val rect = Imgproc.boundingRect(contour) val centroidX = rect.x + rect.width.toDouble() / 2 @@ -160,6 +198,11 @@ class ContourOMRHelper(private val config: ContourOMRHelperConfig) : OMRHelper(c return getPerfectCircle(centroidX, centroidY, radius) } + /** + * Get the combined number from the darkest rows of each column, given 10 contours for each column + * @param darkestRows list of 10 detected contours for each column + * @return combined number + */ private fun compareAll(contours: List<MatOfPoint>): Int { // Sort contours by column and then by row val contoursSorted = contours.sortedBy { Imgproc.boundingRect(it).x } @@ -189,6 +232,12 @@ class ContourOMRHelper(private val config: ContourOMRHelperConfig) : OMRHelper(c } return getCombinedNumbers(darkestRows.map { it ?: 0 }) } + + /** + * Complete missing contours by filling the missing circles + * @param contours list of detected contours + * @return list of completed contours + */ private fun completeMissingContours(contours: List<MatOfPoint>): List<MatOfPoint> { val sortedContours = contours.sortedBy { Imgproc.boundingRect(it).y } val columnMap = Array(3) { mutableListOf<MatOfPoint>() } @@ -283,6 +332,10 @@ class ContourOMRHelper(private val config: ContourOMRHelperConfig) : OMRHelper(c return result } + /** + * Detect the circles in the OMR section + * @return list of detected contours + */ private fun getAllContours(): List<MatOfPoint> { // Find circle contours in cropped OMR section val contours = mutableListOf<MatOfPoint>() @@ -341,6 +394,11 @@ class ContourOMRHelper(private val config: ContourOMRHelperConfig) : OMRHelper(c return filteredContours } + /** + * Detect the number for the OMR section + * @param contours list of detected contours + * @return detected number + */ override fun detect(section: OMRSection): Int { val omrSectionImage = config.omrCropper.crop(section) @@ -382,12 +440,20 @@ class ContourOMRHelper(private val config: ContourOMRHelperConfig) : OMRHelper(c } } - // Get Section Position For Annotating Purpose + /** + * Get the position of the OMR section + * @param section OMR section + * @return position of the OMR section + */ fun getSectionPosition(section: OMRSection): Rect { return config.omrCropper.sectionPosition(section) } - // Annotating Image For Testing Purpose + /** + * Annotate the image with the detected contour + * @param contourNumber detected contour number + * @return annotated image + */ fun annotateImage(contourNumber: Int): Bitmap { var annotatedImg = currentSectionGray!!.clone() val contours = getAllContours() diff --git a/app/src/main/java/com/k2_9/omrekap/utils/omr/OMRConfigDetector.kt b/app/src/main/java/com/k2_9/omrekap/utils/omr/OMRConfigDetector.kt index 81b7e87..e31612e 100644 --- a/app/src/main/java/com/k2_9/omrekap/utils/omr/OMRConfigDetector.kt +++ b/app/src/main/java/com/k2_9/omrekap/utils/omr/OMRConfigDetector.kt @@ -13,6 +13,9 @@ import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import org.opencv.core.Mat +/** + * Detector for OMR configuration + */ object OMRConfigDetector { private lateinit var loadedConfig: OMRBaseConfiguration private var job: Job? = null @@ -20,6 +23,8 @@ object OMRConfigDetector { /** * Initialize and load the detection configuration data. * Make sure to run this before detecting configurations + * @param context application context + * @throws Exception if failed to load the configuration */ fun loadConfiguration(context: Context) { if (!this::loadedConfig.isInitialized) { 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 index efdbb5e..adbf074 100644 --- 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 @@ -5,12 +5,29 @@ import com.k2_9.omrekap.data.configs.omr.OMRSection import kotlin.math.abs import kotlin.math.floor +/** + * Helper for Optical Mark Recognition (OMR) + * @param config configuration for the OMR helper + */ abstract class OMRHelper(private val config: OMRHelperConfig) { + /** + * Information about the contour + * @param center center of the contour + * @param size size of the contour + */ class ContourInfo(val center: Pair<Int, Int>, val size: Pair<Int, Int>) { + /** Check if the contour is overlapping with another contour + * @param other other contour to check + * @return true if the contour is overlapping with the other contour, false otherwise + */ fun isOverlapping(other: ContourInfo): Boolean { return isColumnOverlapping(other) && isRowOverlapping(other) } + /** Check if the contour is overlapping with another contour horizontally + * @param other other contour to check + * @return true if the contour is overlapping with the other contour horizontally, false otherwise + */ fun isColumnOverlapping(other: ContourInfo): Boolean { val x1 = center.first val x2 = other.center.first @@ -20,6 +37,10 @@ abstract class OMRHelper(private val config: OMRHelperConfig) { return abs(x1 - x2) * 2 < w1 + w2 } + /** Check if the contour is overlapping with another contour vertically + * @param other other contour to check + * @return true if the contour is overlapping with the other contour vertically, false otherwise + */ fun isRowOverlapping(other: ContourInfo): Boolean { val y1 = center.second val y2 = other.center.second @@ -30,13 +51,27 @@ abstract class OMRHelper(private val config: OMRHelperConfig) { } } + /** + * Error when detecting the filled circles + * @param message error message + */ class DetectionError(message: String) : Exception(message) + /** + * Combine the detected numbers into a single integer + * @param numbers list of detected numbers + * @return combined numbers + */ protected fun getCombinedNumbers(numbers: List<Int>): Int { // Combine the detected numbers into a single integer return numbers.joinToString("").toInt() } + /** + * Convert contour infos to numbers + * @param contourInfos list of contour infos + * @return detected numbers + */ protected fun contourInfosToNumbers(contourInfos: List<ContourInfo?>): Int { // Return the detected numbers based on the vertical position of the filled circles for each column if (contourInfos.size != 3) { @@ -62,6 +97,14 @@ abstract class OMRHelper(private val config: OMRHelperConfig) { return getCombinedNumbers(result) } + /** + * Filter contour infos: + * remove overlapping contour infos and choose the one with the highest intensity + * automatically assign null to the column with no filled circle + * @param contourInfos list of contour infos + * @param filledIntensities list of filled intensities + * @return filtered contour infos + */ protected fun filterContourInfos( contourInfos: List<ContourInfo>, filledIntensities: List<Double>, diff --git a/app/src/main/java/com/k2_9/omrekap/utils/omr/TemplateMatchingOMRHelper.kt b/app/src/main/java/com/k2_9/omrekap/utils/omr/TemplateMatchingOMRHelper.kt index d6a4814..1f5e795 100644 --- a/app/src/main/java/com/k2_9/omrekap/utils/omr/TemplateMatchingOMRHelper.kt +++ b/app/src/main/java/com/k2_9/omrekap/utils/omr/TemplateMatchingOMRHelper.kt @@ -10,11 +10,18 @@ import org.opencv.core.Point import org.opencv.core.Rect import org.opencv.imgproc.Imgproc +/** + * Helper for Optical Mark Recognition (OMR) using template matching + * @param config configuration for the OMR helper + */ class TemplateMatchingOMRHelper(private val config: TemplateMatchingOMRHelperConfig) : OMRHelper(config) { private var currentSectionGray: Mat? = null private var currentSectionBinary: Mat? = null + /** Get the rectangles of the matched template in the current section + * @return list of pairs of rectangles and their similarity scores + */ private fun getMatchRectangles(): List<Pair<Rect, Double>> { // Load the template image val template = config.template @@ -67,6 +74,10 @@ class TemplateMatchingOMRHelper(private val config: TemplateMatchingOMRHelperCon return matchedRectangles } + /** Get the contour information from the matched rectangles + * @param matchedRectangles list of pairs of rectangles and their similarity scores + * @return pair of list of contour information and list of similarity scores + */ private fun getContourInfos(matchedRectangles: List<Pair<Rect, Double>>): Pair<List<ContourInfo>, List<Double>> { // Initialize a set to keep track of added rectangles val addedRectangles = mutableSetOf<Rect>() @@ -115,6 +126,10 @@ class TemplateMatchingOMRHelper(private val config: TemplateMatchingOMRHelperCon return Pair(sortedContours, sortedSimilarities) } + /** Annotation for the image with the detected filled circles + * @param contourNumber detected number for the filled circles + * @return annotated image as Bitmap + */ fun annotateImage(contourNumber: Int): Bitmap { val annotatedImg = currentSectionGray!!.clone() val matchedRectangles = getMatchRectangles() @@ -136,6 +151,10 @@ class TemplateMatchingOMRHelper(private val config: TemplateMatchingOMRHelperCon return annotatedImageBitmap } + /** Detect the filled circles in the section + * @param section the OMR section to detect + * @return detected number for the filled circles + */ override fun detect(section: OMRSection): Int { val omrSectionImage = config.omrCropper.crop(section) -- GitLab