react-native-nitro-pose-exercises 1.1.13 → 1.1.15
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/android/build.gradle +1 -1
- package/android/gradle.properties +1 -1
- package/android/src/main/java/com/margelo/nitro/nitroposeexercises/NitroPoseExercises.kt +146 -101
- package/ios/NitroPoseExercises.swift +61 -30
- package/lib/module/NitroPoseExercises.nitro.js.map +1 -1
- package/lib/module/config/pushup.js +24 -43
- package/lib/module/config/pushup.js.map +1 -1
- package/lib/typescript/src/NitroPoseExercises.nitro.d.ts +2 -1
- package/lib/typescript/src/NitroPoseExercises.nitro.d.ts.map +1 -1
- package/lib/typescript/src/config/pushup.d.ts.map +1 -1
- package/nitrogen/generated/android/c++/JHybridNitroPoseExercisesSpec.cpp +6 -2
- package/nitrogen/generated/android/c++/JHybridNitroPoseExercisesSpec.hpp +2 -1
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroposeexercises/HybridNitroPoseExercisesSpec.kt +5 -1
- package/nitrogen/generated/ios/c++/HybridNitroPoseExercisesSpecSwift.hpp +8 -2
- package/nitrogen/generated/ios/swift/HybridNitroPoseExercisesSpec.swift +2 -1
- package/nitrogen/generated/ios/swift/HybridNitroPoseExercisesSpec_cxx.swift +17 -2
- package/nitrogen/generated/shared/c++/HybridNitroPoseExercisesSpec.cpp +2 -1
- package/nitrogen/generated/shared/c++/HybridNitroPoseExercisesSpec.hpp +2 -1
- package/package.json +24 -4
- package/src/NitroPoseExercises.nitro.ts +13 -2
- package/src/config/pushup.ts +19 -43
- package/android/src/main/cpp/frame_helper.cpp +0 -37
- package/android/src/main/java/com/margelo/nitro/nitroposeexercises/FrameHelper.kt +0 -12
package/android/build.gradle
CHANGED
|
@@ -1,23 +1,32 @@
|
|
|
1
1
|
package com.margelo.nitro.nitroposeexercises
|
|
2
2
|
|
|
3
|
-
import
|
|
4
|
-
import
|
|
5
|
-
import android.
|
|
3
|
+
import com.margelo.nitro.camera.HybridFrameSpec
|
|
4
|
+
import com.margelo.nitro.camera.public.NativeFrame
|
|
5
|
+
import com.google.android.gms.tasks.Tasks
|
|
6
|
+
import java.util.concurrent.TimeUnit
|
|
7
|
+
|
|
8
|
+
// import android.graphics.Matrix
|
|
6
9
|
import androidx.annotation.Keep
|
|
7
10
|
import com.facebook.proguard.annotations.DoNotStrip
|
|
8
|
-
import com.google.mlkit.vision.common.InputImage
|
|
9
11
|
import com.google.mlkit.vision.pose.PoseDetection
|
|
10
12
|
import com.google.mlkit.vision.pose.PoseDetector
|
|
11
13
|
import com.google.mlkit.vision.pose.PoseLandmark
|
|
12
14
|
import com.google.mlkit.vision.pose.defaults.PoseDetectorOptions
|
|
13
15
|
import com.margelo.nitro.NitroModules
|
|
14
16
|
import com.margelo.nitro.core.Promise
|
|
15
|
-
import com.margelo.nitro.camera.HybridFrameSpec
|
|
16
17
|
import kotlin.math.acos
|
|
17
18
|
import kotlin.math.max
|
|
18
19
|
import kotlin.math.min
|
|
19
20
|
import kotlin.math.sqrt
|
|
20
21
|
|
|
22
|
+
// import android.media.Image
|
|
23
|
+
// import android.graphics.Bitmap
|
|
24
|
+
import com.google.mlkit.vision.common.InputImage
|
|
25
|
+
// import com.margelo.nitro.core.ArrayBuffer
|
|
26
|
+
// import java.nio.ByteBuffer
|
|
27
|
+
|
|
28
|
+
// import java.nio.ByteOrder
|
|
29
|
+
|
|
21
30
|
@Keep
|
|
22
31
|
@DoNotStrip
|
|
23
32
|
class NitroPoseExercises : HybridNitroPoseExercisesSpec() {
|
|
@@ -205,88 +214,91 @@ override fun isReady(): Boolean {
|
|
|
205
214
|
// Frame Processing (ML Kit — async with cached results)
|
|
206
215
|
// ═══════════════════════════════════════════════════════════
|
|
207
216
|
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
frameCount++
|
|
213
|
-
if (frameCount % processEveryNFrames != 0) return
|
|
214
|
-
|
|
215
|
-
try {
|
|
216
|
-
val nativeBuffer = frame.getNativeBuffer()
|
|
217
|
-
val bitmap = FrameHelper.hardwareBufferToBitmap(nativeBuffer.pointer) ?: return
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
val rotation = rotationDegreesFromFrame(frame)
|
|
221
|
-
val inputImage = InputImage.fromBitmap(bitmap, rotation)
|
|
222
|
-
|
|
223
|
-
val imageWidth = bitmap.width.toDouble()
|
|
224
|
-
val imageHeight = bitmap.height.toDouble()
|
|
225
|
-
|
|
226
|
-
poseDetector!!.process(inputImage)
|
|
227
|
-
.addOnSuccessListener { pose ->
|
|
228
|
-
val poseLandmarks = pose.allPoseLandmarks
|
|
229
|
-
|
|
230
|
-
if (poseLandmarks.isNotEmpty()) {
|
|
231
|
-
if (poseWasLost) {
|
|
232
|
-
poseWasLost = false
|
|
233
|
-
onPoseRegained?.invoke()
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
val landmarkArray = Array(34) { Landmark(x = 0.0, y = 0.0, z = 0.0, visibility = 0.0) }
|
|
237
|
-
|
|
238
|
-
for (poseLandmark in poseLandmarks) {
|
|
239
|
-
val mediaPipeIndex = mlKitToMediaPipeMap[poseLandmark.landmarkType] ?: continue
|
|
240
|
-
if (mediaPipeIndex >= 34) continue
|
|
241
|
-
|
|
242
|
-
landmarkArray[mediaPipeIndex] = Landmark(
|
|
243
|
-
x = poseLandmark.position.x.toDouble() / imageWidth,
|
|
244
|
-
y = poseLandmark.position.y.toDouble() / imageHeight,
|
|
245
|
-
z = poseLandmark.position3D.z.toDouble(),
|
|
246
|
-
visibility = poseLandmark.inFrameLikelihood.toDouble()
|
|
247
|
-
)
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
synchronized(landmarkLock) {
|
|
251
|
-
cachedLandmarks = landmarkArray
|
|
252
|
-
}
|
|
253
|
-
} else {
|
|
254
|
-
if (!poseWasLost) {
|
|
255
|
-
poseWasLost = true
|
|
256
|
-
onPoseLost?.invoke()
|
|
257
|
-
}
|
|
258
|
-
synchronized(landmarkLock) {
|
|
259
|
-
cachedLandmarks = emptyArray()
|
|
260
|
-
}
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
bitmap.recycle()
|
|
264
|
-
}
|
|
265
|
-
.addOnFailureListener { e ->
|
|
266
|
-
println("[PoseExercise] ML Kit error: ${e.message}")
|
|
267
|
-
bitmap.recycle()
|
|
268
|
-
}
|
|
217
|
+
// Reusable scratch — allocated once, never GC'd per frame
|
|
218
|
+
@Volatile private var lastProcessTime: Long = 0L
|
|
219
|
+
private val minIntervalMs: Long = 66L // ~15fps cap; bump down to 33 for ~30fps
|
|
269
220
|
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
221
|
+
// Time-based throttle — more reliable than frame-count under variable FPS
|
|
222
|
+
@Volatile private var lastProcessTime: Long = 0L
|
|
223
|
+
private val minIntervalMs: Long = 66L // ~15fps; lower to 33 for ~30fps once release build is fast enough
|
|
224
|
+
|
|
225
|
+
override fun processFrameAndroid(frame: HybridFrameSpec) {
|
|
226
|
+
if (_status != SessionStatus.ACTIVE && _status != SessionStatus.COUNTDOWN) return
|
|
227
|
+
if (!isInitialized || poseDetector == null) return
|
|
228
|
+
|
|
229
|
+
val now = System.currentTimeMillis()
|
|
230
|
+
if (now - lastProcessTime < minIntervalMs) return
|
|
231
|
+
lastProcessTime = now
|
|
232
|
+
|
|
233
|
+
val nativeFrame = frame as? NativeFrame ?: return
|
|
234
|
+
val imageProxy = nativeFrame.image ?: return
|
|
235
|
+
val mediaImage = imageProxy.image ?: return
|
|
236
|
+
|
|
237
|
+
try {
|
|
238
|
+
val rotation = imageProxy.imageInfo.rotationDegrees
|
|
239
|
+
val inputImage = InputImage.fromMediaImage(mediaImage, rotation)
|
|
240
|
+
|
|
241
|
+
val rotated = rotation == 90 || rotation == 270
|
|
242
|
+
val imageWidth = (if (rotated) mediaImage.height else mediaImage.width).toDouble()
|
|
243
|
+
val imageHeight = (if (rotated) mediaImage.width else mediaImage.height).toDouble()
|
|
244
|
+
|
|
245
|
+
// SYNC inference — the frame's underlying ImageProxy is only valid until
|
|
246
|
+
// VisionCamera disposes the frame after this method returns. Tasks.await
|
|
247
|
+
// blocks the worklet thread which is exactly what we want here.
|
|
248
|
+
val pose = try {
|
|
249
|
+
Tasks.await(poseDetector!!.process(inputImage), 200, TimeUnit.MILLISECONDS)
|
|
250
|
+
} catch (e: Exception) {
|
|
251
|
+
// Timeout or failure — skip this frame quietly
|
|
252
|
+
null
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
if (pose == null) return
|
|
256
|
+
|
|
257
|
+
val poseLandmarks = pose.allPoseLandmarks
|
|
258
|
+
|
|
259
|
+
if (poseLandmarks.isNotEmpty()) {
|
|
260
|
+
if (poseWasLost) {
|
|
261
|
+
poseWasLost = false
|
|
262
|
+
onPoseRegained?.invoke()
|
|
274
263
|
}
|
|
275
264
|
|
|
276
|
-
|
|
265
|
+
val landmarkArray = Array(34) { Landmark(x = 0.0, y = 0.0, z = 0.0, visibility = 0.0) }
|
|
266
|
+
|
|
267
|
+
for (poseLandmark in poseLandmarks) {
|
|
268
|
+
val mediaPipeIndex = mlKitToMediaPipeMap[poseLandmark.landmarkType] ?: continue
|
|
269
|
+
if (mediaPipeIndex >= 34) continue
|
|
277
270
|
|
|
278
|
-
|
|
279
|
-
|
|
271
|
+
landmarkArray[mediaPipeIndex] = Landmark(
|
|
272
|
+
x = (poseLandmark.position3D.x / imageWidth).coerceIn(0.0, 1.0),
|
|
273
|
+
y = (poseLandmark.position3D.y / imageHeight).coerceIn(0.0, 1.0),
|
|
274
|
+
z = poseLandmark.position3D.z.toDouble(),
|
|
275
|
+
visibility = poseLandmark.inFrameLikelihood.toDouble()
|
|
276
|
+
)
|
|
280
277
|
}
|
|
281
278
|
|
|
282
|
-
|
|
283
|
-
|
|
279
|
+
synchronized(landmarkLock) {
|
|
280
|
+
cachedLandmarks = landmarkArray
|
|
281
|
+
_landmarks = landmarkArray
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
processExerciseLogic()
|
|
285
|
+
} else {
|
|
286
|
+
if (!poseWasLost) {
|
|
287
|
+
poseWasLost = true
|
|
288
|
+
onPoseLost?.invoke()
|
|
289
|
+
}
|
|
284
290
|
}
|
|
291
|
+
} catch (e: Exception) {
|
|
292
|
+
println("[PoseExercise] processFrameAndroid error: ${e.message}")
|
|
285
293
|
}
|
|
294
|
+
// NOTE: we do NOT close the ImageProxy ourselves.
|
|
295
|
+
// VisionCamera owns the frame lifecycle and will release it when
|
|
296
|
+
// frame.dispose() runs in the JS worklet after we return.
|
|
297
|
+
}
|
|
286
298
|
|
|
287
|
-
|
|
288
|
-
//
|
|
289
|
-
|
|
299
|
+
override fun processFrameIOS(frame: HybridFrameSpec) {
|
|
300
|
+
// no-op on Android
|
|
301
|
+
}
|
|
290
302
|
|
|
291
303
|
private fun processExerciseLogic() {
|
|
292
304
|
val config = exerciseConfig ?: return
|
|
@@ -624,54 +636,87 @@ private fun isPostureValid(family: PostureFamily, threshold: Double): Boolean {
|
|
|
624
636
|
val lk = _landmarks[25]; val rk = _landmarks[26]
|
|
625
637
|
val la = _landmarks[27]; val ra = _landmarks[28]
|
|
626
638
|
|
|
627
|
-
//
|
|
628
|
-
val
|
|
629
|
-
|
|
630
|
-
if (!torsoVisible) return false
|
|
631
|
-
|
|
632
|
-
val shoulderY = (ls.y + rs.y) / 2
|
|
633
|
-
val hipY = (lh.y + rh.y) / 2
|
|
634
|
-
val shoulderX = (ls.x + rs.x) / 2
|
|
635
|
-
val hipX = (lh.x + rh.x) / 2
|
|
639
|
+
// Shoulders mandatory; everything below is optional for close-range framing
|
|
640
|
+
val shouldersVisible = ls.visibility > threshold && rs.visibility > threshold
|
|
641
|
+
if (!shouldersVisible) return false
|
|
636
642
|
|
|
643
|
+
val hipsVisible = lh.visibility > threshold && rh.visibility > threshold
|
|
637
644
|
val kneesVisible = lk.visibility > threshold && rk.visibility > threshold
|
|
638
645
|
val anklesVisible = la.visibility > threshold && ra.visibility > threshold
|
|
646
|
+
|
|
647
|
+
val shoulderY = (ls.y + rs.y) / 2
|
|
648
|
+
val shoulderX = (ls.x + rs.x) / 2
|
|
649
|
+
val hipY = if (hipsVisible) (lh.y + rh.y) / 2 else shoulderY
|
|
650
|
+
val hipX = if (hipsVisible) (lh.x + rh.x) / 2 else shoulderX
|
|
639
651
|
val kneeY = if (kneesVisible) (lk.y + rk.y) / 2 else hipY
|
|
640
652
|
val ankleY = if (anklesVisible) (la.y + ra.y) / 2 else kneeY
|
|
641
653
|
|
|
654
|
+
// Mirror-invariant — works for front and back camera identically
|
|
655
|
+
val shoulderWidth = kotlin.math.abs(ls.x - rs.x)
|
|
656
|
+
|
|
642
657
|
return when (family) {
|
|
643
658
|
PostureFamily.HORIZONTALPRONE, PostureFamily.SUPINE -> {
|
|
659
|
+
if (!hipsVisible) return false
|
|
660
|
+
|
|
661
|
+
// Case A: side view — shoulders/hips/(ankles) in a horizontal band
|
|
644
662
|
val ys = if (anklesVisible)
|
|
645
663
|
listOf(shoulderY, hipY, ankleY)
|
|
646
664
|
else
|
|
647
665
|
listOf(shoulderY, hipY)
|
|
648
|
-
(ys.max() - ys.min()) < 0.25
|
|
666
|
+
if ((ys.max() - ys.min()) < 0.25) return true
|
|
667
|
+
|
|
668
|
+
// Case B: front-facing prone (pushup head-on) — large y-spread,
|
|
669
|
+
// fall back to upper-body geometry
|
|
670
|
+
val le = _landmarks[13]; val re = _landmarks[14]
|
|
671
|
+
val lw = _landmarks[15]; val rw = _landmarks[16]
|
|
672
|
+
val upperBodyVisible = le.visibility > threshold && re.visibility > threshold &&
|
|
673
|
+
lw.visibility > threshold && rw.visibility > threshold
|
|
674
|
+
if (!upperBodyVisible) return false
|
|
675
|
+
|
|
676
|
+
val wristY = (lw.y + rw.y) / 2
|
|
677
|
+
wristY > shoulderY + 0.03 && shoulderWidth > 0.10
|
|
649
678
|
}
|
|
679
|
+
|
|
650
680
|
PostureFamily.STANDINGUPRIGHT -> {
|
|
651
|
-
if (
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
681
|
+
if (hipsVisible) {
|
|
682
|
+
if (kneesVisible) {
|
|
683
|
+
shoulderY < hipY - 0.05 &&
|
|
684
|
+
hipY < kneeY + 0.05 &&
|
|
685
|
+
(if (anklesVisible) kneeY < ankleY else true)
|
|
686
|
+
} else {
|
|
687
|
+
shoulderY < hipY - 0.05
|
|
688
|
+
}
|
|
655
689
|
} else {
|
|
656
|
-
|
|
690
|
+
// Hips cropped (close-range front-facing) — accept based on shoulder span
|
|
691
|
+
shoulderWidth > 0.08
|
|
657
692
|
}
|
|
658
693
|
}
|
|
694
|
+
|
|
659
695
|
PostureFamily.SEATED -> {
|
|
660
|
-
|
|
661
|
-
shoulderY < hipY - 0.05 && kotlin.math.abs(hipY - kneeY) < 0.20
|
|
662
|
-
|
|
663
|
-
|
|
696
|
+
when {
|
|
697
|
+
hipsVisible && kneesVisible -> shoulderY < hipY - 0.05 && kotlin.math.abs(hipY - kneeY) < 0.20
|
|
698
|
+
hipsVisible -> shoulderY < hipY - 0.05
|
|
699
|
+
else -> shoulderWidth > 0.08
|
|
664
700
|
}
|
|
665
701
|
}
|
|
702
|
+
|
|
666
703
|
PostureFamily.SIDEPLANK -> {
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
704
|
+
if (!hipsVisible) false
|
|
705
|
+
else {
|
|
706
|
+
val ySpread = kotlin.math.abs(shoulderY - hipY)
|
|
707
|
+
val shoulderHipDx = kotlin.math.abs(shoulderX - hipX)
|
|
708
|
+
ySpread < 0.20 && shoulderHipDx < 0.15
|
|
709
|
+
}
|
|
670
710
|
}
|
|
711
|
+
|
|
671
712
|
PostureFamily.INVERTED -> {
|
|
672
|
-
if (!anklesVisible) false
|
|
713
|
+
if (!hipsVisible || !anklesVisible) false
|
|
673
714
|
else hipY < shoulderY && hipY < ankleY
|
|
674
715
|
}
|
|
716
|
+
|
|
675
717
|
PostureFamily.NONE -> true
|
|
676
718
|
}
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
|
|
677
722
|
}
|
|
@@ -178,7 +178,7 @@ private static func cgOrientation(orientation: CameraOrientation, isMirrored: Bo
|
|
|
178
178
|
// MARK: - Frame Processing (Apple Vision)
|
|
179
179
|
// ═══════════════════════════════════════════════════════════
|
|
180
180
|
|
|
181
|
-
|
|
181
|
+
func processFrameIOS(frame: any HybridFrameSpec) throws {
|
|
182
182
|
guard _status == .active || _status == .countdown else { return }
|
|
183
183
|
|
|
184
184
|
guard isInitialized else { return }
|
|
@@ -243,8 +243,6 @@ private static func cgOrientation(orientation: CameraOrientation, isMirrored: Bo
|
|
|
243
243
|
visibility: confidence
|
|
244
244
|
)
|
|
245
245
|
|
|
246
|
-
// Debug logging — uncomment to verify mapping
|
|
247
|
-
// print("[PoseExercise] \(jointName.rawValue.rawValue) → index \(mediaPipeIndex): x=\(String(format: "%.3f", point.location.x)) y=\(String(format: "%.3f", 1.0 - Double(point.location.y))) conf=\(String(format: "%.2f", confidence))")
|
|
248
246
|
|
|
249
247
|
} catch {
|
|
250
248
|
// Joint not detected — leave as zero visibility
|
|
@@ -263,6 +261,10 @@ private static func cgOrientation(orientation: CameraOrientation, isMirrored: Bo
|
|
|
263
261
|
}
|
|
264
262
|
}
|
|
265
263
|
|
|
264
|
+
// func processFrameAndroid(buffer: ArrayBuffer, width: Double, height: Double, rotation: Double) {
|
|
265
|
+
func processFrameAndroid(frame: any HybridFrameSpec) {
|
|
266
|
+
// no-op on iOS
|
|
267
|
+
}
|
|
266
268
|
// ═══════════════════════════════════════════════════════════
|
|
267
269
|
// MARK: - Exercise Logic Engine
|
|
268
270
|
// ═══════════════════════════════════════════════════════════
|
|
@@ -353,56 +355,85 @@ private func isPostureValid(family: PostureFamily, threshold: Double) -> Bool {
|
|
|
353
355
|
let lk = _landmarks[25], rk = _landmarks[26]
|
|
354
356
|
let la = _landmarks[27], ra = _landmarks[28]
|
|
355
357
|
|
|
356
|
-
//
|
|
357
|
-
let
|
|
358
|
-
|
|
359
|
-
guard torsoVisible else { return false }
|
|
360
|
-
|
|
361
|
-
let shoulderY = (ls.y + rs.y) / 2
|
|
362
|
-
let hipY = (lh.y + rh.y) / 2
|
|
363
|
-
let shoulderX = (ls.x + rs.x) / 2
|
|
364
|
-
let hipX = (lh.x + rh.x) / 2
|
|
358
|
+
// Shoulders are mandatory; hips/knees/ankles are optional for close-range framing
|
|
359
|
+
let shouldersVisible = ls.visibility > threshold && rs.visibility > threshold
|
|
360
|
+
guard shouldersVisible else { return false }
|
|
365
361
|
|
|
362
|
+
let hipsVisible = lh.visibility > threshold && rh.visibility > threshold
|
|
366
363
|
let kneesVisible = lk.visibility > threshold && rk.visibility > threshold
|
|
367
364
|
let anklesVisible = la.visibility > threshold && ra.visibility > threshold
|
|
365
|
+
|
|
366
|
+
let shoulderY = (ls.y + rs.y) / 2
|
|
367
|
+
let shoulderX = (ls.x + rs.x) / 2
|
|
368
|
+
let hipY = hipsVisible ? (lh.y + rh.y) / 2 : shoulderY
|
|
369
|
+
let hipX = hipsVisible ? (lh.x + rh.x) / 2 : shoulderX
|
|
368
370
|
let kneeY = kneesVisible ? (lk.y + rk.y) / 2 : hipY
|
|
369
371
|
let ankleY = anklesVisible ? (la.y + ra.y) / 2 : kneeY
|
|
370
372
|
|
|
373
|
+
// Shoulder width is camera-mirror invariant: |x1 - x2| is the same
|
|
374
|
+
// whether the front camera mirror has been applied or not.
|
|
375
|
+
let shoulderWidth = (ls.x - rs.x).magnitude
|
|
376
|
+
|
|
371
377
|
switch family {
|
|
372
378
|
case .horizontalprone, .supine:
|
|
373
|
-
//
|
|
379
|
+
// Need hips to make any meaningful judgment for prone/supine
|
|
380
|
+
guard hipsVisible else { return false }
|
|
381
|
+
|
|
382
|
+
// Case A: side view — shoulders, hips, ankles in a horizontal band
|
|
374
383
|
let ys: [Double] = anklesVisible
|
|
375
384
|
? [shoulderY, hipY, ankleY]
|
|
376
385
|
: [shoulderY, hipY]
|
|
377
|
-
|
|
386
|
+
let ySpread = (ys.max() ?? 0) - (ys.min() ?? 0)
|
|
387
|
+
if ySpread < 0.25 {
|
|
388
|
+
return true
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
// Case B: front-facing prone (e.g. pushup viewed head-on) — body extends
|
|
392
|
+
// away along Z, so y-spread is large. Fall back to upper-body geometry.
|
|
393
|
+
let le = _landmarks[13], re = _landmarks[14]
|
|
394
|
+
let lw = _landmarks[15], rw = _landmarks[16]
|
|
395
|
+
let upperBodyVisible = le.visibility > threshold && re.visibility > threshold
|
|
396
|
+
&& lw.visibility > threshold && rw.visibility > threshold
|
|
397
|
+
guard upperBodyVisible else { return false }
|
|
398
|
+
|
|
399
|
+
let wristY = (lw.y + rw.y) / 2
|
|
400
|
+
guard wristY > shoulderY + 0.03 else { return false }
|
|
401
|
+
guard shoulderWidth > 0.10 else { return false }
|
|
402
|
+
return true
|
|
378
403
|
|
|
379
404
|
case .standingupright:
|
|
380
|
-
//
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
405
|
+
// Front-facing standing: user often crops hips/knees at close range.
|
|
406
|
+
// Use a tiered check based on what's visible.
|
|
407
|
+
if hipsVisible {
|
|
408
|
+
if kneesVisible {
|
|
409
|
+
return shoulderY < hipY - 0.05
|
|
410
|
+
&& hipY < kneeY + 0.05
|
|
411
|
+
&& (anklesVisible ? kneeY < ankleY : true)
|
|
412
|
+
}
|
|
413
|
+
return shoulderY < hipY - 0.05
|
|
387
414
|
}
|
|
415
|
+
// Hips off-frame — accept if shoulders form a reasonable horizontal span,
|
|
416
|
+
// which means the person is upright and facing (or backing) the camera.
|
|
417
|
+
return shoulderWidth > 0.08
|
|
388
418
|
|
|
389
419
|
case .seated:
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
420
|
+
if hipsVisible && kneesVisible {
|
|
421
|
+
return shoulderY < hipY - 0.05 && (hipY - kneeY).magnitude < 0.20
|
|
422
|
+
}
|
|
423
|
+
if hipsVisible {
|
|
394
424
|
return shoulderY < hipY - 0.05
|
|
395
425
|
}
|
|
426
|
+
// Seated with hips out of frame is rare; fall back to upright shoulder span
|
|
427
|
+
return shoulderWidth > 0.08
|
|
396
428
|
|
|
397
429
|
case .sideplank:
|
|
398
|
-
|
|
399
|
-
let ySpread =
|
|
400
|
-
let shoulderHipDx =
|
|
430
|
+
guard hipsVisible else { return false }
|
|
431
|
+
let ySpread = (shoulderY - hipY).magnitude
|
|
432
|
+
let shoulderHipDx = (shoulderX - hipX).magnitude
|
|
401
433
|
return ySpread < 0.20 && shoulderHipDx < 0.15
|
|
402
434
|
|
|
403
435
|
case .inverted:
|
|
404
|
-
|
|
405
|
-
guard anklesVisible else { return false }
|
|
436
|
+
guard hipsVisible, anklesVisible else { return false }
|
|
406
437
|
return hipY < shoulderY && hipY < ankleY
|
|
407
438
|
|
|
408
439
|
case .none:
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"names":["NitroModules","nitroPoseExercises","createHybridObject"],"sourceRoot":"../../src","sources":["NitroPoseExercises.nitro.ts"],"mappings":";;AAAA,SAA4BA,YAAY,QAAQ,4BAA4B;;AAI5E;;AAqBA;;AASA;;AAsCA;;AAmCA;;
|
|
1
|
+
{"version":3,"names":["NitroModules","nitroPoseExercises","createHybridObject"],"sourceRoot":"../../src","sources":["NitroPoseExercises.nitro.ts"],"mappings":";;AAAA,SAA4BA,YAAY,QAAQ,4BAA4B;;AAI5E;;AAqBA;;AASA;;AAsCA;;AAmCA;;AAuDA,MAAMC,kBAAkB,GACtBD,YAAY,CAACE,kBAAkB,CAAqB,cAAc,CAAC;AAErE,SAASD,kBAAkB","ignoreList":[]}
|
|
@@ -9,67 +9,48 @@
|
|
|
9
9
|
export const PUSHUP_CONFIG = {
|
|
10
10
|
name: 'Push-Up',
|
|
11
11
|
type: 'rep',
|
|
12
|
-
postureFamily: '
|
|
13
|
-
visibilityThreshold: 0.
|
|
14
|
-
// ← add
|
|
12
|
+
postureFamily: 'none',
|
|
13
|
+
visibilityThreshold: 0.3,
|
|
15
14
|
cameraAngle: 'front',
|
|
16
15
|
angles: [{
|
|
17
16
|
name: 'leftElbow',
|
|
18
17
|
landmarkA: 11,
|
|
19
|
-
// left shoulder
|
|
20
18
|
landmarkB: 13,
|
|
21
|
-
|
|
22
|
-
landmarkC: 15 // left wrist
|
|
19
|
+
landmarkC: 15
|
|
23
20
|
}, {
|
|
24
21
|
name: 'rightElbow',
|
|
25
22
|
landmarkA: 12,
|
|
26
|
-
// right shoulder
|
|
27
23
|
landmarkB: 14,
|
|
28
|
-
|
|
29
|
-
landmarkC: 16 // right wrist
|
|
30
|
-
}, {
|
|
31
|
-
name: 'leftHip',
|
|
32
|
-
landmarkA: 11,
|
|
33
|
-
// left shoulder
|
|
34
|
-
landmarkB: 23,
|
|
35
|
-
// left hip (vertex)
|
|
36
|
-
landmarkC: 27 // left ankle
|
|
37
|
-
}, {
|
|
38
|
-
name: 'rightHip',
|
|
39
|
-
landmarkA: 12,
|
|
40
|
-
// right shoulder
|
|
41
|
-
landmarkB: 24,
|
|
42
|
-
// right hip (vertex)
|
|
43
|
-
landmarkC: 28 // right ankle
|
|
24
|
+
landmarkC: 16
|
|
44
25
|
}],
|
|
45
|
-
phases: [
|
|
26
|
+
phases: [
|
|
27
|
+
// Calibrated for 2D-projected angles in front-facing portrait filming.
|
|
28
|
+
// Real anatomical 180° appears as ~140-150° due to perspective foreshortening.
|
|
29
|
+
{
|
|
46
30
|
phase: 'up',
|
|
47
31
|
angleName: 'leftElbow',
|
|
48
|
-
minAngle:
|
|
32
|
+
minAngle: 130,
|
|
49
33
|
maxAngle: 180
|
|
50
34
|
}, {
|
|
51
35
|
phase: 'down',
|
|
52
36
|
angleName: 'leftElbow',
|
|
53
|
-
minAngle:
|
|
54
|
-
maxAngle:
|
|
37
|
+
minAngle: 40,
|
|
38
|
+
maxAngle: 80
|
|
55
39
|
}],
|
|
56
40
|
repSequence: ['up', 'down', 'up'],
|
|
57
|
-
formRules: [
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
angleName: 'leftHip',
|
|
70
|
-
minAngle: 160,
|
|
71
|
-
maxAngle: 180
|
|
41
|
+
formRules: [
|
|
42
|
+
// Trigger when descending but stalled above true-down threshold
|
|
43
|
+
{
|
|
44
|
+
name: 'shallowRep',
|
|
45
|
+
message: 'Go lower',
|
|
46
|
+
severity: 'info',
|
|
47
|
+
angleName: 'leftElbow',
|
|
48
|
+
minAngle: 80,
|
|
49
|
+
maxAngle: 130 // "limbo zone" — too low to be up, too high to be down
|
|
50
|
+
// Note: ideally this only fires when angle has stalled, not on the way down.
|
|
51
|
+
// If your formRule engine doesn't support velocity/stall detection,
|
|
52
|
+
// accept that it'll fire briefly during transitions too.
|
|
72
53
|
}],
|
|
73
|
-
holdDurationMs: 0
|
|
54
|
+
holdDurationMs: 0
|
|
74
55
|
};
|
|
75
56
|
//# sourceMappingURL=pushup.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"names":["PUSHUP_CONFIG","name","type","postureFamily","visibilityThreshold","cameraAngle","angles","landmarkA","landmarkB","landmarkC","phases","phase","angleName","minAngle","maxAngle","repSequence","formRules","message","severity","holdDurationMs"],"sourceRoot":"../../../src","sources":["config/pushup.ts"],"mappings":";;AAEA;AACA;AACA;AACA;AACA;;AAEA,OAAO,MAAMA,aAA6B,GAAG;EAC3CC,IAAI,EAAE,SAAS;EACfC,IAAI,EAAE,KAAK;EACXC,aAAa,EAAE,
|
|
1
|
+
{"version":3,"names":["PUSHUP_CONFIG","name","type","postureFamily","visibilityThreshold","cameraAngle","angles","landmarkA","landmarkB","landmarkC","phases","phase","angleName","minAngle","maxAngle","repSequence","formRules","message","severity","holdDurationMs"],"sourceRoot":"../../../src","sources":["config/pushup.ts"],"mappings":";;AAEA;AACA;AACA;AACA;AACA;;AAEA,OAAO,MAAMA,aAA6B,GAAG;EAC3CC,IAAI,EAAE,SAAS;EACfC,IAAI,EAAE,KAAK;EACXC,aAAa,EAAE,MAAM;EACrBC,mBAAmB,EAAE,GAAG;EACxBC,WAAW,EAAE,OAAO;EACpBC,MAAM,EAAE,CACN;IAAEL,IAAI,EAAE,WAAW;IAAEM,SAAS,EAAE,EAAE;IAAEC,SAAS,EAAE,EAAE;IAAEC,SAAS,EAAE;EAAG,CAAC,EAClE;IAAER,IAAI,EAAE,YAAY;IAAEM,SAAS,EAAE,EAAE;IAAEC,SAAS,EAAE,EAAE;IAAEC,SAAS,EAAE;EAAG,CAAC,CACpE;EACDC,MAAM,EAAE;EACN;EACA;EACA;IAAEC,KAAK,EAAE,IAAI;IAAEC,SAAS,EAAE,WAAW;IAAEC,QAAQ,EAAE,GAAG;IAAEC,QAAQ,EAAE;EAAI,CAAC,EACrE;IAAEH,KAAK,EAAE,MAAM;IAAEC,SAAS,EAAE,WAAW;IAAEC,QAAQ,EAAE,EAAE;IAAEC,QAAQ,EAAE;EAAG,CAAC,CACtE;EACDC,WAAW,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,CAAC;EACjCC,SAAS,EAAE;EACT;EACA;IACEf,IAAI,EAAE,YAAY;IAClBgB,OAAO,EAAE,UAAU;IACnBC,QAAQ,EAAE,MAAM;IAChBN,SAAS,EAAE,WAAW;IACtBC,QAAQ,EAAE,EAAE;IACZC,QAAQ,EAAE,GAAG,CAAE;IACf;IACA;IACA;EACF,CAAC,CACF;EACDK,cAAc,EAAE;AAClB,CAAC","ignoreList":[]}
|
|
@@ -80,7 +80,8 @@ interface NitroPoseExercises extends HybridObject<{
|
|
|
80
80
|
release(): void;
|
|
81
81
|
loadExercise(config: ExerciseConfig): void;
|
|
82
82
|
readonly status: SessionStatus;
|
|
83
|
-
|
|
83
|
+
processFrameIOS(frame: Frame): void;
|
|
84
|
+
processFrameAndroid(frame: Frame): void;
|
|
84
85
|
onRepComplete: ((data: RepData) => void) | undefined;
|
|
85
86
|
onPhaseChange: ((phase: ExercisePhase) => void) | undefined;
|
|
86
87
|
onFormFeedback: ((feedback: FormFeedback) => void) | undefined;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"NitroPoseExercises.nitro.d.ts","sourceRoot":"","sources":["../../../src/NitroPoseExercises.nitro.ts"],"names":[],"mappings":"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
|
|
1
|
+
{"version":3,"file":"NitroPoseExercises.nitro.d.ts","sourceRoot":"","sources":["../../../src/NitroPoseExercises.nitro.ts"],"names":[],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"pushup.d.ts","sourceRoot":"","sources":["../../../../src/config/pushup.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,6BAA6B,CAAC;AAQlE,eAAO,MAAM,aAAa,EAAE,
|
|
1
|
+
{"version":3,"file":"pushup.d.ts","sourceRoot":"","sources":["../../../../src/config/pushup.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,6BAA6B,CAAC;AAQlE,eAAO,MAAM,aAAa,EAAE,cAgC3B,CAAC"}
|
|
@@ -329,8 +329,12 @@ namespace margelo::nitro::nitroposeexercises {
|
|
|
329
329
|
static const auto method = _javaPart->javaClassStatic()->getMethod<void(jni::alias_ref<JExerciseConfig> /* config */)>("loadExercise");
|
|
330
330
|
method(_javaPart, JExerciseConfig::fromCpp(config));
|
|
331
331
|
}
|
|
332
|
-
void JHybridNitroPoseExercisesSpec::
|
|
333
|
-
static const auto method = _javaPart->javaClassStatic()->getMethod<void(jni::alias_ref<margelo::nitro::camera::JHybridFrameSpec::JavaPart> /* frame */)>("
|
|
332
|
+
void JHybridNitroPoseExercisesSpec::processFrameIOS(const std::shared_ptr<margelo::nitro::camera::HybridFrameSpec>& frame) {
|
|
333
|
+
static const auto method = _javaPart->javaClassStatic()->getMethod<void(jni::alias_ref<margelo::nitro::camera::JHybridFrameSpec::JavaPart> /* frame */)>("processFrameIOS");
|
|
334
|
+
method(_javaPart, std::dynamic_pointer_cast<margelo::nitro::camera::JHybridFrameSpec>(frame)->getJavaPart());
|
|
335
|
+
}
|
|
336
|
+
void JHybridNitroPoseExercisesSpec::processFrameAndroid(const std::shared_ptr<margelo::nitro::camera::HybridFrameSpec>& frame) {
|
|
337
|
+
static const auto method = _javaPart->javaClassStatic()->getMethod<void(jni::alias_ref<margelo::nitro::camera::JHybridFrameSpec::JavaPart> /* frame */)>("processFrameAndroid");
|
|
334
338
|
method(_javaPart, std::dynamic_pointer_cast<margelo::nitro::camera::JHybridFrameSpec>(frame)->getJavaPart());
|
|
335
339
|
}
|
|
336
340
|
void JHybridNitroPoseExercisesSpec::startSession(double targetReps, double countdownSeconds) {
|
|
@@ -78,7 +78,8 @@ namespace margelo::nitro::nitroposeexercises {
|
|
|
78
78
|
std::shared_ptr<Promise<void>> initialize(const std::string& modelPath) override;
|
|
79
79
|
void release() override;
|
|
80
80
|
void loadExercise(const ExerciseConfig& config) override;
|
|
81
|
-
void
|
|
81
|
+
void processFrameIOS(const std::shared_ptr<margelo::nitro::camera::HybridFrameSpec>& frame) override;
|
|
82
|
+
void processFrameAndroid(const std::shared_ptr<margelo::nitro::camera::HybridFrameSpec>& frame) override;
|
|
82
83
|
void startSession(double targetReps, double countdownSeconds) override;
|
|
83
84
|
void pauseSession() override;
|
|
84
85
|
void resumeSession() override;
|
|
@@ -184,7 +184,11 @@ abstract class HybridNitroPoseExercisesSpec: HybridObject() {
|
|
|
184
184
|
|
|
185
185
|
@DoNotStrip
|
|
186
186
|
@Keep
|
|
187
|
-
abstract fun
|
|
187
|
+
abstract fun processFrameIOS(frame: com.margelo.nitro.camera.HybridFrameSpec): Unit
|
|
188
|
+
|
|
189
|
+
@DoNotStrip
|
|
190
|
+
@Keep
|
|
191
|
+
abstract fun processFrameAndroid(frame: com.margelo.nitro.camera.HybridFrameSpec): Unit
|
|
188
192
|
|
|
189
193
|
@DoNotStrip
|
|
190
194
|
@Keep
|
|
@@ -218,8 +218,14 @@ namespace margelo::nitro::nitroposeexercises {
|
|
|
218
218
|
std::rethrow_exception(__result.error());
|
|
219
219
|
}
|
|
220
220
|
}
|
|
221
|
-
inline void
|
|
222
|
-
auto __result = _swiftPart.
|
|
221
|
+
inline void processFrameIOS(const std::shared_ptr<margelo::nitro::camera::HybridFrameSpec>& frame) override {
|
|
222
|
+
auto __result = _swiftPart.processFrameIOS(frame);
|
|
223
|
+
if (__result.hasError()) [[unlikely]] {
|
|
224
|
+
std::rethrow_exception(__result.error());
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
inline void processFrameAndroid(const std::shared_ptr<margelo::nitro::camera::HybridFrameSpec>& frame) override {
|
|
228
|
+
auto __result = _swiftPart.processFrameAndroid(frame);
|
|
223
229
|
if (__result.hasError()) [[unlikely]] {
|
|
224
230
|
std::rethrow_exception(__result.error());
|
|
225
231
|
}
|
|
@@ -29,7 +29,8 @@ public protocol HybridNitroPoseExercisesSpec_protocol: HybridObject {
|
|
|
29
29
|
func initialize(modelPath: String) throws -> Promise<Void>
|
|
30
30
|
func release() throws -> Void
|
|
31
31
|
func loadExercise(config: ExerciseConfig) throws -> Void
|
|
32
|
-
func
|
|
32
|
+
func processFrameIOS(frame: (any HybridFrameSpec)) throws -> Void
|
|
33
|
+
func processFrameAndroid(frame: (any HybridFrameSpec)) throws -> Void
|
|
33
34
|
func startSession(targetReps: Double, countdownSeconds: Double) throws -> Void
|
|
34
35
|
func pauseSession() throws -> Void
|
|
35
36
|
func resumeSession() throws -> Void
|
|
@@ -487,9 +487,24 @@ open class HybridNitroPoseExercisesSpec_cxx {
|
|
|
487
487
|
}
|
|
488
488
|
|
|
489
489
|
@inline(__always)
|
|
490
|
-
public final func
|
|
490
|
+
public final func processFrameIOS(frame: bridge.std__shared_ptr_margelo__nitro__camera__HybridFrameSpec_) -> bridge.Result_void_ {
|
|
491
491
|
do {
|
|
492
|
-
try self.__implementation.
|
|
492
|
+
try self.__implementation.processFrameIOS(frame: { () -> any HybridFrameSpec in
|
|
493
|
+
let __unsafePointer = bridge.get_std__shared_ptr_margelo__nitro__camera__HybridFrameSpec_(frame)
|
|
494
|
+
let __instance = HybridFrameSpec_cxx.fromUnsafe(__unsafePointer)
|
|
495
|
+
return __instance.getHybridFrameSpec()
|
|
496
|
+
}())
|
|
497
|
+
return bridge.create_Result_void_()
|
|
498
|
+
} catch (let __error) {
|
|
499
|
+
let __exceptionPtr = __error.toCpp()
|
|
500
|
+
return bridge.create_Result_void_(__exceptionPtr)
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
@inline(__always)
|
|
505
|
+
public final func processFrameAndroid(frame: bridge.std__shared_ptr_margelo__nitro__camera__HybridFrameSpec_) -> bridge.Result_void_ {
|
|
506
|
+
do {
|
|
507
|
+
try self.__implementation.processFrameAndroid(frame: { () -> any HybridFrameSpec in
|
|
493
508
|
let __unsafePointer = bridge.get_std__shared_ptr_margelo__nitro__camera__HybridFrameSpec_(frame)
|
|
494
509
|
let __instance = HybridFrameSpec_cxx.fromUnsafe(__unsafePointer)
|
|
495
510
|
return __instance.getHybridFrameSpec()
|
|
@@ -39,7 +39,8 @@ namespace margelo::nitro::nitroposeexercises {
|
|
|
39
39
|
prototype.registerHybridMethod("initialize", &HybridNitroPoseExercisesSpec::initialize);
|
|
40
40
|
prototype.registerHybridMethod("release", &HybridNitroPoseExercisesSpec::release);
|
|
41
41
|
prototype.registerHybridMethod("loadExercise", &HybridNitroPoseExercisesSpec::loadExercise);
|
|
42
|
-
prototype.registerHybridMethod("
|
|
42
|
+
prototype.registerHybridMethod("processFrameIOS", &HybridNitroPoseExercisesSpec::processFrameIOS);
|
|
43
|
+
prototype.registerHybridMethod("processFrameAndroid", &HybridNitroPoseExercisesSpec::processFrameAndroid);
|
|
43
44
|
prototype.registerHybridMethod("startSession", &HybridNitroPoseExercisesSpec::startSession);
|
|
44
45
|
prototype.registerHybridMethod("pauseSession", &HybridNitroPoseExercisesSpec::pauseSession);
|
|
45
46
|
prototype.registerHybridMethod("resumeSession", &HybridNitroPoseExercisesSpec::resumeSession);
|
|
@@ -103,7 +103,8 @@ namespace margelo::nitro::nitroposeexercises {
|
|
|
103
103
|
virtual std::shared_ptr<Promise<void>> initialize(const std::string& modelPath) = 0;
|
|
104
104
|
virtual void release() = 0;
|
|
105
105
|
virtual void loadExercise(const ExerciseConfig& config) = 0;
|
|
106
|
-
virtual void
|
|
106
|
+
virtual void processFrameIOS(const std::shared_ptr<margelo::nitro::camera::HybridFrameSpec>& frame) = 0;
|
|
107
|
+
virtual void processFrameAndroid(const std::shared_ptr<margelo::nitro::camera::HybridFrameSpec>& frame) = 0;
|
|
107
108
|
virtual void startSession(double targetReps, double countdownSeconds) = 0;
|
|
108
109
|
virtual void pauseSession() = 0;
|
|
109
110
|
virtual void resumeSession() = 0;
|
package/package.json
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "react-native-nitro-pose-exercises",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.15",
|
|
4
4
|
"description": "Real-time on-device exercise tracking for React Native. Rep counting, form validation, and skeleton overlay powered by Apple Vision (iOS) and Google ML Kit (Android) with VisionCamera v5 via Nitro Modules.",
|
|
5
5
|
"main": "./lib/module/index.js",
|
|
6
6
|
"types": "./lib/typescript/src/index.d.ts",
|
|
7
|
+
"installConfig": {
|
|
8
|
+
"hoistingLimits": "workspaces"
|
|
9
|
+
},
|
|
7
10
|
"exports": {
|
|
8
11
|
".": {
|
|
9
12
|
"source": "./src/index.tsx",
|
|
@@ -89,8 +92,10 @@
|
|
|
89
92
|
"react-native": "0.85.3",
|
|
90
93
|
"react-native-builder-bob": "^0.41.0",
|
|
91
94
|
"react-native-nitro-modules": "^0.35.9",
|
|
95
|
+
"react-native-reanimated": "4.4.0",
|
|
92
96
|
"react-native-svg": "^15.15.5",
|
|
93
97
|
"react-native-vision-camera": "^5.0.11",
|
|
98
|
+
"react-native-vision-camera-resizer": "^5.0.11",
|
|
94
99
|
"react-native-vision-camera-worklets": "^5.0.11",
|
|
95
100
|
"react-native-worklets": "^0.9.1",
|
|
96
101
|
"release-it": "^19.2.4",
|
|
@@ -103,12 +108,27 @@
|
|
|
103
108
|
"react-native-nitro-modules": "*",
|
|
104
109
|
"react-native-reanimated": "*",
|
|
105
110
|
"react-native-vision-camera": "*",
|
|
111
|
+
"react-native-vision-camera-resizer": "*",
|
|
106
112
|
"react-native-vision-camera-worklets": "*",
|
|
107
113
|
"react-native-worklets": "*"
|
|
108
114
|
},
|
|
109
|
-
"workspaces":
|
|
110
|
-
"
|
|
111
|
-
|
|
115
|
+
"workspaces": {
|
|
116
|
+
"packages": [
|
|
117
|
+
"example"
|
|
118
|
+
],
|
|
119
|
+
"nohoist": [
|
|
120
|
+
"**/react-native",
|
|
121
|
+
"**/react-native/**",
|
|
122
|
+
"**/react-native-vision-camera",
|
|
123
|
+
"**/react-native-vision-camera/**",
|
|
124
|
+
"**/react-native-vision-camera-resizer",
|
|
125
|
+
"**/react-native-vision-camera-resizer/**",
|
|
126
|
+
"**/react-native-vision-camera-worklets",
|
|
127
|
+
"**/react-native-vision-camera-worklets/**",
|
|
128
|
+
"**/react-native-nitro-modules",
|
|
129
|
+
"**/react-native-nitro-modules/**"
|
|
130
|
+
]
|
|
131
|
+
},
|
|
112
132
|
"packageManager": "yarn@4.11.0",
|
|
113
133
|
"react-native-builder-bob": {
|
|
114
134
|
"source": "src",
|
|
@@ -121,8 +121,19 @@ interface NitroPoseExercises extends HybridObject<{
|
|
|
121
121
|
// Session control
|
|
122
122
|
readonly status: SessionStatus;
|
|
123
123
|
|
|
124
|
-
//
|
|
125
|
-
|
|
124
|
+
// iOS: zero-copy Vision path
|
|
125
|
+
processFrameIOS(frame: Frame): void;
|
|
126
|
+
|
|
127
|
+
// Android
|
|
128
|
+
processFrameAndroid(frame: Frame): void;
|
|
129
|
+
|
|
130
|
+
// // Android: pre-resized RGBA buffer from VisionCamera Resizer (Vulkan-accelerated)
|
|
131
|
+
// processFrameAndroid(
|
|
132
|
+
// buffer: ArrayBuffer,
|
|
133
|
+
// width: number,
|
|
134
|
+
// height: number,
|
|
135
|
+
// rotation: number
|
|
136
|
+
// ): void;
|
|
126
137
|
|
|
127
138
|
// Callbacks (set from JS side)
|
|
128
139
|
onRepComplete: ((data: RepData) => void) | undefined;
|
package/src/config/pushup.ts
CHANGED
|
@@ -9,57 +9,33 @@ import type { ExerciseConfig } from '../NitroPoseExercises.nitro';
|
|
|
9
9
|
export const PUSHUP_CONFIG: ExerciseConfig = {
|
|
10
10
|
name: 'Push-Up',
|
|
11
11
|
type: 'rep',
|
|
12
|
-
postureFamily: '
|
|
13
|
-
visibilityThreshold: 0.
|
|
12
|
+
postureFamily: 'none',
|
|
13
|
+
visibilityThreshold: 0.3,
|
|
14
14
|
cameraAngle: 'front',
|
|
15
15
|
angles: [
|
|
16
|
-
{
|
|
17
|
-
|
|
18
|
-
landmarkA: 11, // left shoulder
|
|
19
|
-
landmarkB: 13, // left elbow (vertex)
|
|
20
|
-
landmarkC: 15, // left wrist
|
|
21
|
-
},
|
|
22
|
-
{
|
|
23
|
-
name: 'rightElbow',
|
|
24
|
-
landmarkA: 12, // right shoulder
|
|
25
|
-
landmarkB: 14, // right elbow (vertex)
|
|
26
|
-
landmarkC: 16, // right wrist
|
|
27
|
-
},
|
|
28
|
-
{
|
|
29
|
-
name: 'leftHip',
|
|
30
|
-
landmarkA: 11, // left shoulder
|
|
31
|
-
landmarkB: 23, // left hip (vertex)
|
|
32
|
-
landmarkC: 27, // left ankle
|
|
33
|
-
},
|
|
34
|
-
{
|
|
35
|
-
name: 'rightHip',
|
|
36
|
-
landmarkA: 12, // right shoulder
|
|
37
|
-
landmarkB: 24, // right hip (vertex)
|
|
38
|
-
landmarkC: 28, // right ankle
|
|
39
|
-
},
|
|
16
|
+
{ name: 'leftElbow', landmarkA: 11, landmarkB: 13, landmarkC: 15 },
|
|
17
|
+
{ name: 'rightElbow', landmarkA: 12, landmarkB: 14, landmarkC: 16 },
|
|
40
18
|
],
|
|
41
19
|
phases: [
|
|
42
|
-
|
|
43
|
-
|
|
20
|
+
// Calibrated for 2D-projected angles in front-facing portrait filming.
|
|
21
|
+
// Real anatomical 180° appears as ~140-150° due to perspective foreshortening.
|
|
22
|
+
{ phase: 'up', angleName: 'leftElbow', minAngle: 130, maxAngle: 180 },
|
|
23
|
+
{ phase: 'down', angleName: 'leftElbow', minAngle: 40, maxAngle: 80 },
|
|
44
24
|
],
|
|
45
25
|
repSequence: ['up', 'down', 'up'],
|
|
46
26
|
formRules: [
|
|
27
|
+
// Trigger when descending but stalled above true-down threshold
|
|
47
28
|
{
|
|
48
|
-
name: '
|
|
49
|
-
message: '
|
|
50
|
-
severity: '
|
|
51
|
-
angleName: '
|
|
52
|
-
minAngle:
|
|
53
|
-
maxAngle:
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
message: "Lower your hips — you're piking up",
|
|
58
|
-
severity: 'warning',
|
|
59
|
-
angleName: 'leftHip',
|
|
60
|
-
minAngle: 160,
|
|
61
|
-
maxAngle: 180,
|
|
29
|
+
name: 'shallowRep',
|
|
30
|
+
message: 'Go lower',
|
|
31
|
+
severity: 'info',
|
|
32
|
+
angleName: 'leftElbow',
|
|
33
|
+
minAngle: 80,
|
|
34
|
+
maxAngle: 130, // "limbo zone" — too low to be up, too high to be down
|
|
35
|
+
// Note: ideally this only fires when angle has stalled, not on the way down.
|
|
36
|
+
// If your formRule engine doesn't support velocity/stall detection,
|
|
37
|
+
// accept that it'll fire briefly during transitions too.
|
|
62
38
|
},
|
|
63
39
|
],
|
|
64
|
-
holdDurationMs: 0,
|
|
40
|
+
holdDurationMs: 0,
|
|
65
41
|
};
|
|
@@ -1,37 +0,0 @@
|
|
|
1
|
-
#include <jni.h>
|
|
2
|
-
#include <android/hardware_buffer_jni.h>
|
|
3
|
-
|
|
4
|
-
extern "C"
|
|
5
|
-
JNIEXPORT jobject JNICALL
|
|
6
|
-
Java_com_margelo_nitro_nitroposeexercises_FrameHelper_hardwareBufferToBitmap(
|
|
7
|
-
JNIEnv *env,
|
|
8
|
-
jclass clazz,
|
|
9
|
-
jlong pointer
|
|
10
|
-
) {
|
|
11
|
-
AHardwareBuffer *buffer = reinterpret_cast<AHardwareBuffer *>(pointer);
|
|
12
|
-
if (!buffer) return nullptr;
|
|
13
|
-
|
|
14
|
-
// Convert AHardwareBuffer* to Java HardwareBuffer
|
|
15
|
-
jobject hardwareBuffer = AHardwareBuffer_toHardwareBuffer(env, buffer);
|
|
16
|
-
if (!hardwareBuffer) return nullptr;
|
|
17
|
-
|
|
18
|
-
// Call Bitmap.wrapHardwareBuffer(hardwareBuffer, null) to create a hardware-backed Bitmap
|
|
19
|
-
jclass bitmapClass = env->FindClass("android/graphics/Bitmap");
|
|
20
|
-
jmethodID wrapMethod = env->GetStaticMethodID(
|
|
21
|
-
bitmapClass,
|
|
22
|
-
"wrapHardwareBuffer",
|
|
23
|
-
"(Landroid/hardware/HardwareBuffer;Landroid/graphics/ColorSpace;)Landroid/graphics/Bitmap;"
|
|
24
|
-
);
|
|
25
|
-
jobject hwBitmap = env->CallStaticObjectMethod(bitmapClass, wrapMethod, hardwareBuffer, nullptr);
|
|
26
|
-
if (!hwBitmap) return nullptr;
|
|
27
|
-
|
|
28
|
-
// Copy to a software ARGB_8888 Bitmap (ML Kit needs software bitmap)
|
|
29
|
-
jclass configClass = env->FindClass("android/graphics/Bitmap$Config");
|
|
30
|
-
jfieldID argbField = env->GetStaticFieldID(configClass, "ARGB_8888", "Landroid/graphics/Bitmap$Config;");
|
|
31
|
-
jobject argbConfig = env->GetStaticObjectField(configClass, argbField);
|
|
32
|
-
|
|
33
|
-
jmethodID copyMethod = env->GetMethodID(bitmapClass, "copy", "(Landroid/graphics/Bitmap$Config;Z)Landroid/graphics/Bitmap;");
|
|
34
|
-
jobject softBitmap = env->CallObjectMethod(hwBitmap, copyMethod, argbConfig, JNI_FALSE);
|
|
35
|
-
|
|
36
|
-
return softBitmap;
|
|
37
|
-
}
|