Browse Source

sprint2

master
Backpack 2 months ago
parent
commit
e8582482bf
42 changed files with 0 additions and 2988 deletions
  1. +0
    -43
      main.ts
  2. +0
    -6
      package-lock.json
  3. BIN
      public/mediapipe/gesture_recognizer.task
  4. +0
    -20
      public/mediapipe/wasm/vision_wasm_internal.js
  5. BIN
      public/mediapipe/wasm/vision_wasm_internal.wasm
  6. +0
    -20
      public/mediapipe/wasm/vision_wasm_nosimd_internal.js
  7. BIN
      public/mediapipe/wasm/vision_wasm_nosimd_internal.wasm
  8. BIN
      sprint2/WaveControl-sprint2.pptx
  9. +0
    -31
      src/components/AutoStart.vue
  10. +0
    -77
      src/components/CircleProgress.vue
  11. +0
    -81
      src/components/DevTool.vue
  12. +0
    -82
      src/components/GestureCard.vue
  13. +0
    -23
      src/components/GestureIcon.vue
  14. +0
    -41
      src/components/Menu.vue
  15. +0
    -24
      src/hand_landmark/VideoDetector.vue
  16. +0
    -190
      src/hand_landmark/detector.ts
  17. +0
    -21
      src/hand_landmark/gesture_handler.ts
  18. +0
    -24
      wavecontrol-test/.gitignore
  19. +0
    -3
      wavecontrol-test/.vscode/extensions.json
  20. +0
    -5
      wavecontrol-test/README.md
  21. +0
    -13
      wavecontrol-test/index.html
  22. +0
    -1765
      wavecontrol-test/package-lock.json
  23. +0
    -27
      wavecontrol-test/package.json
  24. BIN
      wavecontrol-test/public/mediapipe/gesture_recognizer.task
  25. +0
    -20
      wavecontrol-test/public/mediapipe/wasm/vision_wasm_internal.js
  26. BIN
      wavecontrol-test/public/mediapipe/wasm/vision_wasm_internal.wasm
  27. +0
    -20
      wavecontrol-test/public/mediapipe/wasm/vision_wasm_nosimd_internal.js
  28. BIN
      wavecontrol-test/public/mediapipe/wasm/vision_wasm_nosimd_internal.wasm
  29. +0
    -1
      wavecontrol-test/public/vite.svg
  30. +0
    -29
      wavecontrol-test/src/App.vue
  31. +0
    -1
      wavecontrol-test/src/assets/vue.svg
  32. +0
    -41
      wavecontrol-test/src/components/HelloWorld.vue
  33. +0
    -24
      wavecontrol-test/src/hand_landmark/VideoDetector.vue
  34. +0
    -190
      wavecontrol-test/src/hand_landmark/detector.ts
  35. +0
    -21
      wavecontrol-test/src/hand_landmark/gesture_handler.ts
  36. +0
    -5
      wavecontrol-test/src/main.ts
  37. +0
    -79
      wavecontrol-test/src/style.css
  38. +0
    -1
      wavecontrol-test/src/vite-env.d.ts
  39. +0
    -15
      wavecontrol-test/tsconfig.app.json
  40. +0
    -7
      wavecontrol-test/tsconfig.json
  41. +0
    -25
      wavecontrol-test/tsconfig.node.json
  42. +0
    -13
      wavecontrol-test/vite.config.ts

+ 0
- 43
main.ts View File

@ -1,43 +0,0 @@
import { Hands } from '@mediapipe/hands';
import { Camera } from '@mediapipe/camera_utils';
const videoElement = document.createElement('video');
videoElement.style.display = 'none';
document.body.appendChild(videoElement);
const hands = new Hands({
locateFile: (file) => `https://cdn.jsdelivr.net/npm/@mediapipe/hands/${file}`,
});
hands.setOptions({
maxNumHands: 1,
modelComplexity: 1,
minDetectionConfidence: 0.7,
minTrackingConfidence: 0.7,
});
hands.onResults((results) => {
if (results.multiHandLandmarks && results.multiHandLandmarks.length > 0) {
const landmarks = results.multiHandLandmarks[0];
// 检测食指是否伸出:tip高于pip(y坐标更小)
const tip = landmarks[8]; // 食指指尖
const pip = landmarks[6]; // 第二个关节
if (tip.y < pip.y) {
console.log('✅ 食指伸出');
} else {
console.log('❌ 食指未伸出');
}
}
});
const camera = new Camera(videoElement, {
onFrame: async () => {
await hands.send({ image: videoElement });
},
width: 640,
height: 480,
});
camera.start();

