storage-security/main.go

160 lines
5.3 KiB
Go

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
}