vision_rs/models/yolo/loss/assign.rs
1/*
2 * Copyright 2026 Teenygrad
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17
18//! Simplified TaskAlignedAssigner (CPU-side).
19//!
20//! For each GT box, alignment scores are:
21//! score = cls_score ^ alpha * iou ^ beta
22//!
23//! The top-k anchors per GT are selected as positives. Conflicts (multiple GTs
24//! assigning the same anchor) are broken by highest alignment score.
25
26/// Result of assigning GT targets to anchors for a single image.
27pub struct AssignResult {
28 /// Whether each anchor is a positive. Shape: `[A]`.
29 pub is_positive: Vec<bool>,
30 /// GT box (cx, cy, w, h) assigned to each anchor (only valid for positives).
31 /// Shape: `[4, A]` channels-first.
32 pub target_boxes: Vec<f32>,
33 /// GT class index assigned to each anchor (only valid for positives).
34 /// Shape: `[A]`.
35 pub target_cls: Vec<usize>,
36 /// Soft target per anchor: `(align / max_align_for_gt) * max_iou_for_gt`.
37 ///
38 /// Zero for negatives. Used as the soft class label AND box loss weight,
39 /// matching the ultralytics E2ELoss normalisation. Shape: `[A]`.
40 pub soft_target: Vec<f32>,
41}
42
43/// Compute the IoU between a predicted XYWH box and a GT XYWH box.
44fn iou_xywh(p: [f32; 4], g: [f32; 4]) -> f32 {
45 let (px1, py1) = (p[0] - p[2] * 0.5, p[1] - p[3] * 0.5);
46 let (px2, py2) = (p[0] + p[2] * 0.5, p[1] + p[3] * 0.5);
47 let (gx1, gy1) = (g[0] - g[2] * 0.5, g[1] - g[3] * 0.5);
48 let (gx2, gy2) = (g[0] + g[2] * 0.5, g[1] + g[3] * 0.5);
49
50 let ix1 = px1.max(gx1);
51 let iy1 = py1.max(gy1);
52 let ix2 = px2.min(gx2);
53 let iy2 = py2.min(gy2);
54 let inter = (ix2 - ix1).max(0.0) * (iy2 - iy1).max(0.0);
55 let union = p[2] * p[3] + g[2] * g[3] - inter;
56 inter / (union + 1e-7)
57}
58
59/// TaskAlignedAssigner parameters.
60#[derive(Clone)]
61pub struct TaskAlignedAssigner {
62 /// Top-k anchors selected per GT.
63 pub top_k: usize,
64 /// Exponent on the predicted class confidence.
65 pub alpha: f32,
66 /// Exponent on the predicted IoU.
67 pub beta: f32,
68}
69
70impl Default for TaskAlignedAssigner {
71 fn default() -> Self {
72 Self { top_k: 8, alpha: 0.5, beta: 6.0 }
73 }
74}
75
76impl TaskAlignedAssigner {
77 /// Assign GT boxes to anchors for a **single image**.
78 ///
79 /// # Arguments
80 /// * `pred_xywh` – decoded anchor predictions `[4, A]` channels-first
81 /// * `pred_scores` – per-anchor class logits `[nc, A]` channels-first (sigmoid applied internally)
82 /// * `anchor_cx` – anchor centre x `[A]`
83 /// * `anchor_cy` – anchor centre y `[A]`
84 /// * `gt_boxes` – GT boxes `[[cx, cy, w, h]; M]`
85 /// * `gt_cls` – GT class indices `[M]`
86 pub fn assign(
87 &self,
88 pred_xywh: &[f32],
89 pred_scores: &[f32],
90 anchor_cx: &[f32],
91 anchor_cy: &[f32],
92 gt_boxes: &[[f32; 4]],
93 gt_cls: &[usize],
94 ) -> AssignResult {
95 let a = anchor_cx.len();
96 let m = gt_boxes.len();
97 let nc = pred_scores.len() / a;
98
99 let mut target_boxes = vec![0.0f32; 4 * a];
100 let mut target_cls = vec![0usize; a];
101 let mut soft_target = vec![0.0f32; a];
102 let mut assigned_score = vec![-1.0f32; a]; // -1 = unassigned
103 let mut assigned_gt = vec![0usize; a];
104 let mut is_positive = vec![false; a];
105
106 if m == 0 {
107 return AssignResult { is_positive, target_boxes, target_cls, soft_target };
108 }
109
110 // Pre-compute sigmoid of predicted class scores.
111 let sig: Vec<f32> = pred_scores.iter().map(|&x| 1.0 / (1.0 + (-x).exp())).collect();
112
113 // Per-GT tracking for soft-target normalisation.
114 let mut gt_max_align = vec![0.0f32; m]; // max alignment score among top-k
115 let mut gt_max_iou = vec![0.0f32; m]; // max IoU among top-k
116
117 // Flat list of (gt_idx, anchor_idx, align_score, iou) from top-k per GT.
118 let mut candidates: Vec<(usize, usize, f32, f32)> = Vec::new();
119
120 // Phase 1: per-GT top-k selection — collect candidates and track per-GT maxes.
121 for (gi, >_box) in gt_boxes.iter().enumerate() {
122 let gc = gt_cls[gi];
123 let [gx, gy, gw, gh] = gt_box;
124
125 let mut scores: Vec<(usize, f32, f32)> = (0..a).filter_map(|ai| {
126 let cx = anchor_cx[ai];
127 let cy = anchor_cy[ai];
128 if cx < gx - gw * 0.5 || cx > gx + gw * 0.5
129 || cy < gy - gh * 0.5 || cy > gy + gh * 0.5
130 {
131 return None;
132 }
133
134 let p = [pred_xywh[ai], pred_xywh[a+ai], pred_xywh[2*a+ai], pred_xywh[3*a+ai]];
135 let iou = iou_xywh(p, gt_box).max(0.0);
136 let cls_score = if gc < nc { sig[gc * a + ai] } else { 0.0 };
137 let align = cls_score.powf(self.alpha) * iou.powf(self.beta);
138 Some((ai, align, iou))
139 }).collect();
140
141 if scores.is_empty() { continue; }
142
143 scores.sort_unstable_by(|a, b| b.1.partial_cmp(&a.1).unwrap());
144 scores.truncate(self.top_k);
145
146 for &(ai, align, iou) in &scores {
147 if align > gt_max_align[gi] { gt_max_align[gi] = align; }
148 if iou > gt_max_iou[gi] { gt_max_iou[gi] = iou; }
149 candidates.push((gi, ai, align, iou));
150 }
151 }
152
153 // Phase 2: conflict resolution — highest alignment score wins — and box assignment.
154 for (gi, ai, align, _iou) in &candidates {
155 if *align > assigned_score[*ai] {
156 assigned_score[*ai] = *align;
157 assigned_gt[*ai] = *gi;
158 is_positive[*ai] = true;
159 target_cls[*ai] = gt_cls[*gi];
160 let gt_box = gt_boxes[*gi];
161 target_boxes[*ai] = gt_box[0];
162 target_boxes[a + *ai] = gt_box[1];
163 target_boxes[2*a + *ai] = gt_box[2];
164 target_boxes[3*a + *ai] = gt_box[3];
165 }
166 }
167
168 // Phase 3: soft target = (align / max_align_for_gt) * max_iou_for_gt.
169 const EPS: f32 = 1e-8;
170 for ai in 0..a {
171 if is_positive[ai] {
172 let gi = assigned_gt[ai];
173 let align = assigned_score[ai];
174 soft_target[ai] = (align / (gt_max_align[gi] + EPS)) * gt_max_iou[gi];
175 }
176 }
177
178 AssignResult { is_positive, target_boxes, target_cls, soft_target }
179 }
180}