Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Decompose splines with t flexibly updated #31

Merged
merged 1 commit into from
Nov 4, 2024
Merged

Conversation

weihsinyeh
Copy link
Collaborator

The evaluation testbed is font-edit, where I modified the implementation to use fixed-point arithmetic.
points_comparison
dcj_calls_comparison

The font generated by the original implementation is as follows:
original_output

The font generated by the implementation of decomposing splines with flexibly updated t is as follows:
my_way_output

@jserv jserv requested a review from jouae August 8, 2024 07:06
src/spline.c Outdated Show resolved Hide resolved
src/spline.c Outdated Show resolved Hide resolved
@jserv
Copy link
Contributor

jserv commented Aug 8, 2024

Replace "See also: #2" with "Close #2" in the git commit message, so that this issue can be closed automatically once this pull request is merged.
See https://docs.github.com/en/issues/tracking-your-work-with-issues/linking-a-pull-request-to-an-issue

Copy link
Collaborator

@jouae jouae left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Although this approach requires more computation to decompose the
spline into fewer segments, the extra effort is justified for improved
fit.

You can include statistical data such as mean, standard deviation, etc., in the PR and present it simply in the commit. Remember to mention the number of observations used.

src/spline.c Outdated Show resolved Hide resolved
@jouae
Copy link
Collaborator

jouae commented Aug 8, 2024

  • Left side one is the origin and right side one is your method
    left_origin_right_modified

I merged two GIFs and slowed down the switching speed so that more people can clearly see your contributions.

@weihsinyeh
Copy link
Collaborator Author

weihsinyeh commented Aug 8, 2024

original - Average number of _de_casteljau calls per point: 1.99
original - Average points per character: 18.89
flexibly update - Average number of _de_casteljau calls per point: 4.53
flexibly update - Average points per character: 16.30

@jserv
Copy link
Contributor

jserv commented Aug 8, 2024

Refine the git commit messages upon your experiments.

@jserv jserv requested a review from jouae August 9, 2024 20:31
@jserv
Copy link
Contributor

jserv commented Aug 15, 2024

FYI: vector-graphics/src/bezier.h is an interesting implementation.

@weihsinyeh weihsinyeh marked this pull request as draft August 18, 2024 04:43
jserv

This comment was marked as outdated.

src/spline.c Outdated Show resolved Hide resolved
@jserv
Copy link
Contributor

jserv commented Aug 19, 2024

Google's skia performs subdivision using de Casteljau's algorithm, that is, repeated linear interpolation between adjacent points. See src/base/SkBezierCurves.cpp.

jserv

This comment was marked as outdated.

jserv

This comment was marked as resolved.

jserv

This comment was marked as resolved.

@weihsinyeh
Copy link
Collaborator Author

The following is the graph to illustrate the first time of calling the function De Casteljau's is redundant calculation.
renderspline drawio (3)
I observed that when we apply shift 0.25 in the flexible setting, it will increase the average points per character. This indicates that when we limit the scope of 't' to [0, 0.25], we will fail to find some optimal point that has maximum curvature in the scope of 't' to [0.25, 0.5]. To verify it, we limit the scope of 't' to just [0,0.25]. The experiment is as follows:

Original (shift2) - Average number of _de_casteljau calls per point: 1.51
Original (shift2) - Average points per character: 18.98
Original (shift2) (limit scope) - Average number of _de_casteljau calls per point: 1.18
Original (shift2) (limit scope) - Average points per character: 22.57

Therefore, limiting the scope can decrease the average number of function '_de_casteljau' calls per point and increase the number of points. This means that these points do not always have the maximum curvature. In other words, some optimal point is located when the scope of t is [0.25, 1]. Therefore, in this pull request, we can dynamically increase the shift value without limitting the scope.

image

@weihsinyeh
Copy link
Collaborator Author

Another Idea:
There may be a new approach to rendering Bézier curves. Instead of calculating the points 'AB' 'BC' 'ABBC' in the left segment, which are colored red, we could calculate positions 'D' 'CD' 'BCCD' using only the right segment, which are colored blue.
renderspline drawio (6)

This is because after rendering this turn, we only use the outcome of the right (blue) of the original spline to render an unhandled segment of spline. The left of the original spline (red) is dropped. What's even worse, not only the left of the original spline will be dropped, but also the intermediate results will be dropped (black font).

@weihsinyeh weihsinyeh marked this pull request as ready for review November 2, 2024 12:12
@weihsinyeh
Copy link
Collaborator Author

Assess the appropriate values for tolerance when rendering B´ezier Curve.

image

The following images show the effect of four settings on 56 fonts, which are rendered using curves. Only these fonts are used to calculate the average number of _de_casteljau calls per point and the average points per character.

Original (tolerance 0.0625) :
all_font_original

Original (tolerance 0.25) :
all_font_original_tolerance_0 25

Original (shift2) :
all_font_original_shift2

Original (shift2) (tolerance 0.25) :
all_font_original_shift2_tolerance_0 25

