Browse Source

fix: resolve critical OpenCV Mat memory leaks

- Create MatUtils.kt with safe resource management patterns
- Fix major Mat leak in ScreenCaptureService.kt image processing pipeline
- Fix Mat leaks in EnhancedOCR.kt enhanceImageForOCR() and upscaleImage()
- Add Mat.use() extension for automatic cleanup with try-finally
- Add useMats() utility for multi-Mat operations
- Add releaseSafely() for batch Mat cleanup with error handling

Critical stability fix: prevents native memory leaks that cause app crashes
during extended detection use. Mat objects now guaranteed to be released
even in exception paths.

Related: REFACTORING_TASKS.md CRITICAL-001

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
fix/critical-001-opencv-mat-memory-leaks
Quildra 5 months ago
parent
commit
d128de911d
  1. 82
      app/src/main/java/com/quillstudios/pokegoalshelper/EnhancedOCR.kt
  2. 46
      app/src/main/java/com/quillstudios/pokegoalshelper/ScreenCaptureService.kt
  3. 124
      app/src/main/java/com/quillstudios/pokegoalshelper/utils/MatUtils.kt

82
app/src/main/java/com/quillstudios/pokegoalshelper/EnhancedOCR.kt

@ -7,6 +7,8 @@ import android.util.Log
import org.opencv.android.Utils
import org.opencv.core.*
import org.opencv.imgproc.Imgproc
import com.quillstudios.pokegoalshelper.utils.MatUtils.use
import com.quillstudios.pokegoalshelper.utils.MatUtils.useMats
import kotlin.math.max
class EnhancedOCR(private val context: Context) {
@ -131,48 +133,56 @@ class EnhancedOCR(private val context: Context) {
}
private fun enhanceImageForOCR(mat: Mat): Mat {
val enhanced = Mat()
try {
// Convert to grayscale for better OCR
val gray = Mat()
if (mat.channels() == 3) {
Imgproc.cvtColor(mat, gray, Imgproc.COLOR_BGR2GRAY)
} else if (mat.channels() == 4) {
Imgproc.cvtColor(mat, gray, Imgproc.COLOR_BGRA2GRAY)
} else {
mat.copyTo(gray)
return useMats(Mat(), Mat(), Mat()) { enhanced, gray, blurred ->
try {
// Convert to grayscale for better OCR
if (mat.channels() == 3) {
Imgproc.cvtColor(mat, gray, Imgproc.COLOR_BGR2GRAY)
} else if (mat.channels() == 4) {
Imgproc.cvtColor(mat, gray, Imgproc.COLOR_BGRA2GRAY)
} else {
mat.copyTo(gray)
}
// Apply CLAHE for better contrast
val clahe = Imgproc.createCLAHE(2.0, Size(8.0, 8.0))
clahe.apply(gray, enhanced)
// Optional: Apply slight Gaussian blur to reduce noise
Imgproc.GaussianBlur(enhanced, blurred, Size(1.0, 1.0), 0.0)
// Return a safe copy that won't be auto-released
val result = Mat()
blurred.copyTo(result)
result
} catch (e: Exception) {
Log.e(TAG, "❌ Error enhancing image for OCR", e)
val fallback = Mat()
mat.copyTo(fallback)
fallback
}
// Apply CLAHE for better contrast
val clahe = Imgproc.createCLAHE(2.0, Size(8.0, 8.0))
clahe.apply(gray, enhanced)
// Optional: Apply slight Gaussian blur to reduce noise
val blurred = Mat()
Imgproc.GaussianBlur(enhanced, blurred, Size(1.0, 1.0), 0.0)
gray.release()
enhanced.release()
return blurred
} catch (e: Exception) {
Log.e(TAG, "❌ Error enhancing image for OCR", e)
mat.copyTo(enhanced)
return enhanced
}
}
private fun upscaleImage(mat: Mat): Mat {
val upscaled = Mat()
try {
val newSize = Size(mat.cols() * UPSCALE_FACTOR, mat.rows() * UPSCALE_FACTOR)
Imgproc.resize(mat, upscaled, newSize, 0.0, 0.0, Imgproc.INTER_CUBIC)
} catch (e: Exception) {
Log.e(TAG, "❌ Error upscaling image", e)
mat.copyTo(upscaled)
return Mat().use { upscaled ->
try {
val newSize = Size(mat.cols() * UPSCALE_FACTOR, mat.rows() * UPSCALE_FACTOR)
Imgproc.resize(mat, upscaled, newSize, 0.0, 0.0, Imgproc.INTER_CUBIC)
// Return a safe copy
val result = Mat()
upscaled.copyTo(result)
result
} catch (e: Exception) {
Log.e(TAG, "❌ Error upscaling image", e)
val fallback = Mat()
mat.copyTo(fallback)
fallback
}
}
return upscaled
}
private fun performBasicOCR(mat: Mat): String {

46
app/src/main/java/com/quillstudios/pokegoalshelper/ScreenCaptureService.kt

@ -28,6 +28,7 @@ import org.opencv.android.Utils
import org.opencv.core.*
import org.opencv.imgproc.Imgproc
import org.opencv.imgcodecs.Imgcodecs
import com.quillstudios.pokegoalshelper.utils.MatUtils.use
import com.google.mlkit.vision.common.InputImage
import com.google.mlkit.vision.text.TextRecognition
import com.google.mlkit.vision.text.latin.TextRecognizerOptions
@ -410,30 +411,31 @@ class ScreenCaptureService : Service() {
Log.d(TAG, "🖼️ CAPTURE DEBUG: final bitmap=${croppedBitmap.width}x${croppedBitmap.height}")
// Convert to OpenCV Mat for analysis
val mat = Mat()
Utils.bitmapToMat(croppedBitmap, mat)
// DEBUG: Check color conversion
Log.d(TAG, "🎨 COLOR DEBUG: Mat type=${mat.type()}, channels=${mat.channels()}")
Log.d(TAG, "🎨 COLOR DEBUG: OpenCV expects BGR, Android Bitmap is ARGB")
// Sample a center pixel to check color values
if (mat.rows() > 0 && mat.cols() > 0) {
val centerY = mat.rows() / 2
val centerX = mat.cols() / 2
val pixel = mat.get(centerY, centerX)
if (pixel != null && pixel.size >= 3) {
val b = pixel[0].toInt()
val g = pixel[1].toInt()
val r = pixel[2].toInt()
Log.d(TAG, "🎨 COLOR DEBUG: Center pixel (${centerX},${centerY}) BGR=($b,$g,$r) -> RGB=(${r},${g},${b})")
Log.d(TAG, "🎨 COLOR DEBUG: Center pixel hex = #${String.format("%02x%02x%02x", r, g, b)}")
// Convert to OpenCV Mat for analysis - with proper resource management
Mat().use { mat ->
Utils.bitmapToMat(croppedBitmap, mat)
// DEBUG: Check color conversion
Log.d(TAG, "🎨 COLOR DEBUG: Mat type=${mat.type()}, channels=${mat.channels()}")
Log.d(TAG, "🎨 COLOR DEBUG: OpenCV expects BGR, Android Bitmap is ARGB")
// Sample a center pixel to check color values
if (mat.rows() > 0 && mat.cols() > 0) {
val centerY = mat.rows() / 2
val centerX = mat.cols() / 2
val pixel = mat.get(centerY, centerX)
if (pixel != null && pixel.size >= 3) {
val b = pixel[0].toInt()
val g = pixel[1].toInt()
val r = pixel[2].toInt()
Log.d(TAG, "🎨 COLOR DEBUG: Center pixel (${centerX},${centerY}) BGR=($b,$g,$r) -> RGB=(${r},${g},${b})")
Log.d(TAG, "🎨 COLOR DEBUG: Center pixel hex = #${String.format("%02x%02x%02x", r, g, b)}")
}
}
}
// Run YOLO analysis
analyzePokemonScreen(mat)
// Run YOLO analysis
analyzePokemonScreen(mat)
}
// Clean up
if (croppedBitmap != bitmap) {

124
app/src/main/java/com/quillstudios/pokegoalshelper/utils/MatUtils.kt

@ -0,0 +1,124 @@
package com.quillstudios.pokegoalshelper.utils
import org.opencv.core.Mat
import android.util.Log
/**
* OpenCV Mat resource management utilities to prevent native memory leaks.
*
* Mat objects hold native memory that must be explicitly released to avoid memory leaks.
* These utilities provide safe patterns for Mat lifecycle management.
*/
object MatUtils {
private const val TAG = "MatUtils"
/**
* Execute a block with a Mat resource, ensuring it's properly released.
*
* Usage:
* ```kotlin
* val result = Mat().use { mat ->
* // Use mat safely
* processImage(mat)
* return@use someResult
* }
* ```
*/
fun <T> Mat.use(block: (Mat) -> T): T {
try {
return block(this)
} finally {
try {
this.release()
} catch (e: Exception) {
Log.w(TAG, "Warning: Error releasing Mat", e)
}
}
}
/**
* Execute a block with multiple Mat resources, ensuring all are properly released.
*
* Usage:
* ```kotlin
* val result = useMats(Mat(), Mat()) { mat1, mat2 ->
* // Use mats safely
* processImages(mat1, mat2)
* return@useMats someResult
* }
* ```
*/
fun <T> useMats(mat1: Mat, mat2: Mat, block: (Mat, Mat) -> T): T {
try {
return block(mat1, mat2)
} finally {
releaseSafely(mat1, mat2)
}
}
/**
* Execute a block with three Mat resources.
*/
fun <T> useMats(mat1: Mat, mat2: Mat, mat3: Mat, block: (Mat, Mat, Mat) -> T): T {
try {
return block(mat1, mat2, mat3)
} finally {
releaseSafely(mat1, mat2, mat3)
}
}
/**
* Safely release multiple Mat objects, logging any errors.
*/
fun releaseSafely(vararg mats: Mat) {
for (mat in mats) {
try {
mat.release()
} catch (e: Exception) {
Log.w(TAG, "Warning: Error releasing Mat", e)
}
}
}
/**
* Create a Mat with automatic resource management.
*
* Usage:
* ```kotlin
* val result = createMat { mat ->
* // mat is automatically released
* Utils.bitmapToMat(bitmap, mat)
* processImage(mat)
* }
* ```
*/
fun <T> createMat(block: (Mat) -> T): T {
return Mat().use(block)
}
/**
* Copy a Mat safely with automatic cleanup.
*/
fun Mat.safeCopy(): Mat {
val copy = Mat()
try {
this.copyTo(copy)
return copy
} catch (e: Exception) {
copy.release()
throw e
}
}
/**
* Debug helper to track Mat memory usage.
*/
fun logMatInfo(mat: Mat, label: String) {
try {
val bytes = mat.total() * mat.elemSize()
Log.d(TAG, "$label: Mat ${mat.cols()}x${mat.rows()}, ${mat.channels()} channels, ${bytes} bytes")
} catch (e: Exception) {
Log.d(TAG, "$label: Mat is empty or invalid")
}
}
}
Loading…
Cancel
Save