diff --git a/game/贪吃蛇/snake_game.html b/game/贪吃蛇/snake_game.html new file mode 100644 index 0000000..18e7bc1 --- /dev/null +++ b/game/贪吃蛇/snake_game.html @@ -0,0 +1,828 @@ + + + + + + AI 姿态控制贪吃蛇 + + + + + + + + + +

AI 姿态控制贪吃蛇

+ +
+
+ + +
+ +
+ +
+ +
+
得分: 0
+
姿态识别: 未识别
+
控制指令: 静止
+
等待模型导入...
+
+
+ + + +
+
+

姿态控制说明

+
    +
  • 向上: 双手举过头顶
  • +
  • 向下: 双手放在身体两侧或下垂
  • +
  • 向左: 左手平举
  • +
  • 向右: 右手平举
  • +
  • 静止: 保持站立姿势 (无特定动作)
  • +
+

(确保在摄像头画面中能清晰识别全身)

+
+
+
+ + +
+

游戏结束!

+

最终得分: 0

+ +
+ + + + diff --git a/game/钢琴/index.html b/game/钢琴/index.html new file mode 100644 index 0000000..542518f --- /dev/null +++ b/game/钢琴/index.html @@ -0,0 +1,792 @@ + + + + + + AI 空气钢琴 - 手势控制 + + + + + + + + + + + + + +

AI 空气钢琴

+

通过手势弹奏虚拟钢琴!

+ +
+ +
+ + +
正在加载模型,请稍候...
+
+ + +
+
+

模型管理

+ + +
+ +
+

演奏控制

+ +
+ 实时手势: + 未识别 +
+
+ 置信度: + 0% +
+
+ 当前演奏音符: + +
+
+ +
+

音符映射

+

请确保您的手势分类与音符对应:

+
    + +
  • ID 0: 中央C (C4) → 音段 1
  • +
  • ID 1: D4 → 音段 2
  • +
  • ID 2: E4 → 音段 3
  • +
  • ID 3: F4 → 音段 4
  • +
  • ID 4: G4 → 音段 5
  • +
  • ID 5: A4 → 音段 6
  • +
  • ID 6: B4 → 音段 7
  • +
  • ID 7: 高音C (C5) → 音段 8
  • +