+ 0
- 6
package-lock.json View File

@ -1,6 +0,0 @@
{
"name": "wavecontrol",
"lockfileVersion": 3,
"requires": true,
"packages": {}
}

BIN
public/mediapipe/gesture_recognizer.task View File


+ 0
- 20
public/mediapipe/wasm/vision_wasm_internal.js
File diff suppressed because it is too large
View File


BIN
public/mediapipe/wasm/vision_wasm_internal.wasm View File


+ 0
- 20
public/mediapipe/wasm/vision_wasm_nosimd_internal.js
File diff suppressed because it is too large
View File


BIN
public/mediapipe/wasm/vision_wasm_nosimd_internal.wasm View File


BIN
sprint2/WaveControl-sprint2.pptx View File


+ 0
- 31
src/components/AutoStart.vue View File

@ -1,31 +0,0 @@
<script setup lang="ts">
import { Power } from "@icon-park/vue-next";
import { disable, enable } from "@tauri-apps/plugin-autostart";
import { watch } from "vue";
import { use_app_store } from "@/store/app";
const app_store = use_app_store();
watch(
() => app_store.config.auto_start,
async (value) => {
if (value) {
await enable();
} else {
await disable();
}
}
);
</script>
<template>
<n-space align="center" style="display: flex; align-items: center">
<span style="display: flex; align-items: center">
<n-icon size="20" style="margin-right: 8px">
<Power />
</n-icon>
<span>{{ $t("开机自启动") }}</span>
</span>
<n-switch v-model:value="app_store.config.auto_start" />
</n-space>
</template>

+ 0
- 77
src/components/CircleProgress.vue View File

@ -1,77 +0,0 @@
<template>
<div class="circle-progress">
<svg :width="size" :height="size" viewBox="0 0 100 100">
<!-- 背景圆环 -->
<circle
cx="50"
cy="50"
:r="radius"
fill="none"
:stroke="backgroundColor"
:stroke-width="strokeWidth"
/>
<!-- 进度圆环 -->
<circle
cx="50"
cy="50"
:r="radius"
fill="none"
:stroke="color"
:stroke-width="strokeWidth"
:stroke-dasharray="circumference"
:stroke-dashoffset="dashOffset"
class="progress"
/>
<!-- 中心文本 -->
<slot>
<text x="50" y="50" text-anchor="middle" dominant-baseline="middle" class="progress-text">
{{ text }}
{{ percentage }}%
</text>
</slot>
</svg>
</div>
</template>
<script setup lang="ts">
import { computed } from 'vue';
interface Props {
percentage: number;
size?: number;
strokeWidth?: number;
color?: string;
backgroundColor?: string;
text?: string;
}
const props = withDefaults(defineProps<Props>(), {
size: 100,
strokeWidth: 6,
color: '#409eff',
backgroundColor: '#e5e9f2',
text: ''
});
const radius = computed(() => 50 - props.strokeWidth / 2);
const circumference = computed(() => 2 * Math.PI * radius.value);
const dashOffset = computed(() =>
circumference.value * (1 - props.percentage / 100)
);
</script>
<style lang="scss" scoped>
.circle-progress {
display: inline-block;
.progress {
transform: rotate(-90deg);
transform-origin: center;
}
.progress-text {
font-size: 14px;
fill: #606266;
}
}
</style>

+ 0
- 81
src/components/DevTool.vue View File

