Skip to main content

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}