1316 lines
43 KiB
HTML
1316 lines
43 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="zh-CN">
|
||
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<title>低空数智实时目标检测系统</title>
|
||
<script src="{{ url_for('static', filename='socket.io.min.js') }}"></script>
|
||
<script src="{{ url_for('static', filename='chart.js') }}"></script>
|
||
<script src="{{ url_for('static', filename='flv.min.js') }}"></script>
|
||
<style>
|
||
* {
|
||
margin: 0;
|
||
padding: 0;
|
||
box-sizing: border-box;
|
||
font-family: 'PingFang SC', 'Microsoft YaHei', sans-serif;
|
||
}
|
||
|
||
body {
|
||
background-color: #1e1e2e;
|
||
color: #e0def4;
|
||
line-height: 1.6;
|
||
overflow-x: hidden;
|
||
}
|
||
|
||
.container {
|
||
max-width: 1800px;
|
||
margin: 0 auto;
|
||
padding: 20px;
|
||
}
|
||
|
||
header {
|
||
background: linear-gradient(135deg, #312b7c 0%, #1a1a1a 100%);
|
||
padding: 20px 30px;
|
||
border-radius: 10px;
|
||
margin-bottom: 30px;
|
||
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.5);
|
||
border: 1px solid #4c4ca7;
|
||
}
|
||
|
||
h1 {
|
||
font-size: 2.5rem;
|
||
background: linear-gradient(90deg, #eb6ea5, #c3a5ff);
|
||
-webkit-background-clip: text;
|
||
-webkit-text-fill-color: transparent;
|
||
text-align: center;
|
||
margin-bottom: 15px;
|
||
text-shadow: 0 0 10px rgba(146, 94, 255, 0.5);
|
||
}
|
||
|
||
.description {
|
||
text-align: center;
|
||
margin-bottom: 15px;
|
||
color: #a6adc8;
|
||
font-size: 1.1rem;
|
||
}
|
||
|
||
.config-container {
|
||
display: flex;
|
||
justify-content: center;
|
||
gap: 20px;
|
||
margin-bottom: 15px;
|
||
flex-wrap: wrap;
|
||
}
|
||
|
||
.config-box {
|
||
background: rgba(40, 44, 82, 0.7);
|
||
padding: 12px 25px;
|
||
border-radius: 8px;
|
||
font-size: 1.1rem;
|
||
display: flex;
|
||
align-items: center;
|
||
border: 1px solid #5a61c5;
|
||
min-width: 300px;
|
||
}
|
||
|
||
.config-label {
|
||
margin-right: 10px;
|
||
color: #c3a5ff;
|
||
}
|
||
|
||
.config-select {
|
||
background: rgba(30, 30, 46, 0.8);
|
||
color: #e0def4;
|
||
border: 1px solid #5a61c5;
|
||
border-radius: 5px;
|
||
padding: 8px 12px;
|
||
flex: 1;
|
||
font-size: 1rem;
|
||
}
|
||
|
||
.status-container {
|
||
display: flex;
|
||
justify-content: center;
|
||
align-items: center;
|
||
gap: 20px;
|
||
margin-bottom: 15px;
|
||
flex-wrap: wrap;
|
||
}
|
||
|
||
.status-box {
|
||
background: rgba(40, 44, 82, 0.7);
|
||
padding: 12px 25px;
|
||
border-radius: 8px;
|
||
font-size: 1.1rem;
|
||
display: flex;
|
||
align-items: center;
|
||
border: 1px solid #5a61c5;
|
||
}
|
||
|
||
.status-indicator {
|
||
width: 15px;
|
||
height: 15px;
|
||
border-radius: 50%;
|
||
margin-right: 10px;
|
||
}
|
||
|
||
.active {
|
||
background-color: #9beb72;
|
||
box-shadow: 0 0 10px rgba(155, 235, 114, 0.5);
|
||
}
|
||
|
||
.inactive {
|
||
background-color: #f96060;
|
||
}
|
||
|
||
.fps-value {
|
||
font-weight: bold;
|
||
color: #9beb72;
|
||
}
|
||
|
||
.frame-count {
|
||
font-weight: bold;
|
||
color: #c3a5ff;
|
||
}
|
||
|
||
.controls {
|
||
display: flex;
|
||
justify-content: center;
|
||
gap: 20px;
|
||
margin-top: 20px;
|
||
}
|
||
|
||
.btn {
|
||
padding: 12px 30px;
|
||
font-size: 1.1rem;
|
||
border: none;
|
||
border-radius: 8px;
|
||
cursor: pointer;
|
||
transition: all 0.3s;
|
||
font-weight: 600;
|
||
background: linear-gradient(135deg, #6a5af9, #d66efd);
|
||
color: white;
|
||
box-shadow: 0 5px 15px rgba(106, 90, 249, 0.4);
|
||
}
|
||
|
||
.btn:disabled {
|
||
background: #444444;
|
||
color: #777777;
|
||
cursor: not-allowed;
|
||
box-shadow: none;
|
||
}
|
||
|
||
.btn:hover:not(:disabled) {
|
||
transform: translateY(-3px);
|
||
box-shadow: 0 8px 20px rgba(106, 90, 249, 0.6);
|
||
}
|
||
|
||
.video-section {
|
||
display: flex;
|
||
gap: 30px;
|
||
margin-bottom: 30px;
|
||
}
|
||
|
||
.video-container {
|
||
flex: 1;
|
||
background: rgba(30, 30, 46, 0.8);
|
||
border-radius: 15px;
|
||
overflow: hidden;
|
||
position: relative;
|
||
border: 1px solid #4c4ca7;
|
||
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.3);
|
||
}
|
||
|
||
.video-container h2 {
|
||
background: linear-gradient(90deg, #312b7c, #4c4ca7);
|
||
padding: 15px;
|
||
margin: 0;
|
||
font-size: 1.4rem;
|
||
text-align: center;
|
||
}
|
||
|
||
.video-content {
|
||
padding: 15px;
|
||
height: 500px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
background-color: #0f0f1b;
|
||
}
|
||
|
||
.video-player {
|
||
width: 100%;
|
||
height: 100%;
|
||
background-color: #000;
|
||
border-radius: 8px;
|
||
}
|
||
|
||
.statistics {
|
||
background: rgba(30, 30, 46, 0.8);
|
||
border-radius: 15px;
|
||
padding: 25px;
|
||
margin-bottom: 30px;
|
||
border: 1px solid #4c4ca7;
|
||
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.3);
|
||
}
|
||
|
||
.statistics h2 {
|
||
margin-bottom: 20px;
|
||
text-align: center;
|
||
font-size: 1.8rem;
|
||
color: #c3a5ff;
|
||
}
|
||
|
||
.chart-container {
|
||
height: 300px;
|
||
position: relative;
|
||
}
|
||
|
||
.detections {
|
||
background: rgba(30, 30, 46, 0.8);
|
||
border-radius: 15px;
|
||
padding: 25px;
|
||
border: 1px solid #4c4ca7;
|
||
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.3);
|
||
}
|
||
|
||
.detections h2 {
|
||
margin-bottom: 20px;
|
||
text-align: center;
|
||
font-size: 1.8rem;
|
||
color: #c3a5ff;
|
||
}
|
||
|
||
#detection-list {
|
||
list-style: none;
|
||
max-height: 300px;
|
||
overflow-y: auto;
|
||
padding: 10px;
|
||
background: rgba(20, 20, 35, 0.5);
|
||
border-radius: 8px;
|
||
border: 1px solid #5a61c5;
|
||
}
|
||
|
||
#detection-list::-webkit-scrollbar {
|
||
width: 10px;
|
||
}
|
||
|
||
#detection-list::-webkit-scrollbar-track {
|
||
background: rgba(20, 20, 35, 0.5);
|
||
}
|
||
|
||
#detection-list::-webkit-scrollbar-thumb {
|
||
background: linear-gradient(to bottom, #6a5af9, #d66efd);
|
||
border-radius: 5px;
|
||
}
|
||
|
||
.detection-item {
|
||
background: rgba(40, 44, 82, 0.7);
|
||
border-radius: 8px;
|
||
padding: 15px;
|
||
margin-bottom: 15px;
|
||
display: flex;
|
||
flex-direction: column;
|
||
border: 1px solid #5a61c5;
|
||
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.2);
|
||
}
|
||
|
||
.detection-header {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
margin-bottom: 8px;
|
||
font-weight: bold;
|
||
}
|
||
|
||
.detection-class {
|
||
color: #eb6ea5;
|
||
font-size: 1.2rem;
|
||
}
|
||
|
||
.detection-confidence {
|
||
color: #9beb72;
|
||
}
|
||
|
||
.detection-info {
|
||
display: grid;
|
||
grid-template-columns: 1fr 1fr;
|
||
gap: 10px;
|
||
}
|
||
|
||
.info-label {
|
||
color: #a6adc8;
|
||
}
|
||
|
||
.info-value {
|
||
color: #c3a5ff;
|
||
font-weight: 500;
|
||
}
|
||
|
||
/* 系统消息专用样式 */
|
||
.system-message .detection-header {
|
||
border-bottom: 1px solid #5a61c5;
|
||
padding-bottom: 8px;
|
||
}
|
||
|
||
.system-message .detection-title {
|
||
color: #42c8ff;
|
||
font-size: 1.2rem;
|
||
}
|
||
|
||
.system-message .detection-time {
|
||
color: #a6adc8;
|
||
font-size: 0.9rem;
|
||
}
|
||
|
||
.system-message .detection-content {
|
||
padding-top: 10px;
|
||
color: #e0def4;
|
||
line-height: 1.5;
|
||
}
|
||
|
||
footer {
|
||
text-align: center;
|
||
padding: 20px;
|
||
color: #a6adc8;
|
||
margin-top: 30px;
|
||
border-top: 1px solid #5a61c5;
|
||
}
|
||
|
||
.floating-buttons {
|
||
position: fixed;
|
||
bottom: 30px;
|
||
right: 30px;
|
||
display: flex;
|
||
gap: 15px;
|
||
}
|
||
|
||
.floating-btn {
|
||
width: 60px;
|
||
height: 60px;
|
||
border-radius: 50%;
|
||
background: linear-gradient(135deg, #6a5af9, #d66efd);
|
||
color: white;
|
||
border: none;
|
||
font-size: 1.5rem;
|
||
cursor: pointer;
|
||
box-shadow: 0 8px 20px rgba(106, 90, 249, 0.5);
|
||
transition: all 0.3s;
|
||
display: flex;
|
||
justify-content: center;
|
||
align-items: center;
|
||
}
|
||
|
||
.floating-btn:hover {
|
||
transform: translateY(-5px) rotate(10deg);
|
||
box-shadow: 0 12px 25px rgba(106, 90, 249, 0.7);
|
||
}
|
||
|
||
.stream-controls {
|
||
display: flex;
|
||
justify-content: center;
|
||
gap: 15px;
|
||
margin-top: 15px;
|
||
}
|
||
|
||
.stream-btn {
|
||
padding: 8px 20px;
|
||
background: rgba(76, 76, 167, 0.5);
|
||
border: 1px solid #5a61c5;
|
||
color: #e0def4;
|
||
border-radius: 5px;
|
||
cursor: pointer;
|
||
transition: all 0.3s;
|
||
}
|
||
|
||
.stream-btn:hover {
|
||
background: rgba(106, 90, 249, 0.7);
|
||
}
|
||
|
||
.stream-url {
|
||
display: flex;
|
||
align-items: center;
|
||
margin-top: 10px;
|
||
padding: 10px;
|
||
background: rgba(30, 30, 46, 0.5);
|
||
border-radius: 5px;
|
||
font-size: 0.9rem;
|
||
}
|
||
|
||
.stream-url input {
|
||
flex: 1;
|
||
background: transparent;
|
||
border: none;
|
||
color: #e0def4;
|
||
padding: 5px;
|
||
font-size: 0.9rem;
|
||
}
|
||
|
||
.stream-url button {
|
||
background: rgba(106, 90, 249, 0.5);
|
||
border: none;
|
||
color: white;
|
||
padding: 5px 10px;
|
||
border-radius: 4px;
|
||
cursor: pointer;
|
||
margin-left: 5px;
|
||
}
|
||
|
||
.stream-url button:hover {
|
||
background: rgba(106, 90, 249, 0.8);
|
||
}
|
||
|
||
.url-label {
|
||
margin-right: 10px;
|
||
color: #a6adc8;
|
||
}
|
||
|
||
/* Toast通知系统 */
|
||
.toast-container {
|
||
position: fixed;
|
||
top: 20px;
|
||
right: 20px;
|
||
z-index: 1000;
|
||
max-width: 350px;
|
||
}
|
||
|
||
.toast {
|
||
background: rgba(40, 44, 82, 0.95);
|
||
border-radius: 8px;
|
||
padding: 15px 20px;
|
||
margin-bottom: 15px;
|
||
border-left: 4px solid #6a5af9;
|
||
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.3);
|
||
display: flex;
|
||
align-items: flex-start;
|
||
animation: slideIn 0.3s, fadeOut 0.5s 4.5s;
|
||
transition: transform 0.3s, opacity 0.3s;
|
||
}
|
||
|
||
.toast-icon {
|
||
font-size: 1.5rem;
|
||
margin-right: 12px;
|
||
color: #6a5af9;
|
||
}
|
||
|
||
.toast-content {
|
||
flex: 1;
|
||
}
|
||
|
||
.toast-title {
|
||
font-weight: bold;
|
||
color: #c3a5ff;
|
||
margin-bottom: 5px;
|
||
font-size: 1.1rem;
|
||
}
|
||
|
||
.toast-message {
|
||
color: #e0def4;
|
||
line-height: 1.4;
|
||
}
|
||
|
||
.toast-close {
|
||
background: none;
|
||
border: none;
|
||
color: #a6adc8;
|
||
font-size: 1.2rem;
|
||
cursor: pointer;
|
||
margin-left: 10px;
|
||
padding: 0;
|
||
line-height: 1;
|
||
}
|
||
|
||
.toast-close:hover {
|
||
color: #eb6ea5;
|
||
}
|
||
|
||
@keyframes slideIn {
|
||
from {
|
||
transform: translateX(100%);
|
||
opacity: 0;
|
||
}
|
||
|
||
to {
|
||
transform: translateX(0);
|
||
opacity: 1;
|
||
}
|
||
}
|
||
|
||
@keyframes fadeOut {
|
||
from {
|
||
opacity: 1;
|
||
}
|
||
|
||
to {
|
||
opacity: 0;
|
||
}
|
||
}
|
||
|
||
@media (max-width: 1200px) {
|
||
.video-section {
|
||
flex-direction: column;
|
||
}
|
||
|
||
.video-container {
|
||
height: 450px;
|
||
}
|
||
}
|
||
|
||
@media (max-width: 768px) {
|
||
h1 {
|
||
font-size: 2rem;
|
||
}
|
||
|
||
.video-content {
|
||
height: 350px;
|
||
}
|
||
|
||
.status-container {
|
||
flex-direction: column;
|
||
}
|
||
|
||
.controls {
|
||
flex-direction: column;
|
||
}
|
||
|
||
.btn {
|
||
width: 100%;
|
||
}
|
||
|
||
.stream-controls {
|
||
flex-direction: column;
|
||
}
|
||
|
||
.stream-url {
|
||
flex-direction: column;
|
||
align-items: flex-start;
|
||
}
|
||
|
||
.stream-url input {
|
||
width: 100%;
|
||
margin-top: 5px;
|
||
}
|
||
|
||
.stream-url button {
|
||
width: 100%;
|
||
margin-top: 5px;
|
||
margin-left: 0;
|
||
}
|
||
|
||
.config-container {
|
||
flex-direction: column;
|
||
}
|
||
|
||
.config-box {
|
||
width: 100%;
|
||
}
|
||
|
||
.toast-container {
|
||
left: 20px;
|
||
right: 20px;
|
||
max-width: none;
|
||
}
|
||
}
|
||
</style>
|
||
</head>
|
||
|
||
<body>
|
||
<div class="container">
|
||
<header>
|
||
<h1>低空数智实时目标检测系统</h1>
|
||
<p class="description">基于深度学习的目标检测系统,实时分析视频流并展示检测结果</p>
|
||
|
||
<div class="config-container">
|
||
<div class="config-box">
|
||
<span class="config-label">推流地址:</span>
|
||
<select id="stream-select" class="config-select">
|
||
<option value="rtmp://123.132.248.154:6009/live/14">测试车辆视频</option>
|
||
<option value="rtmp://box.wisestcity.com:1935/live/5">机场推流</option>
|
||
<option value="rtmp://box.wisestcity.com:1935/live/7">无人机推流</option>
|
||
<option value="rtmp://localhost:1935/live/14">测试火灾视频</option>
|
||
<option value="rtmp://box.wisestcity.com:1935/live/10">测试游泳视频</option>
|
||
|
||
</select>
|
||
</div>
|
||
<div class="config-box">
|
||
<span class="config-label">模型选择:</span>
|
||
<select id="model-select" class="config-select">
|
||
<option value="car.pt">车辆识别</option>
|
||
<option value="yolov8x.pt">YOLOv8 XLarge</option>
|
||
<option value="yolo11n.pt">YOLOv11 N</option>
|
||
<option value="yolo11x.pt">YOLOv11 X</option>
|
||
<option value="best.pt">火情识别</option>
|
||
<option value="20250901\\2025090109483695260039.pt">溺水识别</option>
|
||
|
||
</select>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="status-container">
|
||
<div class="status-box">
|
||
<div class="status-indicator inactive" id="status-indicator"></div>
|
||
<span>状态: <span id="connection-status">未连接</span></span>
|
||
</div>
|
||
<div class="status-box">
|
||
<span>FPS: <span class="fps-value" id="fps-value">0</span></span>
|
||
</div>
|
||
<div class="status-box">
|
||
<span>总帧数: <span class="frame-count" id="frame-count">0</span></span>
|
||
</div>
|
||
<div class="status-box">
|
||
<span>检测目标数: <span class="frame-count" id="detections-count">0</span></span>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="controls">
|
||
<button id="start-btn" class="btn">开始检测</button>
|
||
<button id="stop-btn" class="btn" disabled>停止检测</button>
|
||
</div>
|
||
</header>
|
||
|
||
<section class="video-section">
|
||
<div class="video-container">
|
||
<h2>原始视频流</h2>
|
||
<div class="video-content">
|
||
<video id="original-stream" class="video-player" controls></video>
|
||
</div>
|
||
<div class="stream-controls">
|
||
<button id="play-original" class="stream-btn">播放</button>
|
||
<button id="pause-original" class="stream-btn">暂停</button>
|
||
<button id="mute-original" class="stream-btn">静音</button>
|
||
</div>
|
||
<div class="stream-url">
|
||
<span class="url-label">推流地址:</span>
|
||
<input type="text" id="original-url" value="http://123.132.248.154:8800/flv/live/14.flv">
|
||
<button id="update-original">更新</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="video-container">
|
||
<h2>检测后视频流 <span id="objects-count" style="font-size: 0.8rem; color: #eb6ea5;">(0个检测对象)</span>
|
||
</h2>
|
||
<div class="video-content">
|
||
<video id="annotated-stream" class="video-player" controls></video>
|
||
</div>
|
||
<div class="stream-controls">
|
||
<button id="play-annotated" class="stream-btn">播放</button>
|
||
<button id="pause-annotated" class="stream-btn">暂停</button>
|
||
<button id="mute-annotated" class="stream-btn">静音</button>
|
||
</div>
|
||
<div class="stream-url">
|
||
<span class="url-label">推流地址:</span>
|
||
<input type="text" id="annotated-url" value="http://123.132.248.154:8800/flv/live/11.flv">
|
||
<button id="update-annotated">更新</button>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
<section class="statistics">
|
||
<h2>目标检测统计</h2>
|
||
<div class="chart-container">
|
||
<canvas id="detection-chart"></canvas>
|
||
</div>
|
||
</section>
|
||
|
||
<section class="detections">
|
||
<h2>实时检测结果</h2>
|
||
<ul id="detection-list">
|
||
<li class="detection-item system-message">
|
||
<div class="detection-header">
|
||
<span class="detection-title">系统信息</span>
|
||
<span class="detection-time">准备就绪</span>
|
||
</div>
|
||
<div class="detection-content">
|
||
点击"开始检测"按钮启动系统
|
||
</div>
|
||
</li>
|
||
</ul>
|
||
</section>
|
||
|
||
<footer>
|
||
<p>YOLOv8 目标检测系统 © 2025 | 实时视频分析解决方案</p>
|
||
</footer>
|
||
|
||
<div class="floating-buttons">
|
||
<button class="floating-btn" id="snapshot-btn">📷</button>
|
||
<button class="floating-btn" id="toggle-btn">⬛</button>
|
||
</div>
|
||
|
||
<!-- Toast通知容器 -->
|
||
<div class="toast-container" id="toast-container"></div>
|
||
</div>
|
||
|
||
<script>
|
||
// 全局变量
|
||
let socket;
|
||
let chart;
|
||
let detectionActive = false;
|
||
let originalPlayer = null;
|
||
let annotatedPlayer = null;
|
||
let currentStream = null;
|
||
let currentModel = "yolov8m";
|
||
|
||
// DOM元素
|
||
const startBtn = document.getElementById('start-btn');
|
||
const stopBtn = document.getElementById('stop-btn');
|
||
const statusIndicator = document.getElementById('status-indicator');
|
||
const connectionStatus = document.getElementById('connection-status');
|
||
const fpsValue = document.getElementById('fps-value');
|
||
const frameCount = document.getElementById('frame-count');
|
||
const detectionsCount = document.getElementById('detections-count');
|
||
const objectsCount = document.getElementById('objects-count');
|
||
const originalStream = document.getElementById('original-stream');
|
||
const annotatedStream = document.getElementById('annotated-stream');
|
||
const detectionList = document.getElementById('detection-list');
|
||
const snapshotBtn = document.getElementById('snapshot-btn');
|
||
const toggleBtn = document.getElementById('toggle-btn');
|
||
const originalUrl = document.getElementById('original-url');
|
||
const annotatedUrl = document.getElementById('annotated-url');
|
||
const streamSelect = document.getElementById('stream-select');
|
||
const modelSelect = document.getElementById('model-select');
|
||
const playOriginalBtn = document.getElementById('play-original');
|
||
const pauseOriginalBtn = document.getElementById('pause-original');
|
||
const muteOriginalBtn = document.getElementById('mute-original');
|
||
const playAnnotatedBtn = document.getElementById('play-annotated');
|
||
const pauseAnnotatedBtn = document.getElementById('pause-annotated');
|
||
const muteAnnotatedBtn = document.getElementById('mute-annotated');
|
||
const updateOriginalBtn = document.getElementById('update-original');
|
||
const updateAnnotatedBtn = document.getElementById('update-annotated');
|
||
const toastContainer = document.getElementById('toast-container');
|
||
|
||
// 初始化页面
|
||
document.addEventListener('DOMContentLoaded', function () {
|
||
// 初始化检测图表
|
||
initializeChart();
|
||
|
||
// 设置按钮事件
|
||
startBtn.addEventListener('click', startDetection);
|
||
stopBtn.addEventListener('click', stopDetection);
|
||
snapshotBtn.addEventListener('click', takeSnapshot);
|
||
toggleBtn.addEventListener('click', toggleFullscreen);
|
||
|
||
// 视频控制事件
|
||
playOriginalBtn.addEventListener('click', () => playStream('original'));
|
||
pauseOriginalBtn.addEventListener('click', () => pauseStream('original'));
|
||
muteOriginalBtn.addEventListener('click', () => muteStream('original'));
|
||
playAnnotatedBtn.addEventListener('click', () => playStream('annotated'));
|
||
pauseAnnotatedBtn.addEventListener('click', () => pauseStream('annotated'));
|
||
muteAnnotatedBtn.addEventListener('click', () => muteStream('annotated'));
|
||
updateOriginalBtn.addEventListener('click', () => updateStream('original'));
|
||
updateAnnotatedBtn.addEventListener('click', () => updateStream('annotated'));
|
||
|
||
// 监听选择变化
|
||
streamSelect.addEventListener('change', updateStreamSelection);
|
||
modelSelect.addEventListener('change', updateModelSelection);
|
||
|
||
// 显示系统提示
|
||
showSystemMessage('欢迎使用YOLOv8目标检测系统,正在检查服务状态...');
|
||
|
||
// 检查服务状态并尝试恢复
|
||
checkServiceStatus();
|
||
});
|
||
|
||
// 初始化检测统计图表
|
||
function initializeChart() {
|
||
const ctx = document.getElementById('detection-chart').getContext('2d');
|
||
chart = new Chart(ctx, {
|
||
type: 'line',
|
||
data: {
|
||
labels: [],
|
||
datasets: [{
|
||
label: '检测目标数量',
|
||
data: [],
|
||
borderColor: '#eb6ea5',
|
||
backgroundColor: 'rgba(235, 110, 165, 0.2)',
|
||
tension: 0.4,
|
||
borderWidth: 3,
|
||
pointRadius: 3
|
||
}]
|
||
},
|
||
options: {
|
||
responsive: true,
|
||
maintainAspectRatio: false,
|
||
plugins: {
|
||
legend: {
|
||
labels: {
|
||
color: '#a6adc8',
|
||
font: {
|
||
size: 14
|
||
}
|
||
}
|
||
},
|
||
tooltip: {
|
||
backgroundColor: 'rgba(30, 30, 46, 0.9)',
|
||
titleColor: '#c3a5ff',
|
||
bodyColor: '#e0def4',
|
||
borderColor: '#6a5af9',
|
||
borderWidth: 1
|
||
}
|
||
},
|
||
scales: {
|
||
y: {
|
||
beginAtZero: true,
|
||
ticks: {
|
||
color: '#a6adc8'
|
||
},
|
||
grid: {
|
||
color: 'rgba(90, 95, 140, 0.2)'
|
||
}
|
||
},
|
||
x: {
|
||
ticks: {
|
||
color: '#a6adc8'
|
||
},
|
||
grid: {
|
||
color: 'rgba(90, 95, 140, 0.2)'
|
||
}
|
||
}
|
||
},
|
||
animation: {
|
||
duration: 300
|
||
}
|
||
}
|
||
});
|
||
}
|
||
|
||
// 更新检测统计图表
|
||
function updateChart(count, time_str) {
|
||
if (chart.data.datasets[0].data.length >= 20) {
|
||
chart.data.datasets[0].data.shift();
|
||
chart.data.labels.shift()
|
||
}
|
||
chart.data.datasets[0].data.push(count);
|
||
chart.data.labels.push(time_str);
|
||
chart.update();
|
||
}
|
||
|
||
// 初始化视频播放器
|
||
function initPlayer(type) {
|
||
const videoElement = type === 'original' ? originalStream : annotatedStream;
|
||
// let url = type === 'original' ? originalUrl.value : annotatedUrl.value;
|
||
// url = url.replace("rtmp","http").replace("1935","8081")
|
||
// url = url + ".flv"
|
||
url = originalUrl.value
|
||
if (flvjs.isSupported()) {
|
||
try {
|
||
if (type === 'original' && originalPlayer) {
|
||
originalPlayer.destroy();
|
||
originalPlayer = null;
|
||
}
|
||
if (type === 'annotated' && annotatedPlayer) {
|
||
annotatedPlayer.destroy();
|
||
annotatedPlayer = null;
|
||
}
|
||
|
||
const player = flvjs.createPlayer({
|
||
type: 'flv',
|
||
url: url,
|
||
isLive: true
|
||
});
|
||
|
||
player.attachMediaElement(videoElement);
|
||
player.load();
|
||
|
||
player.play().catch(e => {
|
||
console.log(`${type}播放器自动播放失败:`, e);
|
||
showSystemMessage(`${type}播放器自动播放失败: ${e.message}`);
|
||
});
|
||
|
||
if (type === 'original') {
|
||
originalPlayer = player;
|
||
} else {
|
||
annotatedPlayer = player;
|
||
}
|
||
|
||
return player;
|
||
} catch (error) {
|
||
console.error(`${type}播放器创建失败:`, error);
|
||
showSystemMessage(`${type}播放器创建失败: ${error.message}`);
|
||
return null;
|
||
}
|
||
} else {
|
||
showSystemMessage('当前浏览器不支持FLV播放');
|
||
return null;
|
||
}
|
||
}
|
||
|
||
// 播放视频流
|
||
function playStream(type) {
|
||
const videoElement = type === 'original' ? originalStream : annotatedStream;
|
||
const player = type === 'original' ? originalPlayer : annotatedPlayer;
|
||
|
||
if (player) {
|
||
try {
|
||
player.play();
|
||
} catch (e) {
|
||
console.log('播放失败:', e);
|
||
showSystemMessage('播放失败: ' + e.message);
|
||
}
|
||
} else {
|
||
initPlayer(type);
|
||
}
|
||
}
|
||
|
||
// 暂停视频流
|
||
function pauseStream(type) {
|
||
const videoElement = type === 'original' ? originalStream : annotatedStream;
|
||
if (videoElement && !videoElement.paused) {
|
||
videoElement.pause();
|
||
}
|
||
}
|
||
|
||
// 静音视频流
|
||
function muteStream(type) {
|
||
const videoElement = type === 'original' ? originalStream : annotatedStream;
|
||
if (videoElement) {
|
||
videoElement.muted = !videoElement.muted;
|
||
}
|
||
}
|
||
|
||
// 更新视频流地址
|
||
function updateStream(type) {
|
||
// let url = type === 'original' ? originalUrl.value : annotatedUrl.value;
|
||
let url = annotatedUrl.value;
|
||
if (url) {
|
||
initPlayer(type);
|
||
}
|
||
}
|
||
|
||
// 显示系统消息(修复版)
|
||
function showSystemMessage(message) {
|
||
// 创建系统消息元素
|
||
const item = document.createElement('li');
|
||
item.className = 'detection-item system-message';
|
||
|
||
const now = new Date();
|
||
const timeString = `${now.getHours().toString().padStart(2, '0')}:${now.getMinutes().toString().padStart(2, '0')}:${now.getSeconds().toString().padStart(2, '0')}`;
|
||
|
||
item.innerHTML = `
|
||
<div class="detection-header">
|
||
<span class="detection-title">系统信息</span>
|
||
<span class="detection-time">${timeString}</span>
|
||
</div>
|
||
<div class="detection-content">
|
||
${message}
|
||
</div>
|
||
`;
|
||
|
||
// 添加到列表顶部
|
||
detectionList.insertBefore(item, detectionList.firstChild);
|
||
|
||
// 限制消息数量
|
||
if (detectionList.children.length > 20) {
|
||
detectionList.removeChild(detectionList.lastChild);
|
||
}
|
||
|
||
// 自动滚动到最新消息
|
||
detectionList.scrollTop = 0;
|
||
|
||
// 同时显示Toast通知
|
||
showToast('系统通知', message);
|
||
}
|
||
|
||
// 显示Toast通知
|
||
function showToast(title, message) {
|
||
const toast = document.createElement('div');
|
||
toast.className = 'toast';
|
||
|
||
toast.innerHTML = `
|
||
<div class="toast-icon">ℹ️</div>
|
||
<div class="toast-content">
|
||
<div class="toast-title">${title}</div>
|
||
<div class="toast-message">${message}</div>
|
||
</div>
|
||
<button class="toast-close">×</button>
|
||
`;
|
||
|
||
toastContainer.appendChild(toast);
|
||
|
||
// 添加关闭事件
|
||
const closeBtn = toast.querySelector('.toast-close');
|
||
closeBtn.addEventListener('click', () => {
|
||
toast.style.transform = 'translateX(100%)';
|
||
toast.style.opacity = '0';
|
||
setTimeout(() => {
|
||
toast.remove();
|
||
}, 300);
|
||
});
|
||
|
||
// 自动移除
|
||
setTimeout(() => {
|
||
toast.style.opacity = '0';
|
||
setTimeout(() => {
|
||
toast.remove();
|
||
}, 500);
|
||
}, 5000);
|
||
}
|
||
|
||
// 更新检测结果列表
|
||
function updateDetectionList(detections) {
|
||
// 移除所有系统消息
|
||
const systemMessages = detectionList.querySelectorAll('.system-message');
|
||
systemMessages.forEach(msg => msg.remove());
|
||
|
||
// 清空检测结果
|
||
const detectionItems = detectionList.querySelectorAll('.detection-item:not(.system-message)');
|
||
detectionItems.forEach(item => item.remove());
|
||
|
||
if (detections.length === 0) {
|
||
const item = document.createElement('li');
|
||
item.className = 'detection-item';
|
||
item.innerHTML = `
|
||
<div class="detection-header">
|
||
<span class="detection-class">未检测到目标</span>
|
||
<span class="detection-confidence">${new Date().toLocaleTimeString()}</span>
|
||
</div>
|
||
`;
|
||
detectionList.appendChild(item);
|
||
return;
|
||
}
|
||
|
||
// 按置信度排序
|
||
const sortedDetections = [...detections].sort((a, b) => b.confidence - a.confidence);
|
||
|
||
// 创建检测项
|
||
sortedDetections.forEach(det => {
|
||
const item = document.createElement('li');
|
||
item.className = 'detection-item';
|
||
|
||
const box = det.box.map(num => Math.round(num));
|
||
|
||
item.innerHTML = `
|
||
<div class="detection-header">
|
||
<span class="detection-class">${det.class_name}</span>
|
||
<span class="detection-confidence">置信度: ${det.confidence.toFixed(2)}</span>
|
||
</div>
|
||
<div class="detection-info">
|
||
<div class="info-label">位置:</div>
|
||
<div class="info-value">[${box.join(', ')}]</div>
|
||
|
||
<div class="info-label">类别ID:</div>
|
||
<div class="info-value">${det.class_id}</div>
|
||
|
||
<div class="info-label">检测时间:</div>
|
||
<div class="info-value">${new Date().toLocaleTimeString()}</div>
|
||
</div>
|
||
`;
|
||
|
||
detectionList.appendChild(item);
|
||
});
|
||
}
|
||
|
||
// 检查服务状态
|
||
function checkServiceStatus() {
|
||
fetch('/status')
|
||
.then(response => response.json())
|
||
.then(data => {
|
||
if (data.active) {
|
||
// 服务正在运行,恢复UI状态
|
||
restoreUIState(data);
|
||
showSystemMessage('检测服务正在运行中,正在恢复连接...');
|
||
|
||
// 自动连接WebSocket
|
||
connectWebSocket();
|
||
|
||
// 尝试初始化视频播放器
|
||
setTimeout(() => {
|
||
initPlayer('original');
|
||
initPlayer('annotated');
|
||
}, 1000);
|
||
} else {
|
||
showSystemMessage('检测服务当前未运行,点击"开始检测"按钮启动系统');
|
||
}
|
||
})
|
||
.catch(error => {
|
||
console.error('获取服务状态失败:', error);
|
||
showSystemMessage('无法获取服务状态');
|
||
});
|
||
}
|
||
|
||
// 恢复UI状态
|
||
function restoreUIState(status) {
|
||
startBtn.disabled = true;
|
||
stopBtn.disabled = false;
|
||
connectionStatus.textContent = '已连接';
|
||
connectionStatus.style.color = '#9beb72';
|
||
statusIndicator.className = 'status-indicator active';
|
||
|
||
// 更新统计信息
|
||
fpsValue.textContent = status.fps.toFixed(1);
|
||
frameCount.textContent = status.frame_count;
|
||
detectionsCount.textContent = status.detections_count;
|
||
objectsCount.textContent = `(${status.detections_count}个检测对象)`;
|
||
|
||
// 更新图表
|
||
updateChart(status.detections_count);
|
||
|
||
detectionActive = true;
|
||
}
|
||
|
||
// 连接WebSocket
|
||
function connectWebSocket() {
|
||
// 初始化Socket连接
|
||
socket = io("http://192.168.10.131:9025");
|
||
|
||
// 连接事件
|
||
socket.on('connect', () => {
|
||
connectionStatus.textContent = '已连接';
|
||
connectionStatus.style.color = '#9beb72';
|
||
statusIndicator.className = 'status-indicator active';
|
||
showSystemMessage('已成功连接到实时检测服务');
|
||
});
|
||
|
||
socket.on('disconnect', () => {
|
||
connectionStatus.textContent = '已断开';
|
||
connectionStatus.style.color = '#f96060';
|
||
statusIndicator.className = 'status-indicator inactive';
|
||
resetUI();
|
||
showSystemMessage('服务器连接已断开');
|
||
});
|
||
|
||
// 接收检测结果
|
||
socket.on('detection_results', (data) => {
|
||
console.log('data----------------', data)
|
||
// 更新统计信息
|
||
fpsValue.textContent = data.fps.toFixed(1);
|
||
frameCount.textContent = data.frame_count;
|
||
detectionsCount.textContent = data.detections.length;
|
||
objectsCount.textContent = `(${data.detections.length}个检测对象)`;
|
||
|
||
// 更新图表
|
||
updateChart(data.detections.length, data.time_str);
|
||
|
||
// 更新检测结果列表
|
||
updateDetectionList(data.detections);
|
||
});
|
||
|
||
// 错误处理
|
||
socket.on('error', (error) => {
|
||
connectionStatus.textContent = '错误';
|
||
connectionStatus.style.color = '#f96060';
|
||
statusIndicator.className = 'status-indicator inactive';
|
||
resetUI();
|
||
showSystemMessage('连接错误: ' + error.message);
|
||
});
|
||
}
|
||
|
||
// 开始检测
|
||
function startDetection() {
|
||
if (detectionActive) {
|
||
showSystemMessage('检测已在运行中');
|
||
return;
|
||
}
|
||
|
||
// 获取当前选择的推流地址和模型
|
||
const selectedStream = streamSelect.value;
|
||
const selectedModel = modelSelect.value;
|
||
|
||
// 更新UI状态
|
||
startBtn.disabled = true;
|
||
stopBtn.disabled = false;
|
||
connectionStatus.textContent = '连接中...';
|
||
connectionStatus.style.color = '#f9c862';
|
||
statusIndicator.className = 'status-indicator';
|
||
|
||
showSystemMessage('正在连接到检测服务器...');
|
||
|
||
// 向服务端发送开始检测的命令
|
||
fetch('/start_detection', {
|
||
method: 'POST',
|
||
headers: {
|
||
'Content-Type': 'application/json'
|
||
},
|
||
body: JSON.stringify({
|
||
rtmp_url: selectedStream,
|
||
model_name: selectedModel
|
||
})
|
||
})
|
||
.then(response => response.json())
|
||
.then(data => {
|
||
if (data.status === 'success') {
|
||
detectionActive = true;
|
||
showSystemMessage('目标检测已启动,正在接收实时视频数据...');
|
||
|
||
// 连接WebSocket
|
||
connectWebSocket();
|
||
|
||
// 初始化视频播放器
|
||
initPlayer('original');
|
||
initPlayer('annotated');
|
||
} else {
|
||
showSystemMessage('启动检测失败: ' + data.message);
|
||
resetUI();
|
||
}
|
||
})
|
||
.catch(error => {
|
||
showSystemMessage('启动检测请求失败: ' + error.message);
|
||
resetUI();
|
||
});
|
||
}
|
||
|
||
// 停止检测
|
||
function stopDetection() {
|
||
if (!detectionActive) {
|
||
showSystemMessage('检测未运行');
|
||
return;
|
||
}
|
||
|
||
fetch('/stop_detection', {
|
||
method: 'POST',
|
||
headers: {
|
||
'Content-Type': 'application/json'
|
||
}
|
||
})
|
||
.then(response => response.json())
|
||
.then(data => {
|
||
if (data.status === 'success') {
|
||
detectionActive = false;
|
||
showSystemMessage('目标检测已停止');
|
||
resetUI();
|
||
|
||
// 断开Socket连接
|
||
if (socket) {
|
||
socket.disconnect();
|
||
}
|
||
|
||
// 停止视频播放
|
||
pauseStream('original');
|
||
pauseStream('annotated');
|
||
|
||
// 销毁播放器
|
||
if (originalPlayer) {
|
||
originalPlayer.destroy();
|
||
originalPlayer = null;
|
||
}
|
||
if (annotatedPlayer) {
|
||
annotatedPlayer.destroy();
|
||
annotatedPlayer = null;
|
||
}
|
||
} else {
|
||
showSystemMessage('停止检测失败: ' + data.message);
|
||
}
|
||
})
|
||
.catch(error => {
|
||
showSystemMessage('停止检测请求失败: ' + error.message);
|
||
});
|
||
}
|
||
|
||
// 重置UI状态
|
||
function resetUI() {
|
||
startBtn.disabled = false;
|
||
stopBtn.disabled = true;
|
||
connectionStatus.textContent = '未连接';
|
||
connectionStatus.style.color = '#f96060';
|
||
statusIndicator.className = 'status-indicator inactive';
|
||
}
|
||
|
||
// 截图功能
|
||
function takeSnapshot() {
|
||
if (!annotatedStream || !annotatedStream.videoWidth) {
|
||
showSystemMessage('当前无检测画面可保存');
|
||
return;
|
||
}
|
||
|
||
// 创建canvas捕获当前帧
|
||
const canvas = document.createElement('canvas');
|
||
canvas.width = annotatedStream.videoWidth;
|
||
canvas.height = annotatedStream.videoHeight;
|
||
const ctx = canvas.getContext('2d');
|
||
ctx.drawImage(annotatedStream, 0, 0, canvas.width, canvas.height);
|
||
|
||
// 创建下载链接
|
||
const link = document.createElement('a');
|
||
link.download = `检测截图_${new Date().toISOString().replace(/[:.]/g, '-')}.png`;
|
||
link.href = canvas.toDataURL('image/png');
|
||
link.click();
|
||
|
||
showSystemMessage('检测截图已保存');
|
||
}
|
||
|
||
// 全屏切换
|
||
function toggleFullscreen() {
|
||
if (!document.fullscreenElement) {
|
||
document.documentElement.requestFullscreen();
|
||
toggleBtn.textContent = '⬜';
|
||
} else {
|
||
document.exitFullscreen();
|
||
toggleBtn.textContent = '⬛';
|
||
}
|
||
}
|
||
|
||
// 更新推流地址选择
|
||
function updateStreamSelection() {
|
||
const selectedStream = streamSelect.value;
|
||
originalUrl.value = selectedStream;
|
||
showSystemMessage(`已选择推流地址: ${selectedStream}`);
|
||
}
|
||
|
||
// 更新模型选择
|
||
function updateModelSelection() {
|
||
const selectedModel = modelSelect.value;
|
||
showSystemMessage(`已选择模型: ${modelSelect.options[modelSelect.selectedIndex].text}`);
|
||
}
|
||
</script>
|
||
</body>
|
||
|
||
</html> |