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 = ` +
+ +
+
+
${detection.class}
+
置信度: ${(detection.confidence * 100).toFixed(2)}%
+
+ `; + elements.detectionsList.appendChild(detectionElement); + }); + + // 在画布上绘制检测框 + drawDetectionBoxes(results.detections); +} + +// 绘制检测框 +function drawDetectionBoxes(detections) { + const ctx = elements.canvas.getContext('2d'); + ctx.clearRect(0, 0, elements.canvas.width, elements.canvas.height); + + detections.forEach(detection => { + const [x, y, width, height] = detection.bbox; + + ctx.strokeStyle = '#4a90e2'; + ctx.lineWidth = 2; + ctx.strokeRect(x, y, width, height); + + // 绘制标签背景 + ctx.fillStyle = '#4a90e2'; + const label = `${detection.class} ${(detection.confidence * 100).toFixed(0)}%`; + const labelWidth = ctx.measureText(label).width + 10; + ctx.fillRect(x, y - 25, labelWidth, 20); + + // 绘制标签文本 + ctx.fillStyle = '#ffffff'; + ctx.font = '14px Arial'; + ctx.fillText(label, x + 5, y - 10); + }); +} + +// 辅助函数 +function updateCurrentTime() { + const now = new Date(); + elements.currentTime.textContent = now.toLocaleTimeString(); +} + +function updateStatus(status) { + elements.detectionStatus.textContent = status; +} + +function updateDetectionStats(processingTime) { + detectionCount++; + totalTime += processingTime; + + elements.detectionCountElement.textContent = detectionCount; + elements.avgTime.textContent = `${(totalTime / detectionCount).toFixed(0)}ms`; +} + +function showLoading() { + elements.loadingOverlay.style.display = 'flex'; +} + +function hideLoading() { + elements.loadingOverlay.style.display = 'none'; +} + +function showError(message) { + elements.errorMessage.textContent = message; + elements.error.style.display = 'block'; + setTimeout(() => { + elements.error.style.display = 'none'; + }, 5000); +} + +// 导出结果 +function exportResults() { + const results = []; + elements.detectionsList.querySelectorAll('.detection-item').forEach(item => { + const label = item.querySelector('.detection-label').textContent; + const confidence = item.querySelector('.detection-confidence').textContent; + results.push({ label, confidence }); + }); + + const blob = new Blob([JSON.stringify(results, null, 2)], { type: 'application/json' }); + const url = URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = `detection-results-${new Date().toISOString()}.json`; + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + URL.revokeObjectURL(url); +} + +// 清除结果 +function clearResults() { + elements.results.style.display = 'none'; + elements.detectionsList.innerHTML = ''; + const ctx = elements.canvas.getContext('2d'); + ctx.clearRect(0, 0, elements.canvas.width, elements.canvas.height); +} + +// 键盘快捷键 +function handleKeyboardShortcuts(event) { + if (event.ctrlKey) { + switch(event.key.toLowerCase()) { + case 'o': + event.preventDefault(); + elements.imageInput.click(); + break; + case 'c': + event.preventDefault(); + if (elements.startCamera.style.display !== 'none') { + startCamera(); + } else { + stopCamera(); + } + break; + case 's': + event.preventDefault(); + exportResults(); + break; + case 'd': + event.preventDefault(); + toggleTheme(); + break; + } + } +} + +// 摄像头分辨率 +function getCameraResolution() { + const resolutions = { + 'hd': { width: 1280, height: 720 }, + 'fhd': { width: 1920, height: 1080 }, + '4k': { width: 3840, height: 2160 } + }; + return resolutions[elements.cameraResolution.value] || resolutions.hd; +} + +function handleResolutionChange() { + if (stream) { + stopCamera(); + startCamera(); + } +} + +// 自动检测设置 +function handleAutoDetect() { + if (elements.autoDetect.checked) { + showError('自动检测已开启'); + } +} + +function handleContinuousDetection() { + if (elements.continuousDetection.checked && stream) { + startContinuousDetection(); + } +} \ No newline at end of file diff --git a/src/yolo测试代码/static/uploads/20250711_103124_jpg b/src/yolo测试代码/static/uploads/20250711_103124_jpg new file mode 100644 index 0000000..0fdc1f1 Binary files /dev/null and b/src/yolo测试代码/static/uploads/20250711_103124_jpg differ diff --git a/src/yolo测试代码/templates/index.html b/src/yolo测试代码/templates/index.html new file mode 100644 index 0000000..797203a --- /dev/null +++ b/src/yolo测试代码/templates/index.html @@ -0,0 +1,249 @@ + + + + + + 无人机检测系统 - AI智能识别 + + + + + + + + + + +
+ +
+
+
+
+ +
+
检测状态
+

