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

666 lines
27 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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:24``qt_gui_main()` 是唯一入口CLI 仅一行:
```python
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 + 右侧 `ControlPanelWidget`3 子 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`
```python
@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_250`marker = 0.8 × square
- **5×3**:小空间备选
### 3.2 标定主入口
`core_processes/capture_volume_calibration/run_anipose_capture_volume_calibration.py:17-35`
`AniposeCameraCalibrator` @ `anipose_camera_calibration/anipose_camera_calibrator.py:42-87`
### 3.3 标定流程
**内参(每相机)**`freemocap_anipose.py:2178-2202``CameraGroup.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-513``extract_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` 关键 import`freemocap_anipose.py:17-18`
```python
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`
```python
@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-190``triangulate_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`
```python
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-1143``CameraGroup.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` 走两条路:
```python
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-86`YOLO crop → MediaPipe pose |
默认 tracker 在 `data_layer/recording_models/recording_info_model.py:42``active_tracker="mediapipe"`
### 4.2 检测入口
`core_processes/process_motion_capture_videos/processing_pipeline_functions/image_tracking_pipeline_functions.py:26-98`
```python
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
```python
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-96``tracking_params.num_processes`。默认 `multiprocessing.cpu_count() - 1``experimental/alternative_trackers/run_yolo.py:20-22`)。
每路视频独立进程,避开 Python GIL。
### 4.5 Landmark 映射
`post_process_skeleton_data/post_process_skeleton.py:130-136`
```python
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`
```python
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-104``skellyforge.TaskWorkerThread`,任务顺序固定(`post_process_skeleton.py:83`
```python
[
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 — **中位数骨长标准化 + 子关节级联传播**
```python
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`
```python
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`
```python
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`
```python
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`)调用外部插件:
```python
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`
- Windows`Program 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`
```python
# 如果 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`
```python
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`
```python
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`
```python
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` — 支持加权平均生成虚拟点:
```python
# 典型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`
```python
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`
```python
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`
```python
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:24``gui/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-67`DLT/ `: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。