// +build !circleci /* DESCRIPTION A filter that detects motion and discards frames without motion. The filter uses a Mixture of Gaussians method (MoG) to determine what is background and what is foreground. AUTHORS Scott Barnard LICENSE mog.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 ( "fmt" "image" "io" "bitbucket.org/ausocean/av/revid/config" "gocv.io/x/gocv" ) const ( defaultMOGMinArea = 25.0 defaultMOGThreshold = 20.0 defaultMOGHistory = 500 defaultMOGKernel = 3 defaultMOGDownscaling = 2 defaultMOGInterval = 1 ) // MOG is a filter that provides basic motion detection. MoG is short for // Mixture of Gaussians method. type MOG struct { debugging debugWindows 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. 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. scale float64 // The factor that frames will be downscaled by for motion detection. } // NewMOG returns a pointer to a new MOG filter struct. func NewMOG(dst io.WriteCloser, c config.Config) *MOG { // Validate parameters. if c.MotionMinArea <= 0 { c.LogInvalidField("MotionMinArea", defaultMOGMinArea) c.MotionMinArea = defaultMOGMinArea } if c.MotionThreshold <= 0 { c.LogInvalidField("MotionThreshold", defaultMOGThreshold) c.MotionThreshold = defaultMOGThreshold } if c.MotionHistory == 0 { c.LogInvalidField("MotionHistory", defaultMOGHistory) c.MotionHistory = defaultMOGHistory } if c.MotionDownscaling <= 0 { c.LogInvalidField("MotionDownscaling", defaultMOGDownscaling) c.MotionDownscaling = defaultMOGDownscaling } if c.MotionInterval <= 0 { c.LogInvalidField("MotionInterval", defaultMOGInterval) c.MotionInterval = defaultMOGInterval } if c.MotionKernel <= 0 { c.LogInvalidField("MotionKernel", defaultMOGKernel) c.MotionKernel = defaultMOGKernel } bs := gocv.NewBackgroundSubtractorMOG2WithParams(int(c.MotionHistory), c.MotionThreshold, false) k := gocv.GetStructuringElement(gocv.MorphRect, image.Pt(3, 3)) return &MOG{ dst: dst, area: c.MotionMinArea, bs: &bs, knl: k, hold: make([][]byte, c.MotionInterval-1), hf: c.MotionInterval, scale: 1 / float64(c.MotionDownscaling), debugging: newWindows("MOG"), } } // Implements io.Closer. // Close frees resources used by gocv, because it has to be done manually, due to // it using c-go. func (m *MOG) Close() error { m.bs.Close() m.knl.Close() m.debugging.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 *MOG) 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) } defer img.Close() imgDelta := gocv.NewMat() defer imgDelta.Close() // Downsize image to speed up calculations. gocv.Resize(img, &img, image.Point{}, m.scale, m.scale, gocv.InterpolationNearestNeighbor) // 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. m.debugging.show(img, imgDelta, len(contours) > 0, &contours) // Don't write to destination if there is no motion. if len(contours) == 0 { return len(f), nil } // 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) }