├── .gitignore ├── README.md ├── examples ├── Gopkg.lock ├── Gopkg.toml ├── main.go ├── path.png └── plot.go ├── line.go ├── line_test.go ├── simplify.go └── simplify_test.go /.gitignore: -------------------------------------------------------------------------------- 1 | vendor/ -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Ramer-Douglas-Peucker Algorithm 2 | Given a curve, composed of line segments, find a similar curve with fewer points. 3 | 4 | ## Results 5 | ![result_1](./examples/path.png) 6 | 7 | ## Usage 8 | Go to `examples/` and run `dep ensure`. Then run `go install` if you don't mind the binary name is 9 | `examples`. If you do, use `go build`. 10 | 11 | go build -o rdp && ./rdp 12 | 13 | Use `SimplifyPath` to run RDP path simplfication algorithm. 14 | ```golang 15 | points = make([]rdp.Point{}, 0, 100) 16 | for range 100 { 17 | // Insert your points 18 | } 19 | 20 | threshold := 0.5 21 | results := rdp.SimplifyPath(points, threshold) 22 | ``` 23 | -------------------------------------------------------------------------------- /examples/Gopkg.lock: -------------------------------------------------------------------------------- 1 | # This file is autogenerated, do not edit; changes may be undone by the next 'dep ensure'. 2 | 3 | 4 | [[projects]] 5 | branch = "master" 6 | name = "github.com/ajstarks/svgo" 7 | packages = ["."] 8 | revision = "7338bd80e7908a1cdc427fd4d222f883b254f771" 9 | 10 | [[projects]] 11 | branch = "master" 12 | name = "github.com/golang/freetype" 13 | packages = [".","raster","truetype"] 14 | revision = "e2365dfdc4a05e4b8299a783240d4a7d5a65d4e4" 15 | 16 | [[projects]] 17 | name = "github.com/jung-kurt/gofpdf" 18 | packages = ["."] 19 | revision = "14c1db30737a138f8d9797cffea58783892b2fae" 20 | version = "v1.0.0" 21 | 22 | [[projects]] 23 | branch = "master" 24 | name = "github.com/llgcode/draw2d" 25 | packages = [".","draw2dbase","draw2dimg"] 26 | revision = "f52c8a71aff06ab8df41843d33ab167b36c971cd" 27 | 28 | [[projects]] 29 | branch = "master" 30 | name = "golang.org/x/exp" 31 | packages = ["rand"] 32 | revision = "3d87b88a115fa4e65fadcbf34e6a60e8040cec41" 33 | 34 | [[projects]] 35 | branch = "master" 36 | name = "golang.org/x/image" 37 | packages = ["draw","font","math/f64","math/fixed","tiff","tiff/lzw"] 38 | revision = "c73c2afc3b812cdd6385de5a50616511c4a3d458" 39 | 40 | [[projects]] 41 | branch = "master" 42 | name = "gonum.org/v1/gonum" 43 | packages = ["blas","blas/blas64","blas/gonum","floats","internal/asm/c128","internal/asm/f32","internal/asm/f64","internal/math32","lapack","lapack/gonum","lapack/lapack64","mat"] 44 | revision = "70492dcef1a52513768253d34010afe76412249c" 45 | 46 | [[projects]] 47 | branch = "master" 48 | name = "gonum.org/v1/plot" 49 | packages = [".","palette","plotter","plotutil","tools/bezier","vg","vg/draw","vg/fonts","vg/vgeps","vg/vgimg","vg/vgpdf","vg/vgsvg"] 50 | revision = "f52aaf5deade9478d2b8f14ed4cf0e2c16e6dc6d" 51 | 52 | [solve-meta] 53 | analyzer-name = "dep" 54 | analyzer-version = 1 55 | inputs-digest = "486784c849aac3cce55920430a04fa2959acb1c503c820f7393f14f1c345c783" 56 | solver-name = "gps-cdcl" 57 | solver-version = 1 58 | -------------------------------------------------------------------------------- /examples/Gopkg.toml: -------------------------------------------------------------------------------- 1 | 2 | # Gopkg.toml example 3 | # 4 | # Refer to https://github.com/golang/dep/blob/master/docs/Gopkg.toml.md 5 | # for detailed Gopkg.toml documentation. 6 | # 7 | # required = ["github.com/user/thing/cmd/thing"] 8 | # ignored = ["github.com/user/project/pkgX", "bitbucket.org/user/project/pkgA/pkgY"] 9 | # 10 | # [[constraint]] 11 | # name = "github.com/user/project" 12 | # version = "1.0.0" 13 | # 14 | # [[constraint]] 15 | # name = "github.com/user/project2" 16 | # branch = "dev" 17 | # source = "github.com/myfork/project2" 18 | # 19 | # [[override]] 20 | # name = "github.com/x/y" 21 | # version = "2.4.0" 22 | 23 | 24 | [[constraint]] 25 | branch = "master" 26 | name = "gonum.org/v1/plot" 27 | -------------------------------------------------------------------------------- /examples/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "rdp" 6 | 7 | "gonum.org/v1/plot/plotter" 8 | ) 9 | 10 | const Threshold = 1 11 | 12 | // ToPoints converts gonum plotter.XYs to point structs. 13 | func ToPoints(xys plotter.XYs) []rdp.Point { 14 | points := make([]rdp.Point, 0, len(xys)) 15 | for i := range xys { 16 | points = append(points, rdp.Point{X: xys[i].X, Y: xys[i].Y}) 17 | } 18 | 19 | return points 20 | } 21 | 22 | // ToXYs converts point structs to gonum plotter.XYs. 23 | func ToXYs(points []rdp.Point) plotter.XYs { 24 | xys := make(plotter.XYs, len(points)) 25 | for i := range points { 26 | xys[i].X = points[i].X 27 | xys[i].Y = points[i].Y 28 | } 29 | 30 | return xys 31 | } 32 | 33 | func main() { 34 | xys := RandomXYs(200, 0.5) 35 | simXYs := ToXYs(rdp.SimplifyPath(ToPoints(xys), Threshold)) 36 | fmt.Println("Saving plot...") 37 | if err := SavePlot(xys, simXYs); err != nil { 38 | panic(err) 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /examples/path.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/calvinfeng/rdp-path-simplification/ae464721f91c34a39c42eff3b78dd30b2573c70f/examples/path.png -------------------------------------------------------------------------------- /examples/plot.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "math" 5 | "math/rand" 6 | 7 | "gonum.org/v1/plot" 8 | "gonum.org/v1/plot/plotter" 9 | "gonum.org/v1/plot/plotutil" 10 | "gonum.org/v1/plot/vg" 11 | ) 12 | 13 | // SavePlot creates a plot of path and simplified path. 14 | func SavePlot(orig, simp plotter.XYs) error { 15 | p, err := plot.New() 16 | if err != nil { 17 | return err 18 | } 19 | 20 | p.Title.Text = "Visualize Path" 21 | p.X.Label.Text = "X" 22 | p.Y.Label.Text = "Y" 23 | 24 | err = plotutil.AddLinePoints(p, "Original Path", orig, "Simplified Path", simp) 25 | if err != nil { 26 | return err 27 | } 28 | 29 | return p.Save(14*vg.Inch, 7*vg.Inch, "path.png") 30 | } 31 | 32 | // RandomXYs generates a XYs using Sine wave with additional noises. 33 | func RandomXYs(n int, scale float64) plotter.XYs { 34 | xys := make(plotter.XYs, n) 35 | increment := float64(2*math.Pi) / float64(n) 36 | 37 | for i := range xys { 38 | xys[i].X = float64(i) * increment 39 | xys[i].Y = math.Sin(xys[i].X) + scale*rand.Float64() 40 | } 41 | 42 | return xys 43 | } 44 | -------------------------------------------------------------------------------- /line.go: -------------------------------------------------------------------------------- 1 | package rdp 2 | 3 | import "math" 4 | 5 | // Point represents a 2D point on a Cartesian plane. 6 | type Point struct { 7 | X float64 8 | Y float64 9 | } 10 | 11 | // Line represents a line segment. 12 | type Line struct { 13 | Start Point 14 | End Point 15 | } 16 | 17 | // DistanceToPoint returns the perpendicular distance of a point to the line. 18 | func (l Line) DistanceToPoint(pt Point) float64 { 19 | a, b, c := l.Coefficients() 20 | return math.Abs(a*pt.X+b*pt.Y+c) / math.Sqrt(a*a+b*b) 21 | } 22 | 23 | // Coefficients returns the three coefficients that define a line. 24 | // A line can represent by the following equation. 25 | // 26 | // ax + by + c = 0 27 | // 28 | func (l Line) Coefficients() (a, b, c float64) { 29 | a = l.Start.Y - l.End.Y 30 | b = l.End.X - l.Start.X 31 | c = l.Start.X*l.End.Y - l.End.X*l.Start.Y 32 | 33 | return a, b, c 34 | } 35 | -------------------------------------------------------------------------------- /line_test.go: -------------------------------------------------------------------------------- 1 | package rdp 2 | 3 | import "testing" 4 | 5 | func TestLine(t *testing.T) { 6 | t.Run("TestCoefficients", func(t *testing.T) { 7 | lines := []Line{ 8 | Line{Start: Point{0, 0}, End: Point{2, 2}}, 9 | Line{Start: Point{-1, 0}, End: Point{3, 5}}, 10 | Line{Start: Point{-3, -5}, End: Point{3, 7}}, 11 | } 12 | 13 | for _, l := range lines { 14 | a, b, c := l.Coefficients() 15 | 16 | // Expected slope 17 | expected := (l.End.Y - l.Start.Y) / (l.End.X - l.Start.X) 18 | 19 | slope := -1 * a / b 20 | if slope != expected { 21 | t.Error("Wrong slope", slope) 22 | } 23 | 24 | // Expected y intercept 25 | expected = l.End.Y - (expected * l.End.X) 26 | yIntercept := -1 * c / b 27 | if yIntercept != expected { 28 | t.Error("Wrong y-intercept", yIntercept) 29 | } 30 | } 31 | }) 32 | 33 | t.Run("TestDistanceToPoint", func(t *testing.T) { 34 | l := Line{Start: Point{0, 0}, End: Point{10, 0}} 35 | 36 | points := []Point{ 37 | Point{5, 5}, 38 | Point{3, 6}, 39 | Point{1, -7}, 40 | Point{-4, -10}, 41 | } 42 | 43 | expectations := []float64{5, 6, 7, 10} 44 | 45 | for i, p := range points { 46 | if expectations[i] != l.DistanceToPoint(p) { 47 | t.Errorf("%f is not equal to %f", expectations[i], l.DistanceToPoint(p)) 48 | } 49 | } 50 | }) 51 | } 52 | -------------------------------------------------------------------------------- /simplify.go: -------------------------------------------------------------------------------- 1 | package rdp 2 | 3 | // SimplifyPath accepts a list of points and epsilon as threshold, simplifies a path by dropping 4 | // points that do not pass threshold values. 5 | func SimplifyPath(points []Point, ep float64) []Point { 6 | if len(points) <= 2 { 7 | return points 8 | } 9 | 10 | l := Line{Start: points[0], End: points[len(points)-1]} 11 | 12 | idx, maxDist := seekMostDistantPoint(l, points) 13 | if maxDist >= ep { 14 | left := SimplifyPath(points[:idx+1], ep) 15 | right := SimplifyPath(points[idx:], ep) 16 | return append(left[:len(left)-1], right...) 17 | } 18 | 19 | // If the most distant point fails to pass the threshold test, then just return the two points 20 | return []Point{points[0], points[len(points)-1]} 21 | } 22 | 23 | func seekMostDistantPoint(l Line, points []Point) (idx int, maxDist float64) { 24 | for i := 0; i < len(points); i++ { 25 | d := l.DistanceToPoint(points[i]) 26 | if d > maxDist { 27 | maxDist = d 28 | idx = i 29 | } 30 | } 31 | 32 | return idx, maxDist 33 | } 34 | -------------------------------------------------------------------------------- /simplify_test.go: -------------------------------------------------------------------------------- 1 | package rdp 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestSimplifyPath(t *testing.T) { 8 | points := []Point{ 9 | Point{0, 0}, 10 | Point{1, 2}, 11 | Point{2, 7}, 12 | Point{3, 1}, 13 | Point{4, 8}, 14 | Point{5, 2}, 15 | Point{6, 8}, 16 | Point{7, 3}, 17 | Point{8, 3}, 18 | Point{9, 0}, 19 | } 20 | 21 | t.Run("Threshold=0", func(t *testing.T) { 22 | if len(SimplifyPath(points, 0)) != 10 { 23 | t.Error("simplified path should have all points") 24 | } 25 | }) 26 | 27 | t.Run("Threshold=2", func(t *testing.T) { 28 | if len(SimplifyPath(points, 2)) != 7 { 29 | t.Error("simplified path should only have 7 points") 30 | } 31 | }) 32 | 33 | t.Run("Threshold=5", func(t *testing.T) { 34 | if len(SimplifyPath(points, 100)) != 2 { 35 | t.Error("simplified path should only have two points") 36 | } 37 | }) 38 | } 39 | 40 | func TestSeekMostDistantPoint(t *testing.T) { 41 | l := Line{Start: Point{0, 0}, End: Point{10, 0}} 42 | points := []Point{ 43 | Point{13, 13}, 44 | Point{1, 15}, 45 | Point{1, 1}, 46 | Point{3, 6}, 47 | } 48 | 49 | idx, maxDist := seekMostDistantPoint(l, points) 50 | 51 | if idx != 1 { 52 | t.Error("failed to find most distant point away from a line") 53 | } 54 | 55 | if maxDist != 15 { 56 | t.Error("maximum distance is incorrect") 57 | } 58 | } 59 | --------------------------------------------------------------------------------