diff --git a/filter/filters_circleci.go b/filter/filters_circleci.go index f6d46082..bd9a6fee 100644 --- a/filter/filters_circleci.go +++ b/filter/filters_circleci.go @@ -32,7 +32,7 @@ import ( ) // NewMOGFilter returns a pointer to a new NoOp struct for testing purposes only. -func NewMOGFilter(dst io.WriteCloser, area, threshold float64, history int, debug bool) *NoOp { +func NewMOGFilter(dst io.WriteCloser, area, threshold float64, history int, debug bool, hf int) *NoOp { return &NoOp{dst: dst} } diff --git a/filter/mog.go b/filter/mog.go index 9c647bd2..20fd5807 100644 --- a/filter/mog.go +++ b/filter/mog.go @@ -40,23 +40,26 @@ import ( // MOGFilter is a filter that provides basic motion detection. MoG is short for // Mixture of Gaussians method. type MOGFilter struct { - dst io.WriteCloser - area float64 - bs *gocv.BackgroundSubtractorMOG2 - knl gocv.Mat - debug bool - windows []*gocv.Window + dst io.WriteCloser // Destination to which motion containing frames go. + area float64 // The minimum area that a contour can be found in. + bs *gocv.BackgroundSubtractorMOG2 // Uses the MOG algorithm to find the difference between the current and background frame. + knl gocv.Mat // Matrix that is used for calculations. + debug bool // If true then debug windows with the bounding boxes and difference will be shown on the screen. + windows []*gocv.Window // Holds debug windows. + hold [][]byte // Will hold all frames up to hf (so only every hf frame is motion detected). + hf int // The number of frames to be held. + hfCount int // Counter for the hold array. } // NewMOGFilter returns a pointer to a new MOGFilter struct. -func NewMOGFilter(dst io.WriteCloser, area, threshold float64, history int, debug bool) *MOGFilter { +func NewMOGFilter(dst io.WriteCloser, area, threshold float64, history int, debug bool, hf int) *MOGFilter { bs := gocv.NewBackgroundSubtractorMOG2WithParams(history, threshold, false) k := gocv.GetStructuringElement(gocv.MorphRect, image.Pt(3, 3)) var windows []*gocv.Window if debug { windows = []*gocv.Window{gocv.NewWindow("MOG: Bounding boxes"), gocv.NewWindow("MOG: Motion")} } - return &MOGFilter{dst, area, &bs, k, debug, windows} + return &MOGFilter{dst, area, &bs, k, debug, windows, make([][]byte, hf-1), hf, 0} } // Implements io.Closer. @@ -75,6 +78,13 @@ func (m *MOGFilter) 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 *MOGFilter) Write(f []byte) (int, error) { + if m.hfCount < (m.hf - 1) { + m.hold[m.hfCount] = f + m.hfCount++ + return len(f), nil + } + + m.hfCount = 0 img, err := gocv.IMDecode(f, gocv.IMReadColor) if err != nil { return 0, fmt.Errorf("image can't be decoded: %w", err) @@ -125,9 +135,16 @@ func (m *MOGFilter) Write(f []byte) (int, error) { // Don't write to destination if there is no motion. if len(contours) == 0 { - return 0, nil + return len(f), nil } - // Write to destination. + // Write to destination, past 4 frames then current frame. + for i, h := range m.hold { + _, err := m.dst.Write(h) + m.hold[i] = nil + if err != nil { + return len(f), fmt.Errorf("could not write previous frames: %w", err) + } + } return m.dst.Write(f) } diff --git a/revid/config/config.go b/revid/config/config.go index 710a75c5..194faf33 100644 --- a/revid/config/config.go +++ b/revid/config/config.go @@ -85,6 +85,7 @@ const ( defaultClipDuration = 0 defaultAudioInputCodec = codecutil.ADPCM defaultPSITime = 2 + defaultMotionInterval = 5 // Ring buffer defaults. defaultRBMaxElements = 10000 @@ -275,6 +276,7 @@ type Config struct { HorizontalFlip bool // HorizontalFlip flips video horizontally for Raspivid input. VerticalFlip bool // VerticalFlip flips video vertically for Raspivid input. Filters []int // Defines the methods of filtering to be used in between lexing and encoding. + MotionInterval int // Sets the number of frames that are held before the filter is used (on the nth frame) PSITime int // Sets the time between a packet being sent. // Ring buffer parameters. @@ -328,6 +330,7 @@ var TypeData = map[string]string{ "MOGHistory": "uint", "MOGMinArea": "float", "MOGThreshold": "float", + "MotionInterval": "int", "RBCapacity": "uint", "RBMaxElements": "uint", "RBWriteTimeout": "uint", @@ -462,6 +465,10 @@ func (c *Config) Validate() error { c.Logger.Log(logger.Info, pkg+"PSITime bad or unset, defaulting", "PSITime", defaultPSITime) c.PSITime = defaultPSITime } + if c.MotionInterval <= 0 { + c.Logger.Log(logger.Info, pkg+"MotionInterval bad or unset, defaulting", "MotionInterval", defaultMotionInterval) + c.MotionInterval = defaultMotionInterval + } if c.MinFPS <= 0 { c.Logger.Log(logger.Info, pkg+"MinFPS bad or unset, defaulting", "MinFPS", defaultMinFPS) diff --git a/revid/revid.go b/revid/revid.go index a05c9650..c7ea4886 100644 --- a/revid/revid.go +++ b/revid/revid.go @@ -63,6 +63,24 @@ const ( rtmpConnectionTimeout = 10 ) +// Motion filter parameters. +const minFPS = 1.0 + +// KNN specific parameters. +const ( + knnMinArea = 25.0 + knnThreshold = 300 + knnHistory = 300 + knnKernel = 9 +) + +// MOG specific parameters. +const ( + mogMinArea = 50 + mogThreshold = 100 + mogHistory = 100 +) + const pkg = "revid: " type Logger interface { @@ -337,11 +355,11 @@ func (r *Revid) setupPipeline(mtsEnc func(dst io.WriteCloser, rate float64) (io. case config.FilterNoOp: r.filters[i] = filter.NewNoOp(dst) case config.FilterMOG: - r.filters[i] = filter.NewMOGFilter(dst, r.cfg.MOGMinArea, r.cfg.MOGThreshold, int(r.cfg.MOGHistory), r.cfg.ShowWindows) + r.filters[i] = filter.NewMOGFilter(dst, mogMinArea, mogThreshold, mogHistory, r.cfg.ShowWindows, r.cfg.MotionInterval) case config.FilterVariableFPS: - r.filters[i] = filter.NewVariableFPSFilter(dst, r.cfg.MinFPS, filter.NewMOGFilter(dst, r.cfg.MOGMinArea, r.cfg.MOGThreshold, int(r.cfg.MOGHistory), r.cfg.ShowWindows)) + r.filters[i] = filter.NewVariableFPSFilter(dst, minFPS, filter.NewMOGFilter(r.encoders, mogMinArea, mogThreshold, mogHistory, r.cfg.ShowWindows, r.cfg.MotionInterval)) case config.FilterKNN: - r.filters[i] = filter.NewKNNFilter(dst, r.cfg.KNNMinArea, r.cfg.KNNThreshold, int(r.cfg.KNNHistory), int(r.cfg.KNNKernel), r.cfg.ShowWindows) + r.filters[i] = filter.NewKNNFilter(dst, knnMinArea, knnThreshold, knnHistory, knnKernel, r.cfg.ShowWindows) default: panic("Undefined Filter") } @@ -671,6 +689,13 @@ func (r *Revid) Update(vars map[string]string) error { } r.cfg.Filters[i] = v } + case "MotionInterval": + v, err := strconv.Atoi(value) + if err != nil || v < 0 { + r.cfg.Logger.Log(logger.Warning, pkg+"invalid MotionInterval var", "value", value) + break + } + r.cfg.MotionInterval = v case "PSITime": v, err := strconv.Atoi(value) if err != nil || v < 0 {