From eb017b50a3539aeb964517ea4766233b6aa34957 Mon Sep 17 00:00:00 2001 From: Chih-Kang Chang Date: Thu, 16 Jul 2026 17:05:41 -0700 Subject: [PATCH 1/3] Fix video-based intrinsics calibration (frame extraction and cached corner points) - extract_frames(): fix frame-extracted check calling .stem on a bool (always raised, silently caught, so video frames were never extracted) - extract_frames(): write extracted frames next to the source video, not the current working directory - calibrate_intrinsics(): don't let a cache miss in Image_points.json overwrite the real objp checkerboard grid with [], which broke findCorners()'s return value and crashed the caller's unpacking --- Pose2Sim/calibration.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/Pose2Sim/calibration.py b/Pose2Sim/calibration.py index b0d8e0f6..5d41d3a1 100644 --- a/Pose2Sim/calibration.py +++ b/Pose2Sim/calibration.py @@ -759,12 +759,12 @@ def calibrate_intrinsics(calib_dir, intrinsics_config_dict, save_debug_images=Tr if show_detection_intrinsics == True: # If previously labeled points exist, check if they are satisfying if 'image_points' in locals(): - imgp = next((entry['image_points_2d'] for entry in image_points if entry["cam_name"] == cam_name), []) - objp = next((entry['object_points_3d'] for entry in image_points if entry["cam_name"] == cam_name), []) - if len(imgp) > 0 and len(objp) > 0: + cached_imgp = next((entry['image_points_2d'] for entry in image_points if entry["cam_name"] == cam_name), []) + cached_objp = next((entry['object_points_3d'] for entry in image_points if entry["cam_name"] == cam_name), []) + if len(cached_imgp) > 0 and len(cached_objp) > 0: # recalculate reprojected points - imgp = np.array(imgp).reshape(-1, 2) - objp = np.array(objp).reshape(-1, 3) + imgp = np.array(cached_imgp).reshape(-1, 2) + objp = np.array(cached_objp).reshape(-1, 3) saved_img_path = create_image_labels(img_path, imgp, calib_dir, 'int', reprojected_points=None, show=True, save=save_debug_images) # Are you satisfied? If so, add to imgpoints and continue; else, redo detection satisfied = show_qt_message_box( @@ -1440,7 +1440,7 @@ def extract_frames(video_path, extract_every_N_sec=1, overwrite_extraction=False - extracted frames in folder ''' - if not Path(Path(video_path).exists().stem + '_00000.png') or overwrite_extraction: + if not list(Path(video_path).parent.glob(f'{Path(video_path).stem}_*.png')) or overwrite_extraction: cap = cv2.VideoCapture(str(video_path)) if cap.isOpened(): fps = round(cap.get(cv2.CAP_PROP_FPS)) @@ -1450,7 +1450,7 @@ def extract_frames(video_path, extract_every_N_sec=1, overwrite_extraction=False ret, frame = cap.read() if ret == True: if frame_nb % (fps*extract_every_N_sec) == 0: - img_path = (Path(video_path).stem + '_' +str(frame_nb).zfill(5)+'.png') + img_path = Path(video_path).parent / (Path(video_path).stem + '_' +str(frame_nb).zfill(5)+'.png') cv2.imwrite(str(img_path), frame) frame_nb+=1 else: From a2ec23324f51b7276a4ed26fe2ed59024b63ef98 Mon Sep 17 00:00:00 2001 From: Chih-Kang Chang Date: Fri, 17 Jul 2026 10:29:22 -0700 Subject: [PATCH 2/3] Fix synchronization using same video for all cameras cam_names was derived with `Path(j_dir).name.split('_')[0]`, which assumes camera names never contain underscores. Video files named like "cam_1.mp4" or "cam_2.mp4" produce pose directories "cam_1_json", "cam_2_json", etc., all truncated to the same "cam" name. This collapsed the per-camera video lookup dict to a single entry, so every camera reused one video during person/ frame selection, and offset logs printed "Camera cam and cam". Strip only the trailing "_json" suffix instead, preserving the full camera name regardless of underscores. --- Pose2Sim/synchronization.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Pose2Sim/synchronization.py b/Pose2Sim/synchronization.py index fbf62528..2b371a61 100644 --- a/Pose2Sim/synchronization.py +++ b/Pose2Sim/synchronization.py @@ -1490,7 +1490,7 @@ def synchronize_cams_all(config_dict): nb_frames_per_cam = [len(fnmatch.filter(os.listdir(json_dir), '*.json')) for json_dir in json_dirs] cam_nb = len(json_dirs) cam_list = list(range(cam_nb)) - cam_names = [Path(j_dir).name.split('_')[0] for j_dir in json_dirs] + cam_names = [re.sub(r'_json$', '', Path(j_dir).name) for j_dir in json_dirs] # frame range selection f_range = [[0, min([len(j) for j in json_files_names])] if frame_range in ('all', 'auto', []) else frame_range][0] From 263f9cb708f71accd71e3f0d85cb7f746dbc974f Mon Sep 17 00:00:00 2001 From: Chih-Kang Chang Date: Fri, 17 Jul 2026 15:22:47 -0700 Subject: [PATCH 3/3] Fix IndexError in markerAugmentation and validate participant_height/mass against trc file count The augmentation loop iterated `range(len(subject_mass))` instead of `range(len(trc_files))`, so a longer participant_mass/participant_height list in Config.toml (e.g. a multi-person value left over when multi_person is false) caused `trc_files[p]` to raise IndexError once p exceeded the actual number of trc files. - When multi_person is false, participant_height/participant_mass must now resolve to exactly one value matching the one expected trc file; a mismatch raises a clear ValueError instead of silently padding/trimming into a later crash. - When multi_person is true, mismatched list lengths still only warn and pad/trim, since a valid person can be dropped from pose-3d during triangulation. - If multiple trc files are found while multi_person is false (e.g. after rerunning filtering() with a different filter type), fall back to the most recently modified one with a warning, since pose-3d is never cleaned between runs and this is a normal workflow. --- Pose2Sim/markerAugmentation.py | 35 ++++++++++++++++++++++++++-------- 1 file changed, 27 insertions(+), 8 deletions(-) diff --git a/Pose2Sim/markerAugmentation.py b/Pose2Sim/markerAugmentation.py index 5babaf2a..d1af3f37 100644 --- a/Pose2Sim/markerAugmentation.py +++ b/Pose2Sim/markerAugmentation.py @@ -89,7 +89,8 @@ def augment_markers_all(config_dict): frame_range = config_dict.get('project', {}).get('frame_range', 'auto') subject_height = config_dict.get('project', {}).get('participant_height', 'auto') subject_mass = config_dict.get('project', {}).get('participant_mass', 70.0) - + multi_person = config_dict.get('project', {}).get('multi_person', False) + large_hip_knee_angles = config_dict.get('kinematics', {}).get('large_hip_knee_angles', 90) trimmed_extrema_percent = config_dict.get('kinematics', {}).get('trimmed_extrema_percent', 50) default_height = config_dict.get('kinematics', {}).get('default_height', 1.7) @@ -112,6 +113,12 @@ def augment_markers_all(config_dict): trc_files = trc_no_filtering sorted(trc_files, key=natural_sort_key) + if not multi_person and len(trc_files) > 1: + most_recent_trc_file = max(trc_files, key=lambda f: f.stat().st_mtime) + ignored_trc_files = [f.name for f in trc_files if f != most_recent_trc_file] + logging.warning(f"multi_person is set to false in Config.toml but {len(trc_files)} trc files were found in {pose_3d_dir}. Using the most recently modified one ({most_recent_trc_file.name}) and ignoring {ignored_trc_files}.") + trc_files = [most_recent_trc_file] + # Add missing markers if needed for trc_file in trc_files: # Import TRC file @@ -160,9 +167,15 @@ def augment_markers_all(config_dict): subject_height.append(height) elif not type(subject_height) == list: # int or float subject_height = [subject_height] - if len(subject_height) < len(trc_files): - logging.warning(f"Number of subject heights does not match number of TRC files. Missing heights are set to {default_height}m.") - subject_height += [default_height] * (len(trc_files) - len(subject_height)) + if len(subject_height) != len(trc_files): + if not multi_person: + raise ValueError(f"Multi_person is set to false in Config.toml but participant_height has {len(subject_height)} value(s) while {len(trc_files)} trc file(s) were found. Expected exactly one matching value.") + elif len(subject_height) < len(trc_files): + logging.warning(f"Number of subject heights does not match number of TRC files. Missing heights are set to {default_height}m.") + subject_height += [default_height] * (len(trc_files) - len(subject_height)) + else: + logging.warning("Number of subject heights does not match number of TRC files. Extra heights are ignored.") + subject_height = subject_height[:len(trc_files)] # Get subject masses if subject_mass is None or subject_mass == 0: @@ -170,12 +183,18 @@ def augment_markers_all(config_dict): logging.warning("No subject mass found in Config.toml. Using default mass of 70kg.") elif not type(subject_mass) == list: subject_mass = [subject_mass] - if len(subject_mass) < len(trc_files): - logging.warning("Number of subject masses does not match number of TRC files. Missing masses are set to 70kg.") - subject_mass += [70] * (len(trc_files) - len(subject_mass)) + if len(subject_mass) != len(trc_files): + if not multi_person: + raise ValueError(f"Multi_person is set to false in Config.toml but participant_mass has {len(subject_mass)} value(s) while {len(trc_files)} trc file(s) were found. Expected exactly one matching value.") + elif len(subject_mass) < len(trc_files): + logging.warning("Number of subject masses does not match number of TRC files. Missing masses are set to 70kg.") + subject_mass += [70] * (len(trc_files) - len(subject_mass)) + else: + logging.warning("Number of subject masses does not match number of TRC files. Extra masses are ignored.") + subject_mass = subject_mass[:len(trc_files)] # Run marker augmentation - for p in range(len(subject_mass)): + for p in range(len(trc_files)): trc_file = trc_files[p] trc_file_out = Path(trc_file).stem + f'_{augmenterModelName}.trc'