Browse Source

fix: implement bitmap memory management for CRITICAL-002

- Add proper bitmap cleanup in ScreenCaptureService.processImage()
- Fix bitmap leaks in convertImageToMat() with try-finally blocks
- Ensure bitmap.recycle() called in all exception paths
- Add safe cleanup in EnhancedOCR.performBasicOCR()
- Wrap OCR bitmap operations with proper resource management

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

Co-Authored-By: Claude <noreply@anthropic.com>
critical-002-bitmap-memory
Quildra 5 months ago
parent
commit
ca512afd4f
  1. 26
      app/src/main/java/com/quillstudios/pokegoalshelper/EnhancedOCR.kt
  2. 46
      app/src/main/java/com/quillstudios/pokegoalshelper/ScreenCaptureService.kt

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

@ -119,9 +119,13 @@ class EnhancedOCR(private val context: Context) {
} }
// Method 4: Class-specific OCR optimization // Method 4: Class-specific OCR optimization
val classOptimizedText = performClassSpecificOCR(croppedMat, detection.className) try {
if (classOptimizedText.isNotEmpty()) { val classOptimizedText = performClassSpecificOCR(croppedMat, detection.className)
candidates.add(Pair(classOptimizedText, 1.3f)) // Highest weight for class-specific if (classOptimizedText.isNotEmpty()) {
candidates.add(Pair(classOptimizedText, 1.3f)) // Highest weight for class-specific
}
} catch (e: Exception) {
Log.e(TAG, "❌ Error in class-specific OCR", e)
} }
} catch (e: Exception) { } catch (e: Exception) {
@ -193,21 +197,23 @@ class EnhancedOCR(private val context: Context) {
// - Firebase ML Text Recognition // - Firebase ML Text Recognition
// - Custom OCR model // - Custom OCR model
try { var bitmap: Bitmap? = null
return try {
// Convert Mat to Bitmap for OCR processing // Convert Mat to Bitmap for OCR processing
val bitmap = Bitmap.createBitmap(mat.cols(), mat.rows(), Bitmap.Config.ARGB_8888) bitmap = Bitmap.createBitmap(mat.cols(), mat.rows(), Bitmap.Config.ARGB_8888)
Utils.matToBitmap(mat, bitmap) Utils.matToBitmap(mat, bitmap)
// TODO: Integrate with actual OCR library // TODO: Integrate with actual OCR library
// For now, return a placeholder based on image properties // For now, return a placeholder based on image properties
val result = simulateOCRResult(bitmap) simulateOCRResult(bitmap)
bitmap.recycle()
return result
} catch (e: Exception) { } catch (e: Exception) {
Log.e(TAG, "❌ Error in basic OCR", e) Log.e(TAG, "❌ Error in basic OCR", e)
return "" ""
} finally {
// Always clean up bitmap
bitmap?.recycle()
} }
} }

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

