Powered by AppSignal & Oban Pro

Membrane YOLO Plugin demos

examples/yolo.livemd

Membrane YOLO Plugin demos

hardware_acceleration =
  case :os.type() do
    {:unix, :darwin} -> :coreml
    {:unix, :linux} -> :cuda
  end

with {:unix, :darwin} <- :os.type() do
  System.put_env("PATH", "/opt/homebrew/bin:#{System.get_env("PATH")}")
end

Mix.install(
  [
    {:membrane_yolo_plugin, "~> 0.1.0"},
    {:membrane_core, "~> 1.0"},
    {:membrane_camera_capture_plugin, "~> 0.7.4"},
    {:membrane_ffmpeg_swscale_plugin, "~> 0.16.3"},
    {:boombox, "~> 0.2.8"},
    {:exla, "~> 0.10"},
    {:kino, "~> 0.18"}
  ],
  config: [
    ortex: [
      {Ortex.Native, [features: [hardware_acceleration]]}
    ],
    nx: [
      default_backend: EXLA.Backend
    ]
  ]
)

Logger.configure(level: :info)

Download fixtures and model

tmp_dir = System.tmp_dir!() |> Path.join("membrane_yolo_plugin")
File.mkdir(tmp_dir)

model_name = "yolox_l.onnx"
model_path = tmp_dir |> Path.join(model_name)

if not File.exists?(model_path) do
  model_url =
    "https://github.com/Megvii-BaseDetection/YOLOX/releases/download/0.1.1rc0/#{model_name}"

  %{body: data} = Req.get!(model_url)
  File.write!(model_path, data)
end

fixtures_url =
  "https://raw.githubusercontent.com/membraneframework/membrane_yolo_plugin/master/examples/fixtures"

long_mp4_name = "street.mp4"
long_mp4_path = tmp_dir |> Path.join(long_mp4_name)

if not File.exists?(long_mp4_name) do
  %{status: 200, body: data} = Req.get!("#{fixtures_url}/#{long_mp4_name}")
  File.write!(long_mp4_path, data)
end

short_mp4_name = "street_short.mp4"
short_mp4_path = tmp_dir |> Path.join(short_mp4_name)

if not File.exists?(short_mp4_path) do
  %{status: 200, body: data} = Req.get!("#{fixtures_url}/#{short_mp4_name}")
  File.write!(short_mp4_path, data)
end

classes_name = "coco_classes.json"
classes_path = tmp_dir |> Path.join(classes_name)

if not File.exists?(classes_path) do
  classes_url =   
    "https://raw.githubusercontent.com/membraneframework/membrane_yolo_plugin/master/examples/models/coco_classes.json"
  
  %{status: 200, body: data} = Req.get!(classes_url)
  File.write!(classes_path, data)
end

:ok

Live object detection on a stream from the local camera

Let’s define a Membrane Pipeline, that takes a video stream from the computer camera and performs live object detection…

defmodule YOLO.CameraCapture.Pipeline do
  use Membrane.Pipeline

  @impl true
  def handle_init(_ctx, _opts) do
    spec =
      child(:camera_capture, Membrane.CameraCapture)
      |> child(:swscale_converter, %Membrane.FFmpeg.SWScale.Converter{
        format: :RGB,
        output_width: 640
      })
      |> child(:yolo_detector, %Membrane.YOLO.Detector{
        mode: :live_low_latency,
        yolo_model:
          YOLO.load(
            model_impl: YOLO.Models.YOLOX,
            model_path: unquote(model_path),
            classes_path: unquote(classes_path),
            eps: [unquote(hardware_acceleration)]
          )
      })
      |> child(:yolo_drawer, Membrane.YOLO.Drawer)
      |> via_in(:input, options: [kind: :video])
      |> child(:boombox_sink, %Boombox.Bin{output: :player})

    {[spec: spec], %{}}
  end
end

… and run it. After up to few seconds a player showing the stream with bouncing boxes should appear. If it is not visible after few seconds, look for it in the dock.

{:ok, _supervisor, _pipeline} = Membrane.Pipeline.start_link(YOLO.CameraCapture.Pipeline, [])
Process.sleep(:infinity)

Live object detection on a MP4 file

We can also take a MP4 file and play it, performing live object detection in the same moment.

