1use scirs2_core::ndarray::Array2;
4
5use crate::error::{Result, VisionError};
6
7#[derive(Debug, Clone)]
13struct Conv2D {
14 kernel: Array2<f64>,
16 bias: Vec<f64>,
18 kernel_size: usize,
20 stride: usize,
22 in_channels: usize,
24 out_channels: usize,
26}
27
28impl Conv2D {
29 fn new(in_channels: usize, out_channels: usize, kernel_size: usize, stride: usize) -> Self {
30 let k2 = kernel_size * kernel_size;
31 let fan_in = in_channels * k2;
32 let scale = (2.0 / fan_in as f64).sqrt();
33 let mut kernel = Array2::zeros((out_channels, in_channels * k2));
34 for i in 0..out_channels {
35 for j in 0..in_channels * k2 {
36 kernel[[i, j]] = ((i * 7 + j * 13 + 5) as f64).sin() * scale;
37 }
38 }
39 Self {
40 kernel,
41 bias: vec![0.0; out_channels],
42 kernel_size,
43 stride,
44 in_channels,
45 out_channels,
46 }
47 }
48
49 fn forward(
54 &self,
55 input: &Array2<f64>,
56 h: usize,
57 w: usize,
58 ) -> Result<(Array2<f64>, usize, usize)> {
59 if input.nrows() != h * w {
60 return Err(VisionError::DimensionMismatch(format!(
61 "Conv2D: input rows {} != h*w {}",
62 input.nrows(),
63 h * w
64 )));
65 }
66 if input.ncols() != self.in_channels {
67 return Err(VisionError::DimensionMismatch(format!(
68 "Conv2D: input cols {} != in_channels {}",
69 input.ncols(),
70 self.in_channels
71 )));
72 }
73
74 let pad = self.kernel_size / 2;
75 let h_out = (h + 2 * pad - self.kernel_size) / self.stride + 1;
76 let w_out = (w + 2 * pad - self.kernel_size) / self.stride + 1;
77
78 let mut out = Array2::zeros((h_out * w_out, self.out_channels));
79
80 for oy in 0..h_out {
81 for ox in 0..w_out {
82 let out_idx = oy * w_out + ox;
83 for oc in 0..self.out_channels {
84 let mut val = self.bias[oc];
85 for ky in 0..self.kernel_size {
86 for kx in 0..self.kernel_size {
87 let iy = oy * self.stride + ky;
88 let ix = ox * self.stride + kx;
89 let iy_s = iy as isize - pad as isize;
91 let ix_s = ix as isize - pad as isize;
92 if iy_s < 0 || iy_s >= h as isize || ix_s < 0 || ix_s >= w as isize {
93 continue;
94 }
95 let in_idx = iy_s as usize * w + ix_s as usize;
96 for ic in 0..self.in_channels {
97 let k_idx = ic * self.kernel_size * self.kernel_size
98 + ky * self.kernel_size
99 + kx;
100 val += input[[in_idx, ic]] * self.kernel[[oc, k_idx]];
101 }
102 }
103 }
104 out[[out_idx, oc]] = val;
105 }
106 }
107 }
108
109 Ok((out, h_out, w_out))
110 }
111}
112
113fn relu_inplace(arr: &mut Array2<f64>) {
115 arr.mapv_inplace(|v| v.max(0.0));
116}
117
118#[derive(Debug, Clone)]
129pub struct SimpleBEVBackbone {
130 conv1: Conv2D,
131 conv2: Conv2D,
132 conv3: Conv2D,
133}
134
135impl SimpleBEVBackbone {
136 pub fn new(in_channels: usize) -> Self {
138 Self {
139 conv1: Conv2D::new(in_channels, 64, 3, 2),
140 conv2: Conv2D::new(64, 128, 3, 2),
141 conv3: Conv2D::new(128, 256, 3, 2),
142 }
143 }
144
145 pub fn forward(
150 &self,
151 bev_features: &Array2<f64>,
152 h: usize,
153 w: usize,
154 ) -> Result<Vec<(Array2<f64>, usize, usize)>> {
155 let (mut f1, h1, w1) = self.conv1.forward(bev_features, h, w)?;
156 relu_inplace(&mut f1);
157
158 let (mut f2, h2, w2) = self.conv2.forward(&f1, h1, w1)?;
159 relu_inplace(&mut f2);
160
161 let (mut f3, h3, w3) = self.conv3.forward(&f2, h2, w2)?;
162 relu_inplace(&mut f3);
163
164 Ok(vec![(f1, h1, w1), (f2, h2, w2), (f3, h3, w3)])
165 }
166}
167
168#[derive(Debug, Clone)]
175pub struct FeaturePyramidNeck {
176 lateral2: Conv2D,
178 lateral3: Conv2D,
179 out_channels: usize,
181}
182
183impl FeaturePyramidNeck {
184 pub fn new(out_channels: usize) -> Self {
186 Self {
188 lateral2: Conv2D::new(128, out_channels, 1, 1),
189 lateral3: Conv2D::new(256, out_channels, 1, 1),
190 out_channels,
191 }
192 }
193
194 pub fn forward(
200 &self,
201 scales: &[(Array2<f64>, usize, usize)],
202 ) -> Result<(Array2<f64>, usize, usize)> {
203 if scales.len() < 3 {
204 return Err(VisionError::InvalidParameter(
205 "FeaturePyramidNeck expects 3 scales".to_string(),
206 ));
207 }
208
209 let (ref f1, h1, w1) = scales[0];
210 let (ref f2, h2, w2) = scales[1];
211 let (ref f3, h3, w3) = scales[2];
212
213 let (l2, _, _) = self.lateral2.forward(f2, h2, w2)?;
215 let (l3, _, _) = self.lateral3.forward(f3, h3, w3)?;
216
217 let l3_up = nearest_upsample(&l3, h3, w3, h2, w2);
219 let fused2 = elementwise_add(&l3_up, &l2)?;
221
222 let fused2_up = nearest_upsample(&fused2, h2, w2, h1, w1);
224
225 let n1_cols = f1.ncols();
228 let mut out = Array2::zeros((h1 * w1, self.out_channels));
229 let copy_cols = n1_cols.min(self.out_channels);
230 for r in 0..h1 * w1 {
231 for c in 0..copy_cols {
232 out[[r, c]] = f1[[r, c]];
233 }
234 for c in 0..self.out_channels {
236 out[[r, c]] += fused2_up[[r, c]];
237 }
238 }
239
240 Ok((out, h1, w1))
241 }
242}
243
244fn nearest_upsample(
246 feat: &Array2<f64>,
247 h_in: usize,
248 w_in: usize,
249 h_out: usize,
250 w_out: usize,
251) -> Array2<f64> {
252 let c = feat.ncols();
253 let mut out = Array2::zeros((h_out * w_out, c));
254 for oy in 0..h_out {
255 let iy = (oy * h_in) / h_out.max(1);
256 let iy = iy.min(h_in.saturating_sub(1));
257 for ox in 0..w_out {
258 let ix = (ox * w_in) / w_out.max(1);
259 let ix = ix.min(w_in.saturating_sub(1));
260 let src = iy * w_in + ix;
261 let dst = oy * w_out + ox;
262 for k in 0..c {
263 out[[dst, k]] = feat[[src, k]];
264 }
265 }
266 }
267 out
268}
269
270fn elementwise_add(a: &Array2<f64>, b: &Array2<f64>) -> Result<Array2<f64>> {
272 if a.shape() != b.shape() {
273 return Err(VisionError::DimensionMismatch(format!(
274 "elementwise_add: shapes {:?} vs {:?}",
275 a.shape(),
276 b.shape()
277 )));
278 }
279 Ok(a + b)
280}
281
282#[cfg(test)]
287mod tests {
288 use super::*;
289 use scirs2_core::ndarray::Array2;
290
291 #[test]
292 fn conv2d_output_shape() {
293 let conv = Conv2D::new(4, 8, 3, 2);
294 let input = Array2::zeros((16 * 16, 4));
295 let (out, h, w) = conv.forward(&input, 16, 16).expect("conv forward");
296 assert_eq!(h, 8);
297 assert_eq!(w, 8);
298 assert_eq!(out.nrows(), 64);
299 assert_eq!(out.ncols(), 8);
300 }
301
302 #[test]
303 fn backbone_forward() {
304 let bb = SimpleBEVBackbone::new(4);
305 let input = Array2::zeros((16 * 16, 4));
306 let scales = bb.forward(&input, 16, 16).expect("backbone forward");
307 assert_eq!(scales.len(), 3);
308 assert_eq!(scales[0].1, 8);
310 assert_eq!(scales[1].1, 4);
311 assert_eq!(scales[2].1, 2);
312 }
313
314 #[test]
315 fn fpn_forward() {
316 let bb = SimpleBEVBackbone::new(4);
317 let input = Array2::zeros((16 * 16, 4));
318 let scales = bb.forward(&input, 16, 16).expect("backbone forward");
319 let fpn = FeaturePyramidNeck::new(64);
320 let (out, h, w) = fpn.forward(&scales).expect("fpn forward");
321 assert_eq!(h, 8);
322 assert_eq!(w, 8);
323 assert_eq!(out.ncols(), 64);
324 }
325
326 #[test]
327 fn nearest_upsample_identity() {
328 let feat = Array2::from_elem((4, 2), 1.0);
329 let up = nearest_upsample(&feat, 2, 2, 2, 2);
330 assert_eq!(up.shape(), &[4, 2]);
331 assert!((up[[0, 0]] - 1.0).abs() < 1e-12);
332 }
333}