Files
freemocap-source-analysis/.memory/source-analysis.md
2026-05-27 01:00:12 +08:00

27 KiB
Raw Permalink Blame History

FreeMoCap v1.8.2 源码深度解析

创建日期2026-05-27 上游版本v1.8.2commit clone 自 https://github.com/freemocap/freemocap 主仓 LOC14,210freemocap 包内 145 个 .py 文件) 仓库定位8.8k★ / 808 fork / AGPL-3.0 / Python 3.103.12


0. 一句话定性

FreeMoCap 主仓本身不是动捕算法库,而是一个 PySide6 GUI 编排器——它把 7 个 skelly_* 子包(采集 / 检测 / 同步 / 后处理 / 可视化 / Blender 导出)和 aniposelib(多视角几何核心)串成一条端到端的离线动捕流水线。论真正的算力,主仓不到 30%70% 在外部依赖。


1. 依赖拓扑:算力在哪里

pyproject.toml:68-84 暴露了真相:

外部包 版本 在流水线中的角色
skellycam 2025.09.1097 多摄像头采集PySide6 widget
skellytracker[all] 2025.10.1024 2D 关键点检测(包装 MediaPipe/YOLO/OpenPose
skelly_synchronize 2025.04.1037 多机位时间戳软同步(音频/亮度)
skellyforge 2024.12.1009 后处理 task pipeline插值/滤波/旋转)
skelly_viewer 2025.04.1028 3D 骨架可视化
ajc27_freemocap_blender_addon 2026.04.1039 Blender 桥接插件
aniposelib 0.4.3 多视角几何真核心DLT 三角化、Bundle Adjustment、外参标定
opencv-contrib-python 4.8.* ChArUco 检测、相机标定底层
PySide6 6.66.8 GUI 框架
pydantic 2.* 数据 schema

关键判断:抄 FreeMoCap 不如抄 aniposelib + skellytracker——这两个包才是真正可复用的"动捕基础设施"。FreeMoCap 主仓贡献的是 GUI 编排和参数管理(ProcessingParameterModel),算法都在外面。


2. 入口与主流程

2.1 程序入口

source/freemocap/__main__.py:24qt_gui_main() 是唯一入口CLI 仅一行:

def main():
    ...
    qt_gui_main()  # main_window/freemocap_main.py:29-59

启动后进 PySide6 主事件循环,支持 EXIT_CODE_REBOOT 重启机制(freemocap_main.py:87)。

2.2 主窗口 5 大 Tab

MainWindow(QMainWindow) @ gui/qt/main_window/freemocap_main_window.py:92-150 用一个 CentralTabWidget 串起 5 个 Tab + 右侧 ControlPanelWidget3 子 Tab+ 底部 LogViewWidget

Tab Widget 来源
0 HomeWidget 本仓 widgets/home_widget.py
1 SkellyCamWidget录制 外部库 skellycam
2 SkellyViewer3D 可视化) 外部库 skelly_viewer
3 DirectoryViewWidget 本仓
4 ActiveRecordingInfoWidget 本仓

右侧 ControlPanel① 摄像头配置 → ② 数据处理标定→2D→三角化→后处理→ ③ 导出Blender / Jupyter

2.3 端到端流水线

[1] 用户点 "Start New Session"  →  handle_start_new_session_action()
                                   freemocap_main_window.py:195
[2] 摄像头 TabSkellyCamWidget 启动多机位录制(外部库)
[3] 录制完成发信号 videos_saved_to_this_folder_signal
                                   freemocap_main_window.py:232
[4] 若勾选 "Auto Process Videos":自动点击 Process 按钮
                                   freemocap_main_window.py:177
[5] 处理流水线(后台子进程):
       标定CHARUCO 视频 → camera_calibration.toml
         ↓
       视频时间戳软同步skelly_synchronizeaudio / brightness
         ↓
       2D 检测skellytracker → MediaPipe / YOLO / OpenPose
         ↓
       3D 三角化aniposelib DLT + 加权异常点剔除)
         ↓
       后处理skellyforge插值 → Butterworth 滤波 → 找参考帧 → 坐标系旋转)
         ↓
       刚体约束enforce_rigid_bones
         ↓
       质心计算segment COM + total body COM
         ↓
       导出CSV / NPY / Blender .blend / Jupyter
