Saturday, August 4, 2012

Importing Vicon IQ Motion Capture into Blender

In the Fused Media Lab at De Montfort University's Faculty of Technology, I used a Vicon multi-camera infrared tracking system to capture the upper-body, arm and hand motions of Tofail Ahmed while he sang khyāl alāps. The software was Vicon IQ. IQ is no longer supported by Vicon, and its export formats are not widely recognized any more.

Therefore, I explored a multitude of dead-ends in trying to get the motion capture data into Blender 2.6x. Here's the solution I ultimately developed. One probably wouldn't want to go through this for high volumes of motion capture sessions for different subjects, but it is a reasonable solution for transferring one session.

Export Data

First, export the skeleton joint movement and rotations data from Vicon, using the CSV (comma separated value) format, global rather than local orientation. It contains world-space rotations and translations for each joint. The joint angles are Euler angles, specified by X Y and Z rotations in degrees, applied in that order (as they appear in the spreadsheet).

Create The Blender Skeleton

The next step is to manually create a skeleton of armatures in Blender matching the calibrated skeleton used in the motion capture. One can guide the process by looking at the calibrated skeleton file — the Vicon.vsk file. The .vsk is in XML format, so can be opened in a text editor. The first section is the <KinematicModel> section. Within that, the <Parameters> section will list the name and values of parameters used in the construction of the skeleton. After that, the <Skeleton> section defines each "segment" or bone and the hierarchy of relationships between the bones. The hierarchy is modeled within the XML hierarchy itself. For example, in my skeleton, pelvis is the parent of thorax is the parent of head, lclavicle and rclavicle. So this part of the XML, simplified, is arranged as follows (… indicates stuff left out). Notice how the parameters defined above now appear in the skeleton definition, usually in defining the position of joints (and hence the length of bones):

    <Segment NAME="pelvis" POSITION="0 0 0" …>
        <Segment NAME="thorax" POSITION="-50 0 Back" …>
            <Segment NAME="head" POSITION="0 0 Neck" …>
            <Segment NAME="lclavicle" POSITION="0 0 Neck" …>
                … (whole left arm descends from here)
            <Segment NAME="rclavicle" POSITION="0 0 Neck" …>
               … (whole right arm descends from here)

Notice that the position of each joint is defined relative to its parent rather than in global coordinates. We will need global coordinates to create the skeleton in Blender.

Further, the Vicon coordinate system is Y pointing left, X pointing back, Z pointing up, while the Blender coordinate system is X pointing right, Y pointing back, Z pointing up. So we need to map Vicon X to Blender Y, the negative of Vicon Y to Blender X, and Vicon Z to Blender Z.

I built an Excel spreadsheet to take the Vicon local coordinates and convert them to Blender coordinates, then, based on the hierarchy, accumulate the values into global coordinates:

So, using these absolute/global values, one can create the skeleton in Blender. To be safe, I took care to ensure that the roll was set to 0 for all bones. This skeleton will be huge by the standards of Blender units. Scaling will come later.

Import Motion Data

To import the Vicon IQ motion data from the CSVs, I used Hans P.G.'s CSV F-Curve Importer Blender addon (much thanks to Hans). This requires some preparation, however…

Convert Frame Rate

My data was captured at 120 fps, so it had to be converted to 30 fps by throwing away 3 out of every 4 rows. I did this by using Excel's "Advanced Filter". First I added a column next to the 'frames' column and using mod(4) applied to the frame number (using Fill Down to copy the formula to all cells)…

… then I added second sheet and placed the search criteria for a filtering operation there. We want to select any row where FrameMod = 1. Notice in the formula bar that one has to enter ="=1" for this to work.

Using Data > Advanced Filter…, designate "Copy to another location". The list range will be the range of the original data, criteria range will be the two search criteria cells, and destination should be a starting cell somewhere below the original data. The filtered data will appear here.

The frame numbers will need to be resequenced. I copied frame numbers from the original data and pasted to the new, filtered data.

I then deleted the FrameMod column.

