From c4d809be275afee5e33ddae06f9e985162de28d1 Mon Sep 17 00:00:00 2001 From: Ella Pietraroia Date: Fri, 20 Dec 2019 10:42:51 +1030 Subject: [PATCH 01/29] Filter interface take 2 This branch was made because we were having problems with rebasing the original filter-interface branch. Filter-interface was used to make interface for filters and adding into pipeline made a new file (filter.go) that conatins package Filter. This package has the filter interface, with one filter, NoOp. More filters can be added. The filtering stage was placed inbetween lexing and encoding by changing the LexTo function to have the filter as it's destination and making the destination of the filter to be r.encoders --- filter/filter.go | 48 ++++++++++++++++++++++++++++++++++++++++++ filter/mog.go | 2 +- go.mod | 1 - revid/config/config.go | 8 +++++++ revid/revid.go | 22 +++++++++++++++++-- 5 files changed, 77 insertions(+), 4 deletions(-) create mode 100644 filter/filter.go diff --git a/filter/filter.go b/filter/filter.go new file mode 100644 index 00000000..2434d113 --- /dev/null +++ b/filter/filter.go @@ -0,0 +1,48 @@ +/* +NAME + filter.go + +AUTHORS + Ella Pietraroia + +LICENSE + filter.go is Copyright (C) 2019 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 provides the interface and implementations of the filters to be used +// on video input that has been lexed +package filter + +import ( + "io" +) + +type Filter interface { + io.WriteCloser + //NB: Filter interface may evolve with more methods as required +} + +// The NoOp filter will perform no operation on the data that is being recieved, +// it will pass it on to the encoder with no changes. +type NoOp struct { + dst io.Writer +} + +func NewNoOp(dst io.Writer) *NoOp { return &NoOp{dst: dst} } + +func (n *NoOp) Write(p []byte) (int, error) { return n.dst.Write(p) } + +func (n *NoOp) Close() error { return nil } diff --git a/filter/mog.go b/filter/mog.go index 99fe332b..a6343ce0 100644 --- a/filter/mog.go +++ b/filter/mog.go @@ -46,7 +46,7 @@ type MOGFilter struct { } // NewMOGFilter returns a pointer to a new MOGFilter. -func NewMOGFilter(dst io.WriteCloser, area, threshold float64, history, kernelSize int, debug bool) *MogFilter { +func NewMOGFilter(dst io.WriteCloser, area, threshold float64, history, kernelSize int, debug bool) *MOGFilter { bs := gocv.NewBackgroundSubtractorMOG2WithParams(history, threshold, false) k := gocv.GetStructuringElement(gocv.MorphRect, image.Pt(kernelSize, kernelSize)) var windows []*gocv.Window diff --git a/go.mod b/go.mod index 1ab98014..67c14597 100644 --- a/go.mod +++ b/go.mod @@ -12,5 +12,4 @@ require ( github.com/pkg/errors v0.8.1 github.com/yobert/alsa v0.0.0-20180630182551-d38d89fa843e gocv.io/x/gocv v0.21.0 - gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 // indirect ) diff --git a/revid/config/config.go b/revid/config/config.go index 9457199b..9bb51322 100644 --- a/revid/config/config.go +++ b/revid/config/config.go @@ -108,6 +108,13 @@ const ( QualityExcellent ) +// The different filter methods that can be used (currently these are all motion filters +// that will only send video with motion in it) +const ( + FilterNoOp = iota + FilterMOG +) + // Config provides parameters relevant to a revid instance. A new config must // be passed to the constructor. Default values for these fields are defined // as consts above. @@ -248,6 +255,7 @@ type Config struct { HorizontalFlip bool // HorizontalFlip flips video horizontally for Raspivid input. VerticalFlip bool // VerticalFlip flips video vertically for Raspivid input. + FilterMethod int // Defines the method of filtering to be used in between lexing and encoding PSITime int // Sets the time between a packet being sent // RTMP ring buffer parameters. diff --git a/revid/revid.go b/revid/revid.go index b2b399dc..cb098bc0 100644 --- a/revid/revid.go +++ b/revid/revid.go @@ -49,6 +49,7 @@ import ( "bitbucket.org/ausocean/av/device/geovision" "bitbucket.org/ausocean/av/device/raspivid" "bitbucket.org/ausocean/av/device/webcam" + "bitbucket.org/ausocean/av/filter" "bitbucket.org/ausocean/av/revid/config" "bitbucket.org/ausocean/iot/pi/netsender" "bitbucket.org/ausocean/utils/ioext" @@ -110,7 +111,10 @@ type Revid struct { // lexTo, encoder and packer handle transcoding the input stream. lexTo func(dest io.Writer, src io.Reader, delay time.Duration) error - // encoders will hold the multiWriteCloser that writes to encoders from the lexer. + // filter will hold the filter interface that will write to the chosen filter from the lexer. + filter filter.Filter + + // encoders will hold the multiWriteCloser that writes to encoders from the filter. encoders io.WriteCloser // running is used to keep track of revid's running state between methods. @@ -324,6 +328,13 @@ func (r *Revid) setupPipeline(mtsEnc func(dst io.WriteCloser, rate float64) (io. r.encoders = multiWriter(encoders...) + switch r.cfg.FilterMethod { + case config.FilterNoOp: + r.filter = filter.NewNoOp(r.encoders) + default: + r.filter = filter.NewNoOp(r.encoders) + } + switch r.cfg.Input { case config.InputRaspivid: r.input = raspivid.New(r.cfg.Logger) @@ -611,6 +622,13 @@ func (r *Revid) Update(vars map[string]string) error { default: r.cfg.Logger.Log(logger.Warning, pkg+"invalid VerticalFlip param", "value", value) } + case "FilterMethod": + m := map[string]int{"NoOp": config.FilterNoOp, "MOG": config.FilterMOG} + v, ok := m[value] + if !ok { + r.cfg.Logger.Log(logger.Warning, pkg+"invalid FilterMethod param", "value", value) + } + r.cfg.FilterMethod = v case "PSITime": v, err := strconv.Atoi(value) if err != nil || v < 0 { @@ -719,7 +737,7 @@ func (r *Revid) Update(vars map[string]string) error { // processFrom is run as a routine to read from a input data source, lex and // then send individual access units to revid's encoders. func (r *Revid) processFrom(read io.Reader, delay time.Duration) { - r.err <- r.lexTo(r.encoders, read, delay) + r.err <- r.lexTo(r.filter, read, delay) r.cfg.Logger.Log(logger.Info, pkg+"finished lexing") r.wg.Done() } From cbe36f03b0ca2fc697524ff48afdff4768544c91 Mon Sep 17 00:00:00 2001 From: Scott Date: Fri, 20 Dec 2019 12:10:49 +1030 Subject: [PATCH 02/29] revid/revid.go Added MOGFilter as Filter option --- revid/revid.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/revid/revid.go b/revid/revid.go index cb098bc0..61e3be3e 100644 --- a/revid/revid.go +++ b/revid/revid.go @@ -331,6 +331,8 @@ func (r *Revid) setupPipeline(mtsEnc func(dst io.WriteCloser, rate float64) (io. switch r.cfg.FilterMethod { case config.FilterNoOp: r.filter = filter.NewNoOp(r.encoders) + case config.FilterMOG: + r.filter = filter.NewMOGFilter(r.encoders, 25, 20, 500, 3, true) default: r.filter = filter.NewNoOp(r.encoders) } From 736417485850c487b4dd39463f1a4fd8b05e4f6d Mon Sep 17 00:00:00 2001 From: Scott Date: Fri, 20 Dec 2019 15:37:49 +1030 Subject: [PATCH 03/29] Filters shall close when revid stops. The Stop method for revid calls the filter's Close method to free filter resources. --- filter/mog.go | 2 +- revid/revid.go | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/filter/mog.go b/filter/mog.go index a6343ce0..7ae23ce2 100644 --- a/filter/mog.go +++ b/filter/mog.go @@ -65,7 +65,7 @@ func (m *MOGFilter) Close() error { for _, window := range m.windows { window.Close() } - return m.dst.Close() + return nil } // Implements io.Writer. diff --git a/revid/revid.go b/revid/revid.go index 61e3be3e..55b46656 100644 --- a/revid/revid.go +++ b/revid/revid.go @@ -450,6 +450,10 @@ func (r *Revid) Stop() { if err != nil { r.cfg.Logger.Log(logger.Error, pkg+"failed to close pipeline", "error", err.Error()) } + err = r.filter.Close() + if err != nil { + r.cfg.Logger.Log(logger.Error, pkg+"failed to close pipeline", "error", err.Error()) + } r.cfg.Logger.Log(logger.Info, pkg+"closed pipeline") if r.cmd != nil && r.cmd.Process != nil { From d4bfa0828870d6e6df8970ca3659ac2fdb896adf Mon Sep 17 00:00:00 2001 From: Ella Pietraroia Date: Mon, 23 Dec 2019 11:29:25 +1030 Subject: [PATCH 04/29] Documentation comments --- filter/filter.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/filter/filter.go b/filter/filter.go index 2434d113..d415d64f 100644 --- a/filter/filter.go +++ b/filter/filter.go @@ -23,7 +23,7 @@ LICENSE */ // Package filter provides the interface and implementations of the filters to be used -// on video input that has been lexed +// on video input that has been lexed. package filter import ( @@ -32,7 +32,7 @@ import ( type Filter interface { io.WriteCloser - //NB: Filter interface may evolve with more methods as required + //NB: Filter interface may evolve with more methods as required. } // The NoOp filter will perform no operation on the data that is being recieved, From 6d5acb8941b98e23b0dd833b4d1897bcc7207600 Mon Sep 17 00:00:00 2001 From: Ella Pietraroia Date: Mon, 23 Dec 2019 11:59:17 +1030 Subject: [PATCH 05/29] Documentation comments --- filter/filter.go | 1 + revid/config/config.go | 5 ++--- revid/revid.go | 9 +++++---- 3 files changed, 8 insertions(+), 7 deletions(-) diff --git a/filter/filter.go b/filter/filter.go index d415d64f..ec182531 100644 --- a/filter/filter.go +++ b/filter/filter.go @@ -30,6 +30,7 @@ import ( "io" ) +// Interface for all filters. type Filter interface { io.WriteCloser //NB: Filter interface may evolve with more methods as required. diff --git a/revid/config/config.go b/revid/config/config.go index 9bb51322..9a19f4ab 100644 --- a/revid/config/config.go +++ b/revid/config/config.go @@ -108,8 +108,7 @@ const ( QualityExcellent ) -// The different filter methods that can be used (currently these are all motion filters -// that will only send video with motion in it) +// The different media filters. const ( FilterNoOp = iota FilterMOG @@ -255,7 +254,7 @@ type Config struct { HorizontalFlip bool // HorizontalFlip flips video horizontally for Raspivid input. VerticalFlip bool // VerticalFlip flips video vertically for Raspivid input. - FilterMethod int // Defines the method of filtering to be used in between lexing and encoding + Filter int // Defines the method of filtering to be used in between lexing and encoding. PSITime int // Sets the time between a packet being sent // RTMP ring buffer parameters. diff --git a/revid/revid.go b/revid/revid.go index 55b46656..d3cd3cc4 100644 --- a/revid/revid.go +++ b/revid/revid.go @@ -328,13 +328,13 @@ func (r *Revid) setupPipeline(mtsEnc func(dst io.WriteCloser, rate float64) (io. r.encoders = multiWriter(encoders...) - switch r.cfg.FilterMethod { + switch r.cfg.Filter { case config.FilterNoOp: r.filter = filter.NewNoOp(r.encoders) case config.FilterMOG: r.filter = filter.NewMOGFilter(r.encoders, 25, 20, 500, 3, true) default: - r.filter = filter.NewNoOp(r.encoders) + panic("Undefined Filter") } switch r.cfg.Input { @@ -451,6 +451,7 @@ func (r *Revid) Stop() { r.cfg.Logger.Log(logger.Error, pkg+"failed to close pipeline", "error", err.Error()) } err = r.filter.Close() + if err != nil { r.cfg.Logger.Log(logger.Error, pkg+"failed to close pipeline", "error", err.Error()) } @@ -628,13 +629,13 @@ func (r *Revid) Update(vars map[string]string) error { default: r.cfg.Logger.Log(logger.Warning, pkg+"invalid VerticalFlip param", "value", value) } - case "FilterMethod": + case "Filter": m := map[string]int{"NoOp": config.FilterNoOp, "MOG": config.FilterMOG} v, ok := m[value] if !ok { r.cfg.Logger.Log(logger.Warning, pkg+"invalid FilterMethod param", "value", value) } - r.cfg.FilterMethod = v + r.cfg.Filter = v case "PSITime": v, err := strconv.Atoi(value) if err != nil || v < 0 { From 51709d2fd363c32f8dadfbcd567edc3a26b6e84e Mon Sep 17 00:00:00 2001 From: Ella Pietraroia Date: Mon, 23 Dec 2019 12:01:27 +1030 Subject: [PATCH 06/29] Documentation comments --- revid/revid.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/revid/revid.go b/revid/revid.go index d3cd3cc4..8389939f 100644 --- a/revid/revid.go +++ b/revid/revid.go @@ -445,13 +445,13 @@ func (r *Revid) Stop() { r.cfg.Logger.Log(logger.Error, pkg+"could not stop input", "error", err.Error()) } - r.cfg.Logger.Log(logger.Info, pkg+"closing pipeline") + r.cfg.Logger.Log(logger.Info, pkg+"closing pid peline") err = r.encoders.Close() if err != nil { r.cfg.Logger.Log(logger.Error, pkg+"failed to close pipeline", "error", err.Error()) } - err = r.filter.Close() + err = r.filter.Close() if err != nil { r.cfg.Logger.Log(logger.Error, pkg+"failed to close pipeline", "error", err.Error()) } From d0102779ed2cafbb35c863d32645e45dc95f9563 Mon Sep 17 00:00:00 2001 From: Ella Pietraroia Date: Mon, 23 Dec 2019 15:27:46 +1030 Subject: [PATCH 07/29] stderr implementation in webcam same stderr messages made avaliable in raspivid.go now in webcam, so can see when there are problems with with the webcam, rather then our code --- device/webcam/webcam.go | 42 ++++++++++++++++++++++++++++++++++++----- 1 file changed, 37 insertions(+), 5 deletions(-) diff --git a/device/webcam/webcam.go b/device/webcam/webcam.go index 0ac08751..47c1d46c 100644 --- a/device/webcam/webcam.go +++ b/device/webcam/webcam.go @@ -28,6 +28,7 @@ import ( "errors" "fmt" "io" + "io/ioutil" "os/exec" "strings" @@ -61,15 +62,19 @@ var ( // Webcam is an implementation of the AVDevice interface for a Webcam. Webcam // uses an ffmpeg process to pipe the video data from the webcam. type Webcam struct { - out io.ReadCloser - log config.Logger - cfg config.Config - cmd *exec.Cmd + out io.ReadCloser + log config.Logger + cfg config.Config + cmd *exec.Cmd + done chan struct{} } // New returns a new Webcam. func New(l config.Logger) *Webcam { - return &Webcam{log: l} + return &Webcam{ + log: l, + done: make(chan struct{}), + } } // Name returns the name of the device. @@ -151,6 +156,32 @@ func (w *Webcam) Start() error { return fmt.Errorf("failed to create pipe: %w", err) } + stderr, err := w.cmd.StderrPipe() + if err != nil { + return fmt.Errorf("could not pipe command error: %w", err) + } + + go func() { + for { + select { + case <-w.done: + w.cfg.Logger.Log(logger.Info, "raspivid.Stop() called, finished checking stderr") + return + default: + buf, err := ioutil.ReadAll(stderr) + if err != nil { + w.cfg.Logger.Log(logger.Error, "could not read stderr", "error", err) + return + } + + if len(buf) != 0 { + w.cfg.Logger.Log(logger.Error, "error from raspivid stderr", "error", string(buf)) + return + } + } + } + }() + err = w.cmd.Start() if err != nil { return fmt.Errorf("failed to start ffmpeg: %w", err) @@ -161,6 +192,7 @@ func (w *Webcam) Start() error { // Stop will kill the ffmpeg process and close the output pipe. func (w *Webcam) Stop() error { + close(w.done) if w.cmd == nil || w.cmd.Process == nil { return errors.New("ffmpeg process was never started") } From 0da4c3b618cfb28c3153912b86a76a60176ad980 Mon Sep 17 00:00:00 2001 From: Ella Pietraroia Date: Mon, 23 Dec 2019 15:37:20 +1030 Subject: [PATCH 08/29] device/webcam/webcam.go: fixed error output --- device/webcam/webcam.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/device/webcam/webcam.go b/device/webcam/webcam.go index 47c1d46c..fbb3a91b 100644 --- a/device/webcam/webcam.go +++ b/device/webcam/webcam.go @@ -165,7 +165,7 @@ func (w *Webcam) Start() error { for { select { case <-w.done: - w.cfg.Logger.Log(logger.Info, "raspivid.Stop() called, finished checking stderr") + w.cfg.Logger.Log(logger.Info, "webcam.Stop() called, finished checking stderr") return default: buf, err := ioutil.ReadAll(stderr) @@ -175,7 +175,7 @@ func (w *Webcam) Start() error { } if len(buf) != 0 { - w.cfg.Logger.Log(logger.Error, "error from raspivid stderr", "error", string(buf)) + w.cfg.Logger.Log(logger.Error, "error from webcam stderr", "error", string(buf)) return } } From b8f70ddaa1bbac6e9196bc2f553b4d254f5f7858 Mon Sep 17 00:00:00 2001 From: Scott Date: Mon, 23 Dec 2019 16:21:49 +1030 Subject: [PATCH 09/29] revid/config/config.go Changed case of logging variable. --- revid/config/config.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/revid/config/config.go b/revid/config/config.go index 9a19f4ab..eb16d56d 100644 --- a/revid/config/config.go +++ b/revid/config/config.go @@ -286,7 +286,7 @@ var TypeData = map[string]string{ "Input": "enum:raspivid,rtsp,v4l,file", "InputCodec": "enum:H264,MJPEG", "InputPath": "string", - "Logging": "enum:Debug,Info,Warning,Error,Fatal", + "logging": "enum:Debug,Info,Warning,Error,Fatal", "MinFrames": "uint", "MTSRBElementSize": "int", "MTSRBSize": "int", From af2de3f938077e579a2b075d2243494503636eb0 Mon Sep 17 00:00:00 2001 From: Scott Date: Mon, 23 Dec 2019 16:55:40 +1030 Subject: [PATCH 10/29] Created singular Output var --- revid/config/config.go | 1 + revid/revid.go | 19 +++++++++++++++++-- 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/revid/config/config.go b/revid/config/config.go index 9a19f4ab..fd9c2fcf 100644 --- a/revid/config/config.go +++ b/revid/config/config.go @@ -292,6 +292,7 @@ var TypeData = map[string]string{ "MTSRBSize": "int", "MTSRBWriteTimeout": "int", "OutputPath": "string", + "Output": "enum:File,Http,Rtmp,Rtp", "Outputs": "string", "Quantization": "uint", "Rotation": "uint", diff --git a/revid/revid.go b/revid/revid.go index 8389939f..6d2832a3 100644 --- a/revid/revid.go +++ b/revid/revid.go @@ -523,7 +523,7 @@ func (r *Revid) Update(vars map[string]string) error { default: r.cfg.Logger.Log(logger.Warning, pkg+"invalid InputCodec variable value", "value", value) } - case "Output": + case "Outputs": outputs := strings.Split(value, ",") r.cfg.Outputs = make([]uint8, len(outputs)) @@ -538,10 +538,25 @@ func (r *Revid) Update(vars map[string]string) error { case "Rtp": r.cfg.Outputs[i] = config.OutputRTP default: - r.cfg.Logger.Log(logger.Warning, pkg+"invalid output param", "value", value) + r.cfg.Logger.Log(logger.Warning, pkg+"invalid outputs param", "value", value) continue } } + case "Output": + r.cfg.Outputs = make([]uint8, 1) + switch value { + case "File": + r.cfg.Outputs[0] = config.OutputFile + case "Http": + r.cfg.Outputs[0] = config.OutputHTTP + case "Rtmp": + r.cfg.Outputs[0] = config.OutputRTMP + case "Rtp": + r.cfg.Outputs[0] = config.OutputRTP + default: + r.cfg.Logger.Log(logger.Warning, pkg+"invalid output param", "value", value) + continue + } case "RtmpUrl": r.cfg.RTMPURL = value From 9876b0cd35d3f0dd8d3afce2952ff41c5190ac28 Mon Sep 17 00:00:00 2001 From: Scott Date: Tue, 24 Dec 2019 14:08:42 +1030 Subject: [PATCH 11/29] Created motion filter that has a minimum frame rate. --- filter/variable_fps.go | 68 ++++++++++++++++++++++++++++++++++++++++++ revid/config/config.go | 1 + revid/revid.go | 4 ++- 3 files changed, 72 insertions(+), 1 deletion(-) create mode 100644 filter/variable_fps.go diff --git a/filter/variable_fps.go b/filter/variable_fps.go new file mode 100644 index 00000000..39c68d0d --- /dev/null +++ b/filter/variable_fps.go @@ -0,0 +1,68 @@ +/* +DESCRIPTION + A motion filter that has a variable frame rate. When motion is detected, + the filter sends all frames and when it is not, the filter sends frames + at a reduced rate, as set by a parameter. + +AUTHORS + Scott Barnard + +LICENSE + variable_fps.go is Copyright (C) 2019 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 ( + "io" +) + +// VariableFps is a filter that has a variable frame rate. When motion is +// detected, the filter sends all frames and when it is not, the filter +// sends frames at a reduced framerate. +type VariableFps struct { + motionFilter Filter + dst io.WriteCloser + frames uint + count uint +} + +// NewVariableFps returns a pointer to a new VariableFps filter. +func NewVariableFps(dst io.WriteCloser, fps float64) *VariableFps { + frames := uint(25 / fps) + mf := NewMOGFilter(dst, 25, 20, 500, 3, true) + return &VariableFps{mf, dst, frames, 0} +} + +// Implements io.Writer. +// Write applies the motion filter to the video stream. Frames are sent +// at a reduced frame rate, except when motion is detected, then all frames +// with motion are sent. +func (v *VariableFps) Write(f []byte) (int, error) { + v.count++ + if v.count > v.frames { + v.count = 0 + return v.dst.Write(f) + } + + return v.motionFilter.Write(f) +} + +// Implements io.Closer. +// Close calls the motion filter's Close method. +func (v *VariableFps) Close() error { + return v.motionFilter.Close() +} diff --git a/revid/config/config.go b/revid/config/config.go index 9a19f4ab..0fb687e8 100644 --- a/revid/config/config.go +++ b/revid/config/config.go @@ -112,6 +112,7 @@ const ( const ( FilterNoOp = iota FilterMOG + FilterVariableFPS ) // Config provides parameters relevant to a revid instance. A new config must diff --git a/revid/revid.go b/revid/revid.go index 8389939f..8c221738 100644 --- a/revid/revid.go +++ b/revid/revid.go @@ -333,6 +333,8 @@ func (r *Revid) setupPipeline(mtsEnc func(dst io.WriteCloser, rate float64) (io. r.filter = filter.NewNoOp(r.encoders) case config.FilterMOG: r.filter = filter.NewMOGFilter(r.encoders, 25, 20, 500, 3, true) + case config.FilterVariableFPS: + r.filter = filter.NewVariableFps(r.encoders, 1.0) default: panic("Undefined Filter") } @@ -630,7 +632,7 @@ func (r *Revid) Update(vars map[string]string) error { r.cfg.Logger.Log(logger.Warning, pkg+"invalid VerticalFlip param", "value", value) } case "Filter": - m := map[string]int{"NoOp": config.FilterNoOp, "MOG": config.FilterMOG} + m := map[string]int{"NoOp": config.FilterNoOp, "MOG": config.FilterMOG, "VariableFPS": config.FilterVariableFPS} v, ok := m[value] if !ok { r.cfg.Logger.Log(logger.Warning, pkg+"invalid FilterMethod param", "value", value) From 5286ded51f308c712f55372d566f6bdfdf666e1a Mon Sep 17 00:00:00 2001 From: Scott Date: Fri, 27 Dec 2019 13:51:50 +1030 Subject: [PATCH 12/29] Small fixes and simplifications --- filter/variable_fps.go | 12 ++++++------ revid/revid.go | 2 +- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/filter/variable_fps.go b/filter/variable_fps.go index 39c68d0d..606b3656 100644 --- a/filter/variable_fps.go +++ b/filter/variable_fps.go @@ -41,9 +41,9 @@ type VariableFps struct { } // NewVariableFps returns a pointer to a new VariableFps filter. -func NewVariableFps(dst io.WriteCloser, fps float64) *VariableFps { - frames := uint(25 / fps) - mf := NewMOGFilter(dst, 25, 20, 500, 3, true) +func NewVariableFps(dst io.WriteCloser, minFPS float64, debug bool) *VariableFps { + frames := uint(25 / minFPS) + mf := NewMOGFilter(dst, 25, 20, 500, 3, debug) return &VariableFps{mf, dst, frames, 0} } @@ -52,9 +52,9 @@ func NewVariableFps(dst io.WriteCloser, fps float64) *VariableFps { // at a reduced frame rate, except when motion is detected, then all frames // with motion are sent. func (v *VariableFps) Write(f []byte) (int, error) { - v.count++ - if v.count > v.frames { - v.count = 0 + v.count = (v.count + 1) % v.frames + + if v.count == 0 { return v.dst.Write(f) } diff --git a/revid/revid.go b/revid/revid.go index 8c221738..74bbe65a 100644 --- a/revid/revid.go +++ b/revid/revid.go @@ -334,7 +334,7 @@ func (r *Revid) setupPipeline(mtsEnc func(dst io.WriteCloser, rate float64) (io. case config.FilterMOG: r.filter = filter.NewMOGFilter(r.encoders, 25, 20, 500, 3, true) case config.FilterVariableFPS: - r.filter = filter.NewVariableFps(r.encoders, 1.0) + r.filter = filter.NewVariableFps(r.encoders, 1.0, true) default: panic("Undefined Filter") } From 62af00ff4f6d6cbf54f78c5d91a51f4cf538548e Mon Sep 17 00:00:00 2001 From: Scott Date: Tue, 31 Dec 2019 10:54:13 +1030 Subject: [PATCH 13/29] PR changes #1 --- filter/{variable_fps.go => vfps.go} | 29 ++++++++++++++--------------- revid/revid.go | 2 +- 2 files changed, 15 insertions(+), 16 deletions(-) rename filter/{variable_fps.go => vfps.go} (68%) diff --git a/filter/variable_fps.go b/filter/vfps.go similarity index 68% rename from filter/variable_fps.go rename to filter/vfps.go index 606b3656..93b6f543 100644 --- a/filter/variable_fps.go +++ b/filter/vfps.go @@ -8,7 +8,7 @@ AUTHORS Scott Barnard LICENSE - variable_fps.go is Copyright (C) 2019 the Australian Ocean Lab (AusOcean) + vfps.go is Copyright (C) 2019 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 @@ -30,39 +30,38 @@ import ( "io" ) -// VariableFps is a filter that has a variable frame rate. When motion is +// VariableFPSFilter is a filter that has a variable frame rate. When motion is // detected, the filter sends all frames and when it is not, the filter // sends frames at a reduced framerate. -type VariableFps struct { - motionFilter Filter - dst io.WriteCloser - frames uint - count uint +type VariableFPSFilter struct { + filter Filter + dst io.WriteCloser + frames uint + count uint } -// NewVariableFps returns a pointer to a new VariableFps filter. -func NewVariableFps(dst io.WriteCloser, minFPS float64, debug bool) *VariableFps { +// NewVariableFPSFilter returns a pointer to a new VariableFPSFilter struct. +func NewVariableFPSFilter(dst io.WriteCloser, minFPS float64, filter Filter) *VariableFPSFilter { frames := uint(25 / minFPS) - mf := NewMOGFilter(dst, 25, 20, 500, 3, debug) - return &VariableFps{mf, dst, frames, 0} + return &VariableFPSFilter{filter, dst, frames, 0} } // Implements io.Writer. // Write applies the motion filter to the video stream. Frames are sent // at a reduced frame rate, except when motion is detected, then all frames // with motion are sent. -func (v *VariableFps) Write(f []byte) (int, error) { +func (v *VariableFPSFilter) Write(f []byte) (int, error) { v.count = (v.count + 1) % v.frames if v.count == 0 { return v.dst.Write(f) } - return v.motionFilter.Write(f) + return v.filter.Write(f) } // Implements io.Closer. // Close calls the motion filter's Close method. -func (v *VariableFps) Close() error { - return v.motionFilter.Close() +func (v *VariableFPSFilter) Close() error { + return v.filter.Close() } diff --git a/revid/revid.go b/revid/revid.go index 74bbe65a..073c4348 100644 --- a/revid/revid.go +++ b/revid/revid.go @@ -334,7 +334,7 @@ func (r *Revid) setupPipeline(mtsEnc func(dst io.WriteCloser, rate float64) (io. case config.FilterMOG: r.filter = filter.NewMOGFilter(r.encoders, 25, 20, 500, 3, true) case config.FilterVariableFPS: - r.filter = filter.NewVariableFps(r.encoders, 1.0, true) + r.filter = filter.NewVariableFPSFilter(r.encoders, 1.0, filter.NewMOGFilter(r.encoders, 25, 20, 500, 3, true)) default: panic("Undefined Filter") } From e0760f00965b3a6abaca66227db94773c1d6717e Mon Sep 17 00:00:00 2001 From: Scott Date: Tue, 31 Dec 2019 12:26:35 +1030 Subject: [PATCH 14/29] Circle-CV ignores gocv/OpenCV for testing. --- .circleci/config.yml | 4 ++-- exp/gocv-exp/main.go | 2 ++ filter/filters_circleci.go | 37 +++++++++++++++++++++++++++++++++++++ filter/mog.go | 4 +++- 4 files changed, 44 insertions(+), 3 deletions(-) create mode 100644 filter/filters_circleci.go diff --git a/.circleci/config.yml b/.circleci/config.yml index 85bfbcc7..b4ad0827 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -23,9 +23,9 @@ jobs: - run: go get -d -t -v ./... - - run: go build -v ./... + - run: go build -v -tags circleci ./... - - run: go test -v ./... + - run: go test -v -tags circleci ./... - save_cache: key: v1-pkg-cache diff --git a/exp/gocv-exp/main.go b/exp/gocv-exp/main.go index 9de10184..8083df5a 100644 --- a/exp/gocv-exp/main.go +++ b/exp/gocv-exp/main.go @@ -1,3 +1,5 @@ +// +build !circleci + // What it does: // // This example detects motion using a delta threshold from the first frame, diff --git a/filter/filters_circleci.go b/filter/filters_circleci.go new file mode 100644 index 00000000..b5286f97 --- /dev/null +++ b/filter/filters_circleci.go @@ -0,0 +1,37 @@ +// +build circleci + +/* +DESCRIPTION + Replaces filters that use the gocv package when Circle-CI builds revid. This + is needed because Circle-CI does not have a copy of Open CV installed. + +AUTHORS + Scott Barnard + +LICENSE + filters_circleci.go is Copyright (C) 2019 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 ( + "io" +) + +// NewMOGFilter returns a pointer to a new NoOp struct for testing purposes only. +func NewMOGFilter(dst io.WriteCloser, area, threshold float64, history, kernelSize int, debug bool) *NoOp { + return &NoOp{dst: dst} +} diff --git a/filter/mog.go b/filter/mog.go index 7ae23ce2..283ea034 100644 --- a/filter/mog.go +++ b/filter/mog.go @@ -1,3 +1,5 @@ +// +build !circleci + /* DESCRIPTION A filter that detects motion and discards frames without motion. The @@ -45,7 +47,7 @@ type MOGFilter struct { windows []*gocv.Window } -// NewMOGFilter returns a pointer to a new MOGFilter. +// NewMOGFilter returns a pointer to a new MOGFilter struct. func NewMOGFilter(dst io.WriteCloser, area, threshold float64, history, kernelSize int, debug bool) *MOGFilter { bs := gocv.NewBackgroundSubtractorMOG2WithParams(history, threshold, false) k := gocv.GetStructuringElement(gocv.MorphRect, image.Pt(kernelSize, kernelSize)) From a02a60b00883a45805a6cdd783028801a9bdcb4f Mon Sep 17 00:00:00 2001 From: Ella Pietraroia Date: Mon, 23 Dec 2019 14:58:35 +1030 Subject: [PATCH 15/29] making new knn file, plus adding knn option into variables --- revid/config/config.go | 1 + revid/revid.go | 4 +++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/revid/config/config.go b/revid/config/config.go index 00c4dda0..d13157b2 100644 --- a/revid/config/config.go +++ b/revid/config/config.go @@ -113,6 +113,7 @@ const ( FilterNoOp = iota FilterMOG FilterVariableFPS + FilterKNN ) // Config provides parameters relevant to a revid instance. A new config must diff --git a/revid/revid.go b/revid/revid.go index d2558057..33e3b498 100644 --- a/revid/revid.go +++ b/revid/revid.go @@ -335,6 +335,8 @@ func (r *Revid) setupPipeline(mtsEnc func(dst io.WriteCloser, rate float64) (io. r.filter = filter.NewMOGFilter(r.encoders, 25, 20, 500, 3, true) case config.FilterVariableFPS: r.filter = filter.NewVariableFPSFilter(r.encoders, 1.0, filter.NewMOGFilter(r.encoders, 25, 20, 500, 3, true)) + case config.FilterKNN: + r.filter = filter.NewKNNFilter(r.encoders, 25, 20, 500, 3, true) default: panic("Undefined Filter") } @@ -647,7 +649,7 @@ func (r *Revid) Update(vars map[string]string) error { r.cfg.Logger.Log(logger.Warning, pkg+"invalid VerticalFlip param", "value", value) } case "Filter": - m := map[string]int{"NoOp": config.FilterNoOp, "MOG": config.FilterMOG, "VariableFPS": config.FilterVariableFPS} + m := map[string]int{"NoOp": config.FilterNoOp, "MOG": config.FilterMOG, "VariableFPS": config.FilterVariableFPS, "KNN": config.FilterKNN} v, ok := m[value] if !ok { r.cfg.Logger.Log(logger.Warning, pkg+"invalid FilterMethod param", "value", value) From 8f3faeb197085ec57911b786b82d701b2eab9ce1 Mon Sep 17 00:00:00 2001 From: Ella Pietraroia Date: Mon, 23 Dec 2019 14:59:48 +1030 Subject: [PATCH 16/29] adding knn.go file --- filter/knn.go | 127 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 127 insertions(+) create mode 100644 filter/knn.go diff --git a/filter/knn.go b/filter/knn.go new file mode 100644 index 00000000..efa43862 --- /dev/null +++ b/filter/knn.go @@ -0,0 +1,127 @@ +/* +DESCRIPTION + A filter that detects motion and discards frames without motion. The + filter uses a Mixture of Gaussians method (KNN) to determine what is + background and what is foreground. + +AUTHORS + Scott Barnard + +LICENSE + KNN.go is Copyright (C) 2019 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 ( + "image" + "image/color" + "io" + + "gocv.io/x/gocv" +) + +// KNNFilter is a filter that provides basic motion detection. KNN is short for +// Mixture of Gaussians method. +type KNNFilter struct { + dst io.WriteCloser + area float64 + bs *gocv.BackgroundSubtractorKNN + knl gocv.Mat + debug bool + windows []*gocv.Window +} + +// NewKNNFilter returns a pointer to a new KNNFilter. +func NewKNNFilter(dst io.WriteCloser, area, threshold float64, history, kernelSize int, debug bool) *KNNFilter { + bs := gocv.NewBackgroundSubtractorKNNWithParams(history, threshold, false) + k := gocv.GetStructuringElement(gocv.MorphRect, image.Pt(kernelSize, kernelSize)) + var windows []*gocv.Window + if debug { + windows = []*gocv.Window{gocv.NewWindow("Debug: Bounding boxes"), gocv.NewWindow("Debug: Motion")} + } + return &KNNFilter{dst, area, &bs, k, debug, windows} +} + +// Implements io.Closer. +// Close frees resources used by gocv, because it has to be done manually, due to +// it using c-go. +func (m *KNNFilter) Close() error { + m.bs.Close() + m.knl.Close() + for _, window := range m.windows { + window.Close() + } + 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 (m *KNNFilter) Write(f []byte) (int, error) { + img, _ := gocv.IMDecode(f, gocv.IMReadColor) + defer img.Close() + + imgDelta := gocv.NewMat() + defer imgDelta.Close() + + // Seperate foreground and background. + m.bs.Apply(img, &imgDelta) + + // Threshold imgDelta. + gocv.Threshold(imgDelta, &imgDelta, 25, 255, gocv.ThresholdBinary) + + // Remove noise. + gocv.Erode(imgDelta, &imgDelta, m.knl) + gocv.Dilate(imgDelta, &imgDelta, m.knl) + + // Fill small holes. + gocv.Dilate(imgDelta, &imgDelta, m.knl) + gocv.Erode(imgDelta, &imgDelta, m.knl) + + // Find contours and reject ones with a small area. + var contours [][]image.Point + allContours := gocv.FindContours(imgDelta, gocv.RetrievalExternal, gocv.ChainApproxSimple) + for _, c := range allContours { + if gocv.ContourArea(c) > m.area { + contours = append(contours, c) + } + } + + // Draw debug information. + if m.debug { + for _, c := range contours { + rect := gocv.BoundingRect(c) + gocv.Rectangle(&img, rect, color.RGBA{0, 0, 255, 0}, 1) + } + + if len(contours) > 0 { + gocv.PutText(&img, "Motion", image.Pt(32, 32), gocv.FontHersheyPlain, 2.0, color.RGBA{255, 0, 0, 0}, 2) + } + + m.windows[0].IMShow(img) + m.windows[1].IMShow(imgDelta) + m.windows[0].WaitKey(1) + } + + // Don't write to destination if there is no motion. + if len(contours) == 0 { + return -1, nil + } + + // Write to destination. + return m.dst.Write(f) +} From 3e238bb676293f409c9ccfeb5f2dc8eee36a961f Mon Sep 17 00:00:00 2001 From: Ella Pietraroia Date: Tue, 31 Dec 2019 13:26:36 +1030 Subject: [PATCH 17/29] chaning parameters of filter --- revid/revid.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/revid/revid.go b/revid/revid.go index 33e3b498..5f4df7d8 100644 --- a/revid/revid.go +++ b/revid/revid.go @@ -336,7 +336,7 @@ func (r *Revid) setupPipeline(mtsEnc func(dst io.WriteCloser, rate float64) (io. case config.FilterVariableFPS: r.filter = filter.NewVariableFPSFilter(r.encoders, 1.0, filter.NewMOGFilter(r.encoders, 25, 20, 500, 3, true)) case config.FilterKNN: - r.filter = filter.NewKNNFilter(r.encoders, 25, 20, 500, 3, true) + r.filter = filter.NewKNNFilter(r.encoders, 25, 300, 300, 9, true) default: panic("Undefined Filter") } From 51e45c6ffa5378b916d37e745008b3f22f1641e6 Mon Sep 17 00:00:00 2001 From: Ella Pietraroia Date: Tue, 31 Dec 2019 13:37:47 +1030 Subject: [PATCH 18/29] Documentation comments --- filter/knn.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/filter/knn.go b/filter/knn.go index efa43862..3e494253 100644 --- a/filter/knn.go +++ b/filter/knn.go @@ -1,11 +1,11 @@ /* DESCRIPTION A filter that detects motion and discards frames without motion. The - filter uses a Mixture of Gaussians method (KNN) to determine what is + filter uses a K-Nearest Neighbours (KNN) to determine what is background and what is foreground. AUTHORS - Scott Barnard + Ella Pietraroia LICENSE KNN.go is Copyright (C) 2019 the Australian Ocean Lab (AusOcean) @@ -35,7 +35,7 @@ import ( ) // KNNFilter is a filter that provides basic motion detection. KNN is short for -// Mixture of Gaussians method. +// K-Nearest Neighbours method. type KNNFilter struct { dst io.WriteCloser area float64 From 65f1c89ff2220eb828dcec312da11dbc620449bc Mon Sep 17 00:00:00 2001 From: Ella Pietraroia Date: Tue, 31 Dec 2019 15:18:06 +1030 Subject: [PATCH 19/29] adding constants for the integer arguments --- revid/revid.go | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/revid/revid.go b/revid/revid.go index 5f4df7d8..fd3f83cf 100644 --- a/revid/revid.go +++ b/revid/revid.go @@ -76,6 +76,15 @@ const ( rtmpConnectionTimeout = 10 ) +// KNN filter properties +const ( + knnMinArea = 25.0 + knnThreshold = 300 + knnHistory = 300 + knnKernel = 9 + knnShowWindows = true +) + const pkg = "revid: " type Logger interface { @@ -336,7 +345,7 @@ func (r *Revid) setupPipeline(mtsEnc func(dst io.WriteCloser, rate float64) (io. case config.FilterVariableFPS: r.filter = filter.NewVariableFPSFilter(r.encoders, 1.0, filter.NewMOGFilter(r.encoders, 25, 20, 500, 3, true)) case config.FilterKNN: - r.filter = filter.NewKNNFilter(r.encoders, 25, 300, 300, 9, true) + r.filter = filter.NewKNNFilter(r.encoders, knnMinArea, knnThreshold, knnHistory, knnKernel, knnShowWindows) default: panic("Undefined Filter") } From d44e795ea26cfcf26d351165f48afc8db781a6c1 Mon Sep 17 00:00:00 2001 From: Ella Pietraroia Date: Mon, 23 Dec 2019 14:58:35 +1030 Subject: [PATCH 20/29] making new knn file, plus adding knn option into variables --- revid/config/config.go | 1 + revid/revid.go | 4 +++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/revid/config/config.go b/revid/config/config.go index 00c4dda0..d13157b2 100644 --- a/revid/config/config.go +++ b/revid/config/config.go @@ -113,6 +113,7 @@ const ( FilterNoOp = iota FilterMOG FilterVariableFPS + FilterKNN ) // Config provides parameters relevant to a revid instance. A new config must diff --git a/revid/revid.go b/revid/revid.go index d2558057..33e3b498 100644 --- a/revid/revid.go +++ b/revid/revid.go @@ -335,6 +335,8 @@ func (r *Revid) setupPipeline(mtsEnc func(dst io.WriteCloser, rate float64) (io. r.filter = filter.NewMOGFilter(r.encoders, 25, 20, 500, 3, true) case config.FilterVariableFPS: r.filter = filter.NewVariableFPSFilter(r.encoders, 1.0, filter.NewMOGFilter(r.encoders, 25, 20, 500, 3, true)) + case config.FilterKNN: + r.filter = filter.NewKNNFilter(r.encoders, 25, 20, 500, 3, true) default: panic("Undefined Filter") } @@ -647,7 +649,7 @@ func (r *Revid) Update(vars map[string]string) error { r.cfg.Logger.Log(logger.Warning, pkg+"invalid VerticalFlip param", "value", value) } case "Filter": - m := map[string]int{"NoOp": config.FilterNoOp, "MOG": config.FilterMOG, "VariableFPS": config.FilterVariableFPS} + m := map[string]int{"NoOp": config.FilterNoOp, "MOG": config.FilterMOG, "VariableFPS": config.FilterVariableFPS, "KNN": config.FilterKNN} v, ok := m[value] if !ok { r.cfg.Logger.Log(logger.Warning, pkg+"invalid FilterMethod param", "value", value) From 0da2d3182a306348a197d3a8a9f7f6184d6c98d8 Mon Sep 17 00:00:00 2001 From: Ella Pietraroia Date: Mon, 23 Dec 2019 14:59:48 +1030 Subject: [PATCH 21/29] adding knn.go file --- filter/knn.go | 127 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 127 insertions(+) create mode 100644 filter/knn.go diff --git a/filter/knn.go b/filter/knn.go new file mode 100644 index 00000000..efa43862 --- /dev/null +++ b/filter/knn.go @@ -0,0 +1,127 @@ +/* +DESCRIPTION + A filter that detects motion and discards frames without motion. The + filter uses a Mixture of Gaussians method (KNN) to determine what is + background and what is foreground. + +AUTHORS + Scott Barnard + +LICENSE + KNN.go is Copyright (C) 2019 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 ( + "image" + "image/color" + "io" + + "gocv.io/x/gocv" +) + +// KNNFilter is a filter that provides basic motion detection. KNN is short for +// Mixture of Gaussians method. +type KNNFilter struct { + dst io.WriteCloser + area float64 + bs *gocv.BackgroundSubtractorKNN + knl gocv.Mat + debug bool + windows []*gocv.Window +} + +// NewKNNFilter returns a pointer to a new KNNFilter. +func NewKNNFilter(dst io.WriteCloser, area, threshold float64, history, kernelSize int, debug bool) *KNNFilter { + bs := gocv.NewBackgroundSubtractorKNNWithParams(history, threshold, false) + k := gocv.GetStructuringElement(gocv.MorphRect, image.Pt(kernelSize, kernelSize)) + var windows []*gocv.Window + if debug { + windows = []*gocv.Window{gocv.NewWindow("Debug: Bounding boxes"), gocv.NewWindow("Debug: Motion")} + } + return &KNNFilter{dst, area, &bs, k, debug, windows} +} + +// Implements io.Closer. +// Close frees resources used by gocv, because it has to be done manually, due to +// it using c-go. +func (m *KNNFilter) Close() error { + m.bs.Close() + m.knl.Close() + for _, window := range m.windows { + window.Close() + } + 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 (m *KNNFilter) Write(f []byte) (int, error) { + img, _ := gocv.IMDecode(f, gocv.IMReadColor) + defer img.Close() + + imgDelta := gocv.NewMat() + defer imgDelta.Close() + + // Seperate foreground and background. + m.bs.Apply(img, &imgDelta) + + // Threshold imgDelta. + gocv.Threshold(imgDelta, &imgDelta, 25, 255, gocv.ThresholdBinary) + + // Remove noise. + gocv.Erode(imgDelta, &imgDelta, m.knl) + gocv.Dilate(imgDelta, &imgDelta, m.knl) + + // Fill small holes. + gocv.Dilate(imgDelta, &imgDelta, m.knl) + gocv.Erode(imgDelta, &imgDelta, m.knl) + + // Find contours and reject ones with a small area. + var contours [][]image.Point + allContours := gocv.FindContours(imgDelta, gocv.RetrievalExternal, gocv.ChainApproxSimple) + for _, c := range allContours { + if gocv.ContourArea(c) > m.area { + contours = append(contours, c) + } + } + + // Draw debug information. + if m.debug { + for _, c := range contours { + rect := gocv.BoundingRect(c) + gocv.Rectangle(&img, rect, color.RGBA{0, 0, 255, 0}, 1) + } + + if len(contours) > 0 { + gocv.PutText(&img, "Motion", image.Pt(32, 32), gocv.FontHersheyPlain, 2.0, color.RGBA{255, 0, 0, 0}, 2) + } + + m.windows[0].IMShow(img) + m.windows[1].IMShow(imgDelta) + m.windows[0].WaitKey(1) + } + + // Don't write to destination if there is no motion. + if len(contours) == 0 { + return -1, nil + } + + // Write to destination. + return m.dst.Write(f) +} From 197fbcf24221a3b5bf159dc5e1b046f9c18d092b Mon Sep 17 00:00:00 2001 From: Ella Pietraroia Date: Tue, 31 Dec 2019 13:26:36 +1030 Subject: [PATCH 22/29] chaning parameters of filter --- revid/revid.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/revid/revid.go b/revid/revid.go index 33e3b498..5f4df7d8 100644 --- a/revid/revid.go +++ b/revid/revid.go @@ -336,7 +336,7 @@ func (r *Revid) setupPipeline(mtsEnc func(dst io.WriteCloser, rate float64) (io. case config.FilterVariableFPS: r.filter = filter.NewVariableFPSFilter(r.encoders, 1.0, filter.NewMOGFilter(r.encoders, 25, 20, 500, 3, true)) case config.FilterKNN: - r.filter = filter.NewKNNFilter(r.encoders, 25, 20, 500, 3, true) + r.filter = filter.NewKNNFilter(r.encoders, 25, 300, 300, 9, true) default: panic("Undefined Filter") } From 6d45572a8221b085b44fd2bda7e1667b803d2dbf Mon Sep 17 00:00:00 2001 From: Ella Pietraroia Date: Tue, 31 Dec 2019 13:37:47 +1030 Subject: [PATCH 23/29] Documentation comments --- filter/knn.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/filter/knn.go b/filter/knn.go index efa43862..3e494253 100644 --- a/filter/knn.go +++ b/filter/knn.go @@ -1,11 +1,11 @@ /* DESCRIPTION A filter that detects motion and discards frames without motion. The - filter uses a Mixture of Gaussians method (KNN) to determine what is + filter uses a K-Nearest Neighbours (KNN) to determine what is background and what is foreground. AUTHORS - Scott Barnard + Ella Pietraroia LICENSE KNN.go is Copyright (C) 2019 the Australian Ocean Lab (AusOcean) @@ -35,7 +35,7 @@ import ( ) // KNNFilter is a filter that provides basic motion detection. KNN is short for -// Mixture of Gaussians method. +// K-Nearest Neighbours method. type KNNFilter struct { dst io.WriteCloser area float64 From 8ebfa0a7fbbb58c0769686b947a07f3c3b66e092 Mon Sep 17 00:00:00 2001 From: Ella Pietraroia Date: Tue, 31 Dec 2019 15:18:06 +1030 Subject: [PATCH 24/29] adding constants for the integer arguments --- revid/revid.go | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/revid/revid.go b/revid/revid.go index 5f4df7d8..fd3f83cf 100644 --- a/revid/revid.go +++ b/revid/revid.go @@ -76,6 +76,15 @@ const ( rtmpConnectionTimeout = 10 ) +// KNN filter properties +const ( + knnMinArea = 25.0 + knnThreshold = 300 + knnHistory = 300 + knnKernel = 9 + knnShowWindows = true +) + const pkg = "revid: " type Logger interface { @@ -336,7 +345,7 @@ func (r *Revid) setupPipeline(mtsEnc func(dst io.WriteCloser, rate float64) (io. case config.FilterVariableFPS: r.filter = filter.NewVariableFPSFilter(r.encoders, 1.0, filter.NewMOGFilter(r.encoders, 25, 20, 500, 3, true)) case config.FilterKNN: - r.filter = filter.NewKNNFilter(r.encoders, 25, 300, 300, 9, true) + r.filter = filter.NewKNNFilter(r.encoders, knnMinArea, knnThreshold, knnHistory, knnKernel, knnShowWindows) default: panic("Undefined Filter") } From 40b5374856dca766aab2086754774ed4fb8277fa Mon Sep 17 00:00:00 2001 From: Ella Pietraroia Date: Tue, 31 Dec 2019 15:22:57 +1030 Subject: [PATCH 25/29] fixing circleci build problem with knn filter --- filter/filters_circleci.go | 4 ++++ filter/knn.go | 2 ++ 2 files changed, 6 insertions(+) diff --git a/filter/filters_circleci.go b/filter/filters_circleci.go index b5286f97..b6ebcc70 100644 --- a/filter/filters_circleci.go +++ b/filter/filters_circleci.go @@ -35,3 +35,7 @@ import ( func NewMOGFilter(dst io.WriteCloser, area, threshold float64, history, kernelSize int, debug bool) *NoOp { return &NoOp{dst: dst} } + +func NewKNNFilter(dst io.WriteCloser, area, threshold float64, history, kernelSize int, debug bool) *KNNFilter { + return &NoOp{dst: dst} +} diff --git a/filter/knn.go b/filter/knn.go index 3e494253..d4eb13ed 100644 --- a/filter/knn.go +++ b/filter/knn.go @@ -1,4 +1,6 @@ /* +// +build !circleci + DESCRIPTION A filter that detects motion and discards frames without motion. The filter uses a K-Nearest Neighbours (KNN) to determine what is From 8c32265ae367f91f28b095a025e060e96893177b Mon Sep 17 00:00:00 2001 From: Ella Pietraroia Date: Tue, 31 Dec 2019 15:29:58 +1030 Subject: [PATCH 26/29] error message for decoding image --- filter/knn.go | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/filter/knn.go b/filter/knn.go index d4eb13ed..cc65d1de 100644 --- a/filter/knn.go +++ b/filter/knn.go @@ -29,6 +29,7 @@ LICENSE package filter import ( + "fmt" "image" "image/color" "io" @@ -74,7 +75,10 @@ func (m *KNNFilter) Close() error { // Write applies the motion filter to the video stream. Only frames with motion // are written to the destination encoder, frames without are discarded. func (m *KNNFilter) Write(f []byte) (int, error) { - img, _ := gocv.IMDecode(f, gocv.IMReadColor) + img, err := gocv.IMDecode(f, gocv.IMReadColor) + if err != nil { + return 0, fmt.Errorf("can't decode image: %w", err) + } defer img.Close() imgDelta := gocv.NewMat() From 062b341e683f0db69dbf2ae5066ada551de201f9 Mon Sep 17 00:00:00 2001 From: Ella Pietraroia Date: Tue, 31 Dec 2019 15:39:09 +1030 Subject: [PATCH 27/29] fixing merge problems --- filter/knn.go | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/filter/knn.go b/filter/knn.go index 74abe098..cc65d1de 100644 --- a/filter/knn.go +++ b/filter/knn.go @@ -1,9 +1,6 @@ /* -<<<<<<< HEAD // +build !circleci -======= ->>>>>>> 65f1c89ff2220eb828dcec312da11dbc620449bc DESCRIPTION A filter that detects motion and discards frames without motion. The filter uses a K-Nearest Neighbours (KNN) to determine what is @@ -32,10 +29,7 @@ LICENSE package filter import ( -<<<<<<< HEAD "fmt" -======= ->>>>>>> 65f1c89ff2220eb828dcec312da11dbc620449bc "image" "image/color" "io" @@ -81,14 +75,10 @@ func (m *KNNFilter) Close() error { // Write applies the motion filter to the video stream. Only frames with motion // are written to the destination encoder, frames without are discarded. func (m *KNNFilter) Write(f []byte) (int, error) { -<<<<<<< HEAD img, err := gocv.IMDecode(f, gocv.IMReadColor) if err != nil { return 0, fmt.Errorf("can't decode image: %w", err) } -======= - img, _ := gocv.IMDecode(f, gocv.IMReadColor) ->>>>>>> 65f1c89ff2220eb828dcec312da11dbc620449bc defer img.Close() imgDelta := gocv.NewMat() From f06d9641c80ae08b1eca3bc3e6e51a9b72e5c7e4 Mon Sep 17 00:00:00 2001 From: Ella Pietraroia Date: Tue, 31 Dec 2019 15:40:47 +1030 Subject: [PATCH 28/29] moving build !circleci --- filter/knn.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/filter/knn.go b/filter/knn.go index cc65d1de..4ce51d9e 100644 --- a/filter/knn.go +++ b/filter/knn.go @@ -1,6 +1,6 @@ -/* // +build !circleci +/* DESCRIPTION A filter that detects motion and discards frames without motion. The filter uses a K-Nearest Neighbours (KNN) to determine what is From ab211be6ebc5b29f78f571c054d422731db47e28 Mon Sep 17 00:00:00 2001 From: Ella Pietraroia Date: Tue, 31 Dec 2019 16:10:09 +1030 Subject: [PATCH 29/29] filter/filters_circleci.go fix type to NoOp --- filter/filters_circleci.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/filter/filters_circleci.go b/filter/filters_circleci.go index b6ebcc70..e9fc155a 100644 --- a/filter/filters_circleci.go +++ b/filter/filters_circleci.go @@ -36,6 +36,6 @@ func NewMOGFilter(dst io.WriteCloser, area, threshold float64, history, kernelSi return &NoOp{dst: dst} } -func NewKNNFilter(dst io.WriteCloser, area, threshold float64, history, kernelSize int, debug bool) *KNNFilter { +func NewKNNFilter(dst io.WriteCloser, area, threshold float64, history, kernelSize int, debug bool) *NoOp { return &NoOp{dst: dst} }