news 2026/4/25 2:32:18

infoGCN++的理解2——数据预处理之skeleton转pkl

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
infoGCN++的理解2——数据预处理之skeleton转pkl

目录

一、前言

二、get_skes_available_name.py

解释

在整个数据预处理流程中的作用

三、get_raw_skes_data.py

1. 整体流程

2. get_raw_bodies_data 详解

2.1 输入文件格式

2.2 核心逻辑

关键:按 bodyID 聚合不同帧的数据

2.3 多人场景的运动量计算

2.4 返回值

3. get_raw_skes_data 详解

4. 主程序部分

5. 最终数据结构示例

问题1:colors[b, np.newaxis] 这是什么语法?

1. np.newaxis 是什么?

2. 原始数据 colors 的维度

3. colors[b, np.newaxis] 做了什么?

4. 为什么需要这样做?——为了后续用 vstack 拼接

初始化时:

追加后续帧时:

5. 直观比喻

6. 其他等价写法

问题2:

1. 形状成因分析:堆叠的是关节,不是帧

对比 colors 的处理

2. 这样存储的目的(或历史原因)

3. 对 motion 计算的影响(现在该怎样理解?)

4. 如何验证或继续使用?

5. 完整数据结构总结(修正后)

1. 触发条件:多人场景

2. 数据形状:(总关节数, 3)

3. 方差计算:np.var(body_data['joints'], axis=0)

物理含义

4. 求和:np.sum(...)

5. 为什么用这种混合方差?

6. 常见后续用法示例

7. 与你之前理解的差异


一、前言

https://github.com/stnoah1/infogcn2

cd ./data/ntu # Get skeleton of each performer python get_raw_skes_data.py

二、get_skes_available_name.py

在运行get_raw_skes_data.py之前先要生成'statistics/skes_available_name.txt'

注意get_skes_available_name.py也是放在data/ntu路径下,并且在这个路径下运行

import os import glob # 设置骨架文件所在目录 skes_dir = '../nturgbd_raw/nturgb+d_skeletons/' # 创建文件夹 os.makedirs("./statistics", exist_ok=True) # 设置输出文件路径 output_file = './statistics/skes_available_name.txt' # 查找所有 .skeleton 文件 skes_files = glob.glob(os.path.join(skes_dir, '*.skeleton')) # 提取文件名(不带扩展名) skes_names = [os.path.splitext(os.path.basename(f))[0] for f in skes_files] # 排序(可选) skes_names.sort() # 写入到文件 with open(output_file, 'w') as f: for name in skes_names: f.write(name + '\n') print(f"Generated {output_file} with {len(skes_names)} entries.")

这段代码的功能非常简单:扫描骨架文件目录,生成一个包含所有可用骨架文件名的列表文件。这个列表文件正是前面数据预处理代码中读取的skes_available_name.txt


解释

python

skes_files = glob.glob(os.path.join(skes_dir, '*.skeleton'))
  • os.path.join(skes_dir, '*.skeleton'):拼接出搜索模式,例如'../nturgbd_raw/nturgb+d_skeletons/*.skeleton'

  • glob.glob(...):返回匹配该模式的所有文件路径列表,每个元素是一个完整路径字符串。

  • 结果示例:['../nturgbd_raw/nturgb+d_skeletons/S001C001P001R001A001.skeleton', ...]

python

skes_names = [os.path.splitext(os.path.basename(f))[0] for f in skes_files]

这是一个列表推导式,拆解如下:

  • os.path.basename(f):从完整路径中提取文件名,如'S001C001P001R001A001.skeleton'

  • os.path.splitext(...):将文件名拆分成(主名, 扩展名),返回元组,如('S001C001P001R001A001', '.skeleton')

  • [0]取元组第一个元素,即不带扩展名的纯文件名

  • 最终skes_names是一个字符串列表,包含所有骨架文件的纯名称。

python

skes_names.sort()
  • 对文件名进行升序排序,保证后续处理顺序一致、可重复。


在整个数据预处理流程中的作用

  1. 确认数据集完整性:通过扫描实际目录,得到所有真实存在的骨架文件,避免手动维护文件列表可能出现的遗漏或错误。

  2. 供后续脚本使用:生成的skes_available_name.txt会被get_raw_skes_data中的np.loadtxt读取,依次处理每个文件。

  3. 便于过滤:有时数据集可能缺失部分文件,这个步骤能自动生成现有文件的清单,配合日志可识别丢失样本。

运行
(infogcn2_env) E:\reset\my_infogcn\data\ntu>python get_skes_available_name.py

