ultralytics_inference/inference.rs
1// Ultralytics 🚀 AGPL-3.0 License - https://ultralytics.com/license
2
3//! Inference configuration and common types.
4//!
5//! This module defines the [`InferenceConfig`] struct, which controls various parameters
6//! for YOLO model inference, such as confidence thresholds, Non-Maximum Suppression (NMS),
7//! input image sizing, and hardware execution options.
8
9use std::fmt;
10use std::str::FromStr;
11
12pub(crate) fn handle_deprecated_precision(
13 quantize: Option<Quantization>,
14 half: Option<bool>,
15) -> Option<Quantization> {
16 if quantize.is_some() {
17 return quantize;
18 }
19 half.map_or(quantize, |enabled| {
20 crate::warn!(
21 "'half' is deprecated and will be removed in the future. Use 'quantize' instead."
22 );
23 enabled.then_some(Quantization::Fp16)
24 })
25}
26
27/// Inference precision requested through the `quantize` argument.
28///
29/// Values and aliases match the Ultralytics Python package: `8`/`int8`/`w8a8`,
30/// `16`/`fp16`/`w16a16`, `32`/`fp32`/`w32a32`, `w8a16`, and `w8a32`.
31#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
32pub enum Quantization {
33 /// INT8 weights and activations.
34 Int8,
35 /// FP16 weights and activations.
36 Fp16,
37 /// FP32 weights and activations.
38 Fp32,
39 /// INT8 weights and 16-bit activations.
40 W8a16,
41 /// INT8 weights and FP32 activations.
42 W8a32,
43}
44
45impl Quantization {
46 /// Return the canonical Ultralytics `quantize` value.
47 #[must_use]
48 pub const fn as_str(self) -> &'static str {
49 match self {
50 Self::Int8 => "8",
51 Self::Fp16 => "16",
52 Self::Fp32 => "32",
53 Self::W8a16 => "w8a16",
54 Self::W8a32 => "w8a32",
55 }
56 }
57}
58
59impl fmt::Display for Quantization {
60 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
61 f.write_str(self.as_str())
62 }
63}
64
65impl FromStr for Quantization {
66 type Err = String;
67
68 fn from_str(value: &str) -> Result<Self, Self::Err> {
69 match value.to_ascii_lowercase().as_str() {
70 "8" | "int8" | "w8a8" => Ok(Self::Int8),
71 "16" | "fp16" | "w16a16" => Ok(Self::Fp16),
72 "32" | "fp32" | "w32a32" => Ok(Self::Fp32),
73 "w8a16" => Ok(Self::W8a16),
74 "w8a32" => Ok(Self::W8a32),
75 _ => Err(format!(
76 "'quantize={value}' is invalid. Valid 'quantize' values are 8, 16, 32, \
77 'int8', 'fp16', 'fp32', 'w8a8', 'w16a16', 'w32a32', 'w8a16', or 'w8a32'. \
78 See https://docs.ultralytics.com/modes/export#quantization-options"
79 )),
80 }
81 }
82}
83
84/// Configuration for YOLO inference.
85///
86/// This struct is used to customize the behavior of the inference engine.
87/// It uses a builder pattern for convenient construction.
88///
89/// # Examples
90///
91/// Basic configuration:
92/// ```rust
93/// use ultralytics_inference::InferenceConfig;
94///
95/// let config = InferenceConfig::new()
96/// .with_confidence(0.5)
97/// .with_iou(0.45)
98/// .with_max_det(300)
99/// .with_imgsz(640, 640);
100/// ```
101///
102/// With specific hardware device:
103/// ```rust
104/// use ultralytics_inference::{InferenceConfig, Device};
105///
106/// let config = InferenceConfig::new()
107/// .with_confidence(0.5)
108/// .with_device(Device::Cuda(0));
109/// ```
110#[derive(Debug, Clone)]
111#[allow(clippy::struct_excessive_bools)]
112pub struct InferenceConfig {
113 /// Confidence threshold for detections (0.0 to 1.0).
114 /// Detections with confidence scores lower than this value will be discarded.
115 pub confidence_threshold: f32,
116 /// Intersection over Union (`IoU`) threshold for Non-Maximum Suppression (NMS) (0.0 to 1.0).
117 /// Used to merge overlapping boxes. Lower values filter more duplicates.
118 pub iou_threshold: f32,
119 /// Maximum number of detections to return per image.
120 /// The top-k detections sorted by confidence will be returned.
121 pub max_det: usize,
122 /// Explicit input image size (height, width).
123 /// If `None`, the model's metadata will be used to determine input size.
124 pub imgsz: Option<(usize, usize)>,
125 /// Batch size for inference when using [`BatchProcessor`](crate::batch::BatchProcessor).
126 /// If `None`, defaults to 1 (single-image inference).
127 pub batch: Option<usize>,
128 /// Number of intra-op threads for ONNX Runtime.
129 /// Setting this to `0` lets [`YOLOModel::load`](crate::YOLOModel::load) resolve it to
130 /// [`std::thread::available_parallelism`] when it builds the session, falling back to
131 /// `4` if that cannot be determined.
132 pub num_threads: usize,
133 /// Requested inference precision. `None` uses the model's native precision.
134 pub quantize: Option<Quantization>,
135 /// Legacy FP16 inference flag. Use [`Self::quantize`] instead.
136 #[doc(hidden)]
137 pub half: bool,
138 /// Hardware device to use for inference.
139 /// If `None`, the best available device will be automatically selected.
140 pub device: Option<crate::Device>,
141 /// Whether to save annotated results.
142 /// Defaults to `true`.
143 pub save: bool,
144 /// Whether to save individual frames instead of a video file when input is video.
145 /// Defaults to `false` (save as video).
146 pub save_frames: bool,
147 /// Whether to use minimal padding (rectangular inference). Defaults to `true`.
148 pub rect: bool,
149 /// Class IDs to filter predictions. If `None`, all classes are returned.
150 /// Useful for focusing on specific objects in multi-class detection tasks.
151 pub classes: Option<Vec<usize>>,
152 /// Use the `CUDA` preprocess fast path when available.
153 ///
154 /// Defaults to `true`. The flag is only consulted when the crate was
155 /// compiled with the `cuda-preprocess` feature **and** the selected
156 /// device is `CUDA` or `TensorRT` (or one of those EPs is registered by
157 /// default). In every other configuration the value is ignored and the
158 /// standard CPU preprocess path runs.
159 pub cuda_preprocess: bool,
160 /// Upper bound, in bytes, on the CUDA execution provider's memory arena.
161 ///
162 /// `None` (default) lets the arena grow as far as the device allows. Only
163 /// consulted with the `cuda` feature and a CUDA device: a `TensorRT` device
164 /// ignores it, as does a limit of `0`. The cap covers the arena alone, so
165 /// peak device memory stays well above it.
166 pub cuda_memory_limit: Option<usize>,
167}
168
169impl Default for InferenceConfig {
170 fn default() -> Self {
171 Self {
172 confidence_threshold: Self::DEFAULT_CONF,
173 iou_threshold: Self::DEFAULT_IOU,
174 max_det: Self::DEFAULT_MAX_DET,
175 imgsz: None,
176 batch: None,
177 num_threads: 0, // 0 = resolve to `available_parallelism()` when the session is built
178 quantize: Self::DEFAULT_QUANTIZE,
179 half: Self::DEFAULT_HALF,
180 device: None,
181 save: Self::DEFAULT_SAVE,
182 save_frames: Self::DEFAULT_SAVE_FRAMES,
183 rect: Self::DEFAULT_RECT,
184 classes: None,
185 cuda_preprocess: Self::DEFAULT_CUDA_PREPROCESS,
186 cuda_memory_limit: None,
187 }
188 }
189}
190
191impl InferenceConfig {
192 /// Default confidence threshold (0.0 to 1.0).
193 pub const DEFAULT_CONF: f32 = 0.25;
194 /// Default `IoU` threshold for NMS (0.0 to 1.0).
195 pub const DEFAULT_IOU: f32 = 0.7;
196 /// Default maximum number of detections per image.
197 pub const DEFAULT_MAX_DET: usize = 300;
198 /// Default inference precision. `None` uses the model's native precision.
199 pub const DEFAULT_QUANTIZE: Option<Quantization> = None;
200 /// Legacy default retained for source compatibility.
201 #[doc(hidden)]
202 pub const DEFAULT_HALF: bool = false;
203 /// Default for saving annotated results.
204 pub const DEFAULT_SAVE: bool = true;
205 /// Default for saving individual frames (vs video).
206 pub const DEFAULT_SAVE_FRAMES: bool = false;
207 /// Default for rectangular (minimal padding) inference.
208 pub const DEFAULT_RECT: bool = true;
209 /// Default input image size for standard YOLO models (height, width).
210 pub const DEFAULT_IMGSZ: (usize, usize) = (640, 640);
211 /// Default input image size for OBB models (height, width).
212 pub const DEFAULT_OBB_IMGSZ: (usize, usize) = (1024, 1024);
213 /// Default for the CUDA preprocess fast path: on whenever the crate is
214 /// built with the `cuda-preprocess` feature and the device permits it.
215 pub const DEFAULT_CUDA_PREPROCESS: bool = true;
216
217 /// Create a new configuration with default values.
218 ///
219 /// # Returns
220 ///
221 /// * A new `InferenceConfig` instance with default settings.
222 #[must_use]
223 pub fn new() -> Self {
224 Self::default()
225 }
226
227 /// Set the batch size.
228 ///
229 /// # Arguments
230 ///
231 /// * `batch` - The batch size.
232 ///
233 /// # Returns
234 ///
235 /// * The modified `InferenceConfig`.
236 #[must_use]
237 pub const fn with_batch(mut self, batch: usize) -> Self {
238 self.batch = Some(batch);
239 self
240 }
241
242 /// Set the confidence threshold.
243 ///
244 /// Detections with a confidence score below this threshold will be filtered out.
245 ///
246 /// # Arguments
247 ///
248 /// * `threshold` - The minimum confidence score (0.0 to 1.0).
249 ///
250 /// # Returns
251 ///
252 /// * The modified `InferenceConfig`.
253 #[must_use]
254 pub const fn with_confidence(mut self, threshold: f32) -> Self {
255 self.confidence_threshold = threshold;
256 self
257 }
258
259 /// Set the `IoU` threshold for Non-Maximum Suppression (NMS).
260 ///
261 /// NMS suppresses overlapping bounding boxes. This threshold determines how much overlap
262 /// is allowed before boxes are considered duplicates.
263 ///
264 /// # Arguments
265 ///
266 /// * `threshold` - The `IoU` threshold (0.0 to 1.0).
267 ///
268 /// # Returns
269 ///
270 /// * The modified `InferenceConfig`.
271 #[must_use]
272 pub const fn with_iou(mut self, threshold: f32) -> Self {
273 self.iou_threshold = threshold;
274 self
275 }
276
277 /// Set the maximum number of detections to return.
278 ///
279 /// Only the top `max` detections (sorted by confidence) will be kept after NMS.
280 ///
281 /// # Arguments
282 ///
283 /// * `max` - The maximum number of detections.
284 ///
285 /// # Returns
286 ///
287 /// * The modified `InferenceConfig`.
288 #[must_use]
289 pub const fn with_max_det(mut self, max: usize) -> Self {
290 self.max_det = max;
291 self
292 }
293
294 /// Set the input image size.
295 ///
296 /// This explicitly sets the size to resize images to before inference.
297 /// If not set, the model's internal metadata size will be used.
298 ///
299 /// # Arguments
300 ///
301 /// * `height` - The target image height.
302 /// * `width` - The target image width.
303 ///
304 /// # Returns
305 ///
306 /// * The modified `InferenceConfig`.
307 #[must_use]
308 pub const fn with_imgsz(mut self, height: usize, width: usize) -> Self {
309 self.imgsz = Some((height, width));
310 self
311 }
312
313 /// Set the number of threads for inference.
314 ///
315 /// # Arguments
316 ///
317 /// * `threads` - The number of intra-op threads. Set to `0` to use every available core.
318 ///
319 /// # Returns
320 ///
321 /// * The modified `InferenceConfig`.
322 #[must_use]
323 pub const fn with_threads(mut self, threads: usize) -> Self {
324 self.num_threads = threads;
325 self
326 }
327
328 /// Set the requested inference precision.
329 ///
330 /// The accepted schemes match the Python package's `quantize` argument.
331 /// On CPU this selects the requested precision where the execution provider
332 /// supports it; an FP16 ONNX model always runs at FP32 weights there, because
333 /// ONNX Runtime widens the graph while building the session.
334 ///
335 /// # Returns
336 ///
337 /// * The modified `InferenceConfig`.
338 #[must_use]
339 pub const fn with_quantize(mut self, quantize: Quantization) -> Self {
340 self.quantize = Some(quantize);
341 self
342 }
343
344 /// Set FP16 inference using the legacy precision argument.
345 #[doc(hidden)]
346 #[must_use]
347 pub const fn with_half(mut self, half: bool) -> Self {
348 self.half = half;
349 self
350 }
351
352 #[cfg(not(target_arch = "wasm32"))]
353 pub(crate) fn normalize_precision(&mut self) {
354 self.quantize = handle_deprecated_precision(self.quantize, self.half.then_some(true));
355 self.half = false;
356 }
357
358 /// Enable or disable the CUDA preprocess fast path.
359 ///
360 /// When `true` (default), and the crate was built with the
361 /// `cuda-preprocess` feature, and the selected device is CUDA or
362 /// `TensorRT`, [`YOLOModel::predict_image`](crate::YOLOModel::predict_image)
363 /// dispatches to a fused CUDA kernel for letterbox + normalize +
364 /// HWC→CHW and feeds the result to ORT as a zero-copy device tensor.
365 /// Set to `false` to force the standard CPU preprocess path even when
366 /// the feature is available.
367 ///
368 /// In any configuration where the fast path can't run (feature off,
369 /// non-CUDA device, or runtime fallback), the value is silently ignored.
370 #[must_use]
371 pub const fn with_cuda_preprocess(mut self, enabled: bool) -> Self {
372 self.cuda_preprocess = enabled;
373 self
374 }
375
376 /// Set the hardware device for inference.
377 ///
378 /// # Arguments
379 ///
380 /// * `device` - The device to use (e.g. `Device::Cpu`, `Device::Cuda(0)`,
381 /// `Device::CoreMl`, `Device::IntelGpu`).
382 ///
383 /// # Example
384 ///
385 /// ```rust
386 /// use ultralytics_inference::{Device, InferenceConfig};
387 ///
388 /// let config = InferenceConfig::new()
389 /// .with_device(Device::CoreMl); // CoreML on Apple Silicon
390 ///
391 /// // OpenVINO on Intel hardware (intel:cpu, intel:gpu, intel:npu)
392 /// let intel = InferenceConfig::new()
393 /// .with_device(Device::IntelGpu);
394 /// ```
395 ///
396 /// # Returns
397 ///
398 /// * The modified `InferenceConfig`.
399 #[must_use]
400 pub const fn with_device(mut self, device: crate::Device) -> Self {
401 self.device = Some(device);
402 self
403 }
404
405 /// Set whether to save annotated results.
406 ///
407 /// # Arguments
408 ///
409 /// * `save` - `true` to save results, `false` to skip saving.
410 ///
411 /// # Returns
412 ///
413 /// * The modified `InferenceConfig`.
414 #[must_use]
415 pub const fn with_save(mut self, save: bool) -> Self {
416 self.save = save;
417 self
418 }
419
420 /// Set whether to save individual frames for video inputs.
421 ///
422 /// # Arguments
423 ///
424 /// * `save_frames` - `true` to save frames, `false` to save as video.
425 ///
426 /// # Returns
427 ///
428 /// * The modified `InferenceConfig`.
429 #[must_use]
430 pub const fn with_save_frames(mut self, save_frames: bool) -> Self {
431 self.save_frames = save_frames;
432 self
433 }
434
435 /// Set whether to use minimal padding (rectangular inference).
436 ///
437 /// # Arguments
438 ///
439 /// * `rect` - `true` to enable, `false` to disable.
440 ///
441 /// # Returns
442 ///
443 /// * The modified `InferenceConfig`.
444 #[must_use]
445 pub const fn with_rect(mut self, rect: bool) -> Self {
446 self.rect = rect;
447 self
448 }
449
450 /// Set the class IDs to filter predictions.
451 ///
452 /// Only detections belonging to the specified classes will be returned.
453 ///
454 /// # Arguments
455 ///
456 /// * `classes` - A vector of class IDs to keep.
457 ///
458 /// # Example
459 ///
460 /// ```rust
461 /// use ultralytics_inference::InferenceConfig;
462 ///
463 /// // Only detect persons (class 0) and cars (class 2)
464 /// let config = InferenceConfig::new()
465 /// .with_classes(vec![0, 2]);
466 /// ```
467 ///
468 /// # Returns
469 ///
470 /// * The modified `InferenceConfig`.
471 #[must_use]
472 pub fn with_classes(mut self, classes: Vec<usize>) -> Self {
473 self.classes = Some(classes);
474 self
475 }
476 /// Check if a class should be included in the results.
477 ///
478 /// # Arguments
479 ///
480 /// * `class_id` - The class index to check.
481 ///
482 /// # Returns
483 ///
484 /// * `true` if the class should be kept.
485 /// * `false` if the class should be filtered out.
486 #[must_use]
487 pub fn keep_class(&self, class_id: usize) -> bool {
488 self.classes.as_ref().is_none_or(|c| c.contains(&class_id))
489 }
490
491 /// Cap the CUDA execution provider's memory arena at `limit` bytes.
492 ///
493 /// Use this to stop ONNX Runtime from reserving most of the GPU so the
494 /// device can be shared with other processes. Has no effect unless the
495 /// crate is built with the `cuda` feature and a CUDA device is selected.
496 /// A limit the graph cannot fit in fails the load with an ONNX Runtime arena
497 /// error, so leave the model room to run.
498 ///
499 /// # Example
500 ///
501 /// ```rust
502 /// use ultralytics_inference::InferenceConfig;
503 ///
504 /// // Limit the CUDA arena to 2 GiB.
505 /// let config = InferenceConfig::new().with_cuda_memory_limit(2 * 1024 * 1024 * 1024);
506 /// ```
507 ///
508 /// # Returns
509 ///
510 /// * The modified `InferenceConfig`.
511 #[must_use]
512 pub const fn with_cuda_memory_limit(mut self, limit: usize) -> Self {
513 self.cuda_memory_limit = Some(limit);
514 self
515 }
516}
517
518#[cfg(test)]
519mod tests {
520 use super::*;
521
522 #[test]
523 fn test_config_default() {
524 let config = InferenceConfig::default();
525 assert!((config.confidence_threshold - InferenceConfig::DEFAULT_CONF).abs() < f32::EPSILON);
526 assert!((config.iou_threshold - InferenceConfig::DEFAULT_IOU).abs() < f32::EPSILON);
527 assert_eq!(config.max_det, 300);
528 }
529
530 #[test]
531 fn test_config_builder() {
532 let config = InferenceConfig::new()
533 .with_confidence(0.5)
534 .with_iou(0.6)
535 .with_max_det(300)
536 .with_imgsz(640, 640)
537 .with_threads(8);
538
539 assert!((config.confidence_threshold - 0.5).abs() < f32::EPSILON);
540 assert!((config.iou_threshold - 0.6).abs() < f32::EPSILON);
541 assert_eq!(config.max_det, 300);
542 assert_eq!(config.imgsz, Some((640, 640)));
543 assert_eq!(config.num_threads, 8);
544 }
545
546 #[test]
547 fn test_keep_class() {
548 let config = InferenceConfig::default();
549 assert!(config.keep_class(0));
550 assert!(config.keep_class(100));
551
552 let config_filtered = InferenceConfig::new().with_classes(vec![1, 3]);
553 assert!(config_filtered.keep_class(1));
554 assert!(config_filtered.keep_class(3));
555 assert!(!config_filtered.keep_class(0));
556 assert!(!config_filtered.keep_class(2));
557 }
558
559 #[test]
560 fn test_remaining_builders() {
561 let config = InferenceConfig::new()
562 .with_batch(4)
563 .with_quantize(Quantization::Fp16)
564 .with_cuda_preprocess(false)
565 .with_device(crate::Device::Cpu)
566 .with_save(false)
567 .with_save_frames(true)
568 .with_rect(false)
569 .with_cuda_memory_limit(2 * 1024 * 1024 * 1024);
570
571 assert_eq!(config.batch, Some(4));
572 assert_eq!(config.quantize, Some(Quantization::Fp16));
573 assert!(!config.cuda_preprocess);
574 assert_eq!(config.device, Some(crate::Device::Cpu));
575 assert!(!config.save);
576 assert!(config.save_frames);
577 assert!(!config.rect);
578 assert_eq!(config.cuda_memory_limit, Some(2 * 1024 * 1024 * 1024));
579 }
580
581 #[test]
582 fn test_default_constants() {
583 // Defaults applied by `default()` match the public constants.
584 let c = InferenceConfig::default();
585 assert_eq!(c.max_det, InferenceConfig::DEFAULT_MAX_DET);
586 assert_eq!(c.save, InferenceConfig::DEFAULT_SAVE);
587 assert_eq!(c.rect, InferenceConfig::DEFAULT_RECT);
588 assert!(c.batch.is_none());
589 assert!(c.device.is_none());
590 assert!(c.classes.is_none());
591 assert_eq!(c.quantize, InferenceConfig::DEFAULT_QUANTIZE);
592 }
593
594 #[test]
595 fn test_quantization_aliases() {
596 for (value, expected) in [
597 ("8", Quantization::Int8),
598 ("int8", Quantization::Int8),
599 ("w8a8", Quantization::Int8),
600 ("16", Quantization::Fp16),
601 ("fp16", Quantization::Fp16),
602 ("w16a16", Quantization::Fp16),
603 ("32", Quantization::Fp32),
604 ("fp32", Quantization::Fp32),
605 ("w32a32", Quantization::Fp32),
606 ("w8a16", Quantization::W8a16),
607 ("W8A32", Quantization::W8a32),
608 ] {
609 assert_eq!(value.parse::<Quantization>().unwrap(), expected);
610 }
611 assert_eq!(Quantization::Int8.to_string(), "8");
612 assert!("4".parse::<Quantization>().is_err());
613 }
614
615 #[test]
616 fn test_deprecated_half_mapping() {
617 let mut config = InferenceConfig::new().with_half(true);
618 config.normalize_precision();
619 assert_eq!(config.quantize, Some(Quantization::Fp16));
620 assert!(!config.half);
621
622 let mut config = InferenceConfig::new()
623 .with_half(true)
624 .with_quantize(Quantization::Fp32);
625 config.normalize_precision();
626 assert_eq!(config.quantize, Some(Quantization::Fp32));
627 assert!(!config.half);
628 }
629}