Skip to content

Commit 1dbccc6

Browse files
Support custom pose keypoint counts on Android (#556)
Co-authored-by: Glenn Jocher <glenn.jocher@ultralytics.com>
1 parent 6d24a4b commit 1dbccc6

5 files changed

Lines changed: 58 additions & 28 deletions

File tree

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,9 @@
1+
## 0.6.9
2+
3+
- **Fix**: Android pose inference now derives the keypoint count from the model output feature width, allowing custom
4+
pose models such as 21-keypoint hand landmarks to return all keypoints instead of requiring COCO's 17-keypoint shape
5+
(#555).
6+
17
## 0.6.8
28

39
- **Performance**: Speed up Android single-image preprocessing for NCHW LiteRT/QNN models by writing planar CHW input

android/src/main/kotlin/com/ultralytics/yolo/PoseEstimator.kt

Lines changed: 49 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -24,22 +24,24 @@ class PoseEstimator(
2424
}
2525

2626
companion object {
27-
// xywh(4) + conf(1) + keypoints(17*3=51) = 56
28-
private const val OUTPUT_FEATURES = 56
29-
private const val KEYPOINTS_COUNT = 17
27+
private const val BOX_CONF_FEATURES = 5 // Normal pose prefix: box x/y/w/h plus confidence.
28+
private const val KEYPOINT_FEATURES = 3 // One keypoint triplet: x, y, and confidence.
29+
private const val END_TO_END_MAX_ROWS = 300 // Ultralytics default max_det for capped end-to-end outputs.
3030
}
3131

3232
// Reusable float input for the CompiledModel input buffer.
3333
private lateinit var floatInput: FloatArray
3434
private lateinit var inputBitmap: Bitmap
3535
private lateinit var intValues: IntArray
36-
36+
3737
// Reuse output arrays to reduce allocations
3838
private var outDim2 = 0
39-
39+
4040
// Output dimensions
4141
private var batchSize = 0
4242
private var numAnchors = 0
43+
private var keypointStart = BOX_CONF_FEATURES
44+
private var keypointCount = 0
4345
private var isEndToEnd = false
4446
private lateinit var outputLayout: OutputLayout
4547

@@ -61,13 +63,22 @@ class PoseEstimator(
6163
modelInputSize = Pair(inWidth, inHeight)
6264

6365
val outputShape = rtModel.outputDims.getOrNull(0) ?: IntArray(0)
64-
batchSize = if (outputShape.isNotEmpty()) outputShape[0] else 1
65-
isEndToEnd = outputShape[2] < outputShape[1] && outputShape[2] >= 6
66+
require(outputShape.size >= 3) {
67+
"Unexpected pose output shape. Expected [batch, features, anchors] or [batch, anchors, features], Actual=${outputShape.contentToString()}"
68+
}
69+
70+
batchSize = outputShape[0]
71+
val featuresFirstPose = isPoseFeatureCount(outputShape[1]) && outputShape[2] > END_TO_END_MAX_ROWS
72+
val anchorsFirstPose = isPoseFeatureCount(outputShape[2]) && outputShape[1] > END_TO_END_MAX_ROWS
73+
isEndToEnd = !featuresFirstPose &&
74+
!anchorsFirstPose &&
75+
outputShape[1] <= END_TO_END_MAX_ROWS &&
76+
isEndToEndFeatureCount(outputShape[2])
6677
outputLayout = when {
67-
outputShape[1] == OUTPUT_FEATURES -> OutputLayout.FEATURES_FIRST
68-
outputShape[2] == OUTPUT_FEATURES || isEndToEnd -> OutputLayout.ANCHORS_FIRST
78+
featuresFirstPose -> OutputLayout.FEATURES_FIRST
79+
isEndToEnd || anchorsFirstPose -> OutputLayout.ANCHORS_FIRST
6980
else -> throw IllegalArgumentException(
70-
"Unexpected output feature size. Expected $OUTPUT_FEATURES or end-to-end rows, Actual=${outputShape.contentToString()}"
81+
"Unexpected pose output feature size. Expected 5 + keypoints * 3 features or end-to-end rows, Actual=${outputShape.contentToString()}"
7182
)
7283
}
7384

@@ -83,6 +94,8 @@ class PoseEstimator(
8394
}
8495
}
8596

97+
keypointStart = if (isEndToEnd && (outFeatures - 6) % 3 == 0) 6 else BOX_CONF_FEATURES
98+
keypointCount = keypointCountFromFeatureCount(outFeatures, keypointStart)
8699
outDim2 = outputShape[2]
87100

88101
floatInput = FloatArray(inWidth * inHeight * 3)
@@ -122,7 +135,7 @@ class PoseEstimator(
122135

123136
// Apply numItemsThreshold limit
124137
val limitedDetections = rawDetections.take(numItemsThreshold)
125-
138+
126139
val boxes = limitedDetections.map { it.box }
127140
val keypointsList = limitedDetections.map { it.keypoints }
128141

@@ -181,10 +194,10 @@ class PoseEstimator(
181194

182195
val kpArray = mutableListOf<Pair<Float, Float>>()
183196
val kpConfArray = mutableListOf<Float>()
184-
for (k in 0 until KEYPOINTS_COUNT) {
185-
val rawKx = featureValue(flat, 5 + k * 3, j)
186-
val rawKy = featureValue(flat, 5 + k * 3 + 1, j)
187-
val kpC = featureValue(flat, 5 + k * 3 + 2, j)
197+
for (k in 0 until keypointCount) {
198+
val rawKx = featureValue(flat, BOX_CONF_FEATURES + k * 3, j)
199+
val rawKy = featureValue(flat, BOX_CONF_FEATURES + k * 3 + 1, j)
200+
val kpC = featureValue(flat, BOX_CONF_FEATURES + k * 3 + 2, j)
188201

189202
val (finalKx, finalKy) = inputPointFromOutputPoint(
190203
rawKx,
@@ -202,13 +215,13 @@ class PoseEstimator(
202215
(fx / origWidth) to (fy / origHeight)
203216
}
204217
val boxObj = Box(0, labelName(0), conf, rectF, normBox)
205-
218+
206219
val keypoints = Keypoints(
207220
xyn = xynList,
208221
xy = kpArray,
209222
conf = kpConfArray
210223
)
211-
224+
212225
detections.add(
213226
PoseDetection(
214227
box = boxObj,
@@ -228,9 +241,6 @@ class PoseEstimator(
228241
origHeight: Int
229242
): List<PoseDetection> {
230243
val detections = mutableListOf<PoseDetection>()
231-
val fieldCount = outDim2
232-
val keypointStart = if ((fieldCount - 6) % 3 == 0) 6 else 5
233-
val keypointCount = (fieldCount - keypointStart) / 3
234244

235245
for (j in 0 until numAnchors) {
236246
val conf = featureValue(flat, 4, j)
@@ -295,18 +305,34 @@ class PoseEstimator(
295305
}
296306
}
297307

308+
private fun isPoseFeatureCount(featureCount: Int): Boolean {
309+
return featureCount >= BOX_CONF_FEATURES + KEYPOINT_FEATURES &&
310+
(featureCount - BOX_CONF_FEATURES) % KEYPOINT_FEATURES == 0
311+
}
312+
313+
private fun isEndToEndFeatureCount(featureCount: Int): Boolean {
314+
return isPoseFeatureCount(featureCount) ||
315+
(featureCount >= 6 + KEYPOINT_FEATURES && (featureCount - 6) % KEYPOINT_FEATURES == 0)
316+
}
317+
318+
private fun keypointCountFromFeatureCount(featureCount: Int, keypointStart: Int = BOX_CONF_FEATURES): Int {
319+
require(featureCount >= keypointStart && (featureCount - keypointStart) % KEYPOINT_FEATURES == 0) {
320+
"Unexpected pose output feature size. Expected keypoint triplets after $keypointStart box fields, Actual=$featureCount"
321+
}
322+
return (featureCount - keypointStart) / KEYPOINT_FEATURES
323+
}
298324

299325
private fun nmsPoseDetections(
300326
detections: List<PoseDetection>,
301327
iouThreshold: Float
302328
): List<PoseDetection> {
303329
val confidenceThreshold = 0.25f // Hardcoded second-pass threshold
304330
val filteredDetections = detections.filter { it.box.conf >= confidenceThreshold }
305-
331+
306332
if (filteredDetections.size <= 1) {
307333
return filteredDetections
308334
}
309-
335+
310336
val sorted = filteredDetections.sortedByDescending { it.box.conf }
311337
val picked = mutableListOf<PoseDetection>()
312338
val used = BooleanArray(sorted.size)
@@ -340,7 +366,6 @@ class PoseEstimator(
340366
return if (unionArea <= 0f) 0f else (interArea / unionArea)
341367
}
342368

343-
344369
override fun setConfidenceThreshold(conf: Double) {
345370
confidenceThreshold = conf.toFloat()
346371
super.setConfidenceThreshold(conf)
@@ -350,7 +375,7 @@ class PoseEstimator(
350375
iouThreshold = iou.toFloat()
351376
super.setIouThreshold(iou)
352377
}
353-
378+
354379
override fun setNumItemsThreshold(n: Int) {
355380
numItemsThreshold = n
356381
super.setNumItemsThreshold(n)
@@ -368,5 +393,4 @@ class PoseEstimator(
368393
val box: Box,
369394
val keypoints: Keypoints
370395
)
371-
372396
}

example/pubspec.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
name: ultralytics_yolo_example
44
description: "Demonstrates how to use the ultralytics_yolo plugin."
55
publish_to: "none"
6-
version: 0.6.8+16
6+
version: 0.6.9+17
77

88
environment:
99
sdk: ^3.8.1

ios/ultralytics_yolo.podspec

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
#
55
Pod::Spec.new do |s|
66
s.name = 'ultralytics_yolo'
7-
s.version = '0.6.8'
7+
s.version = '0.6.9'
88
s.summary = 'Flutter plugin for YOLO (You Only Look Once) models'
99
s.description = <<-DESC
1010
Flutter plugin for YOLO (You Only Look Once) models, supporting object detection, segmentation, classification, pose estimation and oriented bounding boxes (OBB) on both Android and iOS.

pubspec.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
name: ultralytics_yolo
44
description: Flutter plugin for Ultralytics YOLO computer vision models.
5-
version: 0.6.8
5+
version: 0.6.9
66
homepage: https://github.com/ultralytics/yolo-flutter-app
77
repository: https://github.com/ultralytics/yolo-flutter-app
88
issue_tracker: https://github.com/ultralytics/yolo-flutter-app/issues

0 commit comments

Comments
 (0)