So I thought I'd play with simplifying curves.  

If a curve is sampled at a sequence of points, you can draw an
approximation to the curve by simply connecting consecutive points
with lines; it is often possible to greatly decrease the number of
points without greatly decreasing the accuracy of the curve.

This program demonstrates one way to decrease the number of points,
simply by removing some points without disturbing their neighbors.  I
tried three methods of picking which points to remove: picking a point
at random, picking the point with the dullest angle, and picking the
point that, with its two neighbors, forms the triangle smallest in
area.  A fourth method, picking the point nearest to the line between
its two neighbors, occurred to me but was not tried.

The random removal algorithm worked surprisingly well.  The
smallest-triangle method appeared to perform better, but still had the
undesirable fault of removing sharp corners.  The dullest-angle method
does not work particularly well, but perhaps the angle computation
could keep the smallest-triangle method from removing sharp corners.

I still need to analyze these more carefully, as well as look at the
existing literature.  (My early CiteSeer searches haven't found
anything yet, but I know it's out there.)


#!/usr/local/bin/python
import Tkinter, sys, random, math

scrawls = []

class scrawl:
    def __init__(self, canvas, point):
        self.points = [point]
        self.lines = []
        self.canvas = canvas
        self.tag = "scrawl%d" % id(self)
    def addpoint(self, point):
        oldx, oldy = self.points[-1]
        newx, newy = point
        line = self.canvas.create_line(oldx, oldy, newx, newy, fill='#996666')
        self.canvas.addtag_withtag(self.tag, line)
        self.points.append(point)
        self.lines.append(line)
    def move(self, dx, dy):
        for ii in range(len(self.points)):
            x, y = self.points[ii]
            self.points[ii] = x + dx, y + dy
        # quicker way than fixlines():
        #self.canvas.move(self.tag, dx, dy)
        self.fixlines()
    def fixlines(self):
        if self.points: oldx, oldy = self.points[0]
        for ii in range(1, len(self.points)):
            x, y = self.points[ii]
            self.canvas.coords(self.lines[ii-1], oldx, oldy, x, y)
            oldx, oldy = x, y
    def twicepointarea(self, pointindex):
        "Return twice the area of the triangle made by a point and its two neighbors."
        x0, y0 = self.points[pointindex - 1]
        x1, y1 = self.points[pointindex]
        x2, y2 = self.points[pointindex + 1]
        x1, y1 = x1 - x0, y1 - y0
        x2, y2 = x2 - x0, y2 - y0
        # I wonder if this would be numerically stable in floating-point.
        return abs(x1 * y2 - x2 * y1)
    def pointanglecos(self, pointindex):
        """Return the cosine of the angle at this point.

        This is useful because when culling points, we want to cull
        points whose angles are dullest (closest to being 180 degrees)
        and furthest from being sharp (0 degrees).

        """
        x1, y1 = self.points[pointindex - 1]
        x0, y0 = self.points[pointindex]
        x2, y2 = self.points[pointindex + 1]
        x1, y1 = x1 - x0, y1 - y0
        x2, y2 = x2 - x0, y2 - y0
        if ((x1 == 0 and y1 == 0) or (x2 == 0 and y2 == 0)):
            # no point in keeping a point with a zerolength edge
            # so return dullest possible value
            return -1
        return ((x1 * x2 + y1 * y2)
                / math.sqrt(x1 * x1 + y1 * y1 + 0.0)
                / math.sqrt(x2 * x2 + y2 * y2 + 0.0))
    def smallestpoint(self):
        rv = 1
        smallestarea = self.twicepointarea(rv)
        for ii in range(2, len(self.points) - 1):
            area = self.twicepointarea(ii)
            if area < smallestarea: rv, smallestarea = ii, area
        return rv, smallestarea
    def dullestpoint(self):
        rv = 1
        dullestcos = self.pointanglecos(rv)
        for ii in range(2, len(self.points) - 1):
            cos = self.pointanglecos(ii)
            if cos < dullestcos: rv, dullestcos = ii, cos
        return rv, dullestcos
    def delpoint(self, pointindex):
        self.canvas.delete(self.lines[pointindex-1])
        del self.lines[pointindex-1]
        del self.points[pointindex]
        self.fixlines()
    def __len__(self):
        return len(self.points)

class drawer:
    def __init__(self, canvas):
        self.canvas = canvas
    def pendown(self, ev):
        self.scrawl = scrawl(self.canvas, (ev.x, ev.y))
        scrawls.append(self.scrawl)
    def move(self, ev):
        self.scrawl.addpoint((ev.x, ev.y))
    def penup(self, ev):
        self.move(ev)
        del self.scrawl

def wiggle(canvas):
    if scrawls:
        dx = random.randrange(-1, 2)
        dy = random.randrange(-1, 2)
        scrawls[-1].move(dx, dy)
    canvas.after(50, lambda canvas=canvas: wiggle(canvas))

def shorten(canvas):
    if scrawls:
        lastscrawl = scrawls[-1]
        if len(lastscrawl) > 2:
            lastscrawl.delpoint(random.randrange(1, len(lastscrawl)-1))
    canvas.after(50, lambda canvas=canvas: shorten(canvas))

areathreshold = 100
def areashorten(canvas):
    if scrawls:
        lastscrawl = scrawls[-1]
        if len(lastscrawl) > 2:
            ii, area = lastscrawl.smallestpoint()
            if area > areathreshold:
                #print "%d too big" % area
                pass
            else:
                #print "%d (%d < %s)" % (ii, area, `areathreshold`),
                #sys.stdout.flush()
                lastscrawl.delpoint(ii)
        lab['text'] = str(len(lastscrawl))
    canvas.after(50, lambda canvas=canvas: areashorten(canvas))

costhreshold = 0
def angleshorten(canvas):
    if scrawls:
        lastscrawl = scrawls[-1]
        if len(lastscrawl) > 2:
            ii, anglecos = lastscrawl.dullestpoint()
            if anglecos > costhreshold:
                #print "%d too big" % (math.acos(anglecos) * 180 / math.pi)
                pass
            else:
                #print "%d (%s < %s)" % (ii, anglecos, `costhreshold`),
                #sys.stdout.flush()
                lastscrawl.delpoint(ii)
        lab['text'] = str(len(lastscrawl))
    canvas.after(50, lambda canvas=canvas: angleshorten(canvas))

costhreshold = -1  # disable
def setthreshold(newthreshold):
    global areathreshold
    areathreshold = int(newthreshold)
def setanglethreshold(newthreshold):
    global costhreshold
    #print "newthreshold is %s" % `newthreshold`
    #print "in radians: %s" % (float(newthreshold) * math.pi/180)
    costhreshold = math.cos(float(newthreshold) * math.pi / 180)

mainwin = Tkinter.Tk()
c = Tkinter.Canvas(mainwin, background="white")
# reportpoint = lambda ev: sys.stdout.write("%s, %s\n" % (ev.x, ev.y))
mydrawer = drawer(c)
c.bind("<ButtonPress-1>", mydrawer.pendown)
c.bind("<B1-Motion>", mydrawer.move)
c.bind("<ButtonRelease-1>", mydrawer.penup)
c.pack(side='top', expand='1', fill='both')
lab = Tkinter.Label(mainwin)
lab.pack(side='left')

angleframe = Tkinter.Frame(mainwin)
angleframe.pack(expand='1', fill='both', side='top')
sllabel = Tkinter.Label(angleframe, text='Max angle:')
sl = Tkinter.Scale(angleframe, orient='horizontal', command=setanglethreshold,
                   to='0')
sl['from'] = 180
sl.set(180)
sllabel.pack(side='left')
sl.pack(expand='1', fill='both', side='top')

areaframe = Tkinter.Frame(mainwin)
areaframe.pack(expand='1', fill='both', side='top')
arealabel = Tkinter.Label(areaframe, text='Max area:')
asl = Tkinter.Scale(areaframe, orient='horizontal', command=setthreshold,
                    to='1000')
arealabel.pack(side='left')
asl.pack(expand='1', fill='both', side='top')
#wiggle(c)
areashorten(c)
angleshorten(c)
Tkinter.mainloop()


-- 
<[EMAIL PROTECTED]>       Kragen Sitaker     <http://www.pobox.com/~kragen/>
We have always been quite clear that Win95 and Win98 are not the systems to 
use if you are in a hostile security environment.  -- Paul Leach
The Internet is hostile.                -- Paul Leach <[EMAIL PROTECTED]> 


Reply via email to