Generated ./statistics/skes_available_name.txt with 56880 entries.

./statistics/skes_available_name.txt 每行是一个不带扩展名的文件名

S001C001P001R001A001
S001C001P001R001A002
......

三、get_raw_skes_data.py

# Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. import os.path as osp import os import numpy as np import pickle import logging def get_raw_bodies_data(skes_path, ske_name, frames_drop_skes, frames_drop_logger): """ Get raw bodies data from a skeleton sequence. Each body's data is a dict that contains the following keys: - joints: raw 3D joints positions. Shape: (num_frames x 25, 3) - colors: raw 2D color locations. Shape: (num_frames, 25, 2) - interval: a list which stores the frame indices of this body. - motion: motion amount (only for the sequence with 2 or more bodyIDs). Return: a dict for a skeleton sequence with 3 key-value pairs: - name: the skeleton filename. - data: a dict which stores raw data of each body. - num_frames: the number of valid frames. """ ske_file = osp.join(skes_path, ske_name + '.skeleton') assert osp.exists(ske_file), 'Error: Skeleton file %s not found' % ske_file # Read all data from .skeleton file into a list (in string format) print('Reading data from %s' % ske_file[-29:]) with open(ske_file, 'r') as fr: str_data = fr.readlines() num_frames = int(str_data[0].strip('\r\n')) frames_drop = [] bodies_data = dict() valid_frames = -1 # 0-based index current_line = 1 # 示例数据 num_frames:103,即这个动作视频有103帧 for f in range(num_frames): num_bodies = int(str_data[current_line].strip('\r\n')) # 在我们的示例数据中,一帧通常只有一个人, num_bodies:1 current_line += 1 # 该帧没有检测到人,丢弃,记录索引到 frames_drop if num_bodies == 0: # no data in this frame, drop it frames_drop.append(f) # 0-based index continue # 有效帧计数器,有效帧就是说这一帧有骨骼点数据 valid_frames += 1 # 为该帧所有人初始化数组(一次性读入所有人的关节和颜色) # 在我们的示例数据中,只有1个人,num_bodies:1 # 初始化之后数值都是0,后面会从sketelon数据中获取实际骨骼点数据 joints = np.zeros((num_bodies, 25, 3), dtype=np.float32) colors = np.zeros((num_bodies, 25, 2), dtype=np.float32) # 遍历这一帧的多个人,如果这一帧就1个人,那就是处理那1个人 for b in range(num_bodies): bodyID = str_data[current_line].strip('\r\n').split()[0] # bodyID: 72057594037931101 current_line += 1 num_joints = int(str_data[current_line].strip('\r\n')) # 25 joints # num_joints: 25 current_line += 1 # 遍历一个人身上的25个骨骼点 for j in range(num_joints): # temp_str就是拿到这个人的一个骨骼点数据,比如 # 0.2181153 0.1725972 3.785547 277.419 191.8218 1036.233 519.1677 -0.2059419 0.05349901 0.9692109 -0.1239193 2 temp_str = str_data[current_line].strip('\r\n').split() # 取temp_str的索引第0到第2,即0.2181153 0.1725972 3.785547 joints[b, j, :] = np.array(temp_str[:3], dtype=np.float32) # 取temp_str的索引第5到第6,即1036.233 519.1677 colors[b, j, :] = np.array(temp_str[5:7], dtype=np.float32) current_line += 1 # 此人在当前序列中是第一次出现,需要初始化。 if bodyID not in bodies_data: # Add a new body's data body_data = dict() body_data['joints'] = joints[b] # ndarray: (25, 3) # colors 是整个当前帧的颜色数组,形状 (num_bodies, 25, 2) # colors[b] 取出第 b 个身体的颜色数据,形状为 (25, 2) # colors[b, np.newaxis]:np.newaxis 在 b 的位置之后增加一个维度,相当于 colors[b:b+1, :]。 # 原始 colors[b] 是 (25, 2);加上np.newaxis 在第 0 维后,变成 (1, 25, 2)。 # 目的是按 时间轴 累积同一个 bodyID 的数据 # body_data['colors'] 最终要包含该身体在所有帧中的颜色信息,形状是 (帧数, 25, 2)。 body_data['colors'] = colors[b, np.newaxis] # ndarray: (1, 25, 2) body_data['interval'] = [valid_frames] # the index of the first frame else: # 此人在之前的帧中已经出现过,只需追加当前帧数据 body_data = bodies_data[bodyID] # Stack each body's data of each frame along the frame order # 这里有个问题 # 如果一个人不断地堆叠多帧的joints之后会变成(帧数x25,3) # 为什么不是(帧数,25,3)? body_data['joints'] = np.vstack((body_data['joints'], joints[b])) # 如果一个人不断地堆叠多帧的colors之后会变成(帧数,25,2) body_data['colors'] = np.vstack((body_data['colors'], colors[b, np.newaxis])) # 这里有个问题,取此人的body_data['interval']的最后一个数字然后加1? # 更精确的写法应该是直接 append(valid_frames),因为 valid_frames 已经维护正确了 # 直接加 1 的方式仅适用于“每帧都有效”的情况,若中间有丢弃帧则会导致索引错位。 pre_frame_idx = body_data['interval'][-1] body_data['interval'].append(pre_frame_idx + 1) # add a new frame index bodies_data[bodyID] = body_data # Update bodies_data num_frames_drop = len(frames_drop) assert num_frames_drop < num_frames, \ 'Error: All frames data (%d) of %s is missing or lost' % (num_frames, ske_name) if num_frames_drop > 0: frames_drop_skes[ske_name] = np.array(frames_drop, dtype=int) frames_drop_logger.info('{}: {} frames missed: {}\n'.format(ske_name, num_frames_drop, frames_drop)) # Calculate motion (only for the sequence with 2 or more bodyIDs) if len(bodies_data) > 1: for body_data in bodies_data.values(): body_data['motion'] = np.sum(np.var(body_data['joints'], axis=0)) return {'name': ske_name, 'data': bodies_data, 'num_frames': num_frames - num_frames_drop} def get_raw_skes_data(): skes_name = np.loadtxt(skes_name_file, dtype=str) num_files = skes_name.size print('Found %d available skeleton files.' % num_files) raw_skes_data = [] frames_cnt = np.zeros(num_files, dtype=int) for (idx, ske_name) in enumerate(skes_name): bodies_data = get_raw_bodies_data(skes_path, ske_name, frames_drop_skes, frames_drop_logger) # bodies_data 是个字典,包含一个skeleton文件的name和data,一个skeleton文件是一个动作视频 # data是把这个动作视频里面的每个人单独存储一组数据 # 比如说72057594037931101这个id的人存一组数据,包括joints、colors、interval # # 但因为一个人的动作也可能会存在某帧的骨架点漏检测,所以用interval记录哪些帧是包含这个人的骨架的 # 那如果这个动作视频有多个人,就可能有多个不同id的key,这个示例数据只有一个人 # bodies_data = { # 'name': 'S001C001P001R001A001' # 'data': {'72057594037931101': {'joints': (2575, 3), 'colors': (103, 25, 2), 'interval': [0, 1, 2, 3, 4, 5, 6, 7, 8, ...102]}, } # } raw_skes_data.append(bodies_data) frames_cnt[idx] = bodies_data['num_frames'] if (idx + 1) % 1000 == 0: print('Processed: %.2f%% (%d / %d)' % \ (100.0 * (idx + 1) / num_files, idx + 1, num_files)) with open(save_data_pkl, 'wb') as fw: pickle.dump(raw_skes_data, fw, pickle.HIGHEST_PROTOCOL) np.savetxt(osp.join(save_path, 'raw_data', 'frames_cnt.txt'), frames_cnt, fmt='%d') print('Saved raw bodies data into %s' % save_data_pkl) print('Total frames: %d' % np.sum(frames_cnt)) with open(frames_drop_pkl, 'wb') as fw: pickle.dump(frames_drop_skes, fw, pickle.HIGHEST_PROTOCOL) if __name__ == '__main__': # save_path = './' # 改成你自己的绝对路径可避免一些相对路径导致的问题 save_path = r'E:\reset\my_infogcn\data\ntu' skes_path = '../nturgbd_raw/nturgb+d_skeletons/' stat_path = osp.join(save_path, 'statistics') raw_data_path = osp.join(save_path, 'raw_data') if not osp.exists(raw_data_path): os.makedirs(raw_data_path, exist_ok=True) # 指定可用骨架文件名列表的完整路径 skes_name_file = osp.join(stat_path, 'skes_available_name.txt') # 指定最终保存所有原始骨架数据的 pickle 文件路径 save_data_pkl = osp.join(save_path, 'raw_data', 'raw_skes_data.pkl') # 指定保存丢弃帧信息的 pickle 文件路径 # 该文件存储一个字典,键是骨架文件名,值是一个 numpy 数组,记录了该文件中哪些帧被丢弃(num_bodies == 0 的帧索引) frames_drop_pkl = osp.join(save_path, 'raw_data', 'frames_drop_skes.pkl') # 创建一个名为 'frames_drop' 的 Logger 对象,专门用来记录丢弃帧的日志。 # logging.getLogger('frames_drop') 返回一个命名 logger,如果之前同名的已存在则复用,否则新建。 frames_drop_logger = logging.getLogger('frames_drop') # 设置该 logger 的日志级别为 INFO,意味着它会处理 INFO、WARNING、ERROR、CRITICAL 级别的消息。DEBUG 级别的消息将被忽略。 frames_drop_logger.setLevel(logging.INFO) # 给 logger 添加一个文件处理器,将日志信息写入 ./raw_data/frames_drop.log 文件中。 # FileHandler 以追加模式写入(默认),确保多批次运行时日志不会丢失。 # 这样无论哪个骨架文件有丢帧,都会记录在这个统一的 log 文件里,方便后续审查。 frames_drop_logger.addHandler(logging.FileHandler(osp.join(save_path, 'raw_data', 'frames_drop.log'))) # 初始化一个空字典 frames_drop_skes,用来累积收集所有存在丢帧的骨架文件信息。 # 键为文件名,值为 numpy 数组(丢帧索引列表)。它会在 get_raw_bodies_data 内部被填充,最后保存到 frames_drop_pkl。 frames_drop_skes = dict() # 将所有序列解析后的多身体数据列表序列化到raw_skes_data.pkl这个文件。 get_raw_skes_data() with open(frames_drop_pkl, 'wb') as fw: pickle.dump(frames_drop_skes, fw, pickle.HIGHEST_PROTOCOL)

