From f6505488bbb4cf39cdc755dce3902a720c43b42c Mon Sep 17 00:00:00 2001 From: Russell Stanley Date: Tue, 12 Apr 2022 13:32:34 +0930 Subject: [PATCH] turbidity: incorporate transform matrix variable into turbidity sensor --- cmd/rv/main.go | 2 +- cmd/rv/probe.go | 44 ++++++++++++++++++-- cmd/rv/probe_circleci.go | 2 +- cmd/rv/probe_test.go | 28 ++++++++++++- turbidity/transform.go | 80 +++++++++++++++++++++++++++++++++++++ turbidity/turbidity.go | 42 ++++--------------- turbidity/turbidity_test.go | 14 +++---- 7 files changed, 161 insertions(+), 51 deletions(-) create mode 100644 turbidity/transform.go diff --git a/cmd/rv/main.go b/cmd/rv/main.go index 50869bbe..242889f1 100644 --- a/cmd/rv/main.go +++ b/cmd/rv/main.go @@ -158,7 +158,7 @@ func main() { log.Log(logger.Fatal, pkg+"could not initialise revid", "error", err.Error()) } - p, err = NewTurbidityProbe(*log, 60*time.Second, rv.Config().TransformMatrix) + p, err = NewTurbidityProbe(*log, 60*time.Second) if err != nil { log.Log(logger.Fatal, "could not create new turbidity probe", "error", err.Error()) } diff --git a/cmd/rv/probe.go b/cmd/rv/probe.go index 8f697c80..d2cb42fb 100644 --- a/cmd/rv/probe.go +++ b/cmd/rv/probe.go @@ -40,6 +40,7 @@ import ( "gonum.org/v1/gonum/stat" "bitbucket.org/ausocean/av/codec/h264" + "bitbucket.org/ausocean/av/revid/config" "bitbucket.org/ausocean/av/turbidity" "bitbucket.org/ausocean/utils/logger" ) @@ -73,18 +74,22 @@ type turbidityProbe struct { } // NewTurbidityProbe returns a new turbidity probe. -func NewTurbidityProbe(log logger.Logger, delay time.Duration, transformMatrix []float64) (*turbidityProbe, error) { +func NewTurbidityProbe(log logger.Logger, delay time.Duration) (*turbidityProbe, error) { tp := new(turbidityProbe) tp.log = log tp.delay = delay tp.ticker = *time.NewTicker(delay) tp.buffer = bytes.NewBuffer(*new([]byte)) - tp.transform = transformMatrix + + tp.transform = []float64{0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0} + H, err := floatToMat(tp.transform) + if err != nil { + return nil, fmt.Errorf("failed to convert float slice to mat: %w", err) + } // Create the turbidity sensor. - standard := gocv.IMRead("../../turbidity/images/default.jpg", gocv.IMReadGrayScale) template := gocv.IMRead("../../turbidity/images/template.jpg", gocv.IMReadGrayScale) - ts, err := turbidity.NewTurbiditySensor(template, standard, k1, k2, filterSize, scale, alpha, log) + ts, err := turbidity.NewTurbiditySensor(template, H, k1, k2, filterSize, scale, alpha, log) if err != nil { return nil, fmt.Errorf("failed to create turbidity sensor: %w", err) } @@ -143,6 +148,27 @@ func (tp *turbidityProbe) Close() error { return nil } +func (tp *turbidityProbe) Update(config config.Config) error { + if len(config.TransformMatrix) != 9 { + return errors.New("transformation matrix has incorrect size") + } + for i := range tp.transform { + if tp.transform[i] != config.TransformMatrix[i] { + // Update the turbidity sensor with new transformation + tp.log.Log(logger.Debug, "updating the turbidity sensor with new transformation") + tp.transform = config.TransformMatrix + newTransform, err := floatToMat(tp.transform) + if err != nil { + return fmt.Errorf("failed to convert float slice to mat: %w", err) + } + tp.ts.H = newTransform + return nil + } + } + tp.log.Log(logger.Debug, "not update requried") + return nil +} + func (tp *turbidityProbe) turbidityCalculation() error { var imgs []gocv.Mat img := gocv.NewMat() @@ -210,3 +236,13 @@ func cleanUp(file string, vc *gocv.VideoCapture) error { } return nil } + +func floatToMat(array []float64) (gocv.Mat, error) { + H := gocv.NewMatWithSize(3, 3, gocv.MatTypeCV64F) + for i := 0; i < H.Rows(); i++ { + for j := 0; j < H.Cols(); j++ { + H.SetDoubleAt(i, j, array[i*H.Cols()+j]) + } + } + return H, nil +} diff --git a/cmd/rv/probe_circleci.go b/cmd/rv/probe_circleci.go index 060424d1..3c82038e 100644 --- a/cmd/rv/probe_circleci.go +++ b/cmd/rv/probe_circleci.go @@ -40,7 +40,7 @@ type turbidityProbe struct { } // NewTurbidityProbe returns an empty turbidity probe for CircleCI testing only. -func NewTurbidityProbe(log logger.Logger, delay time.Duration, transformMatrix []float64) (*turbidityProbe, error) { +func NewTurbidityProbe(log logger.Logger, delay time.Duration) (*turbidityProbe, error) { tp := new(turbidityProbe) return tp, nil } diff --git a/cmd/rv/probe_test.go b/cmd/rv/probe_test.go index f04d6b10..3b5327a1 100644 --- a/cmd/rv/probe_test.go +++ b/cmd/rv/probe_test.go @@ -30,6 +30,7 @@ import ( "testing" "time" + "bitbucket.org/ausocean/av/revid/config" "bitbucket.org/ausocean/utils/logger" "gopkg.in/natefinch/lumberjack.v2" ) @@ -44,12 +45,21 @@ func TestProbe(t *testing.T) { MaxAge: logMaxAge, } log := logger.New(logVerbosity, io.MultiWriter(fileLog), logSuppress) - transformMatrix := []float64{0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0} + updatedMatrix := []float64{-0.2731048063, -0.0020501869, 661.0275911942, 0.0014327789, -0.2699443748, 339.3921028016, 0.0000838317, 0.0000476486, 1.0} + config := config.Config{TransformMatrix: updatedMatrix} - ts, err := NewTurbidityProbe(*log, time.Microsecond, transformMatrix) + ts, err := NewTurbidityProbe(*log, time.Microsecond) if err != nil { t.Fatalf("failed to create turbidity probe") } + + t.Log(ts.transform) + err = ts.Update(config) + if err != nil { + t.Fatalf("could not update probe: %v", err) + } + t.Log(ts.transform) + video, err := ioutil.ReadFile("logo.h264") if err != nil { t.Fatalf("failed to read file: %v", err) @@ -61,3 +71,17 @@ func TestProbe(t *testing.T) { } t.Logf("contrast: %v, sharpness: %v\n", ts.contrast, ts.sharpness) } + +/* +func printMat(mat gocv.Mat) { + for i := 0; i < mat.Rows(); i++ { + for j := 0; j < mat.Cols(); j++ { + fmt.Printf(" %.10f", mat.GetDoubleAt(i, j)) + if i < 2 || j < 2 { + fmt.Print(",") + } + } + } + fmt.Println() +} +*/ diff --git a/turbidity/transform.go b/turbidity/transform.go new file mode 100644 index 00000000..d855ba19 --- /dev/null +++ b/turbidity/transform.go @@ -0,0 +1,80 @@ +//go:build !nocv +// +build !nocv + +/* +DESCRIPTION + +AUTHORS + Russell Stanley + +LICENSE + 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 + 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 turbidity + +import ( + "errors" + "fmt" + "image" + + "gocv.io/x/gocv" +) + +// Homography constants +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. +) + +func FindTransform(standardPath, templatePath string) (gocv.Mat, error) { + mask := gocv.NewMat() + standard := gocv.IMRead(standardPath, gocv.IMReadColor) + standardCorners := gocv.NewMat() + + template := gocv.IMRead(templatePath, gocv.IMReadGrayScale) + templateCorners := gocv.NewMat() + + // Validate template image is not empty and has valid corners. + if template.Empty() { + return gocv.NewMat(), errors.New("template image is empty") + } + if !gocv.FindChessboardCorners(template, image.Pt(3, 3), &templateCorners, gocv.CalibCBNormalizeImage) { + return gocv.NewMat(), errors.New("could not find corners in template image") + } + + // Validate standard image is not empty and has valid corners. + if standard.Empty() { + return gocv.NewMat(), errors.New("standard image is empty") + } + if !gocv.FindChessboardCorners(standard, image.Pt(3, 3), &standardCorners, gocv.CalibCBNormalizeImage) { + return gocv.NewMat(), errors.New("could not find corners in standard image") + } + + H := gocv.FindHomography(standardCorners, &templateCorners, gocv.HomograpyMethodRANSAC, ransacThreshold, &mask, maxIter, confidence) + fmt.Print(H.Type()) + for i := 0; i < H.Rows(); i++ { + for j := 0; j < H.Cols(); j++ { + fmt.Printf(" %.10f", H.GetDoubleAt(i, j)) + if i < 2 || j < 2 { + fmt.Print(",") + } + } + } + fmt.Println() + return H, nil +} diff --git a/turbidity/turbidity.go b/turbidity/turbidity.go index cc8578c2..dd5bec8a 100644 --- a/turbidity/turbidity.go +++ b/turbidity/turbidity.go @@ -41,53 +41,27 @@ import ( "gocv.io/x/gocv" ) -// Homography constants -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. -) - // 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 - standard, standardCorners gocv.Mat - H gocv.Mat - k1, k2, sobelFilterSize int - scale, alpha float64 - log logger.Logger + template gocv.Mat + H gocv.Mat + k1, k2, sobelFilterSize int + scale, alpha float64 + log logger.Logger } // NewTurbiditySensor returns a new TurbiditySensor. -func NewTurbiditySensor(template, standard gocv.Mat, k1, k2, sobelFilterSize int, scale, alpha float64, log logger.Logger) (*TurbiditySensor, error) { +func NewTurbiditySensor(template, H gocv.Mat, k1, k2, sobelFilterSize int, scale, alpha float64, log logger.Logger) (*TurbiditySensor, error) { ts := new(TurbiditySensor) - templateCorners := gocv.NewMat() - standardCorners := gocv.NewMat() - mask := gocv.NewMat() // Validate template image is not empty and has valid corners. if template.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") - } + ts.template = template - ts.templateCorners = templateCorners - - // Validate standard image is not empty and has valid corners. - if standard.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") - } - - ts.standard = standard - ts.standardCorners = standardCorners - ts.H = gocv.FindHomography(ts.standardCorners, &ts.templateCorners, gocv.HomograpyMethodRANSAC, ransacThreshold, &mask, maxIter, confidence) - + ts.H = H ts.k1, ts.k2, ts.sobelFilterSize = k1, k2, sobelFilterSize ts.alpha, ts.scale = alpha, scale ts.log = log diff --git a/turbidity/turbidity_test.go b/turbidity/turbidity_test.go index 72f4e5bf..efe8f240 100644 --- a/turbidity/turbidity_test.go +++ b/turbidity/turbidity_test.go @@ -77,7 +77,10 @@ func TestImages(t *testing.T) { log := *logger.New(logVerbosity, io.MultiWriter(fileLog), logSuppress) template := gocv.IMRead("images/template.jpg", gocv.IMReadGrayScale) - standard := gocv.IMRead("images/default.jpg", gocv.IMReadGrayScale) + H, err := FindTransform("images/default.jpg", "images/template.jpg") + if err != nil { + t.Fatalf("could not find transformation: %v", err) + } imgs := make([][]gocv.Mat, nImages) @@ -89,18 +92,11 @@ func TestImages(t *testing.T) { } } - ts, err := NewTurbiditySensor(template, standard, k1, k2, filterSize, scale, alpha, log) + ts, err := NewTurbiditySensor(template, H, k1, k2, filterSize, scale, alpha, log) if err != nil { t.Fatalf("could not create turbidity sensor: %v", err) } - for i := 0; i < ts.H.Rows(); i++ { - for j := 0; j < ts.H.Cols(); j++ { - fmt.Printf(" %v,\t", ts.H.GetDoubleAt(i, j)) - } - fmt.Println() - } - results, err := NewResults(nImages) if err != nil { t.Fatalf("could not create results: %v", err)