@ -1,81 +0,0 @@
<template>
<div
v-if="is_dev"
class="dev-tool"
:class="{ 'dev-tool--expanded': isExpanded }"
>
<div class="dev-tool__toggle" @click="toggleToolbox">
<span class="dev-tool__icon">🔧</span>
</div>
<!-- <div class="dev-tool__content" v-if="isExpanded">
<div class="dev-tool__item" @click="createSubWindowClick">创建子窗口</div>
</div> -->
</div>
</template>
<script setup lang="ts">
import { createSubWindow } from "@/utils/subWindow";
import { ref } from "vue";
const is_dev = import.meta.env.DEV;
const isExpanded = ref(false);
const toggleToolbox = () => {
isExpanded.value = !isExpanded.value;
};
const createSubWindowClick = () => {
createSubWindow("/sub-window", "subWindow");
};
</script>
<style scoped lang="scss">
.dev-tool {
position: fixed;
top: 50%;
left: 0;
transform: translateY(-50%);
background-color: #fff;
border-radius: 0 8px 8px 0;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
transition: all 0.3s ease;
z-index: 9999;
&--expanded {
width: 200px;
}
&__toggle {
padding: 10px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
border-bottom: 1px solid #eee;
}
&__icon {
font-size: 20px;
}
&__content {
padding: 10px;
}
&__item {
padding: 8px 12px;
cursor: pointer;
border-radius: 4px;
margin-bottom: 5px;
transition: background-color 0.2s ease;
&:hover {
background-color: #f5f5f5;
}
&:last-child {
margin-bottom: 0;
}
}
}
</style>

+ 0
- 82
src/components/GestureCard.vue View File

