diff --git a/.vscode/settings.json b/.vscode/settings.json
new file mode 100644
index 0000000..6f3a291
--- /dev/null
+++ b/.vscode/settings.json
@@ -0,0 +1,3 @@
+{
+ "liveServer.settings.port": 5501
+}
\ No newline at end of file
diff --git a/game/分类器.zip b/game/分类器.zip
new file mode 100644
index 0000000..13fedf0
Binary files /dev/null and b/game/分类器.zip differ
diff --git a/game/分类器/index.html b/game/分类器/index.html
index ef65931..8c5ef96 100644
--- a/game/分类器/index.html
+++ b/game/分类器/index.html
@@ -3,7 +3,7 @@
-
📦 Web Serial 实时分类器
+
-
正在检查 Web Serial API 兼容性...
-
-
+
+
+
+
🔌 串口连接
+
正在检查 Web Serial API 兼容性...
+
+
+
+
+
-
+
+
+
🧠 模型管理
+
正在加载 MobileNet 模型...
+
+
+
+
-
正在加载 MobileNet 和 KNN 模型...
-
-
-
+
+
+
📹 实时预测
+
+
+
+
+
-
-
-
-
-
等待识别...
+
+
等待识别...
+
diff --git a/game/分类器/script.js b/game/分类器/script.js
index 3d2f202..d188ceb 100644
--- a/game/分类器/script.js
+++ b/game/分类器/script.js
@@ -9,6 +9,7 @@ const STOP_WEBCAM_BTN = document.getElementById('stopWebcamBtn');
const MODEL_STATUS = document.getElementById('modelStatus');
const SERIAL_STATUS = document.getElementById('serialStatus');
const PREDICTION_OUTPUT = document.getElementById('prediction');
+const WEBCAM_STATUS_DISPLAY = document.getElementById('webcam-status-display'); // !!! ADDED !!!
let mobilenet; // 这个变量将存储加载后的 MobileNet 模型实例
let knnClassifier; // 这个变量将存储 KNN 分类器实例
@@ -55,9 +56,18 @@ function updateSerialUI(isConnected) {
}
}
+// !!! MODIFIED: Adjusted updateWebcamUI to use the new WEBCAM_STATUS_DISPLAY !!!
function updateWebcamUI(isRunning) {
START_WEBCAM_BTN.disabled = isRunning;
STOP_WEBCAM_BTN.disabled = !isRunning;
+ if (isRunning) {
+ showStatus(WEBCAM_STATUS_DISPLAY, 'info', '摄像头已启动,等待模型预测...');
+ PREDICTION_OUTPUT.classList.remove('idle', 'error'); // !!! ADDED !!!
+ } else {
+ showStatus(WEBCAM_STATUS_DISPLAY, 'info', '摄像头未启动');
+ PREDICTION_OUTPUT.classList.add('idle'); // !!! ADDED !!!
+ PREDICTION_OUTPUT.textContent = '等待识别...'; // !!! ADDED !!!
+ }
}
function updateModelUI(isLoaded) {
@@ -70,6 +80,7 @@ function updateModelUI(isLoaded) {
// ===================================
async function initModel() {
showStatus(MODEL_STATUS, 'info', '正在加载 MobileNet 模型...');
+ showStatus(WEBCAM_STATUS_DISPLAY, 'info', '系统初始化中...'); // !!! ADDED !!!
try {
// 确保 window.mobilenet 和 window.knnClassifier 库已加载
if (!window.mobilenet || !window.knnClassifier) {
@@ -108,6 +119,7 @@ async function initModel() {
} catch (error) {
showStatus(MODEL_STATUS, 'error', `模型加载失败: ${error.message}`);
+ showStatus(WEBCAM_STATUS_DISPLAY, 'error', '模型加载失败'); // !!! ADDED !!!
console.error('MobileNet/KNN加载失败:', error);
}
}
@@ -328,6 +340,7 @@ async function loadKNNModel(jsonUrl = null, binUrl = null) {
} catch (error) {
showStatus(MODEL_STATUS, 'error', `加载 KNN 模型失败: ${error.message}`);
+ showStatus(WEBCAM_STATUS_DISPLAY, 'error', '模型加载失败'); // !!! ADDED !!!
console.error('加载 KNN 模型总失败:', error);
updateModelUI(false);
// 重新抛出错误,以便 initModel 可以捕获 CDN 加载失败的情况
@@ -368,6 +381,7 @@ async function loadSingleJsonModel(modelData) {
updateModelUI(true);
} catch (error) {
showStatus(MODEL_STATUS, 'error', `加载单文件JSON模型失败: ${error.message}`);
+ showStatus(WEBCAM_STATUS_DISPLAY, 'error', '模型加载失败'); // !!! ADDED !!!
console.error('加载单文件JSON模型失败:', error);
updateModelUI(false);
throw error; // 重新抛出错误
@@ -401,11 +415,13 @@ async function startWebcam() {
// ===================================
isPredicting = true;
predictLoop();
- showStatus(MODEL_STATUS, 'info', '摄像头已启动,开始实时预测...');
+ showStatus(WEBCAM_STATUS_DISPLAY, 'success', `摄像头已运行,识别中...`); // !!! MODIFIED !!!
+ PREDICTION_OUTPUT.classList.remove('idle', 'error'); // !!! ADDED !!!
};
} catch (error) {
showStatus(MODEL_STATUS, 'error', `无法访问摄像头: ${error.message}`);
+ showStatus(WEBCAM_STATUS_DISPLAY, 'error', '无法启动摄像头'); // !!! MODIFIED !!!
console.error('启动摄像头失败:', error);
updateWebcamUI(false);
}
@@ -418,9 +434,8 @@ function stopWebcam() {
}
isPredicting = false;
VIDEO.srcObject = null;
- updateWebcamUI(false);
- PREDICTION_OUTPUT.textContent = '等待识别...';
- showStatus(MODEL_STATUS, 'info', '摄像头已停止。');
+ updateWebcamUI(false); // !!! MODIFIED !!!
+ showStatus(WEBCAM_STATUS_DISPLAY, 'info', '摄像头已停止'); // !!! MODIFIED !!!
// ===================================
// 停止摄像头时,清除任何待确认的命令,并发送“停止”或“复位”命令
@@ -455,10 +470,11 @@ async function predictLoop() {
if (!knnClassifier || knnClassifier.getNumClasses() === 0) {
features.dispose();
PREDICTION_OUTPUT.textContent = 'KNN 分类器未就绪或无数据。';
- commandCandidate = '0'; // 使用默认命令
+ PREDICTION_OUTPUT.classList.add('error'); // !!! ADDED !!!
+ commandCandidate = '0';
} else {
const prediction = await knnClassifier.predictClass(features, k);
- features.dispose(); // 及时释放 Tensor 内存
+ features.dispose();
if (prediction && prediction.confidences) {
let maxConfidence = 0;
@@ -473,33 +489,36 @@ async function predictLoop() {
}
});
- const confidenceThreshold = 0.75; // 75%置信度
+ const confidenceThreshold = 0.75;
if (predictedClassIndex !== -1 && maxConfidence > confidenceThreshold) {
const className = classNames[predictedClassIndex] || `Class ${predictedClassIndex + 1}`;
const percentage = (maxConfidence * 100).toFixed(1);
PREDICTION_OUTPUT.textContent = `识别为: ${className} (${percentage}%)`;
+ PREDICTION_OUTPUT.classList.remove('idle', 'error'); // !!! MODIFIED !!!
- // 根据类别设置本帧的候选命令
if (predictedClassIndex === 0) {
commandCandidate = '1';
} else if (predictedClassIndex === 1) {
commandCandidate = '2';
} else {
- commandCandidate = '0'; // 未匹配到特定类别,或默认复位
+ commandCandidate = '0';
}
} else {
PREDICTION_OUTPUT.textContent = `未知或不确定... (最高置信度: ${(maxConfidence * 100).toFixed(1)}%)`;
- commandCandidate = '0'; // 不确定也发送'0'回退
+ PREDICTION_OUTPUT.classList.add('idle'); // !!! MODIFIED !!!
+ commandCandidate = '0';
}
} else {
PREDICTION_OUTPUT.textContent = '无法识别。';
- commandCandidate = '0'; // 无法识别也发送 '0' 回退
+ PREDICTION_OUTPUT.classList.add('error'); // !!! ADDED !!!
+ commandCandidate = '0';
}
}
} catch (error) {
console.error('预测错误:', error);
PREDICTION_OUTPUT.textContent = `预测错误: ${error.message}`;
- commandCandidate = '0'; // 错误时也发送'0'
+ PREDICTION_OUTPUT.classList.add('error'); // !!! ADDED !!!
+ commandCandidate = '0';
}
// =========================================================
diff --git a/game/橘子/index.html b/game/橘子/index.html
new file mode 100644
index 0000000..adb1f81
--- /dev/null
+++ b/game/橘子/index.html
@@ -0,0 +1,407 @@
+
+
+
+
+
+
Goood Space - 实时分类器
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
🧠 模型管理
+
正在加载 MobileNet 模型...
+
+
+
+
+
+
+
+
📹 实时预测
+
+
+
+
+
+
+
+
等待识别...
+
+
+
+
+
+
diff --git a/game/橘子/script.js b/game/橘子/script.js
new file mode 100644
index 0000000..42ae5e3
--- /dev/null
+++ b/game/橘子/script.js
@@ -0,0 +1,512 @@
+// script.js
+
+const VIDEO = document.getElementById('webcam');
+// const CONNECT_SERIAL_BTN = document.getElementById('connectSerialBtn'); // REMOVED
+// const DISCONNECT_SERIAL_BTN = document.getElementById('disconnectSerialBtn'); // REMOVED
+const LOAD_MODEL_BTN = document.getElementById('loadModelBtn');
+const START_WEBCAM_BTN = document.getElementById('startWebcamBtn');
+const STOP_WEBCAM_BTN = document.getElementById('stopWebcamBtn');
+const MODEL_STATUS = document.getElementById('modelStatus');
+// const SERIAL_STATUS = document.getElementById('serialStatus'); // REMOVED
+const PREDICTION_OUTPUT = document.getElementById('prediction');
+const WEBCAM_STATUS_DISPLAY = document.getElementById('webcam-status-display');
+
+let mobilenet;
+let knnClassifier;
+let classNames = [];
+let webcamStream = null;
+let isPredicting = false;
+
+// REMOVED: Web Serial API variables
+// let serialPort = null;
+// let serialWriter = null;
+// const SERIAL_BAUD_RATE = 9600;
+// const SERIAL_SEND_MIN_INTERVAL = 500;
+// let lastSerialCommand = '';
+// let lastSerialSendTime = 0;
+
+// REMOVED: Serial connection state variables
+// let isSerialConnectedState = false;
+// let lastSentClassCommand = null;
+
+// REMOVED: Confirmation sending logic variables
+// let pendingCommandToSend = null;
+// let pendingCommandTimerId = null;
+// const CONFIRMATION_DELAY_MS = 100;
+
+
+// ===================================
+// Helper Functions (UI Status)
+// ===================================
+function showStatus(element, type, message) {
+ element.className = `status-message status-${type}`;
+ element.textContent = message;
+}
+
+// REMOVED: updateSerialUI function
+/*
+function updateSerialUI(isConnected) {
+ CONNECT_SERIAL_BTN.disabled = isConnected;
+ DISCONNECT_SERIAL_BTN.disabled = !isConnected;
+ isSerialConnectedState = isConnected;
+ if (!isConnected) {
+ showStatus(SERIAL_STATUS, 'info', '串口未连接。点击 "连接串口" 开始。');
+ }
+}
+*/
+
+function updateWebcamUI(isRunning) {
+ START_WEBCAM_BTN.disabled = isRunning;
+ STOP_WEBCAM_BTN.disabled = !isRunning;
+ if (isRunning) {
+ showStatus(WEBCAM_STATUS_DISPLAY, 'info', '摄像头已启动,等待模型预测...');
+ PREDICTION_OUTPUT.classList.remove('idle', 'error');
+ } else {
+ showStatus(WEBCAM_STATUS_DISPLAY, 'info', '摄像头未启动');
+ PREDICTION_OUTPUT.classList.add('idle');
+ PREDICTION_OUTPUT.textContent = '等待识别...';
+ }
+}
+
+function updateModelUI(isLoaded) {
+ LOAD_MODEL_BTN.disabled = false;
+ START_WEBCAM_BTN.disabled = !isLoaded;
+}
+
+// ===================================
+// Core Logic: Model & Webcam
+// ===================================
+async function initModel() {
+ showStatus(MODEL_STATUS, 'info', '正在加载 MobileNet 模型...');
+ showStatus(WEBCAM_STATUS_DISPLAY, 'info', '系统初始化中...');
+ try {
+ if (!window.tf || !window.mobilenet || !window.knnClassifier) { // Added tf check
+ showStatus(MODEL_STATUS, 'error', 'TensorFlow.js 核心库或模型库未加载。请检查 HTML 引入。');
+ console.error('TensorFlow.js 核心库或模型库未加载。');
+ return;
+ }
+
+ mobilenet = await window.mobilenet.load({ version: 2, alpha: 1.0 });
+ knnClassifier = window.knnClassifier.create();
+ showStatus(MODEL_STATUS, 'success', 'MobileNet 模型和 KNN 分类器已加载。');
+ updateModelUI(false);
+
+ const cdnModelBaseUrl = 'https://goood-space-assets.oss-cn-beijing.aliyuncs.com/public/models/';
+ const cdnModelJsonFileName = 'knn-model-juzi.json';
+ const cdnModelBinFileName = 'knn-model-juzi.bin';
+
+ const cdnJsonUrl = `${cdnModelBaseUrl}${cdnModelJsonFileName}`;
+ const cdnBinUrl = `${cdnModelBaseUrl}${cdnModelBinFileName}`;
+
+ console.log(`尝试从 CDN 加载模型: ${cdnJsonUrl}, ${cdnBinUrl}`);
+ showStatus(MODEL_STATUS, 'info', '正在尝试从 CDN 自动加载 KNN 模型...');
+
+ try {
+ await loadKNNModel(cdnJsonUrl, cdnBinUrl);
+ console.log('CDN 模型自动加载成功。');
+ } catch (cdnError) {
+ showStatus(MODEL_STATUS, 'warning', `从 CDN 加载 KNN 模型失败: ${cdnError.message}。您可以尝试手动加载。`);
+ console.warn('CDN KNN 模型加载失败:', cdnError);
+ updateModelUI(false);
+ }
+
+ } catch (error) {
+ showStatus(MODEL_STATUS, 'error', `模型加载失败: ${error.message}`);
+ showStatus(WEBCAM_STATUS_DISPLAY, 'error', '模型加载失败');
+ console.error('MobileNet/KNN加载失败:', error);
+ }
+}
+
+async function getFeatures(img) {
+ if (!mobilenet) {
+ throw new Error("MobileNet model is not loaded.");
+ }
+ return tf.tidy(() => {
+ const embeddings = mobilenet.infer(img, true);
+ const norm = tf.norm(embeddings);
+ const normalized = tf.div(embeddings, norm);
+ return normalized;
+ });
+}
+
+// loadSingleJsonModel 保持不变
+async function loadSingleJsonModel(modelData) {
+ try {
+ knnClassifier.clearAllClasses();
+ Object.keys(modelData.dataset).forEach(key => {
+ const data = modelData.dataset[key];
+ const featureDim = modelData.featureDim || 1280;
+ if (data.length % featureDim !== 0) {
+ throw new Error(`类别 ${key} 的特征数据长度 ${data.length} 与特征维度 ${featureDim} 不匹配!`);
+ }
+ const numSamples = data.length / featureDim;
+ const tensor = tf.tensor(data, [numSamples, featureDim]);
+ knnClassifier.addExample(tensor, parseInt(key));
+ tf.dispose(tensor);
+ });
+
+ if (modelData.classList && Array.isArray(modelData.classList)) {
+ classNames = modelData.classList.map(c => c.name);
+ } else if (modelData.classNames && Array.isArray(modelData.classNames)) {
+ classNames = modelData.classNames;
+ } else {
+ console.warn('模型JSON中未找到 classList/classNames 字段,使用默认类别名称。');
+ classNames = Object.keys(modelData.dataset).map(key => `Class ${parseInt(key) + 1}`);
+ }
+
+ showStatus(MODEL_STATUS, 'success', `模型 (单文件JSON格式) 加载成功!类别: ${classNames.join(', ')}。`);
+ updateModelUI(true);
+ } catch (error) {
+ showStatus(MODEL_STATUS, 'error', `加载单文件JSON模型失败: ${error.message}`);
+ showStatus(WEBCAM_STATUS_DISPLAY, 'error', '模型加载失败');
+ console.error('加载单文件JSON模型失败:', error);
+ updateModelUI(false);
+ throw error;
+ }
+}
+
+
+async function loadKNNModel(jsonUrl = null, binUrl = null) {
+ if (!knnClassifier) {
+ showStatus(MODEL_STATUS, 'error', 'KNN 分类器未初始化。请先加载 MobileNet 模型。');
+ return;
+ }
+
+ let modelData = null;
+ let binData = null;
+ let modelName = '未知模型';
+
+ try {
+ if (jsonUrl && binUrl) {
+ showStatus(MODEL_STATUS, 'info', `正在从 CDN 加载模型配置文件 (${jsonUrl})...`);
+ const jsonResponse = await fetch(jsonUrl);
+ if (!jsonResponse.ok) {
+ throw new Error(`无法从 ${jsonUrl} 加载.json文件: ${jsonResponse.statusText}`);
+ }
+ modelData = await jsonResponse.json();
+ modelName = jsonUrl.split('/').pop();
+
+ showStatus(MODEL_STATUS, 'info', `正在从 CDN 加载模型权重 (${binUrl})...`);
+ const binResponse = await fetch(binUrl);
+ if (!binResponse.ok) {
+ throw new Error(`无法从 ${binUrl} 加载.bin文件: ${binResponse.statusText}`);
+ }
+ const arrayBuffer = await binResponse.arrayBuffer();
+ binData = new Float32Array(arrayBuffer);
+
+ if (modelData.dataFile && !binUrl.endsWith(modelData.dataFile)) {
+ console.warn(`CDN 加载警告:.bin URL (${binUrl}) 与 .json 中定义的 dataFile (${modelData.dataFile}) 不匹配。继续加载。`);
+ }
+
+ } else {
+ const inputJson = document.createElement('input');
+ inputJson.type = 'file';
+ inputJson.accept = '.json';
+ inputJson.multiple = false;
+
+ showStatus(MODEL_STATUS, 'info', '请先选择 KNN 模型配置文件 (.json)...');
+
+ await new Promise((resolve, reject) => {
+ inputJson.onchange = async (e) => {
+ const jsonFile = e.target.files[0];
+ if (!jsonFile) {
+ showStatus(MODEL_STATUS, 'info', '未选择 .json 文件。');
+ updateModelUI(false);
+ return reject(new Error('No JSON file selected.'));
+ }
+
+ showStatus(MODEL_STATUS, 'info', `正在解析 ${jsonFile.name}...`);
+ modelName = jsonFile.name;
+
+ try {
+ const reader = new FileReader();
+ const jsonText = await new Promise((res, rej) => {
+ reader.onload = () => res(reader.result);
+ reader.onerror = () => rej(reader.error);
+ reader.readAsText(jsonFile);
+ });
+ modelData = JSON.parse(jsonText);
+
+ if (!modelData.dataFile) {
+ console.warn('模型JSON文件不包含 "dataFile" 字段,尝试以旧的单文件JSON格式加载。');
+ await loadSingleJsonModel(modelData);
+ return resolve();
+ }
+
+ } catch (error) {
+ showStatus(MODEL_STATUS, 'error', `解析 .json 文件失败: ${error.message}`);
+ console.error('解析 .json 失败:', error);
+ updateModelUI(false);
+ return reject(error);
+ }
+
+ const inputBin = document.createElement('input');
+ inputBin.type = 'file';
+ inputBin.accept = '.bin';
+ inputBin.multiple = false;
+
+ showStatus(MODEL_STATUS, 'info', `已加载 .json 文件。请选择对应的权重文件 "${modelData.dataFile}" (.bin)...`);
+
+ inputBin.onchange = async (eBin) => {
+ const binFile = eBin.target.files[0];
+ if (!binFile) {
+ showStatus(MODEL_STATUS, 'info', '未选择 .bin 文件。');
+ updateModelUI(false);
+ return reject(new Error('No BIN file selected.'));
+ }
+
+ if (binFile.name !== modelData.dataFile) {
+ showStatus(MODEL_STATUS, 'error', `选择的 .bin 文件名 "${binFile.name}" 与 .json 中定义的 "${modelData.dataFile}" 不匹配!请选择正确的文件。`);
+ updateModelUI(false);
+ return reject(new Error('BIN file name mismatch.'));
+ }
+
+ showStatus(MODEL_STATUS, 'info', `正在读取 ${binFile.name} (二进制权重文件)...`);
+ try {
+ const reader = new FileReader();
+ const arrayBuffer = await new Promise((res, rej) => {
+ reader.onload = () => res(reader.result);
+ reader.onerror = () => rej(reader.error);
+ reader.readAsArrayBuffer(binFile);
+ });
+ binData = new Float32Array(arrayBuffer);
+ resolve();
+ } catch (error) {
+ showStatus(MODEL_STATUS, 'error', `读取 .bin 文件失败: ${error.message}`);
+ console.error('读取 .bin 失败:', error);
+ updateModelUI(false);
+ return reject(error);
+ }
+ };
+ inputBin.click();
+ };
+ inputJson.click();
+ });
+ }
+
+ if (!modelData) {
+ return;
+ }
+
+ if (modelData && binData) {
+ knnClassifier.clearAllClasses();
+
+ Object.keys(modelData.dataset).forEach(label => {
+ const classDataMeta = modelData.dataset[label];
+ const startFloat32ElementIndex = classDataMeta.start;
+ const numFloat32Elements = classDataMeta.length;
+
+ const featureDim = modelData.featureDim || 1280;
+
+ if (startFloat32ElementIndex + numFloat32Elements > binData.length) {
+ throw new Error(`模型数据错误: 类别 ${label} 的数据超出 .bin 文件范围。`);
+ }
+
+ const classFeatures = binData.subarray(startFloat32ElementIndex, startFloat32ElementIndex + numFloat32Elements);
+
+ if (classFeatures.length === 0) {
+ console.warn(`类别 ${label} 没有找到特征数据,跳过。`);
+ return;
+ }
+
+ if (classFeatures.length % featureDim !== 0) {
+ const actualSamples = classFeatures.length / featureDim;
+ console.error(
+ `--- 类别: ${label} ---`,
+ `起始 Float32 元素索引: ${startFloat32ElementIndex}`,
+ `该类别 Float32 元素数量: ${numFloat32Elements}`,
+ `ERROR: 特征数据长度 (${classFeatures.length} 个 Float32 元素) 与特征维度 (${featureDim}) 不匹配!` +
+ `实际样本数计算为 ${actualSamples} (预期为整数)。`,
+ `请检查您的模型导出逻辑和训练数据的完整性。`
+ );
+ throw new Error("模型数据完整性错误:特征数据长度与维度不匹配。");
+ }
+
+ const numSamples = classFeatures.length / featureDim;
+
+ for (let i = 0; i < numSamples; i++) {
+ const startIndex = i * featureDim;
+ const endIndex = (i + 1) * featureDim;
+ const sampleFeatures = classFeatures.subarray(startIndex, endIndex);
+
+ const sampleTensor = tf.tensor(sampleFeatures, [1, featureDim]);
+
+ knnClassifier.addExample(sampleTensor, parseInt(label));
+ tf.dispose(sampleTensor);
+ }
+ });
+
+ if (modelData.classList && Array.isArray(modelData.classList)) {
+ classNames = modelData.classList.map(c => c.name);
+ } else {
+ console.warn('模型JSON中未找到 classList 字段或格式不正确,使用默认类别名称。');
+ classNames = Object.keys(modelData.dataset).map(key => `Class ${parseInt(key) + 1}`);
+ }
+
+ showStatus(MODEL_STATUS, 'success', `KNN 模型 "${modelName}" 加载成功!类别: ${classNames.join(', ')}。`);
+ updateModelUI(true);
+
+ } else if (modelData && !binData && !jsonUrl) {
+ showStatus(MODEL_STATUS, 'error', '未知模型加载状态:仅有 JSON 数据,没有 BIN 数据。');
+ updateModelUI(false);
+ }
+
+ } catch (error) {
+ showStatus(MODEL_STATUS, 'error', `加载 KNN 模型失败: ${error.message}`);
+ showStatus(WEBCAM_STATUS_DISPLAY, 'error', '模型加载失败');
+ console.error('加载 KNN 模型总失败:', error);
+ updateModelUI(false);
+ throw error;
+ }
+}
+
+
+async function startWebcam() {
+ if (webcamStream) return;
+
+ if (!knnClassifier || knnClassifier.getNumClasses() === 0) {
+ showStatus(MODEL_STATUS, 'error', '请先加载训练好的模型!');
+ return;
+ }
+
+ try {
+ const stream = await navigator.mediaDevices.getUserMedia({ video: { facingMode: 'user' }, audio: false });
+ VIDEO.srcObject = stream;
+ webcamStream = stream;
+ updateWebcamUI(true);
+
+ VIDEO.onloadeddata = () => {
+ // REMOVED: Serial related state resets
+ isPredicting = true;
+ predictLoop();
+ showStatus(WEBCAM_STATUS_DISPLAY, 'success', `摄像头已运行,识别中...`);
+ PREDICTION_OUTPUT.classList.remove('idle', 'error');
+ };
+
+ } catch (error) {
+ showStatus(MODEL_STATUS, 'error', `无法访问摄像头: ${error.message}`);
+ showStatus(WEBCAM_STATUS_DISPLAY, 'error', '无法启动摄像头');
+ console.error('启动摄像头失败:', error);
+ updateWebcamUI(false);
+ }
+}
+
+function stopWebcam() {
+ if (webcamStream) {
+ webcamStream.getTracks().forEach(track => track.stop());
+ webcamStream = null;
+ }
+ isPredicting = false;
+ VIDEO.srcObject = null;
+ updateWebcamUI(false);
+ showStatus(WEBCAM_STATUS_DISPLAY, 'info', '摄像头已停止');
+
+ // REMOVED: Serial related state resets
+ // If there were any non-serial resource cleanup here, it would be moved.
+}
+
+// REMOVED: Serial confirmation logic variables and functions
+// predictLoop will be simplified to just predict and update UI.
+let currentDetectedClassLabel = '等待识别...'; // For displaying the current prediction.
+
+async function predictLoop() {
+ if (!isPredicting) return;
+
+ if (VIDEO.readyState === 4 && VIDEO.videoWidth > 0 && VIDEO.videoHeight > 0) {
+ try {
+ const features = await getFeatures(VIDEO);
+ const k = 3;
+
+ if (!knnClassifier || knnClassifier.getNumClasses() === 0) {
+ features.dispose();
+ PREDICTION_OUTPUT.textContent = 'KNN 分类器未就绪或无数据。';
+ PREDICTION_OUTPUT.classList.add('error');
+ currentDetectedClassLabel = '模型未就绪';
+ } else {
+ const prediction = await knnClassifier.predictClass(features, k);
+ features.dispose();
+
+ if (prediction && prediction.confidences) {
+ let maxConfidence = 0;
+ let predictedClassIndex = -1;
+
+ const confidencesArray = Object.entries(prediction.confidences).map(([key, value]) => ({ index: parseInt(key), confidence: value }));
+
+ confidencesArray.forEach(({ index, confidence }) => {
+ if (confidence > maxConfidence) {
+ maxConfidence = confidence;
+ predictedClassIndex = index;
+ }
+ });
+
+ const confidenceThreshold = 0.75;
+ if (predictedClassIndex !== -1 && maxConfidence > confidenceThreshold) {
+ const className = classNames[predictedClassIndex] || `Class ${predictedClassIndex + 1}`;
+ const percentage = (maxConfidence * 100).toFixed(1);
+ PREDICTION_OUTPUT.textContent = `识别为: ${className} (${percentage}%)`;
+ PREDICTION_OUTPUT.classList.remove('idle', 'error');
+ currentDetectedClassLabel = className;
+
+ // Original logic had commandCandidate = '1', '2', '0' etc.
+ // Since serial is removed, this part is now purely for UI display.
+ } else {
+ PREDICTION_OUTPUT.textContent = `未知或不确定... (最高置信度: ${(maxConfidence * 100).toFixed(1)}%)`;
+ PREDICTION_OUTPUT.classList.add('idle');
+ currentDetectedClassLabel = '未知或不确定';
+ }
+ } else {
+ PREDICTION_OUTPUT.textContent = '无法识别。';
+ PREDICTION_OUTPUT.classList.add('error');
+ currentDetectedClassLabel = '无法识别';
+ }
+ }
+ } catch (error) {
+ console.error('预测错误:', error);
+ PREDICTION_OUTPUT.textContent = `预测错误: ${error.message}`;
+ PREDICTION_OUTPUT.classList.add('error');
+ currentDetectedClassLabel = `错误: ${error.message}`;
+ }
+ }
+ requestAnimationFrame(predictLoop);
+}
+
+
+// REMOVED: Web Serial API Logic and Event Listeners
+/*
+async function checkWebSerialCompatibility() { ... }
+async function connectSerial() { ... }
+async function disconnectSerial() { ... }
+async function sendToSerialPort(command) { ... }
+CONNECT_SERIAL_BTN.addEventListener('click', connectSerial);
+DISCONNECT_SERIAL_BTN.addEventListener('click', disconnectSerial);
+*/
+
+// ===================================
+// Event Listeners (Simplified)
+// ===================================
+LOAD_MODEL_BTN.addEventListener('click', () => loadKNNModel(null, null));
+START_WEBCAM_BTN.addEventListener('click', startWebcam);
+STOP_WEBCAM_BTN.addEventListener('click', stopWebcam);
+
+// ===================================
+// Initialization (Simplified)
+// ===================================
+document.addEventListener('DOMContentLoaded', () => {
+ // REMOVED: checkWebSerialCompatibility();
+ initModel();
+});
+
+// Added cleanup for TensorFlow.js on window close/reload
+window.onbeforeunload = () => {
+ if (animationFrameId) {
+ cancelAnimationFrame(animationFrameId);
+ }
+ if (mobilenet) {
+ // mobilenet.dispose(); // MobileNet is part of TF.js, tf.disposeAll() handles it
+ }
+ if (knnClassifier) {
+ knnClassifier.clearAllClasses();
+ }
+ tf.disposeAll();
+ console.log('Resources cleaned up (tf.disposeAll()).');
+};
diff --git a/game/石头剪刀布/game.html b/game/石头剪刀布/game.html
index 8b1a365..ef722c6 100644
--- a/game/石头剪刀布/game.html
+++ b/game/石头剪刀布/game.html
@@ -3,9 +3,8 @@
-
AI剪刀石头布
+
Goood Space - AI 石头剪刀布
-
+
+
-
-
-
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
手势分类映射
-
-
分类0 (未加载) → 石头 ✊
-
分类1 (未加载) → 剪刀 ✌️
-
分类2 (未加载) → 布 ✋
-
-
-
-
-
胜利: 0
-
失败: 0
-
平局: 0
-
-
-
-
-
-
-
-
-
-
-
-
+
-
-
-
-
-
+
+
-
-
+
+
+
+ 你的得分
+ 0
+
+
+ AI得分
+ 0
+
+
-
-
-
WIN
-
+
+
准备开始游戏
+
+
+ 你的选择
+ -
+
+
+ AI的选择
+ -
+
+
+
+
+
+
+
+
+
+
+
+
+
+
游戏说明
+
+ - ✊ 石头 - 握拳手势
+ - ✋ 布 - 张开手掌
+ - ✌️ 剪刀 - 比V手势
+ - 确保手部在摄像头画面中清晰可见
+ - 系统将自动识别您的手势并与AI对战
+
+
+
-
+