@weihsinyeh
Copy link
Collaborator Author

weihsinyeh commented Nov 3, 2024

We perform multiple iterations each time we render a spline. In each iteration, there are multiple _de_casteljau function calls, which aim to find the most suitable shift value. The y-axis represents the number of shift attempts within one iteration. The x-axis represents the iterations of one spline. Since each spline has a different number of iterations, the iteration count is normalized to the range (0, 1). This allows us to observe the variation in the number of shift attempts at different stages of the iterations during the process of rendering a spline.
image
From the distribution above, we can see that in the original method, it takes an average of more than two shifts to find the optimal value during the first 60% of the spline rendering. It is also evident that as the rendering of a spline process reaches the later stages, the number of shifts gradually decreases. Therefore, based on the first finding, we set the initial shift to 2. Additionally, due to its gradually decreasing nature, the shifts used in different iterations of each spline will be stored as a global variable to record the value of the last shift change. This way, in the next iteration, we can directly use this value instead of starting from a shift of 2 again.

jserv

This comment was marked as duplicate.

src/spline.c Outdated Show resolved Hide resolved
Copy link
Contributor

@jserv jserv left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Squash the git commits. A single and informative one is preferable.

@weihsinyeh
Copy link
Collaborator Author

weihsinyeh commented Nov 3, 2024

Assess the appropriate values for tolerance when rendering B´ezier Curve.
image
Should I evaluate it based on my own level of acceptance of the characters under different tolerances?

@@ -91,16 +92,19 @@ static void _twin_spline_decompose(twin_path_t *path,
{
/* Draw starting point */
_twin_path_sdraw(path, spline->a.x, spline->a.y);

int shift = 2;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add comments.

The previous method for decomposing a spline into fewer segments aimed
to identify the optimal value of 't', which corresponds to the location
of maximum curvature on the curve. However, using a bisection method to
locate this point increases the average number of '_de_casteljau' calls
per point, as demonstrated in Experiment 2. Even with the early stopping
method applied, the improvement is little.

Moreover, given the high cost of finding points with the bisection
method, we have decided to abandon the flexible update of 't' using
bisection. Therefore, we can reduce computation cost by only using
shift operation to determine the coefficients of lerp in the
'_de_casteljau' function.

Based on the previous experiments, we have identified that the primary
issue is the necessity to call the '_de_casteljau' function twice to
generate a point, even under the original settings. On average, the
number of _de_casteljau calls per point is 1.99. Ideally, we want the
rendering process to call the '_de_casteljau' function only once for
each point in every iteration, indicating that the first call is
redundant.

Also, we've observed that during the initial 60 percent of the rendering
process, the original method on average requires more than two shift
attempts to determine the optimal value in every iteration. Therefore,
we propose adjusting the initial 't' value from 0.5 to 0.25 by applying
an initial shift of 2.

Additionally, as the spline rendering process progresses to the later
stages, the amount of shift gradually decreases. Hence, we store the
amount of shift used in each iteration as a global variable to record
the value of the last shift change. This way allows us to use this value
directly in the next iteration, eliminating the need to start from an
initial shift of 2 again.

Furthermore, instead of merely decreasing 't' by adding the amount of
shift, we also reduce the amount of shift to a minimum of 1. Based on
Experiment 3, this limitation of the scope can decrease the average
number of function '_de_casteljau' calls per point and increase the
number of points when rendering. This means that these points do not
always have the maximum curvature, and some optimal points with maximum
curvature will be overlooked while limiting the scope of 't' to [0,
0.25].

In summary, we have chosen the Original (shift2) setting in this pull
request, as demonstrated in Experiment 1.

The modified implementation of font-edit, which utilizes fixed-point
arithmetic, serves as the evaluation testbed for the following
experiments:

Experiment 1:
Original - Average number of _de_casteljau calls per point: 1.99
Original - Average points per character: 18.89
Original (shift2) - Average number of _de_casteljau calls per point:
1.51
Original (shift2) - Average points per character: 18.98

Experiment 2:
Flexible - Average number of _de_casteljau calls per point: 4.53
Flexible - Average points per character: 16.30
Flexible (shift2) - Average number of _de_casteljau calls per point:
4.40
Flexible (shift2) - Average points per character: 21.16
Flexible (early stopping) - Average number of _de_casteljau calls per
point: 4.23
Flexible (early stopping) - Average points per character: 16.18
Flexible (early stopping) (shift2) - Average number of _de_casteljau
calls per point: 3.99
Flexible (early stopping) (shift2) - Average points per character: 21.09

Experiment 3:
Original (shift2) (limit scope) - Average number of _de_casteljau calls
per point: 1.18
Original (shift2) (limit scope) - Average points per character: 22.57
@jserv jserv merged commit 37cab7f into sysprog21:main Nov 4, 2024
3 checks passed
@jserv
Copy link
Contributor

jserv commented Nov 4, 2024

Thank @weihsinyeh for the great work!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants