mirror of
https://github.com/jehiah/TrafficSpeed.git
synced 2025-09-29 13:42:15 +08:00
382 lines
10 KiB
Go
382 lines
10 KiB
Go
package project
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"html/template"
|
|
"image"
|
|
"log"
|
|
"net/http"
|
|
"os"
|
|
"path/filepath"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/anthonynsimon/bild/transform"
|
|
"github.com/jehiah/TrafficSpeed/img/avgimg"
|
|
"github.com/jehiah/TrafficSpeed/img/imgutils"
|
|
)
|
|
|
|
// roughly 7s of frames
|
|
const bgFrameCount = 15
|
|
const bgFrameSkip = 15
|
|
|
|
type Project struct {
|
|
Filename string `json:"filename"` // Filename is not relative to Dir
|
|
Dir string `json:"dir,omitempty"`
|
|
Settings `json:"settings"`
|
|
|
|
Duration time.Duration `json:"duration,omitempty"`
|
|
VideoResolution string `json:"video_resolution,omitempty"`
|
|
Frames int64 `json:"frames,omitempty"`
|
|
|
|
Seek float64 `json:"-"`
|
|
Step int `json:"-"`
|
|
Response Response `json:"-"`
|
|
|
|
Err error `json:"-"`
|
|
}
|
|
|
|
// Settings are the user configurable options
|
|
type Settings struct {
|
|
PreCrop *BBox `json:"pre_crop,omitempty"`
|
|
Rotate float64 `json:"rotate,omitempty"` // radians
|
|
PostCrop *BBox `json:"post_crop,omitempty"`
|
|
Masks Masks `json:"masks,omitempty"`
|
|
Tolerance uint8 `json:"tolerance,omitempty"`
|
|
Blur int `json:"blur,omitempty"`
|
|
ContiguousPixels int `json:"contiguous_pixels,omitempty"`
|
|
MinMass int `json:"min_mass,omitempty"`
|
|
Calibrations []*Calibration `json:"-"`
|
|
}
|
|
|
|
type Response struct {
|
|
PreCroppedResolution string `json:"pre_cropped_resolution,omitempty"`
|
|
CroppedResolution string `json:"cropped_resolution,omitempty"`
|
|
OverviewGif template.URL `json:"overview_gif,omitempty"`
|
|
Step4Img template.URL `json:"step_4_img,omitempty"` // mask
|
|
Step4MaskImg template.URL `json:"step_4_mask_img,omitempty"`
|
|
BackgroundImg template.URL `json:"background_img,omitempty"`
|
|
FrameAnalysis []FrameAnalysis `json:"frame_analysis,omitempty"`
|
|
VehiclePositions []VehiclePosition
|
|
Step6Img template.URL `json:"step_6_img,omitempty"`
|
|
|
|
DebugImages []template.URL
|
|
}
|
|
|
|
type frameImage struct {
|
|
Frame int
|
|
Time time.Duration
|
|
Image *image.RGBA
|
|
}
|
|
|
|
// NewProject starst a project for the specified video file
|
|
func NewProject(f string, iterator *Iterator) *Project {
|
|
p := &Project{
|
|
Filename: f,
|
|
VideoResolution: iterator.VideoResolution(),
|
|
}
|
|
return p
|
|
}
|
|
|
|
// LoadProject loads a project from a JSON file
|
|
func LoadProject(f string) (*Project, error) {
|
|
var p Project
|
|
r, err := os.Open(f)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
err = json.NewDecoder(r).Decode(&p)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
p.Dir = filepath.Dir(f)
|
|
return &p, nil
|
|
}
|
|
|
|
func (p *Project) Iterator() (*Iterator, error) {
|
|
videoFile := filepath.Join(p.Dir, p.Filename)
|
|
iterator, err := NewIterator(videoFile)
|
|
return iterator, err
|
|
}
|
|
|
|
func (p *Project) Load(req *http.Request) error {
|
|
getf64 := func(key string, d float64) float64 {
|
|
if v := req.Form.Get(key); v != "" {
|
|
if f, err := strconv.ParseFloat(v, 64); err == nil {
|
|
return f
|
|
}
|
|
}
|
|
return d
|
|
}
|
|
geti64 := func(key string, d int64) int64 {
|
|
if v := req.Form.Get(key); v != "" {
|
|
if i, err := strconv.ParseInt(v, 10, 64); err == nil {
|
|
return i
|
|
}
|
|
}
|
|
return d
|
|
}
|
|
getuint8 := func(key string, d uint8) uint8 {
|
|
if v := req.Form.Get(key); v != "" {
|
|
if i, err := strconv.ParseUint(v, 10, 8); err == nil {
|
|
return uint8(i)
|
|
}
|
|
}
|
|
return d
|
|
}
|
|
|
|
p.PreCrop = ParseBBox(req.Form.Get("pre_crop"))
|
|
p.Rotate = getf64("rotate", 0)
|
|
p.PostCrop = ParseBBox(req.Form.Get("post_crop"))
|
|
p.Tolerance = getuint8("tolerance", 40)
|
|
p.Blur = int(geti64("blur", 2))
|
|
p.ContiguousPixels = int(geti64("contiguous_pixels", 3))
|
|
p.MinMass = int(geti64("min_mass", 50))
|
|
p.Seek = getf64("seek", 0)
|
|
p.Step = int(geti64("next", 0))
|
|
|
|
for _, s := range req.Form["calibration"] {
|
|
c := ParseCalibration(s)
|
|
if c != nil {
|
|
p.Calibrations = append(p.Calibrations, c)
|
|
} else {
|
|
log.Printf("error parsing calibration %q", s)
|
|
}
|
|
}
|
|
|
|
p1, p2 := req.Form.Get("point1"), req.Form.Get("point2")
|
|
if p1 != "" && p2 != "" {
|
|
switch {
|
|
case p.Step == 2:
|
|
p.PreCrop = &BBox{ParsePoint(p1), ParsePoint(p2)}
|
|
case p.Rotate == 0 && p.Step == 3:
|
|
p.Rotate = Radians(ParsePoint(p1), ParsePoint(p2))
|
|
log.Printf("calculated rotation radians %v from a:%v b:%v", p.Rotate, p1, p2)
|
|
case p.Step == 4:
|
|
p.PostCrop = &BBox{ParsePoint(p1), ParsePoint(p2)}
|
|
case p.Step == 6:
|
|
p.Calibrations = append(p.Calibrations, &Calibration{
|
|
Seek: p.Seek,
|
|
A: ParsePoint(p1),
|
|
B: ParsePoint(p2),
|
|
Inches: getf64("inches", 0),
|
|
})
|
|
p.Seek = 0
|
|
default:
|
|
log.Panicf("unknown point for step %v", p.Step)
|
|
}
|
|
}
|
|
|
|
for i, m := range req.Form["mask"] {
|
|
if mm, ok := ParseMask(m); ok {
|
|
p.Masks = append(p.Masks, mm)
|
|
} else if !ok && len(strings.TrimSpace(m)) > 0 {
|
|
p.Err = fmt.Errorf("Error Parsing Mask #%d %q", i+1, m)
|
|
break
|
|
}
|
|
}
|
|
p.Masks = p.Masks.Uniq()
|
|
return nil
|
|
}
|
|
|
|
func (p *Project) Run() (Response, error) {
|
|
start := time.Now()
|
|
defer func() {
|
|
log.Printf("Run took %s", time.Since(start))
|
|
}()
|
|
|
|
p.SetStep()
|
|
var results Response
|
|
var analysis = &FrameAnalysis{}
|
|
|
|
iterator, err := p.Iterator()
|
|
if err != nil {
|
|
return results, err
|
|
}
|
|
defer iterator.Close()
|
|
|
|
// set overview img
|
|
bg := &avgimg.MedianRGBA{}
|
|
var bgavg *image.RGBA
|
|
var framePositions []FramePosition
|
|
var pendingAnalysis []frameImage
|
|
analyzer := &Analyzer{
|
|
BWCutoff: p.Tolerance,
|
|
BlurRadius: p.Blur,
|
|
ContinguousPixels: p.ContiguousPixels,
|
|
MinMass: p.MinMass,
|
|
}
|
|
|
|
setFrame := p.Frames == 0
|
|
log.Printf("step %v", p.Step)
|
|
for iterator.Next() {
|
|
frame := iterator.Frame()
|
|
// log.Printf("frame %d time %s", frame, iterator.Duration())
|
|
if setFrame {
|
|
p.Frames = int64(frame)
|
|
p.Duration = iterator.Duration()
|
|
}
|
|
// log.Printf("loop frame:%d duration:%s", frame, iterator.Duration())
|
|
interested := true
|
|
switch {
|
|
case frame == 0:
|
|
case p.Step == 5 && len(bg.Images) < bgFrameCount:
|
|
// get all frames until we have a background because frames are dependent on the previous frame
|
|
case p.Step == 5 && analysis.NeedsMore():
|
|
case p.Step == 6:
|
|
default:
|
|
interested = false
|
|
}
|
|
|
|
img := imgutils.RGBA(iterator.Image().(*image.YCbCr))
|
|
rgbImg := img
|
|
if interested {
|
|
log.Printf("interested in frame %d time %s", frame, iterator.Duration())
|
|
}
|
|
|
|
if frame == 0 {
|
|
|
|
if p.PreCrop != nil {
|
|
log.Printf("PreCrop %v", p.PreCrop)
|
|
img = img.SubImage(p.PreCrop.Rect()).(*image.RGBA)
|
|
results.PreCroppedResolution = fmt.Sprintf("%dx%d", p.PreCrop.Dx(), p.PreCrop.Dy())
|
|
}
|
|
|
|
if p.Step == 2 {
|
|
err = p.SaveImage(img, "step2_crop.png")
|
|
if err != nil {
|
|
return results, err
|
|
}
|
|
}
|
|
|
|
if p.Step >= 3 {
|
|
log.Printf("rotating %v", p.Rotate)
|
|
// apply rotation
|
|
|
|
img = transform.Rotate(img, RadiansToDegrees(p.Rotate), &transform.RotationOptions{ResizeBounds: true})
|
|
err = p.SaveImage(img, "step3_rotate.png")
|
|
if err != nil {
|
|
return results, err
|
|
}
|
|
}
|
|
if p.Step >= 4 {
|
|
// rotate & crop
|
|
log.Printf("PostCrop %v", p.PostCrop)
|
|
if p.PostCrop != nil {
|
|
img = img.SubImage(p.PostCrop.Rect()).(*image.RGBA)
|
|
// img = transform.Crop(img, p.BBox.Rect())
|
|
results.CroppedResolution = fmt.Sprintf("%dx%d", p.PostCrop.Dx(), p.PostCrop.Dy())
|
|
} else {
|
|
results.CroppedResolution = fmt.Sprintf("%dx%d", img.Bounds().Dx(), img.Bounds().Dy())
|
|
}
|
|
results.Step4Img = dataImg(img, "image/webp")
|
|
}
|
|
if p.Step >= 5 {
|
|
// mask
|
|
p.Masks.Apply(img)
|
|
results.Step4MaskImg = dataImg(img, "image/webp")
|
|
}
|
|
}
|
|
|
|
switch {
|
|
case p.Step == 5 && len(bg.Images) < bgFrameCount && frame%bgFrameSkip == 0:
|
|
fallthrough
|
|
case p.Step == 5 && analysis.NeedsMore() || p.Step == 6:
|
|
if p.PreCrop != nil {
|
|
rgbImg = rgbImg.SubImage(p.PreCrop.Rect()).(*image.RGBA)
|
|
}
|
|
|
|
if p.Rotate != 0 {
|
|
rgbImg = transform.Rotate(rgbImg, RadiansToDegrees(p.Rotate), &transform.RotationOptions{ResizeBounds: true})
|
|
}
|
|
if p.PostCrop != nil {
|
|
rgbImg = rgbImg.SubImage(p.PostCrop.Rect()).(*image.RGBA)
|
|
}
|
|
p.Masks.Apply(rgbImg)
|
|
}
|
|
|
|
if p.Step >= 5 && len(bg.Images) < bgFrameCount && frame%bgFrameSkip == 0 {
|
|
bg.Images = append(bg.Images, rgbImg)
|
|
if len(bg.Images) == bgFrameCount {
|
|
log.Printf("calculating background from %d frames", len(bg.Images))
|
|
bgavg = bg.Image()
|
|
analyzer.Background = bgavg
|
|
results.BackgroundImg = dataImg(bgavg, "")
|
|
}
|
|
}
|
|
if p.Step == 5 && analysis.NeedsMore() {
|
|
log.Printf("saving frame %d for analysis later", frame)
|
|
analysis.images = append(analysis.images, rgbImg)
|
|
}
|
|
|
|
// set every frame, so this ends w/ the last value
|
|
if p.Step == 6 && bgavg == nil {
|
|
pendingAnalysis = append(pendingAnalysis, frameImage{frame, iterator.Duration(), rgbImg})
|
|
}
|
|
|
|
if p.Step == 6 && bgavg != nil {
|
|
// process pending frames
|
|
log.Printf("extracting vehicle position from %d pending frames", len(pendingAnalysis))
|
|
for _, pf := range pendingAnalysis {
|
|
if pf.Frame%50 == 0 && pf.Frame > 0 {
|
|
log.Printf("... frame %d", pf.Frame)
|
|
}
|
|
positions := analyzer.Positions(pf.Image)
|
|
if len(positions) > 0 {
|
|
framePositions = append(framePositions, FramePosition{pf.Frame, pf.Time, positions})
|
|
}
|
|
}
|
|
pendingAnalysis = nil
|
|
positions := analyzer.Positions(rgbImg)
|
|
if len(positions) > 0 {
|
|
framePositions = append(framePositions, FramePosition{frame, iterator.Duration(), positions})
|
|
}
|
|
}
|
|
if p.Step <= 1 && frame == 0 {
|
|
break
|
|
}
|
|
if p.Step == 6 && frame >= 200 {
|
|
break
|
|
}
|
|
}
|
|
if err = iterator.Error(); err != nil {
|
|
return results, err
|
|
}
|
|
if p.Step >= 5 && bgavg == nil {
|
|
if len(bg.Images) > 0 {
|
|
log.Printf("calculating background from %d frames", len(bg.Images))
|
|
bgavg = bg.Image()
|
|
analyzer.Background = bgavg
|
|
results.BackgroundImg = dataImg(bgavg, "")
|
|
|
|
if p.Step == 6 {
|
|
log.Printf("extracting vehicle position from %d pending frames", len(pendingAnalysis))
|
|
for _, pf := range pendingAnalysis {
|
|
if pf.Frame%50 == 0 && pf.Frame > 0 {
|
|
log.Printf("... frame %d", pf.Frame)
|
|
}
|
|
positions := analyzer.Positions(pf.Image)
|
|
if len(positions) > 0 {
|
|
framePositions = append(framePositions, FramePosition{pf.Frame, pf.Time, positions})
|
|
}
|
|
}
|
|
pendingAnalysis = nil
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
if p.Step == 6 {
|
|
results.VehiclePositions = TrackVehicles(framePositions)
|
|
}
|
|
|
|
if p.Step == 5 && bgavg != nil {
|
|
analysis.Calculate(bgavg, p.Blur, p.ContiguousPixels, p.MinMass, p.Tolerance)
|
|
results.FrameAnalysis = append(results.FrameAnalysis, *analysis)
|
|
}
|
|
|
|
return results, nil
|
|
}
|