From cdee6f6f6f93a5a24266c15faa778a843e41420c Mon Sep 17 00:00:00 2001 From: kang Date: Wed, 27 May 2026 01:00:12 +0800 Subject: [PATCH] auto-save 2026-05-27 01:00 (~2) --- .memory/source-analysis.md | 665 ++++++++++++++++++++++++++++++++++++- .memory/worklog.json | 7 + 2 files changed, 664 insertions(+), 8 deletions(-) diff --git a/.memory/source-analysis.md b/.memory/source-analysis.md index 34e24c9..3ddfe7d 100644 --- a/.memory/source-analysis.md +++ b/.memory/source-analysis.md @@ -1,16 +1,665 @@ -# FreeMoCap 源码解析 源码解析 +# FreeMoCap v1.8.2 源码深度解析 > 创建日期:2026-05-27 -> 上游版本:待填写 +> 上游版本:v1.8.2(commit clone 自 https://github.com/freemocap/freemocap) +> 主仓 LOC:14,210(freemocap 包内 145 个 .py 文件) +> 仓库定位:8.8k★ / 808 fork / AGPL-3.0 / Python 3.10–3.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.6–6.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 | SkellyViewer(3D 可视化) | **外部库 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] 摄像头 Tab:SkellyCamWidget 启动多机位录制(外部库) +[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_synchronize:audio / 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)` | +| CSV(by_trajectory) | `{recording}_by_trajectory.csv` | 每帧一行,列=每个 marker 的 x/y/z | +| CSV(by_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 有具名 landmark:body_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_/ + └── recording_/ + ├── 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 + ├── _camera_calibration.toml + └── .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" | 主流程默认只接 MediaPipe;YOLO / 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。 diff --git a/.memory/worklog.json b/.memory/worklog.json index 5e6f19b..0793401 100644 --- a/.memory/worklog.json +++ b/.memory/worklog.json @@ -6,6 +6,13 @@ "message": "项目创建: FreeMoCap 源码解析", "hash": "", "files_changed": 0 + }, + { + "ts": "2026-05-27T00:49:56+08:00", + "type": "commit", + "message": "init: project scaffold", + "hash": "d430655", + "files_changed": 9 } ] }