From 3b2e262821e2c8cf0485385390c73ffffcf65250 Mon Sep 17 00:00:00 2001 From: Daniel Jour Date: Fri, 22 Nov 2019 22:37:06 +0100 Subject: [PATCH] Keyframe: New implementation calculating values ondemand - GetValue() calculates the value at the given position now ondemand, without caching values as previously. First, it uses binary search to find the segment (start and end point) containing the given position. Constant and linear interpolation are straightforward, bezier curves are calculating by binary search between the end points until a point on the curve is found with a X coordinate close enough to the given position. That points Y coordinate is returned. - IsIncreasing(), GetRepeatFraction(), GetDelta() use GetValue() instead of accessing the (removed) Values vector. - Removed now unused private functions, and the unused public Process(). First test results show good performance improvements for the "rendering case" (setting up the keyframe points at once, then getting all values) and drastic performance improvements for the "preview case" (changing keyframe points, mixed with getting values). --- include/KeyFrame.h | 26 --- src/KeyFrame.cpp | 491 ++++++++++----------------------------------- 2 files changed, 105 insertions(+), 412 deletions(-) diff --git a/include/KeyFrame.h b/include/KeyFrame.h index 4ea076d7..aafd6c4b 100644 --- a/include/KeyFrame.h +++ b/include/KeyFrame.h @@ -63,25 +63,7 @@ namespace openshot { */ class Keyframe { private: - bool needs_update; - double FactorialLookup[4]; std::vector Points; ///< Vector of all Points - std::vector Values; ///< Vector of all Values (i.e. the processed coordinates from the curve) - - // Process an individual segment - void ProcessSegment(int Segment, Point p1, Point p2); - - // create lookup table for fast factorial calculation - void CreateFactorialTable(); - - // Get a factorial for a coordinate - double Factorial(int64_t n); - - // Calculate the factorial function for Bernstein basis - double Ni(int64_t n, int64_t i); - - // Calculate Bernstein Basis - double Bernstein(int64_t n, int64_t i, double t); public: @@ -155,14 +137,6 @@ namespace openshot { void SetJson(std::string value); ///< Load JSON string into this object void SetJsonValue(Json::Value root); ///< Load Json::JsonValue into this object - /** - * @brief Calculate all of the values for this keyframe. - * - * This clears any existing data in the "values" vector. This method is automatically called - * by AddPoint(), so you don't typically need to call this method. - */ - void Process(); - /// Remove a point by matching a coordinate void RemovePoint(Point p); diff --git a/src/KeyFrame.cpp b/src/KeyFrame.cpp index 79e3ca45..2cd0b35a 100644 --- a/src/KeyFrame.cpp +++ b/src/KeyFrame.cpp @@ -37,26 +37,18 @@ using namespace openshot; // Constructor which sets the default point & coordinate at X=1 -Keyframe::Keyframe(double value) : needs_update(true) { - // Init the factorial table, needed by bezier curves - CreateFactorialTable(); - +Keyframe::Keyframe(double value) { // Add initial point AddPoint(Point(value)); } // Keyframe constructor -Keyframe::Keyframe() : needs_update(true) { - // Init the factorial table, needed by bezier curves - CreateFactorialTable(); +Keyframe::Keyframe() { } // Add a new point on the key-frame. Each point has a primary coordinate, // a left handle, and a right handle. void Keyframe::AddPoint(Point p) { - // mark as dirty - needs_update = true; - // candidate is not less (greater or equal) than the new point in // the X coordinate. std::vector::iterator candidate = @@ -221,23 +213,78 @@ Point Keyframe::GetMaxPoint() { // Get the value at a specific index double Keyframe::GetValue(int64_t index) { - // Check if it needs to be processed - if (needs_update) - Process(); + if (Points.empty()) { + return 0; + } + std::vector::iterator candidate = + std::lower_bound(begin(Points), end(Points), Point(index, -1), [](Point const & l, Point const & r) { + return l.co.X < r.co.X; + }); - // Is index a valid point? - if (index >= 0 && index < Values.size()) - // Return value - return Values[index].Y; - else if (index < 0 && Values.size() > 0) - // Return the minimum value - return Values[0].Y; - else if (index >= Values.size() && Values.size() > 0) - // return the maximum value - return Values[Values.size() - 1].Y; - else - // return a blank coordinate (0,0) - return 0.0; + if (candidate == end(Points)) { + // index is behind last point + return Points.back().co.Y; + } + if (candidate == begin(Points)) { + // index is at or before first point + return Points.front().co.Y; + } + if (candidate->co.X == index) { + // index is directly on a point + return candidate->co.Y; + } + std::vector::iterator predecessor = candidate - 1; + assert(predecessor->co.X < index); + assert(index < candidate->co.X); + + // CONSTANT and LINEAR interpolations are fast to compute! + if (candidate->interpolation == CONSTANT) { + return predecessor->co.Y; + } + if (candidate->interpolation == LINEAR) { + double const diff_Y = candidate->co.Y - predecessor->co.Y; + double const diff_X = candidate->co.X - predecessor->co.X; + double const slope = diff_Y / diff_X; + return predecessor->co.Y + slope * (index - predecessor->co.X); + } + + // BEZIER curve! + // TODO: use switch instead of if for compiler warning support! + assert(candidate->interpolation == BEZIER); + + double const X_diff = candidate->co.X - predecessor->co.X; + double const Y_diff = candidate->co.Y - predecessor->co.Y; + Coordinate const p0 = predecessor->co; + Coordinate const p1 = Coordinate(p0.X + predecessor->handle_right.X * X_diff, p0.Y + predecessor->handle_right.Y * Y_diff); + Coordinate const p2 = Coordinate(p0.X + candidate->handle_left.X * X_diff, p0.Y + candidate->handle_left.Y * Y_diff); + Coordinate const p3 = candidate->co; + + double t = 0.5; + double t_step = 0.25; + do { + // Bernstein polynoms + double B[4] = {1, 3, 3, 1}; + double oneMinTExp = 1; + double tExp = 1; + for (int i = 0; i < 4; ++i, tExp *= t) { + B[i] *= tExp; + } + for (int i = 0; i < 4; ++i, oneMinTExp *= 1 - t) { + B[4 - i - 1] *= oneMinTExp; + } + double const x = p0.X * B[0] + p1.X * B[1] + p2.X * B[2] + p3.X * B[3]; + double const y = p0.Y * B[0] + p1.Y * B[1] + p2.Y * B[2] + p3.Y * B[3]; + if (abs(index - x) < 0.01) { + return y; + } + if (x > index) { + t -= t_step; + } + else { + t += t_step; + } + t_step /= 2; + } while (true); } // Get the rounded INT value at a specific index @@ -255,30 +302,17 @@ int64_t Keyframe::GetLong(int64_t index) // Get the direction of the curve at a specific index (increasing or decreasing) bool Keyframe::IsIncreasing(int index) { - // Check if it needs to be processed - if (needs_update) - Process(); - - // Is index a valid point? - if (index >= 1 && (index + 1) < Values.size()) { - int64_t current_value = GetLong(index); - int64_t next_value = 0; - - // Loop forwards and look for the next unique value - for (std::vector::iterator forwards_it = Values.begin() + (index + 1); forwards_it != Values.end(); forwards_it++) { - next_value = long(round((*forwards_it).Y)); - if (next_value != current_value) { - break; - } - } - - if (current_value >= next_value) { - // Decreasing - return false; - } + if (index < 1 || (index + 1) >= GetLength()) { + return true; } - // return default true (since most curves increase) - return true; + int64_t const current = GetLong(index); + // TODO: skip over constant sections. + do { + int64_t const next = GetLong(++index); + if (next > current) return true; + if (next < current) return false; + } while (index < GetLength()); + return false; } // Generate JSON string of this object @@ -337,10 +371,6 @@ void Keyframe::SetJson(std::string value) { // Load Json::JsonValue into this object void Keyframe::SetJsonValue(Json::Value root) { - - // mark as dirty - needs_update = true; - // Clear existing points Points.clear(); @@ -365,22 +395,15 @@ void Keyframe::SetJsonValue(Json::Value root) { // This is depreciated and will be removed soon. Fraction Keyframe::GetRepeatFraction(int64_t index) { - // Check if it needs to be processed - if (needs_update) - Process(); - // Is index a valid point? - if (index >= 1 && (index + 1) < Values.size()) { + if (index >= 1 && (index + 1) < GetLength()) { int64_t current_value = GetLong(index); - int64_t previous_value = 0; - int64_t next_value = 0; int64_t previous_repeats = 0; int64_t next_repeats = 0; // Loop backwards and look for the next unique value - for (std::vector::iterator backwards_it = Values.begin() + index; backwards_it != Values.begin(); backwards_it--) { - previous_value = long(round((*backwards_it).Y)); - if (previous_value == current_value) { + for (int64_t i = index; i > 0; --i) { + if (GetLong(i) == current_value) { // Found same value previous_repeats++; } else { @@ -390,9 +413,8 @@ Fraction Keyframe::GetRepeatFraction(int64_t index) } // Loop forwards and look for the next unique value - for (std::vector::iterator forwards_it = Values.begin() + (index + 1); forwards_it != Values.end(); forwards_it++) { - next_value = long(round((*forwards_it).Y)); - if (next_value == current_value) { + for (int64_t i = index + 1; i < GetLength(); ++i) { + if (GetLong(i) == current_value) { // Found same value next_repeats++; } else { @@ -412,40 +434,10 @@ Fraction Keyframe::GetRepeatFraction(int64_t index) // Get the change in Y value (from the previous Y value) double Keyframe::GetDelta(int64_t index) { - // Check if it needs to be processed - if (needs_update) - Process(); - - // Is index a valid point? - if (index >= 1 && (index + 1) < Values.size()) { - int64_t current_value = GetLong(index); - int64_t previous_value = 0; - int64_t previous_repeats = 0; - - // Loop backwards and look for the next unique value - for (std::vector::iterator backwards_it = Values.begin() + index; backwards_it != Values.begin(); backwards_it--) { - previous_value = long(round((*backwards_it).Y)); - if (previous_value == current_value) { - // Found same value - previous_repeats++; - } else { - // Found non repeating value, no more repeats found - break; - } - } - - // Check for matching previous value (special case for 1st element) - if (current_value == previous_value) - previous_value = 0; - - if (previous_repeats == 1) - return current_value - previous_value; - else - return 0.0; - } - else - // return a blank coordinate - return 0.0; + if (index < 1) return 0; + if (index == 1 && ! Points.empty()) return Points[0].co.Y; + if (index >= GetLength()) return 0; + return GetLong(index) - GetLong(index - 1); } // Get a point at a specific index @@ -460,18 +452,14 @@ Point const & Keyframe::GetPoint(int64_t index) { // Get the number of values (i.e. coordinates on the X axis) int64_t Keyframe::GetLength() { - // Check if it needs to be processed - if (needs_update) - Process(); - - // return the size of the Values vector - return Values.size(); + if (Points.empty()) return 0; + if (Points.size() == 1) return 1; + return round(Points.back().co.X) + 1; } // Get the number of points (i.e. # of points) int64_t Keyframe::GetCount() { - // return the size of the Values vector return Points.size(); } @@ -486,8 +474,6 @@ void Keyframe::RemovePoint(Point p) { if (p.co.X == existing_point.co.X && p.co.Y == existing_point.co.Y) { // Remove the matching point, and break out of loop Points.erase(Points.begin() + x); - // mark as dirty - needs_update = true; return; } } @@ -501,8 +487,6 @@ void Keyframe::RemovePoint(int64_t index) { // Is index a valid point? if (index >= 0 && index < Points.size()) { - // mark as dirty - needs_update = true; // Remove a specific point by index Points.erase(Points.begin() + index); } @@ -520,10 +504,6 @@ void Keyframe::UpdatePoint(int64_t index, Point p) { } void Keyframe::PrintPoints() { - // Check if it needs to be processed - if (needs_update) - Process(); - cout << fixed << setprecision(4); for (std::vector::iterator it = Points.begin(); it != Points.end(); it++) { Point p = *it; @@ -532,300 +512,39 @@ void Keyframe::PrintPoints() { } void Keyframe::PrintValues() { - // Check if it needs to be processed - if (needs_update) - Process(); - cout << fixed << setprecision(4); - cout << "Frame Number (X)\tValue (Y)\tIs Increasing\tRepeat Numerator\tRepeat Denominator\tDelta (Y Difference)" << endl; + cout << "Frame Number (X)\tValue (Y)\tIs Increasing\tRepeat Numerator\tRepeat Denominator\tDelta (Y Difference)\n"; - for (std::vector::iterator it = Values.begin() + 1; it != Values.end(); it++) { - Coordinate c = *it; - cout << long(round(c.X)) << "\t" << c.Y << "\t" << IsIncreasing(c.X) << "\t" << GetRepeatFraction(c.X).num << "\t" << GetRepeatFraction(c.X).den << "\t" << GetDelta(c.X) << endl; + for (uint64_t i = 1; i < GetLength(); ++i) { + cout << i << "\t" << GetValue(i) << "\t" << IsIncreasing(i) << "\t" ; + cout << GetRepeatFraction(i).num << "\t" << GetRepeatFraction(i).den << "\t" << GetDelta(i) << "\n"; } } -void Keyframe::Process() { - - #pragma omp critical (keyframe_process) - { - // only process if needed - if (needs_update && Points.size() == 0) { - // Clear all values - Values.clear(); - } - else if (needs_update && Points.size() > 0) - { - // Clear all values - Values.clear(); - - // fill in all values between 1 and 1st point's co.X - Point p1 = Points[0]; - if (Points.size() > 1) - // Fill in previous X values (before 1st point) - for (int64_t x = 0; x < p1.co.X; x++) - Values.push_back(Coordinate(Values.size(), p1.co.Y)); - else - // Add a single value (since we only have 1 point) - Values.push_back(Coordinate(Values.size(), p1.co.Y)); - - // Loop through each pair of points (1 less than the max points). Each - // pair of points is used to process a segment of the keyframe. - Point p2(0, 0); - for (int64_t x = 0; x < Points.size() - 1; x++) { - p1 = Points[x]; - p2 = Points[x + 1]; - - // process segment p1,p2 - ProcessSegment(x, p1, p2); - } - } - - // reset flag - needs_update = false; - } -} - -void Keyframe::ProcessSegment(int Segment, Point p1, Point p2) { - // Determine the number of values for this segment - int64_t number_of_values = round(p2.co.X) - round(p1.co.X); - - // Exit function if no values - if (number_of_values == 0) - return; - - // Based on the interpolation mode, fill the "values" vector with the coordinates - // for this segment - switch (p2.interpolation) { - - // Calculate the "values" for this segment in with a LINEAR equation, effectively - // creating a straight line with coordinates. - case LINEAR: { - // Get the difference in value - double current_value = p1.co.Y; - double value_difference = p2.co.Y - p1.co.Y; - double value_increment = 0.0f; - - // Get the increment value, but take into account the - // first segment has 1 extra value - value_increment = value_difference / (double) (number_of_values); - - if (Segment == 0) - // Add an extra value to the first segment - number_of_values++; - else - // If not 1st segment, skip the first value - current_value += value_increment; - - // Add each increment to the values vector - for (int64_t x = 0; x < number_of_values; x++) { - // add value as a coordinate to the "values" vector - Values.push_back(Coordinate(Values.size(), current_value)); - - // increment value - current_value += value_increment; - } - - break; - } - - // Calculate the "values" for this segment using a quadratic Bezier curve. This creates a - // smooth curve. - case BEZIER: { - - // Always increase the number of points by 1 (need all possible points - // to correctly calculate the curve). - number_of_values++; - number_of_values *= 4; // We need a higher resolution curve (4X) - - // Diff between points - double X_diff = p2.co.X - p1.co.X; - double Y_diff = p2.co.Y - p1.co.Y; - - std::vector segment_coordinates; - segment_coordinates.push_back(p1.co); - segment_coordinates.push_back(Coordinate(p1.co.X + (p1.handle_right.X * X_diff), p1.co.Y + (p1.handle_right.Y * Y_diff))); - segment_coordinates.push_back(Coordinate(p1.co.X + (p2.handle_left.X * X_diff), p1.co.Y + (p2.handle_left.Y * Y_diff))); - segment_coordinates.push_back(p2.co); - - std::vector raw_coordinates; - int64_t npts = segment_coordinates.size(); - int64_t icount, jcount; - double step, t; - double last_x = -1; // small number init, to track the last used x - - // Calculate points on curve - icount = 0; - t = 0; - - step = (double) 1.0 / (number_of_values - 1); - - for (int64_t i1 = 0; i1 < number_of_values; i1++) { - if ((1.0 - t) < 5e-6) - t = 1.0; - - jcount = 0; - - double new_x = 0.0f; - double new_y = 0.0f; - - for (int64_t i = 0; i < npts; i++) { - Coordinate co = segment_coordinates[i]; - double basis = Bernstein(npts - 1, i, t); - new_x += basis * co.X; - new_y += basis * co.Y; - } - - // Add new value to the vector - Coordinate current_value(new_x, new_y); - - // Add all values for 1st segment - raw_coordinates.push_back(current_value); - - // increment counters - icount += 2; - t += step; - } - - // Loop through the raw coordinates, and map them correctly to frame numbers. For example, - // we can't have duplicate X values, since X represents our frame numbers. - int64_t current_frame = p1.co.X; - double current_value = p1.co.Y; - for (int64_t i = 0; i < raw_coordinates.size(); i++) - { - // Get the raw coordinate - Coordinate raw = raw_coordinates[i]; - - if (current_frame == round(raw.X)) - // get value of raw coordinate - current_value = raw.Y; - else - { - // Missing X values (use last known Y values) - int64_t number_of_missing = round(raw.X) - current_frame; - for (int64_t missing = 0; missing < number_of_missing; missing++) - { - // Add new value to the vector - Coordinate new_coord(current_frame, current_value); - - if (Segment == 0 || (Segment > 0 && current_frame > p1.co.X)) - // Add to "values" vector - Values.push_back(new_coord); - - // Increment frame - current_frame++; - } - - // increment the current value - current_value = raw.Y; - } - } - - // Add final coordinate - Coordinate new_coord(current_frame, current_value); - Values.push_back(new_coord); - - break; - } - - // Calculate the "values" of this segment by maintaining the value of p1 until the - // last point, and then make the value jump to p2. This effectively just jumps - // the value, instead of ramping up or down the value. - case CONSTANT: { - - if (Segment == 0) - // first segment has 1 extra value - number_of_values++; - - // Add each increment to the values vector - for (int64_t x = 0; x < number_of_values; x++) { - if (x < (number_of_values - 1)) { - // Not the last value of this segment - // add coordinate to "values" - Values.push_back(Coordinate(Values.size(), p1.co.Y)); - } else { - // This is the last value of this segment - // add coordinate to "values" - Values.push_back(Coordinate(Values.size(), p2.co.Y)); - } - } - break; - } - - } -} - -// Create lookup table for fast factorial calculation -void Keyframe::CreateFactorialTable() { - // Only 4 lookups are needed, because we only support 4 coordinates per curve - FactorialLookup[0] = 1.0; - FactorialLookup[1] = 1.0; - FactorialLookup[2] = 2.0; - FactorialLookup[3] = 6.0; -} - -// Get a factorial for a coordinate -double Keyframe::Factorial(int64_t n) { - assert(n >= 0 && n <= 3); - return FactorialLookup[n]; /* returns the value n! as a SUMORealing point number */ -} - -// Calculate the factorial function for Bernstein basis -double Keyframe::Ni(int64_t n, int64_t i) { - double ni; - double a1 = Factorial(n); - double a2 = Factorial(i); - double a3 = Factorial(n - i); - ni = a1 / (a2 * a3); - return ni; -} - -// Calculate Bernstein basis -double Keyframe::Bernstein(int64_t n, int64_t i, double t) { - double basis; - double ti; /* t^i */ - double tni; /* (1 - t)^i */ - - /* Prevent problems with pow */ - if (t == 0.0 && i == 0) - ti = 1.0; - else - ti = pow(t, i); - - if (n == i && t == 1.0) - tni = 1.0; - else - tni = pow((1 - t), (n - i)); - - // Bernstein basis - basis = Ni(n, i) * ti * tni; - return basis; -} // Scale all points by a percentage (good for evenly lengthening or shortening an openshot::Keyframe) // 1.0 = same size, 1.05 = 5% increase, etc... void Keyframe::ScalePoints(double scale) { + // TODO: What if scale is small so that two points land on the + // same X coordinate? + // TODO: What if scale < 0? + // Loop through each point (skipping the 1st point) for (int64_t point_index = 1; point_index < Points.size(); point_index++) { // Scale X value Points[point_index].co.X = round(Points[point_index].co.X * scale); - - // Mark for re-processing - needs_update = true; } } // Flip all the points in this openshot::Keyframe (useful for reversing an effect or transition, etc...) void Keyframe::FlipPoints() { - // Loop through each point for (int64_t point_index = 0, reverse_index = Points.size() - 1; point_index < reverse_index; point_index++, reverse_index--) { // Flip the points using std::swap; swap(Points[point_index].co.Y, Points[reverse_index].co.Y); + // TODO: check that this has the desired effect even with + // regards to handles! } - - // Mark for re-processing - needs_update = true; }