Convert Axis Orientation

The Y values of the translation parameters (NOT the angle parameters) need to be multiplied by -1. One way to do this is to place a -1 in an empty cell and copy the cell. Then select the column that needs to be altered and choose Edit > Paste Special… > Multiply. (The translation parameters are indicated by the <t-X>, <t-Y>, and <t-Z> column heads.)

Convert to Headerless CSV

The header row (the row giving the names for each column) needs to be removed. Now save to a CSV file.

It will be useful to copy that header row into another Excel file for easy reference during the import step, since we will need to know which column number is which. The columns will be referred to by 0-based count, so I found it useful to number them:

Setup Locator Empties for the Joints

Create an Empty for each joint. These are what will be keyframed…

Import the Joint Locations

I used the CSV F-Curve Importer v0_7_alpha1 to import the joint location data one joint at a time. Select a single joint-locator Empty and run the importer to keyframe its location.

I had to comment out the following in the v0_7_alpha1code to get it to run in Blender 2.63. That may not be necessary now:
* import unittest
* the def main()... block
* the class Test_FCurvePointAdder... block

The spreadsheet now contains Vicon global X -Y Z for each joint. I swapped this to -Y X Z during the input to map properly. I assigned a single Action Name to each XYZ set. Here is an example of reading in the Head location from 0-based column numbers 13, 14 and 15, indexed 1 0 2 to do the axis swap (notice that the F-Curve Importer pane shows up under Scene Properties):

Constrain the Bones to the Joint Locators

Working in Pose Mode, one can apply bone constraints. The root of the skeleton (Pelvis, in my case) should have a Copy Location constraint tying it to the Pelvis joint locator Empty. Then add a TrackTo constraint pointing at the next joint locator (Thorax, in my case) [Edit 22 Sep 2015 a better solution for all of the tracking of bones to locators is to use the StretchTo constraint rather than TrackTo, no volume effect, Plane = z was my preference]

Each bone after this point requires only one constraint: a TrackTo to the next joint location:

This will likely move the skeleton way off to some other location in Blender space, but it should now be animated.

N.B. the above may not treat location of free joints (pelvis, thorax, and head in this example) precisely correctly. Comparing the global and local joint export files from IQ, I was not able to come up with a consistent interpretation of how to handle these. For example, it seems to me that free joints below the root joint imply variable bone-lengths, which does not make much sense to me – and I suspect can't be implemented in Blender. However, the above worked well enough for my purposes.

Add Head Rotations

So we are able to get this far without having to import any actual rotation data. The head provides an exception. We now know its location, but not its rotation. This will need to be imported from the Global CSV. But this requires some more prep. The Vicon CSV is in angles, but Blender's internal routines work in radians (even if the interface displays degrees). So the CSV angle data needs to be converted to radians.

One way to do this is to copy the column of data to another spreadsheet. Then fill the next column with an =RADIANS formula. Select and copy the resulting numbers, and paste over the data in the original spreadsheet using Paste Special > Values.

Select the Head locator empty and run the F-Curve importer to import the X Y & Z rotations. As with the translation import above, these should be indexed 1 0 2 in order to swap the X and Y axis.

In my case, it made sense to add a block to represent the head, and add a Copy Location constraint and Copy Rotation constraint, both tied to the Head locator.

Parent, Reposition and Scale

I created an empty at the exact origin of the pelvis, then parented all locator Empties, the head block, and the skeleton to this Empty. This empty serves as the root of the whole bundle, providing one point of control for positioning, rotation and scaling. A scale of 0.01 brought my figure down to something closer to normal Blender working scale.

Optional Joint Rotations

If one needs to make joint locators also reflect joint rotation, one could add a Copy Rotation constraint to a locator, select the Armature as the target, then — in the bone indicator that will appear — indicate the bone. [Edit 22 Sep 2015 -- this is a bad idea, actually. Creates a circular definition between the skeleton constraints and the locator, yielding a 'dependency cycle' error]

No comments:

Post a Comment