这段代码是NTU RGB+D 数据预处理的一部分,功能是:

从原始的.skeleton文件中读取骨架数据,按身体 ID分别收集所有有效帧的 3D 关节坐标和 2D 颜色坐标,最终保存成 pickle 文件,供后续训练使用。

同时还会统计每个序列丢失的帧数并记录日志。


1. 整体流程

  1. skes_available_name.txt读取所有可用的骨架文件名。

  2. 对每个文件调用get_raw_bodies_data解析:

    • 跳过帧内没有检测到人的帧(num_bodies = 0)。

    • 把同一bodyID在不同帧的数据纵向堆叠,形成该身体的完整时间序列。

    • 如果场景中有多人(len(bodies_data) > 1),额外计算每个身体的运动量(关节坐标的方差总和)。

  3. 把所有序列的数据保存为raw_skes_data.pkl

  4. 保存每个序列的有效帧数、丢弃帧信息。


2.get_raw_bodies_data详解

python

def get_raw_bodies_data(skes_path, ske_name, frames_drop_skes, frames_drop_logger):

2.1 输入文件格式

.skeleton文件的结构(与之前解析的格式一致):

  • 第一行:总帧数num_frames

  • 每一帧:

    • 一行:该帧中的身体数量num_bodies

    • 对每个身体:

      • 一行:bodyID+ 其他元信息(这里只取第一个字段作为 ID)

      • 一行:关节数num_joints(固定 25)

      • 25 行:每行 12 个数值,这里用到:

        • 前 3 个:x, y, z(3D 关节坐标)

        • 第 6、7 个:color_x, color_y(2D 颜色坐标)