就绪

+
+
+
+ +
+
识别次数
+

0

+
+
+
+ +
+
平均耗时
+

0ms

+
+
+
+ +
+
系统状态
+

正常

+
+
+
+
+
+ + +
+ +
+
+
+ + +
+
+
+ + +
+
+ +
+
+ +
+ +

点击或拖拽图片到此处

+ 支持 JPG、PNG、GIF 格式 + +
+
+
+
+ + +
+
+
+ + + + + +
+ + +
+
+
+ + + + + + +
+
+ + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/yolo测试代码/utils/__pycache__/detector.cpython-312.pyc b/src/yolo测试代码/utils/__pycache__/detector.cpython-312.pyc new file mode 100644 index 0000000..452c14e Binary files /dev/null and b/src/yolo测试代码/utils/__pycache__/detector.cpython-312.pyc differ diff --git a/src/yolo测试代码/utils/detector.py b/src/yolo测试代码/utils/detector.py new file mode 100644 index 0000000..89d1bb4 --- /dev/null +++ b/src/yolo测试代码/utils/detector.py @@ -0,0 +1,107 @@ +from ultralytics import YOLO +import cv2 +import numpy as np +from typing import Tuple, List +import logging +import os + +# 配置日志 +logging.basicConfig(level=logging.INFO) +logger = logging.getLogger(__name__) + +class DroneDetector: + """无人机检测器类""" + + def __init__(self, model_path: str): + """ + 初始化检测器 + + Args: + model_path: YOLO模型路径 + """ + if not os.path.exists(model_path): + raise FileNotFoundError(f"模型文件不存在:{model_path}") + + try: + logger.info(f"正在加载模型:{model_path}") + self.model = YOLO(model_path) + logger.info("模型加载成功") + except Exception as e: + logger.error(f"加载模型时出错:{str(e)}") + raise + + def detect_image(self, image: np.ndarray) -> Tuple[np.ndarray, List[dict]]: + """ + 对输入图片进行无人机检测 + + Args: + image: 输入图片(OpenCV格式,BGR) + + Returns: + processed_image: 处理后的图片(带有标注框) + detections: 检测结果列表,每个元素包含位置和置信度信息 + """ + try: + if image is None: + raise ValueError("输入图片为空") + + if not isinstance(image, np.ndarray): + raise TypeError("输入图片必须是numpy数组格式") + + # 记录图片信息 + logger.info(f"处理图片,形状:{image.shape}") + + # 执行检测 + results = self.model(image) + + # 获取第一帧的结果 + result = results[0] + + # 处理检测结果 + detections = [] + + # 在图片上绘制检测框 + annotated_frame = image.copy() + + if len(result.boxes) > 0: + boxes = result.boxes.cpu().numpy() + for box in boxes: + try: + # 获取边界框坐标 + x1, y1, x2, y2 = box.xyxy[0].astype(int) + + # 获取置信度 + confidence = float(box.conf[0]) + + # 获取类别 + class_id = int(box.cls[0]) + class_name = result.names[class_id] + + # 存储检测结果 + detection = { + 'bbox': [int(x1), int(y1), int(x2), int(y2)], # 确保所有值都是普通整数 + 'confidence': float(confidence), # 确保是普通浮点数 + 'class_name': str(class_name) # 确保是字符串 + } + detections.append(detection) + + # 绘制边界框 + cv2.rectangle(annotated_frame, (x1, y1), (x2, y2), (0, 255, 0), 2) + + # 添加标签 + label = f'{class_name}: {confidence:.2f}' + cv2.putText(annotated_frame, label, (x1, y1 - 10), + cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 255, 0), 2) + + logger.info(f"检测到目标:{detection}") + + except Exception as e: + logger.error(f"处理单个检测框时出错:{str(e)}") + continue + + logger.info(f"检测完成,找到 {len(detections)} 个目标") + return annotated_frame, detections + + except Exception as e: + logger.error(f"检测过程中出错:{str(e)}") + raise \ No newline at end of file