use fuel_ir::Shape;
use fuel_tensor::lazy::Tensor;
use std::sync::Arc;
#[derive(Debug, Clone)]
pub struct YoloV8Config {
pub image_size: usize,
pub num_classes: usize,
pub reg_max: usize,
pub ch: [usize; 5],
pub backbone_c2f_n: [usize; 3],
pub backbone_c2f_n_p5: usize,
pub bn_eps: f64,
}
impl YoloV8Config {
pub fn v8n() -> Self {
Self {
image_size: 640,
num_classes: 80,
reg_max: 16,
ch: [16, 32, 64, 128, 256],
backbone_c2f_n: [1, 2, 2],
backbone_c2f_n_p5: 1,
bn_eps: 1e-3,
}
}
}
#[derive(Debug, Clone)]
pub struct CbsWeights {
pub conv_w: Arc<[f32]>,
pub bn_scale: Arc<[f32]>,
pub bn_shift: Arc<[f32]>,
}
impl CbsWeights {
pub fn fuse_bn(
conv_w: Arc<[f32]>,
bn_gamma: &[f32],
bn_beta: &[f32],
bn_mean: &[f32],
bn_var: &[f32],
eps: f64,
) -> Self {
let c_out = bn_gamma.len();
assert_eq!(bn_beta.len(), c_out);
assert_eq!(bn_mean.len(), c_out);
assert_eq!(bn_var.len(), c_out);
let mut scale = vec![0.0_f32; c_out];
let mut shift = vec![0.0_f32; c_out];
for i in 0..c_out {
let s = bn_gamma[i] / (bn_var[i] + eps as f32).sqrt();
scale[i] = s;
shift[i] = bn_beta[i] - bn_mean[i] * s;
}
Self {
conv_w,
bn_scale: Arc::from(scale),
bn_shift: Arc::from(shift),
}
}
pub fn with_scale_shift(
conv_w: Arc<[f32]>,
bn_scale: Arc<[f32]>,
bn_shift: Arc<[f32]>,
) -> Self {
Self {
conv_w,
bn_scale,
bn_shift,
}
}
}
#[derive(Debug, Clone)]
pub struct BottleneckWeights {
pub cv1: CbsWeights,
pub cv2: CbsWeights,
pub add_residual: bool,
}
#[derive(Debug, Clone)]
pub struct C2fWeights {
pub cv1: CbsWeights,
pub cv2: CbsWeights,
pub bottlenecks: Vec<BottleneckWeights>,
pub c_inner: usize,
}
#[derive(Debug, Clone)]
pub struct SppfWeights {
pub cv1: CbsWeights,
pub cv2: CbsWeights,
}
#[derive(Debug, Clone)]
pub struct DetectScaleWeights {
pub cls_cv1: CbsWeights,
pub cls_cv2: CbsWeights,
pub cls_out_w: Arc<[f32]>, pub cls_out_b: Arc<[f32]>, pub reg_cv1: CbsWeights,
pub reg_cv2: CbsWeights,
pub reg_out_w: Arc<[f32]>, pub reg_out_b: Arc<[f32]>, }
#[derive(Debug, Clone)]
pub struct YoloV8Weights {
pub stem: CbsWeights, pub down_p2: CbsWeights, pub c2f_p2: C2fWeights, pub down_p3: CbsWeights, pub c2f_p3: C2fWeights,
pub down_p4: CbsWeights, pub c2f_p4: C2fWeights,
pub down_p5: CbsWeights, pub c2f_p5: C2fWeights,
pub sppf: SppfWeights,
pub neck_up_p4: C2fWeights, pub neck_up_p3: C2fWeights, pub neck_down_p4: CbsWeights, pub neck_out_p4: C2fWeights, pub neck_down_p5: CbsWeights, pub neck_out_p5: C2fWeights, pub detect_s: DetectScaleWeights, pub detect_m: DetectScaleWeights, pub detect_l: DetectScaleWeights, }
fn per_channel_affine(
x: &Tensor,
scale: &Arc<[f32]>,
shift: &Arc<[f32]>,
c: usize,
h: usize,
w: usize,
) -> fuel_ir::error::Result<Tensor> {
let s = x
.const_f32_like(scale.clone(), Shape::from_dims(&[c]))?
.reshape(Shape::from_dims(&[1, c, 1, 1]))?
.broadcast_to(Shape::from_dims(&[1, c, h, w]))?;
let sh = x
.const_f32_like(shift.clone(), Shape::from_dims(&[c]))?
.reshape(Shape::from_dims(&[1, c, 1, 1]))?
.broadcast_to(Shape::from_dims(&[1, c, h, w]))?;
x.mul(&s)?.add(&sh)
}
fn cbs(
x: &Tensor,
cw: &CbsWeights,
c_in: usize,
c_out: usize,
k: usize,
s: usize,
p: usize,
groups: usize,
h_out: usize,
w_out: usize,
) -> fuel_ir::error::Result<Tensor> {
let w_t = x.const_f32_like(
cw.conv_w.clone(),
Shape::from_dims(&[c_out, c_in / groups, k, k]),
)?;
let conv = x.conv2d(&w_t, None, (s, s), (p, p), groups)?;
let affine = per_channel_affine(&conv, &cw.bn_scale, &cw.bn_shift, c_out, h_out, w_out)?;
Ok(affine.silu())
}
fn max_pool_s1_composed(
x: &Tensor,
c: usize,
h: usize,
w: usize,
k: usize,
p: usize,
) -> fuel_ir::error::Result<Tensor> {
assert_eq!(
k,
2 * p + 1,
"max_pool_s1_composed assumes padding = (k-1)/2"
);
let padded = pad_hw_zeros(x, c, h, w, p)?;
let mut acc: Option<Tensor> = None;
for ky in 0..k {
let row = padded.slice(2, ky, h)?;
for kx in 0..k {
let win = row.slice(3, kx, w)?;
acc = Some(match acc {
None => win,
Some(a) => a.maximum(&win)?,
});
}
}
Ok(acc.expect("max_pool_s1_composed: at least one tap"))
}
fn pad_hw_zeros(
x: &Tensor,
c: usize,
h: usize,
w: usize,
p: usize,
) -> fuel_ir::error::Result<Tensor> {
if p == 0 {
return Ok(x.clone());
}
let z_w = x.const_f32_like(vec![0.0_f32; c * h * p], Shape::from_dims(&[1, c, h, p]))?;
let x_wpad = z_w.concat(x, 3)?.concat(&z_w, 3)?;
let w_p = w + 2 * p;
let z_h = x.const_f32_like(
vec![0.0_f32; c * p * w_p],
Shape::from_dims(&[1, c, p, w_p]),
)?;
z_h.concat(&x_wpad, 2)?.concat(&z_h, 2)
}
fn bottleneck(
x: &Tensor,
bw: &BottleneckWeights,
c: usize,
h: usize,
w: usize,
) -> fuel_ir::error::Result<Tensor> {
let y = cbs(x, &bw.cv1, c, c, 3, 1, 1, 1, h, w)?;
let y = cbs(&y, &bw.cv2, c, c, 3, 1, 1, 1, h, w)?;
if bw.add_residual { x.add(&y) } else { Ok(y) }
}
fn c2f(
x: &Tensor,
cw: &C2fWeights,
c_in: usize,
c_out: usize,
h: usize,
w: usize,
) -> fuel_ir::error::Result<Tensor> {
let c = cw.c_inner; let expanded = cbs(x, &cw.cv1, c_in, 2 * c, 1, 1, 0, 1, h, w)?;
let a = expanded.slice(1, 0, c)?;
let b = expanded.slice(1, c, c)?;
let mut parts: Vec<Tensor> = vec![a, b.clone()];
let mut cur = b;
for bn in &cw.bottlenecks {
cur = bottleneck(&cur, bn, c, h, w)?;
parts.push(cur.clone());
}
let mut stacked = parts[0].clone();
for p in &parts[1..] {
stacked = stacked.concat(p, 1)?;
}
let merged_c = (2 + cw.bottlenecks.len()) * c;
cbs(&stacked, &cw.cv2, merged_c, c_out, 1, 1, 0, 1, h, w)
}
fn sppf(
x: &Tensor,
sw: &SppfWeights,
c_in: usize,
c_out: usize,
h: usize,
w: usize,
) -> fuel_ir::error::Result<Tensor> {
let c_mid = c_in / 2;
let y0 = cbs(x, &sw.cv1, c_in, c_mid, 1, 1, 0, 1, h, w)?;
let y1 = max_pool_s1_composed(&y0, c_mid, h, w, 5, 2)?;
let y2 = max_pool_s1_composed(&y1, c_mid, h, w, 5, 2)?;
let y3 = max_pool_s1_composed(&y2, c_mid, h, w, 5, 2)?;
let cat = y0.concat(&y1, 1)?.concat(&y2, 1)?.concat(&y3, 1)?;
cbs(&cat, &sw.cv2, 4 * c_mid, c_out, 1, 1, 0, 1, h, w)
}
fn upsample_nearest_2x(x: &Tensor, c: usize, h: usize, w: usize) -> fuel_ir::error::Result<Tensor> {
let x6 = x.reshape(Shape::from_dims(&[1, c, h, 1, w, 1]))?;
let x6 = x6.concat(&x6, 3)?;
let x6 = x6.concat(&x6, 5)?;
x6.reshape(Shape::from_dims(&[1, c, 2 * h, 2 * w]))
}
fn raw_conv(
x: &Tensor,
w: &Arc<[f32]>,
b: &Arc<[f32]>,
c_in: usize,
c_out: usize,
k: usize,
p: usize,
) -> fuel_ir::error::Result<Tensor> {
let w_t = x.const_f32_like(w.clone(), Shape::from_dims(&[c_out, c_in, k, k]))?;
let b_t = x.const_f32_like(b.clone(), Shape::from_dims(&[c_out]))?;
x.conv2d(&w_t, Some(&b_t), (1, 1), (p, p), 1)
}
fn detect_branch(
x: &Tensor,
dw: &DetectScaleWeights,
cfg: &YoloV8Config,
c_in: usize,
h: usize,
w: usize,
) -> fuel_ir::error::Result<(Tensor, Tensor)> {
let cls = cbs(x, &dw.cls_cv1, c_in, c_in, 3, 1, 1, 1, h, w)?;
let cls = cbs(&cls, &dw.cls_cv2, c_in, c_in, 3, 1, 1, 1, h, w)?;
let cls_logits = raw_conv(
&cls,
&dw.cls_out_w,
&dw.cls_out_b,
c_in,
cfg.num_classes,
1,
0,
)?;
let reg = cbs(x, &dw.reg_cv1, c_in, c_in, 3, 1, 1, 1, h, w)?;
let reg = cbs(®, &dw.reg_cv2, c_in, c_in, 3, 1, 1, 1, h, w)?;
let reg_logits = raw_conv(
®,
&dw.reg_out_w,
&dw.reg_out_b,
c_in,
4 * cfg.reg_max,
1,
0,
)?;
Ok((cls_logits, reg_logits))
}
fn dfl_decode(
reg_logits: &Tensor,
reg_max: usize,
n_anchors: usize,
) -> fuel_ir::error::Result<Tensor> {
let r = reg_max;
let y = reg_logits.reshape(Shape::from_dims(&[1, 4, r, n_anchors]))?;
let y = y.permute([0, 1, 3, 2_usize])?; let probs = y.softmax_last_dim()?;
let bins: Vec<f32> = (0..r).map(|i| i as f32).collect();
let bins_t = reg_logits
.const_f32_like(bins, Shape::from_dims(&[r]))?
.reshape(Shape::from_dims(&[1, 1, 1, r]))?
.broadcast_to(Shape::from_dims(&[1, 4, n_anchors, r]))?;
let weighted = probs.mul(&bins_t)?;
weighted.sum_dim(3) }
#[derive(Debug, Clone)]
pub struct YoloV8RawOutput {
pub cls_logits: Tensor,
pub reg_dists: Tensor,
pub strides: Vec<f32>,
pub grid_xy: Vec<f32>,
}
#[derive(Debug, Clone)]
pub struct YoloV8Model {
pub config: YoloV8Config,
pub weights: YoloV8Weights,
}
impl YoloV8Model {
pub fn forward(&self, image: &[f32]) -> fuel_ir::error::Result<YoloV8RawOutput> {
let cfg = &self.config;
let isize = cfg.image_size;
assert_eq!(
image.len(),
3 * isize * isize,
"forward: image wrong length"
);
let x = Tensor::from_f32(
image.to_vec(),
Shape::from_dims(&[1, 3, isize, isize]),
&fuel_tensor::Device::cpu(),
)?;
let (h1, w1) = (isize / 2, isize / 2);
let x = cbs(&x, &self.weights.stem, 3, cfg.ch[0], 3, 2, 1, 1, h1, w1)?;
let (h2, w2) = (isize / 4, isize / 4);
let x = cbs(
&x,
&self.weights.down_p2,
cfg.ch[0],
cfg.ch[1],
3,
2,
1,
1,
h2,
w2,
)?;
let x = c2f(&x, &self.weights.c2f_p2, cfg.ch[1], cfg.ch[1], h2, w2)?;
let (h3, w3) = (isize / 8, isize / 8);
let x = cbs(
&x,
&self.weights.down_p3,
cfg.ch[1],
cfg.ch[2],
3,
2,
1,
1,
h3,
w3,
)?;
let p3 = c2f(&x, &self.weights.c2f_p3, cfg.ch[2], cfg.ch[2], h3, w3)?;
let (h4, w4) = (isize / 16, isize / 16);
let x = cbs(
&p3,
&self.weights.down_p4,
cfg.ch[2],
cfg.ch[3],
3,
2,
1,
1,
h4,
w4,
)?;
let p4 = c2f(&x, &self.weights.c2f_p4, cfg.ch[3], cfg.ch[3], h4, w4)?;
let (h5, w5) = (isize / 32, isize / 32);
let x = cbs(
&p4,
&self.weights.down_p5,
cfg.ch[3],
cfg.ch[4],
3,
2,
1,
1,
h5,
w5,
)?;
let x = c2f(&x, &self.weights.c2f_p5, cfg.ch[4], cfg.ch[4], h5, w5)?;
let p5 = sppf(&x, &self.weights.sppf, cfg.ch[4], cfg.ch[4], h5, w5)?;
let up_p5 = upsample_nearest_2x(&p5, cfg.ch[4], h5, w5)?; let cat_p4 = up_p5.concat(&p4, 1)?; let n_up_p4 = c2f(
&cat_p4,
&self.weights.neck_up_p4,
cfg.ch[4] + cfg.ch[3],
cfg.ch[3],
h4,
w4,
)?;
let up_p4 = upsample_nearest_2x(&n_up_p4, cfg.ch[3], h4, w4)?;
let cat_p3 = up_p4.concat(&p3, 1)?; let n_up_p3 = c2f(
&cat_p3,
&self.weights.neck_up_p3,
cfg.ch[3] + cfg.ch[2],
cfg.ch[2],
h3,
w3,
)?;
let down_p4 = cbs(
&n_up_p3,
&self.weights.neck_down_p4,
cfg.ch[2],
cfg.ch[2],
3,
2,
1,
1,
h4,
w4,
)?;
let cat_d_p4 = down_p4.concat(&n_up_p4, 1)?; let n_out_p4 = c2f(
&cat_d_p4,
&self.weights.neck_out_p4,
cfg.ch[2] + cfg.ch[3],
cfg.ch[3],
h4,
w4,
)?;
let down_p5 = cbs(
&n_out_p4,
&self.weights.neck_down_p5,
cfg.ch[3],
cfg.ch[3],
3,
2,
1,
1,
h5,
w5,
)?;
let cat_d_p5 = down_p5.concat(&p5, 1)?; let n_out_p5 = c2f(
&cat_d_p5,
&self.weights.neck_out_p5,
cfg.ch[3] + cfg.ch[4],
cfg.ch[4],
h5,
w5,
)?;
let (cls_s, reg_s) =
detect_branch(&n_up_p3, &self.weights.detect_s, cfg, cfg.ch[2], h3, w3)?;
let (cls_m, reg_m) =
detect_branch(&n_out_p4, &self.weights.detect_m, cfg, cfg.ch[3], h4, w4)?;
let (cls_l, reg_l) =
detect_branch(&n_out_p5, &self.weights.detect_l, cfg, cfg.ch[4], h5, w5)?;
let flatten_positions =
|t: &Tensor, c: usize, h: usize, w: usize| -> fuel_ir::error::Result<Tensor> {
t.reshape(Shape::from_dims(&[1, c, h * w]))
};
let cls_s_f = flatten_positions(&cls_s, cfg.num_classes, h3, w3)?;
let cls_m_f = flatten_positions(&cls_m, cfg.num_classes, h4, w4)?;
let cls_l_f = flatten_positions(&cls_l, cfg.num_classes, h5, w5)?;
let cls_cat = cls_s_f.concat(&cls_m_f, 2)?.concat(&cls_l_f, 2)?;
let reg_ch = 4 * cfg.reg_max;
let reg_s_f = flatten_positions(®_s, reg_ch, h3, w3)?;
let reg_m_f = flatten_positions(®_m, reg_ch, h4, w4)?;
let reg_l_f = flatten_positions(®_l, reg_ch, h5, w5)?;
let reg_cat = reg_s_f.concat(®_m_f, 2)?.concat(®_l_f, 2)?;
let n_total = h3 * w3 + h4 * w4 + h5 * w5;
let reg_dists = dfl_decode(®_cat, cfg.reg_max, n_total)?;
let mut grid_xy = Vec::with_capacity(2 * n_total);
let mut strides = Vec::with_capacity(n_total);
for (h, w, s) in [(h3, w3, 8.0_f32), (h4, w4, 16.0_f32), (h5, w5, 32.0_f32)] {
for y in 0..h {
for x in 0..w {
grid_xy.push(x as f32 + 0.5);
grid_xy.push(y as f32 + 0.5);
strides.push(s);
}
}
}
Ok(YoloV8RawOutput {
cls_logits: cls_cat,
reg_dists,
strides,
grid_xy,
})
}
}
#[derive(Debug, Clone, Copy)]
pub struct Detection {
pub class_id: usize,
pub score: f32,
pub bbox: [f32; 4],
}
#[derive(Debug, Clone, Copy)]
pub struct NmsConfig {
pub score_threshold: f32,
pub iou_threshold: f32,
pub top_k: usize,
}
impl Default for NmsConfig {
fn default() -> Self {
Self {
score_threshold: 0.25,
iou_threshold: 0.45,
top_k: 300,
}
}
}
#[allow(clippy::needless_range_loop)]
pub fn decode_and_nms(
raw: &YoloV8RawOutput,
num_classes: usize,
nms: &NmsConfig,
) -> Vec<Detection> {
let cls = raw.cls_logits.realize_f32(); let reg = raw.reg_dists.realize_f32(); let n = raw.strides.len();
assert_eq!(cls.len(), num_classes * n);
assert_eq!(reg.len(), 4 * n);
assert_eq!(raw.grid_xy.len(), 2 * n);
let mut per_class_keep: Vec<Vec<(f32, [f32; 4])>> = vec![Vec::new(); num_classes];
for i in 0..n {
let stride = raw.strides[i];
let cx = raw.grid_xy[2 * i] * stride;
let cy = raw.grid_xy[2 * i + 1] * stride;
#[allow(clippy::erasing_op)] let l = reg[0 * n + i] * stride;
let t = reg[1 * n + i] * stride;
let r = reg[2 * n + i] * stride;
let b = reg[3 * n + i] * stride;
let bbox = [cx - l, cy - t, cx + r, cy + b];
for c in 0..num_classes {
let logit = cls[c * n + i];
let score = 1.0_f32 / (1.0 + (-logit).exp());
if score >= nms.score_threshold {
per_class_keep[c].push((score, bbox));
}
}
}
let mut out: Vec<Detection> = Vec::new();
for c in 0..num_classes {
let mut cand = std::mem::take(&mut per_class_keep[c]);
cand.sort_by(|a, b| b.0.partial_cmp(&a.0).unwrap_or(std::cmp::Ordering::Equal));
cand.truncate(nms.top_k);
let mut kept: Vec<(f32, [f32; 4])> = Vec::new();
for (s, box_) in cand {
if kept
.iter()
.any(|(_, kb)| iou_xyxy(&box_, kb) > nms.iou_threshold)
{
continue;
}
kept.push((s, box_));
}
for (s, box_) in kept {
out.push(Detection {
class_id: c,
score: s,
bbox: box_,
});
}
}
out.sort_by(|a, b| {
b.score
.partial_cmp(&a.score)
.unwrap_or(std::cmp::Ordering::Equal)
});
out
}
fn iou_xyxy(a: &[f32; 4], b: &[f32; 4]) -> f32 {
let ix1 = a[0].max(b[0]);
let iy1 = a[1].max(b[1]);
let ix2 = a[2].min(b[2]);
let iy2 = a[3].min(b[3]);
let iw = (ix2 - ix1).max(0.0);
let ih = (iy2 - iy1).max(0.0);
let inter = iw * ih;
let a_area = (a[2] - a[0]).max(0.0) * (a[3] - a[1]).max(0.0);
let b_area = (b[2] - b[0]).max(0.0) * (b[3] - b[1]).max(0.0);
let union = a_area + b_area - inter;
if union <= 0.0 { 0.0 } else { inter / union }
}
impl YoloV8Weights {
pub fn zeros(cfg: &YoloV8Config) -> Self {
let z = |n: usize| Arc::<[f32]>::from(vec![0.0_f32; n]);
let o = |n: usize| Arc::<[f32]>::from(vec![1.0_f32; n]);
let cbs_zero = |cin: usize, cout: usize, k: usize, groups: usize| -> CbsWeights {
CbsWeights::with_scale_shift(z(cout * (cin / groups) * k * k), o(cout), z(cout))
};
let bottleneck_zero = |c: usize, add_residual: bool| -> BottleneckWeights {
BottleneckWeights {
cv1: cbs_zero(c, c, 3, 1),
cv2: cbs_zero(c, c, 3, 1),
add_residual,
}
};
let c2f_zero = |c_in: usize, c_out: usize, n: usize, add_res: bool| -> C2fWeights {
let c = c_out / 2;
let cv1 = cbs_zero(c_in, 2 * c, 1, 1);
let merged = (2 + n) * c;
let cv2 = cbs_zero(merged, c_out, 1, 1);
let bottlenecks = (0..n).map(|_| bottleneck_zero(c, add_res)).collect();
C2fWeights {
cv1,
cv2,
bottlenecks,
c_inner: c,
}
};
let sppf_zero = |c: usize| -> SppfWeights {
SppfWeights {
cv1: cbs_zero(c, c / 2, 1, 1),
cv2: cbs_zero(2 * c, c, 1, 1),
}
};
let detect_zero = |c_in: usize| -> DetectScaleWeights {
DetectScaleWeights {
cls_cv1: cbs_zero(c_in, c_in, 3, 1),
cls_cv2: cbs_zero(c_in, c_in, 3, 1),
cls_out_w: z(cfg.num_classes * c_in),
cls_out_b: z(cfg.num_classes),
reg_cv1: cbs_zero(c_in, c_in, 3, 1),
reg_cv2: cbs_zero(c_in, c_in, 3, 1),
reg_out_w: z(4 * cfg.reg_max * c_in),
reg_out_b: z(4 * cfg.reg_max),
}
};
Self {
stem: cbs_zero(3, cfg.ch[0], 3, 1),
down_p2: cbs_zero(cfg.ch[0], cfg.ch[1], 3, 1),
c2f_p2: c2f_zero(cfg.ch[1], cfg.ch[1], cfg.backbone_c2f_n[0], true),
down_p3: cbs_zero(cfg.ch[1], cfg.ch[2], 3, 1),
c2f_p3: c2f_zero(cfg.ch[2], cfg.ch[2], cfg.backbone_c2f_n[1], true),
down_p4: cbs_zero(cfg.ch[2], cfg.ch[3], 3, 1),
c2f_p4: c2f_zero(cfg.ch[3], cfg.ch[3], cfg.backbone_c2f_n[2], true),
down_p5: cbs_zero(cfg.ch[3], cfg.ch[4], 3, 1),
c2f_p5: c2f_zero(cfg.ch[4], cfg.ch[4], cfg.backbone_c2f_n_p5, true),
sppf: sppf_zero(cfg.ch[4]),
neck_up_p4: c2f_zero(cfg.ch[4] + cfg.ch[3], cfg.ch[3], 1, false),
neck_up_p3: c2f_zero(cfg.ch[3] + cfg.ch[2], cfg.ch[2], 1, false),
neck_down_p4: cbs_zero(cfg.ch[2], cfg.ch[2], 3, 1),
neck_out_p4: c2f_zero(cfg.ch[2] + cfg.ch[3], cfg.ch[3], 1, false),
neck_down_p5: cbs_zero(cfg.ch[3], cfg.ch[3], 3, 1),
neck_out_p5: c2f_zero(cfg.ch[3] + cfg.ch[4], cfg.ch[4], 1, false),
detect_s: detect_zero(cfg.ch[2]),
detect_m: detect_zero(cfg.ch[3]),
detect_l: detect_zero(cfg.ch[4]),
}
}
}
impl YoloV8Weights {
pub fn load_from_mmapped(
_st: &fuel_loaders::safetensors::MmapedSafetensors,
_cfg: &YoloV8Config,
) -> fuel_ir::error::Result<Self> {
Err(fuel_ir::error::Error::Msg(
"YoloV8Weights::load_from_mmapped: detection-head + multi-scale \
C2f/SPPF naming pending; construct YoloV8Weights via the \
explicit struct literal or contribute the loader."
.to_string(),
)
.bt())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn v8n_config_dims() {
let cfg = YoloV8Config::v8n();
assert_eq!(cfg.ch, [16, 32, 64, 128, 256]);
assert_eq!(cfg.num_classes, 80);
assert_eq!(cfg.reg_max, 16);
}
#[test]
fn forward_shapes_synthetic_tiny() {
let mut cfg = YoloV8Config::v8n();
cfg.image_size = 64;
let weights = YoloV8Weights::zeros(&cfg);
let model = YoloV8Model {
config: cfg.clone(),
weights,
};
let image = vec![0.0_f32; 3 * cfg.image_size * cfg.image_size];
let raw = model.forward(&image).unwrap();
let cls = raw.cls_logits.realize_f32();
let reg = raw.reg_dists.realize_f32();
let n_expected = (64 / 8) * (64 / 8) + (64 / 16) * (64 / 16) + (64 / 32) * (64 / 32);
assert_eq!(n_expected, 84);
assert_eq!(cls.len(), cfg.num_classes * n_expected);
assert_eq!(reg.len(), 4 * n_expected);
assert_eq!(raw.strides.len(), n_expected);
assert_eq!(raw.grid_xy.len(), 2 * n_expected);
assert!(cls.iter().all(|v| v.is_finite()));
assert!(reg.iter().all(|v| v.is_finite()));
let cls_ref = raw.cls_logits.realize_f32();
let reg_ref = raw.reg_dists.realize_f32();
fuel_tensor::test_utils::assert_allclose_f32(&cls, &cls_ref, 1e-4, 1e-3);
fuel_tensor::test_utils::assert_allclose_f32(®, ®_ref, 1e-4, 1e-3);
}
#[test]
fn iou_xyxy_matches_known_values() {
let a = [0.0, 0.0, 1.0, 1.0];
let b = [0.5, 0.5, 1.5, 1.5];
let iou = iou_xyxy(&a, &b);
assert!((iou - 0.25 / 1.75).abs() < 1e-6);
let c = [2.0, 2.0, 3.0, 3.0];
assert_eq!(iou_xyxy(&a, &c), 0.0);
}
#[test]
fn nms_dedupes_overlapping_boxes() {
let reg: Vec<f32> = vec![1.0_f32; 16]; let raw = YoloV8RawOutput {
cls_logits: Tensor::from_f32(
vec![5.0_f32, 5.0, 5.0, 5.0],
Shape::from_dims(&[1, 1, 4]),
&fuel_tensor::Device::cpu(),
)
.unwrap(),
reg_dists: Tensor::from_f32(
reg,
Shape::from_dims(&[1, 4, 4]),
&fuel_tensor::Device::cpu(),
)
.unwrap(),
strides: vec![8.0; 4],
grid_xy: vec![10.0, 10.0, 10.0, 10.0, 10.0, 10.0, 10.0, 10.0],
};
let nms = NmsConfig {
score_threshold: 0.5,
iou_threshold: 0.45,
top_k: 300,
};
let dets = decode_and_nms(&raw, 1, &nms);
assert_eq!(dets.len(), 1, "NMS should collapse 4 identical boxes to 1");
assert_eq!(dets[0].class_id, 0);
assert!(dets[0].score > 0.99);
}
}