--- license: apache-2.0 library_name: LiteRT pipeline_tag: keypoint-detection tags: [litert, tflite, on-device, android, gpu, hand-pose, keypoint-detection, rtmpose, mmpose] base_model: open-mmlab/mmpose --- # RTMPose-Hand — LiteRT (on-device 21-keypoint hand pose, fully-GPU) [RTMPose](https://github.com/open-mmlab/mmpose/tree/main/projects/rtmpose) (mmpose, CSPNeXt + RTMCC/SimCC head) **hand** pose, converted to **LiteRT** and running **fully on the `CompiledModel` GPU** (ML Drift) on Android. The **21 standard hand keypoints** (wrist + 4 joints × 5 fingers) for a single centered hand. ![RTMPose-Hand — input | hand skeleton (on-device LiteRT GPU)](samples/sample.png) ## On-device (Pixel 8a, Tensor G3 — verified) | | | |---|---| | nodes on GPU | **333 / 333** LITERT_CL (full residency) | | inference | **~4 ms** (256×256) | | size | 28 MB (fp16) | | accuracy | device-vs-PyTorch SimCC corr **0.999**, 21/21 keypoints | ``` image[1,3,256,256] (ImageNet 0-255) →[GPU: CSPNeXt + RTMCC]→ simcc_x[1,21,512], simcc_y[1,21,512] ``` ## Minimal usage **Android (Kotlin, CompiledModel GPU)** ```kotlin val model = CompiledModel.create(context.assets, "rtmhand_fp16.tflite", CompiledModel.Options(Accelerator.GPU), null) val inputs = model.createInputBuffers() val outputs = model.createOutputBuffers() inputs[0].writeFloat(chw) // [1,3,256,256] mmpose mean/std (0-255 RGB), NCHW model.run(inputs, outputs) val simccX = outputs[0].readFloat() // [1,21,512] val simccY = outputs[1].readFloat() // [1,21,512]; keypoint = argmax / 2 ``` **Python (desktop verification)** ```python MEAN = np.array([123.675, 116.28, 103.53], np.float32) STD = np.array([58.395, 57.12, 57.375], np.float32) import numpy as np from PIL import Image from ai_edge_litert.interpreter import Interpreter img = Image.open("hand.jpg").convert("RGB").resize((256, 256)) # centered subject crop x = ((np.asarray(img, np.float32) - MEAN) / STD).transpose(2, 0, 1)[None] it = Interpreter(model_path="rtmhand_fp16.tflite"); it.allocate_tensors() it.set_tensor(it.get_input_details()[0]["index"], x); it.invoke() od = it.get_output_details() # output 0 = simcc_x, 1 = simcc_y sx = it.get_tensor(od[0]["index"])[0] # simcc_x [21,512] sy = it.get_tensor(od[1]["index"])[0] # simcc_y [21,512] kx, ky = sx.argmax(-1) / 2.0, sy.argmax(-1) / 2.0 # 21 keypoints, px in 256x256 for i, (a, b) in enumerate(zip(kx, ky)): print(f"kp{i}: ({a:.1f}, {b:.1f})") ``` ## How it converts (litert-torch) Identical RTMPose-family recipe (both numerically exact, no PixelShuffle since there's no neck): 1. **`ScaleNorm` (RMS) → SafeRMSNorm** — fp16-overflow all-zero-head fix (scale x down by S=64 before squaring). 2. **GAU `act@act` BMM → broadcast-multiply + reduce-sum**. Result: banned ops NONE, all tensors ≤4D, tflite-vs-torch corr **1.0**, device-vs-torch corr **0.999**. ## Preprocessing Center-crop to square, resize to 256×256, ImageNet 0-255 normalize, NCHW. Top-down — one centered hand. SimCC argmax (÷ split=2) → pixel. ## License [Apache-2.0](https://github.com/open-mmlab/mmpose/blob/main/LICENSE). Upstream: [open-mmlab/mmpose](https://github.com/open-mmlab/mmpose) RTMPose-Hand.