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

Add script to generate an .svg file from a greyscale image #2

Open
wants to merge 13 commits into
base: master
Choose a base branch
from
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
/cache
*.png

*.svg
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ Some code for generating topographic contour maps. Documentation is currently la

`cmd/contours/main.go` will generate contours for a grayscale input image. Again there are some constants at the top of the file that you can configure.

`cmd/grey2svg/main.go` will generate contours for a grayscale input image and output an SVG file.

## Examples

#### Colorado
Expand Down
1 change: 1 addition & 0 deletions _config.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
theme: jekyll-theme-minimal
66 changes: 22 additions & 44 deletions cmd/contours/main.go
Original file line number Diff line number Diff line change
@@ -1,61 +1,62 @@
package main

import (
"bufio"
"fmt"
"image"
"image/draw"
"math"
"os"

"github.com/disintegration/imaging"
"github.com/fogleman/gg"
"github.com/fogleman/terrarium"
)

const (
Size = 1600
Padding = 0
LineWidth = 1
Steps = 200 // Z slice step size
ImageDownscalingFactor = 1 // 1x = original quality, 0.5x = half-resolution (experimental. removes detail from original image to smoothen output)
Size = 10000 // size in px
Padding = 0
LineWidth = 1
)

func main() {
// validate const
if ImageDownscalingFactor > 1 {
panic("ImageDownscalingFactor must be <= 1")
}
// load image
src, err := gg.LoadImage(os.Args[1])
if err != nil {
panic(err)
}
// apply downscaling
if ImageDownscalingFactor < 1 {
newWidth := int(float32(src.Bounds().Dx()) * ImageDownscalingFactor)
newHeight := int(float32(src.Bounds().Dy()) * ImageDownscalingFactor)
src = imaging.Resize(src, newWidth, newHeight, imaging.NearestNeighbor)
}

gray, _ := ensureGray16(src)
w := gray.Bounds().Size().X
h := gray.Bounds().Size().Y
a := gray16Grid(gray)

// hist := make(map[float64]int)
// for _, v := range a {
// hist[v] += 1
// }
// for k := 0; k < 256; k++ {
// fmt.Println(k, hist[float64(k)])
// }

var paths []terrarium.Path
for i := 0; i < 65535; i += 1024 {
for i := 0; i < 65535; i += Steps {
z := float64(i)
// if hist[z] == 0 {
// continue
// }
p := terrarium.Slice(a, w, h, z+1e-7)
fmt.Println(z, len(p))
paths = append(paths, p...)
if len(p) > 0 {
fmt.Println("z:", z, len(p))
paths = append(paths, p...)
}
}

fmt.Println("rendering image...")
im := renderPaths(paths, Size, Padding, LineWidth)

fmt.Println("writing png...")
gg.SavePNG("out.png", im)

// fmt.Println("writing axi...")
// saveAxi("out.axi", paths)
}

func ensureGray16(im image.Image) (*image.Gray16, bool) {
Expand Down Expand Up @@ -104,25 +105,6 @@ func grayToArray(gray *image.Gray) []float64 {
return a
}

func saveAxi(filename string, paths []terrarium.Path) error {
file, err := os.Create(filename)
if err != nil {
return err
}
defer file.Close()
w := bufio.NewWriter(file)
for _, path := range paths {
for i, p := range path {
if i != 0 {
fmt.Fprintf(w, " ")
}
fmt.Fprintf(w, "%g,%g", p.X, p.Y)
}
fmt.Fprintln(w)
}
return w.Flush()
}

func renderPaths(paths []terrarium.Path, size, pad int, lw float64) image.Image {
x0 := paths[0][0].X
x1 := paths[0][0].X
Expand Down Expand Up @@ -164,9 +146,5 @@ func renderPaths(paths []terrarium.Path, size, pad int, lw float64) image.Image
dc.SetRGB(0, 0, 0)
dc.SetLineWidth(lw)
dc.Stroke()
// dc.Identity()
// dc.DrawCircle(float64(dc.Width()/2), float64(dc.Height()/2), 8)
// dc.SetRGBA(1, 0, 0, 0.9)
// dc.Fill()
return dc.Image()
}
158 changes: 158 additions & 0 deletions cmd/gray2svg/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
package main

import (
"fmt"
"image"
"image/color"
"image/draw"
"math"
"os"
"strconv"

"github.com/disintegration/imaging"
"github.com/fogleman/gg"
"github.com/fogleman/terrarium"
"github.com/llgcode/draw2d/draw2dsvg"
)

const (
Steps = 100 // Z slice step size
ImageDownscalingFactor = 1 // 1x = original quality, 0.5x = half-resolution (experimental. removes detail from original image to smoothen output)
Size = 10000 // size in px
Padding = 0
LineWidth = 1
)

func main() {
// validate const
if ImageDownscalingFactor > 1 {
panic("ImageDownscalingFactor must be <= 1")
}
// load image
src, err := gg.LoadImage(os.Args[1])
if err != nil {
panic(err)
}
// apply downscaling
if ImageDownscalingFactor < 1 {
newWidth := int(float32(src.Bounds().Dx()) * ImageDownscalingFactor)
newHeight := int(float32(src.Bounds().Dy()) * ImageDownscalingFactor)
src = imaging.Resize(src, newWidth, newHeight, imaging.NearestNeighbor)
}

gray, _ := ensureGray16(src)
w := gray.Bounds().Size().X
h := gray.Bounds().Size().Y
a := gray16Grid(gray)

var paths []terrarium.Path
for i := 0; i < 65535; i += Steps {
z := float64(i)
p := terrarium.Slice(a, w, h, z+1e-7)
if len(p) > 0 {
fmt.Println("z:", z, len(p))
paths = append(paths, p...)
}
}

fmt.Println("rendering image...")
dest := renderPaths(paths, Size, Padding, LineWidth)

fmt.Println("writing svg...")
draw2dsvg.SaveToSvgFile("out.svg", dest)
}

func ensureGray16(im image.Image) (*image.Gray16, bool) {
switch im := im.(type) {
case *image.Gray16:
return im, true
default:
dst := image.NewGray16(im.Bounds())
draw.Draw(dst, im.Bounds(), im, image.ZP, draw.Src)
return dst, false
}
}

func gray16Grid(im *image.Gray16) []float64 {
w := im.Bounds().Size().X
h := im.Bounds().Size().Y
grid := make([]float64, w*h)
index := 0
for y := 0; y < h; y++ {
i := im.PixOffset(0, y)
for x := 0; x < w; x++ {
a := int(im.Pix[i]) << 8
b := int(im.Pix[i+1])
value := (a | b)
grid[index] = float64(value)
index++
i += 2
}
}
return grid
}

func grayToArray(gray *image.Gray) []float64 {
w := gray.Bounds().Size().X
h := gray.Bounds().Size().Y
a := make([]float64, w*h)
index := 0
for y := 0; y < h; y++ {
i := gray.PixOffset(0, y)
for x := 0; x < w; x++ {
a[index] = float64(gray.Pix[i])
index++
i++
}
}
return a
}

func renderPaths(paths []terrarium.Path, size, pad int, lw float64) *draw2dsvg.Svg {
x0 := paths[0][0].X
x1 := paths[0][0].X
y0 := paths[0][0].Y
y1 := paths[0][0].Y
for _, path := range paths {
for _, p := range path {
if p.X < x0 {
x0 = p.X
}
if p.X > x1 {
x1 = p.X
}
if p.Y < y0 {
y0 = p.Y
}
if p.Y > y1 {
y1 = p.Y
}
}
}
pw := x1 - x0
ph := y1 - y0
sx := float64(size-pad*2) / pw
sy := float64(size-pad*2) / ph
scale := math.Min(sx, sy)
fmt.Println(scale)

svg := draw2dsvg.NewSvg()
svg.Width = strconv.Itoa(int(pw*scale) + pad*2)
svg.Height = strconv.Itoa(int(ph*scale) + pad*2)
gc := draw2dsvg.NewGraphicContext(svg)
gc.SetStrokeColor(color.Black)
gc.SetLineWidth(LineWidth)
gc.Translate(float64(pad), float64(pad))
gc.Scale(scale, scale)
gc.Translate(-x0, -y0)
for _, path := range paths {
gc.MoveTo(path[0].X, path[0].Y)
for i := 1; i < (len(path) - 1); i += 1 {
gc.LineTo(path[i].X, path[i].Y)
}
}
gc.Close()
gc.Stroke()

return svg
}