/*
Video streaming, recording, write image if detect motion
ビデオストリーミング、録画、動きを検出した場合の画像の書き込み
```
$ uname -a ; lsb_release -a
Linux raspberrypi 5.10.17-v7l+ #1414 SMP Fri Apr 30 13:20:47 BST 2021 armv7l GNU/Linux
No LSB modules are available.
Distributor ID: Raspbian
Description: Raspbian GNU/Linux 10 (buster)
Release: 10
Codename: buster
```
- https://qiita.com/phamvanhung2e123/items/96489f32932fcd21345e
$ go get github.com/pkg/profile
$ sudo apt install -y --no-install-recommends graphviz graphviz-dev
```
func main() {
defer profile.Start(profile.ProfilePath(".")).Stop()
defer profile.Start(profile.MemProfile, profile.ProfilePath(".")).Stop()
```
$ go tool pprof -http=":8081" main cpu.pprof
$ go tool pprof -http=":8081" main mem.pprof
---
- https://github.com/OsamaJBR/golang-camera-web-server
- https://takuya-1st.hatenablog.jp/entry/20120302/1330675856
- https://gobot.io/documentation/examples/tello_opencv/
http://localhost:3000/
*/
package main
import (
"fmt"
"html/template"
"image"
"image/color"
"io"
"io/fs"
"log"
"net/http"
"os"
"path"
"path/filepath"
"runtime"
"strconv"
"sync"
"time"
// _ "net/http/pprof"
"gocv.io/x/gocv"
"gopkg.in/natefinch/lumberjack.v2"
)
var (
webcam *gocv.VideoCapture
frame []byte
mutex = &sync.Mutex{}
)
/*
2160p: 3840x2160 4K
1440p: 2560x1440
1080p: 1920x1080 フルハイビジョン画質 = フルHD (o)
720p: 1280x720 ハイビジョン画質 = HD
480p: 854x480 (o)
480p: 720x480 DVDと同等な画面解像度 (x)
360p: 640x360
240p: 426x240
144p: 256x144
*/
// 1080p @ 30 fps, 720p @ 60 fps, 640x480p 60/90
const (
// webcamWidth = 960 // ...
// webcamHeight = 540 // ...
// webcamWidth = 720 // cannot
// webcamHeight = 480 // cannot
// webcamWidth = 1080 // A
// webcamHeight = 720 // A
webcamWidth = 854 // B
webcamHeight = 480 // B
// webcamFPS = float64(30) // Hard
// webcamFPS = float64(30 / 2) // Smooth
webcamFPS = float64(30 / 4) //
// minimumArea = 3000 // motion
minimumArea = 30000 // motion
// codec = "X264" // 重い
// videoExt = ".avi"
codec = "mp4v"
videoExt = ".mp4"
videoLength = time.Duration(10 * time.Second)
logPath = "log"
tmpPath = "tmp"
videoPath = "videos"
imagePath = "images"
)
func main() {
// defer profile.Start(profile.ProfilePath(".")).Stop()
// defer profile.Start(profile.MemProfile, profile.ProfilePath(".")).Stop()
/*
go func() {
log.Println(http.ListenAndServe("localhost:6060", nil))
}()
*/
// // runtime.GC()
// debug.FreeOSMemory()
// logfile
exePath, err := os.Executable()
if err != nil {
log.Fatal(err)
}
basename := filepath.Base(exePath)
// logPath := filepath.Join(filepath.Dir(exePath), "log", basename[:len(basename)-len(filepath.Ext(basename))]) + ".log" // 拡張子を変更
logPath := filepath.Join("log", basename[:len(basename)-len(filepath.Ext(basename))]) + ".log" // 拡張子を変更
log.Println("logfile", logPath)
l := lumberjack.Logger{
Filename: logPath,
MaxSize: 64, // 64, // MB
MaxBackups: 3, // 3,
MaxAge: 28, // days
Compress: true,
}
if runtime.GOOS == "windows" {
// Windows 向け build で コンソールを表示しない場合
// GOOS=windows GOARCH=amd64 go build -ldflags="-H windowsgui"
// io.MultiWriter(os.Stdout, &l) を利用すると log ファイルに出力されない
log.SetOutput(&l)
} else {
log.SetOutput(io.MultiWriter(os.Stdout, &l))
}
// log.SetFlags(log.Ldate | log.Ltime)
log.SetFlags(log.LstdFlags | log.Lmicroseconds)
// make directories
perm32, _ := strconv.ParseUint("0700", 8, 32)
if _, err := os.Stat(logPath); err != nil {
os.Mkdir(logPath, fs.FileMode(perm32))
}
if _, err := os.Stat(tmpPath); err != nil {
os.Mkdir(tmpPath, fs.FileMode(perm32))
}
if _, err := os.Stat(videoPath); err != nil {
os.Mkdir(videoPath, fs.FileMode(perm32))
}
if _, err := os.Stat(imagePath); err != nil {
os.Mkdir(imagePath, fs.FileMode(perm32))
}
//
host := "0.0.0.0:3000"
// open webcam
if len(os.Args) < 2 {
log.Println(">> device /dev/video0 (default)")
webcam, err = gocv.VideoCaptureDevice(0)
} else {
log.Println(">> file/url :: " + os.Args[1])
webcam, err = gocv.VideoCaptureFile(os.Args[1])
}
if err != nil {
log.Println("Error opening capture device", err)
return
}
webcam.Set(gocv.VideoCaptureFrameWidth, webcamWidth)
webcam.Set(gocv.VideoCaptureFrameHeight, webcamHeight)
webcam.Set(gocv.VideoCaptureFPS, webcamFPS)
defer webcam.Close()
// start capturing
go getFrames()
log.Println("Capturing. Open http://" + host)
// start http server
http.HandleFunc("/video", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "multipart/x-mixed-replace; boundary=frame")
for {
mutex.Lock()
data := "--frame\r\n Content-Type: image/jpeg\r\n\r\n" + string(frame) + "\r\n\r\n"
mutex.Unlock()
time.Sleep(30 * time.Millisecond)
w.Write([]byte(data))
}
})
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
t, err := template.ParseFiles("index.html")
if err != nil {
log.Panic(err)
}
t.Execute(w, "index")
})
log.Fatal(http.ListenAndServe(host, nil))
}
func getFrames() {
img := gocv.NewMat()
defer img.Close()
imgResized := gocv.NewMat()
defer imgResized.Close()
frameWidth := int(webcam.Get(gocv.VideoCaptureFrameWidth))
frameHeight := int(webcam.Get(gocv.VideoCaptureFrameHeight))
fps := webcam.Get(gocv.VideoCaptureFPS)
log.Printf("Set to the camera........width %d, height %d, fps %f\n", webcamWidth, webcamHeight, webcamFPS)
log.Printf("Returned by the camera...width %d, height %d, fps %f\n", frameWidth, frameHeight, fps)
tmpPaths := []string{path.Join(tmpPath, "out0"+videoExt), path.Join(tmpPath, "out1"+videoExt)}
var err error
var videoWriter2 [2]*gocv.VideoWriter
// videoWriter2[0], err = gocv.VideoWriterFile(tmpPaths[0], codec, fps, frameWidth, frameHeight, true)
videoWriter2[0], err = gocv.VideoWriterFile(tmpPaths[0], codec, webcamFPS, webcamWidth, webcamHeight, true)
if err != nil {
log.Panic("Can not open video writer 0", err)
return
}
defer videoWriter2[0].Close()
// videoWriter2[1], err = gocv.VideoWriterFile(tmpPaths[1], codec, fps, frameWidth, frameHeight, true)
videoWriter2[1], err = gocv.VideoWriterFile(tmpPaths[1], codec, webcamFPS, webcamWidth, webcamHeight, true)
if err != nil {
log.Panic("Can not open video writer 1", err)
return
}
defer videoWriter2[1].Close()
var videoWriter = videoWriter2[0]
// motion
// https://github.com/hybridgroup/gocv/blob/release/cmd/motion-detect/main.go
imgDelta := gocv.NewMat()
defer imgDelta.Close()
imgThresh := gocv.NewMat()
defer imgThresh.Close()
mog2 := gocv.NewBackgroundSubtractorMOG2()
defer mog2.Close()
doingMotion := true
go func() {
time.Sleep(10 * time.Second) // 起動直後は無視する
doingMotion = false
fmt.Println("Motion detect start")
}()
imagePathSFmt := fmt.Sprintf("%s/%%s.jpg", imagePath)
// Double VideoWriter
videoPathSFmt := fmt.Sprintf("%s/%%s%s", videoPath, videoExt)
const dateFormat = "2006-01-02_15-04-05"
go func() {
out := fmt.Sprintf(videoPathSFmt, time.Now().Format(dateFormat))
a := 0
for {
b := a
if a == 0 {
a = 1
} else {
a = 0
}
time.Sleep(videoLength) // video length
mutex.Lock()
videoWriter = videoWriter2[a] // swap
mutex.Unlock()
if err := videoWriter2[b].Close(); err != nil {
log.Println("gocv.VideoWriter.Close()", err)
}
if err := os.Rename(tmpPaths[b], out); err != nil {
log.Println("Rename", err)
}
out = fmt.Sprintf(videoPathSFmt, time.Now().Format(dateFormat))
videoWriter2[b] = nil
// runtime.GC()
// videoWriter2[b], err = gocv.VideoWriterFile(tmpPaths[b], codec, fps, frameWidth, frameHeight, true)
videoWriter2[b], err = gocv.VideoWriterFile(tmpPaths[b], codec, webcamFPS, webcamWidth, webcamHeight, true)
if err != nil {
log.Println("Can not open video writer", b, err)
}
}
}()
isWebcamReading := false
for {
timeNow := time.Now()
timeStamp := timeNow.Format("2006-01-02 15:04:05")
if !isWebcamReading {
mutex.Lock() // *A
}
isWebcamReading = true
if ok := webcam.Read(&img); !ok {
log.Println("Device closed")
continue
}
if img.Empty() {
continue
}
isWebcamReading = false
gocv.Resize(img, &imgResized, image.Point{}, float64(0.5), float64(0.5), 0)
gocv.PutText(&img, timeStamp, image.Pt(10, img.Rows()-10), gocv.FontHersheyPlain, 1, color.RGBA{255, 0, 0, 0}, 1)
if err := videoWriter.Write(img); err != nil {
log.Println("Can not write frame to temporary file", err)
}
// gocv.Resize(img, &imgResized, image.Point{}, float64(0.5), float64(0.5), 0)
mutex.Unlock() // *A
// motion
if !doingMotion {
// log.Println("-- doMotion")
doingMotion = true
go func() {
// log.Println("-- go motion")
defer func() {
time.Sleep(30 * time.Millisecond) // good
// time.Sleep(60 * time.Millisecond)
// time.Sleep(100 * time.Millisecond)
// time.Sleep(300 * time.Millisecond)
doingMotion = false
}()
// mutex.Lock()
imgClone := imgResized.Clone()
// mutex.Unlock()
defer imgClone.Close()
// first phase of cleaning up image, obtain foreground only
mog2.Apply(imgClone, &imgDelta)
// remaining cleanup of the image to use for finding contours.
// first use threshold
gocv.Threshold(imgDelta, &imgThresh, 25, 255, gocv.ThresholdBinary)
// then dilate
kernel := gocv.GetStructuringElement(gocv.MorphRect, image.Pt(3, 3))
defer kernel.Close()
gocv.Dilate(imgThresh, &imgThresh, kernel)
// now find contours
contours := gocv.FindContours(imgThresh, gocv.RetrievalExternal, gocv.ChainApproxSimple)
defer contours.Close()
detected := false
for i := 0; i < contours.Size(); i++ {
area := gocv.ContourArea(contours.At(i))
if area < minimumArea {
continue
}
log.Println("Motion detected", "area", area)
rect := gocv.BoundingRect(contours.At(i))
// fmt.Printf("%d %d %d %d", rect.Max.X, rect.Max.Y, imgClone.Cols(), imgClone.Rows())
// エリア全体か?
if rect.Min.X == 0 && rect.Min.Y == 0 && rect.Max.X == imgClone.Cols() && rect.Max.Y == imgClone.Rows() {
continue
}
log.Println("Motion detected")
detected = true
// draw detected area
gocv.Rectangle(&imgClone, rect, color.RGBA{0, 0, 255, 0}, 2)
gocv.DrawContours(&imgClone, contours, i, color.RGBA{255, 0, 0, 0}, 2)
}
if detected {
// log.Println("-- put image")
timeNow := time.Now()
gocv.PutText(&imgClone, timeNow.Format("2006-01-02 15:04:05"), image.Pt(10, img.Rows()-10), gocv.FontHersheyPlain, 1, color.RGBA{255, 0, 0, 0}, 1)
gocv.IMWrite(fmt.Sprintf(imagePathSFmt, timeNow.Format("2006-01-02_15-04-05.000")), imgClone)
}
}()
}
gocv.PutText(&imgResized, timeStamp, image.Pt(10, imgResized.Rows()-10), gocv.FontHersheyPlain, 1, color.RGBA{255, 0, 0, 0}, 1)
mutex.Lock()
frame = nil
// runtime.GC()
frame, _ = gocv.IMEncode(".jpg", imgResized) // err: nil しか帰ってこない 2021-06-04
mutex.Unlock()
}
}