mirror of https://bitbucket.org/ausocean/av.git
Merged in basic-filter (pull request #366)
Basic filter Approved-by: Saxon Milton <saxon.milton@gmail.com>
This commit is contained in:
commit
c90cc40949
|
@ -0,0 +1,217 @@
|
||||||
|
/*
|
||||||
|
DESCRIPTION
|
||||||
|
A filter that detects motion and discards frames without motion. The
|
||||||
|
filter uses a difference method looking at each individual pixel to
|
||||||
|
determine what is background and what is foreground.
|
||||||
|
|
||||||
|
AUTHORS
|
||||||
|
Ella Pietraroia <ella@ausocean.org>
|
||||||
|
|
||||||
|
LICENSE
|
||||||
|
basic.go is 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 filter
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"image"
|
||||||
|
"image/color"
|
||||||
|
"image/jpeg"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"strconv"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"golang.org/x/image/font"
|
||||||
|
"golang.org/x/image/font/basicfont"
|
||||||
|
"golang.org/x/image/math/fixed"
|
||||||
|
)
|
||||||
|
|
||||||
|
const debugfile = "motion.mjpeg"
|
||||||
|
|
||||||
|
type pixel struct{ r, g, b uint32 }
|
||||||
|
|
||||||
|
// Basic is a filter that provides basic motion detection via a difference
|
||||||
|
// method.
|
||||||
|
type Basic struct {
|
||||||
|
dst io.WriteCloser
|
||||||
|
img image.Image
|
||||||
|
bg [][]pixel
|
||||||
|
bwImg *image.RGBA
|
||||||
|
thresh int
|
||||||
|
pix int
|
||||||
|
w int
|
||||||
|
h int
|
||||||
|
file io.WriteCloser
|
||||||
|
motion int
|
||||||
|
debug bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewBasic returns a pointer to a new Basic filter struct.
|
||||||
|
func NewBasic(dst io.WriteCloser, debug bool, t, p int) *Basic {
|
||||||
|
bwImg := image.NewRGBA(image.Rect(0, 0, 0, 0))
|
||||||
|
var file io.WriteCloser
|
||||||
|
var err error
|
||||||
|
file = nil
|
||||||
|
if debug {
|
||||||
|
file, err = os.Create(debugfile)
|
||||||
|
if err != nil {
|
||||||
|
panic(fmt.Sprintf("could not create debug file: %v", err))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return &Basic{dst, nil, nil, bwImg, t, p, 0, 0, file, 0, debug}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Implements io.Closer.
|
||||||
|
func (bf *Basic) Close() error {
|
||||||
|
if bf.debug {
|
||||||
|
err := bf.file.Close()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("file cannot be closed: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Implements io.Writer.
|
||||||
|
// Write applies the motion filter to the video stream. Only frames with motion
|
||||||
|
// are written to the destination encoder, frames without are discarded.
|
||||||
|
func (bf *Basic) Write(f []byte) (int, error) {
|
||||||
|
// Decode MJPEG.
|
||||||
|
var err error
|
||||||
|
bf.img, err = jpeg.Decode(bytes.NewReader(f))
|
||||||
|
if err != nil {
|
||||||
|
return 0, fmt.Errorf("image can't be decoded: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// First frame must be set as the first background image.
|
||||||
|
if bf.bg == nil {
|
||||||
|
bounds := bf.img.Bounds()
|
||||||
|
bf.w = bounds.Max.X
|
||||||
|
bf.h = bounds.Max.Y
|
||||||
|
|
||||||
|
bf.bwImg = image.NewRGBA(image.Rect(0, 0, bf.w, bf.h))
|
||||||
|
|
||||||
|
bf.bg = make([][]pixel, bf.h)
|
||||||
|
for j, _ := range bf.bg {
|
||||||
|
bf.bg[j] = make([]pixel, bf.w)
|
||||||
|
for i, _ := range bf.bg[j] {
|
||||||
|
p := bf.img.At(i, j)
|
||||||
|
r, g, b, _ := p.RGBA()
|
||||||
|
bf.bg[j][i].r = r
|
||||||
|
bf.bg[j][i].b = b
|
||||||
|
bf.bg[j][i].g = g
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return len(f), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use 4x goroutines to each process one row of pixels.
|
||||||
|
var j int
|
||||||
|
j = 0
|
||||||
|
var wg sync.WaitGroup
|
||||||
|
|
||||||
|
for j < bf.h {
|
||||||
|
wg.Add(4)
|
||||||
|
go bf.process(j, &wg)
|
||||||
|
go bf.process(j+1, &wg)
|
||||||
|
go bf.process(j+2, &wg)
|
||||||
|
go bf.process(j+3, &wg)
|
||||||
|
j = j + 4
|
||||||
|
wg.Wait()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Will save a video of where motion is detected in motion.mjpeg (in the current folder).
|
||||||
|
if bf.debug {
|
||||||
|
err := bf.saveFrame()
|
||||||
|
if err != nil {
|
||||||
|
return len(f), fmt.Errorf("image can't be encoded for debug video: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If there are not enough motion pixels then discard the frame.
|
||||||
|
if bf.motion < bf.pix {
|
||||||
|
return len(f), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write all motion frames.
|
||||||
|
return bf.dst.Write(f)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Go routine for one row of the image to be processed.
|
||||||
|
func (bf *Basic) process(j int, wg *sync.WaitGroup) {
|
||||||
|
for i, _ := range bf.bg[j] {
|
||||||
|
n := bf.img.At(i, j)
|
||||||
|
r, b, g, _ := n.RGBA()
|
||||||
|
|
||||||
|
// Compare the difference of the RGB values of each pixel to the background image.
|
||||||
|
diffR := absDiff(r, bf.bg[j][i].r)
|
||||||
|
diffG := absDiff(g, bf.bg[j][i].g)
|
||||||
|
diffB := absDiff(g, bf.bg[j][i].b)
|
||||||
|
|
||||||
|
diff := diffR + diffG + diffB
|
||||||
|
|
||||||
|
if diff > bf.thresh {
|
||||||
|
bf.motion++
|
||||||
|
}
|
||||||
|
|
||||||
|
if bf.debug {
|
||||||
|
if diff > bf.thresh {
|
||||||
|
bf.bwImg.SetRGBA(i, j, color.RGBA{0xff, 0xff, 0xff, 0xff})
|
||||||
|
} else {
|
||||||
|
bf.bwImg.SetRGBA(i, j, color.RGBA{0x00, 0x00, 0x00, 0xff})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update backgound image.
|
||||||
|
bf.bg[j][i].r = r
|
||||||
|
bf.bg[j][i].b = b
|
||||||
|
bf.bg[j][i].g = g
|
||||||
|
}
|
||||||
|
wg.Done()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Writes a visualisation of the motion being detected to file.
|
||||||
|
func (bf *Basic) saveFrame() error {
|
||||||
|
col := color.RGBA{200, 100, 0, 255} // Red text.
|
||||||
|
d := &font.Drawer{
|
||||||
|
Dst: bf.bwImg,
|
||||||
|
Src: image.NewUniform(col),
|
||||||
|
Face: basicfont.Face7x13,
|
||||||
|
Dot: fixed.P(20, 30),
|
||||||
|
}
|
||||||
|
var s string
|
||||||
|
if bf.motion > bf.pix {
|
||||||
|
s = strconv.Itoa(bf.motion) + " Motion"
|
||||||
|
} else {
|
||||||
|
s = strconv.Itoa(bf.motion)
|
||||||
|
}
|
||||||
|
d.DrawString(s)
|
||||||
|
return jpeg.Encode(bf.file, bf.bwImg, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns the absolute value of the difference of two uint32 numbers.
|
||||||
|
func absDiff(a, b uint32) int {
|
||||||
|
c := int(a) - int(b)
|
||||||
|
if c < 0 {
|
||||||
|
return -c
|
||||||
|
} else {
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
1
go.mod
1
go.mod
|
@ -12,5 +12,6 @@ require (
|
||||||
github.com/pkg/errors v0.8.1
|
github.com/pkg/errors v0.8.1
|
||||||
github.com/yobert/alsa v0.0.0-20180630182551-d38d89fa843e
|
github.com/yobert/alsa v0.0.0-20180630182551-d38d89fa843e
|
||||||
gocv.io/x/gocv v0.21.0
|
gocv.io/x/gocv v0.21.0
|
||||||
|
golang.org/x/image v0.0.0-20200119044424-58c23975cae1
|
||||||
gopkg.in/natefinch/lumberjack.v2 v2.0.0
|
gopkg.in/natefinch/lumberjack.v2 v2.0.0
|
||||||
)
|
)
|
||||||
|
|
3
go.sum
3
go.sum
|
@ -56,7 +56,10 @@ go.uber.org/zap v1.10.0 h1:ORx85nbTijNz8ljznvCMR1ZBIPKFn3jQrag10X2AsuM=
|
||||||
go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
|
go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
|
||||||
gocv.io/x/gocv v0.21.0 h1:dVjagrupZrfCRY0qPEaYWgoNMRpBel6GYDH4mvQOK8Y=
|
gocv.io/x/gocv v0.21.0 h1:dVjagrupZrfCRY0qPEaYWgoNMRpBel6GYDH4mvQOK8Y=
|
||||||
gocv.io/x/gocv v0.21.0/go.mod h1:Rar2PS6DV+T4FL+PM535EImD/h13hGVaHhnCu1xarBs=
|
gocv.io/x/gocv v0.21.0/go.mod h1:Rar2PS6DV+T4FL+PM535EImD/h13hGVaHhnCu1xarBs=
|
||||||
|
golang.org/x/image v0.0.0-20200119044424-58c23975cae1 h1:5h3ngYt7+vXCDZCup/HkCQgW5XwmSvR/nA2JmJ0RErg=
|
||||||
|
golang.org/x/image v0.0.0-20200119044424-58c23975cae1/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
||||||
golang.org/x/sys v0.0.0-20190913121621-c3b328c6e5a7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20190913121621-c3b328c6e5a7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
|
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
|
||||||
|
|
|
@ -106,6 +106,10 @@ const (
|
||||||
defaultMOGMinArea = 25.0
|
defaultMOGMinArea = 25.0
|
||||||
defaultMOGThreshold = 20.0
|
defaultMOGThreshold = 20.0
|
||||||
defaultMOGHistory = 500
|
defaultMOGHistory = 500
|
||||||
|
|
||||||
|
// Basic filter parameter defaults
|
||||||
|
defaultBasicThreshold = 45000
|
||||||
|
defaultBasicPixels = 1000
|
||||||
)
|
)
|
||||||
|
|
||||||
// Quality represents video quality.
|
// Quality represents video quality.
|
||||||
|
@ -128,6 +132,7 @@ const (
|
||||||
FilterVariableFPS
|
FilterVariableFPS
|
||||||
FilterKNN
|
FilterKNN
|
||||||
FilterDifference
|
FilterDifference
|
||||||
|
FilterBasic
|
||||||
)
|
)
|
||||||
|
|
||||||
// OS names
|
// OS names
|
||||||
|
@ -300,6 +305,9 @@ type Config struct {
|
||||||
MOGThreshold float64 // Intensity value from the KNN motion detection algorithm that is considered motion.
|
MOGThreshold float64 // Intensity value from the KNN motion detection algorithm that is considered motion.
|
||||||
MOGHistory uint // Length of MOG filter's history
|
MOGHistory uint // Length of MOG filter's history
|
||||||
|
|
||||||
|
BasicThreshold int
|
||||||
|
BasicPixels int
|
||||||
|
|
||||||
// If true will restart reading of input after an io.EOF.
|
// If true will restart reading of input after an io.EOF.
|
||||||
Loop bool
|
Loop bool
|
||||||
|
|
||||||
|
@ -313,6 +321,8 @@ type Config struct {
|
||||||
// can be set over the web. It is a psuedo const.
|
// can be set over the web. It is a psuedo const.
|
||||||
var TypeData = map[string]string{
|
var TypeData = map[string]string{
|
||||||
"AutoWhiteBalance": "enum:off,auto,sun,cloud,shade,tungsten,fluorescent,incandescent,flash,horizon",
|
"AutoWhiteBalance": "enum:off,auto,sun,cloud,shade,tungsten,fluorescent,incandescent,flash,horizon",
|
||||||
|
"BasicPixels": "int",
|
||||||
|
"BasicThreshold": "int",
|
||||||
"BitDepth": "int",
|
"BitDepth": "int",
|
||||||
"Brightness": "uint",
|
"Brightness": "uint",
|
||||||
"BurstPeriod": "uint",
|
"BurstPeriod": "uint",
|
||||||
|
@ -323,7 +333,7 @@ var TypeData = map[string]string{
|
||||||
"DiffThreshold": "float",
|
"DiffThreshold": "float",
|
||||||
"Exposure": "enum:auto,night,nightpreview,backlight,spotlight,sports,snow,beach,verylong,fixedfps,antishake,fireworks",
|
"Exposure": "enum:auto,night,nightpreview,backlight,spotlight,sports,snow,beach,verylong,fixedfps,antishake,fireworks",
|
||||||
"FileFPS": "int",
|
"FileFPS": "int",
|
||||||
"Filters": "enums:NoOp,MOG,VariableFPS,KNN,Difference",
|
"Filters": "enums:NoOp,MOG,VariableFPS,KNN,Difference,Basic",
|
||||||
"FrameRate": "uint",
|
"FrameRate": "uint",
|
||||||
"Height": "uint",
|
"Height": "uint",
|
||||||
"HorizontalFlip": "bool",
|
"HorizontalFlip": "bool",
|
||||||
|
@ -524,6 +534,16 @@ func (c *Config) Validate() error {
|
||||||
c.MOGHistory = defaultMOGHistory
|
c.MOGHistory = defaultMOGHistory
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if c.BasicThreshold <= 0 {
|
||||||
|
c.logInvalidField("BasicThreshold", defaultBasicThreshold)
|
||||||
|
c.BasicThreshold = defaultBasicThreshold
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.BasicPixels <= 0 {
|
||||||
|
c.logInvalidField("BasicPixels", defaultBasicPixels)
|
||||||
|
c.BasicPixels = defaultBasicPixels
|
||||||
|
}
|
||||||
|
|
||||||
if c.ShowWindows {
|
if c.ShowWindows {
|
||||||
os, err := osName()
|
os, err := osName()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
@ -348,6 +348,8 @@ func (r *Revid) setupPipeline(mtsEnc func(dst io.WriteCloser, rate float64) (io.
|
||||||
r.filters[i] = filter.NewKNN(dst, r.cfg.KNNMinArea, r.cfg.KNNThreshold, int(r.cfg.KNNHistory), int(r.cfg.KNNKernel), r.cfg.ShowWindows, r.cfg.MotionInterval)
|
r.filters[i] = filter.NewKNN(dst, r.cfg.KNNMinArea, r.cfg.KNNThreshold, int(r.cfg.KNNHistory), int(r.cfg.KNNKernel), r.cfg.ShowWindows, r.cfg.MotionInterval)
|
||||||
case config.FilterDifference:
|
case config.FilterDifference:
|
||||||
r.filters[i] = filter.NewDifference(dst, r.cfg.ShowWindows, r.cfg.DiffThreshold)
|
r.filters[i] = filter.NewDifference(dst, r.cfg.ShowWindows, r.cfg.DiffThreshold)
|
||||||
|
case config.FilterBasic:
|
||||||
|
r.filters[i] = filter.NewBasic(dst, r.cfg.ShowWindows, r.cfg.BasicThreshold, r.cfg.BasicPixels)
|
||||||
default:
|
default:
|
||||||
panic("Undefined Filter")
|
panic("Undefined Filter")
|
||||||
}
|
}
|
||||||
|
@ -439,7 +441,6 @@ func (r *Revid) Start() error {
|
||||||
if r.cfg.FileFPS != 0 {
|
if r.cfg.FileFPS != 0 {
|
||||||
d = time.Duration(1000/r.cfg.FileFPS) * time.Millisecond
|
d = time.Duration(1000/r.cfg.FileFPS) * time.Millisecond
|
||||||
}
|
}
|
||||||
|
|
||||||
r.wg.Add(1)
|
r.wg.Add(1)
|
||||||
go r.processFrom(r.input, d)
|
go r.processFrom(r.input, d)
|
||||||
|
|
||||||
|
@ -686,7 +687,7 @@ func (r *Revid) Update(vars map[string]string) error {
|
||||||
}
|
}
|
||||||
case "Filters":
|
case "Filters":
|
||||||
filters := strings.Split(value, ",")
|
filters := strings.Split(value, ",")
|
||||||
m := map[string]int{"NoOp": config.FilterNoOp, "MOG": config.FilterMOG, "VariableFPS": config.FilterVariableFPS, "KNN": config.FilterKNN, "Difference": config.FilterDifference}
|
m := map[string]int{"NoOp": config.FilterNoOp, "MOG": config.FilterMOG, "VariableFPS": config.FilterVariableFPS, "KNN": config.FilterKNN, "Difference": config.FilterDifference, "Basic": config.FilterBasic}
|
||||||
r.cfg.Filters = make([]int, len(filters))
|
r.cfg.Filters = make([]int, len(filters))
|
||||||
for i, filter := range filters {
|
for i, filter := range filters {
|
||||||
v, ok := m[filter]
|
v, ok := m[filter]
|
||||||
|
@ -876,6 +877,21 @@ func (r *Revid) Update(vars map[string]string) error {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
r.cfg.MOGHistory = uint(v)
|
r.cfg.MOGHistory = uint(v)
|
||||||
|
|
||||||
|
case "BasicThreshold":
|
||||||
|
v, err := strconv.Atoi(value)
|
||||||
|
if err != nil {
|
||||||
|
r.cfg.Logger.Log(logger.Warning, pkg+"invalid BasicThreshold var", "value", value)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
r.cfg.BasicThreshold = v
|
||||||
|
case "BasicPixels":
|
||||||
|
v, err := strconv.Atoi(value)
|
||||||
|
if err != nil {
|
||||||
|
r.cfg.Logger.Log(logger.Warning, pkg+"invalid BasicPixels var", "value", value)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
r.cfg.BasicPixels = v
|
||||||
case "FileFPS":
|
case "FileFPS":
|
||||||
v, err := strconv.Atoi(value)
|
v, err := strconv.Atoi(value)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
Loading…
Reference in New Issue