2020-10-23 17:55:21 +00:00
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
2020-10-30 00:42:14 +00:00
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
2020-10-29 04:52:10 +00:00
syncFolder = "/sync"
2020-10-23 17:55:21 +00:00
)
2020-10-30 00:42:14 +00:00
var ( // evil global variables
2020-10-23 17:55:21 +00:00
lastMotionDetectedTime time . Time
currentRecording * gocv . VideoWriter
img , imgDelta , imgThresh gocv . Mat
mog2 gocv . BackgroundSubtractorMOG2
osdColor color . RGBA
)
2020-10-30 00:42:14 +00:00
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" )
2020-10-23 17:55:21 +00:00
2020-10-30 00:42:14 +00:00
// LIGHTS
2020-10-23 17:55:21 +00:00
img = gocv . NewMat ( )
imgDelta = gocv . NewMat ( )
imgThresh = gocv . NewMat ( )
mog2 = gocv . NewBackgroundSubtractorMOG2 ( )
osdColor = color . RGBA { 0 , 0 , 255 , 0 }
2020-10-30 00:42:14 +00:00
frameCount := 0
2020-10-23 17:55:21 +00:00
defer img . Close ( )
defer imgDelta . Close ( )
defer imgThresh . Close ( )
defer mog2 . Close ( )
2020-10-30 00:42:14 +00:00
// CAMERA
2020-10-23 17:55:21 +00:00
webcam , err := gocv . OpenVideoCapture ( deviceID )
if err != nil {
2020-10-29 04:52:10 +00:00
log . Fatalf ( "error opening video capture device: %v\n" , deviceID )
2020-10-23 17:55:21 +00:00
return
}
defer webcam . Close ( )
fmt . Printf ( "Start reading device: %v\n" , deviceID )
2020-10-30 00:42:14 +00:00
// 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
2020-10-29 03:17:11 +00:00
for i := 0 ; i < 20 ; i ++ {
if ok := webcam . Read ( & img ) ; ! ok {
2020-10-29 04:52:10 +00:00
log . Fatalf ( "video capture device closed: %v\n" , deviceID )
2020-10-29 03:17:11 +00:00
return
}
2020-10-29 03:18:36 +00:00
detectMotion ( img )
2020-10-29 03:17:11 +00:00
}
2020-10-30 00:42:14 +00:00
// ACTION
// main loop - each iteration is a different video frame
2020-10-23 17:55:21 +00:00
for {
if ok := webcam . Read ( & img ) ; ! ok {
2020-10-29 04:52:10 +00:00
log . Fatalf ( "video capture device closed: %v\n" , deviceID )
2020-10-23 17:55:21 +00:00
return
}
if img . Empty ( ) {
continue
}
2020-10-30 00:42:14 +00:00
// 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.
2020-10-29 04:32:45 +00:00
if frameCount >= motionDetectInterval {
if detectMotion ( img ) {
2020-10-30 00:42:14 +00:00
// Determine if a new recording needs to start, we may already have one running
2020-10-30 00:48:24 +00:00
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.
2020-10-29 04:32:45 +00:00
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 {
2020-10-29 04:52:10 +00:00
log . Fatalf ( "error opening video writer device: %v\n" , err )
2020-10-29 04:32:45 +00:00
return
}
2020-10-23 17:55:21 +00:00
}
2020-10-29 04:32:45 +00:00
// And always update the timestamp
lastMotionDetectedTime = time . Now ( )
2020-10-23 17:55:21 +00:00
}
2020-10-29 04:32:45 +00:00
frameCount = 0
2020-10-23 17:55:21 +00:00
}
2020-10-29 04:34:44 +00:00
frameCount ++
2020-10-23 17:55:21 +00:00
2020-10-30 00:42:14 +00:00
// Determine if we are currently recording and if so, then write the video frame to the current recording file
2020-10-23 17:55:21 +00:00
if currentRecording != nil {
2020-10-30 00:42:14 +00:00
// OSD / timestamp in upper left of video
2020-10-23 17:55:21 +00:00
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
2020-10-29 03:17:11 +00:00
if lastMotionDetectedTime . Add ( time . Second * recordLengthAfterMotion ) . Before ( time . Now ( ) ) {
2020-10-23 17:55:21 +00:00
log . Printf ( "motion has not been detected for the last %d seconds stopping recording to file" , recordLengthAfterMotion )
err = currentRecording . Close ( )
if err != nil {
2020-10-29 04:52:10 +00:00
log . Fatalf ( "failed to close openCV file recording handle: %v" , err )
2020-10-23 17:55:21 +00:00
}
currentRecording = nil
}
}
}
2020-10-29 04:55:28 +00:00
log . Printf ( "shutting down" )
2020-10-23 17:55:21 +00:00
}
// Returns true if motion detected in current frame
func detectMotion ( frame gocv . Mat ) bool {
2020-10-30 00:42:14 +00:00
// First phase of cleaning up image, obtain foreground only
// See https://docs.opencv.org/master/d1/dc5/tutorial_background_subtraction.html
2020-10-23 17:55:21 +00:00
mog2 . Apply ( frame , & imgDelta )
2020-10-30 00:42:14 +00:00
// 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
2020-10-23 17:55:21 +00:00
gocv . Threshold ( imgDelta , & imgThresh , 25 , 255 , gocv . ThresholdBinary )
2020-10-30 00:42:14 +00:00
// Then dilate
// https://docs.opencv.org/3.4/db/df6/tutorial_erosion_dilatation.html
2020-10-23 17:55:21 +00:00
kernel := gocv . GetStructuringElement ( gocv . MorphRect , image . Pt ( 3 , 3 ) )
defer kernel . Close ( )
gocv . Dilate ( imgThresh , & imgThresh , kernel )
2020-10-30 00:42:14 +00:00
// Now find contours
// https://docs.opencv.org/3.4/d4/d73/tutorial_py_contours_begin.html
2020-10-23 17:55:21 +00:00
contours := gocv . FindContours ( imgThresh , gocv . RetrievalExternal , gocv . ChainApproxSimple )
2020-10-30 00:42:14 +00:00
// 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
2020-10-23 17:55:21 +00:00
for _ , c := range contours {
area := gocv . ContourArea ( c )
if area < minimumMotionArea {
continue
}
return true
}
return false
}