RT-DETR
A detection transformer built for real-time inference: it decodes a fixed set of queries rather than a dense grid, so it runs no NMS. LibreYOLO carries three versions of it, told apart by the checkpoint you load, and version 2 also serves oriented boxes.
- Tasks
- detection, oriented boxes
- Sizes
- rtdetr: r18, r34, r50, r50m, r101, l, x at 640 px; rtdetrv2: r18, r34, r50, r50m, r101 at 640 px; rtdetrv4: s, m, l, x at 640 px
- Install
pip install libreyolo- Support tier
- Core, since v1.1.0. Core trainable detectors: features follow the flagships in the same release wave.
- Upstream
- RT-DETR, RT-DETRv2 and RT-DETRv4 by Baidu (versions 1 and 2), Peking University and Tsinghua University (version 4), Apache-2.0. Paper, source
- Licenses
- Code Apache-2.0, weights Apache-2.0. Commercial use
Install
RT-DETR needs no optional extra. Everything it imports is in the base install,
and the rtdetr extra is a stable name that adds nothing to it.
pip install libreyoloAdapter fine-tuning with lora=True is the exception, and needs the lora
extra.
pip install "libreyolo[lora]"Predict
Weights download from Hugging Face on first use and are cached locally.
from libreyolo import LibreYOLO, SAMPLE_IMAGE model = LibreYOLO("LibreRTDETRr18.pt")result = model(SAMPLE_IMAGE, save=True) for box in result.boxes: print(box.cls, box.conf, box.xyxy)libreyolo predict model=LibreRTDETRr18.pt save=True \ source=https://raw.githubusercontent.com/LibreYOLO/libreyolo/release/libreyolo/assets/parkour.jpgfrom libreyolo import LibreYOLO # The version is part of the file name, and the factory routes on the# checkpoint, so all three load the same way.model = LibreYOLO("LibreRTDETRv4s.pt") # Any source the library accepts: file, folder, URL, webcam index,# RTSP stream, or a .streams listfor result in model.predict("clip.mp4", stream=True, save=True): print(len(result.boxes))from libreyolo import LibreYOLO # Version 2 only. The -obb suffix selects the task, and the checkpoint# is recognized as oriented from its own tensors, so no task argument# is needed. These weights are DOTA v1.0, 15 aerial classes at 1024 px.model = LibreYOLO("LibreRTDETRv2n-obb.pt")result = model("aerial.png", save=True) obb = result.obbprint(obb.xywhr) # (N, 5): cx, cy, w, h, radiansprint(obb.xyxyxyxy) # the same rows as four corner pointsprint(result.boxes.xyxy) # enclosing axis-aligned boxeslibreyolo predict model=LibreRTDETRv2n-obb.pt source=aerial.png save=TrueThe returned Results object is the one every family returns, so swapping in a
different detector is a one line change. conf and max_det filter a top-k
decode over queries and classes; there is no NMS step to tune, and iou is
accepted but unused. An oriented checkpoint fills result.obb natively and
also fills result.boxes with the enclosing axis-aligned rectangles. See
prediction for sources, streaming and result handling.
Variants
Three versions, two tasks between them, and the size codes do not run in a single series. Version 1 names its sizes after the backbone, ResNet or HGNetv2. Version 2 reuses the ResNet names only: version 1 already ships the two HGNetv2 sizes, and version 2's results there were close enough that LibreYOLO publishes no duplicate weights for them. Version 4 uses a plain letter series, which collides with version 1's HGNetv2 names, so a size code on its own does not identify a model. The version is written into the checkpoint file name.
| Checkpoint | Input (px) | mAP 50-95 | Params (M) |
|---|---|---|---|
| LibreRTDETRl | 640 | 55.8 | 32.93 |
| LibreRTDETRr101 | 640 | 56.8 | 76.56 |
| LibreRTDETRr18 | 640 | 49.7 | 20.18 |
| LibreRTDETRr34 | 640 | 52.2 | 31.44 |
| LibreRTDETRr50 | 640 | 55.9 | 42.89 |
| LibreRTDETRr50m | 640 | 53.8 | 36.59 |
| LibreRTDETRx | 640 | 57.9 | 67.37 |
| LibreRTDETRv2r101 | 640 | 56.8 | 76.56 |
| LibreRTDETRv2r18 | 640 | 50.8 | 20.18 |
| LibreRTDETRv2r34 | 640 | 53.2 | 31.44 |
| LibreRTDETRv2r50 | 640 | 55.7 | 42.89 |
| LibreRTDETRv2r50m | 640 | 54.8 | 36.59 |
| LibreRTDETRv4l | 640 | 57.8 | 31.24 |
| LibreRTDETRv4m | 640 | 56.5 | 19.59 |
| LibreRTDETRv4s | 640 | 52.8 | 10.32 |
| LibreRTDETRv4x | 640 | 60.0 | 62.62 |
COCO val2017, 500 images. Measured by the LibreYOLO benchmark harness and published on Vision Analysis, where latency across hardware and runtimes is compared and the full run records live.
Version 2 keeps version 1's architecture and state dict layout and changes how
the deformable attention samples, which is why the two are told apart by the
metadata in the checkpoint rather than by shape. Version 4 is a different
lineage: it reuses D-FINE's architecture and trainer, and its weights come from
distilling a DINOv3 vision foundation model teacher into an HGNetv2 student. In
LibreYOLO LibreRTDETRv4 is a subclass of LibreDFINE with the mask head
pinned off, so it stays detection only.
Oriented boxes on version 2
Version 2 is the one version that carries a second task. Its supported tasks
are detect and obb, and the two do not share a graph or a size series.
Detection uses the ResNet sizes at 640 px; oriented detection uses an HGNetv2
series, n, s, m, l and x, at 1024 px, and the input size resolves per task
rather than per family. A checkpoint is recognized as oriented from its own
tensors, by the five-coordinate box heads and the version 2 sampling
parameters, so -obb weights load into the oriented graph without a task
argument and a mismatch between the two is a hard error rather than a silent
reinterpretation.
The published files are LibreRTDETRv2n-obb.pt through
LibreRTDETRv2x-obb.pt. They are the official DOTA v1.0 single-scale
checkpoints converted into LibreYOLO's format, 15 aerial classes from plane and
ship through harbor and helicopter, and their class names are stamped into the
checkpoint. Unlike the detection side, the oriented task is inference only:
prediction, validation and export work, and train() on an oriented model
raises. Tracking and test-time augmentation do not support oriented boxes
either. Oriented detection covers the task,
the label format and the metrics.
Train
Training starts from a published checkpoint. pretrained is accepted and then
dropped on all three versions, so pretrained=False does not give you a
randomly initialized model. Everything in this section is about detection:
version 2's oriented task is inference only, and there is no transfer path from
detection weights to it, because the two use different backbones.
from libreyolo import LibreYOLO model = LibreYOLO("LibreRTDETRr18.pt") # coco128.yaml downloads a 128-image sample on first use. Point `data`# at your own dataset YAML for a real run.model.train(data="coco128.yaml", epochs=50, batch=4, lr0=1e-4)libreyolo train model=LibreRTDETRr18.pt data=coco128.yaml \ epochs=50 batch=4 lr0=1e-4# Needs the lora extra: pip install "libreyolo[lora]"from libreyolo import LibreYOLO model = LibreYOLO("LibreRTDETRr18.pt")model.train(data="coco128.yaml", epochs=50, lora=True)libreyolo train model=LibreRTDETRr18.pt data=coco128.yaml \ epochs=50 device=0,1Learning rate is the argument to get right, and each version carries its own
default rather than the library-wide one. The Python train() signature reads
it from that version's training config, and the CLI resolves the same value
when lr0 is not passed. Versions 1 and 2 also take lr_backbone and default
it to a twentieth of lr0, following the original recipe; version 4 runs
through the D-FINE trainer, which scales the backbone parameter group with
backbone_lr_mult instead.
Leave imgsz at the checkpoint's native size unless you have a reason to
change it. Validation and prediction at other sizes work, with one residual: a
rectangular size whose token count matches the native size still reuses an
embedding built for the wrong aspect ratio.
See training for datasets, augmentation, multi-GPU and loggers.
RT-DETRv4 enables amp=True with amp_dtype="float16" by default. Pass amp=False for FP32.
Validate
val() returns a dictionary of metrics/ keys covering precision, recall,
mAP 50 and mAP 50-95, measured against any dataset in the format you trained on.
from libreyolo import LibreYOLO model = LibreYOLO("LibreRTDETRr18.pt") # val() returns a plain dict, not an objectmetrics = model.val(data="coco128.yaml") print(metrics["metrics/mAP50-95"])print(metrics["metrics/mAP50"])print(metrics["metrics/precision"], metrics["metrics/recall"])libreyolo val model=LibreRTDETRr18.pt data=coco128.yaml# coco-val-only.yaml fetches the 5000 val2017 images and skips the# training set. It carries an embedded download script, so it needs# explicit permission unless the dataset is already local.libreyolo val model=LibreRTDETRr18.pt data=coco-val-only.yaml \ allow_download_scripts=Truefrom libreyolo import LibreYOLO # Oriented validation matches with rotated IoU, so a prediction in the# right place at the wrong angle counts as a miss.model = LibreYOLO("LibreRTDETRv2n-obb.pt")metrics = model.val(data="my-obb-dataset.yaml") print(metrics["metrics/mAP50-95(OBB)"])print(metrics["metrics/mAP50(OBB)"])The rows in the benchmark table above come from the LibreYOLO benchmark harness; the note under that table records which dataset produced them and links the run records.
Oriented validation runs through the same call and reports the same keys, plus
four repeated under an (OBB) suffix. Matching uses rotated IoU rather than
the IoU of the enclosing rectangles, so an angle error is a miss. augment=True
is rejected on this task.
Export
| Task | ONNX | TorchScript | ExecuTorch | TensorRT | OpenVINO | Paddle | MNN | RKNN | ncnn | TFLite | CoreML | Core AI |
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Detection | Detection to ONNX: supported | Detection to TorchScript: supported | Detection to ExecuTorch: supported | Detection to TensorRT: supported | Detection to OpenVINO: supported | Detection to Paddle: not supported | Detection to MNN: supported | Detection to RKNN: not supported | Detection to ncnn: not supported | Detection to TFLite: not supported | Detection to CoreML: not supported | Detection to Core AI: supported |
| Oriented boxes | Oriented boxes to ONNX: supported | Oriented boxes to TorchScript: supported | Oriented boxes to ExecuTorch: supported | Oriented boxes to TensorRT: supported | Oriented boxes to OpenVINO: supported | Oriented boxes to Paddle: not supported | Oriented boxes to MNN: not supported | Oriented boxes to RKNN: not supported | Oriented boxes to ncnn: not supported | Oriented boxes to TFLite: not supported | Oriented boxes to CoreML: not supported | Oriented boxes to Core AI: not supported |
The matrix covers the lineage as one page: where the three versions disagree about a format, the cell shows the weakest of the three, so nothing here is oversold for whichever version you load. The oriented row belongs to version 2 alone. ONNX and TorchScript are validated there, at FP32, batch 1 and a fixed 1024 by 1024 canvas; OpenVINO, TensorRT and ExecuTorch convert and reload but have not met raw-output parity across the full query set, so the top boxes agree to a fraction of a pixel while the tail drifts.
An exported artifact loads back through LibreYOLO() on its file suffix, so a
.onnx or .engine file behaves like a checkpoint and returns the same
Results.
# Needs the onnx extra: pip install "libreyolo[onnx]"from libreyolo import LibreYOLO model = LibreYOLO("LibreRTDETRr18.pt")path = model.export(format="onnx")print(path)libreyolo export model=LibreRTDETRr18.pt format=onnx# ONNX and TorchScript are the validated targets for the oriented task,# at FP32, batch 1, on a fixed 1024 by 1024 canvas.libreyolo export model=LibreRTDETRv2n-obb.pt format=onnx imgsz=1024libreyolo export model=LibreRTDETRv2n-obb.pt format=torchscript imgsz=1024from libreyolo import LibreYOLO, SAMPLE_IMAGE # The factory routes on the file suffix, so an exported artifact loads# like any checkpoint and returns the same Results object.model = LibreYOLO("LibreRTDETRr18.onnx")result = model(SAMPLE_IMAGE) print(result.boxes.xyxy)ONNX inputs and outputs
LibreRTDETR and LibreRTDETRv2
| Tensor | images float32 [batch, 3, height, width] |
|---|---|
| Color | RGB |
| Scaling | pixel / 255 |
| Normalization | none |
| Resize | stretch to width x height (no padding, aspect ratio not kept), bilinear |
Square input only. Export rejects a non-square imgsz, and height equals width, 640 by default.
LibreRTDETR also exports at other square sizes, such as 512. LibreRTDETRv2 exports at 640 only: any other imgsz fails during export with a tensor size mismatch.
Stretch the whole image to width x height with cv2.resize and INTER_LINEAR on the 8-bit RGB image. There is no letterbox and no padding, so the aspect ratio is not kept.
Then divide by 255. There is no mean or std normalization.
Detection, default export
| Output | Shape | Meaning |
|---|---|---|
pred_logits | [batch, queries, nc] | Raw class logits per query, sigmoid not applied. 300 queries and 80 classes for the COCO checkpoints. |
pred_boxes | [batch, queries, 4] | Boxes as cx, cy, w, h, normalized 0 to 1 of the input. |
Postprocessing
- Apply sigmoid to pred_logits.
- Flatten the scores to queries x nc and take the top max_det (300 by default) over all queries x classes. For flat index k, the query is k // nc and the class is k % nc. One query can yield several classes.
- Keep the entries whose score is strictly above the confidence threshold (0.25 by default). There is no NMS, so the IoU threshold is not used.
- Convert the query's box to x1, y1, x2, y2 with x1 = cx - w / 2, y1 = cy - h / 2, x2 = cx + w / 2, y2 = cy + h / 2.
- Map boxes back to the original image: multiply x by the original width and y by the original height. The input was stretched, so no pad offset applies. LibreYOLO does not clip these boxes.
- nms=True is not available for this family: export raises NotImplementedError.
LibreRTDETRv2 OBB
| Tensor | images float32 [batch, 3, height, width] |
|---|---|
| Color | RGB |
| Scaling | pixel / 255 |
| Normalization | none |
| Resize | custom, Pillow bilinear |
How to reproduce Pillow's bilinear resize
LibreYOLO resizes with Pillow's BILINEAR filter (Image.resize). It is not OpenCV INTER_LINEAR or INTER_AREA: when shrinking, Pillow widens the filter by the shrink factor, and OpenCV's resizes change detections and move boxes by several pixels. To reproduce it without Pillow, resize one axis, round to 8 bits, then resize the other axis and round again. Per axis, let s = in_size / out_size and f = max(s, 1). Output pixel i has center c = (i + 0.5) * s and reads source pixels j from int(c - f + 0.5) to int(c + f + 0.5) - 1, clipped to the image, each with weight max(0, 1 - |j + 0.5 - c| / f), divided by the sum of the weights. Pillow works in fixed point and picks the axis order from the image shape, so float weights can differ from it by one level on a few pixels. That does not change the results in our checks.
Square input only, 1024 x 1024. Export rejects a non-square imgsz, and any other square size fails during export with a tensor size mismatch.
Scale by r = min(height / h, width / w) and resize to (round(w * r), round(h * r)). This rounds to the nearest integer instead of truncating. An exact half goes to the even integer, as Python round does.
Paste the resized image in the top-left corner of a canvas filled with 0. The padding is on the right and bottom only.
On the test image, cv2.resize with INTER_LINEAR or INTER_AREA changes the detection count and moves boxes by more than 2 px.
Pillow computes each pass in fixed point: it rounds every weight to an integer with 22 fractional bits (round half away from zero), sums weight x pixel in integers, adds 2^21, shifts right by 22 and clips to 0 to 255.
Use that integer arithmetic for this model. Float weights differ from Pillow by a level on a few pixels, and on the test image that turns one box by 0.2 rad.
Divide the whole canvas by 255 after the resize. There is no mean or std normalization.
Oriented boxes, default export
| Output | Shape | Meaning |
|---|---|---|
pred_logits | [batch, queries, nc] | Raw class logits per query, sigmoid not applied. 300 queries and 15 DOTA classes for the released checkpoints. |
pred_boxes | [batch, queries, 5] | Rotated boxes as cx, cy, w, h, a. cx and w are normalized 0 to 1 of the input width, cy and h of the input height, both on the padded canvas. a is the angle divided by pi, 0 to 1. |
Postprocessing
- Apply sigmoid to pred_logits.
- Flatten the scores to queries x nc and take the top max_det (300 by default) over all queries x classes. For flat index k, the query is k // nc and the class is k % nc. One query can yield several classes.
- Keep the entries whose score is strictly above the confidence threshold (0.25 by default). There is no NMS, rotated or axis-aligned, so the IoU threshold is not used.
- Map boxes back to the original image: multiply cx and w by the input width, cy and h by the input height, then divide all four by r. The padding is on the right and bottom, so no offset applies. LibreYOLO does not clip these boxes.
- The angle in radians is a x pi. LibreYOLO reports each box as cx, cy, w, h, angle in original-image pixels and radians.
- The corners are (cx + dx cos(angle) - dy sin(angle), cy + dx sin(angle) + dy cos(angle)) for (dx, dy) = (-w / 2, -h / 2), (w / 2, -h / 2), (w / 2, h / 2), (-w / 2, h / 2). With y pointing down, a positive angle turns the box clockwise on screen.
- LibreYOLO's axis-aligned box for a detection is the smallest box that encloses its four corners.
- nms=True is not available for this family: export raises NotImplementedError.
LibreRTDETRv4
| Tensor | images float32 [batch, 3, height, width] |
|---|---|
| Color | RGB |
| Scaling | pixel / 255 |
| Normalization | none |
| Resize | stretch to width x height (no padding, aspect ratio not kept), Pillow bilinear |
How to reproduce Pillow's bilinear resize
LibreYOLO resizes with Pillow's BILINEAR filter (Image.resize). It is not OpenCV INTER_LINEAR or INTER_AREA: when shrinking, Pillow widens the filter by the shrink factor, and OpenCV's resizes change detections and move boxes by several pixels. To reproduce it without Pillow, resize one axis, round to 8 bits, then resize the other axis and round again. Per axis, let s = in_size / out_size and f = max(s, 1). Output pixel i has center c = (i + 0.5) * s and reads source pixels j from int(c - f + 0.5) to int(c + f + 0.5) - 1, clipped to the image, each with weight max(0, 1 - |j + 0.5 - c| / f), divided by the sum of the weights. Pillow works in fixed point and picks the axis order from the image shape, so float weights can differ from it by one level on a few pixels. That does not change the results in our checks.
Square input only. Export rejects a non-square imgsz, and height equals width, 640 by default. Other square sizes, such as 512, also export.
Batch size 1 only. The graph declares a dynamic batch axis, but batches of 2 or more fail in 1.7.0.
Divide by 255 after the resize. There is no mean or std normalization.
Detection, default export
| Output | Shape | Meaning |
|---|---|---|
pred_logits | [batch, queries, nc] | Raw class logits per query, sigmoid not applied. 300 queries and 80 classes for the COCO checkpoints. |
pred_boxes | [batch, queries, 4] | Boxes as cx, cy, w, h, normalized 0 to 1 of the input. |
Postprocessing
- Apply sigmoid to pred_logits.
- Flatten the scores to queries x nc and take the top max_det (300 by default) over all queries x classes. For flat index k, the query is k // nc and the class is k % nc. One query can yield several classes.
- Keep the entries whose score is strictly above the confidence threshold (0.25 by default). There is no NMS, so the IoU threshold is not used.
- Convert the query's box to x1, y1, x2, y2 with x1 = cx - w / 2, y1 = cy - h / 2, x2 = cx + w / 2, y2 = cy + h / 2.
- Map boxes back to the original image: multiply x by the original width and y by the original height. The input was stretched, so no pad offset applies. LibreYOLO does not clip these boxes.
- nms=True is not available for this family: export raises NotImplementedError.
To check your own implementation, run the same file through LibreYOLO: LibreYOLO("model.onnx").predict("image.jpg") returns the reference results. Verified with LibreYOLO 1.7.0.
Checkpoints
Every published weight file for this family.
| File | Input (px) | Weights license |
|---|---|---|
| Detection | ||
| LibreRTDETRr34.pt | 640 | apache-2.0 |
| LibreRTDETRr18.pt | 640 | apache-2.0 |
| LibreRTDETRr50.pt | 640 | apache-2.0 |
| LibreRTDETRr50m.pt | 640 | apache-2.0 |
| LibreRTDETRr101.pt | 640 | apache-2.0 |
| LibreRTDETRl.pt | 640 | apache-2.0 |
| LibreRTDETRx.pt | 640 | apache-2.0 |
| LibreRTDETRv2r18.pt | 640 | apache-2.0 |
| LibreRTDETRv2r34.pt | 640 | apache-2.0 |
| LibreRTDETRv2r50m.pt | 640 | apache-2.0 |
| LibreRTDETRv2r50.pt | 640 | apache-2.0 |
| LibreRTDETRv2r101.pt | 640 | apache-2.0 |
| LibreRTDETRv4s.pt | 640 | apache-2.0 |
| LibreRTDETRv4m.pt | 640 | apache-2.0 |
| LibreRTDETRv4l.pt | 640 | apache-2.0 |
| LibreRTDETRv4x.pt | 640 | apache-2.0 |
| Oriented boxes | ||
| LibreRTDETRv2n-obb.pt | apache-2.0 | |
| LibreRTDETRv2s-obb.pt | 640 | apache-2.0 |
| LibreRTDETRv2m-obb.pt | 640 | apache-2.0 |
| LibreRTDETRv2l-obb.pt | 640 | apache-2.0 |
| LibreRTDETRv2x-obb.pt | 640 | apache-2.0 |
Every file above exists in the LibreYOLO org today and downloads on first use.
The file name carries the version, then the size, then the task. Detection
weights are LibreRTDETR<size>.pt, LibreRTDETRv2<size>.pt and
LibreRTDETRv4<size>.pt, all at 640 px. Oriented weights exist for version 2
only and add the task suffix, LibreRTDETRv2n-obb.pt through
LibreRTDETRv2x-obb.pt, all at 1024 px and trained on DOTA v1.0 rather than
COCO.
Licensing
Check the license on the Hugging Face repository of the specific weights you download. Every checkpoint in the LibreYOLO org carries one, and they are not always the same across a family. That repository is the authoritative source; the summary below describes what applied when this page was last verified.
This is a description of the licenses involved, not legal advice. If the answer matters commercially, read the licenses yourself and take your own counsel.
- Original work
- RT-DETR, RT-DETRv2 and RT-DETRv4, Baidu (versions 1 and 2), Peking University and Tsinghua University (version 4)
- Upstream license
- Apache-2.0
- Upstream source
- github.com/lyuwenyu/RT-DETR
- LibreYOLO code
- MIT
- Weights
- Apache-2.0, republished at huggingface.co/LibreYOLO
- Interpretation
- Apache-2.0 is a permissive license, so these weights can be used in commercial and closed-source products. It asks you to keep its license text and attribution notices with any copy of the weights you redistribute, and it grants a patent license. It places no obligation on your own application code, and weights you train yourself on your own data are yours. All three versions carry it, across two repositories and three papers: RT-DETR and RT-DETRv2 at github.com/lyuwenyu/RT-DETR, and RT-DETRv4 at github.com/RT-DETRs/RT-DETRv4, which is cited separately (arXiv 2510.25257). RT-DETRv4 distills from a DINOv3 teacher while training only; the released student weights hold no DINOv3 parameters, and the tensors that fed the teacher are dropped when a checkpoint is converted, so Meta's DINOv3 license does not reach them.
Citation
@misc{lv2023detrs,
title={DETRs Beat YOLOs on Real-time Object Detection},
author={Yian Zhao and Wenyu Lv and Shangliang Xu and Jinman Wei and Guanzhong Wang and Qingqing Dang and Yi Liu and Jie Chen},
year={2023},
eprint={2304.08069},
archivePrefix={arXiv},
primaryClass={cs.CV}
}
@misc{lv2024rtdetrv2improvedbaselinebagoffreebies,
title={RT-DETRv2: Improved Baseline with Bag-of-Freebies for Real-Time Detection Transformer},
author={Wenyu Lv and Yian Zhao and Qinyao Chang and Kui Huang and Guanzhong Wang and Yi Liu},
year={2024},
eprint={2407.17140},
archivePrefix={arXiv},
primaryClass={cs.CV},
url={https://arxiv.org/abs/2407.17140},
}Copied from the authors' citation block at github.com/lyuwenyu/RT-DETR#citation.
The block above is what the authors publish for versions 1 and 2 detection. Version 2's oriented weights have a third upstream, the Apache-2.0 RiO-DETR repository at github.com/RicePasteM/RiO-DETR, which is where the DOTA checkpoints come from; cite that project if you used one. Version 4 is a separate paper by a different group and has its own citation block at github.com/RT-DETRs/RT-DETRv4; cite that one if you used a version 4 checkpoint.