# 原理
# 凸包算法
这里使用食指的闭合来做
实际测试中发现凸包算法不太行,当 8 和 6 在附近的时候,在检测的帧率较高且出错率较高的时候,对 食指伸出
这个检测结果暴增
比如当拳头向着摄像头的时候
# 距离算法
https://blog.csdn.net/qq_43550173/article/details/116273477
6 号点和 8 号点到 0 号点的距离来判断食指是否伸出还是闭合
# 凸包算法代码
调整了界面显示,加入控制摄像头打开和关闭的开关
<template> | |
<div class="container"> | |
<el-card shadow="hover"> | |
<el-row> | |
<el-col :span="24"> | |
<div class="facedetect"> | |
<!-- 注意下面的两个标签都设置镜像翻转 --> | |
<video ref="videoElement" style="transform:rotateY(180deg)"></video> | |
<canvas class="output_canvas" ref="canvasElement" :width="canvasWidth" :height="canvasHeight" | |
style="transform:rotateY(180deg)"></canvas> | |
</div> | |
</el-col> | |
</el-row> | |
<el-row justify="space-around"> | |
<el-col :span="12" style="text-align:center;"> | |
<el-button type="success" size="large" @click="startDetect">开始识别</el-button> | |
</el-col> | |
<el-col :span="12" style="text-align:center;"> | |
<el-button type="primary" size="large" @click="stopDetect">停止识别</el-button> | |
</el-col> | |
<el-col style="text-align:center;"> | |
<el-button type="info" size="large" @click="savePic">保存图片</el-button> | |
</el-col> | |
</el-row> | |
</el-card> | |
</div> | |
</template> | |
<script setup lang="ts" > | |
// 上面的 video 显示是摄像头的,canvas 是处理后的 | |
import { Hands, HAND_CONNECTIONS, InputImage } from "@mediapipe/hands"; | |
import { Camera } from "@mediapipe/camera_utils"; | |
import { drawConnectors, drawLandmarks } from "@mediapipe/drawing_utils"; | |
import { onMounted, ref, watch } from 'vue' | |
import { tr } from "element-plus/es/locale"; | |
import axios from "axios"; | |
import { useUsersStore } from '@/store/user'; | |
import { useRouter } from "vue-router"; | |
// 全局信息存储 | |
const store = useUsersStore() | |
// 监听当前路由变化,关闭摄像头 | |
const router = useRouter() | |
const videoElement = ref<HTMLVideoElement | null>(null) | |
const canvasElement = ref<HTMLCanvasElement | null>(null) | |
let camera: Camera | |
let canvasWidth = ref(500) | |
let canvasHeight = ref(500) | |
let preDetectTwo: boolean = false | |
// 检测是否在凸包点内的函数 | |
function isInPolygon(checkPoint: number[], polygonPoints: number[][]): boolean { | |
let counter = 0; | |
let i: number; | |
let xinters: number; | |
let p1: number[], p2: number[]; | |
let pointCount = polygonPoints.length; | |
p1 = polygonPoints[0]; | |
for (i = 1; i <= pointCount; i++) { | |
p2 = polygonPoints[i % pointCount]; | |
if ( | |
checkPoint[0] > Math.min(p1[0], p2[0]) && | |
checkPoint[0] <= Math.max(p1[0], p2[0]) | |
) { | |
if (checkPoint[1] <= Math.max(p1[1], p2[1])) { | |
if (p1[0] != p2[0]) { | |
xinters = | |
(checkPoint[0] - p1[0]) * | |
(p2[1] - p1[1]) / | |
(p2[0] - p1[0]) + | |
p1[1]; | |
if (p1[1] == p2[1] || checkPoint[1] <= xinters) { | |
counter++; | |
} | |
} | |
} | |
} | |
p1 = p2; | |
} | |
if (counter % 2 == 0) { | |
return false; | |
} else { | |
return true; | |
} | |
} | |
onMounted(() => { | |
const canvasCtx = canvasElement.value?.getContext("2d"); | |
function onResults(results: any) { | |
canvasCtx?.save(); | |
canvasCtx?.clearRect(0, 0, canvasElement.value?.width as number, canvasElement.value?.height as number); | |
canvasCtx?.drawImage( | |
results?.image, 0, 0, canvasElement.value?.width as number, canvasElement.value?.height as number); | |
if (results?.multiHandLandmarks) { | |
// 这里的代码是同时绘制多只手的,到时候只绘制一只手吧 | |
for (const landmarks of results?.multiHandLandmarks) { | |
if (landmarks == undefined || canvasCtx == undefined) { | |
continue | |
} | |
//console.log (landmarks); // 数组,装着对象 {x, y, z} | |
// console.log(results.image); | |
// 下面添加检测手势的代码 | |
// 采集所有关键点的坐标 | |
let width = 1280, height = 720 | |
let list_lms: number[][] = [] | |
for (let i = 0; i < landmarks.length; i++) { | |
list_lms.push([landmarks[i]['x'] * canvasWidth.value, landmarks[i]['y'] * canvasHeight.value]) | |
} | |
//console.log ("所有点为"); | |
// console.log(list_lms); | |
// 凸包点 | |
let hull_list: number[][] = [] | |
let hull_index: number[] = [0, 1, 2, 3, 6, 10, 14, 19, 18, 17, 10] | |
for (let i = 0; i < hull_index.length; i++) { | |
hull_list.push(list_lms[hull_index[i]]) | |
} | |
// console.log("=======================\n"); | |
//console.log ("凸包点为"); | |
// console.log(hull_list); | |
// 检测食指指尖是否在凸包内 | |
let res: boolean = isInPolygon(list_lms[8], hull_list) | |
if (res == false && preDetectTwo == false) { | |
// 如果食指伸出, | |
preDetectTwo = true | |
console.log("食指伸出"); | |
} else if (res == false && preDetectTwo) { | |
// 食指继续伸出,不响应 | |
} else if (res == true) { | |
// 没有检测到,置为 false,无论上次是否检测到 | |
preDetectTwo = false | |
} | |
// console.log(res); | |
// if (res) { | |
// console.log ("在"); | |
// } else { | |
// console.log ("不在"); | |
// } | |
// 上面是检测手势的代码 | |
drawConnectors(canvasCtx as CanvasRenderingContext2D, landmarks, HAND_CONNECTIONS, | |
{ color: '#00FF00', lineWidth: 5 }); | |
drawLandmarks(canvasCtx as CanvasRenderingContext2D, landmarks, { color: '#FF0000', lineWidth: 2 }); | |
} | |
} | |
canvasCtx?.restore(); | |
} | |
// 相机对象 | |
camera = new Camera(videoElement.value as HTMLVideoElement, { | |
onFrame: async () => { | |
await hands.send({ image: videoElement.value as InputImage }); | |
}, | |
width: canvasWidth.value, | |
height: canvasHeight.value | |
}); | |
const hands = new Hands({ | |
locateFile: (file) => { | |
console.log("进入file"); | |
return `https://cdn.jsdelivr.net/npm/@mediapipe/hands/${file}`; | |
} | |
}); | |
hands.setOptions({ | |
maxNumHands: 2, | |
modelComplexity: 1, | |
minDetectionConfidence: 0.5, | |
minTrackingConfidence: 0.5 | |
}); | |
hands.onResults(onResults); | |
// camera.start() | |
}) | |
let isOn: boolean = false | |
const startDetect = () => { | |
if (isOn) { | |
return | |
} | |
isOn = true | |
camera.start() | |
} | |
const stopDetect = () => { | |
if (isOn == false) { | |
return | |
} | |
isOn = false | |
camera.stop() | |
} | |
watch(router.currentRoute, () => { | |
if (isOn) { | |
// 上面括号之前写的 camera | |
camera.stop() | |
} | |
}) | |
const savePic = () => { | |
// 要先判断 vedio 是否处于启动状态 | |
if (isOn) { | |
let canvas = document.createElement('canvas') | |
canvas.width = 500; | |
canvas.height = 500; | |
let ctx = canvas.getContext('2d'); | |
ctx?.drawImage(videoElement?.value as CanvasImageSource, 0, 0, canvas.width, canvas.height); | |
// 生成图片 base64 编码 | |
let base64 = canvas.toDataURL('image/png') | |
// console.log(base64); | |
} else { | |
// console.log(""); | |
alert("请先打开摄像头") | |
} | |
} | |
</script> | |
<style scoped> | |
.container { | |
display: flex; | |
justify-content: center; | |
} | |
.facedetect { | |
height: 500px; | |
width: 1100px; | |
display: flex; | |
justify-content: space-around; | |
} | |
</style> |
# 距离算法代码
<template> | |
<div class="container"> | |
<el-card shadow="hover"> | |
<el-row> | |
<el-col :span="24"> | |
<div class="facedetect"> | |
<!-- 注意下面的两个标签都设置镜像翻转 --> | |
<video ref="videoElement" style="transform:rotateY(180deg)"></video> | |
<canvas class="output_canvas" ref="canvasElement" :width="canvasWidth" :height="canvasHeight" | |
style="transform:rotateY(180deg)"></canvas> | |
</div> | |
</el-col> | |
</el-row> | |
<el-row justify="space-around"> | |
<el-col :span="12" style="text-align:center;"> | |
<el-button type="success" size="large" @click="startDetect">开始识别</el-button> | |
</el-col> | |
<el-col :span="12" style="text-align:center;"> | |
<el-button type="primary" size="large" @click="stopDetect">停止识别</el-button> | |
</el-col> | |
<el-col style="text-align:center;"> | |
<el-button type="info" size="large" @click="savePic">保存图片</el-button> | |
</el-col> | |
</el-row> | |
</el-card> | |
</div> | |
</template> | |
<script setup lang="ts" > | |
// 上面的 video 显示是摄像头的,canvas 是处理后的 | |
import { Hands, HAND_CONNECTIONS, InputImage } from "@mediapipe/hands"; | |
import { Camera } from "@mediapipe/camera_utils"; | |
import { drawConnectors, drawLandmarks } from "@mediapipe/drawing_utils"; | |
import { onMounted, ref, watch } from 'vue' | |
import { tr } from "element-plus/es/locale"; | |
import axios from "axios"; | |
import { useUsersStore } from '@/store/user'; | |
import { useRouter } from "vue-router"; | |
// 全局信息存储 | |
const store = useUsersStore() | |
// 监听当前路由变化,关闭摄像头 | |
const router = useRouter() | |
const videoElement = ref<HTMLVideoElement | null>(null) | |
const canvasElement = ref<HTMLCanvasElement | null>(null) | |
let camera: Camera | |
let canvasWidth = ref(500) | |
let canvasHeight = ref(500) | |
let preDetectTwo: boolean = false | |
let pre_one_finger: boolean = false | |
let pre_two_finger: boolean = false | |
// 检测是否在凸包点内的函数 | |
function isInPolygon(checkPoint: number[], polygonPoints: number[][]): boolean { | |
let counter = 0; | |
let i: number; | |
let xinters: number; | |
let p1: number[], p2: number[]; | |
let pointCount = polygonPoints.length; | |
p1 = polygonPoints[0]; | |
for (i = 1; i <= pointCount; i++) { | |
p2 = polygonPoints[i % pointCount]; | |
if ( | |
checkPoint[0] > Math.min(p1[0], p2[0]) && | |
checkPoint[0] <= Math.max(p1[0], p2[0]) | |
) { | |
if (checkPoint[1] <= Math.max(p1[1], p2[1])) { | |
if (p1[0] != p2[0]) { | |
xinters = | |
(checkPoint[0] - p1[0]) * | |
(p2[1] - p1[1]) / | |
(p2[0] - p1[0]) + | |
p1[1]; | |
if (p1[1] == p2[1] || checkPoint[1] <= xinters) { | |
counter++; | |
} | |
} | |
} | |
} | |
p1 = p2; | |
} | |
if (counter % 2 == 0) { | |
return false; | |
} else { | |
return true; | |
} | |
} | |
onMounted(() => { | |
const canvasCtx = canvasElement.value?.getContext("2d"); | |
function onResults(results: any) { | |
canvasCtx?.save(); | |
canvasCtx?.clearRect(0, 0, canvasElement.value?.width as number, canvasElement.value?.height as number); | |
canvasCtx?.drawImage( | |
results?.image, 0, 0, canvasElement.value?.width as number, canvasElement.value?.height as number); | |
if (results?.multiHandLandmarks) { | |
// 这里的代码是同时绘制多只手的,到时候只绘制一只手吧 | |
for (const landmarks of results?.multiHandLandmarks) { | |
if (landmarks == undefined || canvasCtx == undefined) { | |
continue | |
} | |
// 画关键点和连接线 | |
drawConnectors(canvasCtx as CanvasRenderingContext2D, landmarks, HAND_CONNECTIONS, | |
{ color: '#00FF00', lineWidth: 5 }); | |
drawLandmarks(canvasCtx as CanvasRenderingContext2D, landmarks, { color: '#FF0000', lineWidth: 2 }); | |
//console.log (landmarks); // 数组,装着对象 {x, y, z} | |
// console.log(results.image); | |
// 下面添加检测手势的代码 | |
// 采集所有关键点的坐标 | |
let list_lms: number[][] = [] | |
for (let i = 0; i < landmarks.length; i++) { | |
list_lms.push([landmarks[i]['x'] * canvasWidth.value, landmarks[i]['y'] * canvasHeight.value]) | |
} | |
//console.log ("所有点为"); | |
// console.log(list_lms); | |
let x0 = list_lms[0][0], y0 = list_lms[0][1] | |
let x6 = list_lms[6][0], y6 = list_lms[6][1] | |
let x8 = list_lms[8][0], y8 = list_lms[8][1] | |
let x10 = list_lms[8][0], y10 = list_lms[8][1] | |
let x12 = list_lms[12][0], y12 = list_lms[12][1] | |
let dis06 = Math.hypot(x6 - x0, y6 - y0) | |
let dis08 = Math.hypot(x8 - x0, y8 - y0) | |
//console.log ("0 到 6 的距离是", dis06); | |
//console.log ("0 到 8 的距离是", dis08); | |
let two_out = dis06 < dis08 | |
let dis0_10 = Math.hypot(x10 - x0, y10 - y0) | |
let dis0_12 = Math.hypot(x12 - x0, y12 - y0) | |
let three_out = dis0_10 < dis0_12 | |
// console.log(pre_one_finger, two_out, three_out); | |
if (pre_one_finger == false && two_out == true && three_out == false) { | |
pre_one_finger = true | |
pre_two_finger = false | |
console.log("伸出食指"); | |
// 检测到一根手指 | |
return | |
// 要不要设置 pre_two_finger 为 false 呢 | |
} | |
else if (pre_one_finger == true && two_out == true && three_out == false) { | |
// 上次检测到一根手指,现在还是检测到一根手指,直接返回 | |
return | |
} | |
else if (pre_two_finger == false && two_out == true && three_out == true) { | |
// 首次检测到两根手指 | |
pre_one_finger = false | |
pre_two_finger = true | |
console.log("伸出食指和中指"); | |
return | |
} else if(pre_two_finger == true && two_out == true && three_out == true) { | |
// 上次检测到两根手指,现在还是检测到两根手指,直接返回 | |
return | |
} | |
// if (res == false && preDetectTwo == false) { | |
// // 如果食指伸出,且上一次检测检测时食指未伸出 | |
// preDetectTwo = true | |
// console.log ("食指伸出"); | |
// } else if (res == false && preDetectTwo) { | |
// // 食指继续伸出,不响应 | |
// } else if (res == true) { | |
// // 没有检测到,置为 false,无论上次是否检测到 | |
// preDetectTwo = false | |
// } | |
// 下面的代码的凸包算法 | |
// 凸包点 | |
// let hull_list: number[][] = [] | |
// let hull_index: number[] = [0, 1, 2, 3, 6, 10, 14, 19, 18, 17, 10] | |
// for (let i = 0; i < hull_index.length; i++) { | |
// hull_list.push(list_lms[hull_index[i]]) | |
// } | |
// console.log("=======================\n"); | |
//console.log ("凸包点为"); | |
// console.log(hull_list); | |
// 检测食指指尖是否在凸包内 | |
// let res: boolean = isInPolygon(list_lms[8], hull_list) | |
// if (res == false && preDetectTwo == false) { | |
// // 如果食指伸出,且上一次检测检测时食指未伸出 | |
// preDetectTwo = true | |
// console.log ("食指伸出"); | |
// } else if (res == false && preDetectTwo) { | |
// // 食指继续伸出,不响应 | |
// } else if (res == true) { | |
// // 没有检测到,置为 false,无论上次是否检测到 | |
// preDetectTwo = false | |
// } | |
// console.log(res); | |
// if (res) { | |
// console.log ("在"); | |
// } else { | |
// console.log ("不在"); | |
// } | |
// 上面的代码的凸包算法 | |
// 上面是检测手势的代码 | |
} | |
} | |
canvasCtx?.restore(); | |
} | |
// 相机对象 | |
camera = new Camera(videoElement.value as HTMLVideoElement, { | |
onFrame: async () => { | |
await hands.send({ image: videoElement.value as InputImage }); | |
}, | |
width: canvasWidth.value, | |
height: canvasHeight.value | |
}); | |
const hands = new Hands({ | |
locateFile: (file) => { | |
console.log("进入file"); | |
return `https://cdn.jsdelivr.net/npm/@mediapipe/hands/${file}`; | |
} | |
}); | |
hands.setOptions({ | |
maxNumHands: 2, | |
modelComplexity: 1, | |
minDetectionConfidence: 0.5, | |
minTrackingConfidence: 0.5 | |
}); | |
hands.onResults(onResults); | |
// camera.start() | |
}) | |
let isOn: boolean = false | |
const startDetect = () => { | |
if (isOn) { | |
return | |
} | |
isOn = true | |
camera.start() | |
} | |
const stopDetect = () => { | |
if (isOn == false) { | |
return | |
} | |
isOn = false | |
camera.stop() | |
} | |
watch(router.currentRoute, () => { | |
if (isOn) { | |
// 上面括号之前写的 camera | |
camera.stop() | |
} | |
}) | |
const savePic = () => { | |
// 要先判断 vedio 是否处于启动状态 | |
if (isOn) { | |
let canvas = document.createElement('canvas') | |
canvas.width = 500; | |
canvas.height = 500; | |
let ctx = canvas.getContext('2d'); | |
ctx?.drawImage(videoElement?.value as CanvasImageSource, 0, 0, canvas.width, canvas.height); | |
// 生成图片 base64 编码 | |
let base64 = canvas.toDataURL('image/png') | |
// console.log(base64); | |
} else { | |
// console.log(""); | |
alert("请先打开摄像头") | |
} | |
} | |
</script> | |
<style scoped> | |
.container { | |
display: flex; | |
justify-content: center; | |
} | |
.facedetect { | |
height: 500px; | |
width: 1100px; | |
display: flex; | |
justify-content: space-around; | |
} | |
</style> |
# 距离算法 食指 四指
<template> | |
<div class="container"> | |
<el-card shadow="hover"> | |
<el-row> | |
<el-col :span="24"> | |
<div class="facedetect"> | |
<!-- 注意下面的两个标签都设置镜像翻转 --> | |
<video ref="videoElement" style="transform:rotateY(180deg)"></video> | |
<canvas class="output_canvas" ref="canvasElement" :width="canvasWidth" :height="canvasHeight" | |
style="transform:rotateY(180deg)"></canvas> | |
</div> | |
</el-col> | |
</el-row> | |
<el-row justify="space-around"> | |
<el-col :span="12" style="text-align:center;"> | |
<el-button type="success" size="large" @click="startDetect">开始识别</el-button> | |
</el-col> | |
<el-col :span="12" style="text-align:center;"> | |
<el-button type="primary" size="large" @click="stopDetect">停止识别</el-button> | |
</el-col> | |
<el-col style="text-align:center;"> | |
<el-button type="info" size="large" @click="savePic">保存图片</el-button> | |
</el-col> | |
</el-row> | |
<el-row justify="space-around"> | |
<el-col :span="12" style="text-align:center;"> | |
<!-- <el-button ref="leftbtn" type="success" size="large" @click="leftbtnclick"> 左按钮 & lt;/el-button> --> | |
<button ref="leftbtn" @click="leftbtnclick">左按钮</button> | |
</el-col> | |
<el-col :span="12" style="text-align:center;"> | |
<!-- <el-button ref="rightbtn" type="primary" size="large" @click="rightbtnclick"> 右按钮 & lt;/el-button> --> | |
<button ref="rightbtn" @click="rightbtnclick">左按钮</button> | |
</el-col> | |
</el-row> | |
</el-card> | |
</div> | |
</template> | |
<script setup lang="ts" > | |
// 上面的 video 显示是摄像头的,canvas 是处理后的 | |
import { Hands, HAND_CONNECTIONS, InputImage } from "@mediapipe/hands"; | |
import { Camera } from "@mediapipe/camera_utils"; | |
import { drawConnectors, drawLandmarks } from "@mediapipe/drawing_utils"; | |
import { onMounted, ref, watch } from 'vue' | |
import { fa, tr } from "element-plus/es/locale"; | |
import axios from "axios"; | |
import { useUsersStore } from '@/store/user'; | |
import { useRouter } from "vue-router"; | |
///////////////////////////////// | |
const leftbtn = ref<HTMLButtonElement | null>(null) | |
const rightbtn = ref<HTMLButtonElement | null>(null) | |
const leftbtnclick = () => { | |
console.log("左按钮被点击"); | |
} | |
const rightbtnclick = () => { | |
console.log("右按钮被点击"); | |
} | |
//////////////////////////////// | |
// 全局信息存储 | |
const store = useUsersStore() | |
// 监听当前路由变化,关闭摄像头 | |
const router = useRouter() | |
const videoElement = ref<HTMLVideoElement | null>(null) | |
const canvasElement = ref<HTMLCanvasElement | null>(null) | |
let camera: Camera | |
let canvasWidth = ref(500) | |
let canvasHeight = ref(500) | |
let preDetectTwo: boolean = false | |
let pre_one_finger: boolean = false | |
let pre_two_finger: boolean = false | |
let pre_four_finger: boolean = false | |
let exeBefore: boolean = false | |
// 检测是否在凸包点内的函数 | |
function isInPolygon(checkPoint: number[], polygonPoints: number[][]): boolean { | |
let counter = 0; | |
let i: number; | |
let xinters: number; | |
let p1: number[], p2: number[]; | |
let pointCount = polygonPoints.length; | |
p1 = polygonPoints[0]; | |
for (i = 1; i <= pointCount; i++) { | |
p2 = polygonPoints[i % pointCount]; | |
if ( | |
checkPoint[0] > Math.min(p1[0], p2[0]) && | |
checkPoint[0] <= Math.max(p1[0], p2[0]) | |
) { | |
if (checkPoint[1] <= Math.max(p1[1], p2[1])) { | |
if (p1[0] != p2[0]) { | |
xinters = | |
(checkPoint[0] - p1[0]) * | |
(p2[1] - p1[1]) / | |
(p2[0] - p1[0]) + | |
p1[1]; | |
if (p1[1] == p2[1] || checkPoint[1] <= xinters) { | |
counter++; | |
} | |
} | |
} | |
} | |
p1 = p2; | |
} | |
if (counter % 2 == 0) { | |
return false; | |
} else { | |
return true; | |
} | |
} | |
onMounted(() => { | |
const canvasCtx = canvasElement.value?.getContext("2d"); | |
function onResults(results: any) { | |
canvasCtx?.save(); | |
canvasCtx?.clearRect(0, 0, canvasElement.value?.width as number, canvasElement.value?.height as number); | |
canvasCtx?.drawImage( | |
results?.image, 0, 0, canvasElement.value?.width as number, canvasElement.value?.height as number); | |
if (results?.multiHandLandmarks) { | |
// 这里的代码是同时绘制多只手的,到时候只绘制一只手吧 | |
for (const landmarks of results?.multiHandLandmarks) { | |
if (landmarks == undefined || canvasCtx == undefined) { | |
continue | |
} | |
// 画关键点和连接线 | |
drawConnectors(canvasCtx as CanvasRenderingContext2D, landmarks, HAND_CONNECTIONS, | |
{ color: '#00FF00', lineWidth: 5 }); | |
drawLandmarks(canvasCtx as CanvasRenderingContext2D, landmarks, { color: '#FF0000', lineWidth: 2 }); | |
//console.log (landmarks); // 数组,装着对象 {x, y, z} | |
// console.log(results.image); | |
// 下面添加检测手势的代码 | |
// 采集所有关键点的坐标 | |
let list_lms: number[][] = [] | |
for (let i = 0; i < landmarks.length; i++) { | |
list_lms.push([landmarks[i]['x'] * canvasWidth.value, landmarks[i]['y'] * canvasHeight.value]) | |
} | |
//console.log ("所有点为"); | |
// console.log(list_lms); | |
let x0 = list_lms[0][0], y0 = list_lms[0][1] | |
let x6 = list_lms[6][0], y6 = list_lms[6][1] | |
let x8 = list_lms[8][0], y8 = list_lms[8][1] | |
let x10 = list_lms[10][0], y10 = list_lms[10][1] | |
let x12 = list_lms[12][0], y12 = list_lms[12][1] | |
let x14 = list_lms[14][0], y14 = list_lms[14][1] | |
let x16 = list_lms[16][0], y16 = list_lms[16][1] | |
let x18 = list_lms[18][0], y18 = list_lms[18][1] | |
let x20 = list_lms[20][0], y20 = list_lms[20][1] | |
let dis06 = Math.hypot(x6 - x0, y6 - y0) | |
let dis08 = Math.hypot(x8 - x0, y8 - y0) | |
//console.log ("0 到 6 的距离是", dis06); | |
//console.log ("0 到 8 的距离是", dis08); | |
let two_out = dis06 < dis08 | |
let dis0_10 = Math.hypot(x10 - x0, y10 - y0) | |
let dis0_12 = Math.hypot(x12 - x0, y12 - y0) | |
let three_out = dis0_10 < dis0_12 | |
let dis0_14 = Math.hypot(x14 - x0, y14 - y0) | |
let dis0_16 = Math.hypot(x16 - x0, y16 - y0) | |
let dis0_18 = Math.hypot(x18 - x0, y18 - y0) | |
let dis0_20 = Math.hypot(x20 - x0, y20 - y0) | |
let one_finger = dis06 < dis08 && dis0_10 > dis0_12 && dis0_14 > dis0_16 && dis0_18 > dis0_20 | |
let four_finger = dis06 < dis08 && dis0_10 < dis0_12 && dis0_14 < dis0_16 && dis0_18 < dis0_20 | |
console.log(pre_one_finger, pre_four_finger, one_finger, four_finger); | |
/** | |
* 在实际测试中发现,当上一步是伸出食指和中指,然后缩回来的过程中 | |
* 发现会检测到食指伸出,因此下面加一个判断,当上一步是伸出食指和中指且 | |
* 这次是伸出食指的时候直接返回,且要保证只执行一次 | |
*/ | |
// if(pre_one_finger == false && one_finger == true) { | |
// console.log ("伸出食指"); | |
// pre_one_finger = true | |
// pre_four_finger = false | |
// return | |
// }else if(pre_four_finger == false && four_finger == true) { | |
// console.log ("伸出四指"); | |
// pre_four_finger = true | |
// pre_one_finger = false | |
// } | |
// else { | |
// // 这里有问题, | |
// pre_one_finger = false | |
// pre_four_finger = false | |
// } | |
// 食指 | |
if (pre_one_finger == false && one_finger == true && four_finger == false) { | |
console.log("伸出食指"); | |
pre_one_finger = true | |
pre_four_finger = false | |
return | |
} else if (pre_one_finger == true && one_finger == true) { | |
// return | |
} else if(one_finger == false) { | |
pre_one_finger = false | |
} | |
// 四指 | |
if (pre_four_finger == false && four_finger == true && one_finger == false) { | |
console.log("伸出四指"); | |
pre_four_finger = true | |
pre_one_finger = false | |
return | |
} else if (pre_four_finger == true && four_finger == true) { | |
// return | |
} else if(four_finger == false) { | |
pre_four_finger = false | |
} | |
/* | |
if (pre_one_finger == false && two_out == true && three_out == false) { | |
pre_one_finger = true | |
pre_two_finger = false | |
console.log ("伸出食指"); | |
console.log (leftbtn.value); | |
if (leftbtn.value instanceof HTMLButtonElement) { | |
leftbtn.value.click () | |
} | |
// 检测到一根手指 | |
return | |
// 要不要设置 pre_two_finger 为 false 呢 | |
} | |
else if (pre_one_finger == true && two_out == true && three_out == false) { | |
// 上次检测到一根手指,现在还是检测到一根手指,直接返回 | |
return | |
} | |
else if (pre_two_finger == false && two_out == true && three_out == true) { | |
// 首次检测到两根手指 | |
pre_one_finger = false | |
pre_two_finger = true | |
//exeBefore = true | |
if (rightbtn.value instanceof HTMLButtonElement) { | |
rightbtn.value.click () | |
} | |
console.log ("伸出食指和中指"); | |
return | |
} else if (pre_two_finger == true && two_out == true && three_out == true) { | |
// 上次检测到两根手指,现在还是检测到两根手指,直接返回 | |
return | |
} else { | |
// 检测到其他手势,将 pre_one_finger 和 pre_two_finger 置为 false | |
pre_one_finger = false | |
} | |
*/ | |
// if (res == false && preDetectTwo == false) { | |
// // 如果食指伸出,且上一次检测检测时食指未伸出 | |
// preDetectTwo = true | |
// console.log ("食指伸出"); | |
// } else if (res == false && preDetectTwo) { | |
// // 食指继续伸出,不响应 | |
// } else if (res == true) { | |
// // 没有检测到,置为 false,无论上次是否检测到 | |
// preDetectTwo = false | |
// } | |
} | |
} | |
canvasCtx?.restore(); | |
} | |
// 相机对象 | |
camera = new Camera(videoElement.value as HTMLVideoElement, { | |
onFrame: async () => { | |
await hands.send({ image: videoElement.value as InputImage }); | |
}, | |
width: canvasWidth.value, | |
height: canvasHeight.value | |
}); | |
const hands = new Hands({ | |
locateFile: (file) => { | |
console.log("进入file"); | |
return `https://cdn.jsdelivr.net/npm/@mediapipe/hands/${file}`; | |
} | |
}); | |
hands.setOptions({ | |
maxNumHands: 2, | |
modelComplexity: 1, | |
minDetectionConfidence: 0.5, | |
minTrackingConfidence: 0.5 | |
}); | |
hands.onResults(onResults); | |
// camera.start() | |
}) | |
let isOn: boolean = false | |
const startDetect = () => { | |
if (isOn) { | |
return | |
} | |
isOn = true | |
camera.start() | |
} | |
const stopDetect = () => { | |
if (isOn == false) { | |
return | |
} | |
isOn = false | |
camera.stop() | |
} | |
watch(router.currentRoute, () => { | |
if (isOn) { | |
// 上面括号之前写的 camera | |
camera.stop() | |
} | |
}) | |
const savePic = () => { | |
// 要先判断 vedio 是否处于启动状态 | |
if (isOn) { | |
let canvas = document.createElement('canvas') | |
canvas.width = 500; | |
canvas.height = 500; | |
let ctx = canvas.getContext('2d'); | |
ctx?.drawImage(videoElement?.value as CanvasImageSource, 0, 0, canvas.width, canvas.height); | |
// 生成图片 base64 编码 | |
let base64 = canvas.toDataURL('image/png') | |
// console.log(base64); | |
} else { | |
// console.log(""); | |
alert("请先打开摄像头") | |
} | |
} | |
</script> | |
<style scoped> | |
.container { | |
display: flex; | |
justify-content: center; | |
} | |
.facedetect { | |
height: 500px; | |
width: 1100px; | |
display: flex; | |
justify-content: space-around; | |
} | |
</style> |
# 预览 pdf
<!-- <div style="width:1000px; height:1000px;background-color:pink;margin: auto;"> | |
<!-- <pdf :src="url"> --> | |
<!-- </pdf> --> | |
<object data="http://storage.xuetangx.com/public_assets/xuetangx/PDF/PlayerAPI_v1.0.6.pdf" type="application/pdf" width=800 height=800> | |
This browser does not support PDFs. Please download the PDF to view it: <a href="unix test.pdf">Download | |
PDF</a> | |
</object> | |
<!-- <iframe src="http://storage.xuetangx.com/public_assets/xuetangx/PDF/PlayerAPI_v1.0.6.pdf" style="width:1000px; height:1000px;background-color:pink;margin: auto;" frameborder="0"></iframe> --> | |
</div> --> |
https://www.zhangxinxu.com/wordpress/2015/11/html-input-type-file/
文件转 base64
https://blog.csdn.net/qq_34664239/article/details/94595508?spm=1001.2101.3001.6650.2&utm_medium=distribute.pc_relevant.none-task-blog-2~default~BlogCommendFromBaidu~Rate-2-94595508-blog-108580573.pc_relevant_vip_default&depth_1-utm_source=distribute.pc_relevant.none-task-blog-2~default~BlogCommendFromBaidu~Rate-2-94595508-blog-108580573.pc_relevant_vip_default&utm_relevant_index=3
data:application/pdf;base64,
28 个字符
翻页效果实现
<template> | |
<div id="container"> | |
<button @click="nextPage">按钮</button> | |
/ | |
<pdf src="http://storage.xuetangx.com/public_assets/xuetangx/PDF/PlayerAPI_v1.0.6.pdf" | |
:page="currentPage"></pdf> | |
</div> | |
</template> | |
<script setup lang="ts"> | |
import { onMounted, ref } from 'vue' | |
import pdf from 'vue3-pdf' | |
let currentPage = ref(1) | |
let pageCount = ref(0) | |
const nextPage = () => { | |
currentPage.value += 1 | |
} | |
</script> | |
<style scoped> | |
#container { | |
height: 60%; | |
width: 60%; | |
border: 20px; | |
margin: auto; | |
} | |
</style> |
# 将文件转为 base64,直接填在 src 中
<template> | |
<div id="container"> | |
<input type="file" ref="fileinput" @change="uploadFile" /> | |
<button @click="nextPage">按钮</button> | |
/ | |
<pdf :src="url" :page="currentPage"></pdf> | |
</div> | |
</template> | |
<script setup lang="ts"> | |
import { onMounted, ref } from 'vue' | |
import pdf from 'vue3-pdf' | |
let currentPage = ref(1) | |
let pageCount = ref(0) | |
let url = ref('http://storage.xuetangx.com/public_assets/xuetangx/PDF/PlayerAPI_v1.0.6.pdf') | |
const nextPage = () => { | |
currentPage.value += 1 | |
} | |
let fileinput = ref<HTMLInputElement | null>(null) | |
const uploadFile = () => { | |
let inputDom = fileinput.value; // 拿到 dom 元素 | |
console.log(inputDom); | |
if (inputDom != null && inputDom.files != null) { | |
let file = inputDom.files[0]; | |
console.log(file); | |
console.log(file.name); | |
// let url = URL.createObjectURL(file) | |
let reader = new FileReader(); | |
reader.readAsDataURL(file); | |
let data | |
reader.onload = function () { | |
data = reader.result | |
console.log(reader.result); //base64 | |
//console.log ("拿到 data"); | |
url.value = (data as string) | |
console.log(typeof data); | |
} | |
} | |
} | |
</script> | |
<style scoped> | |
#container { | |
height: 60%; | |
width: 60%; | |
border: 20px; | |
margin: auto; | |
} | |
</style> |