diff --git a/filter/basic.go b/filter/basic.go new file mode 100644 index 00000000..587facfe --- /dev/null +++ b/filter/basic.go @@ -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 + +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 + } + +} diff --git a/go.mod b/go.mod index dfa6f093..35f29b69 100644 --- a/go.mod +++ b/go.mod @@ -12,5 +12,6 @@ require ( github.com/pkg/errors v0.8.1 github.com/yobert/alsa v0.0.0-20180630182551-d38d89fa843e gocv.io/x/gocv v0.21.0 + golang.org/x/image v0.0.0-20200119044424-58c23975cae1 gopkg.in/natefinch/lumberjack.v2 v2.0.0 ) diff --git a/go.sum b/go.sum index 1b06d678..9a4cad8a 100644 --- a/go.sum +++ b/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= gocv.io/x/gocv v0.21.0 h1:dVjagrupZrfCRY0qPEaYWgoNMRpBel6GYDH4mvQOK8Y= 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/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/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= diff --git a/revid/config/config.go b/revid/config/config.go index 1793215e..cc94c082 100644 --- a/revid/config/config.go +++ b/revid/config/config.go @@ -106,6 +106,10 @@ const ( defaultMOGMinArea = 25.0 defaultMOGThreshold = 20.0 defaultMOGHistory = 500 + + // Basic filter parameter defaults + defaultBasicThreshold = 45000 + defaultBasicPixels = 1000 ) // Quality represents video quality. @@ -128,6 +132,7 @@ const ( FilterVariableFPS FilterKNN FilterDifference + FilterBasic ) // OS names @@ -300,6 +305,9 @@ type Config struct { MOGThreshold float64 // Intensity value from the KNN motion detection algorithm that is considered motion. MOGHistory uint // Length of MOG filter's history + BasicThreshold int + BasicPixels int + // If true will restart reading of input after an io.EOF. Loop bool @@ -313,6 +321,8 @@ type Config struct { // can be set over the web. It is a psuedo const. var TypeData = map[string]string{ "AutoWhiteBalance": "enum:off,auto,sun,cloud,shade,tungsten,fluorescent,incandescent,flash,horizon", + "BasicPixels": "int", + "BasicThreshold": "int", "BitDepth": "int", "Brightness": "uint", "BurstPeriod": "uint", @@ -323,7 +333,7 @@ var TypeData = map[string]string{ "DiffThreshold": "float", "Exposure": "enum:auto,night,nightpreview,backlight,spotlight,sports,snow,beach,verylong,fixedfps,antishake,fireworks", "FileFPS": "int", - "Filters": "enums:NoOp,MOG,VariableFPS,KNN,Difference", + "Filters": "enums:NoOp,MOG,VariableFPS,KNN,Difference,Basic", "FrameRate": "uint", "Height": "uint", "HorizontalFlip": "bool", @@ -524,6 +534,16 @@ func (c *Config) Validate() error { 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 { os, err := osName() if err != nil { diff --git a/revid/revid.go b/revid/revid.go index 5dd4a778..9dd04862 100644 --- a/revid/revid.go +++ b/revid/revid.go @@ -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) case config.FilterDifference: 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: panic("Undefined Filter") } @@ -439,7 +441,6 @@ func (r *Revid) Start() error { if r.cfg.FileFPS != 0 { d = time.Duration(1000/r.cfg.FileFPS) * time.Millisecond } - r.wg.Add(1) go r.processFrom(r.input, d) @@ -686,7 +687,7 @@ func (r *Revid) Update(vars map[string]string) error { } case "Filters": 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)) for i, filter := range filters { v, ok := m[filter] @@ -876,6 +877,21 @@ func (r *Revid) Update(vars map[string]string) error { break } 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": v, err := strconv.Atoi(value) if err != nil {