Files
Myolotrain/app/static/js/attention_tracker.js
2025-05-13 10:57:14 +08:00

1165 lines
44 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 自注意力追踪模块 - 前端JavaScript实现
*
* 该模块提供了与后端自注意力追踪服务交互的功能,包括:
* 1. 摄像头追踪
* 删除视频追踪功能20250512
*/
// 全局变量
let API_BASE_URL = '';
let TRACKING_API_URL = '';
let trackingPollingManager = null;
// 追踪状态
let isTracking = false;
let cameraStream = null;
let videoElement = null;
let canvasElement = null;
let canvasContext = null;
let detectionCanvasElement = null;
let detectionCanvasContext = null;
let animationFrameId = null;
let selectedTargetId = null;
let selectedClassId = null;
let selectedClassName = null;
// 在文档加载完成后初始化
document.addEventListener('DOMContentLoaded', function() {
// 获取API基础URL
API_BASE_URL = window.API_URL || '/api';
TRACKING_API_URL = `${API_BASE_URL}/tracking`;
// 初始化轮询管理器
try {
if (typeof PollingManager === 'object') {
trackingPollingManager = PollingManager;
console.log('轮询管理器初始化成功');
} else {
console.warn('PollingManager未定义跳过初始化');
// 创建一个空对象,避免后续代码出错
trackingPollingManager = {
startPolling: function(url, interval, callback) {
console.warn('轮询功能不可用');
if (typeof callback === 'function') {
callback({ status: 'error', message: '轮询功能不可用' });
}
},
stopPolling: function() { console.warn('轮询功能不可用'); }
};
}
} catch (error) {
console.error('初始化轮询管理器失败:', error);
// 创建一个空对象,避免后续代码出错
trackingPollingManager = {
startPolling: function(url, interval, callback) {
console.warn('轮询功能不可用');
if (typeof callback === 'function') {
callback({ status: 'error', message: '轮询功能不可用' });
}
},
stopPolling: function() { console.warn('轮询功能不可用'); }
};
}
});
/**
* 初始化追踪页面
*/
function initTrackingPage() {
console.log('初始化追踪页面');
// 加载模型列表
loadModelsForTracking();
// 绑定摄像头追踪相关事件
initCameraTracking();
}
/**
* 加载模型列表
*/
function loadModelsForTracking() {
fetch(`${API_URL}/models`)
.then(response => response.json())
.then(models => {
// 填充摄像头追踪模型选择器
const cameraTrackingModelSelect = document.getElementById('camera-tracking-model-select');
if (cameraTrackingModelSelect) {
// 保留默认选项
const defaultOption = cameraTrackingModelSelect.querySelector('option');
cameraTrackingModelSelect.innerHTML = '';
cameraTrackingModelSelect.appendChild(defaultOption);
// 添加模型选项
models.forEach(model => {
const option = document.createElement('option');
option.value = model.id;
option.textContent = `${model.name} (${model.type}, ${model.task})`;
cameraTrackingModelSelect.appendChild(option);
});
}
})
.catch(error => {
console.error('加载模型列表失败:', error);
});
}
/**
* 初始化摄像头追踪
*/
function initCameraTracking() {
console.log('初始化摄像头追踪');
// 获取DOM元素
videoElement = document.getElementById('camera-video');
// 移除旧的画布元素(如果存在)
const oldCanvas = document.getElementById('tracking-canvas');
if (oldCanvas) {
oldCanvas.parentNode.removeChild(oldCanvas);
}
// 创建新的画布元素 - 用于捕获视频帧
canvasElement = document.createElement('canvas');
canvasElement.id = 'tracking-canvas';
canvasElement.className = 'position-absolute top-0 start-0';
canvasElement.style.width = '100%';
canvasElement.style.height = '100%';
canvasElement.style.zIndex = '1000';
canvasElement.style.position = 'absolute';
canvasElement.style.top = '0';
canvasElement.style.left = '0';
canvasElement.style.display = 'block';
canvasElement.style.pointerEvents = 'none';
canvasElement.style.backgroundColor = 'transparent';
// 创建新的检测框画布元素 - 专门用于绘制检测框和追踪框
detectionCanvasElement = document.createElement('canvas');
detectionCanvasElement.id = 'detection-canvas';
detectionCanvasElement.className = 'position-absolute top-0 start-0';
detectionCanvasElement.style.width = '100%';
detectionCanvasElement.style.height = '100%';
detectionCanvasElement.style.zIndex = '1001'; // 确保在视频和主画布之上
detectionCanvasElement.style.position = 'absolute';
detectionCanvasElement.style.top = '0';
detectionCanvasElement.style.left = '0';
detectionCanvasElement.style.display = 'block';
detectionCanvasElement.style.pointerEvents = 'none';
detectionCanvasElement.style.backgroundColor = 'transparent';
// 将画布添加到视频容器中
if (videoElement && videoElement.parentNode) {
videoElement.parentNode.appendChild(canvasElement);
videoElement.parentNode.appendChild(detectionCanvasElement);
console.log('已创建并添加新的画布元素和检测框画布元素');
} else {
console.error('无法添加画布元素,因为视频元素或其父节点不存在');
}
// 绑定摄像头追踪按钮事件
const startCameraTrackingButton = document.getElementById('start-camera-tracking');
const stopCameraTrackingButton = document.getElementById('stop-camera-tracking');
if (startCameraTrackingButton) {
startCameraTrackingButton.addEventListener('click', startCameraTracking);
}
if (stopCameraTrackingButton) {
stopCameraTrackingButton.addEventListener('click', stopCameraTracking);
}
// 绑定重置追踪器按钮事件
const resetTrackingButton = document.getElementById('reset-tracking');
if (resetTrackingButton) {
resetTrackingButton.addEventListener('click', resetTracker);
}
}
/**
* 开始摄像头追踪
*/
function startCameraTracking() {
try {
console.log('开始摄像头追踪');
// 检查是否已经在追踪
if (isTracking) {
console.log('已经在追踪中');
return;
}
// 获取模型ID
const modelSelect = document.getElementById('camera-tracking-model-select');
const modelId = modelSelect ? modelSelect.value : 'default';
console.log('模型ID:', modelId);
// 获取追踪参数
let confThreshold = 0.25; // 默认值
let iouThreshold = 0.45; // 默认值
const confThresholdElement = document.getElementById('camera-conf-threshold');
if (confThresholdElement) {
confThreshold = confThresholdElement.value;
console.log('置信度阈值:', confThreshold);
} else {
console.warn('未找到置信度阈值元素,使用默认值:', confThreshold);
}
const iouThresholdElement = document.getElementById('camera-iou-threshold');
if (iouThresholdElement) {
iouThreshold = iouThresholdElement.value;
console.log('IoU阈值:', iouThreshold);
} else {
console.warn('未找到IoU阈值元素使用默认值:', iouThreshold);
}
// 显示追踪容器
const trackingContainer = document.getElementById('camera-tracking-container');
if (trackingContainer) {
trackingContainer.style.display = 'block';
} else {
console.warn('未找到追踪容器元素');
}
// 显示停止按钮,隐藏开始按钮
const startButton = document.getElementById('start-camera-tracking');
const stopButton = document.getElementById('stop-camera-tracking');
if (startButton) startButton.style.display = 'none';
if (stopButton) stopButton.style.display = 'inline-block';
// 更新追踪状态
const trackingStatus = document.getElementById('camera-tracking-status');
if (trackingStatus) {
trackingStatus.textContent = '正在初始化摄像头...';
} else {
console.warn('未找到追踪状态元素');
}
} catch (error) {
console.error('启动摄像头追踪时发生错误:', error);
alert('启动摄像头追踪失败: ' + error.message);
}
// 获取摄像头设备ID和追踪状态元素
const cameraSelect = document.getElementById('camera-select');
const deviceId = cameraSelect ? cameraSelect.value : '';
const trackingStatus = document.getElementById('camera-tracking-status');
// 重置追踪器
resetTracker()
.then(() => {
try {
console.log('重置追踪器成功,准备打开摄像头');
// 打开摄像头
const constraints = {
video: deviceId ? { deviceId: { exact: deviceId } } : true
};
console.log('摄像头约束:', constraints);
return navigator.mediaDevices.getUserMedia(constraints);
} catch (error) {
console.error('准备打开摄像头时出错:', error);
throw error;
}
})
.then(stream => {
try {
console.log('成功获取摄像头流');
// 保存流
cameraStream = stream;
// 设置视频源
if (videoElement) {
videoElement.srcObject = stream;
console.log('已设置视频源');
// 等待视频元数据加载
return new Promise(resolve => {
videoElement.onloadedmetadata = () => {
console.log('视频元数据已加载');
videoElement.play();
resolve();
};
// 添加错误处理
videoElement.onerror = (e) => {
console.error('视频加载错误:', e);
resolve(); // 继续流程
};
// 添加超时处理
setTimeout(() => {
if (videoElement.readyState === 0) {
console.warn('视频元数据加载超时,继续流程');
resolve();
}
}, 5000);
});
} else {
console.error('视频元素不存在');
throw new Error('视频元素不存在');
}
} catch (error) {
console.error('设置视频源时出错:', error);
throw error;
}
})
.then(() => {
try {
console.log('准备设置画布');
// 设置画布大小
if (canvasElement && videoElement) {
// 确保画布大小与视频实际大小匹配
// 如果视频尺寸不可用,使用固定的尺寸
const videoWidth = videoElement.videoWidth || 640;
const videoHeight = videoElement.videoHeight || 480;
// 确保画布尺寸不为0
canvasElement.width = videoWidth > 0 ? videoWidth : 640;
canvasElement.height = videoHeight > 0 ? videoHeight : 480;
console.log(`设置画布尺寸: ${canvasElement.width} x ${canvasElement.height}`);
// 获取2D绘图上下文
canvasContext = canvasElement.getContext('2d', { alpha: true });
if (!canvasContext) {
console.error('无法获取2D绘图上下文');
}
// 设置检测框画布的尺寸
if (detectionCanvasElement) {
detectionCanvasElement.width = canvasElement.width;
detectionCanvasElement.height = canvasElement.height;
console.log(`设置检测框画布尺寸: ${detectionCanvasElement.width} x ${detectionCanvasElement.height}`);
// 获取检测框画布的2D绘图上下文
detectionCanvasContext = detectionCanvasElement.getContext('2d', { alpha: true });
if (!detectionCanvasContext) {
console.error('无法获取检测框画布的2D绘图上下文');
}
} else {
console.error('检测框画布元素不存在');
}
// 只在调试模式下绘制测试矩形
window.DEBUG_MODE = false; // 默认关闭调试模式
// 获取视频容器元素
const videoContainer = videoElement.parentElement;
if (videoContainer) {
// 确保容器使用相对定位,这样画布的绝对定位才能正确工作
videoContainer.style.position = 'relative';
videoContainer.style.overflow = 'hidden';
console.log('设置视频容器样式:', videoContainer.style.cssText);
}
console.log('已设置画布样式');
} else {
console.error('画布或视频元素不存在');
if (!canvasElement) console.error('画布元素不存在');
if (!videoElement) console.error('视频元素不存在');
}
// 更新追踪状态
if (trackingStatus) {
trackingStatus.textContent = '追踪中...';
console.log('已更新追踪状态为"追踪中..."');
}
// 开始追踪
isTracking = true;
console.log('开始追踪调用processFrame');
requestAnimationFrame(processFrame);
} catch (error) {
console.error('设置画布时出错:', error);
throw error;
}
})
.catch(error => {
console.error('启动摄像头追踪失败:', error);
// 更新追踪状态
if (trackingStatus) {
trackingStatus.textContent = `启动失败: ${error.message}`;
}
// 显示错误提示
alert('启动摄像头追踪失败: ' + error.message);
});
}
/**
* 停止摄像头追踪
*/
function stopCameraTracking() {
console.log('停止摄像头追踪');
// 停止追踪
isTracking = false;
// 取消动画帧请求
if (animationFrameId) {
cancelAnimationFrame(animationFrameId);
animationFrameId = null;
console.log('已取消动画帧请求');
}
// 停止摄像头流
if (cameraStream) {
cameraStream.getTracks().forEach(track => {
track.stop();
console.log('已停止摄像头轨道:', track.kind);
});
cameraStream = null;
console.log('已停止摄像头流');
}
// 清除视频源
if (videoElement) {
videoElement.srcObject = null;
console.log('已清除视频源');
}
// 清除画布
if (canvasContext && canvasElement) {
canvasContext.clearRect(0, 0, canvasElement.width, canvasElement.height);
console.log('已清除主画布');
}
// 清除检测框画布
if (detectionCanvasContext && detectionCanvasElement) {
detectionCanvasContext.clearRect(0, 0, detectionCanvasElement.width, detectionCanvasElement.height);
console.log('已清除检测框画布');
}
// 显示开始按钮,隐藏停止按钮
const startButton = document.getElementById('start-camera-tracking');
const stopButton = document.getElementById('stop-camera-tracking');
if (startButton) startButton.style.display = 'inline-block';
if (stopButton) stopButton.style.display = 'none';
console.log('已更新按钮显示状态');
// 更新追踪状态
const trackingStatus = document.getElementById('camera-tracking-status');
if (trackingStatus) {
trackingStatus.textContent = '已停止';
console.log('已更新追踪状态为"已停止"');
}
// 重置选中的目标
selectedTargetId = null;
selectedClassId = null;
selectedClassName = null;
console.log('已重置选中的目标');
// 清空检测到的目标列表
updateDetectedObjectsList([]);
// 清空追踪目标列表
updateTrackedObjectsList([]);
console.log('已清空目标列表');
}
/**
* 重置追踪器
*/
function resetTracker() {
console.log('重置追踪器');
return Promise.resolve({});
}
/**
* 根据ID生成颜色
*/
function getColorById(id) {
// 使用固定的颜色列表
const colors = [
[255, 0, 0], // 红色
[0, 255, 0], // 绿色
[0, 0, 255], // 蓝色
[255, 255, 0], // 黄色
[255, 0, 255], // 紫色
[0, 255, 255], // 青色
[128, 0, 0], // 深红色
[0, 128, 0], // 深绿色
[0, 0, 128], // 深蓝色
[128, 128, 0] // 橄榄色
];
// 使用ID取模选择颜色
return colors[id % colors.length];
}
/**
* 更新检测到的目标列表
*/
function updateDetectedObjectsList(detections) {
console.log('更新检测到的目标列表:', detections);
// 获取检测到的目标列表容器
const detectedObjectsList = document.getElementById('detected-objects-list');
if (!detectedObjectsList) {
console.warn('未找到检测到的目标列表容器');
return;
}
// 清空列表
detectedObjectsList.innerHTML = '';
// 如果没有检测结果,显示提示信息
if (!detections || detections.length === 0) {
const emptyItem = document.createElement('li');
emptyItem.className = 'list-group-item text-center';
emptyItem.textContent = '未检测到目标';
detectedObjectsList.appendChild(emptyItem);
return;
}
// 按类别分组
const groupedDetections = {};
detections.forEach(det => {
const className = det.class_name || 'unknown';
const classId = det.class_id !== undefined ? det.class_id : -1;
if (!groupedDetections[className]) {
groupedDetections[className] = {
count: 0,
classId: classId,
items: []
};
}
groupedDetections[className].count++;
groupedDetections[className].items.push(det);
});
// 为每个类别创建列表项
Object.keys(groupedDetections).forEach(className => {
const group = groupedDetections[className];
const listItem = document.createElement('li');
listItem.className = 'list-group-item d-flex justify-content-between align-items-center';
// 创建类别名称和数量标签
const nameSpan = document.createElement('span');
nameSpan.textContent = `${className} (ID: ${group.classId})`;
const countBadge = document.createElement('span');
countBadge.className = 'badge bg-primary rounded-pill';
countBadge.textContent = group.count;
// 创建追踪按钮
const trackButton = document.createElement('button');
// 检查是否是当前选中的类别
if (selectedClassId !== null && selectedClassId == group.classId) {
trackButton.className = 'btn btn-sm btn-danger ms-2';
trackButton.textContent = '取消追踪';
} else {
trackButton.className = 'btn btn-sm btn-success ms-2';
trackButton.textContent = '追踪';
}
trackButton.dataset.classId = group.classId;
trackButton.dataset.className = className;
// 绑定追踪按钮点击事件
trackButton.addEventListener('click', function() {
console.log('追踪按钮被点击');
// 检查当前按钮状态
if (this.textContent === '追踪') {
// 设置选中的类别
selectedClassId = group.classId;
selectedClassName = className;
console.log(`选中类别: ${className} (ID: ${group.classId})`);
// 更新按钮状态
const allTrackButtons = document.querySelectorAll('#detected-objects-list button');
allTrackButtons.forEach(btn => {
if (btn.dataset.classId == group.classId) {
btn.textContent = '取消追踪';
btn.className = 'btn btn-sm btn-danger ms-2';
} else {
btn.textContent = '追踪';
btn.className = 'btn btn-sm btn-success ms-2';
}
});
} else {
// 取消选中
selectedClassId = null;
selectedClassName = null;
this.textContent = '追踪';
this.className = 'btn btn-sm btn-success ms-2';
console.log('取消选中类别');
// 发送取消追踪请求
const cancelTrackingFormData = new FormData();
cancelTrackingFormData.append('cancel_tracking', 'true');
fetch(`${TRACKING_API_URL}/track-frame`, {
method: 'POST',
body: cancelTrackingFormData
})
.then(response => {
if (!response.ok) {
throw new Error(`取消追踪请求失败: ${response.status} ${response.statusText}`);
}
console.log('已发送取消追踪请求');
})
.catch(error => {
console.error('取消追踪请求错误:', error);
});
}
});
// 将元素添加到列表项
listItem.appendChild(nameSpan);
const rightGroup = document.createElement('div');
rightGroup.appendChild(countBadge);
rightGroup.appendChild(trackButton);
listItem.appendChild(rightGroup);
// 将列表项添加到容器
detectedObjectsList.appendChild(listItem);
});
}
/**
* 更新追踪目标列表
*/
function updateTrackedObjectsList(tracks) {
console.log('更新追踪目标列表:', tracks);
// 获取追踪目标列表容器
const trackedObjectsList = document.getElementById('tracked-objects-list');
if (!trackedObjectsList) {
console.warn('未找到追踪目标列表容器');
return;
}
// 清空列表
trackedObjectsList.innerHTML = '';
// 如果没有追踪结果,显示提示信息
if (!tracks || tracks.length === 0) {
const emptyItem = document.createElement('li');
emptyItem.className = 'list-group-item text-center';
// 根据是否选择了追踪目标显示不同的提示信息
if (selectedClassId !== null) {
emptyItem.textContent = `正在等待类别ID为 ${selectedClassId} 的目标出现...`;
emptyItem.style.color = 'blue';
} else {
emptyItem.textContent = '未选择追踪目标';
}
trackedObjectsList.appendChild(emptyItem);
return;
}
// 如果在单目标追踪模式下,但没有该类别的追踪结果
if (selectedClassId !== null && !tracks.some(track => track.class_id == selectedClassId)) {
const emptyItem = document.createElement('li');
emptyItem.className = 'list-group-item text-center';
emptyItem.textContent = `正在等待类别ID为 ${selectedClassId} 的目标出现...`;
emptyItem.style.color = 'blue';
trackedObjectsList.appendChild(emptyItem);
return;
}
// 在单目标追踪模式下,只显示选中类别的追踪目标
const tracksToShow = selectedClassId !== null
? tracks.filter(track => track.class_id == selectedClassId)
: tracks;
// 为每个追踪目标创建列表项
tracksToShow.forEach(track => {
const listItem = document.createElement('li');
listItem.className = 'list-group-item d-flex justify-content-between align-items-center';
// 创建ID和类别标签
const idSpan = document.createElement('span');
// 确保显示正确的类别名称和ID
const className = track.class_name || '未知';
const classId = track.class_id !== undefined ? track.class_id : '未知';
const confidence = Math.round((track.confidence || 0) * 100);
idSpan.textContent = `ID: ${track.id} - ${className} (类别ID: ${classId}, 置信度: ${confidence}%)`;
// 根据ID生成颜色
const color = getColorById(track.id);
const colorStr = `rgb(${color[0]}, ${color[1]}, ${color[2]})`;
// 创建颜色标记
const colorMark = document.createElement('span');
colorMark.className = 'color-mark me-2';
colorMark.style.display = 'inline-block';
colorMark.style.width = '12px';
colorMark.style.height = '12px';
colorMark.style.backgroundColor = colorStr;
colorMark.style.borderRadius = '50%';
// 将颜色标记添加到ID标签前面
idSpan.insertBefore(colorMark, idSpan.firstChild);
// 将元素添加到列表项
listItem.appendChild(idSpan);
// 将列表项添加到容器
trackedObjectsList.appendChild(listItem);
});
// 如果过滤后没有追踪目标,显示提示信息
if (selectedClassId !== null && tracksToShow.length === 0 && tracks.length > 0) {
const emptyItem = document.createElement('li');
emptyItem.className = 'list-group-item text-center';
emptyItem.textContent = `未找到类别ID为 ${selectedClassId} 的追踪目标`;
emptyItem.style.color = 'orange';
trackedObjectsList.appendChild(emptyItem);
}
}
/**
* 绘制检测结果和追踪结果
*/
function drawDetectionsAndTracks(detections, tracks) {
console.log('绘制检测和追踪结果:', {
detections: detections ? detections.length : 0,
tracks: tracks ? tracks.length : 0
});
// 如果检测框画布不存在,则退出
if (!detectionCanvasElement || !detectionCanvasContext) {
console.error('检测框画布不存在,无法绘制');
return;
}
// 清除检测框画布
detectionCanvasContext.clearRect(0, 0, detectionCanvasElement.width, detectionCanvasElement.height);
// 绘制检测框
if (detections && detections.length > 0) {
detections.forEach(det => {
// 如果在单目标追踪模式下,只显示选中类别的检测框
if (selectedClassId !== null && det.class_id != selectedClassId) {
return; // 跳过非选中类别的检测框
}
// 获取边界框
const bbox = det.bbox;
if (!bbox || bbox.length !== 4) {
console.warn('无效的边界框:', bbox);
return;
}
// 计算边界框坐标
const x = bbox[0];
const y = bbox[1];
const width = bbox[2] - bbox[0];
const height = bbox[3] - bbox[1];
// 设置检测框样式
detectionCanvasContext.strokeStyle = 'rgba(0, 255, 0, 0.8)'; // 绿色
detectionCanvasContext.lineWidth = 2;
detectionCanvasContext.setLineDash([]); // 实线
// 绘制检测框
detectionCanvasContext.beginPath();
detectionCanvasContext.rect(x, y, width, height);
detectionCanvasContext.stroke();
// 绘制类别标签
const label = `${det.class_name} (${Math.round(det.confidence * 100)}%)`;
detectionCanvasContext.font = '14px Arial';
detectionCanvasContext.fillStyle = 'rgba(0, 255, 0, 0.8)';
detectionCanvasContext.fillRect(x, y - 20, detectionCanvasContext.measureText(label).width + 10, 20);
detectionCanvasContext.fillStyle = 'black';
detectionCanvasContext.fillText(label, x + 5, y - 5);
});
}
// 绘制追踪框
if (tracks && tracks.length > 0) {
tracks.forEach(track => {
// 如果在单目标追踪模式下,只显示选中类别的追踪框
if (selectedClassId !== null && track.class_id != selectedClassId) {
return; // 跳过非选中类别的追踪框
}
// 获取边界框
const bbox = track.bbox;
if (!bbox || bbox.length !== 4) {
console.warn('无效的追踪边界框:', bbox);
return;
}
// 计算边界框坐标
const x = bbox[0];
const y = bbox[1];
const width = bbox[2] - bbox[0];
const height = bbox[3] - bbox[1];
// 根据ID生成颜色
const color = getColorById(track.id);
const colorStr = `rgba(${color[0]}, ${color[1]}, ${color[2]}, 0.8)`;
// 设置追踪框样式
detectionCanvasContext.strokeStyle = colorStr;
detectionCanvasContext.lineWidth = 2;
detectionCanvasContext.setLineDash([5, 5]); // 虚线
// 绘制追踪框
detectionCanvasContext.beginPath();
detectionCanvasContext.rect(x, y, width, height);
detectionCanvasContext.stroke();
// 绘制ID标签
const label = `ID: ${track.id} - ${track.class_name}`;
detectionCanvasContext.font = '14px Arial';
detectionCanvasContext.fillStyle = colorStr;
detectionCanvasContext.fillRect(x, y - 20, detectionCanvasContext.measureText(label).width + 10, 20);
detectionCanvasContext.fillStyle = 'white';
detectionCanvasContext.fillText(label, x + 5, y - 5);
// 绘制轨迹
if (track.trajectory && track.trajectory.length > 1) {
detectionCanvasContext.strokeStyle = colorStr;
detectionCanvasContext.lineWidth = 2;
detectionCanvasContext.setLineDash([]); // 实线
detectionCanvasContext.beginPath();
detectionCanvasContext.moveTo(track.trajectory[0][0], track.trajectory[0][1]);
for (let i = 1; i < track.trajectory.length; i++) {
detectionCanvasContext.lineTo(track.trajectory[i][0], track.trajectory[i][1]);
}
detectionCanvasContext.stroke();
}
});
}
}
// 帧率控制变量
let lastFrameTime = 0;
const FRAME_INTERVAL = 200; // 每200毫秒处理一帧约等于5fps
/**
* 处理视频帧
*/
function processFrame() {
// 如果不在追踪状态,则退出
if (!isTracking) {
console.log('未在追踪状态退出processFrame');
return;
}
// 如果视频或画布元素不存在,则退出
if (!videoElement || !canvasElement || !canvasContext) {
console.error('视频或画布元素不存在退出processFrame');
return;
}
// 帧率控制 - 限制处理频率,减少资源占用
const currentTime = Date.now();
if (currentTime - lastFrameTime < FRAME_INTERVAL) {
// 如果距离上一帧处理时间不足FRAME_INTERVAL则跳过当前帧处理
animationFrameId = requestAnimationFrame(processFrame);
return;
}
lastFrameTime = currentTime;
try {
// 绘制视频帧到画布
canvasContext.drawImage(videoElement, 0, 0, canvasElement.width, canvasElement.height);
// 将画布转换为Blob对象
canvasElement.toBlob(blob => {
// 创建一个文件对象
const file = new File([blob], "frame.jpg", { type: "image/jpeg" });
// 创建FormData对象
const formData = new FormData();
formData.append('file', file); // 使用'file'而不是'image'与API期望一致
// 获取模型ID
const modelSelect = document.getElementById('camera-tracking-model-select');
const modelId = modelSelect ? modelSelect.value : 'default';
// 添加模型ID参数
if (modelId && modelId !== 'default') {
formData.append('model_id', modelId);
}
// 添加置信度和IoU阈值参数
formData.append('conf_thres', 0.25);
formData.append('iou_thres', 0.45);
// 发送检测请求
fetch(`${API_BASE_URL}/detection/`, {
method: 'POST',
body: formData
})
.then(response => {
if (!response.ok) {
throw new Error(`检测请求失败: ${response.status} ${response.statusText}`);
}
return response.json();
})
.catch(error => {
console.error('检测请求错误:', error);
// 返回一个空的检测结果,避免前端崩溃
return { detections: [] };
})
.then(response => {
// 提取检测结果
let detections = [];
if (response && response.detections && Array.isArray(response.detections)) {
// 使用detections字段
detections = response.detections;
console.log('使用API返回的检测结果');
} else if (response && Array.isArray(response)) {
// 直接使用数组
detections = response;
console.log('使用API返回的检测结果数组');
} else {
console.log('未找到有效的检测结果,使用模拟数据');
// 获取画布尺寸
const canvasWidth = canvasElement.width;
const canvasHeight = canvasElement.height;
// 创建默认的测试检测结果
detections = [
{
bbox: [Math.floor(canvasWidth * 0.2), Math.floor(canvasHeight * 0.2),
Math.floor(canvasWidth * 0.4), Math.floor(canvasHeight * 0.6)],
class_id: 0,
confidence: 0.9,
class_name: "person"
},
{
bbox: [Math.floor(canvasWidth * 0.6), Math.floor(canvasHeight * 0.3),
Math.floor(canvasWidth * 0.8), Math.floor(canvasHeight * 0.7)],
class_id: 1,
confidence: 0.85,
class_name: "car"
}
];
}
// 继续处理检测结果
console.log('检测结果:', detections);
// 如果没有检测结果,使用默认的测试检测结果
if (detections.length === 0 && window.DEBUG_MODE) {
console.log('没有检测结果,使用默认的测试检测结果');
// 获取画布尺寸
const canvasWidth = canvasElement.width;
const canvasHeight = canvasElement.height;
// 创建默认的测试检测结果
detections = [
{
bbox: [Math.floor(canvasWidth * 0.2), Math.floor(canvasHeight * 0.2),
Math.floor(canvasWidth * 0.4), Math.floor(canvasHeight * 0.6)],
class_id: 0,
confidence: 0.9,
class_name: "person"
},
{
bbox: [Math.floor(canvasWidth * 0.6), Math.floor(canvasHeight * 0.3),
Math.floor(canvasWidth * 0.8), Math.floor(canvasHeight * 0.7)],
class_id: 0,
confidence: 0.85,
class_name: "person"
}
];
}
console.log('检测结果:', detections);
// 创建一个全局变量来存储格式化后的检测结果
window.formattedDetections = [];
// 格式化检测结果
if (Array.isArray(detections)) {
window.formattedDetections = detections.map(det => {
// 处理不同格式的边界框
let bbox;
if (det.bbox) {
bbox = det.bbox;
} else {
// 默认边界框
bbox = [0, 0, 100, 100];
}
// 确保边界框坐标是数字
bbox = bbox.map(coord => {
const num = parseFloat(coord);
return isNaN(num) ? 0 : num;
});
return {
bbox: bbox,
class_id: det.class_id || 0,
confidence: det.confidence || 0.5,
class_name: det.class_name || 'object'
};
});
}
console.log('格式化后的检测结果:', window.formattedDetections);
// 更新检测到的目标列表
updateDetectedObjectsList(window.formattedDetections);
// 发送追踪请求
const trackingFormData = new FormData();
trackingFormData.append('image', file); // 这里使用'image'是正确的与API期望一致
trackingFormData.append('detections', JSON.stringify(window.formattedDetections));
// 添加追踪参数
if (selectedClassId !== null) {
trackingFormData.append('target_class_id', selectedClassId);
trackingFormData.append('enable_tracking', 'true');
console.log('追踪请求使用类别ID:', selectedClassId);
} else {
// 确保明确设置为false
trackingFormData.append('enable_tracking', 'false');
}
// 明确设置cancel_tracking参数
trackingFormData.append('cancel_tracking', 'false');
// 注意tracking API不需要model_id参数
console.log('发送追踪请求,检测结果数量:', window.formattedDetections.length);
// 发送追踪请求
fetch(`${TRACKING_API_URL}/track-frame`, {
method: 'POST',
body: trackingFormData
})
.then(response => {
if (!response.ok) {
throw new Error(`追踪请求失败: ${response.status} ${response.statusText}`);
}
return response.json();
})
.catch(error => {
console.error('追踪请求错误:', error);
// 返回一个空的追踪结果,避免前端崩溃
return { tracks: [] };
})
.then(response => {
// 如果没有追踪结果,使用检测结果创建模拟的追踪结果
if (!response.tracks || response.tracks.length === 0) {
console.log('没有追踪结果,使用检测结果创建模拟的追踪结果');
// 使用检测结果创建模拟的追踪结果
const mockTracks = window.formattedDetections.map((det, index) => {
return {
id: index + 1, // 使用索引作为ID
bbox: det.bbox,
class_id: det.class_id,
class_name: det.class_name,
confidence: det.confidence,
trajectory: [
[det.bbox[0], det.bbox[1]], // 左上角作为轨迹起点
]
};
});
return { tracks: mockTracks };
}
return response;
})
.then(response => {
console.log('追踪响应:', response);
// 提取追踪结果
let tracks = [];
if (response && response.tracks && Array.isArray(response.tracks)) {
// 使用tracks字段
tracks = response.tracks;
console.log('使用tracks字段中的追踪结果');
} else if (response && Array.isArray(response)) {
// 直接使用数组
tracks = response;
console.log('使用直接返回的追踪结果数组');
} else {
console.log('未找到有效的追踪结果');
}
// 如果选择了特定类别,只显示该类别的检测框
let detectionsToShow = window.formattedDetections;
if (selectedClassId !== null) {
detectionsToShow = window.formattedDetections.filter(det =>
det.class_id == selectedClassId || det.class_name == selectedClassName
);
console.log(`仅显示类别 ${selectedClassName} (ID: ${selectedClassId}) 的检测框,共 ${detectionsToShow.length}`);
}
// 保存当前的检测结果和追踪结果,以便在点击追踪按钮时能够重新绘制
// 只保存必要的数据,减少内存占用
window.currentDetections = detectionsToShow.map(det => ({
bbox: det.bbox,
class_id: det.class_id,
class_name: det.class_name,
confidence: det.confidence
}));
window.currentTracks = tracks.map(track => ({
id: track.id,
bbox: track.bbox,
class_id: track.class_id,
class_name: track.class_name,
confidence: track.confidence,
trajectory: track.trajectory ? track.trajectory.slice(-10) : [] // 只保留最近10个轨迹点
}));
// 绘制检测结果和追踪结果
drawDetectionsAndTracks(detectionsToShow, tracks);
// 更新追踪目标列表
updateTrackedObjectsList(tracks);
// 继续处理下一帧
animationFrameId = requestAnimationFrame(processFrame);
})
.catch(error => {
console.error('处理帧失败:', error);
// 继续处理下一帧
animationFrameId = requestAnimationFrame(processFrame);
});
});
}, 'image/jpeg');
} catch (error) {
console.error('处理视频帧时出错:', error);
// 继续处理下一帧
animationFrameId = requestAnimationFrame(processFrame);
}
}
// 在页面加载时注册追踪页面初始化函数
document.addEventListener('DOMContentLoaded', function() {
// 将追踪页面初始化函数添加到页面加载函数中
if (typeof loadPage === 'function') {
const originalLoadPage = loadPage;
loadPage = function(page) {
originalLoadPage(page);
if (page === 'tracking') {
initTrackingPage();
}
};
}
});