code cleanup

This commit is contained in:
Russell Stanley 2022-01-06 13:55:40 +10:30
parent d43b38471e
commit 2ec62db850
6 changed files with 203 additions and 135 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

View File

@ -1,12 +1,9 @@
//go:build !nocv
// +build !nocv
/* /*
DESCRIPTION DESCRIPTION
Turbidity is a program to measure water clarity using computer vison Plotting functions for the turbidity sensor results.
AUTHORS AUTHORS
Russell Stanley <russell@ausocean.org> Russell Stanley <russell@ausocean.org>
LICENSE LICENSE
Copyright (C) 2020 the Australian Ocean Lab (AusOcean) Copyright (C) 2020 the Australian Ocean Lab (AusOcean)
@ -29,79 +26,25 @@ package main
import ( import (
"fmt" "fmt"
"log"
"math" "math"
"gonum.org/v1/plot" "gonum.org/v1/plot"
"gonum.org/v1/plot/plotter" "gonum.org/v1/plot/plotter"
"gonum.org/v1/plot/plotutil" "gonum.org/v1/plot/plotutil"
"gonum.org/v1/plot/vg" "gonum.org/v1/plot/vg"
"gocv.io/x/gocv"
) )
const (
nImages = 13
nSamples = 10
)
func main() {
// Load template and standard image.
template := gocv.IMRead("template.jpg", gocv.IMReadGrayScale)
standard := gocv.IMRead("default.jpg", gocv.IMReadGrayScale)
imgs := make([][]gocv.Mat, nImages)
// 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.
ts := TurbiditySensor{template: template, standard: standard, k1: 8, k2: 8, sobelFilterSize: 3, scale: 1.0, alpha: 1.0}
var finalRes Results
finalRes.new(nImages)
// 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 {
log.Fatalf("Evaluation Failed: %v", err)
}
// Add the average result from camera burst.
finalRes.update(average(sample_result.saturation), average(sample_result.contrast), float64(i)*2.5, i)
}
// Plot the final results.
err := plotResults(finalRes.turbidity, normalize(finalRes.saturation), normalize(finalRes.contrast))
if err != nil {
log.Fatalf("Plotting Failed: %v", err)
}
log.Printf("Saturation: %v", finalRes.saturation)
log.Printf("Contrast: %v", finalRes.contrast)
}
// Plotting Functions.
// Normalize values in a slice between 0 and 1. // Normalize values in a slice between 0 and 1.
func normalize(slice []float64) []float64 { func normalize(slice []float64) []float64 {
max := -math.MaxFloat64 max := -math.MaxFloat64
min := math.MaxFloat64 min := math.MaxFloat64
out := make([]float64, len(slice)) out := make([]float64, len(slice))
if len(slice) <= 1 { if len(slice) <= 1 {
return slice return slice
} }
// Find the max and min values of the slice.
for i := range slice { for i := range slice {
if slice[i] > max { if slice[i] > max {
max = slice[i] max = slice[i]
@ -119,9 +62,9 @@ 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
out := 0.0 // Sum all elements in the slice.
for i := range slice { for i := range slice {
out += slice[i] out += slice[i]
} }
@ -129,7 +72,6 @@ func average(slice []float64) float64 {
} }
func plotResults(x, saturation, contrast []float64) error { func plotResults(x, saturation, contrast []float64) error {
err := plotToFile( err := plotToFile(
"Results", "Results",
"Almond Milk (ml)", "Almond Milk (ml)",

View File

@ -3,10 +3,10 @@
/* /*
DESCRIPTION DESCRIPTION
Results struct used to store results from the turbidity sensor Results struct used to store results from the turbidity sensor.
AUTHORS AUTHORS
Russell Stanley <russell@ausocean.org> Russell Stanley <russell@ausocean.org>
LICENSE LICENSE
Copyright (C) 2020 the Australian Ocean Lab (AusOcean) Copyright (C) 2020 the Australian Ocean Lab (AusOcean)
@ -27,22 +27,33 @@ LICENSE
package main package main
// struct to hold the results of the turbidity sensor. import "fmt"
// Struct to hold the results of the turbidity sensor.
type Results struct { type Results struct {
turbidity []float64 Turbidity []float64
saturation []float64 Saturation []float64
contrast []float64 Contrast []float64
} }
func (r *Results) new(n int) { // Results constructor
r.turbidity = make([]float64, n) func NewResults(n int) (*Results, error) {
r.saturation = make([]float64, n)
r.contrast = make([]float64, n) if n <= 0 {
return nil, fmt.Errorf("invalid result size: %v.", n)
}
r := new(Results)
r.Turbidity = make([]float64, n)
r.Saturation = make([]float64, n)
r.Contrast = make([]float64, n)
return r, nil
} }
// Update results to add new values at specified index. // Update results to add new values at specified index.
func (r *Results) update(saturation, contrast, turbidity float64, index int) { func (r *Results) Update(newSat, newCont, newTurb float64, index int) {
r.saturation[index] = saturation r.Saturation[index] = newSat
r.contrast[index] = contrast r.Contrast[index] = newCont
r.turbidity[index] = turbidity r.Turbidity[index] = newTurb
} }

View File

@ -3,11 +3,11 @@
/* /*
DESCRIPTION DESCRIPTION
Holds the turbidity sensor struct. Can evaluate 4x4 chessboard markers Holds the turbidity sensor struct. Can evaluate 4x4 chessboard markers
in an image to measure the sharpness and contrast. in an image to measure the sharpness and contrast.
AUTHORS AUTHORS
Russell Stanley <russell@ausocean.org> Russell Stanley <russell@ausocean.org>
LICENSE LICENSE
Copyright (C) 2020 the Australian Ocean Lab (AusOcean) Copyright (C) 2020 the Australian Ocean Lab (AusOcean)
@ -37,89 +37,102 @@ import (
"gocv.io/x/gocv" "gocv.io/x/gocv"
) )
// Turbidity Sensor. // TurbiditySensor is a software based turbidity sensor that uses CV to determine saturation and constrast level
// of a chessboard-like target submerged in water that can be correlated to turbidity/visibility values.
type TurbiditySensor struct { type TurbiditySensor struct {
template, standard gocv.Mat template, standard gocv.Mat
k1, k2, sobelFilterSize int k1, k2, sobelFilterSize int
alpha, scale float64 scale, alpha float64
} }
// Given a slice of test images, return the sharpness and contrast scores. // Turbidity sensor constructor.
func (ts TurbiditySensor) Evaluate(imgs []gocv.Mat) (Results, error) { func NewTurbiditySensor(template, standard gocv.Mat, k1, k2, sobelFilterSize int, scale, alpha float64) (*TurbiditySensor, error) {
ts := new(TurbiditySensor)
var result Results if template.Empty() {
result.new(len(imgs)) return nil, errors.New("template image is empty.")
}
ts.template = template
if standard.Empty() {
return nil, errors.New("standard image is empty.")
}
ts.standard = standard
ts.k1, ts.k2, ts.sobelFilterSize = k1, k2, sobelFilterSize
ts.alpha, ts.scale = alpha, scale
return ts, nil
}
// Evaluate, given a slice of images, return the sharpness and contrast scores.
func (ts TurbiditySensor) Evaluate(imgs []gocv.Mat) (*Results, error) {
result, err := NewResults(len(imgs))
if err != nil {
return result, err
}
for i := range imgs { for i := range imgs {
// Transform image. // 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 result, fmt.Errorf("image %v: %w", i, err)
} }
// Apply sobel filter. // Apply sobel filter.
edge := ts.Sobel(marker) edge := ts.sobel(marker)
// Evaluate image. // Evaluate image.
scores, 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(scores[0], scores[1], float64(i*10), i) result.Update(sharpScore, contScore, float64(i), i)
} }
return result, nil return result, nil
} }
// Evaluate image sharpness and contrast using blocks of size k1 by k2. Return a slice of the respective scores. // EvaluateImage will evaluate image sharpness and contrast using blocks of size k1 by k2. Return the respective scores.
func (ts TurbiditySensor) EvaluateImage(img, edge gocv.Mat) ([]float64, error) { func (ts TurbiditySensor) EvaluateImage(img, edge gocv.Mat) (float64, float64, error) {
var sharpness float64
result := make([]float64, 2) // [0.0, 0.0] var contrast float64
if img.Rows()%ts.k1 != 0 || img.Cols()%ts.k2 != 0 { if img.Rows()%ts.k1 != 0 || img.Cols()%ts.k2 != 0 {
return nil, 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 := int(img.Rows() / ts.k1) lStep := img.Rows() / ts.k1
kStep := int(img.Cols() / ts.k2) kStep := img.Cols() / ts.k2
for l := 0; l < img.Rows(); l = l + lStep {
for k := 0; k < img.Cols(); k = k + kStep {
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. // Enhancement Measure Estimation (EME), provides a measure of the sharpness.
err := ts.EvaluateBlock(edge, l, k, l+lStep, k+kStep, result, "EME", ts.alpha) sharpValue := ts.evaluateBlockEME(edge, l, k, l+lStep, k+kStep)
if err != nil { sharpness += sharpValue
return nil, err
}
// AMEE, provides a measure of the contrast. // AMEE, provides a measure of the contrast.
err = ts.EvaluateBlock(img, l, k, l+lStep, k+kStep, result, "AMEE", ts.alpha) contValue := ts.evaluateBlockAMEE(img, l, k, l+lStep, k+kStep)
if err != nil { contrast += contValue
return nil, err
}
} }
} }
// EME. // Scale EME based on block size.
result[0] = 2.0 / (float64(ts.k1) * float64(ts.k2)) * result[0] sharpness = 2.0 / (float64(ts.k1 * ts.k2)) * sharpness
// AMEE. // Scale and flip AMEE based on block size.
result[1] = -1.0 / (float64(ts.k1) * float64(ts.k2)) * result[1] contrast = -1.0 / (float64(ts.k1 * ts.k2)) * contrast
return result, nil return sharpness, contrast, nil
} }
// Evaluate a block within an image and add to to the result slice. func (ts TurbiditySensor) minMax(img gocv.Mat, xStart, yStart, xEnd, yEnd int) (float64, float64) {
func (ts TurbiditySensor) EvaluateBlock(img gocv.Mat, xStart, yStart, xEnd, yEnd int, result []float64, operation string, alpha float64) error {
max := -math.MaxFloat64 max := -math.MaxFloat64
min := math.MaxFloat64 min := math.MaxFloat64
for i := xStart; i < xEnd; i++ { for i := xStart; i < xEnd; i++ {
for j := yStart; j < yEnd; j++ { for j := yStart; j < yEnd; j++ {
value := float64(img.GetUCharAt(i, j)) value := float64(img.GetUCharAt(i, j))
// Check max/min conditions, zero values are ignored. // Check max/min conditions, zero values are ignored.
@ -131,32 +144,47 @@ func (ts TurbiditySensor) EvaluateBlock(img gocv.Mat, xStart, yStart, xEnd, yEnd
} }
} }
} }
return max, min
}
// Evaluate a block within an image and return the value to be added to the sharpness result.
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 which have no information are ignored.
if max != -math.MaxFloat64 && min != math.MaxFloat64 && max != min { if max != -math.MaxFloat64 && min != math.MaxFloat64 && max != min {
if operation == "EME" { return math.Log(max / min)
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)
}
} }
return nil return 0.0
} }
// Search image for matching template. Returns the transformed image which best match the template. // Evaluate a block within an image and return the value to be added to the contrast result.
func (ts TurbiditySensor) Transform(img gocv.Mat) (gocv.Mat, error) { 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.
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)
}
return 0.0
}
// transform will search img 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() out := gocv.NewMat()
mask := gocv.NewMat() mask := gocv.NewMat()
corners_img := gocv.NewMat() corners_img := gocv.NewMat()
corners_template := gocv.NewMat() corners_template := gocv.NewMat()
// Check image is valid.
if img.Empty() {
return out, errors.New("image is empty, cannot transform")
}
// Find corners in image. // Find corners in image.
if !gocv.FindChessboardCorners(ts.standard, image.Pt(3, 3), &corners_img, gocv.CalibCBNormalizeImage) { 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) { if !gocv.FindChessboardCorners(ts.standard, image.Pt(3, 3), &corners_img, gocv.CalibCBNormalizeImage) {
return out, errors.New("Could not find corners in default image") return out, errors.New("Could not find corners in default image")
} }
@ -174,9 +202,8 @@ func (ts TurbiditySensor) Transform(img gocv.Mat) (gocv.Mat, error) {
return out, nil return out, nil
} }
// Apply sobel filter to an image with a given scale and return the result. // sobel will apply sobel filter to an image and return the result.
func (ts TurbiditySensor) Sobel(img gocv.Mat) gocv.Mat { func (ts TurbiditySensor) sobel(img gocv.Mat) gocv.Mat {
dx := gocv.NewMat() dx := gocv.NewMat()
dy := gocv.NewMat() dy := gocv.NewMat()
sobel := gocv.NewMat() sobel := gocv.NewMat()

View File

@ -0,0 +1,88 @@
/*
DESCRIPTION
Testing functions for the turbidity sensor using images from
previous experiment.
AUTHORS
Russell Stanley <russell@ausocean.org>
LICENSE
Copyright (C) 2020 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
Free Software Foundation, either version 3 of the License, or (at your
option) any later version.
It is distributed in the hope that it will be useful, but WITHOUT
ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
for more details.
You should have received a copy of the GNU General Public License
in gpl.txt. If not, see http://www.gnu.org/licenses.
*/
package main
import (
"fmt"
"testing"
"gocv.io/x/gocv"
)
const (
nImages = 13 // Number of images to test. (Max 13)
nSamples = 10 // Number of samples for each image. (Max 10)
increment = 2.5
)
func TestImages(t *testing.T) {
// Load template and standard image.
template := gocv.IMRead("images/template.jpg", gocv.IMReadGrayScale)
standard := gocv.IMRead("images/default.jpg", gocv.IMReadGrayScale)
imgs := make([][]gocv.Mat, nImages)
// 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.
ts, err := NewTurbiditySensor(template, standard, 8, 8, 3, 1.0, 1.0)
if err != nil {
t.Fatal(err)
}
// Create results
results, err := NewResults(nImages)
if err != nil {
t.Fatal(err)
}
// 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 {
t.Fatalf("Evaluation Failed: %v", err)
}
// Add the average result from camera burst.
results.Update(average(sample_result.Saturation), average(sample_result.Contrast), float64(i)*increment, i)
}
// Plot the final results.
err = plotResults(results.Turbidity, normalize(results.Saturation), normalize(results.Contrast))
if err != nil {
t.Fatalf("Plotting Failed: %v", err)
}
t.Logf("Saturation: %v", results.Saturation)
t.Logf("Contrast: %v", results.Contrast)
}