yolo模型1.0 #2

Merged
pka3vx8jw merged 2 commits from 王琰 into main 6 months ago

@ -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"
}
```

@ -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)

@ -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

@ -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;
}

@ -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 = `<i class="bi bi-${theme === 'light' ? 'moon' : 'sun'}"></i>`;
}
// 事件监听器初始化
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 = `
<div class="detection-icon">
<i class="bi bi-bullseye"></i>
</div>
<div class="detection-info">
<div class="detection-label">${detection.class}</div>
<div class="detection-confidence">置信度: ${(detection.confidence * 100).toFixed(2)}%</div>
</div>
`;
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();
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

@ -0,0 +1,249 @@
<!DOCTYPE html>
<html lang="zh-CN" data-theme="light">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>无人机检测系统 - AI智能识别</title>
<!-- 使用国内CDN -->
<link href="https://fonts.loli.net/css2?family=Noto+Sans+SC:wght@400;500;700&display=swap" rel="stylesheet">
<link href="https://cdn.bootcdn.net/ajax/libs/bootstrap-icons/1.7.2/font/bootstrap-icons.css" rel="stylesheet">
<link href="https://cdn.bootcdn.net/ajax/libs/twitter-bootstrap/5.1.3/css/bootstrap.min.css" rel="stylesheet">
<link href="/static/css/style.css" rel="stylesheet">
</head>
<body>
<!-- 导航栏 -->
<nav class="navbar navbar-expand-lg navbar-dark">
<div class="container">
<a class="navbar-brand" href="#">
<i class="bi bi-camera-reels"></i>
无人机检测系统
</a>
<div class="navbar-text text-white ms-auto d-none d-md-flex align-items-center">
<i class="bi bi-clock me-2"></i>
<span id="current-time">加载中...</span>
</div>
</div>
</nav>
<div class="container main-container">
<!-- 状态面板 -->
<div class="status-panel card mb-4">
<div class="card-body">
<div class="row">
<div class="col-md-3 status-item">
<i class="bi bi-camera-video"></i>
<div class="status-info">
<h5>检测状态</h5>
<p id="detection-status">就绪</p>
</div>
</div>
<div class="col-md-3 status-item">
<i class="bi bi-graph-up"></i>
<div class="status-info">
<h5>识别次数</h5>
<p id="detection-count">0</p>
</div>
</div>
<div class="col-md-3 status-item">
<i class="bi bi-clock-history"></i>
<div class="status-info">
<h5>平均耗时</h5>
<p id="avg-time">0ms</p>
</div>
</div>
<div class="col-md-3 status-item">
<i class="bi bi-lightning-charge"></i>
<div class="status-info">
<h5>系统状态</h5>
<p id="system-status">正常</p>
</div>
</div>
</div>
</div>
</div>
<!-- 主要内容区 -->
<div class="content-wrapper">
<!-- 模式选择卡片 -->
<div class="mode-selector card">
<div class="card-body">
<div class="btn-group w-100" role="group">
<button type="button" class="btn btn-mode active" id="imageMode">
<i class="bi bi-image"></i>
图片模式
</button>
<button type="button" class="btn btn-mode" id="cameraMode">
<i class="bi bi-camera"></i>
摄像头模式
</button>
</div>
</div>
</div>
<!-- 操作区域 -->
<div class="operation-area card">
<div class="card-body">
<!-- 图片模式区域 -->
<div id="imageSection" class="upload-section">
<div class="upload-zone" id="dropZone">
<input type="file" class="file-input" id="imageInput" accept="image/*">
<div class="upload-content">
<i class="bi bi-cloud-arrow-up"></i>
<p>点击或拖拽图片到此处</p>
<small class="text-muted">支持 JPG、PNG、GIF 格式</small>
<div class="upload-preview" id="imagePreview" style="display: none;">
<img src="" alt="预览图" id="previewImg">
<button class="btn btn-danger btn-sm remove-preview">
<i class="bi bi-x"></i>
</button>
</div>
</div>
</div>
<div class="upload-options mt-3">
<div class="form-check form-switch">
<input class="form-check-input" type="checkbox" id="autoDetect">
<label class="form-check-label" for="autoDetect">自动检测</label>
</div>
</div>
</div>
<!-- 摄像头模式区域 -->
<div id="cameraSection" class="camera-section" style="display: none;">
<div class="camera-controls">
<button class="btn btn-primary btn-lg btn-camera" id="startCamera">
<i class="bi bi-camera-video"></i>
开启摄像头
</button>
<button class="btn btn-danger btn-lg btn-camera" id="stopCamera" style="display: none;">
<i class="bi bi-camera-video-off"></i>
关闭摄像头
</button>
</div>
<div class="camera-options mt-3">
<div class="row g-3">
<div class="col-md-6">
<div class="form-check form-switch">
<input class="form-check-input" type="checkbox" id="continuousDetection">
<label class="form-check-label" for="continuousDetection">连续检测</label>
</div>
</div>
<div class="col-md-6">
<select class="form-select" id="cameraResolution">
<option value="hd">高清 (HD)</option>
<option value="fhd">全高清 (FHD)</option>
<option value="4k">超高清 (4K)</option>
</select>
</div>
</div>
</div>
<video id="video" style="display: none;" autoplay playsinline></video>
</div>
<!-- 显示区域 -->
<div class="display-area">
<canvas id="canvas"></canvas>
<div class="loading-overlay" style="display: none;">
<div class="spinner-border text-primary" role="status">
<span class="visually-hidden">加载中...</span>
</div>
<p>正在处理...</p>
</div>
</div>
</div>
</div>
<!-- 检测结果卡片 -->
<div id="results" class="results-card card" style="display: none;">
<div class="card-header">
<div class="d-flex justify-content-between align-items-center">
<h5 class="card-title mb-0">
<i class="bi bi-list-check"></i>
检测结果
</h5>
<div class="btn-group">
<button class="btn btn-sm btn-light" id="exportResults">
<i class="bi bi-download"></i>
导出
</button>
<button class="btn btn-sm btn-light" id="clearResults">
<i class="bi bi-trash"></i>
清除
</button>
</div>
</div>
</div>
<div class="card-body">
<div id="detectionsList" class="detection-list"></div>
</div>
</div>
<!-- 错误提示 -->
<div id="error" class="alert alert-danger alert-dismissible fade show" role="alert" style="display: none;">
<i class="bi bi-exclamation-triangle-fill"></i>
<span class="error-message"></span>
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
</div>
</div>
<!-- 页脚 -->
<footer class="footer">
<div class="container">
<div class="row align-items-center">
<div class="col-md-6">
<p class="mb-0">
© 2024 无人机检测系统 | 基于YOLO技术
</p>
</div>
<div class="col-md-6 text-md-end">
<div class="footer-links">
<a href="#" class="text-muted me-3">关于我们</a>
<a href="#" class="text-muted me-3">使用帮助</a>
<a href="#" class="text-muted">联系支持</a>
</div>
</div>
</div>
</div>
</footer>
<!-- 主题切换按钮 -->
<button class="theme-toggle" id="themeToggle">
<i class="bi bi-moon"></i>
</button>
<!-- 快捷键提示模态框 -->
<div class="modal fade" id="shortcutsModal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">键盘快捷键</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<div class="shortcuts-list">
<div class="shortcut-item">
<span class="key">Ctrl + O</span>
<span class="description">打开文件</span>
</div>
<div class="shortcut-item">
<span class="key">Ctrl + C</span>
<span class="description">开启摄像头</span>
</div>
<div class="shortcut-item">
<span class="key">Ctrl + S</span>
<span class="description">保存结果</span>
</div>
<div class="shortcut-item">
<span class="key">Ctrl + D</span>
<span class="description">切换深色模式</span>
</div>
</div>
</div>
</div>
</div>
</div>
<script src="https://cdn.bootcdn.net/ajax/libs/twitter-bootstrap/5.1.3/js/bootstrap.bundle.min.js"></script>
<script src="/static/js/main.js"></script>
</body>
</html>

@ -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
Loading…
Cancel
Save