修改后端'实验名称(备注信息)': f"{video_name}/{area_name}"中的area_name,使用originalName,使得导出的excel表格中显示的是对应关联ROI的名称,而不是roi,如何修改,优化代码,代码如下:import os
import cv2
import base64
import json
import numpy as np
import pandas as pd
from PIL import Image, ImageDraw, ImageFont
from collections import defaultdict
from channels.generic.websocket import AsyncWebsocketConsumer
from django.conf import settings
from ultralytics import YOLO
import time
import asyncio
import logging
from index.page_4_1_auto_set import image_detect
logger = logging.getLogger(__name__)
class VideoAnalyzer:
def __init__(self, scale_info, movement_threshold=0.5):
"""
初始化视频分析器
:param scale_info: 比例尺信息字典
:param movement_threshold: 移动状态判断阈值(厘米/秒)
"""
# 计算像素到厘米的转换因子
if 'pixels_per_cm' in scale_info:
self.pixels_per_cm = scale_info['pixels_per_cm']
else:
self.pixels_per_cm = scale_info['pixel_length'] / scale_info['real_length']
self.movement_threshold = movement_threshold
# 存储中间结果
self.frame_data = [] # 存储每帧的检测结果
self.trajectories = defaultdict(list) # {roi_key: [center_points]}
self.results = {} # 最终计算结果
self.movement_state_data = defaultdict(list) # 存储移动状态数据
self.acceleration_state_data = defaultdict(list) # 存储加速度状态数据
self.in_analysis_zone = defaultdict(bool) # 跟踪是否在分析区内
self.roi_area_mapping = {} # 存储ROI到分析区的映射
def set_roi_area_mapping(self, roi_area_mapping):
"""设置ROI到分析区的映射"""
self.roi_area_mapping = roi_area_mapping
def process_frame(self, frame_index, timestamp, centers_dict, in_analysis_zone_dict):
"""
处理每帧的检测结果
:param frame_index: 帧索引
:param timestamp: 当前帧的时间戳(秒)
:param centers_dict: 每个ROI的中心点坐标 {roi_key: (x, y)}
:param in_analysis_zone_dict: 每个ROI是否在分析区内 {roi_key: bool}
"""
# 存储当前帧的检测结果
self.frame_data.append({
'frame_index': frame_index,
'timestamp': timestamp,
'centers': centers_dict,
'in_analysis_zone': in_analysis_zone_dict
})
# 更新每个ROI的轨迹
for roi_key, center in centers_dict.items():
self.trajectories[roi_key].append((
timestamp,
center,
in_analysis_zone_dict.get(roi_key, False)
))
# 更新当前是否在分析区内的状态
self.in_analysis_zone[roi_key] = in_analysis_zone_dict.get(roi_key, False)
def calculate_movement_metrics(self):
"""计算运动指标"""
for roi_key, trajectory in self.trajectories.items():
if len(trajectory) < 2:
# 轨迹点不足,无法计算
self.results[roi_key] = {
'total_distance_cm': 0,
'frame_distances': [],
'analysis_zone_distances': [],
'average_speed_cm_s': 0,
'max_speed_cm_s': 0,
'min_speed_cm_s': 0,
'distances': [],
'speeds': [],
'accelerations': [],
'movement_state_data': [],
'acceleration_data': [],
'acceleration_states': [],
'movement_episodes': [], # 新增:存储移动事件
'area_id': self.roi_area_mapping.get(roi_key, 'unknown') # 添加区域ID
}
continue
# 初始化指标
frame_distances = [] # 每帧移动距离
analysis_zone_distances = [] # 分析区内移动距离
speeds = [] # 每帧速度
accelerations = [] # 每帧加速度
movement_states = [] # 每帧移动状态
acceleration_states = [] # 每帧加速度状态
total_distance = 0 # 总移动距离
total_analysis_zone_distance = 0 # 分析区内总移动距离
movement_episodes = [] # 存储移动事件
# 计算每帧的距离和速度
for i in range(1, len(trajectory)):
prev_time, prev_center, prev_in_zone = trajectory[i - 1]
curr_time, curr_center, curr_in_zone = trajectory[i]
# 检查中心点是否为None(目标未检测到)
if prev_center is None or curr_center is None:
# 如果任一中心点为None,则距离和速度设为0
cm_distance = 0
speed = 0
movement_state = '静止'
else:
# 计算像素距离
pixel_distance = self._euclidean_distance(prev_center, curr_center)
# 转换为厘米
cm_distance = pixel_distance / self.pixels_per_cm
time_diff = curr_time - prev_time
# 计算速度 (厘米/秒)
speed = cm_distance / time_diff if time_diff > 0 else 0
movement_state = '移动' if speed > self.movement_threshold else '静止'
frame_distances.append(cm_distance)
total_distance += cm_distance
# 如果在分析区内,记录分析区移动距离
if prev_in_zone and curr_in_zone:
analysis_zone_distances.append(cm_distance)
total_analysis_zone_distance += cm_distance
else:
analysis_zone_distances.append(0)
speeds.append(speed)
# 判断移动状态
movement_states.append({
'timestamp': curr_time,
'state': movement_state
})
# 计算移动事件(连续的移动状态)
current_episode = None
for i, state_data in enumerate(movement_states):
if state_data['state'] == '移动':
if current_episode is None:
# 开始新的移动事件
current_episode = {
'start_time': state_data['timestamp'],
'end_time': state_data['timestamp'],
'duration': 0
}
else:
# 更新移动事件结束时间
current_episode['end_time'] = state_data['timestamp']
else:
if current_episode is not None:
# 结束当前移动事件
current_episode['duration'] = current_episode['end_time'] - current_episode['start_time']
movement_episodes.append(current_episode)
current_episode = None
# 处理最后一个移动事件
if current_episode is not None:
current_episode['duration'] = current_episode['end_time'] - current_episode['start_time']
movement_episodes.append(current_episode)
# 计算加速度和加速度状态
avg_acceleration = 0
max_acceleration = 0
min_acceleration = 0
if len(speeds) >= 2:
for i in range(1, len(speeds)):
time_diff = trajectory[i + 1][0] - trajectory[i][0]
acceleration = (speeds[i] - speeds[i - 1]) / time_diff if time_diff > 0 else 0
accelerations.append(acceleration)
# 计算加速度统计
if accelerations:
avg_acceleration = np.mean(accelerations)
max_acceleration = np.max(accelerations)
min_acceleration = np.min(accelerations)
# 判断加速度状态
for acc in accelerations:
acceleration_state = '高加速度' if acc > avg_acceleration else '低加速度'
acceleration_states.append(acceleration_state)
# 存储结果
self.results[roi_key] = {
'total_distance_cm': total_distance,
'total_analysis_zone_distance_cm': total_analysis_zone_distance,
'frame_distances': frame_distances,
'analysis_zone_distances': analysis_zone_distances,
'average_speed_cm_s': np.mean(speeds) if speeds else 0,
'max_speed_cm_s': np.max(speeds) if speeds else 0,
'min_speed_cm_s': np.min(speeds) if speeds else 0,
'speeds': speeds,
'accelerations': accelerations,
'movement_state_data': movement_states,
'acceleration_data': [{'timestamp': trajectory[i + 1][0], 'acceleration': acc}
for i, acc in enumerate(accelerations)],
'acceleration_states': acceleration_states,
'avg_acceleration': avg_acceleration,
'max_acceleration': max_acceleration,
'min_acceleration': min_acceleration,
'movement_episodes': movement_episodes, # 新增:移动事件
'area_id': self.roi_area_mapping.get(roi_key, 'unknown') # 添加区域ID
}
def generate_excel_report(self, video_name, experiment_info=None):
"""生成Excel报告,格式与统计数据值.xlsx一致"""
# 准备数据
data = []
# 获取实验信息和时间范围
exp_name = experiment_info.get('name', '') if experiment_info else ''
exp_note = experiment_info.get('note', '') if experiment_info else ''
duration = experiment_info.get('duration', 0) if experiment_info else 0
# 格式化时间范围
def format_duration(seconds):
hours = int(seconds // 3600)
minutes = int((seconds % 3600) // 60)
secs = seconds % 60
return f"{hours:02d}:{minutes:02d}:{secs:06.3f}"
end_time = format_duration(duration)
time_range = f"00:00:00.000-{end_time}"
for roi_key, metrics in self.results.items():
# 获取区域名称
area_id = metrics.get('area_id', 'unknown')
area_name = f"区域{area_id.split('-')[-1]}" if area_id != 'unknown' else '未知区域'
# 1. 移动距离计算
frame_distances = metrics['frame_distances']
n_distance = len(frame_distances)
distance_mean = np.mean(frame_distances) if n_distance > 0 else 0
distance_se = np.std(frame_distances) / np.sqrt(n_distance) if n_distance > 1 else 0
# 2. 分析区移动距离
analysis_zone_distances = metrics['analysis_zone_distances']
n_analysis_zone = len(analysis_zone_distances)
analysis_zone_mean = np.mean(analysis_zone_distances) if n_analysis_zone > 0 else 0
analysis_zone_se = np.std(analysis_zone_distances) / np.sqrt(n_analysis_zone) if n_analysis_zone > 1 else 0
# 3. 速度计算
speeds = metrics['speeds']
n_speed = len(speeds)
speed_mean = np.mean(speeds) if n_speed > 0 else 0
speed_se = np.std(speeds) / np.sqrt(n_speed) if n_speed > 1 else 0
# 4. 移动状态计算(关键修复)
movement_states = metrics['movement_state_data']
movement_durations = []
current_duration = 0
last_timestamp = movement_states[0]['timestamp'] if movement_states else 0
for i in range(1, len(movement_states)):
current = movement_states[i]
prev = movement_states[i - 1]
time_diff = current['timestamp'] - prev['timestamp']
if current['state'] == '移动':
current_duration += time_diff
elif current_duration > 0:
movement_durations.append(current_duration)
current_duration = 0
if current_duration > 0:
movement_durations.append(current_duration)
movement_episodes = metrics['movement_episodes']
n_movement_state = len(movement_episodes)
movement_durations = [episode['duration'] for episode in movement_episodes]
movement_duration_mean = np.mean(movement_durations) if n_movement_state > 0 else 0
movement_duration_se = np.std(movement_durations) / np.sqrt(n_movement_state) if n_movement_state > 1 else 0
total_movement_duration = sum(movement_durations)
# 5. 加速度计算
accelerations = metrics['accelerations']
n_acc = len(accelerations)
avg_acc = metrics['avg_acceleration']
acc_se = np.std(accelerations) / np.sqrt(n_acc) if n_acc > 1 else 0
# 加速度极值
max_acc = np.max(accelerations) if accelerations else 0
min_acc = np.min(accelerations) if accelerations else 0
# 关键修复:确保数据字典有23个元素
data.append({
'实验名称(备注信息)': f"{video_name}/{area_name}",
'移动距离_N': n_distance,
'移动距离_平均值': round(distance_mean, 3),
'移动距离_总距离': round(metrics['total_distance_cm'], 3),
'移动距离_标准误差': round(distance_se, 3),
'分析区移动距离_N': n_analysis_zone,
'分析区移动距离_平均值': round(analysis_zone_mean, 3),
'分析区移动距离_累计移动距离': round(metrics['total_analysis_zone_distance_cm'], 3),
'分析区移动距离_标准误差': round(analysis_zone_se, 3),
'速度_N': n_speed,
'速度_平均值': round(speed_mean, 3),
'速度_标准误差': round(speed_se, 3),
'移动状态_N': n_movement_state,
'移动状态_平均值': round(movement_duration_mean, 3),
'移动状态_累计持续时间': round(total_movement_duration, 3),
'移动状态_标准误差': round(movement_duration_se, 3),
'加速度_最大值_N': n_acc,
'加速度_最大值_平均值': round(max_acc, 3),
'加速度最大值_标准误差': 0, # 单个值无标准误差
'加速度_最小值_N': n_acc,
'加速度_最小值_平均值': round(min_acc, 3),
'加速度最小值_标准误差': 0 # 单个值无标准误差
})
# 创建DataFrame
df = pd.DataFrame(data)
# 保存到文件
report_dir = os.path.join(settings.MEDIA_ROOT, 'reports')
os.makedirs(report_dir, exist_ok=True)
report_path = os.path.join(report_dir, f"{video_name}_analysis.xlsx")
# 创建Excel写入器
with pd.ExcelWriter(report_path, engine='openpyxl') as writer:
# 写入数据(现在有23列)
df.to_excel(writer, sheet_name='检测结果', index=False)
# 获取工作簿和工作表对象
workbook = writer.book
worksheet = writer.sheets['检测结果']
# 定义不同区域的背景色
colors = {
'移动距离': 'FFE6B3', # 浅橙色
'分析区移动距离': 'B3E6FF', # 浅蓝色
'速度': 'B3FFB3', # 浅绿色
'移动状态': 'FFB3E6', # 浅粉色
'加速度': 'E6B3FF' # 浅紫色
}
# 导入样式类
from openpyxl.styles import PatternFill, Alignment, Font, Border, Side
# 设置居中对齐
center_alignment = Alignment(horizontal='center', vertical='center')
# 设置边框样式
thin_border = Border(
left=Side(style='thin'),
right=Side(style='thin'),
top=Side(style='thin'),
bottom=Side(style='thin')
)
# 设置列宽
for col in worksheet.columns:
column_letter = col[0].column_letter
worksheet.column_dimensions[column_letter].width = 11.75
# 添加多级表头
# 第一行标题
worksheet.merge_cells('A1:V1')
title_cell = worksheet['A1']
title_cell.value = f"{exp_name} ({exp_note}) {time_range}" if exp_note else f"{exp_name} {time_range}"
title_cell.alignment = center_alignment
title_cell.font = Font(bold=True, size=14)
title_cell.border = thin_border
# 第二行分类 - 设置居中并添加背景色
worksheet.merge_cells('A2:A6')
worksheet['A2'] = '指标'
worksheet['A2'].alignment = center_alignment
worksheet['A2'].border = thin_border
# 移动距离区域
worksheet.merge_cells('B2:E2')
move_dist_cell = worksheet['B2']
move_dist_cell.value = '移动距离'
move_dist_cell.alignment = center_alignment
move_dist_cell.fill = PatternFill(start_color=colors['移动距离'], end_color=colors['移动距离'],
fill_type='solid')
move_dist_cell.font = Font(bold=True)
move_dist_cell.border = thin_border
# 分析区移动距离区域
worksheet.merge_cells('F2:I2')
area_dist_cell = worksheet['F2']
area_dist_cell.value = '分析区移动距离'
area_dist_cell.alignment = center_alignment
area_dist_cell.fill = PatternFill(start_color=colors['分析区移动距离'], end_color=colors['分析区移动距离'],
fill_type='solid')
area_dist_cell.font = Font(bold=True)
area_dist_cell.border = thin_border
# 速度区域
worksheet.merge_cells('J2:L2')
speed_cell = worksheet['J2']
speed_cell.value = '速度'
speed_cell.alignment = center_alignment
speed_cell.fill = PatternFill(start_color=colors['速度'], end_color=colors['速度'], fill_type='solid')
speed_cell.font = Font(bold=True)
speed_cell.border = thin_border
# 移动状态区域
worksheet.merge_cells('M2:P2')
move_state_cell = worksheet['M2']
move_state_cell.value = '移动状态'
move_state_cell.alignment = center_alignment
move_state_cell.fill = PatternFill(start_color=colors['移动状态'], end_color=colors['移动状态'],
fill_type='solid')
move_state_cell.font = Font(bold=True)
move_state_cell.border = thin_border
# 加速度区域
worksheet.merge_cells('Q2:V2')
acc_cell = worksheet['Q2']
acc_cell.value = '加速度'
acc_cell.alignment = center_alignment
acc_cell.fill = PatternFill(start_color=colors['加速度'], end_color=colors['加速度'], fill_type='solid')
acc_cell.font = Font(bold=True)
acc_cell.border = thin_border
# 第三行指标 - 设置背景色和边框
for col in range(2, 23): # B到V列
cell = worksheet.cell(row=3, column=col)
cell.border = thin_border
cell.alignment = center_alignment
# 根据列范围设置背景色
if 2 <= col <= 5: # B-E列:移动距离
cell.fill = PatternFill(start_color=colors['移动距离'], end_color=colors['移动距离'],
fill_type='solid')
elif 6 <= col <= 9: # F-I列:分析区移动距离
cell.fill = PatternFill(start_color=colors['分析区移动距离'], end_color=colors['分析区移动距离'],
fill_type='solid')
elif 10 <= col <= 12: # J-L列:速度
cell.fill = PatternFill(start_color=colors['速度'], end_color=colors['速度'], fill_type='solid')
elif 13 <= col <= 16: # M-P列:移动状态
cell.fill = PatternFill(start_color=colors['移动状态'], end_color=colors['移动状态'],
fill_type='solid')
elif 17 <= col <= 22: # Q-V列:加速度
cell.fill = PatternFill(start_color=colors['加速度'], end_color=colors['加速度'], fill_type='solid')
# 设置第三行具体内容
worksheet['B3'] = ''
worksheet.merge_cells('C3:D3')
worksheet['C3'] = '中心点'
worksheet['C3'].alignment = center_alignment
worksheet['C3'].fill = PatternFill(start_color=colors['移动距离'], end_color=colors['移动距离'],
fill_type='solid')
worksheet['E3'] = ''
worksheet['F3'] = ''
worksheet.merge_cells('G3:H3')
worksheet['G3'] = '中心点/1'
worksheet['G3'].alignment = center_alignment
worksheet['G3'].fill = PatternFill(start_color=colors['分析区移动距离'], end_color=colors['分析区移动距离'],
fill_type='solid')
worksheet['I3'] = ''
worksheet['J3'] = ''
worksheet['K3'] = '中心点'
worksheet['L3'] = ''
worksheet['M3'] = ''
worksheet.merge_cells('N3:O3')
worksheet['N3'] = '中心点/移动'
worksheet['N3'].alignment = center_alignment
worksheet['N3'].fill = PatternFill(start_color=colors['移动状态'], end_color=colors['移动状态'],
fill_type='solid')
worksheet['P3'] = ''
worksheet['Q3'] = ''
worksheet['R3'] = '中心点'
worksheet['S3'] = ''
worksheet['T3'] = ''
worksheet['U3'] = '中心点'
worksheet['V3'] = ''
# 第四行具体指标 - 设置背景色和边框
for col in range(2, 23): # B到V列
cell = worksheet.cell(row=4, column=col)
cell.border = thin_border
cell.alignment = center_alignment
# 根据列范围设置背景色
if 2 <= col <= 5: # B-E列:移动距离
cell.fill = PatternFill(start_color=colors['移动距离'], end_color=colors['移动距离'],
fill_type='solid')
elif 6 <= col <= 9: # F-I列:分析区移动距离
cell.fill = PatternFill(start_color=colors['分析区移动距离'], end_color=colors['分析区移动距离'],
fill_type='solid')
elif 10 <= col <= 12: # J-L列:速度
cell.fill = PatternFill(start_color=colors['速度'], end_color=colors['速度'], fill_type='solid')
elif 13 <= col <= 16: # M-P列:移动状态
cell.fill = PatternFill(start_color=colors['移动状态'], end_color=colors['移动状态'],
fill_type='solid')
elif 17 <= col <= 22: # Q-V列:加速度
cell.fill = PatternFill(start_color=colors['加速度'], end_color=colors['加速度'], fill_type='solid')
# 设置第四行具体内容
worksheet['B4'] = ''
worksheet['C4'] = ''
worksheet['D4'] = ''
worksheet['E4'] = ''
worksheet['F4'] = ''
worksheet['G4'] = ''
worksheet['H4'] = ''
worksheet['I4'] = ''
worksheet['J4'] = ''
worksheet['K4'] = ''
worksheet['L4'] = ''
worksheet['M4'] = ''
worksheet['N4'] = ''
worksheet['O4'] = ''
worksheet['P4'] = ''
worksheet['Q4'] = ''
worksheet['R4'] = '最大值'
worksheet['S4'] = ''
worksheet['T4'] = ''
worksheet['U4'] = '最小值'
worksheet['V4'] = ''
# 第五行具体指标 - 设置背景色和边框
for col in range(2, 23): # B到V列
cell = worksheet.cell(row=5, column=col)
cell.border = thin_border
cell.alignment = center_alignment
# 根据列范围设置背景色
if 2 <= col <= 5: # B-E列:移动距离
cell.fill = PatternFill(start_color=colors['移动距离'], end_color=colors['移动距离'],
fill_type='solid')
elif 6 <= col <= 9: # F-I列:分析区移动距离
cell.fill = PatternFill(start_color=colors['分析区移动距离'], end_color=colors['分析区移动距离'],
fill_type='solid')
elif 10 <= col <= 12: # J-L列:速度
cell.fill = PatternFill(start_color=colors['速度'], end_color=colors['速度'], fill_type='solid')
elif 13 <= col <= 16: # M-P列:移动状态
cell.fill = PatternFill(start_color=colors['移动状态'], end_color=colors['移动状态'],
fill_type='solid')
elif 17 <= col <= 22: # Q-V列:加速度
cell.fill = PatternFill(start_color=colors['加速度'], end_color=colors['加速度'], fill_type='solid')
# 设置第五行具体内容
worksheet['B5'] = 'N'
worksheet['C5'] = '平均值'
worksheet['D5'] = '总距离'
worksheet['E5'] = '标准误差'
worksheet['F5'] = 'N'
worksheet['G5'] = '平均值'
worksheet['H5'] = '累计移动距离'
worksheet['I5'] = '标准误差'
worksheet['J5'] = 'N'
worksheet['K5'] = '平均值'
worksheet['L5'] = '标准误差'
worksheet['M5'] = 'N'
worksheet['N5'] = '平均值'
worksheet['O5'] = '累计持续时间'
worksheet['P5'] = '标准误差'
worksheet['Q5'] = 'N'
worksheet['R5'] = '平均值'
worksheet['S5'] = '标准误差'
worksheet['T5'] = 'N'
worksheet['U5'] = '平均值'
worksheet['V5'] = '标准误差'
# 第六行单位 - 设置背景色和边框,并确保文字居中
for col in range(2, 23): # B到V列
cell = worksheet.cell(row=6, column=col)
cell.border = thin_border
cell.alignment = center_alignment
# 根据列范围设置背景色
if 2 <= col <= 5: # B-E列:移动距离
cell.fill = PatternFill(start_color=colors['移动距离'], end_color=colors['移动距离'],
fill_type='solid')
elif 6 <= col <= 9: # F-I列:分析区移动距离
cell.fill = PatternFill(start_color=colors['分析区移动距离'], end_color=colors['分析区移动距离'],
fill_type='solid')
elif 10 <= col <= 12: # J-L列:速度
cell.fill = PatternFill(start_color=colors['速度'], end_color=colors['速度'], fill_type='solid')
elif 13 <= col <= 16: # M-P列:移动状态
cell.fill = PatternFill(start_color=colors['移动状态'], end_color=colors['移动状态'],
fill_type='solid')
elif 17 <= col <= 22: # Q-V列:加速度
cell.fill = PatternFill(start_color=colors['加速度'], end_color=colors['加速度'], fill_type='solid')
# 设置第六行具体内容
worksheet['B6'] = ''
worksheet['C6'] = '厘米'
worksheet['D6'] = '厘米'
worksheet['E6'] = '厘米'
worksheet['F6'] = ''
worksheet['G6'] = '厘米'
worksheet['H6'] = '厘米'
worksheet['I6'] = '厘米'
worksheet['J6'] = ''
worksheet['K6'] = '厘米/秒'
worksheet['L6'] = '厘米/秒'
worksheet['M6'] = ''
worksheet['N6'] = '秒'
worksheet['O6'] = '秒'
worksheet['P6'] = '秒'
worksheet['Q6'] = ''
worksheet['R6'] = '厘米/秒²'
worksheet['S6'] = '厘米/秒²'
worksheet['T6'] = ''
worksheet['U6'] = '厘米/秒²'
worksheet['V6'] = '厘米/秒²'
# 从第七行开始写入数据并设置样式
for row_idx, row_data in enumerate(df.values, 7):
for col_idx, value in enumerate(row_data[:22], 1): # 只处理A-V列
cell = worksheet.cell(row=row_idx, column=col_idx, value=value)
cell.border = thin_border
cell.alignment = center_alignment
# 为数据行设置背景色
if col_idx >= 2: # 从B列开始
if 2 <= col_idx <= 5: # B-E列:移动距离
cell.fill = PatternFill(start_color=colors['移动距离'], end_color=colors['移动距离'],
fill_type='solid')
elif 6 <= col_idx <= 9: # F-I列:分析区移动距离
cell.fill = PatternFill(start_color=colors['分析区移动距离'],
end_color=colors['分析区移动距离'],
fill_type='solid')
elif 10 <= col_idx <= 12: # J-L列:速度
cell.fill = PatternFill(start_color=colors['速度'], end_color=colors['速度'],
fill_type='solid')
elif 13 <= col_idx <= 16: # M-P列:移动状态
cell.fill = PatternFill(start_color=colors['移动状态'], end_color=colors['移动状态'],
fill_type='solid')
elif 17 <= col_idx <= 22: # Q-V列:加速度
cell.fill = PatternFill(start_color=colors['加速度'], end_color=colors['加速度'],
fill_type='solid')
return f"{settings.MEDIA_URL}reports/{os.path.basename(report_path)}"
def generate_json_summary(self):
"""生成JSON格式的摘要数据"""
summary = {}
for roi_key, metrics in self.results.items():
summary[roi_key] = {
'total_distance': metrics['total_distance_cm'],
'total_analysis_zone_distance': metrics['total_analysis_zone_distance_cm'],
'average_speed': metrics['average_speed_cm_s'],
'max_speed': metrics['max_speed_cm_s'],
'min_speed': metrics['min_speed_cm_s'],
'movement_count': len([s for s in metrics['movement_state_data'] if s['state'] == '移动']),
'total_movement_time': sum([1 for s in metrics['movement_state_data'] if s['state'] == '移动']),
'acceleration_data': metrics['acceleration_data'],
'acceleration_states': metrics['acceleration_states'],
'avg_acceleration': metrics['avg_acceleration'],
'area_id': metrics.get('area_id', 'unknown') # 添加区域ID
}
return summary
def get_current_metrics(self):
"""获取当前帧的运动指标"""
current_metrics = {}
for roi_key, trajectory in self.trajectories.items():
if len(trajectory) < 2:
current_metrics[roi_key] = {
'current_speed': 0,
'current_distance': 0,
'current_acceleration': 0,
'in_analysis_zone': self.in_analysis_zone.get(roi_key, False),
'area_id': self.roi_area_mapping.get(roi_key, 'unknown') # 添加区域ID
}
continue
# 获取最近的两个点
prev_time, prev_center, prev_in_zone = trajectory[-2]
curr_time, curr_center, curr_in_zone = trajectory[-1]
# 检查中心点是否为None(目标未检测到)
if prev_center is None or curr_center is None:
current_metrics[roi_key] = {
'current_speed': 0,
'current_distance': 0,
'current_acceleration': 0,
'in_analysis_zone': curr_in_zone,
'area_id': self.roi_area_mapping.get(roi_key, 'unknown') # 添加区域ID
}
continue
# 计算像素距离
pixel_distance = self._euclidean_distance(prev_center, curr_center)
# 转换为厘米
cm_distance = pixel_distance / self.pixels_per_cm
# 计算时间差
time_diff = curr_time - prev_time
# 计算速度 (厘米/秒)
speed = cm_distance / time_diff if time_diff > 0 else 0
# 计算加速度 (需要至少三个点)
acceleration = 0
if len(trajectory) >= 3:
prev_prev_time, prev_prev_center, _ = trajectory[-3]
# 检查前一个点是否存在
if prev_prev_center is not None:
prev_pixel_distance = self._euclidean_distance(prev_prev_center, prev_center)
prev_cm_distance = prev_pixel_distance / self.pixels_per_cm
prev_time_diff = prev_time - prev_prev_time
prev_speed = prev_cm_distance / prev_time_diff if prev_time_diff > 0 else 0
acceleration = (speed - prev_speed) / time_diff if time_diff > 0 else 0
current_metrics[roi_key] = {
'current_speed': speed,
'current_distance': cm_distance,
'current_acceleration': acceleration,
'in_analysis_zone': curr_in_zone,
'area_id': self.roi_area_mapping.get(roi_key, 'unknown') # 添加区域ID
}
return current_metrics
def _euclidean_distance(self, point1, point2):
"""计算两点之间的欧氏距离,如果任一点为None则返回0"""
if point1 is None or point2 is None:
return 0.0
return np.sqrt((point2[0] - point1[0]) ** 2 + (point2[1] - point1[1]) ** 2)
class VideoDetectionConsumer(AsyncWebsocketConsumer):
async def connect(self):
logger.info(f"WebSocket 连接尝试: {self.scope}")
await self.accept()
logger.info("WebSocket 连接已建立")
self.last_time = time.time()
self.start_time = time.time()
self.frame_count = 0
self.total_processing_time = 0
async def disconnect(self, close_code):
pass
async def receive(self, text_data=None, bytes_data=None):
if text_data:
text_data_json = json.loads(text_data)
action = text_data_json.get('action')
video_name = text_data_json.get('video')
# 处理暂停指令
if action == 'pause_detection':
if hasattr(self, 'detection_paused'):
self.detection_paused = True
self.paused_frame_index = text_data_json.get('current_frame', 0)
await self.send(text_data=json.dumps({
'type': 'pause_ack',
'message': '检测已暂停',
'paused_frame': self.paused_frame_index
}))
# 处理继续指令
elif action == 'resume_detection':
if hasattr(self, 'detection_paused'):
self.detection_paused = False
self.resume_from_frame = text_data_json.get('resume_from', 0)
await self.send(text_data=json.dumps({
'type': 'resume_ack',
'message': '检测已继续',
'resume_from': self.resume_from_frame
}))
# 处理取消指令
elif action == 'cancel_detection':
# 设置取消标志
self.detection_canceled = True
# 解除暂停状态以确保能退出等待循环
if hasattr(self, 'detection_paused'):
self.detection_paused = False
await self.send(text_data=json.dumps({
'type': 'cancel_ack',
'message': '检测已取消'
}))
# 处理常规检测启动
elif action == 'start_detection':
# 确保临时目录存在
temp_dir = os.path.join(settings.BASE_DIR, 'temp')
if not os.path.exists(temp_dir):
os.makedirs(temp_dir)
video_path = os.path.join(temp_dir, video_name)
# 检查视频文件是否存在
if not os.path.exists(video_path):
await self.send(text_data=json.dumps({
'type': 'error',
'message': f'视频文件不存在: {video_path}'
}))
return
model_path = os.path.join(settings.BASE_DIR,
"C:/Users/16660/Desktop/网页搭建/Behaviewer/models/best.pt")
output_video_path = os.path.join(settings.MEDIA_ROOT, 'videos', video_name)
output_video_dir = os.path.dirname(output_video_path)
if not os.path.exists(output_video_dir):
os.makedirs(output_video_dir)
# 初始化检测状态变量
self.detection_paused = False
self.paused_frame_index = 0
self.detection_canceled = False
# 启动视频处理任务
asyncio.create_task(self.detect_objects_in_video(model_path, video_path, output_video_path))
# 处理自定义检测启动
elif action == 'start_custom_detection':
# 处理detection页面的自定义检测
temp_dir = os.path.join(settings.BASE_DIR, 'temp')
if not os.path.exists(temp_dir):
os.makedirs(temp_dir)
video_path = os.path.join(temp_dir, video_name)
# 检查视频文件是否存在
if not os.path.exists(video_path):
await self.send(text_data=json.dumps({
'type': 'error',
'message': f'视频文件不存在: {video_path}'
}))
return
# 获取检测参数和ROI数据
params = text_data_json.get('params', {})
roi_data = text_data_json.get('roi', {})
scale_info = text_data_json.get('scale_info', {}) # 新增
# 获取实验信息
experiment_info = text_data_json.get('experiment_info', {})
# 验证比例尺信息
if 'pixel_length' not in scale_info or 'real_length' not in scale_info:
await self.send(text_data=json.dumps({
'type': 'error',
'message': '缺少比例尺信息,无法进行厘米转换'
}))
return
output_video_path = os.path.join(settings.MEDIA_ROOT, 'videos', f"Test_{video_name}")
output_video_dir = os.path.dirname(output_video_path)
if not os.path.exists(output_video_dir):
os.makedirs(output_video_dir)
# 初始化检测状态变量
self.detection_paused = False
self.paused_frame_index = 0
self.detection_canceled = False
# 启动自定义视频处理任务
asyncio.create_task(self.process_custom_detection(
video_path, output_video_path, params, roi_data, scale_info, experiment_info
))
async def detect_objects_in_video(self, model_path, video_path, output_path):
try:
# 加载模型
model = YOLO(model_path)
# 打开视频
cap = cv2.VideoCapture(video_path)
if not cap.isOpened():
await self.send(text_data=json.dumps({
'type': 'error',
'message': f'无法打开视频文件: {video_path}'
}))
return
total_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
fps = cap.get(cv2.CAP_PROP_FPS)
width = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))
height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
# 创建视频写入器
fourcc = cv2.VideoWriter_fourcc(*'mp4v')
out = cv2.VideoWriter(output_path, fourcc, fps, (width, height))
frame_index = 0
while cap.isOpened():
ret, frame = cap.read()
if not ret:
break
# 处理帧
frame_index += 1
start_time = time.time()
# 目标检测
results = model(frame)
annotated_frame = results[0].plot()
# 计算处理时间
processing_time = time.time() - start_time
self.total_processing_time += processing_time
self.frame_count += 1
# 计算当前FPS
current_fps = 1.0 / processing_time if processing_time > 0 else 0
# 添加FPS显示
fps_text = f"FPS: {current_fps:.2f}"
cv2.putText(annotated_frame, fps_text, (10, 30),
cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 255, 0), 2)
# 保存处理后的帧
out.write(annotated_frame)
# 将处理后的帧转换为base64
_, buffer = cv2.imencode('.jpg', annotated_frame)
frame_base64 = base64.b64encode(buffer).decode('utf-8')
# 计算进度
progress = frame_index / total_frames
# 发送处理后的帧
await self.send(text_data=json.dumps({
'type': 'frame',
'frame': frame_base64,
'objects': len(results[0].boxes),
'fps': current_fps,
'progress': progress
}))
# 稍微延迟以控制发送速率
await asyncio.sleep(0.01)
# 释放资源
cap.release()
out.release()
# 计算平均FPS
avg_fps = self.frame_count / self.total_processing_time if self.total_processing_time > 0 else 0
# 发送完成消息
output_video_url = f'{settings.MEDIA_URL}videos/{os.path.basename(output_path)}'
await self.send(text_data=json.dumps({
'type': 'end',
'output_video_url': output_video_url,
'total_frames': total_frames,
'avg_fps': avg_fps,
'fps': fps
}))
except Exception as e:
await self.send(text_data=json.dumps({
'type': 'error',
'message': f'处理错误: {str(e)}'
}))
import traceback
traceback.print_exc()
async def process_custom_detection(self, video_path, output_path, params, roi_data, scale_info,
experiment_info=None):
"""
自定义视频检测处理流程,包含运动指标计算
"""
try:
# 从接收的消息中获取实验信息
experiment_name = experiment_info.get('name', os.path.basename(
video_path)) if experiment_info else os.path.basename(video_path)
experiment_note = experiment_info.get('note', '') if experiment_info else ''
# 验证比例尺信息
if 'pixel_length' not in scale_info or 'real_length' not in scale_info:
await self.send(text_data=json.dumps({
'type': 'error',
'message': '缺少比例尺信息,无法进行厘米转换'
}))
return
# 构建符合 image_detect 要求的 config_area_current
config_area_current = {}
roi_name_mapping = {} # 初始化 ROI 名称映射
roi_area_mapping = {} # 初始化 ROI 到分析区的映射
for roi_key, roi_info in roi_data.items():
polygons = roi_info.get('polygons')
if polygons and len(polygons) > 0 and len(polygons[0]) >= 3:
config_area_current[roi_key] = [polygons]
# 获取areaId并提取分析区编号
area_id = roi_info.get('areaId', '')
original_name = roi_info.get('originalName', roi_key)
# 存储ROI到分析区的映射
roi_area_mapping[roi_key] = area_id
# if area_id.startswith('analysis-'):
# # 提取分析区编号(例如:从"analysis-1"中提取"1")
# analysis_num = area_id.split('-')[1]
# roi_name_mapping[roi_key] = f'分析区{analysis_num}'
#
# else:
# # 对于没有关联到区域的ROI,使用原始名称(去掉后缀)
# if '-' in original_name:
# # 提取基础名称(去掉"-矩形"或"-圆形"后缀)
# base_name = original_name.split('-')[0]
# roi_name_mapping[roi_key] = base_name
# else:
# roi_name_mapping[roi_key] = original_name
if not config_area_current:
await self.send(text_data=json.dumps({
'type': 'error',
'message': '无效的 ROI 数据格式'
}))
return
# 初始化视频分析器
analyzer = VideoAnalyzer(scale_info)
analyzer.set_roi_area_mapping(roi_area_mapping) # 设置ROI到分析区的映射
# 打开视频
cap = cv2.VideoCapture(video_path)
if not cap.isOpened():
await self.send(text_data=json.dumps({
'type': 'error',
'message': f'无法打开视频文件: {video_path}'
}))
return
# 获取视频信息
total_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
fps = cap.get(cv2.CAP_PROP_FPS)
frame_interval = 1.0 / fps if fps > 0 else 0.033 # 默认30fps
width = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))
height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
# 初始化检测状态变量
self.detection_paused = False
self.paused_frame_index = 0
self.detection_canceled = False
self.frame_count = 0
self.total_processing_time = 0
# 发送初始化消息,包含总帧数信息
await self.send(text_data=json.dumps({
'type': 'detection_init',
'total_frames': total_frames
}))
# 创建视频写入器
fourcc = cv2.VideoWriter_fourcc(*'mp4v')
out = cv2.VideoWriter(output_path, fourcc, fps, (width, height))
# 初始化帧计数器
frame_index = self.paused_frame_index if self.paused_frame_index > 0 else 0
self.frame_count = frame_index
self.total_processing_time = 0
# 如果是从暂停处继续,跳转到指定帧
if frame_index > 0:
cap.set(cv2.CAP_PROP_POS_FRAMES, frame_index)
while cap.isOpened() and not self.detection_canceled:
# 检查暂停状态
while self.detection_paused and not self.detection_canceled:
# 发送暂停状态更新
await self.send(text_data=json.dumps({
'type': 'detection_paused',
'frame_index': frame_index,
'total_frames': total_frames
}))
await asyncio.sleep(0.5) # 暂停检查间隔
if self.detection_canceled:
break
ret, frame = cap.read()
if not ret:
break
# 处理帧
frame_index += 1
start_time = time.time()
timestamp = frame_index * frame_interval # 当前帧时间戳
# 使用 image_detect 处理当前帧
result = image_detect(
frame,
config_area_current,
currentback=params['currentback'],
kernal_erode=params['kernal_erode'],
kernal_dilate=params['kernal_dilate'],
kernal_erode_2=params['kernal_erode_2'],
min_area=params['min_area'],
max_area=params['max_area'],
adjust_threshold=params['adjust_threshold'],
feature=params.get('feature', 1) # 默认为中心点检测
)
# 根据实际返回值解包
if len(result) == 2:
processed_frame, centers_dict = result
else:
# 容错处理
processed_frame = result[0]
centers_dict = result[1] if len(result) > 1 else {}
# 计算每个ROI是否在分析区内
in_analysis_zone_dict = {}
for roi_key in centers_dict.keys():
# 这里需要根据ROI配置和检测到的中心点判断是否在分析区内
# 假设所有检测到的目标都在分析区内,实际应根据ROI配置进行判断
in_analysis_zone_dict[roi_key] = True
# 记录当前帧的分析结果
analyzer.process_frame(frame_index, timestamp, centers_dict, in_analysis_zone_dict)
# 计算处理时间和FPS
processing_time = time.time() - start_time
self.total_processing_time += processing_time
self.frame_count += 1
current_fps = 1.0 / processing_time if processing_time > 0 else 0
# 使用PIL绘制中文字符
def draw_chinese_text(image, text, position, font_size=20, color=(0, 255, 0)):
# 将BGR图像转换为RGB
img_pil = Image.fromarray(cv2.cvtColor(image, cv2.COLOR_BGR2RGB))
draw = ImageDraw.Draw(img_pil)
# 加载中文字体(确保系统中有中文字体文件,或者提供字体文件路径)
try:
font = ImageFont.truetype("Behaviewer/static/simhei.ttf", font_size) # 使用黑体
except:
font = ImageFont.load_default()
# 绘制文本
draw.text(position, text, font=font, fill=color)
# 转换回OpenCV格式
return cv2.cvtColor(np.array(img_pil), cv2.COLOR_RGB2BGR)
# 修改FPS显示部分
fps_text = f"FPS: {current_fps:.2f}"
processed_frame = draw_chinese_text(processed_frame, fps_text, (10, 30), 20, (0, 255, 0))
# 添加实时指标显示
current_metrics = analyzer.get_current_metrics()
# 修改实时指标显示部分,使用 ROI 名称映射
if frame_index > 1:
display_line = 1
for roi_key, metrics in current_metrics.items():
area_name = roi_name_mapping.get(roi_key, roi_key)
if metrics['current_speed'] > 0.1:
text = f"{area_name}: {metrics['current_speed']:.2f} cm/s"
processed_frame = draw_chinese_text(processed_frame, text, (10, 30 + 25 * display_line), 16,
(0, 255, 255))
display_line += 1
if metrics['current_distance'] > 0:
text = f" 移动: {metrics['current_distance']:.2f} cm"
processed_frame = draw_chinese_text(processed_frame, text, (10, 30 + 25 * display_line),
14, (0, 200, 255))
display_line += 1
if abs(metrics['current_acceleration']) > 0.1:
acc_text = "加速" if metrics['current_acceleration'] > 0 else "减速"
text = f" {acc_text}: {abs(metrics['current_acceleration']):.2f} cm/s²"
processed_frame = draw_chinese_text(processed_frame, text, (10, 30 + 25 * display_line),
14, (0, 200, 255))
display_line += 1
# 保存处理后的帧
out.write(processed_frame)
# 转换为base64发送到前端
_, buffer = cv2.imencode('.jpg', processed_frame)
frame_base64 = base64.b64encode(buffer).decode('utf-8')
# 计算进度
progress = frame_index / total_frames
# 发送处理后的帧(包含帧索引信息)
await self.send(text_data=json.dumps({
'type': 'custom_detection_frame',
'frame': frame_base64,
'fps': current_fps,
'progress': progress,
'frame_index': frame_index,
'total_frames': total_frames,
'current_metrics': current_metrics
}))
# 控制发送速率
await asyncio.sleep(0.01)
# 释放资源
cap.release()
out.release()
# 如果是正常结束而非取消,计算最终指标并生成报告
if not self.detection_canceled:
# 计算运动指标
analyzer.calculate_movement_metrics()
# 保存实验信息(在使用前定义)
video_name = os.path.basename(video_path)
json_summary = analyzer.generate_json_summary()
# 新增:提取关键指标(为每个ROI都计算)
key_metrics = {}
for roi_key, roi_metrics in analyzer.results.items():
key_metrics[roi_key] = {
'total_distance': round(roi_metrics['total_distance_cm'], 3),
'total_analysis_zone_distance': round(roi_metrics['total_analysis_zone_distance_cm'], 3),
'avg_speed': round(roi_metrics['average_speed_cm_s'], 3),
'max_acceleration': round(roi_metrics['max_acceleration'], 3),
'min_acceleration': round(roi_metrics['min_acceleration'], 3),
'total_movement_duration': round(sum(ep['duration'] for ep in roi_metrics['movement_episodes']),
3),
'area_id': roi_metrics.get('area_id', 'unknown')
}
experiment_info = {
'name': experiment_name,
'note': experiment_note,
'duration': self.total_processing_time,
'total_frames': total_frames,
'metrics': json_summary
}
# 生成报告(现在 experiment_info 已经定义)
report_url = analyzer.generate_excel_report(video_name, experiment_info)
# 计算平均FPS
avg_fps = self.frame_count / self.total_processing_time if self.total_processing_time > 0 else 0
# 发送完成消息
output_video_url = f'{settings.MEDIA_URL.rstrip("/")}/videos/{os.path.basename(output_path)}'
await self.send(text_data=json.dumps({
'type': 'custom_detection_end',
'output_video_url': output_video_url,
'analysis_report_url': report_url,
'metrics_summary': json_summary,
'key_metrics': key_metrics, # 新增关键指标字段
'roi_area_mapping': roi_area_mapping, # 新增ROI到分析区的映射
'total_frames': total_frames,
'processed_frames': frame_index,
'avg_fps': avg_fps,
'fps': fps,
'experiment_info': {
'name': experiment_name,
'note': experiment_note,
'duration': self.total_processing_time, # 这是总处理时间
'total_frames': total_frames,
'metrics': json_summary
}
}))
else:
# 发送取消消息
await self.send(text_data=json.dumps({
'type': 'detection_canceled',
'processed_frames': frame_index,
'total_frames': total_frames
}))
except Exception as e:
await self.send(text_data=json.dumps({
'type': 'error',
'message': f'处理错误: {str(e)}'
}))
import traceback
traceback.print_exc()