How to build a bvh player that can be used to visualize human motions in Mathematica? Any idea or sample code would be appreciated.
bvh = Import["https://www.dropbox.com/s/vtr0o4aj9spu3cm/sample.bvh?dl=1"]
How to build a bvh player that can be used to visualize human motions in Mathematica? Any idea or sample code would be appreciated.
bvh = Import["https://www.dropbox.com/s/vtr0o4aj9spu3cm/sample.bvh?dl=1"]
TL;DR: A package (Mathematica v10) can be found at the very bottom of this post.
6: Tiny update: Import
can now use the ".bvh" extension to determine the import type. The code that does this is ugly, but I don't see any other way at the moment.
out = Import["C:\\Female1_C03_Run.bvh"]
5: Added error checking and registered the package as an official importer for "BVH" files, so an intermediate importing stage is no longer necessary:
out = Import["C:\\cmuconvert-daz-01-09\\06\\06_08.bvh", "BVH"]
4: A major update. I separated drawing and parsing of the file, increasing speed and user comfort in the process. Bone structure and joint positions are now determined by a separate pass through the parsed data. Separation of structure (which remains the same during the whole movie) and joint positions prepares for the use of GraphicsComplex
. I used a few V10 features, but it should be very easy to work around those to get it to work for earlier versions.
I haven't updated the text in original post, but the new code does not deviate so much from the description as to make it fully obsolete, so I leave it there as basic documentation of the code.
The main functionality is now provided in the form of a package (at the very bottom of this post). The main function is BVHGet
:
It returns an object BVHData
which is not unlike like objects such as FittedModel
and TimeSeries
:
Getting your BVH file processed now works as follows:
Needs["BVHImporter`"]
bvh = Import["C:\\cmuconvert-daz-01-09\\06\\06_08.bvh", "String"];
out = BVHGet[bvh]
With more details revealed:
Note that parsing takes most time. Frames are processed (on my laptop) at a rate of about 200 frames/sec.
With the output BVHData object assigned to the variable out
you can perform a lot of fun tricks.
The trace of all joints through time:
Point /@ out["JointsStack"] // Graphics3D
Same for the bones:
Graphics3D[
MapIndexed[
{Opacity[0.1], Hue[#2[[1]]/out["FrameCount"]], GraphicsComplex[#1, Line /@ out["Bones"]]} &,
out["JointsStack"]
]
]
Easily generate a Manipulate
:
out["Manipulate"]
Make an animated GIF:
out["AnimatedGIF", "C:\\dribble.gif"]
Or even generate a 3D Graph
(a V10 functionality).
Manipulate[
GraphPlot3D[
Graph[
Range[out["JointCount"]],
DirectedEdge @@@ out["Bones"]
],
VertexCoordinateRules -> out["JointsStack"][[i]]
], {i, 1, out["FrameCount"], 1}
]
There are several To-Do items, particularly some options to fine-tune things would be useful. Error handling (trying to parse non-BVH files etc.) would be nice too.
The movie above was taken from the cgspeed site which contains BVH conversions of the 2500-motion Carnegie-Mellon motion capture dataset.
3: Generalized the code to work with BVH files that use different rotation orderings.
2: Fixed another bug that caused the code to run much too slow
1: Found a bug (which derived from the vague BVH format description that I used), so the OP's demo file now runs correctly:
I also re-rendered the movies at the bottom of this post, which look much better now.
First, I use the free motion cap databases that you can find on the Advanced Computing Center for the Arts and Design website. I tried the 'female1' dataset. After unzipping to a working directory I import one of the BVH files contained in it as follows:
bvh = Import["c:\\Female1_bvh\\Female1_C03_Run.bvh", "String"];
For those who want to see how such a file looks like, check this short example file. Basically, a BVH file consists of a body description part with several nested segments, with fixed offsets with respect to the connecting joints and rotations that are read from the data fork of the file which contains the motion capture data. The following code separates these two parts:
bvhHierarchy = StringReplace[bvh, ___ ~~ "HIERARCHY" ~~ start__ ~~ "MOTION" ~~ ___ :> start];
bvhMotion = StringReplace[bvh, ___ ~~ "MOTION" ~~ end___ :> end];
A one-liner to tokenize the elements in the file:
tokenize[code_String] := StringSplit[code, (" " | "\n" | "\r" | "\t") ..]
Get the number of frames, frame time and mocap data section:
{frames, frameTime, data} =
tokenize[bvhMotion] /. {___, "Frames:", frs_, "Frame", "Time:", ft_, data__} :>
{ToExpression@frs,
ToExpression@ft,
Partition[Internal`StringToDouble /@ {data},
Length[{data}]/ToExpression[frs]
]};
Notice the use of Internal`StringToDouble
to convert the data in strings to reals. I don't like the use of this undocumented function, but ToExpression
doesn't handle the occasional number with exponential notation (1.0e3) correctly and the V10 function Interpreter["Number"]
is incredibly slow.
tokenize@bvhHierarchy
I define a couple of patterns to be used in later parsing:
ClearAll[channelPattern, offsetPattern, bracketPattern]
bracketPattern = PatternSequence["{", Except["{" | "}"] .., "}"];
channelPattern = PatternSequence["CHANNELS", n_, channels__] /;
Length[{channels}] == ToExpression[n];
offsetPattern = PatternSequence["OFFSET", x_, y_, z_];
bracketPattern
is a pattern to find the innermost pair of matching curly brackets. channelPattern
is a pattern to find the CHANNEL keyword and the corresponding parameters (usually three rotation angles, though an additional offset can be prepended). offsetPattern
is a pattern to find the OFFSET keyword and the corresponding parameters. The parameters mentioned above are actually just 'slots' that are to be filled in with data from the data section.
Recursive code to actually transform CHANNEL and OFFSET keywords to functions:
ClearAll[parse]
parse[left___, cp : channelPattern, right___] :=
expr[parse[left], offset @@ ({cp}[[3 ;; 5]]),
channel @@ ({cp}[[6 ;;]]), parse[right]] /; Length[{cp}] == 8
parse[left___, cp : channelPattern, right___] :=
expr[parse[left], channel @@ ({cp}[[3 ;;]]), parse[right]] /;
Length[{cp}] == 5
parse[left___, op : offsetPattern, right___] :=
expr[parse[left], offset @@ ({op}[[2 ;;]]), parse[right]];
parse[{tokens___}] := parse[tokens];
parse[tokens___] := expr[tokens];
I used these functions so that I would be able to address those pieces by their head later on. I needed that in early versions of the code, but it might be unnecessary with some rewriting.
Now the code to recursively built the body tree:
ClearAll[parseBrackets]
parseBrackets[left___, h1_, h2_, bp : bracketPattern, right___] :=
parseBrackets[left, h1 @@ ({bp}[[2 ;; -2]]), right];
Example result:
parseBrackets@parse@tokenize@bvhHierarchy /. expr -> Sequence /. parseBrackets[a:___] -> a
The replacements at the end of the code above remove the scaffolding that remains at the end of the building process. As you can see most of the unnecessary details are gone now (such as the names of the nodes).
In tree form, and removing a few details, it looks like this:
parseBrackets@parse@tokenize@bvhHierarchy
/. {expr -> Sequence, channel[___] -> c, offset[___] -> off} /.
parseBrackets[a : ___] -> a // TreeForm
As I wrote above the BVH file is full of 'Zrotation', 'Xrotation' stuff that are merely placeholders. It is not really clear from the few not-too-precise BVH descriptions that I read, but I assume that they are to be matched with elements in each data line in order of appearance. So, I have to replace those placeholders with something that indicates their original position if they are moved around. The following piece of code does that and also takes care of the possibly varying order of rotations:
makeSlots[tokens_] :=
Module[{i = 1},
Which[
MatchQ[#, Alternatives["Xposition", "Yposition", "Zposition"]], slot[i++],
# === "Xrotation", xr[slot[i++]],
# === "Yrotation", yr[slot[i++]],
# === "Zrotation", zr[slot[i++]],
Head[#] == String && StringMatchQ[#, NumberString],
ToExpression[#],
True, #
] & /@ tokens
]
The rotation matrices. The order of rotations is dealt with in makeSlots
:
xRot[x_] = RotationMatrix[x Degree, {1, 0, 0}];
yRot[y_] = RotationMatrix[y Degree, {0, 1, 0}];
zRot[z_] = RotationMatrix[z Degree, {0, 0, 1}];
Applying the actual rotations is delayed until late in the process. This is done by the undefined functions xr
,yr
, and zr
in makeSlots
. The following utility function takes care of activation when needed.
channelToRotation[c_channel] := Dot @@ (c /. {xr -> xRot, yr -> yRot, zr -> zRot})
Since the tree is a recursive construction it can be handled with a recursive function:
ClearAll[ROOT, JOINT]
ROOT[os_List, d_List, r_List, rest___] := ROOT[os + d, r, rest]
ROOT[os_List, r_List, p : PatternSequence[__JOINT, ___END]] :=
ROOT[os, r, #] & /@ {p} /; Length[{p}] > 1
ROOT[os_List, r_List, p_JOINT] := {Line[{os, os + r.p[[1]]}],
ROOT[os + r.p[[1]], r.p[[2]], Sequence @@ p[[3 ;;]]]}
ROOT[os_List, r_List, p_END] := Line[{os, os + r.p[[1]]}]
Note that so far the tree contains the passive keyword strings "ROOT", "JOINT" and "END". The above definitions do nothing until the moment I replace those strings with the actual function heads.
The non-defined slot
function is replaced by Slot
so as to turn the body complex into a pure function that can be applied on the lines of mocap data successively.
bodyFunction =
Function[parseBrackets@parse@makeSlots@tokenize@bvhHierarchy /.
{expr -> Sequence, slot -> Slot} // Evaluate];
With all this in place the actual processing of the separate frames can be done in a simple loop. Replacing the string keywords with their defined function names activates the interpretation process.
motion =
Monitor[
Table[temp =
bodyFunction @@ data[[i]] /.
{offset -> List, c_channel -> channelToRotation[c]} /.
{"ROOT" -> ROOT, "JOINT" -> JOINT, "End" -> END} /.
{parseBrackets[a : ___] -> a} //
Graphics3D, {i, Length@data}
],
Row[{i, temp}]
];
boundingBox =
{Min /@ Transpose[(Min /@ (PlotRange /. AbsoluteOptions[#, PlotRange])) & /@ motion],
Max /@ Transpose[(Max /@ (PlotRange /. AbsoluteOptions[#, PlotRange])) & /@ motion]};
Manipulate[
Show[
motion[[i]],
ViewVertical -> {0, 1, 0},
ViewPoint -> {7, 2, -7},
PlotRange -> Transpose[boundingBox],
ImageSize -> 500,
FaceGrids -> {{0, -1, 0}},
Boxed -> False
], {i, 1, Length@data, 1}]
Export["C:\\WalkTurnAround180.gif",
Table[
Show[
motion[[i]],
ViewVertical -> {0, 1, 0},
ViewPoint -> {7, 2, -7},
PlotRange -> Transpose[boundingBox],
ImageSize -> 500,
FaceGrids -> {{0, -1, 0}},
Boxed -> False
], {i, 1, Length@data, 1}],
"DisplayDurations" -> frameTime,
AnimationRepetitions -> Infinity
]
BeginPackage["BVHImporter`"];
BVHGet::usage = "BVHGet[BVHcode] parses the string BVHcode that contains the contents of a BVH file. It returns a BVHData object that can be interrogated for various aspects of the result.";
BVHData::usage="BVHData is the output object of BVHGet. It knows the following methods:\n\n\"BoundingBox\" - Get the bounding box containing all the movie frames\n\"JointsStack\" - Get the stack of movie frames. Each frame consists of a list of joint positions\n\"Bones\" - Get the bone structure (as a list of joint pairs)\n\"FrameTime\" - Time allocated for the display of a single move frame\n\"FrameCount\" - Number of frames in the movie\n\"JointCount\" - Number of joints in the object structure\n\"BoneCount\" - Get the number of bones\n\"ParseTime\" - Get the total time needed to parse the BVH file and to prepare calculations\n\"FrameProcessingTime\" - Get the total time to process all frames in the movie\n\"Manipulate\" - Generate a Manipulate containing the movie\n\"AnimatedGIF\" - Generates a animated GIF movie. Needs the file name as the second argument.\n\"Properties\" - Get the list of properties";
Begin["`Private`"];
tokenize[code_String] :=
StringSplit[code, (" " | "\n" | "\r" | "\t") ..];
makeSlots[tokens_] :=
Module[{i = 1},
Which[
MatchQ[#, Alternatives["Xposition", "Yposition", "Zposition"]],
slot[i++],
# === "Xrotation", xr[slot[i++]],
# === "Yrotation", yr[slot[i++]],
# === "Zrotation", zr[slot[i++]],
Head[#] == String && StringMatchQ[#, NumberString],
ToExpression[#],
True, #
] & /@ tokens
];
bracketPattern = PatternSequence["{", Except["{" | "}"] .., "}"];
channelPattern =
PatternSequence["CHANNELS", n_,
channels : Alternatives[_slot, _xr, _yr, _zr] ..] /;
Length[{channels}] == ToExpression[n] &&
Mod[ToExpression[n], 3] == 0;
(* note: the above Alternatives part -added to improve file syntax \
checking- requires makeSlots is executed before parse *)
offsetPattern = PatternSequence["OFFSET", x_, y_, z_];
parse[left___, cp : channelPattern, right___] :=
expr[parse[left], offset @@ ({cp}[[3 ;; 5]]),
channel @@ ({cp}[[6 ;;]]), parse[right]] /; Length[{cp}] == 8;
parse[left___, cp : channelPattern, right___] :=
expr[parse[left], channel @@ ({cp}[[3 ;;]]), parse[right]] /;
Length[{cp}] == 5;
parse[left___, op : offsetPattern, right___] :=
expr[parse[left], offset @@ ({op}[[2 ;;]]), parse[right]];
parse[{tokens___}] := parse[tokens];
parse[tokens___] := expr[tokens];
parseBrackets[left___, h1_, h2_, bp : bracketPattern, right___] :=
parseBrackets[left, h1 @@ ({bp}[[2 ;; -2]]), right];
ROOT[os_List, d_List, r_List, rest___] := (Sow[os + d]; i = 2;
ROOT[os + d, r, rest]);
ROOT[os_List, r_List, p : PatternSequence[__JOINT, ___END]] :=
ROOT[os, r, #] & /@ {p} /; Length[{p}] > 1;
ROOT[os_List, r_List, p_JOINT] := (Sow[os + r.p[[1]]];
ROOT[os + r.p[[1]], r.p[[2]], Sequence @@ p[[3 ;;]]]);
ROOT[os_List, r_List, p_END] := Sow[os + r.p[[1]]];
ROOT2[os_List, d_List, r_List, rest___] := (i = 1; ROOT2[1, rest]);
ROOT2[start_Integer, p : PatternSequence[__JOINT, ___END]] :=
ROOT2[start, #] & /@ {p} /; Length[{p}] > 1;
ROOT2[start_Integer, p_JOINT] := (Sow[{start, ++i}];
ROOT2[i, Sequence @@ p[[3 ;;]]]);
ROOT2[start_Integer, p_END] := Sow[{start, ++i}];
xRot[x_] = RotationMatrix[x Degree, {1, 0, 0}];
yRot[y_] = RotationMatrix[y Degree, {0, 1, 0}];
zRot[z_] = RotationMatrix[z Degree, {0, 0, 1}];
channelToRotation[c_channel] :=
Dot @@ (c /. {xr -> xRot, yr -> yRot, zr -> zRot});
BVHGet::kwd = "This file does not seem to be a BVH file because one or more expected keywords are missing";
BVHGet::enc = "Non-ASCII codes found in file";
BVHGet::root = "File contains more than one ROOT node, which the current implementation does not handle";
BVHGet::syntax = "The file seems to contain a syntax error. It could not be parsed correctly";
BVHGet::version = "This package requires Mathematica version 10 or higher";
BVHGet[bvh_String] :=
Module[{bvhHierarchy, bvhMotion, frames, frameTime, data,
parseResult, bodyFunction, bones, jointsStack, jointCount,
boundingBox, boneCount, parseTime, frameProcessingTime},
If[$VersionNumber < 10, Message[BVHGet::version]; Return[$Failed]];
If[
AnyTrue[{"HIERARCHY", "MOTION", "ROOT", "JOINT", "END", "OFFSET",
"CHANNELS", "Zrotation", "Xrotation", "Yrotation", "Xposition",
"Yposition", "Zposition"},
StringFreeQ[bvh, #, IgnoreCase -> True] &],
Message[BVHGet::kwd]; Return[$Failed]
];
If[
StringCount[bvh, "ROOT", IgnoreCase -> True] > 1,
Message[BVHGet::root]; Return[$Failed]
];
If[
Max[ToCharacterCode[bvh]] > 127,
Message[BVHGet::enc]; Return[$Failed]
];
bvhHierarchy =
StringReplace[
bvh, ___ ~~ "HIERARCHY" ~~ start__ ~~ "MOTION" ~~ ___ :> start];
bvhMotion = StringReplace[bvh, ___ ~~ "MOTION" ~~ end___ :> end];
{frames, frameTime, data} =
tokenize[
bvhMotion] /. {___, "Frames:", frs_, "Frame", "Time:", ft_,
data__} :> {ToExpression@frs, ToExpression@ft,
Partition[Internal`StringToDouble /@ {data},
Length[{data}]/ToExpression[frs]]};
parseTime =
AbsoluteTiming[
(parseResult =
parseBrackets@parse@makeSlots@tokenize@bvhHierarchy;)
] // First;
If[
\[Not]
FreeQ[parseResult, "CHANNELS" | "OFFSET" | expr[_?NumberQ ..]],
Message[BVHGet::syntax]; Return[$Failed]
];
bodyFunction =
Function[
parseResult /. {expr -> Sequence, slot -> Slot} // Evaluate];
If[
\[Not] SyntaxQ[ToString@bodyFunction],
Message[BVHGet::syntax]; Return[$Failed]
];
bones =
Reap[bodyFunction @@ data[[1]] /. {offset -> List,
c_channel -> channelToRotation[c]} /. {"ROOT" -> ROOT2,
"JOINT" -> JOINT, "End" -> END} /. {parseBrackets[a : ___] ->
a}];
If[
\[Not] FreeQ[bones, _ROOT2 | _JOINT | _END],
Message[BVHGet::syntax]; Return[$Failed]
];
bones = bones // Last // Last;
frameProcessingTime =
AbsoluteTiming[
(jointsStack =
Table[
Reap[bodyFunction @@ data[[i]] /. {offset -> List,
c_channel -> channelToRotation[c]} /. {"ROOT" ->
ROOT, "JOINT" -> JOINT,
"End" -> END} /. {parseBrackets[a : ___] -> a}] //
Last // Last,
{i, frames}];)
] // First;
jointCount = Length[jointsStack[[1]]];
boneCount = Length[bones];
boundingBox = {Min /@ Transpose[Join @@ jointsStack],
Max /@ Transpose[Join @@ jointsStack]};
BVHData[<|"JointsStack" -> jointsStack, "Bones" -> bones,
"BoundingBox" -> boundingBox, "FrameTime" -> frameTime,
"FrameCount" -> frames, "JointCount" -> jointCount,
"BoneCount" -> boneCount, "ParseTime" -> parseTime,
"FrameProcessingTime" -> frameProcessingTime|>]
];
BVHData /: Format[b:BVHData[a_Association]] :=
RawBoxes[
BoxForm`ArrangeSummaryBox["BVHData", b,
Graphics3D[
GraphicsComplex[a[["JointsStack", a["FrameCount"]/2 // Round]],
Line /@ a["Bones"]], ViewVertical -> {0, 1, 0},
ViewPoint -> {7, 2, -7}, ImageSize -> 20,
FaceGrids -> {{0, -1, 0}}, Boxed -> False, PlotRangePadding -> 0
],
{
BoxForm`MakeSummaryItem[{"Frame count: ", a["FrameCount"]},
StandardForm],
BoxForm`MakeSummaryItem[{"Joint count: ", a["JointCount"]},
StandardForm]
},
{
BoxForm`MakeSummaryItem[{"Bone count: ", a["BoneCount"]},
StandardForm],
BoxForm`MakeSummaryItem[{"Bounding box: ", a["BoundingBox"]},
StandardForm],
BoxForm`MakeSummaryItem[{"Frame display time (s): ",
a["FrameTime"]}, StandardForm],
BoxForm`MakeSummaryItem[{"Parse time (s): ", a["ParseTime"]},
StandardForm],
BoxForm`MakeSummaryItem[{"Frame processing time (s): ",
a["FrameProcessingTime"]}, StandardForm]
}, StandardForm
]
];
BVHData[a_Association]["BoundingBox"] := a["BoundingBox"];
BVHData[a_Association]["JointsStack"] := a["JointsStack"];
BVHData[a_Association]["Bones"] := a["Bones"];
BVHData[a_Association]["FrameTime"] := a["FrameTime"];
BVHData[a_Association]["FrameCount"] := a["FrameCount"];
BVHData[a_Association]["JointCount"] := a["JointCount"];
BVHData[a_Association]["BoneCount"] := a["BoneCount"];
BVHData[a_Association]["ParseTime"] := a["ParseTime"];
BVHData[a_Association]["FrameProcessingTime"] :=
a["FrameProcessingTime"];
BVHData[a_Association]["Properties"] := {"BoundingBox", "JointsStack",
"Bones", "FrameTime", "FrameCount", "JointCount", "BoneCount",
"ParseTime", "FrameProcessingTime", "Manipulate", "AnimatedGIF",
"Properties"};
BVHData[a_Association]["Manipulate"] :=
Manipulate[
Show[
Graphics3D[
GraphicsComplex[a["JointsStack"][[i]], Line /@ a["Bones"]]],
ViewVertical -> {0, 1, 0},
ViewPoint -> {7, 2, -7},
PlotRange -> Transpose[a["BoundingBox"]],
ImageSize -> 500,
FaceGrids -> {{0, -1, 0}},
Boxed -> False, PlotRangePadding -> 0
], {{i, 1, "Frame"}, 1, a["FrameCount"], 1,
Appearance -> "Labeled"}];
BVHData[a_Association]["AnimatedGIF", fileName_String] :=
Export[fileName,
Table[
Graphics3D[
GraphicsComplex[a["JointsStack"][[i]], Line /@ a["Bones"]],
ViewVertical -> {0, 1, 0},
ViewPoint -> {7, 2, -7},
PlotRange -> Transpose[a["BoundingBox"]],
ImageSize -> 200,
FaceGrids -> {{0, -1, 0}},
Boxed -> False
], {i, 1, a["FrameCount"], 1}],
"DisplayDurations" -> a["FrameTime"],
AnimationRepetitions -> Infinity
];
ImportExport`RegisterImport["BVH", BVHImporter`BVHImport];
BVHImporter`BVHImport[filename_String] := BVHImporter`BVHGet[Import[filename, "String"]];
Unprotect[Import];
Import[name_String, opts___?OptionQ] := Import[name, "BVH", opts] /; FileExtension[name] === "bvh";
(* The code in this question (http://mathematica.stackexchange.com/q/51192/57) did not work. Neither did the answer. Wolfram support could not provide a more elegant solution so far. We use a trick here. In fact we don't have any options, but we need to add the option part to the argument template to be slightly more specific overall than an existing one that would also match. In this way we get to be evaluated before the other one, otherwise we'd be shadowed. *)
Protect[Import];
End[ ];
EndPackage[ ];
Plot3D
:)
$\endgroup$
Commented
Sep 29, 2014 at 21:59
Sometimes, the file size and number of frames of a bvh file can be very large, one might skip every other frames to reduce the file size and cut half of the number of frames. The example below, has 3602 frames and 2,559KB in size, one can cut it into half with the following codes:
srcFile = "https://www.dropbox.com/s/j4k5f4vfqb592tw/Dance01.bvh?dl=1";
cpyToFile = "c:\\images\\Dance_chop.bvh";
bvh2 = Import[srcFile, "Lines"];
MotionPos = Position[bvh2, "MOTION"] // Last // Last
bvh2[[MotionPos + 1]] =
"Frames: " <> ToString@Length[bvh2[[MotionPos + 3 ;; -1 ;; 2]] ];
Export[ cpyToFile, Catenate[ { bvh2[[1 ;; MotionPos + 2]],
bvh2[[MotionPos + 3 ;; -1 ;; 2]] }], "Table", "FieldSeparators" -> "\n" ]
The result file Dance_chop.bvh now has 1801 frames and 1,283KB in size.
And sometimes, we might prefer a continuous motion by looping back from the end to the beginning, the code below: skip every other frames first then append the reverse afterward to obtain a loop-back motion.
srcFile = "c:\\images\\Dance_chop.bvh";
cpyToFile2 = "c:\\images\\Dance_Loopback.bvh";
bvh2 = Import[srcFile, "Lines"];
MotionPos = Position[bvh2, "MOTION"] // Last // Last
bvh2[[MotionPos + 1]] = "Frames: " <> ToString[Length@
bvh2[[MotionPos + 3 ;; -1 ;; 2]] +
Length@bvh2[[-1 ;; MotionPos + 3 ;; -2]] ];
Export[cpyToFile2, Catenate[ { bvh2[[1 ;; MotionPos + 2]],
bvh2[[MotionPos + 3 ;; -1 ;; 2]], bvh2[[-1 ;; MotionPos + 3 ;; -2]] }] ,
"Table", "FieldSeparators" -> "\n" ]
The GIF below was created by using Sjoerd C. de Vries' code after chopping the original bvh file twice and applied looping-back once:
Code for creating the GIF file above:
bvh = Import["c:\\images\\Dance_Loopback2.bvh"];
gifFile = "C:\\images\\dance02.gif";
out = BVHGet[bvh] ;
Export[ gifFile,
Table[
Show[
Graphics3D[
GraphicsComplex[
out["JointsStack"][[i]],
Line /@ out["Bones"]]
],
ViewVertical -> {0, 1, 0},
ViewPoint -> {-7, 2, -7},
PlotRange -> Transpose[boundingBox],
ImageSize -> 400,
FaceGrids -> {{0, -1, 0}},
Boxed -> False],
{i, 1, 902, 1}],
"DisplayDurations" -> 1./12,
AnimationRepetitions -> Infinity]
Some of the bvh files include details of the fingers movements of the object. Like the one below, these can be deleted if one doesn't want them:
The figure can be shown as a Graph:
srcFile = "https://www.dropbox.com/s/d8wv4zurzaputvc/Look.bvh?dl=1";
bvh = Import[ srcFile, "String"];
out = BVHGet[bvh];
DirEdg = DirectedEdge @@@ out["Bones"];
bonesNo = Length@out["Bones"];
Graph[
Range[out["JointCount"]],
DirEdg,
VertexLabels -> "Name",
VertexLabelStyle -> Directive[Red, Italic, 10]
]
And use Manipulate to delete the unwant-fingers and/or bones:
Manipulate[
Module[{x},
col = ConstantArray[Blue, bonesNo ] ;
sel = ConstantArray[True, bonesNo ] ;
Dynamic@Graph[Range[bonesNo],
EventHandler[#,
{
"MouseDown" :> (col[[#2]] =
col[[#2]] /. {Red -> Blue, Blue -> Red};
sel[[#2]] = sel[[#2]] /. {True -> False, False -> True}
)
}] & @@@ Thread[{DirEdg, Range[bonesNo]}],
EdgeStyle -> (Rule[#, #2] & @@@
Thread@{
DirEdg, {Thickness[0.02], col[[#]]} & /@ Range[bonesNo] })
]],
Row[{Style["Click to toggle : ", Bold, 12], Style[Red],
Style["-> Unwant, "], Style[Blue], Style[" -> Keep ", 12]}],
Button["Confirm Change", NewBones = Pick[out["Bones"], sel] ;]
]
And change it into a Match-dancer with the latest Tube command.
NewLst = Union@Flatten@Pick[out["Bones"], sel];
forMap = MapIndexed[#1 -> k[#2 // Last] &, NewLst];
NewBonesLng = Length@NewLst;
bckMap = k[#] -> # & /@ Range[NewBonesLng];
NewBones = NewBones /. forMap /. bckMap;
NewJointsStack =
Table[Extract[out["JointsStack"][[i]], #] & /@ NewLst, {i, 1, out["FrameCount"]}];
Manipulate[
GraphPlot3D[
Graph[
Range[NewBonesLng],
DirectedEdge @@@ NewBones
],
VertexCoordinateRules -> NewJointsStack[[i]],
VertexRenderingFunction -> ({Black, Sphere[#1, 3.5]} &),
EdgeRenderingFunction -> (Tube[ #1 , 2] &),
ViewVertical -> {0, 1, 0},
ViewPoint -> {vx, 1, vy},
Boxed -> False,
ImageSize -> 300,
PlotRange -> Transpose[out["BoundingBox"]],
FaceGrids -> {{0, -1, 0}}
],
Grid[{{
Control[{{i, 1, "Frame"}, 1, out["FrameCount"], 1,
ImageSize -> Tiny, Appearance -> "Labeled"}],
Control[{{vx, -7, "xAngle"}, -10, 10, 1, ImageSize -> Tiny,
Appearance -> "Labeled"}],
Control[{{vy, 1, "yAngle"}, -5, 5, 1, ImageSize -> Tiny,
Appearance -> "Labeled"} ]}
}]
]
At last, I have to confess that most of the codes above were stolen from Sjoerd C. de Vries' answer to this question. And I would like to say thanks to him for his generous sharing. Studying his codes and surrounded with the inviting coffee aroma at one of the local Starbuck made my last few Saturday afternoons so enjoyable.