Welcome to the Second Life Forums Archive

These forums are CLOSED. Please visit the new forums HERE

BVH filter program

Lear Cale
wordy bugger
Join date: 22 Aug 2007
Posts: 3,569
03-13-2008 20:09
I made an animation using QAvimator (poorly, no doubt), and wasn't surprised at how jerky the results were. So, I wrote a simple Pythong program to low-pass filter the BVH file and the results are quite pleasing!

For Python source and example input & output files, see http://learjeff.net/SL/anim:
- bvFilt.py
- f-s8.bvh - the original
- f-s8-filt.bvh - the filtered version

I'd love for someone to snag them and post a simple machinima, but I've never done that.

To use it, you have to go to python.org and install Python. There's no GUI, so copy the python source into the same directory as your anims, open a command window there (on Windows, or whatever Mac folks do to get a Linux shell there), and issue the command:

From: someone
bvFilt.py myAnim.bvh 8


where 8 is the filter constant; bigger numbers for smoother motion. It creates myAnim-filt.bvh.

No range checking on the filter constant: 1 produces the same as the input file, and smaller numbers will cause nervous fits. If it's too big, you'll see a glitch at the loop point. (For a 60-frame 30 FPS Qavimator file, max filter value seems to be about 24. The longer the animation, the more filtering you can do without getting this problem.)

Note: This program assumes the whole animation is looped (i.e., in 0%, out 100%). I haven't yet thought of a good way to handle other types without glitches.

All frames other than the first frame (T frame) are modified. Therefore, you might want to postprocess with BVHacker to avoid key frame joints that look like zero to SL; the filtering process can easily undo your hard work to manually avoid this problem (where SL thinks the joint is not used in the anim, so it fiddles with it). Careful, though, this may cause a glitch.

Enjoy!

I'd *love* it if someone embedded the simple logic into a nice GUI program like BVHacker or QAvimator. Source is free to use for any purpose other than illegal, immoral, or fattening behavior.

Is this kind of feature built into Poser? It should be, especially since the coding is trivial.
Lee Ponzu
What Would Steve Do?
Join date: 28 Jun 2006
Posts: 1,770
03-14-2008 10:59
I can't open the python file. I didn't try the .bvh files.

Permission bits maybe?
_____________________
So many monkeys, so little Shakespeare.
Lear Cale
wordy bugger
Join date: 22 Aug 2007
Posts: 3,569
03-14-2008 18:27
Hmmm, evidently my server thinks, if it's a script, don't allow downloads. I'll have to rename it to fool the damn server into thinking it's just a text file, dammit.

However, meanwhile, I can't upload anything to my server: FTP no longer works.

StartLogic has been a very reliable service up to now, so I'll cut 'em a little slack, but this is very annoying.

Also, I have to update the script anyway. It put commas in the output, which should be space-separated only. QAvimator didn't mind, but SL doesn't like it.
Lear Cale
wordy bugger
Join date: 22 Aug 2007
Posts: 3,569
03-14-2008 19:12
Here's the Python source, for anyone who's interested. To copy this source, Quote this message (as though you were going to quote it in a reply) and copy the source. That way it'll be indented properly, and Python doesn't work without correct indentation.

CODE


# Filter BVC files

# Changes:
# 0.3: filter constant specification by RC or cutoff freq
# Use as many repetitions of the anim as necessary to get 1% accuracy (overkill)

Version = "0.3"

import math
import time
import profile
import warnings

import struct


FrameCount = 0
FrameTime = 0.0

##########################################
Usage = """
%s: low-pass filter BVH animation

Usage: %s <infile> [<fc>]

where:
<infile> is the .bvh file to read.
<fc> is the filter constant, one of the following:
a number greater than 1, higher numbers yeiled more filtering
-f <number> - cutoff frequency in Hz, for DSP guys
-rc <number> - RC time constant, in sec, for hardware types

The output file is created by inserting "-filt" before the ".bvh" extension on the
intput file name.

Examples:
%s myAnim.bvh 8.0 # alpha = 1/8.0
%s myAnim.bvh -f 0.682
%s myAnim.bvh -rc 0.23333

All the above do the same thing, just different ways to specify the amount of filtering.

Version: %s
"""
##########################################


def usage(args):

import os.path
cmd = os.path.basename(args[0])
print Usage % (cmd, cmd, cmd, cmd, cmd, Version)

def copyHeader(inf, outf):
global FrameCount
global FrameTime

line = inf.readline()
while line != "MOTION\n":
outf.write(line)
line = inf.readline()
outf.write(line)

line = inf.readline() # Frames: n
outf.write(line)
FrameCount = int(line.split(":")[1])

line = inf.readline() # Frame Time: n.nnnn
outf.write(line)
print line
FrameTime = float(line.split(":")[1])


def getVals(line):

fvals = []

if line == "":
return None

strvals = line.split(" ")
for ix in range(len(strvals)):
val = strvals[ix]
fvals += [float(strvals[ix])]

return fvals

def bvhFilter(
inf, # input file
outf, # output file
invAlpha, # 1/decay constant
rc, # RC time constant (ignored unless alpha is zero)
freq, # cutoff frequency (ignored unless alpha and rc are zero)
):

global FrameCount
global FrameTime


copyHeader(inf, outf)

duration = (FrameCount - 1) * FrameTime

print " # frames =", FrameCount
print " frame time =", FrameTime, "sec"
print " duration =", duration, "sec"

if invAlpha != 0.0:
alpha = 1.0 / invAlpha
else:
if rc == 0.0:
if freq == 0.0:
print "Zero filter constant not permitted"
sys.exit(1)

