/
canny.go
169 lines (153 loc) · 5.16 KB
/
canny.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
package edgedetection
import (
"errors"
"github.com/ernyoke/imger/blur"
"github.com/ernyoke/imger/grayscale"
"github.com/ernyoke/imger/padding"
"github.com/ernyoke/imger/utils"
"image"
"image/color"
"math"
)
// CannyGray computes the edges of a given grayscale image using the Canny edge detection algorithm. The returned image
// is a grayscale image represented on 8 bits.
func CannyGray(img *image.Gray, lower float64, upper float64, kernelSize uint) (*image.Gray, error) {
// blur the image using Gaussian filter
blurred, err := blur.GaussianBlurGray(img, float64(kernelSize), 1, padding.BorderConstant)
if err != nil {
return nil, err
}
// get vertical and horizontal edges using Sobel filter
vertical, err := VerticalSobelGray(blurred, padding.BorderConstant)
if err != nil {
return nil, err
}
horizontal, err := HorizontalSobelGray(blurred, padding.BorderConstant)
if err != nil {
return nil, err
}
// calculate the gradient values and orientation angles for each pixel
g, theta, err := gradientAndOrientation(vertical, horizontal)
if err != nil {
return nil, err
}
// "thin" the edges using non-max suppression procedure
thinEdges := nonMaxSuppression(blurred, g, theta)
// hysteresis
hist := threshold(thinEdges, g, lower, upper)
//_ = threshold(thinEdges, g, lower, upper)
return hist, nil
}
// CannyRGBA computes the edges of a given RGBA image using the Canny edge detection algorithm. The returned image is a
// grayscale image represented on 8 bits.
func CannyRGBA(img *image.RGBA, lower float64, upper float64, kernelSize uint) (*image.Gray, error) {
return CannyGray(grayscale.Grayscale(img), lower, upper, kernelSize)
}
func gradientAndOrientation(vertical *image.Gray, horizontal *image.Gray) ([][]float64, [][]float64, error) {
size := vertical.Bounds().Size()
theta := make([][]float64, size.X)
g := make([][]float64, size.X)
for x := 0; x < size.X; x++ {
theta[x] = make([]float64, size.Y)
g[x] = make([]float64, size.Y)
err := errors.New("none")
for y := 0; y < size.Y; y++ {
px := float64(vertical.GrayAt(x, y).Y)
py := float64(horizontal.GrayAt(x, y).Y)
g[x][y] = math.Hypot(px, py)
theta[x][y], err = orientation(math.Atan2(float64(vertical.GrayAt(x, y).Y), float64(horizontal.GrayAt(x, y).Y)))
if err != nil {
return nil, nil, err
}
}
}
return g, theta, nil
}
func isBetween(val float64, lowerBound float64, upperBound float64) bool {
return val >= lowerBound && val < upperBound
}
func orientation(x float64) (float64, error) {
angle := 180 * x / math.Pi
if isBetween(angle, 0, 22.5) || isBetween(angle, -180, -157.5) {
return 0, nil
}
if isBetween(angle, 157.5, 180) || isBetween(angle, -22.5, 0) {
return 0, nil
}
if isBetween(angle, 22.5, 67.5) || isBetween(angle, -157.5, -112.5) {
return 45, nil
}
if isBetween(angle, 67.5, 112.5) || isBetween(angle, -112.5, -67.5) {
return 90, nil
}
if isBetween(angle, 112.5, 157.5) || isBetween(angle, -67.5, -22.5) {
return 135, nil
}
return 0, errors.New("invalid angle")
}
func isBiggerThenNeighbours(val float64, neighbour1 float64, neighbour2 float64) bool {
return val > neighbour1 && val > neighbour2
}
func nonMaxSuppression(img *image.Gray, g [][]float64, theta [][]float64) *image.Gray {
size := img.Bounds().Size()
thinEdges := image.NewGray(image.Rect(0, 0, size.X, size.Y))
utils.ParallelForEachPixel(size, func(x, y int) {
isLocalMax := false
if x > 0 && x < size.X-1 && y > 0 && y < size.Y-1 {
switch theta[x][y] {
case 45:
if isBiggerThenNeighbours(g[x][y], g[x+1][y-1], g[x-1][y+1]) {
isLocalMax = true
}
case 90:
if isBiggerThenNeighbours(g[x][y], g[x+1][y], g[x-1][y]) {
isLocalMax = true
}
case 135:
if isBiggerThenNeighbours(g[x][y], g[x-1][y-1], g[x+1][y+1]) {
isLocalMax = true
}
case 0:
if isBiggerThenNeighbours(g[x][y], g[x][y+1], g[x][y-1]) {
isLocalMax = true
}
}
}
if isLocalMax {
thinEdges.SetGray(x, y, color.Gray{Y: utils.MaxUint8})
}
})
return thinEdges
}
func threshold(img *image.Gray, g [][]float64, lowerBound float64, upperBound float64) *image.Gray {
size := img.Bounds().Size()
res := image.NewGray(image.Rect(0, 0, size.X, size.Y))
utils.ParallelForEachPixel(size, func(x int, y int) {
p := img.GrayAt(x, y)
if p.Y == utils.MaxUint8 {
if g[x][y] < lowerBound {
res.SetGray(x, y, color.Gray{Y: utils.MinUint8})
}
if g[x][y] > upperBound {
res.SetGray(x, y, color.Gray{Y: utils.MaxUint8})
}
}
})
utils.ParallelForEachPixel(size, func(x int, y int) {
p := img.GrayAt(x, y)
if p.Y == utils.MaxUint8 && x > 0 && x < size.X-1 && y > 0 && y < size.Y-1 {
if g[x][y] >= lowerBound && g[x][y] <= upperBound {
if checkNeighbours(x, y, res) {
res.SetGray(x, y, color.Gray{Y: utils.MinUint8})
}
}
}
})
return res
}
func checkNeighbours(x, y int, img *image.Gray) bool {
return img.GrayAt(x-1, y-1).Y == utils.MaxUint8 || img.GrayAt(x-1, y).Y == utils.MaxUint8 ||
img.GrayAt(x-1, y+1).Y == utils.MaxUint8 || img.GrayAt(x, y-1).Y == utils.MaxUint8 ||
img.GrayAt(x, y+1).Y == utils.MaxUint8 || img.GrayAt(x+1, y-1).Y == utils.MaxUint8 ||
img.GrayAt(x+1, y).Y == utils.MaxUint8 || img.GrayAt(x+1, y+1).Y == utils.MaxUint8
}