上传文件至 /

This commit is contained in:
Wang_Run_Ze 2025-06-27 10:50:34 +08:00
parent 0ed9a0db0d
commit 226083349d
5 changed files with 1401 additions and 0 deletions

329
index.html Normal file
View File

@ -0,0 +1,329 @@
{% extends "layout.html" %}
{% block content %}
<div class="row">
<!-- 左侧面板:检测结果 -->
<div class="col-md-8">
<div class="card shadow-sm">
<div class="card-header bg-dark text-white">
<h5 class="card-title mb-0"><i class="bi bi-list-check"></i> 检测结果列表</h5>
</div>
<div class="card-body">
{% if reports %}
<div class="table-responsive">
<table class="table table-striped table-hover table-bordered">
<thead class="table-dark">
<tr>
<th><i class="bi bi-calendar"></i> 日期时间</th>
<th><i class="bi bi-film"></i> 视频数</th>
<th class="text-center"><i class="bi bi-eye"></i> 操作</th>
</tr>
</thead>
<tbody>
{% for report in reports %}
<tr>
<td>{{ report.timestamp }}</td>
<td class="text-center">{{ report.video_count }}</td>
<td class="text-center">
<div class="d-flex justify-content-center gap-2">
<a href="{{ url_for('view_report', report_filename=report.id) }}" class="btn btn-sm btn-primary">
<i class="bi bi-file-text"></i> 查看报告
</a>
<form action="{{ url_for('delete_report', report_filename=report.id) }}" method="post" class="d-inline">
<button type="submit" class="btn btn-sm btn-danger" onclick="return confirm('确定要删除此报告吗?')">
<i class="bi bi-trash"></i> 删除
</button>
</form>
</div>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% else %}
<div class="alert alert-info">
<i class="bi bi-info-circle me-2"></i> 暂无检测结果。请上传视频并开始检测。
</div>
{% endif %}
</div>
</div>
</div>
<!-- 右侧面板:文件处理和配置 -->
<div class="col-md-4">
<div class="card mb-4 shadow-sm">
<div class="card-header bg-dark text-white">
<h5 class="card-title mb-0"><i class="bi bi-box"></i> 模型管理</h5>
</div>
<div class="card-body">
<form action="{{ url_for('upload_model') }}" method="post" enctype="multipart/form-data">
<div class="upload-area mb-3" onclick="document.getElementById('model_file').click();">
<div class="upload-icon">
<i class="bi bi-cloud-arrow-up"></i>
</div>
<p>点击上传新模型 (.pt 文件)</p>
<input type="file" class="form-control d-none" id="model_file" name="model_file" accept=".pt">
</div>
<button type="submit" class="btn btn-primary w-100 py-2">
<i class="bi bi-upload"></i> 上传模型
</button>
</form>
<hr>
<div class="d-flex justify-content-between align-items-center mt-3">
<h6><i class="bi bi-list-check"></i> 可用模型:</h6>
</div>
<ul class="list-group mt-2">
{% if models %}
{% for model in models %}
<li class="list-group-item d-flex justify-content-between align-items-center">
<div>
<i class="bi bi-file-earmark-binary me-2"></i> {{ model }}
</div>
<form action="{{ url_for('delete_model', filename=model) }}" method="post" class="d-inline">
<button type="submit" class="btn btn-sm btn-outline-danger" onclick="return confirm('确定要删除此模型吗?')">
<i class="bi bi-trash"></i>
</button>
</form>
</li>
{% endfor %}
{% else %}
<li class="list-group-item text-center text-muted">
<i class="bi bi-exclamation-circle"></i> 暂无可用模型
</li>
{% endif %}
</ul>
</div>
</div>
<div class="card mb-4 shadow-sm">
<div class="card-header bg-dark text-white">
<h5 class="card-title mb-0"><i class="bi bi-camera-video"></i> 视频文件管理</h5>
</div>
<div class="card-body">
<div class="alert alert-info">
<i class="bi bi-info-circle me-2"></i> <strong>请直接将视频文件放入以下文件夹:</strong>
<div class="mt-2 p-2 bg-light rounded">
<code>{{ config.INPUT_FOLDER }}</code>
</div>
<p class="small mt-2 mb-0">支持格式: mp4, avi, mov, mkv, wmv, flv, webm</p>
</div>
<div class="d-flex justify-content-between align-items-center mt-3">
<h6><i class="bi bi-list-check"></i> 当前视频文件:</h6>
<form action="{{ url_for('clear_input_folder') }}" method="post">
<button type="submit" class="btn btn-outline-danger btn-sm">
<i class="bi bi-trash"></i> 清空
</button>
</form>
</div>
<ul class="list-group mt-2">
{% if videos %}
{% for video in videos %}
<li class="list-group-item d-flex justify-content-between align-items-center">
<div>
<i class="bi bi-film me-2"></i> {{ video }}
</div>
<form action="{{ url_for('delete_video', filename=video) }}" method="post" class="d-inline">
<button type="submit" class="btn btn-sm btn-outline-danger" onclick="return confirm('确定要删除此视频吗?')">
<i class="bi bi-trash"></i>
</button>
</form>
</li>
{% endfor %}
{% else %}
<li class="list-group-item text-center text-muted">
<i class="bi bi-exclamation-circle"></i> 暂无视频文件
</li>
{% endif %}
</ul>
</div>
</div>
<div class="card shadow-sm">
<div class="card-header bg-dark text-white">
<h5 class="card-title mb-0"><i class="bi bi-play-circle"></i> 开始检测</h5>
</div>
<div class="card-body">
<form action="{{ url_for('start_detection') }}" method="post" id="detection-form">
<div class="mb-3">
<label for="model_select" class="form-label"><i class="bi bi-box-seam"></i> 选择模型</label>
<select class="form-select" id="model_select" name="model_select" required>
<option value="" selected disabled>-- 请选择模型 --</option>
{% for model in models %}
{% if model is string %}
<option value="{{ model }}">{{ model }}</option>
{% else %}
<option value="{{ model.filename }}"
data-analyzed="{{ model.analyzed }}"
data-class-count="{{ model.class_count }}"
data-scene-types="{{ model.scene_types|join(', ') if model.scene_types else '' }}">
{{ model.filename }}
{% if model.analyzed and model.class_count and model.class_count > 0 %}
- {{ model.class_count }}种场景: {{ model.scene_types|join(', ') if model.scene_types else '未知' }}
{% endif %}
</option>
{% endif %}
{% endfor %}
</select>
<div id="model-info" class="mt-2" style="display: none;">
<small class="text-muted">
<i class="bi bi-info-circle"></i>
<span id="model-details"></span>
</small>
</div>
</div>
<div class="mb-3">
<label for="confidence_threshold" class="form-label">
<i class="bi bi-sliders"></i> 置信度阈值: <span class="confidence-value" id="confidence_value">{{ confidence_threshold }}</span>
</label>
<input type="range" class="form-range" id="confidence_threshold" name="confidence_threshold"
min="0.1" max="0.9" step="0.05" value="{{ confidence_threshold }}">
<div class="d-flex justify-content-between">
<small>0.1 (低精度)</small>
<small>0.9 (高精度)</small>
</div>
</div>
<button type="submit" class="btn btn-success w-100 py-2" id="start-detection-btn" {% if detection_in_progress %}disabled="disabled"{% endif %}>
<i class="bi bi-play-fill"></i> 开始检测
</button>
</form>
{% if detection_in_progress %}
<div class="mt-3" id="detection-progress-container">
<h6 class="text-center">检测进行中...</h6>
<div class="progress mb-2">
<div class="progress-bar progress-bar-striped progress-bar-animated"
role="progressbar" data-progress="{{ detection_progress }}"
aria-valuenow="{{ detection_progress }}" aria-valuemin="0" aria-valuemax="100">
{{ detection_progress }}%
</div>
</div>
<div class="text-center">
<p class="mb-1" id="detection-status">{{ detection_status }}</p>
<small class="text-muted" id="video-info"></small>
<br>
<small class="text-muted" id="frame-info"></small>
</div>
</div>
{% endif %}
</div>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
<script>
// 初始化进度条宽度
document.addEventListener('DOMContentLoaded', function() {
const progressBar = document.querySelector('.progress-bar');
if (progressBar) {
const progress = progressBar.getAttribute('data-progress');
if (progress) {
progressBar.style.width = progress + '%';
}
}
});
// 更新置信度显示值
const confidenceSlider = document.getElementById('confidence_threshold');
const confidenceValue = document.getElementById('confidence_value');
confidenceSlider.addEventListener('input', function() {
confidenceValue.textContent = this.value;
});
// 模型选择变化事件
const modelSelect = document.getElementById('model_select');
const modelInfo = document.getElementById('model-info');
const modelDetails = document.getElementById('model-details');
modelSelect.addEventListener('change', function() {
const selectedOption = this.options[this.selectedIndex];
const analyzed = selectedOption.getAttribute('data-analyzed') === 'True';
const classCount = selectedOption.getAttribute('data-class-count');
const sceneTypes = selectedOption.getAttribute('data-scene-types');
if (analyzed && classCount > 0) {
modelDetails.textContent = `此模型可检测 ${classCount} 种场景类型: ${sceneTypes}`;
modelInfo.style.display = 'block';
} else if (this.value) {
modelDetails.textContent = '模型信息未分析,将在首次使用时进行分析';
modelInfo.style.display = 'block';
} else {
modelInfo.style.display = 'none';
}
});
// 视频文件管理相关脚本已移除
document.getElementById('model_file').addEventListener('change', function() {
if (this.files.length > 0) {
const fileName = this.files[0].name;
this.closest('.upload-area').querySelector('p').innerHTML = `已选择: ${fileName}`;
}
});
</script>
{% if detection_in_progress %}
<script>
// 检测进度更新
function updateProgress() {
fetch('{{ url_for("get_detection_progress") }}')
.then(response => response.json())
.then(data => {
const progressBar = document.querySelector('.progress-bar');
progressBar.style.width = data.progress + '%';
progressBar.setAttribute('aria-valuenow', data.progress);
progressBar.textContent = data.progress + '%';
// 更新状态文本,添加检测时长信息
let statusText = data.status;
if (data.elapsed_time) {
statusText += ` (已用时间: ${data.elapsed_time})`;
}
document.getElementById('detection-status').textContent = statusText;
// 更新视频信息
const videoInfo = document.getElementById('video-info');
if (data.current_video && data.total_videos > 0) {
videoInfo.textContent = `当前视频: ${data.current_video} (${data.current_video_index}/${data.total_videos})`;
} else {
videoInfo.textContent = '';
}
// 更新帧信息
const frameInfo = document.getElementById('frame-info');
if (data.current_frame_count > 0) {
frameInfo.textContent = `已处理帧数: ${data.current_frame_count}`;
} else {
frameInfo.textContent = '';
}
if (data.in_progress) {
setTimeout(updateProgress, 1000);
} else {
// 检测完成,刷新页面
setTimeout(() => {
window.location.reload();
}, 2000);
}
})
.catch(error => {
console.error('获取进度信息失败:', error);
// 如果获取进度失败,继续尝试
setTimeout(updateProgress, 2000);
});
}
// 开始轮询进度
updateProgress();
</script>
{% endif %}
{% endblock %}

