This repository has no description
0

Configure Feed

Select the types of activity you want to include in your feed.

fix: improve efficiency

+136 -21
+41 -16
posedetection/src/androidMain/kotlin/com.performancecoachlab/posedetection/camera/CameraView.android.kt
··· 60 60 import androidx.camera.core.CameraInfo 61 61 import androidx.camera.core.CameraSelector 62 62 import co.touchlab.kermit.Logger 63 + import java.util.concurrent.atomic.AtomicBoolean 63 64 64 65 // Data class to hold recording state for each recording ID 65 66 data class RecordingSlot( ··· 235 236 236 237 } 237 238 239 + val gate = remember { FrameGate() } 240 + 238 241 LaunchedEffect(lifecycleOwner, frontCamera) { 239 242 val cameraProvider = ProcessCameraProvider.getInstance(context).get() 240 243 cameraProvider.unbindAll() ··· 248 251 val preview = Preview.Builder().build().also { 249 252 it.surfaceProvider = previewView.surfaceProvider 250 253 } 251 - val imageAnalysis = ImageAnalysis.Builder().build().also { analysis -> 252 - analysis.setAnalyzer(executor) { imageProxy -> 253 - val timestamp = System.currentTimeMillis() 254 - val area = focus 255 - val poseClient = if (currentDetectMode.doPose()) poseDetector else null 256 - val objectClient = if (currentDetectMode.doObject()) objectDetector?.getDetector() else null 257 - imageProxy.process( 258 - objectClient, poseClient, timestamp, area 259 - ){ analysisResult, _bitmap -> 260 - customObjectRepository.updateCustomObject(analysisResult.objects) 261 - analysisResult.skeleton?.let { skel -> 262 - skeletonRepository.updateSkeleton(skel) 254 + val imageAnalysis = ImageAnalysis.Builder() 255 + .setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST) 256 + .setOutputImageFormat(ImageAnalysis.OUTPUT_IMAGE_FORMAT_RGBA_8888) 257 + .build() 258 + .also { analysis -> 259 + analysis.setAnalyzer(executor) { imageProxy -> 260 + // Drop frames while we're still working on the previous one. 261 + if (!gate.tryEnter()) { 262 + imageProxy.close() 263 + return@setAnalyzer 263 264 } 264 - bitmap = _bitmap.asImageBitmap().let { inbmp -> 265 + 266 + val timestamp = System.currentTimeMillis() 267 + val area = focus 268 + val poseClient = if (currentDetectMode.doPose()) poseDetector else null 269 + val objectClient = if (currentDetectMode.doObject()) objectDetector?.getDetector() else null 270 + imageProxy.process( 271 + objectClient, poseClient, timestamp, area 272 + ) { analysisResult, _bitmap -> 273 + try { 274 + customObjectRepository.updateCustomObject(analysisResult.objects) 275 + analysisResult.skeleton?.let { skel -> 276 + skeletonRepository.updateSkeleton(skel) 277 + } 278 + bitmap = _bitmap.asImageBitmap().let { inbmp -> 265 279 controller?.setRequestDataProvider { 266 280 CameraViewData( 267 281 width = inbmp.width.toFloat(), ··· 276 290 ) 277 291 } 278 292 addFrameToActiveRecordings(inbmp, timestamp) 279 - inbmp.drawResults(if(drawSkeleton) analysisResult.skeleton else null, drawObjects?.invoke(analysisResult.objects)?: emptyList()) 293 + inbmp.drawResults( 294 + if (drawSkeleton) analysisResult.skeleton else null, 295 + drawObjects?.invoke(analysisResult.objects) ?: emptyList() 296 + ) 280 297 } 281 - imageProxy.close() 298 + } finally { 299 + gate.exit() 300 + } 301 + } 282 302 } 283 303 } 284 - } 285 304 val camera = cameraProvider.bindToLifecycle( 286 305 lifecycleOwner, 287 306 cameraSelector, ··· 366 385 height = height.toFloat(), 367 386 ) 368 387 } 388 + 389 + private class FrameGate { 390 + private val busy = AtomicBoolean(false) 391 + fun tryEnter(): Boolean = busy.compareAndSet(false, true) 392 + fun exit() { busy.set(false) } 393 + }
+95 -5
posedetection/src/androidMain/kotlin/com/performancecoachlab/posedetection/camera/Utils.android.kt
··· 68 68 } 69 69 } 70 70 71 + /** 72 + * Separate pool for analysis/masking bitmaps (must match analysisBitmap size). 73 + * We keep a single instance because analysis runs on a single-thread executor. 74 + */ 75 + private object AnalysisBitmapPool { 76 + private var cached: Bitmap? = null 77 + private var cachedW: Int = 0 78 + private var cachedH: Int = 0 79 + private var cachedConfig: Bitmap.Config = Bitmap.Config.ARGB_8888 80 + 81 + fun obtain(width: Int, height: Int, config: Bitmap.Config = Bitmap.Config.ARGB_8888): Bitmap { 82 + val bmp = cached 83 + return if ( 84 + bmp != null && 85 + !bmp.isRecycled && 86 + cachedW == width && 87 + cachedH == height && 88 + cachedConfig == config 89 + ) { 90 + // Clear the bitmap; canvas draw will overwrite but masking may not cover entire area. 91 + bmp.eraseColor(android.graphics.Color.TRANSPARENT) 92 + bmp 93 + } else { 94 + createBitmap(width, height, config).also { newBmp -> 95 + cached = newBmp 96 + cachedW = width 97 + cachedH = height 98 + cachedConfig = config 99 + } 100 + } 101 + } 102 + } 103 + 71 104 private fun Bitmap.rotateToNew(degrees: Int): Bitmap { 72 105 if (degrees % 360 == 0) return this 73 106 val m = Matrix().apply { postRotate(degrees.toFloat()) } ··· 88 121 focusArea: Rect?, 89 122 onComplete: (AnalysisResult, Bitmap) -> Unit 90 123 ) { 91 - if (objectDetector == null && poseDetector == null) return 124 + // If both are null, still close to avoid stalling the camera pipeline. 125 + if (objectDetector == null && poseDetector == null) { 126 + close() 127 + return 128 + } 92 129 93 130 val rotationDegrees = imageInfo.rotationDegrees 94 131 95 - // 1) Create ONE rotated bitmap in "analysis space" (used by MLKit + drawing + coordinate mapping). 132 + // Convert to bitmap ASAP so we can close the ImageProxy immediately. 96 133 val analysisBitmap: Bitmap = toBitmap().rotateToNew(rotationDegrees) 134 + close() 97 135 98 136 // 2) MLKit image must match analysisBitmap coordinate space. Rotation is now 0. 99 137 val mlKitImage: InputImage? = poseDetector?.let { 100 - val masked = analysisBitmap.applyFocusAreaMask(focusArea, rotationDegrees) 138 + val masked = analysisBitmap.applyFocusAreaMaskPooled(focusArea, rotationDegrees) 101 139 InputImage.fromBitmap(masked, 0) 102 140 } 103 141 ··· 126 164 } 127 165 } 128 166 129 - // If no objectDetector, we still want pose results; if no poseDetector, we still want objects. 130 167 process( 131 168 tensorImage = processedTensorImage, 132 169 mlKitImage = mlKitImage, ··· 298 335 299 336 300 337 fun Bitmap.applyFocusAreaMask(focusArea: Rect?, angle: Int = 0): Bitmap { 338 + // Keep old API for call sites that truly want a new bitmap. 301 339 return focusArea?.let { rect -> 302 340 val result = this.copy(this.config ?: Bitmap.Config.ARGB_8888, true) 303 341 val canvas = Canvas(result) ··· 312 350 right = rect.bottom, 313 351 bottom = 1f - rect.left 314 352 ) 353 + 315 354 180 -> Rect( 316 355 left = 1f - rect.right, 317 356 top = 1f - rect.bottom, 318 357 right = 1f - rect.left, 319 358 bottom = 1f - rect.top 320 359 ) 360 + 321 361 270 -> Rect( 322 362 left = 1f - rect.bottom, 323 363 top = rect.left, 324 364 right = 1f - rect.top, 325 365 bottom = rect.right 326 366 ) 367 + 327 368 else -> rect // 0 degrees or any other angle 328 369 } 329 370 ··· 336 377 337 378 // Black out bottom area 338 379 if (focusRect.bottom < height) { 339 - canvas.drawRect(0f, focusRect.bottom.toFloat(), width.toFloat(),height.toFloat(), paint) 380 + canvas.drawRect(0f, focusRect.bottom.toFloat(), width.toFloat(), height.toFloat(), paint) 340 381 } 341 382 342 383 // Black out left area ··· 351 392 352 393 result 353 394 } ?: this 395 + } 396 + 397 + /** 398 + * More efficient variant of applyFocusAreaMask: draws into a pooled bitmap, avoiding Bitmap.copy(). 399 + * Returns a bitmap in the same size/coords as the receiver. 400 + */ 401 + fun Bitmap.applyFocusAreaMaskPooled(focusArea: Rect?, angle: Int = 0): Bitmap { 402 + if (focusArea == null) return this 403 + 404 + val out = AnalysisBitmapPool.obtain(width, height, this.config ?: Bitmap.Config.ARGB_8888) 405 + val canvas = Canvas(out) 406 + // Draw the source into the pooled bitmap. 407 + canvas.drawBitmap(this, 0f, 0f, null) 408 + 409 + val paint = Paint().apply { color = android.graphics.Color.BLACK } 410 + 411 + val transformedRect = when (angle % 360) { 412 + 90 -> Rect( 413 + left = focusArea.top, 414 + top = 1f - focusArea.right, 415 + right = focusArea.bottom, 416 + bottom = 1f - focusArea.left 417 + ) 418 + 419 + 180 -> Rect( 420 + left = 1f - focusArea.right, 421 + top = 1f - focusArea.bottom, 422 + right = 1f - focusArea.left, 423 + bottom = 1f - focusArea.top 424 + ) 425 + 426 + 270 -> Rect( 427 + left = 1f - focusArea.bottom, 428 + top = focusArea.left, 429 + right = 1f - focusArea.top, 430 + bottom = focusArea.right 431 + ) 432 + 433 + else -> focusArea 434 + } 435 + 436 + val focusRect = transformedRect.toGraphicsRect(width, height) 437 + 438 + if (focusRect.top > 0) canvas.drawRect(0f, 0f, width.toFloat(), focusRect.top.toFloat(), paint) 439 + if (focusRect.bottom < height) canvas.drawRect(0f, focusRect.bottom.toFloat(), width.toFloat(), height.toFloat(), paint) 440 + if (focusRect.left > 0) canvas.drawRect(0f, focusRect.top.toFloat(), focusRect.left.toFloat(), focusRect.bottom.toFloat(), paint) 441 + if (focusRect.right < width) canvas.drawRect(focusRect.right.toFloat(), focusRect.top.toFloat(), width.toFloat(), focusRect.bottom.toFloat(), paint) 442 + 443 + return out 354 444 } 355 445 356 446 data class BoundingBox(