MazeHunter
For the Mobile Development semester in school I’ve worked on an android app. MazeHunter is a drawing game for children with dyslexia. It trains the brain hemispheres in a fun and playful way.
The app is played on a tablet, with a whiteboard in front of it to draw on. The tablet has a mirror in front of the camera so that it is able to capture the whiteboard.
The previous group of students used an image processing library to be able to detect the drawn shapes and cut them from the background.
I’ve worked on an app that is used test and tweak the image processing steps. Previously the code was stuffed in a single function. I’ve split the function up in separate steps and made the variables of each step modifiable in the app.
class OpenCVActivity : AppCompatActivity(), IManageVariables { private lateinit var binding: ActivityOpencvBinding private var selectedOperation: Int = 0 private var nOperations: Int = 0 private var variables = Array(7){ mutableMapOf<String, AppVariable>() } private var appVariables = arrayListOf<AppVariable>() private val appVariableAdapter = AppVariableAdapter(appVariables, this) private var images : ArrayList<Bitmap> = ArrayList() private var mats : ArrayList<Mat> = ArrayList() private var selectedImageIndex = 0 private lateinit var operationsDropdown: Spinner private lateinit var resetButton: Button private lateinit var applyButton: Button private lateinit var undoButton: Button private lateinit var rawInputImage: Bitmap private lateinit var currentImage: Mat override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) binding = ActivityOpencvBinding.inflate(layoutInflater) setContentView(binding.root) checkOpenCVLoaded() getInputImage() resetImage() //create example data createVariableData() // Set the adapter binding.rvAppVariables.layoutManager = LinearLayoutManager(this, RecyclerView.VERTICAL, false) binding.rvAppVariables.adapter = appVariableAdapter binding.rvAppVariables.addItemDecoration(DividerItemDecoration(this, DividerItemDecoration.VERTICAL)) setupDropDownMenu() bindButtons() addButtonListeners() updateAppVariable(1) } private fun checkOpenCVLoaded() { if(OpenCVLoader.initDebug()) Toast.makeText(applicationContext, "OpenCV loaded successfully", Toast.LENGTH_SHORT).show() else Toast.makeText(applicationContext, "Could not load OpenCV", Toast.LENGTH_SHORT).show() } private fun getInputImage() { var testDrawingResource = intent.getIntExtra("testDrawingResource", 0) if(testDrawingResource == 0) { Snackbar.make(binding.ivPreviewImage, "Couldn't load the selected test drawing", Snackbar.LENGTH_SHORT).show() finish() return } rawInputImage = BitmapFactory.decodeResource(resources, testDrawingResource) } private fun setupDropDownMenu() { operationsDropdown = binding.ddOperations var dropdownNames = arrayOf("0. Transform Perspective", "1. Grayscale", "2. Gaussian Blur", "3. Canny (Edge Detection)", "4. Morph", "5. Contour", "6. Crop") var adapter = ArrayAdapter(this, R.layout.support_simple_spinner_dropdown_item, dropdownNames) operationsDropdown.adapter = adapter nOperations = dropdownNames.size operationsDropdown.setOnItemSelectedListener(object : OnItemSelectedListener { override fun onItemSelected(adapterView: AdapterView<*>?, view: View?, i: Int, l: Long) { // Your code here updateSelectedOperation() updateAppVariable(selectedOperation) } override fun onNothingSelected(adapterView: AdapterView<*>?) { return } }) } private fun bindButtons() { resetButton = binding.btnPreview applyButton = binding.btnApply undoButton = binding.btnUndo } private fun updateAppVariable(index: Int) { appVariables.clear() if(variables[index].isNotEmpty()) appVariables.addAll(variables[index].values) appVariableAdapter.notifyDataSetChanged() } private fun addButtonListeners() { resetButton.setOnClickListener { resetImage() updateSelectedOperation() operationsDropdown.setSelection(0) } applyButton.setOnClickListener{ displayProcessedImage() storeProcessedImage() updateSelectedOperation() operationsDropdown.setSelection((selectedOperation+1) % nOperations) } undoButton.setOnClickListener{ undo() updateSelectedOperation() operationsDropdown.setSelection(selectedOperation-1 % nOperations) } } private fun resetImage() { images.clear() mats.clear() currentImage = Mat() bitmapToMat(rawInputImage, currentImage) images.add(rawInputImage) mats.add(currentImage) selectedImageIndex = 0 binding.ivPreviewImage.setImageBitmap(rawInputImage) } private fun displayProcessedImage() { updateSelectedOperation() when(selectedOperation){ 0 -> transformPerspective() 1 -> getGrayscaleImage() 2 -> getGaussianBlurImage( Size(variables[2]["width"]!!.value.toDouble(), variables[2]["height"]!!.value.toDouble()), variables[2]["sigmaX"]!!.value.toDouble(), variables[2]["sigmaY"]!!.value.toDouble()) 3 -> getCannyImage( variables[3]["cannyMinThreshold"]!!.value.toDouble(), variables[3]["ratio"]!!.value.toInt(), variables[3]["apertureSize"]!!.value.toInt()) 4 -> getMorphImage( variables[4]["dilateSize"]!!.value.toDouble(), variables[4]["dilateIterations"]!!.value.toInt(), variables[4]["erodeIterations"]!!.value.toInt(), variables[4]["dilatePointX"]!!.value.toDouble(), variables[4]["dilatePointY"]!!.value.toDouble(), variables[4]["erodePointX"]!!.value.toDouble(), variables[4]["erodePointY"]!!.value.toDouble()) 5 -> getContourImage() 6 -> getCroppedImage() else -> Log.d("error", "selectedOperation out of bounds") } binding.ivPreviewImage.setImageBitmap(images[images.size - 1]) } private fun updateSelectedOperation() { //get the name of the selected item in the dropdown menu val dropdownText: String = operationsDropdown.selectedItem.toString() Log.d("selectedOperation", dropdownText) //store the number(index) that's in front of the name selectedOperation = dropdownText[0].toString().toInt() } private fun storeProcessedImage() { selectedImageIndex++ currentImage = mats[mats.size - 1] } private fun undo() { if(selectedImageIndex == 0) { Snackbar.make(binding.ivPreviewImage, "Can't undo more steps", Snackbar.LENGTH_SHORT).show() return } selectedImageIndex-- mats.removeAt(mats.size - 1) images.removeAt(images.size - 1) currentImage = mats[mats.size - 1] binding.ivPreviewImage.setImageBitmap(images[images.size - 1]) } private fun createVariableData() { storeVariableData(2, AppVariable("width", 0f)) storeVariableData(2, AppVariable("height", 0f)) storeVariableData(2, AppVariable("sigmaX", 11f)) storeVariableData(2, AppVariable("sigmaY", 11f)) storeVariableData(3, AppVariable("cannyMinThreshold", 4.5f)) //Tussen 4 - 8 storeVariableData(3, AppVariable("ratio", 3f)) //recommended 2:1 and 3:1 storeVariableData(3, AppVariable("apertureSize", 3f)) storeVariableData(4, AppVariable("dilateSize", 10f)) storeVariableData(4, AppVariable("dilateIterations", 6f)) storeVariableData(4, AppVariable("erodeIterations", 6f)) storeVariableData(4, AppVariable("dilatePointX", -1f)) storeVariableData(4, AppVariable("dilatePointY", -1f)) storeVariableData(4, AppVariable("erodePointX", -1f)) storeVariableData(4, AppVariable("erodePointY", -1f)) } private fun storeVariableData(index: Int, variable: AppVariable) { variables[index][variable.name] = variable } //gets called from the Recyclerview override fun updateVariable(appVariable: AppVariable) { variables[selectedOperation][appVariable.name] = appVariable } private fun transformPerspective() { val imageHeight = rawInputImage.height val imageWidth = rawInputImage.width val mat = Mat() bitmapToMat(rawInputImage, mat) cvtColor(mat, mat, COLOR_RGB2RGBA) val bovekantPct = 39 // percentage vanaf boven wat eraf gaat val onderkantPct = 87 // percentage vanaf boven wat in beeld komt val breedtePct = 50 // Hoeveel de bovenkant smaller wordt vergeleken met de onderkant val linksRechtsPct = 13 // Het percentage wat er links en rechts af gaat val leftMargin = imageWidth * linksRechtsPct.toDouble() / 100 val srcWidth = imageWidth * (100 - linksRechtsPct.toDouble() * 2) / 100 val linksBovenX = (100-breedtePct).toDouble()/200 * srcWidth + leftMargin val rechtsBovenX = (imageWidth - linksBovenX) val bovenY = imageHeight.toDouble() * (bovekantPct.toDouble()/100) val onderY = imageHeight.toDouble() * (onderkantPct.toDouble()/100) val linksOnderX = leftMargin val rechtsOnderX = imageWidth - leftMargin val srcmat = MatOfPoint2f( Point(linksBovenX, bovenY), Point(rechtsBovenX, bovenY), Point(linksOnderX, onderY), Point(rechtsOnderX, onderY) ) //Input image val dstmat = MatOfPoint2f( Point(0.0, 0.0), Point(imageWidth.toDouble() - 1, 0.0), Point(0.0, imageHeight.toDouble() - 1), Point(imageWidth.toDouble() - 1, imageHeight.toDouble() - 1) ) val perspectiveTransform = getPerspectiveTransform(srcmat, dstmat) val dst = mat.clone() warpPerspective(mat, dst, perspectiveTransform, Size(imageWidth + 0.0, imageHeight + 0.0)) var outputImage = Bitmap.createBitmap(dst.cols(), dst.rows(), Bitmap.Config.ARGB_8888) matToBitmap(dst, outputImage) images.add(outputImage) mats.add(dst) } private fun getGrayscaleImage() { val inputMat = mats[mats.size - 1] cvtColor(inputMat, inputMat, COLOR_RGB2RGBA) //!grayscale val grayscaleMat = Mat() cvtColor(inputMat, grayscaleMat, COLOR_RGB2GRAY) //show grayscale image var grayscaleImage = Bitmap.createBitmap( grayscaleMat.cols(), grayscaleMat.rows(), Bitmap.Config.ARGB_8888 ) matToBitmap(grayscaleMat, grayscaleImage) images.add(grayscaleImage) mats.add(grayscaleMat) } private fun getGaussianBlurImage(point: Size, sigmaX: Double, sigmaY: Double) { val inputMat = mats[mats.size - 1] //!GaussianBlur val gaussianBlurMat = Mat() GaussianBlur(inputMat, gaussianBlurMat, point, sigmaX, sigmaY) //show gaussian blur image var gaussianImage = Bitmap.createBitmap( gaussianBlurMat.cols(), gaussianBlurMat.rows(), Bitmap.Config.ARGB_8888 ) matToBitmap(gaussianBlurMat, gaussianImage) images.add(gaussianImage) mats.add(gaussianBlurMat) } private fun getCannyImage(cannyMinThreshold: Double, ratio: Int, apertureSize: Int) { val inputMat = mats[mats.size - 1] //!Canny val cannyMat = Mat() inputMat.copyTo(cannyMat) Canny(cannyMat, cannyMat, cannyMinThreshold, cannyMinThreshold * ratio, apertureSize) //show canny mask image var cannyImage = Bitmap.createBitmap(cannyMat.cols(), cannyMat.rows(), Bitmap.Config.ARGB_8888) matToBitmap(cannyMat, cannyImage) images.add(cannyImage) mats.add(cannyMat) } private fun getMorphImage(dilateSize: Double, dilateIterations: Int, erodeIterations: Int, dilatePointX: Double, dilatePointY: Double, erodePointX: Double, erodePointY: Double) { val inputMat = mats[mats.size - 1] //!Morphological transformation val cannyMorphMat = Mat() inputMat.copyTo(cannyMorphMat) val morphKernel = getStructuringElement(MORPH_ELLIPSE, Size(dilateSize, dilateSize)) //groter maken van de lijnen/ vastmaken aan elkaar dilate(cannyMorphMat, cannyMorphMat, morphKernel, Point(dilatePointX, dilatePointY), dilateIterations) //"wegvreten" erode(cannyMorphMat, cannyMorphMat, morphKernel, Point(erodePointX, erodePointY), erodeIterations) //show morph var morphImage = Bitmap.createBitmap( cannyMorphMat.cols(), cannyMorphMat.rows(), Bitmap.Config.ARGB_8888 ) matToBitmap(cannyMorphMat, morphImage) images.add(morphImage) mats.add(cannyMorphMat) } private fun getContourImage() { val inputMat = mats[mats.size - 1] //!Colour in section var white : Scalar = Scalar(255.0, 255.0, 255.0) var contours: List<MatOfPoint> = ArrayList() var hierarchy = Mat() findContours(inputMat, contours, hierarchy, RETR_TREE, CHAIN_APPROX_SIMPLE) //filled = -1 //drawContours(cannyMorphMat, contours, -1, white, -1) //fill contours in with white for (contour in contours) fillPoly(inputMat, listOf(contour), white) //show contours var contourImage = Bitmap.createBitmap( inputMat.cols(), inputMat.rows(), Bitmap.Config.ARGB_8888 ) matToBitmap(inputMat, contourImage) images.add(contourImage) mats.add(inputMat) } private fun getCroppedImage() { val startingMat = mats[1] val inputMat = mats[mats.size - 1] //!cut out input image var mask : Mat = inputMat var transparentBackground : Scalar = Scalar(0.0, 0.0, 0.0, 0.0) var crop = Mat(startingMat.size(), startingMat.type(), transparentBackground) startingMat.copyTo(crop, mask) //show output image var outputImage = Bitmap.createBitmap(crop.cols(), crop.rows(), Bitmap.Config.ARGB_8888) matToBitmap(crop, outputImage) images.add(outputImage) mats.add(crop) } }