From 9071b7fc18595c8d19178f6053545b44010ae6a4 Mon Sep 17 00:00:00 2001 From: Nick White Date: Mon, 20 Jul 2020 18:29:30 +0100 Subject: Add pggraph tool --- README | 2 + cmd/pggraph/main.go | 207 ++++++++++++++++++++++++++++++++++++++++++++++++++++ go.mod | 10 ++- go.sum | 18 +++++ wipesides.go | 2 +- 5 files changed, 237 insertions(+), 2 deletions(-) create mode 100644 cmd/pggraph/main.go diff --git a/README b/README index a1bb516..8938e43 100644 --- a/README +++ b/README @@ -17,6 +17,8 @@ in their own right as well as serving as examples of using the package. - binarize : binarises an image using the sauvola algorithm + - pggraph : creates a graph showing the proportion of black + pixels for slices through an image - preproc : binarises and wipes an image - preprocmulti : binarises and wipes an image with multiple binarisation ksize values diff --git a/cmd/pggraph/main.go b/cmd/pggraph/main.go new file mode 100644 index 0000000..9d9d26c --- /dev/null +++ b/cmd/pggraph/main.go @@ -0,0 +1,207 @@ +// Copyright 2020 Nick White. +// Use of this source code is governed by the GPLv3 +// license that can be found in the LICENSE file. + +// pggraph creates a graph showing the proportion of black pixels +// for slices through an image. This is useful to determine +// appropriate parameters to pass to the wipe functions in this +// module. +package main + +import ( + "flag" + "fmt" + "image" + "image/draw" + _ "image/png" + _ "image/jpeg" + "io" + "log" + "os" + "sort" + + "rescribe.xyz/integralimg" + chart "github.com/wcharczuk/go-chart" +) + +const usage = `Usage: pggraph [-vertical] [-width] inimg graphname + +Creates a graph showing the proportion of black pixels +for slices through an image. This is useful to determine +appropriate parameters to pass to the wipe functions in +this module. +` + +const middlePercent = 20 +const tickEvery = 30 + +// sideways flips an image sideways +func sideways(img *image.Gray) *image.Gray { + b := img.Bounds() + newb := image.Rect(b.Min.Y, b.Min.X, b.Max.Y, b.Max.X) + new := image.NewGray(newb) + for x := b.Min.X; x < b.Max.X; x++ { + for y := b.Min.Y; y < b.Max.Y; y++ { + new.SetGray(y, x, img.GrayAt(x, y)) + } + } + return new +} + +func graph(title string, points map[int]float64, w io.Writer) error { + var xvals, yvals []float64 + var xs []int + var midxvals, midyvals []float64 + var midxs []int + + if len(points) < 2 { + return fmt.Errorf("Not enough points to graph, only %d\n", len(points)) + } + + for x, _ := range points { + xs = append(xs, x) + } + sort.Ints(xs) + + for _, x := range xs { + xvals = append(xvals, float64(x)) + yvals = append(yvals, points[x]) + } + + mainSeries := chart.ContinuousSeries{ + Style: chart.Style{ + StrokeColor: chart.ColorBlue, + FillColor: chart.ColorAlternateBlue, + }, + XValues: xvals, + YValues: yvals, + } + + numAroundMiddle := int(middlePercent / 2 * float64(len(xs)) / float64(100)) + if numAroundMiddle > 1 { + for i := (len(xs) / 2) - numAroundMiddle; i < (len(xs) / 2) + numAroundMiddle; i++ { + midxs = append(midxs, xs[i]) + } + } else { + midxs = xs + } + + for _, x := range midxs { + midxvals = append(midxvals, float64(x)) + midyvals = append(midyvals, points[x]) + } + + middleSeries := chart.ContinuousSeries{ + XValues: midxvals, + YValues: midyvals, + } + + minSeries := &chart.MinSeries{ + Style: chart.Style{ + StrokeColor: chart.ColorAlternateGray, + StrokeDashArray: []float64{5.0, 5.0}, + }, + InnerSeries: middleSeries, + } + + maxSeries := &chart.MaxSeries{ + Style: chart.Style{ + StrokeColor: chart.ColorAlternateGray, + StrokeDashArray: []float64{5.0, 5.0}, + }, + InnerSeries: middleSeries, + } + + width := xs[1] + var ticks []chart.Tick + // if width is larger than tickEvery, just have a tick for each point, + // otherwise have a tick every tickEvery period + if width > tickEvery { + for _, v := range xs { + ticks = append(ticks, chart.Tick{float64(v), fmt.Sprintf("%d", v)}) + } + } else { + for i := 0; i < len(xs) - 1; i += tickEvery { + ticks = append(ticks, chart.Tick{float64(xs[i]), fmt.Sprintf("%d", xs[i])}) + } + if len(ticks) > 1 { + lastx := xs[len(xs) - 1] + ticks[len(ticks) - 1] = chart.Tick{float64(lastx), fmt.Sprintf("%d", lastx)} + } + } + + graph := chart.Chart{ + Title: title, + Width: 1920, + Height: 800, + XAxis: chart.XAxis{ + Name: "Pixel number", + Ticks: ticks, + }, + YAxis: chart.YAxis{ + Name: "Proportion of black pixels", + }, + Series: []chart.Series{ + mainSeries, + minSeries, + maxSeries, + chart.LastValueAnnotationSeries(minSeries), + chart.LastValueAnnotationSeries(maxSeries), + }, + } + + return graph.Render(chart.PNG, w) +} + +func main() { + flag.Usage = func() { + fmt.Fprintf(flag.CommandLine.Output(), usage) + flag.PrintDefaults() + } + vertical := flag.Bool("vertical", false, "Slice image vertically (from top to bottom) rather than horizontally") + width := flag.Int("width", 5, "Width of slice in pixels (height if in vertical mode)") + flag.Parse() + if flag.NArg() < 2 { + flag.Usage() + os.Exit(1) + } + + f, err := os.Open(flag.Arg(0)) + defer f.Close() + if err != nil { + log.Fatalf("Could not open file %s: %v\n", flag.Arg(0), err) + } + img, _, err := image.Decode(f) + if err != nil { + log.Fatalf("Could not decode image: %v\n", err) + } + b := img.Bounds() + gray := image.NewGray(image.Rect(0, 0, b.Dx(), b.Dy())) + draw.Draw(gray, b, img, b.Min, draw.Src) + if *vertical { + gray = sideways(gray) + } + integral := integralimg.ToIntegralImg(gray) + + points := make(map[int]float64) + maxx := len(integral[0]) -1 + for x := 0; x + *width < maxx; x += *width { + w := integral.GetVerticalWindow(x, *width) + points[x] = w.Proportion() + } + + f, err = os.Create(flag.Arg(1)) + defer f.Close() + if err != nil { + log.Fatalf("Could not create file %s: %v\n", flag.Arg(1), err) + } + + title := fmt.Sprintf("Proportion of black pixels over %s (width %d)", flag.Arg(0), *width) + if *vertical { + title += " (vertical)" + } + err = graph(title, points, f) + if err != nil { + log.Fatalf("Could not create graph: %v\n", err) + } +} diff --git a/go.mod b/go.mod index 0237e41..cd9d133 100644 --- a/go.mod +++ b/go.mod @@ -1,3 +1,11 @@ module rescribe.xyz/preproc -require rescribe.xyz/integralimg v0.1.1 +go 1.14 + +require ( + github.com/blend/go-sdk v1.1.1 // indirect + github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect + github.com/wcharczuk/go-chart v2.0.2-0.20191206192251-962b9abdec2b+incompatible + golang.org/x/image v0.0.0-20200618115811-c13761719519 // indirect + rescribe.xyz/integralimg v0.1.1 +) diff --git a/go.sum b/go.sum index afa027d..7b10a68 100644 --- a/go.sum +++ b/go.sum @@ -1,2 +1,20 @@ +cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +github.com/DataDog/datadog-go v0.0.0-20180822151419-281ae9f2d895/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ= +github.com/airbrake/gobrake v3.6.1+incompatible/go.mod h1:wM4gu3Cn0W0K7GUuVWnlXZU11AGBXMILnrdOU8Kn00o= +github.com/blend/go-sdk v1.1.1 h1:R7PcwuIxYvrGc/r9TLLfMpajIboTjqs/HyQouzgJ7mQ= +github.com/blend/go-sdk v1.1.1/go.mod h1:IP1XHXFveOXHRnojRJO7XvqWGqyzevtXND9AdSztAe8= +github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 h1:DACJavvAHhabrF08vX0COfcOBJRhZ8lUbR+ZWIs0Y5g= +github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k= +github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= +github.com/opentracing/opentracing-go v1.0.2/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= +github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/wcharczuk/go-chart v2.0.2-0.20191206192251-962b9abdec2b+incompatible h1:ahpaSRefPekV3gcXot2AOgngIV8WYqzvDyFe3i7W24w= +github.com/wcharczuk/go-chart v2.0.2-0.20191206192251-962b9abdec2b+incompatible/go.mod h1:PF5tmL4EIx/7Wf+hEkpCqYi5He4u90sw+0+6FhrryuE= +golang.org/x/image v0.0.0-20200618115811-c13761719519 h1:1e2ufUJNM3lCHEY5jIgac/7UTjd6cgJNdatjPdFWf34= +golang.org/x/image v0.0.0-20200618115811-c13761719519/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/oauth2 v0.0.0-20181203162652-d668ce993890/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/tools v0.0.0-20181205014116-22934f0fdb62/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= rescribe.xyz/integralimg v0.1.1 h1:riLayPKKM5bs/ZyIcYhbUs4qP8wQOWc+tMxOF3vuscI= rescribe.xyz/integralimg v0.1.1/go.mod h1:2euyPigpyIixTWO6JtFEhDp/3YKA6yy+d8g17oL0L0s= diff --git a/wipesides.go b/wipesides.go index ebfb24f..26b7d25 100644 --- a/wipesides.go +++ b/wipesides.go @@ -149,7 +149,7 @@ func toonarrow(img *image.Gray, lowedge int, highedge int, min int) bool { return false } -// func sideways flips an image by sideways +// sideways flips an image sideways func sideways(img *image.Gray) *image.Gray { b := img.Bounds() newb := image.Rect(b.Min.Y, b.Min.X, b.Max.Y, b.Max.X) -- cgit v1.2.1-24-ge1ad