76
layout.html Normal file
View File

@ -0,0 +1,76 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>视频检测系统</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.8.1/font/bootstrap-icons.css">
<link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}">
<link rel="shortcut icon" href="{{ url_for('static', filename='favicon.ico') }}" type="image/x-icon">
</head>
<body>
<nav class="navbar navbar-expand-lg navbar-dark bg-dark mb-4">
<div class="container">
<a class="navbar-brand" href="{{ url_for('index') }}">
<i class="bi bi-camera-video"></i> 视频检测系统
</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarNav">
<ul class="navbar-nav ms-auto">
<li class="nav-item">
<a class="nav-link" href="{{ url_for('index') }}">
<i class="bi bi-house"></i> 首页
</a>
</li>
</ul>
</div>
</div>
</nav>
<div class="container">
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
{% for category, message in messages %}
<div class="alert alert-{{ category }} alert-dismissible fade show" role="alert">
{% if category == 'success' %}
<i class="bi bi-check-circle-fill me-2"></i>
{% elif category == 'danger' %}
<i class="bi bi-exclamation-triangle-fill me-2"></i>
{% elif category == 'warning' %}
<i class="bi bi-exclamation-circle-fill me-2"></i>
{% else %}
<i class="bi bi-info-circle-fill me-2"></i>
{% endif %}
{{ message }}
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
{% endfor %}
{% endif %}
{% endwith %}
{% block content %}{% endblock %}
</div>
<footer class="footer mt-5 py-3 bg-dark text-white">
<div class="container">
<div class="row">
<div class="col-md-6 text-center text-md-start">
<h5><i class="bi bi-camera-video"></i> 视频检测系统</h5>
<p class="small">基于YOLO的检测系统</p>
</div>
<div class="col-md-6 text-center text-md-end">
<p class="mb-0">© 2025 视频检测系统</p>
<p class="small">版本 1.0.0</p>
</div>
</div>
</div>
</footer>
<script src="https://cdn.jsdelivr.net/npm/jquery@3.6.0/dist/jquery.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"></script>
{% block scripts %}{% endblock %}
</body>
</html>

