-
Notifications
You must be signed in to change notification settings - Fork 4
/
Copy pathmain.go
186 lines (155 loc) · 5.4 KB
/
main.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
// Copyright Bjørn Borud 2019 Use of this source code is governed by
// the license found in the accompanying LICENSE file.
//
// Simple utility for turning a bitmap into colored dots whose
// diameter is proportional to the luminescence of the region the dot
// represents and the color is the average color of the area.
//
// This program is probably slow, and fairly suboptimal stemming from
// the fact that I have absolutely no experience writing graphics
// utilities. But hopefully it is easy to read and understand.
//
package main
import (
"flag"
"fmt"
"image"
"log"
"math"
"os"
"path/filepath"
"strings"
svg "github.com/ajstarks/svgo"
_ "image/gif"
_ "image/jpeg"
_ "image/png"
)
var (
inputFile = flag.String("f", "", "input image in either JPEG, PNG or GIF")
outputFile = flag.String("o", "", "Output file")
boxSize = flag.Int("b", 50, "Box size for dots")
scale = flag.Int("s", 1, "Scale with which svg fill will be scaled compared to original file")
lumaThreshold = flag.Float64("t", 1.0, "Luma threshold - don't draw dots above this luminescence value. Value from 0.0 to 1.0")
color = flag.Bool("c", true, "Use average color for area rather than just black")
bt701 = flag.Bool("l", false, "Use BT.701 instead of BT.601 for luma calculations")
lumaArea = flag.Bool("a", false, "Use the luma as the surface area instead of the radius")
)
// readImage reads the source image. What formats it can understand
// depends on what formats have been loaded.
func readImage(fileName string) (image.Image, error) {
imgFile, err := os.Open(fileName)
if err != nil {
return nil, err
}
defer imgFile.Close()
img, _, err := image.Decode(imgFile)
if err != nil {
return nil, err
}
return img, nil
}
// Calculate luma based on rgb values using ITU BT.709. The value
// returned is between 0.0 and 1.0 so it is convenient to be used for
// scaling other values.
func lumaBT709(r uint32, g uint32, b uint32) float64 {
return ((0.2126 * float64(r)) + (0.7152 * float64(g)) + (0.0722 * float64(b))) / 255.0
}
// Calculate luma based on rgb values using ITU BT.601. This gives
// more weight to the red and blue components. The value returned is
// between 0.0 and 1.0 so it is convenient to be used for scaling
// other values.
func lumaBT601(r uint32, g uint32, b uint32) float64 {
return ((0.299 * float64(r)) + (0.587 * float64(g)) + (0.144 * float64(b))) / 255.0
}
// makeDots creates dots whose diameter is proportional to the
// luminance and color is the average color of the area in the image
// they represent.
func makeDots(img image.Image, boxSize int, lumaThreshold float64, color bool, bt709 bool, outputFileName string, scale int, lumaArea bool) {
// Create the output file
svgFile, err := os.Create(outputFileName)
if err != nil {
log.Fatalf("Unable to open svg file %s: %v", outputFileName, err)
}
defer svgFile.Close()
width := img.Bounds().Dx()
height := img.Bounds().Dy()
// Create new SVG canvas whose width and height are the same as
// the pixels of the original picture just to make coordinates
// match up.
canvas := svg.New(svgFile)
canvas.Start(width*scale, height*scale)
defer canvas.End()
// Calculate useful values
boxHalf := boxSize / 2
boxSizeSquared := boxSize * boxSize
widthSteps := width / boxSize
heightSteps := height / boxSize
// Choose luma function
lumaFunc := lumaBT601
if *bt701 {
lumaFunc = lumaBT709
}
// Step through the entire image dividing it up into boxes with
// edges that are boxSize long and calculate the average color for
// each box.
for x := 0; x < widthSteps; x++ {
for y := 0; y < heightSteps; y++ {
var rSum, gSum, bSum uint32
for i := 0; i < boxSize; i++ {
for j := 0; j < boxSize; j++ {
cx := (x * boxSize) + i
cy := (y * boxSize) + j
r, g, b, _ := img.At(cx, cy).RGBA()
rSum += r
gSum += g
bSum += b
}
}
// Calculate the average color for the box
rSum /= uint32(boxSizeSquared)
gSum /= uint32(boxSizeSquared)
bSum /= uint32(boxSizeSquared)
// Compensating for annoying scaling factor somewhere
// internally in the color package
rSum /= 0x101
bSum /= 0x101
gSum /= 0x101
luma := lumaFunc(rSum, gSum, bSum)
if luma >= lumaThreshold {
continue
}
// Calculate radius either by taking luma as area or as radius
// The factor 1.7 is used to compensate for the fact that otherwise the radius could never reach the maximal value
var radius float64
if lumaArea {
radius = math.Sqrt((1.0-luma)/math.Pi) * 1.7 * float64(boxHalf*scale)
} else {
radius = ((1.0 - luma) * float64(boxHalf*scale))
}
if color {
canvas.Circle(((x*boxSize)+boxHalf)*scale, ((y*boxSize)+boxHalf)*scale, int(radius), fmt.Sprintf("fill:#%02x%02x%02x;stroke:none", rSum, gSum, bSum))
} else {
canvas.Circle(((x*boxSize)+boxHalf)*scale, ((y*boxSize)+boxHalf)*scale, int(radius), "fill:black;stroke:none")
}
}
}
}
func main() {
flag.Parse()
if *inputFile == "" {
flag.Usage()
return
}
if *lumaThreshold < 0.0 || *lumaThreshold > 1.0 {
log.Fatalf("Invalid luma threshold, must be between 0.0 and 1.0")
}
img, err := readImage(*inputFile)
if err != nil {
log.Fatalf("Error reading image %s: %v", *inputFile, err)
}
if *outputFile == "" {
fn := strings.TrimSuffix(*inputFile, filepath.Ext(*inputFile)) + ".svg"
outputFile = &fn
}
makeDots(img, *boxSize, *lumaThreshold, *color, *bt701, *outputFile, *scale, *lumaArea)
}