diff --git a/app/src/main/java/com/quillstudios/pokegoalshelper/EnhancedOCR.kt b/app/src/main/java/com/quillstudios/pokegoalshelper/EnhancedOCR.kt index d894606..de27650 100644 --- a/app/src/main/java/com/quillstudios/pokegoalshelper/EnhancedOCR.kt +++ b/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 { diff --git a/app/src/main/java/com/quillstudios/pokegoalshelper/ScreenCaptureService.kt b/app/src/main/java/com/quillstudios/pokegoalshelper/ScreenCaptureService.kt index 6565670..50885d2 100644 --- a/app/src/main/java/com/quillstudios/pokegoalshelper/ScreenCaptureService.kt +++ b/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) { diff --git a/app/src/main/java/com/quillstudios/pokegoalshelper/utils/MatUtils.kt b/app/src/main/java/com/quillstudios/pokegoalshelper/utils/MatUtils.kt new file mode 100644 index 0000000..d1f31d9 --- /dev/null +++ b/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 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 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 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 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") + } + } +} \ No newline at end of file