av/turbidity/turbidity.go

228 lines
7.0 KiB
Go
Raw Normal View History

//go:build !ignore
// +build !ignore
/*
DESCRIPTION
Holds the turbidity sensor struct. Can evaluate 4x4 chessboard markers
in an image to measure the sharpness and contrast.
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.
*/
2022-01-05 05:46:08 +03:00
2021-12-15 06:55:14 +03:00
package main
import (
"errors"
2021-12-15 06:55:14 +03:00
"fmt"
2021-12-21 04:43:05 +03:00
"image"
"math"
2021-12-15 06:55:14 +03:00
"gocv.io/x/gocv"
)
// 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.
2021-12-21 04:43:05 +03:00
type TurbiditySensor struct {
template, templateCorners gocv.Mat
standard, standardCorners gocv.Mat
k1, k2, sobelFilterSize int
scale, alpha float64
2021-12-21 04:43:05 +03:00
}
// NewTurbiditySensor constructor for a turbidity sensor.
func NewTurbiditySensor(template, standard gocv.Mat, k1, k2, sobelFilterSize int, scale, alpha float64) (*TurbiditySensor, error) {
ts := new(TurbiditySensor)
templateCorners := gocv.NewMat()
standardCorners := gocv.NewMat()
2021-12-21 04:43:05 +03:00
// 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
2021-12-21 04:43:05 +03:00
// 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.k1, ts.k2, ts.sobelFilterSize = k1, k2, sobelFilterSize
ts.alpha, ts.scale = alpha, scale
return ts, nil
}
2021-12-21 04:43:05 +03:00
// 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 {
// Transform image.
marker, err := ts.transform(imgs[i])
2021-12-21 04:43:05 +03:00
if err != nil {
return result, fmt.Errorf("image %v: %w", i, err)
2021-12-21 04:43:05 +03:00
}
// Apply sobel filter.
edge := ts.sobel(marker)
2021-12-23 09:10:35 +03:00
// Evaluate image.
sharpScore, contScore, err := ts.EvaluateImage(marker, edge)
2021-12-21 04:43:05 +03:00
if err != nil {
return result, err
}
result.Update(sharpScore, contScore, float64(i), i)
2021-12-21 04:43:05 +03:00
}
return result, nil
}
// 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, float64, error) {
var sharpness float64
var contrast float64
2021-12-15 06:55:14 +03:00
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)
2021-12-21 04:43:05 +03:00
}
lStep := img.Rows() / ts.k1
kStep := img.Cols() / ts.k2
2021-12-15 06:55:14 +03:00
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
2021-12-21 04:43:05 +03:00
// AMEE, provides a measure of the contrast.
contValue := ts.evaluateBlockAMEE(img, l, k, l+lStep, k+kStep)
contrast += contValue
2021-12-21 04:43:05 +03:00
}
2021-12-15 06:55:14 +03:00
}
2021-12-21 04:43:05 +03:00
// Scale EME based on block size.
sharpness = 2.0 / (float64(ts.k1 * ts.k2)) * sharpness
2021-12-21 04:43:05 +03:00
// Scale and flip AMEE based on block size.
contrast = -1.0 / (float64(ts.k1 * ts.k2)) * contrast
2021-12-21 04:43:05 +03:00
return sharpness, contrast, nil
2021-12-21 04:43:05 +03:00
}
// minMax returns the max and min pixel values of an image block.
func (ts TurbiditySensor) minMax(img gocv.Mat, xStart, yStart, xEnd, yEnd int) (float64, float64) {
max := -math.MaxFloat64
min := math.MaxFloat64
2021-12-21 04:43:05 +03:00
for i := xStart; i < xEnd; i++ {
for j := yStart; j < yEnd; j++ {
2021-12-21 04:43:05 +03:00
value := float64(img.GetUCharAt(i, j))
// Check max/min conditions, zero values are ignored.
2021-12-21 04:43:05 +03:00
if value > max && value != 0.0 {
max = value
}
if value < min && value != 0.0 {
min = value
}
}
}
return max, min
}
// evaluateBlockEME will evaluate an image block 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)
2021-12-21 04:43:05 +03:00
// Blocks which have no information are ignored.
if max != -math.MaxFloat64 && min != math.MaxFloat64 && max != min {
return math.Log(max / min)
2021-12-21 04:43:05 +03:00
}
return 0.0
2021-12-21 04:43:05 +03:00
}
// evaluateBlockAMEE will evaluate an image block and return the value to be added to the contrast result.
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) {
2021-12-21 04:43:05 +03:00
out := gocv.NewMat()
mask := gocv.NewMat()
imgCorners := gocv.NewMat()
2021-12-21 04:43:05 +03:00
// Check image is valid.
if img.Empty() {
return out, errors.New("image is empty, cannot transform")
}
// Check image for corners, if non can be found corners will be set to default value.
if !gocv.FindChessboardCorners(img, image.Pt(3, 3), &imgCorners, gocv.CalibCBFastCheck) {
imgCorners = ts.standardCorners
2021-12-21 04:43:05 +03:00
}
// Find and apply transformation.
H := gocv.FindHomography(imgCorners, &ts.templateCorners, gocv.HomograpyMethodRANSAC, 3.0, &mask, 2000, 0.995)
2021-12-21 04:43:05 +03:00
gocv.WarpPerspective(img, &out, H, image.Pt(ts.template.Rows(), ts.template.Cols()))
return out, nil
}
// sobel will apply sobel filter to an image and return the result.
func (ts TurbiditySensor) sobel(img gocv.Mat) gocv.Mat {
2021-12-21 04:43:05 +03:00
dx := gocv.NewMat()
dy := gocv.NewMat()
sobel := gocv.NewMat()
// Apply filter.
gocv.Sobel(img, &dx, gocv.MatTypeCV64F, 0, 1, ts.sobelFilterSize, ts.scale, 0.0, gocv.BorderConstant)
gocv.Sobel(img, &dy, gocv.MatTypeCV64F, 1, 0, ts.sobelFilterSize, ts.scale, 0.0, gocv.BorderConstant)
2021-12-21 04:43:05 +03:00
// Convert to unsigned.
2021-12-21 04:43:05 +03:00
gocv.ConvertScaleAbs(dx, &dx, 1.0, 0.0)
gocv.ConvertScaleAbs(dy, &dy, 1.0, 0.0)
// Add x and y components.
2021-12-21 04:43:05 +03:00
gocv.AddWeighted(dx, 0.5, dy, 0.5, 0, &sobel)
return sobel
2021-12-15 06:55:14 +03:00
}