rc = 1.0 / (2 * math.pi * freq)
alpha = FrameTime / (FrameTime + rc)


rc = FrameTime/alpha - FrameTime
freq = 1.0 / (2 * math.pi * rc)

print " 1/alpha =", 1.0/alpha
print " RC constant =", rc, "sec"
print " cutoff freq =", freq, "Hz"

if alpha < 0.0001:
print "Filter constant too low"
sys.exit(1)

# skip first frame
line = inf.readline()
outf.write(line)
FrameCount -= 1 # don't count the T frame

data = inf.read()

# Load the input several times to handle larger filter constants.
# We'll only use the last copy for output.


req_dur = rc * 5.0 # required duration for 1% accuracy at loop
repeats = int(math.ceil(req_dur / duration))
print " Buffer Repeats =", repeats

if repeats > 100:
print "Surely you must be joking! Little motion would remain; use a static pose."
sys.exit(1)

alldata = data
for ix in range(0, repeats):
alldata += data

import StringIO
inp = StringIO.StringIO(alldata)

frameIndex = -(repeats * FrameCount)

# preload filter with first line
fvals = getVals(inp.readline().strip())
frameIndex += 1

# For remaining lines, filter:
# fv += alpha * (iv - fv)

ivals = getVals(inp.readline().strip())
frameIndex += 1

while ivals != None:
ovals = []
for ix in range(len(fvals)):
fvals[ix] += alpha * (ivals[ix] - fvals[ix])
ovals += [str(fvals[ix])]
if 0 <= frameIndex < FrameCount:
outf.write(" ".join(ovals))
outf.write("\n")
ivals = getVals(inp.readline().strip())
frameIndex += 1

def bvFilt(args):
infname = args[1]
del args[1]

try:
inf = file(infname, "r")
except IOError, msg:
print msg
sys.exit(1)

basename = ".".join(infname.split(".")[0:-1])

outfname = basename + "-filt.bvh"
print "output file:", outfname

try:
outf = file(outfname, "w")
except IOError, msg:
print msg
sys.exit(1)

invAlpha = 0.0
freq = 0.0
rc = 0.0

if len(args) < 2:
invAlpha = 2.0
elif (args[1] == "-f"):
if (len(args) < 3):
print "Expecting filter cutoff frequency"
sys.exit(1)
freq = float(args[2])
del args[1:2]
elif (args[1] == "-rc"):
if (len(args) < 3):
print "Expecting filter time constant"
sys.exit(1)
rc = float(args[2])
del args[1:2]
else:
invAlpha = float(args[1])
del args[1]

bvhFilter(inf, outf, invAlpha, rc, freq)


if __name__ == "__main__":

import sys
if len(sys.argv) < 2:
usage(sys.argv)
else:
bvFilt(sys.argv)

Lear Cale
wordy bugger
Join date: 22 Aug 2007
Posts: 3,569
03-15-2008 06:23
Got things working with my service provider again. I've uploaded the latest version, but added a ".txt" extension so you can download it. Delete the ".txt" extension after downloading.

I also have two filtered copies of the animation, one using a filter constant of 3 and one using 8.

BTW, the program also allows you to specify the filter constant as an RC time constant (for hardware jocks) or as a cutoff frequency (for signal processing types). They all do the same thing, just different ways to specify the amount of smoothness.
Lear Cale
wordy bugger
Join date: 22 Aug 2007
Posts: 3,569
03-17-2008 12:37
Update.

I changed the filter from an IIR lowpass to an FIR (convolution), and I added the ability to add noise to the animation.

Noise can make a static pose look less static, and it can add a bit of naturalness to normal animations.

The FIR filter has two benefits.

First, it's phase-linear, meaning that the peak effect of a movement doesn't get delayed, as it did with the first filter. This will help folks doing coordinated animations.

Second, it filters the noise better. The IIR filter would jump, whereas the FIR is much smoother. For those interesed, I used a symmetric kernel with exponentially decreasing values from the midpoint, normalized for unity gain. If it sounds like I know what I'm talking about ... well, guess again :)

The RC and cutoff values are no longer accurate, but I suspect they're in the ballpark, so I left them in for now.

Same website as above, find "bvFilt1.py", and an example static pose that's been randomized.
Ravanne Sullivan
Pole Dancer Extraordinair
Join date: 10 Dec 2005
Posts: 674
03-17-2008 17:02
Getting a 500 error when trying to download the scripts.
_____________________
Ravanne's Dance Poles and Animations

Available at my Superstore and Showroom on Insula de Somni
http://slurl.com/secondlife/Insula de Somni/94/194/27/
Lear Cale
wordy bugger
Join date: 22 Aug 2007
Posts: 3,569
03-17-2008 18:14
OK, please try again. It wouldn't even allow "*.py.txt", so they're now just "*.txt" files. Rename to "*.py" after downloading.
Lear Cale
wordy bugger
Join date: 22 Aug 2007
Posts: 3,569
04-12-2008 14:41
I added a "breathing" feature to the filter program. It simply arches the back a bit and rotates shoulders to comensate. You specify the number of breaths for the animation and the depth, where 1.0 is normal breathing. Filtering happens after breathing is added, and I find that filtering using a constant of at least 4 works best.

Find the Python source as "bfFilt2.txt"; you'll need to rename it to "bvFilt.py". Also, in case it matters, I use 8 spaces per tab. Probably forgot to detab before posting the latest.

If I do any more to this thing, it would be to allow the user to select which joints to process. Sure would be nice to have a GUI for that -- any GUI programmers who are interested please pipe up!