2.2 核心逻辑

python

for f in range(num_frames): num_bodies = int(str_data[current_line].strip('\r\n'))
  • 遍历每一帧,先读身体数量。

  • 如果num_bodies == 0该帧没有检测到人,丢弃,记录索引到frames_drop

  • 否则valid_frames += 1(有效帧计数器,从 0 开始)。

python

joints = np.zeros((num_bodies, 25, 3), dtype=np.float32) colors = np.zeros((num_bodies, 25, 2), dtype=np.float32)
  • 为该帧所有身体预分配数组(一次性读入所有身体的关节和颜色)。

python

for b in range(num_bodies): bodyID = str_data[current_line].strip('\r\n').split()[0] # 如 "0", "1" current_line += 1 num_joints = int(str_data[current_line].strip('\r\n')) current_line += 1 for j in range(num_joints): temp_str = str_data[current_line].strip('\r\n').split() joints[b, j, :] = np.array(temp_str[:3], dtype=np.float32) colors[b, j, :] = np.array(temp_str[5:7], dtype=np.float32) current_line += 1
  • 循环读取每个身体的 25 个关节,填充到jointscolors

关键:按 bodyID 聚合不同帧的数据

python

if bodyID not in bodies_data: # 第一次出现这个身体 body_data = dict() body_data['joints'] = joints[b] # (25, 3) body_data['colors'] = colors[b, np.newaxis] # (1, 25, 2) body_data['interval'] = [valid_frames] # 该身体出现的第一个有效帧索引 else: # 已经见过这个身体 body_data = bodies_data[bodyID] body_data['joints'] = np.vstack((body_data['joints'], joints[b])) body_data['colors'] = np.vstack((body_data['colors'], colors[b, np.newaxis])) pre_frame_idx = body_data['interval'][-1] body_data['interval'].append(pre_frame_idx + 1)
  • 第一次:直接存入当前帧的关节数据(25, 3),颜色(1, 25, 2),并记录该身体起始帧索引。

  • 再次出现:用np.vstack沿着时间轴把当前帧数据追加进去,joints变成(已有帧数+1, 25, 3)interval也随之延长。

  • interval的意义:存的是该身体所在的有效帧的编号(0-based),用于后续时间对齐或插值。