246
report.html Normal file
View File

@ -0,0 +1,246 @@
{% extends "layout.html" %}
{% block content %}
<div class="container">
<div class="row mb-4">
<div class="col-12">
<nav aria-label="breadcrumb" class="mb-3">
<ol class="breadcrumb">
<li class="breadcrumb-item"><a href="{{ url_for('index') }}"><i class="bi bi-house"></i> 首页</a></li>
<li class="breadcrumb-item active" aria-current="page"><i class="bi bi-file-earmark-text"></i> 检测报告</li>
</ol>
</nav>
<div class="card">
<div class="card-header bg-dark text-white d-flex justify-content-between align-items-center">
<h5 class="card-title mb-0"><i class="bi bi-clipboard-data"></i> 检测报告详情</h5>
<a href="{{ url_for('index') }}" class="btn btn-sm btn-outline-light"><i class="bi bi-arrow-left"></i> 返回</a>
</div>
<div class="card-body">
<div class="row">
<div class="col-md-6">
<div class="report-info-card">
<h6 class="border-bottom pb-2 mb-3"><i class="bi bi-info-circle"></i> 基本信息</h6>
<div class="row">
<div class="col-md-6">
<div class="info-item">
<span class="info-label"><i class="bi bi-calendar-event"></i> 检测时间:</span>
<span class="info-value">{{ report.timestamp }}</span>
</div>
</div>
<div class="col-md-6">
<div class="info-item">
<span class="info-label"><i class="bi bi-box-seam"></i> 使用模型:</span>
<span class="info-value">{{ report.model_path }}</span>
</div>
</div>
</div>
<div class="row mt-3">
<div class="col-md-6">
<div class="info-item">
<span class="info-label"><i class="bi bi-sliders"></i> 置信度阈值:</span>
<span class="info-value">{{ report.confidence_threshold }}</span>
</div>
</div>
<div class="col-md-6">
<div class="info-item">
<span class="info-label"><i class="bi bi-film"></i> 处理视频数量:</span>
<span class="info-value">{{ report.statistics.total_videos }}</span>
</div>
</div>
</div>
<div class="row mt-3">
<div class="col-md-6">
<div class="info-item">
<span class="info-label"><i class="bi bi-images"></i> 总帧数:</span>
<span class="info-value">{{ report.statistics.total_frames }}</span>
</div>
</div>
<div class="col-md-6">
<div class="info-item">
<span class="info-label"><i class="bi bi-eye"></i> 检测帧数:</span>
<span class="info-value">{{ report.statistics.detected_frames }}</span>
</div>
</div>
</div>
</div>
</div>
<div class="col-md-6">
<div class="report-stats-card">
<h6 class="border-bottom pb-2 mb-3"><i class="bi bi-bar-chart"></i> 检测统计</h6>
<div class="chart-container">
<canvas id="detectionChart"></canvas>
</div>
</div>
</div>
</div>
<h4>检测统计</h4>
<div class="row mb-4">
<div class="col-md-6">
<canvas id="detectionChart"></canvas>
</div>
<div class="col-md-6">
<canvas id="framesChart"></canvas>
</div>
</div>
<div class="row">
<div class="col-12">
<div class="card">
<div class="card-header bg-dark text-white">
<h5 class="card-title mb-0"><i class="bi bi-camera-video"></i> 视频检测结果</h5>
</div>
<div class="card-body">
<div class="table-responsive">
<table class="table table-striped table-hover">
<thead>
<tr>
<th><i class="bi bi-film"></i> 视频名称</th>
<th><i class="bi bi-images"></i> 总帧数</th>
<th><i class="bi bi-eye"></i> 检测帧数</th>
<th><i class="bi bi-bar-chart"></i> 检测率</th>
<th><i class="bi bi-gear"></i> 操作</th>
</tr>
</thead>
<tbody>
{% for video in report.statistics.detection_results %}
<tr>
<td>{{ video.video_name }}</td>
<td>{{ video.total_frames }}</td>
<td>{{ video.detections|length }}</td>
<td>
{% if video.total_frames %}
{% set detection_rate = (video.detections|length / video.total_frames * 100)|round(2) %}
<div class="progress" style="height: 20px;">
<div class="progress-bar {% if detection_rate < 5 %}bg-success{% elif detection_rate < 15 %}bg-warning{% else %}bg-danger{% endif %}"
role="progressbar" style="width: {{ detection_rate }}%;"
aria-valuenow="{{ detection_rate }}" aria-valuemin="0" aria-valuemax="100">
{{ detection_rate }}%
</div>
</div>
{% else %}
-
{% endif %}
</td>
<td>
<a href="{{ url_for('view_video_results', video_folder=video.video_name.split('.')[0]) }}" class="btn btn-sm btn-primary">
<i class="bi bi-search"></i> 查看详情
</a>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
<script>
// 检测统计图表
const detectionCtx = document.getElementById('detectionChart').getContext('2d');
const framesCtx = document.getElementById('framesChart').getContext('2d');
// 计算检测到的各类别数量
const classStats = {};
{% for video in report.statistics.detection_results %}
{% for detection in video.detections %}
{% for item in detection.detections %}
const classId = item.class_id;
const className = item.class_name;
if (!classStats[classId]) {
classStats[classId] = {
count: 0,
name: className
};
}
classStats[classId].count += 1;
{% endfor %}
{% endfor %}
{% endfor %}
// 准备图表数据
const classLabels = [];
const classCounts = [];
for (const classId in classStats) {
classLabels.push(classStats[classId].name);
classCounts.push(classStats[classId].count);
}
// 创建检测类别分布图表
new Chart(detectionCtx, {
type: 'pie',
data: {
labels: classLabels,
datasets: [{
label: '检测数量',
data: classCounts,
backgroundColor: [
'rgba(255, 99, 132, 0.7)',
'rgba(54, 162, 235, 0.7)',
'rgba(255, 206, 86, 0.7)',
'rgba(75, 192, 192, 0.7)',
'rgba(153, 102, 255, 0.7)'
],
borderWidth: 1
}]
},
options: {
responsive: true,
plugins: {
title: {
display: true,
text: '检测类别分布'
},
legend: {
position: 'bottom'
}
}
}
});
// 创建帧统计图表
new Chart(framesCtx, {
type: 'bar',
data: {
labels: ['总帧数', '检测帧数', '保存帧数'],
datasets: [{
label: '帧数统计',
data: [
{{ report.statistics.total_frames }},
{{ report.statistics.detected_frames }},
{{ report.statistics.saved_frames }}
],
backgroundColor: [
'rgba(54, 162, 235, 0.7)',
'rgba(255, 99, 132, 0.7)',
'rgba(75, 192, 192, 0.7)'
],
borderWidth: 1
}]
},
options: {
responsive: true,
plugins: {
title: {
display: true,
text: '帧处理统计'
},
legend: {
display: false
}
},
scales: {
y: {
beginAtZero: true
}
}
}
});
</script>
{% endblock %}

