#![cfg_attr(docsrs, feature(doc_cfg))]
#[cfg(any(
all(feature = "ort-download-binaries", feature = "ort-load-dynamic"),
all(feature = "ort-download-binaries", feature = "ort-pkg-config"),
all(feature = "ort-load-dynamic", feature = "ort-pkg-config"),
))]
compile_error!(
"ONNX Runtime strategy features are mutually exclusive; enable at most one of `ort-download-binaries`, `ort-load-dynamic`, or `ort-pkg-config`."
);
mod config;
mod error;
mod foreground;
mod inference;
mod mask;
pub mod runtime;
mod vectorizer;
#[doc(inline)]
pub use crate::config::{
DEFAULT_MODEL_PATH, ENV_MODEL_PATH, InferenceSettings, MaskProcessingOptions,
};
#[doc(inline)]
pub use crate::error::{OutlineError, OutlineResult};
pub use vectorizer::MaskVectorizer;
#[cfg(feature = "vectorizer-vtracer")]
#[cfg_attr(docsrs, doc(cfg(feature = "vectorizer-vtracer")))]
#[doc(inline)]
pub use vectorizer::vtracer::{TraceOptions, VtracerSvgVectorizer, trace_to_svg_string};
use std::path::Path;
use std::path::PathBuf;
use std::sync::{Arc, Mutex};
use image::imageops::FilterType;
use image::{GrayImage, RgbImage, RgbaImage};
use crate::foreground::compose_foreground;
use crate::inference::CachedInferenceSession;
use crate::mask::{MaskOperation, apply_operations, operations_from_options};
#[derive(Debug)]
pub struct Outline {
settings: InferenceSettings,
default_mask_processing: MaskProcessingOptions,
cached_session: Mutex<Option<Arc<CachedInferenceSession>>>,
}
impl Clone for Outline {
fn clone(&self) -> Self {
Self {
settings: self.settings.clone(),
default_mask_processing: self.default_mask_processing.clone(),
cached_session: Mutex::new(None),
}
}
}
impl Outline {
pub fn new(model_path: impl Into<PathBuf>) -> Self {
Self {
settings: InferenceSettings::new(model_path),
default_mask_processing: MaskProcessingOptions::default(),
cached_session: Mutex::new(None),
}
}
pub fn from_env_or_default() -> Self {
let resolved = std::env::var_os(ENV_MODEL_PATH)
.map(PathBuf::from)
.unwrap_or_else(|| PathBuf::from(DEFAULT_MODEL_PATH));
Self::new(resolved)
}
pub fn try_from_env() -> OutlineResult<Self> {
if let Some(from_env) = std::env::var_os(ENV_MODEL_PATH) {
return Ok(Self::new(PathBuf::from(from_env)));
}
Err(OutlineError::Io(std::io::Error::new(
std::io::ErrorKind::NotFound,
format!(
"Model path not specified in env {}; set the variable to proceed",
ENV_MODEL_PATH
),
)))
}
pub fn with_input_resize_filter(mut self, filter: FilterType) -> Self {
self.settings.input_resize_filter = filter;
self
}
pub fn with_output_resize_filter(mut self, filter: FilterType) -> Self {
self.settings.output_resize_filter = filter;
self
}
pub fn with_intra_threads(mut self, intra_threads: Option<usize>) -> Self {
if self.settings.intra_threads != intra_threads {
self.settings.intra_threads = intra_threads;
self.cached_session = Mutex::new(None);
}
self
}
pub fn with_default_mask_processing(mut self, options: MaskProcessingOptions) -> Self {
self.default_mask_processing = options;
self
}
pub fn default_mask_processing(&self) -> &MaskProcessingOptions {
&self.default_mask_processing
}
fn get_or_init_cached_session(&self) -> OutlineResult<Arc<CachedInferenceSession>> {
let mut cached_session = self
.cached_session
.lock()
.map_err(|_| std::io::Error::other("outline session cache mutex poisoned"))?;
if let Some(session) = cached_session.as_ref() {
return Ok(Arc::clone(session));
}
let session = Arc::new(CachedInferenceSession::new(&self.settings)?);
*cached_session = Some(Arc::clone(&session));
Ok(session)
}
pub fn for_image(&self, image_path: impl AsRef<Path>) -> OutlineResult<InferencedMatte> {
let session = self.get_or_init_cached_session()?;
let (rgb, matte) = session.run_matte_pipeline(&self.settings, image_path.as_ref())?;
Ok(InferencedMatte::new(
rgb,
matte,
self.default_mask_processing.clone(),
))
}
}
#[derive(Debug, Clone)]
pub struct InferencedMatte {
rgb_image: Arc<RgbImage>,
raw_matte: Arc<GrayImage>,
default_mask_processing: MaskProcessingOptions,
}
impl InferencedMatte {
fn new(
rgb_image: RgbImage,
raw_matte: GrayImage,
default_mask_processing: MaskProcessingOptions,
) -> Self {
Self {
rgb_image: Arc::new(rgb_image),
raw_matte: Arc::new(raw_matte),
default_mask_processing,
}
}
pub fn rgb_image(&self) -> &RgbImage {
self.rgb_image.as_ref()
}
pub fn raw_matte(&self) -> &GrayImage {
self.raw_matte.as_ref()
}
pub fn matte(&self) -> MatteHandle {
MatteHandle {
rgb_image: Arc::clone(&self.rgb_image),
raw_matte: Arc::clone(&self.raw_matte),
default_mask_processing: self.default_mask_processing.clone(),
operations: Vec::new(),
}
}
}
#[derive(Debug, Clone)]
pub struct MatteHandle {
rgb_image: Arc<RgbImage>,
raw_matte: Arc<GrayImage>,
default_mask_processing: MaskProcessingOptions,
operations: Vec<MaskOperation>,
}
impl MatteHandle {
pub fn raw(&self) -> GrayImage {
(*self.raw_matte).clone()
}
pub fn into_image(self) -> GrayImage {
(*self.raw_matte).clone()
}
pub fn save(&self, path: impl AsRef<Path>) -> OutlineResult<()> {
self.raw_matte.as_ref().save(path)?;
Ok(())
}
pub fn blur(mut self) -> Self {
let sigma = self.default_mask_processing.blur_sigma;
self.operations.push(MaskOperation::Blur { sigma });
self
}
pub fn blur_with(mut self, sigma: f32) -> Self {
self.operations.push(MaskOperation::Blur { sigma });
self
}
pub fn threshold(mut self) -> Self {
let value = self.default_mask_processing.mask_threshold;
self.operations.push(MaskOperation::Threshold { value });
self
}
pub fn threshold_with(mut self, value: u8) -> Self {
self.operations.push(MaskOperation::Threshold { value });
self
}
pub fn dilate(mut self) -> Self {
let radius = self.default_mask_processing.dilation_radius;
self.operations.push(MaskOperation::Dilate { radius });
self
}
pub fn dilate_with(mut self, radius: f32) -> Self {
self.operations.push(MaskOperation::Dilate { radius });
self
}
pub fn fill_holes(mut self) -> Self {
let threshold = self.default_mask_processing.mask_threshold;
self.operations.push(MaskOperation::FillHoles { threshold });
self
}
pub fn processed(self) -> OutlineResult<MaskHandle> {
self.process_with_options(None)
}
pub fn processed_with(self, options: &MaskProcessingOptions) -> OutlineResult<MaskHandle> {
self.process_with_options(Some(options))
}
fn process_with_options(
mut self,
options: Option<&MaskProcessingOptions>,
) -> OutlineResult<MaskHandle> {
let mut ops = std::mem::take(&mut self.operations);
match options {
Some(custom) => ops.extend(operations_from_options(custom)),
None if ops.is_empty() => {
ops.extend(operations_from_options(&self.default_mask_processing))
}
None => {}
}
let mask = apply_operations(self.raw_matte.as_ref(), &ops);
Ok(MaskHandle::new(
Arc::clone(&self.rgb_image),
mask,
self.default_mask_processing,
))
}
pub fn foreground(&self) -> OutlineResult<ForegroundHandle> {
let rgba = compose_foreground(self.rgb_image.as_ref(), self.raw_matte.as_ref())?;
Ok(ForegroundHandle { image: rgba })
}
pub fn trace<V>(&self, vectorizer: &V, options: &V::Options) -> OutlineResult<V::Output>
where
V: MaskVectorizer,
{
vectorizer.vectorize(self.raw_matte.as_ref(), options)
}
}
#[derive(Debug, Clone)]
pub struct MaskHandle {
rgb_image: Arc<RgbImage>,
mask: GrayImage,
default_mask_processing: MaskProcessingOptions,
operations: Vec<MaskOperation>,
}
impl MaskHandle {
fn new(
rgb_image: Arc<RgbImage>,
mask: GrayImage,
default_mask_processing: MaskProcessingOptions,
) -> Self {
Self {
rgb_image,
mask,
default_mask_processing,
operations: Vec::new(),
}
}
pub fn raw(&self) -> GrayImage {
self.mask.clone()
}
pub fn image(&self) -> &GrayImage {
&self.mask
}
pub fn into_image(self) -> GrayImage {
self.mask
}
pub fn save(&self, path: impl AsRef<Path>) -> OutlineResult<()> {
self.mask.save(path)?;
Ok(())
}
pub fn blur(mut self) -> Self {
let sigma = self.default_mask_processing.blur_sigma;
self.operations.push(MaskOperation::Blur { sigma });
self
}
pub fn blur_with(mut self, sigma: f32) -> Self {
self.operations.push(MaskOperation::Blur { sigma });
self
}
pub fn threshold(mut self) -> Self {
let value = self.default_mask_processing.mask_threshold;
self.operations.push(MaskOperation::Threshold { value });
self
}
pub fn threshold_with(mut self, value: u8) -> Self {
self.operations.push(MaskOperation::Threshold { value });
self
}
pub fn dilate(mut self) -> Self {
let radius = self.default_mask_processing.dilation_radius;
self.operations.push(MaskOperation::Dilate { radius });
self
}
pub fn dilate_with(mut self, radius: f32) -> Self {
self.operations.push(MaskOperation::Dilate { radius });
self
}
pub fn fill_holes(mut self) -> Self {
let threshold = self.default_mask_processing.mask_threshold;
self.operations.push(MaskOperation::FillHoles { threshold });
self
}
pub fn processed(self) -> OutlineResult<MaskHandle> {
self.process_with_options(None)
}
pub fn processed_with(self, options: &MaskProcessingOptions) -> OutlineResult<MaskHandle> {
self.process_with_options(Some(options))
}
fn process_with_options(
mut self,
options: Option<&MaskProcessingOptions>,
) -> OutlineResult<MaskHandle> {
let mut ops = std::mem::take(&mut self.operations);
match options {
Some(custom) => ops.extend(operations_from_options(custom)),
None if ops.is_empty() => {
ops.extend(operations_from_options(&self.default_mask_processing))
}
None => {}
}
let mask = apply_operations(&self.mask, &ops);
Ok(MaskHandle::new(
self.rgb_image,
mask,
self.default_mask_processing,
))
}
pub fn foreground(&self) -> OutlineResult<ForegroundHandle> {
let rgba = compose_foreground(self.rgb_image.as_ref(), &self.mask)?;
Ok(ForegroundHandle { image: rgba })
}
pub fn trace<V>(&self, vectorizer: &V, options: &V::Options) -> OutlineResult<V::Output>
where
V: MaskVectorizer,
{
vectorizer.vectorize(&self.mask, options)
}
}
pub struct ForegroundHandle {
image: RgbaImage,
}
impl ForegroundHandle {
pub fn image(&self) -> &RgbaImage {
&self.image
}
pub fn into_image(self) -> RgbaImage {
self.image
}
pub fn save(&self, path: impl AsRef<Path>) -> OutlineResult<()> {
self.image.save(path)?;
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::io::Write;
use std::sync::Mutex;
use tempfile::NamedTempFile;
static ENV_LOCK: Mutex<()> = Mutex::new(());
const ORT_IDENTITY_MODEL_BYTES: &[u8] = include_bytes!("../tests/fixtures/identity.ort");
fn ort_identity_model_file() -> NamedTempFile {
let mut file = tempfile::Builder::new()
.suffix(".ort")
.tempfile()
.expect("failed to create temporary identity model");
file.write_all(ORT_IDENTITY_MODEL_BYTES)
.expect("failed to write temporary identity model");
file.flush()
.expect("failed to flush temporary identity model");
file
}
mod outline_new {
use super::*;
#[test]
fn user_value_is_stored_directly() {
let outline = Outline::new("/explicit/model.onnx");
assert_eq!(
outline.settings.model_path,
PathBuf::from("/explicit/model.onnx")
);
}
#[test]
fn user_value_ignores_env_var() {
let _lock = ENV_LOCK.lock().unwrap();
unsafe { std::env::set_var(ENV_MODEL_PATH, "env.onnx") };
let outline = Outline::new("user.onnx");
unsafe { std::env::remove_var(ENV_MODEL_PATH) };
assert_eq!(outline.settings.model_path, PathBuf::from("user.onnx"));
}
}
mod outline_from_env_or_default {
use super::*;
#[test]
fn uses_env_var_when_set() {
let _lock = ENV_LOCK.lock().unwrap();
unsafe { std::env::set_var(ENV_MODEL_PATH, "/from/env.onnx") };
let outline = Outline::from_env_or_default();
unsafe { std::env::remove_var(ENV_MODEL_PATH) };
assert_eq!(outline.settings.model_path, PathBuf::from("/from/env.onnx"));
}
#[test]
fn falls_back_to_default_when_env_unset() {
let _lock = ENV_LOCK.lock().unwrap();
unsafe { std::env::remove_var(ENV_MODEL_PATH) };
let outline = Outline::from_env_or_default();
assert_eq!(
outline.settings.model_path,
PathBuf::from(DEFAULT_MODEL_PATH)
);
}
}
mod outline_try_from_env {
use super::*;
#[test]
fn succeeds_when_env_set() {
let _lock = ENV_LOCK.lock().unwrap();
unsafe { std::env::set_var(ENV_MODEL_PATH, "/from/env.onnx") };
let result = Outline::try_from_env();
unsafe { std::env::remove_var(ENV_MODEL_PATH) };
let outline = result.expect("should succeed when env is set");
assert_eq!(outline.settings.model_path, PathBuf::from("/from/env.onnx"));
}
#[test]
fn errors_when_env_unset() {
let _lock = ENV_LOCK.lock().unwrap();
unsafe { std::env::remove_var(ENV_MODEL_PATH) };
let result = Outline::try_from_env();
assert!(result.is_err());
}
}
mod outline_session_cache {
use super::*;
#[test]
fn session_is_reused_within_same_outline() {
let model = ort_identity_model_file();
let outline = Outline::new(model.path());
let first = outline
.get_or_init_cached_session()
.expect("should initialize cached session");
let second = outline
.get_or_init_cached_session()
.expect("should reuse cached session");
assert!(Arc::ptr_eq(&first, &second));
}
#[test]
fn clone_starts_with_fresh_session_cache() {
let model = ort_identity_model_file();
let outline = Outline::new(model.path());
let original = outline
.get_or_init_cached_session()
.expect("should initialize cached session");
let cloned = outline.clone();
let cloned_session = cloned
.get_or_init_cached_session()
.expect("should initialize cloned cached session");
assert!(!Arc::ptr_eq(&original, &cloned_session));
}
#[test]
fn non_session_settings_keep_cached_session() {
let model = ort_identity_model_file();
let outline = Outline::new(model.path());
let cached = outline
.get_or_init_cached_session()
.expect("should initialize cached session");
let outline = outline.with_input_resize_filter(FilterType::Nearest);
let reused = outline
.get_or_init_cached_session()
.expect("should reuse cached session for non-session setting changes");
assert!(Arc::ptr_eq(&cached, &reused));
}
#[test]
fn intra_threads_change_clears_cached_session() {
let model = ort_identity_model_file();
let outline = Outline::new(model.path());
let cached = outline
.get_or_init_cached_session()
.expect("should initialize cached session");
let outline = outline.with_intra_threads(Some(1));
let rebuilt = outline
.get_or_init_cached_session()
.expect("should rebuild cached session after thread change");
assert!(!Arc::ptr_eq(&cached, &rebuilt));
}
}
}