最后把body_data放回bodies_data[bodyID]

2.3 多人场景的运动量计算

python

if len(bodies_data) > 1: for body_data in bodies_data.values(): body_data['motion'] = np.sum(np.var(body_data['joints'], axis=0))
  • np.var(body_data['joints'], axis=0):沿着时间轴计算每个关节坐标(x, y, z)的方差,得到(25, 3)

  • np.sum(...):把所有关节所有坐标的方差加总,得到一个标量,表示该身体的整体运动幅度。

  • 只有多人时才会计算,可能用于后续判断哪个是“主要人物”。

2.4 返回值

python

return {'name': ske_name, 'data': bodies_data, 'num_frames': num_frames - num_frames_drop}
  • name:骨架文件名。

  • data:字典,键为bodyID,值为包含joints,colors,interval(及可能的motion)的字典。

  • num_frames:实际有效帧数(总帧数 − 丢弃帧数)。


3.get_raw_skes_data详解

python

def get_raw_skes_data(): skes_name = np.loadtxt(skes_name_file, dtype=str) ... for (idx, ske_name) in enumerate(skes_name): bodies_data = get_raw_bodies_data(...) raw_skes_data.append(bodies_data) frames_cnt[idx] = bodies_data['num_frames']
  • 读取所有骨架文件名列表。

  • 逐个处理,将返回的字典存入列表raw_skes_data

  • 记录每个序列的有效帧数frames_cnt

最后:

python

with open(save_data_pkl, 'wb') as fw: pickle.dump(raw_skes_data, fw, pickle.HIGHEST_PROTOCOL) np.savetxt(osp.join(save_path, 'raw_data', 'frames_cnt.txt'), frames_cnt, fmt='%d') with open(frames_drop_pkl, 'wb') as fw: pickle.dump(frames_drop_skes, fw, pickle.HIGHEST_PROTOCOL)
  • 保存原始数据raw_skes_data.pkl(整个列表,每个元素是一个序列的字典)。

  • 保存各序列帧数到文本文件。

  • 保存丢弃帧信息frames_drop_skes.pkl(记录每个序列哪些帧无效)。


4. 主程序部分

python

if __name__ == '__main__': save_path = './' skes_path = '../nturgbd_raw/nturgb+d_skeletons/' ... get_raw_skes_data() with open(frames_drop_pkl, 'wb') as fw: pickle.dump(frames_drop_skes, fw, pickle.HIGHEST_PROTOCOL)
  • 设置路径和文件名。

  • 调用get_raw_skes_data()

  • 注意get_raw_skes_data内部已经保存了一次frames_drop_pkl,主程序末尾又保存了一次,属于重复操作,但无副作用。


5. 最终数据结构示例

raw_skes_data列表中的一个元素(对应一个.skeleton文件):

python