defmodule YOLO.MP4.LivePipeline do
  use Membrane.Pipeline

  @impl true
  def handle_init(_ctx, _opts) do
    spec =
      child(:mp4_source, %Boombox.Bin{input: unquote(long_mp4_path)})
      |> via_out(:output, options: [kind: :video])
      |> child(:transcoder, %Membrane.Transcoder{output_stream_format: Membrane.RawVideo})
      |> child(:swscale_converter, %Membrane.FFmpeg.SWScale.Converter{
        format: :RGB,
        output_width: 640
      })
      |> child(:realtimer, Membrane.Realtimer)
      |> child(:yolo_detector, %Membrane.YOLO.Detector{
        mode: :live,
        yolo_model:
          YOLO.load(
            model_impl: YOLO.Models.YOLOX,
            model_path: unquote(model_path),
            classes_path: unquote(classes_path),
            eps: [unquote(hardware_acceleration)]
          ),
        additional_latency: Membrane.Time.milliseconds(500)
      })
      |> child(:yolo_drawer, Membrane.YOLO.Drawer)
      |> via_in(:input, options: [kind: :video])
      |> child(:boombox_sink, %Boombox.Bin{output: :player})

    {[spec: spec], %{}}
  end

  @impl true
  def handle_child_notification(:processing_finished, :boombox_sink, _ctx, state) do
    {[terminate: :normal], state}
  end
end

If you won’t see a player displaying a modified stream, look for it in open apps on your computer. There is a chance it is opend, but didn’t pop up ;)

{:ok, supervisor, _pipeline} = Membrane.Pipeline.start_link(YOLO.MP4.LivePipeline, [])
Process.monitor(supervisor)

receive do
  {:DOWN, _ref, :process, _pid, :normal} -> :ok
  {:DOWN, _ref, :process, _pid, reason} -> reason
end

Offline object detection on a MP4 file

In both examples above, we have used Membrane.YOLO.LiveFilter. If you want to perform offline object detection, because you don’t need live performance, you can use Membrane.YOLO.OfflineFilter.

The pipeline below takes an MP4 file, performs object detection on each frame from this file and saves results in another MP4 file.

result_file = tmp_dir |> Path.join("street_with_bounding_boxes.mp4")

defmodule YOLO.MP4.OfflinePipeline do
  use Membrane.Pipeline
  require Membrane.Logger

  @impl true
  def handle_init(_ctx, _opts) do
    frame = Kino.Frame.new() |> Kino.render()
    Kino.Frame.render(frame, "Processed 0 ms of 10_000 ms of fixture video")

    spec =
      child(:mp4_source, %Boombox.Bin{input: unquote(short_mp4_path)})
      |> via_out(:output, options: [kind: :video])
      |> child(:transcoder, %Membrane.Transcoder{output_stream_format: Membrane.RawVideo})
      |> child(:rgb_converter, %Membrane.FFmpeg.SWScale.Converter{
        format: :RGB,
        output_width: 640
      })
      |> child(:yolo_detector, %Membrane.YOLO.Detector{
        mode: :offline,
        yolo_model:
          YOLO.load(
            model_impl: YOLO.Models.YOLOX,
            model_path: unquote(model_path),
            classes_path: unquote(classes_path),
            eps: [unquote(hardware_acceleration)]
          )
      })
      |> child(:yolo_drawer, Membrane.YOLO.Drawer)
      |> child(:debug_logger, %Membrane.Debug.Filter{
        handle_buffer: fn buffer ->
          pts_ms = Membrane.Time.as_milliseconds(buffer.pts, :round)

          Kino.Frame.render(
            frame,
            "Processed #{inspect(pts_ms)} ms of 10_000 ms of fixture video"
          )
        end
      })
      |> child(:i420_converter, %Membrane.FFmpeg.SWScale.Converter{
        format: :I420
      })
      |> via_in(:input, options: [kind: :video])
      |> child(:boombox_sink, %Boombox.Bin{output: unquote(result_file)})

    {[spec: spec], %{}}
  end

  @impl true
  def handle_child_notification(:processing_finished, :boombox_sink, _ctx, state) do
    {[terminate: :normal], state}
  end
end

We can now run this pipeline and wait until it finishes the processing.

{:ok, supervisor, _pipeline} = Membrane.Pipeline.start_link(YOLO.MP4.OfflinePipeline, [])
Process.monitor(supervisor)

receive do
  {:DOWN, _ref, :process, _pid, :normal} -> :ok
end

Now, let’s display the result stream using Boombox. If the player won’t pop up on its own, look for it in your dock.

Boombox.play(result_file)