@ -1,82 +0,0 @@
<template>
<n-card class="gesture-card" :bordered="false">
<n-space align="center" class="gesture-content">
<div class="gesture-icon" :class="{ 'double-hand': isDoubleHand }">
<slot name="icon"></slot>
</div>
<div class="gesture-info">
<h3>{{ title }}</h3>
<p>{{ description }}</p>
<slot name="extra"></slot>
</div>
</n-space>
</n-card>
</template>
<script setup lang="ts">
defineProps<{
title: string;
description: string;
isDoubleHand?: boolean;
}>();
</script>
<style scoped lang="scss">
.gesture-card {
transition: all 0.3s ease;
background: linear-gradient(145deg, #f8faff, #ffffff);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05);
border: 1px solid #e5e9f2;
border-radius: 12px;
&:hover {
transform: translateY(-2px);
box-shadow: 0 8px 24px rgba(64, 152, 252, 0.15);
border-color: #4098fc;
}
}
.gesture-content {
padding: 12px;
}
.gesture-icon {
background: rgba(64, 152, 252, 0.1);
padding: 16px;
border-radius: 12px;
display: flex;
align-items: center;
justify-content: center;
border: 1px solid rgba(64, 152, 252, 0.2);
}
.gesture-info {
flex: 1;
h3 {
margin: 0 0 4px 0;
font-size: 1.1rem;
color: #2c3e50;
}
p {
margin: 0;
color: #666;
font-size: 0.9rem;
}
}
.double-hand {
display: flex;
gap: 8px;
:deep(svg) {
width: 32px;
height: 32px;
}
}
.flipped {
transform: scaleX(-1);
}
</style>

+ 0
- 23
src/components/GestureIcon.vue View File

@ -1,23 +0,0 @@
<template>
<component
:is="icon"
theme="outline"
size="40"
fill="#4098fc"
:stroke-width="3"
:class="{ flipped }"
/>
</template>
<script setup lang="ts">
defineProps<{
icon: any;
flipped?: boolean;
}>();
</script>
<style scoped>
.flipped {
transform: scaleX(-1);
}
</style>

+ 0
- 41
src/components/Menu.vue View File

@ -1,41 +0,0 @@
<script setup lang="ts">
import type { MenuOption } from "naive-ui";
import { NMenu } from "naive-ui";
import { ref } from "vue";
import { useRouter } from "vue-router";
const router = useRouter();
const menuOptions: MenuOption[] = [
{
label: "首页",
key: "/",
},
{
label: "操作指南",
key: "/guide",
},
// {
// label: "",
// key: "/update",
// }
];
const activeKey = ref("/");
const handleUpdateValue = (key: string) => {
activeKey.value = key;
router.push(key);
};
</script>
<template>
<n-menu
:options="menuOptions"
v-model:value="activeKey"
mode="vertical"
@update:value="handleUpdateValue"
/>
</template>
<style scoped></style>

+ 0
- 24
src/hand_landmark/VideoDetector.vue View File

@ -1,24 +0,0 @@
import { onMounted, ref } from 'vue';
import { Detector } from './hand_landmark/detector';
const videoRef = ref<HTMLVideoElement | null>(null);
const detector = new Detector();
onMounted(async () => {
await detector.initialize(); //
const video = videoRef.value!;
video.width = 640;
video.height = 480;
navigator.mediaDevices.getUserMedia({ video: true }).then((stream) => {
video.srcObject = stream;
video.play();
// 200ms
setInterval(async () => {
const result = await detector.detect(video);
await detector.process(result);
}, 200);
});
});

+ 0
- 190
src/hand_landmark/detector.ts View File

@ -1,190 +0,0 @@
import {
FilesetResolver,
GestureRecognizer,
} from "@mediapipe/tasks-vision";
// 手势
export enum HandGesture {
// 食指举起,移动鼠标
ONLY_INDEX_UP = "only_index_up",
INDEX_AND_THUMB_UP = "index_and_thumb_up"
}
interface HandLandmark {
x: number;
y: number;
z: number;
}
export interface HandInfo {
landmarks: HandLandmark[];
handedness: "Left" | "Right";
score: number;
categoryName?: string;
}
interface DetectionResult {
leftHand?: HandInfo;
rightHand?: HandInfo;
// 原始检测结果,以防需要访问其他数据
rawResult: any;
}
/**
* -
* :
* 1.
* 2.
* 3. ()
* 4.
*/
export class Detector {
private detector: GestureRecognizer | null = null;
constructor(private modelPath = "/mediapipe/gesture_recognizer.task") {}
async initialize(useCanvas = false) {
const vision = await FilesetResolver.forVisionTasks("/mediapipe/wasm");
try {
const params: any = {
baseOptions: {
modelAssetPath: this.modelPath,
delegate: "GPU",
},
runningMode: "VIDEO",
numHands: 1,
};
if (useCanvas) {
params.canvas = document.createElement("canvas");
}
this.detector = await GestureRecognizer.createFromOptions(vision, params);
} catch (error: any) {
if (error.toString().includes("kGpuService")) {
await this.initialize(true);
} else {
throw error;
}
}
}
// 检测视频帧中的手部(保持独立)
async detect(video: HTMLVideoElement): Promise<DetectionResult> {
if (!this.detector) {
throw new Error("检测器未初始化");
}
const now = performance.now();
const result = await this.detector.recognizeForVideo(video, now);
const detection: DetectionResult = {
rawResult: result,
};
if (result.landmarks && result.handedness) {
for (let i = 0; i < result.landmarks.length; i++) {
const hand: HandInfo = {
landmarks: result.landmarks[i],
handedness: result.handedness[i][0].categoryName as "Left" | "Right",
score: result.handedness[i][0].score,
};
if (result.gestures.length > 0) {
hand.categoryName = result.gestures[0][0].categoryName;
}
if (hand.handedness === "Left") {
detection.leftHand = hand;
} else {
detection.rightHand = hand;
}
}
}
return detection;
}
/**
*
*/
static _fingersUp(hand: HandInfo): number[] {
const fingers: number[] = [];
const tipIds = [4, 8, 12, 16, 20]; // 从大拇指开始,依次为每个手指指尖
// 检测大拇指
if (hand.handedness === "Right") {
if (hand.landmarks[tipIds[0]].x < hand.landmarks[tipIds[0] - 1].x) {
fingers.push(0);
} else {
fingers.push(1);
}
} else {
if (hand.landmarks[tipIds[0]].x > hand.landmarks[tipIds[0] - 1].x) {
fingers.push(0);
} else {
fingers.push(1);
}
}
// 检测其他四个手指
for (let id = 1; id < 5; id++) {
if (hand.landmarks[tipIds[id]].y < hand.landmarks[tipIds[id] - 2].y) {
fingers.push(1);
} else {
fingers.push(0);
}
}
return fingers;
}
/**
*
*/
public static getSingleHandGesture(hand: HandInfo): HandGesture {
const fingers = this._fingersUp(hand);
const fingerState = fingers.join(",");
// 定义手势映射表
const gestureMap = new Map<string, HandGesture>([
// 食指举起,移动鼠标
["0,1,0,0,0", HandGesture.ONLY_INDEX_UP],
// 鼠标左键点击手势
["1,1,0,0,0", HandGesture.INDEX_AND_THUMB_UP],
]);
if (gestureMap.has(fingerState)) {
return gestureMap.get(fingerState) as HandGesture;
}
// 返回默认值
return HandGesture.OTHER;
}
/**
*
*/
async process(detection: DetectionResult): Promise<HandGesture> {
const hand = detection.rightHand ?? detection.leftHand;
if (!hand) {
console.log("没检测到手");
return HandGesture.OTHER;
}
const gesture = Detector.getSingleHandGesture(hand);
console.log("当前手势状态是:", gesture);
if (gesture === HandGesture.ONLY_INDEX_UP) {
console.log("识别到食指竖起!");
}
if (gesture === HandGesture.INDEX_AND_THUMB_UP) {
console.log("识别到食指和大拇指竖起!");
}
return gesture;
}
}

+ 0
- 21
src/hand_landmark/gesture_handler.ts View File

@ -1,21 +0,0 @@
// gesture_handler.ts
import { HandGesture, HandInfo } from "./detector";
export class GestureHandler {
private previousGesture: HandGesture | null = null;
private previousGestureCount = 0;
private minGestureCount = 3;
handleGesture(gesture: HandGesture, hand: HandInfo) {
if (gesture === this.previousGesture) {
this.previousGestureCount++;
} else {
this.previousGesture = gesture;
this.previousGestureCount = 1;
}
if (this.previousGestureCount >= this.minGestureCount) {
console.log("识别手势类型:", gesture);
}
}
}

+ 0
- 24
wavecontrol-test/.gitignore View File

@ -1,24 +0,0 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

+ 0
- 3
wavecontrol-test/.vscode/extensions.json View File

@ -1,3 +0,0 @@
{
"recommendations": ["Vue.volar"]
}

+ 0
- 5
wavecontrol-test/README.md View File

@ -1,5 +0,0 @@
# Vue 3 + TypeScript + Vite
This template should help get you started developing with Vue 3 and TypeScript in Vite. The template uses Vue 3 `<script setup>` SFCs, check out the [script setup docs](https://v3.vuejs.org/api/sfc-script-setup.html#sfc-script-setup) to learn more.
Learn more about the recommended Project Setup and IDE Support in the [Vue Docs TypeScript Guide](https://vuejs.org/guide/typescript/overview.html#project-setup).

+ 0
- 13
wavecontrol-test/index.html View File

@ -1,13 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite + Vue + TS</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>

+ 0
- 1765
wavecontrol-test/package-lock.json
File diff suppressed because it is too large
View File


+ 0
- 27
wavecontrol-test/package.json View File

@ -1,27 +0,0 @@
{
"name": "wavecontrol-test",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vue-tsc -b && vite build",
"preview": "vite preview"
},
"dependencies": {
"@mediapipe/tasks-vision": "^0.10.22-rc.20250304",
"@tensorflow-models/handpose": "^0.1.0",
"@tensorflow/tfjs-backend-webgl": "^4.22.0",
"@tensorflow/tfjs-core": "^4.22.0",
"fingerpose": "^0.1.0",
"vue": "^3.5.17"
},
"devDependencies": {
"@types/node": "^24.0.12",
"@vitejs/plugin-vue": "^6.0.0",
"@vue/tsconfig": "^0.7.0",
"typescript": "~5.8.3",
"vite": "^7.0.3",
"vue-tsc": "^2.2.12"
}
}

BIN
wavecontrol-test/public/mediapipe/gesture_recognizer.task View File


+ 0
- 20
wavecontrol-test/public/mediapipe/wasm/vision_wasm_internal.js
File diff suppressed because it is too large
View File


BIN
wavecontrol-test/public/mediapipe/wasm/vision_wasm_internal.wasm View File


+ 0
- 20
wavecontrol-test/public/mediapipe/wasm/vision_wasm_nosimd_internal.js
File diff suppressed because it is too large
View File


BIN
wavecontrol-test/public/mediapipe/wasm/vision_wasm_nosimd_internal.wasm View File


+ 0
- 1
wavecontrol-test/public/vite.svg View File

@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>

+ 0
- 29
wavecontrol-test/src/App.vue View File

@ -1,29 +0,0 @@
<template>
<video ref="videoRef" autoplay playsinline muted style="display: none;" />
</template>
<script setup lang="ts">
import { onMounted, ref } from 'vue';
import { Detector } from './hand_landmark/detector';
const videoRef = ref<HTMLVideoElement | null>(null);
const detector = new Detector();
onMounted(async () => {
await detector.initialize();
const video = videoRef.value!;
video.width = 640;
video.height = 480;
navigator.mediaDevices.getUserMedia({ video: true }).then((stream) => {
video.srcObject = stream;
video.play();
setInterval(async () => {
const result = await detector.detect(video);
await detector.process(result);
}, 200);
});
});
</script>

+ 0
- 1
wavecontrol-test/src/assets/vue.svg View File

@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="37.07" height="36" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 198"><path fill="#41B883" d="M204.8 0H256L128 220.8L0 0h97.92L128 51.2L157.44 0h47.36Z"></path><path fill="#41B883" d="m0 0l128 220.8L256 0h-51.2L128 132.48L50.56 0H0Z"></path><path fill="#35495E" d="M50.56 0L128 133.12L204.8 0h-47.36L128 51.2L97.92 0H50.56Z"></path></svg>

+ 0
- 41
wavecontrol-test/src/components/HelloWorld.vue View File

@ -1,41 +0,0 @@
<script setup lang="ts">
import { ref } from 'vue'
defineProps<{ msg: string }>()
const count = ref(0)
</script>
<template>
<h1>{{ msg }}</h1>
<div class="card">
<button type="button" @click="count++">count is {{ count }}</button>
<p>
Edit
<code>components/HelloWorld.vue</code> to test HMR
</p>
</div>
<p>
Check out
<a href="https://vuejs.org/guide/quick-start.html#local" target="_blank"
>create-vue</a
>, the official Vue + Vite starter
</p>
<p>
Learn more about IDE Support for Vue in the
<a
href="https://vuejs.org/guide/scaling-up/tooling.html#ide-support"
target="_blank"
>Vue Docs Scaling up Guide</a
>.
</p>
<p class="read-the-docs">Click on the Vite and Vue logos to learn more</p>
</template>
<style scoped>
.read-the-docs {
color: #888;
}
</style>

+ 0
- 24
wavecontrol-test/src/hand_landmark/VideoDetector.vue View File

@ -1,24 +0,0 @@
import { onMounted, ref } from 'vue';
import { Detector } from './hand_landmark/detector';
const videoRef = ref<HTMLVideoElement | null>(null);
const detector = new Detector();
onMounted(async () => {
await detector.initialize(); //
const video = videoRef.value!;
video.width = 640;
video.height = 480;
navigator.mediaDevices.getUserMedia({ video: true }).then((stream) => {
video.srcObject = stream;
video.play();
// 200ms
setInterval(async () => {
const result = await detector.detect(video);
await detector.process(result);
}, 200);
});
});

+ 0
- 190
wavecontrol-test/src/hand_landmark/detector.ts View File

@ -1,190 +0,0 @@
import {
FilesetResolver,
GestureRecognizer,
} from "@mediapipe/tasks-vision";
// 手势
export enum HandGesture {
// 食指举起,移动鼠标
ONLY_INDEX_UP = "only_index_up",
INDEX_AND_THUMB_UP = "index_and_thumb_up"
}
interface HandLandmark {
x: number;
y: number;
z: number;
}
export interface HandInfo {
landmarks: HandLandmark[];
handedness: "Left" | "Right";
score: number;
categoryName?: string;
}
interface DetectionResult {
leftHand?: HandInfo;
rightHand?: HandInfo;
// 原始检测结果,以防需要访问其他数据
rawResult: any;
}
/**
* -
* :
* 1.
* 2.
* 3. ()
* 4.
*/
export class Detector {
private detector: GestureRecognizer | null = null;
constructor(private modelPath = "/mediapipe/gesture_recognizer.task") {}
async initialize(useCanvas = false) {
const vision = await FilesetResolver.forVisionTasks("/mediapipe/wasm");
try {
const params: any = {
baseOptions: {
modelAssetPath: this.modelPath,
delegate: "GPU",
},
runningMode: "VIDEO",
numHands: 1,
};
if (useCanvas) {
params.canvas = document.createElement("canvas");
}
this.detector = await GestureRecognizer.createFromOptions(vision, params);
} catch (error: any) {
if (error.toString().includes("kGpuService")) {
await this.initialize(true);
} else {
throw error;
}
}
}
// 检测视频帧中的手部(保持独立)
async detect(video: HTMLVideoElement): Promise<DetectionResult> {
if (!this.detector) {
throw new Error("检测器未初始化");
}
const now = performance.now();
const result = await this.detector.recognizeForVideo(video, now);
const detection: DetectionResult = {
rawResult: result,
};
if (result.landmarks && result.handedness) {
for (let i = 0; i < result.landmarks.length; i++) {
const hand: HandInfo = {
landmarks: result.landmarks[i],
handedness: result.handedness[i][0].categoryName as "Left" | "Right",
score: result.handedness[i][0].score,
};
if (result.gestures.length > 0) {
hand.categoryName = result.gestures[0][0].categoryName;
}
if (hand.handedness === "Left") {
detection.leftHand = hand;
} else {
detection.rightHand = hand;
}
}
}
return detection;
}
/**
*
*/
static _fingersUp(hand: HandInfo): number[] {
const fingers: number[] = [];
const tipIds = [4, 8, 12, 16, 20]; // 从大拇指开始,依次为每个手指指尖
// 检测大拇指
if (hand.handedness === "Right") {
if (hand.landmarks[tipIds[0]].x < hand.landmarks[tipIds[0] - 1].x) {
fingers.push(0);
} else {
fingers.push(1);
}
} else {
if (hand.landmarks[tipIds[0]].x > hand.landmarks[tipIds[0] - 1].x) {
fingers.push(0);
} else {
fingers.push(1);
}
}
// 检测其他四个手指
for (let id = 1; id < 5; id++) {
if (hand.landmarks[tipIds[id]].y < hand.landmarks[tipIds[id] - 2].y) {
fingers.push(1);
} else {
fingers.push(0);
}
}
return fingers;
}
/**
*
*/
public static getSingleHandGesture(hand: HandInfo): HandGesture {
const fingers = this._fingersUp(hand);
const fingerState = fingers.join(",");
// 定义手势映射表
const gestureMap = new Map<string, HandGesture>([
// 食指举起,移动鼠标
["0,1,0,0,0", HandGesture.ONLY_INDEX_UP],
// 鼠标左键点击手势
["1,1,0,0,0", HandGesture.INDEX_AND_THUMB_UP],
]);
if (gestureMap.has(fingerState)) {
return gestureMap.get(fingerState) as HandGesture;
}
// 返回默认值
return HandGesture.OTHER;
}
/**
*
*/
async process(detection: DetectionResult): Promise<HandGesture> {
const hand = detection.rightHand ?? detection.leftHand;
if (!hand) {
console.log("没检测到手");
return HandGesture.OTHER;
}
const gesture = Detector.getSingleHandGesture(hand);
console.log("当前手势状态是:", gesture);
if (gesture === HandGesture.ONLY_INDEX_UP) {
console.log("识别到食指竖起!");
}
if (gesture === HandGesture.INDEX_AND_THUMB_UP) {
console.log("识别到食指和大拇指竖起!");
}
return gesture;
}
}

+ 0
- 21
wavecontrol-test/src/hand_landmark/gesture_handler.ts View File

@ -1,21 +0,0 @@
// gesture_handler.ts
import { HandGesture, HandInfo } from "./detector";
export class GestureHandler {
private previousGesture: HandGesture | null = null;
private previousGestureCount = 0;
private minGestureCount = 3;
handleGesture(gesture: HandGesture, hand: HandInfo) {
if (gesture === this.previousGesture) {
this.previousGestureCount++;
} else {
this.previousGesture = gesture;
this.previousGestureCount = 1;
}
if (this.previousGestureCount >= this.minGestureCount) {
console.log("识别手势类型:", gesture);
}
}
}

+ 0
- 5
wavecontrol-test/src/main.ts View File

@ -1,5 +0,0 @@
import { createApp } from 'vue'
import './style.css'
import App from './App.vue'
createApp(App).mount('#app')

+ 0
- 79
wavecontrol-test/src/style.css View File

@ -1,79 +0,0 @@
:root {
font-family: system-ui, Avenir, Helvetica, Arial, sans-serif;
line-height: 1.5;
font-weight: 400;
color-scheme: light dark;
color: rgba(255, 255, 255, 0.87);
background-color: #242424;
font-synthesis: none;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
a {
font-weight: 500;
color: #646cff;
text-decoration: inherit;
}
a:hover {
color: #535bf2;
}
body {
margin: 0;
display: flex;
place-items: center;
min-width: 320px;
min-height: 100vh;
}
h1 {
font-size: 3.2em;
line-height: 1.1;
}
button {
border-radius: 8px;
border: 1px solid transparent;
padding: 0.6em 1.2em;
font-size: 1em;
font-weight: 500;
font-family: inherit;
background-color: #1a1a1a;
cursor: pointer;
transition: border-color 0.25s;
}
button:hover {
border-color: #646cff;
}
button:focus,
button:focus-visible {
outline: 4px auto -webkit-focus-ring-color;
}
.card {
padding: 2em;
}
#app {
max-width: 1280px;
margin: 0 auto;
padding: 2rem;
text-align: center;
}
@media (prefers-color-scheme: light) {
:root {
color: #213547;
background-color: #ffffff;
}
a:hover {
color: #747bff;
}
button {
background-color: #f9f9f9;
}
}

+ 0
- 1
wavecontrol-test/src/vite-env.d.ts View File

@ -1 +0,0 @@
/// <reference types="vite/client" />

+ 0
- 15
wavecontrol-test/tsconfig.app.json View File

@ -1,15 +0,0 @@
{
"extends": "@vue/tsconfig/tsconfig.dom.json",
"compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"erasableSyntaxOnly": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedSideEffectImports": true
},
"include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.vue"]
}

+ 0
- 7
wavecontrol-test/tsconfig.json View File

@ -1,7 +0,0 @@
{
"files": [],
"references": [
{ "path": "./tsconfig.app.json" },
{ "path": "./tsconfig.node.json" }
]
}

+ 0
- 25
wavecontrol-test/tsconfig.node.json View File

@ -1,25 +0,0 @@
{
"compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
"target": "ES2023",
"lib": ["ES2023"],
"module": "ESNext",
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"verbatimModuleSyntax": true,
"moduleDetection": "force",
"noEmit": true,
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"erasableSyntaxOnly": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedSideEffectImports": true
},
"include": ["vite.config.ts"]
}

+ 0
- 13
wavecontrol-test/vite.config.ts View File

@ -1,13 +0,0 @@
/// <reference types="node" />
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import path from 'path'
export default defineConfig({
plugins: [vue()],
resolve: {
alias: {
'@': path.resolve(__dirname, 'src'),
},
},
})

Loading…
Cancel
Save