diff --git a/examples/opencv.go b/examples/opencv.go deleted file mode 100644 index 00ee6cf..0000000 --- a/examples/opencv.go +++ /dev/null @@ -1,112 +0,0 @@ -// +build gocv - -package examples - -import ( - "fmt" - "image" - "image/color" - "io" - "log" - - ffmpeg "github.com/u2takey/ffmpeg-go" - "gocv.io/x/gocv" -) - -func TestExampleOpenCvFaceDetect(t *testing.T) { - ExampleFaceDetection("./sample_data/head-pose-face-detection-male-short.mp4", - "./sample_data/haarcascade_frontalface_default.xml") -} - -func readProcess(infileName string, writer io.WriteCloser) <-chan error { - log.Println("Starting ffmpeg process1") - done := make(chan error) - go func() { - err := ffmpeg.Input(infileName). - Output("pipe:", - ffmpeg.KwArgs{ - "format": "rawvideo", "pix_fmt": "rgb24", - }). - WithOutput(writer). - Run() - log.Println("ffmpeg process1 done") - _ = writer.Close() - done <- err - close(done) - }() - return done -} - -func openCvProcess(xmlFile string, reader io.ReadCloser, w, h int) { - // open display window - window := gocv.NewWindow("Face Detect") - defer window.Close() - - // color for the rect when faces detected - blue := color.RGBA{0, 0, 255, 0} - - classifier := gocv.NewCascadeClassifier() - defer classifier.Close() - - if !classifier.Load(xmlFile) { - fmt.Printf("Error reading cascade file: %v\n", xmlFile) - return - } - - frameSize := w * h * 3 - buf := make([]byte, frameSize, frameSize) - for { - n, err := io.ReadFull(reader, buf) - if n == 0 || err == io.EOF { - return - } else if n != frameSize || err != nil { - panic(fmt.Sprintf("read error: %d, %s", n, err)) - } - img, err := gocv.NewMatFromBytes(h, w, gocv.MatTypeCV8UC3, buf) - if err != nil { - fmt.Println("decode fail", err) - } - if img.Empty() { - continue - } - img2 := gocv.NewMat() - gocv.CvtColor(img, &img2, gocv.ColorBGRToRGB) - - // detect faces - rects := classifier.DetectMultiScale(img2) - fmt.Printf("found %d faces\n", len(rects)) - - // draw a rectangle around each face on the original image, - // along with text identifing as "Human" - for _, r := range rects { - gocv.Rectangle(&img2, r, blue, 3) - - size := gocv.GetTextSize("Human", gocv.FontHersheyPlain, 1.2, 2) - pt := image.Pt(r.Min.X+(r.Min.X/2)-(size.X/2), r.Min.Y-2) - gocv.PutText(&img2, "Human", pt, gocv.FontHersheyPlain, 1.2, blue, 2) - } - - // show the image in the window, and wait 1 millisecond - window.IMShow(img2) - img.Close() - img2.Close() - if window.WaitKey(10) >= 0 { - break - } - } - return -} - -func ExampleFaceDetection(inputFile, xmlFile string) { - w, h := getVideoSize(inputFile) - log.Println(w, h) - - pr1, pw1 := io.Pipe() - done1 := readProcess(inputFile, pw1) - openCvProcess(xmlFile, pr1, w, h) - err := <-done1 - if err != nil { - panic(err) - } - log.Println("Done") -} diff --git a/examples/opencv_test.go b/examples/opencv_test.go new file mode 100644 index 0000000..95024e1 --- /dev/null +++ b/examples/opencv_test.go @@ -0,0 +1,197 @@ +// +build gocv + +// uncomment line above for gocv examples + +package examples + +import ( + "fmt" + "image" + "image/color" + "io" + "log" + "testing" + + ffmpeg "github.com/u2takey/ffmpeg-go" + "gocv.io/x/gocv" +) + +// TestExampleOpenCvFaceDetect will: take a video as input => use opencv for face detection => draw box and show a window +// This example depends on gocv and opencv, please refer: https://pkg.go.dev/gocv.io/x/gocv for installation. +func TestExampleOpenCvFaceDetectWithVideo(t *testing.T) { + inputFile := "./sample_data/head-pose-face-detection-male-short.mp4" + xmlFile := "./sample_data/haarcascade_frontalface_default.xml" + + w, h := getVideoSize(inputFile) + log.Println(w, h) + + pr1, pw1 := io.Pipe() + readProcess(inputFile, pw1) + openCvProcess(xmlFile, pr1, w, h) + log.Println("Done") +} + +func readProcess(infileName string, writer io.WriteCloser) { + log.Println("Starting ffmpeg process1") + go func() { + err := ffmpeg.Input(infileName). + Output("pipe:", + ffmpeg.KwArgs{ + "format": "rawvideo", "pix_fmt": "rgb24", + }). + WithOutput(writer). + ErrorToStdOut(). + Run() + log.Println("ffmpeg process1 done") + _ = writer.Close() + if err != nil { + panic(err) + } + }() + return +} + +func openCvProcess(xmlFile string, reader io.ReadCloser, w, h int) { + // open display window + window := gocv.NewWindow("Face Detect") + defer window.Close() + + // color for the rect when faces detected + blue := color.RGBA{B: 255} + + classifier := gocv.NewCascadeClassifier() + defer classifier.Close() + + if !classifier.Load(xmlFile) { + fmt.Printf("Error reading cascade file: %v\n", xmlFile) + return + } + + frameSize := w * h * 3 + buf := make([]byte, frameSize, frameSize) + for { + n, err := io.ReadFull(reader, buf) + if n == 0 || err == io.EOF { + return + } else if n != frameSize || err != nil { + panic(fmt.Sprintf("read error: %d, %s", n, err)) + } + img, err := gocv.NewMatFromBytes(h, w, gocv.MatTypeCV8UC3, buf) + if err != nil { + fmt.Println("decode fail", err) + } + if img.Empty() { + continue + } + img2 := gocv.NewMat() + gocv.CvtColor(img, &img2, gocv.ColorBGRToRGB) + + // detect faces + rects := classifier.DetectMultiScale(img2) + fmt.Printf("found %d faces\n", len(rects)) + + // draw a rectangle around each face on the original image, along with text identifing as "Human" + for _, r := range rects { + gocv.Rectangle(&img2, r, blue, 3) + + size := gocv.GetTextSize("Human", gocv.FontHersheyPlain, 1.2, 2) + pt := image.Pt(r.Min.X+(r.Min.X/2)-(size.X/2), r.Min.Y-2) + gocv.PutText(&img2, "Human", pt, gocv.FontHersheyPlain, 1.2, blue, 2) + } + + // show the image in the window, and wait 1 millisecond + window.IMShow(img2) + img.Close() + img2.Close() + if window.WaitKey(10) >= 0 { + break + } + } + return +} + +// TestExampleOpenCvFaceDetectWithCamera will: task stream from webcam => use opencv for face detection => output with ffmpeg +// This example depends on gocv and opencv, please refer: https://pkg.go.dev/gocv.io/x/gocv for installation. +func TestExampleOpenCvFaceDetectWithCamera(t *testing.T) { + deviceID := "0" // camera device id + xmlFile := "./sample_data/haarcascade_frontalface_default.xml" + + webcam, err := gocv.OpenVideoCapture(deviceID) + if err != nil { + fmt.Printf("error opening video capture device: %v\n", deviceID) + return + } + defer webcam.Close() + + // prepare image matrix + img := gocv.NewMat() + defer img.Close() + + if ok := webcam.Read(&img); !ok { + panic(fmt.Sprintf("Cannot read device %v", deviceID)) + } + fmt.Printf("img: %vX%v\n", img.Cols(), img.Rows()) + + pr1, pw1 := io.Pipe() + writeProcess("./sample_data/face_detect.mp4", pr1, img.Cols(), img.Rows()) + + // color for the rect when faces detected + blue := color.RGBA{B: 255} + + // load classifier to recognize faces + classifier := gocv.NewCascadeClassifier() + defer classifier.Close() + + if !classifier.Load(xmlFile) { + fmt.Printf("Error reading cascade file: %v\n", xmlFile) + return + } + + fmt.Printf("Start reading device: %v\n", deviceID) + for i := 0; i < 200; i++ { + if ok := webcam.Read(&img); !ok { + fmt.Printf("Device closed: %v\n", deviceID) + return + } + if img.Empty() { + continue + } + + // detect faces + rects := classifier.DetectMultiScale(img) + fmt.Printf("found %d faces\n", len(rects)) + + // draw a rectangle around each face on the original image, along with text identifing as "Human" + for _, r := range rects { + gocv.Rectangle(&img, r, blue, 3) + + size := gocv.GetTextSize("Human", gocv.FontHersheyPlain, 1.2, 2) + pt := image.Pt(r.Min.X+(r.Min.X/2)-(size.X/2), r.Min.Y-2) + gocv.PutText(&img, "Human", pt, gocv.FontHersheyPlain, 1.2, blue, 2) + } + pw1.Write(img.ToBytes()) + } + pw1.Close() + log.Println("Done") +} + +func writeProcess(outputFile string, reader io.ReadCloser, w, h int) { + log.Println("Starting ffmpeg process1") + go func() { + err := ffmpeg.Input("pipe:", + ffmpeg.KwArgs{"format": "rawvideo", + "pix_fmt": "bgr24", "s": fmt.Sprintf("%dx%d", w, h), + }). + Overlay(ffmpeg.Input("./sample_data/overlay.png"), ""). + Output(outputFile). + WithInput(reader). + ErrorToStdOut(). + OverWriteOutput(). + Run() + log.Println("ffmpeg process1 done") + if err != nil { + panic(err) + } + _ = reader.Close() + }() +} diff --git a/examples/sample_data/out1.mp4 b/examples/sample_data/out1.mp4 deleted file mode 100644 index 91de804..0000000 Binary files a/examples/sample_data/out1.mp4 and /dev/null differ diff --git a/ffmpeg_test.go b/ffmpeg_test.go index efc47e1..4b1fb39 100644 --- a/ffmpeg_test.go +++ b/ffmpeg_test.go @@ -84,6 +84,31 @@ func TestSimpleOutputArgs(t *testing.T) { "2", "-vf", "fps=3", "imageFromVideo_%d.jpg"}, cmd.GetArgs()) } +func TestAutomaticStreamSelection(t *testing.T) { + // example from http://ffmpeg.org/ffmpeg-all.html + input := []*Stream{Input("A.avi"), Input("B.mp4")} + out1 := Output(input, "out1.mkv") + out2 := Output(input, "out2.wav") + out3 := Output(input, "out3.mov", KwArgs{"map": "1:a", "c:a": "copy"}) + cmd := MergeOutputs(out1, out2, out3) + printArgs(cmd.GetArgs()) + printGraph(cmd) +} + +func TestLabeledFiltergraph(t *testing.T) { + // example from http://ffmpeg.org/ffmpeg-all.html + in1, in2, in3 := Input("A.avi"), Input("B.mp4"), Input("C.mkv") + in2Split := in2.Get("v").Hue(KwArgs{"s": 0}).Split() + overlay := Filter([]*Stream{in1, in2}, "overlay", nil) + aresample := Filter([]*Stream{in1, in2, in3}, "aresample", nil) + out1 := Output([]*Stream{in2Split.Get("outv1"), overlay, aresample}, "out1.mp4", KwArgs{"an": ""}) + out2 := Output([]*Stream{in1, in2, in3}, "out2.mkv") + out3 := in2Split.Get("outv2").Output("out3.mkv", KwArgs{"map": "1:a:0"}) + cmd := MergeOutputs(out1, out2, out3) + printArgs(cmd.GetArgs()) + printGraph(cmd) +} + func ComplexFilterExample() *Stream { split := Input(TestInputFile1).VFlip().Split() split0, split1 := split.Get("0"), split.Get("1") @@ -311,6 +336,19 @@ func TestView(t *testing.T) { t.Log(b) } +func printArgs(args []string) { + for _, a := range args { + fmt.Printf("%s ", a) + } + fmt.Println() +} + +func printGraph(s *Stream) { + fmt.Println() + v, _ := s.View(ViewTypeFlowChart) + fmt.Println(v) +} + //func TestAvFoundation(t *testing.T) { // out := Input("default:none", KwArgs{"f": "avfoundation", "framerate": "30"}). // Output("output.mp4", KwArgs{"format": "mp4"}). diff --git a/filters.go b/filters.go index ad5c6a0..725fec7 100644 --- a/filters.go +++ b/filters.go @@ -5,7 +5,7 @@ import ( "strconv" ) -func AssetType(hasType, expectType string, action string) { +func AssertType(hasType, expectType string, action string) { if hasType != expectType { panic(fmt.Sprintf("cannot %s on non-%s", action, expectType)) } @@ -20,32 +20,32 @@ func Filter(streamSpec []*Stream, filterName string, args Args, kwArgs ...KwArgs } func (s *Stream) Filter(filterName string, args Args, kwArgs ...KwArgs) *Stream { - AssetType(s.Type, "FilterableStream", "filter") + AssertType(s.Type, "FilterableStream", "filter") return Filter([]*Stream{s}, filterName, args, MergeKwArgs(kwArgs)) } func (s *Stream) Split() *Node { - AssetType(s.Type, "FilterableStream", "split") + AssertType(s.Type, "FilterableStream", "split") return NewFilterNode("split", []*Stream{s}, 1, nil, nil) } func (s *Stream) ASplit() *Node { - AssetType(s.Type, "FilterableStream", "asplit") + AssertType(s.Type, "FilterableStream", "asplit") return NewFilterNode("asplit", []*Stream{s}, 1, nil, nil) } func (s *Stream) SetPts(expr string) *Node { - AssetType(s.Type, "FilterableStream", "setpts") + AssertType(s.Type, "FilterableStream", "setpts") return NewFilterNode("setpts", []*Stream{s}, 1, []string{expr}, nil) } func (s *Stream) Trim(kwargs ...KwArgs) *Stream { - AssetType(s.Type, "FilterableStream", "trim") + AssertType(s.Type, "FilterableStream", "trim") return NewFilterNode("trim", []*Stream{s}, 1, nil, MergeKwArgs(kwargs)).Stream("", "") } func (s *Stream) Overlay(overlayParentNode *Stream, eofAction string, kwargs ...KwArgs) *Stream { - AssetType(s.Type, "FilterableStream", "overlay") + AssertType(s.Type, "FilterableStream", "overlay") if eofAction == "" { eofAction = "repeat" } @@ -55,24 +55,24 @@ func (s *Stream) Overlay(overlayParentNode *Stream, eofAction string, kwargs ... } func (s *Stream) HFlip(kwargs ...KwArgs) *Stream { - AssetType(s.Type, "FilterableStream", "hflip") + AssertType(s.Type, "FilterableStream", "hflip") return NewFilterNode("hflip", []*Stream{s}, 1, nil, MergeKwArgs(kwargs)).Stream("", "") } func (s *Stream) VFlip(kwargs ...KwArgs) *Stream { - AssetType(s.Type, "FilterableStream", "vflip") + AssertType(s.Type, "FilterableStream", "vflip") return NewFilterNode("vflip", []*Stream{s}, 1, nil, MergeKwArgs(kwargs)).Stream("", "") } func (s *Stream) Crop(x, y, w, h int, kwargs ...KwArgs) *Stream { - AssetType(s.Type, "FilterableStream", "crop") + AssertType(s.Type, "FilterableStream", "crop") return NewFilterNode("crop", []*Stream{s}, 1, []string{ strconv.Itoa(w), strconv.Itoa(h), strconv.Itoa(x), strconv.Itoa(y), }, MergeKwArgs(kwargs)).Stream("", "") } func (s *Stream) DrawBox(x, y, w, h int, color string, thickness int, kwargs ...KwArgs) *Stream { - AssetType(s.Type, "FilterableStream", "drawbox") + AssertType(s.Type, "FilterableStream", "drawbox") args := MergeKwArgs(kwargs) if thickness != 0 { args["t"] = thickness @@ -83,7 +83,7 @@ func (s *Stream) DrawBox(x, y, w, h int, color string, thickness int, kwargs ... } func (s *Stream) Drawtext(text string, x, y int, escape bool, kwargs ...KwArgs) *Stream { - AssetType(s.Type, "FilterableStream", "drawtext") + AssertType(s.Type, "FilterableStream", "drawtext") args := MergeKwArgs(kwargs) if escape { text = fmt.Sprintf("%q", text) @@ -119,17 +119,17 @@ func (s *Stream) Concat(streams []*Stream, kwargs ...KwArgs) *Stream { } func (s *Stream) ZoomPan(kwargs ...KwArgs) *Stream { - AssetType(s.Type, "FilterableStream", "zoompan") + AssertType(s.Type, "FilterableStream", "zoompan") return NewFilterNode("zoompan", []*Stream{s}, 1, nil, MergeKwArgs(kwargs)).Stream("", "") } func (s *Stream) Hue(kwargs ...KwArgs) *Stream { - AssetType(s.Type, "FilterableStream", "hue") + AssertType(s.Type, "FilterableStream", "hue") return NewFilterNode("hue", []*Stream{s}, 1, nil, MergeKwArgs(kwargs)).Stream("", "") } // todo fix this func (s *Stream) ColorChannelMixer(kwargs ...KwArgs) *Stream { - AssetType(s.Type, "FilterableStream", "colorchannelmixer") + AssertType(s.Type, "FilterableStream", "colorchannelmixer") return NewFilterNode("colorchannelmixer", []*Stream{s}, 1, nil, MergeKwArgs(kwargs)).Stream("", "") } diff --git a/view.go b/view.go index 8eb2bf7..9c1cbce 100644 --- a/view.go +++ b/view.go @@ -81,6 +81,7 @@ func visualizeForMermaidAsFlowChart(s *Stream) (string, error) { next := outGoingMap[node.Hash()] for k, v := range next { for _, nextNode := range v { + // todo ignore merged output label := string(k) if label == "" { label = "<>"