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")) // My preferred timestamp format is RFC3339, however there are weird filesystems out there that don't like colons in the names of files such as NTFS or FAT32. 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 }