@ -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();
|
||||
}
|
||||
}
|
||||
|
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>
|
||||
Binary file not shown.
Loading…
Reference in new issue