1 use anyhow::{Error, Result};
2 use image::{DynamicImage, RgbImage};
3 use std::fs;
4 use wasi_nn::{self, ExecutionTarget, GraphBuilder, GraphEncoding};
5 
6 pub fn main() -> Result<(), Error> {
7     // Read the model file (Resnet18)
8     let model = fs::read("fixture/model.pt")?;
9     let graph = GraphBuilder::new(GraphEncoding::Pytorch, ExecutionTarget::CPU)
10         .build_from_bytes(&[&model])?;
11 
12     let mut context = graph.init_execution_context()?;
13 
14     let image = fs::read("fixture/kitten.png")?;
15     // Preprocessing. Normalize data based on model requirements https://github.com/onnx/models/tree/main/validated/vision/classification/mobilenet#preprocessing
16     let tensor_data = preprocess(
17         image.as_slice(),
18         224,
19         224,
20         &[0.485, 0.456, 0.406],
21         &[0.229, 0.224, 0.225],
22     );
23     let precision = wasi_nn::TensorType::F32;
24     // Resnet18 model input is NCHW
25     let shape = &[1, 3, 224, 224];
26     // Set the input tensor. PyTorch models do not use ports, so it is set to 0 here.
27     // Tensors are passed to the model, and the model's forward method processes these tensors.
28     context.set_input(0, precision, shape, &tensor_data)?;
29     context.compute()?;
30     let mut output_buffer = vec![0f32; 1000];
31     context.get_output(0, &mut output_buffer[..])?;
32     let result = softmax(output_buffer);
33     println!(
34         "Found results, sorted top 5: {:?}",
35         &sort_results(&result)[..5]
36     );
37     Ok(())
38 }
39 
40 // Resize image to height x width, and then converts the pixel precision to FP32, normalize with
41 // given mean and std. The resulting RGB pixel vector is then returned.
42 fn preprocess(image: &[u8], height: u32, width: u32, mean: &[f32], std: &[f32]) -> Vec<u8> {
43     let dyn_img: DynamicImage = image::load_from_memory(image).unwrap().resize_exact(
44         width,
45         height,
46         image::imageops::Triangle,
47     );
48     let rgb_img: RgbImage = dyn_img.to_rgb8();
49 
50     // Get an array of the pixel values
51     let raw_u8_arr: &[u8] = &rgb_img.as_raw()[..];
52 
53     // Create an array to hold the f32 value of those pixels
54     let bytes_required = raw_u8_arr.len() * 4;
55     let mut u8_f32_arr: Vec<u8> = vec![0; bytes_required];
56 
57     // Read the number as a f32 and break it into u8 bytes
58     for i in 0..raw_u8_arr.len() {
59         let u8_f32: f32 = raw_u8_arr[i] as f32;
60         let rgb_iter = i % 3;
61 
62         // Normalize the pixel
63         let norm_u8_f32: f32 = (u8_f32 / 255.0 - mean[rgb_iter]) / std[rgb_iter];
64 
65         // Convert it to u8 bytes and write it with new shape
66         let u8_bytes = norm_u8_f32.to_ne_bytes();
67         for j in 0..4 {
68             u8_f32_arr[(raw_u8_arr.len() * 4 * rgb_iter / 3) + (i / 3) * 4 + j] = u8_bytes[j];
69         }
70     }
71     u8_f32_arr
72 }
73 
74 fn softmax(output_tensor: Vec<f32>) -> Vec<f32> {
75     let max_val = output_tensor
76         .iter()
77         .cloned()
78         .fold(f32::NEG_INFINITY, f32::max);
79 
80     // Compute the exponential of each element subtracted by max_val for numerical stability.
81     let exps: Vec<f32> = output_tensor.iter().map(|&x| (x - max_val).exp()).collect();
82 
83     // Compute the sum of the exponentials.
84     let sum_exps: f32 = exps.iter().sum();
85 
86     // Normalize each element to get the probabilities.
87     exps.iter().map(|&exp| exp / sum_exps).collect()
88 }
89 
90 fn sort_results(buffer: &[f32]) -> Vec<InferenceResult> {
91     let mut results: Vec<InferenceResult> = buffer
92         .iter()
93         .enumerate()
94         .map(|(c, p)| InferenceResult(c, *p))
95         .collect();
96     results.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap());
97     results
98 }
99 
100 #[derive(Debug, PartialEq)]
101 struct InferenceResult(usize, f32);
102