设计思路,如下:
1.通过采集卡将视频信号输出到个人PC中
2.PC按设置好的时间,视频属性分片保存
3.将步骤2中的视频,按预处理要求,得到待计算的视频片段
4.使用SSIM算法计算预处理后的视频,将计算得到的数据存放在硬盘中
5.WEB端,分页按时间倒序展示,视屏卡顿情况
6.循环执行上述1~5步骤,直到视频输出结束
ps:根据视频的质量的不同,计算时间和硬盘空间要求也要具体区分准备
代码A,实现了视频采集,预处理和计算的阶梯循环运行
#################################### 代码A ################################
import time
import multiprocessing
import cv2
from skimage.metrics import structural_similarity as ssim
import matplotlib.pyplot as plt
import os
class LagAnalysis():
# 文件名称
file_name = 0
# 单文件最大时长 单位秒
file_time = 900
# 文件记录最大数量
file_max = 3
# 文件分辨率大小
file_resolution_ratio = (640, 480)
# 文件帧率单位
file_frame_rate = 60
# 原始文件路径
file_o_path = os.getcwd() + "\\original"
# 预处理文件路径
file_p_path = os.getcwd() + "\\pretreatment"
# 解析结果文件路径
file_r_path = os.getcwd() + "\\result"
def record(self):
# 初始化摄像头
cap = cv2.VideoCapture(0) # 0 通常是默认摄像头的标识
# 检查摄像头是否成功打开
if not cap.isOpened():
print("无法打开摄像头")
exit()
# 设置视频编码格式和输出视频文件
fourcc = cv2.VideoWriter_fourcc(*'XVID')
name = self.file_name
out_file = "original/output_" + str(name) + ".avi"
out = cv2.VideoWriter(out_file, fourcc, self.file_frame_rate, self.file_resolution_ratio)
flag = 0
# 单文件包含最大帧数
flag1 = self.file_time * self.file_frame_rate
# 循环捕获视频帧
while cap.isOpened() and name < self.file_max:
ret, frame = cap.read()
if ret:
if flag < flag1:
# 写入帧到输出视频文件
out.write(frame)
flag = flag + 1
else:
out.release()
flag = 0
name = name + 1
if name < self.file_max:
out_file = "original/output_" + str(name) + ".avi"
out = cv2.VideoWriter(out_file, fourcc, self.file_frame_rate, self.file_resolution_ratio)
else:
break
# 释放资源
cap.release()
out.release()
cv2.destroyAllWindows()
def pretreatment(self, x, y, width, height, f):
# 预处理视频函数
index = f.find("/")
out_file = "pretreatment/" + f[index + 1:]
fourcc = cv2.VideoWriter_fourcc(*'XVID')
cap = cv2.VideoCapture(f)
out = cv2.VideoWriter(out_file, fourcc, self.file_frame_rate, (width, height))
# 检查视频是否成功打开
if not cap.isOpened():
print("Error: Could not open video.")
exit()
# 通过循环读取视频的每一帧
while True:
# 读取下一帧,ret是一个布尔值,表示是否成功读取
# frame是读取到的帧,如果读取失败,则为None
ret, frame = cap.read()
# 如果正确读取帧,进行处理
if ret:
# 展示帧
# cv2.imshow('Frame', frame)
cropped_image = frame[y:y + height, x:x + width]
# cv2.imshow('Cropped Image', cropped_image)
out.write(cropped_image)
# time.sleep(10)
else:
# 如果读取帧失败,退出循环
break
cap.release()
out.release()
cv2.destroyAllWindows()
def calculate(self, width, height, f):
# 预处理视频函数
index = f.find("/")
index1 = f.find(".")
out_file = "result/" + f[index + 1:index1] + ".txt"
# fourcc = cv2.VideoWriter_fourcc(*'XVID')
fourcc = cv2.VideoWriter_fourcc(*'XVID')
cap = cv2.VideoCapture(f)
# 检查视频是否成功打开
if not cap.isOpened():
print("Error: Could not open video.")
exit()
ret, frame = cap.read()
old_frame = frame
# 打开文件进行写入
with open(out_file, 'w') as file:
# 通过循环读取视频的每一帧
while True:
# 读取下一帧,ret是一个布尔值,表示是否成功读取
# frame是读取到的帧,如果读取失败,则为None
ret, frame = cap.read()
# 如果正确读取帧,进行处理
if ret:
score, diff = self.compare_images(old_frame, frame)
file.write(str(score))
file.write("\n")
else:
# 如果读取帧失败,退出循环
break
cap.release()
cv2.destroyAllWindows()
file.close()
def compare_images(self, imageA, imageB):
# 转换图片为灰度
grayA = cv2.cvtColor(imageA, cv2.COLOR_BGR2GRAY)
grayB = cv2.cvtColor(imageB, cv2.COLOR_BGR2GRAY)
# 计算SSIM
score, diff = ssim(grayA, grayB, full=True)
diff = (diff * 255).astype("uint8")
return score, diff
def show_images(self, imageA, imageB, diff):
fig, axes = plt.subplots(1, 3, figsize=(20, 8))
ax = axes.ravel()
ax[0].imshow(imageA, cmap=plt.cm.gray)
ax[0].set_title('Image A')
ax[1].imshow(imageB, cmap=plt.cm.gray)
ax[1].set_title('Image B')
ax[2].imshow(diff, cmap=plt.cm.gray)
ax[2].set_title('Difference')
for a in ax:
a.axis('off')
plt.show()
def listen1(self):
# 监听指定目录下是否有新的待预处理的文件
name = self.file_name
flag = 0
o_name = ""
next_name = ""
while name < self.file_max and flag < self.file_max:
file_list = [file for file in os.listdir(self.file_o_path) if
os.path.isfile(os.path.join(self.file_o_path, file))]
print(file_list)
next_name = "output_" + str(name + 1) + ".avi"
o_name = "original/output_" + str(name) + ".avi"
if next_name in file_list:
self.pretreatment(0, 0, 640, 240, o_name)
name = name + 1
if name == self.file_max-1:
flag = flag + 1
time.sleep(self.file_time)
self.pretreatment(0, 0, 640, 240, o_name)
def listen2(self):
# 监听指定目录下是否有新的待计算的预处理文件
name = self.file_name
flag = 0
o_name = ""
next_name = ""
while name < self.file_max and flag < self.file_max:
file_list = [file for file in os.listdir(self.file_p_path) if
os.path.isfile(os.path.join(self.file_p_path, file))]
print(file_list)
next_name = "output_" + str(name + 1) + ".mp4"
o_name = "pretreatment/output_" + str(name) + ".mp4"
if next_name in file_list:
self.calculate(640, 240, o_name)
name = name + 1
if name == self.file_max-1:
flag = flag + 1
# time.sleep(self.file_time)
self.calculate(640, 240, o_name)
if __name__ == "__main__":
a = LagAnalysis()
process = multiprocessing.Process(target=a.listen1)
process.start()
process1 = multiprocessing.Process(target=a.listen2)
process1.start()
a.record()
# process.join()
process1.join()
代码B,实现了后端获取计算结果的分页功能
// 代码B
const express = require('express');
const fs = require('fs');
const path = require('path');
const app = express();
const port = 3000;
// const txtDirectory = path.join(__dirname, 'txt_files'); // Adjust this path to your txt files directory
const txtDirectory = path.join("D:/others/python/ts_autotest/private/", 'result');
app.use(express.static('public'));
app.get('/files', (req, res) => {
const page = parseInt(req.query.page) || 1;
const limit = parseInt(req.query.limit) || 20;
fs.readdir(txtDirectory, (err, files) => {
if (err) {
return res.status(500).json({ error: 'Failed to read directory' });
}
const txtFiles = files.filter(file => file.endsWith('.txt'));
const totalFiles = txtFiles.length;
const totalPages = Math.ceil(totalFiles / limit);
const startIndex = (page - 1) * limit;
const endIndex = Math.min(startIndex + limit, totalFiles);
const selectedFiles = txtFiles.slice(startIndex, endIndex);
const fileDataPromises = selectedFiles.map(file => {
const filePath = path.join(txtDirectory, file);
return new Promise((resolve, reject) => {
fs.readFile(filePath, 'utf-8', (err, data) => {
if (err) {
return reject(err);
}
const parsedData = data.split('\n').map(Number);
resolve({ fileName: file, data: parsedData });
});
});
});
Promise.all(fileDataPromises)
.then(fileData => res.json({ files: fileData, totalPages }))
.catch(err => res.status(500).json({ error: 'Failed to read files' }));
});
});
app.listen(port, () => {
console.log(`Server is running at http://localhost:${port}`);
});
代码C,实现了前端展示计算结果的折线图
扫描二维码关注公众号,回复:
17602043 查看本文章

