use std::collections::BTreeMap;
use std::path::{Path, PathBuf};
use crate::error::{Error, Result};
use crate::sample::{Sample, Tensor};
use crate::source::{Source, SourceIterator};
pub struct ImageFolderSource {
root: PathBuf,
samples: Vec<(PathBuf, i64)>,
classes: Vec<String>,
}
impl ImageFolderSource {
pub fn new(root: impl Into<PathBuf>) -> Result<Self> {
let root = root.into();
let mut class_dirs = BTreeMap::new();
let entries = std::fs::read_dir(&root).map_err(|e| Error::ReadFailed {
path: root.clone(),
reason: e.to_string(),
})?;
for entry in entries {
let entry = entry.map_err(|e| Error::ReadFailed {
path: root.clone(),
reason: e.to_string(),
})?;
let path = entry.path();
if path.is_dir() {
if let Some(name) = path.file_name().and_then(|n| n.to_str()) {
class_dirs.insert(name.to_string(), path.clone());
}
}
}
if class_dirs.is_empty() {
return Err(Error::EmptySource {
pattern: root.to_string_lossy().to_string(),
});
}
let mut classes = Vec::with_capacity(class_dirs.len());
let mut samples = Vec::new();
for (idx, (class_name, dir_path)) in class_dirs.into_iter().enumerate() {
classes.push(class_name);
let read_failed = |e: std::io::Error| Error::ReadFailed {
path: dir_path.clone(),
reason: e.to_string(),
};
let files = std::fs::read_dir(&dir_path).map_err(&read_failed)?;
for file in files {
let file = file.map_err(&read_failed)?;
let path = file.path();
if path.is_file() {
let label = i64::try_from(idx).map_err(|_| Error::InvalidConfig {
reason: format!("class index {} exceeds i64::MAX", idx),
})?;
samples.push((path, label));
}
}
}
if samples.is_empty() {
return Err(Error::EmptySource {
pattern: root.to_string_lossy().to_string(),
});
}
Ok(Self {
root,
samples,
classes,
})
}
pub fn classes(&self) -> &[String] {
&self.classes
}
}
impl Source for ImageFolderSource {
fn open(&self) -> Result<Box<dyn SourceIterator>> {
Ok(Box::new(ImageFolderIterator {
samples: self.samples.clone(),
index: 0,
}))
}
fn len_hint(&self) -> Option<u64> {
Some(self.samples.len() as u64)
}
fn name(&self) -> &str {
self.root.to_str().map_or("ImageFolder", |s| s)
}
}
struct ImageFolderIterator {
samples: Vec<(PathBuf, i64)>,
index: u64,
}
impl SourceIterator for ImageFolderIterator {
fn next_sample(&mut self) -> Option<Result<Sample>> {
let Ok(idx) = usize::try_from(self.index) else {
return Some(Err(Error::InvalidConfig {
reason: "source index exceeded addressable memory on this platform".to_string(),
}));
};
if idx >= self.samples.len() {
return None;
}
let (path, label) = &self.samples[idx];
let index = self.index;
self.index += 1;
Some(load_image_folder_sample(path, *label, index))
}
}
fn load_image_folder_sample(path: &Path, label: i64, index: u64) -> Result<Sample> {
let metadata = std::fs::metadata(path).map_err(|e| Error::ReadFailed {
path: path.to_path_buf(),
reason: e.to_string(),
})?;
if metadata.len() > crate::pipeline::MAX_LOAD_FILE_SIZE {
return Err(Error::ReadFailed {
path: path.to_path_buf(),
reason: format!(
"file size {} exceeds maximum {} bytes. Fix: check for oversized files or increase MAX_LOAD_FILE_SIZE",
metadata.len(),
crate::pipeline::MAX_LOAD_FILE_SIZE
),
});
}
let data = std::fs::read(path).map_err(|e| Error::ReadFailed {
path: path.to_path_buf(),
reason: e.to_string(),
})?;
let filename = path.to_string_lossy().to_string();
Ok(Sample::new()
.with("data", Tensor::bytes(data))
.with("label", Tensor::i64(&[label], vec![1]))
.with("path", Tensor::bytes(filename.as_bytes().to_vec()))
.with_metadata(path.to_string_lossy(), index))
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
use tempfile::tempdir;
#[test]
fn image_folder_source() {
let dir = tempdir().unwrap();
let class_a = dir.path().join("cats");
let class_b = dir.path().join("dogs");
fs::create_dir(&class_a).unwrap();
fs::create_dir(&class_b).unwrap();
fs::write(class_a.join("1.jpg"), "cat1").unwrap();
fs::write(class_a.join("2.jpg"), "cat2").unwrap();
fs::write(class_b.join("1.jpg"), "dog1").unwrap();
let source = ImageFolderSource::new(dir.path()).unwrap();
assert_eq!(source.len_hint().unwrap(), 3);
assert_eq!(source.classes(), &["cats".to_string(), "dogs".to_string()]);
let mut iter = source.open().unwrap();
let mut count = 0;
while let Some(res) = iter.next_sample() {
let sample = res.unwrap();
assert!(sample.contains("data"));
assert!(sample.contains("label"));
assert!(sample.contains("path"));
count += 1;
}
assert_eq!(count, 3);
}
#[cfg(unix)]
#[test]
fn unreadable_class_dir_fails_loud_not_silently_skipped() {
use std::os::unix::fs::PermissionsExt;
let dir = tempdir().unwrap();
let class = dir.path().join("dog");
fs::create_dir(&class).unwrap();
fs::write(class.join("1.bin"), "x").unwrap();
let other = dir.path().join("cat");
fs::create_dir(&other).unwrap();
fs::write(other.join("1.bin"), "y").unwrap();
fs::set_permissions(&class, fs::Permissions::from_mode(0o000)).unwrap();
let reproducible = fs::read_dir(&class).is_err();
let result = ImageFolderSource::new(dir.path());
fs::set_permissions(&class, fs::Permissions::from_mode(0o755)).unwrap();
if reproducible {
match result {
Err(Error::ReadFailed { .. }) => {}
Err(other) => panic!("expected ReadFailed, got a different error: {other:?}"),
Ok(_) => panic!("unreadable class dir was silently skipped instead of failing loud"),
}
}
}
}