Let me get the obvious out of the way first. Face detection on a Jetson is not new. NVIDIA ships samples for it, half the tutorials on the internet do it, and I am fairly sure somebody has done it on a Raspberry Pi wearing a HAT. So no, this is not a post about discovering face detection.
This is a post about my two Jetsons, an Orin Nano Super and an AGX Orin, that live headless on a shelf with no monitor, no keyboard, and no camera plugged into either of them. I had just finished moving the Nano to JetPack 7.2.1 and pruning both boards down to a lean server install (no desktop, no Docker, nothing starting at boot), and I wanted a small, honest, end-to-end test that they were alive and doing real work. A face detector is a good smoke test. It either draws a box on your nose or it does not.
The twist, and the part I actually cared about, is where the camera lives. It is the one in my MacBook Pro. The Jetson never sees a camera device. Frames leave the Mac's browser, ride over the USB-C cable that was already plugged in, and land on the Jetson as plain HTTP. The Jetson runs the model and sends back coordinates. That is the whole trick, and it is the reason I bothered.
USB as a network, not as a webcam
Plug a Jetson developer kit into a Mac over USB-C and it does not show up as a camera or a disk. It shows up as a network interface. NVIDIA's device-mode service brings up a small bridge on the board with a fixed private address, and macOS gets a matching “Linux for Tegra” interface on the same tiny subnet. So I can SSH to the board with no Wi-Fi, no router, and no idea which DHCP address it picked up today.
Once SSH works, the rest is old plumbing. A launcher on the Mac copies the app to the
board, starts a tiny Python HTTP server there that listens only on the Jetson's own
loopback, and opens an ssh -L tunnel so that
http://127.0.0.1:8765 on the Mac is that server. The browser
opens the page, and here is the part I like: because the page comes from localhost,
the browser treats it as a secure context and happily hands over
getUserMedia. The camera stays a Mac camera. The HTML just happens to be
served by a Jetson.
Mac camera
-> browser (getUserMedia, canvas, JPEG at quality 0.8, fit within 640x480)
-> POST /api/detect to http://127.0.0.1:8765
-> ssh -L tunnel over the USB-C cable
-> Jetson loopback server (OpenCV decode, longest side resized to 320)
-> YuNet through cv2.FaceDetectorYN on two CPU threads
<- JSON: boxes, five landmarks, confidence, timings
<- browser draws the result on the exact frame it sent
Nothing is queued. The browser sends one frame, waits for the answer, draws the boxes on that same frame, and only then grabs the next one, capped at 20 fps. If the board gets slow, the picture gets slower; it does not fall behind. A second tab hitting a busy detector gets an HTTP 429 and a polite message. I have seen too many “live” previews with boxes floating three frames behind the face to build another one.
The model, and why the CPU
The detector is YuNet, the 2023 ONNX model from the OpenCV Zoo. It is a 232 KB file that returns a rectangle, five landmarks (eyes, nose, mouth corners), and a score, and it runs through OpenCV's DNN module on the CPU. Yes, the CPU, on a board with a GPU and TensorRT sitting right there. That was deliberate. The Nano ships with NVIDIA's OpenCV 4.8.0 already installed, YuNet is tiny, and I wanted to see what the plain path gives me before touching CUDA. The server pins the model by SHA-256 and refuses to start on a mismatch, which sounds paranoid until the day a Git LFS pointer file shows up where the model should be.
On the Orin Nano Super, with the browser capturing at 640x480 and inference at 320x240, I watched 19.7 completed frames per second, 13 to 19 ms of inference on the board, and 27 to 43 ms round trips measured from the browser. That live run was on JetPack 6.2; after the 7.2.1 upgrade the fixture tests landed in the same place, with a median of 16.1 ms inference. On the AGX Orin over the same USB tunnel, inference came in at 12.9 to 16 ms with 24 to 28 ms round trips, though that was a fixture run, not the live camera. These are readings off my desk, not a benchmark, and I would be lying if I said the AGX was breaking a sweat. For a 320-pixel face detector on the CPU, the GPU is simply not the bottleneck. JPEG encoding and the trip over the cable account for most of the rest.
One face, five dots, and the numbers I actually care about in the sidebar. The kitchen is not part of the model.
Two Jetsons, two surprises
The Nano was the easy one. The AGX Orin on JetPack 7 (L4T R39.2.1) had two surprises
waiting. First, it ships with no Python OpenCV at all, and the apt package is 4.6, too
old for FaceDetectorYN. A pip opencv-python-headless 4.11
wheel in ~/.local, pinned with numpy<2 so it does not
fight the system NumPy, took care of that.
The second one was less obvious. macOS binds the AGX's USB gadget through NCM, which
shows up on the board as usb1. NVIDIA's device-mode start script
hardcodes usb0 in its ECM/NCM block, so usb1 never joins the
bridge and the Mac's “Linux for Tegra” port sits there saying
inactive. One line in that script, reading the interface name from the
gadget's configfs instead of assuming it, and the port came alive. I keep the original
next to it, because a JetPack update will happily overwrite the fix.
And one rule I learned by doing it wrong: never plug both Jetsons into the Mac at the same time. They both answer at the same fixed USB address. SSH host key aliases keep their trust records apart, but they cannot make two boards share one address.
What it is not
It is a detector, not a recognizer. There are no identities, no embeddings, no database, and nothing is written to disk on either machine; a frame lives in memory for the length of one request. The server binds only to loopback on the Jetson, the tunnel binds only to loopback on the Mac, and the server checks Host and Origin, caps bodies at 1 MB and decoded frames at 1280x720, and closes the connection after each request. It installs no service and touches nothing at boot. Close the launcher and the Jetson process exits with it, because the server watches its SSH stdin for EOF. That last one is my favorite line in the whole project.
What excites me here is not the box around my face. It is that a headless board on a shelf became a useful sidekick for my laptop over the one cable that was already plugged in, with no camera purchase, no Wi-Fi setup, and no cloud in the loop. The same shape, Mac captures, Jetson computes, browser shows, should work for anything that fits in a JPEG and comes back as JSON.
The code, the API table, the test suite, and the dated verification notes are in the nano-face repository. Next on my list is to move inference to TensorRT just to see whether it matters at this size (I suspect not), and then to try something heavier than a face detector now that the plumbing is boring and the cable is proven.
More to come :-)
No comments:
Post a Comment