更新记录
1.0.3(2026-03-09)
动作检测直接基于关键点几何特征(角度、距离),删除复杂的置信度评分
1.0.2(2026-03-09)
优化运动识别
1.0.1(2026-03-08)
根据追踪点优化识别准确
查看更多
平台兼容性
uni-app(4.81)
| Vue2 |
Vue3 |
Chrome |
Safari |
app-vue |
app-nvue |
Android |
iOS |
鸿蒙 |
| √ |
√ |
- |
- |
× |
√ |
6.0 |
× |
- |
| 微信小程序 |
支付宝小程序 |
抖音小程序 |
百度小程序 |
快手小程序 |
京东小程序 |
鸿蒙元服务 |
QQ小程序 |
飞书小程序 |
小红书小程序 |
快应用-华为 |
快应用-联盟 |
| - |
- |
- |
- |
- |
- |
- |
- |
- |
- |
- |
- |
rd-ai-vision AI视觉识别插件
高性能 AI 视觉识别 UTS 插件(仅支持 Android 端),集成 MediaPipe AI 能力,支持手势识别、姿态检测、运动计数、面部检测等功能。
技术栈
| 技术 |
说明 |
| MediaPipe |
Google 开源机器学习框架,用于人体姿态、手势、面部检测 |
| CameraX |
Android Jetpack 相机库,提供高性能相机预览和图像分析 |
| Kotlin |
原生 Android 开发语言 |
| UTS |
uni-app 跨平台原生开发语言 |
MediaPipe 模型
| 模型 |
功能 |
关键点数量 |
| Pose Landmarker |
人体姿态检测 |
33 个关键点 |
| Hand Landmarker |
手势识别 |
每只手 21 个关键点 |
| Face Mesh |
面部网格检测 |
478 个关键点 |
功能特性
基础功能
- 自定义相机预览(支持多种缩放模式)
- 前后摄像头切换
- 闪光灯控制
- 关键帧截图
AI 识别功能
| 功能 |
说明 |
| 手势识别 |
支持识别 OK、指向、剪刀手、摇滚、竖大拇指、张开手掌、握拳等手势 |
| 姿态检测 |
实时检测 33 个人体关键点,提供 3D 坐标和世界坐标 |
| 运动计数 |
自动识别深蹲、俯卧撑、开合跳、弓步、高抬腿、踢腿、卷腹、臀桥、平板支撑、举手、侧平举等动作并计数 |
| 面部检测 |
检测 478 个面部关键点,可用于面部表情分析 |
安装
安装插件
将 uni_modules/rd-ai-vision 目录复制到项目的 uni_modules 目录下。
权限要求
Android
| 权限 |
说明 |
CAMERA |
相机权限 |
RECORD_AUDIO |
麦克风权限(视频录制) |
READ_EXTERNAL_STORAGE |
读取存储权限 |
WRITE_EXTERNAL_STORAGE |
写入存储权限 |
组件使用示例
基础用法
<template>
<view class="container">
<!-- AI 视觉组件 -->
<rd-ai-vision
ref="aiVision"
class="camera-preview"
cameraPosition="front"
:scaleType="3"
@onCameraReady="onCameraReady"
@onError="onError"
@onGestureDetected="onGestureDetected"
@onPoseDetected="onPoseDetected"
@onMotionRecognized="onMotionRecognized"
@onFaceDetected="onFaceDetected"
/>
</view>
</template>
<script>
export default {
data() {
return {
isReady: false
}
},
methods: {
onCameraReady(e) {
console.log('相机已就绪')
this.isReady = true
// 启用各种检测功能
this.$refs.aiVision.enablePoseDetection() // 姿态检测
this.$refs.aiVision.enableGestureDetection() // 手势检测
this.$refs.aiVision.enableFaceDetection() // 面部检测
},
onError(e) {
console.error('错误:', e.detail.message)
},
onGestureDetected(e) {
const gestures = e.detail.gestures
if (gestures.length > 0) {
console.log('检测到手势:', gestures[0].gesture, '置信度:', gestures[0].confidence)
}
},
onPoseDetected(e) {
const { landmarks, worldLandmarks } = e.detail
console.log('检测到姿态:', landmarks.length, '个关键点')
},
onMotionRecognized(e) {
const { action, confidence, details } = e.detail
console.log('运动识别:', action, '置信度:', confidence)
console.log('计数:', details)
},
onFaceDetected(e) {
const { landmarks, score } = e.detail
console.log('检测到面部:', landmarks.length, '个关键点')
}
},
onUnload() {
// 释放资源
this.$refs.aiVision?.prepareForUnload()
}
}
</script>
<style>
.container {
flex: 1;
background-color: #000;
}
.camera-preview {
width: 100%;
height: 100%;
}
</style>
完整示例(健身计数应用)
<template>
<view class="container">
<!-- 相机组件 -->
<rd-ai-vision
ref="aiVision"
class="camera-preview"
cameraPosition="front"
:scaleType="3"
@onCameraReady="onCameraReady"
@onMotionRecognized="onMotionRecognized"
@onGestureDetected="onGestureDetected"
/>
<!-- 运动信息显示 -->
<view class="motion-overlay">
<view class="action-info">
<text class="action-text">{{ currentAction }}</text>
<text class="confidence-text" v-if="confidence > 0">
置信度: {{ (confidence * 100).toFixed(0) }}%
</text>
</view>
<view class="counts-grid">
<view class="count-item">
<text class="count-number">{{ motionCounts.squat || 0 }}</text>
<text class="count-label">深蹲</text>
</view>
<view class="count-item">
<text class="count-number">{{ motionCounts.pushUp || 0 }}</text>
<text class="count-label">俯卧撑</text>
</view>
<view class="count-item">
<text class="count-number">{{ motionCounts.jumpingJack || 0 }}</text>
<text class="count-label">开合跳</text>
</view>
<view class="count-item">
<text class="count-number">{{ motionCounts.lunge || 0 }}</text>
<text class="count-label">弓步</text>
</view>
</view>
<!-- 手势显示 -->
<view class="gesture-info" v-if="currentGesture">
<text class="gesture-text">手势: {{ currentGesture }}</text>
</view>
</view>
<!-- 控制按钮 -->
<view class="controls">
<button @click="switchCamera">切换摄像头</button>
<button @click="toggleFlash">闪光灯</button>
<button @click="resetCounts">重置计数</button>
<button @click="captureFrame">截图</button>
</view>
</view>
</template>
<script>
export default {
data() {
return {
isReady: false,
currentAction: '准备中...',
confidence: 0,
currentGesture: '',
motionCounts: {
squat: 0,
pushUp: 0,
jumpingJack: 0,
lunge: 0,
armRaise: 0,
highKnee: 0,
sideRaise: 0,
crunch: 0,
gluteBridge: 0,
kick: 0
}
}
},
methods: {
onCameraReady(e) {
console.log('相机就绪')
this.isReady = true
this.currentAction = '站立'
// 启用检测
this.$refs.aiVision.enablePoseDetection()
this.$refs.aiVision.enableGestureDetection()
},
onMotionRecognized(e) {
const { action, confidence, details } = e.detail
if (action && action !== '无数据') {
this.currentAction = action
this.confidence = confidence
// 更新计数
if (details) {
this.motionCounts = {
squat: details.squatCount || this.motionCounts.squat,
pushUp: details.pushUpCount || this.motionCounts.pushUp,
jumpingJack: details.jumpingJackCount || this.motionCounts.jumpingJack,
lunge: details.lungeCount || this.motionCounts.lunge,
armRaise: details.armRaiseCount || this.motionCounts.armRaise,
highKnee: details.highKneeCount || this.motionCounts.highKnee,
sideRaise: details.sideRaiseCount || this.motionCounts.sideRaise,
crunch: details.crunchCount || this.motionCounts.crunch,
gluteBridge: details.gluteBridgeCount || this.motionCounts.gluteBridge,
kick: details.kickCount || this.motionCounts.kick
}
}
}
},
onGestureDetected(e) {
const gestures = e.detail.gestures
if (gestures && gestures.length > 0) {
this.currentGesture = gestures[0].gesture
} else {
this.currentGesture = ''
}
},
switchCamera() {
this.$refs.aiVision.switchCamera()
},
toggleFlash() {
const result = this.$refs.aiVision.toggleFlash()
uni.showToast({ title: result ? '闪光灯已开启' : '闪光灯已关闭', icon: 'none' })
},
resetCounts() {
this.$refs.aiVision.resetMotionCounters()
this.motionCounts = {
squat: 0, pushUp: 0, jumpingJack: 0, lunge: 0,
armRaise: 0, highKnee: 0, sideRaise: 0,
crunch: 0, gluteBridge: 0, kick: 0
}
uni.showToast({ title: '计数已重置', icon: 'success' })
},
captureFrame() {
this.$refs.aiVision.requestKeyFrame()
}
},
onUnload() {
this.$refs.aiVision?.prepareForUnload()
}
}
</script>
<style>
.container {
flex: 1;
background-color: #000;
}
.camera-preview {
width: 100%;
height: 100%;
position: absolute;
}
.motion-overlay {
position: absolute;
top: 40rpx;
left: 20rpx;
right: 20rpx;
}
.action-info {
background-color: rgba(0, 0, 0, 0.6);
padding: 20rpx;
border-radius: 16rpx;
margin-bottom: 20rpx;
}
.action-text {
color: #fff;
font-size: 48rpx;
font-weight: bold;
}
.confidence-text {
color: #4CAF50;
font-size: 28rpx;
margin-left: 20rpx;
}
.counts-grid {
display: flex;
flex-wrap: wrap;
background-color: rgba(0, 0, 0, 0.6);
padding: 20rpx;
border-radius: 16rpx;
}
.count-item {
width: 25%;
text-align: center;
padding: 10rpx;
}
.count-number {
color: #4CAF50;
font-size: 48rpx;
font-weight: bold;
display: block;
}
.count-label {
color: #aaa;
font-size: 24rpx;
}
.gesture-info {
background-color: rgba(76, 175, 80, 0.8);
padding: 16rpx 32rpx;
border-radius: 30rpx;
margin-top: 20rpx;
align-self: flex-start;
}
.gesture-text {
color: #fff;
font-size: 32rpx;
}
.controls {
position: absolute;
bottom: 40rpx;
left: 20rpx;
right: 20rpx;
display: flex;
justify-content: space-around;
}
.controls button {
background-color: rgba(255, 255, 255, 0.2);
color: #fff;
font-size: 28rpx;
padding: 20rpx 30rpx;
border-radius: 40rpx;
}
</style>
API 文档
Props 属性
| 属性 |
类型 |
默认值 |
说明 |
cameraPosition |
String |
'back' |
摄像头位置:'front' 前置 / 'back' 后置 |
scaleType |
Number |
3 |
预览缩放模式 |
| scaleType 取值: |
值 |
模式 |
说明 |
| 0 |
FIT_CENTER |
保持比例,居中显示,可能有黑边 |
| 1 |
FIT_START |
保持比例,靠上显示 |
| 2 |
FIT_END |
保持比例,靠下显示 |
| 3 |
FILL_CENTER |
填充视图,居中裁剪,无黑边 |
| 4 |
FILL_START |
填充视图,靠上裁剪 |
| 5 |
FILL_END |
填充视图,靠下裁剪 |
Events 事件
| 事件名 |
说明 |
回调参数 |
onCameraReady |
相机就绪 |
{ detail: { message } } |
onError |
错误回调 |
{ detail: { message } } |
onPoseDetected |
姿态检测 |
{ detail: { timestamp, landmarks, worldLandmarks } } |
onMotionRecognized |
运动识别 |
{ detail: { action, confidence, details } } |
onGestureDetected |
手势检测 |
{ detail: { gestures } } |
onFaceDetected |
面部检测 |
{ detail: { timestamp, landmarks, score } } |
onKeyFrameCaptured |
截图完成 |
{ detail: { imagePath } } |
Methods 方法
| 方法名 |
参数 |
说明 |
startCamera() |
- |
启动相机 |
stopCamera() |
- |
停止相机 |
switchCamera() |
- |
切换摄像头 |
toggleFlash() |
- |
切换闪光灯,返回当前状态 |
setTorchMode(mode) |
"on" / "off" |
设置闪光灯模式 |
changeScaleType(type) |
0-5 |
动态更改缩放模式 |
enablePoseDetection() |
- |
启用姿态检测 |
disablePoseDetection() |
- |
禁用姿态检测 |
enableGestureDetection() |
- |
启用手势检测 |
disableGestureDetection() |
- |
禁用手势检测 |
enableFaceDetection() |
- |
启用面部检测 |
disableFaceDetection() |
- |
禁用面部检测 |
resetMotionCounters() |
- |
重置运动计数器 |
getMotionCounts() |
- |
获取运动计数 Map |
setShowLandmarks(show) |
boolean |
设置是否显示关键点 |
captureFrame() |
- |
立即截图,返回路径 |
requestKeyFrame() |
- |
请求截图(异步回调) |
prepareForUnload() |
- |
准备卸载,释放资源 |
回调数据说明
onPoseDetected
{
timestamp: number, // 时间戳
landmarks: [{ // 33 个关键点(归一化坐标)
x: number, y: number, z: number,
visibility: number, presence: number
}],
worldLandmarks: [{ // 3D 世界坐标
x: number, y: number, z: number
}]
}
onMotionRecognized
{
action: string, // 动作名称
confidence: number, // 置信度 0-1
details: { // 计数详情
squatCount: number,
pushUpCount: number,
jumpingJackCount: number,
// ... 更多动作计数
}
}
onGestureDetected
{
gestures: [{
gesture: string, // 手势名称
confidence: number, // 置信度
isLeftHand: boolean, // 是否左手
score: number // 手部检测分数
}]
}
支持的手势
| 手势 |
名称 |
| OK |
OK手势 |
| POINTING |
指向 |
| PEACE |
剪刀手 |
| ROCK |
摇滚 |
| THUMBS_UP |
竖大拇指 |
| OPEN_PALM |
张开手掌 |
| FIST |
握拳 |
支持的运动
| 运动 |
名称 |
计数键 |
| 深蹲 |
STR_SQUAT |
squatCount |
| 俯卧撑 |
STR_PUSHUP |
pushUpCount |
| 开合跳 |
STR_JUMPINGJACK |
jumpingJackCount |
| 弓步 |
STR_LUNGE |
lungeCount |
| 举手 |
STR_ARMRAISE |
armRaiseCount |
| 高抬腿 |
STR_HIGHKNEE |
highKneeCount |
| 侧平举 |
STR_SIDERAISE |
sideRaiseCount |
| 卷腹 |
STR_CRUNCH |
crunchCount |
| 臀桥 |
STR_GLUTEBRIDGE |
gluteBridgeCount |
| 踢腿 |
STR_KICK |
kickCount |
| 平板支撑 |
STR_PLANK |
- |
注意事项
- 本插件仅支持 Android 端
- 最低支持 Android 6.0 (API 23)
- 建议使用 HBuilderX Alpha 版本进行开发
- 页面卸载时务必调用
prepareForUnload() 释放资源