Added tuning feature and hand palm action to reset the timer

This commit is contained in:
harine
2026-09-07 19:38:43 +08:00
parent 41b567929a
commit e7cb217de8
18 changed files with 1164 additions and 178 deletions
@@ -0,0 +1,73 @@
package com.example.jnicpp.bowling
import android.graphics.Bitmap
import android.media.MediaMetadataRetriever
import android.provider.MediaStore
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry
import org.junit.Assert.assertNotNull
import org.junit.Test
import org.junit.runner.RunWith
import java.io.File
import java.io.FileOutputStream
/**
* Diagnostic tool: extracts specific frames from a recorded video at given
* millisecond offsets, saving each as a PNG for visual inspection. Used to
* spot-check whether a confirmed step timestamp from a debug trace
* corresponds to real foot motion in the recorded video's baked-in
* skeleton overlay, or to a bowler standing still.
*
* Reads the video by querying its MediaStore content Uri (by display
* name) rather than copying it into app-external storage first --
* `connectedAndroidTest` reinstalls the app package before every run,
* which wipes app-external storage each time, but the video's own
* MediaStore entry (it was saved into the public Movies/bowling
* collection by CameraXController.startRecording) is untouched by that.
*/
@RunWith(AndroidJUnit4::class)
class FrameExtractTest {
@Test
fun extractFrames() {
val context = InstrumentationRegistry.getInstrumentation().targetContext
val displayName = "bowling_20260907_183828.mp4"
val uri = MediaStore.Video.Media.EXTERNAL_CONTENT_URI
val projection = arrayOf(MediaStore.Video.Media._ID, MediaStore.Video.Media.DISPLAY_NAME)
var videoUri: android.net.Uri? = null
var totalRows = 0
val names = StringBuilder()
context.contentResolver.query(uri, projection, null, null, null)?.use { cursor ->
while (cursor.moveToNext()) {
totalRows++
val name = cursor.getString(cursor.getColumnIndexOrThrow(MediaStore.Video.Media.DISPLAY_NAME))
if (totalRows <= 5) names.append(name).append("; ")
if (name == displayName) {
val id = cursor.getLong(cursor.getColumnIndexOrThrow(MediaStore.Video.Media._ID))
videoUri = android.content.ContentUris.withAppendedId(uri, id)
}
}
}
assertNotNull("video '$displayName' not found in MediaStore (saw $totalRows rows, e.g.: $names)", videoUri)
val outDir = context.getExternalFilesDir(null)!!.resolve("frames").apply { mkdirs() }
// Millisecond offsets into the video to extract, set per-investigation.
val offsetsMs = listOf(59526L, 65668L, 102581L, 135492L, 7288L, 8657L)
val retriever = MediaMetadataRetriever()
retriever.setDataSource(context, videoUri)
for (offsetMs in offsetsMs) {
val bitmap: Bitmap? = retriever.getFrameAtTime(offsetMs * 1000, MediaMetadataRetriever.OPTION_CLOSEST)
if (bitmap != null) {
val file = File(outDir, "frame_${offsetMs}.png")
FileOutputStream(file).use { out ->
bitmap.compress(Bitmap.CompressFormat.PNG, 90, out)
}
}
}
retriever.release()
}
}
@@ -61,7 +61,7 @@ class VideoStepReplayTest {
val frame = buildPoseFrame(t, landmarks, smoothedAnkleHip, angles)
val result = liveStepDetector.update(frame)
finalStepCount = result.stepCount
logger.log(landmarks, t, finalStepCount)
logger.log(landmarks, t, finalStepCount, result.handRaiseProgress)
framesProcessed++
}
t += stepMs