diff --git a/filter/basic.go b/filter/basic.go index 19fac225..fec5b0bd 100644 --- a/filter/basic.go +++ b/filter/basic.go @@ -3,14 +3,14 @@ /* DESCRIPTION A filter that detects motion and discards frames without motion. The - filter uses a Mixture of Gaussians method (MoG) to determine what is - background and what is foreground. + filter uses a difference method looking at each individual pixel to + determine what is background and what is foreground. AUTHORS Ella Pietraroia LICENSE - mog.go is Copyright (C) 2019 the Australian Ocean Lab (AusOcean) + basic.go is Copyright (C) 2020 the Australian Ocean Lab (AusOcean) It is free software: you can redistribute it and/or modify them under the terms of the GNU General Public License as published by the @@ -38,13 +38,127 @@ import ( "os" "strconv" "sync" - "time" "golang.org/x/image/font" "golang.org/x/image/font/basicfont" "golang.org/x/image/math/fixed" ) +const filename = "motion.mjpeg" + +const ( + threshold = 45000 + pixels = 1000 +) + +type pixel struct { + r uint32 + g uint32 + b uint32 +} + +// BasicFilter is a filter that provides basic motion detection via a difference +// method. +type Basic struct { + dst io.WriteCloser + img image.Image + bg [][]pixel + bwImg *image.RGBA + w int + h int + file io.WriteCloser + motion int + debug bool +} + +// NewBasicFilter returns a pointer to a new Basic filter struct. +func NewBasicFilter(dst io.WriteCloser, debug bool) *Basic { + bwImg := image.NewRGBA(image.Rect(0, 0, 0, 0)) + var file io.WriteCloser + var err error + file = nil + if debug { + file, err = os.Create(filename) + if err != nil { + panic(fmt.Sprintf("could not create debug file: %v", err)) + } + } else { + file = nil + } + return &Basic{dst, nil, nil, bwImg, 0, 0, file, 0, debug} +} + +// Implements io.Closer. +// Close frees resources used by gocv, because it has to be done manually, due to +// it using c-go. +func (bf *Basic) Close() error { + return nil +} + +// Implements io.Writer. +// Write applies the motion filter to the video stream. Only frames with motion +// are written to the destination encoder, frames without are discarded. +func (bf *Basic) Write(f []byte) (int, error) { + // Decode MJPEG. + var err error + bf.img, err = jpeg.Decode(bytes.NewReader(f)) + if err != nil { + return 0, fmt.Errorf("image can't be decoded: %w", err) + } + + // First frame must be set as the first background image + if bf.bg == nil { + bounds := bf.img.Bounds() + bf.w = bounds.Max.X + bf.h = bounds.Max.Y + + bf.bwImg = image.NewRGBA(image.Rect(0, 0, bf.w, bf.h)) + + bf.bg = make([][]pixel, bf.h) + for i, _ := range bf.bg { + bf.bg[i] = make([]pixel, bf.w) + for j, _ := range bf.bg[i] { + p := bf.img.At(i, j) + r, g, b, _ := p.RGBA() + bf.bg[i][j].r = r + bf.bg[i][j].b = b + bf.bg[i][j].g = g + } + } + return len(f), nil + } + + // Use 4x goroutines to each process one row of pixels. + j := 0 + var wg *sync.WaitGroup + + for j < bf.h { + wg.Add(4) + go bf.process(j, wg) + go bf.process(j+1, wg) + go bf.process(j+2, wg) + go bf.process(j+3, wg) + j = j + 4 + wg.Wait() + } + + // Will save a video of where motion is detected in motion.mjpeg (in the current folder). + if bf.debug { + err := bf.saveFrame() + if err != nil { + return len(f), err + } + } + + // If there are not enough motion pixels then discard the frame. + if bf.motion < pixels { + return len(f), nil + } + + // Write all motion frames. + return bf.dst.Write(f) +} + func absDiff(a, b uint32) int { c := int(a) - int(b) if c < 0 { @@ -55,160 +169,54 @@ func absDiff(a, b uint32) int { } -// MOGFilter is a filter that provides basic motion detection. MoG is short for -// Mixture of Gaussians method. -type BasicFilter struct { - dst io.WriteCloser - img image.Image - bg [][][3]uint32 - bwImg *image.RGBA - w int - h int - file io.WriteCloser - motion int - debug bool -} - -// NewMOGFilter returns a pointer to a new MOGFilter struct. -func NewBasicFilter(dst io.WriteCloser, debug bool) *BasicFilter { - bwImg := image.NewRGBA(image.Rect(0, 0, 0, 0)) - var file io.WriteCloser - var err error - if debug { - file, err = os.Create("motion.mjpeg") - if err != nil { - panic("debug file didn't create") - } - } else { - file = nil - } - return &BasicFilter{dst, nil, nil, bwImg, 0, 0, file, 0, debug} -} - -// Implements io.Closer. -// Close frees resources used by gocv, because it has to be done manually, due to -// it using c-go. -func (b *BasicFilter) Close() error { - return nil -} - // Go routine for one row of the image to be processed -func (b *BasicFilter) Process(j int, wg *sync.WaitGroup) { - defer wg.Done() - for i := 0; i < b.w; i++ { - n := b.img.At(i, j) - R, G, B, _ := n.RGBA() +func (bf *Basic) process(j int, wg *sync.WaitGroup) { + for i := 0; i < bf.w; i++ { + n := bf.img.At(i, j) + r, b, g, _ := n.RGBA() // Compare the difference of the RGB values of each pixel to the background image. - diffB := absDiff(B, b.bg[j][i][2]) - diffR := absDiff(R, b.bg[j][i][0]) - diffG := absDiff(G, b.bg[j][i][1]) + diffR := absDiff(r, bf.bg[j][i].r) + diffG := absDiff(g, bf.bg[j][i].g) + diffB := absDiff(g, bf.bg[j][i].b) diff := diffR + diffG + diffB - if diff > 45000 { - b.motion++ + if diff > threshold { + bf.motion++ } - if b.debug { - if diff > 45000 { - b.bwImg.SetRGBA(i, j, color.RGBA{0xff, 0xff, 0xff, 0xff}) + if bf.debug { + if diff > threshold { + bf.bwImg.SetRGBA(i, j, color.RGBA{0xff, 0xff, 0xff, 0xff}) } else { - b.bwImg.SetRGBA(i, j, color.RGBA{0x00, 0x00, 0x00, 0xff}) + bf.bwImg.SetRGBA(i, j, color.RGBA{0x00, 0x00, 0x00, 0xff}) } } // Update backgound image. - copy(b.bg[j][i][:], []uint32{R, G, B}) + bf.bg[i][j].r = r + bf.bg[i][j].b = b + bf.bg[i][j].g = g } + wg.Done() } -// Implements io.Writer. -// Write applies the motion filter to the video stream. Only frames with motion -// are written to the destination encoder, frames without are discarded. -func (b *BasicFilter) Write(f []byte) (int, error) { - t0 := time.Now() - // Decode MJPEG. - r := bytes.NewReader(f) - var err error - b.img, err = jpeg.Decode(r) - if err != nil { - return 0, fmt.Errorf("image can't be decoded: %w", err) +func (bf *Basic) saveFrame() error { + col := color.RGBA{200, 100, 0, 255} + d := &font.Drawer{ + Dst: bf.bwImg, + Src: image.NewUniform(col), + Face: basicfont.Face7x13, + Dot: fixed.P(20, 30), } - - t1 := time.Now() - - // Get background image and save a new background image if needed - // first frame must always be sent. - if b.bg == nil { - bounds := b.img.Bounds() - b.w = bounds.Max.X - b.h = bounds.Max.Y - - b.bwImg = image.NewRGBA(image.Rect(0, 0, b.w, b.h)) - - b.bg = make([][][3]uint32, b.h) - for i, _ := range b.bg { - b.bg[i] = make([][3]uint32, b.w) - } - - for j := 0; j < b.h; j++ { - for i := 0; i < b.w; i++ { - n := b.img.At(i, j) - R, G, B, _ := n.RGBA() - copy(b.bg[j][i][:], []uint32{R, G, B}) - } - } - return len(f), nil + var s string + if bf.motion > 1000 { + s = strconv.Itoa(bf.motion) + " Motion" + } else { + s = strconv.Itoa(bf.motion) } - - // Use 4x goroutines to each process one row of pixels - j := 0 - var m sync.Mutex - m.Lock() - var wg sync.WaitGroup - - for j < b.h { - wg.Add(4) - go b.Process(j, &wg) - go b.Process(j+1, &wg) - go b.Process(j+2, &wg) - go b.Process(j+3, &wg) - j = j + 4 - wg.Wait() - } - - if j >= b.h { - m.Unlock() - } - - // Will save a video of where motion is detected in motion.mjpeg (in the current folder). - if b.debug { - col := color.RGBA{200, 100, 0, 255} - d := &font.Drawer{ - Dst: b.bwImg, - Src: image.NewUniform(col), - Face: basicfont.Face7x13, - Dot: fixed.P(20, 30), - } - var s string - if b.motion > 1000 { - s = strconv.Itoa(b.motion) + " Motion" - } else { - s = strconv.Itoa(b.motion) - } - d.DrawString(s) - err = jpeg.Encode(b.file, b.bwImg, nil) - if err != nil { - return len(f), err - } - } - - // If there are not enough motion pixels then discard the frame. - if b.motion < 1000 { - return len(f), nil - } - - // Write all motion frames. - return b.dst.Write(f) + d.DrawString(s) + err := jpeg.Encode(bf.file, bf.bwImg, nil) + return err }