389 lines
18 KiB
HTML
389 lines
18 KiB
HTML
{% 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 %} |