This repository has no description
0

Configure Feed

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

Merge branch 'release-4.12.0' into release-4.12.1

# Conflicts:
# posedetection/build.gradle.kts
# sample/composeApp/src/commonMain/kotlin/com/nate/posedetection/App.kt

+6 -490
+1 -1
README.md
··· 11 11 Import the Compose library 12 12 13 13 ```kotlin 14 - implementation("com.performancecoachlab.posedetection:posedetection-compose:4.9.0") 14 + implementation("com.performancecoachlab.posedetection:posedetection-compose:4.12.0") 15 15 ``` 16 16 17 17 Add camera use to your android manifest
+1 -1
posedetection/build.gradle.kts
··· 31 31 developerConnection.set("scm:git:ssh://git@tangled.sh:nateholland.bsky.social/PoseDetection") 32 32 } 33 33 } 34 - //signAllPublications() 34 + signAllPublications() 35 35 } 36 36 plugins { 37 37 alias(libs.plugins.multiplatform)
+2 -1
posedetection/src/iosMain/kotlin/com/performancecoachlab/posedetection/camera/FrameProcessor.kt
··· 330 330 onObjectsProcessed(emptyList()) 331 331 return@VNCoreMLRequest 332 332 } 333 - val results = request?.results as? List<*> ?: emptyList<Any>() 333 + val results = request?.results 334 + as? List<*> ?: emptyList<Any>() 334 335 val recognized = 335 336 results.filterIsInstance<VNRecognizedObjectObservation>() 336 337 val analysisObjects = recognized.map { observation ->
sample/composeApp/src/androidMain/assets/yolo11n_dataset640_20260410_123550__best_float32.tflite

This is a binary file and will not be displayed.

sample/composeApp/src/androidMain/assets/yolo11n_dataset_dataset.tflite

This is a binary file and will not be displayed.

sample/composeApp/src/androidMain/assets/yolo11n_dataset_lb416_20260410_145848__best_float32.tflite

This is a binary file and will not be displayed.

sample/composeApp/src/androidMain/assets/yolo11n_dataset_v10_20260410_185942__best_float32.tflite

This is a binary file and will not be displayed.

sample/composeApp/src/androidMain/assets/yolo11n_dataset_v11_dataset_v11_20260410_201918__best_float32.tflite

This is a binary file and will not be displayed.

sample/composeApp/src/androidMain/assets/yolo11n_su_416.tflite

This is a binary file and will not be displayed.

sample/composeApp/src/androidMain/assets/yolo11s_dataset_v11_dataset_v11_20260410_201918__best_float32.tflite

This is a binary file and will not be displayed.

sample/composeApp/src/androidMain/assets/yolo26n_dataset_v11_dataset_v11_20260410_201918__best_float32.tflite

This is a binary file and will not be displayed.

sample/composeApp/src/androidMain/assets/yolo26n_v11_320_fp16_20260410_215716__best_float16.tflite

This is a binary file and will not be displayed.

sample/composeApp/src/androidMain/assets/yolo26n_v11_320_fp32_20260410_215716__best_float32.tflite

This is a binary file and will not be displayed.

sample/composeApp/src/androidMain/assets/yolo26n_v11_512_fp16_20260410_211853__best_float16.tflite

This is a binary file and will not be displayed.

sample/composeApp/src/androidMain/assets/yolo26n_v11_512_fp32_20260410_211853__best_float32.tflite

This is a binary file and will not be displayed.

sample/composeApp/src/androidMain/assets/yolo26n_v11_rect_384x288_fp16_20260411_075952__best_float16.tflite

This is a binary file and will not be displayed.

sample/composeApp/src/androidMain/assets/yolo26n_v11_rect_384x288_fp16_20260411_083350__best_float16.tflite

This is a binary file and will not be displayed.

sample/composeApp/src/androidMain/assets/yolo26n_v11_rect_384x288_fp32_20260411_075952__best_float32.tflite

This is a binary file and will not be displayed.

sample/composeApp/src/androidMain/assets/yolo26n_v11_rect_384x288_fp32_20260411_083350__best_float32.tflite

This is a binary file and will not be displayed.

sample/composeApp/src/androidMain/assets/yolo26n_v11_rect_512x384_fp16_20260411_090924__best_float16.tflite

This is a binary file and will not be displayed.

sample/composeApp/src/androidMain/assets/yolo26n_v11_rect_512x384_fp32_20260411_090924__best_float32.tflite

This is a binary file and will not be displayed.

-431
sample/composeApp/src/commonMain/kotlin/com/nate/posedetection/App.kt
··· 398 398 } 399 399 } 400 400 401 - // DebugTestScreen removed. 402 - // CameraSample starts below. 403 - private object DebugTestScreenRemoved 404 - 405 - @Suppress("unused") 406 - @Composable 407 - private fun debugTestScreenRemoved() { 408 - val availableModels = discoverModels() 409 - var selectedModel by remember(availableModels) { mutableStateOf(availableModels.firstOrNull()) } 410 - val modelPath = selectedModel?.path ?: ModelPath() 411 - val generalModel = rememberObjectModel(modelPath) 412 - 413 - val skeletonRepository = remember { SkeletonRepository() } 414 - val customObjectRepository = remember { CustomObjectRespository() } 415 - val skeleton by skeletonRepository.skeletonFlow.collectAsState() 416 - val customObjects by customObjectRepository.customObjectFlow.collectAsState() 417 - var permissionGranted by remember { mutableStateOf(false) } 418 - var detectMode by remember { mutableStateOf(DetectMode.BOTH) } 419 - 420 - // Test phases: WAITING_PERMISSION -> PREVIEW -> RECORDING -> COMPARING -> DONE 421 - var phase by remember { mutableStateOf("WAITING_PERMISSION") } 422 - var recordingId: String? by remember { mutableStateOf(null) } 423 - var originalVideoPath by remember { mutableStateOf("") } 424 - var processedVideoPath: String? by remember { mutableStateOf(null) } 425 - // Comparison test data 426 - data class ComparisonResult( 427 - val image: ImageBitmap, 428 - val label: String, 429 - val frameTs: Long, // video PTS of this frame 430 - val frameElapsedMs: Long, // elapsed from video start 431 - val expectedElapsedMs: Long, // where the skeleton was expected to match 432 - val liveSkeleton: Skeleton?, 433 - val videoPath: String, 434 - val allTimestamps: List<Long>, 435 - val angleMatch: Float, 436 - val posMatch: Float 437 - ) 438 - var comparisonImages by remember { mutableStateOf<List<ComparisonResult>>(emptyList()) } 439 - var findBestResult by remember { mutableStateOf<Pair<Int, List<Pair<ImageBitmap, String>>>?>(null) } 440 - var statusText by remember { mutableStateOf("Waiting for camera permission...") } 441 - 442 - 443 - // Timing 444 - var recordTimeMs by remember { mutableStateOf(0L) } 445 - var extractTimeMs by remember { mutableStateOf(0L) } 446 - var frameCount by remember { mutableStateOf(0) } 447 - 448 - // Controller to get feed dimensions from CameraView. 449 - val controller = remember { CameraViewControllerImpl() } 450 - var feedWidth by remember { mutableStateOf(0f) } 451 - var feedHeight by remember { mutableStateOf(0f) } 452 - // Actual CameraView display size in pixels (for cropOffsetY calculation) 453 - var cameraViewPxW by remember { mutableStateOf(0f) } 454 - var cameraViewPxH by remember { mutableStateOf(0f) } 455 - 456 - // Saved analysis results during recording, keyed by timestamp ms. 457 - val analysisResults = remember { mutableMapOf<Long, AnalysisResult>() } 458 - // Wall-clock time when recording starts. 459 - var videoStartWallClock by remember { mutableStateOf(0L) } 460 - var recordingActive by remember { mutableStateOf(false) } 461 - 462 - // Deferred to wait for video save callback 463 - val videoSavedDeferred = remember { mutableStateOf<CompletableDeferred<String>?>(null) } 464 - 465 - val coroutineScope = rememberCoroutineScope() 466 - 467 - PermissionProvider().apply { 468 - if (!hasCameraPermission()) RequestCameraPermission(onGranted = { 469 - permissionGranted = true 470 - }, onDenied = { permissionGranted = false }) else permissionGranted = true 471 - } 472 - 473 - // Save skeleton + objects together. Objects don't have their own timestamp, 474 - // so we cache the latest objects and attach them when a skeleton arrives. 475 - var latestObjects by remember { mutableStateOf<List<AnalysisObject>>(emptyList()) } 476 - LaunchedEffect(Unit) { 477 - customObjectRepository.customObjectFlow.collect { objs -> 478 - if (objs != null) latestObjects = objs 479 - } 480 - } 481 - LaunchedEffect(Unit) { 482 - skeletonRepository.skeletonFlow.collect { skel -> 483 - // Only save skeletons after recording has started (videoStartWallClock set by onRecordToggled) 484 - if (recordingActive && videoStartWallClock > 0 && skel != null) { 485 - analysisResults[skel.timestamp] = AnalysisResult(skel, latestObjects) 486 - } 487 - } 488 - } 489 - 490 - // Go to preview once permission is granted 491 - LaunchedEffect(permissionGranted) { 492 - if (permissionGranted && phase == "WAITING_PERMISSION") { 493 - phase = "PREVIEW" 494 - statusText = "Tap Record to start" 495 - } 496 - } 497 - 498 - // Recording trigger — launched when phase becomes RECORDING 499 - LaunchedEffect(phase) { 500 - if (phase == "RECORDING") { 501 - statusText = "Recording 3 seconds..." 502 - analysisResults.clear() 503 - val deferred = CompletableDeferred<String>() 504 - videoSavedDeferred.value = deferred 505 - val recordStart = Clock.System.now().toEpochMilliseconds() 506 - videoStartWallClock = 0L 507 - recordingActive = false 508 - val beforeSetId = Clock.System.now().toEpochMilliseconds() 509 - println("RECORD-TIMING: before setRecordingId wallMs=$beforeSetId") 510 - recordingId = "${Clock.System.now().epochSeconds}" 511 - val afterSetId = Clock.System.now().toEpochMilliseconds() 512 - println("RECORD-TIMING: after setRecordingId wallMs=$afterSetId delta=${afterSetId - beforeSetId}ms") 513 - 514 - delay(1500) 515 - 516 - recordingId = null 517 - statusText = "Waiting for video to save..." 518 - val savedPath = deferred.await() 519 - recordTimeMs = Clock.System.now().toEpochMilliseconds() - recordStart 520 - 521 - val goodSkeletons = analysisResults.values.count { it.skeleton != null && it.skeleton!!.joints().size >= 10 } 522 - println("DEBUG: skeleton data: $goodSkeletons good skeletons") 523 - 524 - originalVideoPath = savedPath 525 - phase = "COMPARING" 526 - statusText = "Analysing timestamp relationship..." 527 - 528 - val capturedAnalysisResults = analysisResults.toMap() 529 - val capturedVideoStart = videoStartWallClock 530 - 531 - val extractJob = coroutineScope.launch(kotlinx.coroutines.Dispatchers.Default) { 532 - val timestamps = listVideoFrameTimestamps(savedPath) 533 - val savedKeys = capturedAnalysisResults.keys.sorted() 534 - val firstTs = timestamps.firstOrNull() ?: return@launch 535 - println("DEBUG: ${timestamps.size} video frames, ${savedKeys.size} analysis results") 536 - println("DEBUG: videoStartWallClock=$capturedVideoStart") 537 - println("DEBUG: video PTS range: ${timestamps.first()}..${timestamps.last()} span=${timestamps.last() - timestamps.first()}ms") 538 - println("DEBUG: skeleton ts range: ${savedKeys.firstOrNull()}..${savedKeys.lastOrNull()} span=${if (savedKeys.size > 1) savedKeys.last() - savedKeys.first() else 0}ms") 539 - 540 - // Diagnostic: compare video PTS against skeleton timestamps 541 - // Video PTS is relative (starts from ~0). Skeleton timestamps are wall-clock. 542 - // If we subtract videoStartWallClock from skeleton timestamps, they should 543 - // become comparable to video PTS. The difference reveals the lag. 544 - val relativeKeys = savedKeys.map { it - capturedVideoStart } 545 - println("DIAG: relative skeleton timestamps (first 10): ${relativeKeys.take(10)}") 546 - println("DIAG: video PTS (first 10): ${timestamps.take(10)}") 547 - 548 - // For each skeleton, find the closest video PTS and report the difference 549 - val offsets = relativeKeys.mapNotNull { skelRelTs -> 550 - val closestPts = timestamps.minByOrNull { kotlin.math.abs(it - skelRelTs) } ?: return@mapNotNull null 551 - skelRelTs - closestPts 552 - } 553 - if (offsets.isNotEmpty()) { 554 - println("DIAG: skeleton-to-video offsets (first 10): ${offsets.take(10)}") 555 - println("DIAG: avg offset=${offsets.average().toLong()}ms min=${offsets.min()}ms max=${offsets.max()}ms") 556 - } 557 - 558 - // Pick a frame near 1.5s that has BOTH skeleton (10+ joints) AND objects 559 - val targetTime = capturedVideoStart + 750L 560 - val bestKey = savedKeys 561 - .filter { key -> 562 - val r = capturedAnalysisResults[key] 563 - r?.skeleton != null && r.skeleton!!.joints().size >= 10 && r.objects.isNotEmpty() 564 - } 565 - .minByOrNull { kotlin.math.abs(it - targetTime) } 566 - ?: savedKeys // fallback: just skeleton if no frame has both 567 - .filter { capturedAnalysisResults[it]?.skeleton?.joints()?.size ?: 0 >= 10 } 568 - .minByOrNull { kotlin.math.abs(it - targetTime) } 569 - val liveResult = bestKey?.let { capturedAnalysisResults[it] } 570 - val liveSkel = liveResult?.skeleton 571 - val liveObjects = liveResult?.objects ?: emptyList() 572 - if (liveSkel == null || liveSkel.joints().size < 10) { 573 - println("DEBUG: no good skeleton found") 574 - phase = "DONE" 575 - statusText = "No skeleton with objects found" 576 - return@launch 577 - } 578 - println("DEBUG: selected frame has ${liveSkel.joints().size} joints, ${liveObjects.size} objects") 579 - val skelVideoTs = bestKey - capturedVideoStart 580 - // Log all skeleton timestamps near the target for debugging 581 - val nearKeys = savedKeys.filter { kotlin.math.abs(it - targetTime) < 500 } 582 - println("DEBUG: skeleton at sensorTs=$bestKey videoTs=${skelVideoTs}ms joints=${liveSkel.joints().size}") 583 - println("DEBUG: videoStartWallClock=$capturedVideoStart targetTime=$targetTime") 584 - println("DEBUG: nearby skeleton keys (within 500ms of target): ${nearKeys.map { it - capturedVideoStart }}") 585 - println("DEBUG: first 5 skeleton keys (relative): ${savedKeys.take(5).map { it - capturedVideoStart }}") 586 - println("DEBUG: video PTS first=${timestamps.first()} last=${timestamps.last()} count=${timestamps.size}") 587 - // Show the gap between video start and first skeleton 588 - println("DEBUG: gap from recording start to first skeleton: ${savedKeys.first() - capturedVideoStart}ms") 589 - 590 - // Extract ALL frames and draw the same skeleton on each 591 - val results = mutableListOf<ComparisonResult>() 592 - var expectedPageIdx = 0 593 - 594 - extractFrames(savedPath, timestamps).collect { inputFrame -> 595 - val frameBitmap = inputFrame.toImageBitmap() 596 - val frameElapsed = inputFrame.timestamp - firstTs 597 - 598 - val output = ImageBitmap(frameBitmap.width, frameBitmap.height) 599 - val canvas = Canvas(output) 600 - val drawScope = CanvasDrawScope() 601 - drawScope.draw(Density(1f), LayoutDirection.Ltr, canvas, Size(frameBitmap.width.toFloat(), frameBitmap.height.toFloat())) { 602 - drawImage(frameBitmap) 603 - val fw = frameBitmap.width.toFloat(); val fh = frameBitmap.height.toFloat() 604 - val sA = liveSkel.width / liveSkel.height; val fA = fw / fh 605 - val coX: Float; val coY: Float; val sx: Float; val sy: Float 606 - if (kotlin.math.abs(sA - fA) > 0.01f) { 607 - if (sA > fA) { sy = fh / liveSkel.height; val vW = liveSkel.height * fA; coX = (liveSkel.width - vW) / 2f; sx = fw / vW; coY = 0f } 608 - else { sx = fw / liveSkel.width; val vH = liveSkel.width / fA; coY = (liveSkel.height - vH) / 2f; sy = fh / vH; coX = 0f } 609 - } else { sx = fw / liveSkel.width; sy = fh / liveSkel.height; coX = 0f; coY = 0f } 610 - fun pt(c: Skeleton.SkeletonCoordinate?) = c?.let { androidx.compose.ui.geometry.Offset((it.x - coX) * sx, (it.y - coY) * sy) } 611 - val conns = listOf(liveSkel.leftShoulder to liveSkel.leftElbow, liveSkel.leftElbow to liveSkel.leftWrist, liveSkel.rightShoulder to liveSkel.rightElbow, liveSkel.rightElbow to liveSkel.rightWrist, liveSkel.leftShoulder to liveSkel.rightShoulder, liveSkel.leftShoulder to liveSkel.leftHip, liveSkel.rightShoulder to liveSkel.rightHip, liveSkel.leftHip to liveSkel.rightHip, liveSkel.leftHip to liveSkel.leftKnee, liveSkel.leftKnee to liveSkel.leftAnkle, liveSkel.rightHip to liveSkel.rightKnee, liveSkel.rightKnee to liveSkel.rightAnkle) 612 - for ((f, t) in conns) { val p1 = pt(f); val p2 = pt(t); if (p1 != null && p2 != null) drawLine(Color.Blue, p1, p2, strokeWidth = 4f) } 613 - for (j in listOfNotNull(liveSkel.leftShoulder, liveSkel.rightShoulder, liveSkel.leftElbow, liveSkel.rightElbow, liveSkel.leftWrist, liveSkel.rightWrist, liveSkel.leftHip, liveSkel.rightHip, liveSkel.leftKnee, liveSkel.rightKnee, liveSkel.leftAnkle, liveSkel.rightAnkle)) { pt(j)?.let { drawCircle(Color.Blue, 6f, it) } } 614 - // Draw objects (green rectangles) with same aspect ratio correction 615 - for (obj in liveObjects) { 616 - val ofw = obj.frameSize.width.toFloat() 617 - val ofh = obj.frameSize.height.toFloat() 618 - val oA = ofw / ofh 619 - val ocX: Float; val ocY: Float; val osx: Float; val osy: Float 620 - if (kotlin.math.abs(oA - fA) > 0.01f) { 621 - if (oA > fA) { osy = fh / ofh; val vW = ofh * fA; ocX = (ofw - vW) / 2f; osx = fw / vW; ocY = 0f } 622 - else { osx = fw / ofw; val vH = ofw / fA; ocY = (ofh - vH) / 2f; osy = fh / vH; ocX = 0f } 623 - } else { osx = fw / ofw; osy = fh / ofh; ocX = 0f; ocY = 0f } 624 - val left = (obj.boundingBox.left - ocX) * osx 625 - val top = (obj.boundingBox.top - ocY) * osy 626 - val right = (obj.boundingBox.right - ocX) * osx 627 - val bottom = (obj.boundingBox.bottom - ocY) * osy 628 - drawRect(Color.Green, topLeft = androidx.compose.ui.geometry.Offset(left, top), size = Size(right - left, bottom - top), style = Stroke(3f)) 629 - } 630 - } 631 - 632 - if (frameElapsed <= skelVideoTs) expectedPageIdx = results.size 633 - val offset = frameElapsed - skelVideoTs 634 - results.add(ComparisonResult( 635 - image = output, 636 - label = "${frameElapsed}ms (${if (offset >= 0) "+" else ""}${offset}ms)", 637 - frameTs = inputFrame.timestamp, frameElapsedMs = frameElapsed, 638 - expectedElapsedMs = skelVideoTs, liveSkeleton = liveSkel, 639 - videoPath = savedPath, allTimestamps = timestamps, 640 - angleMatch = -1f, posMatch = -1f 641 - )) 642 - statusText = "Frame ${results.size}/${timestamps.size}..." 643 - } 644 - 645 - println("DEBUG: ${results.size} frames, expected page=$expectedPageIdx (${skelVideoTs}ms)") 646 - comparisonImages = results 647 - findBestResult = expectedPageIdx to emptyList() 648 - phase = "DONE" 649 - statusText = "Skeleton from ${skelVideoTs}ms — swipe to find match" 650 - } // end of launch(Dispatchers.Default) 651 - } 652 - } 653 - 654 - Column(modifier = Modifier.fillMaxSize()) { 655 - // Status bar 656 - Text( 657 - text = statusText, 658 - modifier = Modifier.fillMaxWidth().padding(8.dp), 659 - textAlign = TextAlign.Center, 660 - style = androidx.compose.material3.MaterialTheme.typography.titleSmall, 661 - ) 662 - 663 - // Timing info 664 - if (phase == "DONE") { 665 - val recordSec = (recordTimeMs / 100.0).roundToLong() / 10.0 666 - val extractSec = (extractTimeMs / 100.0).roundToLong() / 10.0 667 - val fps = if (extractTimeMs > 0) (frameCount * 1000.0 / extractTimeMs).roundToLong() else 0 668 - Text( 669 - text = "Record: ${recordSec}s | Extract+encode: ${extractSec}s ($frameCount frames, ~${fps} fps)", 670 - modifier = Modifier.fillMaxWidth().padding(horizontal = 8.dp), 671 - textAlign = TextAlign.Center, 672 - fontSize = 12.sp, 673 - ) 674 - } 675 - 676 - when (phase) { 677 - "WAITING_PERMISSION" -> { 678 - Box(modifier = Modifier.weight(1f).fillMaxWidth(), contentAlignment = Alignment.Center) { 679 - CircularProgressIndicator() 680 - } 681 - } 682 - "PREVIEW", "RECORDING" -> { 683 - if (permissionGranted) { 684 - var menuExpanded by remember { mutableStateOf(false) } 685 - DetectOrientation { orientation -> 686 - Box(modifier = Modifier.weight(1f).fillMaxWidth()) { 687 - key(orientation, detectMode) { 688 - CameraView( 689 - skeletonRepository = skeletonRepository, 690 - customObjectRepository = customObjectRepository, 691 - detectMode = detectMode, 692 - drawSkeleton = true, 693 - objectModel = generalModel, 694 - modifier = Modifier.fillMaxSize() 695 - .onSizeChanged { size -> 696 - cameraViewPxW = size.width.toFloat() 697 - cameraViewPxH = size.height.toFloat() 698 - }, 699 - frontCamera = false, 700 - recordingId = recordingId, 701 - controller = controller, 702 - onRecordToggled = { recording -> 703 - recordingActive = recording 704 - if (recording) { 705 - val callbackWallMs = Clock.System.now().toEpochMilliseconds() 706 - videoStartWallClock = callbackWallMs 707 - println("RECORD-TIMING: onRecordToggled(true) wallMs=$callbackWallMs") 708 - controller.requestData { data -> 709 - feedWidth = data.width 710 - feedHeight = data.height 711 - } 712 - } 713 - }, 714 - onVideoSaved = { id, url -> 715 - videoSavedDeferred.value?.complete(url) 716 - }, 717 - ) 718 - } 719 - // Menu overlay (same as CameraSample) 720 - if (phase == "PREVIEW") { 721 - Box(modifier = Modifier.padding(12.dp).align(Alignment.TopEnd)) { 722 - IconButton(onClick = { menuExpanded = true }) { 723 - Text("⋮", color = Color.White, fontSize = 22.sp) 724 - } 725 - DropdownMenu(expanded = menuExpanded, onDismissRequest = { menuExpanded = false }) { 726 - // Detection mode 727 - DropdownMenuItem(text = { 728 - Column { 729 - Text("Detection Mode", modifier = Modifier.fillMaxWidth(), textAlign = TextAlign.Center) 730 - listOf("Pose" to DetectMode.POSE, "Objects" to DetectMode.OBJECT, "Both" to DetectMode.BOTH, "None" to DetectMode.NONE).forEach { (label, mode) -> 731 - Row(horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically, modifier = Modifier.fillMaxWidth()) { 732 - Text(label); Spacer(Modifier.width(12.dp)) 733 - RadioButton(selected = detectMode == mode, onClick = { detectMode = mode }) 734 - } 735 - } 736 - } 737 - }, onClick = {}) 738 - HorizontalDivider() 739 - // Model picker 740 - DropdownMenuItem(text = { 741 - Column { 742 - Text("Model", modifier = Modifier.fillMaxWidth(), textAlign = TextAlign.Center) 743 - availableModels.forEach { model -> 744 - Row(horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically, modifier = Modifier.fillMaxWidth()) { 745 - Text(model.name, fontSize = 13.sp); Spacer(Modifier.width(12.dp)) 746 - RadioButton(selected = selectedModel == model, onClick = { selectedModel = model }) 747 - } 748 - } 749 - } 750 - }, onClick = {}) 751 - HorizontalDivider() 752 - // Record button 753 - DropdownMenuItem( 754 - text = { Text("Record 1.5 seconds") }, 755 - onClick = { menuExpanded = false; phase = "RECORDING" } 756 - ) 757 - } 758 - } 759 - } 760 - } 761 - } 762 - } 763 - } 764 - "EXTRACTING" -> { 765 - Box(modifier = Modifier.weight(1f).fillMaxWidth(), contentAlignment = Alignment.Center) { 766 - CircularProgressIndicator() 767 - } 768 - } 769 - "COMPARING" -> { 770 - Box(modifier = Modifier.weight(1f).fillMaxWidth(), contentAlignment = Alignment.Center) { 771 - CircularProgressIndicator() 772 - } 773 - } 774 - "DONE" -> { 775 - if (comparisonImages.isNotEmpty()) { 776 - val initialPage = findBestResult?.first ?: 0 777 - val pagerState = rememberPagerState(initialPage = initialPage, pageCount = { comparisonImages.size }) 778 - Column( 779 - modifier = Modifier.weight(1f).fillMaxWidth().padding(8.dp), 780 - verticalArrangement = Arrangement.spacedBy(4.dp) 781 - ) { 782 - val comp = comparisonImages.getOrNull(pagerState.currentPage) 783 - val lagText = "" 784 - Text( 785 - "${lagText}${pagerState.currentPage + 1}/${comparisonImages.size} — ${comp?.label ?: ""}", 786 - fontSize = 12.sp, textAlign = TextAlign.Center, modifier = Modifier.fillMaxWidth() 787 - ) 788 - HorizontalPager( 789 - state = pagerState, 790 - modifier = Modifier.weight(1f).fillMaxWidth() 791 - ) { page -> 792 - val c = comparisonImages[page] 793 - Image( 794 - bitmap = c.image, 795 - contentDescription = c.label, 796 - contentScale = ContentScale.Fit, 797 - modifier = Modifier.fillMaxSize() 798 - ) 799 - } 800 - Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(8.dp)) { 801 - androidx.compose.material3.Button( 802 - onClick = { 803 - val c = comparisonImages.getOrNull(pagerState.currentPage) 804 - if (c != null) { 805 - val lagMs = c.frameElapsedMs - c.expectedElapsedMs 806 - println("BEST-MATCH: lagMs=$lagMs frameElapsed=${c.frameElapsedMs}ms expected=${c.expectedElapsedMs}ms mode=$detectMode model=${selectedModel?.name} page=${pagerState.currentPage}/${comparisonImages.size}") 807 - } 808 - }, 809 - modifier = Modifier.weight(1f) 810 - ) { Text("Best Match") } 811 - androidx.compose.material3.Button( 812 - onClick = { 813 - comparisonImages = emptyList() 814 - findBestResult = null 815 - phase = "PREVIEW" 816 - statusText = "Tap Record to start" 817 - }, 818 - modifier = Modifier.weight(1f) 819 - ) { Text("New Recording") } 820 - } 821 - } 822 - } else { 823 - Box(modifier = Modifier.weight(1f).fillMaxWidth(), contentAlignment = Alignment.Center) { 824 - Text("No comparison images produced") 825 - } 826 - } 827 - } 828 - } 829 - } 830 - } 831 - 832 401 @OptIn(ExperimentalTime::class) 833 402 @Composable 834 403 fun CameraSample() {
+2 -2
sample/iosApp/iosApp.xcodeproj/project.pbxproj
··· 302 302 CODE_SIGN_STYLE = Automatic; 303 303 CURRENT_PROJECT_VERSION = 1; 304 304 DEVELOPMENT_ASSET_PATHS = "\"iosApp/Preview Content\""; 305 - DEVELOPMENT_TEAM = 6H9FHG23L3; 305 + DEVELOPMENT_TEAM = FAGG2XS28P; 306 306 ENABLE_PREVIEWS = YES; 307 307 GENERATE_INFOPLIST_FILE = YES; 308 308 INFOPLIST_FILE = iosApp/Info.plist; ··· 328 328 CODE_SIGN_STYLE = Automatic; 329 329 CURRENT_PROJECT_VERSION = 1; 330 330 DEVELOPMENT_ASSET_PATHS = "\"iosApp/Preview Content\""; 331 - DEVELOPMENT_TEAM = 6H9FHG23L3; 331 + DEVELOPMENT_TEAM = FAGG2XS28P; 332 332 ENABLE_PREVIEWS = YES; 333 333 GENERATE_INFOPLIST_FILE = YES; 334 334 INFOPLIST_FILE = iosApp/Info.plist;
sample/iosApp/iosApp/models/yolo26n_v11_rect_512x384.mlpackage/Data/com.apple.CoreML/model.mlmodel

This is a binary file and will not be displayed.

sample/iosApp/iosApp/models/yolo26n_v11_rect_512x384.mlpackage/Data/com.apple.CoreML/weights/weight.bin

This is a binary file and will not be displayed.

-18
sample/iosApp/iosApp/models/yolo26n_v11_rect_512x384.mlpackage/Manifest.json
··· 1 - { 2 - "fileFormatVersion": "1.0.0", 3 - "itemInfoEntries": { 4 - "18ba14b4-753b-45e3-9fb2-68cec8f7110b": { 5 - "author": "com.apple.CoreML", 6 - "description": "CoreML Model Weights", 7 - "name": "weights", 8 - "path": "com.apple.CoreML/weights" 9 - }, 10 - "5192aff5-6847-4a09-98db-c5ed17988277": { 11 - "author": "com.apple.CoreML", 12 - "description": "CoreML Model Specification", 13 - "name": "model.mlmodel", 14 - "path": "com.apple.CoreML/model.mlmodel" 15 - } 16 - }, 17 - "rootModelIdentifier": "5192aff5-6847-4a09-98db-c5ed17988277" 18 - }
sample/iosApp/iosApp/models/yolo26n_v11_rect_640x480.mlpackage/Data/com.apple.CoreML/model.mlmodel

This is a binary file and will not be displayed.

sample/iosApp/iosApp/models/yolo26n_v11_rect_640x480.mlpackage/Data/com.apple.CoreML/weights/weight.bin

This is a binary file and will not be displayed.

-18
sample/iosApp/iosApp/models/yolo26n_v11_rect_640x480.mlpackage/Manifest.json
··· 1 - { 2 - "fileFormatVersion": "1.0.0", 3 - "itemInfoEntries": { 4 - "0dd5a52c-fa82-4dae-8775-fe06d157127d": { 5 - "author": "com.apple.CoreML", 6 - "description": "CoreML Model Weights", 7 - "name": "weights", 8 - "path": "com.apple.CoreML/weights" 9 - }, 10 - "ca543875-0854-4de4-8d98-72b69258f5a4": { 11 - "author": "com.apple.CoreML", 12 - "description": "CoreML Model Specification", 13 - "name": "model.mlmodel", 14 - "path": "com.apple.CoreML/model.mlmodel" 15 - } 16 - }, 17 - "rootModelIdentifier": "ca543875-0854-4de4-8d98-72b69258f5a4" 18 - }
sample/iosApp/iosApp/models/yolo26n_v11_rect_960x736.mlpackage/Data/com.apple.CoreML/model.mlmodel

This is a binary file and will not be displayed.

sample/iosApp/iosApp/models/yolo26n_v11_rect_960x736.mlpackage/Data/com.apple.CoreML/weights/weight.bin

This is a binary file and will not be displayed.

-18
sample/iosApp/iosApp/models/yolo26n_v11_rect_960x736.mlpackage/Manifest.json
··· 1 - { 2 - "fileFormatVersion": "1.0.0", 3 - "itemInfoEntries": { 4 - "7ff8395c-6cac-4c82-a673-d6c45b49b4e7": { 5 - "author": "com.apple.CoreML", 6 - "description": "CoreML Model Specification", 7 - "name": "model.mlmodel", 8 - "path": "com.apple.CoreML/model.mlmodel" 9 - }, 10 - "a94ab001-e4e2-4b39-939a-21ab438b9fa9": { 11 - "author": "com.apple.CoreML", 12 - "description": "CoreML Model Weights", 13 - "name": "weights", 14 - "path": "com.apple.CoreML/weights" 15 - } 16 - }, 17 - "rootModelIdentifier": "7ff8395c-6cac-4c82-a673-d6c45b49b4e7" 18 - }