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> Russell Stanley <russell@ausocean.org>
LICENSE 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 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 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. in gpl.txt. If not, see http://www.gnu.org/licenses.
*/ */
package main package turbidity
import ( import (
"fmt" "fmt"
@ -63,15 +63,13 @@ func normalize(slice []float64) []float64 {
// Return the average of a slice. // Return the average of a slice.
func average(slice []float64) float64 { func average(slice []float64) float64 {
var out float64 var out float64
// Sum all elements in the slice.
for i := range slice { for i := range slice {
out += slice[i] out += slice[i]
} }
return out / float64(len(slice)) return out / float64(len(slice))
} }
func plotResults(x, saturation, contrast []float64) error { func plotResults(x, sharpness, contrast []float64) error {
err := plotToFile( err := plotToFile(
"Results", "Results",
"Almond Milk (ml)", "Almond Milk (ml)",
@ -79,7 +77,7 @@ func plotResults(x, saturation, contrast []float64) error {
func(p *plot.Plot) error { func(p *plot.Plot) error {
return plotutil.AddLinePoints(p, return plotutil.AddLinePoints(p,
"Contrast", plotterXY(x, contrast), "Contrast", plotterXY(x, contrast),
"Saturation", plotterXY(x, saturation), "Sharpness", plotterXY(x, sharpness),
) )
}, },
) )

View File

@ -9,7 +9,7 @@ AUTHORS
Russell Stanley <russell@ausocean.org> Russell Stanley <russell@ausocean.org>
LICENSE 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 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 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. in gpl.txt. If not, see http://www.gnu.org/licenses.
*/ */
package main package turbidity
import "fmt" import "fmt"
// Results holds the results of the turbidity sensor. // Results holds the results of the turbidity sensor.
type Results struct { type Results struct {
Turbidity []float64 Turbidity []float64
Saturation []float64 Sharpness []float64
Contrast []float64 Contrast []float64
} }
// NewResults constructs the results object. // NewResults returns a new Results
func NewResults(n int) (*Results, error) { func NewResults(n int) (*Results, error) {
if n <= 0 { 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 := new(Results)
r.Turbidity = make([]float64, n) r.Turbidity = make([]float64, n)
r.Saturation = make([]float64, n) r.Sharpness = make([]float64, n)
r.Contrast = make([]float64, n) r.Contrast = make([]float64, n)
return r, nil return r, nil
@ -53,7 +52,7 @@ func NewResults(n int) (*Results, error) {
// Update adds new values to slice at specified index. // Update adds new values to slice at specified index.
func (r *Results) Update(newSat, newCont, newTurb float64, index int) { func (r *Results) Update(newSat, newCont, newTurb float64, index int) {
r.Saturation[index] = newSat r.Sharpness[index] = newSat
r.Contrast[index] = newCont r.Contrast[index] = newCont
r.Turbidity[index] = newTurb r.Turbidity[index] = newTurb
} }

View File

@ -3,14 +3,16 @@
/* /*
DESCRIPTION DESCRIPTION
Holds the turbidity sensor struct. Can evaluate 4x4 chessboard markers Holds the turbidity sensor struct. Can evaluate 4x4 chessboard markers in an
in an image to measure the sharpness and contrast. 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 AUTHORS
Russell Stanley <russell@ausocean.org> Russell Stanley <russell@ausocean.org>
LICENSE 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 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 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. in gpl.txt. If not, see http://www.gnu.org/licenses.
*/ */
package main package turbidity
import ( import (
"errors" "errors"
@ -37,7 +39,7 @@ import (
"gocv.io/x/gocv" "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. // of a chessboard-like target submerged in water that can be correlated to turbidity/visibility values.
type TurbiditySensor struct { type TurbiditySensor struct {
template, templateCorners gocv.Mat template, templateCorners gocv.Mat
@ -46,7 +48,7 @@ type TurbiditySensor struct {
scale, alpha float64 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) { func NewTurbiditySensor(template, standard gocv.Mat, k1, k2, sobelFilterSize int, scale, alpha float64) (*TurbiditySensor, error) {
ts := new(TurbiditySensor) ts := new(TurbiditySensor)
templateCorners := gocv.NewMat() 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. // Validate template image is not empty and has valid corners.
if template.Empty() { 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) { if !gocv.FindChessboardCorners(template, image.Pt(3, 3), &templateCorners, gocv.CalibCBNormalizeImage) {
return nil, errors.New("could not find corners in template image") 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. // Validate standard image is not empty and has valid corners.
if standard.Empty() { 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) { if !gocv.FindChessboardCorners(standard, image.Pt(3, 3), &standardCorners, gocv.CalibCBNormalizeImage) {
return nil, errors.New("could not find corners in standard image") 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.k1, ts.k2, ts.sobelFilterSize = k1, k2, sobelFilterSize
ts.alpha, ts.scale = alpha, scale ts.alpha, ts.scale = alpha, scale
return ts, nil 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) { func (ts TurbiditySensor) Evaluate(imgs []gocv.Mat) (*Results, error) {
result, err := NewResults(len(imgs)) result, err := NewResults(len(imgs))
if err != nil { if err != nil {
return result, err return nil, fmt.Errorf("could not create results: %w", err)
} }
for i := range imgs { for i := range imgs {
// Transform image.
marker, err := ts.transform(imgs[i]) marker, err := ts.transform(imgs[i])
if err != nil { 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) edge := ts.sobel(marker)
// Evaluate image.
sharpScore, contScore, err := ts.EvaluateImage(marker, edge) sharpScore, contScore, err := ts.EvaluateImage(marker, edge)
if err != nil { if err != nil {
return result, err return result, err
} }
result.Update(sharpScore, contScore, float64(i), i) result.Update(sharpScore, contScore, float64(i), i)
} }
return result, nil 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 { 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) return math.NaN(), math.NaN(), fmt.Errorf("dimensions not compatible (%v, %v)", ts.k1, ts.k2)
} }
lStep := img.Rows() / ts.k1 lStep := img.Rows() / ts.k1
kStep := img.Cols() / ts.k2 kStep := img.Cols() / ts.k2
for l := 0; l < img.Rows(); l += lStep { for l := 0; l < img.Rows(); l += lStep {
for k := 0; k < img.Cols(); k += kStep { for k := 0; k < img.Cols(); k += kStep {
// Enhancement Measure Estimation (EME), provides a measure of the sharpness. // Enhancement Measure Estimation (EME), provides a measure of the sharpness.
sharpValue := ts.evaluateBlockEME(edge, l, k, l+lStep, k+kStep) sharpness += ts.evaluateBlockEME(edge, l, k, l+lStep, k+kStep)
sharpness += sharpValue
// AMEE, provides a measure of the contrast. // AMEE, provides a measure of the contrast.
contValue := ts.evaluateBlockAMEE(img, l, k, l+lStep, k+kStep) contrast += ts.evaluateBlockAMEE(img, l, k, l+lStep, k+kStep)
contrast += contValue
} }
} }
@ -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 { func (ts TurbiditySensor) evaluateBlockEME(img gocv.Mat, xStart, yStart, xEnd, yEnd int) float64 {
max, min := ts.minMax(img, xStart, yStart, xEnd, yEnd) 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 { if max != -math.MaxFloat64 && min != math.MaxFloat64 && max != min {
return math.Log(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 { func (ts TurbiditySensor) evaluateBlockAMEE(img gocv.Mat, xStart, yStart, xEnd, yEnd int) float64 {
max, min := ts.minMax(img, xStart, yStart, xEnd, yEnd) 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 { if max != -math.MaxFloat64 && min != math.MaxFloat64 && max != min {
contrast := (max + min) / (max - min) contrast := (max + min) / (max - min)
return math.Pow(ts.alpha*(contrast), ts.alpha) * math.Log(contrast) 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() out := gocv.NewMat()
mask := gocv.NewMat() mask := gocv.NewMat()
imgCorners := 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() { if img.Empty() {
return out, errors.New("image is empty, cannot transform") 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. // 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())) gocv.WarpPerspective(img, &out, H, image.Pt(ts.template.Rows(), ts.template.Cols()))
return out, nil return out, nil

View File

@ -7,7 +7,7 @@ AUTHORS
Russell Stanley <russell@ausocean.org> Russell Stanley <russell@ausocean.org>
LICENSE 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 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 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. in gpl.txt. If not, see http://www.gnu.org/licenses.
*/ */
package main package turbidity
import ( import (
"fmt" "fmt"
@ -33,12 +33,18 @@ import (
) )
const ( const (
nImages = 13 // Number of images to test. (Max 13) nImages = 13 // Number of images to test. (Max 13)
nSamples = 10 // Number of samples for each image. (Max 10) nSamples = 10 // Number of samples for each image. (Max 10)
increment = 2.5 increment = 2.5 // Increment of the turbidity level
) )
func TestImages(t *testing.T) { func TestImages(t *testing.T) {
const (
k1, k2 = 8, 8
filterSize = 3
scale, alpha = 1.0, 1.0
)
// Load template and standard image. // Load template and standard image.
template := gocv.IMRead("images/template.jpg", gocv.IMReadGrayScale) template := gocv.IMRead("images/template.jpg", gocv.IMReadGrayScale)
standard := gocv.IMRead("images/default.jpg", gocv.IMReadGrayScale) standard := gocv.IMRead("images/default.jpg", gocv.IMReadGrayScale)
@ -54,15 +60,15 @@ func TestImages(t *testing.T) {
} }
// Create turbidity sensor. // 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 { if err != nil {
t.Fatal(err) t.Fatal("could not create turbidity sensor: %w", err)
} }
// Create results. // Create results.
results, err := NewResults(nImages) results, err := NewResults(nImages)
if err != nil { 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. // Score each image by calculating the average score from camera burst.
@ -70,19 +76,19 @@ func TestImages(t *testing.T) {
// Evaluate camera burst. // Evaluate camera burst.
sample_result, err := ts.Evaluate(imgs[i]) sample_result, err := ts.Evaluate(imgs[i])
if err != nil { if err != nil {
t.Fatalf("Evaluation Failed: %v", err) t.Fatalf("evaluation Failed: %w", err)
} }
// Add the average result from camera burst. // 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. // 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 { 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) t.Logf("Contrast: %v", results.Contrast)
} }