[6] processing_finished_signal → 自动加载到 SkellyViewer Tab
                                   freemocap_main_window.py:274-276

3. 标定 + 三角化(招牌核心)

3.1 CHARUCO 棋盘定义

core_processes/capture_volume_calibration/charuco_stuff/charuco_board_definition.py:7-44

@dataclass
class CharucoBoardDefinition:
    name: str
    number_of_squares_width: int
    number_of_squares_height: int
    black_square_side_length: int
    aruco_marker_length_proportional: float
    aruco_marker_dict: Dict = cv2.aruco.getPredefinedDictionary(cv2.aruco.DICT_4X4_250)

预设两块板:

  • 7×5默认ArUco DICT_4X4_250marker = 0.8 × square
  • 5×3:小空间备选

3.2 标定主入口

core_processes/capture_volume_calibration/run_anipose_capture_volume_calibration.py:17-35AniposeCameraCalibrator @ anipose_camera_calibration/anipose_camera_calibrator.py:42-87

3.3 标定流程

内参(每相机)freemocap_anipose.py:2178-2202CameraGroup.calibrate_videos()。 内参初始化用 cv2.initCameraMatrix2D()freemocap_anipose.py:2091)。

外参(相机间关系)

  1. ChArUco 2D 检测 — _get_charuco_2d_data() @ freemocap_anipose.py:2150-2168(调 skellytracker
  2. 外参初始化 — get_initial_extrinsics() @ freemocap_anipose.py:499-513extract_rtvecs from aniposelib
  3. 相机图连通性 — get_calibration_graph() @ freemocap_anipose.py:387-403
  4. 相机矩阵 — compute_camera_matrices() @ freemocap_anipose.py:427-434
  5. Bundle Adjustment 迭代优化bundle_adjust_iter() @ freemocap_anipose.py:1145-1265

aniposelib 关键 importfreemocap_anipose.py:17-18

from aniposelib.boards import extract_points, extract_rtvecs, get_video_params, merge_rows, CharucoBoard
from aniposelib.utils import get_rtvec, make_M

3.4 三角化算法Numba 加速的 DLT

freemocap_anipose.py:55-67

@jit(nopython=True, parallel=False)
def triangulate_simple(points, camera_mats):
    num_cams = len(camera_mats)
    A = np.zeros((num_cams * 2, 4))
    for i in range(num_cams):
        x, y = points[i]
        mat = camera_mats[i]
        A[(i * 2): (i * 2 + 1)] = x * mat[2] - mat[0]
        A[(i * 2 + 1): (i * 2 + 2)] = y * mat[2] - mat[1]
    u, s, vh = np.linalg.svd(A, full_matrices=True)
    p3d = vh[-1]
    p3d = p3d[:3] / p3d[3]  # 齐次坐标归一化
    return p3d

线性 DLT 求解 4×N 系统 → SVD → 取最小奇异向量 → 齐次坐标归一。@jit(nopython=True) 让单点三角化贴近 C 速度。

3.5 招牌设计:加权异常点剔除三角化

freemocap_anipose.py:88-190triangulate_with_outlier_rejection()

不同于硬 RANSAC"要么用要么扔"FreeMoCap 走柔和路线:

全相机三角化  →  计算重投影误差
  ↓ 若超 target_reprojection_error
系统地枚举"丢 1 / 2 / ... / maximum_cameras_to_drop 个相机"的所有子集组合
  ↓ 每个子集都三角化一次 + 算误差
对每个有效子集赋权weight = exp(-5.0 × error / target_reprojection_error)
  ↓
所有子集的 3D 点加权平均 → 输出最终 3D + 每相机置信度权重

参数(freemocap_anipose.py:91-93

minimum_cameras_for_triangulation: int = 2
maximum_cameras_to_drop: int = 1
target_reprojection_error: float = 0.01

为什么牛:硬剔除会丢信息,软剔除(指数权重)让"还行"的相机也能贡献。这对低成本多机位4-6 个 webcam会有 1-2 个角度差)尤其重要。

3.6 重投影误差诊断

freemocap_anipose.py:1105-1143CameraGroup.reprojection_error() 返回 (n_cams, n_points, 2) 误差张量。 triangulate_3d_data.py:74-82 把误差按 (相机, 帧, 关键点) reshape 存档。 diagnostics/calibration/calculate_calibration_diagnostics.py:12-46 输出 CSV相邻 ChArUco 角点距离的 mean/median/std/与标称值偏差。

3.7 多机位同步:纯软件

没有硬件触发synchronize_videos_thread_worker.py:6,49-61 走两条路:

from skelly_synchronize.skelly_synchronize import (
    synchronize_videos_from_audio,         # 默认:音频特征对齐
    synchronize_videos_from_brightness,    # 备选:亮度跳变对齐
)

录制时只记每帧时间戳(synchronized_videos/timestamps/*.npy),同步在后处理阶段做:找音频对齐峰 → 算各路相对偏移 → 帧重索引。


4. 2D 关键点检测Tracker 策略模式)

4.1 支持的 Tracker

Tracker 模型类 状态
MediaPipe Holistic(默认) MediapipeModelInfo / MediapipeTrackingParams 主流程
YOLO YOLOModelInfo / YOLOTrackingParams experimental/alternative_trackers/run_yolo.py:7
OpenPose OpenPoseModelInfo / OpenPoseTrackingParams experimental/alternative_trackers/run_openpose.py:8
YOLOMediapipeCombo YOLOMediapipeComboTracker image_tracking_pipeline_functions.py:85-86YOLO crop → MediaPipe pose

默认 tracker 在 data_layer/recording_models/recording_info_model.py:42active_tracker="mediapipe"

4.2 检测入口

core_processes/process_motion_capture_videos/processing_pipeline_functions/image_tracking_pipeline_functions.py:26-98

def run_image_tracking_pipeline(...) -> np.ndarray:
    image_data_numCams_numFrames_numTrackedPts_XYZ = process_folder_of_videos(
        model_info=processing_parameters.tracking_model_info,
        tracking_params=processing_parameters.tracking_parameters_model,
        synchronized_video_path=synchronized_videos_folder_path,
        output_folder_path=output_data_folder_path,
        num_processes=tracking_params.num_processes,
    )

外包装薄如纸——实际检测全在 skellytracker.process_folder_of_videos() 里。这是设计的力量FreeMoCap 不关心 tracker 内部,只接收统一 ndarray。

4.3 数据格式

tests/test_image_tracking_data_shape.py:26-61 钉死了 shape

shape = (num_cameras, num_frames, num_landmarks, 3)
                                                 # ^^^^^ xy + confidence

注意:第 4 维是 3 不是 2——多出来的是 confidence。3D 三角化阶段才会丢掉 conf 通道(triangulate_3d_data.py:26-46 期望 shape[3] == 2由上游切片

4.4 并发multiprocessing

image_tracking_pipeline_functions.py:89-96tracking_params.num_processes。默认 multiprocessing.cpu_count() - 1experimental/alternative_trackers/run_yolo.py:20-22)。

每路视频独立进程,避开 Python GIL。

4.5 Landmark 映射

post_process_skeleton_data/post_process_skeleton.py:130-136

def get_landmark_names(model_info: ModelInfo) -> list:
    if hasattr(model_info, "body_landmark_names"):
        return model_info.body_landmark_names
    elif hasattr(model_info, "landmark_names"):
        return model_info.landmark_names

属性反射 + duck typing——添加新 tracker 不改主流程,只要 ModelInfo 子类提供 *_landmark_names 字段即可。

4.6 缓存路径

recording_info_model.py:116-122:每个 tracker 的 2D 结果写 output_data/raw_data/{tracker}_data_2d.npy前缀命名让多 tracker 结果共存对比。


5. 3D 后处理skellyforge 任务管道)

5.1 Butterworth 滤波

参数(data_layer/recording_models/post_processing_parameter_models.py:25-28

class ButterworthFilterParametersModel(BaseModel):
    sampling_rate: float = 30
    cutoff_frequency: float = 7
    order: int = 4

默认 30 fps、7 Hz 截止、4 阶——典型人体运动学滤波(人手脚最快动作 ~10 Hz 上限)。

5.2 任务管道

post_process_skeleton_data/post_process_skeleton.py:69-104skellyforge.TaskWorkerThread,任务顺序固定(post_process_skeleton.py:83

[
    TASK_INTERPOLATION,        # 缺帧插值max_gap_to_fill=10
    TASK_FILTERING,            # Butterworth
    TASK_FINDING_GOOD_FRAME,   # 找标定参考帧
    TASK_SKELETON_ROTATION,    # 坐标系旋转
]

每个任务可独立开关boolean 参数挂在 PostProcessingParametersModel)。

5.3 刚体约束(自实现,不依赖 skellyforge

post_process_skeleton_data/enforce_rigid_bones.py

  • calculate_bone_lengths_and_statistics() @ line 10-41 — 每帧算骨长,求 median/stdev
  • enforce_rigid_bones() @ line 44-86 — 中位数骨长标准化 + 子关节级联传播
direction = (distal - proximal) / |distal - proximal|
adjustment = (desired_length - current_length) * direction
rigid_marker_data[distal_marker][frame] += adjustment
adjust_children(distal_marker, frame, adjustment, ...)  # 递归

效果:把当前帧的骨长拉回到全序列中位数,再把这个偏移传给下游所有子关节(保持相对位姿)。处理了 2D 检测误差导致的"骨头忽长忽短"。

5.4 坐标系对齐

90° 绕 X 轴旋转utilities/geometry/rotate_by_90_degrees_around_x_axis.py:4-14

swapped[:, :, 0] = raw[:, :, 0]    # X 不变
swapped[:, :, 1] = raw[:, :, 2]    # Y ← Z
swapped[:, :, 2] = -raw[:, :, 1]   # Z ← -Y

从"摄像头坐标系Z 朝前)"换到"人体运动学坐标系Y 朝上)"。可选投影到 Z 平面:project_3d_data_to_z_plane() @ process_single_camera_skeleton_data.py:16-44

5.5 质心计算(替代关节角度)

post_process_skeleton_data/calculate_center_of_mass.py:12-77,116-141

def calculate_center_of_mass_from_skeleton(skeleton: Skeleton):
    return segment_com_data, total_body_com
    #      shape (frames, segments, 3)   shape (frames, 3)

注意v1.8.2 没有原生关节角度Euler / 四元数)输出,只有关键点位置 + 质心。要做关节角度分析得拿 NPY 自己算。


6. 数据导出

6.1 格式矩阵

格式 路径 用途
NPY output_data/mediapipe_skeleton_3d.npy 主产物shape (frames, markers, 3)
CSVby_trajectory {recording}_by_trajectory.csv 每帧一行,列=每个 marker 的 x/y/z
CSVby_frame tidy {recording}_by_frame.csv tidy 格式frame/timestamp/model/keypoint/x/y/z/reprojection_error
JSON {recording}_by_frame.json 完整原始数据
.blend {recording}.blend Blender 场景(带骨架 armature
.ipynb auto_generated_notebook.ipynb Jupyter 数据探索模板

不支持 FBX / glTF——要用得手动从 .blend 二次导出。

6.2 Blender 桥接(最有意思)

策略:不用 bpy 内嵌进程,而是外挂 Blender 可执行作为子进程

core_processes/export_data/blender_stuff/export_to_blender/methods/ajc_addon/run_ajc_addon_main.py:73-99

command_list = [
    str(blender_exe_path),
    "--background",          # 无 GUI
    "--python",
    simple_run_script,       # methods/ajc_addon/run_simple.py
    "--",
    str(recording_folder_path),
    str(blender_file_path),
]
blender_process = run_subprocess(command_list=command_list)
while True:
    output = blender_process.stdout.readline()
    if blender_process.poll() is not None:
        break
    if output:
        logging.debug(output.strip().decode())
blender_process.terminate()

子进程脚本(run_simple.py:4-8)调用外部插件:

from ajc27_freemocap_blender_addon.main import ajc27_run_as_main_function
ajc27_run_as_main_function(recording_path=..., blend_file_path=...)

插件首次运行时由 bpy_install_addon.py:33-51 现场安装:打 ZIP → bpy.ops.preferences.addon_install(overwrite=True, ...)bpy.ops.wm.save_userpref()

Blender 路径自动检测get_best_guess_of_blender_path.py:27-86

  • WindowsProgram Files/Blender Foundation
  • macOS/Applications/Blender.app/Contents/MacOS/Blender
  • Linux/usr/bin/blender

为什么这设计聪明bpy 作为 Python 库装起来很折磨(要 Blender 版本对齐),子进程模式把 Blender 当成黑盒服务FreeMoCap 主进程的 Python 环境干净。

6.3 列名约定tracker-aware

post_process_skeleton_data/split_and_save.py:67-121

# 如果 model_info 有具名 landmarkbody_nose_x, body_left_shoulder_y
column_names.append(f"{category}_{name}_{x|y|z}")
# 否则降级到数字索引body_0000_x, body_0001_x
column_names.append(f"{category}_{str(i).zfill(4)}_{x|y|z}")

split 也按 category 切(split_and_save.py:32-64

for category in ["body", "left_hand", "right_hand", "face"]:
    n = getattr(model_info, f"num_tracked_points_{category}")
    split_data[category] = skeleton_3d_data[:, prev_index:prev_index+n, :]

6.4 时间戳问题

CSV 不带时间戳——行号即帧索引(split_and_save.py:152-154)。帧率元数据存在 PostProcessingParametersModel.framerate(默认 30不内嵌到导出文件。后续做精确时序分析得拿 synchronized_videos/timestamps/*.npy 配合用。


7. Skeleton Schema统一骨架定义

7.1 核心 Pydantic 模型

data_layer/skeleton_models/skeleton.py:13-133

class Skeleton(BaseModel):
    model_config = ConfigDict(arbitrary_types_allowed=True)
    markers: MarkerInfo
    num_tracked_points: int
    segments: Optional[Dict[str, Segment]] = None
    marker_data: Dict[str, np.ndarray] = {}             # 每点 shape (frames, 3)
    virtual_marker_data: Dict[str, np.ndarray] = {}
    joint_hierarchy: Optional[Dict[str, List[str]]] = None
    center_of_mass_definitions: Optional[Dict[str, SegmentAnthropometry]] = None
    num_frames: Optional[int] = None

7.2 段定义

data_layer/skeleton_models/segments.py:8-17

class Segment(BaseModel):
    proximal: str
    distal: str

class SegmentAnthropometry(BaseModel):
    segment_com_length: float       # 质心位置占段长比例
    segment_com_percentage: float   # 段占全身质量比例

7.3 虚拟关键点

data_layer/skeleton_models/marker_info.py:38-68 — 支持加权平均生成虚拟点:

# 典型mid_shoulder = 0.5 * left_shoulder + 0.5 * right_shoulder
class MarkerInfo(BaseModel):
    original_marker_names: List[str]
    virtual_marker_definition: Optional[VirtualMarkerInfo] = None
    all_markers_list: List[str] = Field(default_factory=list)

这让上层算法(质心、骨长)能用稳定的解剖学参考点,不受单点遮挡影响。


8. GUI / 数据层架构

8.1 RecordingInfoModel路径属性生成器

data_layer/recording_models/recording_info_model.py:41-217

class RecordingInfoModel:
    @property
    def synchronized_videos_folder_path(self) -> str: ...
    @property
    def output_data_folder_path(self) -> str: ...
    @property
    def data_2d_npy_file_path(self) -> str: ...      # {tracker}_2dData...
    @property
    def data_3d_npy_file_path(self) -> str: ...
    @property
    def reprojection_error_data_npy_file_path(self) -> str: ...
    @property
    def total_body_center_of_mass_npy_file_path(self) -> str: ...
    @property
    def calibration_toml_path(self) -> str: ...
    @property
    def status_check(self) -> Dict[...]:             # 递归查文件存在性
        ...

不存路径字符串、只用属性生成器——recording.path 变了之后所有派生路径自动跟。status_check 是状态机的反面:不维护"录制完成 / 处理完成"标志位,而是每次按需扫文件存在性。

8.2 录制目录结构

freemocap_data/recording_sessions/
└── session_<timestamp>/
    └── recording_<timestamp>/
        ├── synchronized_videos/
        │   ├── camera_0.mp4
        │   ├── camera_1.mp4
        │   └── timestamps/
        │       ├── camera_0_timestamps.npy
        │       └── camera_1_timestamps.npy
        ├── annotated_videos/                        # 可选2D 关键点叠加视频
        ├── output_data/
        │   ├── mediapipe_2dData_numCams_numFrames_numTrackedPoints_pixelXY.npy
        │   ├── raw_data/
        │   │   ├── mediapipe_3dData_numFrames_numTrackedPoints_spatialXYZ.npy
        │   │   └── mediapipe_3dData_numFrames_numTrackedPoints_reprojectionError.npy
        │   ├── center_of_mass/
        │   │   └── mediapipe_total_body_center_of_mass_xyz.npy
        │   ├── mediapipe_skeleton_3d.npy
        │   └── recording_parameters.json
        ├── <recording>_camera_calibration.toml
        └── <recording>.blend

文件名常量在 system/paths_and_filenames/file_and_folder_names.py:1-83

8.3 后台任务QThread + multiprocessing 双层夹心

gui/qt/workers/process_motion_capture_data_thread_worker.py:16-76

class ProcessMotionCaptureDataThreadWorker(QThread):
    in_progress = Signal(...)
    finished = Signal(bool)

    def run(self):
        self._process = multiprocessing.Process(
            target=process_recording_folder,
            args=(...),
        )
        self._process.start()
        while self._process.is_alive():
            time.sleep(0.01)
            if not self._queue.empty():
                record = self._queue.get()
                self.in_progress.emit(record)
        self.finished.emit(self._success)
  • QThread 外层Qt 信号槽友好、生命周期可控、可以发 in_progress 给 UI
  • multiprocessing.Process 内层:真正干活,绕开 GIL
  • multiprocessing.Queue:从工作进程把日志/进度推回 QThread

这是 GUI 应用的"重活外包"教科书模式UI 线程绝对不卡,重计算独立崩溃也不杀掉主进程。

8.4 跨平台路径

system/paths_and_filenames/path_getters.py:31-250

def os_independent_home_dir():           # Path.home()
def get_freemocap_data_folder_path():    # ~/.freemocap_data 或自定义
def get_recording_session_folder_path():
def create_new_session_folder():
def get_calibrations_folder_path():
def get_logs_info_and_settings_folder_path():

全部走 pathlib.Path——自动适配 mac/Linux/Windows。GUI 状态持久化用 gui_state.json 存自定义数据根目录。


9. 招牌设计 Top 10值得抄什么

  1. 加权异常点剔除三角化 (freemocap_anipose.py:88-190) — exp(-5·error/threshold) 软权重,多相机时鲁棒胜过硬 RANSAC
  2. @jit(nopython=True) 单点 DLT 三角化 (freemocap_anipose.py:55-67) — Numba 让 Python 几何运算贴近 C
  3. RecordingInfoModel 属性生成器 + status_check — 不维护状态机,按需扫文件
  4. QThread × multiprocessing.Process × Queue × Signal 四件套 — GUI 重活外包标准范式
  5. Tracker 策略模式ModelInfo + duck typing — 加新 tracker 不动主流程
  6. {tracker}_data_2d.npy 前缀命名 — 多算法结果共存对比
  7. skellyforge TaskWorkerThread 管道[INTERPOLATION, FILTERING, FINDING_FRAME, ROTATION] 可拼装
  8. 刚体骨长中位数约束 + 子树级联传播 (enforce_rigid_bones.py) — 不用 IK 就能压住骨头抖
  9. Blender 子进程模式--background --python script.py,主进程 Python 环境干净
  10. CHARUCO + 软件音频/亮度同步 — 用 webcam 也能搞动捕

10. 局限性(别被官方话术骗了)

宣传 实情
"实时动作捕捉" 离线后处理。录的时候是实时同步采集,但 2D 检测 / 三角化 / 后处理全是录完了一起跑
"实时关节角度" 不输出关节角度。v1.8.2 只有关键点位置和质心,关节角度得自己算
"支持任意 tracker" 主流程默认只接 MediaPipeYOLO / OpenPose 在 experimental/ 没上主线
"GPU 加速" 主仓不暴露 GPU 配置,全靠 MediaPipe 内部判断
"媲美 Vicon" 重投影误差量级 ~ 像素级Vicon 亚毫米级标定。差 2-3 个数量级

11. 适合什么 / 不适合什么

适合

  • 教学 / 科研动作分析(人体生物力学、运动学研究)
  • 低成本 3D 角色动画毛坯(导 Blender 后人手 retarget
  • 隐私敏感场景(数据全本地)

不适合

  • 实时游戏 / VR 全身追踪
  • 高精度临床步态分析(亚毫米级别)
  • 商业产品发行(AGPL-3.0——SaaS 化要开源整个调用链)

12. 延伸思考(可改造方向)

  1. 抽离 aniposelib + 加权剔除算法:单独打个 Python 库给所有"多相机三角化"业务用,不必带 PySide6。
  2. 替换 skellyforge 后处理:上 Kalman 滤波 / 双向 LSTM 时序去抖,比 Butterworth 强。
  3. 加关节角度计算:拿 Skeleton.joint_hierarchy + Pydantic 模型,每段两个 segment 算夹角,输出 Euler / 四元数。手机 GUI Agent 项目(个人重点)的姿态识别可以参考。
  4. GPU MediaPipe:用 mediapipe.tasks.python.vision.PoseLandmarker 替代旧 holistic API启 GPU delegate。
  5. WebRTC 实时模式:用 skellycam 出 RTP 流 → 服务器侧实时 2D + 增量三角化 → 真做到"实时动捕"。但需要硬件触发同步才靠谱。

13. 文件级索引(快速查表)

模块 关键文件
入口 freemocap/__main__.py:24gui/qt/freemocap_main.py:29
主窗口 gui/qt/main_window/freemocap_main_window.py:92-150
CHARUCO 板 core_processes/capture_volume_calibration/charuco_stuff/charuco_board_definition.py:7-44
标定主入口 core_processes/capture_volume_calibration/run_anipose_capture_volume_calibration.py:17-35
三角化算法 .../anipose_camera_calibration/freemocap_anipose.py:55-67DLT/ :88-190(加权剔除)
Bundle Adjustment .../freemocap_anipose.py:1145-1265
重投影误差 .../freemocap_anipose.py:1105-1143
软同步 .../synchronize_videos_thread_worker.py:6,49-61
2D 检测入口 core_processes/process_motion_capture_videos/processing_pipeline_functions/image_tracking_pipeline_functions.py:26-98
Tracker 参数模型 data_layer/recording_models/post_processing_parameter_models.py:25-44
Butterworth data_layer/recording_models/post_processing_parameter_models.py:25-28
后处理任务管道 core_processes/post_process_skeleton_data/post_process_skeleton.py:69-104
刚体约束 core_processes/post_process_skeleton_data/enforce_rigid_bones.py:10-86
坐标系旋转 utilities/geometry/rotate_by_90_degrees_around_x_axis.py:4-14
质心计算 core_processes/post_process_skeleton_data/calculate_center_of_mass.py:12-141
导出 split & save core_processes/post_process_skeleton_data/split_and_save.py:32-177
Blender 子进程 core_processes/export_data/blender_stuff/export_to_blender/methods/ajc_addon/run_ajc_addon_main.py:73-99
Blender 路径检测 core_processes/export_data/blender_stuff/get_best_guess_of_blender_path.py:27-86
Skeleton schema data_layer/skeleton_models/skeleton.py:13-133
RecordingInfoModel data_layer/recording_models/recording_info_model.py:41-217
后台任务 worker gui/qt/workers/process_motion_capture_data_thread_worker.py:16-76
跨平台路径 system/paths_and_filenames/path_getters.py:31-250

解析方法4 个 Explore 子 agent 并行拆 ①标定+三角化 ②pose 检测 ③后处理+导出 ④GUI+数据层,主线程读 pyproject.toml + 入口串总线索。所有论断带 file:line。