+
+
+
+ + + + diff --git a/game/钢琴/sounds/A2.mp3 b/game/钢琴/sounds/A2.mp3 new file mode 100644 index 0000000..415de14 Binary files /dev/null and b/game/钢琴/sounds/A2.mp3 differ diff --git a/game/钢琴/sounds/B2.mp3 b/game/钢琴/sounds/B2.mp3 new file mode 100644 index 0000000..6f3fce2 Binary files /dev/null and b/game/钢琴/sounds/B2.mp3 differ diff --git a/game/钢琴/sounds/C2.mp3 b/game/钢琴/sounds/C2.mp3 new file mode 100644 index 0000000..8f7d778 Binary files /dev/null and b/game/钢琴/sounds/C2.mp3 differ diff --git a/game/钢琴/sounds/C3.mp3 b/game/钢琴/sounds/C3.mp3 new file mode 100644 index 0000000..f0aa866 Binary files /dev/null and b/game/钢琴/sounds/C3.mp3 differ diff --git a/game/钢琴/sounds/D2.mp3 b/game/钢琴/sounds/D2.mp3 new file mode 100644 index 0000000..d4e57d3 Binary files /dev/null and b/game/钢琴/sounds/D2.mp3 differ diff --git a/game/钢琴/sounds/E2.mp3 b/game/钢琴/sounds/E2.mp3 new file mode 100644 index 0000000..4d9a607 Binary files /dev/null and b/game/钢琴/sounds/E2.mp3 differ diff --git a/game/钢琴/sounds/F2.mp3 b/game/钢琴/sounds/F2.mp3 new file mode 100644 index 0000000..83d5eaa Binary files /dev/null and b/game/钢琴/sounds/F2.mp3 differ diff --git a/game/钢琴/sounds/G2.mp3 b/game/钢琴/sounds/G2.mp3 new file mode 100644 index 0000000..5b36960 Binary files /dev/null and b/game/钢琴/sounds/G2.mp3 differ diff --git a/姿态分类/script.js b/姿态分类/script.js index e5b942b..1355ded 100644 --- a/姿态分类/script.js +++ b/姿态分类/script.js @@ -1,15 +1,17 @@ /** * ============================================================================= - * 动态版 - 姿态识别与模型管理脚本 (v2.0) + * 动态版 - 姿态识别与模型管理脚本 (v2.1) + * - 新增自动采集样本功能 * ============================================================================= * 功能列表: * - 实时姿态检测 (MoveNet) * - KNN 分类器训练 * - 实时姿态预测 * - 坐标完美对齐 (Canvas与Video重叠) - * - ✅ 动态添加/删除/重命名姿态类别 - * - ✅ 模型导出为包含类别信息的 JSON 文件 - * - ✅ 从 JSON 文件导入模型并恢复类别状态 + * - 动态添加/删除/重命名姿态类别 + * - 模型导出为包含类别信息的 JSON 文件 + * - 从 JSON 文件导入模型并恢复类别状态 + * - ✅ 新增:自动采集10次样本,间隔0.3秒 * ============================================================================= */ @@ -32,6 +34,7 @@ const fileImporter = document.getElementById('file-importer'); let detector, classifier, animationFrameId; let isPredicting = false; +let isAutoCollecting = false; // 新增:标记是否正在进行自动采集 // 📌 核心状态管理: 使用一个对象来管理所有动态状态 const appState = { @@ -105,12 +108,14 @@ function createClassUI(classId, className) { poseClassDiv.className = 'pose-class'; poseClassDiv.dataset.classId = classId; + // 📌 修改这里:添加 btn-auto-sample 按钮 poseClassDiv.innerHTML = `
(0 样本)
+
@@ -124,10 +129,17 @@ function createClassUI(classId, className) { appState.classMap[classId] = e.target.value; }); + const autoSampleButton = poseClassDiv.querySelector('.btn-auto-sample'); // 新增 + autoSampleButton.addEventListener('click', () => toggleAutoCollection(classId, autoSampleButton)); // 新增 + const sampleButton = poseClassDiv.querySelector('.btn-sample'); sampleButton.addEventListener('click', () => addExample(classId)); - if (isPredicting) sampleButton.disabled = true; // 如果在预测中,禁用新按钮 + // 初始化时根据预测状态禁用按钮 + if (isPredicting) { + sampleButton.disabled = true; + autoSampleButton.disabled = true; // 新增 + } const deleteButton = poseClassDiv.querySelector('.btn-delete-class'); deleteButton.addEventListener('click', () => deleteClass(classId)); @@ -177,11 +189,81 @@ async function addExample(classId) { updateSampleCounts(); checkExportAbility(); + console.log(`为类别 ${appState.classMap[classId]} 采集1个样本。`); + return true; // 表示采集成功 } else { console.warn(`为类别 ${appState.classMap[classId]} 采集样本失败,未检测到姿态。`); + return false; // 表示采集失败 } } +// --- 新增:自动采集逻辑 --- +let autoCollectionIntervalId = null; // 用于存储 setInterval ID +let autoCollectionCount = 0; // 计数器 +const AUTO_COLLECTION_TOTAL = 10; // 总共采集次数 +const AUTO_COLLECTION_INTERVAL = 300; // 间隔时间 0.3 秒 + +async function toggleAutoCollection(classId, buttonElement) { + if (isAutoCollecting) { + // 如果正在自动采集,则停止 + stopAutoCollection(buttonElement); + } else { + // 否则,开始自动采集 + startAutoCollection(classId, buttonElement); + } +} + +async function startAutoCollection(classId, buttonElement) { + isAutoCollecting = true; + autoCollectionCount = 0; + + // 禁用其他采集和预测按钮 + predictButton.disabled = true; + exportButton.disabled = true; + importButton.disabled = true; + addClassButton.disabled = true; + document.querySelectorAll('.btn-sample, .btn-auto-sample, .btn-delete-class, .class-name-input').forEach(btn => { + if (btn !== buttonElement) { // 不禁用当前自动采集按钮 + btn.disabled = true; + } + if (btn.classList.contains('class-name-input')) btn.disabled = true; + }); + + buttonElement.innerText = `停止采集 (0/${AUTO_COLLECTION_TOTAL})`; + buttonElement.classList.add('stop'); // 添加停止样式 + + const performCollection = async () => { + if (autoCollectionCount < AUTO_COLLECTION_TOTAL) { + const success = await addExample(classId); // 调用手动采集功能 + if (success) { + autoCollectionCount++; + } + buttonElement.innerText = `停止采集 (${autoCollectionCount}/${AUTO_COLLECTION_TOTAL})`; + } else { + stopAutoCollection(buttonElement); + alert(`类别 "${appState.classMap[classId]}" 自动采集完成!`); + } + }; + + // 立即执行一次,然后设置定时器 + await performCollection(); + if (autoCollectionCount < AUTO_COLLECTION_TOTAL) { + autoCollectionIntervalId = setInterval(performCollection, AUTO_COLLECTION_INTERVAL); + } +} + +function stopAutoCollection(buttonElement) { + clearInterval(autoCollectionIntervalId); + autoCollectionIntervalId = null; + isAutoCollecting = false; + buttonElement.innerText = '自动采集'; + buttonElement.classList.remove('stop'); // 移除停止样式 + + // 重新启用按钮(根据应用状态) + updatePredictionUI(); // 根据预测状态重新启用/禁用相关按钮 + enableControls(); // 重新启用添加类别、导出、导入按钮 +} + // --- 模型与预测逻辑 --- /** @@ -205,16 +287,20 @@ async function mainLoop() { if (poses && poses.length > 0) { drawPose(poses[0]); - if (isPredicting && classifier.getNumClasses() > 0) { + // 只有当不在自动采集状态时才进行预测 + if (isPredicting && classifier.getNumClasses() > 0 && !isAutoCollecting) { const poseTensor = flattenPose(poses[0]); const result = await classifier.predictClass(poseTensor, 3); poseTensor.dispose(); const confidence = Math.round(result.confidences[result.label] * 100); - // 📌 动态获取类别名称 const predictedClassName = appState.classMap[result.label] || '未知类别'; resultElement.innerText = `姿态: ${predictedClassName} (${confidence}%)`; + } else if (isAutoCollecting) { + resultElement.innerText = "自动采集中..."; } + } else { + resultElement.innerText = "未检测到姿态"; } animationFrameId = requestAnimationFrame(mainLoop); } @@ -237,7 +323,6 @@ function exportModel() { datasetObj[key] = data.arraySync(); }); - // 📌 导出格式大更新: 同时保存 classMap 和 dataset const modelData = { classMap: appState.classMap, dataset: datasetObj @@ -268,7 +353,6 @@ function importModel(event) { try { const modelData = JSON.parse(e.target.result); - // 📌 导入格式验证 if (!modelData.classMap || !modelData.dataset) { throw new Error("无效的模型文件格式。"); } @@ -364,40 +448,53 @@ function updateSampleCounts() { * 根据状态更新UI */ function updatePredictionUI() { - const allActionButtons = document.querySelectorAll('.btn-sample, .btn-delete-class, .btn-add-class, #btn-import'); + // 禁用所有采集按钮(包括手动和自动)和删除按钮 + document.querySelectorAll('.btn-sample, .btn-auto-sample, .btn-delete-class').forEach(btn => btn.disabled = isPredicting || isAutoCollecting); + + // 禁用添加类别和导入模型的按钮 + addClassButton.disabled = isPredicting || isAutoCollecting; + importButton.disabled = isPredicting || isAutoCollecting; + + // 禁用类别名称输入框 + document.querySelectorAll('.class-name-input').forEach(input => input.disabled = isPredicting || isAutoCollecting); + if (isPredicting) { predictButton.innerText = "停止预测"; predictButton.classList.add('stop'); resultElement.innerText = "正在分析..."; - allActionButtons.forEach(btn => btn.disabled = true); - document.querySelectorAll('.class-name-input').forEach(input => input.disabled = true); - checkExportAbility(); } else { predictButton.innerText = "开始预测"; predictButton.classList.remove('stop'); resultElement.innerText = "已停止"; - allActionButtons.forEach(btn => btn.disabled = false); - document.querySelectorAll('.class-name-input').forEach(input => input.disabled = false); - checkExportAbility(); } // 只有在有类别且有样本时才能预测 - predictButton.disabled = isPredicting ? false : classifier.getNumClasses() === 0; + predictButton.disabled = isPredicting ? false : classifier.getNumClasses() === 0 || isAutoCollecting; + checkExportAbility(); } +/** + * 通用启用/禁用控件 (在自动采集停止后调用) + */ function enableControls() { - [predictButton, importButton, exportButton, addClassButton].forEach(btn => btn.disabled = false); - checkExportAbility(); + // 重新评估所有按钮的状态 + // 自动采集按钮的状态由其自身管理 + predictButton.disabled = classifier.getNumClasses() === 0; + importButton.disabled = false; // 导入按钮总是可以手动启用 + addClassButton.disabled = false; + checkExportAbility(); // 重新检查导出按钮 + updatePredictionUI(); // 再次调用,确保其他按钮状态正确 } /** 检查是否可以导出模型并更新按钮状态 */ function checkExportAbility() { - exportButton.disabled = isPredicting || classifier.getNumClasses() === 0; + exportButton.disabled = isPredicting || classifier.getNumClasses() === 0 || isAutoCollecting; } function cleanup() { if (detector) detector.dispose(); if (classifier) classifier.clearAllClasses(); if (animationFrameId) cancelAnimationFrame(animationFrameId); + if (autoCollectionIntervalId) clearInterval(autoCollectionIntervalId); // 清理自动采集定时器 } // --- 启动应用 --- diff --git a/姿态分类/style.css b/姿态分类/style.css index d5dc028..212e748 100644 --- a/姿态分类/style.css +++ b/姿态分类/style.css @@ -169,7 +169,7 @@ h3 { .class-actions { display: flex; align-items: center; - gap: 0.5rem; /* 按钮间距 */ + gap: 5px; /* 按钮间距 */ } /* 📌 新增: 删除按钮样式 */ @@ -254,3 +254,32 @@ h3 { display: flex; gap: 1rem; } + + +.btn-auto-sample { + padding: 8px 15px; + font-size: 0.9em; + background-color: #5cb85c; /* 绿色 */ + color: white; + border: none; + border-radius: 4px; + cursor: pointer; + transition: background-color 0.2s ease; + margin-right: 8px; /* 与手动采集按钮之间留出间距 */ + } + + .btn-auto-sample:hover { + background-color: #4cae4c; + } + + .btn-auto-sample:disabled { + background-color: #cccccc; + cursor: not-allowed; + } + /* 添加一个停止按钮样式 */ + .btn-auto-sample.stop { + background-color: #d9534f; /* 红色 */ + } + .btn-auto-sample.stop:hover { + background-color: #c9302c; + } \ No newline at end of file