Skip to content
Permalink
Browse files
update
  • Loading branch information
Daniel Bisig committed Dec 7, 2021
1 parent d2422b3 commit d48c9fe7b93204a2d207103de502a4eb0f6402ba
Show file tree
Hide file tree
Showing 10 changed files with 1,084 additions and 0 deletions.
@@ -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")
@@ -0,0 +1,5 @@
{
"mocapFileName": "data/MUR_AccumulationMovementOnPlace_01_mb_proc_rh.json",
"oscSendAddress": "127.0.0.1",
"oscSendPort": 9003
}
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
@@ -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
@@ -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

0 comments on commit d48c9fe

Please sign in to comment.