Downloads · 30 days
63
57% of all-time downloads
CanelE452/pallet-pose-yolo26m-ft
pallet-pose-yolo26m-ft is a keypoint detection model from CanelE452. Use it for the keypoint detection task on the model card, and read the license before you ship it in a product. It is set up for ultralytics. The card lists the license as agpl-3.0.
Single-stage pallet 6-DoF pose model for forklift deployment. One RGB frame in, one bounding box + 9 keypoints out; a downstream PnP turns those keypoints into a 6-DoF pose (yaw / lateral offset / forward distance).
Downloads · 30 days
63
57% of all-time downloads
All-time downloads
111
Public
Repo size
47.6 MB
Likes
0
Public
Click a slice to open those files.
.pt47.6 MB · 100%
From the Hugging Face model README
Single-stage pallet 6-DoF pose model for forklift deployment. One RGB frame in, one bounding box + 9 keypoints out; a downstream PnP turns those keypoints into a 6-DoF pose (yaw / lateral offset / forward distance).
This is the medium variant (23.6M params). A smaller sibling trained on identical data with an identical recipe is at CanelE452/pallet-pose-yolo26n-ft (nano, 2.7M params) — see Which one should you use, because the medium is not clearly better.
Two contract details are not optional. Get either wrong and the model looks broken.
The model was trained on padded frames so that pallets clipped by the image border stay learnable. Match that at inference:
padded = cv2.copyMakeBorder(img, 100, 100, 100, 100, cv2.BORDER_REFLECT_101)
Subtract 100 from predicted coordinates before doing anything geometric (PnP, drawing).
Index 0-3 is always the face pointing at the camera — it is not tied to a fixed side of the physical pallet.
0 near_top_left 1 near_top_right <- face toward the camera (fork pockets)
2 near_bottom_right 3 near_bottom_left
4 far_top_left 5 far_top_right <- opposite face
6 far_bottom_right 7 far_bottom_left
8 centroid
top = {0, 1, 4, 5}
bottom = {2, 3, 6, 7}
Never enable horizontal flip augmentation if you finetune this further. The ordering is
left-right asymmetric; fliplr silently corrupts it. Training used fliplr=0.0.
If you do enable it, the correct mapping is flip_idx: [1, 0, 3, 2, 5, 4, 7, 6, 8]
(0↔1, 2↔3, 4↔5, 6↔7, centroid fixed) — the identity permutation is wrong.
import cv2, numpy as np
from ultralytics import YOLO
model = YOLO("pallet_yolo26m_pose_ft.pt")
PAD = 100
img = cv2.imread("frame.png") # 640x480 BGR
padded = cv2.copyMakeBorder(img, PAD, PAD, PAD, PAD, cv2.BORDER_REFLECT_101)
r = model.predict(padded, imgsz=640, conf=0.4, verbose=False)[0]
if r.boxes is not None and len(r.boxes):
i = int(np.argmax(r.boxes.conf.cpu().numpy())) # highest-confidence instance
kps = r.keypoints.xy.cpu().numpy()[i] - PAD # (9, 2) in original image coords
conf = float(r.boxes.conf.cpu().numpy()[i])
Feed the keypoints to PnP with the pallet's known 3-D corner model. In the reference
deployment: SOLVEPNP_SQPNP + refineLM, using only keypoints whose confidence ≥ 0.5.
Index 8 (centroid) is a real correspondence and may be included.
| setting | value |
|---|---|
imgsz | 640 |
conf | 0.4 (can go to 0.25, see Tuning) |
| padding | 100 px BORDER_REFLECT_101 |
| input | 640×480 BGR (deployment camera) |
The synthetic-only pretrain had a structural blind spot: all 73,916 training images contained a pallet, so the model had never seen a pallet-free scene. On deployment video it confidently labelled the forklift's own forks and a metal fence as pallets — both are horizontal-slat structures that genuinely resemble a pallet.
The fix was not more augmentation or class weighting. It was adding background images (empty labels) from the deployment camera itself.
Evaluated on a held-out real set of 161 manually annotated frames excluded from training, and on the 911-frame deployment sequence.
Detection rate on pallet-free frames, swept over confidence threshold (lower is better):
| model | @0.05 | @0.10 | @0.25 | @0.40 |
|---|---|---|---|---|
| synthetic-only pretrain (nano) | 50.6% | 30.5% | 0.0% | 0.0% |
| this model (medium ft) | 0.0% | 0.0% | 0.0% | 0.0% |
These 259 frames were part of the finetune set, so this figure is in-sample. The held-out numbers below are the honest ones.
| model | detection | keypoint err (median) | keypoint err (p90) |
|---|---|---|---|
| synthetic-only pretrain (nano) | 88.2% | 9.30 px | 28.41 px |
| nano finetune | 97.5% | 7.38 px | 26.75 px |
| this model (medium ft) | 96.9% | 6.51 px | 31.56 px |
Keypoint error is the median L2 distance over visible keypoints, in original-image pixels.
Paired comparison on the 153 held-out frames both models detect:
medium more accurate 87 frames
nano more accurate 66 frames
median difference -0.24 px (medium better)
mean difference +1.48 px (medium worse — driven by its tail)
Wilcoxon p = 0.14 -> NOT statistically significant
Error-tail breakdown:
| threshold | nano | medium |
|---|---|---|
| > 10 px | 50 | 42 |
| > 15 px | 28 | 26 |
| > 20 px | 19 | 21 |
| > 30 px | 11 | 15 |
The medium is better in the middle of the distribution and worse in the tail. It wins more frames than it loses, but not by a statistically significant margin.
Inference latency (RTX 3080, 640×640 padded, single frame):
| model | params | latency |
|---|---|---|
| nano | 2.7 M | 12.3 ms |
| medium | 23.6 M | 15.5 ms |
Recommendation: start with the nano — it is 8.7× smaller, has a tighter error tail, and the accuracy difference is not significant. Take the medium if your platform has room and you care about typical-case precision more than worst-case, or if you plan to finetune further on more data (the larger model has more headroom).
Both variants share these — they come from the data, not the architecture.
single_cls=True with one instance per image; the
reference pipeline takes the highest-confidence box. Multi-pallet scenes are untested.conf0.4 is the deployment default. Because the false-positive rate is 0% even at 0.05, you can
lower the threshold to 0.25 to recover some distant/clipped detections at little risk.
| base | yolo26m-pose (Ultralytics), then a synthetic-only pretrain (73,916 images) |
| finetune data | 16,694 images = 3,140 real (157 unique ×20) + 1,554 background (259 unique ×6) + 12,000 synthetic |
| schedule | 40 epochs, SGD, lr0=0.002, cos_lr, batch=32, nbs=64, imgsz=640, patience=0, seed=42 |
| augmentation | mosaic=0.15 (closed for last 10), translate=0.10, scale=0.25, HSV; all flips and rotations disabled |
| hardware | 2× GPU, DDP (device=0,1) |
Synthetic images are kept in the mix so the finetune does not forget the keypoint structure learned during pretraining — 157 real images alone are 0.2% of the pretrain set.
patience=0 disables early stopping deliberately: with the default patience the nano run
stopped at epoch 16 and saved epoch 1 as "best", because Ultralytics' fitness is computed on
the synthetic validation split.
The bundled validation set is synthetic, and its mAP does not predict real-world quality. On the nano variant, pushing synthetic pose mAP from 0.9448 to 0.9573 produced no measurable real improvement (paired Wilcoxon on the held-out set, p = 0.83). Judge changes on real data.
| file | description |
|---|---|
pallet_yolo26m_pose_ft.pt | the model (47.6 MB) |
inference_config.yaml | the inference contract as machine-readable config |
training_args.yaml | full Ultralytics hyperparameters |
SHA256SUMS | checksums |
AGPL-3.0, inherited from the Ultralytics YOLO base weights. If you deploy this over a network service, AGPL obligations apply to your application. For a commercial license, contact Ultralytics.