Browse Source

feat: implement centralized logging and standardized error handling

- Created PGHLog utility with "PGH - " prefix for easy log filtering
- Enhanced MLResult with specific error types (MLErrorType enum)
- Updated MLInferenceEngine interface to use MLResult pattern
- Converted all Log.* calls to PGHLog.* in core components
- Added type-specific error recovery strategies with onErrorType()
- Improved error propagation and debugging capabilities

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

Co-Authored-By: Claude <noreply@anthropic.com>
arch-002-ml-inference-engine
Quildra 5 months ago
parent
commit
1722c33842
  1. 33
      app/src/main/java/com/quillstudios/pokegoalshelper/MainActivity.kt
  2. 242
      app/src/main/java/com/quillstudios/pokegoalshelper/ScreenCaptureService.kt
  3. 36
      app/src/main/java/com/quillstudios/pokegoalshelper/controllers/DetectionController.kt
  4. 8
      app/src/main/java/com/quillstudios/pokegoalshelper/ml/MLInferenceEngine.kt
  5. 60
      app/src/main/java/com/quillstudios/pokegoalshelper/ml/MLResult.kt
  6. 123
      app/src/main/java/com/quillstudios/pokegoalshelper/ml/YOLOInferenceEngine.kt
  7. 132
      app/src/main/java/com/quillstudios/pokegoalshelper/utils/PGHLog.kt

33
app/src/main/java/com/quillstudios/pokegoalshelper/MainActivity.kt

@ -11,6 +11,7 @@ import android.os.Build
import android.os.Bundle
import android.provider.Settings
import android.util.Log
import com.quillstudios.pokegoalshelper.utils.PGHLog
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import androidx.activity.ComponentActivity
@ -43,7 +44,7 @@ class MainActivity : ComponentActivity() {
private val screenCapturePermissionLauncher = registerForActivityResult(
ActivityResultContracts.StartActivityForResult()
) { result ->
Log.d(TAG, "Screen capture permission result: ${result.resultCode}")
PGHLog.d(TAG, "Screen capture permission result: ${result.resultCode}")
if (result.resultCode == Activity.RESULT_OK) {
val data = result.data
if (data != null) {
@ -51,41 +52,41 @@ class MainActivity : ComponentActivity() {
startScreenCaptureService(data)
// FloatingUI now handled by service overlay
isCapturing = true
Log.d(TAG, "Screen capture service started successfully")
PGHLog.d(TAG, "Screen capture service started successfully")
} catch (e: Exception) {
Log.e(TAG, "Failed to start screen capture service", e)
PGHLog.e(TAG, "Failed to start screen capture service", e)
isCapturing = false
}
} else {
Log.e(TAG, "Screen capture permission granted but no data received")
PGHLog.e(TAG, "Screen capture permission granted but no data received")
}
} else {
Log.e(TAG, "Screen capture permission denied with result code: ${result.resultCode}")
PGHLog.e(TAG, "Screen capture permission denied with result code: ${result.resultCode}")
}
}
private fun initializeOpenCV() {
if (OpenCVLoader.initLocal()) {
Log.d(TAG, "OpenCV loaded successfully")
PGHLog.d(TAG, "OpenCV loaded successfully")
// Test OpenCV
val testMat = Mat(100, 100, CvType.CV_8UC3)
Log.d(TAG, "Mat created: ${testMat.rows()}x${testMat.cols()}")
PGHLog.d(TAG, "Mat created: ${testMat.rows()}x${testMat.cols()}")
// Initialize ONNX YOLO detector for testing - now using MLInferenceEngine in service
// yoloDetector = YOLOOnnxDetector(this)
// if (yoloDetector!!.initialize()) {
// Log.d(TAG, "✅ ONNX YOLO detector initialized successfully")
// PGHLog.d(TAG, "✅ ONNX YOLO detector initialized successfully")
// } else {
// Log.e(TAG, "❌ ONNX YOLO detector initialization failed")
// PGHLog.e(TAG, "❌ ONNX YOLO detector initialization failed")
// }
Log.d(TAG, "✅ Using new MLInferenceEngine architecture in ScreenCaptureService")
PGHLog.d(TAG, "✅ Using new MLInferenceEngine architecture in ScreenCaptureService")
} else {
Log.e(TAG, "OpenCV initialization failed")
PGHLog.e(TAG, "OpenCV initialization failed")
}
}
private fun testYOLODetection() {
Log.i(TAG, "🧪 YOLO testing now handled by MLInferenceEngine in ScreenCaptureService")
PGHLog.i(TAG, "🧪 YOLO testing now handled by MLInferenceEngine in ScreenCaptureService")
// yoloDetector?.testWithStaticImage()
}
@ -121,10 +122,10 @@ class MainActivity : ComponentActivity() {
ActivityResultContracts.RequestPermission()
) { isGranted ->
if (isGranted) {
Log.d(TAG, "Notification permission granted, checking overlay permission")
PGHLog.d(TAG, "Notification permission granted, checking overlay permission")
requestScreenCapturePermission() // Re-check other permissions
} else {
Log.e(TAG, "Notification permission denied")
PGHLog.e(TAG, "Notification permission denied")
}
}
@ -133,10 +134,10 @@ class MainActivity : ComponentActivity() {
) { _ ->
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
if (Settings.canDrawOverlays(this)) {
Log.d(TAG, "Overlay permission granted, proceeding with screen capture")
PGHLog.d(TAG, "Overlay permission granted, proceeding with screen capture")
proceedWithScreenCapture()
} else {
Log.e(TAG, "Overlay permission denied")
PGHLog.e(TAG, "Overlay permission denied")
}
}
}

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

