fastembed/image_embedding/
impl.rs1#[cfg(feature = "hf-hub")]
2use hf_hub::api::sync::ApiRepo;
3use image::DynamicImage;
4use ndarray::{Array3, ArrayView3};
5use ort::{
6 session::{builder::GraphOptimizationLevel, Session},
7 value::Value,
8};
9#[cfg(feature = "hf-hub")]
10use std::path::PathBuf;
11use std::{io::Cursor, path::Path, thread::available_parallelism};
12
13use crate::{
14 common::normalize, models::image_embedding::models_list, Embedding, ImageEmbeddingModel,
15 ModelInfo,
16};
17use anyhow::anyhow;
18#[cfg(feature = "hf-hub")]
19use anyhow::Context;
20
21#[cfg(feature = "hf-hub")]
22use super::ImageInitOptions;
23use super::{
24 init::{ImageInitOptionsUserDefined, UserDefinedImageEmbeddingModel},
25 utils::{Compose, Transform, TransformData},
26 ImageEmbedding, DEFAULT_BATCH_SIZE,
27};
28
29impl ImageEmbedding {
30 #[cfg(feature = "hf-hub")]
36 pub fn try_new(options: ImageInitOptions) -> anyhow::Result<Self> {
37 let ImageInitOptions {
38 model_name,
39 execution_providers,
40 cache_dir,
41 show_download_progress,
42 } = options;
43
44 let threads = available_parallelism()?.get();
45
46 let model_repo = ImageEmbedding::retrieve_model(
47 model_name.clone(),
48 cache_dir.clone(),
49 show_download_progress,
50 )?;
51
52 let preprocessor_file = model_repo
53 .get("preprocessor_config.json")
54 .context("Failed to retrieve preprocessor_config.json")?;
55 let preprocessor = Compose::from_file(preprocessor_file)?;
56
57 let model_file_name = ImageEmbedding::get_model_info(&model_name).model_file;
58 let model_file_reference = model_repo
59 .get(&model_file_name)
60 .context(format!("Failed to retrieve {}", model_file_name))?;
61
62 let session = Session::builder()?
63 .with_execution_providers(execution_providers)?
64 .with_optimization_level(GraphOptimizationLevel::Level3)?
65 .with_intra_threads(threads)?
66 .commit_from_file(model_file_reference)?;
67
68 Ok(Self::new(preprocessor, session))
69 }
70
71 pub fn try_new_from_user_defined(
75 model: UserDefinedImageEmbeddingModel,
76 options: ImageInitOptionsUserDefined,
77 ) -> anyhow::Result<Self> {
78 let ImageInitOptionsUserDefined {
79 execution_providers,
80 } = options;
81
82 let threads = available_parallelism()?.get();
83
84 let preprocessor = Compose::from_bytes(model.preprocessor_file)?;
85
86 let session = Session::builder()?
87 .with_execution_providers(execution_providers)?
88 .with_optimization_level(GraphOptimizationLevel::Level3)?
89 .with_intra_threads(threads)?
90 .commit_from_memory(&model.onnx_file)?;
91
92 Ok(Self::new(preprocessor, session))
93 }
94
95 fn new(preprocessor: Compose, session: Session) -> Self {
97 Self {
98 preprocessor,
99 session,
100 }
101 }
102
103 #[cfg(feature = "hf-hub")]
105 fn retrieve_model(
106 model: ImageEmbeddingModel,
107 cache_dir: PathBuf,
108 show_download_progress: bool,
109 ) -> anyhow::Result<ApiRepo> {
110 use crate::common::pull_from_hf;
111
112 pull_from_hf(model.to_string(), cache_dir, show_download_progress)
113 }
114
115 pub fn list_supported_models() -> Vec<ModelInfo<ImageEmbeddingModel>> {
117 models_list()
118 }
119
120 pub fn get_model_info(model: &ImageEmbeddingModel) -> ModelInfo<ImageEmbeddingModel> {
122 ImageEmbedding::list_supported_models()
123 .into_iter()
124 .find(|m| &m.model == model)
125 .expect("Model not found in supported models list. This is a bug - please report it.")
126 }
127
128 pub fn embed_bytes(
130 &mut self,
131 images: &[&[u8]],
132 batch_size: Option<usize>,
133 ) -> anyhow::Result<Vec<Embedding>> {
134 let batch_size = batch_size.unwrap_or(DEFAULT_BATCH_SIZE);
135
136 let output = images
137 .chunks(batch_size)
138 .map(|batch| {
139 let inputs = batch
141 .iter()
142 .map(|img| {
143 image::ImageReader::new(Cursor::new(img))
144 .with_guessed_format()?
145 .decode()
146 .map_err(|err| anyhow!("image decode: {}", err))
147 })
148 .collect::<Result<_, _>>()?;
149
150 self.embed_images(inputs)
151 })
152 .collect::<anyhow::Result<Vec<_>>>()?
153 .into_iter()
154 .flatten()
155 .collect();
156
157 Ok(output)
158 }
159
160 pub fn embed<S: AsRef<Path> + Send + Sync>(
165 &mut self,
166 images: impl AsRef<[S]>,
167 batch_size: Option<usize>,
168 ) -> anyhow::Result<Vec<Embedding>> {
169 let images = images.as_ref();
170 let batch_size = batch_size.unwrap_or(DEFAULT_BATCH_SIZE);
172
173 let output = images
174 .chunks(batch_size)
175 .map(|batch| {
176 let inputs = batch
178 .iter()
179 .map(|img| {
180 image::ImageReader::open(img)?
181 .decode()
182 .map_err(|err| anyhow!("image decode: {}", err))
183 })
184 .collect::<Result<_, _>>()?;
185
186 self.embed_images(inputs)
187 })
188 .collect::<anyhow::Result<Vec<_>>>()?
189 .into_iter()
190 .flatten()
191 .collect();
192
193 Ok(output)
194 }
195
196 pub fn embed_images(&mut self, imgs: Vec<DynamicImage>) -> anyhow::Result<Vec<Embedding>> {
198 let inputs = imgs
199 .into_iter()
200 .map(|img| {
201 let pixels = self.preprocessor.transform(TransformData::Image(img))?;
202 match pixels {
203 TransformData::NdArray(array) => Ok(array),
204 _ => Err(anyhow!("Preprocessor configuration error!")),
205 }
206 })
207 .collect::<anyhow::Result<Vec<Array3<f32>>>>()?;
208
209 let inputs_view: Vec<ArrayView3<f32>> = inputs.iter().map(|img| img.view()).collect();
211 let pixel_values_array = ndarray::stack(ndarray::Axis(0), &inputs_view)?;
212
213 let input_name = self.session.inputs[0].name.clone();
214 let session_inputs = ort::inputs![
215 input_name => Value::from_array(pixel_values_array)?,
216 ];
217
218 let outputs = self.session.run(session_inputs)?;
219
220 let last_hidden_state_key = match outputs.len() {
223 1 => vec![outputs
224 .keys()
225 .next()
226 .ok_or_else(|| anyhow!("Expected one output but found none"))?],
227 _ => vec!["image_embeds", "last_hidden_state"],
228 };
229
230 let (shape, data) = last_hidden_state_key
232 .iter()
233 .find_map(|&key| {
234 outputs
235 .get(key)
236 .and_then(|v| v.try_extract_tensor::<f32>().ok())
237 })
238 .ok_or_else(|| anyhow!("Could not extract tensor from any known output key"))?;
239 let shape: Vec<usize> = shape.iter().map(|&d| d as usize).collect();
240 let output_array = ndarray::ArrayViewD::from_shape(shape.as_slice(), data)?;
241
242 let embeddings = match output_array.ndim() {
243 3 => {
244 (0..output_array.shape()[0])
248 .map(|batch_idx| {
249 let cls_embedding = output_array
250 .slice(ndarray::s![batch_idx, 0, ..])
251 .to_owned()
252 .to_vec();
253 normalize(&cls_embedding)
254 })
255 .collect()
256 }
257 2 => {
258 output_array
260 .outer_iter()
261 .map(|row| {
262 row.as_slice()
263 .ok_or_else(|| anyhow!("Failed to convert array row to slice"))
264 .map(normalize)
265 })
266 .collect::<anyhow::Result<Vec<_>>>()?
267 }
268 _ => {
269 return Err(anyhow!(
270 "Unexpected output tensor shape: {:?}",
271 output_array.shape()
272 ))
273 }
274 };
275
276 Ok(embeddings)
277 }
278}