diff --git a/turbidity/main.go b/turbidity/main.go index ad9c2e3d..e1b3034a 100644 --- a/turbidity/main.go +++ b/turbidity/main.go @@ -2,6 +2,8 @@ package main import ( "fmt" + "log" + "math" "gonum.org/v1/plot" "gonum.org/v1/plot/plotter" @@ -11,62 +13,65 @@ import ( "gocv.io/x/gocv" ) -func main() { - n_images := 6 - n_samples := 10 +const ( + nImages = 6 + nSamples = 10 +) - // Load template +func main() { + nImages := 6 + nSamples := 10 + + // Load template and standard image. template := gocv.IMRead("template.jpg", gocv.IMReadGrayScale) standard := gocv.IMRead("standard.jpg", gocv.IMReadGrayScale) - images := make([][]gocv.Mat, n_images) + imgs := make([][]gocv.Mat, nImages) - //Load images - for i := 0; i < n_images; i++ { - images[i] = make([]gocv.Mat, n_samples) - for j := 0; j < n_samples; j++ { - images[i][j] = gocv.IMRead(fmt.Sprintf("images/t-%v/000%v.jpg", i, j), gocv.IMReadGrayScale) + // Load test images. + for i := range imgs { + imgs[i] = make([]gocv.Mat, nSamples) + for j := range imgs[i] { + imgs[i][j] = gocv.IMRead(fmt.Sprintf("images/t-%v/000%v.jpg", i, j), gocv.IMReadGrayScale) } } - // Create turbidity sensor + // Create turbidity sensor. ts := TurbiditySensor{template: template, standard: standard, k1: 90, k2: 90, scale: 5.0, alpha: 1.0} - var final_result Results - final_result.New(n_images) + var finalRes Results + finalRes.New(nImages) - // Score each image by calculating the average score from camera burst - for i := range images { - - //Evaluate camera burst - sample_result, err := ts.Evaluate(n_samples, images[i]) + // Score each image by calculating the average score from camera burst. + for i := range imgs { + // Evaluate camera burst. + sample_result, err := ts.Evaluate(imgs[i]) if err != nil { - fmt.Println(err) + log.Fatalf("Evaluation Failed: %v", err) } - // Append the average result from camera burst - final_result.Append(Average(sample_result.saturation), Average(sample_result.contrast), float64((i+1)*10), i) + // Add the average result from camera burst. + finalRes.Update(average(sample_result.saturation), average(sample_result.contrast), float64(i*10), i) } - // Plot the final results - err := PlotResults(final_result.turbidity, Normalize(final_result.saturation), Normalize(final_result.contrast)) - - fmt.Println(final_result.saturation) - fmt.Println(final_result.contrast) - + // Plot the final results. + err := plotResults(finalRes.turbidity, normalize(finalRes.saturation), normalize(finalRes.contrast)) if err != nil { - fmt.Println(err) + log.Fatalf("Plotting Failed: %v", err) } + + log.Printf("Saturation: %v", finalRes.saturation) + log.Printf("Contrast: %v", finalRes.contrast) } -// PLOTTING FUNCTIONS +// Plotting Functions. -// Normalize values in a slice between 0 and 1 -func Normalize(slice []float64) []float64 { +// Normalize values in a slice between 0 and 1. +func normalize(slice []float64) []float64 { - max := -1e10 - min := 1e10 + max := -math.MaxFloat64 + min := math.MaxFloat64 out := make([]float64, len(slice)) @@ -90,8 +95,8 @@ func Normalize(slice []float64) []float64 { return out } -// Return the average of a slice -func Average(slice []float64) float64 { +// Return the average of a slice. +func average(slice []float64) float64 { out := 0.0 @@ -102,7 +107,7 @@ func Average(slice []float64) float64 { return out / float64(len(slice)) } -func PlotResults(x, saturation, contrast []float64) error { +func plotResults(x, saturation, contrast []float64) error { err := plotToFile( "Results", @@ -117,7 +122,7 @@ func PlotResults(x, saturation, contrast []float64) error { ) if err != nil { - return fmt.Errorf("Could not plot results: %v", err) + return fmt.Errorf("Could not plot results: %w", err) } return nil @@ -127,16 +132,13 @@ func PlotResults(x, saturation, contrast []float64) error { // provided draw function, and then saves to a PNG file with filename of name. func plotToFile(name, xTitle, yTitle string, draw func(*plot.Plot) error) error { p := plot.New() - p.Title.Text = name p.X.Label.Text = xTitle p.Y.Label.Text = yTitle - err := draw(p) if err != nil { return fmt.Errorf("could not draw plot contents: %w", err) } - if err := p.Save(15*vg.Centimeter, 15*vg.Centimeter, "plots/"+name+".png"); err != nil { return fmt.Errorf("could not save plot: %w", err) } @@ -145,9 +147,7 @@ func plotToFile(name, xTitle, yTitle string, draw func(*plot.Plot) error) error // plotterXY provides a plotter.XYs type value based on the given x and y data. func plotterXY(x, y []float64) plotter.XYs { - xy := make(plotter.XYs, len(x)) - for i := range x { xy[i].X = x[i] xy[i].Y = y[i] diff --git a/turbidity/results.go b/turbidity/results.go index 701e037a..b75dc9ff 100644 --- a/turbidity/results.go +++ b/turbidity/results.go @@ -1,5 +1,6 @@ package main +// struct to hold the results of the turbidity sensor. type Results struct { turbidity []float64 saturation []float64 @@ -12,7 +13,8 @@ func (r *Results) New(n int) { r.contrast = make([]float64, n) } -func (r *Results) Append(saturation, contrast, turbidity float64, index int) { +// Update results to add new values at specified index. +func (r *Results) Update(saturation, contrast, turbidity float64, index int) { r.saturation[index] = saturation r.contrast[index] = contrast r.turbidity[index] = turbidity diff --git a/turbidity/turbidity.go b/turbidity/turbidity.go index cba8ebfe..afef3849 100644 --- a/turbidity/turbidity.go +++ b/turbidity/turbidity.go @@ -1,6 +1,7 @@ package main import ( + "errors" "fmt" "image" "math" @@ -8,97 +9,93 @@ import ( "gocv.io/x/gocv" ) -// TURBIDITY SENSOR +// Turbidity Sensor. type TurbiditySensor struct { template, standard gocv.Mat k1, k2 int //block size alpha, scale float64 } -// Given a slice of test images of size n, return the sharpness and contrast scores -func (ts TurbiditySensor) Evaluate(n int, images []gocv.Mat) (Results, error) { +// Given a slice of test images, return the sharpness and contrast scores. +func (ts TurbiditySensor) Evaluate(imgs []gocv.Mat) (Results, error) { var result Results - result.New(n) + result.New(len(imgs)) - for i := range images { - - // Transform image - marker, err := ts.Transform(images[i]) + for i := range imgs { + // Transform image. + marker, err := ts.Transform(imgs[i]) if err != nil { - return result, fmt.Errorf("Image %v: %v", i, err) + return result, fmt.Errorf("Image %v: %w", i, err) } - // Apply sobel filter + // Apply sobel filter. edge := ts.Sobel(marker, ts.scale) - // Evaluate + // Evaluate image. out, err := ts.EvaluateImage(marker, edge, ts.k1, ts.k2, ts.alpha) - if err != nil { return result, err } - result.Append(out[0], out[1], float64((i+1)*10), i) + result.Update(out[0], out[1], float64(i*10), i) } return result, nil } -// Evaluate image sharpness and contrast using blocks of size k1 by k2. Return a slice of the respective scores +// Evaluate image sharpness and contrast using blocks of size k1 by k2. Return a slice of the respective scores. func (ts TurbiditySensor) EvaluateImage(img, edge gocv.Mat, k1, k2 int, alpha float64) ([]float64, error) { - // Slice to store results + // Slice to store results. result := make([]float64, 2) // [0.0, 0.0] if img.Rows()%k1 != 0 || img.Cols()%k2 != 0 { - return result, fmt.Errorf("Dimensions not compatible (%v, %v)", k1, k2) + return nil, fmt.Errorf("Dimensions not compatible (%v, %v)", k1, k2) } - l_step := int(img.Rows() / k1) - k_step := int(img.Cols() / k2) + lStep := int(img.Rows() / k1) + kStep := int(img.Cols() / k2) - for l := 0; l < img.Rows(); l = l + l_step { - for k := 0; k < img.Cols(); k = k + k_step { - - //EME - err := ts.EvaluateBlock(edge, l, k, l+l_step, k+k_step, result, "EME", alpha) + for l := 0; l < img.Rows(); l = l + lStep { + for k := 0; k < img.Cols(); k = k + kStep { + // Enhancement Measure Estimation (EME), provides a measure of the sharpness. + err := ts.EvaluateBlock(edge, l, k, l+lStep, k+kStep, result, "EME", alpha) if err != nil { - return result, err + return nil, err } - //AMEE - err = ts.EvaluateBlock(img, l, k, l+l_step, k+k_step, result, "AMEE", alpha) - + // AMEE, provides a measure of the contrast. + err = ts.EvaluateBlock(img, l, k, l+lStep, k+kStep, result, "AMEE", alpha) if err != nil { - return result, err + return nil, err } } } - // EME - result[0] = 2.0 / (float64(k1) * float64(k1)) * result[0] + // EME. + result[0] = 2.0 / (float64(k1) * float64(k2)) * result[0] - // AMEE - result[1] = -1.0 / (float64(k1) * float64(k1)) * result[1] + // AMEE. + result[1] = -1.0 / (float64(k1) * float64(k2)) * result[1] return result, nil } -// Evaluate a block within and image and add to to the result slice +// Evaluate a block within an image and add to to the result slice. func (ts TurbiditySensor) EvaluateBlock(img gocv.Mat, l1, k1, l2, k2 int, result []float64, operation string, alpha float64) error { - max := -1e10 - min := 1e10 + max := -math.MaxFloat64 + min := math.MaxFloat64 for i := l1; i < l2; i++ { for j := k1; j < k2; j++ { value := float64(img.GetUCharAt(i, j)) - // Check max/min conditions, zero values are ignored + // Check max/min conditions, zero values are ignored. if value > max && value != 0.0 { max = value } @@ -108,17 +105,13 @@ func (ts TurbiditySensor) EvaluateBlock(img gocv.Mat, l1, k1, l2, k2 int, result } } - // Blocks which have no information are ignored - if max != -1e10 && min != 1e10 && max != min { + // Blocks which have no information are ignored. + if max != -math.MaxFloat64 && min != math.MaxFloat64 && max != min { if operation == "EME" { - result[0] += math.Log(max / min) - } else if operation == "AMEE" { - contrast := (max + min) / (max - min) result[1] += math.Pow(alpha*(contrast), alpha) * math.Log(contrast) - } else { return fmt.Errorf("Invalid operation: %v", operation) } @@ -126,54 +119,50 @@ func (ts TurbiditySensor) EvaluateBlock(img gocv.Mat, l1, k1, l2, k2 int, result return nil } -// Search image for matching template. Returns the transformed image which best match the template +// Search image for matching template. Returns the transformed image which best match the template. func (ts TurbiditySensor) Transform(img gocv.Mat) (gocv.Mat, error) { - out := gocv.NewMat() mask := gocv.NewMat() - corners_img := gocv.NewMat() corners_template := gocv.NewMat() - // Find corners in image + // Find corners in image. if !gocv.FindChessboardCorners(img, image.Pt(3, 3), &corners_img, gocv.CalibCBNormalizeImage) { - - // Apply default if transformation fails + // Apply default if transformation fails. // fmt.Println("Corner detection failed applying standard transformation") - if !gocv.FindChessboardCorners(ts.standard, image.Pt(3, 3), &corners_img, gocv.CalibCBNormalizeImage) { - return out, fmt.Errorf("Could not find corners in default image") + return out, errors.New("Could not find corners in default image") } } - // Find corners in template + // Find corners in template. if !gocv.FindChessboardCorners(ts.template, image.Pt(3, 3), &corners_template, gocv.CalibCBNormalizeImage) { - return out, fmt.Errorf("Could not find corners in template") + return out, errors.New("Could not find corners in template") } - // Find and apply transformation + // Find and apply transformation. H := gocv.FindHomography(corners_img, &corners_template, gocv.HomograpyMethodRANSAC, 3.0, &mask, 2000, 0.995) gocv.WarpPerspective(img, &out, H, image.Pt(ts.template.Rows(), ts.template.Cols())) return out, nil } -// Apply sobel filter to an image with a given scale and return the result +// Apply sobel filter to an image with a given scale and return the result. func (ts TurbiditySensor) Sobel(img gocv.Mat, scale float64) gocv.Mat { dx := gocv.NewMat() dy := gocv.NewMat() sobel := gocv.NewMat() - // Apply filter + // Apply filter. gocv.Sobel(img, &dx, gocv.MatTypeCV64F, 0, 1, 3, scale, 0.0, gocv.BorderConstant) gocv.Sobel(img, &dy, gocv.MatTypeCV64F, 1, 0, 3, scale, 0.0, gocv.BorderConstant) - // Convert to unsigned + // Convert to unsigned. gocv.ConvertScaleAbs(dx, &dx, 1.0, 0.0) gocv.ConvertScaleAbs(dy, &dy, 1.0, 0.0) - // Add x and y components + // Add x and y components. gocv.AddWeighted(dx, 0.5, dy, 0.5, 0, &sobel) return sobel