1use std::path::{Path, PathBuf};
2
3#[derive(Debug, Clone)]
5pub struct ImageEntry {
6 pub path: PathBuf,
7 pub step: u64,
8 pub tag: String,
9 pub size_bytes: u64,
10}
11
12pub fn discover_images(experiment_dir: &Path) -> Vec<ImageEntry> {
16 let images_dir = experiment_dir.join("images");
17 if !images_dir.is_dir() {
18 return Vec::new();
19 }
20
21 let mut entries = Vec::new();
22
23 let read_dir = match std::fs::read_dir(&images_dir) {
24 Ok(rd) => rd,
25 Err(_) => return Vec::new(),
26 };
27
28 for entry in read_dir.flatten() {
29 let path = entry.path();
30 if path.extension().and_then(|e| e.to_str()) != Some("png") {
31 continue;
32 }
33
34 if let Some(parsed) = parse_image_filename(&path) {
35 let size_bytes = entry.metadata().map(|m| m.len()).unwrap_or(0);
36 entries.push(ImageEntry {
37 path,
38 step: parsed.0,
39 tag: parsed.1,
40 size_bytes,
41 });
42 }
43 }
44
45 entries.sort_by(|a, b| a.step.cmp(&b.step).then_with(|| a.tag.cmp(&b.tag)));
46 entries
47}
48
49pub fn find_latest_image(experiment_dir: &Path, tag: Option<&str>) -> Option<ImageEntry> {
51 let mut images = discover_images(experiment_dir);
52 if let Some(tag_filter) = tag {
53 images.retain(|e| e.tag == tag_filter);
54 }
55 images.into_iter().last()
56}
57
58fn parse_image_filename(path: &Path) -> Option<(u64, String)> {
60 let stem = path.file_stem()?.to_str()?;
61 let parts: Vec<&str> = stem.splitn(3, '_').collect();
62 if parts.len() < 3 || parts[0] != "step" {
63 return None;
64 }
65 let step = parts[1].parse::<u64>().ok()?;
66 let tag = parts[2].to_string();
67 Some((step, tag))
68}
69
70#[cfg(test)]
71mod tests {
72 use super::*;
73 use std::fs;
74
75 #[test]
76 fn test_parse_image_filename() {
77 let path = Path::new("/tmp/images/step_000500_render.png");
78 let (step, tag) = parse_image_filename(path).unwrap();
79 assert_eq!(step, 500);
80 assert_eq!(tag, "render");
81 }
82
83 #[test]
84 fn test_parse_image_filename_with_underscores_in_tag() {
85 let path = Path::new("/tmp/images/step_001000_depth_map.png");
86 let (step, tag) = parse_image_filename(path).unwrap();
87 assert_eq!(step, 1000);
88 assert_eq!(tag, "depth_map");
89 }
90
91 #[test]
92 fn test_parse_image_filename_invalid() {
93 assert!(parse_image_filename(Path::new("/tmp/images/random.png")).is_none());
94 assert!(parse_image_filename(Path::new("/tmp/images/step_abc_tag.png")).is_none());
95 }
96
97 #[test]
98 fn test_discover_images() {
99 let dir = tempfile::tempdir().unwrap();
100 let images_dir = dir.path().join("images");
101 fs::create_dir_all(&images_dir).unwrap();
102
103 fs::write(images_dir.join("step_000000_render.png"), b"fake").unwrap();
105 fs::write(images_dir.join("step_000000_gt.png"), b"fake").unwrap();
106 fs::write(images_dir.join("step_000100_render.png"), b"fake").unwrap();
107 fs::write(images_dir.join("not_an_image.txt"), b"nope").unwrap();
108
109 let entries = discover_images(dir.path());
110 assert_eq!(entries.len(), 3);
111 assert_eq!(entries[0].step, 0);
112 assert_eq!(entries[0].tag, "gt");
113 assert_eq!(entries[1].step, 0);
114 assert_eq!(entries[1].tag, "render");
115 assert_eq!(entries[2].step, 100);
116 }
117
118 #[test]
119 fn test_discover_images_empty() {
120 let dir = tempfile::tempdir().unwrap();
121 let entries = discover_images(dir.path());
122 assert!(entries.is_empty());
123 }
124
125 #[test]
126 fn test_find_latest_image() {
127 let dir = tempfile::tempdir().unwrap();
128 let images_dir = dir.path().join("images");
129 fs::create_dir_all(&images_dir).unwrap();
130
131 fs::write(images_dir.join("step_000000_render.png"), b"fake").unwrap();
132 fs::write(images_dir.join("step_000100_render.png"), b"fake").unwrap();
133 fs::write(images_dir.join("step_000100_depth.png"), b"fake").unwrap();
134
135 let latest = find_latest_image(dir.path(), Some("render")).unwrap();
136 assert_eq!(latest.step, 100);
137 assert_eq!(latest.tag, "render");
138
139 let latest_any = find_latest_image(dir.path(), None).unwrap();
140 assert_eq!(latest_any.step, 100);
141 }
142}