use serde_yaml::Value;
use std::collections::BTreeMap;
use std::path::Path;
#[derive(Debug, Clone, PartialEq)]
pub enum YamlVal {
Int(i64),
Float(f64),
Bool(bool),
Str(String),
List(Vec<i64>),
NestedList(Vec<Vec<i64>>),
None,
}
impl YamlVal {
pub fn as_i64(&self) -> Option<i64> {
match self {
YamlVal::Int(i) => Some(*i),
YamlVal::Float(f) => Some(*f as i64),
YamlVal::Bool(b) => Some(*b as i64),
_ => None,
}
}
pub fn as_f64(&self) -> Option<f64> {
match self {
YamlVal::Int(i) => Some(*i as f64),
YamlVal::Float(f) => Some(*f),
YamlVal::Bool(b) => Some(if *b { 1.0 } else { 0.0 }),
_ => None,
}
}
pub fn as_bool(&self) -> Option<bool> {
match self {
YamlVal::Bool(b) => Some(*b),
YamlVal::Int(i) => Some(*i != 0),
_ => None,
}
}
pub fn as_str(&self) -> Option<&str> {
match self {
YamlVal::Str(s) => Some(s),
_ => None,
}
}
pub fn as_list(&self) -> Option<&[i64]> {
match self {
YamlVal::List(l) => Some(l),
_ => None,
}
}
pub fn is_list(&self) -> bool {
matches!(self, YamlVal::List(_))
}
}
#[derive(Debug, Clone)]
pub struct LayerRow {
pub from: Vec<i64>,
pub repeats: i64,
pub module: String,
pub args: Vec<YamlVal>,
}
#[derive(Debug, Clone)]
pub struct ModelYaml {
pub nc: i64,
pub reg_max: i64,
pub end2end: bool,
pub activation: Option<String>,
pub kpt_shape: Option<(i64, i64)>,
pub depth_multiple: f64,
pub width_multiple: f64,
pub max_channels: f64,
pub scale: String,
pub backbone: Vec<LayerRow>,
pub head: Vec<LayerRow>,
pub yaml_file: String,
}
fn value_to_yamlval(v: &Value) -> YamlVal {
match v {
Value::Number(n) => {
if let Some(i) = n.as_i64() {
YamlVal::Int(i)
} else {
YamlVal::Float(n.as_f64().unwrap_or(0.0))
}
}
Value::Bool(b) => YamlVal::Bool(*b),
Value::String(s) => YamlVal::Str(s.clone()),
Value::Sequence(seq) => {
if seq.iter().all(|x| x.as_sequence().is_some()) {
YamlVal::NestedList(
seq.iter()
.map(|x| x.as_sequence().unwrap().iter().filter_map(|v| v.as_i64()).collect())
.collect(),
)
} else {
YamlVal::List(seq.iter().filter_map(|x| x.as_i64()).collect())
}
}
Value::Null => YamlVal::None,
other => YamlVal::Str(format!("{other:?}")),
}
}
fn parse_rows(seq: &[Value]) -> Vec<LayerRow> {
seq.iter()
.map(|row| {
let r = row.as_sequence().expect("layer row must be a list [from, repeats, module, args]");
let from_v = &r[0];
let from = match from_v {
Value::Sequence(s) => s.iter().filter_map(|x| x.as_i64()).collect(),
Value::Number(n) => vec![n.as_i64().unwrap_or(-1)],
_ => vec![-1],
};
let repeats = r.get(1).and_then(|x| x.as_i64()).unwrap_or(1);
let module = r.get(2).and_then(|x| x.as_str()).unwrap_or("").to_string();
let args = r
.get(3)
.and_then(|x| x.as_sequence())
.map(|s| s.iter().map(value_to_yamlval).collect())
.unwrap_or_default();
LayerRow { from, repeats, module, args }
})
.collect()
}
pub fn parse_model_yaml_text(text: &str, scale_hint: &str, yaml_file: &str) -> Result<ModelYaml, crate::UlError> {
let v: Value = serde_yaml::from_str(text)?;
let nc = v.get("nc").and_then(|x| x.as_i64()).unwrap_or(80);
let reg_max = v.get("reg_max").and_then(|x| x.as_i64()).unwrap_or(16);
let end2end = v.get("end2end").and_then(|x| x.as_bool()).unwrap_or(false);
let activation = v.get("activation").and_then(|x| x.as_str()).map(|s| s.to_string());
let kpt_shape = v.get("kpt_shape").and_then(|x| x.as_sequence()).and_then(|s| {
Some((s.first()?.as_i64()?, s.get(1)?.as_i64()?))
});
let depth_multiple = v
.get("depth_multiple")
.and_then(|x| x.as_f64())
.unwrap_or(1.0);
let width_multiple = v
.get("width_multiple")
.and_then(|x| x.as_f64())
.unwrap_or(1.0);
let scales: Option<BTreeMap<String, [f64; 3]>> = v.get("scales").and_then(|s| {
let m = s.as_mapping()?;
let mut out = BTreeMap::new();
for (k, val) in m {
let name = k.as_str()?.to_string();
let arr = val.as_sequence()?;
let f = |i: usize| arr.get(i).and_then(|x| x.as_f64()).unwrap_or(1.0);
out.insert(name, [f(0), f(1), f(2)]);
}
Some(out)
});
let (scale, depth_multiple, width_multiple, max_channels) = match (&scales, scale_hint) {
(Some(scales), hint) if !hint.is_empty() && scales.contains_key(hint) => {
let [d, w, m] = scales[hint];
(hint.to_string(), d, w, m)
}
(Some(scales), _) => {
let first = scales.keys().next().cloned().unwrap_or_default();
log::warn!("no model scale passed. Assuming scale='{first}'.");
let [d, w, m] = scales[&first];
(first, d, w, m)
}
(None, _) => (String::new(), depth_multiple, width_multiple, f64::INFINITY),
};
let empty = vec![];
let backbone = parse_rows(v.get("backbone").and_then(|x| x.as_sequence()).unwrap_or(&empty));
let head = parse_rows(v.get("head").and_then(|x| x.as_sequence()).unwrap_or(&empty));
Ok(ModelYaml {
nc,
reg_max,
end2end,
activation,
kpt_shape,
depth_multiple,
width_multiple,
max_channels,
scale,
backbone,
head,
yaml_file: yaml_file.to_string(),
})
}
pub fn guess_model_scale(stem: &str) -> String {
let re = regex::Regex::new(r"yolo(e-)?[v]?\d+([nslmx])").unwrap();
re.captures(stem).and_then(|c| c.get(2)).map(|m| m.as_str().to_string()).unwrap_or_default()
}
pub mod embedded {
include!(concat!(env!("OUT_DIR"), "/cfg_index.rs"));
}
pub fn load_model_yaml(path_or_name: &str) -> Result<ModelYaml, crate::UlError> {
let p = Path::new(path_or_name);
if p.exists() {
let text = std::fs::read_to_string(p)?;
let stem = p.file_stem().and_then(|s| s.to_str()).unwrap_or("model");
let scale = guess_model_scale(stem);
return parse_model_yaml_text(&text, &scale, path_or_name);
}
let name = p.file_name().and_then(|s| s.to_str()).unwrap_or(path_or_name);
let stem = name.strip_suffix(".yaml").unwrap_or(name);
let scale = guess_model_scale(stem);
if let Some(rel) = resolve_embedded_named(stem) {
let text = embedded::get(&rel)
.ok_or_else(|| crate::UlError::Msg(format!("embedded cfg '{rel}' missing")))?;
return parse_model_yaml_text(text, &scale, &rel);
}
let bare = strip_scale(stem, &scale);
let rel = resolve_embedded(&bare).ok_or_else(|| {
crate::UlError::Msg(format!(
"model yaml '{path_or_name}' not found on disk or embedded registry"
))
})?;
let text = embedded::get(&rel)
.ok_or_else(|| crate::UlError::Msg(format!("embedded cfg '{rel}' missing")))?;
parse_model_yaml_text(text, &scale, &rel)
}
fn strip_scale(stem: &str, scale: &str) -> String {
if scale.is_empty() {
return stem.to_string();
}
let re = regex::Regex::new(r"(yolo(?:e-)?[v]?\d+)(?:[nslmx])").unwrap();
re.replace(stem, "${1}").to_string()
}
fn resolve_embedded_named(stem: &str) -> Option<String> {
for d in ["", "v8/", "11/", "12/", "26/", "v10/", "v9/", "v6/", "v5/", "v3/", "rt-detr/"] {
let c = if d.is_empty() { format!("models/{stem}.yaml") } else { format!("models/{d}{stem}.yaml") };
if embedded::contains(&c) {
return Some(c);
}
}
None
}
fn resolve_embedded(bare: &str) -> Option<String> {
const DIRS: &[&str] = &["", "v8/", "11/", "12/", "26/", "v10/", "v9/", "v6/", "v5/", "v3/", "rt-detr/"];
for d in DIRS {
let c = if d.is_empty() { format!("models/{bare}.yaml") } else { format!("models/{d}{bare}.yaml") };
if embedded::contains(&c) {
return Some(c);
}
}
None
}
#[derive(Debug, Clone)]
pub struct DatasetYaml {
pub path: String,
pub train: Vec<String>,
pub val: Vec<String>,
pub test: Vec<String>,
pub names: Vec<String>,
pub kpt_shape: Option<(i64, i64)>,
pub channel_names: Option<Vec<String>>,
}
fn resolve_split(base: &Path, items: &[String]) -> Vec<String> {
items
.iter()
.map(|s| {
let p = Path::new(s);
if p.is_absolute() {
s.clone()
} else {
base.join(p).to_string_lossy().into_owned()
}
})
.collect()
}
pub fn parse_dataset_yaml(path: &Path) -> Result<DatasetYaml, crate::UlError> {
let text = std::fs::read_to_string(path)?;
let v: Value = serde_yaml::from_str(&text)?;
let base = path.parent().unwrap_or(Path::new(".")).to_path_buf();
let raw_path = v.get("path").and_then(|x| x.as_str()).unwrap_or("").to_string();
let root = if Path::new(&raw_path).is_absolute() {
std::path::PathBuf::from(&raw_path)
} else {
base.join(&raw_path)
};
let get_split = |key: &str| -> Vec<String> {
match v.get(key) {
Some(Value::String(s)) => vec![root.join(s).to_string_lossy().into_owned()],
Some(Value::Sequence(seq)) => seq
.iter()
.filter_map(|x| x.as_str())
.map(|s| root.join(s).to_string_lossy().into_owned())
.collect(),
_ => vec![],
}
};
let names: Vec<String> = match v.get("names") {
Some(Value::Sequence(seq)) => seq.iter().map(|x| x.as_str().unwrap_or("?").to_string()).collect(),
Some(Value::Mapping(m)) => {
let mut pairs: Vec<(i64, String)> = m
.iter()
.filter_map(|(k, val)| Some((k.as_i64()?, val.as_str()?.to_string())))
.collect();
pairs.sort();
pairs.into_iter().map(|(_, n)| n).collect()
}
_ => vec![],
};
Ok(DatasetYaml {
path: root.to_string_lossy().into_owned(),
train: resolve_split(&root, &get_split("train")),
val: resolve_split(&root, &get_split("val")),
test: resolve_split(&root, &get_split("test")),
names,
kpt_shape: v.get("kpt_shape").and_then(|x| x.as_sequence()).and_then(|s| {
Some((s.first()?.as_i64()?, s.get(1)?.as_i64()?))
}),
channel_names: v
.get("channels")
.and_then(|x| x.as_sequence())
.map(|s| s.iter().map(|x| x.as_str().unwrap_or("?").to_string()).collect()),
})
}
#[cfg(test)]
mod tests {
use super::*;
const YOLO11: &str = include_str!("../ultralytics/cfg/models/11/yolo11.yaml");
#[test]
fn parse_yolo11() {
let y = parse_model_yaml_text(YOLO11, "n", "11/yolo11.yaml").unwrap();
assert_eq!(y.nc, 80);
assert_eq!(y.scale, "n");
assert!((y.depth_multiple - 0.50).abs() < 1e-9);
assert!((y.width_multiple - 0.25).abs() < 1e-9);
assert_eq!(y.max_channels, 1024.0);
assert_eq!(y.reg_max, 16);
assert!(!y.end2end);
assert_eq!(y.backbone[0].module, "Conv");
assert_eq!(y.backbone[0].args[0].as_i64(), Some(64));
}
#[test]
fn guess_scale() {
assert_eq!(guess_model_scale("yolo11n"), "n");
assert_eq!(guess_model_scale("yolov8x-pose"), "x");
assert_eq!(guess_model_scale("yolo26"), "");
}
#[test]
fn strip() {
assert_eq!(strip_scale("yolo11n", "n"), "yolo11");
assert_eq!(strip_scale("yolov8n-pose", "n"), "yolov8-pose");
assert_eq!(strip_scale("yolo26n-seg", "n"), "yolo26-seg");
}
}