diff --git a/Sports2D/Sports2D.ipynb b/Sports2D/Sports2D.ipynb index 94280c7..92851fd 100644 --- a/Sports2D/Sports2D.ipynb +++ b/Sports2D/Sports2D.ipynb @@ -1 +1 @@ -{"cells":[{"attachments":{},"cell_type":"markdown","metadata":{},"source":["\"Open"]},{"attachments":{},"cell_type":"markdown","metadata":{"id":"X38L6tanrnrB"},"source":["# Sports2D\n","\n","When seeking to optimize a sports movement, it is often useful to compute joint or segment angles. \\\n","**[Sports2D](https://github.com/davidpagnon/Sports2D)** offers a way to compute them from a video, on any platform, including your smartphone.\\\n","Run cells one after another by clicking on the ▶️ button.\n","\n","
\n","\n",""]},{"attachments":{},"cell_type":"markdown","metadata":{"id":"T6FyIv-MAxGf"},"source":["\n","## Additional information"]},{"attachments":{},"cell_type":"markdown","metadata":{"id":"ZGOSkQKqBwf8"},"source":["Please find more information about how it works under the hood [here](https://github.com/davidpagnon/Sports2D). \n","\n","While you're there, do not hesitate to hit the star button, I would appreciate the support! \\\n","If you use Sports2D, please cite:\n","\n"," @misc{Pagnon2023,\n"," author = {Pagnon, David},\n"," title = {Sports2D - Angles from video},\n"," year = {2023},\n"," doi= {10.5281/zenodo.7903963},\n"," publisher = {GitHub},\n"," journal = {GitHub repository},\n"," howpublished = {\\url{https://github.com/davidpagnon/Sports2D}},\n"," }\n","\n","
\n","\n","**Warning:**\n","- Results are only as good as the pose estimation algorithm, i.e., they are not perfect. Moreover, they will not be good if your video is blurry. Finally, they are acceptable only if the persons move in the 2D plane, from right to left or from left to right. If you need research-grade 3D markerless kinematics, consider using several cameras with [Pose2Sim](https://github.com/perfanalytics/pose2sim) instead.\n","- Your data will be sent to the Google servers, which do not follow the European GDPR requirements regarding privacy. If you don't want this, you can [run Sports 2D on your computer](https://github.com/davidpagnon/Sports2D) instead.\n","- The server disconnects after 90 minutes of idle time, and after a few hours in any case. Make sure you have downloaded your results beforehand.\n","- Unless you subscribed, there is a time limit on the usage of GPUs on Colab. However, these are only mandatory if you run OpenPose. If you run BlazePose, or if you want to analyze previously saved results, you can deactivate GPUs via `Runtime -> Change runtime type`\n"]},{"cell_type":"markdown","metadata":{"id":"4_cobxbdkBmb"},"source":["# Installation"]},{"cell_type":"code","execution_count":3,"metadata":{"cellView":"form","executionInfo":{"elapsed":248,"status":"ok","timestamp":1684614861687,"user":{"displayName":"David Pagnon","userId":"05832985921561105783"},"user_tz":-120},"id":"j5e0AeND0PfC"},"outputs":[],"source":["#@markdown You can choose to: \n","#@markdown - **Install just once**. Full install takes about 1.2 Go on your Google Drive (270 Mo for Python, 875 Mo for OpenPose),\\\n","#@markdown but next times the environment should be set up in about 2 minutes, including on your smartphone.\n","#@markdown - **Install every time**. Full install takes about 22 minute (2 minutes for Python, 20 minutes for OpenPose), \\\n","#@markdown but no connexion to Google Drive nor storage space is needed.\n","installation_type = 'Install once' #@param [\"Install once\", \"Install every time\"]\n","\n","#@markdown
\n","\n","#@markdown You can also choose to:\n","#@markdown - **Install OpenPose** if you need to go for accuracy and multi-person analysis.\n","#@markdown - **Not install OpenPose** for a much faster and lighter installation with BlazePose instead. \n","install_openpose = 'Install OpenPose' #@param [\"Install OpenPose\", \"Do not install OpenPose\"]\n","\n","import os, sys\n","from google.colab import files, drive\n","\n","def connect_to_Gdrive():\n"," print(\"Connecting to Google Drive...\")\n"," # Add GDrive notebooks to colab\n"," drive.mount('/content/drive')\n"," if not os.path.exists(sports2d_path):\n"," !mkdir -p '/content/drive/My Drive/Sports2D'\n"," else:\n"," print(f\"{sports2d_path} already exists.\")\n"," !cp -r $sports2d_path '/content/drive/My Drive'\n"," !rm -r $sports2d_path\n"," os.symlink('/content/drive/My Drive/Sports2D', sports2d_path)\n"," sys.path.insert(0, sports2d_path)\n","\n","def install_Sports2D_in_GDrive():\n"," print(\"Installing Sports2D in Google Drive...\")\n"," python_path = os.path.join(sports2d_path,'Sports2D_python')\n"," if not os.path.exists(python_path):\n"," # Install Python libraries in GDrive\n"," !PYTHONUSERBASE=$python_path pip install --user Sports2D\n"," else: \n"," print('Sports2D already installed.')\n"," # Add the GDrive python installation path to PYTHONPATH\n"," sys.path.append(os.path.join(python_path,\"lib/python3.10/site-packages\"))\n","\n","def install_openpose():\n"," print(\"Installing OpenPose in Google Drive...\")\n"," git_repo_url = 'https://github.com/CMU-Perceptual-Computing-Lab/openpose.git'\n"," if not os.path.exists(openpose_path):\n"," !mkdir -p $openpose_path\n"," # install new CMake because of CUDA10\n"," !wget -q https://cmake.org/files/v3.13/cmake-3.13.0-Linux-x86_64.tar.gz\n"," !tar xfz cmake-3.13.0-Linux-x86_64.tar.gz --strip-components=1 -C /usr/local\n"," !git clone $git_repo_url\n"," !sed -i 's/execute_process(COMMAND git checkout master WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}\\/3rdparty\\/caffe)/execute_process(COMMAND git checkout f019d0dfe86f49d1140961f8c7dec22130c83154 WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}\\/3rdparty\\/caffe)/g' openpose/CMakeLists.txt\n"," # install system dependencies\n"," !apt-get -qq install -y libatlas-base-dev libprotobuf-dev libleveldb-dev libsnappy-dev libhdf5-serial-dev protobuf-compiler libgflags-dev libgoogle-glog-dev liblmdb-dev opencl-headers ocl-icd-opencl-dev libviennacl-dev\n"," # build openpose\n"," !cd openpose && rm -rf build || true && mkdir build && cd build && cmake .. && make -j`nproc` \n"," # Install body_25b model\n"," !wget posefs1.perception.cs.cmu.edu/OpenPose/models/pose/1_25BBkg/body_25b/pose_iter_XXXXXX.caffemodel -P /content/openpose/models/pose/body_25b\n"," !wget https://raw.githubusercontent.com/CMU-Perceptual-Computing-Lab/openpose_train/master/experimental_models/1_25BBkg/body_25b/pose_deploy.prototxt -P /content/openpose/models/pose/body_25b \n"," # Get compatible CUDA and CuDNN versions in order to avoid `CudaSuccess (2 vs. 0) out of memory` error\n"," !apt -qq install --allow-change-held-packages libcudnn8=8.1.0.77-1+cuda11.2\n"," # Move openpose folder to GDrive\n"," !mv /content/openpose/* $openpose_path && rm -r /content/openpose\n"," !rm -rf /content/openpose\n"," !chmod 755 $openpose_path/build/examples/openpose/openpose.bin\n"," # Copy libraries in system path\n"," !cp $openpose_path/build/src/openpose/libopenpose.so.1.7.0 /usr/local/lib\n"," !cp $openpose_path/build/caffe/lib/libcaffe.so.1.0.0 /usr/local/lib\n"," !sudo ldconfig\n"," else:\n"," print(\"OpenPose already installed.\")\n"," print(\"Setting up the environment...\")\n"," # Allow execution of OpenPose\n"," !chmod 755 $openpose_path/build/examples/openpose/openpose.bin\n"," # Install system dependencies\n"," !apt-get -qq install -y libatlas-base-dev libprotobuf-dev libleveldb-dev libsnappy-dev libhdf5-serial-dev protobuf-compiler libgflags-dev libgoogle-glog-dev liblmdb-dev opencl-headers ocl-icd-opencl-dev libviennacl-dev &> /dev/null\n"," # Get compatible CUDA and CuDNN versions in order to avoid `CudaSuccess (2 vs. 0) out of memory` error\n"," !apt -qq install --allow-change-held-packages libcudnn8=8.1.0.77-1+cuda11.2 &> /dev/null\n"," # Copy libraries in system path\n"," !cp $openpose_path/build/src/openpose/libopenpose.so.1.7.0 /usr/local/lib\n"," !cp $openpose_path/build/caffe/lib/libcaffe.so.1.0.0 /usr/local/lib\n"," !sudo ldconfig\n","\n","sports2d_path = '/content/Sports2D'\n","python_path = os.path.join(sports2d_path,'Sports2D_python')\n","openpose_path = os.path.join(sports2d_path,'Sports2D_openpose')\n","video_path = os.path.join(sports2d_path,'Sports2D_videos')\n","result_path = os.path.join(sports2d_path,'Sports2D_results')\n","\n","# Use the right Python version\n","!sudo update-alternatives --install /usr/bin/python3 python3 /usr/bin/python3.10 1\n","# GDrive tools\n","!apt-get -qq install xattr &> /dev/null\n","\n","if installation_type == \"Install every time\":\n"," print('Installing Sports2D')\n"," !pip install sports2D\n"," if install_openpose == 'Install OpenPose':\n"," install_openpose()\n","\n","elif installation_type == \"Install once\":\n"," connect_to_Gdrive()\n"," install_Sports2D_in_GDrive()\n"," if install_openpose == 'Install OpenPose':\n"," install_openpose()\n","\n","print(\"Done.\")"]},{"attachments":{},"cell_type":"markdown","metadata":{"id":"gv5lSY_pZx8I"},"source":["# Upload videos"]},{"cell_type":"code","execution_count":null,"metadata":{"cellView":"form","id":"5dQqA5E6YeUS"},"outputs":[],"source":["#@markdown Upload your videos if needed, and convert them if needed.\\\n","#@markdown **Note:** As this takes time, you should probably try to trim your videos beforehand.\n","\n","def upload_videos():\n"," if not os.path.exists(video_path):\n"," !mkdir -p $video_path\n"," %cd $video_path\n"," uploaded = files.upload()\n","\n","def convert_vertical_iphone_vids(vids):\n"," # Pose algorithms do not see the rotation in certain videos taken in portrait mode. Convert them\n"," for i, vid in enumerate(vids):\n"," iphone_portrait = !ffprobe -loglevel error -select_streams v:0 -show_entries stream_tags=rotate -of default=nw=1:nk=1 -i $vid\n"," if iphone_portrait:\n"," vid_out = os.path.splitext(vid)[0]+'_converted'+os.path.splitext(vid)[1]\n"," print(f'{vid} is rotated. Converting it to {vid_out}...')\n"," !ffmpeg -i $vid $vid_out -loglevel quiet\n"," vids[i] = vid_out\n"," os.remove(vid)\n","\n","# upload videos\n","upload_videos()\n","\n","# convert them if needed\n","vids = os.listdir(video_path)\n","vids = [f for f in vids if not f.startswith('.') and f!='logs.txt']\n","convert_vertical_iphone_vids(vids)\n","\n","# list them\n","vids = os.listdir(video_path)\n","vids = [f for f in vids if not f.startswith('.')]\n","print(f'{vids} uploaded and ready to be processed.')"]},{"cell_type":"markdown","metadata":{"id":"n5L3Z5YVrZ2R"},"source":["# Run Sports2D\n"]},{"cell_type":"markdown","metadata":{"id":"3A8t09re07Vu"},"source":["## Specify video to analyze\n"]},{"cell_type":"code","execution_count":null,"metadata":{"cellView":"form","id":"66aTPLL1jAlZ"},"outputs":[],"source":["import toml\n","\n","# Retrieve Sports2D Config file\n","if not os.path.exists(result_path):\n"," !mkdir -p $result_path\n","\n","if installation_type == 'Install every time':\n"," !cp /usr/local/lib/python3.10/dist-packages/Sports2D/Demo/Config_demo.toml $result_path/Config.toml\n","if installation_type == 'Install once':\n"," !cp $python_path/lib/python3.10/site-packages/Sports2D/Demo/Config_demo.toml $result_path/Config.toml\n","%cd $result_path\n","config_dict = toml.load('Config.toml')\n","\n","video_name = 'input1.mp4' #@param {type:\"string\"}\n","video_name = os.path.basename(video_name)\n","pose_model = 'BODY_25B'\n","config_dict.get('project').update({'video_dir':video_path})\n","config_dict.get('project').update({'video_file':video_name})\n","config_dict.get('project').update({'result_dir':result_path})\n","config_dict.get('pose').update({'pose_algo':'OPENPOSE'})\n","config_dict.get('pose').get('OPENPOSE').update({'openpose_model':'BODY_25B'})\n","config_dict.get('pose').get('OPENPOSE').update({'openpose_path':openpose_path})\n","\n","video_full_path = os.path.join(video_path, video_name)\n","if os.path.isfile(video_full_path)==True: \n"," print(f'Video found at {video_full_path}')\n","else: \n"," print(f'Video not found at {video_full_path}')"]},{"cell_type":"markdown","metadata":{"id":"g6JGcVET0wZ_"},"source":["## Settings (optional)"]},{"cell_type":"markdown","metadata":{"id":"NGwcZYGdTrew"},"source":["### Pose settings"]},{"cell_type":"code","execution_count":3,"metadata":{"cellView":"form","executionInfo":{"elapsed":232,"status":"ok","timestamp":1684577247028,"user":{"displayName":"David Pagnon","userId":"05832985921561105783"},"user_tz":-120},"id":"iMQvTam9mWzc"},"outputs":[],"source":["#@markdown **OpenPose** supports multi-person analysis and is more accurate, but it requires a heavier install than **BlazePose**:\n","pose_algo = 'OpenPose' #@param [\"OpenPose\", \"BlazePose\"]\n","\n","#@markdown
\n","\n","#@markdown - **If OpenPose** algorithm is chosen.\\ \n","#@markdown **BODY_25** is the standard model, but **BODY_25B** is more accurate:\n","if pose_algo == 'OpenPose':\n"," if install_openpose != 'Install OpenPose':\n"," print('WARNING: OpenPose not installed!')\n"," else:\n"," pose_model = 'BODY_25B' #@param [\"BODY_25B\", \"BODY_25\"]\n"," config_dict.get('pose').update({'pose_algo':'OPENPOSE'})\n"," config_dict.get('pose').get('OPENPOSE').update({'openpose_model':pose_model})\n"," \n","#@markdown
\n","\n","#@markdown - **If BlazePose** is chosen.\n","if pose_algo == 'BlazePose':\n"," model_complexity = 'Most accurate' #@param [\"Most accurate\", \"Default\", \"Fastest\"]\n"," model_complexity_dict = {'Most accurate':2, 'Default': 1, 'Fastest': 0}\n"," model_complexity_nb = model_complexity_dict[model_complexity]\n"," pose_model = 'BLAZEPOSE'\n"," \n"," config_dict.get('pose').update({'pose_algo':'BLAZEPOSE'})\n"," config_dict.get('pose').get('BLAZEPOSE').update({'model_complexity':model_complexity_nb})"]},{"cell_type":"markdown","metadata":{"id":"PRgCqkmr0ob3"},"source":["### Angle settings"]},{"cell_type":"code","execution_count":null,"metadata":{"cellView":"form","id":"tDcG9TK8kiPJ"},"outputs":[],"source":["#@markdown - Select **joint angles** among:\n","right_ankle = True #@param {type:\"boolean\"}\n","left_ankle = True #@param {type:\"boolean\"}\n","right_knee = True #@param {type:\"boolean\"}\n","left_knee = True #@param {type:\"boolean\"}\n","right_hip = True #@param {type:\"boolean\"}\n","left_hip = True #@param {type:\"boolean\"}\n","right_shoulder = True #@param {type:\"boolean\"}\n","left_shoulder = True #@param {type:\"boolean\"}\n","right_elbow = True #@param {type:\"boolean\"}\n","left_elbow = True #@param {type:\"boolean\"}\n","\n","joint_angles_var = [right_ankle, left_ankle, right_knee, left_knee, right_hip, left_hip, right_shoulder, left_shoulder, right_elbow, left_elbow]\n","joint_angles_dict = {'Right ankle':right_ankle, 'Left ankle': left_ankle, 'Right knee':right_knee, 'Left knee':left_knee, 'Right hip':right_hip, 'Left hip':left_hip, 'Right shoulder':right_shoulder, 'Left shoulder':left_shoulder, 'Right elbow':right_elbow, 'Left elbow':left_elbow}\n","joint_angles_val = [key for key in joint_angles_dict.keys() if joint_angles_dict[key]]\n","\n","config_dict.get('compute_angles').update({'joint_angles':joint_angles_val})\n","\n","#@markdown
\n","\n","#@markdown - Select **segment angles** among:\n","right_foot = True #@param {type:\"boolean\"}\n","left_foot = True #@param {type:\"boolean\"}\n","right_shank = True #@param {type:\"boolean\"}\n","left_shank = True #@param {type:\"boolean\"}\n","right_thigh = True #@param {type:\"boolean\"}\n","left_thigh = True #@param {type:\"boolean\"}\n","trunk = True #@param {type:\"boolean\"}\n","right_arm = True #@param {type:\"boolean\"}\n","left_arm = True #@param {type:\"boolean\"}\n","right_forearm = True #@param {type:\"boolean\"}\n","left_forearm = True #@param {type:\"boolean\"}\n","\n","segment_angles_var = [right_foot, left_foot, right_shank, left_shank, right_thigh, left_thigh, trunk, right_arm, left_arm, right_forearm, left_forearm]\n","segment_angles_dict = {'Right foot':right_foot, 'Left foot': left_foot, 'Right shank':right_shank, 'Left shank':left_shank, 'Right thigh':right_thigh, 'Left thigh':left_thigh, 'Trunk':trunk, 'Right arm':right_arm, 'Left arm':left_arm, 'Right forearm':right_forearm, 'Left forearm':left_forearm}\n","segment_angles_val = [key for key in segment_angles_dict.keys() if segment_angles_dict[key]]\n","\n","config_dict.get('compute_angles').update({'segment_angles':segment_angles_val})"]},{"attachments":{},"cell_type":"markdown","metadata":{"id":"Xh6MzePd1OYO"},"source":["### Advanced pose settings"]},{"cell_type":"code","execution_count":null,"metadata":{"cellView":"form","id":"HAyWZGC9EUGw"},"outputs":[],"source":["#@markdown Do not run pose estimation again if files already exist:\n","load_pose = True #@param {type:\"boolean\"}\n","config_dict.get('pose_advanced').update({'load_pose':load_pose})\n","\n","#@markdown
\n","\n","#@markdown Choose whether to save images and video of pose estimation:\n","save_img = True #@param {type:\"boolean\"}\n","save_vid = True #@param {type:\"boolean\"}\n","config_dict.get('pose_advanced').update({'save_img':save_img})\n","config_dict.get('pose_advanced').update({'save_vid':save_vid})\n","\n","#@markdown
\n","\n","#@markdown Interpolate gaps only if they are smaller than XX frames:\n","interp_gap_smaller_than = 5 #@param {type:\"slider\", min:0, max:100, step:1}\n","\n","#@markdown
\n","\n","#@markdown Choose whether to filter results:\n","filter = True #@param {type:\"boolean\"}\n","filter_type = 'butterworth' #@param [\"butterworth\", \"gaussian\", \"LOESS\", \"median\"]\n","config_dict.get('pose_advanced').update({'filter':filter})\n","config_dict.get('pose_advanced').update({'filter_type':filter_type})\n","\n","#@markdown
\n","\n","#@markdown - If Butterworth filter is chosen, specify its order and cut-off frequency (in Hz):\n","order = 4 #@param {type:\"integer\"}\n","config_dict.get('pose_advanced').get('butterworth').update({'order':order})\n","cut_off_frequency = 4 #@param {type:\"integer\"}\n","config_dict.get('pose_advanced').get('butterworth').update({'cut_off_frequency':cut_off_frequency})\n","\n","#@markdown
\n","\n","#@markdown - If Gaussian filter is chosen, specify its sigma kernel (in pixel):\n","sigma_kernel = 1 #@param {type:\"integer\"}\n","config_dict.get('pose_advanced').get('gaussian').update({'sigma_kernel':sigma_kernel})\n","\n","#@markdown
\n","\n","#@markdown - If LOESS filter is chosen, specify the number of values used:\n","nb_values_used = 5 #@param {type:\"integer\"}\n","config_dict.get('pose_advanced').get('loess').update({'nb_values_used':nb_values_used})\n","\n","#@markdown
\n","\n","#@markdown - If Median filter is chosen, specify its kernel size (in frames):\n","kernel_size = 3 #@param {type:\"integer\"}\n","config_dict.get('pose_advanced').get('median').update({'kernel_size':kernel_size})"]},{"attachments":{},"cell_type":"markdown","metadata":{"id":"Zg-vLGpH1VWm"},"source":["### Advanced angle settings"]},{"cell_type":"code","execution_count":null,"metadata":{"cellView":"form","id":"HQNCNoYJGIDg"},"outputs":[],"source":["#@markdown Choose whether to save images and video of angle estimation:\n","save_img = True #@param {type:\"boolean\"}\n","save_vid = True #@param {type:\"boolean\"}\n","config_dict.get('compute_angles_advanced').update({'save_img':save_img})\n","config_dict.get('compute_angles_advanced').update({'save_vid':save_vid})\n","\n","#@markdown
\n","\n","#@markdown Choose whether to filter results:\n","filter = False #@param {type:\"boolean\"}\n","filter_type = 'butterworth' #@param [\"butterworth\", \"gaussian\", \"LOESS\", \"median\"]\n","config_dict.get('compute_angles_advanced').update({'filter':filter})\n","config_dict.get('compute_angles_advanced').update({'filter_type':filter_type})\n","\n","#@markdown
\n","\n","#@markdown - If Butterworth filter is chosen, specify its order and cut-off frequency (in Hz):\n","order = 4 #@param {type:\"integer\"}\n","config_dict.get('compute_angles_advanced').get('butterworth').update({'order':order})\n","cut_off_frequency = 4 #@param {type:\"integer\"}\n","config_dict.get('compute_angles_advanced').get('butterworth').update({'cut_off_frequency':cut_off_frequency})\n","\n","#@markdown
\n","\n","#@markdown - If Gaussian filter is chosen, specify its sigma kernel (in pixel):\n","sigma_kernel = 1 #@param {type:\"integer\"}\n","config_dict.get('compute_angles_advanced').get('gaussian').update({'sigma_kernel':sigma_kernel})\n","\n","#@markdown
\n","\n","#@markdown - If LOESS filter is chosen, specify the number of values used:\n","nb_values_used = 5 #@param {type:\"integer\"}\n","config_dict.get('compute_angles_advanced').get('loess').update({'nb_values_used':nb_values_used})\n","\n","#@markdown
\n","\n","#@markdown - If Median filter is chosen, specify its kernel size (in frames):\n","kernel_size = 3 #@param {type:\"integer\"}\n","config_dict.get('compute_angles_advanced').get('median').update({'kernel_size':kernel_size})"]},{"cell_type":"markdown","metadata":{"id":"-Yb9GNRJGN28"},"source":["## Run analysis"]},{"cell_type":"code","execution_count":null,"metadata":{"cellView":"form","id":"7yT2Zmq3nAju"},"outputs":[],"source":["#@markdown Let's go!\n","\n","import logging, logging.handlers\n","logging.basicConfig(format='%(message)s', level=logging.INFO, force=True,\n"," handlers = [logging.handlers.TimedRotatingFileHandler(os.path.join(result_path, 'logs.txt'), when='D', interval=7), logging.StreamHandler()]) \n","\n","from Sports2D import Sports2D\n","%cd $result_path\n","with open('Config.toml', \"w\") as config_file: toml.dump(config_dict, config_file)\n","Sports2D.detect_pose('Config.toml')\n","Sports2D.compute_angles('Config.toml')"]},{"cell_type":"markdown","metadata":{"id":"H0bzVORGpFmD"},"source":["# Retrieve results"]},{"cell_type":"markdown","metadata":{"id":"0avZJwEfStiT"},"source":["## Show video"]},{"cell_type":"code","execution_count":null,"metadata":{"cellView":"form","id":"nZ3Ud9zLgOoQ"},"outputs":[],"source":["#@markdown Display last uploaded video after analysis:\n","show_last = 'Yes' #@param [\"Yes\", \"No\"]\n","#@markdown If \"No\", please type in the video you wish to display (check left pane for a list of them):\n","show_video_name = 'input1_BODY_25B.mp4' #@param {type:\"string\"}\n","\n","def show_local_mp4_video(file_name, width=640, height=480):\n"," import io\n"," import base64\n"," from IPython.display import HTML\n"," video_encoded = base64.b64encode(io.open(file_name, 'rb').read())\n"," return HTML(data=''''''.format(width, height, video_encoded.decode('ascii')))\n","\n","if show_last == 'Yes':\n"," vid_to_show = os.path.join(result_path, os.path.splitext(video_name)[0] + '_' + pose_model + os.path.splitext(video_name)[1])\n"," if not os.path.isfile(vid_to_show):\n"," vid_to_show = os.path.join(result_path, os.path.splitext(video_name)[0] + '_' + pose_model + '.mp4')\n","else:\n"," vid_to_show = os.path.join(result_path, show_video_name)\n"," if not os.path.isfile(vid_to_show):\n"," vid_to_show = os.path.join(video_path, show_video_name)\n"," if not os.path.isfile(vid_to_show):\n"," vid_to_show = os.path.join('/content', show_video_name)\n"," if not os.path.isfile(vid_to_show):\n"," vid_to_show = show_video_name\n","print(vid_to_show)\n","\n","# Check that the codec can be read in Colab\n","codec = !ffprobe -v error -select_streams v:0 -show_entries stream=codec_name -of default=noprint_wrappers=1:nokey=1 $vid_to_show\n","if codec[0] == 'mpeg4':\n"," print('Codec not supported: converting video...')\n"," vid_converted = os.path.join('/content', os.path.basename(vid_to_show))\n"," !ffmpeg -i $vid_to_show $vid_converted -y -loglevel quiet\n"," vid_to_show = vid_converted\n","show_local_mp4_video(vid_to_show, width=640, height=480)"]},{"cell_type":"markdown","metadata":{"id":"vJijh-gK3Wvg"},"source":["## Plot angles"]},{"cell_type":"code","execution_count":null,"metadata":{"cellView":"form","id":"0cOwHHXoBrCz"},"outputs":[],"source":["import pandas as pd\n","import plotly.express as px\n","\n","#@markdown Display knee angle results of the first person detected in the last uploaded video:\n","plot_last = 'Yes' #@param [\"Yes\", \"No\"]\n","\n","#@markdown
\n","\n","#@markdown If \"No\", please type in the name of the csv file you wish to plot, either of positions or angles (check left pane for a list of them):\n","plot_csv_name = 'input1_BODY_25B_person0_angles.csv' #@param {type:\"string\"}\n","#@markdown And type in the names of the variables you are interested in:\n","plot_variable_names = \"['Right knee','Left knee']\" #@param {type:\"string\"}\n","\n","#@markdown
\n","\n","#@markdown Display table too:\n","display_table = 'Yes' #@param [\"Yes\", \"No\"]\n","\n","\n","if plot_last == 'Yes':\n"," csv_to_show = os.path.join(result_path, os.path.splitext(video_name)[0] + '_' + pose_model + '_angles.csv')\n"," if not os.path.isfile(csv_to_show):\n"," csv_to_show = os.path.join(result_path, os.path.splitext(video_name)[0] + '_' + pose_model + '_person0_angles.csv')\n"," if not os.path.isfile(csv_to_show):\n"," csv_to_show = os.path.join(result_path, os.path.splitext(video_name)[0] + '_converted' + pose_model + '_angles.csv')\n"," if not os.path.isfile(csv_to_show):\n"," csv_to_show = os.path.join(result_path, os.path.splitext(video_name)[0] + '_converted' + pose_model + '_person0_angles.csv')\n","else:\n"," csv_to_show = os.path.join(result_path, plot_csv_name)\n"," if not os.path.isfile(csv_to_show):\n"," csv_to_show = plot_csv_name\n","\n","# Retrieve csv results\n","table = pd.read_csv(csv_to_show, index_col=0, header=[0,1,2,3])\n","table = table.droplevel([0,1], axis=1)\n","try:\n"," eval(plot_variable_names)\n"," table_select = table[eval(plot_variable_names)]\n"," table_select.columns = [' '.join(col).strip() for col in table_select.columns.values]\n"," fig = px.line(data_frame=table_select, width=1310, height=699)\n"," fig.show()\n","except:\n"," pass\n","\n","if display_table=='Yes':\n"," display(table)"]},{"cell_type":"markdown","metadata":{"id":"Wd2Iw0NkNqGl"},"source":["## View in Google drive"]},{"cell_type":"code","execution_count":null,"metadata":{"cellView":"form","id":"AH232HTEcZ7f"},"outputs":[],"source":["#@markdown If you chose \"Install once\", results are already stored on your Google Drive.\\\n","#@markdown If you chose \"Install every time\", results will first be copied there.\n","\n","#@markdown `Warning:` If this leads you to a 404 error, run this cell again after a few seconds.\n","\n","# Buttons to link to Gdrive\n","import ipywidgets as widgets\n","from IPython.display import display, Javascript, clear_output, HTML\n","from google.colab import drive\n","\n","# Create button\n","def Gdrive_results_onbuttonclicked(url):\n"," with output:\n"," display(Javascript(f'window.open(\"{url.tooltip}\");'))\n","\n","if installation_type == 'Install every time':\n"," connect_to_Gdrive()\n","\n","os.chdir(result_path)\n","gdrive_result_id = !xattr -p 'user.drive.id' $result_path\n","gdrive_result_path = 'https://drive.google.com/drive/folders/' + gdrive_result_id[0]\n","button = widgets.Button(description=\"View in Google Drive\", tooltip=gdrive_result_path, button_style='info')\n","button.on_click(Gdrive_results_onbuttonclicked)\n","output = widgets.Output()\n","display(button, output)"]},{"cell_type":"markdown","metadata":{"id":"qp6AcxojSZ3F"},"source":["## Download results"]},{"cell_type":"code","execution_count":null,"metadata":{"cellView":"form","id":"02zkC4QCSWic"},"outputs":[],"source":["#@markdown Download all results. \\\n","#@markdown Alternatively, you can download them individually after finding them in the left pane.\n","\n","# Compress result folder\n","!zip -r /content/Sports2D_results.zip /content/Sports2D/Sports2D_results\n","\n","# Download zip file\n","from google.colab import files\n","files.download('/content/Sports2D_results.zip')"]}],"metadata":{"accelerator":"GPU","colab":{"collapsed_sections":["T6FyIv-MAxGf","g6JGcVET0wZ_","NGwcZYGdTrew","PRgCqkmr0ob3","Xh6MzePd1OYO","Zg-vLGpH1VWm"],"gpuType":"T4","provenance":[{"file_id":"https://github.com/davidpagnon/Sports2D/blob/main/Sports2D/Sports2D.ipynb","timestamp":1683923169803}]},"gpuClass":"standard","kernelspec":{"display_name":"Python 3","language":"python","name":"python3"},"language_info":{"name":"python","version":"3.7.3 (v3.7.3:ef4ec6ed12, Mar 25 2019, 22:22:05) [MSC v.1916 64 bit (AMD64)]"},"vscode":{"interpreter":{"hash":"0a54084e6b208ee8d1ce3989ffc20924477a5f55f5a43e22e699a6741623861e"}}},"nbformat":4,"nbformat_minor":0} +{"cells":[{"attachments":{},"cell_type":"markdown","metadata":{},"source":["\"Open"]},{"attachments":{},"cell_type":"markdown","metadata":{"id":"X38L6tanrnrB"},"source":["# Sports2D\n","\n","When seeking to optimize a sports movement, it is often useful to compute joint or segment angles. \\\n","**[Sports2D](https://github.com/davidpagnon/Sports2D)** offers a way to compute them from a video, on any platform, including your smartphone.\\\n","Run cells one after another by clicking on the ▶️ button.\n","\n","
\n","\n",""]},{"attachments":{},"cell_type":"markdown","metadata":{"id":"T6FyIv-MAxGf"},"source":["\n","## Additional information"]},{"attachments":{},"cell_type":"markdown","metadata":{"id":"ZGOSkQKqBwf8"},"source":["Please find more information about how it works under the hood [here](https://github.com/davidpagnon/Sports2D). \n","\n","While you're there, do not hesitate to hit the star button, I would appreciate the support! \\\n","If you use Sports2D, please cite:\n","\n"," @misc{Pagnon2023,\n"," author = {Pagnon, David},\n"," title = {Sports2D - Angles from video},\n"," year = {2023},\n"," doi= {10.5281/zenodo.7903963},\n"," publisher = {GitHub},\n"," journal = {GitHub repository},\n"," howpublished = {\\url{https://github.com/davidpagnon/Sports2D}},\n"," }\n","\n","
\n","\n","**Warning:**\n","- Results are only as good as the pose estimation algorithm, i.e., they are not perfect. Moreover, they will not be good if your video is blurry. Finally, they are acceptable only if the persons move in the 2D plane, from right to left or from left to right. If you need research-grade 3D markerless kinematics, consider using several cameras with [Pose2Sim](https://github.com/perfanalytics/pose2sim) instead.\n","- Your data will be sent to the Google servers, which do not follow the European GDPR requirements regarding privacy. If you don't want this, you can [run Sports 2D on your computer](https://github.com/davidpagnon/Sports2D) instead.\n","- The server disconnects after 90 minutes of idle time, and after a few hours in any case. Make sure you have downloaded your results beforehand.\n","- Unless you subscribed, there is a time limit on the usage of GPUs on Colab. However, these are only mandatory if you run OpenPose. If you run BlazePose, or if you want to analyze previously saved results, you can deactivate GPUs via `Runtime -> Change runtime type`\n"]},{"cell_type":"markdown","metadata":{"id":"4_cobxbdkBmb"},"source":["# Installation"]},{"cell_type":"code","execution_count":3,"metadata":{"cellView":"form","executionInfo":{"elapsed":248,"status":"ok","timestamp":1684614861687,"user":{"displayName":"David Pagnon","userId":"05832985921561105783"},"user_tz":-120},"id":"j5e0AeND0PfC"},"outputs":[],"source":["#@markdown You can choose to: \n","#@markdown - **Install just once**. Full install takes about 1.2 Go on your Google Drive (270 Mo for Python, 875 Mo for OpenPose),\\\n","#@markdown but next times the environment should be set up in about 2 minutes, including on your smartphone.\n","#@markdown - **Install every time**. Full install takes about 22 minute (2 minutes for Python, 20 minutes for OpenPose), \\\n","#@markdown but no connexion to Google Drive nor storage space is needed.\n","installation_type = 'Install once' #@param [\"Install once\", \"Install every time\"]\n","\n","#@markdown
\n","\n","#@markdown You can also choose to:\n","#@markdown - **Install OpenPose** if you need to go for accuracy and multi-person analysis.\n","#@markdown - **Not install OpenPose** for a much faster and lighter installation with BlazePose instead. \n","install_openpose = 'Install OpenPose' #@param [\"Install OpenPose\", \"Do not install OpenPose\"]\n","\n","import os, sys\n","from google.colab import files, drive\n","\n","def connect_to_Gdrive():\n"," print(\"Connecting to Google Drive...\")\n"," # Add GDrive notebooks to colab\n"," drive.mount('/content/drive')\n"," if not os.path.exists(sports2d_path):\n"," !mkdir -p '/content/drive/My Drive/Sports2D'\n"," else:\n"," print(f\"{sports2d_path} already exists.\")\n"," !cp -r $sports2d_path '/content/drive/My Drive'\n"," !rm -r $sports2d_path\n"," os.symlink('/content/drive/My Drive/Sports2D', sports2d_path)\n"," sys.path.insert(0, sports2d_path)\n","\n","def install_Sports2D_in_GDrive():\n"," print(\"Installing Sports2D in Google Drive...\")\n"," python_path = os.path.join(sports2d_path,'Sports2D_python')\n"," if not os.path.exists(python_path):\n"," # Install Python libraries in GDrive\n"," !PYTHONUSERBASE=$python_path pip install --user Sports2D\n"," else: \n"," print('Sports2D already installed.')\n"," # Add the GDrive python installation path to PYTHONPATH\n"," sys.path.append(os.path.join(python_path,\"lib/python3.10/site-packages\"))\n","\n","def install_openpose():\n"," print(\"Installing OpenPose in Google Drive...\")\n"," git_repo_url = 'https://github.com/CMU-Perceptual-Computing-Lab/openpose.git'\n"," if not os.path.exists(openpose_path):\n"," !mkdir -p $openpose_path\n"," # install new CMake because of CUDA10\n"," !wget -q https://cmake.org/files/v3.13/cmake-3.13.0-Linux-x86_64.tar.gz\n"," !tar xfz cmake-3.13.0-Linux-x86_64.tar.gz --strip-components=1 -C /usr/local\n"," !git clone $git_repo_url\n"," !sed -i 's/execute_process(COMMAND git checkout master WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}\\/3rdparty\\/caffe)/execute_process(COMMAND git checkout f019d0dfe86f49d1140961f8c7dec22130c83154 WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}\\/3rdparty\\/caffe)/g' openpose/CMakeLists.txt\n"," # install system dependencies\n"," !apt-get -qq install -y libatlas-base-dev libprotobuf-dev libleveldb-dev libsnappy-dev libhdf5-serial-dev protobuf-compiler libgflags-dev libgoogle-glog-dev liblmdb-dev opencl-headers ocl-icd-opencl-dev libviennacl-dev\n"," # build openpose\n"," !cd openpose && rm -rf build || true && mkdir build && cd build && cmake .. && make -j`nproc` \n"," # Install body_25b model\n"," !wget posefs1.perception.cs.cmu.edu/OpenPose/models/pose/1_25BBkg/body_25b/pose_iter_XXXXXX.caffemodel -P /content/openpose/models/pose/body_25b\n"," !wget https://raw.githubusercontent.com/CMU-Perceptual-Computing-Lab/openpose_train/master/experimental_models/1_25BBkg/body_25b/pose_deploy.prototxt -P /content/openpose/models/pose/body_25b \n"," # Get compatible CUDA and CuDNN versions in order to avoid `CudaSuccess (2 vs. 0) out of memory` error\n"," !apt -qq install --allow-change-held-packages libcudnn8=8.1.0.77-1+cuda11.2\n"," # Move openpose folder to GDrive\n"," !mv /content/openpose/* $openpose_path && rm -r /content/openpose\n"," !rm -rf /content/openpose\n"," !chmod 755 $openpose_path/build/examples/openpose/openpose.bin\n"," # Copy libraries in system path\n"," !cp $openpose_path/build/src/openpose/libopenpose.so.1.7.0 /usr/local/lib\n"," !cp $openpose_path/build/caffe/lib/libcaffe.so.1.0.0 /usr/local/lib\n"," !sudo ldconfig\n"," else:\n"," print(\"OpenPose already installed.\")\n"," print(\"Setting up the environment...\")\n"," # Allow execution of OpenPose\n"," !chmod 755 $openpose_path/build/examples/openpose/openpose.bin\n"," # Install system dependencies\n"," !apt-get -qq install -y libatlas-base-dev libprotobuf-dev libleveldb-dev libsnappy-dev libhdf5-serial-dev protobuf-compiler libgflags-dev libgoogle-glog-dev liblmdb-dev opencl-headers ocl-icd-opencl-dev libviennacl-dev &> /dev/null\n"," # Get compatible CUDA and CuDNN versions in order to avoid `CudaSuccess (2 vs. 0) out of memory` error\n"," !apt -qq install --allow-change-held-packages libcudnn8=8.1.0.77-1+cuda11.2 &> /dev/null\n"," # Copy libraries in system path\n"," !cp $openpose_path/build/src/openpose/libopenpose.so.1.7.0 /usr/local/lib\n"," !cp $openpose_path/build/caffe/lib/libcaffe.so.1.0.0 /usr/local/lib\n"," !sudo ldconfig\n","\n","sports2d_path = '/content/Sports2D'\n","python_path = os.path.join(sports2d_path,'Sports2D_python')\n","openpose_path = os.path.join(sports2d_path,'Sports2D_openpose')\n","video_path = os.path.join(sports2d_path,'Sports2D_videos')\n","result_path = os.path.join(sports2d_path,'Sports2D_results')\n","\n","# Use the right Python version\n","!sudo update-alternatives --install /usr/bin/python3 python3 /usr/bin/python3.10 1\n","# GDrive tools\n","!apt-get -qq install xattr &> /dev/null\n","\n","if installation_type == \"Install every time\":\n"," print('Installing Sports2D')\n"," !pip install sports2D\n"," if install_openpose == 'Install OpenPose':\n"," install_openpose()\n","\n","elif installation_type == \"Install once\":\n"," connect_to_Gdrive()\n"," install_Sports2D_in_GDrive()\n"," if install_openpose == 'Install OpenPose':\n"," install_openpose()\n","\n","print(\"Done.\")"]},{"attachments":{},"cell_type":"markdown","metadata":{"id":"gv5lSY_pZx8I"},"source":["# Upload videos"]},{"cell_type":"code","execution_count":null,"metadata":{"cellView":"form","id":"5dQqA5E6YeUS"},"outputs":[],"source":["#@markdown Upload your videos if needed, and convert them if needed.\\\n","#@markdown **Note:** As this takes time, you should probably try to trim your videos beforehand.\n","\n","def upload_videos():\n"," if not os.path.exists(video_path):\n"," !mkdir -p $video_path\n"," %cd $video_path\n"," uploaded = files.upload()\n","\n","def convert_vertical_iphone_vids(vids):\n"," # Pose algorithms do not see the rotation in certain videos taken in portrait mode. Convert them\n"," for i, vid in enumerate(vids):\n"," iphone_portrait = !ffprobe -loglevel error -select_streams v:0 -show_entries stream_tags=rotate -of default=nw=1:nk=1 -i $vid\n"," if iphone_portrait:\n"," vid_out = os.path.splitext(vid)[0]+'_converted'+os.path.splitext(vid)[1]\n"," print(f'{vid} is rotated. Converting it to {vid_out}...')\n"," !ffmpeg -i $vid $vid_out -loglevel quiet\n"," vids[i] = vid_out\n"," os.remove(vid)\n","\n","# upload videos\n","upload_videos()\n","\n","# convert them if needed\n","vids = os.listdir(video_path)\n","vids = [f for f in vids if not f.startswith('.') and f!='logs.txt']\n","convert_vertical_iphone_vids(vids)\n","\n","# list them\n","vids = os.listdir(video_path)\n","vids = [f for f in vids if not f.startswith('.')]\n","print(f'{vids} uploaded and ready to be processed.')"]},{"cell_type":"markdown","metadata":{"id":"n5L3Z5YVrZ2R"},"source":["# Run Sports2D\n"]},{"cell_type":"markdown","metadata":{"id":"3A8t09re07Vu"},"source":["## Specify video to analyze\n"]},{"cell_type":"code","execution_count":null,"metadata":{"cellView":"form","id":"66aTPLL1jAlZ"},"outputs":[],"source":["import toml\n","\n","# Retrieve Sports2D Config file\n","if not os.path.exists(result_path):\n"," !mkdir -p $result_path\n","\n","if installation_type == 'Install every time':\n"," !cp /usr/local/lib/python3.10/dist-packages/Sports2D/Demo/Config_demo.toml $result_path/Config.toml\n","if installation_type == 'Install once':\n"," !cp $python_path/lib/python3.10/site-packages/Sports2D/Demo/Config_demo.toml $result_path/Config.toml\n","%cd $result_path\n","config_dict = toml.load('Config.toml')\n","\n","video_name = 'input1.mp4' #@param {type:\"string\"}\n","video_name = os.path.basename(video_name)\n","pose_model = 'BODY_25B'\n","config_dict.get('project').update({'video_dir':video_path})\n","config_dict.get('project').update({'video_file':video_name})\n","config_dict.get('project').update({'result_dir':result_path})\n","config_dict.get('pose').update({'pose_algo':'OPENPOSE'})\n","config_dict.get('pose').get('OPENPOSE').update({'openpose_model':'BODY_25B'})\n","config_dict.get('pose').get('OPENPOSE').update({'openpose_path':openpose_path})\n","\n","video_full_path = os.path.join(video_path, video_name)\n","if os.path.isfile(video_full_path)==True: \n"," print(f'Video found at {video_full_path}')\n","else: \n"," print(f'Video not found at {video_full_path}')"]},{"cell_type":"markdown","metadata":{"id":"g6JGcVET0wZ_"},"source":["## Settings (optional)"]},{"cell_type":"markdown","metadata":{"id":"NGwcZYGdTrew"},"source":["### Pose settings"]},{"cell_type":"code","execution_count":3,"metadata":{"cellView":"form","executionInfo":{"elapsed":232,"status":"ok","timestamp":1684577247028,"user":{"displayName":"David Pagnon","userId":"05832985921561105783"},"user_tz":-120},"id":"iMQvTam9mWzc"},"outputs":[],"source":["#@markdown **OpenPose** supports multi-person analysis and is more accurate, but it requires a heavier install than **BlazePose**:\n","pose_algo = 'OpenPose' #@param [\"OpenPose\", \"BlazePose\"]\n","\n","#@markdown
\n","\n","#@markdown - **If OpenPose** algorithm is chosen.\\ \n","#@markdown **BODY_25** is the standard model, but **BODY_25B** is more accurate:\n","if pose_algo == 'OpenPose':\n"," if install_openpose != 'Install OpenPose':\n"," print('WARNING: OpenPose not installed!')\n"," else:\n"," pose_model = 'BODY_25B' #@param [\"BODY_25B\", \"BODY_25\"]\n"," config_dict.get('pose').update({'pose_algo':'OPENPOSE'})\n"," config_dict.get('pose').get('OPENPOSE').update({'openpose_model':pose_model})\n"," \n","#@markdown
\n","\n","#@markdown - **If BlazePose** is chosen.\n","if pose_algo == 'BlazePose':\n"," model_complexity = 'Most accurate' #@param [\"Most accurate\", \"Default\", \"Fastest\"]\n"," model_complexity_dict = {'Most accurate':2, 'Default': 1, 'Fastest': 0}\n"," model_complexity_nb = model_complexity_dict[model_complexity]\n"," pose_model = 'BLAZEPOSE'\n"," \n"," config_dict.get('pose').update({'pose_algo':'BLAZEPOSE'})\n"," config_dict.get('pose').get('BLAZEPOSE').update({'model_complexity':model_complexity_nb})"]},{"cell_type":"markdown","metadata":{"id":"PRgCqkmr0ob3"},"source":["### Angle settings"]},{"cell_type":"code","execution_count":null,"metadata":{"cellView":"form","id":"tDcG9TK8kiPJ"},"outputs":[],"source":["#@markdown - Select **joint angles** among:\n","right_ankle = True #@param {type:\"boolean\"}\n","left_ankle = True #@param {type:\"boolean\"}\n","right_knee = True #@param {type:\"boolean\"}\n","left_knee = True #@param {type:\"boolean\"}\n","right_hip = True #@param {type:\"boolean\"}\n","left_hip = True #@param {type:\"boolean\"}\n","right_shoulder = True #@param {type:\"boolean\"}\n","left_shoulder = True #@param {type:\"boolean\"}\n","right_elbow = True #@param {type:\"boolean\"}\n","left_elbow = True #@param {type:\"boolean\"}\n","\n","joint_angles_var = [right_ankle, left_ankle, right_knee, left_knee, right_hip, left_hip, right_shoulder, left_shoulder, right_elbow, left_elbow]\n","joint_angles_dict = {'Right ankle':right_ankle, 'Left ankle': left_ankle, 'Right knee':right_knee, 'Left knee':left_knee, 'Right hip':right_hip, 'Left hip':left_hip, 'Right shoulder':right_shoulder, 'Left shoulder':left_shoulder, 'Right elbow':right_elbow, 'Left elbow':left_elbow}\n","joint_angles_val = [key for key in joint_angles_dict.keys() if joint_angles_dict[key]]\n","\n","config_dict.get('compute_angles').update({'joint_angles':joint_angles_val})\n","\n","#@markdown
\n","\n","#@markdown - Select **segment angles** among:\n","right_foot = True #@param {type:\"boolean\"}\n","left_foot = True #@param {type:\"boolean\"}\n","right_shank = True #@param {type:\"boolean\"}\n","left_shank = True #@param {type:\"boolean\"}\n","right_thigh = True #@param {type:\"boolean\"}\n","left_thigh = True #@param {type:\"boolean\"}\n","trunk = True #@param {type:\"boolean\"}\n","right_arm = True #@param {type:\"boolean\"}\n","left_arm = True #@param {type:\"boolean\"}\n","right_forearm = True #@param {type:\"boolean\"}\n","left_forearm = True #@param {type:\"boolean\"}\n","\n","segment_angles_var = [right_foot, left_foot, right_shank, left_shank, right_thigh, left_thigh, trunk, right_arm, left_arm, right_forearm, left_forearm]\n","segment_angles_dict = {'Right foot':right_foot, 'Left foot': left_foot, 'Right shank':right_shank, 'Left shank':left_shank, 'Right thigh':right_thigh, 'Left thigh':left_thigh, 'Trunk':trunk, 'Right arm':right_arm, 'Left arm':left_arm, 'Right forearm':right_forearm, 'Left forearm':left_forearm}\n","segment_angles_val = [key for key in segment_angles_dict.keys() if segment_angles_dict[key]]\n","\n","config_dict.get('compute_angles').update({'segment_angles':segment_angles_val})"]},{"attachments":{},"cell_type":"markdown","metadata":{"id":"Xh6MzePd1OYO"},"source":["### Advanced pose settings"]},{"cell_type":"code","execution_count":null,"metadata":{"cellView":"form","id":"HAyWZGC9EUGw"},"outputs":[],"source":["#@markdown Do not run pose estimation again if files already exist:\n","load_pose = True #@param {type:\"boolean\"}\n","config_dict.get('pose_advanced').update({'load_pose':load_pose})\n","\n","#@markdown
\n","\n","#@markdown Choose whether to save images and video of pose estimation:\n","save_img = True #@param {type:\"boolean\"}\n","save_vid = True #@param {type:\"boolean\"}\n","config_dict.get('pose_advanced').update({'save_img':save_img})\n","config_dict.get('pose_advanced').update({'save_vid':save_vid})\n","\n","#@markdown
\n","\n","#@markdown Interpolate gaps only if they are smaller than XX frames:\n","interp_gap_smaller_than = 5 #@param {type:\"slider\", min:0, max:100, step:1}\n","\n","#@markdown
\n","\n","#@markdown Choose whether to filter results:\n","filter = True #@param {type:\"boolean\"}\n","filter_type = 'butterworth' #@param [\"butterworth\", \"gaussian\", \"LOESS\", \"median\"]\n","config_dict.get('pose_advanced').update({'filter':filter})\n","config_dict.get('pose_advanced').update({'filter_type':filter_type})\n","\n","#@markdown
\n","\n","#@markdown - If Butterworth filter is chosen, specify its order and cut-off frequency (in Hz):\n","order = 4 #@param {type:\"integer\"}\n","config_dict.get('pose_advanced').get('butterworth').update({'order':order})\n","cut_off_frequency = 4 #@param {type:\"integer\"}\n","config_dict.get('pose_advanced').get('butterworth').update({'cut_off_frequency':cut_off_frequency})\n","\n","#@markdown
\n","\n","#@markdown - If Gaussian filter is chosen, specify its sigma kernel (in pixel):\n","sigma_kernel = 1 #@param {type:\"integer\"}\n","config_dict.get('pose_advanced').get('gaussian').update({'sigma_kernel':sigma_kernel})\n","\n","#@markdown
\n","\n","#@markdown - If LOESS filter is chosen, specify the number of values used:\n","nb_values_used = 5 #@param {type:\"integer\"}\n","config_dict.get('pose_advanced').get('loess').update({'nb_values_used':nb_values_used})\n","\n","#@markdown
\n","\n","#@markdown - If Median filter is chosen, specify its kernel size (in frames):\n","kernel_size = 3 #@param {type:\"integer\"}\n","config_dict.get('pose_advanced').get('median').update({'kernel_size':kernel_size})"]},{"attachments":{},"cell_type":"markdown","metadata":{"id":"Zg-vLGpH1VWm"},"source":["### Advanced angle settings"]},{"cell_type":"code","execution_count":null,"metadata":{"cellView":"form","id":"HQNCNoYJGIDg"},"outputs":[],"source":["#@markdown Choose whether to save images and video of angle estimation:\n","save_img = True #@param {type:\"boolean\"}\n","save_vid = True #@param {type:\"boolean\"}\n","config_dict.get('compute_angles_advanced').update({'save_img':save_img})\n","config_dict.get('compute_angles_advanced').update({'save_vid':save_vid})\n","\n","#@markdown
\n","\n","#@markdown Choose whether to filter results:\n","filter = False #@param {type:\"boolean\"}\n","filter_type = 'butterworth' #@param [\"butterworth\", \"gaussian\", \"LOESS\", \"median\"]\n","config_dict.get('compute_angles_advanced').update({'filter':filter})\n","config_dict.get('compute_angles_advanced').update({'filter_type':filter_type})\n","\n","#@markdown
\n","\n","#@markdown - If Butterworth filter is chosen, specify its order and cut-off frequency (in Hz):\n","order = 4 #@param {type:\"integer\"}\n","config_dict.get('compute_angles_advanced').get('butterworth').update({'order':order})\n","cut_off_frequency = 4 #@param {type:\"integer\"}\n","config_dict.get('compute_angles_advanced').get('butterworth').update({'cut_off_frequency':cut_off_frequency})\n","\n","#@markdown
\n","\n","#@markdown - If Gaussian filter is chosen, specify its sigma kernel (in pixel):\n","sigma_kernel = 1 #@param {type:\"integer\"}\n","config_dict.get('compute_angles_advanced').get('gaussian').update({'sigma_kernel':sigma_kernel})\n","\n","#@markdown
\n","\n","#@markdown - If LOESS filter is chosen, specify the number of values used:\n","nb_values_used = 5 #@param {type:\"integer\"}\n","config_dict.get('compute_angles_advanced').get('loess').update({'nb_values_used':nb_values_used})\n","\n","#@markdown
\n","\n","#@markdown - If Median filter is chosen, specify its kernel size (in frames):\n","kernel_size = 3 #@param {type:\"integer\"}\n","config_dict.get('compute_angles_advanced').get('median').update({'kernel_size':kernel_size})"]},{"cell_type":"markdown","metadata":{"id":"-Yb9GNRJGN28"},"source":["## Run analysis"]},{"cell_type":"code","execution_count":null,"metadata":{"cellView":"form","id":"7yT2Zmq3nAju"},"outputs":[],"source":["#@markdown Let's go!\n","\n","import logging, logging.handlers\n","logging.basicConfig(format='%(message)s', level=logging.INFO, force=True,\n"," handlers = [logging.handlers.TimedRotatingFileHandler(os.path.join(result_path, 'logs.txt'), when='D', interval=7), logging.StreamHandler()]) \n","\n","from Sports2D import Sports2D\n","%cd $result_path\n","with open('Config.toml', \"w\") as config_file: toml.dump(config_dict, config_file)\n","Sports2D.detect_pose('Config.toml')\n","Sports2D.compute_angles('Config.toml')"]},{"cell_type":"markdown","metadata":{"id":"H0bzVORGpFmD"},"source":["# Retrieve results"]},{"cell_type":"markdown","metadata":{"id":"0avZJwEfStiT"},"source":["## Show video"]},{"cell_type":"code","execution_count":null,"metadata":{"cellView":"form","id":"nZ3Ud9zLgOoQ"},"outputs":[],"source":["#@markdown Display last uploaded video after analysis:\n","show_last = 'Yes' #@param [\"Yes\", \"No\"]\n","#@markdown If \"No\", please type in the video you wish to display (check left pane for a list of them):\n","show_video_name = 'input1_BODY_25B.mp4' #@param {type:\"string\"}\n","\n","def show_local_mp4_video(file_name, width=640, height=480):\n"," import io\n"," import base64\n"," from IPython.display import HTML\n"," video_encoded = base64.b64encode(io.open(file_name, 'rb').read())\n"," return HTML(data=''''''.format(width, height, video_encoded.decode('ascii')))\n","\n","if show_last == 'Yes':\n"," vid_to_show = os.path.join(result_path, os.path.splitext(video_name)[0] + '_' + pose_model + os.path.splitext(video_name)[1])\n"," if not os.path.isfile(vid_to_show):\n"," vid_to_show = os.path.join(result_path, os.path.splitext(video_name)[0] + '_' + pose_model + '.mp4')\n","else:\n"," vid_to_show = os.path.join(result_path, show_video_name)\n"," if not os.path.isfile(vid_to_show):\n"," vid_to_show = os.path.join(video_path, show_video_name)\n"," if not os.path.isfile(vid_to_show):\n"," vid_to_show = os.path.join('/content', show_video_name)\n"," if not os.path.isfile(vid_to_show):\n"," vid_to_show = show_video_name\n","print(vid_to_show)\n","\n","# Check that the codec can be read in Colab\n","codec = !ffprobe -v error -select_streams v:0 -show_entries stream=codec_name -of default=noprint_wrappers=1:nokey=1 $vid_to_show\n","if codec[0] == 'mpeg4':\n"," print('Codec not supported: converting video...')\n"," vid_converted = os.path.join('/content', os.path.basename(vid_to_show))\n"," !ffmpeg -i $vid_to_show $vid_converted -y -loglevel quiet\n"," vid_to_show = vid_converted\n","show_local_mp4_video(vid_to_show, width=640, height=480)"]},{"cell_type":"markdown","metadata":{"id":"vJijh-gK3Wvg"},"source":["## Plot angles"]},{"cell_type":"code","execution_count":null,"metadata":{"cellView":"form","id":"0cOwHHXoBrCz"},"outputs":[],"source":["import pandas as pd\n","import plotly.express as px\n","\n","#@markdown Display knee angle results of the first person detected in the last uploaded video:\n","plot_last = 'Yes' #@param [\"Yes\", \"No\"]\n","\n","#@markdown
\n","\n","#@markdown If \"No\", please type in the name of the csv file you wish to plot, either of positions or angles (check left pane for a list of them):\n","plot_csv_name = 'input1_BODY_25B_person0_angles.csv' #@param {type:\"string\"}\n","#@markdown And type in the names of the variables you are interested in:\n","plot_variable_names = \"['Right knee','Left knee']\" #@param {type:\"string\"}\n","\n","#@markdown
\n","\n","#@markdown Display table too:\n","display_table = 'Yes' #@param [\"Yes\", \"No\"]\n","\n","\n","if plot_last == 'Yes':\n"," csv_to_show = os.path.join(result_path, os.path.splitext(video_name)[0] + '_' + pose_model + '_angles.csv')\n"," if not os.path.isfile(csv_to_show):\n"," csv_to_show = os.path.join(result_path, os.path.splitext(video_name)[0] + '_' + pose_model + '_person0_angles.csv')\n"," if not os.path.isfile(csv_to_show):\n"," csv_to_show = os.path.join(result_path, os.path.splitext(video_name)[0] + '_converted' + pose_model + '_angles.csv')\n"," if not os.path.isfile(csv_to_show):\n"," csv_to_show = os.path.join(result_path, os.path.splitext(video_name)[0] + '_converted' + pose_model + '_person0_angles.csv')\n","else:\n"," csv_to_show = os.path.join(result_path, plot_csv_name)\n"," if not os.path.isfile(csv_to_show):\n"," csv_to_show = plot_csv_name\n","\n","# Retrieve csv results\n","table = pd.read_csv(csv_to_show, index_col=0, header=[0,1,2,3])\n","table = table.droplevel([0,1], axis=1)\n","try:\n"," plot_variable_names = eval(plot_variable_names)\n"," plot_variable_names = ['Time']+plot_variable_names\n"," table_select = table[plot_variable_names]\n"," table_select.columns = [' '.join(col).strip() for col in table_select.columns.values]\n"," fig = px.line(data_frame=table_select, x=table_select.iloc[:,0], y=table_select.iloc[:,1:], width=1310, height=699)\n"," fig.show()\n","except:\n"," print('Variables could not be found in csv file')\n","\n","if display_table=='Yes':\n"," display(table)"]},{"cell_type":"markdown","metadata":{"id":"Wd2Iw0NkNqGl"},"source":["## View in Google drive"]},{"cell_type":"code","execution_count":null,"metadata":{"cellView":"form","id":"AH232HTEcZ7f"},"outputs":[],"source":["#@markdown If you chose \"Install once\", results are already stored on your Google Drive.\\\n","#@markdown If you chose \"Install every time\", results will first be copied there.\n","\n","#@markdown `Warning:` If this leads you to a 404 error, run this cell again after a few seconds.\n","\n","# Buttons to link to Gdrive\n","import ipywidgets as widgets\n","from IPython.display import display, Javascript, clear_output, HTML\n","from google.colab import drive\n","\n","# Create button\n","def Gdrive_results_onbuttonclicked(url):\n"," with output:\n"," display(Javascript(f'window.open(\"{url.tooltip}\");'))\n","\n","if installation_type == 'Install every time':\n"," connect_to_Gdrive()\n","\n","os.chdir(result_path)\n","gdrive_result_id = !xattr -p 'user.drive.id' $result_path\n","gdrive_result_path = 'https://drive.google.com/drive/folders/' + gdrive_result_id[0]\n","button = widgets.Button(description=\"View in Google Drive\", tooltip=gdrive_result_path, button_style='info')\n","button.on_click(Gdrive_results_onbuttonclicked)\n","output = widgets.Output()\n","display(button, output)"]},{"cell_type":"markdown","metadata":{"id":"qp6AcxojSZ3F"},"source":["## Download results"]},{"cell_type":"code","execution_count":null,"metadata":{"cellView":"form","id":"02zkC4QCSWic"},"outputs":[],"source":["#@markdown Download all results. \\\n","#@markdown Alternatively, you can download them individually after finding them in the left pane.\n","\n","# Compress result folder\n","!zip -r /content/Sports2D_results.zip /content/Sports2D/Sports2D_results\n","\n","# Download zip file\n","from google.colab import files\n","files.download('/content/Sports2D_results.zip')"]}],"metadata":{"accelerator":"GPU","colab":{"collapsed_sections":["T6FyIv-MAxGf","g6JGcVET0wZ_","NGwcZYGdTrew","PRgCqkmr0ob3","Xh6MzePd1OYO","Zg-vLGpH1VWm"],"gpuType":"T4","provenance":[{"file_id":"https://github.com/davidpagnon/Sports2D/blob/main/Sports2D/Sports2D.ipynb","timestamp":1683923169803}]},"gpuClass":"standard","kernelspec":{"display_name":"Python 3","language":"python","name":"python3"},"language_info":{"name":"python","version":"3.7.3 (v3.7.3:ef4ec6ed12, Mar 25 2019, 22:22:05) [MSC v.1916 64 bit (AMD64)]"},"vscode":{"interpreter":{"hash":"0a54084e6b208ee8d1ce3989ffc20924477a5f55f5a43e22e699a6741623861e"}}},"nbformat":4,"nbformat_minor":0} diff --git a/Sports2D/Sports2D.py b/Sports2D/Sports2D.py index f025adf..85a0313 100644 --- a/Sports2D/Sports2D.py +++ b/Sports2D/Sports2D.py @@ -145,7 +145,7 @@ def base_params(config_dict): try: 1/frame_rate except ZeroDivisionError: - print(f'Frame rate of {str(video_dir / video_file)} could not be retrieved: check that it exists at the correct path') + print('Frame rate could not be retrieved: check that your video exists at the correct path') raise video.release() diff --git a/Sports2D/Utilities/Blazepose_runsave.py b/Sports2D/Utilities/Blazepose_runsave.py index e5b617d..620375e 100644 --- a/Sports2D/Utilities/Blazepose_runsave.py +++ b/Sports2D/Utilities/Blazepose_runsave.py @@ -49,7 +49,7 @@ ## FUNCTIONS -def save_to_csv_or_h5(kpt_list, output_folder, video_name, to_csv, to_h5): +def save_to_csv_or_h5(kpt_list, fps, output_folder, video_name, to_csv, to_h5): ''' Saves blazepose keypoint coordinates to csv or h5 file, in the DeepLabCut format. @@ -66,15 +66,16 @@ def save_to_csv_or_h5(kpt_list, output_folder, video_name, to_csv, to_h5): ''' # Prepare dataframe file - scorer = ['DavidPagnon']*len(mp_pose.PoseLandmark)*3 - individuals = ['person']*len(mp_pose.PoseLandmark)*3 + scorer = ['DavidPagnon']*(len(mp_pose.PoseLandmark)*3+1) + individuals = ['person']*(len(mp_pose.PoseLandmark)*3+1) bodyparts = [[p]*3 for p in ['Nose', 'LEyeInner', 'LEye', 'LEyeOuter', 'REyeInner', 'REye', 'REyeOuter', 'LEar', 'REar', 'LMouth', 'RMouth', 'LShoulder', 'RShoulder', 'LElbow', 'RElbow', 'LWrist', 'RWrist', 'LPinky', 'RPinky', 'LIndex', 'RIndex', 'LThumb', 'RThumb', 'LHip', 'RHip', 'LKnee', 'RKnee', 'LAnkle', 'RAnkle', 'LHeel', 'RHeel', 'LBigToe', 'RBigToe']] - bodyparts = [item for sublist in bodyparts for item in sublist] - coords = ['x', 'y', 'likelihood']*len(mp_pose.PoseLandmark) - + bodyparts = ['Times'] + [item for sublist in bodyparts for item in sublist] + coords = ['seconds'] + ['x', 'y', 'likelihood']*len(mp_pose.PoseLandmark) tuples = list(zip(scorer, individuals, bodyparts, coords)) index_csv = pd.MultiIndex.from_tuples(tuples, names=['scorer', 'individuals', 'bodyparts', 'coords']) - df = pd.DataFrame(np.array(kpt_list).T, index=index_csv).T + + time = np.expand_dims( np.arange(0,len(kpt_list)/fps, 1/fps), axis=0 ) + df = pd.DataFrame(np.concatenate(( time, np.array(kpt_list).T)), index=index_csv).T if to_csv: csv_file = os.path.join(output_folder, video_name+'_BLAZEPOSE_points.csv') @@ -229,7 +230,7 @@ def blazepose_detec_func(**args): # Save coordinates if to_csv or to_h5: - save_to_csv_or_h5(kpt_list, output_folder, video_name, to_csv, to_h5) + save_to_csv_or_h5(kpt_list, fps, output_folder, video_name, to_csv, to_h5) if to_json: save_to_json(kpt_list, output_folder, video_name) diff --git a/Sports2D/compute_angles.py b/Sports2D/compute_angles.py index 7ceb8ce..57bfe79 100644 --- a/Sports2D/compute_angles.py +++ b/Sports2D/compute_angles.py @@ -123,15 +123,17 @@ def display_figures_fun(df_list): - matplotlib window with tabbed figures for each angle ''' - angle_names = df_list[0].columns.get_level_values(2) + angle_names = df_list[0].iloc[:,1:].columns.get_level_values(2) + time = df_list[0].iloc[:,0] pw = common.plotWindow() for id, angle in enumerate(angle_names): # angles f = plt.figure() plt.plot() - [plt.plot(df_list[0].index, df.iloc[:,id], label=['unfiltered' if i==0 else 'filtered' if i==1 else ''][0]) for i,df in enumerate(df_list)] - plt.ylabel(angle) # nom angle + [plt.plot(time, df.iloc[:,id+1], label=['unfiltered' if i==0 else 'filtered' if i==1 else ''][0]) for i,df in enumerate(df_list)] + plt.xlabel('Time (seconds)') + plt.ylabel(angle) plt.legend() pw.addPlot(angle, f) @@ -187,7 +189,7 @@ def flip_left_right_direction(df_points): righ_orientation = df_points.iloc[:,df_points.columns.get_level_values(2)=='RBigToe'].iloc[:,0] - df_points.iloc[:,df_points.columns.get_level_values(2)=='RHeel'].iloc[:,0] left_orientation = df_points.iloc[:,df_points.columns.get_level_values(2)=='LBigToe'].iloc[:,0] - df_points.iloc[:,df_points.columns.get_level_values(2)=='LHeel'].iloc[:,0] orientation = righ_orientation + left_orientation - df_points.iloc[:,1::3] = df_points.iloc[:,1::3] * np.where(orientation>=0, 1, -1).reshape(-1,1) + df_points.iloc[:,2::3] = df_points.iloc[:,2::3] * np.where(orientation>=0, 1, -1).reshape(-1,1) return df_points @@ -217,7 +219,6 @@ def joint_angles_series_from_points(df_points, angle_params): ang_series = np.where(ang_series>180,ang_series-360,ang_series) ang_series = np.where((ang_series==0) | (ang_series==90) | (ang_series==180), +0, ang_series) - return ang_series @@ -387,17 +388,18 @@ def compute_angles_fun(config_dict): df_angles_list = [] for i, c in enumerate(csv_paths): # Prepare angle csv header - scorer = ['DavidPagnon']*angle_nb - individuals = [f'person{i}']*angle_nb - angs = joint_angles + segment_angles - coords = [joint_angle_dict.get(j)[1] for j in joint_angles] + [segment_angle_dict.get(s)[1] for s in segment_angles] + scorer = ['DavidPagnon']*(angle_nb+1) + individuals = [f'person{i}']*(angle_nb+1) + angs = ['Time'] + joint_angles + segment_angles + coords = ['seconds'] + [joint_angle_dict.get(j)[1] for j in joint_angles] + [segment_angle_dict.get(s)[1] for s in segment_angles] tuples = list(zip(scorer, individuals, angs, coords)) - index_angs_csv = pd.MultiIndex.from_tuples(tuples, names=['scorer', 'individuals', 'joints', 'angles']) + index_angs_csv = pd.MultiIndex.from_tuples(tuples, names=['scorer', 'individuals', 'angs', 'coords']) # Compute angles for each person, for each angle, with each required keypoint position logging.info(f'Person {i}: Computing 2D joint and segment angles.') with open(c) as c_f: df_points = pd.read_csv(c_f, header=[0,1,2,3]) + time = [np.array(df_points.iloc[:,1])] # Flip along x when feet oriented to the left df_points = flip_left_right_direction(df_points) @@ -416,7 +418,7 @@ def compute_angles_fun(config_dict): s_ang_series = segment_angles_series_from_points(df_points, angle_params, s) segment_angle_series += [s_ang_series] - angle_series = joint_angle_series + segment_angle_series + angle_series = time + joint_angle_series + segment_angle_series df_angles = [] df_angles += [pd.DataFrame(angle_series, index=index_angs_csv).T] @@ -464,7 +466,7 @@ def compute_angles_fun(config_dict): writer = cv2.VideoWriter(str(video_pose2), fourcc, fps, (int(W), int(H))) # Preferentially from pose image files - frames_img = sorted(list(img_pose.glob('*.png'))) + frames_img = list(img_pose.glob('*')) if len(frames_img)>0: for frame_nb in range(df_angles_list[0].shape[0]): df_angles_list_frame = [df_angles_list[n].iloc[frame_nb,:] for n in range(len(df_angles_list))] @@ -500,4 +502,4 @@ def compute_angles_fun(config_dict): os.rename(video_pose2,video_pose) if Path.exists(video_pose2): os.remove(video_pose2) - logging.info(f'Done.') + logging.info(f'Done.') \ No newline at end of file diff --git a/Sports2D/detect_pose.py b/Sports2D/detect_pose.py index 8619549..1792bc2 100644 --- a/Sports2D/detect_pose.py +++ b/Sports2D/detect_pose.py @@ -91,14 +91,14 @@ def display_figures_fun(df_list): f = plt.figure() axX = plt.subplot(211) - [plt.plot(df_list[0].index, df.iloc[:,id*3], label=['unfiltered' if i==0 else 'filtered' if i==1 else ''][0]) for i,df in enumerate(df_list)] + [plt.plot(df.iloc[:,0], df.iloc[:,id*3+1], label=['unfiltered' if i==0 else 'filtered' if i==1 else ''][0]) for i,df in enumerate(df_list)] plt.setp(axX.get_xticklabels(), visible=False) axX.set_ylabel(keypoint+' X') plt.legend() axY = plt.subplot(212) - [plt.plot(df_list[0].index, df.iloc[:,id*3+1]) for df in df_list] - plt.setp(axY.get_xticklabels(), visible=False) + [plt.plot(df.iloc[:,0], df.iloc[:,id*3+2]) for df in df_list] + axY.set_xlabel('Time (seconds)') axY.set_ylabel(keypoint+' Y') pw.addPlot(keypoint, f) @@ -263,7 +263,7 @@ def sort_people(keyptpre, keypt, nb_persons_to_detect): return keypt -def json_to_csv(json_path, pose_model, interp_gap_smaller_than, filter_options, show_plots): +def json_to_csv(json_path, frame_rate, pose_model, interp_gap_smaller_than, filter_options, show_plots): ''' Converts frame-by-frame json coordinate files to one csv files per detected person @@ -288,7 +288,7 @@ def json_to_csv(json_path, pose_model, interp_gap_smaller_than, filter_options, # Retrieve coordinates logging.info('Sorting people across frames.') - json_fnames = sorted(list(json_path.glob('*.json'))) + json_fnames = list(json_path.glob('*.json')) nb_persons_to_detect = max([len(json.load(open(json_fname))['people']) for json_fname in json_fnames]) Coords = [np.array([]).reshape(0,keypoints_nb*3)] * nb_persons_to_detect for json_fname in json_fnames: # for each frame @@ -316,18 +316,22 @@ def json_to_csv(json_path, pose_model, interp_gap_smaller_than, filter_options, # Inject coordinates in dataframes and save for i in range(nb_persons_to_detect): # Prepare csv header - scorer = ['DavidPagnon']*keypoints_nb*3 - individuals = [f'person{i}']*keypoints_nb*3 + scorer = ['DavidPagnon']*(keypoints_nb*3+1) + individuals = [f'person{i}']*(keypoints_nb*3+1) bodyparts = [[p]*3 for p in keypoints_names_rearranged] - bodyparts = [item for sublist in bodyparts for item in sublist] - coords = ['x', 'y', 'likelihood']*keypoints_nb + bodyparts = ['Time']+[item for sublist in bodyparts for item in sublist] + coords = ['seconds']+['x', 'y', 'likelihood']*keypoints_nb tuples = list(zip(scorer, individuals, bodyparts, coords)) index_csv = pd.MultiIndex.from_tuples(tuples, names=['scorer', 'individuals', 'bodyparts', 'coords']) - + + # Create dataframe + df_list=[] + time = np.expand_dims( np.arange(0,len(Coords[i])/frame_rate, 1/frame_rate), axis=0 ) + time_coords = np.concatenate(( time, Coords[i].T )) + df_list += [pd.DataFrame(time_coords, index=index_csv).T] + # Interpolate logging.info(f'Person {i}: Interpolating missing sequences if they are smaller than {interp_gap_smaller_than} frames.') - df_list=[] - df_list += [pd.DataFrame(Coords[i].T, index=index_csv).T] df_list[0] = df_list[0].apply(common.interpolate_zeros_nans, axis=0, args = [interp_gap_smaller_than, 'linear']) # Filter @@ -483,9 +487,9 @@ def save_imgvid_reID(video_path, video_result_path, save_vid=1, save_img=1, *pos while(cap.isOpened()): ret, frame = cap.read() if ret == True: - X = [np.array(coord.iloc[f,1::3]) for coord in coords] + X = [np.array(coord.iloc[f,2::3]) for coord in coords] X = [np.where(x==0., np.nan, x) for x in X] - Y = [np.array(coord.iloc[f,2::3]) for coord in coords] + Y = [np.array(coord.iloc[f,3::3]) for coord in coords] Y = [np.where(y==0., np.nan, y) for y in Y] # Draw bounding box @@ -584,7 +588,7 @@ def detect_pose_fun(config_dict): os.chdir(root_dir) # Sort people and save to csv, optionally display plot - json_to_csv(json_path, pose_model, interp_gap_smaller_than, filter_options, show_plots) + json_to_csv(json_path, frame_rate, pose_model, interp_gap_smaller_than, filter_options, show_plots) # Save images and files after reindentification if save_img and save_vid: diff --git a/setup.cfg b/setup.cfg index 26d58f8..5137710 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = sports2d -version = 0.2.0 +version = 0.2.1 author = David Pagnon author_email = contact@david-pagnon.com description = Detect pose and compute 2D joint angles from a video.