Powered by AppSignal & Oban Pro

YOLOv8 on a Nerves device

remote_device_inference.livemd

YOLOv8 on a Nerves device

Setup

This notebook runs against a device already running the nerves_example firmware, with a Hailo accelerator and a camera attached. It drives the functions in NervesExample, so the code lives in the firmware and only the loop lives here.

Get the model onto the device first — see livebooks/download_models.livemd in the nx_hailo repository.

Then start distribution from the device's IEx prompt, over SSH:

cmd("epmd -daemon")
Node.start(:"nerves@<IP OF THE DEVICE>")
Node.set_cookie(:nerves_example_cookie)

Connect this notebook to it with Runtime → Attached Node, using that node name and cookie.

{:ok, {model, classes, input_name, output_name}} = NervesExample.load()
{capture, device} = NervesExample.find_capture()

input_shape = {trunc(capture.frame_height), trunc(capture.frame_width)}
padded_shape = {640, 640}

IO.puts("camera #{device}, frames of #{inspect(input_shape)}")

Inference loop

Each frame is padded to a square, resized to what the model takes, and run through the accelerator. Detections come back normalized against the padded square, so postprocess/2 maps them onto the camera frame before anything is drawn.

frame_interval_ms = 20
score_threshold = 0.5

Kino.animate(frame_interval_ms, fn _ ->
  frame = NervesExample.frame(capture)

  input_tensor =
    frame
    |> NervesExample.resize_and_pad(input_shape, padded_shape)
    |> Evision.Mat.to_nx()
    |> Nx.backend_transfer()

  {:ok, raw_detections} =
    NxHailo.infer(model, %{input_name => input_tensor}, NxHailo.Parsers.YoloV8,
      classes: classes,
      key: output_name
    )

  detections =
    raw_detections
    |> Enum.reject(&(&1.score < score_threshold))
    |> NxHailo.Parsers.YoloV8.postprocess(input_shape)

  NervesExample.YOLODraw.draw_detected_objects(
    frame,
    detections,
    "#{length(detections)} objects at #{div(1000, frame_interval_ms)} fps"
  )
end)