···
213
213
val analysisBitmap: Bitmap = baseBitmap.rotateIntoPooled(rotationDegrees)
214
214
215
215
// 2) MLKit image must match analysisBitmap coordinate space. Rotation is now 0.
216
216
+
// IMPORTANT: focusArea is defined in normalized coordinates of the *displayed* (upright) frame,
217
217
+
// so apply it with angle=0 in this already-rotated bitmap coordinate space.
216
218
val mlKitImage: InputImage? = poseDetector?.let {
217
217
-
val masked = analysisBitmap.applyFocusAreaMaskPooled(focusArea, rotationDegrees)
218
218
-
InputImage.fromBitmap(masked, 0)
219
219
+
if (focusArea == null) {
220
220
+
InputImage.fromBitmap(analysisBitmap, 0)
221
221
+
} else {
222
222
+
// Create the masked bitmap into a dedicated pool so we never overwrite analysisBitmap
223
223
+
val out = PoseMaskBitmapPool.obtain(
224
224
+
analysisBitmap.width,
225
225
+
analysisBitmap.height,
226
226
+
analysisBitmap.config ?: Bitmap.Config.ARGB_8888
227
227
+
)
228
228
+
val canvas = Canvas(out)
229
229
+
canvas.drawBitmap(analysisBitmap, 0f, 0f, null)
230
230
+
231
231
+
// Apply mask in upright coordinates.
232
232
+
out.applyFocusAreaMaskInPlace(
233
233
+
focusArea = focusArea,
234
234
+
angle = 0,
235
235
+
)
236
236
+
237
237
+
InputImage.fromBitmap(out, 0)
238
238
+
}
219
239
}
220
240
221
241
// 3) Tensor input: resize into pooled bitmap (avoid allocating each frame).
···
439
459
} ?: this
440
460
}
441
461
442
442
-
/**
443
443
-
* More efficient variant of applyFocusAreaMask: draws into a pooled bitmap, avoiding Bitmap.copy().
444
444
-
* Returns a bitmap in the same size/coords as the receiver.
445
445
-
*/
446
446
-
fun Bitmap.applyFocusAreaMaskPooled(focusArea: Rect?, angle: Int = 0): Bitmap {
447
447
-
if (focusArea == null) return this
462
462
+
/** Pool solely for pose-masked bitmaps so we never overwrite the analysis bitmap used by object detection. */
463
463
+
private object PoseMaskBitmapPool {
464
464
+
private var cached: Bitmap? = null
465
465
+
private var cachedW: Int = 0
466
466
+
private var cachedH: Int = 0
467
467
+
private var cachedConfig: Bitmap.Config = Bitmap.Config.ARGB_8888
448
468
449
449
-
val out = AnalysisBitmapPool.obtain(width, height, this.config ?: Bitmap.Config.ARGB_8888)
450
450
-
val canvas = Canvas(out)
451
451
-
// Draw the source into the pooled bitmap.
452
452
-
canvas.drawBitmap(this, 0f, 0f, null)
469
469
+
fun obtain(width: Int, height: Int, config: Bitmap.Config = Bitmap.Config.ARGB_8888): Bitmap {
470
470
+
val bmp = cached
471
471
+
return if (
472
472
+
bmp != null &&
473
473
+
!bmp.isRecycled &&
474
474
+
cachedW == width &&
475
475
+
cachedH == height &&
476
476
+
cachedConfig == config
477
477
+
) {
478
478
+
bmp.eraseColor(android.graphics.Color.TRANSPARENT)
479
479
+
bmp
480
480
+
} else {
481
481
+
createBitmap(width, height, config).also { newBmp ->
482
482
+
cached = newBmp
483
483
+
cachedW = width
484
484
+
cachedH = height
485
485
+
cachedConfig = config
486
486
+
}
487
487
+
}
488
488
+
}
489
489
+
}
453
490
491
491
+
/** In-place masking onto the receiver bitmap; assumes the receiver already contains the frame pixels. */
492
492
+
private fun Bitmap.applyFocusAreaMaskInPlace(focusArea: Rect, angle: Int = 0) {
493
493
+
val area = focusArea.normalizedClamped()
494
494
+
if (area.isEffectivelyFullViewport()) return
495
495
+
if (area.width <= 1e-4f || area.height <= 1e-4f) return
496
496
+
497
497
+
val canvas = Canvas(this)
454
498
val paint = Paint().apply { color = android.graphics.Color.BLACK }
455
499
456
500
val transformedRect = when (angle % 360) {
457
501
90 -> Rect(
458
458
-
left = focusArea.top,
459
459
-
top = 1f - focusArea.right,
460
460
-
right = focusArea.bottom,
461
461
-
bottom = 1f - focusArea.left
502
502
+
left = area.top,
503
503
+
top = 1f - area.right,
504
504
+
right = area.bottom,
505
505
+
bottom = 1f - area.left
462
506
)
463
507
464
508
180 -> Rect(
465
465
-
left = 1f - focusArea.right,
466
466
-
top = 1f - focusArea.bottom,
467
467
-
right = 1f - focusArea.left,
468
468
-
bottom = 1f - focusArea.top
509
509
+
left = 1f - area.right,
510
510
+
top = 1f - area.bottom,
511
511
+
right = 1f - area.left,
512
512
+
bottom = 1f - area.top
469
513
)
470
514
471
515
270 -> Rect(
472
472
-
left = 1f - focusArea.bottom,
473
473
-
top = focusArea.left,
474
474
-
right = 1f - focusArea.top,
475
475
-
bottom = focusArea.right
516
516
+
left = 1f - area.bottom,
517
517
+
top = area.left,
518
518
+
right = 1f - area.top,
519
519
+
bottom = area.right
476
520
)
477
521
478
478
-
else -> focusArea
522
522
+
else -> area
479
523
}
480
524
481
525
val focusRect = transformedRect.toGraphicsRect(width, height)
···
484
528
if (focusRect.bottom < height) canvas.drawRect(0f, focusRect.bottom.toFloat(), width.toFloat(), height.toFloat(), paint)
485
529
if (focusRect.left > 0) canvas.drawRect(0f, focusRect.top.toFloat(), focusRect.left.toFloat(), focusRect.bottom.toFloat(), paint)
486
530
if (focusRect.right < width) canvas.drawRect(focusRect.right.toFloat(), focusRect.top.toFloat(), width.toFloat(), focusRect.bottom.toFloat(), paint)
531
531
+
}
487
532
533
533
+
// Keep applyFocusAreaMaskPooled for other call sites, but implement it via the in-place helper.
534
534
+
fun Bitmap.applyFocusAreaMaskPooled(focusArea: Rect?, angle: Int = 0): Bitmap {
535
535
+
if (focusArea == null) return this
536
536
+
537
537
+
val out = AnalysisBitmapPool.obtain(width, height, this.config ?: Bitmap.Config.ARGB_8888)
538
538
+
val canvas = Canvas(out)
539
539
+
canvas.drawBitmap(this, 0f, 0f, null)
540
540
+
541
541
+
out.applyFocusAreaMaskInPlace(focusArea, angle)
488
542
return out
489
543
}
490
544
···
509
563
Logger.d{ "ModelInfo.label: $cls not in labels" }
510
564
return "$cls"
511
565
}
566
566
+
567
567
+
private fun Rect.isEffectivelyFullViewport(eps: Float = 1e-4f): Boolean {
568
568
+
return left <= 0f + eps && top <= 0f + eps && right >= 1f - eps && bottom >= 1f - eps
569
569
+
}
570
570
+
571
571
+
private fun Rect.normalizedClamped(): Rect {
572
572
+
val l = left.coerceIn(0f, 1f)
573
573
+
val t = top.coerceIn(0f, 1f)
574
574
+
val r = right.coerceIn(0f, 1f)
575
575
+
val b = bottom.coerceIn(0f, 1f)
576
576
+
val leftN = min(l, r)
577
577
+
val rightN = max(l, r)
578
578
+
val topN = min(t, b)
579
579
+
val bottomN = max(t, b)
580
580
+
return Rect(left = leftN, top = topN, right = rightN, bottom = bottomN)
581
581
+
}
···
377
377
},
378
378
objectModel = generalModel,
379
379
modifier = Modifier.weight(1f),
380
380
-
// focusArea = Rect(0f,0f,0.1f,1f),
381
381
-
focusArea = null,
380
380
+
focusArea = Rect(0f,0f,1f,1f),
382
381
frontCamera = frontCamera,
383
382
useUltraWide = ultrawide,
384
383
recordingId = recordingId,