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

Make Merge/splinefit/ApproximateSplineFromPoints* more accurate #4552

Closed
linusromer opened this issue Jan 1, 2021 · 24 comments · Fixed by #4567
Closed

Make Merge/splinefit/ApproximateSplineFromPoints* more accurate #4552

linusromer opened this issue Jan 1, 2021 · 24 comments · Fixed by #4567

Comments

@linusromer
Copy link
Contributor

linusromer commented Jan 1, 2021

This is a improvement request. When using "Merge" in FontForge, the resulting merged curve is quite inaccurate in many cases.
The following image shall illustrate this inaccurateness:
merge0
If several segments are to be merged, FontForge's algorithm (right) is even worse in comparison with my algorithm (left): (only the upper right segments have been merged)
merge2

I think splinefit.c and the ApproximateSplineFromPoint* methods are responsible for "Merge".

The main problem of FontForge's algorithm IMHO is the assumption that the point at time t of the approximation contour is near the point at time t of the original contour. But because FontForge's contour are not parameterized by arc length, this assumption is bad.

If you want you may use my algorithm: I have varied the handle lengths until the error of the approximation contour has become minimal. The error is calculated as follows:

  • calculate few (9) points inbetween the start and the end point of the original contour
  • calculate more (99) points inbetween the start and the end point of the approximation contour
  • for each of the original 9 points find the smallest squared distance to the 99 points; the sum of the squared distances is the error

I have made a quick Python plugin to test the algorithm. It seems to work okay (only the User Interface function is bad).

Example font and Python plugin in mergebezier.zip
(I have updated the plugin because I had implemented a small part which made the error calculation faster but worse in some situations. Now it really should follow the description that is already given here.)

@ctrlcctrlv
Copy link
Member

Oh, I see, this isn't Remove Overlap. Yes, I've also noticed this! I might see if I can understand what you're doing differently. I agree it'd be good to get this into FontForge :)

@linusromer
Copy link
Contributor Author

Yes, RemoveOverlap has nothing to do with it. Here is an animated GIF for better illustration (left: own algorithm, right: FF algorithm):
merge

@linusromer
Copy link
Contributor Author

@yg8ijvjvjv No, for quadratic curves there is only one possibility leaving the start direction and the end direction intact. And this possibility is already used by FontForge.

@ctrlcctrlv
Copy link
Member

Most font designers draw with cubic curves and quadratic is an afterthought. If used at all, autogenerated. I wouldn't expect quadratic drawing tools to improve any time soon.

@ctrlcctrlv
Copy link
Member

Patches welcome.

@skef
Copy link
Contributor

skef commented Jan 2, 2021

At this point FontForge mostly respects or fails to respect any given font developer by continuing to exist, and that's it. The "systematic" changes tend to be lower-hanging bug fixes; otherwise larger PRs are driven by individual contributors based on their own interests and/or needs. There's no team of developers dedicated to respecting the needs of some groups and thus disrespecting those of others.

tl;dr: what @ctrlcctrlv said.

@linusromer
Copy link
Contributor Author

Well, I have already considered writing a patch by myself. But this will need some time (maybe forever). The function I want to change is:

Spline *ApproximateSplineFromPointsSlopes(SplinePoint *from, SplinePoint *to,
	FitPoint *mid, int cnt, int order2)

which lives in splinefit.c. The struct SplinePoint is defined in splinefont.h and FitPoint in splinefit.h. mid seems to be an array of points with length cnt. The variable order2 seems to be actually a boolean which is true iff the spline is quadratic.
(Just in case you tried to figure out as well...)

@ctrlcctrlv
Copy link
Member

ctrlcctrlv commented Jan 3, 2021

You are correct about order2. What George was trying to say is that the polynomial is “of order two”, so a cubic Bézier would be “of order three”.

/* even if we are order2, do everything in order3, and then convert */

Also, in FontForge's code, 2 and 3 often have this meaning. See, for example, SplineMake2 (make quadratic spline) vs. SplineMake3. It's a common George-ism.

Bonus: For extra confusion, although George often sticks to order2 for booleans, he uses CID_IsOrder3 once in the UI. 🤭

@linusromer
Copy link
Contributor Author

Meanwhile, I have modified the function ApproximateSplineFromPointsSlopes and removed the now obsolete functions in splinefit.c. The algorithm works as described in the first post of this issue. However, I do not use 9 points on the original spline but the number of points that are given by other function calls (which seem normally just to be the number of on-curve points). So in order to get a better approximation it could be handy to add some points more before merging them... I am not sure, if I want to improve that further because it may be harder to predict how it affects other functions.
Additionally, there seemed to be a duplicate line in splinefit.h which I have removed.

I have tested it today with the newest fontforge sources from github and I did not encounter any problems with the function itself nor related functions (like simplify or ApproximateSplineFromPoints or quadratic splines). Compilations were done with make and ninja as well on a amd64 linux OS.
By the way: I was a bit surprised how fast it is in C in comparison to the Python plugin (about 10 times faster).

Have fun:
splinefit.zip

@skef
Copy link
Contributor

skef commented Jan 6, 2021

