Skip to main content

Sketch Cleanup

The goal of the sketching interface is to mimic an artist's natural sketching gesture. When creating a curve on paper, an artists often draw multiple strokes with varying strengths. The software must cleanup these strokes and produce a smooth curve.

There are a lot of prior works in 2D sketch recognition and cleanup. Most methods are designed to process a complete sketch where all the strokes are drawn. Therefore, they employ heavy machinery to produce a clean curve network, with the cost of performance. In contrast, we only need to recognize one continuous curve at a time, and thus can design a more performant pipeline.

The overall stroke cleanup process works as follows:

  • For each stroke, we resample the stroke to store a smaller subset of points that better capture the geometry of the stroke.
  • We reorder the resampled point set such that the points are ordered to produce a continuous curve that best capture the underlying geometry. This is effectively a dimension reduction problem, where we convert a 2D point set into a 1D line.
  • We fit a sequence of cubic Bezier curves on the ordered points.

Resample Stroke Points

When the user creates a stroke, the device records a sequence SS of 2D points. It is unnecessary to store all the 2D points in memory, and thus we resample the points to select a subset that keeps the general geometry of the stroke. In particular, we use the Ramer-Douglas-Peucker (RDP) algorithm.

Intuitively, the RDP algorithm tries to approximate the points with a line from the first to the last point, and only select the point that is farthest away from that line approximation. Of course this is a poor approximation of anything that is not a line, so we recursively divide the points at the farthest point, and stop until the point set becomes a line. More formally:

  • We add the first point p0p_0 and the last point pnp_n in SS to the result set.
  • We divide SS into two subsets S1S_1 and S2S_2 at index ii. The index ii is the index of the point pip_i that is farthest away from the line l0nl_{0n} from p0p_0 to pnp_n.
  • If the distance from pip_i to l0nl_{0n} is greater than a parameter ฯต\epsilon, then we add pip_i to the result set, and run the algorithm recursively on S1S_1 and S2S_2. Otherwise, we stop the recursion, since the point set is approximately a line.

This algorithm can effectively retain the shape of a point set while reducing the redundant points. We store the resampled points into a tree data structure to perform fast spatial queries.

After the user completes multiple strokes, we obtain a set SS of unordered points. Since we intend to fit a sequence of Bezier curves over the points, we must first first order these points. The first ordering strategy that we employ is iterative local search. The idea is to mimic an ant jumping between the points: Starting from any point pp in SS, fit a tangent line ll over the neighborhood NN of pp, project all points piโˆˆNp_i \in N onto the line, find the points that have the largest projection, and recursively search from these points. This process is similar to how an ant samples its environment and follows the direction of the strongest smell. More formally:

  • Given point pโˆˆSp \in S, find its neighbor points NN.
  • Fit a line ll over all points in NN, and deduce a tangent vector tt.
  • For each piโˆˆNp_i \in N, find argmaxpi((piโˆ’p)โ‹…t)\text{argmax}_{p_i}((p_i - p) \cdot t). Then we recursively run the algorithm on pip_i until we visit all vertices.

To discourage the ordered points from making sharp turns, we also add a penalty to all points piโˆˆNp_i \in N that projects poorly onto the previous tangent direction.

Diffusion Map

One problem of iterative local search is that, if the algorithm makes a mistake any step along the process, for example going on a path that leads to a dead end, then there is no way to backtrack and correct the steps. To improve the algorithm, we need to use a global search approach. Therefore, we implement a diffusion map algorithm for strokes that are difficult to order. In essence, the diffusion map uses random walks to uncover the underlying geometry of the point cloud. For each point, imagine we start a random walk there and, for each step, jump to a neighbor with some probability. Then after a million steps, we get a probability cloud spreading over all points. The diffusion map essentially maps every point to its probability cloud in the eigen-basis of the random walk matrix. More formally,

  • Define a weight matrix WW where wijw_{ij} is the weight between point ii and point jj. In our case, the weight is the Gaussian of the L2 norm, namely eโˆ’โˆฃโˆฃxโˆ’yโˆฃโˆฃ2/ฯตe^{-||x-y||^2/\epsilon}, where ฯต\epsilon controls the radius of the neighborhood.
  • Normalize each row of WW to get the Markov matrix MM, i.e. M=Dโˆ’1WM = D^{-1}W where DD is a diagonal matrix of the row sum of WW. Note that MijM_{ij} is the probability of jumping from point ii to point jj at every step.
  • Note that it's not guaranteed that the Markov matrix is symmetric. Therefore, we find its conjugate S=D1/2WDโˆ’1/2S = D^{1/2}WD^{-1/2}, and get the eigenvalues ฮ›=[ฮป1,โ€ฆ,ฮปn]\Lambda = [\lambda_1, \ldots, \lambda_n] and eigenvectors V=[v1,โ€ฆ,vn]V = [v_1, \ldots, v_n] of SS such that S=Vฮ›VTS = V \Lambda V^T. Therefore, M=(Dโˆ’1/2V)ฮ›(D1/2V)TM = (D^{-1/2}V) \Lambda (D^{1/2}V)^T. We define the right eigenvector ฮฆ=Dโˆ’1/2V=[ฯ•1,โ€ฆ,ฯ•n]\Phi = D^{-1/2}V = [\phi_1, \ldots, \phi_n] and left eigenvector ฮจ=D1/2V=[ฯˆ1,โ€ฆ,ฯˆn]\Psi = D^{1/2}V = [\psi_1, \ldots, \psi_n], and each row of the Markov matrix can be written as M[i,:]=โˆ‘k=1nฮปkฯ•kฯˆkTM[i, :] = \sum_{k=1}^n \lambda_k \phi_k \psi_k^T.
  • Since the ii-th row of MtM^t is the probability distribution of the random walk after tt steps, then we can write the ii-th vertex as [ฮป1tฯ•1,โ€ฆ,ฮปntฯ•n][\lambda_1^t \phi_1, \ldots, \lambda_n^t \phi_n], where we sort the entries by the eigenvalue in decreasing order. Note that the first eigenvalue is always 1, which is a trivial state, so we ignore it. In addition, the eigenvalues toward the end is negligible, especially when tt becomes large. Therefore, we can ignore them as well. Therefore, we map the ii-th vertex to [ฮป2tฯ•2,โ€ฆ,ฮปd+1tฯ•d+1][\lambda_2^t \phi_2, \ldots, \lambda_{d+1}^t \phi_{d+1}], where dd is the target dimension. In our case, we set d=2d=2.

Finally, after we project the original points into 2D, we can easily check if the point cloud is closed or not. If the point cloud is closed, then the projection onto the eigen-basis should be shaped like a circle. Otherwise, the projection should shape like a line.

The final problem is to deal with self-intersection on the strokes. An effective solution is to modify the distance function to include differences in tangent direction for each point. Basically, for each point, we find its local tangent vector via Principal Component Analysis (PCA), and then compare the tangent vectors in addition to the positions.