-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathimage.go
408 lines (347 loc) · 10.1 KB
/
image.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
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
//
// Laser Range Finder
// image.go
//
// Cole Smith - [email protected]
// Eric Lin - [email protected]
// LICENSE: Apache 2.0
//
package rangefinder
import (
"fmt"
"image"
"image/color"
"math"
)
//
// ImageMatrix
//
// Defines an image as a two dimensional array of hues
// from the HSV colorspace
type ImageMatrix struct {
Width int
Height int
Image [][]*Pixel
}
// Generates a new ImageMatrix struct given an input
// image of type image.RGBA
func NewImageMatrix(inputImage *image.RGBA) *ImageMatrix {
// Get Image width and height
bounds := inputImage.Bounds()
width := bounds.Max.X
height := bounds.Max.Y
// Fill the image 2D slice with hues
image := make([][]*Pixel, height)
for i := range image {
image[i] = make([]*Pixel, width)
for j := range image[i] {
pixel := getHSVFromRGBA(inputImage.At(i, j))
image[i][j] = pixel
}
}
return &ImageMatrix{width, height, image}
}
//
// MonoImageMatrix
//
// Defines a new image in binary greyscale using bool values
type MonoImageMatrix struct {
Width int
Height int
ValThreshold float64
HueThreshold float64
Image [][]bool
Info *MonoImageInfo
}
// Meta infomration about a MonoImageMatrix for the purpose
// of machine vision algorithms and other image processing functions
type MonoImageInfo struct {
// Array of Coords that correspond
// to the first true value seen after
// prev false values when parsing a
// MonoImageMatrix
possibleBlobs []coord
// The center of mass of the blobs found
// explicitly by a blob detection algorithm
foundBlobCentroids []coord
}
//// Generates a new MonoImageMatrix struct given an image of type image.RGBA,
//// and the treshold at which the Value (Lume) of an image is considered a 1
//// or a 0 such that: 1 <- pixel >= valueThreshold, 0 <- pixel < valueThreshold
//func NewMonoImageMatrix(inputImage *image.RGBA, valueThreshold float64) *MonoImageMatrix {
//// Get Image width and height
//bounds := inputImage.Bounds()
//width := bounds.Max.X
//height := bounds.Max.Y
//image := make([][]bool, height)
//for i := range image {
//image[i] = make([]bool, width)
//for j := range image[i] {
//val := getHSVFromRGBA(inputImage.At(j, i)).val
//image[i][j] = val >= valueThreshold
//}
//}
//return &MonoImageMatrix{width, height, valueThreshold, image}
//}
// Returns an empty greyscale image of width and height
// Defaults to all pixels false and a valueThreshold of 0
func NewEmptyMonoImageMatrix(width, height int) *MonoImageMatrix {
image := make([][]bool, height)
for i := range image {
image[i] = make([]bool, width)
for j := range image[i] {
image[i][j] = false
}
}
return &MonoImageMatrix{width, height, 0.0, 0.0, image, nil}
}
// Converts an ImageMatrix to a MonoImageMatrix using value thresholding
// Creats a mask where true values are defined for pixels above or equal to the
// valueThreshold and false are defined for pixels below the valueThreshold
func (image ImageMatrix) ConvertToMonoImageMatrixFromValue(valueThreshold float64) *MonoImageMatrix {
mono := make([][]bool, image.Height)
for i, _ := range mono {
mono[i] = make([]bool, image.Width)
for j, _ := range mono[i] {
val := image.Image[i][j].val
mono[i][j] = val >= valueThreshold
}
}
return &MonoImageMatrix{image.Width, image.Height, valueThreshold, 0.0, mono, nil}
}
// Converts an ImageMatrix to a MonoImageMatrix using hue thresholding where hueTarget
// is the hue angle to be thresheld, and hueThreshold is the maxiumum difference in hue angle
// allowed for a pixel
// Creates a mask where true values are defined for pixels with hue differences within the hue threshold
// and false for pixels with hue differences greater than the hue threshold
func (image ImageMatrix) ConvertToMonoImageMatrixFromHue(hueTarget, hueThreshold float64) *MonoImageMatrix {
mono := make([][]bool, image.Height)
for i, _ := range mono {
mono[i] = make([]bool, image.Width)
for j, _ := range mono[i] {
hue := image.Image[i][j].hue
hueDifference := math.Abs(hue - hueTarget)
mono[i][j] = hueThreshold >= hueDifference
}
}
return &MonoImageMatrix{image.Width, image.Height, 0.0, hueThreshold, mono, nil}
}
// Creates a MonoImageMatrix from the set intersect of two MonoImageMatrix structs.
// Will return nil and an error if the images are not the same size
func GetMonoIntersectMatrix(mono1, mono2 *MonoImageMatrix) (*MonoImageMatrix, error) {
// Images must be the same size
if mono1.Width != mono2.Width || mono1.Height != mono2.Height {
return nil, fmt.Errorf("MonoImageMatrix: Cannot get intersect of diferent sizes")
}
intersect := NewEmptyMonoImageMatrix(mono1.Width, mono1.Height)
for i, _ := range intersect.Image {
for j, _ := range intersect.Image[i] {
intersect.Image[i][j] = mono1.Image[i][j] && mono2.Image[i][j]
}
}
return intersect, nil
}
//
// Pixel (and coord)
//
// A pixel for an image defined in the
// HSV colorspace
type Pixel struct {
hue float64
sat float64
val float64
}
// Represents a Pixel location in an image
type coord struct {
x int
y int
}
// Returns a new Coord struct from x, y
func newCoord(x, y int) *coord {
return &coord{x, y}
}
//
// Dot Detection Functions
//
// Finds blobs in MonoImageMatrix and then appends results to
// the MonoImageMatrix's MonoImageInfo struct in the
// foundBlobCentroids field
func (image *MonoImageMatrix) FindBlobs() [][]*coord {
const MIN_BLOB_SIZE = 50
var blobs [][]*coord
img := image.Image
var visited []*coord
// Function to search visited array
inVisited := func(x, y int) bool {
for _, px := range visited {
if px.x == x && px.y == y {
return true
}
}
return false
}
for i := range img {
for j := range img[i] {
if !img[i][j] || inVisited(i, j) {
continue
}
foundBlobs := findBlobHelper(image, newCoord(i, j), nil)
visited = append(visited, foundBlobs...)
if len(foundBlobs) >= MIN_BLOB_SIZE {
blobs = append(blobs, foundBlobs)
}
}
}
return blobs
}
func findBlobHelper(image *MonoImageMatrix, start *coord, visited []*coord) []*coord {
i := start.x
j := start.y
// Function to search visited array
inVisited := func(x, y int) bool {
for _, px := range visited {
if px.x == x && px.y == y {
return true
}
}
return false
}
// Valid Pixel, Check Neighbors
currentCoord := newCoord(i, j)
visited = append(visited, currentCoord)
// Find neighbors
neighbors := checkNeighbors(image, currentCoord)
if len(neighbors) == 0 {
return visited
}
// Run algorithm using neighbors
for _, px := range neighbors {
if !inVisited(px.x, px.y) {
visited = findBlobHelper(image, px, visited)
}
}
return visited
}
func checkNeighbors(image *MonoImageMatrix, start *coord) []*coord {
var neighbors []*coord
i := start.x
j := start.y
w := image.Width - 1
h := image.Height - 1
if !(i+1 > w) {
if !(j+1 > h) && image.Image[i+1][j+1] {
neighbors = append(neighbors, newCoord(i+1, j+1))
}
if image.Image[i+1][j] && image.Image[i+1][j] {
neighbors = append(neighbors, newCoord(i+1, j))
}
}
if !(i-1 < 0) {
if !(j-1 < 0) && image.Image[i-1][j-1] {
neighbors = append(neighbors, newCoord(i-1, j-1))
}
if image.Image[i-1][j] && image.Image[i-1][j] {
neighbors = append(neighbors, newCoord(i-1, j))
}
}
return neighbors
}
// Returns the centroid of the marked pixel cluster of a binary image
func getCentroid(coords []*coord) *coord {
avgX := 0
avgY := 0
for _, px := range coords {
avgX += px.x
avgY += px.y
}
// Int division truncates decimals
avgX = avgX / len(coords)
avgY = avgY / len(coords)
return newCoord(avgX, avgY)
}
// Given a series of connected coords, take the difference of
// min and max values for X and Y. The differences for X and Y
// are made a ratio as:
// [ abs(minX) - abs(maxX) ] / [ abs(minY) - abs(maxY) ]
// or
// [ abs(minY) - abs(maxY) ] / [ abs(minX) - abs(maxX) ]
// A ratio of 1.0 denotes a perfectly square bounding rectangle,
// (a circle blob). Anything less, denotes the oval ratio
func getCircleRatio(blob []*coord) float64 {
maxX, maxY, minX, minY := 0.0, 0.0, math.MaxFloat64, math.MaxFloat64
for _, px := range blob {
x := float64(px.x)
y := float64(px.y)
maxX, maxY = math.Max(maxX, x), math.Max(maxY, y)
minX, minY = math.Min(minX, x), math.Min(minY, y)
}
diffX, diffY := math.Abs(maxX-minX), math.Abs(maxY-minY)
return math.Min(diffX, diffY) / math.Max(diffX, diffY)
}
// Calls getCircleRatio
// A ratio threshold is provided to determine the minimum value
// for which a ratio is considered a circle.
func blobIsCircle(blob []*coord, ratioThreshold float64) bool {
return getCircleRatio(blob) >= ratioThreshold
}
//
// Exported Functions
//
// TODO
// Binds the pixel offset of the laser dot from the center plane
// of the image to a specified inital distance of units.
// Example: (image, 0.64, 1, "meters")
func Calibrate(image ImageMatrix, laserHue float64, initialDistance int, unitSuffix string) {
}
// TODO
// Iterates through image array to detect the laser dot. The pixels that
// match the hue, plus or minus the threshold value, will be marked true
// on a binary image.
func detectDotInImage(image ImageMatrix, laserHue int) MonoImageMatrix {
dotImage := NewEmptyMonoImageMatrix(image.Width, image.Height)
return *dotImage
}
//
// Color Conversion
//
// Returns a Hue angle as a float64 from an RGBA Color
func getHSVFromRGBA(rgba color.Color) *Pixel {
//Get RGB values
red, green, blue, _ := rgba.RGBA()
r := float64(red)
g := float64(green)
b := float64(blue)
//Set up computed variables
var hue float64 = 0.0
var sat float64 = 0.0
var val float64 = 0.0
//var d float64 = 0.0
//var h float64 = 0.0
//Standardize rgb values
r = r / 65535.0
g = g / 65535.0
b = b / 65535.0
//Get min and max for RGB
min := math.Min(math.Min(r, g), b)
max := math.Max(math.Max(r, g), b)
//If min is equal to max, we can assume it is black and white
if min == max {
return &Pixel{0, 0, min}
}
// Calculate Hue
if r == max {
hue = (g - b) / (max - min)
} else if g == max {
hue = 2.0 + (b-r)/(max-min)
} else {
hue = 4.0 + (r-g)/(max-min)
}
hue = hue * 60
if hue < 0 {
hue = hue + 360
}
// Calculate Saturation and Value
sat = (max - min) / max
val = max
return &Pixel{hue, sat, val}
}