Permalink
Show file tree
Hide file tree
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Showing
10 changed files
with
1,084 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,52 @@ | ||
# Motion Capture Player | ||
|
||
Daniel Bisig - Coventry University, UK - [ad5041@coventry.ac.uk](ad5041@coventry.ac.uk) | ||
|
||
## Abstract | ||
|
||
This software is a simple tool for playing motion capture data and simultaneously sending this data via the open sound control (OSC) protocol to a client. The player operates either on skeleton joints (positions and orientations) or markers (positions only). This data is stored in a custom file format that can be created using simple python scripts. The player is written in C++ using the OpenFrameworks creative coding environment. | ||
|
||
## Usage | ||
![Player](./content/player_screenshot.jpg "Player") | ||
|
||
**Conversion of Skeleton Motion Capture Data** | ||
|
||
Conversion of skeleton motion capture data is from the Biovision (.bvh) format into the custom json format required by the player. | ||
|
||
python bvhconv.py --input ../data/example_mocap.bvh --output ../data/example_mocap_skel.json | ||
|
||
**Conversion of Marker Motion Capture Data** | ||
|
||
Conversion of marker data is from the IBM (.c3d) format into the custom json format format required by the player. | ||
|
||
python c3dconv.py --input ../data/example_mocap.c3d --output ../data/example_mocap_markers.json --frameskip 6 | ||
|
||
The --frameskip argument determines the interval (in number of frames) at which marker positions should be exported. With --frameskip 6, every sixth frame will be exported. This argument has been introduced to cope with the fact that marker data is often recorded at much higher framerate than skeleton data. | ||
|
||
**Compiling the Motion Capture Player** | ||
|
||
Currently, the player requires OpenFrameworks version 0.11.0 to compile. Furthermore, it has the following dependencies: | ||
|
||
- [ofxDabBase](https://bitbucket.org/dbisig/ofxdabbase_011/src/master/ "Bitbucket") | ||
- [ofxDabMath](https://bitbucket.org/dbisig/ofxdabmath_011/src/master/ "Bitbucket") | ||
- [ofxDabOsc](https://bitbucket.org/dbisig/ofxdabosc_011/src/master/ "Bitbucket") | ||
- [ofxImGui](https://github.com/jvcleave/ofxImGui "Github") | ||
- [ofxJSON](https://github.com/jeffcrouse/ofxJSON "Github") | ||
|
||
**Configuration of Motion Capture Player** | ||
|
||
To configure the player, the file ./bin/data/config.json needs to be edited. Configuration involves specifying the file the player should read motion capture data from and the address and port the player sends the OSC messages to. | ||
|
||
"mocapFileName": "data/MUR_AccumulationMovementOnPlace_01_mb_proc_rh.json", | ||
"oscSendAddress": "127.0.0.1", | ||
"oscSendPort": 9003 | ||
|
||
The OSC message content is as follows: | ||
|
||
For skeleton data: /mocap/skel | ||
|
||
|
||
|
||
## Resources | ||
|
||
- E2-Create [Project Page](https://wp.coventry.domains/e2create/ "Project Page") |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
{ | ||
"mocapFileName": "data/MUR_AccumulationMovementOnPlace_01_mb_proc_rh.json", | ||
"oscSendAddress": "127.0.0.1", | ||
"oscSendPort": 9003 | ||
} |
BIN
+28.8 KB
content/player_screenshot.jpg
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,53 @@ | ||
import numpy as np | ||
|
||
class BVH_Joint(): | ||
def __init__(self, name, parent=None, children=None): | ||
self.name = name | ||
self.parent = parent | ||
self.children = children | ||
|
||
class BVH_MocapData(): | ||
def __init__(self): | ||
self.skeleton = {} | ||
self.values = None | ||
self.channel_names = [] | ||
self.framerate = 0.0 | ||
self.root_name = '' | ||
|
||
def traverse(self, j=None): | ||
stack = [self.root_name] | ||
while stack: | ||
joint = stack.pop() | ||
yield joint | ||
for c in self.skeleton[joint]['children']: | ||
stack.append(c) | ||
|
||
def clone(self): | ||
import copy | ||
new_data = BVH_MocapData() | ||
new_data.skeleton = copy.copy(self.skeleton) | ||
new_data.values = copy.copy(self.values) | ||
new_data.channel_names = copy.copy(self.channel_names) | ||
new_data.root_name = copy.copy(self.root_name) | ||
new_data.framerate = copy.copy(self.framerate) | ||
return new_data | ||
|
||
def get_all_channels(self): | ||
'''Returns all of the channels parsed from the file as a 2D numpy array''' | ||
|
||
frames = [f[1] for f in self.values] | ||
return np.asarray([[channel[2] for channel in frame] for frame in frames]) | ||
|
||
def get_skeleton_tree(self): | ||
tree = [] | ||
root_key = [j for j in self.skeleton if self.skeleton[j]['parent']==None][0] | ||
|
||
root_joint = BVH_Joint(root_key) | ||
|
||
def get_empty_channels(self): | ||
#TODO | ||
pass | ||
|
||
def get_constant_channels(self): | ||
#TODO | ||
pass |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,242 @@ | ||
''' | ||
BVH Parser Class | ||
By Omid Alemi | ||
Created: June 12, 2017 | ||
Based on: https://gist.github.com/johnfredcee/2007503 | ||
''' | ||
import re | ||
import numpy as np | ||
from bvh_data import BVH_Joint, BVH_MocapData | ||
|
||
class BVH_Scanner(): | ||
''' | ||
A wrapper class for re.Scanner | ||
''' | ||
def __init__(self): | ||
|
||
def identifier(scanner, token): | ||
return 'IDENT', token | ||
|
||
def operator(scanner, token): | ||
return 'OPERATOR', token | ||
|
||
def digit(scanner, token): | ||
return 'DIGIT', token | ||
|
||
def open_brace(scanner, token): | ||
return 'OPEN_BRACE', token | ||
|
||
def close_brace(scanner, token): | ||
return 'CLOSE_BRACE', token | ||
|
||
self.scanner = re.Scanner([ | ||
(r'[a-zA-Z_]\w*', identifier), | ||
#(r'-*[0-9]+(\.[0-9]+)?', digit), # won't work for .34 | ||
#(r'[-+]?[0-9]*\.?[0-9]+', digit), # won't work for 4.56e-2 | ||
#(r'[-+]?[0-9]*\.?[0-9]+([eE][-+]?[0-9]+)?', digit), | ||
(r'-*[0-9]*\.?[0-9]+([eE][-+]?[0-9]+)?', digit), | ||
(r'}', close_brace), | ||
(r'}', close_brace), | ||
(r'{', open_brace), | ||
(r':', None), | ||
(r'\s+', None) | ||
]) | ||
|
||
def scan(self, stuff): | ||
return self.scanner.scan(stuff) | ||
|
||
|
||
|
||
class BVH_Parser(): | ||
''' | ||
A class to parse a BVH file. | ||
Extracts the skeleton and channel values | ||
''' | ||
def __init__(self, filename=None): | ||
self.reset() | ||
|
||
def reset(self): | ||
self._skeleton = {} | ||
self.bone_context = [] | ||
self._motion_channels = [] | ||
self._motions = [] | ||
self.current_token = 0 | ||
self.framerate = 0.0 | ||
self.root_name = '' | ||
|
||
self.scanner = BVH_Scanner() | ||
|
||
self.data = BVH_MocapData() | ||
|
||
|
||
def parse(self, filename): | ||
self.reset() | ||
|
||
with open(filename, 'r') as bvh_file: | ||
raw_contents = bvh_file.read() | ||
tokens, remainder = self.scanner.scan(raw_contents) | ||
self._parse_hierarchy(tokens) | ||
self.current_token = self.current_token + 1 | ||
self._parse_motion(tokens) | ||
|
||
self.data.skeleton = self._skeleton | ||
self.data.channel_names = self._motion_channels | ||
self.data.values = self._to_DataFrame() | ||
self.data.root_name = self.root_name | ||
self.data.framerate = self.framerate | ||
|
||
return self.data | ||
|
||
def _to_DataFrame(self): | ||
'''Returns all of the channels parsed from the file as a pandas DataFrame''' | ||
|
||
import pandas as pd | ||
time_index = pd.to_timedelta([f[0] for f in self._motions], unit='s') | ||
frames = [f[1] for f in self._motions] | ||
channels = np.asarray([[channel[2] for channel in frame] for frame in frames]) | ||
column_names = ['%s_%s'%(c[0], c[1]) for c in self._motion_channels] | ||
|
||
return pd.DataFrame(data=channels, index=time_index, columns=column_names) | ||
|
||
|
||
def _new_bone(self, parent, name): | ||
bone = {'parent': parent, 'channels': [], 'offsets': [],'children': []} | ||
return bone | ||
|
||
def _push_bone_context(self,name): | ||
self.bone_context.append(name) | ||
|
||
def _get_bone_context(self): | ||
return self.bone_context[len(self.bone_context)-1] | ||
|
||
def _pop_bone_context(self): | ||
self.bone_context = self.bone_context[:-1] | ||
return self.bone_context[len(self.bone_context)-1] | ||
|
||
def _read_offset(self, bvh, token_index): | ||
if bvh[token_index] != ('IDENT', 'OFFSET'): | ||
return None, None | ||
token_index = token_index + 1 | ||
offsets = [0.0] * 3 | ||
for i in range(3): | ||
offsets[i] = float(bvh[token_index][1]) | ||
token_index = token_index + 1 | ||
return offsets, token_index | ||
|
||
def _read_channels(self, bvh, token_index): | ||
if bvh[token_index] != ('IDENT', 'CHANNELS'): | ||
return None, None | ||
token_index = token_index + 1 | ||
channel_count = int(bvh[token_index][1]) | ||
token_index = token_index + 1 | ||
channels = [""] * channel_count | ||
for i in range(channel_count): | ||
channels[i] = bvh[token_index][1] | ||
token_index = token_index + 1 | ||
return channels, token_index | ||
|
||
def _parse_joint(self, bvh, token_index): | ||
end_site = False | ||
joint_id = bvh[token_index][1] | ||
token_index = token_index + 1 | ||
joint_name = bvh[token_index][1] | ||
token_index = token_index + 1 | ||
|
||
parent_name = self._get_bone_context() | ||
|
||
if (joint_id == "End"): | ||
joint_name = parent_name+ '_Nub' | ||
end_site = True | ||
joint = self._new_bone(parent_name, joint_name) | ||
if bvh[token_index][0] != 'OPEN_BRACE': | ||
print('Was expecting brance, got ', bvh[token_index]) | ||
return None | ||
token_index = token_index + 1 | ||
offsets, token_index = self._read_offset(bvh, token_index) | ||
joint['offsets'] = offsets | ||
if not end_site: | ||
channels, token_index = self._read_channels(bvh, token_index) | ||
joint['channels'] = channels | ||
for channel in channels: | ||
self._motion_channels.append((joint_name, channel)) | ||
|
||
self._skeleton[joint_name] = joint | ||
self._skeleton[parent_name]['children'].append(joint_name) | ||
|
||
while (bvh[token_index][0] == 'IDENT' and bvh[token_index][1] == 'JOINT') or (bvh[token_index][0] == 'IDENT' and bvh[token_index][1] == 'End'): | ||
self._push_bone_context(joint_name) | ||
token_index = self._parse_joint(bvh, token_index) | ||
self._pop_bone_context() | ||
|
||
if bvh[token_index][0] == 'CLOSE_BRACE': | ||
return token_index + 1 | ||
|
||
print('Unexpected token ', bvh[token_index]) | ||
|
||
def _parse_hierarchy(self, bvh): | ||
self.current_token = 0 | ||
if bvh[self.current_token] != ('IDENT', 'HIERARCHY'): | ||
return None | ||
self.current_token = self.current_token + 1 | ||
if bvh[self.current_token] != ('IDENT', 'ROOT'): | ||
return None | ||
self.current_token = self.current_token + 1 | ||
if bvh[self.current_token][0] != 'IDENT': | ||
return None | ||
|
||
root_name = bvh[self.current_token][1] | ||
root_bone = self._new_bone(None, root_name) | ||
self.current_token = self.current_token + 2 #skipping open brace | ||
offsets, self.current_token = self._read_offset(bvh, self.current_token) | ||
channels, self.current_token = self._read_channels(bvh, self.current_token) | ||
root_bone['offsets'] = offsets | ||
root_bone['channels'] = channels | ||
self._skeleton[root_name] = root_bone | ||
self._push_bone_context(root_name) | ||
|
||
for channel in channels: | ||
self._motion_channels.append((root_name, channel)) | ||
|
||
while bvh[self.current_token][1] == 'JOINT': | ||
self.current_token = self._parse_joint(bvh, self.current_token) | ||
|
||
self.root_name = root_name | ||
|
||
def _parse_motion(self, bvh): | ||
if bvh[self.current_token][0] != 'IDENT': | ||
print('Unexpected text') | ||
return None | ||
if bvh[self.current_token][1] != 'MOTION': | ||
print('No motion section') | ||
return None | ||
self.current_token = self.current_token + 1 | ||
if bvh[self.current_token][1] != 'Frames': | ||
return None | ||
self.current_token = self.current_token + 1 | ||
frame_count = int(bvh[self.current_token][1]) | ||
self.current_token = self.current_token + 1 | ||
if bvh[self.current_token][1] != 'Frame': | ||
return None | ||
self.current_token = self.current_token + 1 | ||
if bvh[self.current_token][1] != 'Time': | ||
return None | ||
self.current_token = self.current_token + 1 | ||
frame_rate = float(bvh[self.current_token][1]) | ||
|
||
self.framerate = frame_rate | ||
|
||
self.current_token = self.current_token + 1 | ||
|
||
frame_time = 0.0 | ||
self._motions = [()] * frame_count | ||
for i in range(frame_count): | ||
channel_values = [] | ||
for channel in self._motion_channels: | ||
channel_values.append((channel[0], channel[1], float(bvh[self.current_token][1]))) | ||
self.current_token = self.current_token + 1 | ||
self._motions[i] = (frame_time, channel_values) | ||
frame_time = frame_time + frame_rate |
Oops, something went wrong.