{ 'name': 'S001C001P001R001A001', 'num_frames': 100, # 有效帧数 'data': { '0': { # 身体 0 'joints': ndarray (100, 25, 3), # 时间 × 关节 × 坐标 'colors': ndarray (100, 25, 2), 'interval': [0, 1, 2, ..., 99], 'motion': 12.345 # 如果多人则存在 }, '1': { # 身体 1(如果有) ... } } }

joints是沿时间轴纵向堆叠的,形状(N_frames, 25, 3),与之前动画代码中的单帧形状(25, 3)完全对应。


这段代码的巧妙之处在于:它不假定每帧只有一个人,也不假定同一个人在所有帧中都出现,而是通过bodyID动态管理多个身体的时间序列,为后续多人交互分析提供了灵活的数据结构。

问题1:colors[b, np.newaxis] 这是什么语法?

colors[b, np.newaxis]是 NumPy 中的数组索引操作,利用np.newaxis增加一个新维度。下面拆解它的含义、效果以及为什么要用它。


1.np.newaxis是什么?

  • np.newaxis是 NumPy 提供的一个常量,等于None

  • 它在数组索引(方括号)中出现时,作用就是在指定位置插入一个长度为 1 的新维度

所以:

python

colors[b, np.newaxis]

完全等价于:

python

colors[b, None]

2. 原始数据colors的维度

在这段代码中:

colors = np.zeros((num_bodies, 25, 2), dtype=np.float32)

colors是一个 3 维数组:

  • 第 0 维:身体索引(num_bodies个身体)

  • 第 1 维:关节索引(25 个关节)

  • 第 2 维:颜色通道(2 个值:color_x, color_y)

当你执行colors[b]时:

  • 取出了第b个身体的数据。

  • 结果的形状由(num_bodies, 25, 2)变为(25, 2)—— 丢失了“身体”这一维,只剩下关节 × 颜色。


3.colors[b, np.newaxis]做了什么?

np.newaxis放在第1个索引位置(即b的后面),相当于告诉 NumPy:“在b这个轴的位置之后,插入一个大小为 1 的新轴”。

原先的索引是[b],现在变成了[b, np.newaxis]

维度变化过程:

  • 原数组形状:(num_bodies, 25, 2)

  • 取出第b个身体:索引b后,第 0 维被压缩掉。

  • np.newaxis要求在b之后立即插入新维度,所以结果形状变为:

    (1, 25, 2)

    即保留了“身体”的位置作为大小为 1 的维度。

比较:

python

colors[b] # shape: (25, 2) colors[b, np.newaxis] # shape: (1, 25, 2)

4. 为什么需要这样做?——为了后续用vstack拼接

这个脚本的目的是按时间轴累积同一个bodyID的数据。
body_data['colors']最终要包含该身体在所有帧中的颜色信息,形状是(帧数, 25, 2)

初始化时:

body_data['colors'] = colors[b, np.newaxis] # 形状 (1, 25, 2)

代表这是第 1 帧,时间维度长度为 1。

追加后续帧时:

python

body_data['colors'] = np.vstack((body_data['colors'], colors[b, np.newaxis]))
  • body_data['colors']形状:(已有帧数, 25, 2)

  • 新帧数据colors[b, np.newaxis]形状:(1, 25, 2)

  • np.vstack要求输入的数组除了第 0 维(堆叠方向)之外的所有维度完全相同

  • 如果直接传colors[b](形状(25, 2)),无法与(已有帧数, 25, 2)堆叠,会报维度不匹配错误。

  • 所以必须用np.newaxis(25, 2)提升为(1, 25, 2),使它能作为“一帧”与历史数据拼合。


5. 直观比喻

可以把数组想象成表格:

  • colors[b]是一张 25 行 × 2 列的二维表(某一帧的颜色数据)。

  • body_data['colors']是一叠这样的表(多帧堆叠),每一张表是其中一层。

  • 新加一帧时,不能直接把一张表放进一堆表里,需要把它包装成“单张表的叠”(1 张的堆),所以用np.newaxis在最外面加一个“层”维度。


6. 其他等价写法

以下写法与colors[b, np.newaxis]完全等价,也可加深理解:

colors[b:b+1] # 切片,保留第0维,形状 (1, 25, 2) colors[b][np.newaxis] # 先取出 (25,2),再在开头加一维 → (1, 25, 2) np.expand_dims(colors[b], axis=0) # 显式扩展维度

代码作者选择colors[b, np.newaxis]是一种简洁且常见的 NumPy 写法。

总结:colors[b, np.newaxis]就是取出第 b 个身体的颜色数据,同时保留一个大小为 1 的时间维度,以便后续沿时间轴堆叠。

问题2:

# Calculate motion (only for the sequence with 2 or more bodyIDs)
if len(bodies_data) > 1:
for body_data in bodies_data.values():
body_data['motion'] = np.sum(np.var(body_data['joints'], axis=0))
什么意思?

我调试了一下body_data['joints']的形状是(帧数x25, 3),并不是(帧数, 25, 3)

这三行代码的作用是:当一帧中有多个身体时,为每个身体计算一个“运动量”指标

1. 形状成因分析:堆叠的是关节,不是帧

关键代码回顾:

python

# 初始化 body_data['joints'] = joints[b] # joints[b] 形状 (25, 3) # 追加 body_data['joints'] = np.vstack((body_data['joints'], joints[b]))

np.vstack沿着第 0 维(行)拼接。

  • 第一次:(25, 3)→ 身体数据

  • 第二次:将(25, 3)堆叠上去 → 形状(50, 3),对应两帧的所有关节依次排列。

  • 第 N 帧后:形状(N × 25, 3)

所以最终存储的是将所有帧的 25 个关节直接首尾相接的一张二维大表,而不是保留帧维度的三维数组(N, 25, 3)

对比 colors 的处理

python

body_data['colors'] = colors[b, np.newaxis] # (1, 25, 2) body_data['colors'] = np.vstack((..., colors[b, np.newaxis])) # 沿时间轴堆叠成 (N, 25, 2)

colors用了np.newaxis显式保留了时间维度,所以形状为(帧数, 25, 2)
joints没有加np.newaxis,因此被展平成(帧数×25, 3)


2. 这样存储的目的(或历史原因)

  • NTU 某些早期处理代码采用这种“骨架序列展开为帧-关节矩阵”格式,因为一些图卷积网络或传统方法习惯将一整段序列表示成(所有关节总数, 特征),直接输入。

  • 这种格式丢失了“一帧 25 个关节”的结构化信息,但可以通过interval的长度推断帧数,并根据需要reshape回去:

    python

    N = len(body_data['interval']) joints_3d = body_data['joints'].reshape(N, 25, 3)

3. 对motion计算的影响(现在该怎样理解?)

原始代码:

python

body_data['motion'] = np.sum(np.var(body_data['joints'], axis=0))

此时body_data['joints']是形状(N×25, 3)

  • np.var(..., axis=0)对第 0 维求方差,也就是对所有行(所有帧的所有关节)在 X、Y、Z 三个坐标上分别计算方差。

  • 结果是一个形状(3,)的数组:[var_x, var_y, var_z]

  • np.sum(...)将三个方差相加,得到一个标量。

物理意义:
它衡量的是该人物所有关节在所有时间点上的整体空间分布弥散程度。如果一个人一直在运动,其关节坐标变化范围大,这个值就大;如果基本静止,值就小。
这依然是一个有效的“运动剧烈程度”指标,只是计算方式与“先按关节分算时间方差再求和”略有不同:

方式joints形状np.var计算内容
原设想(N, 25, 3)每个关节在时间轴上的方差 → 得到 (25,3),再求和
实际代码(N×25, 3)所有关节和帧混合在一起,对三个坐标轴求总体方差,再求和

两者在数学上并不等价,但在实际中都能体现“动得多不多”的趋势。原代码采用这种简单粗暴的形式,也许是为了快速过滤,无需 reshape。


4. 如何验证或继续使用?

如果你需要逐关节的时间方差,可以这样做:

python

joints_3d = body_data['joints'].reshape(-1, 25, 3) # (N, 25, 3) joint_var = np.var(joints_3d, axis=0) # (25, 3) motion_per_joint = np.sum(joint_var) # 总运动量

但原始代码直接用的是(N×25, 3)的方差,说明其意图就是整体散布,不必纠结于关节维度。


5. 完整数据结构总结(修正后)

对于一个多帧的身体:

  • joints: 形状(N×25, 3),二维数组,行顺序为 frame0_joint0, frame0_joint1, ..., frame1_joint0, ...

  • colors: 形状(N, 25, 2),三维数组,保留了时间维

  • interval: 列表,长度 N,记录该身体出现的有效帧索引

两者的存储方式不一致,可能是代码演进中的遗留现象。不过这不妨碍功能,如果要统一,可以在使用前reshape一下。

1. 触发条件:多人场景

  • bodies_data字典中键为不同的bodyID,所以len(bodies_data) > 1表示这个.skeleton 序列中检测到多于一个人(例如双人交互或主人物 + 旁观者)。

  • 只在多人时才计算运动量,目的是区分不同人的运动活跃程度,后续可能用于筛选目标人物或过滤静止噪声。


2. 数据形状:(总关节数, 3)

一个身体的所有帧数据被np.vstack拼接成二维数组:

帧0关节0, 帧0关节1, … , 帧0关节24, 帧1关节0, 帧1关节1, … , 帧1关节24, ...

形状为(N×25, 3),其中 N 是有效帧数。
每一行是一个关节的(x, y, z)坐标,行顺序是先按时间、再按关节索引展开的。


3. 方差计算:np.var(body_data['joints'], axis=0)

  • axis=0表示沿第 0 维(即所有行,所有帧的所有关节)计算方差。

  • 结果是一个形状(3,)的数组:[var_x, var_y, var_z]

  • 这相当于混合了该人物全身所有关节、全部时间点的坐标分布,在 X、Y、Z 三个方向上的离散程度。

物理含义
  • 如果该人物在整个序列中基本未动,所有关节坐标集中在一个小范围,方差很小。

  • 如果该人物在做大幅度动作(挥手、跳跃等),坐标跨越范围大,方差较大。


4. 求和:np.sum(...)

  • [var_x, var_y, var_z]三个方差相加,得到一个标量motion

  • 这个标量综合反映了该人物在三个轴向上的整体运动剧烈程度


5. 为什么用这种混合方差?

从数学上看,这种计算没有保留关节的独立信息(因为所有关节的时间序列被拉平了),但它依然是一个简单有效的“活跃度”指标:

  • 大幅度动作 → 坐标弥散 → 方差大 → motion 大。

  • 静止或小动作 → 坐标集中 → 方差小 → motion 小。

代码注释中写明 “motion amount (only for the sequence with 2 or more bodyIDs)”,说明设计目的就是快速量化每个人的运动量,不需要精细到每个关节的时间方差。这种实现方式简单、计算快,且对于区分主要动作人与静止旁观者已经足够有效。


6. 常见后续用法示例

  • 选择motion最大的bodyID作为主执行者(执行动作分类)。

  • 过滤掉motion小于阈值的身体(可能是误检或背景人)。


7. 与你之前理解的差异

项目此前推测实际代码
joints形状(N, 25, 3),三维带时间轴(N×25, 3),二维混合表
方差计算每个关节单独求时间方差 →(25,3)求和所有关节帧混合求总体离散度 →(3,)求和

两者都能表达运动强度,但粒度不同。实际代码选择了更粗粒度的版本,完全满足区分多人物活跃度的需求。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/4/25 2:27:21

智能搜索代理框架II-Researcher:从RAG到代理增强研究的深度部署指南

1. 项目概述&#xff1a;一个为深度研究而生的智能搜索代理框架如果你曾经尝试过让AI帮你做一次深度的网络调研&#xff0c;比如“对比2024年主流大语言模型在代码生成任务上的表现”&#xff0c;你可能会发现一个尴尬的局面&#xff1a;要么它基于过时的知识库给你一些陈旧的信…

作者头像 李华
网站建设 2026/4/25 2:24:05

每天学一个算法--外部排序(External Sorting)

&#x1f4d8; 教案 23&#xff1a;外部排序&#xff08;External Sorting 工程级&#xff09;一、问题模型&#xff08;必须精确定义&#xff09; 给定一个包含 (N) 条记录的数据集&#xff0c;单条记录大小为 字节&#xff1b;主存&#xff08;RAM&#xff09;可用容量为 (…

作者头像 李华
网站建设 2026/4/25 2:21:28

使用LLaMA-Factory进行LoRA微调实战(一步步演示)-原理源码解析

1. 问题背景与分析目标 标题对应的技术问题&#xff1a; 在大规模预训练语言模型&#xff08;如LLaMA&#xff09;中进行LoRA微调是提升模型在特定任务中性能的关键步骤。LoRA&#xff08;Low-Rank Adaptation&#xff09;作为一种高效的微调技术&#xff0c;通过引入低秩矩阵的…

作者头像 李华
网站建设 2026/4/25 2:21:21

机器学习工程师必备:Docker容器化实战指南

1. 机器学习工程师的Docker实战指南作为一位在机器学习领域摸爬滚打多年的工程师&#xff0c;我深刻理解环境配置带来的痛苦。记得有一次&#xff0c;我花了整整三天时间只为让同事的电脑跑通我的模型——Python版本不匹配、CUDA驱动冲突、系统库缺失...这些噩梦般的经历促使我…

作者头像 李华
网站建设 2026/4/25 2:20:21

【限时开放|C23内存安全实验室原始数据包】:2026年对Linux 6.12、Zephyr 4.0、FreeRTOS 2026.03的137万行C代码扫描结果(含TOP5致命模式热力图)

更多请点击&#xff1a; https://intelliparadigm.com 第一章&#xff1a;C23内存安全实验室原始数据包全景解读 C23标准在内存安全方面引入了多项关键增强&#xff0c;其中原始数据包&#xff08;Raw Packet&#xff09;分析是验证新约束机制有效性的重要手段。实验室捕获的…

作者头像 李华