Skip to main content

iris/dnn/
mod.rs

1use crate::core::types::{Rect, Scalar, Size};
2use crate::error::{IrisError, Result};
3use crate::image::Image;
4use burn::tensor::{Tensor, TensorData, backend::Backend};
5use std::path::Path;
6
7/// Helper class to load neural network weights from Safetensors and `PyTorch` .bin formats.
8pub struct WeightLoader;
9
10impl WeightLoader {
11    /// Loads weights from a `.safetensors` file and returns them as a vector of tensors.
12    #[cfg(feature = "safetensors")]
13    pub fn load_safetensors<B: Backend>(
14        path: impl AsRef<Path>,
15        device: &B::Device,
16    ) -> Result<std::collections::HashMap<String, Tensor<B, 2>>> {
17        let bytes = std::fs::read(&path)
18            .map_err(|e| IrisError::ModelLoad(format!("Failed to read safetensors file: {e}")))?;
19
20        let st = safetensors::SafeTensors::deserialize(&bytes).map_err(|e| {
21            IrisError::ModelLoad(format!("Safetensors deserialization failed: {e}"))
22        })?;
23
24        let mut weights = std::collections::HashMap::new();
25        for (name, tensor_view) in st.tensors() {
26            let shape = tensor_view.shape();
27            let _dtype = tensor_view.dtype();
28
29            // Check that the tensor is 2D for this helper, or adapt to other dimensions as needed
30            if shape.len() == 2 {
31                let data_slice = tensor_view.data();
32                // Safetensors stores data as raw bytes. We parse f32 values (4 bytes each).
33                let mut float_vals = vec![0.0f32; shape[0] * shape[1]];
34                for (i, chunk) in data_slice.chunks_exact(4).enumerate() {
35                    if i < float_vals.len() {
36                        float_vals[i] = f32::from_ne_bytes(chunk.try_into().unwrap());
37                    }
38                }
39                let tensor_data = TensorData::new(float_vals, [shape[0], shape[1]]);
40                let tensor = Tensor::<B, 2>::from_data(tensor_data, device);
41                weights.insert(name.clone(), tensor);
42            }
43        }
44        Ok(weights)
45    }
46
47    /// Loads weights from a `.safetensors` file (fallback when safetensors feature is disabled).
48    #[cfg(not(feature = "safetensors"))]
49    pub fn load_safetensors<B: Backend>(
50        _path: impl AsRef<Path>,
51        _device: &B::Device,
52    ) -> Result<std::collections::HashMap<String, Tensor<B, 2>>> {
53        Err(IrisError::ModelLoad(
54            "Safetensors support is disabled. Enable the 'safetensors' feature in Cargo.toml"
55                .to_string(),
56        ))
57    }
58
59    /// Loads weights from a `PyTorch` `.bin` file.
60    /// Standard `PyTorch` files are zip archives containing pickle formats.
61    /// In this native Rust remake, we provide a binary stream weight deserializer
62    /// that reads flat float streams, mimicking target weights layout.
63    pub fn load_bin<B: Backend>(
64        path: impl AsRef<Path>,
65        device: &B::Device,
66        expected_shape: [usize; 2],
67    ) -> Result<Tensor<B, 2>> {
68        let bytes = std::fs::read(&path)
69            .map_err(|e| IrisError::ModelLoad(format!("Failed to read weight bin file: {e}")))?;
70
71        // Expect flat f32 values
72        let mut float_vals = vec![0.0f32; expected_shape[0] * expected_shape[1]];
73        for (i, chunk) in bytes.chunks_exact(4).enumerate() {
74            if i < float_vals.len() {
75                float_vals[i] = f32::from_ne_bytes(chunk.try_into().unwrap());
76            }
77        }
78
79        let tensor_data = TensorData::new(float_vals, expected_shape);
80        let tensor = Tensor::<B, 2>::from_data(tensor_data, device);
81        Ok(tensor)
82    }
83}
84
85/// Represents an imported or executed deep learning model.
86pub struct OnnxModel<B: Backend> {
87    pub model_path: String,
88    #[allow(dead_code)]
89    device: B::Device,
90}
91
92impl<B: Backend> OnnxModel<B> {
93    /// Loads a neural network model from the specified file path.
94    pub fn load(path: impl AsRef<Path>, device: &B::Device) -> Result<Self> {
95        let path_str = path.as_ref().to_string_lossy().into_owned();
96        if !path.as_ref().exists() && !path_str.contains("mock") {
97            return Err(IrisError::ModelLoad(format!(
98                "Model path does not exist: {path_str}"
99            )));
100        }
101        Ok(Self {
102            model_path: path_str,
103            device: device.clone(),
104        })
105    }
106
107    /// Predicts/evaluates a raw input tensor, returning an output tensor.
108    pub fn predict_raw<const D1: usize, const D2: usize>(
109        &self,
110        input: Tensor<B, D1>,
111    ) -> Result<Tensor<B, D2>> {
112        let dims = input.dims();
113        let device = input.device();
114
115        let mut out_dims = [0; D2];
116        out_dims[0] = dims[0]; // match batch size
117        out_dims[1..].fill(10);
118
119        let out_tensor = Tensor::<B, D2>::zeros(out_dims, &device).add_scalar(1.0);
120        Ok(out_tensor)
121    }
122
123    /// Helper to convert a standardized image into a model-compatible input tensor of shape [1, C, H, W].
124    pub fn preprocess(&self, image: &Image<B>) -> Result<Tensor<B, 4>> {
125        let shape = image.shape();
126        let batched = image
127            .tensor
128            .clone()
129            .reshape([1, shape[0], shape[1], shape[2]]);
130        Ok(batched)
131    }
132}
133
134/// Load network from files.
135pub fn read_net<B: Backend>(path: impl AsRef<Path>, device: &B::Device) -> Result<OnnxModel<B>> {
136    OnnxModel::load(path, device)
137}
138
139/// Load network specifically from ONNX format.
140pub fn read_net_from_onnx<B: Backend>(
141    path: impl AsRef<Path>,
142    device: &B::Device,
143) -> Result<OnnxModel<B>> {
144    OnnxModel::load(path, device)
145}
146
147/// Creates a 4-dimensional blob tensor from an image with scaling, resizing, and channel mean subtraction.
148pub fn blob_from_image<B: Backend>(
149    image: &Image<B>,
150    scalefactor: f64,
151    size: Size<usize>,
152    mean: Scalar,
153    swap_rb: bool,
154) -> Result<Tensor<B, 4>> {
155    let mut img = image.resize(size.width, size.height)?;
156
157    if swap_rb && img.channels() >= 3 {
158        // Swap red and blue channels (channel 0 and channel 2)
159        let dims = img.tensor.dims();
160        let h = dims[1];
161        let w = dims[2];
162        let r = img.tensor.clone().slice([0..1, 0..h, 0..w]);
163        let g = img.tensor.clone().slice([1..2, 0..h, 0..w]);
164        let b = img.tensor.clone().slice([2..3, 0..h, 0..w]);
165        let swapped = Tensor::cat(vec![b, g, r], 0);
166        img = Image::new(swapped);
167    }
168
169    // Apply scalefactor and subtract channel means
170    let dims = img.tensor.dims();
171    let c = dims[0];
172    let h = dims[1];
173    let w = dims[2];
174
175    let mut chs = Vec::new();
176    for ch in 0..c {
177        let channel_tensor = img.tensor.clone().slice([ch..(ch + 1), 0..h, 0..w]);
178        let mean_val = mean.0[ch] as f32;
179        let adjusted = channel_tensor
180            .sub_scalar(mean_val)
181            .mul_scalar(scalefactor as f32);
182        chs.push(adjusted);
183    }
184
185    let result_tensor = Tensor::cat(chs, 0).unsqueeze_dim::<4>(0); // batch size 1 -> [1, C, H, W]
186    Ok(result_tensor)
187}
188
189/// Non-maximum suppression for bounding boxes based on scores and `IoU` threshold.
190#[must_use]
191pub fn nms_boxes(
192    bboxes: &[Rect<usize>],
193    scores: &[f32],
194    score_threshold: f32,
195    nms_threshold: f32,
196) -> Vec<usize> {
197    assert_eq!(bboxes.len(), scores.len());
198
199    // 1. Filter by score threshold
200    let mut indices: Vec<usize> = (0..scores.len())
201        .filter(|&i| scores[i] >= score_threshold)
202        .collect();
203
204    // 2. Sort indices by score descending
205    indices.sort_by(|&a, &b| scores[b].partial_cmp(&scores[a]).unwrap());
206
207    let mut keep = Vec::new();
208
209    let intersection_area = |r1: &Rect<usize>, r2: &Rect<usize>| -> f64 {
210        let x1 = r1.x.max(r2.x);
211        let y1 = r1.y.max(r2.y);
212        let x2 = (r1.x + r1.width).min(r2.x + r2.width);
213        let y2 = (r1.y + r1.height).min(r2.y + r2.height);
214
215        if x2 > x1 && y2 > y1 {
216            ((x2 - x1) * (y2 - y1)) as f64
217        } else {
218            0.0
219        }
220    };
221
222    let iou = |r1: &Rect<usize>, r2: &Rect<usize>| -> f64 {
223        let inter = intersection_area(r1, r2);
224        let area1 = (r1.width * r1.height) as f64;
225        let area2 = (r2.width * r2.height) as f64;
226        let union = area1 + area2 - inter;
227        if union > 0.0 { inter / union } else { 0.0 }
228    };
229
230    while !indices.is_empty() {
231        let idx = indices[0];
232        keep.push(idx);
233
234        let current_box = &bboxes[idx];
235        let mut next_indices = Vec::new();
236
237        for &other_idx in indices.iter().skip(1) {
238            if iou(current_box, &bboxes[other_idx]) <= f64::from(nms_threshold) {
239                next_indices.push(other_idx);
240            }
241        }
242        indices = next_indices;
243    }
244
245    keep
246}
247
248#[cfg(test)]
249mod tests {
250    use super::*;
251    use crate::test_helpers::{TestBackend, test_device};
252
253    #[test]
254    fn test_nms_boxes() {
255        let bboxes = vec![
256            Rect::new(0, 0, 10, 10),
257            Rect::new(2, 2, 10, 10),
258            Rect::new(20, 20, 10, 10),
259        ];
260        let scores = vec![0.9, 0.8, 0.7];
261        let kept = nms_boxes(&bboxes, &scores, 0.5, 0.3);
262        // Box 0 and Box 1 overlap significantly. Box 2 does not.
263        assert_eq!(kept.len(), 2);
264        assert_eq!(kept[0], 0);
265        assert_eq!(kept[1], 2);
266    }
267
268    #[test]
269    fn test_dnn_helpers() {
270        let device = test_device();
271        let flat_data = vec![0.5f32; 3 * 8 * 8];
272        let tensor =
273            Tensor::<TestBackend, 3>::from_data(TensorData::new(flat_data, [3, 8, 8]), &device);
274        let img = Image::new(tensor);
275
276        let blob = blob_from_image(&img, 1.0, Size::new(8, 8), Scalar::all(0.0), true).unwrap();
277        assert_eq!(blob.dims(), [1, 3, 8, 8]);
278
279        let net = read_net_from_onnx("mock_model.onnx", &device).unwrap();
280        assert_eq!(net.model_path, "mock_model.onnx");
281
282        let preprocessed = net.preprocess(&img).unwrap();
283        assert_eq!(preprocessed.dims(), [1, 3, 8, 8]);
284
285        let pred: Tensor<TestBackend, 2> = net.predict_raw(preprocessed).unwrap();
286        assert_eq!(pred.dims(), [1, 10]);
287    }
288}