@ -380,6 +380,9 @@ class ScreenCaptureService : Service() {
} }
private fun processImage(image: Image) { private fun processImage(image: Image) {
var bitmap: Bitmap? = null
var croppedBitmap: Bitmap? = null
try { try {
val planes = image.planes val planes = image.planes
val buffer = planes[0].buffer val buffer = planes[0].buffer
@ -391,7 +394,7 @@ class ScreenCaptureService : Service() {
Log.d(TAG, "🖼️ CAPTURE DEBUG: screenSize=${screenWidth}x${screenHeight}, expected bitmap=${screenWidth + rowPadding / pixelStride}x${screenHeight}") Log.d(TAG, "🖼️ CAPTURE DEBUG: screenSize=${screenWidth}x${screenHeight}, expected bitmap=${screenWidth + rowPadding / pixelStride}x${screenHeight}")
// Create bitmap from image // Create bitmap from image
val bitmap = Bitmap.createBitmap( bitmap = Bitmap.createBitmap(
screenWidth + rowPadding / pixelStride, screenWidth + rowPadding / pixelStride,
screenHeight, screenHeight,
Bitmap.Config.ARGB_8888 Bitmap.Config.ARGB_8888
@ -401,7 +404,7 @@ class ScreenCaptureService : Service() {
Log.d(TAG, "🖼️ CAPTURE DEBUG: created bitmap=${bitmap.width}x${bitmap.height}") Log.d(TAG, "🖼️ CAPTURE DEBUG: created bitmap=${bitmap.width}x${bitmap.height}")
// Convert to cropped bitmap if needed // Convert to cropped bitmap if needed
val croppedBitmap = if (rowPadding == 0) { croppedBitmap = if (rowPadding == 0) {
Log.d(TAG, "🖼️ CAPTURE DEBUG: No padding, using original bitmap") Log.d(TAG, "🖼️ CAPTURE DEBUG: No padding, using original bitmap")
bitmap bitmap
} else { } else {
@ -437,14 +440,14 @@ class ScreenCaptureService : Service() {
analyzePokemonScreen(mat) analyzePokemonScreen(mat)
} }
// Clean up
if (croppedBitmap != bitmap) {
croppedBitmap.recycle()
}
bitmap.recycle()
} catch (e: Exception) { } catch (e: Exception) {
Log.e(TAG, "Error processing image", e) Log.e(TAG, "Error processing image", e)
} finally {
// Always clean up bitmaps in finally block to prevent leaks
if (croppedBitmap != null && croppedBitmap != bitmap) {
croppedBitmap.recycle()
}
bitmap?.recycle()
} }
} }
@ -747,12 +750,16 @@ class ScreenCaptureService : Service() {
Utils.matToBitmap(processedRoi, bitmap) Utils.matToBitmap(processedRoi, bitmap)
// Use ML Kit OCR // Use ML Kit OCR
val extractedText = performOCR(bitmap, detection.className) val extractedText = try {
performOCR(bitmap, detection.className)
} finally {
// Always clean up bitmap after OCR
bitmap.recycle()
}
// Clean up // Clean up
roi.release() roi.release()
processedRoi.release() processedRoi.release()
bitmap.recycle()
if (extractedText != null) { if (extractedText != null) {
Log.i(TAG, "✅ YOLO SUCCESS: ${detection.className} = '$extractedText' (conf: ${String.format("%.2f", detection.confidence)})") Log.i(TAG, "✅ YOLO SUCCESS: ${detection.className} = '$extractedText' (conf: ${String.format("%.2f", detection.confidence)})")
@ -1149,6 +1156,10 @@ class ScreenCaptureService : Service() {
} }
private fun convertImageToMat(image: Image): Mat? { private fun convertImageToMat(image: Image): Mat? {
var bitmap: Bitmap? = null
var croppedBitmap: Bitmap? = null
var mat: Mat? = null
return try { return try {
val planes = image.planes val planes = image.planes
val buffer = planes[0].buffer val buffer = planes[0].buffer
@ -1157,7 +1168,7 @@ class ScreenCaptureService : Service() {
val rowPadding = rowStride - pixelStride * screenWidth val rowPadding = rowStride - pixelStride * screenWidth
// Create bitmap from image // Create bitmap from image
val bitmap = Bitmap.createBitmap( bitmap = Bitmap.createBitmap(
screenWidth + rowPadding / pixelStride, screenWidth + rowPadding / pixelStride,
screenHeight, screenHeight,
Bitmap.Config.ARGB_8888 Bitmap.Config.ARGB_8888
@ -1165,29 +1176,36 @@ class ScreenCaptureService : Service() {
bitmap.copyPixelsFromBuffer(buffer) bitmap.copyPixelsFromBuffer(buffer)
// Crop bitmap to remove padding if needed // Crop bitmap to remove padding if needed
val croppedBitmap = if (rowPadding == 0) { croppedBitmap = if (rowPadding == 0) {
bitmap bitmap
} else { } else {
val cropped = Bitmap.createBitmap(bitmap, 0, 0, screenWidth, screenHeight) val cropped = Bitmap.createBitmap(bitmap, 0, 0, screenWidth, screenHeight)
bitmap.recycle() // Clean up original bitmap.recycle() // Clean up original
bitmap = null // Mark as recycled
cropped cropped
} }
// Convert bitmap to Mat // Convert bitmap to Mat
val mat = Mat() mat = Mat()
Utils.bitmapToMat(croppedBitmap, mat) Utils.bitmapToMat(croppedBitmap, mat)
// Convert from RGBA to BGR (OpenCV format for proper color channel handling) // Convert from RGBA to BGR (OpenCV format for proper color channel handling)
val bgrMat = Mat() val bgrMat = Mat()
Imgproc.cvtColor(mat, bgrMat, Imgproc.COLOR_RGBA2BGR) Imgproc.cvtColor(mat, bgrMat, Imgproc.COLOR_RGBA2BGR)
// Clean up // Clean up intermediate resources
mat.release() mat.release()
croppedBitmap.recycle() croppedBitmap.recycle()
bgrMat bgrMat
} catch (e: Exception) { } catch (e: Exception) {
Log.e(TAG, "❌ Error converting image to Mat", e) Log.e(TAG, "❌ Error converting image to Mat", e)
// Clean up on error
mat?.release()
if (croppedBitmap != null && croppedBitmap != bitmap) {
croppedBitmap.recycle()
}
bitmap?.recycle()
null null
} }
} }

Loading…
Cancel
Save