上传文件至 /
This commit is contained in:
parent
0ed9a0db0d
commit
226083349d
329
index.html
Normal file
329
index.html
Normal 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
76
layout.html
Normal 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
246
report.html
Normal 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
361
style.css
Normal 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
389
video_results.html
Normal 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 %}
|
||||
Loading…
Reference in New Issue
Block a user