Hakureirm commited on
Commit
bd47b19
·
verified ·
1 Parent(s): 3a8b8c6

Update src/gait_analysis_report.py

Browse files
Files changed (1) hide show
  1. src/gait_analysis_report.py +1908 -122
src/gait_analysis_report.py CHANGED
@@ -4,11 +4,23 @@ import pandas as pd
4
  import matplotlib.pyplot as plt
5
  import seaborn as sns
6
  from dataclasses import dataclass
7
- from typing import List, Dict, Tuple
8
  from datetime import datetime
9
  import os
10
  import cv2
11
  import base64
 
 
 
 
 
 
 
 
 
 
 
 
12
 
13
  @dataclass
14
  class FootprintArea:
@@ -478,6 +490,107 @@ class GaitAnalysisReport:
478
  cap.release()
479
  print("Pressure timeline analysis completed!")
480
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
481
  def analyze_swing_metrics(self):
482
  """分析摆动相关指标"""
483
  print("\n开始分析摆动相关指标...")
@@ -949,18 +1062,36 @@ class GaitAnalysisReport:
949
  plt.close()
950
 
951
  def generate_detailed_table(self):
952
- """生成详细的足印数据表,包含每个足印的所有指标"""
953
  print("\n开始生成详细数据表...")
954
 
955
  if not self.video_path:
956
  raise ValueError("未指定视频文件路径")
957
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
958
  # 打开视频获取足印图像
959
  cap = cv2.VideoCapture(self.video_path)
960
  if not cap.isOpened():
961
  raise ValueError(f"无法打开视频文件: {self.video_path}")
962
 
963
  detailed_data = []
 
964
 
965
  # 按时间顺序排序所有足印
966
  sorted_footprints = sorted(self.footprint_areas, key=lambda x: x.start_frame)
@@ -974,28 +1105,54 @@ class GaitAnalysisReport:
974
  'max_contact': area.end_frame / self.fps
975
  }
976
 
977
- # 2. 提取中间帧的G通道图像
978
  middle_frame = area.start_frame + (area.end_frame - area.start_frame) // 2
979
  cap.set(cv2.CAP_PROP_POS_FRAMES, middle_frame)
980
  ret, frame = cap.read()
981
  if ret:
982
- # ROI
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
983
  x = max(0, area.position['x'])
984
  y = max(0, area.position['y'])
985
  w = area.position['width']
986
  h = area.position['height']
987
  roi = frame[y:y+h, x:x+w]
988
 
989
- # 只保留G通道,R和B通道置0
990
  roi_g = roi.copy()
991
- roi_g[:, :, 0] = 0 # R通道置0
992
- roi_g[:, :, 2] = 0 # B通道置0
993
 
994
- # 转换为base64
995
  _, buffer = cv2.imencode('.png', roi_g)
996
  footprint_data['image'] = base64.b64encode(buffer).decode('utf-8')
997
-
998
- # 添加帧号,替换原来的video_time_ms
999
  footprint_data['frame_id'] = middle_frame
1000
 
1001
  # 3. 计算足印尺寸
@@ -1028,9 +1185,18 @@ class GaitAnalysisReport:
1028
 
1029
  cap.release()
1030
 
 
 
 
 
 
 
 
 
1031
  # 创建最终的数据结构
1032
  output_data = {
1033
- "base_footprint_data": detailed_data
 
1034
  }
1035
 
1036
  # 保存为JSON文件
@@ -1039,8 +1205,66 @@ class GaitAnalysisReport:
1039
  json.dump(output_data, f, indent=2, ensure_ascii=False)
1040
 
