resolve sharpness/saturation terminology issue, updated package names and code cleanup

This commit is contained in:
Russell Stanley 2022-01-07 11:40:20 +10:30
parent 6d97486876
commit 00e1390893
4 changed files with 54 additions and 55 deletions

View File

@ -6,7 +6,7 @@ AUTHORS
Russell Stanley <russell@ausocean.org>
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),
)
},
)

View File

@ -9,7 +9,7 @@ AUTHORS
Russell Stanley <russell@ausocean.org>
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
}

View File

@ -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 <russell@ausocean.org>
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

View File

@ -7,7 +7,7 @@ AUTHORS
Russell Stanley <russell@ausocean.org>
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)
}