設計思路,如下:
? ? ? ? ? 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 osclass 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_nameout_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 + 1else:out.release()flag = 0name = name + 1if 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是讀取到的幀,如果讀取失敗,則為Noneret, 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:# 如果讀取幀失敗,退出循環breakcap.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是讀取到的幀,如果讀取失敗,則為Noneret, frame = cap.read()# 如果正確讀取幀,進行處理if ret:score, diff = self.compare_images(old_frame, frame)file.write(str(score))file.write("\n")else:# 如果讀取幀失敗,退出循環breakcap.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)# 計算SSIMscore, diff = ssim(grayA, grayB, full=True)diff = (diff * 255).astype("uint8")return score, diffdef 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_nameflag = 0o_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) ifos.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 + 1if name == self.file_max-1:flag = flag + 1time.sleep(self.file_time)self.pretreatment(0, 0, 640, 240, o_name)def listen2(self):# 監聽指定目錄下是否有新的待計算的預處理文件name = self.file_nameflag = 0o_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) ifos.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 + 1if 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,實現了前端展示計算結果的折線圖
<--- 代碼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>
項目運行成功后,刷新瀏覽器,頁面將實時顯示當前視頻片段的卡頓情況(如下圖)