├── README.markdown ├── example.m └── loadbvh.m /README.markdown: -------------------------------------------------------------------------------- 1 | # Biovision (BVH) in Matlab 2 | 3 | This code loads and parses a Biovision BVH file, calculating joint kinematics for subsequent processing inside Matlab. 4 | 5 | Developed at The University of Adelaide for visualising and post-processing the skeletal data calculated using the [Arena](http://www.naturalpoint.com/optitrack/products/arena/) motion capture software. 6 | 7 | 8 | ## Files 9 | 10 | * `loadbvh.m` — the main Matlab function that you're looking for 11 | * `louise.bvh` — a typical BVH file 12 | * `example.m` — an example which loads `louise.bvh` in Matlab and animates it 13 | 14 | 15 | ## Copyright and licensing details 16 | 17 | Copyright 2012 Will Robertson and The University of Adelaide 18 | 19 | Released under the terms and conditions of the Apache License, v2. 20 | 21 | -------------------------------------------------------------------------------- /example.m: -------------------------------------------------------------------------------- 1 | %% Reading a BVH file and animating it over time 2 | % 3 | % BVH is a text file which contains skeletal data, but its contents needs 4 | % additional processing to draw the wireframe and create the animation. 5 | 6 | clear all 7 | close all 8 | 9 | name = 'louise'; 10 | [skeleton,time] = loadbvh(name); 11 | Njoints = numel(skeleton); 12 | 13 | frame_first = 1; 14 | frame_last = 1500; % =inf to play until the end 15 | frame_step = 20; 16 | write_video = true; 17 | 18 | %% Initialise figure 19 | 20 | hf = figure(1); clf; hold on 21 | hf.Color = 'white'; 22 | ha = gca; 23 | 24 | % General view 25 | title(sprintf('%1.2f seconds (frame %i)',0,1)) 26 | view(30,-30) 27 | axis equal 28 | axis off 29 | 30 | % Set axes to show all points across all time 31 | xmax = 0; xmin = 0; 32 | ymax = 0; ymin = 0; 33 | zmax = 0; zmin = 0; 34 | for nn = 1:Njoints 35 | xmax = max(xmax,max(skeleton(nn).Dxyz(1,:))); 36 | ymax = max(ymax,max(skeleton(nn).Dxyz(2,:))); 37 | zmax = max(zmax,max(skeleton(nn).Dxyz(3,:))); 38 | xmin = min(xmin,min(skeleton(nn).Dxyz(1,:))); 39 | ymin = min(ymin,min(skeleton(nn).Dxyz(2,:))); 40 | zmin = min(zmin,min(skeleton(nn).Dxyz(3,:))); 41 | end 42 | scalefactor = 1.2; 43 | xmax = (xmax+xmin)/2 + scalefactor*(xmax-xmin)/2; 44 | ymax = (ymax+ymin)/2 + scalefactor*(ymax-ymin)/2; 45 | zmax = (zmax+zmin)/2 + scalefactor*(zmax-zmin)/2; 46 | xmin = (xmax+xmin)/2 - scalefactor*(xmax-xmin)/2; 47 | ymin = (ymax+ymin)/2 - scalefactor*(ymax-ymin)/2; 48 | zmin = (zmax+zmin)/2 - scalefactor*(zmax-zmin)/2; 49 | axis([xmin xmax ymin ymax zmin zmax]) 50 | 51 | ha.XLimMode = 'manual'; 52 | ha.YLimMode = 'manual'; 53 | ha.ZLimMode = 'manual'; 54 | 55 | % plot the first time point 56 | for ff = frame_first 57 | for nn = Njoints:-1:1 58 | hp(nn) = plot3(skeleton(nn).Dxyz(1,ff),skeleton(nn).Dxyz(2,ff),skeleton(nn).Dxyz(3,ff),'.','markersize',20); 59 | parent = skeleton(nn).parent; 60 | if parent > 0 61 | hl(nn) = plot3([skeleton(parent).Dxyz(1,ff) skeleton(nn).Dxyz(1,ff)],... 62 | [skeleton(parent).Dxyz(2,ff) skeleton(nn).Dxyz(2,ff)],... 63 | [skeleton(parent).Dxyz(3,ff) skeleton(nn).Dxyz(3,ff)]); 64 | end 65 | end 66 | end 67 | 68 | %% Run animation 69 | 70 | if write_video, vidObj = VideoWriter(name); open(vidObj); end 71 | 72 | for ff = max(frame_first,1):frame_step:min(frame_last,skeleton(1).Nframes) 73 | 74 | title(sprintf('%1.2f seconds (frame %i)',time(ff),ff)) 75 | 76 | % update plot to subsequent time point values 77 | for nn = 1:Njoints 78 | 79 | hp(nn).XData = skeleton(nn).Dxyz(1,ff); 80 | hp(nn).YData = skeleton(nn).Dxyz(2,ff); 81 | hp(nn).ZData = skeleton(nn).Dxyz(3,ff); 82 | 83 | parent = skeleton(nn).parent; 84 | if parent > 0 85 | hl(nn).XData = [skeleton(parent).Dxyz(1,ff) skeleton(nn).Dxyz(1,ff)]; 86 | hl(nn).YData = [skeleton(parent).Dxyz(2,ff) skeleton(nn).Dxyz(2,ff)]; 87 | hl(nn).ZData = [skeleton(parent).Dxyz(3,ff) skeleton(nn).Dxyz(3,ff)]; 88 | end 89 | 90 | end 91 | 92 | drawnow 93 | if write_video, writeVideo(vidObj,getframe(gca)); end 94 | 95 | end 96 | 97 | if write_video, close(vidObj); end 98 | -------------------------------------------------------------------------------- /loadbvh.m: -------------------------------------------------------------------------------- 1 | function [skeleton,time] = loadbvh(fname,varargin) 2 | %% LOADBVH Load a .bvh (Biovision) file. 3 | % 4 | % Loads BVH file specified by FNAME (with or without .bvh extension) 5 | % and parses the file, calculating joint kinematics and storing the 6 | % output in SKELETON. 7 | % 8 | % Optional argument 'delim' allows for setting the delimiter between 9 | % fields. E.g., for a tab-separated BVH file: 10 | % 11 | % skeleton = loadbvh('louise.bvh','delim','\t') 12 | % 13 | % By default 'delim' is set to the space character. 14 | % 15 | % SKELETON is a structure with N elements, where N is the number of joints. 16 | % Each element consists of the following fields: 17 | % 18 | % * `name` -- human-readable description of the joint 19 | % * `nestdepth` -- how many joints away from the origin 20 | % * `parent` -- index to parent joint 21 | % * `offset` -- translation from previous joint to current joint 22 | % * `Nchannels` -- number of channels describing pose 23 | % (usually 3 for rotations only or 6 for 6DOF pose) 24 | % * `Nframes` -- number of time samples 25 | % * `order` -- the Euler angle order; e.g. [3 1 2] = ZXY 26 | % * `Dxyz` -- XYZ displacements (directly from `trans` matrix) 27 | % * `rxyz` -- XYZ rotations (to calculate `trans` matrix) 28 | % * `trans` -- transformation matrix 29 | % 30 | % Some details on the BVH file structure are given in "Motion Capture File 31 | % Formats Explained": http://www.dcs.shef.ac.uk/intranet/research/resmes/CS0111.pdf 32 | % But most of it is fairly self-evident. 33 | 34 | %% Options 35 | 36 | p = inputParser; 37 | p.addParameter('delim',' '); 38 | parse(p,varargin{:}); 39 | opt = p.Results; 40 | 41 | %% Load and parse header data 42 | % 43 | % The file is opened for reading, primarily to extract the header data (see 44 | % next section). However, I don't know how to ask Matlab to read only up 45 | % until the line "MOTION", so we're being a bit inefficient here and 46 | % loading the entire file into memory. Oh well. 47 | 48 | % add a file extension if necessary: 49 | if ~strncmpi(fliplr(fname),'hvb.',4) 50 | fname = [fname,'.bvh']; 51 | end 52 | 53 | fid = fopen(fname); 54 | if fid == -1 55 | error(['File "',fname,'" not found.']) 56 | end 57 | C = textscan(fid,'%s'); 58 | fclose(fid); 59 | C = C{1}; 60 | 61 | 62 | %% Parse data 63 | % 64 | % This is a cheap tokeniser, not particularly clever. 65 | % Iterate word-by-word, counting braces and extracting data. 66 | 67 | % Initialise: 68 | skeleton = []; 69 | ii = 1; 70 | nn = 0; 71 | brace_count = 1; 72 | 73 | while ~strcmp( C{ii} , 'MOTION' ) 74 | 75 | ii = ii+1; 76 | token = C{ii}; 77 | 78 | if strcmp( token , '{' ) 79 | 80 | brace_count = brace_count + 1; 81 | 82 | elseif strcmp( token , '}' ) 83 | 84 | brace_count = brace_count - 1; 85 | 86 | elseif strcmp( token , 'OFFSET' ) 87 | 88 | skeleton(nn).offset = [str2double(C(ii+1)) ; str2double(C(ii+2)) ; str2double(C(ii+3))]; 89 | ii = ii+3; 90 | 91 | elseif strcmp( token , 'CHANNELS' ) 92 | 93 | skeleton(nn).Nchannels = str2double(C(ii+1)); 94 | 95 | % The 'order' field is an index corresponding to the order of 'X' 'Y' 'Z'. 96 | % Subtract 87 because char numbers "X" == 88, "Y" == 89, "Z" == 90. 97 | if skeleton(nn).Nchannels == 3 98 | skeleton(nn).order = [C{ii+2}(1),C{ii+3}(1),C{ii+4}(1)]-87; 99 | elseif skeleton(nn).Nchannels == 6 100 | skeleton(nn).order = [C{ii+5}(1),C{ii+6}(1),C{ii+7}(1)]-87; 101 | else 102 | error('Not sure how to handle not (3 or 6) number of channels.') 103 | end 104 | 105 | if ~all(sort(skeleton(nn).order)==[1 2 3]) 106 | error('Cannot read channels order correctly. Should be some permutation of [''X'' ''Y'' ''Z''].') 107 | end 108 | 109 | ii = ii + skeleton(nn).Nchannels + 1; 110 | 111 | elseif strcmp( token , 'JOINT' ) || strcmp( token , 'ROOT' ) 112 | % Regular joint 113 | 114 | nn = nn+1; 115 | 116 | skeleton(nn).name = C{ii+1}; 117 | skeleton(nn).nestdepth = brace_count; 118 | 119 | if brace_count == 1 120 | % root node 121 | skeleton(nn).parent = 0; 122 | elseif skeleton(nn-1).nestdepth + 1 == brace_count; 123 | % if I am a child, the previous node is my parent: 124 | skeleton(nn).parent = nn-1; 125 | else 126 | % if not, what is the node corresponding to this brace count? 127 | prev_parent = skeleton(nn-1).parent; 128 | while skeleton(prev_parent).nestdepth+1 ~= brace_count 129 | prev_parent = skeleton(prev_parent).parent; 130 | end 131 | skeleton(nn).parent = prev_parent; 132 | end 133 | 134 | ii = ii+1; 135 | 136 | elseif strcmp( [C{ii},' ',C{ii+1}] , 'End Site' ) 137 | % End effector; unnamed terminating joint 138 | % 139 | % N.B. The "two word" token here is why we don't use a switch statement 140 | % for this code. 141 | 142 | nn = nn+1; 143 | 144 | skeleton(nn).name = ' '; 145 | skeleton(nn).offset = [str2double(C(ii+4)) ; str2double(C(ii+5)) ; str2double(C(ii+6))]; 146 | skeleton(nn).parent = nn-1; % always the direct child 147 | skeleton(nn).nestdepth = brace_count; 148 | skeleton(nn).Nchannels = 0; 149 | 150 | end 151 | 152 | end 153 | 154 | %% Initial processing and error checking 155 | 156 | Nnodes = numel(skeleton); 157 | Nchannels = sum([skeleton.Nchannels]); 158 | Nchainends = sum([skeleton.Nchannels]==0); 159 | 160 | % Calculate number of header lines: 161 | % - 5 lines per joint 162 | % - 4 lines per chain end 163 | % - 2 additional lines ('HIERARCHY' and 'MOTION') 164 | Nheaderlines = (Nnodes-Nchainends)*5 + Nchainends*4 + 2; 165 | 166 | fid = fopen(fname); 167 | for ii = 1:Nheaderlines 168 | tline = fgetl(fid); 169 | end 170 | if ~strcmp(tline,'MOTION') 171 | error('Could not parse BVH file %s. Number of header lines (before "MOTION" appears incorrect.',fname) 172 | end 173 | 174 | % get next non-blank line 175 | tline = char([]); 176 | while isempty(tline) 177 | tline = fgetl(fid); 178 | Nheaderlines = Nheaderlines + 1; 179 | end 180 | Nframes = sscanf(tline,'Frames: %f'); 181 | 182 | % get next non-blank line 183 | tline = char([]); 184 | while isempty(tline) 185 | tline = fgetl(fid); 186 | Nheaderlines = Nheaderlines + 1; 187 | end 188 | frame_time = sscanf(tline,'Frame Time: %f'); 189 | 190 | first_line = fgetl(fid); 191 | fclose(fid); 192 | 193 | % get the full data array from here: 194 | rawdata = importdata(fname,opt.delim,Nheaderlines); 195 | 196 | if ~isstruct(rawdata) 197 | error('Could not parse BVH file %s. Check the delimiter. First line of data appears to be:\n\n> %s',fname,first_line) 198 | end 199 | 200 | time = frame_time*(0:Nframes-1); 201 | 202 | if size(rawdata.data,2) ~= Nchannels 203 | error('Error reading BVH file: channels count does not match.') 204 | end 205 | 206 | if size(rawdata.data,1) ~= Nframes 207 | warning('LOADBVH:frames_wrong','Error reading BVH file: frames count does not match; continuing anyway.') 208 | Nframes = size(rawdata.data,1); 209 | end 210 | 211 | %% Load motion data into skeleton structure 212 | % 213 | % We have three possibilities for each node we come across: 214 | % (a) a root node that has displacements already defined, 215 | % for which the transformation matrix can be directly calculated; 216 | % (b) a joint node, for which the transformation matrix must be calculated 217 | % from the previous points in the chain; and 218 | % (c) an end effector, which only has displacement to calculate from the 219 | % previous node's transformation matrix and the offset of the end 220 | % joint. 221 | % 222 | % These are indicated in the skeleton structure, respectively, by having 223 | % six, three, and zero "channels" of data. 224 | % In this section of the code, the channels are read in where appropriate 225 | % and the relevant arrays are pre-initialised for the subsequent calcs. 226 | 227 | channel_count = 0; 228 | 229 | for nn = 1:Nnodes 230 | 231 | if skeleton(nn).Nchannels == 6 % root node 232 | 233 | % assume translational data is always ordered XYZ 234 | skeleton(nn).Dxyz = repmat(skeleton(nn).offset,[1 Nframes]) + rawdata.data(:,channel_count+[1 2 3])'; 235 | skeleton(nn).rxyz(skeleton(nn).order,:) = rawdata.data(:,channel_count+[4 5 6])'; 236 | 237 | % Kinematics of the root element: 238 | skeleton(nn).trans = nan(4,4,Nframes); 239 | for ff = 1:Nframes 240 | skeleton(nn).trans(:,:,ff) = transformation_matrix(skeleton(nn).Dxyz(:,ff) , skeleton(nn).rxyz(:,ff) , skeleton(nn).order); 241 | end 242 | 243 | elseif skeleton(nn).Nchannels == 3 % joint node 244 | 245 | skeleton(nn).rxyz(skeleton(nn).order,:) = rawdata.data(:,channel_count+[1 2 3])'; 246 | skeleton(nn).Dxyz = nan(3,Nframes); 247 | skeleton(nn).trans = nan(4,4,Nframes); 248 | 249 | elseif skeleton(nn).Nchannels == 0 % end node 250 | skeleton(nn).Dxyz = nan(3,Nframes); 251 | end 252 | 253 | channel_count = channel_count + skeleton(nn).Nchannels; 254 | skeleton(nn).Nframes = Nframes; 255 | 256 | end 257 | 258 | 259 | %% Calculate kinematics 260 | % 261 | % No calculations are required for the root nodes. 262 | 263 | % For each joint, calculate the transformation matrix and for convenience 264 | % extract each position in a separate vector. 265 | for nn = find([skeleton.parent] ~= 0 & [skeleton.Nchannels] ~= 0) 266 | 267 | parent = skeleton(nn).parent; 268 | 269 | for ff = 1:Nframes 270 | transM = transformation_matrix( skeleton(nn).offset , skeleton(nn).rxyz(:,ff) , skeleton(nn).order ); 271 | skeleton(nn).trans(:,:,ff) = skeleton(parent).trans(:,:,ff) * transM; 272 | skeleton(nn).Dxyz(:,ff) = skeleton(nn).trans([1 2 3],4,ff); 273 | end 274 | 275 | end 276 | 277 | % For an end effector we don't have rotation data; 278 | % just need to calculate the final position. 279 | for nn = find([skeleton.Nchannels] == 0) 280 | 281 | parent = skeleton(nn).parent; 282 | 283 | for ff = 1:Nframes 284 | transM = skeleton(parent).trans(:,:,ff) * [eye(3), skeleton(nn).offset; 0 0 0 1]; 285 | skeleton(nn).Dxyz(:,ff) = transM([1 2 3],4); 286 | end 287 | 288 | end 289 | 290 | end 291 | 292 | 293 | 294 | function transM = transformation_matrix(displ,rxyz,order) 295 | % Constructs the transformation for given displacement, DISPL, and 296 | % rotations RXYZ. The vector RYXZ is of length three corresponding to 297 | % rotations around the X, Y, Z axes. 298 | % 299 | % The third input, ORDER, is a vector indicating which order to apply 300 | % the planar rotations. E.g., [3 1 2] refers applying rotations RXYZ 301 | % around Z first, then X, then Y. 302 | % 303 | % Years ago we benchmarked that multiplying the separate rotation matrices 304 | % was more efficient than pre-calculating the final rotation matrix 305 | % symbolically, so we don't "optimise" by having a hard-coded rotation 306 | % matrix for, say, 'ZXY' which seems more common in BVH files. 307 | % Should revisit this assumption one day. 308 | % 309 | % Precalculating the cosines and sines saves around 38% in execution time. 310 | 311 | c = cosd(rxyz); 312 | s = sind(rxyz); 313 | 314 | RxRyRz(:,:,1) = [1 0 0; 0 c(1) -s(1); 0 s(1) c(1)]; 315 | RxRyRz(:,:,2) = [c(2) 0 s(2); 0 1 0; -s(2) 0 c(2)]; 316 | RxRyRz(:,:,3) = [c(3) -s(3) 0; s(3) c(3) 0; 0 0 1]; 317 | 318 | rotM = RxRyRz(:,:,order(1))*RxRyRz(:,:,order(2))*RxRyRz(:,:,order(3)); 319 | 320 | transM = [rotM, displ; 0 0 0 1]; 321 | 322 | end 323 | --------------------------------------------------------------------------------