@@ -138,8 +138,9 @@ def pose(
138138 without re-running the network — e.g. run `style='markers'` then `style='skeleton'` fast.
139139 Defaults to True.
140140 save_data (bool, optional): Whether we save the predicted pose data to a file. Defaults to True.
141- data_format (str, optional): Specifies format of pose-data. Accepted values are 'csv', 'tsv'
142- and 'txt'. For multiple output formats, use list, eg. ['csv', 'txt']. Defaults to 'csv'.
141+ data_format (str, optional): Specifies format of pose-data. Accepted values are 'csv', 'tsv',
142+ 'txt' and 'c3d' (motion-capture format; requires the optional ``c3d`` package). For multiple
143+ output formats, use a list, e.g. ['csv', 'c3d']. Defaults to 'csv'.
143144 save_video (bool, optional): Whether we save the video with the estimated pose overlaid on it.
144145 Defaults to True.
145146 style (str, optional): How to draw the pose. `'both'` draws markers (keypoints) connected by
@@ -577,8 +578,11 @@ def save_single_file(of, width, height, model, data, data_format, target_name_da
577578 target_name_data = target_name_data , overwrite = overwrite )
578579
579580 if save_data :
580- save_txt (of , self .width , self .height , model , data , data_format ,
581- target_name_data = target_name_data , overwrite = overwrite )
581+ text_format = _handle_c3d (of , data , OPENPOSE_NAMES .get (model .lower ()), self .fps ,
582+ self .width , self .height , data_format , target_name_data , overwrite )
583+ if text_format is not None :
584+ save_txt (of , self .width , self .height , model , data , text_format ,
585+ target_name_data = target_name_data , overwrite = overwrite )
582586
583587 # Render the average-pose and trajectories images from the collected keypoints
584588 names = OPENPOSE_NAMES .get (model .lower ())
@@ -729,7 +733,9 @@ def _rerender_pose_from_cache(self, style='both', overlay=True, background='blac
729733 for nm in names :
730734 headers .append (f'{ nm } X' )
731735 headers .append (f'{ nm } Y' )
732- _save_pose_txt (of , data , headers , data_format , target_name_data , overwrite )
736+ text_format = _handle_c3d (of , data , names , fps , width , height , data_format , target_name_data , overwrite )
737+ if text_format is not None :
738+ _save_pose_txt (of , data , headers , text_format , target_name_data , overwrite )
733739
734740 avg_frame = (avg_acc / avg_n ).astype (np .uint8 ) if (avg_acc is not None and avg_n > 0 ) else None
735741 average_image , trajectories_image = _render_pose_extras (
@@ -906,7 +912,11 @@ def _pose_mediapipe(
906912 for name in MEDIAPIPE_LANDMARK_NAMES :
907913 headers .append (name .replace ('_' , ' ' ).title () + ' X' )
908914 headers .append (name .replace ('_' , ' ' ).title () + ' Y' )
909- _save_pose_txt (of , data , headers , data_format , target_name_data , overwrite )
915+ c3d_names = [name .replace ('_' , ' ' ).title () for name in MEDIAPIPE_LANDMARK_NAMES ]
916+ text_format = _handle_c3d (of , data , c3d_names , self .fps , self .width , self .height ,
917+ data_format , target_name_data , overwrite )
918+ if text_format is not None :
919+ _save_pose_txt (of , data , headers , text_format , target_name_data , overwrite )
910920
911921 # Render the average-pose and trajectories images from the collected keypoints
912922 names = [name .replace ('_' , ' ' ).title () for name in MEDIAPIPE_LANDMARK_NAMES ]
@@ -935,6 +945,56 @@ def _pose_mediapipe(
935945 return self
936946
937947
948+ def _save_pose_c3d (of , data , names , fps , width , height , target_name_data = None , overwrite = False ):
949+ """
950+ Save pose keypoints to a C3D motion-capture file.
951+
952+ Each landmark becomes a 3D point (x, y, z=0) in pixel coordinates; frames with a
953+ missing detection (0,0) are flagged invalid (residual = -1). Requires the optional
954+ ``c3d`` package (``pip install c3d``).
955+ """
956+ try :
957+ import c3d
958+ except ImportError as exc :
959+ from musicalgestures ._utils import MgError
960+ raise MgError ("Saving pose data as C3D requires the 'c3d' package. "
961+ "Install it with: pip install c3d" ) from exc
962+
963+ out_path = (of + '_pose.c3d' ) if target_name_data is None else (os .path .splitext (target_name_data )[0 ] + '.c3d' )
964+ if not overwrite :
965+ out_path = generate_outfilename (out_path )
966+
967+ n_points = len (names )
968+ writer = c3d .Writer (point_rate = float (fps ))
969+ for row in data :
970+ coords = np .asarray (row [1 :1 + 2 * n_points ], dtype = np .float32 )
971+ points = np .zeros ((n_points , 5 ), dtype = np .float32 )
972+ for j in range (n_points ):
973+ x , y = coords [2 * j ], coords [2 * j + 1 ]
974+ if x == 0 and y == 0 : # missing detection
975+ points [j ] = [0 , 0 , 0 , - 1 , 0 ]
976+ else :
977+ points [j ] = [x * width , y * height , 0.0 , 0.0 , 0 ]
978+ writer .add_frames ([(points , np .zeros ((0 , 1 ), dtype = np .float32 ))])
979+
980+ writer .set_point_labels (names )
981+ with open (out_path , 'wb' ) as h :
982+ writer .write (h )
983+ return out_path
984+
985+
986+ def _handle_c3d (of , data , names , fps , width , height , data_format , target_name_data , overwrite ):
987+ """Save a .c3d file if 'c3d' is among the requested formats; return the remaining
988+ (text) formats to be handled by the text saver, or None if there are none."""
989+ formats = list (data_format ) if isinstance (data_format , (list , tuple )) else [data_format ]
990+ if any (str (f ).lower () == 'c3d' for f in formats ):
991+ _save_pose_c3d (of , data , names , fps , width , height , target_name_data , overwrite )
992+ text = [f for f in formats if str (f ).lower () != 'c3d' ]
993+ if not text :
994+ return None
995+ return text if isinstance (data_format , (list , tuple )) else text [0 ]
996+
997+
938998def _save_pose_txt (of , data , headers , data_format , target_name_data , overwrite ):
939999 """Save pose data to one or more text files (csv / tsv / txt)."""
9401000
0 commit comments