├── examples ├── iceland.jpg ├── function.go ├── image.go └── iceland.go ├── point.go ├── function.go ├── contour.go ├── util.go ├── LICENSE.md ├── README.md ├── contourmap.go └── marching.go /examples/iceland.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fogleman/contourmap/HEAD/examples/iceland.jpg -------------------------------------------------------------------------------- /point.go: -------------------------------------------------------------------------------- 1 | package contourmap 2 | 3 | // Point represents a 2D Cartesian point. 4 | type Point struct { 5 | X, Y float64 6 | } 7 | -------------------------------------------------------------------------------- /function.go: -------------------------------------------------------------------------------- 1 | package contourmap 2 | 3 | // Function returns a height Z for the specified X, Y point in a 2D grid. 4 | type Function func(x, y int) float64 5 | -------------------------------------------------------------------------------- /contour.go: -------------------------------------------------------------------------------- 1 | package contourmap 2 | 3 | // Contour is a list of Points which define an isoline. 4 | // Contours may be open or closed. Closed contours have c[0] == c[len(c)-1]. 5 | type Contour []Point 6 | -------------------------------------------------------------------------------- /util.go: -------------------------------------------------------------------------------- 1 | package contourmap 2 | 3 | import ( 4 | "image" 5 | "image/draw" 6 | ) 7 | 8 | func imageToGray16(im image.Image) *image.Gray16 { 9 | dst := image.NewGray16(im.Bounds()) 10 | draw.Draw(dst, im.Bounds(), im, image.ZP, draw.Src) 11 | return dst 12 | } 13 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright (C) 2018 Michael Fogleman 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /examples/function.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "math" 5 | 6 | "github.com/fogleman/colormap" 7 | "github.com/fogleman/contourmap" 8 | "github.com/fogleman/gg" 9 | ) 10 | 11 | const ( 12 | W = 1600 13 | H = 1600 14 | N = 48 15 | Scale = 150 16 | ) 17 | 18 | func f(i, j int) float64 { 19 | x := (float64(i) - W/2) / Scale 20 | y := (float64(j) - H/2) / Scale 21 | return math.Sin(1.3*x)*math.Cos(0.9*y) + 22 | math.Cos(.8*x)*math.Sin(1.9*y) + 23 | math.Cos(y*.2*x) 24 | } 25 | 26 | func main() { 27 | dc := gg.NewContext(W, H) 28 | dc.SetRGB(1, 1, 1) 29 | dc.Clear() 30 | 31 | m := contourmap.FromFunction(W, H, f).Closed() 32 | z0 := m.Min 33 | z1 := m.Max 34 | for i := 0; i < N; i++ { 35 | t := float64(i) / (N - 1) 36 | z := z0 + (z1-z0)*t 37 | contours := m.Contours(z) 38 | for _, c := range contours { 39 | dc.NewSubPath() 40 | for _, p := range c { 41 | dc.LineTo(p.X, p.Y) 42 | } 43 | } 44 | dc.SetColor(colormap.Viridis.At(t)) 45 | dc.FillPreserve() 46 | dc.SetRGB(0, 0, 0) 47 | dc.SetLineWidth(3) 48 | dc.Stroke() 49 | } 50 | 51 | dc.SavePNG("out.png") 52 | } 53 | -------------------------------------------------------------------------------- /examples/image.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "os" 7 | 8 | "github.com/fogleman/contourmap" 9 | "github.com/fogleman/gg" 10 | ) 11 | 12 | const ( 13 | N = 8 14 | Scale = 1 15 | ) 16 | 17 | func main() { 18 | 19 | if len(os.Args) != 2 { 20 | log.Fatal("Usage: go run image.go input.png") 21 | } 22 | 23 | im, err := gg.LoadImage(os.Args[1]) 24 | if err != nil { 25 | log.Fatal(err) 26 | } 27 | 28 | m := contourmap.FromImage(im).Closed() 29 | // z0 := m.Min 30 | // z1 := m.Max 31 | 32 | w := int(float64(m.W) * Scale) 33 | h := int(float64(m.H) * Scale) 34 | 35 | dc := gg.NewContext(w, h) 36 | dc.SetRGB(1, 1, 1) 37 | dc.Clear() 38 | dc.Scale(Scale, Scale) 39 | 40 | zs := []float64{0.5} 41 | // for i := 1; i < N; i++ { 42 | for _, z := range zs { 43 | // t := float64(i) / N 44 | // z := z0 + (z1-z0)*t 45 | // z = 0.333 46 | // fmt.Println(i, t, z) 47 | fmt.Println(z) 48 | contours := m.Contours(z) 49 | for _, c := range contours { 50 | dc.NewSubPath() 51 | for _, p := range c { 52 | dc.LineTo(p.X, p.Y) 53 | } 54 | } 55 | // dc.SetColor(colormap.Viridis.At(t)) 56 | // dc.FillPreserve() 57 | dc.SetRGB(0, 0, 0) 58 | dc.Stroke() 59 | // break 60 | } 61 | 62 | dc.SavePNG("out.png") 63 | } 64 | -------------------------------------------------------------------------------- /examples/iceland.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | "os" 6 | 7 | "github.com/fogleman/colormap" 8 | "github.com/fogleman/contourmap" 9 | "github.com/fogleman/gg" 10 | ) 11 | 12 | const ( 13 | N = 12 14 | Scale = 1 15 | Background = "77C4D3" 16 | Palette = "70a80075ab007bb00080b30087b8008ebd0093bf009ac400a1c900a7cc00aed100b6d600bcd900c4de00cce300d2e600dbeb00e1ed00eaf200f3f700fafa00ffff05ffff12ffff1cffff29ffff36ffff42ffff4fffff5cffff66ffff73ffff80ffff8cffff99ffffa3ffffb0ffffbdffffc9ffffd6ffffe3ffffedfffffafcfcfcf7f7f7f5f5f5f0f0f0edededebebebe6e6e6e3e3e3dedededbdbdbd6d6d6d4d4d4cfcfcfccccccc7c7c7c4c4c4c2c2c2bdbdbdbababab5b5b5b3b3b3b3b3b3" 17 | ) 18 | 19 | func main() { 20 | if len(os.Args) != 2 { 21 | log.Fatal("Usage: go run iceland.go iceland.jpg") 22 | } 23 | 24 | im, err := gg.LoadImage(os.Args[1]) 25 | if err != nil { 26 | log.Fatal(err) 27 | } 28 | 29 | m := contourmap.FromImage(im).Closed() 30 | z0 := m.Min 31 | z1 := m.Max 32 | 33 | w := int(float64(m.W) * Scale) 34 | h := int(float64(m.H) * Scale) 35 | 36 | dc := gg.NewContext(w, h) 37 | dc.SetRGB(1, 1, 1) 38 | dc.SetColor(colormap.ParseColor(Background)) 39 | dc.Clear() 40 | dc.Scale(Scale, Scale) 41 | 42 | pal := colormap.New(colormap.ParseColors(Palette)) 43 | for i := 0; i < N; i++ { 44 | t := float64(i) / (N - 1) 45 | z := z0 + (z1-z0)*t 46 | contours := m.Contours(z + 1e-9) 47 | for _, c := range contours { 48 | dc.NewSubPath() 49 | for _, p := range c { 50 | dc.LineTo(p.X, p.Y) 51 | } 52 | } 53 | dc.SetColor(pal.At(t)) 54 | dc.FillPreserve() 55 | dc.SetRGB(0, 0, 0) 56 | dc.SetLineWidth(1) 57 | dc.Stroke() 58 | } 59 | 60 | dc.SavePNG("out.png") 61 | } 62 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## contourmap 2 | 3 | Compute contour lines (isolines) for any 2D data in Go. 4 | 5 | ### Installation 6 | 7 | go get -u github.com/fogleman/contourmap 8 | 9 | ### Documentation 10 | 11 | https://godoc.org/github.com/fogleman/contourmap 12 | 13 | ### Example Usage 14 | 15 | #### Creating a ContourMap 16 | 17 | A new `ContourMap` can be generated in many different ways, depending on what type of data you have. 18 | 19 | Use `FromFloat64s` if you have an array of numbers. The length of the array must equal `width * height`. The two-dimensional data is stored in a flat array in row-major order. 20 | 21 | ```go 22 | m := contourmap.FromFloat64s(width, height, data) 23 | ``` 24 | 25 | Use `FromImage` if you have an `image.Image`, such as a grayscale heightmap. 26 | 27 | ```go 28 | m := contourmap.FromImage(im) 29 | ``` 30 | 31 | Use `FromFunction` to specify an arbitrary function that will provide a Z for any given X, Y coordinate. 32 | The function will be called for all points `x = [0, w)` and `y = [0, h)` to determine the Z value at each point in the grid. 33 | 34 | ```go 35 | var f func(x, y int) float64 36 | ... 37 | m := contourmap.FromFunction(width, height, f) 38 | ``` 39 | 40 | #### Finding Contour Lines 41 | 42 | Once your `ContourMap` is created, you can use the `Contours` function to find isolines at any given Z height. This function returns a list of contours where each contour is a list of X, Y points. 43 | A `Contour` may be open or closed. Closed contours have `c[0] == c[len(c)-1]`. 44 | 45 | ```go 46 | contours := m.Contours(z) 47 | for _, contour := range contours { 48 | for _, point := range contour { 49 | // do something with points... 50 | fmt.Println(point.X, point.Y) 51 | } 52 | } 53 | ``` 54 | 55 | #### Closing Contours at the Grid Perimeter 56 | 57 | Contours may end at the edge of the grid data, forming open contours. If you want to force all contours to be closed by following the perimeter of the grid, you can use `ContourMap.Closed` which will generate a new ContourMap that can be used for this purpose: 58 | 59 | ```go 60 | m = m.Closed() // now all contours will be closed paths 61 | ``` 62 | 63 | ### Examples 64 | 65 | Some examples are included to help you get started. 66 | 67 | $ cd go/src/github.com/fogleman/contourmap/examples 68 | $ go run iceland.go iceland.jpg 69 | 70 | ![Iceland Example](https://i.imgur.com/fd7fUnt.png) 71 | 72 | $ go run examples/function.go 73 | 74 | ![Function Example](https://i.imgur.com/lbGXPC9.png) 75 | 76 | -------------------------------------------------------------------------------- /contourmap.go: -------------------------------------------------------------------------------- 1 | package contourmap 2 | 3 | import ( 4 | "image" 5 | "math" 6 | "sort" 7 | ) 8 | 9 | type ContourMap struct { 10 | W int // width of the contour map in pixels 11 | H int // height of the contour map in pixels 12 | Min float64 // minimum value contained in this contour map 13 | Max float64 // maximum value contained in this contour map 14 | grid []float64 15 | } 16 | 17 | // FromFloat64s returns a new ContourMap for the provided 2D grid of values. 18 | // len(grid) must equal w * h. 19 | func FromFloat64s(w, h int, grid []float64) *ContourMap { 20 | min := math.Inf(1) 21 | max := math.Inf(-1) 22 | for _, x := range grid { 23 | if x == closed { 24 | continue 25 | } 26 | min = math.Min(min, x) 27 | max = math.Max(max, x) 28 | } 29 | return &ContourMap{w, h, min, max, grid} 30 | } 31 | 32 | // FromFloat64s returns a new ContourMap for the provided function. 33 | // The function will be called for all points x = [0, w) and y = [0, h) to 34 | // determine the Z value at each point. 35 | func FromFunction(w, h int, f Function) *ContourMap { 36 | grid := make([]float64, w*h) 37 | i := 0 38 | for y := 0; y < h; y++ { 39 | for x := 0; x < w; x++ { 40 | grid[i] = f(x, y) 41 | i++ 42 | } 43 | } 44 | return FromFloat64s(w, h, grid) 45 | } 46 | 47 | // FromImage returns a new ContourMap for the provided image. The image is 48 | // converted to 16-bit grayscale and will have Z values mapped from 49 | // [0, 65535] to [0, 1]. 50 | func FromImage(im image.Image) *ContourMap { 51 | gray := imageToGray16(im) 52 | w := gray.Bounds().Size().X 53 | h := gray.Bounds().Size().Y 54 | grid := make([]float64, w*h) 55 | j := 0 56 | for i := range grid { 57 | x := int(gray.Pix[j])<<8 | int(gray.Pix[j+1]) 58 | grid[i] = float64(x) / 0xffff 59 | j += 2 60 | } 61 | return FromFloat64s(w, h, grid) 62 | } 63 | 64 | func (m *ContourMap) at(x, y int) float64 { 65 | return m.grid[y*m.W+x] 66 | } 67 | 68 | func (m *ContourMap) HistogramZs(numLevels int) []float64 { 69 | // compute histogram 70 | hist := make(map[float64]int) 71 | for _, v := range m.grid { 72 | hist[v]++ 73 | } 74 | 75 | // sort histogram keys 76 | keys := make([]float64, 0, len(hist)) 77 | for key := range hist { 78 | keys = append(keys, key) 79 | } 80 | sort.Float64s(keys) 81 | 82 | result := make([]float64, numLevels) 83 | numPixels := len(m.grid) 84 | for i := 0; i < numLevels; i++ { 85 | // compute number of pixels for this level 86 | t := (float64(i) + 0.5) / float64(numLevels) 87 | pixelCount := int(t * float64(numPixels)) 88 | // find z 89 | var total int 90 | for _, k := range keys { 91 | total += hist[k] 92 | if total >= pixelCount { 93 | result[i] = k 94 | break 95 | } 96 | } 97 | } 98 | return result 99 | } 100 | 101 | // Contours returns a list of contours that represent isolines at the specified 102 | // Z value. 103 | func (m *ContourMap) Contours(z float64) []Contour { 104 | return marchingSquares(m, m.W, m.H, z) 105 | } 106 | 107 | // Closed returns a new ContourMap that will ensure all Contours are closed 108 | // paths by following the border when they would normally stop at the edge 109 | // of the grid. 110 | func (m *ContourMap) Closed() *ContourMap { 111 | w := m.W + 2 112 | h := m.H + 2 113 | grid := make([]float64, w*h) 114 | for i := range grid { 115 | grid[i] = closed 116 | } 117 | for y := 0; y < m.H; y++ { 118 | i := (y+1)*w + 1 119 | j := y * m.W 120 | copy(grid[i:], m.grid[j:j+m.W]) 121 | } 122 | return FromFloat64s(w, h, grid) 123 | } 124 | -------------------------------------------------------------------------------- /marching.go: -------------------------------------------------------------------------------- 1 | package contourmap 2 | 3 | import ( 4 | "math" 5 | ) 6 | 7 | const closed = -math.MaxFloat64 8 | 9 | type edge struct { 10 | X0, Y0 int 11 | X1, Y1 int 12 | Boundary bool 13 | } 14 | 15 | func fraction(z0, z1, z float64) float64 { 16 | const eps = 1e-9 17 | var f float64 18 | if z0 == closed { 19 | f = 0 20 | } else if z1 == closed { 21 | f = 1 22 | } else if z0 != z1 { 23 | f = (z - z0) / (z1 - z0) 24 | } 25 | f = math.Max(f, eps) 26 | f = math.Min(f, 1-eps) 27 | return f 28 | } 29 | 30 | func marchingSquares(m *ContourMap, w, h int, z float64) []Contour { 31 | edgePoint := make(map[edge]Point) 32 | nextEdge := make(map[Point]edge) 33 | for y := 0; y < h-1; y++ { 34 | up := m.at(0, y) 35 | lp := m.at(0, y+1) 36 | for x := 0; x < w-1; x++ { 37 | ul := up 38 | ur := m.at(x+1, y) 39 | ll := lp 40 | lr := m.at(x+1, y+1) 41 | 42 | up = ur 43 | lp = lr 44 | 45 | var squareCase int 46 | if ul > z { 47 | squareCase |= 1 48 | } 49 | if ur > z { 50 | squareCase |= 2 51 | } 52 | if ll > z { 53 | squareCase |= 4 54 | } 55 | if lr > z { 56 | squareCase |= 8 57 | } 58 | 59 | if squareCase == 0 || squareCase == 15 { 60 | continue 61 | } 62 | 63 | fx := float64(x) 64 | fy := float64(y) 65 | 66 | t := Point{fx + fraction(ul, ur, z), fy} 67 | b := Point{fx + fraction(ll, lr, z), fy + 1} 68 | l := Point{fx, fy + fraction(ul, ll, z)} 69 | r := Point{fx + 1, fy + fraction(ur, lr, z)} 70 | 71 | te := edge{x, y, x + 1, y, y == 0} 72 | be := edge{x, y + 1, x + 1, y + 1, y+2 == h} 73 | le := edge{x, y, x, y + 1, x == 0} 74 | re := edge{x + 1, y, x + 1, y + 1, x+2 == w} 75 | 76 | const connectHigh = false 77 | switch squareCase { 78 | case 1: 79 | edgePoint[te] = t 80 | nextEdge[t] = le 81 | case 2: 82 | edgePoint[re] = r 83 | nextEdge[r] = te 84 | case 3: 85 | edgePoint[re] = r 86 | nextEdge[r] = le 87 | case 4: 88 | edgePoint[le] = l 89 | nextEdge[l] = be 90 | case 5: 91 | edgePoint[te] = t 92 | nextEdge[t] = be 93 | case 6: 94 | if connectHigh { 95 | edgePoint[le] = l 96 | nextEdge[l] = te 97 | edgePoint[re] = r 98 | nextEdge[r] = be 99 | } else { 100 | edgePoint[re] = r 101 | nextEdge[r] = te 102 | edgePoint[le] = l 103 | nextEdge[l] = be 104 | } 105 | case 7: 106 | edgePoint[re] = r 107 | nextEdge[r] = be 108 | case 8: 109 | edgePoint[be] = b 110 | nextEdge[b] = re 111 | case 9: 112 | if connectHigh { 113 | edgePoint[te] = t 114 | nextEdge[t] = re 115 | edgePoint[be] = b 116 | nextEdge[b] = le 117 | } else { 118 | edgePoint[te] = t 119 | nextEdge[t] = le 120 | edgePoint[be] = b 121 | nextEdge[b] = re 122 | } 123 | case 10: 124 | edgePoint[be] = b 125 | nextEdge[b] = te 126 | case 11: 127 | edgePoint[be] = b 128 | nextEdge[b] = le 129 | case 12: 130 | edgePoint[le] = l 131 | nextEdge[l] = re 132 | case 13: 133 | edgePoint[te] = t 134 | nextEdge[t] = re 135 | case 14: 136 | edgePoint[le] = l 137 | nextEdge[l] = te 138 | } 139 | } 140 | } 141 | 142 | // pick out all boundary edgePoints 143 | boundaryEdgePoint := make(map[edge]Point) 144 | for e, p := range edgePoint { 145 | if e.Boundary { 146 | boundaryEdgePoint[e] = p 147 | } 148 | } 149 | 150 | var contours []Contour 151 | for len(edgePoint) > 0 { 152 | var contour Contour 153 | 154 | // find an unused edge; prefer starting at a boundary 155 | var e edge 156 | if len(boundaryEdgePoint) > 0 { 157 | for e = range boundaryEdgePoint { 158 | break 159 | } 160 | } else { 161 | for e = range edgePoint { 162 | break 163 | } 164 | } 165 | e0 := e 166 | 167 | // add the first point 168 | // (this allows closed paths to start & end at the same point) 169 | p := edgePoint[e] 170 | contour = append(contour, p) 171 | e = nextEdge[p] 172 | 173 | // follow points until none remain 174 | for { 175 | p, ok := edgePoint[e] 176 | if !ok { 177 | break 178 | } 179 | contour = append(contour, p) 180 | delete(edgePoint, e) 181 | if e.Boundary { 182 | delete(boundaryEdgePoint, e) 183 | } 184 | e = nextEdge[p] 185 | } 186 | 187 | // make sure the first one gets deleted in case of open paths 188 | delete(edgePoint, e0) 189 | if e0.Boundary { 190 | delete(boundaryEdgePoint, e0) 191 | } 192 | 193 | // add the contour 194 | contours = append(contours, contour) 195 | } 196 | 197 | return contours 198 | } 199 | --------------------------------------------------------------------------------