package main import ( "fmt" "image" "image/color" "log" "os" "time" "gocv.io/x/gocv" ) const ( minimumMotionArea = 3000 // Motion detection minimum area needed to move recordLengthAfterMotion = 30 // Number of seconds to keep recording going after motion was last detected motionDetectInterval = 30 // Number of frames between motion detection algorithm running deviceID = 0 // Raspberry Pi camera index - should be 0 if using the camera connector. Might be different if using USB webcam syncFolder = "/sync" ) var ( // evil global variables lastMotionDetectedTime time.Time currentRecording *gocv.VideoWriter img, imgDelta, imgThresh gocv.Mat mog2 gocv.BackgroundSubtractorMOG2 osdColor color.RGBA ) func main() { // Override log output from stdout to a file on disk f, err := os.OpenFile(fmt.Sprintf("%s/storage-security.log", syncFolder), os.O_RDWR|os.O_CREATE|os.O_APPEND, 0666) if err != nil { log.Fatalf("error opening log file: %v", err) } defer f.Close() log.SetOutput(f) log.Print("storage-security starting") // LIGHTS img = gocv.NewMat() imgDelta = gocv.NewMat() imgThresh = gocv.NewMat() mog2 = gocv.NewBackgroundSubtractorMOG2() osdColor = color.RGBA{0, 0, 255, 0} frameCount := 0 defer img.Close() defer imgDelta.Close() defer imgThresh.Close() defer mog2.Close() // CAMERA webcam, err := gocv.OpenVideoCapture(deviceID) if err != nil { log.Fatalf("error opening video capture device: %v\n", deviceID) return } defer webcam.Close() fmt.Printf("Start reading device: %v\n", deviceID) // This is a warm up ladies and gentlemen // There will always be motion / changes in the first few frames // this just bypasses starting a recording upon initialization for i := 0; i < 20; i++ { if ok := webcam.Read(&img); !ok { log.Fatalf("video capture device closed: %v\n", deviceID) return } detectMotion(img) } // ACTION // main loop - each iteration is a different video frame for { if ok := webcam.Read(&img); !ok { log.Fatalf("video capture device closed: %v\n", deviceID) return } if img.Empty() { continue } // Do not run the motion detect algorithm on every frame - it's a very expensive operation (CPU) // While the raspberry pi hardware can keep up, it consumes a lot of unneeded power // It will only run every motionDetectInterval frames. if frameCount >= motionDetectInterval { if detectMotion(img) { // Determine if a new recording needs to start, we may already have one running if time.Now().After(lastMotionDetectedTime.Add(time.Second * recordLengthAfterMotion)) { // fileName := fmt.Sprintf("%s/storage-security-%s.avi", syncFolder, time.Now().Format("2006-01-02-15-04-05")) log.Printf("motion detected, started recording to file named %s", fileName) currentRecording, err = gocv.VideoWriterFile(fileName, "MJPG", 25, img.Cols(), img.Rows(), true) if err != nil { log.Fatalf("error opening video writer device: %v\n", err) return } } // And always update the timestamp lastMotionDetectedTime = time.Now() } frameCount = 0 } frameCount++ // Determine if we are currently recording and if so, then write the video frame to the current recording file if currentRecording != nil { // OSD / timestamp in upper left of video gocv.PutText(&img, time.Now().Format(time.RFC3339), image.Pt(10, 20), gocv.FontHersheyPlain, 1.2, osdColor, 2) currentRecording.Write(img) // Determine if we should stop recording if lastMotionDetectedTime.Add(time.Second * recordLengthAfterMotion).Before(time.Now()) { log.Printf("motion has not been detected for the last %d seconds stopping recording to file", recordLengthAfterMotion) err = currentRecording.Close() if err != nil { log.Fatalf("failed to close openCV file recording handle: %v", err) } currentRecording = nil } } } log.Printf("shutting down") } // Returns true if motion detected in current frame func detectMotion(frame gocv.Mat) bool { // First phase of cleaning up image, obtain foreground only // See https://docs.opencv.org/master/d1/dc5/tutorial_background_subtraction.html mog2.Apply(frame, &imgDelta) // Next the goal is to find contours in the foreground image // But first it needs to be cleaned up // First use threshold // https://docs.opencv.org/master/d7/d4d/tutorial_py_thresholding.html gocv.Threshold(imgDelta, &imgThresh, 25, 255, gocv.ThresholdBinary) // Then dilate // https://docs.opencv.org/3.4/db/df6/tutorial_erosion_dilatation.html kernel := gocv.GetStructuringElement(gocv.MorphRect, image.Pt(3, 3)) defer kernel.Close() gocv.Dilate(imgThresh, &imgThresh, kernel) // Now find contours // https://docs.opencv.org/3.4/d4/d73/tutorial_py_contours_begin.html contours := gocv.FindContours(imgThresh, gocv.RetrievalExternal, gocv.ChainApproxSimple) // No matter what, every camera frame will be slightly different than the subsequent frame // Noise is a thing, so we must search the contours larger than a specified threshold for _, c := range contours { area := gocv.ContourArea(c) if area < minimumMotionArea { continue } return true } return false }