<--- 代码C --->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Text Files to Charts</title>
<style>
body {
font-family: Arial, sans-serif;
}
.container {
display: flex;
flex-direction: column;
flex-wrap: wrap;
margin: 20px;
}
.row {
display: flex;
width: 100%;
margin-bottom: 20px;
}
.chart {
flex: 1;
margin: 0 10px;
}
canvas {
width: 100%;
}
#pagination {
margin-top: 20px;
text-align: center;
}
#pagination button {
margin: 0 5px;
padding: 5px 10px;
}
#modal {
display: none;
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background: white;
padding: 20px;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.5);
}
#modal canvas {
width: 500px;
height: 300px;
}
</style>
</head>
<body>
<div class="container" id="container"></div>
<div id="pagination"></div>
<div id="modal">
<button onclick="closeModal()">Close</button>
<canvas id="zoomChart"></canvas>
</div>
<!-- Include Chart.js library -->
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<script>
const itemsPerPage = 20;
let currentPage = 1;
let totalPages = 1;
async function fetchTextFiles(page) {
const response = await fetch(`/files?page=${page}&limit=${itemsPerPage}`);
const filesData = await response.json();
return filesData;
}
function initializeChart(chartId, data) {
const ctx = document.getElementById(chartId).getContext('2d');
const chart = new Chart(ctx, {
type: 'line',
data: {
labels: data.map((_, index) => `Point ${index + 1}`),
datasets: [{
label: 'Data Points',
data: data,
borderColor: 'rgba(75, 192, 192, 1)',
borderWidth: 2,
fill: false
}]
},
options: {
responsive: true,
scales: {
x: {
beginAtZero: true
},
y: {
beginAtZero: true
}
},
onClick: (event, elements) => {
if (elements.length > 0) {
const elementIndex = elements[0].index;
showModal(data, elementIndex);
}
}
}
});
}
function createRow(data, chartId) {
const row = document.createElement('div');
row.className = 'row';
const chartContainer = document.createElement('div');
chartContainer.className = 'chart';
const canvasElement = document.createElement('canvas');
canvasElement.id = chartId;
chartContainer.appendChild(canvasElement);
row.appendChild(chartContainer);
return row;
}
function updatePaginationControls() {
const paginationContainer = document.getElementById('pagination');
paginationContainer.innerHTML = '';
for (let i = 1; i <= totalPages; i++) {
const button = document.createElement('button');
button.textContent = i;
button.disabled = i === currentPage;
button.addEventListener('click', () => {
currentPage = i;
initializePage();
});
paginationContainer.appendChild(button);
}
}
async function initializePage() {
const container = document.getElementById('container');
container.innerHTML = '';
const filesData = await fetchTextFiles(currentPage);
totalPages = filesData.totalPages;
filesData.files.forEach((fileData, index) => {
const row = createRow(fileData.data, `chart${index + 1}`);
container.appendChild(row);
initializeChart(`chart${index + 1}`, fileData.data);
});
updatePaginationControls();
}
function showModal(data, index) {
const modal = document.getElementById('modal');
const ctx = document.getElementById('zoomChart').getContext('2d');
const zoomData = data.slice(Math.max(0, index - 5), index + 6);
new Chart(ctx, {
type: 'line',
data: {
labels: zoomData.map((_, i) => `Point ${i + 1}`),
datasets: [{
label: 'Zoomed Data Points',
data: zoomData,
borderColor: 'rgba(255, 99, 132, 1)',
borderWidth: 2,
fill: false
}]
},
options: {
responsive: true,
scales: {
x: {
beginAtZero: true
},
y: {
beginAtZero: true
}
}
}
});
modal.style.display = 'block';
}
function closeModal() {
const modal = document.getElementById('modal');
modal.style.display = 'none';
}
document.addEventListener('DOMContentLoaded', function() {
initializePage();
});
</script>
</body>
</html>
项目运行成功后,刷新浏览器,页面将实时显示当前视频片段的卡顿情况(如下图)