1041
  print(f"详细数据表已生成:{output_path}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1042
  return output_data
1043
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1044
  def generate_collection_table(self):
1045
  """生成采集数据表格(Overview)
1046
  从record_params获取:实验名称、开始时间、方向等信息
@@ -1159,13 +1383,15 @@ class GaitAnalysisReport:
1159
  g_channel = roi[:, :, 1]
1160
  current_max = np.max(g_channel)
1161
  current_mean = np.mean(g_channel)
1162
- current_min = np.min(g_channel[g_channel > pressure_threshold])
 
 
 
 
1163
  current_area = np.sum(g_channel > pressure_threshold)
1164
 
1165
  max_intensity = max(max_intensity, current_max)
1166
  mean_intensity = max(mean_intensity, current_mean)
1167
- if current_min.size > 0:
1168
- min_intensity = min(min_intensity, current_min)
1169
  max_area = max(max_area, current_area)
1170
 
1171
  # 计算支撑和摆动相关数据
@@ -1455,101 +1681,132 @@ class GaitAnalysisReport:
1455
  return spacing_stats
1456
 
1457
  def generate_support_table(self) -> dict:
1458
- """生成支撑数据统计表格
1459
-
1460
- Returns:
1461
- dict: 包含足支撑统计数据的字典,包括支撑时间序列和各类支撑的统计数据
1462
- """
1463
- print("\n开始生成足支撑统计数据...")
 
 
 
 
 
 
1464
 
1465
- # 初始化支撑数据结构
1466
  support_stats = {
1467
- "support_sequence": [], # 支撑序列数据
1468
- "support_types": { # 各类支撑的统计
1469
- "standing": 0.0, # 支撑比
1470
- "zero": 0.0, # 无支撑
1471
- "single": 0.0, # 单足支撑
1472
- "diagonal": 0.0, # 对侧足支撑
1473
- "girdle": 0.0, # 同源足支撑
1474
- "lateral": 0.0, # 同侧足支撑
1475
- "three": 0.0, # 三足支撑
1476
- "four": 0.0 # 四足支撑
1477
  }
1478
  }
1479
-
1480
- # 获取所有足印的时间范围
1481
- all_footprints = []
1482
- for area in self.footprint_areas:
1483
- start_time = area.start_frame / self.fps
1484
- end_time = area.end_frame / self.fps
1485
- duration = end_time - start_time
1486
-
1487
- # 确定支撑类型
1488
- def get_footfall_formula(paw_type):
1489
- if paw_type == 'RF': return 'I-RF'
1490
- elif paw_type == 'LF': return 'I-LF'
1491
- elif paw_type == 'RH': return 'I-RH'
1492
- elif paw_type == 'LH': return 'I-LH'
1493
- return ''
1494
-
1495
- all_footprints.append({
1496
- 'start_time': start_time,
1497
- 'duration': duration,
1498
- 'paw_type': area.paw_type,
1499
- 'footfall_formula': get_footfall_formula(area.paw_type)
1500
- })
1501
-
1502
- # 按开始时间排序
1503
- all_footprints.sort(key=lambda x: x['start_time'])
1504
-
1505
  # 分析每个时间点的支撑情况
1506
- total_time = 0
1507
- current_support = 0
1508
- last_time = 0
1509
-
1510
  for fp in all_footprints:
1511
- # 添加到支撑序列
1512
- support_entry = {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1513
  "start_time": round(fp['start_time'], 4),
1514
  "duration": round(fp['duration'], 4),
1515
- "support_formula": str(current_support + 1), # 当前支���数量
1516
- "footfall_formula": fp['footfall_formula'] # 足印类型
1517
- }
1518
- support_stats["support_sequence"].append(support_entry)
1519
 
1520
- # 更新支撑类型统计
1521
- if current_support == 0:
1522
- support_stats["support_types"]["zero"] += fp['duration']
1523
- elif current_support == 1:
1524
- support_stats["support_types"]["single"] += fp['duration']
1525
- elif current_support == 2:
1526
- # 根据支撑的爪子类型判断是对侧还是同侧
1527
- active_paws = [p['paw_type'] for p in all_footprints
1528
- if p['start_time'] <= fp['start_time'] < p['start_time'] + p['duration']]
1529
- if ('RF' in active_paws and 'LH' in active_paws) or ('LF' in active_paws and 'RH' in active_paws):
1530
- support_stats["support_types"]["diagonal"] += fp['duration']
1531
- elif ('RF' in active_paws and 'RH' in active_paws) or ('LF' in active_paws and 'LH' in active_paws):
1532
- support_stats["support_types"]["lateral"] += fp['duration']
 
 
1533
  else:
1534
- support_stats["support_types"]["girdle"] += fp['duration']
1535
- elif current_support == 3:
1536
- support_stats["support_types"]["three"] += fp['duration']
1537
- elif current_support >= 4:
1538
- support_stats["support_types"]["four"] += fp['duration']
1539
-
1540
- current_support += 1
1541
- total_time = max(total_time, fp['start_time'] + fp['duration'])
1542
- last_time = fp['start_time'] + fp['duration']
1543
-
1544
- # 计算总支撑时间和比例
1545
- total_support_time = sum(support_stats["support_types"].values())
1546
- if total_support_time > 0:
1547
- # 转换为百分比
1548
- for key in support_stats["support_types"]:
1549
- support_stats["support_types"][key] = round(
1550
- (support_stats["support_types"][key] / total_support_time) * 100, 4
 
 
 
 
 
 
1551
  )
1552
-
1553
  print("足支撑统计数据生成完成!")
1554
  return support_stats
1555
 
@@ -1593,40 +1850,53 @@ class GaitAnalysisReport:
1593
  # 对每个组内的足印按时间排序
1594
  for paw_type in paw_groups:
1595
  paw_groups[paw_type].sort(key=lambda x: x['start_time'])
1596
-
1597
  # 分析爪子对之间的协调性
 
1598
  def analyze_pair_coordination(anchor_prints, target_prints, pair_type):
1599
  pair_stats = []
1600
  for anchor in anchor_prints:
1601
- # 防止除零错误:检查 duration 是否为 0
1602
- if anchor['duration'] <= 0:
1603
  continue
1604
 
1605
- # 寻找最近的目标足印
1606
- for target in target_prints:
1607
- if target['start_time'] >= anchor['start_time']:
1608
- # 计算协调值
1609
- value = abs(target['start_time'] - anchor['start_time']) / anchor['duration'] * 100
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1610
 
1611
- # 判断协调类型
1612
- peculiarity = "NOR" # 正常
1613
- if value > 100:
1614
- peculiarity = "ANT" # 提前
1615
- elif value == 0:
1616
- peculiarity = "TNA" # 同步
1617
 
1618
- coordination_entry = {
1619
- "value": round(value, 4),
1620
- "anchor_start": round(anchor['start_time'], 4),
1621
- "anchor_duration": round(anchor['duration'], 4),
1622
- "target_start": round(target['start_time'], 4),
1623
- "peculiarity": peculiarity
1624
- }
1625
- pair_stats.append(coordination_entry)
1626
- break
1627
 
1628
  return pair_stats
1629
-
1630
  # 分析所有配对
1631
  pair_configs = [
1632
  ('LF', 'RH', 'LF-RH'),
@@ -1666,6 +1936,1522 @@ class GaitAnalysisReport:
1666
  print("双足协调性统计数据生成完成!")
1667
  return coordination_stats
1668
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1669
  def main():
1670
  # 创建分析器实例
1671
  analyzer = GaitAnalysisReport('data/footprint_fixed_Exp2.json',
 
4
  import matplotlib.pyplot as plt
5
  import seaborn as sns
6
  from dataclasses import dataclass
7
+ from typing import List, Dict, Tuple, Optional
8
  from datetime import datetime
9
  import os
10
  import cv2
11
  import base64
12
+ from scipy.signal import savgol_filter
13
+ import logging
14
+ import io
15
+ import scipy.ndimage as ndimage
16
+ import shutil
17
+ import re
18
+ import subprocess
19
+ import tempfile
20
+ try:
21
+ import ffmpeg
22
+ except ImportError:
23
+ print("ffmpeg-python not installed. Using subprocess for video processing.")
24
 
25
  @dataclass
26
  class FootprintArea:
 
490
  cap.release()
491
  print("Pressure timeline analysis completed!")
492
 
493
+ def analyze_area_timeline(self):
494
+ """分析并绘制足印面积时序图"""
495
+ print("\n开始分析足印面积时序图...")
496
+
497
+ if not self.video_path:
498
+ raise ValueError("未指定视频文件路径")
499
+
500
+ # 打开视频
501
+ cap = cv2.VideoCapture(self.video_path)
502
+ if not cap.isOpened():
503
+ raise ValueError(f"无法打开视频文件: {self.video_path}")
504
+
505
+ # 设置压力阈值(用于计算有效足印面积)
506
+ pressure_threshold = 5
507
+
508
+ # 获取比例尺 (像素到毫米的转换)
509
+ # 假设1厘米 = 10毫米
510
+ scale_factor = self.record_params.get('actual_length', 20) / self.record_params.get('scale_length', 152) * 10
511
+
512
+ # 1. 按爪子类型分组并按时间排序
513
+ paw_groups = {'RF': [], 'RH': [], 'LF': [], 'LH': []}
514
+ for area in self.footprint_areas:
515
+ if area.paw_type in paw_groups:
516
+ paw_groups[area.paw_type].append({
517
+ 'start_frame': max(0, area.start_frame - self.pre_delay_frames),
518
+ 'end_frame': area.end_frame + self.post_delay_frames,
519
+ 'original_start': area.start_frame,
520
+ 'original_end': area.end_frame,
521
+ 'position': area.position
522
+ })
523
+
524
+ # 2. 创建图表 - 增加高度以提供更多空间
525
+ plt.figure(figsize=(15, 10))
526
+
527
+ # 创建子图
528
+ for i, (paw_type, footprints) in enumerate(paw_groups.items()):
529
+ ax = plt.subplot(4, 1, i+1)
530
+
531
+ for footprint in footprints:
532
+ # 初始化变量
533
+ areas = [0]
534
+ times = [footprint['start_frame'] / self.fps]
535
+
536
+ for frame_idx in range(footprint['start_frame'], footprint['end_frame']):
537
+ cap.set(cv2.CAP_PROP_POS_FRAMES, frame_idx)
538
+ ret, frame = cap.read()
539
+ if not ret:
540
+ continue
541
+
542
+ x = max(0, footprint['position']['x'])
543
+ y = max(0, footprint['position']['y'])
544
+ w = footprint['position']['width']
545
+ h = footprint['position']['height']
546
+ roi = frame[y:y+h, x:x+w]
547
+
548
+ if roi.size > 0:
549
+ # 只获取G通道的值
550
+ g_values = roi[:, :, 1]
551
+ # 计算面积(高于阈值的像素数量)
552
+ area_pixels = np.sum(g_values > pressure_threshold)
553
+ # 转换为平方毫米
554
+ area_mm2 = area_pixels * (scale_factor ** 2)
555
+ areas.append(float(area_mm2))
556
+ times.append(frame_idx / self.fps)
557
+
558
+ # 添加结束点
559
+ areas.append(0)
560
+ times.append(footprint['end_frame'] / self.fps)
561
+
562
+ # 绘制面积曲线
563
+ if times:
564
+ plt.plot(times, areas, '-', color=self.colors[paw_type],
565
+ alpha=0.7, label='Area' if i == 0 else '')
566
+ plt.fill_between(times, 0, areas, color=self.colors[paw_type], alpha=0.2)
567
+
568
+ # 设置Y轴标签和标题
569
+ plt.ylabel(f'{paw_type} Area (mm²)')
570
+ plt.title(f'{paw_type} Footprint Area Timeline')
571
+ plt.grid(True, alpha=0.3)
572
+
573
+ # 只在最后一个子图上显示X轴标签
574
+ if i < 3: # 前三个子图
575
+ plt.setp(ax.get_xticklabels(), visible=False)
576
+
577
+ # 添加图例
578
+ plt.legend(bbox_to_anchor=(1.05, 4.5), loc='upper left')
579
+
580
+ # 添加X轴标签(仅在最后一个子图上)
581
+ plt.xlabel('Time (seconds)')
582
+
583
+ # 调整子图间距,增加垂直间距
584
+ plt.subplots_adjust(hspace=0.4)
585
+
586
+ # 保存图片
587
+ plt.savefig(f'{self.result_dir}/plots/area_timeline.png',
588
+ dpi=300, bbox_inches='tight')
589
+ plt.close()
590
+
591
+ cap.release()
592
+ print("足印面积时序图分析完成!")
593
+
594
  def analyze_swing_metrics(self):
595
  """分析摆动相关指标"""
596
  print("\n开始分析摆动相关指标...")
 
1062
  plt.close()
1063
 
1064
  def generate_detailed_table(self):
 
1065
  print("\n开始生成详细数据表...")
1066
 
1067
  if not self.video_path:
1068
  raise ValueError("未指定视频文件路径")
1069
 
1070
+ # 添加调试日志
1071
+ print(f"\n检查关键点数据:")
1072
+ print(f"record_params keys: {self.record_params.keys()}")
1073
+ if 'bodyKeypoints' in self.record_params:
1074
+ print(f"bodyKeypoints 数量: {len(self.record_params['bodyKeypoints'])}")
1075
+ if self.record_params['bodyKeypoints']:
1076
+ print(f"第一个关键点数据示例: {self.record_params['bodyKeypoints'][0]}")
1077
+ else:
1078
+ print("未找到 bodyKeypoints 数据")
1079
+
1080
+ # 创建帧到关键点的映射
1081
+ keypoints_by_frame = {}
1082
+ for kp_data in self.record_params.get('bodyKeypoints', []):
1083
+ frame_id = kp_data['frame_id']
1084
+ keypoints_by_frame[frame_id] = kp_data['keypoints']
1085
+
1086
+ print(f"处理后的关键点帧数: {len(keypoints_by_frame)}")
1087
+
1088
  # 打开视频获取足印图像
1089
  cap = cv2.VideoCapture(self.video_path)
1090
  if not cap.isOpened():
1091
  raise ValueError(f"无法打开视频文件: {self.video_path}")
1092
 
1093
  detailed_data = []
1094
+ movement_data = [] # 新增:存储运动方向数据
1095
 
1096
  # 按时间顺序排序所有足印
1097
  sorted_footprints = sorted(self.footprint_areas, key=lambda x: x.start_frame)
 
1105
  'max_contact': area.end_frame / self.fps
1106
  }
1107
 
1108
+ # 2. 提取中间帧的图像
1109
  middle_frame = area.start_frame + (area.end_frame - area.start_frame) // 2
1110
  cap.set(cv2.CAP_PROP_POS_FRAMES, middle_frame)
1111
  ret, frame = cap.read()
1112
  if ret:
1113
+ # 该帧的关键点
1114
+ if middle_frame in keypoints_by_frame:
1115
+ kp = keypoints_by_frame[middle_frame]
1116
+ nose = kp['nose']
1117
+ tail_base = kp['tail_base']
1118
+
1119
+ # 计算全身图像的边界(增加10%余量)
1120
+ x_min = min(nose[0], tail_base[0])
1121
+ x_max = max(nose[0], tail_base[0])
1122
+ margin = (x_max - x_min) * 0.1
1123
+ x_min = max(0, int(x_min - margin))
1124
+ x_max = min(frame.shape[1], int(x_max + margin))
1125
+
1126
+ # 计算关键点在截取图片中的相对坐标
1127
+ nose_relative_x = int(nose[0] - x_min)
1128
+ nose_relative_y = int(nose[1]) # y坐标保持不变
1129
+ tail_base_relative_x = int(tail_base[0] - x_min)
1130
+ tail_base_relative_y = int(tail_base[1]) # y坐标保持不变
1131
+
1132
+ # 提取全身图像
1133
+ full_body_roi = frame[0:frame.shape[0], x_min:x_max]
1134
+ _, buffer_full = cv2.imencode('.png', full_body_roi)
1135
+ footprint_data['image_fullbody'] = base64.b64encode(buffer_full).decode('utf-8')
1136
+ # 添加关键点完整坐标
1137
+ footprint_data['keypoints'] = {
1138
+ 'nose': {'x': nose_relative_x, 'y': nose_relative_y},
1139
+ 'tail_base': {'x': tail_base_relative_x, 'y': tail_base_relative_y}
1140
+ }
1141
+
1142
+ # 提取足印ROI(原有代码)
1143
  x = max(0, area.position['x'])
1144
  y = max(0, area.position['y'])
1145
  w = area.position['width']
1146
  h = area.position['height']
1147
  roi = frame[y:y+h, x:x+w]
1148
 
1149
+ # 只保留G通道
1150
  roi_g = roi.copy()
1151
+ roi_g[:, :, 0] = 0
1152
+ roi_g[:, :, 2] = 0
1153
 
 
1154
  _, buffer = cv2.imencode('.png', roi_g)
1155
  footprint_data['image'] = base64.b64encode(buffer).decode('utf-8')
 
 
1156
  footprint_data['frame_id'] = middle_frame
1157
 
1158
  # 3. 计算足印尺寸
 
1185
 
1186
  cap.release()
1187
 
1188
+ # 计算平滑的运动方向
1189
+ movement_angles = self._calculate_smooth_movement_angles()
1190
+ for frame_id, angle in movement_angles.items():
1191
+ movement_data.append({
1192
+ 'frame_id': frame_id,
1193
+ 'movement_angle': angle
1194
+ })
1195
+
1196
  # 创建最终的数据结构
1197
  output_data = {
1198
+ "base_footprint_data": detailed_data,
1199
+ "movement_direction_data": movement_data # 新增:作为并列的数据
1200
  }
1201
 
1202
  # 保存为JSON文件
 
1205
  json.dump(output_data, f, indent=2, ensure_ascii=False)
1206
 
1207
  print(f"详细数据表已生成:{output_path}")
1208
+
1209
+ # 添加验证日志
1210
+ print(f"\n数据统计:")
1211
+ print(f"- 足印数据条数: {len(detailed_data)}")
1212
+ print(f"- 运动方向数据条数: {len(movement_data)}")
1213
+ print(f"- 包含全身图像的足印数: {sum(1 for d in detailed_data if 'image_fullbody' in d)}")
1214
+ print(f"- 包含足印图像的足印数: {sum(1 for d in detailed_data if 'image' in d)}")
1215
+
1216
+ # 检查第一条数据的结构(去除图像数据以便打印)
1217
+ if detailed_data:
1218
+ sample_data = detailed_data[0].copy()
1219
+ if 'image' in sample_data:
1220
+ sample_data['image'] = f"[base64 string of length {len(sample_data['image'])}]"
1221
+ if 'image_fullbody' in sample_data:
1222
+ sample_data['image_fullbody'] = f"[base64 string of length {len(sample_data['image_fullbody'])}]"
1223
+ print(f"\n示例数据结构:")
1224
+ print(json.dumps(sample_data, indent=2))
1225
+
1226
  return output_data
1227
 
1228
+ def _calculate_smooth_movement_angles(self):
1229
+ """计算平滑的运动方向角度"""
1230
+ # 收集所有mid点
1231
+ mid_points = []
1232
+ frame_ids = []
1233
+ for kp_data in self.record_params.get('bodyKeypoints', []):
1234
+ frame_ids.append(kp_data['frame_id'])
1235
+ mid_points.append(kp_data['keypoints']['mid'])
1236
+
1237
+ if not mid_points:
1238
+ return {}
1239
+
1240
+ # 转换为numpy数组
1241
+ points = np.array(mid_points)
1242
+
1243
+ # 使用Savitzky-Golay滤波器平滑轨迹
1244
+ window_length = min(len(points), 15) # 窗口长度必须是奇数
1245
+ if window_length % 2 == 0:
1246
+ window_length -= 1
1247
+ if window_length >= 3:
1248
+ smooth_x = savgol_filter(points[:, 0], window_length, 3)
1249
+ smooth_y = savgol_filter(points[:, 1], window_length, 3)
1250
+ else:
1251
+ smooth_x = points[:, 0]
1252
+ smooth_y = points[:, 1]
1253
+
1254
+ # 计算每个点的切线角度
1255
+ angles = {}
1256
+ for i in range(len(smooth_x)-1):
1257
+ dx = smooth_x[i+1] - smooth_x[i]
1258
+ dy = smooth_y[i+1] - smooth_y[i]
1259
+ angle = np.degrees(np.arctan2(dy, dx))
1260
+ angles[frame_ids[i]] = angle
1261
+
1262
+ # 处理最后一个点
1263
+ if frame_ids:
1264
+ angles[frame_ids[-1]] = angles[frame_ids[-2]] if frame_ids[-2] in angles else 0
1265
+
1266
+ return angles
1267
+
1268
  def generate_collection_table(self):
1269
  """生成采集数据表格(Overview)
1270
  从record_params获取:实验名称、开始时间、方向等信息
 
1383
  g_channel = roi[:, :, 1]
1384
  current_max = np.max(g_channel)
1385
  current_mean = np.mean(g_channel)
1386
+ # 修改这里:添加安全检查
1387
+ pressure_pixels = g_channel[g_channel > pressure_threshold]
1388
+ if pressure_pixels.size > 0:
1389
+ current_min = np.min(pressure_pixels)
1390
+ min_intensity = min(min_intensity, current_min)
1391
  current_area = np.sum(g_channel > pressure_threshold)
1392
 
1393
  max_intensity = max(max_intensity, current_max)
1394
  mean_intensity = max(mean_intensity, current_mean)
 
 
1395
  max_area = max(max_area, current_area)
1396
 
1397
  # 计算支撑和摆动相关数据
 
1681
  return spacing_stats
1682
 
1683
  def generate_support_table(self) -> dict:
1684
+ """生成支撑统计数据(带中英文字段)"""
1685
+ # 中英对照表
1686
+ support_type_mapping = {
1687
+ "diagonal": "对角支撑", # 对���交叉支撑(如II-DH)
1688
+ "four": "四肢支撑", # 四肢同时支撑
1689
+ "girdle": "同源支撑", # 同源双肢支撑(如II-GR)
1690
+ "lateral": "同侧支撑", # 同侧双肢支撑(如II-LR)
1691
+ "single": "单肢支撑", # 单肢独立支撑
1692
+ "standing": "站立支撑", # 静止站立状态
1693
+ "three": "三肢支撑", # 三肢同时支撑
1694
+ "zero": "无支撑" # 无任何肢体支撑
1695
+ }
1696
 
 
1697
  support_stats = {
1698
+ "support_sequence": [],
1699
+ "support_types": {
1700
+ "diagonal": 0.0,
1701
+ "four": 0.0,
1702
+ "girdle": 0.0,
1703
+ "lateral": 0.0,
1704
+ "single": 0.0,
1705
+ "standing": 0.0,
1706
+ "three": 0.0,
1707
+ "zero": 0.0
1708
  }
1709
  }
1710
+
1711
+ # 修复时间属性访问方式
1712
+ all_footprints = sorted(
1713
+ [{
1714
+ 'start_time': area.start_frame / self.fps, # 正确计算开始时间
1715
+ 'end_time': area.end_frame / self.fps, # 新增结束时间
1716
+ 'duration': (area.end_frame - area.start_frame) / self.fps,
1717
+ 'paw_type': area.paw_type
1718
+ } for area in self.footprint_areas],
1719
+ key=lambda x: x['start_time']
1720
+ )
1721
+
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1722
  # 分析每个时间点的支撑情况
 
 
 
 
1723
  for fp in all_footprints:
1724
+ # 获取当前所有活动爪子
1725
+ active_paws = [
1726
+ p['paw_type'] for p in all_footprints
1727
+ if p['start_time'] <= fp['start_time'] < (p['start_time'] + p['duration'])
1728
+ ]
1729
+ support_count = len(active_paws)
1730
+
1731
+ # 生成SupportFormula (1-4)
1732
+ support_formula = str(min(support_count, 4)) # 最大显示为4
1733
+
1734
+ # 生成FootfallFormula
1735
+ if support_count == 1:
1736
+ # 单肢模式 I-{爪子类型}
1737
+ footfall_code = f"I-{active_paws[0]}"
1738
+ elif support_count == 2:
1739
+ # 双肢模式 II-{类型}{组合}
1740
+ paw1, paw2 = sorted(active_paws)
1741
+
1742
+ # 判断支撑类型
1743
+ if (paw1[0] != paw2[0]) and (paw1[1] != paw2[1]): # 对侧交叉
1744
+ type_code = "D"
1745
+ pos_code = paw1[1]+paw2[1] # 取前后位置 F/H
1746
+ elif paw1[0] == paw2[0]: # 同源支撑
1747
+ type_code = "G"
1748
+ pos_code = paw1[0] # 取左右侧 R/L
1749
+ else: # 同侧支撑
1750
+ type_code = "L"
1751
+ pos_code = paw1[0] # 取左右侧 R/L
1752
+
1753
+ footfall_code = f"II-{type_code}{pos_code}"
1754
+ elif support_count == 3:
1755
+ # 三肢模式 III-{主导爪}
1756
+ lead_paw = min(active_paws) # 按字母顺序取第一个
1757
+ footfall_code = f"III-{lead_paw}"
1758
+ else:
1759
+ footfall_code = "IV"
1760
+
1761
+ # 记录支撑序列
1762
+ support_stats["support_sequence"].append({
1763
  "start_time": round(fp['start_time'], 4),
1764
  "duration": round(fp['duration'], 4),
1765
+ "support_formula": support_formula,
1766
+ "footfall_formula": footfall_code
1767
+ })
 
1768
 
1769
+ # 初始化支撑类型
1770
+ support_type = "unknown"
1771
+
1772
+ # 根据footfall_code判断支撑类型
1773
+ if support_count == 0:
1774
+ support_type = "zero"
1775
+ elif support_count == 1:
1776
+ support_type = "single"
1777
+ elif support_count == 2:
1778
+ if footfall_code.startswith("II-D"):
1779
+ support_type = "diagonal"
1780
+ elif footfall_code.startswith("II-G"):
1781
+ support_type = "girdle"
1782
+ elif footfall_code.startswith("II-L"):
1783
+ support_type = "lateral"
1784
  else:
1785
+ support_type = "unknown_dual" # 未知双肢类型
1786
+ elif support_count == 3:
1787
+ support_type = "three"
1788
+ elif support_count == 4:
1789
+ support_type = "four"
1790
+
1791
+ # 覆盖判断站立状态
1792
+ if self._is_standing_state(fp['start_time']):
1793
+ support_type = "standing"
1794
+
1795
+ # 持续时间
1796
+ if support_type in support_stats["support_types"]:
1797
+ support_stats["support_types"][support_type] += fp['duration']
1798
+ else:
1799
+ logging.warning(f"未知支撑类型: {support_type} 时间: {fp['start_time']}")
1800
+ continue
1801
+
1802
+ # 计算百分比(如果需要)
1803
+ total_duration = sum(support_stats["support_types"].values())
1804
+ if total_duration > 0:
1805
+ for k in support_stats["support_types"]:
1806
+ support_stats["support_types"][k] = round(
1807
+ (support_stats["support_types"][k] / total_duration) * 100, 4
1808
  )
1809
+
1810
  print("足支撑统计数据生成完成!")
1811
  return support_stats
1812
 
 
1850
  # 对每个组内的足印按时间排序
1851
  for paw_type in paw_groups:
1852
  paw_groups[paw_type].sort(key=lambda x: x['start_time'])
1853
+
1854
  # 分析爪子对之间的协调性
1855
+
1856
  def analyze_pair_coordination(anchor_prints, target_prints, pair_type):
1857
  pair_stats = []
1858
  for anchor in anchor_prints:
1859
+ # 增加有效性检查
1860
+ if anchor['duration'] <= 0 or anchor['start_time'] < 0:
1861
  continue
1862
 
1863
+ # 寻找时间窗口内的目标足印
1864
+ valid_targets = [
1865
+ t for t in target_prints
1866
+ if t['start_time'] >= anchor['start_time'] - 0.5 # 扩大时间窗口
1867
+ and t['start_time'] <= anchor['end_time'] + 0.5
1868
+ ]
1869
+
1870
+ for target in valid_targets:
1871
+ # 修正计算方式(取消绝对值)
1872
+ time_diff = target['start_time'] - anchor['start_time']
1873
+ value = (time_diff / anchor['duration']) * 100
1874
+
1875
+ # 修正异常类型判断
1876
+ if time_diff < 0: # 目标足提前启动
1877
+ peculiarity = "ANT"
1878
+ value = abs(value)
1879
+ elif 0 <= value <= 100:
1880
+ peculiarity = "NOR"
1881
+ else: # 超过100%为滞后
1882
+ peculiarity = "DEL" # 修改异常代码
1883
+ value = value % 100
1884
 
1885
+ # 添加有效性阈值
1886
+ if abs(time_diff) > 2.0: # 超过2秒视为无效数据
1887
+ continue
 
 
 
1888
 
1889
+ coordination_entry = {
1890
+ "value": round(value, 4),
1891
+ "anchor_start": round(anchor['start_time'], 4), # 保持原有名称
1892
+ "anchor_duration": round(anchor['duration'], 4), # 保持原有名称
1893
+ "target_start": round(target['start_time'], 4), # 保持原有名称
1894
+ "peculiarity": peculiarity
1895
+ }
1896
+ pair_stats.append(coordination_entry)
1897
+ break
1898
 
1899
  return pair_stats
 
1900
  # 分析所有配对
1901
  pair_configs = [
1902
  ('LF', 'RH', 'LF-RH'),
 
1936
  print("双足协调性统计数据生成完成!")
1937
  return coordination_stats
1938
 
1939
+ def _is_standing_state(self, current_time: float) -> bool:
1940
+ """修复后的站立状态判断"""
1941
+ # 转换帧号为时间
1942
+ start_time = current_time - 0.5
1943
+ hind_prints = [
1944
+ p for p in self.footprint_areas
1945
+ if p.paw_type in ('LH', 'RH') and
1946
+ (p.start_frame / self.fps) <= current_time <= (p.end_frame / self.fps) and
1947
+ (p.start_frame / self.fps) >= start_time
1948
+ ]
1949
+
1950
+ # 持续时间计算修正
1951
+ return (
1952
+ len(hind_prints) == 2 and
1953
+ all((p.end_frame - p.start_frame)/self.fps >= 0.5 for p in hind_prints)
1954
+ )
1955
+
1956
+ def generate_3d_footprint_analysis(self):
1957
+ """生成3D足印分析数据"""
1958
+ print("\n开始生成3D足印可视化...")
1959
+
1960
+ # 创建保存目录
1961
+ footprint_3d_dir = f'{self.result_dir}/plots/footprints_3d'
1962
+ os.makedirs(footprint_3d_dir, exist_ok=True)
1963
+
1964
+ html_dir = f'{self.result_dir}/plots/interactive_3d'
1965
+ os.makedirs(html_dir, exist_ok=True)
1966
+
1967
+ # 生成静态3D图像
1968
+ self._generate_3d_footprint_plots()
1969
+
1970
+ # 生成交互式3D足印可视化
1971
+ interactive_3d_data = self._generate_interactive_3d_footprints()
1972
+
1973
+ # 创建索引页面
1974
+ self._create_3d_footprint_index(html_dir)
1975
+
1976
+ return interactive_3d_data
1977
+
1978
+ def _generate_3d_footprint_plots(self):
1979
+ """为每个足印区域生成3D图像"""
1980
+ import matplotlib.pyplot as plt
1981
+ from mpl_toolkits.mplot3d import Axes3D
1982
+ import numpy as np
1983
+ import os
1984
+
1985
+ # 创建保存目录
1986
+ footprint_3d_dir = f'{self.result_dir}/plots/footprints_3d'
1987
+ os.makedirs(footprint_3d_dir, exist_ok=True)
1988
+
1989
+ # 按cluster(area_id)分组足印
1990
+ cluster_groups = {}
1991
+ for footprint in self.footprint_areas:
1992
+ if footprint.area_id not in cluster_groups:
1993
+ cluster_groups[footprint.area_id] = []
1994
+ cluster_groups[footprint.area_id].append(footprint)
1995
+
1996
+ # 处理每个cluster
1997
+ for cluster_id, footprints in cluster_groups.items():
1998
+ # 按时间排序并选择中位数时间点的足印作为关��足印
1999
+ sorted_footprints = sorted(footprints, key=lambda x: x.start_frame)
2000
+ key_footprint = sorted_footprints[len(sorted_footprints)//2]
2001
+
2002
+ # 打开视频并提取足印图像
2003
+ if self.video_path:
2004
+ cap = cv2.VideoCapture(self.video_path)
2005
+ if not cap.isOpened():
2006
+ print(f"无法打开视频: {self.video_path}")
2007
+ continue
2008
+
2009
+ # 获取足印中间帧
2010
+ middle_frame = (key_footprint.start_frame + key_footprint.end_frame) // 2
2011
+ cap.set(cv2.CAP_PROP_POS_FRAMES, middle_frame)
2012
+ ret, frame = cap.read()
2013
+
2014
+ if not ret:
2015
+ print(f"无法读取帧: {middle_frame}")
2016
+ cap.release()
2017
+ continue
2018
+
2019
+ # 提取足印ROI
2020
+ x = max(0, key_footprint.position['x'])
2021
+ y = max(0, key_footprint.position['y'])
2022
+ w = key_footprint.position['width']
2023
+ h = key_footprint.position['height']
2024
+
2025
+ if x+w > frame.shape[1] or y+h > frame.shape[0]:
2026
+ print(f"ROI超出图像范围: {x},{y},{w},{h}")
2027
+ cap.release()
2028
+ continue
2029
+
2030
+ roi = frame[y:y+h, x:x+w]
2031
+
2032
+ # 提取绿色通道作为强度图
2033
+ green_channel = roi[:,:,1]
2034
+
2035
+ # 确定方形区域大小(使用最长边)
2036
+ max_side = max(green_channel.shape[0], green_channel.shape[1])
2037
+ square_patch = np.zeros((max_side, max_side), dtype=np.uint8)
2038
+
2039
+ # 将原图放在方形中心
2040
+ start_y = (max_side - green_channel.shape[0]) // 2
2041
+ start_x = (max_side - green_channel.shape[1]) // 2
2042
+ square_patch[start_y:start_y+green_channel.shape[0],
2043
+ start_x:start_x+green_channel.shape[1]] = green_channel
2044
+
2045
+ # 创建网格
2046
+ x = np.linspace(0, square_patch.shape[1]-1, square_patch.shape[1])
2047
+ y = np.linspace(0, square_patch.shape[0]-1, square_patch.shape[0])
2048
+ X, Y = np.meshgrid(x, y)
2049
+
2050
+ # 创建3D图
2051
+ fig = plt.figure(figsize=(10, 8))
2052
+ ax = fig.add_subplot(111, projection='3d')
2053
+
2054
+ # 绘制3D表面
2055
+ surf = ax.plot_surface(X, Y, square_patch, cmap='viridis')
2056
+
2057
+ # 设置标题和标签
2058
+ paw_type = key_footprint.paw_type
2059
+ ax.set_title(f'Footprint 3D View - {paw_type} #{cluster_id}')
2060
+ ax.set_xlabel('X')
2061
+ ax.set_ylabel('Y')
2062
+ ax.set_zlabel('Intensity')
2063
+
2064
+ # 添加颜色条
2065
+ fig.colorbar(surf)
2066
+
2067
+ # 保存图像
2068
+ plt.savefig(f'{footprint_3d_dir}/footprint_{cluster_id}_{paw_type}.png',
2069
+ dpi=300, bbox_inches='tight')
2070
+ plt.close()
2071
+ cap.release()
2072
+
2073
+ def _generate_interactive_3d_footprints(self):
2074
+ """生成交互式3D足印热图可视化并返回base64编码的HTML内容"""
2075
+ try:
2076
+ import plotly.graph_objects as go
2077
+ from plotly.subplots import make_subplots
2078
+ import numpy as np
2079
+ import os
2080
+ import cv2
2081
+ from scipy.interpolate import griddata
2082
+ except ImportError:
2083
+ print("请安装必要的库: pip install plotly scipy")
2084
+ return {}
2085
+
2086
+ # 创建保存目录
2087
+ html_dir = f'{self.result_dir}/plots/interactive_3d'
2088
+ os.makedirs(html_dir, exist_ok=True)
2089
+
2090
+ # 按cluster(area_id)分组足印
2091
+ cluster_groups = {}
2092
+ for footprint in self.footprint_areas:
2093
+ if footprint.area_id not in cluster_groups:
2094
+ cluster_groups[footprint.area_id] = []
2095
+ cluster_groups[footprint.area_id].append(footprint)
2096
+
2097
+ # 存储HTML内容的字典
2098
+ html_data = {}
2099
+
2100
+ # 处理每个cluster生成单独的HTML
2101
+ for cluster_id, footprints in cluster_groups.items():
2102
+ # 按时间排序并选择中位数时间点的足印作为关键足印
2103
+ sorted_footprints = sorted(footprints, key=lambda x: x.start_frame)
2104
+ key_footprint = sorted_footprints[len(sorted_footprints)//2]
2105
+
2106
+ paw_type = key_footprint.paw_type
2107
+ html_path = f'{html_dir}/footprint_{cluster_id}_{paw_type}.html'
2108
+
2109
+ # 打开视频并提取足印图像
2110
+ if self.video_path:
2111
+ cap = cv2.VideoCapture(self.video_path)
2112
+ if not cap.isOpened():
2113
+ print(f"无法打开视频: {self.video_path}")
2114
+ continue
2115
+
2116
+ # 获取足印中间帧
2117
+ middle_frame = (key_footprint.start_frame + key_footprint.end_frame) // 2
2118
+ cap.set(cv2.CAP_PROP_POS_FRAMES, middle_frame)
2119
+ ret, frame = cap.read()
2120
+
2121
+ if not ret:
2122
+ print(f"无法读取帧: {middle_frame}")
2123
+ cap.release()
2124
+ continue
2125
+
2126
+ # 提取足印ROI
2127
+ x = max(0, key_footprint.position['x'])
2128
+ y = max(0, key_footprint.position['y'])
2129
+ w = key_footprint.position['width']
2130
+ h = key_footprint.position['height']
2131
+
2132
+ if x+w > frame.shape[1] or y+h > frame.shape[0]:
2133
+ print(f"ROI超出图像范围: {x},{y},{w},{h}")
2134
+ cap.release()
2135
+ continue
2136
+
2137
+ roi = frame[y:y+h, x:x+w]
2138
+
2139
+ # 检查图像格式并处理
2140
+ if len(roi.shape) == 2 or (len(roi.shape) == 3 and roi.shape[2] == 1):
2141
+ # 单通道图像(灰度图)- 直接使用
2142
+ if len(roi.shape) == 3:
2143
+ gray = roi[:,:,0]
2144
+ else:
2145
+ gray = roi
2146
+ else:
2147
+ # 多通道图像(BGR或RGB)- 先提取绿色通道
2148
+ hsv = cv2.cvtColor(roi, cv2.COLOR_BGR2HSV)
2149
+
2150
+ # 定义绿色的HSV范围
2151
+ lower_green = np.array([40, 40, 40])
2152
+ upper_green = np.array([80, 255, 255])
2153
+
2154
+ # 创建掩码
2155
+ mask = cv2.inRange(hsv, lower_green, upper_green)
2156
+
2157
+ # 应用掩码
2158
+ green_only = cv2.bitwise_and(roi, roi, mask=mask)
2159
+
2160
+ # 转换为灰度图
2161
+ gray = cv2.cvtColor(green_only, cv2.COLOR_BGR2GRAY)
2162
+
2163
+ # 应用高斯模糊平滑过渡
2164
+ smooth_gray = ndimage.gaussian_filter(gray, sigma=1.5)
2165
+
2166
+ # 过滤低值(底色)
2167
+ threshold = np.max(smooth_gray) * 0.1 if np.max(smooth_gray) > 0 else 0
2168
+ filtered_gray = np.where(smooth_gray < threshold, 0, smooth_gray)
2169
+
2170
+ # 确定方形区域大小(使用最长边)
2171
+ max_side = max(filtered_gray.shape[0], filtered_gray.shape[1])
2172
+ square_patch = np.zeros((max_side, max_side), dtype=np.uint8)
2173
+
2174
+ # 将原图放在方形中心
2175
+ start_y = (max_side - filtered_gray.shape[0]) // 2
2176
+ start_x = (max_side - filtered_gray.shape[1]) // 2
2177
+ square_patch[start_y:start_y+filtered_gray.shape[0],
2178
+ start_x:start_x+filtered_gray.shape[1]] = filtered_gray
2179
+
2180
+ # 增加分辨率(插值平滑)
2181
+ factor = 2 # 分辨率提高因子
2182
+ new_size = max_side * factor
2183
+
2184
+ # 原始坐标
2185
+ y_old, x_old = np.mgrid[0:max_side, 0:max_side]
2186
+ # 新坐标
2187
+ y_new, x_new = np.mgrid[0:max_side:complex(0, new_size), 0:max_side:complex(0, new_size)]
2188
+
2189
+ # 执行插值
2190
+ z_upscaled = griddata((y_old.flatten(), x_old.flatten()), square_patch.flatten(),
2191
+ (y_new, x_new), method='cubic', fill_value=0)
2192
+
2193
+ # 再次应用平滑
2194
+ z_upscaled = ndimage.gaussian_filter(z_upscaled, sigma=1)
2195
+
2196
+ # 创建交互式3D图
2197
+ fig = go.Figure(data=[go.Surface(
2198
+ z=z_upscaled,
2199
+ colorscale='Jet', # 热图色彩
2200
+ colorbar=dict(title="强度"),
2201
+ contours = {
2202
+ "z": {"show": True, "start": 0, "end": 255, "size": 10}
2203
+ }
2204
+ )])
2205
+
2206
+ # 设置图表布局和标题
2207
+ fig.update_layout(
2208
+ title=f'足印3D交互式热图 - {paw_type} #{cluster_id}',
2209
+ width=800,
2210
+ height=800,
2211
+ scene=dict(
2212
+ xaxis_title='X',
2213
+ yaxis_title='Y',
2214
+ zaxis_title='强度',
2215
+ aspectratio=dict(x=1, y=1, z=0.5),
2216
+ camera=dict(
2217
+ eye=dict(x=1.5, y=1.5, z=0.8)
2218
+ )
2219
+ ),
2220
+ margin=dict(l=0, r=0, b=0, t=30)
2221
+ )
2222
+
2223
+ # 保存为HTML文件
2224
+ fig.write_html(
2225
+ html_path,
2226
+ include_plotlyjs='cdn',
2227
+ full_html=True,
2228
+ config={
2229
+ 'displayModeBar': True,
2230
+ 'editable': True,
2231
+ 'toImageButtonOptions': {
2232
+ 'format': 'png',
2233
+ 'filename': f'footprint_{cluster_id}_{paw_type}',
2234
+ 'height': 800,
2235
+ 'width': 800,
2236
+ 'scale': 2
2237
+ }
2238
+ }
2239
+ )
2240
+
2241
+ # 读取HTML内容并转换为base64
2242
+ with open(html_path, 'rb') as f:
2243
+ html_content = f.read()
2244
+ html_base64 = base64.b64encode(html_content).decode('utf-8')
2245
+
2246
+ # 保存到字典
2247
+ html_data[f"{cluster_id}_{paw_type}"] = {
2248
+ 'html_base64': html_base64,
2249
+ 'filename': f'footprint_{cluster_id}_{paw_type}.html',
2250
+ 'paw_type': paw_type,
2251
+ 'cluster_id': cluster_id
2252
+ }
2253
+
2254
+ cap.release()
2255
+
2256
+ print(f"交互式3D足印热图已保存至: {html_path}")
2257
+
2258
+ # 创建索引页面
2259
+ self._create_3d_footprint_index(html_dir)
2260
+
2261
+ return html_data
2262
+
2263
+ def _create_3d_footprint_index(self, html_dir):
2264
+ """创建3D足印可视化的HTML索引页面"""
2265
+ html_files = [f for f in os.listdir(html_dir) if f.endswith('.html') and f != 'index.html']
2266
+
2267
+ if not html_files:
2268
+ return
2269
+
2270
+ index_html = f'{html_dir}/index.html'
2271
+
2272
+ # 按足爪类型分组文件
2273
+ paw_types = {
2274
+ 'leftFront': [],
2275
+ 'rightFront': [],
2276
+ 'leftHind': [],
2277
+ 'rightHind': [],
2278
+ 'unknown': []
2279
+ }
2280
+
2281
+ type_map = {
2282
+ 'LF': 'leftFront',
2283
+ 'RF': 'rightFront',
2284
+ 'LH': 'leftHind',
2285
+ 'RH': 'rightHind'
2286
+ }
2287
+
2288
+ for filename in html_files:
2289
+ for paw_code, paw_name in type_map.items():
2290
+ if paw_code in filename:
2291
+ paw_types[paw_name].append(filename)
2292
+ break
2293
+ else:
2294
+ paw_types['unknown'].append(filename)
2295
+
2296
+ # 生成HTML内容
2297
+ with open(index_html, 'w', encoding='utf-8') as f:
2298
+ f.write("""
2299
+ <!DOCTYPE html>
2300
+ <html>
2301
+ <head>
2302
+ <meta charset="UTF-8">
2303
+ <title>足印3D可视化索引</title>
2304
+ <style>
2305
+ body { font-family: Arial, sans-serif; line-height: 1.6; margin: 20px; }
2306
+ h1 { color: #333; text-align: center; }
2307
+ h2 { color: #555; margin-top: 30px; }
2308
+ .container { display: flex; flex-wrap: wrap; justify-content: center; }
2309
+ .item { margin: 10px; text-align: center; }
2310
+ .item a { display: block; padding: 10px; border: 1px solid #ddd; border-radius: 5px;
2311
+ text-decoration: none; color: #555; transition: all 0.3s; }
2312
+ .item a:hover { background-color: #f5f5f5; transform: scale(1.05); }
2313
+ .leftFront a { border-color: #ff9999; }
2314
+ .rightFront a { border-color: #99ff99; }
2315
+ .leftHind a { border-color: #9999ff; }
2316
+ .rightHind a { border-color: #ffff99; }
2317
+ </style>
2318
+ </head>
2319
+ <body>
2320
+ <h1>足印3D交互式可视化索引</h1>
2321
+ """)
2322
+
2323
+ # 添加每种足爪类型的文件链接
2324
+ type_labels = {
2325
+ 'leftFront': '左前爪',
2326
+ 'rightFront': '右前爪',
2327
+ 'leftHind': '左后爪',
2328
+ 'rightHind': '右后爪',
2329
+ 'unknown': '未知类型'
2330
+ }
2331
+
2332
+ for paw_type, files in paw_types.items():
2333
+ if files:
2334
+ f.write(f'<h2>{type_labels[paw_type]}</h2>\n')
2335
+ f.write('<div class="container">\n')
2336
+
2337
+ for filename in sorted(files):
2338
+ # 提取cluster_id
2339
+ parts = filename.split('_')
2340
+ if len(parts) >= 2:
2341
+ cluster_id = parts[1]
2342
+ f.write(f'<div class="item {paw_type}">\n')
2343
+ f.write(f'<a href="{filename}" target="_blank">足印 #{cluster_id}</a>\n')
2344
+ f.write('</div>\n')
2345
+
2346
+ f.write('</div>\n')
2347
+
2348
+ f.write("""
2349
+ </body>
2350
+ </html>
2351
+ """)
2352
+
2353
+ print(f"足印3D可视化索引页面已创建: {index_html}")
2354
+
2355
+ # 索引页也转为base64
2356
+ with open(index_html, 'rb') as f:
2357
+ html_content = f.read()
2358
+ index_base64 = base64.b64encode(html_content).decode('utf-8')
2359
+
2360
+ return index_base64
2361
+
2362
+ def generate_footprint_timeline(self):
2363
+ """生成足印步行图分析数据"""
2364
+ print("\n开始生成足印步行图...")
2365
+
2366
+ # 创建保存目录
2367
+ timeline_dir = f'{self.result_dir}/plots/footprint_timeline'
2368
+ os.makedirs(timeline_dir, exist_ok=True)
2369
+
2370
+ videos_dir = f'{self.result_dir}/videos'
2371
+ os.makedirs(videos_dir, exist_ok=True)
2372
+
2373
+ # 生成足印步行热图视频和图片
2374
+ timeline_data = self._generate_footprint_timeline_video()
2375
+
2376
+ return timeline_data
2377
+
2378
+ def _generate_footprint_timeline_video(self):
2379
+ """生成足印步行热图视频和图片序列,并返回base64编码的数据"""
2380
+ import numpy as np
2381
+ import matplotlib.pyplot as plt
2382
+ from matplotlib.colors import LinearSegmentedColormap
2383
+ import os
2384
+ from matplotlib.patches import Rectangle
2385
+
2386
+ # 创建保存目录
2387
+ timeline_dir = f'{self.result_dir}/plots/footprint_timeline'
2388
+ os.makedirs(timeline_dir, exist_ok=True)
2389
+
2390
+ videos_dir = f'{self.result_dir}/videos'
2391
+ os.makedirs(videos_dir, exist_ok=True)
2392
+
2393
+ # 获取视频信息
2394
+ video_fps = self.fps
2395
+
2396
+ # 按足印类型获取颜色
2397
+ paw_colors = {
2398
+ 'LF': 'red',
2399
+ 'RF': 'green',
2400
+ 'LH': 'blue',
2401
+ 'RH': 'yellow'
2402
+ }
2403
+
2404
+ # 创建热图色彩映射
2405
+ cmap_jet = plt.cm.get_cmap('jet')
2406
+
2407
+ # 按爪子类型分组足印
2408
+ paw_groups = {'RF': [], 'RH': [], 'LF': [], 'LH': []}
2409
+ for area in self.footprint_areas:
2410
+ paw_groups[area.paw_type].append(area)
2411
+
2412
+ # 将footprint_areas重新组织为键值对形式,便于后续处理
2413
+ cluster_to_prints = {}
2414
+ for footprint in self.footprint_areas:
2415
+ if footprint.area_id not in cluster_to_prints:
2416
+ cluster_to_prints[footprint.area_id] = []
2417
+
2418
+ # 从area_id中提取数字部分作为简化ID
2419
+ simple_id = footprint.area_id
2420
+ if footprint.area_id.startswith("footprintArea_"):
2421
+ simple_id = footprint.area_id.replace("footprintArea_", "")
2422
+
2423
+ cluster_to_prints[footprint.area_id].append({
2424
+ 'cluster_id': footprint.area_id,
2425
+ 'simple_id': simple_id, # 存储简化的ID
2426
+ 'paw_type': footprint.paw_type,
2427
+ 'start_frame': footprint.start_frame,
2428
+ 'end_frame': footprint.end_frame,
2429
+ 'position': footprint.position,
2430
+ 'x': footprint.position['x'] + footprint.position['width']/2, # 中心点x
2431
+ 'y': footprint.position['y'] + footprint.position['height']/2, # 中心点y
2432
+ 'w': footprint.position['width'],
2433
+ 'h': footprint.position['height'],
2434
+ 'frame_id': footprint.start_frame # 使用开始帧作为frame_id
2435
+ })
2436
+
2437
+ # 提取每个cluster的关键足印
2438
+ key_footprints = []
2439
+ cluster_heatmaps = {}
2440
+
2441
+ for cluster_id, prints in cluster_to_prints.items():
2442
+ # 按时间排序并选择中位数时间点的足印作为关键足印
2443
+ sorted_prints = sorted(prints, key=lambda x: x['start_frame'])
2444
+ key_print = sorted_prints[len(sorted_prints)//2]
2445
+ key_footprints.append(key_print)
2446
+
2447
+ # 获取足印图像并处理
2448
+ if self.video_path:
2449
+ cap = cv2.VideoCapture(self.video_path)
2450
+ if not cap.isOpened():
2451
+ print(f"无法打开视频: {self.video_path}")
2452
+ continue
2453
+
2454
+ # 获取中间帧
2455
+ middle_frame = (key_print['start_frame'] + key_print['end_frame']) // 2
2456
+ cap.set(cv2.CAP_PROP_POS_FRAMES, middle_frame)
2457
+ ret, frame = cap.read()
2458
+
2459
+ if not ret:
2460
+ print(f"无法读取帧: {middle_frame}")
2461
+ cap.release()
2462
+ continue
2463
+
2464
+ # 提取ROI
2465
+ x = max(0, int(key_print['position']['x']))
2466
+ y = max(0, int(key_print['position']['y']))
2467
+ w = int(key_print['position']['width'])
2468
+ h = int(key_print['position']['height'])
2469
+
2470
+ if x+w > frame.shape[1] or y+h > frame.shape[0]:
2471
+ print(f"ROI超出图像范围: {x},{y},{w},{h}")
2472
+ cap.release()
2473
+ continue
2474
+
2475
+ patch = frame[y:y+h, x:x+w]
2476
+
2477
+ # 检查图像格式并处理
2478
+ if len(patch.shape) == 2 or (len(patch.shape) == 3 and patch.shape[2] == 1):
2479
+ # 单通道图像(灰度图)- 直接使用
2480
+ if len(patch.shape) == 3:
2481
+ gray = patch[:,:,0] # 如果是形状为(h,w,1),提取为(h,w)
2482
+ else:
2483
+ gray = patch
2484
+ else:
2485
+ # 多通道图像(BGR或RGB)- 先提取绿色通道
2486
+ hsv = cv2.cvtColor(patch, cv2.COLOR_BGR2HSV)
2487
+
2488
+ # 定义绿色的HSV范围
2489
+ lower_green = np.array([40, 40, 40])
2490
+ upper_green = np.array([80, 255, 255])
2491
+
2492
+ # 创建掩码
2493
+ mask = cv2.inRange(hsv, lower_green, upper_green)
2494
+
2495
+ # 应用掩码
2496
+ green_only = cv2.bitwise_and(patch, patch, mask=mask)
2497
+
2498
+ # 转换为灰度图
2499
+ gray = cv2.cvtColor(green_only, cv2.COLOR_BGR2GRAY)
2500
+
2501
+ # 应用高斯模糊平滑过渡
2502
+ smooth_gray = ndimage.gaussian_filter(gray, sigma=1.0)
2503
+
2504
+ # 过滤低值(底色)
2505
+ threshold = np.max(smooth_gray) * 0.1 if np.max(smooth_gray) > 0 else 0
2506
+ filtered_gray = np.where(smooth_gray < threshold, 0, smooth_gray)
2507
+
2508
+ # 标准化为0-1范围
2509
+ max_val = np.max(filtered_gray)
2510
+ if max_val > 0:
2511
+ norm_gray = filtered_gray / max_val
2512
+ else:
2513
+ norm_gray = filtered_gray
2514
+
2515
+ # 将灰度图转换为热图
2516
+ heatmap = cmap_jet(norm_gray)
2517
+
2518
+ # 转换为BGR格式(OpenCV使用BGR)
2519
+ heatmap_bgr = (heatmap[:,:,:3][:,:,::-1] * 255).astype(np.uint8)
2520
+
2521
+ # 添加透明度通道
2522
+ alpha = np.where(norm_gray > 0, 0.7, 0).reshape(norm_gray.shape[0], norm_gray.shape[1], 1)
2523
+ heatmap_bgra = np.concatenate([heatmap_bgr, (alpha * 255).astype(np.uint8)], axis=2)
2524
+
2525
+ # 保存该cluster的热图
2526
+ cluster_heatmaps[cluster_id] = {
2527
+ 'heatmap': heatmap_bgra,
2528
+ 'print': key_print
2529
+ }
2530
+
2531
+ cap.release()
2532
+
2533
+ # 按时间排序关键足印
2534
+ key_footprints.sort(key=lambda x: x['start_frame'])
2535
+
2536
+ # 获取帧范围
2537
+ first_frame = min(p['start_frame'] for p in key_footprints)
2538
+ last_frame = max(p['end_frame'] for p in key_footprints)
2539
+
2540
+ # 计算图像边界(只基于关键足印)
2541
+ min_x = min(p['position']['x'] for p in key_footprints)
2542
+ max_x = max(p['position']['x'] + p['position']['width'] for p in key_footprints)
2543
+ min_y = min(p['position']['y'] for p in key_footprints)
2544
+ max_y = max(p['position']['y'] + p['position']['height'] for p in key_footprints)
2545
+
2546
+ # 添加边距
2547
+ margin = 50
2548
+ min_x = max(0, min_x - margin)
2549
+ min_y = max(0, min_y - margin)
2550
+ max_x += margin
2551
+ max_y += margin
2552
+
2553
+ # 计算图像尺寸
2554
+ width = int(max_x - min_x)
2555
+ height = int(max_y - min_y)
2556
+
2557
+ # 确保尺寸是偶数(视频编码要求)
2558
+ width = width + (width % 2)
2559
+ height = height + (height % 2)
2560
+
2561
+ # 初始化视频写入器
2562
+ raw_video_path = f'{videos_dir}/footprint_timeline_raw.mp4'
2563
+ video_path = f'{videos_dir}/footprint_timeline.mp4'
2564
+ fourcc = cv2.VideoWriter_fourcc(*'mp4v')
2565
+ video_writer = cv2.VideoWriter(raw_video_path, fourcc, video_fps, (width, height))
2566
+
2567
+ # 按帧ID对足印分组
2568
+ frame_to_key_prints = {}
2569
+ for p in key_footprints:
2570
+ frame_id = p['start_frame']
2571
+ if frame_id not in frame_to_key_prints:
2572
+ frame_to_key_prints[frame_id] = []
2573
+ frame_to_key_prints[frame_id].append(p)
2574
+
2575
+ # 保存中间进度的帧
2576
+ progress_frames = {}
2577
+ progress_points = [0, 25, 50, 75, 100] # 进度百分比
2578
+ total_frames = last_frame - first_frame
2579
+ progress_frame_ids = [first_frame + int(p * total_frames / 100) for p in progress_points]
2580
+
2581
+ # 生成每一帧的可视化
2582
+ for frame_id in range(first_frame, last_frame + 1):
2583
+ # 创建画布
2584
+ canvas = np.zeros((height, width, 3), dtype=np.uint8)
2585
+ canvas.fill(255) # 白色背景
2586
+
2587
+ # 绘制已出现的所有关键足印
2588
+ for f_id in range(first_frame, frame_id + 1):
2589
+ if f_id in frame_to_key_prints:
2590
+ for footprint in frame_to_key_prints[f_id]:
2591
+ cluster_id = footprint['cluster_id']
2592
+ if cluster_id in cluster_heatmaps:
2593
+ heatmap_data = cluster_heatmaps[cluster_id]
2594
+ heatmap = heatmap_data['heatmap']
2595
+
2596
+ # 计算在画布上的位置
2597
+ x = int(footprint['position']['x'] - min_x)
2598
+ y = int(footprint['position']['y'] - min_y)
2599
+ w = int(footprint['position']['width'])
2600
+ h = int(footprint['position']['height'])
2601
+
2602
+ # 计算缩放比例
2603
+ scale_x = w / heatmap.shape[1]
2604
+ scale_y = h / heatmap.shape[0]
2605
+ scale = min(scale_x, scale_y)
2606
+
2607
+ # 等比例缩放热图
2608
+ new_w = int(heatmap.shape[1] * scale)
2609
+ new_h = int(heatmap.shape[0] * scale)
2610
+
2611
+ if new_w > 0 and new_h > 0: # 确保尺寸有效
2612
+ resized_heatmap = cv2.resize(heatmap, (new_w, new_h))
2613
+
2614
+ # 计算居中偏移
2615
+ offset_x = (w - new_w) // 2
2616
+ offset_y = (h - new_h) // 2
2617
+
2618
+ # 绘制足印类型和简化ID
2619
+ paw_type = footprint['paw_type']
2620
+ simple_id = footprint['simple_id']
2621
+ text = f"{paw_type} {simple_id}"
2622
+ cv2.putText(canvas, text, (x + w + 5, y + h//2),
2623
+ cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 0, 0), 1)
2624
+
2625
+ # 添加热图到画布上
2626
+ # 处理透明度混合
2627
+ for i in range(new_h):
2628
+ for j in range(new_w):
2629
+ if 0 <= x + j + offset_x < width and 0 <= y + i + offset_y < height:
2630
+ alpha = resized_heatmap[i, j, 3] / 255.0
2631
+ if alpha > 0:
2632
+ canvas[y + i + offset_y, x + j + offset_x] = \
2633
+ (1 - alpha) * canvas[y + i + offset_y, x + j + offset_x] + \
2634
+ alpha * resized_heatmap[i, j, :3]
2635
+
2636
+ # 添加当前帧信息
2637
+ cv2.putText(canvas, f"Frame: {frame_id}", (10, 30),
2638
+ cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 0, 0), 2)
2639
+
2640
+ # 保存进度图像
2641
+ if frame_id in progress_frame_ids:
2642
+ progress = progress_points[progress_frame_ids.index(frame_id)]
2643
+ progress_img_path = f'{timeline_dir}/timeline_progress_{progress}.png'
2644
+ cv2.imwrite(progress_img_path, canvas)
2645
+ progress_frames[progress] = progress_img_path
2646
+
2647
+ # 添加到视频
2648
+ video_writer.write(canvas)
2649
+
2650
+ # 释放视频写入器
2651
+ video_writer.release()
2652
+
2653
+ # 使用ffmpeg处理视频,提高兼容性
2654
+ try:
2655
+ print(f"使用ffmpeg处理视频以提高兼容性...")
2656
+
2657
+ # 优先使用ffmpeg-python库
2658
+ if 'ffmpeg' in globals():
2659
+ (
2660
+ ffmpeg
2661
+ .input(raw_video_path)
2662
+ .output(video_path, vcodec='libx264', crf=23, preset='fast', acodec='aac', audio_bitrate='128k')
2663
+ .overwrite_output()
2664
+ .run(quiet=True)
2665
+ )
2666
+ else:
2667
+ # 使用subprocess作为备选方案
2668
+ command = [
2669
+ 'ffmpeg', '-i', raw_video_path,
2670
+ '-c:v', 'libx264', '-crf', '23', '-preset', 'fast',
2671
+ '-c:a', 'aac', '-b:a', '128k',
2672
+ '-y', video_path
2673
+ ]
2674
+ subprocess.run(command, check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
2675
+
2676
+ # 删除原始视频
2677
+ if os.path.exists(video_path) and os.path.getsize(video_path) > 0:
2678
+ os.remove(raw_video_path)
2679
+ print(f"视频处理完成: {video_path}")
2680
+ else:
2681
+ # 如果处理失败,保留原始视频
2682
+ shutil.copy(raw_video_path, video_path)
2683
+ print(f"视频处理失败,使用原始视频")
2684
+
2685
+ except Exception as e:
2686
+ print(f"视频处理失败: {str(e)}")
2687
+ # 如果处理失败,使用原始视频
2688
+ if os.path.exists(raw_video_path):
2689
+ shutil.copy(raw_video_path, video_path)
2690
+
2691
+ # 生成累积图像 - 包含所有足印的单张图像
2692
+ cumulative_img_path = self._generate_cumulative_footprint_image(cluster_heatmaps, min_x, min_y, width, height, key_footprints)
2693
+
2694
+ print(f"足印步行热图视频已保存: {video_path}")
2695
+ print(f"足印步行热图序列已保存: {timeline_dir}")
2696
+
2697
+ # 读取视频和图像为base64
2698
+ video_base64 = ""
2699
+ with open(video_path, 'rb') as f:
2700
+ video_base64 = base64.b64encode(f.read()).decode('utf-8')
2701
+
2702
+ final_img_base64 = ""
2703
+ if os.path.exists(cumulative_img_path):
2704
+ with open(cumulative_img_path, 'rb') as f:
2705
+ final_img_base64 = base64.b64encode(f.read()).decode('utf-8')
2706
+
2707
+ # 返回数据
2708
+ return {
2709
+ 'video_base64': video_base64,
2710
+ 'final_image_base64': final_img_base64
2711
+ }
2712
+
2713
+ def _generate_cumulative_footprint_image(self, cluster_heatmaps, min_x, min_y, width, height, key_footprints):
2714
+ """生成包含所有足印的累积热图图像"""
2715
+ import numpy as np
2716
+ import cv2
2717
+ import os
2718
+ import matplotlib.pyplot as plt
2719
+ from matplotlib.patches import Patch
2720
+
2721
+ # 创建保存目录
2722
+ cumulative_dir = f'{self.result_dir}/plots/footprint_cumulative'
2723
+ os.makedirs(cumulative_dir, exist_ok=True)
2724
+
2725
+ # 创建画布
2726
+ canvas = np.zeros((height, width, 3), dtype=np.uint8)
2727
+ canvas.fill(255) # 白色背景
2728
+
2729
+ # 绘制所有关键足印
2730
+ for footprint in key_footprints:
2731
+ cluster_id = footprint['cluster_id']
2732
+ if cluster_id in cluster_heatmaps:
2733
+ heatmap_data = cluster_heatmaps[cluster_id]
2734
+ heatmap = heatmap_data['heatmap']
2735
+
2736
+ # 计算在画布上的位置
2737
+ x = int(footprint['position']['x'] - min_x)
2738
+ y = int(footprint['position']['y'] - min_y)
2739
+ w = int(footprint['position']['width'])
2740
+ h = int(footprint['position']['height'])
2741
+
2742
+ # 计算缩放比例
2743
+ scale_x = w / heatmap.shape[1]
2744
+ scale_y = h / heatmap.shape[0]
2745
+ scale = min(scale_x, scale_y)
2746
+
2747
+ # 等比例缩放热图
2748
+ new_w = int(heatmap.shape[1] * scale)
2749
+ new_h = int(heatmap.shape[0] * scale)
2750
+
2751
+ if new_w > 0 and new_h > 0: # 确保尺寸有效
2752
+ resized_heatmap = cv2.resize(heatmap, (new_w, new_h))
2753
+
2754
+ # 计算居中偏移
2755
+ offset_x = (w - new_w) // 2
2756
+ offset_y = (h - new_h) // 2
2757
+
2758
+ # 绘制足印类型和简化ID
2759
+ paw_type = footprint['paw_type']
2760
+ simple_id = footprint['simple_id']
2761
+ text = f"{paw_type} {simple_id}"
2762
+ cv2.putText(canvas, text, (x + w + 5, y + h//2),
2763
+ cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 0, 0), 1)
2764
+
2765
+ # 添加热图到画布上(透明度混合)
2766
+ for i in range(new_h):
2767
+ for j in range(new_w):
2768
+ if 0 <= x + j + offset_x < width and 0 <= y + i + offset_y < height:
2769
+ alpha = resized_heatmap[i, j, 3] / 255.0
2770
+ if alpha > 0:
2771
+ canvas[y + i + offset_y, x + j + offset_x] = \
2772
+ (1 - alpha) * canvas[y + i + offset_y, x + j + offset_x] + \
2773
+ alpha * resized_heatmap[i, j, :3]
2774
+
2775
+ # 添加标题
2776
+ cv2.putText(canvas, "All Gait Cumulative", (width//2 - 150, 30),
2777
+ cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 0, 0), 2)
2778
+
2779
+ # 保存高清图像
2780
+ cumulative_path = f'{cumulative_dir}/all_footprints_cumulative.png'
2781
+ cv2.imwrite(cumulative_path, canvas)
2782
+
2783
+ # 创建足印图例
2784
+ self._create_footprint_legend(cumulative_dir)
2785
+
2786
+ print(f"足印累积热图已保存: {cumulative_path}")
2787
+ return cumulative_path
2788
+
2789
+ def _create_footprint_legend(self, output_dir):
2790
+ """创建足印类型图例"""
2791
+ import matplotlib.pyplot as plt
2792
+ import numpy as np
2793
+ import os
2794
+ from matplotlib.patches import Patch
2795
+
2796
+ # 足印类型和颜色(使用英文标签)
2797
+ paw_types = {
2798
+ 'LF': {'color': 'red', 'label': 'Left Front'},
2799
+ 'RF': {'color': 'green', 'label': 'Right Front'},
2800
+ 'LH': {'color': 'blue', 'label': 'Left Hind'},
2801
+ 'RH': {'color': 'yellow', 'label': 'Right Hind'}
2802
+ }
2803
+
2804
+ # 创建图例图像
2805
+ fig, ax = plt.subplots(figsize=(10, 4))
2806
+ ax.axis('off')
2807
+
2808
+ # 添加图例元素
2809
+ legend_elements = []
2810
+ for paw_type, info in paw_types.items():
2811
+ legend_elements.append(
2812
+ Patch(facecolor=info['color'], edgecolor='black', label=f"{paw_type} - {info['label']}")
2813
+ )
2814
+
2815
+ # 创建图例(使用英文)
2816
+ ax.legend(handles=legend_elements, loc='center', fontsize=14, frameon=True,
2817
+ title='Footprint Type Legend', title_fontsize=16)
2818
+
2819
+ # 设置标题(使用英文)
2820
+ plt.title('Heatmap colors indicate pressure intensity, border colors indicate footprint type', fontsize=14)
2821
+ plt.tight_layout()
2822
+
2823
+ # 保存图例
2824
+ legend_path = f"{output_dir}/footprint_legend.png"
2825
+ plt.savefig(legend_path, dpi=200, bbox_inches='tight')
2826
+ plt.close()
2827
+
2828
+ def analyze_angular_velocity(self):
2829
+ """分析并生成触地时间-角速度序列图
2830
+
2831
+ 根据足印数据和关键点数据,计算身体角速度并与触地时间关联
2832
+
2833
+ Returns:
2834
+ dict: 包含统计数据和图表路径
2835
+ """
2836
+ print("\n开始分析触地时间-角速度序列图...")
2837
+
2838
+ # 创建保存目录
2839
+ plot_dir = f'{self.result_dir}/plots'
2840
+ os.makedirs(plot_dir, exist_ok=True)
2841
+
2842
+ # 根据实际可用数据,从记录参数中获取关键点数据
2843
+ keypoints_data = self.record_params.get('bodyKeypoints', [])
2844
+
2845
+ if len(keypoints_data) == 0:
2846
+ print("无关键点数据,无法生成角速度图")
2847
+ return {}
2848
+
2849
+ # 按时间排序足印数据
2850
+ all_footprints = sorted(self.footprint_areas, key=lambda x: x.start_frame)
2851
+
2852
+ # 计算角速度(基于头部和尾部关键点)
2853
+ frame_ids = []
2854
+ angular_velocities = []
2855
+ body_angles = []
2856
+ prev_angle = None
2857
+ prev_frame_id = None
2858
+
2859
+ # 设置颜色
2860
+ colors = {
2861
+ 'LF': 'red',
2862
+ 'RF': 'green',
2863
+ 'LH': 'blue',
2864
+ 'RH': 'yellow'
2865
+ }
2866
+
2867
+ for kp_data in keypoints_data:
2868
+ frame_id = kp_data['frame_id']
2869
+ kps = kp_data['keypoints']
2870
+
2871
+ # 如果有鼻子和尾根点
2872
+ if 'nose' in kps and 'tail_base' in kps:
2873
+ nose = kps['nose']
2874
+ tail_base = kps['tail_base']
2875
+
2876
+ # 计算身体角度
2877
+ dx = nose[0] - tail_base[0]
2878
+ dy = nose[1] - tail_base[1]
2879
+ angle = np.arctan2(dy, dx) * 180 / np.pi
2880
+
2881
+ # 存储帧号和角度
2882
+ body_angles.append((frame_id, angle))
2883
+
2884
+ # 计算角速度
2885
+ if prev_angle is not None and prev_frame_id is not None:
2886
+ # 处理角度环绕(比如从179度到-179度)
2887
+ angle_diff = angle - prev_angle
2888
+ if angle_diff > 180:
2889
+ angle_diff -= 360
2890
+ elif angle_diff < -180:
2891
+ angle_diff += 360
2892
+
2893
+ # 计算每秒变化的角度
2894
+ angular_vel = angle_diff * self.fps / (frame_id - prev_frame_id)
2895
+
2896
+ frame_ids.append(frame_id)
2897
+ angular_velocities.append(angular_vel)
2898
+
2899
+ prev_angle = angle
2900
+ prev_frame_id = frame_id
2901
+
2902
+ # 创建图形
2903
+ plt.figure(figsize=(14, 6))
2904
+
2905
+ # 绘制角速度
2906
+ plt.plot([id / self.fps for id in frame_ids], angular_velocities, 'b-', label='Angular Velocity', alpha=0.7)
2907
+
2908
+ # 添加足印触地时间线
2909
+ max_y = max(abs(min(angular_velocities)), abs(max(angular_velocities))) * 1.1 if angular_velocities else 10
2910
+ for footprint in all_footprints:
2911
+ start_time = footprint.start_frame / self.fps
2912
+ paw_type = footprint.paw_type
2913
+ plt.axvline(x=start_time, color=colors[paw_type], linestyle='--', alpha=0.6)
2914
+ plt.text(start_time, max_y * 0.9, paw_type, rotation=90, color=colors[paw_type], alpha=0.8)
2915
+
2916
+ # 设置图表
2917
+ plt.xlabel('Time (seconds)')
2918
+ plt.ylabel('Angular Velocity (deg/sec)')
2919
+ plt.title('Footfall Angular Velocity Timeline')
2920
+ plt.grid(True, alpha=0.3)
2921
+ plt.legend()
2922
+
2923
+ # 保存图像
2924
+ output_path = f'{plot_dir}/angular_velocity_timeline.png'
2925
+ plt.savefig(output_path, dpi=300, bbox_inches='tight')
2926
+ plt.close()
2927
+
2928
+ print(f"触地时间-角速度序列图已保存: {output_path}")
2929
+
2930
+ # 返回统计数据
2931
+ stats = {
2932
+ 'max_angular_velocity': max(angular_velocities) if angular_velocities else 0,
2933
+ 'min_angular_velocity': min(angular_velocities) if angular_velocities else 0,
2934
+ 'mean_angular_velocity': np.mean(angular_velocities) if angular_velocities else 0,
2935
+ 'image_path': output_path
2936
+ }
2937
+
2938
+ return stats
2939
+
2940
+ def analyze_velocity_timeline(self):
2941
+ """分析并生成触地时间速度序列图
2942
+
2943
+ 根据关键点数据计算线性速度并与触地时间关联
2944
+
2945
+ Returns:
2946
+ dict: 包含统计数据和图表路径
2947
+ """
2948
+ print("\n开始分析触地时间速度序列图...")
2949
+
2950
+ # 创建保存目录
2951
+ plot_dir = f'{self.result_dir}/plots'
2952
+ os.makedirs(plot_dir, exist_ok=True)
2953
+
2954
+ # 根据实际可用数据,从记录参数中获取关键点数据
2955
+ keypoints_data = self.record_params.get('bodyKeypoints', [])
2956
+
2957
+ if len(keypoints_data) == 0:
2958
+ print("无关键点数据,无法生成速度图")
2959
+ return {}
2960
+
2961
+ # 按时间排序足印数据
2962
+ all_footprints = sorted(self.footprint_areas, key=lambda x: x.start_frame)
2963
+
2964
+ # 计算线性速度(基于中心点)
2965
+ frame_ids = []
2966
+ velocities = []
2967
+ prev_pos = None
2968
+ prev_frame_id = None
2969
+
2970
+ # 比例尺:像素到厘米的转换
2971
+ scale_factor = self.record_params.get('actual_length', 20) / self.record_params.get('scale_length', 152)
2972
+
2973
+ # 设置颜色
2974
+ colors = {
2975
+ 'LF': 'red',
2976
+ 'RF': 'green',
2977
+ 'LH': 'blue',
2978
+ 'RH': 'yellow'
2979
+ }
2980
+
2981
+ # 从关键点数据计算速度
2982
+ for kp_data in keypoints_data:
2983
+ frame_id = kp_data['frame_id']
2984
+ kps = kp_data['keypoints']
2985
+
2986
+ # 使用中心点计算速度
2987
+ if 'mid' in kps:
2988
+ mid = kps['mid']
2989
+
2990
+ if prev_pos is not None and prev_frame_id is not None:
2991
+ # 计算距离变化(像素)
2992
+ dx = mid[0] - prev_pos[0]
2993
+ dy = mid[1] - prev_pos[1]
2994
+ distance = np.sqrt(dx**2 + dy**2)
2995
+
2996
+ # 转换为厘米
2997
+ distance_cm = distance * scale_factor
2998
+
2999
+ # 计算时间变化(秒)
3000
+ dt = (frame_id - prev_frame_id) / self.fps
3001
+
3002
+ # 计算速度(厘米/秒)
3003
+ if dt > 0:
3004
+ velocity = distance_cm / dt
3005
+ frame_ids.append(frame_id)
3006
+ velocities.append(velocity)
3007
+
3008
+ prev_pos = mid
3009
+ prev_frame_id = frame_id
3010
+
3011
+ # 使用Savitzky-Golay滤波器平滑速度数据
3012
+ velocities_smooth = velocities.copy() if velocities else []
3013
+ if len(velocities) > 11: # 确保有足够的数据点
3014
+ try:
3015
+ velocities_smooth = savgol_filter(velocities, 11, 3)
3016
+ except Exception as e:
3017
+ print(f"平滑处理失败: {str(e)}")
3018
+
3019
+ # 创建图形
3020
+ plt.figure(figsize=(14, 6))
3021
+
3022
+ # 绘制速度
3023
+ if len(velocities_smooth) > 0:
3024
+ plt.plot([id / self.fps for id in frame_ids], velocities_smooth, 'b-', label='Velocity', alpha=0.7)
3025
+
3026
+ # 添加足印触地时间线
3027
+ max_y = float(np.max(velocities_smooth)) * 1.1
3028
+ for footprint in all_footprints:
3029
+ start_time = footprint.start_frame / self.fps
3030
+ paw_type = footprint.paw_type
3031
+ plt.axvline(x=start_time, color=colors[paw_type], linestyle='--', alpha=0.6)
3032
+ plt.text(start_time, max_y * 0.9, paw_type, rotation=90, color=colors[paw_type], alpha=0.8)
3033
+ else:
3034
+ print("没有有效的速度数据")
3035
+
3036
+ # 设置图表
3037
+ plt.xlabel('Time (seconds)')
3038
+ plt.ylabel('Velocity (cm/sec)')
3039
+ plt.title('Footfall Velocity Timeline')
3040
+ plt.grid(True, alpha=0.3)
3041
+ plt.legend()
3042
+
3043
+ # 保存图像
3044
+ output_path = f'{plot_dir}/velocity_timeline.png'
3045
+ plt.savefig(output_path, dpi=300, bbox_inches='tight')
3046
+ plt.close()
3047
+
3048
+ print(f"触地时间速度序列图已保存: {output_path}")
3049
+
3050
+ # 返回统计数据
3051
+ stats = {}
3052
+ if len(velocities_smooth) > 0:
3053
+ stats = {
3054
+ 'max_velocity': float(np.max(velocities_smooth)),
3055
+ 'min_velocity': float(np.min(velocities_smooth)),
3056
+ 'mean_velocity': float(np.mean(velocities_smooth)),
3057
+ 'image_path': output_path
3058
+ }
3059
+
3060
+ return stats
3061
+
3062
+ def analyze_tail_lateral_movement(self):
3063
+ """分析并生成尾根点侧向移动相位图
3064
+
3065
+ 根据关键点数据分析尾根点的侧向移动并与脚步相位关联
3066
+
3067
+ Returns:
3068
+ dict: 包含统计数据和图表路径
3069
+ """
3070
+ print("\n开始分析尾根点侧向移动相位图...")
3071
+
3072
+ # 创建保存目录
3073
+ plot_dir = f'{self.result_dir}/plots'
3074
+ os.makedirs(plot_dir, exist_ok=True)
3075
+
3076
+ # 根据实际可用数据,从记录参数中获取关键点数据
3077
+ keypoints_data = self.record_params.get('bodyKeypoints', [])
3078
+
3079
+ if len(keypoints_data) == 0:
3080
+ print("无关键点数据,无法生成尾根点移动图")
3081
+ return {}
3082
+
3083
+ # 按时间排序足印数据
3084
+ all_footprints = sorted(self.footprint_areas, key=lambda x: x.start_frame)
3085
+
3086
+ # 提取尾根点数据
3087
+ frame_ids = []
3088
+ tail_lateral_pos = [] # 尾根点侧向位置(相对于运动方向)
3089
+
3090
+ # 计算运动方向
3091
+ # 获取第一个和最后一个鼻子位置,确定整体运动方向
3092
+ first_nose = None
3093
+ last_nose = None
3094
+
3095
+ for kp_data in keypoints_data:
3096
+ kps = kp_data['keypoints']
3097
+ if 'nose' in kps:
3098
+ if first_nose is None:
3099
+ first_nose = kps['nose']
3100
+ last_nose = kps['nose']
3101
+
3102
+ if first_nose is None or last_nose is None:
3103
+ print("无法确定运动方向,无法生成尾根点侧向移动图")
3104
+ return {}
3105
+
3106
+ # 计算整体运动方向
3107
+ direction_vector = [last_nose[0] - first_nose[0], last_nose[1] - first_nose[1]]
3108
+ if direction_vector[0] == 0 and direction_vector[1] == 0:
3109
+ print("无法确定运动方向,无法生成尾根点侧向移动图")
3110
+ return {}
3111
+
3112
+ # 计算运动方向单位向量
3113
+ direction_mag = np.sqrt(direction_vector[0]**2 + direction_vector[1]**2)
3114
+ direction_unit = [direction_vector[0]/direction_mag, direction_vector[1]/direction_mag]
3115
+
3116
+ # 计算垂直于运动方向的向量
3117
+ perpendicular_unit = [-direction_unit[1], direction_unit[0]]
3118
+
3119
+ # 设置颜色
3120
+ colors = {
3121
+ 'LF': 'red',
3122
+ 'RF': 'green',
3123
+ 'LH': 'blue',
3124
+ 'RH': 'yellow'
3125
+ }
3126
+
3127
+ # 计算尾根点的侧向位置
3128
+ for kp_data in keypoints_data:
3129
+ frame_id = kp_data['frame_id']
3130
+ kps = kp_data['keypoints']
3131
+
3132
+ if 'mid' in kps and 'tail_base' in kps:
3133
+ mid = kps['mid'] # 身体中心点
3134
+ tail_base = kps['tail_base'] # 尾根点
3135
+
3136
+ # 计算尾根点相对于中心点的向量
3137
+ relative_vector = [tail_base[0] - mid[0], tail_base[1] - mid[1]]
3138
+
3139
+ # 计算侧向投影(点积)
3140
+ lateral_projection = relative_vector[0] * perpendicular_unit[0] + relative_vector[1] * perpendicular_unit[1]
3141
+
3142
+ frame_ids.append(frame_id)
3143
+ tail_lateral_pos.append(lateral_projection)
3144
+
3145
+ # 使用Savitzky-Golay滤波器平滑尾根点位置数据
3146
+ tail_pos_smooth = tail_lateral_pos.copy() if tail_lateral_pos else []
3147
+ if len(tail_lateral_pos) > 11: # 确保有足够的数据点
3148
+ try:
3149
+ tail_pos_smooth = savgol_filter(tail_lateral_pos, 11, 3)
3150
+ except Exception as e:
3151
+ print(f"平滑处理失败: {str(e)}")
3152
+
3153
+ # 创建图形
3154
+ plt.figure(figsize=(14, 6))
3155
+
3156
+ # 绘制尾根点侧向位置
3157
+ if len(tail_pos_smooth) > 0:
3158
+ plt.plot([id / self.fps for id in frame_ids], tail_pos_smooth, 'b-', label='Tail Lateral Position', alpha=0.7)
3159
+
3160
+ # 添加足印触地时间线
3161
+ y_min = float(np.min(tail_pos_smooth))
3162
+ y_max = float(np.max(tail_pos_smooth))
3163
+
3164
+ for footprint in all_footprints:
3165
+ start_time = footprint.start_frame / self.fps
3166
+ paw_type = footprint.paw_type
3167
+ plt.axvline(x=start_time, color=colors[paw_type], linestyle='--', alpha=0.6)
3168
+ plt.text(start_time, y_max, paw_type, rotation=90, color=colors[paw_type], alpha=0.8)
3169
+ else:
3170
+ print("没有有效的尾根点数据")
3171
+
3172
+ # 设置图表
3173
+ plt.xlabel('Time (seconds)')
3174
+ plt.ylabel('Tail Lateral Position (pixels)')
3175
+ plt.title('Tail Lateral Movement Phase')
3176
+ plt.grid(True, alpha=0.3)
3177
+ plt.legend()
3178
+
3179
+ # 保存图像
3180
+ output_path = f'{plot_dir}/tail_lateral_movement.png'
3181
+ plt.savefig(output_path, dpi=300, bbox_inches='tight')
3182
+ plt.close()
3183
+
3184
+ print(f"尾根点侧向移动相位图已保存: {output_path}")
3185
+
3186
+ # 返回统计数���
3187
+ stats = {}
3188
+ if len(tail_pos_smooth) > 0:
3189
+ stats = {
3190
+ 'max_lateral_position': float(np.max(tail_pos_smooth)),
3191
+ 'min_lateral_position': float(np.min(tail_pos_smooth)),
3192
+ 'mean_lateral_position': float(np.mean(tail_pos_smooth)),
3193
+ 'image_path': output_path
3194
+ }
3195
+
3196
+ return stats
3197
+
3198
+ def analyze_support_swing_phase(self):
3199
+ """分析并生成支撑-摇摆相位图
3200
+
3201
+ 可视化每个爪子的支撑和摇摆相位
3202
+
3203
+ Returns:
3204
+ dict: 包含统计数据和图表路径
3205
+ """
3206
+ print("\n开始分析支撑-摇摆相位图...")
3207
+
3208
+ # 创建保存目录
3209
+ plot_dir = f'{self.result_dir}/plots'
3210
+ os.makedirs(plot_dir, exist_ok=True)
3211
+
3212
+ # 按爪子类型分组并按开始帧排序
3213
+ paw_groups = {'RF': [], 'RH': [], 'LF': [], 'LH': []}
3214
+ for area in self.footprint_areas:
3215
+ paw_groups[area.paw_type].append(area)
3216
+
3217
+ for paw_type in paw_groups:
3218
+ paw_groups[paw_type].sort(key=lambda x: x.start_frame)
3219
+
3220
+ # 设置颜色
3221
+ colors = {
3222
+ 'LF': 'red',
3223
+ 'RF': 'green',
3224
+ 'LH': 'blue',
3225
+ 'RH': 'yellow'
3226
+ }
3227
+
3228
+ # 计算整体时间范围
3229
+ all_footprints = []
3230
+ for paw_type, footprints in paw_groups.items():
3231
+ all_footprints.extend(footprints)
3232
+
3233
+ if not all_footprints:
3234
+ print("无足印数据,无法生成支撑-摇摆相位图")
3235
+ return {}
3236
+
3237
+ # 获取时间范围
3238
+ time_min = min(fp.start_frame for fp in all_footprints) / self.fps
3239
+ time_max = max(fp.end_frame for fp in all_footprints) / self.fps
3240
+
3241
+ # 添加10%的边界
3242
+ time_range = time_max - time_min
3243
+ time_min -= time_range * 0.1
3244
+ time_max += time_range * 0.1
3245
+
3246
+ # 创建图形
3247
+ plt.figure(figsize=(15, 6))
3248
+
3249
+ # 设置Y轴位置
3250
+ y_positions = {'RF': 4, 'RH': 3, 'LF': 2, 'LH': 1}
3251
+
3252
+ # 为每个爪子绘制支撑(stance)和摇摆(swing)阶段
3253
+ for paw_type, footprints in paw_groups.items():
3254
+ y_pos = y_positions[paw_type]
3255
+
3256
+ for i, fp in enumerate(footprints):
3257
+ # 支撑阶段
3258
+ stance_start = fp.start_frame / self.fps
3259
+ stance_end = fp.end_frame / self.fps
3260
+ plt.hlines(y=y_pos, xmin=stance_start, xmax=stance_end,
3261
+ linewidth=10, color=colors[paw_type], alpha=0.7, label=paw_type if i==0 else "")
3262
+
3263
+ # 添加文本标签
3264
+ plt.text(stance_start, y_pos+0.1, f"{paw_type}", fontsize=8, ha='left')
3265
+
3266
+ # 如果有下一个足印,绘制摇摆阶段
3267
+ if i < len(footprints) - 1:
3268
+ next_fp = footprints[i+1]
3269
+ swing_start = stance_end
3270
+ swing_end = next_fp.start_frame / self.fps
3271
+
3272
+ # 摇摆阶段(用虚线)
3273
+ plt.hlines(y=y_pos, xmin=swing_start, xmax=swing_end,
3274
+ linewidth=5, color=colors[paw_type], alpha=0.3, linestyle='--')
3275
+
3276
+ # 设置图表属性
3277
+ plt.yticks(list(y_positions.values()), list(y_positions.keys()))
3278
+ plt.xlabel('Time (seconds)')
3279
+ plt.title('Support-Swing Phase Diagram')
3280
+ plt.xlim(time_min, time_max)
3281
+ plt.grid(True, alpha=0.3)
3282
+
3283
+ # 添加支撑和摇摆的图例
3284
+ from matplotlib.lines import Line2D
3285
+ legend_elements = [
3286
+ Line2D([0], [0], color='black', linewidth=10, alpha=0.7, label='Support Phase'),
3287
+ Line2D([0], [0], color='black', linewidth=5, alpha=0.3, linestyle='--', label='Swing Phase')
3288
+ ]
3289
+ plt.legend(handles=legend_elements, loc='upper right')
3290
+
3291
+ # 保存图像
3292
+ output_path = f'{plot_dir}/support_swing_phase.png'
3293
+ plt.savefig(output_path, dpi=300, bbox_inches='tight')
3294
+ plt.close()
3295
+
3296
+ print(f"支撑-摇摆相位图已保存: {output_path}")
3297
+
3298
+ # 返回相位统计数据
3299
+ phase_stats = {}
3300
+
3301
+ for paw_type, footprints in paw_groups.items():
3302
+ # 计算每个爪子的支撑和摇摆统计数据
3303
+ stance_times = []
3304
+ swing_times = []
3305
+ duty_factors = [] # 占空比 = 支撑时间 / 周期时间
3306
+
3307
+ for i, fp in enumerate(footprints):
3308
+ stance_time = (fp.end_frame - fp.start_frame) / self.fps
3309
+ stance_times.append(stance_time)
3310
+
3311
+ # 如果有下一个足印,计算摇摆时间和占空比
3312
+ if i < len(footprints) - 1:
3313
+ next_fp = footprints[i+1]
3314
+ swing_time = (next_fp.start_frame - fp.end_frame) / self.fps
3315
+ swing_times.append(swing_time)
3316
+
3317
+ cycle_time = stance_time + swing_time
3318
+ duty_factor = stance_time / cycle_time if cycle_time > 0 else 0
3319
+ duty_factors.append(duty_factor)
3320
+
3321
+ # 存储统计数据
3322
+ phase_stats[paw_type] = {
3323
+ 'avg_stance_time': np.mean(stance_times) if stance_times else 0,
3324
+ 'avg_swing_time': np.mean(swing_times) if swing_times else 0,
3325
+ 'avg_duty_factor': np.mean(duty_factors) if duty_factors else 0,
3326
+ 'max_duty_factor': max(duty_factors) if duty_factors else 0,
3327
+ 'min_duty_factor': min(duty_factors) if duty_factors else 0
3328
+ }
3329
+
3330
+ return {
3331
+ 'phase_stats': phase_stats,
3332
+ 'image_path': output_path
3333
+ }
3334
+
3335
+ def analyze_limb_duty_cycle(self):
3336
+ """分析并生成肢体占空比图 (Duty Cycle)
3337
+
3338
+ 计算并可视化各肢体的占空比(支撑时间/周期时间)
3339
+
3340
+ Returns:
3341
+ dict: 包含统计数据和图表路径
3342
+ """
3343
+ print("\n开始分析肢体占空比图...")
3344
+
3345
+ # 创建保存目录
3346
+ plot_dir = f'{self.result_dir}/plots'
3347
+ os.makedirs(plot_dir, exist_ok=True)
3348
+
3349
+ # 按爪子类型分组并按开始帧排序
3350
+ paw_groups = {'RF': [], 'RH': [], 'LF': [], 'LH': []}
3351
+ for area in self.footprint_areas:
3352
+ paw_groups[area.paw_type].append(area)
3353
+
3354
+ for paw_type in paw_groups:
3355
+ paw_groups[paw_type].sort(key=lambda x: x.start_frame)
3356
+
3357
+ # 计算每个爪子的周期时间和占空比
3358
+ duty_cycle_data = {}
3359
+ cycle_times = {}
3360
+
3361
+ for paw_type, footprints in paw_groups.items():
3362
+ duty_cycles = []
3363
+ stance_times = []
3364
+ cycles = []
3365
+
3366
+ for i, fp in enumerate(footprints):
3367
+ # 支撑时间(秒)
3368
+ stance_time = (fp.end_frame - fp.start_frame) / self.fps
3369
+ stance_times.append(stance_time)
3370
+
3371
+ # 如果有下一个足印,计算一个完整周期
3372
+ if i < len(footprints) - 1:
3373
+ next_fp = footprints[i+1]
3374
+ cycle_time = (next_fp.start_frame - fp.start_frame) / self.fps
3375
+ duty_cycle = stance_time / cycle_time if cycle_time > 0 else 0
3376
+
3377
+ duty_cycles.append(duty_cycle)
3378
+ cycles.append(cycle_time)
3379
+
3380
+ duty_cycle_data[paw_type] = duty_cycles
3381
+ cycle_times[paw_type] = cycles
3382
+
3383
+ # 创建图形
3384
+ fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(14, 6))
3385
+
3386
+ # 1. 占空比箱线图
3387
+ paw_order = ['RF', 'LF', 'RH', 'LH']
3388
+ box_data = [duty_cycle_data[paw] for paw in paw_order]
3389
+ box_colors = [self.colors[paw] for paw in paw_order]
3390
+
3391
+ # 绘制箱线图
3392
+ boxplots = ax1.boxplot(box_data, patch_artist=True, labels=paw_order)
3393
+
3394
+ # 设置箱线图颜色
3395
+ for patch, color in zip(boxplots['boxes'], box_colors):
3396
+ patch.set_facecolor(color)
3397
+ patch.set_alpha(0.6)
3398
+
3399
+ # 添加数据点
3400
+ for i, (paw, data) in enumerate(zip(paw_order, box_data)):
3401
+ if data: # 确保有数据
3402
+ x = np.random.normal(i+1, 0.04, size=len(data))
3403
+ ax1.scatter(x, data, alpha=0.6, color=self.colors[paw], s=30)
3404
+
3405
+ ax1.set_title('Limb Duty Cycle Distribution')
3406
+ ax1.set_ylabel('Duty Cycle (Stance/Cycle)')
3407
+ ax1.set_ylim(0, 1)
3408
+ ax1.grid(True, alpha=0.3)
3409
+
3410
+ # 2. 周期时间条形图
3411
+ means = [np.mean(cycle_times[paw]) if cycle_times[paw] else 0 for paw in paw_order]
3412
+ stds = [np.std(cycle_times[paw]) if cycle_times[paw] else 0 for paw in paw_order]
3413
+
3414
+ # 绘制条形图
3415
+ bars = ax2.bar(paw_order, means, color=box_colors, alpha=0.7)
3416
+ ax2.errorbar(paw_order, means, yerr=stds, fmt='none', ecolor='black', capsize=5)
3417
+
3418
+ # 添加数值标签
3419
+ for bar, mean in zip(bars, means):
3420
+ height = bar.get_height()
3421
+ ax2.text(bar.get_x() + bar.get_width()/2., height + 0.02,
3422
+ f'{mean:.2f}s', ha='center', va='bottom')
3423
+
3424
+ ax2.set_title('Average Cycle Time')
3425
+ ax2.set_ylabel('Time (seconds)')
3426
+ ax2.grid(True, alpha=0.3)
3427
+
3428
+ plt.tight_layout()
3429
+
3430
+ # 保存图像
3431
+ output_path = f'{plot_dir}/limb_duty_cycle.png'
3432
+ plt.savefig(output_path, dpi=300, bbox_inches='tight')
3433
+ plt.close()
3434
+
3435
+ print(f"肢体占空比图已保存: {output_path}")
3436
+
3437
+ # 计算并返回统计数据
3438
+ stats = {}
3439
+
3440
+ for paw_type in paw_order:
3441
+ duty_cycles = duty_cycle_data[paw_type]
3442
+ stats[paw_type] = {
3443
+ 'mean_duty_cycle': float(np.mean(duty_cycles)) if duty_cycles else 0,
3444
+ 'std_duty_cycle': float(np.std(duty_cycles)) if duty_cycles else 0,
3445
+ 'max_duty_cycle': float(max(duty_cycles)) if duty_cycles else 0,
3446
+ 'min_duty_cycle': float(min(duty_cycles)) if duty_cycles else 0,
3447
+ 'mean_cycle_time': float(np.mean(cycle_times[paw_type])) if cycle_times[paw_type] else 0
3448
+ }
3449
+
3450
+ return {
3451
+ 'duty_cycle_stats': stats,
3452
+ 'image_path': output_path
3453
+ }
3454
+
3455
  def main():
3456
  # 创建分析器实例
3457
  analyzer = GaitAnalysisReport('data/footprint_fixed_Exp2.json',