This post shows how to run RF-DETR inside NVIDIA DeepStream on a Jetson Orin NX. We cover what DeepStream and RF-DETR are, how to build a TensorRT engine and the parser DeepStream needs to read the model's output, how to run it on RTSP stream, and how to give each class its own box color.
We used NVIDIA's deepstream-import-vision-model agent skill to do most of the import for us. Every number below was measured on JetPack 6.2 with DeepStream 7.1 and TensorRT 10.3.
What Is DeepStream? #
DeepStream is NVIDIA's video analytics SDK, built on GStreamer. It gives you hardware-accelerated pipeline parts (decode, batching, TensorRT inference, on-screen display, encode) that keep frames in GPU memory the whole way through. The ones you will touch in this post:
nvstreammux
: batches N decoded streams into one GPU buffernvinfer
: runs a TensorRT engine and attaches detection metadata to each framenvdsosd
: draws boxes and labels from that metadata
*Keep in mind the inference engine doesn't know or care what your model's output format is; it hands you raw tensors and leaves the interpretation to you. That interpretation lives in a small parser function you write, and it's where most of these projects succeed or fail.
What Is RF-DETR? #
RF-DETR (GitHub) by Roboflow is a DETR-family detector, released under Apache 2.0. It uses 300 learned queries that look over the image features, and each query predicts one box on its own. So there is no anchor grid and no NMS step afterward. It ships in a few sizes; here I'll use RF-DETR Nano, which takes a 384x384 RGB image and puts out two tensors: 300 boxes (center x, center y, width, height, all scaled 0 to 1) and 300 x 91 class scores run through a sigmoid.
Getting the Weights and ONNX #
The rfdetr package downloads pretrained COCO weights the first time you use it, and exports a ready-to-deploy ONNX file for you:
pip install rfdetr
from rfdetr import RFDETRNano
model = RFDETRNano() # downloads pretrained weights
model.export( # writes an ONNX file
output_dir=".",
batch_size=1, # static batch, baked into the graph
)
The export is already simplified and uses a fixed batch size by default, which matters on Jetson. Building the TensorRT engine is one command (about two minutes on the Orin NX):
/usr/src/tensorrt/bin/trtexec \
--onnx=rfdetr-nano.onnx \
--fp16 \
--saveEngine=rfdetr_nano_b1.engine
Telling DeepStream What the Output Tensors Mean #
DeepStream doesn't know the output bytes are boxes, or how they are laid out, so you give it a parser function. DeepStream calls it with the raw output tensors for each frame, and it fills in detection structs (class id, left, top, width, height, confidence).
For RF-DETR, the post-processing steps are: run the class scores through a sigmoid, pick the best class for each query, and turn the center-form box into a corner-form box in pixels. Here is the core of it:
// called by DeepStream with the raw output tensors of one frame
extern "C" bool NvDsInferParseCustomRfdetr(...)
{
// find the two output layers by name
// ("dets"/"labels" from the rfdetr export)
const float *boxes = layer("dets"); // [300, 4] cx, cy, w, h in [0,1]
const float *logits = layer("labels"); // [300, 91] raw class logits
for (int q = 0; q < 300; q++) {
// best class for this query (skip index 0, the background slot)
int best = argmax(logits + q * 91);
float score = sigmoid(logits[q * 91 + best]);
if (score < threshold) continue;
NvDsInferObjectDetectionInfo obj = {}; // zero-init, always
obj.classId = best;
obj.detectionConfidence = score;
// normalized center-form to pixel corner-form
obj.width = boxes[q*4 + 2] * netW;
obj.height = boxes[q*4 + 3] * netH;
obj.left = boxes[q*4 + 0] * netW - obj.width / 2;
obj.top = boxes[q*4 + 1] * netH - obj.height / 2;
objectList.push_back(obj);
}
return true;
}
The parser gets compiled into a small shared library, and the config file tells the system where it is, plus it holds the preprocessing settings. But if you mess any of it up, you won't get any notification that something's wrong - you'll either see no detections or boxes in totally wrong spots.
[property]
onnx-file=rfdetr-nano.onnx
model-engine-file=rfdetr_nano_b1.engine
labelfile-path=labels.txt # 91 COCO class names, one per line
net-scale-factor=0.00392156862745098 # = 1/255
model-color-format=0 # RGB
maintain-aspect-ratio=0 # model squash-resizes to 384x384, no letterbox
cluster-mode=4 # DETR family: no NMS
network-mode=2 # FP16
infer-dims=3;384;384
num-detected-classes=91
custom-lib-path=libnvdsinfer_rfdetr_parser.so
parse-bbox-func-name=NvDsInferParseCustomRfdetr
[class-attrs-all]
pre-cluster-threshold=0.4 # confidence cutoff
Check the model's preprocessing settings instead of assuming ImageNet normalization. This export expects plain 0 to 1 input.
Running on a Live Camera #
Once the engine, parser, and config are in place, running a live camera takes a single deepstream-app
config file:
[application]
enable-perf-measurement=1 # prints per-stream FPS every few seconds
[source0]
enable=1
type=4 # 4 = RTSP source
uri=rtsp://user:pass@CAMERA_IP:554/stream1
latency=200 # jitter buffer, ms
[streammux]
batch-size=1 # match the engine's batch size
width=1280 # frames are scaled to this before inference
height=720
live-source=1
[primary-gie]
enable=1
config-file=config_infer_rfdetr.txt # the nvinfer config above
[sink0]
enable=1
type=1 # 1 = fakesink (headless); use type=4 to re-stream
sync=0
deepstream-app -c ds_app_rtsp.txt
A single 720p stream runs in real time with plenty of headroom on the Orin NX. If you want to record or re-stream the annotated output, the Orin NX has a hardware H.264 encoder (NVENC), so encoding stays on the GPU (nvv4l2h264enc
) instead of the CPU.
Per-Class Colors Need Code, Not Config #
There is no config key for coloring boxes by class. The color lives in per-object metadata, and you change it in a probe (a callback on the pipeline) right before the on-screen display draws the box. That means writing a small app of your own instead of using the stock binary. The Python bindings install as a wheel, with no root needed, and the whole thing is about 20 lines:
import colorsys, pyds
from gi.repository import Gst
PALETTE = [colorsys.hsv_to_rgb((i * 0.618) % 1.0, 0.85, 1.0) for i in range(91)]
def osd_probe(pad, info, _):
batch = pyds.gst_buffer_get_nvds_batch_meta(hash(info.get_buffer()))
l_frame = batch.frame_meta_list
while l_frame:
frame = pyds.NvDsFrameMeta.cast(l_frame.data)
l_obj = frame.obj_meta_list
while l_obj:
obj = pyds.NvDsObjectMeta.cast(l_obj.data)
r, g, b = PALETTE[obj.class_id % 91]
obj.rect_params.border_width = 3
obj.rect_params.border_color.set(r, g, b, 1.0) # box color
obj.text_params.set_bg_clr = 1
obj.text_params.text_bg_clr.set(r, g, b, 0.55) # label background
l_obj = l_obj.next
l_frame = l_frame.next
return Gst.PadProbeReturn.OK
osd.get_static_pad("sink").add_probe(Gst.PadProbeType.BUFFER, osd_probe, 0)
Attach that to the same pipeline, built in Python, and every class gets its own steady color, with the label background matched to the box.
Four streams batched through one RF-DETR Nano engine on a Jetson Orin NX, tiled 2x2 with the colored-box probe above, hardware-encoded and streamed off the device. Every detection here is the same class (car), so they share one color; the probe assigns a distinct hue per class id when the scene has more than one.
Measured Throughput #
Numbers from this setup: RF-DETR Nano, FP16, 384x384, on an Orin NX 16GB in MAXN mode. The TensorRT rows are trtexec on the engine alone; the DeepStream row is the full four-stream pipeline (decode, inference, on-screen display, and H.264 encode), with frames counted on the inference output.
| Metric | Orin NX 16GB (Nano, FP16, 384) |
|---|---|
| TensorRT, batch 1 | 132 img/s (7.6 ms latency) |
| TensorRT, batch 4 | 160 img/s (25 ms/batch) |
| DeepStream, 4x 720p RTSP at 30 fps | 30 fps/stream sustained (120 aggregate) |
| GPU utilization, 4-stream pipeline | ~97% |
| Peak RAM, 4-stream pipeline | 3.6 GB |
Batching from 1 to 4 lifts inference throughput about 20% (132 to 160 img/s), less than the gains you see with CNN detectors, because a detection transformer already keeps the GPU busy at batch 1. And the four live streams hold their full 30 fps with the GPU pinned near 97%: pure inference tops out around 160 img/s, and the rest of the budget goes to decoding four streams, tiling, drawing boxes, and H.264 encoding, all sharing the same GPU. So there's basically no room left to add a fifth 30 fps stream with this model on this device.
Conclusion #
We've done the full path from pretrained weights to live camera inference on a Jetson: export ONNX with the rfdetr
package, build an FP16 TRT engine, add the parser that tells DeepStream what the tensors mean, point deepstream-app at your camera, and add a Python probe when you want per-class colors. The DeepStream import skill does most of this for you.
RF-DETR is on GitHub under Apache 2.0. If you want to train one on your own data, the Custom Model Training guide covers that.
Cite this Post
Use the following entry to cite this post in your research:
Run RF-DETR in NVIDIA DeepStream on Jetson. Roboflow Blog: https://blog.roboflow.com/rf-detr-nvidia-deepstream/