@@ 1,361 @@
+package main
+
+import (
+ "bufio"
+ "bytes"
+ "encoding/json"
+ "flag"
+ "fmt"
+ "image"
+ "image/color"
+ _ "image/jpeg"
+ "log"
+ "os"
+ "time"
+
+ "git.sr.ht/~whereswaldon/latest"
+ // "golang.org/x/image/draw"
+
+ "gioui.org/app"
+ "gioui.org/f32"
+ "gioui.org/io/key"
+ "gioui.org/io/system"
+ "gioui.org/layout"
+ "gioui.org/op"
+ "gioui.org/op/clip"
+ "gioui.org/op/paint"
+ "gioui.org/unit"
+ "gioui.org/widget"
+ "github.com/blackjack/webcam"
+ "github.com/esimov/triangle"
+ "github.com/pkg/profile"
+)
+
+func main() {
+ var useProfiling bool
+ flag.BoolVar(&useProfiling, "profile", false, "enable pprof profiling")
+ go func() {
+ w := app.NewWindow()
+ if err := loop(w, useProfiling); err != nil {
+ log.Fatal(err)
+ }
+ os.Exit(0)
+ }()
+ app.Main()
+}
+
+// dump prints a JSONified struct to stdout.
+func dump(in interface{}) {
+ b, _ := json.MarshalIndent(in, "", " ")
+ fmt.Println(string(b))
+}
+
+// Timings holds performance timing information from throughout the render
+// process.
+type Timings struct {
+ Start, Capture, Decode, Convert, Blur, Gray, Sobel, Point, Delaunay time.Time
+}
+
+func (t Timings) String() string {
+ return fmt.Sprintln("tot:", t.Delaunay.Sub(t.Start))
+}
+
+// Result contains the result of rendering a single frame of video
+type Result struct {
+ // Triangles holds the list of Delaunay triangles representing the frame.
+ Triangles []triangle.Triangle
+ // Frame is the source image data.
+ Frame *image.NRGBA
+ // Timings is the performance information from generating this frame.
+ Timings
+ // Valid indicates whether or not this Result's data can be used. This will
+ // be false if something interrupted the render process.
+ Valid bool
+}
+
+// DebugResult holds all of the intermediate render data.
+type DebugResult struct {
+ Raw *image.NRGBA
+ Blur *image.NRGBA
+ Sobel *image.NRGBA
+ Points []triangle.Point
+ Triangles []triangle.Triangle
+}
+
+// FrameConfig holds the parameters provided to the renderer.
+type FrameConfig struct {
+ Debug bool
+}
+
+// MJPEGFourCC is the 4-byte code corresponding to the MJPEG codec in
+// Video4Linux.
+const MJPEGFourCC = 1196444237
+
+// loops runs the webcam output UI.
+func loop(w *app.Window, useProfiling bool) error {
+ logOut := bufio.NewWriter(log.Writer())
+ log.SetOutput(logOut)
+ defer logOut.Flush()
+ if useProfiling {
+ defer profile.Start().Stop()
+ }
+ cam, err := webcam.Open("/dev/video0")
+ if err != nil {
+ panic(err)
+ }
+ defer cam.Close()
+ if _, supported := cam.GetSupportedFormats()[MJPEGFourCC]; !supported {
+ panic(fmt.Errorf("webcam doesn't support MJPEG"))
+ }
+ sizes := cam.GetSupportedFrameSizes(MJPEGFourCC)
+ target := 0
+ for index, config := range sizes {
+ if config.MinWidth < sizes[target].MinWidth {
+ target = index
+ }
+ }
+ dump(sizes[target])
+ format, width, height, err := cam.SetImageFormat(MJPEGFourCC, sizes[target].MaxWidth, sizes[target].MinHeight)
+ if err != nil {
+ panic(err)
+ }
+ log.Println("webcam confirms", format, width, height)
+ if err := cam.SetBufferCount(1); err != nil {
+ panic(err)
+ }
+
+ if err := cam.StartStreaming(); err != nil {
+ panic(err)
+ }
+
+ p := triangle.Processor{
+ BlurRadius: 4,
+ SobelThreshold: 10,
+ PointsThreshold: 20,
+ MaxPoints: 500,
+ StrokeWidth: 0,
+ Wireframe: 0,
+ }
+
+ var ops op.Ops
+ tickerDuration := time.Second / 15
+ ticker := time.NewTicker(tickerDuration)
+ var init bool
+ var d triangle.Delaunay
+ var tris Result
+ worker := latest.NewWorker(func(in interface{}) interface{} {
+ config := in.(FrameConfig)
+ var timings Timings
+ timings.Start = time.Now()
+ if err := cam.WaitForFrame(1); err != nil {
+ return tris
+ }
+ buf, err := cam.ReadFrame()
+ if err != nil {
+ return tris
+ }
+ timings.Capture = time.Now()
+ frame, _, err := image.Decode(bytes.NewBuffer(buf))
+ if err != nil {
+ return tris
+ }
+ timings.Decode = time.Now()
+ width, height := frame.Bounds().Dx(), frame.Bounds().Dy()
+ asRGBA := triangle.ToNRGBA(frame)
+ timings.Convert = time.Now()
+ blur := asRGBA
+ if p.BlurRadius > 0 {
+ blur = triangle.StackBlur(asRGBA, uint32(p.BlurRadius))
+ }
+ timings.Blur = time.Now()
+ sobel := triangle.SobelFilter(blur, float64(p.SobelThreshold))
+ timings.Sobel = time.Now()
+ points := triangle.GetEdgePoints(sobel, p.PointsThreshold, p.MaxPoints)
+ timings.Point = time.Now()
+
+ triangles := d.Init(width, height).Insert(points).GetTriangles()
+ timings.Delaunay = time.Now()
+ if config.Debug {
+ go DebugWindow(DebugResult{
+ Raw: asRGBA,
+ Blur: blur,
+ Sobel: sobel,
+ Points: points,
+ Triangles: triangles,
+ })
+ }
+ return Result{Triangles: triangles, Frame: asRGBA, Timings: timings, Valid: true}
+
+ })
+ var aspect float32
+ var debug bool
+ for {
+ select {
+ case img := <-worker.Raw():
+ tris = img.(Result)
+ if tris.Valid {
+ if !init {
+ aspect = float32(tris.Frame.Bounds().Dx()) / float32(tris.Frame.Bounds().Dy())
+ }
+ init = true
+ w.Invalidate()
+ }
+ case <-ticker.C:
+ worker.Push(FrameConfig{Debug: debug})
+ debug = false
+ case e := <-w.Events():
+ switch e := e.(type) {
+ case system.DestroyEvent:
+ return e.Err
+ case system.FrameEvent:
+ start := time.Now()
+ sz := layout.FPt(e.Size)
+ szAspect := sz.X / sz.Y
+ if init && (szAspect-aspect > 0.01 || szAspect-aspect < -0.01) {
+ width, height := unit.Dp(sz.Y*aspect), unit.Dp(sz.Y)
+ log.Println(aspect, szAspect, width, height)
+ w.Option(app.Size(width, height))
+ w.Invalidate()
+ }
+ gtx := layout.NewContext(&ops, e)
+ if init {
+ paint.Fill(gtx.Ops, color.NRGBA{A: 255})
+ op.Affine(f32.Affine2D{}.Scale(f32.Point{}, f32.Point{
+ X: float32(gtx.Constraints.Max.X) / float32(tris.Frame.Rect.Dx()),
+ Y: float32(gtx.Constraints.Max.Y) / float32(tris.Frame.Rect.Dy()),
+ })).Add(gtx.Ops)
+ for _, t := range tris.Triangles {
+ LayoutDelaunyTriangle(gtx, t, tris.Frame)
+ }
+ }
+ e.Frame(gtx.Ops)
+ now := time.Now()
+ log.Println("frame", now.Sub(start), "latency", now.Sub(tris.Start))
+ case key.Event:
+ if e.Name == key.NameSpace {
+ debug = true
+ }
+ }
+ }
+ }
+}
+
+// Dp converts an integer quantity into unit.Dp.
+func Dp(in int) unit.Value {
+ return unit.Dp(float32(in))
+}
+
+// DebugWindow launches a new Gio window to present the DebugResult for
+// visual inspection.
+func DebugWindow(result DebugResult) {
+ width := result.Raw.Rect.Dx() * 4
+ height := result.Raw.Rect.Dy() * 4
+ w := app.NewWindow(app.Title("Debug"), app.Size(Dp(width), Dp(height*5)))
+ var ops op.Ops
+ var list layout.List
+ list.Axis = layout.Vertical
+ for {
+ select {
+ case e := <-w.Events():
+ switch e := e.(type) {
+ case system.DestroyEvent:
+ return
+ case system.FrameEvent:
+ gtx := layout.NewContext(&ops, e)
+ list.Layout(gtx, 5, func(gtx layout.Context, index int) layout.Dimensions {
+ switch index {
+ case 0:
+ return LayoutImage(gtx, result.Raw)
+ case 1:
+ return LayoutImage(gtx, result.Blur)
+ case 2:
+ return LayoutImage(gtx, result.Sobel)
+ case 3:
+ return LayoutPointsOnImage(gtx, result.Sobel, result.Points)
+ case 4:
+ return LayoutTrianglesOnImage(gtx, result.Sobel, result.Triangles)
+ default:
+ return layout.Dimensions{}
+ }
+ })
+ e.Frame(gtx.Ops)
+ }
+
+ }
+ }
+}
+
+// LayoutPointsOnImage lays out an image with some points superimposed on
+// top of it.
+func LayoutPointsOnImage(gtx layout.Context, img image.Image, points []triangle.Point) layout.Dimensions {
+ dims := LayoutImage(gtx, img)
+ scale := float32(dims.Size.X) / float32(img.Bounds().Dx())
+ defer op.Save(gtx.Ops).Load()
+ op.Affine(f32.Affine2D{}.Scale(f32.Point{}, f32.Point{X: scale, Y: scale})).Add(gtx.Ops)
+ for _, p := range points {
+ pt := f32.Point{X: float32(p.X), Y: float32(p.Y)}
+ DrawPoint(gtx, pt)
+ }
+ return dims
+}
+
+// DrawPoint renders a small green point at the provided location.
+func DrawPoint(gtx layout.Context, p f32.Point) {
+ paint.FillShape(gtx.Ops, color.NRGBA{G: 255, A: 255},
+ clip.Circle{
+ Center: p,
+ Radius: 0.3,
+ }.Op(gtx.Ops))
+}
+
+// LayoutTrianglesOnImage renders a image with triangles and their center
+// points indicated on top of it.
+func LayoutTrianglesOnImage(gtx layout.Context, img image.Image, tris []triangle.Triangle) layout.Dimensions {
+ dims := LayoutImage(gtx, img)
+ scale := float32(dims.Size.X) / float32(img.Bounds().Dx())
+ defer op.Save(gtx.Ops).Load()
+ op.Affine(f32.Affine2D{}.Scale(f32.Point{}, f32.Point{X: scale, Y: scale})).Add(gtx.Ops)
+ for _, t := range tris {
+ path, center := TriPath(gtx, t)
+ outline := clip.Stroke{
+ Path: path,
+ Style: clip.StrokeStyle{
+ Width: 0.1,
+ },
+ }.Op()
+ paint.FillShape(gtx.Ops, color.NRGBA{G: 255, A: 255}, outline)
+ DrawPoint(gtx, layout.FPt(center))
+ }
+ return dims
+}
+
+// LayoutImage displays an image contained by the gtx constraints.
+func LayoutImage(gtx layout.Context, img image.Image) layout.Dimensions {
+ imgOp := paint.NewImageOp(img)
+ return widget.Image{
+ Src: imgOp,
+ Fit: widget.Contain,
+ }.Layout(gtx)
+}
+
+// TriPath returns the path representing the given triangle as well as the
+// triangle's center point.
+func TriPath(gtx layout.Context, t triangle.Triangle) (clip.PathSpec, image.Point) {
+ p := clip.Path{}
+ p.Begin(gtx.Ops)
+ a, b, c := t.Nodes[0], t.Nodes[1], t.Nodes[2]
+ cx := (a.X + b.X + c.X) / 3
+ cy := (a.Y + b.Y + c.Y) / 3
+ p.MoveTo(layout.FPt(image.Pt(a.X, a.Y)))
+ p.LineTo(layout.FPt(image.Pt(b.X, b.Y)))
+ p.LineTo(layout.FPt(image.Pt(c.X, c.Y)))
+ p.Close()
+ return p.End(), image.Pt(cx, cy)
+}
+
+// LayoutDelaunyTriangle renders a single colored triangle.
+func LayoutDelaunyTriangle(gtx layout.Context, t triangle.Triangle, frame *image.NRGBA) {
+ path, center := TriPath(gtx, t)
+ outline := clip.Outline{Path: path}.Op()
+ paint.FillShape(gtx.Ops, frame.At(center.X, center.Y).(color.NRGBA), outline)
+}