diff --git a/filter/mog.go b/filter/mog.go index 9c647bd2..5a2bb7ad 100644 --- a/filter/mog.go +++ b/filter/mog.go @@ -46,17 +46,22 @@ type MOGFilter struct { knl gocv.Mat debug bool windows []*gocv.Window + hold [][]byte + hf int } +var hfCount int = 0 + // 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} + hold := make([][]byte, hf-1) + return &MOGFilter{dst, area, &bs, k, debug, windows, hold, hf} } // Implements io.Closer. @@ -75,59 +80,74 @@ 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) { - img, err := gocv.IMDecode(f, gocv.IMReadColor) - if err != nil { - return 0, fmt.Errorf("image can't be decoded: %w", err) - } - 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) + if hfCount < (m.hf - 1) { + m.hold[hfCount] = f + hfCount++ + return -1, nil + } else { + img, err := gocv.IMDecode(f, gocv.IMReadColor) + if err != nil { + return 0, fmt.Errorf("image can't be decoded: %w", err) } - } + defer img.Close() - // 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) + 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) + } } - if len(contours) > 0 { - gocv.PutText(&img, "Motion", image.Pt(32, 32), gocv.FontHersheyPlain, 2.0, color.RGBA{255, 0, 0, 0}, 2) + // 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) } - 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 0, nil + } + + // Write to destination, past 4 frames then current frame. + for _, h := range m.hold { + _, err := m.dst.Write(h) + if err != nil { + return 0, err + } + } + + hfCount = 0 + return m.dst.Write(f) } - // Don't write to destination if there is no motion. - if len(contours) == 0 { - return 0, nil - } - - // Write to destination. - return m.dst.Write(f) } diff --git a/revid/config/config.go b/revid/config/config.go index 8768e9db..b37fc759 100644 --- a/revid/config/config.go +++ b/revid/config/config.go @@ -85,6 +85,7 @@ const ( defaultClipDuration = 0 defaultAudioInputCodec = codecutil.ADPCM defaultPSITime = 2 + defaultFilterFrames = 1 // 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. + FilterFrames 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. @@ -460,6 +462,10 @@ func (c *Config) Validate() error { c.Logger.Log(logger.Info, pkg+"PSITime bad or unset, defaulting", "PSITime", defaultPSITime) c.PSITime = defaultPSITime } + if c.FilterFrames <= 0 { + c.Logger.Log(logger.Info, pkg+"FilterFrames bad or unset, defaulting", "FilterFrames", defaultFilterFrames) + c.FilterFrames = defaultFilterFrames + } 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 07c317e4..f5398c72 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,17 +355,16 @@ 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.FilterFrames) 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.FilterFrames)) 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") } dst = r.filters[i] } - } switch r.cfg.Input { @@ -671,6 +688,13 @@ func (r *Revid) Update(vars map[string]string) error { } r.cfg.Filters[i] = v } + case "FilterFrames": + v, err := strconv.Atoi(value) + if err != nil || v < 0 { + r.cfg.Logger.Log(logger.Warning, pkg+"invalid FilterFrames var", "value", value) + break + } + r.cfg.FilterFrames = v case "PSITime": v, err := strconv.Atoi(value) if err != nil || v < 0 {