看到手就亮灯 防夹手视觉光栅
看到手就亮灯 防夹手视觉光栅
# 修改后的 predict_server.py 以支持 FRP
from ultralytics import YOLO
import cv2
import numpy as np
from flask import Flask, request, jsonify, Response
import base64
import json
import logging
from datetime import datetime, timedelta
import threading
import time# 配置日志
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)app = Flask(__name__)# 全局变量用于存储最新的检测信息
detection_info_global = {'detection_count': 0,'class_names': [],'last_updated': None
}# 记录最后访问时间
last_access_time = datetime.now()
# 设置超时时间(秒)
TIMEOUT_SECONDS = 30# 初始化模型
try:model = YOLO('yolov12n.pt') # 修正模型名称logger.info("YOLO模型加载成功")
except Exception as e:logger.error(f"模型加载失败: {e}")raisedef base64_to_image(base64_string):"""将base64字符串转换为OpenCV图像"""try:# 移除base64字符串中的前缀(如果存在)if base64_string.startswith('data:image'):base64_string = base64_string.split(',')[1]# 解码base64字符串image_data = base64.b64decode(base64_string)# 转换为numpy数组np_array = np.frombuffer(image_data, np.uint8)# 解码为OpenCV图像image = cv2.imdecode(np_array, cv2.IMREAD_COLOR)return imageexcept Exception as e:logger.error(f"图像解码失败: {e}")return Nonedef image_to_base64(image):"""将OpenCV图像转换为base64字符串"""try:_, buffer = cv2.imencode('.jpg', image, [int(cv2.IMWRITE_JPEG_QUALITY), 80])jpg_as_text = base64.b64encode(buffer).decode('utf-8')return jpg_as_textexcept Exception as e:logger.error(f"图像编码失败: {e}")return Nonedef reset_detection_info_if_timeout():"""定期检查并重置检测信息"""global detection_info_global, last_access_timewhile True:time.sleep(5) # 每5秒检查一次if datetime.now() - last_access_time > timedelta(seconds=TIMEOUT_SECONDS):# 如果超过30秒没有访问,重置检测信息detection_info_global = {'detection_count': 0,'class_names': [],'last_updated': datetime.now().isoformat()}logger.info("检测信息已重置为默认值")# 启动后台线程来检查超时
timeout_thread = threading.Thread(target=reset_detection_info_if_timeout, daemon=True)
timeout_thread.start()def update_access_time():"""更新最后访问时间"""global last_access_timelast_access_time = datetime.now()@app.route('/detect', methods=['POST'])
def detect_hand():"""处理从前端传来的图像并进行目标检测"""update_access_time() # 更新访问时间start_time = datetime.now()try:# 获取JSON数据data = request.get_json()if not data:return jsonify({'error': '无效的JSON数据'}), 400if 'image' not in data:return jsonify({'error': '缺少图像数据'}), 400# 将base64图像转换为OpenCV格式image = base64_to_image(data['image'])if image is None:return jsonify({'error': '无法解码图像'}), 400# 进行目标检测results = model(image)# 获取检测结果detection_data = []processed_image = image.copy()# 用于存储检测到的类名class_names = []for result in results:# 绘制检测框processed_image = result.plot()# 提取检测信息boxes = result.boxesif boxes is not None:for box in boxes:# 获取类别名称class_id = int(box.cls.item()) if box.cls is not None else -1class_name = model.names[class_id] if class_id >= 0 and class_id < len(model.names) else "Unknown"class_names.append(class_name)detection_info = {'class': class_id,'class_name': class_name,'confidence': float(box.conf.item()) if box.conf is not None else 0.0,'bbox': box.xyxy.cpu().numpy().tolist()[0] if box.xyxy is not None else []}detection_data.append(detection_info)# 更新全局检测信息global detection_info_globaldetection_info_global = {'detection_count': len(detection_data),'class_names': class_names,'last_updated': datetime.now().isoformat()}# 将处理后的图像转换为base64processed_image_base64 = image_to_base64(processed_image)if processed_image_base64 is None:return jsonify({'error': '图像处理失败'}), 500processing_time = (datetime.now() - start_time).total_seconds()# 返回结果response_data = {'processed_image': processed_image_base64,'detections': detection_data,'status': 'success','processing_time': processing_time,'detection_count': len(detection_data)}logger.info(f"检测完成: 发现 {len(detection_data)} 个目标,处理时间: {processing_time:.2f}秒")return jsonify(response_data)except Exception as e:logger.error(f"检测过程中出错: {e}")return jsonify({'error': str(e)}), 500@app.route('/detection-info', methods=['GET'])
def get_detection_info():"""获取最新的检测信息(目标数和名称)"""return jsonify(detection_info_global)@app.route('/health', methods=['GET'])
def health_check():"""健康检查端点"""return jsonify({'status': 'healthy', 'message': 'YOLO服务正在运行','timestamp': datetime.now().isoformat()})@app.route('/info', methods=['GET'])
def service_info():"""服务信息端点"""return jsonify({'service': 'YOLO目标检测服务','version': '1.0','model': 'yolov12n','endpoints': {'/detect': 'POST - 图像目标检测','/detection-info': 'GET - 获取检测信息(目标数和名称)','/health': 'GET - 服务健康检查','/info': 'GET - 服务信息'}})@app.route('/', methods=['GET'])
def home():"""主页"""return jsonify({'message': 'YOLO目标检测服务已启动','documentation': '/info','health_check': '/health'})# 添加CORS支持(如果需要)
@app.after_request
def after_request(response):response.headers.add('Access-Control-Allow-Origin', '*')response.headers.add('Access-Control-Allow-Headers', 'Content-Type')response.headers.add('Access-Control-Allow-Methods', 'GET,PUT,POST,DELETE,OPTIONS')return responseif __name__ == '__main__':print("=" * 50)print("YOLO目标检测服务启动中...")print("模型加载完成")print("服务配置:")print(" 端口: 5000")print(" 主机: 0.0.0.0 (支持外部访问)")print(" 超时设置: 30秒无访问后自动归零")print(" 可用端点:")print(" POST /detect - 图像目标检测")print(" GET /detection-info - 获取检测信息(目标数和名称)")print(" GET /health - 健康检查")print(" GET /info - 服务信息")print(" GET / - 主页")print("=" * 50)# 确保绑定到 0.0.0.0 而不是 127.0.0.1app.run(host='0.0.0.0', port=5000, debug=False)
#include <WiFi.h>
#include <HTTPClient.h>const int LED_PIN = 22;// Wi-Fi 配置
const char* ssid = "外街路5号";
const char* password = "12345678";// 检测信息服务地址
const char* detectionInfoUrl = "http://ns3.llmfindworksnjsgcs.fwh.is/detection-info";void setup() {// 初始化串口,115200 波特率Serial.begin(115200);while (!Serial) {delay(10); // 等待串口监视器打开(某些板子需要)}Serial.println(F("【ESP32 启动中】================================="));Serial.printf("Wi-Fi 名称: %s\n", ssid);Serial.println(F("正在尝试连接 Wi-Fi..."));// 设置 LED 引脚pinMode(LED_PIN, OUTPUT);digitalWrite(LED_PIN, HIGH); // 熄灭(低电平点亮,所以 HIGH 是灭)// 开始连接 Wi-FiWiFi.begin(ssid, password);int retry = 0;while (WiFi.status() != WL_CONNECTED) {delay(250);digitalWrite(LED_PIN, !digitalRead(LED_PIN)); // 快闪表示正在连Serial.print(F(".")); // 打印点,表示还在努力retry++;if (retry % 20 == 0) {Serial.printf(F(" 已尝试 %d 秒...\n"), retry / 4);}}// 连接成功digitalWrite(LED_PIN, HIGH); // 熄灭delay(100);digitalWrite(LED_PIN, LOW); // 闪一下表示成功delay(200);digitalWrite(LED_PIN, HIGH);Serial.println(F("\n【恭喜!Wi-Fi 连接成功】========================"));Serial.printf("IP 地址: %s\n", WiFi.localIP().toString().c_str());Serial.printf("信号强度: %d dBm\n", WiFi.RSSI());Serial.printf("网关: %s\n", WiFi.gatewayIP().toString().c_str());Serial.println(F("开始检测信息服务请求测试..."));
}void loop() {// 检查 Wi-Fi 状态if (WiFi.status() != WL_CONNECTED) {Serial.println(F("❌ Wi-Fi 已断开,正在尝试重连..."));WiFi.reconnect();delay(2000);return;}Serial.println(F("\n--- 开始请求检测信息 ---"));HTTPClient http;http.begin(detectionInfoUrl);http.setUserAgent("ESP32-Detection-Client/1.0"); // 设置 User-AgentSerial.printf("📡 正在请求: %s\n", detectionInfoUrl);int httpCode = http.GET();if (httpCode > 0) {Serial.printf("✅ HTTP 请求成功!状态码: %d\n", httpCode);if (httpCode == HTTP_CODE_OK) {String payload = http.getString();Serial.println(F("📄 响应内容(前 200 字符):"));Serial.println(payload.substring(0, min(200, (int)payload.length())));// 检查返回内容中是否包含"person"if (payload.indexOf("person") >= 0) {Serial.println(F("✅ 检测到 person,LED 闪烁两下"));// LED 闪烁两下for (int i = 0; i < 2; i++) {digitalWrite(LED_PIN, LOW);delay(200);digitalWrite(LED_PIN, HIGH);delay(200);}} else {Serial.println(F("❌ 未检测到 person"));}}} else {Serial.printf("❌ HTTP 请求失败!错误码: %s\n", http.errorToString(httpCode).c_str());Serial.printf("⚠️ 可能原因:服务器无响应、DNS 失败、超时、Wi-Fi 不稳\n");// 蓝灯慢闪表示失败digitalWrite(LED_PIN, LOW);delay(100);digitalWrite(LED_PIN, HIGH);delay(900);}http.end(); // 释放资源Serial.println(F("--- 检测信息服务请求结束 ---"));delay(2000); // 每 5 秒请求一次
}
<!-- camera_solve.html -->
<!DOCTYPE html>
<html lang="zh-CN">
<head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>摄像机处理</title><style>body {font-family: Arial, sans-serif;max-width: 1000px;margin: 0 auto;padding: 20px;background-color: #f5f5f5;}.container {background-color: white;border-radius: 10px;padding: 30px;box-shadow: 0 2px 10px rgba(0,0,0,0.1);text-align: center;}h1 {color: #333;margin-bottom: 30px;}.video-container {margin: 20px auto;width: 100%;max-width: 640px;}#videoElement {width: 100%;max-width: 640px;height: auto;border: 2px solid #ddd;border-radius: 8px;background-color: #000;}.controls {margin: 20px 0;}button {background-color: #007bff;color: white;padding: 12px 24px;border: none;border-radius: 5px;cursor: pointer;font-size: 16px;margin: 0 10px;}button:hover {background-color: #0056b3;}button:disabled {background-color: #cccccc;cursor: not-allowed;}.status {margin: 15px 0;padding: 10px;border-radius: 5px;font-weight: bold;}.error {background-color: #f8d7da;color: #721c24;border: 1px solid #f5c6cb;}.success {background-color: #d4edda;color: #155724;border: 1px solid #c3e6cb;}.info {background-color: #d1ecf1;color: #0c5460;border: 1px solid #bee5eb;}.snapshot-container {margin: 20px auto;text-align: center;}#snapshotCanvas {max-width: 100%;border: 2px solid #ddd;border-radius: 8px;display: none;}.back-link {display: inline-block;margin-top: 20px;padding: 10px 20px;background-color: #6c757d;color: white;text-decoration: none;border-radius: 5px;}.back-link:hover {background-color: #5a6268;}.camera-selector {margin: 15px 0;}select {padding: 10px;font-size: 16px;border-radius: 5px;border: 1px solid #ddd;margin: 0 10px;}.facing-mode-selector {margin: 15px 0;}/* 添加加载动画样式 */.loading {display: inline-block;width: 20px;height: 20px;border: 3px solid #f3f3f3;border-top: 3px solid #007bff;border-radius: 50%;animation: spin 1s linear infinite;margin-right: 10px;vertical-align: middle;}@keyframes spin {0% { transform: rotate(0deg); }100% { transform: rotate(360deg); }}</style>
</head>
<body><div style="margin-bottom: 20px;"><a href="index.html" style="display: inline-block; padding: 10px 15px; background-color: #6c757d; color: white; text-decoration: none; border-radius: 5px;">← 返回主页</a></div><div class="container"><h1>摄像机处理</h1><div class="video-container"><video id="videoElement" autoplay playsinline muted></video></div><div class="facing-mode-selector"><label><input type="radio" name="facingMode" value="user" checked> 前置摄像头</label><label style="margin-left: 20px;"><input type="radio" name="facingMode" value="environment"> 后置摄像头</label></div><div class="camera-selector"><label for="cameraSelect">选择具体摄像头:</label><select id="cameraSelect" disabled><option value="">自动选择</option></select></div><div class="controls"><button id="startCameraBtn">启动摄像机</button><button id="stopCameraBtn" disabled>停止摄像机</button><button id="snapshotBtn" disabled>拍照</button><button id="detectBtn" disabled>发送检测</button><button id="realTimeBtn" disabled>开始实时识别</button></div><div id="statusMessage" class="status info">点击"启动摄像机"按钮开始使用摄像头</div><div class="snapshot-container"><canvas id="snapshotCanvas"></canvas></div><a href="index.html" class="back-link">返回主页</a></div><script>// 获取页面元素const videoElement = document.getElementById('videoElement');const startCameraBtn = document.getElementById('startCameraBtn');const stopCameraBtn = document.getElementById('stopCameraBtn');const snapshotBtn = document.getElementById('snapshotBtn');const statusMessage = document.getElementById('statusMessage');const snapshotCanvas = document.getElementById('snapshotCanvas');const canvasContext = snapshotCanvas.getContext('2d');const cameraSelect = document.getElementById('cameraSelect');const facingModeRadios = document.getElementsByName('facingMode');const detectBtn = document.getElementById('detectBtn');const realTimeBtn = document.getElementById('realTimeBtn');// 存储媒体流对象和设备列表let mediaStream = null;let availableCameras = [];let isRealTimeDetection = false;let realTimeDetectionInterval = null;const detectionInterval = 1500; let backendUrl = null;// 更新状态消息function updateStatus(message, type = 'info') {// 如果是处理中的状态,添加加载动画if (type === 'info' && message.includes('正在处理')) {statusMessage.innerHTML = '<div class="loading"></div>' + message;} else {statusMessage.textContent = message;}statusMessage.className = `status ${type}`;}async function initializeApp() {try {// 使用你提供的 FRP 地址let ipaddress = "ns3.llmfindworksnjsgcs.fwh.is";// 构建后端检测URLbackendUrl = `http://${ipaddress}/detect`;const healthUrl = `http://${ipaddress}/health`;// 测试健康检查端点(使用代理方式避免混合内容问题)try {console.log('正在测试健康检查端点:', healthUrl);// 创建代理请求来避免混合内容问题const proxyResponse = await fetch('./cameraapi/proxy_api.php', {method: 'POST',headers: {'Content-Type': 'application/json'},body: JSON.stringify({url: healthUrl,method: 'GET'})});const proxyResult = await proxyResponse.json();console.log('代理完整响应:', proxyResult);if (proxyResult.success) {const healthData = JSON.parse(proxyResult.data);console.log('健康检查成功:', healthData);updateStatus('健康检查通过,后端服务正常运行', 'success');} else {console.warn('健康检查失败:', proxyResult.error);updateStatus('后端服务可能不可用: ' + proxyResult.error, 'info');// 显示更多错误信息if (proxyResult.url) {console.log('请求的URL:', proxyResult.url);}if (proxyResult.httpCode) {console.log('HTTP状态码:', proxyResult.httpCode);}}} catch (healthError) {console.error('健康检查失败:', healthError);updateStatus('无法连接到后端服务: ' + healthError.message, 'error');}return backendUrl;} catch (error) {console.error('获取配置失败:', error);updateStatus('获取后端配置失败: ' + error.message, 'error');return null;}}// 压缩图像函数function compressImage(canvas, maxWidth, quality) {const width = canvas.width;const height = canvas.height;if (width <= maxWidth) {// 如果图像宽度已经小于等于最大宽度,直接压缩质量return canvas.toDataURL('image/jpeg', quality);}// 创建临时画布用于缩放图像const tempCanvas = document.createElement('canvas');const tempCtx = tempCanvas.getContext('2d');// 计算新的尺寸const scale = maxWidth / width;tempCanvas.width = maxWidth;tempCanvas.height = height * scale;// 绘制缩放后的图像tempCtx.drawImage(canvas, 0, 0, tempCanvas.width, tempCanvas.height);// 返回压缩后的图像数据return tempCanvas.toDataURL('image/jpeg', quality);}async function sendToBackendForDetection() {if (!mediaStream) {updateStatus('请先启动摄像头', 'error');return;}try {// 拍照takeSnapshot();const imageData = compressImage(snapshotCanvas, 640, 0.8);// 发送到后端进行检测updateStatus('正在处理图像...', 'info');// 使用代理发送请求避免混合内容问题const proxyResponse = await fetch('./cameraapi/proxy_api.php', {method: 'POST',headers: {'Content-Type': 'application/json'},body: JSON.stringify({url: backendUrl,method: 'POST',body: JSON.stringify({ image: imageData })})});const proxyResult = await proxyResponse.json();if (!proxyResult.success) {throw new Error(proxyResult.error || 'Proxy request failed');}const result = JSON.parse(proxyResult.data);if (result.status === 'success') {// 显示处理后的图像const processedImage = new Image();processedImage.onload = function() {// 固定画布尺寸以避免忽大忽小const displayWidth = 640;const scale = displayWidth / processedImage.width;const displayHeight = processedImage.height * scale;snapshotCanvas.width = displayWidth;snapshotCanvas.height = displayHeight;// 在画布上显示处理后的图像canvasContext.clearRect(0, 0, snapshotCanvas.width, snapshotCanvas.height);canvasContext.drawImage(processedImage, 0, 0, snapshotCanvas.width, snapshotCanvas.height);// 显示画布snapshotCanvas.style.display = 'block';updateStatus(`检测完成,发现 ${result.detections.length} 个目标`, 'success');// 显示检测信息if (result.detections.length > 0) {console.log('检测结果:', result.detections);}};processedImage.src = 'data:image/jpeg;base64,' + result.processed_image;} else {updateStatus('检测失败: ' + result.error, 'error');}} catch (error) {console.error('处理图像时出错:', error);updateStatus('处理图像时出错: ' + error.message, 'error');}}async function getAvailableCameras() {try {// 获取媒体设备列表const devices = await navigator.mediaDevices.enumerateDevices();// 过滤出视频输入设备availableCameras = devices.filter(device => device.kind === 'videoinput');// 清空选择器cameraSelect.innerHTML = '<option value="">自动选择</option>';// 添加摄像头选项availableCameras.forEach((camera, index) => {const option = document.createElement('option');option.value = camera.deviceId;// 如果有标签名则使用标签名,否则使用默认名称option.text = camera.label || `摄像头 ${index + 1}`;cameraSelect.appendChild(option);});// 启用选择器cameraSelect.disabled = false;return availableCameras;} catch (error) {console.error('获取摄像头列表失败:', error);updateStatus('获取摄像头列表失败: ' + error.message, 'error');return [];}}// 启动指定摄像头 - 使用更简单的实现方式async function startCamera(options = {}) {try {// 更新状态updateStatus('正在请求访问摄像头...', 'info');// 如果当前正在使用摄像头,先停止if (mediaStream) {const tracks = mediaStream.getTracks();tracks.forEach(track => track.stop());}// 构建约束条件 - 使用更简单的实现let constraints = { video: true, audio: false };// 如果指定了摄像头ID,则使用具体设备if (options.deviceId) {constraints = {video: { deviceId: { exact: options.deviceId } },audio: false};} // 否则使用 facingModeelse if (options.facingMode) {constraints = {video: { facingMode: options.facingMode },audio: false};}// 获取用户媒体设备权限mediaStream = await navigator.mediaDevices.getUserMedia(constraints);// 将视频流设置为视频元素的源videoElement.srcObject = mediaStream;// 确保视频开始播放videoElement.play().catch(e => console.error("视频播放失败:", e));// 更新按钮状态startCameraBtn.disabled = true;stopCameraBtn.disabled = false;snapshotBtn.disabled = false;cameraSelect.disabled = false;detectBtn.disabled = false;realTimeBtn.disabled = false;// 更新状态消息updateStatus('摄像头已启动,正在显示视频流', 'success');} catch (error) {console.error('访问摄像头时出错:', error);updateStatus(`无法访问摄像头: ${error.message}`, 'error');startCameraBtn.disabled = false;stopCameraBtn.disabled = true;snapshotBtn.disabled = true;detectBtn.disabled = true;realTimeBtn.disabled = true;}}// 停止摄像机function stopCamera() {if (mediaStream) {// 停止所有媒体轨道const tracks = mediaStream.getTracks();tracks.forEach(track => track.stop());// 清除视频元素的源videoElement.srcObject = null;// 更新按钮状态startCameraBtn.disabled = false;stopCameraBtn.disabled = true;snapshotBtn.disabled = true;detectBtn.disabled = true;realTimeBtn.disabled = true;// 更新状态消息updateStatus('摄像头已停止', 'info');// 如果正在实时识别,则停止if (isRealTimeDetection) {toggleRealTimeDetection();}}}// 拍照功能function takeSnapshot() {if (!mediaStream) return;// 设置画布尺寸与视频相同snapshotCanvas.width = videoElement.videoWidth;snapshotCanvas.height = videoElement.videoHeight;// 将当前视频帧绘制到画布上canvasContext.drawImage(videoElement, 0, 0, snapshotCanvas.width, snapshotCanvas.height);// 显示画布snapshotCanvas.style.display = 'block';// 更新状态消息updateStatus('拍照成功!', 'success');}// 切换实时识别状态function toggleRealTimeDetection() {if (!isRealTimeDetection) {// 开始实时识别isRealTimeDetection = true;realTimeBtn.textContent = '停止实时识别';updateStatus('开始实时识别...', 'info');// 设置定时器,定期发送检测请求realTimeDetectionInterval = setInterval(sendFrameForDetection, detectionInterval);} else {// 停止实时识别isRealTimeDetection = false;realTimeBtn.textContent = '开始实时识别';updateStatus('已停止实时识别', 'info');// 清除定时器if (realTimeDetectionInterval) {clearInterval(realTimeDetectionInterval);realTimeDetectionInterval = null;}}}// 发送当前帧进行检测 - 添加性能计时和图像压缩async function sendFrameForDetection() {if (!mediaStream) {updateStatus('请先启动摄像头', 'error');toggleRealTimeDetection(); // 停止实时识别return;}// 开始计时const startTime = performance.now();try {// 拍照获取当前帧takeSnapshot();// 压缩图像(最大宽度320px,质量0.6)const imageData = compressImage(snapshotCanvas, 320, 0.6);// 发送到后端进行检测updateStatus('正在处理图像...', 'info');// 使用代理发送请求避免混合内容问题const proxyResponse = await fetch('./cameraapi/proxy_api.php', {method: 'POST',headers: {'Content-Type': 'application/json'},body: JSON.stringify({url: backendUrl,method: 'POST',body: JSON.stringify({ image: imageData })})});// 计算请求发送时间const requestTime = performance.now() - startTime;console.log(`请求发送耗时: ${requestTime.toFixed(2)}ms`);const proxyResult = await proxyResponse.json();// 计算总耗时const totalTime = performance.now() - startTime;console.log(`总处理耗时: ${totalTime.toFixed(2)}ms`);if (!proxyResult.success) {throw new Error(proxyResult.error || 'Proxy request failed');}const result = JSON.parse(proxyResult.data);if (result.status === 'success') {// 显示处理后的图像const processedImage = new Image();processedImage.onload = function() {// 固定画布尺寸以避免忽大忽小const displayWidth = 640;const scale = displayWidth / processedImage.width;const displayHeight = processedImage.height * scale;snapshotCanvas.width = displayWidth;snapshotCanvas.height = displayHeight;// 在画布上显示处理后的图像canvasContext.clearRect(0, 0, snapshotCanvas.width, snapshotCanvas.height);canvasContext.drawImage(processedImage, 0, 0, snapshotCanvas.width, snapshotCanvas.height);// 显示画布snapshotCanvas.style.display = 'block';updateStatus(`实时检测完成,发现 ${result.detections.length} 个目标 | 耗时: ${totalTime.toFixed(0)}ms`, 'success');// 显示检测信息if (result.detections.length > 0) {console.log('检测结果:', result.detections);}};processedImage.src = 'data:image/jpeg;base64,' + result.processed_image;} else {updateStatus('检测失败: ' + result.error, 'error');}} catch (error) {const totalTime = performance.now() - startTime;console.error('处理图像时出错:', error);updateStatus('处理图像时出错: ' + error.message + ` | 耗时: ${totalTime.toFixed(0)}ms`, 'error');// 出错时停止实时识别toggleRealTimeDetection();}}// 页面加载完成后绑定事件document.addEventListener('DOMContentLoaded', async function() {await initializeApp();// 检查浏览器是否支持媒体设备APIif (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) {updateStatus('您的浏览器不支持访问媒体设备,请更换浏览器重试', 'error');startCameraBtn.disabled = true;return;}// 绑定按钮事件startCameraBtn.addEventListener('click', () => {const selectedCameraId = cameraSelect.value;const facingMode = document.querySelector('input[name="facingMode"]:checked').value;// 如果选择了具体摄像头,则使用该摄像头if (selectedCameraId) {startCamera({ deviceId: selectedCameraId });} else {// 否则使用 facingModestartCamera({ facingMode: facingMode });}});stopCameraBtn.addEventListener('click', stopCamera);snapshotBtn.addEventListener('click', takeSnapshot);detectBtn.addEventListener('click', sendToBackendForDetection);realTimeBtn.addEventListener('click', toggleRealTimeDetection);// 摄像头选择事件cameraSelect.addEventListener('change', async function() {if (this.value) {// 启动选中的摄像头await startCamera({ deviceId: this.value });}});// facingMode 选择事件facingModeRadios.forEach(radio => {radio.addEventListener('change', function() {// 如果当前正在使用摄像头,则切换if (mediaStream) {const selectedCameraId = cameraSelect.value;const facingMode = document.querySelector('input[name="facingMode"]:checked').value;if (selectedCameraId) {startCamera({ deviceId: selectedCameraId });} else {startCamera({ facingMode: facingMode });}}});});// 获取可用摄像头列表getAvailableCameras();});</script>
</body>
</html>