diff --git a/src/yolo测试代码/README.md b/src/yolo测试代码/README.md new file mode 100644 index 0000000..117cedc --- /dev/null +++ b/src/yolo测试代码/README.md @@ -0,0 +1,75 @@ +# 无人机识别系统 - Web版 + +这是一个基于YOLO模型的无人机识别系统的Web应用版本,提供了简单的网页界面来进行无人机目标检测。 + +## 功能特点 + +- 网页界面操作,支持跨平台访问 +- 支持图片上传和识别 +- 支持实时摄像头识别 +- 显示识别结果和置信度 +- 可视化检测框标注 + +## 使用方法 + +1. 确保已安装所需依赖: +```bash +pip install -r requirements.txt +``` + +2. 运行服务器: +```bash +python app.py +``` + +3. 打开浏览器访问: +``` +http://localhost:5000 +``` + +4. 在网页界面中: + - 点击"选择图片"按钮上传本地图片进行识别 + - 点击"打开摄像头"按钮进行实时识别 + +## 系统要求 + +- Python 3.8+ +- CUDA支持(推荐) +- 现代浏览器(Chrome、Firefox、Edge等) + +## 项目结构 + +``` +├── app.py # Flask应用主程序 +├── requirements.txt # 项目依赖 +├── static/ # 静态文件目录 +│ ├── css/ # 样式文件 +│ └── js/ # JavaScript文件 +├── templates/ # HTML模板目录 +├── utils/ # 工具函数 +│ └── detector.py # 检测器类 +``` + +## API接口 + +### POST /api/detect +上传图片进行检测 + +请求: +- Content-Type: multipart/form-data +- 参数:file(图片文件) + +响应: +```json +{ + "success": true, + "detections": [ + { + "bbox": [x1, y1, x2, y2], + "confidence": 0.95, + "class_name": "drone" + } + ], + "image_url": "/static/results/result_123.jpg" +} +``` \ No newline at end of file diff --git a/src/yolo测试代码/app.log b/src/yolo测试代码/app.log new file mode 100644 index 0000000..e69de29 diff --git a/src/yolo测试代码/app.py b/src/yolo测试代码/app.py new file mode 100644 index 0000000..ce7aff9 --- /dev/null +++ b/src/yolo测试代码/app.py @@ -0,0 +1,221 @@ +import os +from flask import Flask, request, jsonify, render_template, send_from_directory +from werkzeug.utils import secure_filename +import cv2 +import numpy as np +from utils.detector import DroneDetector +import base64 +from datetime import datetime +import logging +import traceback +from flask_cors import CORS +import time +import random + +# 配置日志 +logging.basicConfig( + level=logging.INFO, + format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', + handlers=[ + logging.FileHandler('app.log'), + logging.StreamHandler() + ] +) +logger = logging.getLogger(__name__) + +app = Flask(__name__) +# 启用CORS +CORS(app) + +# 配置 +app.config['UPLOAD_FOLDER'] = 'static/uploads' +app.config['RESULTS_FOLDER'] = 'static/results' +app.config['MAX_CONTENT_LENGTH'] = 16 * 1024 * 1024 # 16MB最大上传限制 +app.config['SEND_FILE_MAX_AGE_DEFAULT'] = 0 # 禁用缓存 + +# 确保上传和结果目录存在 +os.makedirs(app.config['UPLOAD_FOLDER'], exist_ok=True) +os.makedirs(app.config['RESULTS_FOLDER'], exist_ok=True) + +# 初始化检测器 +try: + detector = DroneDetector("D:/drone2/weights/best.pt") +except Exception as e: + logger.error(f"初始化检测器失败:{str(e)}") + raise + +def allowed_file(filename): + """检查文件扩展名是否允许""" + return '.' in filename and filename.rsplit('.', 1)[1].lower() in {'png', 'jpg', 'jpeg', 'gif'} + +def clean_old_files(): + """清理旧的上传和结果文件""" + try: + current_time = time.time() + # 清理超过1小时的文件 + for folder in [app.config['UPLOAD_FOLDER'], app.config['RESULTS_FOLDER']]: + for filename in os.listdir(folder): + filepath = os.path.join(folder, filename) + if os.path.isfile(filepath): + if current_time - os.path.getmtime(filepath) > 3600: # 1小时 + os.remove(filepath) + except Exception as e: + logger.error(f"清理文件时出错:{str(e)}") + +@app.before_request +def before_request(): + """请求预处理""" + # 定期清理文件 + if random.random() < 0.1: # 10%的概率执行清理 + clean_old_files() + +@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, POST, OPTIONS') + response.headers.add('Cache-Control', 'no-store, no-cache, must-revalidate, max-age=0') + return response + +@app.route('/') +def index(): + """渲染主页""" + try: + return render_template('index.html') + except Exception as e: + logger.error(f"渲染主页时出错:{str(e)}") + return jsonify({'success': False, 'error': '服务器内部错误'}), 500 + +@app.route('/api/detect', methods=['POST']) +def detect(): + """处理图片检测请求""" + try: + if 'file' not in request.files: + return jsonify({'success': False, 'error': '没有文件上传'}), 400 + + file = request.files['file'] + if file.filename == '': + return jsonify({'success': False, 'error': '没有选择文件'}), 400 + + if not allowed_file(file.filename): + return jsonify({'success': False, 'error': '不支持的文件类型'}), 400 + + # 保存上传的文件 + filename = secure_filename(file.filename) + timestamp = datetime.now().strftime('%Y%m%d_%H%M%S') + filename = f"{timestamp}_{filename}" + filepath = os.path.join(app.config['UPLOAD_FOLDER'], filename) + + try: + file.save(filepath) + except Exception as e: + logger.error(f"保存文件时出错:{str(e)}") + return jsonify({'success': False, 'error': '保存文件失败'}), 500 + + logger.info(f"接收到文件:{filename}") + + # 读取图片 + try: + image = cv2.imread(filepath) + if image is None: + raise Exception("无法读取图片") + except Exception as e: + logger.error(f"读取图片时出错:{str(e)}") + return jsonify({'success': False, 'error': '读取图片失败'}), 500 + + # 执行检测 + try: + processed_image, detections = detector.detect_image(image) + except Exception as e: + logger.error(f"执行检测时出错:{str(e)}") + return jsonify({'success': False, 'error': '执行检测失败'}), 500 + + # 保存结果图片 + try: + result_filename = f"result_{filename}" + result_filepath = os.path.join(app.config['RESULTS_FOLDER'], result_filename) + cv2.imwrite(result_filepath, processed_image) + except Exception as e: + logger.error(f"保存结果图片时出错:{str(e)}") + return jsonify({'success': False, 'error': '保存结果失败'}), 500 + + logger.info(f"检测完成,结果保存为:{result_filename}") + + # 构建响应 + response = { + 'success': True, + 'detections': detections, + 'image_url': f"/static/results/{result_filename}" + } + + return jsonify(response) + + except Exception as e: + logger.error(f"处理检测请求时出错:{str(e)}\n{traceback.format_exc()}") + return jsonify({ + 'success': False, + 'error': str(e), + 'details': traceback.format_exc() + }), 500 + +@app.route('/api/detect_stream', methods=['POST']) +def detect_stream(): + """处理Base64编码的图片流""" + try: + # 获取Base64编码的图片数据 + data = request.json + if not data or 'image' not in data: + return jsonify({'success': False, 'error': '没有图片数据'}), 400 + + # 解码Base64图片 + try: + image_data = data['image'].split(',')[1] if ',' in data['image'] else data['image'] + image_bytes = base64.b64decode(image_data) + nparr = np.frombuffer(image_bytes, np.uint8) + image = cv2.imdecode(nparr, cv2.IMREAD_COLOR) + except Exception as e: + logger.error(f"解码Base64图片时出错:{str(e)}") + return jsonify({'success': False, 'error': '无法解码图片数据'}), 400 + + if image is None: + return jsonify({'success': False, 'error': '无法解码图片数据'}), 400 + + # 执行检测 + try: + processed_image, detections = detector.detect_image(image) + except Exception as e: + logger.error(f"执行检测时出错:{str(e)}") + return jsonify({'success': False, 'error': '执行检测失败'}), 500 + + # 将处理后的图片编码为Base64 + try: + _, buffer = cv2.imencode('.jpg', processed_image) + processed_image_base64 = base64.b64encode(buffer).decode('utf-8') + except Exception as e: + logger.error(f"编码处理后的图片时出错:{str(e)}") + return jsonify({'success': False, 'error': '无法编码处理后的图片'}), 500 + + # 构建响应 + response = { + 'success': True, + 'detections': detections, + 'image': f"data:image/jpeg;base64,{processed_image_base64}" + } + + return jsonify(response) + + except Exception as e: + logger.error(f"处理视频流时出错:{str(e)}\n{traceback.format_exc()}") + return jsonify({ + 'success': False, + 'error': str(e), + 'details': traceback.format_exc() + }), 500 + +if __name__ == '__main__': + # 添加SSL上下文(如果需要) + # context = ('cert.pem', 'key.pem') + app.run(debug=True, host='0.0.0.0', port=5000) + # app.run(debug=True, host='0.0.0.0', port=5000, ssl_context=context) \ No newline at end of file diff --git a/src/yolo测试代码/requirements.txt b/src/yolo测试代码/requirements.txt new file mode 100644 index 0000000..5d4f953 --- /dev/null +++ b/src/yolo测试代码/requirements.txt @@ -0,0 +1,9 @@ +torch>=2.0.0 +torchvision>=0.15.0 +opencv-python>=4.7.0 +numpy>=1.24.0 +ultralytics>=8.0.0 +Flask>=2.0.0 +Pillow>=9.0.0 +python-dotenv>=0.19.0 +flask-cors>=3.0.10 \ No newline at end of file diff --git a/src/yolo测试代码/static/css/style.css b/src/yolo测试代码/static/css/style.css new file mode 100644 index 0000000..0dfa2ef --- /dev/null +++ b/src/yolo测试代码/static/css/style.css @@ -0,0 +1,337 @@ +/* 全局变量 */ +:root { + --primary-color: #4a90e2; + --secondary-color: #50e3c2; + --background-color: #f8f9fa; + --card-background: #ffffff; + --text-color: #2c3e50; + --border-color: #e9ecef; + --hover-color: #3498db; + --shadow-color: rgba(0, 0, 0, 0.1); + --gradient-start: #4a90e2; + --gradient-end: #50e3c2; +} + +/* 深色模式 */ +[data-theme="dark"] { + --background-color: #1a1a1a; + --card-background: #2d2d2d; + --text-color: #e0e0e0; + --border-color: #404040; + --shadow-color: rgba(0, 0, 0, 0.3); +} + +/* 全局样式 */ +body { + background-color: var(--background-color); + color: var(--text-color); + transition: all 0.3s ease; + min-height: 100vh; + display: flex; + flex-direction: column; +} + +/* 导航栏样式 */ +.navbar { + background: linear-gradient(135deg, var(--gradient-start), var(--gradient-end)); + padding: 1rem 0; + box-shadow: 0 2px 10px var(--shadow-color); +} + +.navbar-brand { + font-size: 1.5rem; + font-weight: 700; + display: flex; + align-items: center; + gap: 0.5rem; +} + +.navbar-brand i { + font-size: 1.8rem; +} + +/* 主容器样式 */ +.main-container { + flex: 1; + padding: 2rem 0; +} + +.content-wrapper { + max-width: 1200px; + margin: 0 auto; +} + +/* 卡片通用样式 */ +.card { + background-color: var(--card-background); + border: 1px solid var(--border-color); + border-radius: 15px; + box-shadow: 0 4px 6px var(--shadow-color); + margin-bottom: 2rem; + transition: transform 0.3s ease, box-shadow 0.3s ease; +} + +.card:hover { + transform: translateY(-5px); + box-shadow: 0 8px 15px var(--shadow-color); +} + +/* 模式选择器样式 */ +.mode-selector { + padding: 1rem; +} + +.btn-mode { + padding: 1rem 2rem; + border: none; + background-color: var(--card-background); + color: var(--text-color); + transition: all 0.3s ease; + display: flex; + align-items: center; + gap: 0.5rem; + font-weight: 500; +} + +.btn-mode i { + font-size: 1.2rem; +} + +.btn-mode.active { + background: linear-gradient(135deg, var(--gradient-start), var(--gradient-end)); + color: white; +} + +/* 操作区域样式 */ +.operation-area { + padding: 2rem; +} + +.upload-zone { + border: 2px dashed var(--border-color); + border-radius: 10px; + padding: 3rem; + text-align: center; + cursor: pointer; + transition: all 0.3s ease; + position: relative; +} + +.upload-zone:hover { + border-color: var(--primary-color); + background-color: rgba(74, 144, 226, 0.05); +} + +.upload-content i { + font-size: 3rem; + color: var(--primary-color); + margin-bottom: 1rem; +} + +.file-input { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + opacity: 0; + cursor: pointer; +} + +/* 摄像头区域样式 */ +.camera-section { + text-align: center; +} + +.camera-controls { + margin-bottom: 2rem; +} + +.btn-camera { + padding: 1rem 2rem; + border-radius: 50px; + font-weight: 500; + display: inline-flex; + align-items: center; + gap: 0.5rem; + transition: all 0.3s ease; +} + +.btn-camera i { + font-size: 1.2rem; +} + +/* 显示区域样式 */ +.display-area { + position: relative; + margin-top: 2rem; + border-radius: 10px; + overflow: hidden; + background-color: var(--background-color); +} + +#canvas { + width: 100%; + max-width: 100%; + height: auto; + display: block; +} + +.loading-overlay { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: rgba(0, 0, 0, 0.7); + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + color: white; +} + +.loading-overlay p { + margin-top: 1rem; + font-weight: 500; +} + +/* 检测结果样式 */ +.results-card { + animation: slideUp 0.5s ease; +} + +.card-header { + background: linear-gradient(135deg, var(--gradient-start), var(--gradient-end)); + color: white; + border-bottom: none; + border-radius: 14px 14px 0 0 !important; +} + +.detection-list { + display: grid; + gap: 1rem; + grid-template-columns: repeat(auto-fill, minmax(250px, 1fr)); +} + +.detection-item { + background-color: var(--background-color); + padding: 1rem; + border-radius: 8px; + display: flex; + align-items: center; + gap: 1rem; +} + +.detection-icon { + width: 40px; + height: 40px; + border-radius: 50%; + background: linear-gradient(135deg, var(--gradient-start), var(--gradient-end)); + display: flex; + align-items: center; + justify-content: center; + color: white; +} + +.detection-info { + flex: 1; +} + +.detection-label { + font-weight: 500; + margin-bottom: 0.25rem; +} + +.detection-confidence { + font-size: 0.875rem; + color: var(--text-color); + opacity: 0.8; +} + +/* 错误提示样式 */ +.alert { + border-radius: 10px; + display: flex; + align-items: center; + gap: 0.5rem; + animation: slideIn 0.3s ease; +} + +/* 页脚样式 */ +.footer { + background-color: var(--card-background); + padding: 1.5rem 0; + margin-top: auto; + border-top: 1px solid var(--border-color); +} + +/* 动画效果 */ +@keyframes slideUp { + from { + opacity: 0; + transform: translateY(20px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +@keyframes slideIn { + from { + opacity: 0; + transform: translateX(-20px); + } + to { + opacity: 1; + transform: translateX(0); + } +} + +/* 响应式设计 */ +@media (max-width: 768px) { + .navbar-brand { + font-size: 1.2rem; + } + + .operation-area { + padding: 1rem; + } + + .upload-zone { + padding: 2rem; + } + + .detection-list { + grid-template-columns: 1fr; + } +} + +/* 主题切换按钮 */ +.theme-toggle { + position: fixed; + bottom: 2rem; + right: 2rem; + width: 50px; + height: 50px; + border-radius: 50%; + background: linear-gradient(135deg, var(--gradient-start), var(--gradient-end)); + color: white; + border: none; + box-shadow: 0 4px 10px var(--shadow-color); + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + transition: all 0.3s ease; + z-index: 1000; +} + +.theme-toggle:hover { + transform: scale(1.1); +} + +.theme-toggle i { + font-size: 1.5rem; +} \ No newline at end of file diff --git a/src/yolo测试代码/static/js/main.js b/src/yolo测试代码/static/js/main.js new file mode 100644 index 0000000..bb00f6c --- /dev/null +++ b/src/yolo测试代码/static/js/main.js @@ -0,0 +1,461 @@ +// 全局变量 +let detectionCount = 0; +let totalTime = 0; +let stream = null; +let isProcessing = false; + +// DOM元素 +const elements = { + video: document.getElementById('video'), + canvas: document.getElementById('canvas'), + imageInput: document.getElementById('imageInput'), + dropZone: document.getElementById('dropZone'), + imageSection: document.getElementById('imageSection'), + cameraSection: document.getElementById('cameraSection'), + startCamera: document.getElementById('startCamera'), + stopCamera: document.getElementById('stopCamera'), + results: document.getElementById('results'), + detectionsList: document.getElementById('detectionsList'), + error: document.getElementById('error'), + errorMessage: document.querySelector('.error-message'), + loadingOverlay: document.querySelector('.loading-overlay'), + imageMode: document.getElementById('imageMode'), + cameraMode: document.getElementById('cameraMode'), + themeToggle: document.getElementById('themeToggle'), + currentTime: document.getElementById('current-time'), + detectionStatus: document.getElementById('detection-status'), + detectionCountElement: document.getElementById('detection-count'), + avgTime: document.getElementById('avg-time'), + systemStatus: document.getElementById('system-status'), + autoDetect: document.getElementById('autoDetect'), + continuousDetection: document.getElementById('continuousDetection'), + cameraResolution: document.getElementById('cameraResolution'), + imagePreview: document.getElementById('imagePreview'), + previewImg: document.getElementById('previewImg'), + exportResults: document.getElementById('exportResults'), + clearResults: document.getElementById('clearResults') +}; + +// 初始化 +document.addEventListener('DOMContentLoaded', () => { + initializeTheme(); + initializeEventListeners(); + updateCurrentTime(); + setInterval(updateCurrentTime, 1000); +}); + +// 主题切换 +function initializeTheme() { + const theme = localStorage.getItem('theme') || 'light'; + document.documentElement.setAttribute('data-theme', theme); + updateThemeIcon(); +} + +function toggleTheme() { + const currentTheme = document.documentElement.getAttribute('data-theme'); + const newTheme = currentTheme === 'light' ? 'dark' : 'light'; + document.documentElement.setAttribute('data-theme', newTheme); + localStorage.setItem('theme', newTheme); + updateThemeIcon(); +} + +function updateThemeIcon() { + const theme = document.documentElement.getAttribute('data-theme'); + elements.themeToggle.innerHTML = ``; +} + +// 事件监听器初始化 +function initializeEventListeners() { + // 模式切换 + elements.imageMode.addEventListener('click', () => switchMode('image')); + elements.cameraMode.addEventListener('click', () => switchMode('camera')); + + // 文件上传 + elements.imageInput.addEventListener('change', handleFileSelect); + elements.dropZone.addEventListener('dragover', handleDragOver); + elements.dropZone.addEventListener('drop', handleDrop); + + // 摄像头控制 + elements.startCamera.addEventListener('click', startCamera); + elements.stopCamera.addEventListener('click', stopCamera); + + // 主题切换 + elements.themeToggle.addEventListener('click', toggleTheme); + + // 结果操作 + elements.exportResults.addEventListener('click', exportResults); + elements.clearResults.addEventListener('click', clearResults); + + // 键盘快捷键 + document.addEventListener('keydown', handleKeyboardShortcuts); + + // 自动检测 + elements.autoDetect.addEventListener('change', handleAutoDetect); + elements.continuousDetection.addEventListener('change', handleContinuousDetection); + elements.cameraResolution.addEventListener('change', handleResolutionChange); +} + +// 模式切换 +function switchMode(mode) { + if (mode === 'image') { + elements.imageMode.classList.add('active'); + elements.cameraMode.classList.remove('active'); + elements.imageSection.style.display = 'block'; + elements.cameraSection.style.display = 'none'; + stopCamera(); + } else { + elements.imageMode.classList.remove('active'); + elements.cameraMode.classList.add('active'); + elements.imageSection.style.display = 'none'; + elements.cameraSection.style.display = 'block'; + } +} + +// 文件处理 +function handleFileSelect(event) { + const file = event.target.files[0]; + if (file) { + processImage(file); + } +} + +function handleDragOver(event) { + event.preventDefault(); + event.stopPropagation(); + elements.dropZone.classList.add('drag-over'); +} + +function handleDrop(event) { + event.preventDefault(); + event.stopPropagation(); + elements.dropZone.classList.remove('drag-over'); + + const file = event.dataTransfer.files[0]; + if (file && file.type.startsWith('image/')) { + processImage(file); + } else { + showError('请上传有效的图片文件'); + } +} + +// 图片处理 +async function processImage(file) { + try { + showLoading(); + updateStatus('处理中'); + + // 显示预览 + const reader = new FileReader(); + reader.onload = (e) => { + elements.previewImg.src = e.target.result; + elements.imagePreview.style.display = 'block'; + }; + reader.readAsDataURL(file); + + const startTime = performance.now(); + + // 创建FormData对象 + const formData = new FormData(); + formData.append('image', file); + + // 发送请求到后端 + const response = await fetch('/detect', { + method: 'POST', + body: formData + }); + + if (!response.ok) { + throw new Error('检测请求失败'); + } + + const result = await response.json(); + + // 计算处理时间 + const endTime = performance.now(); + const processingTime = endTime - startTime; + updateDetectionStats(processingTime); + + // 显示结果 + displayResults(result); + updateStatus('就绪'); + } catch (error) { + showError(error.message); + updateStatus('错误'); + } finally { + hideLoading(); + } +} + +// 摄像头处理 +async function startCamera() { + try { + const resolution = getCameraResolution(); + stream = await navigator.mediaDevices.getUserMedia({ + video: { + width: { ideal: resolution.width }, + height: { ideal: resolution.height } + } + }); + + elements.video.srcObject = stream; + elements.video.style.display = 'block'; + elements.startCamera.style.display = 'none'; + elements.stopCamera.style.display = 'inline-block'; + + if (elements.continuousDetection.checked) { + startContinuousDetection(); + } + + updateStatus('摄像头已开启'); + } catch (error) { + showError('无法访问摄像头'); + updateStatus('错误'); + } +} + +function stopCamera() { + if (stream) { + stream.getTracks().forEach(track => track.stop()); + elements.video.srcObject = null; + elements.video.style.display = 'none'; + elements.startCamera.style.display = 'inline-block'; + elements.stopCamera.style.display = 'none'; + updateStatus('就绪'); + } +} + +// 连续检测 +function startContinuousDetection() { + if (!isProcessing && elements.continuousDetection.checked) { + processCameraFrame(); + } +} + +async function processCameraFrame() { + if (!elements.continuousDetection.checked || !stream) return; + + try { + isProcessing = true; + showLoading(); + updateStatus('处理中'); + + const startTime = performance.now(); + + // 捕获视频帧 + const canvas = document.createElement('canvas'); + canvas.width = elements.video.videoWidth; + canvas.height = elements.video.videoHeight; + const ctx = canvas.getContext('2d'); + ctx.drawImage(elements.video, 0, 0); + + // 将帧转换为blob + const blob = await new Promise(resolve => { + canvas.toBlob(resolve, 'image/jpeg'); + }); + + // 发送到后端 + const formData = new FormData(); + formData.append('image', blob); + + const response = await fetch('/detect', { + method: 'POST', + body: formData + }); + + if (!response.ok) { + throw new Error('检测请求失败'); + } + + const result = await response.json(); + + // 计算处理时间 + const endTime = performance.now(); + const processingTime = endTime - startTime; + updateDetectionStats(processingTime); + + // 显示结果 + displayResults(result); + updateStatus('检测中'); + } catch (error) { + showError(error.message); + updateStatus('错误'); + elements.continuousDetection.checked = false; + } finally { + hideLoading(); + isProcessing = false; + + // 继续下一帧检测 + if (elements.continuousDetection.checked) { + requestAnimationFrame(processCameraFrame); + } + } +} + +// 结果显示 +function displayResults(results) { + elements.results.style.display = 'block'; + elements.detectionsList.innerHTML = ''; + + results.detections.forEach(detection => { + const detectionElement = document.createElement('div'); + detectionElement.className = 'detection-item'; + detectionElement.innerHTML = ` +
+就绪
+0
+0ms
+正常
+点击或拖拽图片到此处
+ 支持 JPG、PNG、GIF 格式 + +