ApproximateSplineFromPointsSlopes is used by the expand stroke code via ApproximateSplineSetFromGen, so that's one subsystem that would need extensive testing if the former is significantly changed. (An improvement in the per-spline fitting algorithm would reduce the number of offset points, generally speaking.)

If it made sense to tune the number of points generated I could look at that. I think that value is currently hard-coded to 10.

@linusromer
Copy link
Contributor Author

I think 10 is only hard-coded with with expand stroke (not merge). I have forgotten about expand stroke for testing and tested it now: It feels significantly slower with my algorithm than before... I suppose, I could make ApproximateSplineFromPointsSlopes 4 times faster and even 10 times faster for half of the cases. Let me figure out this first and than I come back to your offer tuning the number of generated points. It could be that 10 points are important to figure out the self-intersecting points of the stroked path...
@skef How is the self-intersecting point of the stroked path at a high curvature point of the stroking path determined? With removeOverlap?

@ctrlcctrlv
Copy link
Member

... You could also keep two versions of the function around and use them in the appropriate places. 🤷‍♂️

@skef
Copy link
Contributor

skef commented Jan 6, 2021

If there are any self-intersecting or self-cusped shapes passed to the fitting functions they would be strange corner-cases I haven't run into. The code doesn't trace the regions between offset cusps -- it detects them and cuts off the traced shape.

It's possible that if individual "source splines" are cusped or self-intersecting you could get analogous features in the offset contours but it's less likely you'd "see" them in a single fit given that they'll be segmented by the angles determined by the nib control points. Maybe you would. I'm not sure these are cases we need to handle perfectly or even well -- including such splines in your input geometries is sort of "asking for it".

@skef
Copy link
Contributor

skef commented Jan 6, 2021

On 10 fit points: I picked this somewhat arbitrarily as a "sufficient" number. The system I was replacing was much, much slower so I didn't stress too much about tuning it for maximum performance.

ApproximateSplineSetFromGen self-evidently enforces its own fit tolerance. One risk of lowering the number of fit points is failing to detect areas that don't match the offset geometry because they aren't represented in the sample. In theory that could be addressed by splitting the sample used to fit and the sample used to check (e.g. passing four points to ApproximateSplineFromPointsSlopes but then checking all ten points against its output).

@linusromer
Copy link
Contributor Author

I have made the variation rougher i.e. slightly less exact but 4 times faster (both handles 2 times rougher makes it 2*2=4 times faster). And I have calculated the lengths of the handles where they would intersect and which makes it again about 2 times faster for generic cases in font design (i.e. the handles are on the same side of the chord and the angles of the handles to the chord are smaller than 90° each):
splinefit20210107.zip
I have tested it a bit and on my computer the speed for stroking a path is quite okay although I think it is still slower than the original version.
Maybe @ctrlcctrlv is right and I should separate merge and simplify from stroking a path but at the moment I will not look into that because I do not have the time to do so and my current splinefit patch already meets my needs.

@linusromer
Copy link
Contributor Author

Here an example how simplify works with my patch:
p-linus
The same example did not simplify anything before and when the corresponding splines are merge one got without patch:
p-ff

@linusromer
Copy link
Contributor Author

I was wrong writing that I will not look into making a separate function soon. Well, here is a new version of my patch:
splinefit20210108.zip
I have restored the original functions in splinefit.c and named my additional function ApproximateSplineFromPointsSlopesAccurate(). This additional function is mentioned in splinefit.h such that splineuitl2.c can find the function. In splineuitl2.c I have only changed one line in the function SplinesRemoveBetween() from ApproximateSplineFromPointsSlopes() to ApproximateSplineFromPointsSlopesAccurate().
This patch is probably a good compromise: "Merge" is rarely never called in a batch process and I cannot feel any speed issues when I use it (though I know it is about 10 times slower than the original function). The patch does not affect "Simplify" nor "Expand Stroke" (which are called in a batch process and need to be fast).
The only thing that is not elegant is that I have just copied the first part (cases like order2) from the original function. Maybe I should check all these cases first and call the original function then if the cases are met?

By the way, the following was a bad idea for parts that involved inflection points and I ditched it: "And I have calculated the lengths of the handles where they would intersect and which makes it again about 2 times faster for generic cases in font design."

@yg8ijvjvjv

How would the TrueType simplify work then?

I have already pointed out that "Merge" has only one solution for quadratic splines and that Fontforge is already using this solution.

@ctrlcctrlv
Copy link
Member

That's great, you should PR it. If you PR it artifacts will be built so it can get more widely tested as well.

In re Simplify command, if I were you I would add a configuration option for "more aggressive, but slower, simplification". But this isn't needed for merging IMO. I just think it'd make a good option. Others might disagree.

@skef
Copy link
Contributor

skef commented Jan 8, 2021

If this gets PR-ed I'll probably add an option to use it instead of the current function in Expand Stroke. (This is easy enough because Expand Stroke already offers numerous options so it's at most a UI headache.)

Simplify is trickier because it doesn't currently have parameters but @ctrlcctrlv's suggestion of a global config parameter seems like the way to go. We could probably add a one-off control at the python level as well.

I agree that none of this would be needed to merge the PR.

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 a pull request may close this issue.

4 participants
@ctrlcctrlv @skef @linusromer and others