alpha
Login
or
Join now
nateholland.bsky.social
/
PoseDetection
Star
0
Fork
0
Atom
Configure Feed
Issues
Pull Requests
Commits
Tags
Feed URL
Select the types of activity you want to include in your feed.
This repository has no description
Star
0
Fork
0
Atom
Configure Feed
Issues
Pull Requests
Commits
Tags
Feed URL
Select the types of activity you want to include in your feed.
Overview
Issues
Pulls
Pipelines
refactor: consistency in buffer input
author
nate
date
4 months ago
(Feb 7, 2026, 12:49 PM +0200)
commit
6d819964
6d819964e12e6b4b30afbedeb02f2bdbcca62fa1
parent
10283ead
10283ead44df6b9abdef7013d66bc4142e5f020e
+83
-37
2 changed files
Expand all
Collapse all
Unified
Split
posedetection
src
iosMain
kotlin
com
performancecoachlab
posedetection
camera
CameraEngine.kt
FrameProcessor.kt
+1
posedetection/src/iosMain/kotlin/com/performancecoachlab/posedetection/camera/CameraEngine.kt
Reviewed
···
721
721
CMSampleBufferGetImageBuffer(didOutputSampleBuffer),
722
722
timestamp,
723
723
preview = cameraPreviewLayer,
724
724
+
captureConnection = fromConnection,
724
725
onSkeletonProcessed = { skeleton ->
725
726
detectedSkeleton = skeleton
726
727
},
+82
-37
posedetection/src/iosMain/kotlin/com/performancecoachlab/posedetection/camera/FrameProcessor.kt
Reviewed
···
17
17
import kotlinx.cinterop.ptr
18
18
import kotlinx.cinterop.useContents
19
19
import kotlinx.cinterop.value
20
20
+
import platform.AVFoundation.AVCaptureConnection
21
21
+
import platform.AVFoundation.AVCaptureVideoOrientationLandscapeLeft
20
22
import platform.AVFoundation.AVCaptureVideoOrientationLandscapeRight
23
23
+
import platform.AVFoundation.AVCaptureVideoOrientationPortrait
24
24
+
import platform.AVFoundation.AVCaptureVideoOrientationPortraitUpsideDown
21
25
import platform.AVFoundation.AVCaptureVideoPreviewLayer
22
26
import platform.CoreFoundation.CFRelease
23
27
import platform.CoreFoundation.CFRetain
···
28
32
import platform.CoreGraphics.CGRect
29
33
import platform.CoreGraphics.CGRectMake
30
34
import platform.CoreVideo.CVImageBufferRef
35
35
+
import platform.CoreVideo.CVPixelBufferGetHeight
36
36
+
import platform.CoreVideo.CVPixelBufferGetWidth
31
37
import platform.Foundation.NSError
32
38
import platform.Foundation.NSUUID
39
39
+
import platform.ImageIO.kCGImagePropertyOrientationDown
40
40
+
import platform.ImageIO.kCGImagePropertyOrientationDownMirrored
41
41
+
import platform.ImageIO.kCGImagePropertyOrientationLeft
42
42
+
import platform.ImageIO.kCGImagePropertyOrientationLeftMirrored
43
43
+
import platform.ImageIO.kCGImagePropertyOrientationRight
44
44
+
import platform.ImageIO.kCGImagePropertyOrientationRightMirrored
45
45
+
import platform.ImageIO.kCGImagePropertyOrientationUp
46
46
+
import platform.ImageIO.kCGImagePropertyOrientationUpMirrored
33
47
import platform.Vision.VNClassificationObservation
34
48
import platform.Vision.VNCoreMLModel
35
49
import platform.Vision.VNCoreMLRequest
···
53
67
import platform.Vision.VNRecognizedObjectObservation
54
68
import platform.Vision.VNRecognizedPoint
55
69
import platform.Vision.VNRequest
56
56
-
import kotlin.experimental.ExperimentalNativeApi
57
57
-
import kotlin.math.absoluteValue
70
70
+
import kotlin.math.abs
58
71
import kotlin.math.max
59
72
import kotlin.math.min
60
60
-
import kotlin.native.identityHashCode
73
73
+
74
74
+
private const val VN_IMAGE_OPTION_CG_IMAGE_PROPERTY_ORIENTATION = "VNImageOptionCGImagePropertyOrientation"
61
75
62
76
fun bodyPoseHandler(request: VNRequest): List<MutableMap<VNHumanBodyPoseObservationJointName, VNRecognizedPoint>>? {
63
77
try {
···
134
148
}
135
149
136
150
@OptIn(ExperimentalForeignApi::class)
151
151
+
private fun AVCaptureConnection?.visionExifOrientation(): Int {
152
152
+
// Prefer the actual capture connection orientation (not UI orientation).
153
153
+
val videoOrientation = this?.videoOrientation ?: AVCaptureVideoOrientationLandscapeLeft
154
154
+
val mirrored = this?.videoMirrored == true
155
155
+
156
156
+
val exif: UInt = when (videoOrientation) {
157
157
+
// These mappings match Apple's docs: EXIF/CGImagePropertyOrientation values describe
158
158
+
// how to rotate the underlying pixel buffer into "Up".
159
159
+
AVCaptureVideoOrientationPortrait -> if (mirrored) kCGImagePropertyOrientationLeftMirrored else kCGImagePropertyOrientationRight
160
160
+
AVCaptureVideoOrientationPortraitUpsideDown -> if (mirrored) kCGImagePropertyOrientationRightMirrored else kCGImagePropertyOrientationLeft
161
161
+
AVCaptureVideoOrientationLandscapeLeft -> if (mirrored) kCGImagePropertyOrientationDownMirrored else kCGImagePropertyOrientationUp
162
162
+
AVCaptureVideoOrientationLandscapeRight -> if (mirrored) kCGImagePropertyOrientationUpMirrored else kCGImagePropertyOrientationDown
163
163
+
else -> if (mirrored) kCGImagePropertyOrientationDownMirrored else kCGImagePropertyOrientationUp
164
164
+
}
165
165
+
166
166
+
return exif.toInt()
167
167
+
}
168
168
+
169
169
+
@OptIn(ExperimentalForeignApi::class)
137
170
class FrameProcessor(var modelObj: VNCoreMLModel?) {
138
171
private val skelBuffer = SkelBuffer(maxSize = 10)
139
172
private var regionOfInterest = CGRectMake(0.0, 0.0, 1.0, 1.0)
···
182
215
}
183
216
}
184
217
185
185
-
@OptIn(BetaInteropApi::class, ExperimentalNativeApi::class)
218
218
+
@OptIn(BetaInteropApi::class)
186
219
fun analyseFrameForAll(
187
220
cgImage: CGImageRef?,
188
221
timestamp: Long,
···
239
272
}
240
273
}
241
274
AnalysisObject(
242
242
-
trackingId = observation.identityHashCode(),
275
275
+
trackingId = stableTrackingId(observation),
243
276
labels = labels,
244
277
boundingBox = boundingBox.normalize(),
245
278
frameSize = FrameSize(
246
246
-
width = width.toInt().absoluteValue,
247
247
-
height = height.toInt().absoluteValue
279
279
+
width = abs(width.toInt()),
280
280
+
height = abs(height.toInt())
248
281
)
249
282
)
250
283
}
251
284
onObjectsProcessed(analysisObjects)
285
285
+
}.apply {
286
286
+
// Make preprocessing consistent across orientations.
287
287
+
imageCropAndScaleOption = platform.Vision.VNImageCropAndScaleOptionCenterCrop
252
288
}
253
289
}
254
254
-
val options = mapOf<Any?, Any?>(
255
255
-
"orientation" to AVCaptureVideoOrientationLandscapeRight
290
290
+
// For CGImage path we don't have an AVCaptureConnection; assume the CGImage
291
291
+
// is already "up".
292
292
+
val options: Map<Any?, Any?> = mapOf(
293
293
+
VN_IMAGE_OPTION_CG_IMAGE_PROPERTY_ORIENTATION to kCGImagePropertyOrientationUp.toInt()
256
294
)
295
295
+
257
296
val requestForSkeleton = VNDetectHumanBodyPoseRequest { request, error ->
258
297
if (error != null) {
259
298
onSkeletonProcessed(null)
···
363
402
}
364
403
}
365
404
366
366
-
@OptIn(BetaInteropApi::class, ExperimentalNativeApi::class)
405
405
+
@OptIn(BetaInteropApi::class)
367
406
fun analyseBufferForAll(
368
407
buffer: CVImageBufferRef?,
369
408
timestamp: Long,
370
409
preview: AVCaptureVideoPreviewLayer?,
410
410
+
captureConnection: AVCaptureConnection? = preview?.connection,
371
411
onObjectsProcessed: (List<AnalysisObject>) -> Unit,
372
412
onSkeletonProcessed: (Skeleton?) -> Unit
373
413
) {
···
378
418
return
379
419
}
380
420
val retainedBuffer = CFRetain(buffer)
381
381
-
val width = 480uL // You may want to get actual width from buffer if needed
382
382
-
val height = 360uL // You may want to get actual height from buffer if needed
421
421
+
val width = CVPixelBufferGetWidth(buffer).toULong()
422
422
+
val height = CVPixelBufferGetHeight(buffer).toULong()
383
423
memScoped {
384
424
val errorPtr = alloc<ObjCObjectVar<NSError?>>()
385
425
try {
386
386
-
val requestForObjects = if(detectMode.doObject()){
426
426
+
val requestForObjects = if (detectMode.doObject()) {
387
427
modelObj?.let {
388
428
VNCoreMLRequest(it) { request, error ->
389
429
if (error != null) {
···
418
458
}
419
459
}
420
460
AnalysisObject(
421
421
-
trackingId = observation.identityHashCode(),
461
461
+
trackingId = stableTrackingId(observation),
422
462
labels = labels,
423
463
boundingBox = boundingBox.normalize(),
424
464
frameSize = FrameSize(
425
425
-
width = width.toInt().absoluteValue,
426
426
-
height = height.toInt().absoluteValue
465
465
+
width = abs(width.toInt()),
466
466
+
height = abs(height.toInt())
427
467
)
428
468
)
429
469
}
430
470
onObjectsProcessed(analysisObjects)
471
471
+
}.apply {
472
472
+
imageCropAndScaleOption = platform.Vision.VNImageCropAndScaleOptionCenterCrop
431
473
}
432
474
}
433
433
-
}else null
434
434
-
475
475
+
} else null
435
476
436
436
-
val options = mapOf<Any?, Any?>(
437
437
-
"orientation" to AVCaptureVideoOrientationLandscapeRight
477
477
+
val options: Map<Any?, Any?> = mapOf(
478
478
+
VN_IMAGE_OPTION_CG_IMAGE_PROPERTY_ORIENTATION to captureConnection.visionExifOrientation()
438
479
)
439
439
-
val requestForSkeleton = if(detectMode.doPose()) {
480
480
+
481
481
+
val requestForSkeleton = if (detectMode.doPose()) {
440
482
VNDetectHumanBodyPoseRequest { request, error ->
441
483
if (error != null) {
442
484
onSkeletonProcessed(null)
443
485
} else {
444
486
request?.also { vnRequest ->
445
445
-
val recognizedPoints = bodyPoseHandler(vnRequest)?.firstOrNull {
487
487
+
val recognizedPoints = bodyPoseHandler(vnRequest)?.firstOrNull { pointsMap ->
446
488
Skeleton(
447
489
timestamp = timestamp,
448
448
-
leftShoulder = it[VNHumanBodyPoseObservationJointNameLeftShoulder]?.location?.toSkeletonPoint(
490
490
+
leftShoulder = pointsMap[VNHumanBodyPoseObservationJointNameLeftShoulder]?.location?.toSkeletonPoint(
449
491
width,
450
492
height
451
493
),
452
452
-
rightShoulder = it[VNHumanBodyPoseObservationJointNameRightShoulder]?.location?.toSkeletonPoint(
494
494
+
rightShoulder = pointsMap[VNHumanBodyPoseObservationJointNameRightShoulder]?.location?.toSkeletonPoint(
453
495
width,
454
496
height
455
497
),
456
456
-
leftElbow = it[VNHumanBodyPoseObservationJointNameLeftElbow]?.location?.toSkeletonPoint(
498
498
+
leftElbow = pointsMap[VNHumanBodyPoseObservationJointNameLeftElbow]?.location?.toSkeletonPoint(
457
499
width,
458
500
height
459
501
),
460
460
-
rightElbow = it[VNHumanBodyPoseObservationJointNameRightElbow]?.location?.toSkeletonPoint(
502
502
+
rightElbow = pointsMap[VNHumanBodyPoseObservationJointNameRightElbow]?.location?.toSkeletonPoint(
461
503
width,
462
504
height
463
505
),
464
464
-
leftWrist = it[VNHumanBodyPoseObservationJointNameLeftWrist]?.location?.toSkeletonPoint(
506
506
+
leftWrist = pointsMap[VNHumanBodyPoseObservationJointNameLeftWrist]?.location?.toSkeletonPoint(
465
507
width,
466
508
height
467
509
),
468
468
-
rightWrist = it[VNHumanBodyPoseObservationJointNameRightWrist]?.location?.toSkeletonPoint(
510
510
+
rightWrist = pointsMap[VNHumanBodyPoseObservationJointNameRightWrist]?.location?.toSkeletonPoint(
469
511
width,
470
512
height
471
513
),
472
472
-
leftHip = it[VNHumanBodyPoseObservationJointNameLeftHip]?.location?.toSkeletonPoint(
514
514
+
leftHip = pointsMap[VNHumanBodyPoseObservationJointNameLeftHip]?.location?.toSkeletonPoint(
473
515
width,
474
516
height
475
517
),
476
476
-
rightHip = it[VNHumanBodyPoseObservationJointNameRightHip]?.location?.toSkeletonPoint(
518
518
+
rightHip = pointsMap[VNHumanBodyPoseObservationJointNameRightHip]?.location?.toSkeletonPoint(
477
519
width,
478
520
height
479
521
),
480
480
-
leftKnee = it[VNHumanBodyPoseObservationJointNameLeftKnee]?.location?.toSkeletonPoint(
522
522
+
leftKnee = pointsMap[VNHumanBodyPoseObservationJointNameLeftKnee]?.location?.toSkeletonPoint(
481
523
width,
482
524
height
483
525
),
484
484
-
rightKnee = it[VNHumanBodyPoseObservationJointNameRightKnee]?.location?.toSkeletonPoint(
526
526
+
rightKnee = pointsMap[VNHumanBodyPoseObservationJointNameRightKnee]?.location?.toSkeletonPoint(
485
527
width,
486
528
height
487
529
),
488
488
-
leftAnkle = it[VNHumanBodyPoseObservationJointNameLeftAnkle]?.location?.toSkeletonPoint(
530
530
+
leftAnkle = pointsMap[VNHumanBodyPoseObservationJointNameLeftAnkle]?.location?.toSkeletonPoint(
489
531
width,
490
532
height
491
533
),
492
492
-
rightAnkle = it[VNHumanBodyPoseObservationJointNameRightAnkle]?.location?.toSkeletonPoint(
534
534
+
rightAnkle = pointsMap[VNHumanBodyPoseObservationJointNameRightAnkle]?.location?.toSkeletonPoint(
493
535
width,
494
536
height
495
537
),
···
506
548
}
507
549
}?.isInFocusArea(focusArea) ?: false
508
550
}
509
509
-
regionOfInterest =
510
510
-
calculateRegionOfInterest(recognizedPoints)
551
551
+
552
552
+
regionOfInterest = calculateRegionOfInterest(recognizedPoints)
553
553
+
511
554
val updatedSkeleton = Skeleton(
512
555
timestamp = timestamp,
513
556
leftShoulder = recognizedPoints?.get(
···
549
592
height = height.toFloat(),
550
593
width = width.toFloat()
551
594
)
595
595
+
552
596
onSkeletonProcessed(skelBuffer.smooth(updatedSkeleton))
553
597
}
554
598
}
···
636
680
}
637
681
}
638
682
683
683
+
private fun stableTrackingId(observation: VNRecognizedObjectObservation): Int = observation.hashCode()
684
684
+
639
685
data class DetectedObject @OptIn(ExperimentalForeignApi::class) constructor(
640
686
val id: String = NSUUID().UUIDString(),
641
687
val label: String,
642
688
val confidence: Float,
643
689
val boundingBox: CValue<CGRect>
644
690
)
645
645
-