diff --git a/turbidity/plot.go b/turbidity/plot.go index 62f66aa2..1c950ec1 100644 --- a/turbidity/plot.go +++ b/turbidity/plot.go @@ -6,7 +6,7 @@ AUTHORS Russell Stanley LICENSE - Copyright (C) 2020 the Australian Ocean Lab (AusOcean) + Copyright (C) 2021-2022 the Australian Ocean Lab (AusOcean) It is free software: you can redistribute it and/or modify them under the terms of the GNU General Public License as published by the @@ -22,7 +22,7 @@ LICENSE in gpl.txt. If not, see http://www.gnu.org/licenses. */ -package main +package turbidity import ( "fmt" @@ -63,15 +63,13 @@ func normalize(slice []float64) []float64 { // Return the average of a slice. func average(slice []float64) float64 { var out float64 - - // Sum all elements in the slice. for i := range slice { out += slice[i] } return out / float64(len(slice)) } -func plotResults(x, saturation, contrast []float64) error { +func plotResults(x, sharpness, contrast []float64) error { err := plotToFile( "Results", "Almond Milk (ml)", @@ -79,7 +77,7 @@ func plotResults(x, saturation, contrast []float64) error { func(p *plot.Plot) error { return plotutil.AddLinePoints(p, "Contrast", plotterXY(x, contrast), - "Saturation", plotterXY(x, saturation), + "Sharpness", plotterXY(x, sharpness), ) }, ) diff --git a/turbidity/results.go b/turbidity/results.go index df731dc9..0cd7b928 100644 --- a/turbidity/results.go +++ b/turbidity/results.go @@ -9,7 +9,7 @@ AUTHORS Russell Stanley LICENSE - Copyright (C) 2020 the Australian Ocean Lab (AusOcean) + Copyright (C) 2021-2022 the Australian Ocean Lab (AusOcean) It is free software: you can redistribute it and/or modify them under the terms of the GNU General Public License as published by the @@ -25,27 +25,26 @@ LICENSE in gpl.txt. If not, see http://www.gnu.org/licenses. */ -package main +package turbidity import "fmt" // Results holds the results of the turbidity sensor. type Results struct { - Turbidity []float64 - Saturation []float64 - Contrast []float64 + Turbidity []float64 + Sharpness []float64 + Contrast []float64 } -// NewResults constructs the results object. +// NewResults returns a new Results func NewResults(n int) (*Results, error) { - if n <= 0 { - return nil, fmt.Errorf("invalid result size: %v.", n) + return nil, fmt.Errorf("invalid result size: %v", n) } r := new(Results) r.Turbidity = make([]float64, n) - r.Saturation = make([]float64, n) + r.Sharpness = make([]float64, n) r.Contrast = make([]float64, n) return r, nil @@ -53,7 +52,7 @@ func NewResults(n int) (*Results, error) { // Update adds new values to slice at specified index. func (r *Results) Update(newSat, newCont, newTurb float64, index int) { - r.Saturation[index] = newSat + r.Sharpness[index] = newSat r.Contrast[index] = newCont r.Turbidity[index] = newTurb } diff --git a/turbidity/turbidity.go b/turbidity/turbidity.go index 6be68488..6315ed39 100644 --- a/turbidity/turbidity.go +++ b/turbidity/turbidity.go @@ -3,14 +3,16 @@ /* DESCRIPTION - Holds the turbidity sensor struct. Can evaluate 4x4 chessboard markers - in an image to measure the sharpness and contrast. + Holds the turbidity sensor struct. Can evaluate 4x4 chessboard markers in an + image to measure the sharpness and contrast. This implementation is based off + a master thesis from Aalborg University, Turbidity measurement based on computer vision. + The full paper is avaible at https://projekter.aau.dk/projekter/files/306657262/master.pdf AUTHORS Russell Stanley LICENSE - Copyright (C) 2020 the Australian Ocean Lab (AusOcean) + Copyright (C) 2021-2022 the Australian Ocean Lab (AusOcean) It is free software: you can redistribute it and/or modify them under the terms of the GNU General Public License as published by the @@ -26,7 +28,7 @@ LICENSE in gpl.txt. If not, see http://www.gnu.org/licenses. */ -package main +package turbidity import ( "errors" @@ -37,7 +39,7 @@ import ( "gocv.io/x/gocv" ) -// TurbiditySensor is a software based turbidity sensor that uses CV to determine saturation and constrast level +// TurbiditySensor is a software based turbidity sensor that uses CV to determine sharpness and constrast level // of a chessboard-like target submerged in water that can be correlated to turbidity/visibility values. type TurbiditySensor struct { template, templateCorners gocv.Mat @@ -46,7 +48,7 @@ type TurbiditySensor struct { scale, alpha float64 } -// NewTurbiditySensor constructor for a turbidity sensor. +// NewTurbiditySensor returns a new TurbiditySensor. func NewTurbiditySensor(template, standard gocv.Mat, k1, k2, sobelFilterSize int, scale, alpha float64) (*TurbiditySensor, error) { ts := new(TurbiditySensor) templateCorners := gocv.NewMat() @@ -54,7 +56,7 @@ func NewTurbiditySensor(template, standard gocv.Mat, k1, k2, sobelFilterSize int // Validate template image is not empty and has valid corners. if template.Empty() { - return nil, errors.New("template image is empty.") + return nil, errors.New("template image is empty") } if !gocv.FindChessboardCorners(template, image.Pt(3, 3), &templateCorners, gocv.CalibCBNormalizeImage) { return nil, errors.New("could not find corners in template image") @@ -64,7 +66,7 @@ func NewTurbiditySensor(template, standard gocv.Mat, k1, k2, sobelFilterSize int // Validate standard image is not empty and has valid corners. if standard.Empty() { - return nil, errors.New("standard image is empty.") + return nil, errors.New("standard image is empty") } if !gocv.FindChessboardCorners(standard, image.Pt(3, 3), &standardCorners, gocv.CalibCBNormalizeImage) { return nil, errors.New("could not find corners in standard image") @@ -74,7 +76,6 @@ func NewTurbiditySensor(template, standard gocv.Mat, k1, k2, sobelFilterSize int ts.k1, ts.k2, ts.sobelFilterSize = k1, k2, sobelFilterSize ts.alpha, ts.scale = alpha, scale - return ts, nil } @@ -82,28 +83,22 @@ func NewTurbiditySensor(template, standard gocv.Mat, k1, k2, sobelFilterSize int func (ts TurbiditySensor) Evaluate(imgs []gocv.Mat) (*Results, error) { result, err := NewResults(len(imgs)) if err != nil { - return result, err + return nil, fmt.Errorf("could not create results: %w", err) } for i := range imgs { - // Transform image. marker, err := ts.transform(imgs[i]) if err != nil { - return result, fmt.Errorf("image %v: %w", i, err) + return nil, fmt.Errorf("could not transform image: %d: %w", i, err) } - - // Apply sobel filter. edge := ts.sobel(marker) - // Evaluate image. sharpScore, contScore, err := ts.EvaluateImage(marker, edge) if err != nil { return result, err } - result.Update(sharpScore, contScore, float64(i), i) } - return result, nil } @@ -115,19 +110,16 @@ func (ts TurbiditySensor) EvaluateImage(img, edge gocv.Mat) (float64, float64, e if img.Rows()%ts.k1 != 0 || img.Cols()%ts.k2 != 0 { return math.NaN(), math.NaN(), fmt.Errorf("dimensions not compatible (%v, %v)", ts.k1, ts.k2) } - lStep := img.Rows() / ts.k1 kStep := img.Cols() / ts.k2 for l := 0; l < img.Rows(); l += lStep { for k := 0; k < img.Cols(); k += kStep { // Enhancement Measure Estimation (EME), provides a measure of the sharpness. - sharpValue := ts.evaluateBlockEME(edge, l, k, l+lStep, k+kStep) - sharpness += sharpValue + sharpness += ts.evaluateBlockEME(edge, l, k, l+lStep, k+kStep) // AMEE, provides a measure of the contrast. - contValue := ts.evaluateBlockAMEE(img, l, k, l+lStep, k+kStep) - contrast += contValue + contrast += ts.evaluateBlockAMEE(img, l, k, l+lStep, k+kStep) } } @@ -165,7 +157,7 @@ func (ts TurbiditySensor) minMax(img gocv.Mat, xStart, yStart, xEnd, yEnd int) ( func (ts TurbiditySensor) evaluateBlockEME(img gocv.Mat, xStart, yStart, xEnd, yEnd int) float64 { max, min := ts.minMax(img, xStart, yStart, xEnd, yEnd) - // Blocks which have no information are ignored. + // Blocks where all pixel values are equal are ignored to avoid division by 0. if max != -math.MaxFloat64 && min != math.MaxFloat64 && max != min { return math.Log(max / min) } @@ -176,7 +168,7 @@ func (ts TurbiditySensor) evaluateBlockEME(img gocv.Mat, xStart, yStart, xEnd, y func (ts TurbiditySensor) evaluateBlockAMEE(img gocv.Mat, xStart, yStart, xEnd, yEnd int) float64 { max, min := ts.minMax(img, xStart, yStart, xEnd, yEnd) - // Blocks which have no information are ignored. + // Blocks where all pixel values are equal are ignored to avoid division by 0. if max != -math.MaxFloat64 && min != math.MaxFloat64 && max != min { contrast := (max + min) / (max - min) return math.Pow(ts.alpha*(contrast), ts.alpha) * math.Log(contrast) @@ -189,8 +181,12 @@ func (ts TurbiditySensor) transform(img gocv.Mat) (gocv.Mat, error) { out := gocv.NewMat() mask := gocv.NewMat() imgCorners := gocv.NewMat() + const ( + ransacThreshold = 3.0 // Maximum allowed reprojection error to treat a point pair as an inlier. + maxIter = 2000 // The maximum number of RANSAC iterations. + confidence = 0.995 // Confidence level, between 0 and 1. + ) - // Check image is valid. if img.Empty() { return out, errors.New("image is empty, cannot transform") } @@ -200,7 +196,7 @@ func (ts TurbiditySensor) transform(img gocv.Mat) (gocv.Mat, error) { } // Find and apply transformation. - H := gocv.FindHomography(imgCorners, &ts.templateCorners, gocv.HomograpyMethodRANSAC, 3.0, &mask, 2000, 0.995) + H := gocv.FindHomography(imgCorners, &ts.templateCorners, gocv.HomograpyMethodRANSAC, ransacThreshold, &mask, maxIter, confidence) gocv.WarpPerspective(img, &out, H, image.Pt(ts.template.Rows(), ts.template.Cols())) return out, nil diff --git a/turbidity/turbidity_test.go b/turbidity/turbidity_test.go index 40791908..82f38643 100644 --- a/turbidity/turbidity_test.go +++ b/turbidity/turbidity_test.go @@ -7,7 +7,7 @@ AUTHORS Russell Stanley LICENSE - Copyright (C) 2020 the Australian Ocean Lab (AusOcean) + Copyright (C) 2021-2022 the Australian Ocean Lab (AusOcean) It is free software: you can redistribute it and/or modify them under the terms of the GNU General Public License as published by the @@ -23,7 +23,7 @@ LICENSE in gpl.txt. If not, see http://www.gnu.org/licenses. */ -package main +package turbidity import ( "fmt" @@ -33,12 +33,18 @@ import ( ) const ( - nImages = 13 // Number of images to test. (Max 13) - nSamples = 10 // Number of samples for each image. (Max 10) - increment = 2.5 + nImages = 13 // Number of images to test. (Max 13) + nSamples = 10 // Number of samples for each image. (Max 10) + increment = 2.5 // Increment of the turbidity level ) func TestImages(t *testing.T) { + const ( + k1, k2 = 8, 8 + filterSize = 3 + scale, alpha = 1.0, 1.0 + ) + // Load template and standard image. template := gocv.IMRead("images/template.jpg", gocv.IMReadGrayScale) standard := gocv.IMRead("images/default.jpg", gocv.IMReadGrayScale) @@ -54,15 +60,15 @@ func TestImages(t *testing.T) { } // Create turbidity sensor. - ts, err := NewTurbiditySensor(template, standard, 8, 8, 3, 1.0, 1.0) + ts, err := NewTurbiditySensor(template, standard, k1, k2, filterSize, scale, alpha) if err != nil { - t.Fatal(err) + t.Fatal("could not create turbidity sensor: %w", err) } // Create results. results, err := NewResults(nImages) if err != nil { - t.Fatal(err) + t.Fatal("could not create results: %w", err) } // Score each image by calculating the average score from camera burst. @@ -70,19 +76,19 @@ func TestImages(t *testing.T) { // Evaluate camera burst. sample_result, err := ts.Evaluate(imgs[i]) if err != nil { - t.Fatalf("Evaluation Failed: %v", err) + t.Fatalf("evaluation Failed: %w", err) } // Add the average result from camera burst. - results.Update(average(sample_result.Saturation), average(sample_result.Contrast), float64(i)*increment, i) + results.Update(average(sample_result.Sharpness), average(sample_result.Contrast), float64(i)*increment, i) } // Plot the final results. - err = plotResults(results.Turbidity, normalize(results.Saturation), normalize(results.Contrast)) + err = plotResults(results.Turbidity, normalize(results.Sharpness), normalize(results.Contrast)) if err != nil { - t.Fatalf("Plotting Failed: %v", err) + t.Fatalf("plotting Failed: %w", err) } - t.Logf("Saturation: %v", results.Saturation) + t.Logf("Sharpness: %v", results.Sharpness) t.Logf("Contrast: %v", results.Contrast) }