This repository has no description
0

Configure Feed

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

perf: parallelize pose + object detection and reduce throttle frequency

Run pose detection on a dedicated thread concurrently with object
detection so per-frame latency is max(pose, object) instead of the
sum. Also bump detection throttle from 20ms (~50 FPS) to 33ms (~30 FPS)
to reduce CPU contention with minimal perceptual difference.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

+37 -22
+4 -3
posedetection/src/androidMain/kotlin/com.performancecoachlab/posedetection/camera/CameraView.android.kt
··· 123 123 var latestObjects by remember { mutableStateOf<List<AnalysisObject>>(emptyList()) } 124 124 125 125 // Throttle detectors to avoid doing heavy work every frame. 126 - // Tune these to balance smoothness vs CPU usage. 127 - val objectIntervalMs = 20L 128 - val poseIntervalMs = 20L 126 + // 33ms ≈ 30 FPS target — sufficient for smooth real-time feedback 127 + // while leaving CPU headroom for recording and other work. 128 + val objectIntervalMs = 33L 129 + val poseIntervalMs = 33L 129 130 val lastObjectRunAtMs = remember { AtomicLong(0L) } 130 131 val lastPoseRunAtMs = remember { AtomicLong(0L) } 131 132
+33 -19
posedetection/src/androidMain/kotlin/com/performancecoachlab/posedetection/camera/Utils.android.kt
··· 28 28 import org.tensorflow.lite.support.image.TensorImage 29 29 import org.tensorflow.lite.support.tensorbuffer.TensorBuffer 30 30 import java.nio.ByteBuffer 31 + import java.util.concurrent.Executors 31 32 import kotlin.math.absoluteValue 32 33 import kotlin.math.max 33 34 import kotlin.math.min ··· 255 256 ) 256 257 } 257 258 259 + // Shared executor for running pose detection in parallel with object detection. 260 + private val poseExecutor = Executors.newSingleThreadExecutor() 261 + 258 262 // Core processing used by both live camera (ImageProxy) and offline frames (FrameAnalyser). 259 263 fun process( 260 264 tensorImage: TensorImage?, ··· 269 273 mlKitScaleX: Float = 1f, 270 274 mlKitScaleY: Float = 1f, 271 275 ) { 276 + // Launch pose detection on a separate thread so it runs in parallel with object detection. 277 + val poseFuture = if (poseDetector != null && mlKitImage != null) { 278 + poseExecutor.submit<Skeleton?> { 279 + runCatching { 280 + val pose = Tasks.await(poseDetector.process(mlKitImage)) 281 + skeletonFromPoseScaled( 282 + pose = pose, 283 + timestamp = timestamp, 284 + width = width, 285 + height = height, 286 + scaleX = mlKitScaleX, 287 + scaleY = mlKitScaleY, 288 + ) 289 + }.onFailure { t -> 290 + Logger.e(t) { "MLKit poseDetector.process failed" } 291 + }.getOrNull() 292 + } 293 + } else null 294 + 295 + // Run object detection on the current thread (concurrent with pose above). 272 296 val objectsDetected = if (objectDetector != null && tensorImage != null) { 273 297 val outputShape = objectDetector.modelInfo.outputShape 274 298 val output = TensorBuffer.createFixedSize(outputShape, DataType.FLOAT32) ··· 297 321 isElementsFirst = false 298 322 } 299 323 300 - else -> return onComplete( 301 - AnalysisResult(skeleton = null, objects = emptyList()), 302 - bitmap 303 - ) 324 + else -> { 325 + val skeleton = poseFuture?.get() 326 + return onComplete( 327 + AnalysisResult(skeleton = skeleton, objects = emptyList()), 328 + bitmap 329 + ) 330 + } 304 331 } 305 332 306 333 fun valueAt(elementIndex: Int, channelIndex: Int): Float { ··· 356 383 } 357 384 } else emptyList() 358 385 359 - val skeleton: Skeleton? = if (poseDetector != null && mlKitImage != null) { 360 - runCatching { 361 - val pose = Tasks.await(poseDetector.process(mlKitImage)) 362 - skeletonFromPoseScaled( 363 - pose = pose, 364 - timestamp = timestamp, 365 - width = width, 366 - height = height, 367 - scaleX = mlKitScaleX, 368 - scaleY = mlKitScaleY, 369 - ) 370 - }.onFailure { t -> 371 - Logger.e(t) { "MLKit poseDetector.process failed" } 372 - }.getOrNull() 373 - } else null 386 + // Wait for pose result (already running or already done). 387 + val skeleton = poseFuture?.get() 374 388 375 389 onComplete( 376 390 AnalysisResult(