Files
TrafficSpeed/internal/project/project.go
2025-06-13 10:00:56 -04:00

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
}