52 lines
2.3 KiB
Kotlin
52 lines
2.3 KiB
Kotlin
package com.example.jnicpp.bowling
|
|||
|
|
|
||
|
|
import com.google.mlkit.vision.pose.Pose
|
||
|
|
|
||
|
|
/**
|
||
|
|
* A single pose landmark's position and detection confidence, smoothed
|
||
|
|
* across frames by [PoseLandmarkSmoother]. Deliberately independent of ML
|
||
|
|
* Kit's own `PoseLandmark`/`PointF3D` so downstream consumers (angle math,
|
||
|
|
* skeleton drawing) don't need any ML Kit types.
|
||
|
|
*/
|
||
|
|
data class SmoothedLandmark(val x: Float, val y: Float, val inFrameLikelihood: Float)
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Low-pass-filters ML Kit's per-frame [Pose] landmarks with an exponential
|
||
|
|
* moving average, so the drawn skeleton doesn't visibly jitter/flicker from
|
||
|
|
* frame-to-frame detector noise. This smooths `inFrameLikelihood` too, not
|
||
|
|
* just position -- without that, a landmark hovering right around
|
||
|
|
* [PoseSkeletonRenderer.MIN_LIKELIHOOD] makes whole bones repeatedly pop in
|
||
|
|
* and out, which reads as flicker just as much as position jitter does.
|
||
|
|
*
|
||
|
|
* State is per-landmark-type and carries across calls to [smooth], so this
|
||
|
|
* is meant as one instance per detection stream (i.e. per [PoseAnalyzer]) --
|
||
|
|
* create a new one whenever the stream restarts rather than reusing one
|
||
|
|
* across unrelated streams, or the first frame of the new stream will lerp
|
||
|
|
* in from the old stream's last pose.
|
||
|
|
*/
|
||
|
|
class PoseLandmarkSmoother(
|
||
|
|
// Weight given to each new sample; lower = smoother but more lag behind
|
||
|
|
// the true position. 0.4 noticeably cuts jitter while still keeping up
|
||
|
|
// with a fast bowling arm swing.
|
||
|
|
private val smoothingFactor: Float = 0.4f
|
||
|
|
) {
|
||
|
|
private val previous = mutableMapOf<Int, SmoothedLandmark>()
|
||
|
|
|
||
|
|
fun smooth(pose: Pose): Map<Int, SmoothedLandmark> {
|
||
|
|
for (landmark in pose.allPoseLandmarks) {
|
||
|
|
val prev = previous[landmark.landmarkType]
|
||
|
|
val next = if (prev == null) {
|
||
|
|
SmoothedLandmark(landmark.position.x, landmark.position.y, landmark.inFrameLikelihood)
|
||
|
|
} else {
|
||
|
|
SmoothedLandmark(
|
||
|
|
x = prev.x + smoothingFactor * (landmark.position.x - prev.x),
|
||
|
|
y = prev.y + smoothingFactor * (landmark.position.y - prev.y),
|
||
|
|
inFrameLikelihood = prev.inFrameLikelihood +
|
||
|
|
smoothingFactor * (landmark.inFrameLikelihood - prev.inFrameLikelihood)
|
||
|
|
)
|
||
|
|
}
|
||
|
|
previous[landmark.landmarkType] = next
|
||
|
|
}
|
||
|
|
return previous.toMap()
|
||
|
|
}
|
||
|
|
}
|