@ -16,6 +16,8 @@ import android.media.projection.MediaProjectionManager
import android.os.*
import android.util.DisplayMetrics
import android.util.Log
import com.quillstudios.pokegoalshelper.utils.PGHLog
import com.quillstudios.pokegoalshelper.ml.MLErrorType
import android.view.WindowManager
import android.view.View
import android.view.Gravity
@ -148,11 +150,13 @@ class ScreenCaptureService : Service() {
mlInferenceEngine = YOLOInferenceEngine(this)
Thread {
runBlocking {
if (!mlInferenceEngine!!.initialize()) {
Log.e(TAG, "❌ Failed to initialize ML inference engine")
} else {
Log.i(TAG, "✅ ML inference engine initialized for screen capture")
}
mlInferenceEngine!!.initialize()
.onSuccess {
PGHLog.i(TAG, "✅ ML inference engine initialized for screen capture")
}
.onError { errorType, exception, message ->
PGHLog.e(TAG, "❌ Failed to initialize ML inference engine: $message (${errorType.name})", exception)
}
}
}.start()
@ -169,7 +173,7 @@ class ScreenCaptureService : Service() {
onClose = { stopSelf() }
)
Log.d(TAG, "✅ MVC architecture initialized")
PGHLog.d(TAG, "✅ MVC architecture initialized")
}
override fun onBind(intent: Intent?): IBinder = binder
@ -238,7 +242,7 @@ class ScreenCaptureService : Service() {
}
private fun startScreenCapture(resultData: Intent) {
Log.d(TAG, "Starting screen capture")
PGHLog.d(TAG, "Starting screen capture")
try {
val notification = NotificationCompat.Builder(this, CHANNEL_ID)
@ -260,28 +264,28 @@ class ScreenCaptureService : Service() {
)
.build()
Log.d(TAG, "Starting foreground service")
PGHLog.d(TAG, "Starting foreground service")
startForeground(NOTIFICATION_ID, notification)
// Use the screen capture manager to start capture
if (!screenCaptureManager.startCapture(resultData)) {
Log.e(TAG, "Failed to start screen capture via manager")
PGHLog.e(TAG, "Failed to start screen capture via manager")
stopSelf()
return
}
Log.d(TAG, "Screen capture setup complete")
PGHLog.d(TAG, "Screen capture setup complete")
// Show floating overlay
enhancedFloatingFAB?.show()
} catch (e: Exception) {
Log.e(TAG, "Error starting screen capture", e)
PGHLog.e(TAG, "Error starting screen capture", e)
stopSelf()
}
}
private fun stopScreenCapture() {
Log.d(TAG, "Stopping screen capture")
PGHLog.d(TAG, "Stopping screen capture")
handler.removeCallbacks(captureRunnable)
hideDetectionOverlay()
@ -311,7 +315,7 @@ class ScreenCaptureService : Service() {
// Don't close the image yet - it will be closed in triggerManualDetection
}
} catch (e: Exception) {
Log.e(TAG, "Error handling captured image", e)
PGHLog.e(TAG, "Error handling captured image", e)
}
}
@ -320,7 +324,7 @@ class ScreenCaptureService : Service() {
private fun captureScreen() {
// Trigger image capture by reading from the ImageReader
// The onImageAvailableListener will handle the actual processing
Log.d(TAG, "Triggering screen capture...")
PGHLog.d(TAG, "Triggering screen capture...")
}
private fun processImage(image: Image) {
@ -331,7 +335,7 @@ class ScreenCaptureService : Service() {
// Get screen dimensions from the manager
val screenDimensions = screenCaptureManager.getScreenDimensions()
if (screenDimensions == null) {
Log.e(TAG, "Screen dimensions not available from manager")
PGHLog.e(TAG, "Screen dimensions not available from manager")
return
}
val (screenWidth, screenHeight) = screenDimensions
@ -342,8 +346,8 @@ class ScreenCaptureService : Service() {
val rowStride = planes[0].rowStride
val rowPadding = rowStride - pixelStride * screenWidth
Log.d(TAG, "🖼️ CAPTURE DEBUG: pixelStride=$pixelStride, rowStride=$rowStride, rowPadding=$rowPadding")
Log.d(TAG, "🖼️ CAPTURE DEBUG: screenSize=${screenWidth}x${screenHeight}, expected bitmap=${screenWidth + rowPadding / pixelStride}x${screenHeight}")
PGHLog.d(TAG, "🖼️ CAPTURE DEBUG: pixelStride=$pixelStride, rowStride=$rowStride, rowPadding=$rowPadding")
PGHLog.d(TAG, "🖼️ CAPTURE DEBUG: screenSize=${screenWidth}x${screenHeight}, expected bitmap=${screenWidth + rowPadding / pixelStride}x${screenHeight}")
// Create bitmap from image
bitmap = Bitmap.createBitmap(
@ -353,26 +357,26 @@ class ScreenCaptureService : Service() {
)
bitmap.copyPixelsFromBuffer(buffer)
Log.d(TAG, "🖼️ CAPTURE DEBUG: created bitmap=${bitmap.width}x${bitmap.height}")
PGHLog.d(TAG, "🖼️ CAPTURE DEBUG: created bitmap=${bitmap.width}x${bitmap.height}")
// Convert to cropped bitmap if needed
croppedBitmap = if (rowPadding == 0) {
Log.d(TAG, "🖼️ CAPTURE DEBUG: No padding, using original bitmap")
PGHLog.d(TAG, "🖼️ CAPTURE DEBUG: No padding, using original bitmap")
bitmap
} else {
Log.d(TAG, "🖼️ CAPTURE DEBUG: Cropping bitmap from ${bitmap.width}x${bitmap.height} to ${screenWidth}x${screenHeight}")
PGHLog.d(TAG, "🖼️ CAPTURE DEBUG: Cropping bitmap from ${bitmap.width}x${bitmap.height} to ${screenWidth}x${screenHeight}")
Bitmap.createBitmap(bitmap, 0, 0, screenWidth, screenHeight)
}
Log.d(TAG, "🖼️ CAPTURE DEBUG: final bitmap=${croppedBitmap.width}x${croppedBitmap.height}")
PGHLog.d(TAG, "🖼️ CAPTURE DEBUG: final bitmap=${croppedBitmap.width}x${croppedBitmap.height}")
// 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")
PGHLog.d(TAG, "🎨 COLOR DEBUG: Mat type=${mat.type()}, channels=${mat.channels()}")
PGHLog.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) {
@ -383,8 +387,8 @@ class ScreenCaptureService : Service() {
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)}")
PGHLog.d(TAG, "🎨 COLOR DEBUG: Center pixel (${centerX},${centerY}) BGR=($b,$g,$r) -> RGB=(${r},${g},${b})")
PGHLog.d(TAG, "🎨 COLOR DEBUG: Center pixel hex = #${String.format("%02x%02x%02x", r, g, b)}")
}
}
@ -393,7 +397,7 @@ class ScreenCaptureService : Service() {
}
} catch (e: Exception) {
Log.e(TAG, "Error processing image", e)
PGHLog.e(TAG, "Error processing image", e)
} finally {
// Always clean up bitmaps in finally block to prevent leaks
if (croppedBitmap != null && croppedBitmap != bitmap) {
@ -404,24 +408,24 @@ class ScreenCaptureService : Service() {
}
private fun analyzePokemonScreen(mat: Mat) {
Log.i(TAG, "📱 ANALYZING SCREEN: ${mat.cols()}x${mat.rows()}")
PGHLog.i(TAG, "📱 ANALYZING SCREEN: ${mat.cols()}x${mat.rows()}")
// Check if analysis has been stuck for too long (30 seconds max)
val currentTime = System.currentTimeMillis()
if (isAnalyzing && (currentTime - analysisStartTime) > 30000) {
Log.w(TAG, "⚠️ Analysis stuck for >30s, resetting flag")
PGHLog.w(TAG, "⚠️ Analysis stuck for >30s, resetting flag")
isAnalyzing = false
}
// Skip if already analyzing
if (isAnalyzing) {
Log.d(TAG, "⏭️ Skipping analysis - previous cycle still in progress")
PGHLog.d(TAG, "⏭️ Skipping analysis - previous cycle still in progress")
return
}
isAnalyzing = true
analysisStartTime = currentTime
Log.d(TAG, "🔄 Starting new analysis cycle")
PGHLog.d(TAG, "🔄 Starting new analysis cycle")
try {
// Run ML inference first
@ -429,25 +433,35 @@ class ScreenCaptureService : Service() {
Utils.matToBitmap(mat, bitmap)
val detections: List<MLDetection> = runBlocking {
mlInferenceEngine?.detect(bitmap) ?: emptyList()
mlInferenceEngine?.detect(bitmap)
?.onErrorType(MLErrorType.MEMORY_ERROR) { exception, message ->
PGHLog.w(TAG, "⚠️ Memory error during detection, may retry with smaller image: $message")
}
?.onErrorType(MLErrorType.TIMEOUT_ERROR) { exception, message ->
PGHLog.w(TAG, "⚠️ Detection timed out, analysis may be too complex: $message")
}
?.onErrorType(MLErrorType.INVALID_INPUT) { exception, message ->
PGHLog.e(TAG, "❌ Invalid input to ML engine: $message")
}
?.getOrDefault(emptyList()) ?: emptyList()
}
if (detections.isEmpty()) {
Log.i(TAG, "🔍 No Pokemon UI elements detected by ONNX YOLO")
PGHLog.i(TAG, "🔍 No Pokemon UI elements detected by ONNX YOLO")
isAnalyzing = false
return
}
Log.i(TAG, "🎯 ONNX YOLO detected ${detections.size} UI elements")
PGHLog.i(TAG, "🎯 ONNX YOLO detected ${detections.size} UI elements")
// Log ALL detections for debugging
detections.forEachIndexed { index, detection ->
Log.i(TAG, " $index: ${detection.className} (${String.format("%.3f", detection.confidence)}) at [${detection.boundingBox.left}, ${detection.boundingBox.top}, ${detection.boundingBox.width}, ${detection.boundingBox.height}]")
PGHLog.i(TAG, " $index: ${detection.className} (${String.format("%.3f", detection.confidence)}) at [${detection.boundingBox.left}, ${detection.boundingBox.top}, ${detection.boundingBox.width}, ${detection.boundingBox.height}]")
}
// Show breakdown by type
val detectionCounts = detections.groupBy { it.className }.mapValues { it.value.size }
Log.i(TAG, "🔍 Detection counts by type: $detectionCounts")
PGHLog.i(TAG, "🔍 Detection counts by type: $detectionCounts")
// Check for commonly missing elements
val expectedElements = listOf("pokemon_level", "attack_value", "sp_def_value", "shiny_icon",
@ -456,18 +470,18 @@ class ScreenCaptureService : Service() {
detections.none { detection -> detection.className.startsWith(expected.split("_").take(2).joinToString("_")) }
}
if (missingElements.isNotEmpty()) {
Log.w(TAG, "⚠️ Missing expected elements: $missingElements")
PGHLog.w(TAG, "⚠️ Missing expected elements: $missingElements")
}
// Show detection overlay IMMEDIATELY (no OCR blocking)
showYOLODetectionOverlay(detections)
Log.i(TAG, "📺 Overlay displayed with ${detections.size} detections")
PGHLog.i(TAG, "📺 Overlay displayed with ${detections.size} detections")
// Extract Pokemon info using YOLO detections in background
extractPokemonInfoFromYOLOAsync(mat, detections)
} catch (e: Exception) {
Log.e(TAG, "Error analyzing Pokemon screen", e)
PGHLog.e(TAG, "Error analyzing Pokemon screen", e)
isAnalyzing = false
}
}
@ -486,18 +500,18 @@ class ScreenCaptureService : Service() {
handler.post {
try {
if (pokemonInfo != null) {
Log.i(TAG, "🔥 POKEMON DATA EXTRACTED SUCCESSFULLY!")
PGHLog.i(TAG, "🔥 POKEMON DATA EXTRACTED SUCCESSFULLY!")
logPokemonInfo(pokemonInfo)
// TODO: Send to your API
// sendToAPI(pokemonInfo)
} else {
Log.i(TAG, "❌ Could not extract complete Pokemon info")
PGHLog.i(TAG, "❌ Could not extract complete Pokemon info")
}
} finally {
// Analysis cycle complete, allow next one
isAnalyzing = false
val duration = System.currentTimeMillis() - analysisStartTime
Log.d(TAG, "✅ Analysis cycle complete after ${duration}ms - ready for next")
PGHLog.d(TAG, "✅ Analysis cycle complete after ${duration}ms - ready for next")
}
}
@ -505,13 +519,13 @@ class ScreenCaptureService : Service() {
matCopy.release()
} catch (e: Exception) {
Log.e(TAG, "Error in async Pokemon extraction", e)
PGHLog.e(TAG, "Error in async Pokemon extraction", e)
matCopy.release()
// Clear flag on error too
handler.post {
isAnalyzing = false
Log.d(TAG, "❌ Analysis cycle failed - ready for next")
PGHLog.d(TAG, "❌ Analysis cycle failed - ready for next")
}
}
}
@ -519,7 +533,7 @@ class ScreenCaptureService : Service() {
private fun extractPokemonInfoFromYOLO(mat: Mat, detections: List<MLDetection>): PokemonInfo? {
try {
Log.i(TAG, "🎯 Extracting Pokemon info from ${detections.size} YOLO detections")
PGHLog.i(TAG, "🎯 Extracting Pokemon info from ${detections.size} YOLO detections")
// Group detections by type
val detectionMap = detections.groupBy { it.className }
@ -542,7 +556,7 @@ class ScreenCaptureService : Service() {
// Wait for all OCR tasks to complete (max 10 seconds total)
val completed = latch.await(10, TimeUnit.SECONDS)
if (!completed) {
Log.w(TAG, "⏱️ Some OCR tasks timed out after 10 seconds")
PGHLog.w(TAG, "⏱️ Some OCR tasks timed out after 10 seconds")
}
// Extract results
@ -579,20 +593,20 @@ class ScreenCaptureService : Service() {
// Detect tera type
val teraType = detectTeraTypeFromDetections(detectionMap)
Log.i(TAG, "📊 YOLO extraction summary:")
Log.i(TAG, " Nickname: '${nickname ?: "null"}'")
Log.i(TAG, " Level: ${level ?: "null"}")
Log.i(TAG, " Species: '${species ?: "null"}'")
Log.i(TAG, " Nature: '${nature ?: "null"}'")
Log.i(TAG, " Ability: '${ability ?: "null"}'")
Log.i(TAG, " Gender: '${gender ?: "null"}'")
Log.i(TAG, " Pokeball: '${pokeballType ?: "null"}'")
Log.i(TAG, " Types: ${types}")
Log.i(TAG, " Tera: '${teraType ?: "null"}'")
Log.i(TAG, " Shiny: $isShiny")
PGHLog.i(TAG, "📊 YOLO extraction summary:")
PGHLog.i(TAG, " Nickname: '${nickname ?: "null"}'")
PGHLog.i(TAG, " Level: ${level ?: "null"}")
PGHLog.i(TAG, " Species: '${species ?: "null"}'")
PGHLog.i(TAG, " Nature: '${nature ?: "null"}'")
PGHLog.i(TAG, " Ability: '${ability ?: "null"}'")
PGHLog.i(TAG, " Gender: '${gender ?: "null"}'")
PGHLog.i(TAG, " Pokeball: '${pokeballType ?: "null"}'")
PGHLog.i(TAG, " Types: ${types}")
PGHLog.i(TAG, " Tera: '${teraType ?: "null"}'")
PGHLog.i(TAG, " Shiny: $isShiny")
if (nickname.isNullOrBlank() && species.isNullOrBlank() && level == null) {
Log.w(TAG, "⚠️ No essential Pokemon data found with YOLO detection")
PGHLog.w(TAG, "⚠️ No essential Pokemon data found with YOLO detection")
return null
}
@ -621,7 +635,7 @@ class ScreenCaptureService : Service() {
)
} catch (e: Exception) {
Log.e(TAG, "Error extracting Pokemon info from YOLO detections", e)
PGHLog.e(TAG, "Error extracting Pokemon info from YOLO detections", e)
return null
}
}
@ -634,7 +648,7 @@ class ScreenCaptureService : Service() {
results[key] = text
}
} catch (e: Exception) {
Log.e(TAG, "Error in OCR task for $key", e)
PGHLog.e(TAG, "Error in OCR task for $key", e)
synchronized(results) {
results[key] = null
}
@ -653,7 +667,7 @@ class ScreenCaptureService : Service() {
results[key] = level
}
} catch (e: Exception) {
Log.e(TAG, "Error in level OCR task", e)
PGHLog.e(TAG, "Error in level OCR task", e)
synchronized(results) {
results[key] = null
}
@ -689,9 +703,9 @@ class ScreenCaptureService : Service() {
val safeBbox = Rect(clippedX, clippedY, clippedWidth, clippedHeight)
// Debug logging for bounding box transformations
Log.d(TAG, "📏 Expanded bbox for ${detection.className}: [${bbox.left},${bbox.top},${bbox.width},${bbox.height}] → [${expandedBbox.x},${expandedBbox.y},${expandedBbox.width},${expandedBbox.height}]")
PGHLog.d(TAG, "📏 Expanded bbox for ${detection.className}: [${bbox.left},${bbox.top},${bbox.width},${bbox.height}] → [${expandedBbox.x},${expandedBbox.y},${expandedBbox.width},${expandedBbox.height}]")
if (safeBbox.x != expandedBbox.x || safeBbox.y != expandedBbox.y || safeBbox.width != expandedBbox.width || safeBbox.height != expandedBbox.height) {
Log.w(TAG, "⚠️ Clipped bbox for ${detection.className}: expanded=[${expandedBbox.x},${expandedBbox.y},${expandedBbox.width},${expandedBbox.height}] → safe=[${safeBbox.x},${safeBbox.y},${safeBbox.width},${safeBbox.height}] (image: ${mat.cols()}x${mat.rows()})")
PGHLog.w(TAG, "⚠️ Clipped bbox for ${detection.className}: expanded=[${expandedBbox.x},${expandedBbox.y},${expandedBbox.width},${expandedBbox.height}] → safe=[${safeBbox.x},${safeBbox.y},${safeBbox.width},${safeBbox.height}] (image: ${mat.cols()}x${mat.rows()})")
}
// Extract region of interest using safe bounding box
@ -717,14 +731,14 @@ class ScreenCaptureService : Service() {
processedRoi.release()
if (extractedText != null) {
Log.i(TAG, "✅ YOLO SUCCESS: ${detection.className} = '$extractedText' (conf: ${String.format("%.2f", detection.confidence)})")
PGHLog.i(TAG, "✅ YOLO SUCCESS: ${detection.className} = '$extractedText' (conf: ${String.format("%.2f", detection.confidence)})")
} else {
Log.w(TAG, "❌ YOLO FAILED: ${detection.className} - no text found (conf: ${String.format("%.2f", detection.confidence)})")
PGHLog.w(TAG, "❌ YOLO FAILED: ${detection.className} - no text found (conf: ${String.format("%.2f", detection.confidence)})")
}
return extractedText
} catch (e: Exception) {
Log.e(TAG, "Error extracting text from YOLO detection ${detection.className}", e)
PGHLog.e(TAG, "Error extracting text from YOLO detection ${detection.className}", e)
return null
}
}
@ -874,10 +888,10 @@ class ScreenCaptureService : Service() {
private fun showYOLODetectionOverlay(detections: List<MLDetection>) {
try {
Log.i(TAG, "🎨 Creating YOLO detection overlay for ${detections.size} detections")
PGHLog.i(TAG, "🎨 Creating YOLO detection overlay for ${detections.size} detections")
if (detectionOverlay == null) {
Log.i(TAG, "🆕 Creating new DetectionOverlay instance")
PGHLog.i(TAG, "🆕 Creating new DetectionOverlay instance")
detectionOverlay = DetectionOverlay(this)
}
@ -893,12 +907,12 @@ class ScreenCaptureService : Service() {
)
}.toMap()
Log.i(TAG, "📺 Showing YOLO overlay with ${regions.size} regions...")
PGHLog.i(TAG, "📺 Showing YOLO overlay with ${regions.size} regions...")
detectionOverlay?.showOverlay(regions)
Log.i(TAG, "✅ YOLO overlay show command sent")
PGHLog.i(TAG, "✅ YOLO overlay show command sent")
} catch (e: Exception) {
Log.e(TAG, "❌ Error showing YOLO detection overlay", e)
PGHLog.e(TAG, "❌ Error showing YOLO detection overlay", e)
}
}
@ -908,7 +922,7 @@ class ScreenCaptureService : Service() {
private fun performOCR(bitmap: Bitmap, purpose: String): String? {
try {
Log.d(TAG, "🔍 Starting OCR for $purpose - bitmap: ${bitmap.width}x${bitmap.height}")
PGHLog.d(TAG, "🔍 Starting OCR for $purpose - bitmap: ${bitmap.width}x${bitmap.height}")
// Create InputImage for ML Kit
val image = InputImage.fromBitmap(bitmap, 0)
@ -922,18 +936,18 @@ class ScreenCaptureService : Service() {
recognizer.process(image)
.addOnSuccessListener { visionText ->
result = visionText.text.trim()
Log.d(TAG, "🔍 Raw OCR result for $purpose: '${visionText.text}' (blocks: ${visionText.textBlocks.size})")
PGHLog.d(TAG, "🔍 Raw OCR result for $purpose: '${visionText.text}' (blocks: ${visionText.textBlocks.size})")
if (result.isNullOrBlank()) {
Log.w(TAG, "⚠️ OCR for $purpose: NO TEXT DETECTED - ML Kit found ${visionText.textBlocks.size} text blocks but text is empty")
PGHLog.w(TAG, "⚠️ OCR for $purpose: NO TEXT DETECTED - ML Kit found ${visionText.textBlocks.size} text blocks but text is empty")
} else {
Log.i(TAG, "✅ OCR SUCCESS for $purpose: '${result}' (${result!!.length} chars)")
PGHLog.i(TAG, "✅ OCR SUCCESS for $purpose: '${result}' (${result!!.length} chars)")
}
latch.countDown()
}
.addOnFailureListener { e ->
ocrError = e
Log.e(TAG, "❌ OCR failed for $purpose: ${e.message}", e)
PGHLog.e(TAG, "❌ OCR failed for $purpose: ${e.message}", e)
latch.countDown()
}
@ -941,22 +955,22 @@ class ScreenCaptureService : Service() {
val completed = latch.await(5, TimeUnit.SECONDS)
if (!completed) {
Log.e(TAG, "⏱️ OCR timeout for $purpose after 5 seconds")
PGHLog.e(TAG, "⏱️ OCR timeout for $purpose after 5 seconds")
return null
}
if (ocrError != null) {
Log.e(TAG, "❌ OCR error for $purpose: ${ocrError!!.message}")
PGHLog.e(TAG, "❌ OCR error for $purpose: ${ocrError!!.message}")
return null
}
// Clean and process the result
val cleanedResult = cleanOCRResult(result, purpose)
Log.d(TAG, "🧙 Cleaned result for $purpose: '${cleanedResult}'")
PGHLog.d(TAG, "🧙 Cleaned result for $purpose: '${cleanedResult}'")
return cleanedResult
} catch (e: Exception) {
Log.e(TAG, "❌ Error in OCR for $purpose", e)
PGHLog.e(TAG, "❌ Error in OCR for $purpose", e)
return null
}
}
@ -1014,7 +1028,7 @@ class ScreenCaptureService : Service() {
val resized = Mat()
Imgproc.resize(roi, resized, Size(roi.width() * scale, roi.height() * scale))
Log.d(TAG, "🔍 Upscaled OCR region from ${roi.width()}x${roi.height()} to ${resized.width()}x${resized.height()}")
PGHLog.d(TAG, "🔍 Upscaled OCR region from ${roi.width()}x${roi.height()} to ${resized.width()}x${resized.height()}")
resized
} else {
val copy = Mat()
@ -1057,7 +1071,7 @@ class ScreenCaptureService : Service() {
return result
} catch (e: Exception) {
Log.e(TAG, "Error preprocessing image, using original", e)
PGHLog.e(TAG, "Error preprocessing image, using original", e)
// Return copy of original if preprocessing fails
val result = Mat()
roi.copyTo(result)
@ -1082,32 +1096,32 @@ class ScreenCaptureService : Service() {
}
private fun logPokemonInfo(pokemonInfo: PokemonInfo) {
Log.i(TAG, "====== POKEMON INFO EXTRACTED ======")
Log.i(TAG, "🎾 Pokeball: ${pokemonInfo.pokeballType}")
Log.i(TAG, "📛 Nickname: ${pokemonInfo.nickname}")
Log.i(TAG, "⚤ Gender: ${pokemonInfo.gender}")
Log.i(TAG, "📊 Level: ${pokemonInfo.level}")
Log.i(TAG, "🌍 Language: ${pokemonInfo.language}")
Log.i(TAG, "🎮 Game Source: ${pokemonInfo.gameSource}")
Log.i(TAG, "⭐ Favorited: ${pokemonInfo.isFavorited}")
Log.i(TAG, "🔢 Dex #: ${pokemonInfo.nationalDexNumber}")
Log.i(TAG, "🐾 Species: ${pokemonInfo.species}")
Log.i(TAG, "🏷️ Type 1: ${pokemonInfo.primaryType}")
Log.i(TAG, "🏷️ Type 2: ${pokemonInfo.secondaryType}")
Log.i(TAG, "🏆 Stamps: ${pokemonInfo.stamps}")
Log.i(TAG, "🏷️ Labels: ${pokemonInfo.labels}")
Log.i(TAG, "✅ Marks: ${pokemonInfo.marks}")
PGHLog.i(TAG, "====== POKEMON INFO EXTRACTED ======")
PGHLog.i(TAG, "🎾 Pokeball: ${pokemonInfo.pokeballType}")
PGHLog.i(TAG, "📛 Nickname: ${pokemonInfo.nickname}")
PGHLog.i(TAG, "⚤ Gender: ${pokemonInfo.gender}")
PGHLog.i(TAG, "📊 Level: ${pokemonInfo.level}")
PGHLog.i(TAG, "🌍 Language: ${pokemonInfo.language}")
PGHLog.i(TAG, "🎮 Game Source: ${pokemonInfo.gameSource}")
PGHLog.i(TAG, "⭐ Favorited: ${pokemonInfo.isFavorited}")
PGHLog.i(TAG, "🔢 Dex #: ${pokemonInfo.nationalDexNumber}")
PGHLog.i(TAG, "🐾 Species: ${pokemonInfo.species}")
PGHLog.i(TAG, "🏷️ Type 1: ${pokemonInfo.primaryType}")
PGHLog.i(TAG, "🏷️ Type 2: ${pokemonInfo.secondaryType}")
PGHLog.i(TAG, "🏆 Stamps: ${pokemonInfo.stamps}")
PGHLog.i(TAG, "🏷️ Labels: ${pokemonInfo.labels}")
PGHLog.i(TAG, "✅ Marks: ${pokemonInfo.marks}")
if (pokemonInfo.stats != null) {
Log.i(TAG, "📈 Stats: HP:${pokemonInfo.stats.hp} ATK:${pokemonInfo.stats.attack} DEF:${pokemonInfo.stats.defense}")
Log.i(TAG, " SP.ATK:${pokemonInfo.stats.spAttack} SP.DEF:${pokemonInfo.stats.spDefense} SPD:${pokemonInfo.stats.speed}")
PGHLog.i(TAG, "📈 Stats: HP:${pokemonInfo.stats.hp} ATK:${pokemonInfo.stats.attack} DEF:${pokemonInfo.stats.defense}")
PGHLog.i(TAG, " SP.ATK:${pokemonInfo.stats.spAttack} SP.DEF:${pokemonInfo.stats.spDefense} SPD:${pokemonInfo.stats.speed}")
}
Log.i(TAG, "⚔️ Moves: ${pokemonInfo.moves}")
Log.i(TAG, "💪 Ability: ${pokemonInfo.ability}")
Log.i(TAG, "🎭 Nature: ${pokemonInfo.nature}")
Log.i(TAG, "👤 OT: ${pokemonInfo.originalTrainerName}")
Log.i(TAG, "🔢 ID: ${pokemonInfo.originalTrainerId}")
Log.i(TAG, "🎯 Confidence: ${String.format("%.2f", pokemonInfo.extractionConfidence)}")
Log.i(TAG, "====================================")
PGHLog.i(TAG, "⚔️ Moves: ${pokemonInfo.moves}")
PGHLog.i(TAG, "💪 Ability: ${pokemonInfo.ability}")
PGHLog.i(TAG, "🎭 Nature: ${pokemonInfo.nature}")
PGHLog.i(TAG, "👤 OT: ${pokemonInfo.originalTrainerName}")
PGHLog.i(TAG, "🔢 ID: ${pokemonInfo.originalTrainerId}")
PGHLog.i(TAG, "🎯 Confidence: ${String.format("%.2f", pokemonInfo.extractionConfidence)}")
PGHLog.i(TAG, "====================================")
}
private fun convertImageToMat(image: Image): Mat? {
@ -1119,7 +1133,7 @@ class ScreenCaptureService : Service() {
// Get screen dimensions from the manager
val screenDimensions = screenCaptureManager.getScreenDimensions()
if (screenDimensions == null) {
Log.e(TAG, "Screen dimensions not available from manager")
PGHLog.e(TAG, "Screen dimensions not available from manager")
return null
}
val (screenWidth, screenHeight) = screenDimensions
@ -1162,7 +1176,7 @@ class ScreenCaptureService : Service() {
bgrMat
} catch (e: Exception) {
Log.e(TAG, "❌ Error converting image to Mat", e)
PGHLog.e(TAG, "❌ Error converting image to Mat", e)
// Clean up on error
mat?.release()
if (croppedBitmap != null && croppedBitmap != bitmap) {
@ -1174,7 +1188,7 @@ class ScreenCaptureService : Service() {
}
private fun triggerManualDetection() {
Log.d(TAG, "🔍 Manual detection triggered via MVC!")
PGHLog.d(TAG, "🔍 Manual detection triggered via MVC!")
latestImage?.let { image ->
try {
@ -1187,7 +1201,7 @@ class ScreenCaptureService : Service() {
Utils.matToBitmap(mat, bitmap)
val detections: List<MLDetection> = runBlocking {
mlInferenceEngine?.detect(bitmap) ?: emptyList()
mlInferenceEngine?.detect(bitmap)?.getOrDefault(emptyList()) ?: emptyList()
}
// Show detection overlay with results
@ -1200,7 +1214,7 @@ class ScreenCaptureService : Service() {
mat.release()
} else {
Log.e(TAG, "❌ Failed to convert image to Mat")
PGHLog.e(TAG, "❌ Failed to convert image to Mat")
}
// Close the image after processing to free the buffer
@ -1208,10 +1222,10 @@ class ScreenCaptureService : Service() {
latestImage = null
} catch (e: Exception) {
Log.e(TAG, "❌ Error in manual detection", e)
PGHLog.e(TAG, "❌ Error in manual detection", e)
}
} ?: run {
Log.w(TAG, "⚠️ No image available for detection")
PGHLog.w(TAG, "⚠️ No image available for detection")
}
}
@ -1233,11 +1247,11 @@ class ScreenCaptureService : Service() {
ocrExecutor.shutdown()
try {
if (!ocrExecutor.awaitTermination(5, TimeUnit.SECONDS)) {
Log.w(TAG, "OCR executor did not terminate gracefully, forcing shutdown")
PGHLog.w(TAG, "OCR executor did not terminate gracefully, forcing shutdown")
ocrExecutor.shutdownNow()
}
} catch (e: InterruptedException) {
Log.w(TAG, "Interrupted while waiting for OCR executor termination")
PGHLog.w(TAG, "Interrupted while waiting for OCR executor termination")
ocrExecutor.shutdownNow()
Thread.currentThread().interrupt()
}

36
app/src/main/java/com/quillstudios/pokegoalshelper/controllers/DetectionController.kt

@ -1,6 +1,7 @@
package com.quillstudios.pokegoalshelper.controllers
import android.util.Log
import com.quillstudios.pokegoalshelper.utils.PGHLog
// import com.quillstudios.pokegoalshelper.YOLOOnnxDetector
// import com.quillstudios.pokegoalshelper.Detection
import com.quillstudios.pokegoalshelper.ml.MLInferenceEngine
@ -41,7 +42,7 @@ class DetectionController(
// === DetectionUIEvents Implementation ===
override fun onDetectionRequested() {
Log.d(TAG, "🔍 Detection requested via controller")
PGHLog.d(TAG, "🔍 Detection requested via controller")
// The actual detection will be triggered by the service layer
// This event will be handled by the service which has access to the image
detectionRequestCallback?.invoke()
@ -58,7 +59,7 @@ class DetectionController(
}
override fun onClassFilterChanged(className: String?) {
Log.i(TAG, "🔍 Class filter changed to: ${className ?: "ALL CLASSES"}")
PGHLog.i(TAG, "🔍 Class filter changed to: ${className ?: "ALL CLASSES"}")
currentSettings.classFilter = className
@ -75,7 +76,7 @@ class DetectionController(
override fun onDebugModeToggled() {
currentSettings.debugMode = !currentSettings.debugMode
Log.i(TAG, "📊 Debug mode toggled: ${currentSettings.debugMode}")
PGHLog.i(TAG, "📊 Debug mode toggled: ${currentSettings.debugMode}")
// Apply debug mode to ML inference engine
// mlInferenceEngine.toggleShowAllConfidences() // Not implemented yet
@ -89,7 +90,7 @@ class DetectionController(
}
override fun onCoordinateModeChanged(mode: String) {
Log.i(TAG, "🔧 Coordinate mode changed to: $mode")
PGHLog.i(TAG, "🔧 Coordinate mode changed to: $mode")
currentSettings.coordinateMode = mode
@ -111,21 +112,18 @@ class DetectionController(
* This will be called by the service layer
*/
suspend fun processDetection(inputBitmap: android.graphics.Bitmap): List<Detection> {
return try {
uiCallbacks?.onDetectionStarted()
val detections = mlInferenceEngine.detect(inputBitmap)
val detectionCount = detections.size
Log.i(TAG, "✅ Detection completed: $detectionCount objects found")
uiCallbacks?.onDetectionCompleted(detectionCount)
detections
} catch (e: Exception) {
Log.e(TAG, "❌ Detection failed", e)
uiCallbacks?.onDetectionFailed(e.message ?: "Unknown error")
emptyList()
}
uiCallbacks?.onDetectionStarted()
return mlInferenceEngine.detect(inputBitmap)
.onSuccess { detections ->
PGHLog.i(TAG, "✅ Detection completed: ${detections.size} objects found")
uiCallbacks?.onDetectionCompleted(detections.size)
}
.onError { errorType, exception, message ->
PGHLog.e(TAG, "❌ Detection failed: $message (${errorType.name})", exception)
uiCallbacks?.onDetectionFailed(message)
}
.getOrDefault(emptyList())
}
/**

8
app/src/main/java/com/quillstudios/pokegoalshelper/ml/MLInferenceEngine.kt

@ -10,16 +10,16 @@ interface MLInferenceEngine
{
/**
* Initialize the ML model and prepare for inference.
* @return true if initialization was successful, false otherwise
* @return MLResult indicating success or specific failure type
*/
suspend fun initialize(): Boolean
suspend fun initialize(): MLResult<Unit>
/**
* Perform object detection on the provided image.
* @param image The bitmap image to analyze
* @return List of detected objects, empty if no objects found
* @return MLResult containing detected objects or error information
*/
suspend fun detect(image: Bitmap): List<Detection>
suspend fun detect(image: Bitmap): MLResult<List<Detection>>
/**
* Set the confidence threshold for detections.

60
app/src/main/java/com/quillstudios/pokegoalshelper/ml/MLResult.kt

@ -1,12 +1,29 @@
package com.quillstudios.pokegoalshelper.ml
/**
* Categories of ML/CV operation errors for better error handling
*/
enum class MLErrorType(val description: String)
{
INITIALIZATION_FAILED("Model initialization failed"),
INFERENCE_FAILED("Model inference failed"),
PREPROCESSING_FAILED("Image preprocessing failed"),
POSTPROCESSING_FAILED("Result postprocessing failed"),
MEMORY_ERROR("Memory allocation error"),
INVALID_INPUT("Invalid input data"),
TIMEOUT_ERROR("Operation timed out"),
RESOURCE_ERROR("Resource management error"),
CONFIGURATION_ERROR("Configuration error"),
UNKNOWN_ERROR("Unknown error occurred")
}
/**
* Result wrapper for ML operations to standardize error handling
*/
sealed class MLResult<out T>
{
data class Success<T>(val data: T) : MLResult<T>()
data class Error(val exception: Throwable, val message: String) : MLResult<Nothing>()
data class Error(val errorType: MLErrorType, val exception: Throwable, val message: String) : MLResult<Nothing>()
/**
* Returns the data if successful, or null if error
@ -38,9 +55,18 @@ sealed class MLResult<out T>
/**
* Execute block if error
*/
inline fun onError(block: (Throwable, String) -> Unit): MLResult<T>
inline fun onError(block: (MLErrorType, Throwable, String) -> Unit): MLResult<T>
{
if (this is Error) block(errorType, exception, message)
return this
}
/**
* Execute block if specific error type
*/
inline fun onErrorType(errorType: MLErrorType, block: (Throwable, String) -> Unit): MLResult<T>
{
if (this is Error) block(exception, message)
if (this is Error && this.errorType == errorType) block(exception, message)
return this
}
@ -50,7 +76,7 @@ sealed class MLResult<out T>
inline fun <R> map(transform: (T) -> R): MLResult<R> = when (this)
{
is Success -> try { Success(transform(data)) }
catch (e: Exception) { Error(e, "Transform failed: ${e.message}") }
catch (e: Exception) { Error(MLErrorType.UNKNOWN_ERROR, e, "Transform failed: ${e.message}") }
is Error -> this
}
}
@ -58,7 +84,7 @@ sealed class MLResult<out T>
/**
* Helper functions for creating results
*/
inline fun <T> mlTry(operation: () -> T): MLResult<T>
inline fun <T> mlTry(errorType: MLErrorType, operation: () -> T): MLResult<T>
{
return try
{
@ -66,11 +92,11 @@ inline fun <T> mlTry(operation: () -> T): MLResult<T>
}
catch (e: Exception)
{
MLResult.Error(e, "Operation failed: ${e.message}")
MLResult.Error(errorType, e, "${errorType.description}: ${e.message}")
}
}
inline fun <T> mlTryWithMessage(message: String, operation: () -> T): MLResult<T>
inline fun <T> mlTryWithMessage(errorType: MLErrorType, message: String, operation: () -> T): MLResult<T>
{
return try
{
@ -78,6 +104,24 @@ inline fun <T> mlTryWithMessage(message: String, operation: () -> T): MLResult<T
}
catch (e: Exception)
{
MLResult.Error(e, "$message: ${e.message}")
MLResult.Error(errorType, e, "$message: ${e.message}")
}
}
/**
* Create an error result directly
*/
fun <T> mlError(errorType: MLErrorType, message: String, cause: Throwable? = null): MLResult<T>
{
val exception = cause ?: RuntimeException(message)
return MLResult.Error(errorType, exception, message)
}
/**
* Create an error result from exception
*/
fun <T> mlErrorFromException(errorType: MLErrorType, exception: Throwable, customMessage: String? = null): MLResult<T>
{
val message = customMessage ?: "${errorType.description}: ${exception.message}"
return MLResult.Error(errorType, exception, message)
}

123
app/src/main/java/com/quillstudios/pokegoalshelper/ml/YOLOInferenceEngine.kt

@ -4,6 +4,7 @@ import android.content.Context
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.util.Log
import com.quillstudios.pokegoalshelper.utils.PGHLog
import org.opencv.android.Utils
import org.opencv.core.*
import org.opencv.imgproc.Imgproc
@ -68,13 +69,13 @@ class YOLOInferenceEngine(
fun setCoordinateMode(mode: String)
{
COORD_TRANSFORM_MODE = mode
Log.i(TAG, "🔧 Coordinate transform mode changed to: $mode")
PGHLog.i(TAG, "🔧 Coordinate transform mode changed to: $mode")
}
fun toggleShowAllConfidences()
{
SHOW_ALL_CONFIDENCES = !SHOW_ALL_CONFIDENCES
Log.i(TAG, "📊 Show all confidences: $SHOW_ALL_CONFIDENCES")
PGHLog.i(TAG, "📊 Show all confidences: $SHOW_ALL_CONFIDENCES")
}
fun setClassFilter(className: String?)
@ -82,11 +83,11 @@ class YOLOInferenceEngine(
DEBUG_CLASS_FILTER = className
if (className != null)
{
Log.i(TAG, "🔍 Class filter set to: '$className' (ID will be shown in debug output)")
PGHLog.i(TAG, "🔍 Class filter set to: '$className' (ID will be shown in debug output)")
}
else
{
Log.i(TAG, "🔍 Class filter set to: ALL CLASSES")
PGHLog.i(TAG, "🔍 Class filter set to: ALL CLASSES")
}
}
}
@ -206,58 +207,60 @@ class YOLOInferenceEngine(
95 to "ball_icon_ultraball_husui"
)
override suspend fun initialize(): Boolean = withContext(Dispatchers.IO)
override suspend fun initialize(): MLResult<Unit> = withContext(Dispatchers.IO)
{
if (isInitialized) return@withContext true
if (isInitialized) return@withContext MLResult.Success(Unit)
try
{
Log.i(TAG, "🤖 Initializing ONNX YOLO detector...")
mlTry(MLErrorType.INITIALIZATION_FAILED) {
PGHLog.i(TAG, "🤖 Initializing ONNX YOLO detector...")
// Initialize ONNX Runtime environment
ortEnvironment = OrtEnvironment.getEnvironment()
// Copy model from assets to internal storage
Log.i(TAG, "📂 Copying model file: ${config.modelFile}")
PGHLog.i(TAG, "📂 Copying model file: ${config.modelFile}")
val model_path = copyAssetToInternalStorage(config.modelFile)
if (model_path == null)
{
Log.e(TAG, "❌ Failed to copy ONNX model from assets")
return@withContext false
}
Log.i(TAG, "✅ Model copied to: $model_path")
?: throw RuntimeException("Failed to copy ONNX model from assets")
PGHLog.i(TAG, "✅ Model copied to: $model_path")
// Create ONNX session
Log.i(TAG, "📥 Loading ONNX model from: $model_path")
PGHLog.i(TAG, "📥 Loading ONNX model from: $model_path")
val session_options = OrtSession.SessionOptions()
session_options.setOptimizationLevel(OrtSession.SessionOptions.OptLevel.ALL_OPT)
ortSession = ortEnvironment?.createSession(model_path, session_options)
?: throw RuntimeException("Failed to create ONNX session")
Log.i(TAG, "✅ ONNX YOLO detector initialized successfully")
PGHLog.i(TAG, "✅ ONNX YOLO detector initialized successfully")
isInitialized = true
true
}
catch (e: Exception)
{
Log.e(TAG, "❌ Failed to initialize ONNX YOLO detector", e)
false
}.onError { errorType, exception, message ->
PGHLog.e(TAG, "$message", exception)
}
}
override suspend fun detect(image: Bitmap): List<Detection> = withContext(Dispatchers.IO)
override suspend fun detect(image: Bitmap): MLResult<List<Detection>> = withContext(Dispatchers.IO)
{
// Convert Bitmap to Mat first to maintain compatibility with original logic
val input_mat = Mat()
Utils.bitmapToMat(image, input_mat)
try
if (image.isRecycled)
{
detectWithMat(input_mat)
return@withContext mlError(MLErrorType.INVALID_INPUT, "Bitmap is recycled")
}
finally
{
input_mat.release()
mlTry(MLErrorType.INFERENCE_FAILED) {
// Convert Bitmap to Mat first to maintain compatibility with original logic
val input_mat = Mat()
Utils.bitmapToMat(image, input_mat)
try
{
detectWithMat(input_mat)
}
finally
{
input_mat.release()
}
}.onError { errorType, exception, message ->
PGHLog.e(TAG, "❌ Detection failed: $message", exception)
}
}
@ -268,7 +271,7 @@ class YOLOInferenceEngine(
{
if (!isInitialized || ortSession == null)
{
Log.w(TAG, "⚠️ ONNX detector not initialized")
PGHLog.w(TAG, "⚠️ ONNX detector not initialized")
return emptyList()
}
@ -276,7 +279,7 @@ class YOLOInferenceEngine(
return try
{
Log.d(TAG, "🎯 Starting ONNX YOLO detection...")
PGHLog.d(TAG, "🎯 Starting ONNX YOLO detection...")
val detections = if (ENABLE_MULTI_PREPROCESSING)
{
@ -294,13 +297,13 @@ class YOLOInferenceEngine(
totalInferenceTime += lastInferenceTime
inferenceCount++
Log.d(TAG, "🎯 Detection completed: ${detections.size} objects found in ${lastInferenceTime}ms")
PGHLog.d(TAG, "🎯 Detection completed: ${detections.size} objects found in ${lastInferenceTime}ms")
detections
}
catch (e: Exception)
{
Log.e(TAG, "❌ Error during enhanced ONNX YOLO detection", e)
PGHLog.e(TAG, "❌ Error during enhanced ONNX YOLO detection", e)
emptyList()
}
}
@ -344,7 +347,7 @@ class YOLOInferenceEngine(
}
catch (e: Exception)
{
Log.w(TAG, "⚠️ Preprocessing method timed out or failed", e)
PGHLog.w(TAG, "⚠️ Preprocessing method timed out or failed", e)
}
}
@ -353,7 +356,7 @@ class YOLOInferenceEngine(
}
catch (e: Exception)
{
Log.e(TAG, "❌ Error in multiple preprocessing", e)
PGHLog.e(TAG, "❌ Error in multiple preprocessing", e)
return emptyList()
}
}
@ -382,7 +385,7 @@ class YOLOInferenceEngine(
}
catch (e: Exception)
{
Log.e(TAG, "❌ Error in preprocessing method '$method'", e)
PGHLog.e(TAG, "❌ Error in preprocessing method '$method'", e)
return emptyList()
}
}
@ -414,14 +417,14 @@ class YOLOInferenceEngine(
override fun setConfidenceThreshold(threshold: Float)
{
confidenceThreshold = threshold.coerceIn(0.0f, 1.0f)
Log.d(TAG, "🎚️ Confidence threshold set to: $confidenceThreshold")
PGHLog.d(TAG, "🎚️ Confidence threshold set to: $confidenceThreshold")
}
override fun setClassFilter(className: String?)
{
classFilter = className
DEBUG_CLASS_FILTER = className
Log.d(TAG, "🔍 Class filter set to: ${className ?: "none"}")
PGHLog.d(TAG, "🔍 Class filter set to: ${className ?: "none"}")
}
override fun isReady(): Boolean = isInitialized
@ -434,7 +437,7 @@ class YOLOInferenceEngine(
override fun cleanup()
{
Log.d(TAG, "🧹 Cleaning up YOLO Inference Engine")
PGHLog.d(TAG, "🧹 Cleaning up YOLO Inference Engine")
try
{
@ -442,7 +445,7 @@ class YOLOInferenceEngine(
preprocessingExecutor.shutdown()
if (!preprocessingExecutor.awaitTermination(2, TimeUnit.SECONDS))
{
Log.w(TAG, "⚠️ Thread pool shutdown timeout, forcing shutdown")
PGHLog.w(TAG, "⚠️ Thread pool shutdown timeout, forcing shutdown")
preprocessingExecutor.shutdownNow()
}
@ -451,7 +454,7 @@ class YOLOInferenceEngine(
}
catch (e: Exception)
{
Log.e(TAG, "Error during cleanup", e)
PGHLog.e(TAG, "Error during cleanup", e)
}
finally
{
@ -481,7 +484,7 @@ class YOLOInferenceEngine(
}
catch (e: IOException)
{
Log.e(TAG, "Failed to copy asset to internal storage", e)
PGHLog.e(TAG, "Failed to copy asset to internal storage", e)
null
}
}
@ -493,7 +496,7 @@ class YOLOInferenceEngine(
{
try
{
Log.d(TAG, "🔧 Ultralytics preprocessing: input ${inputMat.cols()}x${inputMat.rows()}, type=${inputMat.type()}")
PGHLog.d(TAG, "🔧 Ultralytics preprocessing: input ${inputMat.cols()}x${inputMat.rows()}, type=${inputMat.type()}")
// Step 1: Letterbox resize (preserves aspect ratio with padding)
val letterboxed = letterboxResize(inputMat, config.inputSize, config.inputSize)
@ -510,19 +513,19 @@ class YOLOInferenceEngine(
// Use Gaussian blur as a more reliable alternative to bilateral filter
Imgproc.GaussianBlur(processed_mat, denoised, Size(3.0, 3.0), 0.5)
processed_mat.release()
Log.d(TAG, "✅ Ultralytics preprocessing complete with Gaussian smoothing")
PGHLog.d(TAG, "✅ Ultralytics preprocessing complete with Gaussian smoothing")
return denoised
}
else
{
Log.w(TAG, "⚠️ Smoothing skipped - unsupported image type: ${processed_mat.type()}")
PGHLog.w(TAG, "⚠️ Smoothing skipped - unsupported image type: ${processed_mat.type()}")
denoised.release()
return processed_mat
}
}
catch (e: Exception)
{
Log.e(TAG, "❌ Error in Ultralytics preprocessing", e)
PGHLog.e(TAG, "❌ Error in Ultralytics preprocessing", e)
// Return a copy instead of the original to avoid memory issues
val safe_copy = Mat()
inputMat.copyTo(safe_copy)
@ -573,7 +576,7 @@ class YOLOInferenceEngine(
}
catch (e: Exception)
{
Log.e(TAG, "❌ Error enhancing image", e)
PGHLog.e(TAG, "❌ Error enhancing image", e)
inputMat.copyTo(enhanced)
}
return enhanced
@ -598,7 +601,7 @@ class YOLOInferenceEngine(
}
catch (e: Exception)
{
Log.e(TAG, "❌ Error sharpening image", e)
PGHLog.e(TAG, "❌ Error sharpening image", e)
inputMat.copyTo(sharpened)
}
return sharpened
@ -750,7 +753,7 @@ class YOLOInferenceEngine(
}
catch (e: Exception)
{
Log.e(TAG, "$errorMessage", e)
PGHLog.e(TAG, "$errorMessage", e)
fallback()
}
}
@ -892,7 +895,7 @@ class YOLOInferenceEngine(
val num_detections = 300 // From model output [1, 300, 6]
val features_per_detection = 6 // [x1, y1, x2, y2, confidence, class_id]
Log.d(TAG, "🔍 Parsing NMS output: 300 post-processed detections")
PGHLog.d(TAG, "🔍 Parsing NMS output: 300 post-processed detections")
var valid_detections = 0
@ -937,7 +940,7 @@ class YOLOInferenceEngine(
// Debug logging for all detections if enabled
if (SHOW_ALL_CONFIDENCES && mapped_confidence > 0.1f)
{
Log.d(TAG, "🔍 [DEBUG] Class: $class_name (ID: $class_id), Confidence: %.3f, Original: %.3f".format(mapped_confidence, confidence))
PGHLog.d(TAG, "🔍 [DEBUG] Class: $class_name (ID: $class_id), Confidence: %.3f, Original: %.3f".format(mapped_confidence, confidence))
}
// Apply class filter if set
@ -968,13 +971,13 @@ class YOLOInferenceEngine(
if (valid_detections <= 3)
{
Log.d(TAG, "✅ Valid NMS detection: class=$class_id ($class_name), conf=${String.format("%.4f", mapped_confidence)}")
PGHLog.d(TAG, "✅ Valid NMS detection: class=$class_id ($class_name), conf=${String.format("%.4f", mapped_confidence)}")
}
}
}
}
Log.d(TAG, "🎯 NMS parsing complete: $valid_detections valid detections")
PGHLog.d(TAG, "🎯 NMS parsing complete: $valid_detections valid detections")
return detections.sortedByDescending { it.confidence }
}
@ -1101,7 +1104,7 @@ class YOLOInferenceEngine(
)
final_confidence = (total_weight / overlapping_boxes.size).toFloat()
Log.d(TAG, "🔗 Merged ${overlapping_boxes.size} overlapping detections, final conf: ${String.format("%.3f", final_confidence)}")
PGHLog.d(TAG, "🔗 Merged ${overlapping_boxes.size} overlapping detections, final conf: ${String.format("%.3f", final_confidence)}")
}
results.add(
@ -1163,7 +1166,7 @@ class YOLOInferenceEngine(
best_detection = other_detection
}
suppressed[j] = true
Log.d(TAG, "🔗 Cross-class NMS: merged ${current_detection.className} with ${other_detection.className}")
PGHLog.d(TAG, "🔗 Cross-class NMS: merged ${current_detection.className} with ${other_detection.className}")
}
}
}

132
app/src/main/java/com/quillstudios/pokegoalshelper/utils/PGHLog.kt

@ -0,0 +1,132 @@
package com.quillstudios.pokegoalshelper.utils
import android.util.Log
/**
* Centralized logging utility for PokeGoalsHelper
* Prefixes all logs with "PGH - " for easy filtering
*/
object PGHLog
{
private const val PREFIX = "PGH - "
/**
* Send a VERBOSE log message
*/
fun v(tag: String, msg: String): Int
{
return Log.v(tag, "$PREFIX$msg")
}
/**
* Send a VERBOSE log message with throwable
*/
fun v(tag: String, msg: String, tr: Throwable): Int
{
return Log.v(tag, "$PREFIX$msg", tr)
}
/**
* Send a DEBUG log message
*/
fun d(tag: String, msg: String): Int
{
return Log.d(tag, "$PREFIX$msg")
}
/**
* Send a DEBUG log message with throwable
*/
fun d(tag: String, msg: String, tr: Throwable): Int
{
return Log.d(tag, "$PREFIX$msg", tr)
}
/**
* Send an INFO log message
*/
fun i(tag: String, msg: String): Int
{
return Log.i(tag, "$PREFIX$msg")
}
/**
* Send an INFO log message with throwable
*/
fun i(tag: String, msg: String, tr: Throwable): Int
{
return Log.i(tag, "$PREFIX$msg", tr)
}
/**
* Send a WARN log message
*/
fun w(tag: String, msg: String): Int
{
return Log.w(tag, "$PREFIX$msg")
}
/**
* Send a WARN log message with throwable
*/
fun w(tag: String, msg: String, tr: Throwable): Int
{
return Log.w(tag, "$PREFIX$msg", tr)
}
/**
* Send a WARN log message with throwable only
*/
fun w(tag: String, tr: Throwable): Int
{
return Log.w(tag, PREFIX, tr)
}
/**
* Send an ERROR log message
*/
fun e(tag: String, msg: String): Int
{
return Log.e(tag, "$PREFIX$msg")
}
/**
* Send an ERROR log message with throwable
*/
fun e(tag: String, msg: String, tr: Throwable): Int
{
return Log.e(tag, "$PREFIX$msg", tr)
}
/**
* What a Terrible Failure: Report a condition that should never happen
*/
fun wtf(tag: String, msg: String): Int
{
return Log.wtf(tag, "$PREFIX$msg")
}
/**
* What a Terrible Failure: Report a condition that should never happen with throwable
*/
fun wtf(tag: String, msg: String, tr: Throwable): Int
{
return Log.wtf(tag, "$PREFIX$msg", tr)
}
/**
* What a Terrible Failure: Report a condition that should never happen with throwable only
*/
fun wtf(tag: String, tr: Throwable): Int
{
return Log.wtf(tag, PREFIX, tr)
}
/**
* Checks to see whether or not a log for the specified tag is loggable at the specified level
*/
fun isLoggable(tag: String, level: Int): Boolean
{
return Log.isLoggable(tag, level)
}
}
Loading…
Cancel
Save