mirror of
https://github.com/esimov/pigo-wasm-demos.git
synced 2025-12-24 12:47:56 +08:00
Included basic wasm demo
This commit is contained in:
22
wasm.go
Normal file
22
wasm.go
Normal file
@@ -0,0 +1,22 @@
|
||||
//go:build js && wasm
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/esimov/pigo-wasm-demos/wasm"
|
||||
)
|
||||
|
||||
func main() {
|
||||
c := wasm.NewCanvas()
|
||||
webcam, err := c.StartWebcam()
|
||||
if err != nil {
|
||||
c.Alert("Webcam not detected!")
|
||||
} else {
|
||||
err := webcam.Render()
|
||||
if err != nil {
|
||||
c.Log(fmt.Sprint(err))
|
||||
}
|
||||
}
|
||||
}
|
||||
293
wasm/canvas.go
Normal file
293
wasm/canvas.go
Normal file
@@ -0,0 +1,293 @@
|
||||
package wasm
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math"
|
||||
"syscall/js"
|
||||
|
||||
"github.com/esimov/pigo-wasm-demos/detector"
|
||||
)
|
||||
|
||||
// Canvas struct holds the Javascript objects needed for the Canvas creation
|
||||
type Canvas struct {
|
||||
done chan struct{}
|
||||
succCh chan struct{}
|
||||
errCh chan error
|
||||
|
||||
// DOM elements
|
||||
window js.Value
|
||||
doc js.Value
|
||||
body js.Value
|
||||
windowSize struct{ width, height int }
|
||||
|
||||
// Canvas properties
|
||||
canvas js.Value
|
||||
ctx js.Value
|
||||
reqID js.Value
|
||||
renderer js.Func
|
||||
|
||||
// Webcam properties
|
||||
navigator js.Value
|
||||
video js.Value
|
||||
|
||||
showPupil bool
|
||||
showCoord bool
|
||||
flploc bool
|
||||
markerType string
|
||||
markerIdx int
|
||||
}
|
||||
|
||||
var det *detector.Detector
|
||||
|
||||
// NewCanvas creates and initializes the new Canvas element
|
||||
func NewCanvas() *Canvas {
|
||||
var c Canvas
|
||||
c.window = js.Global()
|
||||
c.doc = c.window.Get("document")
|
||||
c.body = c.doc.Get("body")
|
||||
|
||||
c.windowSize.width = 1024
|
||||
c.windowSize.height = 640
|
||||
|
||||
c.canvas = c.doc.Call("createElement", "canvas")
|
||||
c.canvas.Set("width", c.windowSize.width)
|
||||
c.canvas.Set("height", c.windowSize.height)
|
||||
c.canvas.Set("id", "canvas")
|
||||
c.body.Call("appendChild", c.canvas)
|
||||
|
||||
c.ctx = c.canvas.Call("getContext", "2d")
|
||||
c.showPupil = true
|
||||
c.showCoord = false
|
||||
c.flploc = false
|
||||
c.markerType = "rect"
|
||||
|
||||
det = detector.NewDetector()
|
||||
return &c
|
||||
}
|
||||
|
||||
// Render calls the `requestAnimationFrame` Javascript function in asynchronous mode.
|
||||
func (c *Canvas) Render() error {
|
||||
width, height := c.windowSize.width, c.windowSize.height
|
||||
var data = make([]byte, width*height*4)
|
||||
c.done = make(chan struct{})
|
||||
|
||||
err := det.UnpackCascades()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
c.renderer = js.FuncOf(func(this js.Value, args []js.Value) interface{} {
|
||||
go func() {
|
||||
c.window.Get("stats").Call("begin")
|
||||
|
||||
c.reqID = c.window.Call("requestAnimationFrame", c.renderer)
|
||||
// Draw the webcam frame to the canvas element
|
||||
c.ctx.Call("drawImage", c.video, 0, 0)
|
||||
rgba := c.ctx.Call("getImageData", 0, 0, width, height).Get("data")
|
||||
|
||||
// Convert the rgba value of type Uint8ClampedArray to Uint8Array in order to
|
||||
// be able to transfer it from Javascript to Go via the js.CopyBytesToGo function.
|
||||
uint8Arr := js.Global().Get("Uint8Array").New(rgba)
|
||||
js.CopyBytesToGo(data, uint8Arr)
|
||||
pixels := c.rgbaToGrayscale(data)
|
||||
|
||||
// Empty the data slice to avoid unnecessary memory allocation.
|
||||
// Otherwise, the GC won't clean up the memory address allocated by this slice
|
||||
// and the memory will keep up increasing by each iteration.
|
||||
data = make([]byte, len(data))
|
||||
|
||||
res := det.DetectFaces(pixels, height, width)
|
||||
c.drawDetection(res)
|
||||
|
||||
c.window.Get("stats").Call("end")
|
||||
}()
|
||||
return nil
|
||||
})
|
||||
// Release renderer to free up resources.
|
||||
defer c.renderer.Release()
|
||||
|
||||
c.window.Call("requestAnimationFrame", c.renderer)
|
||||
c.detectKeyPress()
|
||||
<-c.done
|
||||
return nil
|
||||
}
|
||||
|
||||
// Stop stops the rendering.
|
||||
func (c *Canvas) Stop() {
|
||||
c.window.Call("cancelAnimationFrame", c.reqID)
|
||||
c.done <- struct{}{}
|
||||
close(c.done)
|
||||
}
|
||||
|
||||
// StartWebcam reads the webcam data and feeds it into the canvas element.
|
||||
// It returns an empty struct in case of success and error in case of failure.
|
||||
func (c *Canvas) StartWebcam() (*Canvas, error) {
|
||||
var err error
|
||||
c.succCh = make(chan struct{})
|
||||
c.errCh = make(chan error)
|
||||
|
||||
c.video = c.doc.Call("createElement", "video")
|
||||
|
||||
// If we don't do this, the stream will not be played.
|
||||
c.video.Set("autoplay", 1)
|
||||
c.video.Set("playsinline", 1) // important for iPhones
|
||||
|
||||
// The video should fill out all of the canvas
|
||||
c.video.Set("width", 0)
|
||||
c.video.Set("height", 0)
|
||||
|
||||
c.body.Call("appendChild", c.video)
|
||||
|
||||
success := js.FuncOf(func(this js.Value, args []js.Value) interface{} {
|
||||
go func() {
|
||||
c.video.Set("srcObject", args[0])
|
||||
c.video.Call("play")
|
||||
c.succCh <- struct{}{}
|
||||
}()
|
||||
return nil
|
||||
})
|
||||
|
||||
failure := js.FuncOf(func(this js.Value, args []js.Value) interface{} {
|
||||
go func() {
|
||||
err = fmt.Errorf("failed initialising the camera: %s", args[0].String())
|
||||
c.errCh <- err
|
||||
}()
|
||||
return nil
|
||||
})
|
||||
|
||||
opts := js.Global().Get("Object").New()
|
||||
|
||||
videoSize := js.Global().Get("Object").New()
|
||||
videoSize.Set("width", c.windowSize.width)
|
||||
videoSize.Set("height", c.windowSize.height)
|
||||
videoSize.Set("aspectRatio", 1.777777778)
|
||||
|
||||
opts.Set("video", videoSize)
|
||||
opts.Set("audio", false)
|
||||
|
||||
promise := c.window.Get("navigator").Get("mediaDevices").Call("getUserMedia", opts)
|
||||
promise.Call("then", success, failure)
|
||||
|
||||
select {
|
||||
case <-c.succCh:
|
||||
return c, nil
|
||||
case err := <-c.errCh:
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
// rgbaToGrayscale converts the rgb pixel values to grayscale
|
||||
func (c *Canvas) rgbaToGrayscale(data []uint8) []uint8 {
|
||||
rows, cols := c.windowSize.width, c.windowSize.height
|
||||
for r := 0; r < rows; r++ {
|
||||
for c := 0; c < cols; c++ {
|
||||
// gray = 0.2*red + 0.7*green + 0.1*blue
|
||||
data[r*cols+c] = uint8(math.Round(
|
||||
0.2126*float64(data[r*4*cols+4*c+0]) +
|
||||
0.7152*float64(data[r*4*cols+4*c+1]) +
|
||||
0.0722*float64(data[r*4*cols+4*c+2])))
|
||||
}
|
||||
}
|
||||
return data
|
||||
}
|
||||
|
||||
// drawDetection draws the detected faces and eyes.
|
||||
func (c *Canvas) drawDetection(dets [][]int) {
|
||||
for i := 0; i < len(dets); i++ {
|
||||
if dets[i][3] > 50 {
|
||||
c.ctx.Call("beginPath")
|
||||
c.ctx.Set("lineWidth", 3)
|
||||
c.ctx.Set("strokeStyle", "red")
|
||||
|
||||
row, col, scale := dets[i][1], dets[i][0], dets[i][2]
|
||||
if c.showCoord {
|
||||
c.ctx.Set("fillStyle", "red")
|
||||
c.ctx.Set("font", "18px Arial")
|
||||
message := fmt.Sprintf("(%v, %v)", row-scale/2, col-scale/2)
|
||||
txtWidth := c.ctx.Call("measureText", js.ValueOf(message)).Get("width").Int()
|
||||
c.ctx.Call("fillText", message, (row-scale/2)-txtWidth/2, col-scale/2-10)
|
||||
}
|
||||
switch c.markerType {
|
||||
case "rect":
|
||||
c.ctx.Call("rect", row-scale/2, col-scale/2, scale, scale)
|
||||
case "circle":
|
||||
c.ctx.Call("moveTo", row+int(scale/2), col)
|
||||
c.ctx.Call("arc", row, col, scale/2, 0, 2*math.Pi, true)
|
||||
case "ellipse":
|
||||
c.ctx.Call("moveTo", row+int(scale/2), col)
|
||||
c.ctx.Call("ellipse", row, col, scale/2, float64(scale)/1.6, 0, 0, 2*math.Pi)
|
||||
}
|
||||
c.ctx.Call("stroke")
|
||||
|
||||
if c.showPupil {
|
||||
leftPupil := det.DetectLeftPupil(dets[i])
|
||||
if leftPupil != nil {
|
||||
col, row, scale := leftPupil.Col, leftPupil.Row, leftPupil.Scale/8
|
||||
c.ctx.Call("moveTo", col+int(scale), row)
|
||||
c.ctx.Call("arc", col, row, scale, 0, 2*math.Pi, true)
|
||||
}
|
||||
|
||||
rightPupil := det.DetectRightPupil(dets[i])
|
||||
if rightPupil != nil {
|
||||
col, row, scale := rightPupil.Col, rightPupil.Row, rightPupil.Scale/8
|
||||
c.ctx.Call("moveTo", col+int(scale), row)
|
||||
c.ctx.Call("arc", col, row, scale, 0, 2*math.Pi, true)
|
||||
}
|
||||
c.ctx.Call("stroke")
|
||||
|
||||
if c.flploc {
|
||||
flps := det.DetectLandmarkPoints(leftPupil, rightPupil)
|
||||
c.ctx.Call("beginPath")
|
||||
c.ctx.Set("fillStyle", "rgb(0, 255, 0)")
|
||||
for _, flp := range flps {
|
||||
if len(flp) > 0 {
|
||||
col, row, scale = flp[1], flp[0], flp[2]/7
|
||||
c.ctx.Call("moveTo", row, col)
|
||||
c.ctx.Call("arc", row, col, scale, 0, 2*math.Pi, false)
|
||||
}
|
||||
}
|
||||
c.ctx.Call("fill")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// detectKeyPress listen for the keypress event and retrieves the key code.
|
||||
func (c *Canvas) detectKeyPress() {
|
||||
keyEventHandler := js.FuncOf(func(this js.Value, args []js.Value) interface{} {
|
||||
keyCode := args[0].Get("key")
|
||||
switch {
|
||||
case keyCode.String() == "e":
|
||||
c.showPupil = !c.showPupil
|
||||
case keyCode.String() == "c":
|
||||
c.markerIdx++
|
||||
if c.markerIdx > 2 {
|
||||
c.markerIdx = 0
|
||||
}
|
||||
if c.markerIdx == 0 {
|
||||
c.markerType = "rect"
|
||||
} else if c.markerIdx == 1 {
|
||||
c.markerType = "circle"
|
||||
} else if c.markerIdx == 2 {
|
||||
c.markerType = "ellipse"
|
||||
}
|
||||
case keyCode.String() == "f":
|
||||
c.flploc = !c.flploc
|
||||
case keyCode.String() == "x":
|
||||
c.showCoord = !c.showCoord
|
||||
}
|
||||
return nil
|
||||
})
|
||||
c.doc.Call("addEventListener", "keypress", keyEventHandler)
|
||||
}
|
||||
|
||||
// Log calls the `console.log` Javascript function
|
||||
func (c *Canvas) Log(args ...interface{}) {
|
||||
c.window.Get("console").Call("log", args...)
|
||||
}
|
||||
|
||||
// Alert calls the `alert` Javascript function
|
||||
func (c *Canvas) Alert(args ...interface{}) {
|
||||
alert := c.window.Get("alert")
|
||||
alert.Invoke(args...)
|
||||
}
|
||||
Reference in New Issue
Block a user