Browse Source

- Temp checkpoint

feature/pgh-30-long-screenshot-capture
Dan 5 months ago
parent
commit
9e55ffa944
  1. 49
      app/src/main/java/com/quillstudios/pokegoalshelper/ScreenCaptureService.kt
  2. 143
      app/src/main/java/com/quillstudios/pokegoalshelper/capture/LongScreenshotCapture.kt
  3. 8
      app/src/main/java/com/quillstudios/pokegoalshelper/controllers/DetectionController.kt
  4. 574
      app/src/main/java/com/quillstudios/pokegoalshelper/stitching/ImageStitcher.kt
  5. 3
      app/src/main/java/com/quillstudios/pokegoalshelper/ui/EnhancedFloatingFAB.kt
  6. 33
      app/src/main/java/com/quillstudios/pokegoalshelper/ui/interfaces/DetectionUIEvents.kt

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

@ -195,6 +195,7 @@ class ScreenCaptureService : Service() {
startCallback = { startLongScreenshot() },
captureCallback = { captureLongScreenshot() },
finishCallback = { finishLongScreenshot() },
finishAndStitchCallback = { finishLongScreenshotAndStitch() },
cancelCallback = { cancelLongScreenshot() }
)
@ -207,6 +208,7 @@ class ScreenCaptureService : Service() {
onLongScreenshotStart = { detectionController.onLongScreenshotStart() },
onLongScreenshotCapture = { detectionController.onLongScreenshotCapture() },
onLongScreenshotFinish = { detectionController.onLongScreenshotFinish() },
onLongScreenshotFinishAndStitch = { detectionController.onLongScreenshotFinishAndStitch() },
onLongScreenshotCancel = { detectionController.onLongScreenshotCancel() }
)
@ -1370,8 +1372,7 @@ class ScreenCaptureService : Service() {
val screenshots = longScreenshotCapture?.finishCollection() ?: emptyList()
PGHLog.i(TAG, "✅ Long screenshot collection finished with ${screenshots.size} screenshots")
// TODO: Process screenshots for stitching or analysis
// For now, just log the result
// Log the result without stitching
screenshots.forEach { screenshot ->
PGHLog.i(TAG, "📄 Screenshot: ${screenshot.filename} (${screenshot.width}x${screenshot.height})")
}
@ -1383,6 +1384,50 @@ class ScreenCaptureService : Service() {
}
}
private fun finishLongScreenshotAndStitch()
{
try
{
PGHLog.i(TAG, "🧩 Finishing long screenshot collection with stitching")
// Set up stitching callbacks
setupStitchingCallbacks()
// Start the finish and stitch process
longScreenshotCapture?.finishCollectionAndStitch()
}
catch (e: Exception)
{
PGHLog.e(TAG, "❌ Error starting stitching process", e)
}
}
private fun setupStitchingCallbacks()
{
longScreenshotCapture?.setStitchingProgressCallback { progress, message ->
PGHLog.i(TAG, "🧩 Stitching progress: ${(progress * 100).toInt()}% - $message")
// TODO: Update UI with progress
}
longScreenshotCapture?.setStitchingCompleteCallback { result ->
if (result.success)
{
PGHLog.i(TAG, "✅ Stitching completed successfully!")
PGHLog.i(TAG, "📄 Output: ${result.outputPath}")
PGHLog.i(TAG, "📊 Quality: ${result.qualityScore}")
PGHLog.i(TAG, "📐 Dimensions: ${result.finalDimensions}")
// TODO: Notify user of success, maybe show result in gallery or share
}
else
{
PGHLog.e(TAG, "❌ Stitching failed: ${result.errorMessage}")
// TODO: Notify user of failure
}
}
}
private fun cancelLongScreenshot()
{
try

143
app/src/main/java/com/quillstudios/pokegoalshelper/capture/LongScreenshotCapture.kt

@ -11,6 +11,7 @@ import android.media.projection.MediaProjection
import android.os.Handler
import android.os.Looper
import com.quillstudios.pokegoalshelper.utils.PGHLog
import com.quillstudios.pokegoalshelper.stitching.ImageStitcher
import kotlinx.coroutines.*
import java.io.File
import java.io.FileOutputStream
@ -61,6 +62,9 @@ class LongScreenshotCapture(
private val capturedScreenshots = ConcurrentLinkedQueue<CapturedScreenshot>()
private val captureScope = CoroutineScope(Dispatchers.IO + SupervisorJob())
// Image stitching
private val imageStitcher: ImageStitcher by lazy { ImageStitcher(context) }
// Storage
private val storageDir: File by lazy {
File(context.filesDir, "long_screenshots").apply {
@ -71,6 +75,12 @@ class LongScreenshotCapture(
// Callbacks
private var progressCallback: ((count: Int) -> Unit)? = null
private var errorCallback: ((error: String) -> Unit)? = null
private var stitchingProgressCallback: ((progress: Float, message: String) -> Unit)? = null
private var stitchingCompleteCallback: ((result: ImageStitcher.StitchingResult) -> Unit)? = null
// Configuration
private var keepIndividualScreenshots = true // Keep originals when stitching
private var debugMode = false // Debug mode for development/testing
/**
* Initialize the long screenshot system (lightweight - no VirtualDisplay needed)
@ -228,6 +238,94 @@ class LongScreenshotCapture(
}
}
/**
* Stop collection and automatically stitch screenshots into a single image
*/
fun finishCollectionAndStitch()
{
if (!isCapturing.get())
{
PGHLog.w(TAG, "⚠️ No collection in progress")
return
}
try
{
PGHLog.i(TAG, "🏁 Finishing collection and starting stitching process")
val screenshots = finishCollection()
if (screenshots.isEmpty())
{
PGHLog.w(TAG, "⚠️ No screenshots to stitch")
stitchingCompleteCallback?.invoke(
ImageStitcher.StitchingResult(false, errorMessage = "No screenshots to stitch")
)
return
}
// Start stitching process
captureScope.launch {
stitchScreenshots(screenshots)
}
}
catch (e: Exception)
{
PGHLog.e(TAG, "❌ Error starting stitching process", e)
stitchingCompleteCallback?.invoke(
ImageStitcher.StitchingResult(false, errorMessage = "Failed to start stitching: ${e.message}")
)
}
}
private suspend fun stitchScreenshots(screenshots: List<CapturedScreenshot>)
{
try
{
PGHLog.i(TAG, "🧩 Starting stitching process with ${screenshots.size} screenshots")
// Notify progress
stitchingProgressCallback?.invoke(0.1f, "Starting image stitching...")
// Perform stitching
val result = imageStitcher.stitchScreenshots(screenshots)
// If stitching succeeded and we don't want to keep individual screenshots, clean them up
// Debug mode always keeps screenshots for analysis
if (result.success && !keepIndividualScreenshots && !debugMode)
{
PGHLog.i(TAG, "🧹 Cleaning up individual screenshots as requested")
screenshots.forEach { screenshot ->
try
{
File(screenshot.filePath).delete()
PGHLog.d(TAG, "🗑️ Deleted: ${screenshot.filename}")
}
catch (e: Exception)
{
PGHLog.w(TAG, "⚠️ Failed to delete: ${screenshot.filename}", e)
}
}
}
else if (debugMode)
{
PGHLog.i(TAG, "🐛 Debug mode: Keeping all individual screenshots for analysis")
}
// Notify completion
PGHLog.i(TAG, "🧩 Stitching process completed: success=${result.success}")
stitchingCompleteCallback?.invoke(result)
}
catch (e: Exception)
{
PGHLog.e(TAG, "❌ Error during stitching process", e)
stitchingCompleteCallback?.invoke(
ImageStitcher.StitchingResult(false, errorMessage = "Stitching failed: ${e.message}")
)
}
}
/**
* Cancel current collection
*/
@ -285,6 +383,46 @@ class LongScreenshotCapture(
this.errorCallback = callback
}
/**
* Set stitching progress callback
*/
fun setStitchingProgressCallback(callback: (progress: Float, message: String) -> Unit)
{
this.stitchingProgressCallback = callback
}
/**
* Set stitching completion callback
*/
fun setStitchingCompleteCallback(callback: (result: ImageStitcher.StitchingResult) -> Unit)
{
this.stitchingCompleteCallback = callback
}
/**
* Configure whether to keep individual screenshots when stitching
* @param keep true to keep individual screenshots, false to delete them after stitching
*/
fun setKeepIndividualScreenshots(keep: Boolean)
{
this.keepIndividualScreenshots = keep
}
/**
* Enable debug mode for stitching development
* In debug mode, individual screenshots are always kept and additional logging is enabled
* @param enabled true to enable debug mode
*/
fun setDebugMode(enabled: Boolean)
{
this.debugMode = enabled
if (enabled)
{
PGHLog.i(TAG, "🐛 Debug mode enabled for long screenshot stitching")
this.keepIndividualScreenshots = true // Always keep screenshots in debug mode
}
}
private suspend fun processBitmapAsync(bitmap: Bitmap) = withContext(Dispatchers.IO)
{
try
@ -377,10 +515,15 @@ class LongScreenshotCapture(
// Clear stored files
clearStoredScreenshots()
// Clean up stitcher
imageStitcher.cleanup()
// Clear references
mediaProjection = null
progressCallback = null
errorCallback = null
stitchingProgressCallback = null
stitchingCompleteCallback = null
PGHLog.i(TAG, "✅ Long screenshot capture cleaned up")

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

@ -61,6 +61,11 @@ class DetectionController(
longScreenshotFinishCallback?.invoke()
}
override fun onLongScreenshotFinishAndStitch() {
PGHLog.d(TAG, "🧩 Long screenshot finish and stitch requested via controller")
longScreenshotFinishAndStitchCallback?.invoke()
}
override fun onLongScreenshotCancel() {
PGHLog.d(TAG, "📸 Long screenshot cancel requested via controller")
longScreenshotCancelCallback?.invoke()
@ -70,6 +75,7 @@ class DetectionController(
private var longScreenshotStartCallback: (() -> Unit)? = null
private var longScreenshotCaptureCallback: (() -> Unit)? = null
private var longScreenshotFinishCallback: (() -> Unit)? = null
private var longScreenshotFinishAndStitchCallback: (() -> Unit)? = null
private var longScreenshotCancelCallback: (() -> Unit)? = null
/**
@ -87,11 +93,13 @@ class DetectionController(
startCallback: () -> Unit,
captureCallback: () -> Unit,
finishCallback: () -> Unit,
finishAndStitchCallback: () -> Unit,
cancelCallback: () -> Unit
) {
longScreenshotStartCallback = startCallback
longScreenshotCaptureCallback = captureCallback
longScreenshotFinishCallback = finishCallback
longScreenshotFinishAndStitchCallback = finishAndStitchCallback
longScreenshotCancelCallback = cancelCallback
}

574
app/src/main/java/com/quillstudios/pokegoalshelper/stitching/ImageStitcher.kt

@ -0,0 +1,574 @@
package com.quillstudios.pokegoalshelper.stitching
import android.graphics.Bitmap
import android.util.Log
import com.quillstudios.pokegoalshelper.capture.CapturedScreenshot
import com.quillstudios.pokegoalshelper.utils.PGHLog
import com.quillstudios.pokegoalshelper.utils.MatUtils.use
import com.quillstudios.pokegoalshelper.utils.MatUtils.useMats
import com.quillstudios.pokegoalshelper.utils.MatUtils.releaseSafely
import kotlinx.coroutines.*
import org.opencv.android.Utils
import org.opencv.core.*
import org.opencv.imgproc.Imgproc
import org.opencv.imgcodecs.Imgcodecs
import java.io.File
import java.io.FileOutputStream
import kotlin.math.*
/**
* OpenCV-based image stitching engine for combining long screenshots.
*
* Features:
* - Template matching for overlap detection
* - Precise alignment using correlation coefficients
* - Vertical image concatenation with blending
* - Quality assessment for stitching results
* - Memory optimization for large image processing
* - Error recovery for failed stitching operations
*/
class ImageStitcher(
private val context: android.content.Context
)
{
companion object
{
private const val TAG = "ImageStitcher"
// Template matching parameters
private const val MIN_OVERLAP_RATIO = 0.1f // 10% minimum overlap
private const val MAX_OVERLAP_RATIO = 0.8f // 80% maximum overlap
private const val CORRELATION_THRESHOLD = 0.3f // Template matching threshold (real screenshots often have lower correlation)
// Memory management
private const val MAX_IMAGE_DIMENSION = 4096 // Max width/height for processing
private const val TILE_PROCESSING_SIZE = 1024 // Process in tiles to save memory
// Quality assessment
private const val MIN_QUALITY_SCORE = 0.6f // Minimum acceptable quality
}
// Storage
private val outputDir: File by lazy {
File(context.filesDir, "stitched_screenshots").apply {
if (!exists()) mkdirs()
}
}
// Processing scope
private val stitchingScope = CoroutineScope(Dispatchers.IO + SupervisorJob())
/**
* Result of stitching operation
*/
data class StitchingResult(
val success: Boolean,
val outputPath: String? = null,
val qualityScore: Float = 0f,
val errorMessage: String? = null,
val processedCount: Int = 0,
val finalDimensions: Pair<Int, Int>? = null
)
/**
* Overlap detection result
*/
data class OverlapInfo(
val detected: Boolean,
val offsetY: Int = 0,
val confidence: Float = 0f,
val overlapHeight: Int = 0
)
/**
* Stitch a list of screenshots into a single long image
*/
suspend fun stitchScreenshots(screenshots: List<CapturedScreenshot>): StitchingResult = withContext(Dispatchers.IO)
{
if (screenshots.isEmpty())
{
PGHLog.w(TAG, "No screenshots to stitch")
return@withContext StitchingResult(false, errorMessage = "No screenshots provided")
}
if (screenshots.size == 1)
{
PGHLog.i(TAG, "Single screenshot - copying to output")
return@withContext copySingleScreenshot(screenshots[0])
}
try
{
PGHLog.i(TAG, "🧩 Starting stitching process with ${screenshots.size} screenshots")
// Load and validate all images
val imageMats = loadScreenshotMats(screenshots)
if (imageMats.isEmpty())
{
return@withContext StitchingResult(false, errorMessage = "Failed to load screenshot images")
}
try
{
// Perform stitching
val stitchingResult = performStitching(imageMats, screenshots)
return@withContext stitchingResult
}
finally
{
// Clean up loaded images
releaseSafely(*imageMats.toTypedArray())
}
}
catch (e: Exception)
{
PGHLog.e(TAG, "❌ Error during stitching process", e)
return@withContext StitchingResult(false, errorMessage = "Stitching failed: ${e.message}")
}
}
private fun loadScreenshotMats(screenshots: List<CapturedScreenshot>): List<Mat>
{
val imageMats = mutableListOf<Mat>()
for ((index, screenshot) in screenshots.withIndex())
{
try
{
PGHLog.d(TAG, "📖 Loading screenshot ${index + 1}: ${screenshot.filename}")
val mat = Imgcodecs.imread(screenshot.filePath, Imgcodecs.IMREAD_COLOR)
if (mat.empty())
{
PGHLog.e(TAG, "❌ Failed to load image: ${screenshot.filePath}")
continue
}
// Validate image dimensions
if (mat.cols() == 0 || mat.rows() == 0)
{
PGHLog.e(TAG, "❌ Invalid image dimensions: ${mat.cols()}x${mat.rows()}")
mat.release()
continue
}
// Scale down if image is too large for processing
val scaledMat = scaleImageIfNeeded(mat)
if (scaledMat != mat)
{
mat.release()
}
imageMats.add(scaledMat)
PGHLog.d(TAG, "✅ Loaded image ${index + 1}: ${scaledMat.cols()}x${scaledMat.rows()}")
}
catch (e: Exception)
{
PGHLog.e(TAG, "❌ Error loading screenshot ${index + 1}", e)
}
}
PGHLog.i(TAG, "📖 Loaded ${imageMats.size}/${screenshots.size} images successfully")
return imageMats
}
private fun scaleImageIfNeeded(mat: Mat): Mat
{
val maxDim = max(mat.cols(), mat.rows())
if (maxDim <= MAX_IMAGE_DIMENSION)
{
return mat
}
val scale = MAX_IMAGE_DIMENSION.toFloat() / maxDim
val newWidth = (mat.cols() * scale).toInt()
val newHeight = (mat.rows() * scale).toInt()
PGHLog.d(TAG, "🔄 Scaling image from ${mat.cols()}x${mat.rows()} to ${newWidth}x${newHeight}")
val scaledMat = Mat()
Imgproc.resize(mat, scaledMat, Size(newWidth.toDouble(), newHeight.toDouble()))
return scaledMat
}
private suspend fun performStitching(imageMats: List<Mat>, screenshots: List<CapturedScreenshot>): StitchingResult
{
if (imageMats.size != screenshots.size)
{
return StitchingResult(false, errorMessage = "Image count mismatch")
}
// Start with first image
var result = imageMats[0].clone()
var currentHeight = result.rows()
var processedCount = 1
try
{
PGHLog.i(TAG, "🧩 Starting with base image: ${result.cols()}x${result.rows()}")
// Process each subsequent image
for (i in 1 until imageMats.size)
{
val currentImage = imageMats[i]
PGHLog.d(TAG, "🔍 Processing image ${i + 1}/${imageMats.size}")
// Detect overlap with previous section
val overlapInfo = detectOverlap(result, currentImage, currentHeight)
val newResult = if (!overlapInfo.detected)
{
PGHLog.w(TAG, "⚠️ No overlap detected for image ${i + 1}, appending directly")
// Append without overlap
appendImageDirectly(result, currentImage)
}
else
{
PGHLog.i(TAG, "✅ Overlap detected: offset=${overlapInfo.offsetY}, confidence=${overlapInfo.confidence}")
// Blend with overlap
blendImageWithOverlap(result, currentImage, overlapInfo)
}
// Release old result and update to new one
result.release()
result = newResult
currentHeight = result.rows()
processedCount++
// Yield to prevent blocking
kotlinx.coroutines.yield()
}
// Assess final quality
val qualityScore = assessStitchingQuality(result, imageMats.size)
PGHLog.i(TAG, "📊 Stitching quality score: $qualityScore")
if (qualityScore < MIN_QUALITY_SCORE)
{
PGHLog.w(TAG, "⚠️ Low quality stitching result: $qualityScore < $MIN_QUALITY_SCORE")
}
// Save result
val outputPath = saveStitchedImage(result)
return if (outputPath != null)
{
PGHLog.i(TAG, "✅ Stitching completed: ${result.cols()}x${result.rows()}, saved to: $outputPath")
StitchingResult(
success = true,
outputPath = outputPath,
qualityScore = qualityScore,
processedCount = processedCount,
finalDimensions = Pair(result.cols(), result.rows())
)
}
else
{
StitchingResult(false, errorMessage = "Failed to save stitched image")
}
}
finally
{
// Always clean up the result Mat
result.release()
}
}
private fun detectOverlap(baseImage: Mat, newImage: Mat, searchStartY: Int): OverlapInfo
{
PGHLog.d(TAG, "🔍 Overlap detection: base=${baseImage.cols()}x${baseImage.rows()}, new=${newImage.cols()}x${newImage.rows()}")
// For screenshots, let's assume a typical overlap of about 10-30% from the bottom of the base image
// This is much more reliable than complex template matching for UI screenshots
val assumedOverlapRatio = 0.2f // Assume 20% overlap
val overlapHeight = (baseImage.rows() * assumedOverlapRatio).toInt()
val overlapStartY = baseImage.rows() - overlapHeight
// Validate this makes sense
if (overlapHeight < 50 || overlapHeight > baseImage.rows() / 2)
{
PGHLog.d(TAG, "⚠️ Calculated overlap doesn't make sense: $overlapHeight px")
return OverlapInfo(false)
}
// Simple validation - check if the bottom of base image and top of new image are similar
val similarity = checkImageSimilarity(baseImage, newImage, overlapStartY, overlapHeight)
PGHLog.d(TAG, "🎯 Simple overlap check: startY=$overlapStartY, height=$overlapHeight, similarity=$similarity")
if (similarity > 0.3f) // Lower threshold for screenshots
{
PGHLog.i(TAG, "✅ Simple overlap detected: startY=$overlapStartY, height=$overlapHeight, similarity=$similarity")
return OverlapInfo(
detected = true,
offsetY = overlapStartY,
confidence = similarity,
overlapHeight = overlapHeight
)
}
else
{
PGHLog.d(TAG, "🔍 No reliable overlap found - similarity $similarity too low")
return OverlapInfo(false)
}
}
private fun checkImageSimilarity(baseImage: Mat, newImage: Mat, overlapStartY: Int, overlapHeight: Int): Float
{
val baseOverlap = Mat()
val newOverlap = Mat()
val result = Mat()
try
{
// Extract overlap regions
val baseRect = Rect(0, overlapStartY, baseImage.cols(), overlapHeight)
val newRect = Rect(0, 0, newImage.cols(), overlapHeight)
baseImage.submat(baseRect).copyTo(baseOverlap)
newImage.submat(newRect).copyTo(newOverlap)
// Simple correlation check
Imgproc.matchTemplate(baseOverlap, newOverlap, result, Imgproc.TM_CCOEFF_NORMED)
val minMaxResult = Core.minMaxLoc(result)
return minMaxResult.maxVal.toFloat()
}
catch (e: Exception)
{
PGHLog.e(TAG, "❌ Error checking similarity", e)
return 0f
}
finally
{
baseOverlap.release()
newOverlap.release()
result.release()
}
}
private fun appendImageDirectly(baseImage: Mat, newImage: Mat): Mat
{
val combinedHeight = baseImage.rows() + newImage.rows()
val combinedWidth = max(baseImage.cols(), newImage.cols())
return Mat(combinedHeight, combinedWidth, baseImage.type()).apply {
try
{
// Copy base image to top
val baseRect = Rect(0, 0, baseImage.cols(), baseImage.rows())
baseImage.copyTo(this.submat(baseRect))
// Copy new image to bottom
val newRect = Rect(0, baseImage.rows(), newImage.cols(), newImage.rows())
newImage.copyTo(this.submat(newRect))
PGHLog.d(TAG, "📎 Appended image directly: ${this.cols()}x${this.rows()}")
}
catch (e: Exception)
{
PGHLog.e(TAG, "❌ Error appending image", e)
this.release()
throw e
}
}
}
private fun blendImageWithOverlap(baseImage: Mat, newImage: Mat, overlapInfo: OverlapInfo): Mat
{
// Calculate dimensions
val overlapStartY = overlapInfo.offsetY
val overlapHeight = min(overlapInfo.overlapHeight, baseImage.rows() - overlapStartY)
val nonOverlapHeight = newImage.rows() - overlapHeight
val combinedHeight = baseImage.rows() + newImage.rows() - overlapHeight // Subtract overlap to avoid duplication
val combinedWidth = max(baseImage.cols(), newImage.cols())
PGHLog.d(TAG, "🎨 Blend calculation: baseH=${baseImage.rows()}, newH=${newImage.rows()}")
PGHLog.d(TAG, "🎨 Overlap: startY=$overlapStartY, height=$overlapHeight")
PGHLog.d(TAG, "🎨 Result: combinedH=$combinedHeight, nonOverlapH=$nonOverlapHeight")
return Mat(combinedHeight, combinedWidth, baseImage.type()).apply {
try
{
// Copy entire base image first
val baseRect = Rect(0, 0, baseImage.cols(), baseImage.rows())
baseImage.submat(baseRect).copyTo(this.submat(baseRect))
// Calculate where the new image should start (after base image minus overlap)
val newImageStartY = baseImage.rows() - overlapHeight
// Blend overlap region (in the base image area)
if (overlapHeight > 0)
{
blendOverlapRegion(
baseImage, newImage, this,
newImageStartY, overlapHeight
)
}
// Copy non-overlapping part of new image
if (nonOverlapHeight > 0)
{
val newBottomRect = Rect(0, baseImage.rows(), newImage.cols(), nonOverlapHeight)
val newSourceRect = Rect(0, overlapHeight, newImage.cols(), nonOverlapHeight)
newImage.submat(newSourceRect).copyTo(this.submat(newBottomRect))
}
PGHLog.d(TAG, "🎨 Blended image with overlap: ${this.cols()}x${this.rows()}")
}
catch (e: Exception)
{
PGHLog.e(TAG, "❌ Error blending image", e)
this.release()
throw e
}
}
}
private fun blendOverlapRegion(baseImage: Mat, newImage: Mat, result: Mat, startY: Int, height: Int)
{
try
{
PGHLog.d(TAG, "🎨 Blending overlap: startY=$startY, height=$height")
// Define rectangles for blending
val resultBlendRect = Rect(0, startY, min(baseImage.cols(), newImage.cols()), height)
val baseOverlapRect = Rect(0, startY, baseImage.cols(), height)
val newOverlapRect = Rect(0, 0, newImage.cols(), height)
val baseOverlap = Mat()
val newOverlap = Mat()
val blended = Mat()
try
{
baseImage.submat(baseOverlapRect).copyTo(baseOverlap)
newImage.submat(newOverlapRect).copyTo(newOverlap)
// Linear blend: 50% base + 50% new
Core.addWeighted(baseOverlap, 0.5, newOverlap, 0.5, 0.0, blended)
blended.copyTo(result.submat(resultBlendRect))
PGHLog.d(TAG, "🎨 Blend completed successfully")
}
finally
{
baseOverlap.release()
newOverlap.release()
blended.release()
}
}
catch (e: Exception)
{
PGHLog.e(TAG, "❌ Error in overlap blending", e)
}
}
private fun assessStitchingQuality(stitchedImage: Mat, originalCount: Int): Float
{
// Simple quality assessment based on:
// 1. Image dimensions (reasonable aspect ratio)
// 2. Non-zero pixels ratio
// 3. Smoothness metric
try
{
val aspectRatio = stitchedImage.rows().toFloat() / stitchedImage.cols()
val aspectScore = if (aspectRatio > 1.0f) min(1.0f, aspectRatio / (originalCount * 2)) else 0.2f
// Check for reasonable content (non-zero pixels)
val nonZeroPixels = Core.countNonZero(Mat().apply {
Imgproc.cvtColor(stitchedImage, this, Imgproc.COLOR_BGR2GRAY)
})
val totalPixels = stitchedImage.total().toInt()
val contentScore = nonZeroPixels.toFloat() / totalPixels
val finalScore = (aspectScore + contentScore) / 2.0f
return min(1.0f, max(0.0f, finalScore))
}
catch (e: Exception)
{
PGHLog.e(TAG, "❌ Error assessing quality", e)
return 0.5f // Default moderate quality
}
}
private fun saveStitchedImage(image: Mat): String?
{
return try
{
val timestamp = System.currentTimeMillis()
val filename = "stitched_screenshot_${timestamp}.png"
val outputFile = File(outputDir, filename)
val success = Imgcodecs.imwrite(outputFile.absolutePath, image)
if (success)
{
PGHLog.i(TAG, "💾 Saved stitched image: ${outputFile.absolutePath}")
outputFile.absolutePath
}
else
{
PGHLog.e(TAG, "❌ Failed to save stitched image")
null
}
}
catch (e: Exception)
{
PGHLog.e(TAG, "❌ Error saving stitched image", e)
null
}
}
private fun copySingleScreenshot(screenshot: CapturedScreenshot): StitchingResult
{
return try
{
val timestamp = System.currentTimeMillis()
val filename = "single_screenshot_${timestamp}.png"
val outputFile = File(outputDir, filename)
// Copy file
File(screenshot.filePath).copyTo(outputFile, overwrite = true)
StitchingResult(
success = true,
outputPath = outputFile.absolutePath,
qualityScore = 1.0f,
processedCount = 1,
finalDimensions = Pair(screenshot.width, screenshot.height)
)
}
catch (e: Exception)
{
PGHLog.e(TAG, "❌ Error copying single screenshot", e)
StitchingResult(false, errorMessage = "Failed to copy single screenshot: ${e.message}")
}
}
/**
* Clean up resources and cancel any ongoing operations
*/
fun cleanup()
{
try
{
PGHLog.i(TAG, "🧹 Cleaning up ImageStitcher")
stitchingScope.cancel()
}
catch (e: Exception)
{
PGHLog.e(TAG, "❌ Error during cleanup", e)
}
}
}

3
app/src/main/java/com/quillstudios/pokegoalshelper/ui/EnhancedFloatingFAB.kt

@ -36,6 +36,7 @@ class EnhancedFloatingFAB(
private val onLongScreenshotStart: () -> Unit,
private val onLongScreenshotCapture: () -> Unit,
private val onLongScreenshotFinish: () -> Unit,
private val onLongScreenshotFinishAndStitch: () -> Unit,
private val onLongScreenshotCancel: () -> Unit
) {
companion object {
@ -228,7 +229,7 @@ class EnhancedFloatingFAB(
onLongScreenshotCapture()
},
MenuItemData("✅ FINISH", android.R.drawable.ic_menu_save, android.R.color.holo_green_dark) {
onLongScreenshotFinish()
onLongScreenshotFinishAndStitch()
exitLongScreenshotMode()
},
MenuItemData("❌ CANCEL", android.R.drawable.ic_menu_close_clear_cancel, android.R.color.holo_red_dark) {

33
app/src/main/java/com/quillstudios/pokegoalshelper/ui/interfaces/DetectionUIEvents.kt

@ -44,6 +44,11 @@ interface DetectionUIEvents {
*/
fun onLongScreenshotFinish()
/**
* Triggered when user finishes long screenshot collection and wants to stitch
*/
fun onLongScreenshotFinishAndStitch()
/**
* Triggered when user cancels long screenshot collection
*/
@ -103,4 +108,32 @@ interface DetectionUICallbacks {
* @param error Error message
*/
fun onLongScreenshotError(error: String)
// === Image Stitching Callbacks ===
/**
* Called when image stitching process starts
*/
fun onStitchingStarted()
/**
* Called during stitching to report progress
* @param progress Progress percentage (0.0 to 1.0)
* @param message Status message
*/
fun onStitchingProgress(progress: Float, message: String)
/**
* Called when stitching completes successfully
* @param outputPath Path to the stitched image
* @param qualityScore Quality score of the stitching (0.0 to 1.0)
* @param dimensions Final image dimensions (width, height)
*/
fun onStitchingCompleted(outputPath: String, qualityScore: Float, dimensions: Pair<Int, Int>)
/**
* Called when stitching fails
* @param error Error message
*/
fun onStitchingFailed(error: String)
}
Loading…
Cancel
Save