361
style.css Normal file
View File

@ -0,0 +1,361 @@
/* 基础样式 */
body {
font-family: 'Microsoft YaHei', 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background-color: #f8f9fa;
color: #333;
line-height: 1.6;
margin: 0;
padding: 0;
}
.container {
max-width: 1400px;
padding: 20px;
margin: 0 auto;
}
/* 导航栏样式 */
.navbar {
background-color: #343a40 !important;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.navbar-brand {
font-weight: bold;
font-size: 1.5rem;
color: #fff !important;
}
/* 卡片样式 */
.card {
background-color: #fff;
border: 1px solid #dee2e6;
border-radius: 8px;
margin-bottom: 20px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.card-header {
background-color: #343a40;
color: #fff;
border-bottom: 1px solid #dee2e6;
padding: 15px 20px;
border-radius: 8px 8px 0 0;
}
.card-body {
padding: 20px;
}
.card-title {
margin: 0;
font-size: 1.1rem;
font-weight: 600;
}
/* 按钮样式 */
.btn {
border-radius: 4px;
font-weight: 500;
padding: 8px 16px;
border: 1px solid transparent;
transition: all 0.2s ease;
}
.btn-primary {
background-color: #007bff;
border-color: #007bff;
color: #fff;
}
.btn-primary:hover {
background-color: #0056b3;
border-color: #0056b3;
color: #fff;
}
.btn-success {
background-color: #28a745;
border-color: #28a745;
color: #fff;
}
.btn-success:hover {
background-color: #1e7e34;
border-color: #1e7e34;
color: #fff;
}
.btn-danger {
background-color: #dc3545;
border-color: #dc3545;
color: #fff;
}
.btn-danger:hover {
background-color: #c82333;
border-color: #bd2130;
color: #fff;
}
.btn-outline-danger {
color: #dc3545;
border-color: #dc3545;
background-color: transparent;
}
.btn-outline-danger:hover {
background-color: #dc3545;
border-color: #dc3545;
color: #fff;
}
.btn-sm {
padding: 4px 8px;
font-size: 0.875rem;
}
/* 上传区域样式 */
.upload-area {
border: 2px dashed #dee2e6;
border-radius: 8px;
padding: 40px 20px;
text-align: center;
cursor: pointer;
background-color: #f8f9fa;
transition: border-color 0.2s ease;
}
.upload-area:hover {
border-color: #007bff;
background-color: #e9ecef;
}
.upload-icon {
font-size: 2.5rem;
color: #6c757d;
margin-bottom: 10px;
}
.upload-area:hover .upload-icon {
color: #007bff;
}
/* 表格样式 */
.table {
margin-bottom: 0;
background-color: #fff;
}
.table th {
background-color: #343a40;
color: #fff;
border-color: #454d55;
font-weight: 600;
padding: 12px;
}
.table td {
padding: 12px;
vertical-align: middle;
border-color: #dee2e6;
}
.table-striped tbody tr:nth-of-type(odd) {
background-color: #f8f9fa;
}
.table-hover tbody tr:hover {
background-color: #e9ecef;
}
.table-bordered {
border: 1px solid #dee2e6;
}
.table-responsive {
border-radius: 8px;
overflow: hidden;
}
/* 列表组样式 */
.list-group-item {
background-color: #fff;
border: 1px solid #dee2e6;
padding: 12px 16px;
margin-bottom: 2px;
}
.list-group-item:first-child {
border-top-left-radius: 4px;
border-top-right-radius: 4px;
}
.list-group-item:last-child {
border-bottom-left-radius: 4px;
border-bottom-right-radius: 4px;
margin-bottom: 0;
}
.list-group-item:hover {
background-color: #f8f9fa;
}
/* 表单控件样式 */
.form-control, .form-select {
border: 1px solid #ced4da;
border-radius: 4px;
padding: 8px 12px;
background-color: #fff;
transition: border-color 0.2s ease;
}
.form-control:focus, .form-select:focus {
border-color: #007bff;
box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.25);
outline: 0;
}
.form-label {
font-weight: 500;
margin-bottom: 8px;
color: #495057;
}
/* 滑块样式 */
.form-range {
height: 6px;
background-color: #dee2e6;
border-radius: 3px;
outline: none;
}
.form-range::-webkit-slider-thumb {
appearance: none;
width: 20px;
height: 20px;
border-radius: 50%;
background-color: #007bff;
cursor: pointer;
border: 2px solid #fff;
box-shadow: 0 2px 4px rgba(0,0,0,0.2);
}
.form-range::-moz-range-thumb {
width: 20px;
height: 20px;
border-radius: 50%;
background-color: #007bff;
cursor: pointer;
border: 2px solid #fff;
box-shadow: 0 2px 4px rgba(0,0,0,0.2);
}
/* 进度条样式 */
.progress {
height: 20px;
background-color: #e9ecef;
border-radius: 4px;
overflow: hidden;
}
.progress-bar {
background-color: #28a745;
transition: width 0.3s ease;
}
.progress-bar-striped {
background-image: linear-gradient(45deg, rgba(255,255,255,.15) 25%, transparent 25%, transparent 50%, rgba(255,255,255,.15) 50%, rgba(255,255,255,.15) 75%, transparent 75%, transparent);
background-size: 1rem 1rem;
}
.progress-bar-animated {
animation: progress-bar-stripes 1s linear infinite;
}
@keyframes progress-bar-stripes {
0% { background-position: 1rem 0; }
100% { background-position: 0 0; }
}
/* 警告框样式 */
.alert {
padding: 15px;
margin-bottom: 20px;
border: 1px solid transparent;
border-radius: 4px;
}
.alert-info {
color: #0c5460;
background-color: #d1ecf1;
border-color: #bee5eb;
}
.alert-success {
color: #155724;
background-color: #d4edda;
border-color: #c3e6cb;
}
.alert-danger {
color: #721c24;
background-color: #f8d7da;
border-color: #f5c6cb;
}
.alert-warning {
color: #856404;
background-color: #fff3cd;
border-color: #ffeaa7;
}
/* 工具类 */
.text-center { text-align: center; }
.text-muted { color: #6c757d; }
.mb-0 { margin-bottom: 0; }
.mb-1 { margin-bottom: 0.25rem; }
.mb-2 { margin-bottom: 0.5rem; }
.mb-3 { margin-bottom: 1rem; }
.mb-4 { margin-bottom: 1.5rem; }
.mt-2 { margin-top: 0.5rem; }
.mt-3 { margin-top: 1rem; }
.me-2 { margin-right: 0.5rem; }
.w-100 { width: 100%; }
.py-2 { padding-top: 0.5rem; padding-bottom: 0.5rem; }
.d-flex { display: flex; }
.d-inline { display: inline; }
.d-none { display: none; }
.justify-content-between { justify-content: space-between; }
.justify-content-center { justify-content: center; }
.align-items-center { align-items: center; }
.gap-2 { gap: 0.5rem; }
.small { font-size: 0.875rem; }
/* 响应式设计 */
@media (max-width: 768px) {
.container {
padding: 10px;
}
.card-body {
padding: 15px;
}
.upload-area {
padding: 30px 15px;
}
.upload-icon {
font-size: 2rem;
}
.btn {
padding: 6px 12px;
font-size: 0.9rem;
}
.table-responsive {
font-size: 0.875rem;
}
}

389
video_results.html Normal file
View File

@ -0,0 +1,389 @@
{% extends "layout.html" %}
{% block content %}
<div class="container">
<div class="row mb-4">
<div class="col-12">
<nav aria-label="breadcrumb" class="mb-3">
<ol class="breadcrumb">
<li class="breadcrumb-item"><a href="{{ url_for('index') }}"><i class="bi bi-house"></i> 首页</a></li>
<li class="breadcrumb-item"><a href="{{ url_for('view_report', report_filename=report_filename) }}"><i class="bi bi-file-earmark-text"></i> 检测报告</a></li>
<li class="breadcrumb-item active" aria-current="page"><i class="bi bi-film"></i> {{ video_name }}</li>
</ol>
</nav>
<div class="card">
<div class="card-header bg-dark text-white d-flex justify-content-between align-items-center">
<h5 class="card-title mb-0"><i class="bi bi-camera-video"></i> 视频检测结果: {{ video_name }}</h5>
<a href="{{ url_for('view_report', report_filename=report_filename) }}" class="btn btn-sm btn-outline-light"><i class="bi bi-arrow-left"></i> 返回报告</a>
</div>
<div class="card-body">
<div class="row">
<div class="col-md-6">
<div class="video-info-card">
<h6 class="border-bottom pb-2 mb-3"><i class="bi bi-info-circle"></i> 视频信息</h6>
<div class="row">
<div class="col-md-6">
<div class="info-item">
<span class="info-label"><i class="bi bi-film"></i> 视频名称:</span>
<span class="info-value">{{ video_name }}</span>
</div>
</div>
<div class="col-md-6">
<div class="info-item">
<span class="info-label"><i class="bi bi-images"></i> 总帧数:</span>
<span class="info-value">{{ total_frames }}</span>
</div>
</div>
</div>
<div class="row mt-3">
<div class="col-md-6">
<div class="info-item">
<span class="info-label"><i class="bi bi-eye"></i> 检测帧数:</span>
<span class="info-value">{{ frames|length }}</span>
</div>
</div>
<div class="col-md-6">
<div class="info-item">
<span class="info-label"><i class="bi bi-percent"></i> 检测率:</span>
<span class="info-value">
{% if total_frames and frames %}
{{ ((frames|length / total_frames) * 100)|round(2) }}%
{% else %}
-
{% endif %}
</span>
</div>
</div>
</div>
</div>
</div>
<div class="col-md-6">
<div class="detection-timeline-card">
<h6 class="border-bottom pb-2 mb-3"><i class="bi bi-clock-history"></i> 检测时间线</h6>
<div class="chart-container">
<canvas id="timelineChart"></canvas>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="row">
<div class="col-12">
<div class="card">
<div class="card-header bg-dark text-white d-flex justify-content-between align-items-center">
<h5 class="card-title mb-0"><i class="bi bi-images"></i> 检测到的帧 ({{ frames|length }})</h5>
<div class="btn-group">
<button class="btn btn-sm btn-outline-light" id="grid-view-btn" title="网格视图">
<i class="bi bi-grid"></i>
</button>
<button class="btn btn-sm btn-outline-light active" id="list-view-btn" title="列表视图">
<i class="bi bi-list"></i>
</button>
</div>
</div>
<div class="card-body">
{% if frames %}
<div class="mb-3">
<input type="text" class="form-control" id="frame-search" placeholder="搜索帧 (按帧号或类别)">
</div>
<div id="grid-view" class="row frame-container" style="display: none;">
{% for frame in frames %}
<div class="col-md-4 mb-4 frame-item" data-frame-number="{{ frame.frame_number }}" data-classes="{{ frame.classes|join(' ') }}">
<div class="card h-100">
<div class="card-img-container">
<img src="{{ url_for('frame_image', video_folder=video_folder, frame_file=frame.filename) }}"
class="card-img-top" alt="检测帧 #{{ frame.frame_number }}"
loading="lazy">
</div>
<div class="card-body">
<h6 class="card-title">帧 #{{ frame.frame_number }}</h6>
<p class="card-text">
<small><i class="bi bi-clock"></i> 时间: {{ frame.timestamp }}</small><br>
<small><i class="bi bi-tag"></i> 检测类别:
{% for class in frame.classes %}
<span class="badge bg-primary me-1">{{ class }}</span>
{% endfor %}
</small>
</p>
</div>
<div class="card-footer text-center">
<button class="btn btn-sm btn-primary view-full-image"
data-image-url="{{ url_for('frame_image', video_folder=video_folder, frame_file=frame.filename) }}">
<i class="bi bi-zoom-in"></i> 查看大图
</button>
</div>
</div>
</div>
{% endfor %}
</div>
<div id="list-view" class="frame-container">
<div class="table-responsive">
<table class="table table-striped table-hover">
<thead>
<tr>
<th><i class="bi bi-hash"></i> 帧号</th>
<th><i class="bi bi-clock"></i> 时间戳</th>
<th><i class="bi bi-tag"></i> 检测类别</th>
<th><i class="bi bi-image"></i> 预览</th>
<th><i class="bi bi-gear"></i> 操作</th>
</tr>
</thead>
<tbody>
{% for frame in frames %}
<tr class="frame-item" data-frame-number="{{ frame.frame_number }}" data-classes="{{ frame.classes|join(' ') }}">
<td>{{ frame.frame_number }}</td>
<td>{{ frame.timestamp }}</td>
<td>
{% for class in frame.classes %}
<span class="badge bg-primary me-1">{{ class }}</span>
{% endfor %}
</td>
<td>
<img src="{{ url_for('frame_image', video_folder=video_folder, frame_file=frame.filename) }}"
class="img-thumbnail" style="max-height: 50px;" alt="帧 #{{ frame.frame_number }}"
loading="lazy">
</td>
<td>
<button class="btn btn-sm btn-primary view-full-image"
data-image-url="{{ url_for('frame_image', video_folder=video_folder, frame_file=frame.filename) }}">
<i class="bi bi-zoom-in"></i> 查看
</button>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
{% else %}
<div class="alert alert-info">
<i class="bi bi-info-circle me-2"></i> 此视频中没有检测到任何帧。
</div>
{% endif %}
</div>
</div>
</div>
</div>
</div>
<!-- 图片查看模态框 -->
<div class="modal fade" id="imageModal" tabindex="-1" aria-labelledby="imageModalLabel" aria-hidden="true">
<div class="modal-dialog modal-xl">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="imageModalLabel">检测帧详情</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body text-center">
<img src="" id="modalImage" class="img-fluid" alt="检测帧大图">
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">关闭</button>
</div>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<script>
document.addEventListener('DOMContentLoaded', function() {
// 时间线图表
const ctx = document.getElementById('timelineChart').getContext('2d');
// 从帧数据中提取时间线信息
const frameNumbers = [];
const timestamps = [];
const classes = [];
{% for frame in frames %}
frameNumbers.push({{ frame.frame_number }});
timestamps.push('{{ frame.timestamp }}');
classes.push('{{ frame.classes|join(", ") }}');
{% endfor %}
const chart = new Chart(ctx, {
type: 'scatter',
data: {
datasets: [{
label: '检测帧',
data: frameNumbers.map((frame, index) => ({
x: index,
y: frame
})),
backgroundColor: 'rgba(54, 162, 235, 0.7)',
borderColor: 'rgba(54, 162, 235, 1)',
borderWidth: 1,
pointRadius: 5,
pointHoverRadius: 7
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
scales: {
y: {
beginAtZero: true,
title: {
display: true,
text: '帧号'
}
},
x: {
title: {
display: true,
text: '检测序号'
}
}
},
plugins: {
tooltip: {
callbacks: {
label: function(context) {
const index = context.dataIndex;
return [
`帧号: ${frameNumbers[index]}`,
`时间: ${timestamps[index]}`,
`类别: ${classes[index]}`
];
}
}
},
title: {
display: true,
text: '视频检测时间线'
}
}
}
});
// 视图切换
const gridViewBtn = document.getElementById('grid-view-btn');
const listViewBtn = document.getElementById('list-view-btn');
const gridView = document.getElementById('grid-view');
const listView = document.getElementById('list-view');
gridViewBtn.addEventListener('click', function() {
gridView.style.display = 'flex';
listView.style.display = 'none';
gridViewBtn.classList.add('active');
listViewBtn.classList.remove('active');
});
listViewBtn.addEventListener('click', function() {
gridView.style.display = 'none';
listView.style.display = 'block';
listViewBtn.classList.add('active');
gridViewBtn.classList.remove('active');
});
// 帧搜索功能
const frameSearch = document.getElementById('frame-search');
const frameItems = document.querySelectorAll('.frame-item');
frameSearch.addEventListener('input', function() {
const searchTerm = this.value.toLowerCase();
frameItems.forEach(item => {
const frameNumber = item.getAttribute('data-frame-number');
const classes = item.getAttribute('data-classes').toLowerCase();
if (frameNumber.includes(searchTerm) || classes.includes(searchTerm)) {
item.style.display = '';
} else {
item.style.display = 'none';
}
});
});
// 图片查看模态框
const viewFullImageBtns = document.querySelectorAll('.view-full-image');
const modalImage = document.getElementById('modalImage');
const imageModal = new bootstrap.Modal(document.getElementById('imageModal'));
viewFullImageBtns.forEach(btn => {
btn.addEventListener('click', function() {
const imageUrl = this.getAttribute('data-image-url');
modalImage.src = imageUrl;
imageModal.show();
});
});
});
</script>
{% endblock %}
{% block scripts %}
<script>
// 创建检测时间线图表
const timelineCtx = document.getElementById('detectionTimelineChart').getContext('2d');
// 准备数据
const timestamps = [];
const detectionCounts = [];
{% for detection in detection_data.detections %}
timestamps.push({{ detection.timestamp }});
detectionCounts.push({{ detection.detections|length }});
{% endfor %}
new Chart(timelineCtx, {
type: 'line',
data: {
labels: timestamps,
datasets: [{
label: '检测到的目标数量',
data: detectionCounts,
borderColor: 'rgba(75, 192, 192, 1)',
backgroundColor: 'rgba(75, 192, 192, 0.2)',
borderWidth: 2,
pointRadius: 4,
pointBackgroundColor: 'rgba(75, 192, 192, 1)',
fill: true,
tension: 0.1
}]
},
options: {
responsive: true,
plugins: {
title: {
display: true,
text: '检测时间线'
},
tooltip: {
callbacks: {
title: function(tooltipItems) {
return '时间: ' + tooltipItems[0].label + '秒';
}
}
}
},
scales: {
x: {
title: {
display: true,
text: '时间 (秒)'
}
},
y: {
beginAtZero: true,
title: {
display: true,
text: '检测目标数量'
},
ticks: {
stepSize: 1
}
}
}
}
});
</script>
{% endblock %}