Skip to content
Snippets Groups Projects
mocap_tools.py 36.6 KiB
Newer Older
  • Learn to ignore specific revisions
  • # ##### BEGIN GPL LICENSE BLOCK #####
    #
    #  This program is free software; you can redistribute it and/or
    #  modify it under the terms of the GNU General Public License
    #  as published by the Free Software Foundation; either version 2
    #  of the License, or (at your option) any later version.
    #
    #  This program is distributed in the hope that it will be useful,
    #  but WITHOUT ANY WARRANTY; without even the implied warranty of
    #  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    #  GNU General Public License for more details.
    #
    #  You should have received a copy of the GNU General Public License
    #  along with this program; if not, write to the Free Software Foundation,
    #  Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
    #
    # ##### END GPL LICENSE BLOCK #####
    
    # <pep8 compliant>
    
    from math import hypot, sqrt, isfinite, radians, pi
    import bpy
    import time
    from mathutils import Vector, Matrix
    
    
    # A Python implementation of n sized Vectors.
    # Mathutils has a max size of 4, and we need at least 5 for Simplify Curves and even more for Cross Correlation.
    # Vector utility functions
    class NdVector:
        vec = []
    
        def __init__(self, vec):
            self.vec = vec[:]
    
        def __len__(self):
            return len(self.vec)
    
        def __mul__(self, otherMember):
            if (isinstance(otherMember, int) or
                isinstance(otherMember, float)):
                return NdVector([otherMember * x for x in self.vec])
            else:
                a = self.vec
                b = otherMember.vec
                n = len(self)
                return sum([a[i] * b[i] for i in range(n)])
    
        def __sub__(self, otherVec):
            a = self.vec
            b = otherVec.vec
            n = len(self)
            return NdVector([a[i] - b[i] for i in range(n)])
    
        def __add__(self, otherVec):
            a = self.vec
            b = otherVec.vec
            n = len(self)
            return NdVector([a[i] + b[i] for i in range(n)])
    
        def __div__(self, scalar):
            return NdVector([x / scalar for x in self.vec])
    
        def vecLength(self):
            return sqrt(self * self)
    
        def vecLengthSq(self):
            return (self * self)
    
        def normalize(self):
            len = self.length
            self.vec = [x / len for x in self.vec]
    
        def copy(self):
            return NdVector(self.vec)
    
        def __getitem__(self, i):
            return self.vec[i]
    
        def x(self):
            return self.vec[0]
    
        def y(self):
            return self.vec[1]
    
        def resize_2d(self):
            return Vector((self.x, self.y))
    
        length = property(vecLength)
        lengthSq = property(vecLengthSq)
        x = property(x)
        y = property(y)
    
    
    #Sampled Data Point class for Simplify Curves
    class dataPoint:
        index = 0
        # x,y1,y2,y3 coordinate of original point
        co = NdVector((0, 0, 0, 0, 0))
        #position according to parametric view of original data, [0,1] range
        u = 0
        #use this for anything
        temp = 0
    
        def __init__(self, index, co, u=0):
            self.index = index
            self.co = co
            self.u = u
    
    
    #Cross Correlation Function
    #http://en.wikipedia.org/wiki/Cross_correlation
    #IN:   curvesA, curvesB - bpy_collection/list of fcurves to analyze. Auto-Correlation is when they are the same.
    #        margin - When searching for the best "start" frame, how large a neighborhood of frames should we inspect (similar to epsilon in Calculus)
    #OUT:   startFrame, length of new anim, and curvesA
    def crossCorrelationMatch(curvesA, curvesB, margin):
        dataA = []
        dataB = []
    
    Campbell Barton's avatar
    Campbell Barton committed
        start = int(max(curvesA[0].range()[0], curvesB[0].range()[0]))
        end = int(min(curvesA[0].range()[1], curvesB[0].range()[1]))
    
    
        #transfer all fcurves data on each frame to a single NdVector.
        for i in range(1, end):
            vec = []
            for fcurve in curvesA:
                if fcurve.data_path in [otherFcurve.data_path for otherFcurve in curvesB]:
                    vec.append(fcurve.evaluate(i))
            dataA.append(NdVector(vec))
            vec = []
            for fcurve in curvesB:
                if fcurve.data_path in [otherFcurve.data_path for otherFcurve in curvesA]:
                    vec.append(fcurve.evaluate(i))
            dataB.append(NdVector(vec))
    
    135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 859 860 861 862 863 864 865 866 867 868 869 870 871 872 873 874 875 876 877 878 879 880
        #Comparator for Cross Correlation. "Classic" implementation uses dot product, as do we.
        def comp(a, b):
            return a * b
    
        #Create Rxy, which holds the Cross Correlation data.
        N = len(dataA)
        Rxy = [0.0] * N
        for i in range(N):
            for j in range(i, min(i + N, N)):
                Rxy[i] += comp(dataA[j], dataB[j - i])
            for j in range(i):
                Rxy[i] += comp(dataA[j], dataB[j - i + N])
            Rxy[i] /= float(N)
    
        #Find the Local maximums in the Cross Correlation data via numerical derivative.
        def LocalMaximums(Rxy):
            Rxyd = [Rxy[i] - Rxy[i - 1] for i in range(1, len(Rxy))]
            maxs = []
            for i in range(1, len(Rxyd) - 1):
                a = Rxyd[i - 1]
                b = Rxyd[i]
                #sign change (zerocrossing) at point i, denoting max point (only)
                if (a >= 0 and b < 0) or (a < 0 and b >= 0):
                    maxs.append((i, max(Rxy[i], Rxy[i - 1])))
            return [x[0] for x in maxs]
            #~ return max(maxs, key=lambda x: x[1])[0]
    
        #flms - the possible offsets of the first part of the animation. In Auto-Corr, this is the length of the loop.
        flms = LocalMaximums(Rxy[0:int(len(Rxy))])
        ss = []
    
        #for every local maximum, find the best one - i.e. also has the best start frame.
        for flm in flms:
            diff = []
    
            for i in range(len(dataA) - flm):
                diff.append((dataA[i] - dataB[i + flm]).lengthSq)
    
            def lowerErrorSlice(diff, e):
                #index, error at index
                bestSlice = (0, 100000)
                for i in range(e, len(diff) - e):
                    errorSlice = sum(diff[i - e:i + e + 1])
                    if errorSlice < bestSlice[1]:
                        bestSlice = (i, errorSlice, flm)
                return bestSlice
    
            s = lowerErrorSlice(diff, margin)
            ss.append(s)
    
        #Find the best result and return it.
        ss.sort(key=lambda x: x[1])
        return ss[0][2], ss[0][0], dataA
    
    
    #Uses auto correlation (cross correlation of the same set of curves) and trims the active_object's fcurves
    #Except for location curves (which in mocap tend to be not cyclic, e.g. a walk cycle forward)
    #Transfers the fcurve data to a list of NdVector (length of list is number of fcurves), and calls the cross correlation function.
    #Then trims the fcurve accordingly.
    #IN: Nothing, set the object you want as active and call. Assumes object has animation_data.action!
    #OUT: Trims the object's fcurves (except location curves).
    def autoloop_anim():
        context = bpy.context
        obj = context.active_object
    
        def locCurve(x):
            x.data_path == "location"
    
        fcurves = [x for x in obj.animation_data.action.fcurves if not locCurve(x)]
    
        margin = 10
    
        flm, s, data = crossCorrelationMatch(fcurves, fcurves, margin)
        loop = data[s:s + flm]
    
        #performs blending with a root falloff on the seam's neighborhood to ensure good tiling.
        for i in range(1, margin + 1):
            w1 = sqrt(float(i) / margin)
            loop[-i] = (loop[-i] * w1) + (loop[0] * (1 - w1))
    
        for curve in fcurves:
            pts = curve.keyframe_points
            for i in range(len(pts) - 1, -1, -1):
                pts.remove(pts[i])
    
        for c, curve in enumerate(fcurves):
            pts = curve.keyframe_points
            for i in range(len(loop)):
                pts.insert(i + 2, loop[i][c])
    
        context.scene.frame_end = flm
    
    
    #simplifyCurves: performes the bulk of the samples to bezier conversion.
    #IN:    curveGroup - which can be a collection of singleFcurves, or grouped (via nested lists) .
    #         error - threshold of permittable error (max distance) of the new beziers to the original data
    #         reparaError - threshold of error where we should try to fix the parameterization rather than split the existing curve. > error, usually by a small constant factor for best performance.
    #         maxIterations - maximum number of iterations of reparameterizations we should attempt. (Newton-Rahpson is not guarenteed to converge, so this is needed).
    #         group_mode - boolean, indicating wether we should place bezier keyframes on the same x (frame), or optimize each individual curve.
    #OUT: None. Deletes the existing curves and creates the new beziers.
    def simplifyCurves(curveGroup, error, reparaError, maxIterations, group_mode):
    
        #Calculates the unit tangent of point v
        def unitTangent(v, data_pts):
            tang = NdVector((0, 0, 0, 0, 0))
            if v != 0:
                #If it's not the first point, we can calculate a leftside tangent
                tang += data_pts[v].co - data_pts[v - 1].co
            if v != len(data_pts) - 1:
                #If it's not the last point, we can calculate a rightside tangent
                tang += data_pts[v + 1].co - data_pts[v].co
            tang.normalize()
            return tang
    
        #assign parametric u value for each point in original data, via relative arc length
        #http://en.wikipedia.org/wiki/Arc_length
        def chordLength(data_pts, s, e):
            totalLength = 0
            for pt in data_pts[s:e + 1]:
                i = pt.index
                if i == s:
                    chordLength = 0
                else:
                    chordLength = (data_pts[i].co - data_pts[i - 1].co).length
                totalLength += chordLength
                pt.temp = totalLength
            for pt in data_pts[s:e + 1]:
                if totalLength == 0:
                    print(s, e)
                pt.u = (pt.temp / totalLength)
    
        # get binomial coefficient lookup table, this function/table is only called with args
        # (3,0),(3,1),(3,2),(3,3),(2,0),(2,1),(2,2)!
        binomDict = {(3, 0): 1,
        (3, 1): 3,
        (3, 2): 3,
        (3, 3): 1,
        (2, 0): 1,
        (2, 1): 2,
        (2, 2): 1}
    
        #value at pt t of a single bernstein Polynomial
        def bernsteinPoly(n, i, t):
            binomCoeff = binomDict[(n, i)]
            return binomCoeff * pow(t, i) * pow(1 - t, n - i)
    
        # fit a single cubic to data points in range [s(tart),e(nd)].
        def fitSingleCubic(data_pts, s, e):
    
            # A - matrix used for calculating C matrices for fitting
            def A(i, j, s, e, t1, t2):
                if j == 1:
                    t = t1
                if j == 2:
                    t = t2
                u = data_pts[i].u
                return t * bernsteinPoly(3, j, u)
    
            # X component, used for calculating X matrices for fitting
            def xComponent(i, s, e):
                di = data_pts[i].co
                u = data_pts[i].u
                v0 = data_pts[s].co
                v3 = data_pts[e].co
                a = v0 * bernsteinPoly(3, 0, u)
                b = v0 * bernsteinPoly(3, 1, u)
                c = v3 * bernsteinPoly(3, 2, u)
                d = v3 * bernsteinPoly(3, 3, u)
                return (di - (a + b + c + d))
    
            t1 = unitTangent(s, data_pts)
            t2 = unitTangent(e, data_pts)
            c11 = sum([A(i, 1, s, e, t1, t2) * A(i, 1, s, e, t1, t2) for i in range(s, e + 1)])
            c12 = sum([A(i, 1, s, e, t1, t2) * A(i, 2, s, e, t1, t2) for i in range(s, e + 1)])
            c21 = c12
            c22 = sum([A(i, 2, s, e, t1, t2) * A(i, 2, s, e, t1, t2) for i in range(s, e + 1)])
    
            x1 = sum([xComponent(i, s, e) * A(i, 1, s, e, t1, t2) for i in range(s, e + 1)])
            x2 = sum([xComponent(i, s, e) * A(i, 2, s, e, t1, t2) for i in range(s, e + 1)])
    
            # calculate Determinate of the 3 matrices
            det_cc = c11 * c22 - c21 * c12
            det_cx = c11 * x2 - c12 * x1
            det_xc = x1 * c22 - x2 * c12
    
            # if matrix is not homogenous, fudge the data a bit
            if det_cc == 0:
                det_cc = 0.01
    
            # alpha's are the correct offset for bezier handles
            alpha0 = det_xc / det_cc   # offset from right (first) point
            alpha1 = det_cx / det_cc   # offset from left (last) point
    
            sRightHandle = data_pts[s].co.copy()
            sTangent = t1 * abs(alpha0)
            sRightHandle += sTangent  # position of first pt's handle
            eLeftHandle = data_pts[e].co.copy()
            eTangent = t2 * abs(alpha1)
            eLeftHandle += eTangent  # position of last pt's handle.
    
            # return a 4 member tuple representing the bezier
            return (data_pts[s].co,
                  sRightHandle,
                  eLeftHandle,
                  data_pts[e].co)
    
        # convert 2 given data points into a cubic bezier.
        # handles are offset along the tangent at
        # a 3rd of the length between the points.
        def fitSingleCubic2Pts(data_pts, s, e):
            alpha0 = alpha1 = (data_pts[s].co - data_pts[e].co).length / 3
    
            sRightHandle = data_pts[s].co.copy()
            sTangent = unitTangent(s, data_pts) * abs(alpha0)
            sRightHandle += sTangent  # position of first pt's handle
            eLeftHandle = data_pts[e].co.copy()
            eTangent = unitTangent(e, data_pts) * abs(alpha1)
            eLeftHandle += eTangent  # position of last pt's handle.
    
            #return a 4 member tuple representing the bezier
            return (data_pts[s].co,
              sRightHandle,
              eLeftHandle,
              data_pts[e].co)
    
        #evaluate bezier, represented by a 4 member tuple (pts) at point t.
        def bezierEval(pts, t):
            sumVec = NdVector((0, 0, 0, 0, 0))
            for i in range(4):
                sumVec += pts[i] * bernsteinPoly(3, i, t)
            return sumVec
    
        #calculate the highest error between bezier and original data
        #returns the distance and the index of the point where max error occurs.
        def maxErrorAmount(data_pts, bez, s, e):
            maxError = 0
            maxErrorPt = s
            if e - s < 3:
                return 0, None
            for pt in data_pts[s:e + 1]:
                bezVal = bezierEval(bez, pt.u)
                normalize_error = pt.co.length
                if normalize_error == 0:
                    normalize_error = 1
                tmpError = (pt.co - bezVal).length / normalize_error
                if tmpError >= maxError:
                    maxError = tmpError
                    maxErrorPt = pt.index
            return maxError, maxErrorPt
    
        #calculated bezier derivative at point t.
        #That is, tangent of point t.
        def getBezDerivative(bez, t):
            n = len(bez) - 1
            sumVec = NdVector((0, 0, 0, 0, 0))
            for i in range(n - 1):
                sumVec += (bez[i + 1] - bez[i]) * bernsteinPoly(n - 1, i, t)
            return sumVec
    
        #use Newton-Raphson to find a better paramterization of datapoints,
        #one that minimizes the distance (or error)
        # between bezier and original data.
        def newtonRaphson(data_pts, s, e, bez):
            for pt in data_pts[s:e + 1]:
                if pt.index == s:
                    pt.u = 0
                elif pt.index == e:
                    pt.u = 1
                else:
                    u = pt.u
                    qu = bezierEval(bez, pt.u)
                    qud = getBezDerivative(bez, u)
                    #we wish to minimize f(u),
                    #the squared distance between curve and data
                    fu = (qu - pt.co).length ** 2
                    fud = (2 * (qu.x - pt.co.x) * (qud.x)) - (2 * (qu.y - pt.co.y) * (qud.y))
                    if fud == 0:
                        fu = 0
                        fud = 1
                    pt.u = pt.u - (fu / fud)
    
        #Create data_pts, a list of dataPoint type, each is assigned index i, and an NdVector
        def createDataPts(curveGroup, group_mode):
            data_pts = []
            if group_mode:
                print([x.data_path for x in curveGroup])
                for i in range(len(curveGroup[0].keyframe_points)):
                    x = curveGroup[0].keyframe_points[i].co.x
                    y1 = curveGroup[0].evaluate(i)
                    y2 = curveGroup[1].evaluate(i)
                    y3 = curveGroup[2].evaluate(i)
                    y4 = 0
                    if len(curveGroup) == 4:
                        y4 = curveGroup[3].evaluate(i)
                    data_pts.append(dataPoint(i, NdVector((x, y1, y2, y3, y4))))
            else:
                for i in range(len(curveGroup.keyframe_points)):
                    x = curveGroup.keyframe_points[i].co.x
                    y1 = curveGroup.keyframe_points[i].co.y
                    y2 = 0
                    y3 = 0
                    y4 = 0
                    data_pts.append(dataPoint(i, NdVector((x, y1, y2, y3, y4))))
            return data_pts
    
        #Recursively fit cubic beziers to the data_pts between s and e
        def fitCubic(data_pts, s, e):
            # if there are less than 3 points, fit a single basic bezier
            if e - s < 3:
                bez = fitSingleCubic2Pts(data_pts, s, e)
            else:
                #if there are more, parameterize the points
                # and fit a single cubic bezier
                chordLength(data_pts, s, e)
                bez = fitSingleCubic(data_pts, s, e)
    
            #calculate max error and point where it occurs
            maxError, maxErrorPt = maxErrorAmount(data_pts, bez, s, e)
            #if error is small enough, reparameterization might be enough
            if maxError < reparaError and maxError > error:
                for i in range(maxIterations):
                    newtonRaphson(data_pts, s, e, bez)
                    if e - s < 3:
                        bez = fitSingleCubic2Pts(data_pts, s, e)
                    else:
                        bez = fitSingleCubic(data_pts, s, e)
    
            #recalculate max error and point where it occurs
            maxError, maxErrorPt = maxErrorAmount(data_pts, bez, s, e)
    
            #repara wasn't enough, we need 2 beziers for this range.
            #Split the bezier at point of maximum error
            if maxError > error:
                fitCubic(data_pts, s, maxErrorPt)
                fitCubic(data_pts, maxErrorPt, e)
            else:
                #error is small enough, return the beziers.
                beziers.append(bez)
                return
    
        # deletes the sampled points and creates beziers.
        def createNewCurves(curveGroup, beziers, group_mode):
            #remove all existing data points
            if group_mode:
                for fcurve in curveGroup:
                    for i in range(len(fcurve.keyframe_points) - 1, 0, -1):
                        fcurve.keyframe_points.remove(fcurve.keyframe_points[i])
            else:
                fcurve = curveGroup
                for i in range(len(fcurve.keyframe_points) - 1, 0, -1):
                    fcurve.keyframe_points.remove(fcurve.keyframe_points[i])
    
            #insert the calculated beziers to blender data.\
            if group_mode:
                for fullbez in beziers:
                    for i, fcurve in enumerate(curveGroup):
                        bez = [Vector((vec[0], vec[i + 1])) for vec in fullbez]
                        newKey = fcurve.keyframe_points.insert(frame=bez[0].x, value=bez[0].y)
                        newKey.handle_right = (bez[1].x, bez[1].y)
    
                        newKey = fcurve.keyframe_points.insert(frame=bez[3].x, value=bez[3].y)
                        newKey.handle_left = (bez[2].x, bez[2].y)
            else:
                for bez in beziers:
                    for vec in bez:
                        vec.resize_2d()
                    newKey = fcurve.keyframe_points.insert(frame=bez[0].x, value=bez[0].y)
                    newKey.handle_right = (bez[1].x, bez[1].y)
    
                    newKey = fcurve.keyframe_points.insert(frame=bez[3].x, value=bez[3].y)
                    newKey.handle_left = (bez[2].x, bez[2].y)
    
        # indices are detached from data point's frame (x) value and
        # stored in the dataPoint object, represent a range
    
        data_pts = createDataPts(curveGroup, group_mode)
    
        s = 0  # start
        e = len(data_pts) - 1  # end
    
        beziers = []
    
        #begin the recursive fitting algorithm.
        fitCubic(data_pts, s, e)
        #remove old Fcurves and insert the new ones
        createNewCurves(curveGroup, beziers, group_mode)
    
    
    #Main function of simplification, which called by Operator
    #IN:
    #       sel_opt- either "sel" (selected) or "all" for which curves to effect
    #       error- maximum error allowed, in fraction (20% = 0.0020, which is the default),
    #       i.e. divide by 10000 from percentage wanted.
    #       group_mode- boolean, to analyze each curve seperately or in groups,
    #       where a group is all curves that effect the same property/RNA path
    def fcurves_simplify(context, obj, sel_opt="all", error=0.002, group_mode=True):
        # main vars
        fcurves = obj.animation_data.action.fcurves
    
        if sel_opt == "sel":
            sel_fcurves = [fcurve for fcurve in fcurves if fcurve.select]
        else:
            sel_fcurves = fcurves[:]
    
        #Error threshold for Newton Raphson reparamatizing
        reparaError = error * 32
        maxIterations = 16
    
        if group_mode:
            fcurveDict = {}
            #this loop sorts all the fcurves into groups of 3 or 4,
            #based on their RNA Data path, which corresponds to
            #which property they effect
            for curve in sel_fcurves:
                if curve.data_path in fcurveDict:  # if this bone has been added, append the curve to its list
                    fcurveDict[curve.data_path].append(curve)
                else:
                    fcurveDict[curve.data_path] = [curve]  # new bone, add a new dict value with this first curve
            fcurveGroups = fcurveDict.values()
        else:
            fcurveGroups = sel_fcurves
    
        if error > 0.00000:
            #simplify every selected curve.
            totalt = 0
            for i, fcurveGroup in enumerate(fcurveGroups):
                print("Processing curve " + str(i + 1) + "/" + str(len(fcurveGroups)))
                t = time.clock()
                simplifyCurves(fcurveGroup, error, reparaError, maxIterations, group_mode)
                t = time.clock() - t
                print(str(t)[:5] + " seconds to process last curve")
                totalt += t
                print(str(totalt)[:5] + " seconds, total time elapsed")
    
        return
    
    
    # Implementation of non-linear median filter, with variable kernel size
    # Double pass - one marks spikes, the other smooths them
    # Expects sampled keyframes on everyframe
    # IN: None. Performs the operations on the active_object's fcurves. Expects animation_data.action to exist!
    # OUT: None. Fixes the fcurves "in-place".
    def denoise_median():
        context = bpy.context
        obj = context.active_object
        fcurves = obj.animation_data.action.fcurves
        medKernel = 1  # actually *2+1... since it this is offset
        flagKernel = 4
        highThres = (flagKernel * 2) - 1
        lowThres = 0
        for fcurve in fcurves:
            orgPts = fcurve.keyframe_points[:]
            flaggedFrames = []
            # mark frames that are spikes by sorting a large kernel
            for i in range(flagKernel, len(fcurve.keyframe_points) - flagKernel):
                center = orgPts[i]
                neighborhood = orgPts[i - flagKernel: i + flagKernel]
                neighborhood.sort(key=lambda pt: pt.co[1])
                weight = neighborhood.index(center)
                if weight >= highThres or weight <= lowThres:
                    flaggedFrames.append((i, center))
            # clean marked frames with a simple median filter
            # averages all frames in the kernel equally, except center which has no weight
            for i, pt in flaggedFrames:
                newValue = 0
                sumWeights = 0
                neighborhood = [neighpt.co[1] for neighpt in orgPts[i - medKernel: i + medKernel + 1] if neighpt != pt]
                newValue = sum(neighborhood) / len(neighborhood)
                pt.co[1] = newValue
        return
    
    
    # Recieves armature, and rotations all bones by 90 degrees along the X axis
    # This fixes the common axis issue BVH files have when importing.
    # IN: Armature (bpy.types.Armature)
    def rotate_fix_armature(arm_data):
        global_matrix = Matrix.Rotation(radians(90), 4, "X")
        bpy.ops.object.mode_set(mode='EDIT', toggle=False)
        #disconnect all bones for ease of global rotation
        connectedBones = []
        for bone in arm_data.edit_bones:
            if bone.use_connect:
                connectedBones.append(bone.name)
                bone.use_connect = False
    
        #rotate all the bones around their center
        for bone in arm_data.edit_bones:
            bone.transform(global_matrix)
    
        #reconnect the bones
        for bone in connectedBones:
            arm_data.edit_bones[bone].use_connect = True
        bpy.ops.object.mode_set(mode='OBJECT', toggle=False)
    
    
    #Roughly scales the performer armature to match the enduser armature
    #IN: perfromer_obj, enduser_obj, Blender objects whose .data is an armature.
    def scale_fix_armature(performer_obj, enduser_obj):
            perf_bones = performer_obj.data.bones
            end_bones = enduser_obj.data.bones
    
            def calculateBoundingRadius(bones):
                center = Vector()
                for bone in bones:
                    center += bone.head_local
                center /= len(bones)
                radius = 0
                for bone in bones:
                    dist = (bone.head_local - center).length
                    if dist > radius:
                        radius = dist
                return radius
    
            perf_rad = calculateBoundingRadius(performer_obj.data.bones)
            end_rad = calculateBoundingRadius(enduser_obj.data.bones)
            #end_avg = enduser_obj.dimensions
            factor = end_rad / perf_rad * 1.2
            performer_obj.scale *= factor
    
    
    #Guess Mapping
    #Given a performer and enduser armature, attempts to guess the hiearchy mapping
    def guessMapping(performer_obj, enduser_obj):
            perf_bones = performer_obj.data.bones
            end_bones = enduser_obj.data.bones
    
            root = perf_bones[0]
    
            def findBoneSide(bone):
                if "Left" in bone:
                    return "Left", bone.replace("Left", "").lower().replace(".", "")
                if "Right" in bone:
                    return "Right", bone.replace("Right", "").lower().replace(".", "")
                if "L" in bone:
                    return "Left", bone.replace("Left", "").lower().replace(".", "")
                if "R" in bone:
                    return "Right", bone.replace("Right", "").lower().replace(".", "")
                return "", bone
    
            def nameMatch(bone_a, bone_b):
                # nameMatch - recieves two strings, returns 2 if they are relatively the same, 1 if they are the same but R and L and 0 if no match at all
                side_a, noside_a = findBoneSide(bone_a)
                side_b, noside_b = findBoneSide(bone_b)
                if side_a == side_b:
                    if noside_a in noside_b or noside_b in noside_a:
                        return 2
                else:
                    if noside_a in noside_b or noside_b in noside_a:
                        return 1
                return 0
    
            def guessSingleMapping(perf_bone):
                possible_bones = [end_bones[0]]
    
                while possible_bones:
                    for end_bone in possible_bones:
                        match = nameMatch(perf_bone.name, end_bone.name)
                        if match == 2 and not perf_bone.map:
                            perf_bone.map = end_bone.name
                        #~ elif match == 1 and not perf_bone.map:
                            #~ oppo = perf_bones[oppositeBone(perf_bone)].map
                            # if oppo:
                            #   perf_bone = oppo
                    newPossibleBones = []
                    for end_bone in possible_bones:
                        newPossibleBones += list(end_bone.children)
                    possible_bones = newPossibleBones
    
                for child in perf_bone.children:
                    guessSingleMapping(child)
    
            guessSingleMapping(root)
    
    
    # Creates limit rotation constraints on the enduser armature based on range of motion (max min of fcurves) of the performer.
    # IN: context (bpy.context, etc.), and 2 blender objects which are armatures
    # OUT: creates the limit constraints.
    def limit_dof(context, performer_obj, enduser_obj):
        limitDict = {}
        perf_bones = [bone for bone in performer_obj.pose.bones if bone.bone.map]
        c_frame = context.scene.frame_current
        for bone in perf_bones:
            limitDict[bone.bone.map] = [1000, 1000, 1000, -1000, -1000, -1000]
        for t in range(context.scene.frame_start, context.scene.frame_end):
            context.scene.frame_set(t)
            for bone in perf_bones:
                end_bone = enduser_obj.pose.bones[bone.bone.map]
                bake_matrix = bone.matrix
                rest_matrix = end_bone.bone.matrix_local
    
                if end_bone.parent and end_bone.bone.use_inherit_rotation:
                    srcParent = bone.parent
                    parent_mat = srcParent.matrix
                    parent_rest = end_bone.parent.bone.matrix_local
                    parent_rest_inv = parent_rest.inverted()
                    parent_mat_inv = parent_mat.inverted()
                    bake_matrix = parent_mat_inv * bake_matrix
                    rest_matrix = parent_rest_inv * rest_matrix
    
                rest_matrix_inv = rest_matrix.inverted()
                bake_matrix = rest_matrix_inv * bake_matrix
    
                mat = bake_matrix
                euler = mat.to_euler()
                limitDict[bone.bone.map][0] = min(limitDict[bone.bone.map][0], euler.x)
                limitDict[bone.bone.map][1] = min(limitDict[bone.bone.map][1], euler.y)
                limitDict[bone.bone.map][2] = min(limitDict[bone.bone.map][2], euler.z)
                limitDict[bone.bone.map][3] = max(limitDict[bone.bone.map][3], euler.x)
                limitDict[bone.bone.map][4] = max(limitDict[bone.bone.map][4], euler.y)
                limitDict[bone.bone.map][5] = max(limitDict[bone.bone.map][5], euler.z)
        for bone in enduser_obj.pose.bones:
            existingConstraint = [constraint for constraint in bone.constraints if constraint.name == "DOF Limitation"]
            if existingConstraint:
                bone.constraints.remove(existingConstraint[0])
        end_bones = [bone for bone in enduser_obj.pose.bones if bone.name in limitDict.keys()]
        for bone in end_bones:
            #~ if not bone.is_in_ik_chain:
            newCons = bone.constraints.new("LIMIT_ROTATION")
            newCons.name = "DOF Limitation"
            newCons.owner_space = "LOCAL"
            newCons.min_x, newCons.min_y, newCons.min_z, newCons.max_x, newCons.max_y, newCons.max_z = limitDict[bone.name]
            newCons.use_limit_x = True
            newCons.use_limit_y = True
            newCons.use_limit_z = True
        context.scene.frame_set(c_frame)
    
    
    # Removes the constraints that were added by limit_dof on the enduser_obj
    def limit_dof_toggle_off(context, enduser_obj):
        for bone in enduser_obj.pose.bones:
            existingConstraint = [constraint for constraint in bone.constraints if constraint.name == "DOF Limitation"]
            if existingConstraint:
                bone.constraints.remove(existingConstraint[0])
    
    
    # Reparameterizes a blender path via keyframing it's eval_time to match a stride_object's forward velocity.
    # IN: Context, stride object (blender object with location keyframes), path object.
    def path_editing(context, stride_obj, path):
        y_fcurve = [fcurve for fcurve in stride_obj.animation_data.action.fcurves if fcurve.data_path == "location"][1]
        s, e = context.scene.frame_start, context.scene.frame_end  # y_fcurve.range()
        s = int(s)
        e = int(e)
        y_s = y_fcurve.evaluate(s)
        y_e = y_fcurve.evaluate(e)
        direction = (y_e - y_s) / abs(y_e - y_s)
        existing_cons = [constraint for constraint in stride_obj.constraints if constraint.type == "FOLLOW_PATH"]
        for cons in existing_cons:
            stride_obj.constraints.remove(cons)
        path_cons = stride_obj.constraints.new("FOLLOW_PATH")
        if direction < 0:
            path_cons.forward_axis = "TRACK_NEGATIVE_Y"
        else:
            path_cons.forward_axis = "FORWARD_Y"
        path_cons.target = path
        path_cons.use_curve_follow = True
        path.data.path_duration = e - s
        try:
            path.data.animation_data.action.fcurves
        except AttributeError:
            path.data.keyframe_insert("eval_time", frame=0)
        eval_time_fcurve = [fcurve for fcurve in path.data.animation_data.action.fcurves if fcurve.data_path == "eval_time"]
        eval_time_fcurve = eval_time_fcurve[0]
        totalLength = 0
        parameterization = {}
        print("evaluating curve")
        for t in range(s, e - 1):
            if s == t:
                chordLength = 0
            else:
                chordLength = (y_fcurve.evaluate(t) - y_fcurve.evaluate(t + 1))
            totalLength += chordLength
            parameterization[t] = totalLength
        for t in range(s + 1, e - 1):
            if totalLength == 0:
                print("no forward motion")
            parameterization[t] /= totalLength
            parameterization[t] *= e - s
        parameterization[e] = e - s
        for t in parameterization.keys():
            eval_time_fcurve.keyframe_points.insert(frame=t, value=parameterization[t])
        y_fcurve.mute = True
        print("finished path editing")
    
    
    #Animation Stitching
    #Stitches two retargeted animations together via NLA settings.
    #IN: enduser_obj, a blender armature that has had two retargets applied.
    def anim_stitch(context, enduser_obj):
        stitch_settings = enduser_obj.data.stitch_settings
        action_1 = stitch_settings.first_action
        action_2 = stitch_settings.second_action
        if stitch_settings.stick_bone != "":
            selected_bone = enduser_obj.pose.bones[stitch_settings.stick_bone]
        else:
            selected_bone = enduser_obj.pose.bones[0]
        scene = context.scene
        TrackNamesA = enduser_obj.data.mocapNLATracks[action_1]
        TrackNamesB = enduser_obj.data.mocapNLATracks[action_2]
        enduser_obj.data.active_mocap = action_1
        anim_data = enduser_obj.animation_data
        # add tracks for action 2
        mocapAction = bpy.data.actions[TrackNamesB.base_track]
        mocapTrack = anim_data.nla_tracks.new()
        mocapTrack.name = TrackNamesB.base_track
        mocapStrip = mocapTrack.strips.new(TrackNamesB.base_track, stitch_settings.blend_frame, mocapAction)
        mocapStrip.extrapolation = "HOLD_FORWARD"
        mocapStrip.blend_in = stitch_settings.blend_amount
        mocapStrip.action_frame_start += stitch_settings.second_offset
        mocapStrip.action_frame_end += stitch_settings.second_offset
        constraintTrack = anim_data.nla_tracks.new()
        constraintTrack.name = TrackNamesB.auto_fix_track
        constraintAction = bpy.data.actions[TrackNamesB.auto_fix_track]
        constraintStrip = constraintTrack.strips.new(TrackNamesB.auto_fix_track, stitch_settings.blend_frame, constraintAction)
        constraintStrip.extrapolation = "HOLD_FORWARD"
        constraintStrip.blend_in = stitch_settings.blend_amount
        userTrack = anim_data.nla_tracks.new()
        userTrack.name = TrackNamesB.manual_fix_track
        userAction = bpy.data.actions[TrackNamesB.manual_fix_track]
        userStrip = userTrack.strips.new(TrackNamesB.manual_fix_track, stitch_settings.blend_frame, userAction)
        userStrip.extrapolation = "HOLD_FORWARD"
        userStrip.blend_in = stitch_settings.blend_amount
        #stride bone
        if enduser_obj.parent:
            if enduser_obj.parent.name == "stride_bone":
                stride_bone = enduser_obj.parent
                stride_anim_data = stride_bone.animation_data
                stride_anim_data.use_nla = True
                stride_anim_data.action = None
                for track in stride_anim_data.nla_tracks:
                    stride_anim_data.nla_tracks.remove(track)
                actionATrack = stride_anim_data.nla_tracks.new()
                actionATrack.name = TrackNamesA.stride_action
                actionAStrip = actionATrack.strips.new(TrackNamesA.stride_action, 0, bpy.data.actions[TrackNamesA.stride_action])
                actionAStrip.extrapolation = "NOTHING"
                actionBTrack = stride_anim_data.nla_tracks.new()
                actionBTrack.name = TrackNamesB.stride_action
                actionBStrip = actionBTrack.strips.new(TrackNamesB.stride_action, stitch_settings.blend_frame, bpy.data.actions[TrackNamesB.stride_action])
                actionBStrip.action_frame_start += stitch_settings.second_offset
                actionBStrip.action_frame_end += stitch_settings.second_offset
                actionBStrip.extrapolation = "NOTHING"
                #we need to change the stride_bone's action to add the offset
                aStrideCurves = [fcurve for fcurve in bpy.data.actions[TrackNamesA.stride_action].fcurves if fcurve.data_path == "location"]
                bStrideCurves = [fcurve for fcurve in bpy.data.actions[TrackNamesB.stride_action].fcurves if fcurve.data_path == "location"]
                scene.frame_set(stitch_settings.blend_frame - 1)
                desired_pos = (enduser_obj.matrix_world * selected_bone.matrix.to_translation())
                scene.frame_set(stitch_settings.blend_frame)
    
    Campbell Barton's avatar
    Campbell Barton committed
                actual_pos = (enduser_obj.matrix_world * selected_bone.matrix.to_translation())
    
                print(desired_pos, actual_pos)
                offset = Vector(actual_pos) - Vector(desired_pos)
    
                for i, fcurve in enumerate(bStrideCurves):
                    print(offset[i], i, fcurve.array_index)
                    for pt in fcurve.keyframe_points:
                        pt.co.y -= offset[i]
                        pt.handle_left.y -= offset[i]
                        pt.handle_right.y -= offset[i]
    
                #actionBStrip.blend_in = stitch_settings.blend_amount
    
    
    #Guesses setting for animation stitching via Cross Correlation
    def guess_anim_stitch(context, enduser_obj):
        stitch_settings = enduser_obj.data.stitch_settings
        action_1 = stitch_settings.first_action
        action_2 = stitch_settings.second_action
        TrackNamesA = enduser_obj.data.mocapNLATracks[action_1]
        TrackNamesB = enduser_obj.data.mocapNLATracks[action_2]
        mocapA = bpy.data.actions[TrackNamesA.base_track]
        mocapB = bpy.data.actions[TrackNamesB.base_track]
        curvesA = mocapA.fcurves
        curvesB = mocapB.fcurves
        flm, s, data = crossCorrelationMatch(curvesA, curvesB, 10)
        print("Guessed the following for start and offset: ", s, flm)
        enduser_obj.data.stitch_settings.blend_frame = flm
        enduser_obj.data.stitch_settings.second_offset = s