diff --git a/Cargo.lock b/Cargo.lock index ad3c177c2115..56a94c0db844 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -812,6 +812,12 @@ version = "3.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a3e2c3daef883ecc1b5d58c15adae93470a91d425f3532ba1695849656af3fc1" +[[package]] +name = "bytecount" +version = "0.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1e5f035d16fc623ae5f74981db80a439803888314e3a555fd6f04acd51a3205" + [[package]] name = "bytemuck" version = "1.13.1" @@ -887,6 +893,19 @@ dependencies = [ "wasm-bindgen-cli-support", ] +[[package]] +name = "cargo_metadata" +version = "0.14.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4acbb09d9ee8e23699b9634375c72795d095bf268439da88562cf9b501f181fa" +dependencies = [ + "camino", + "cargo-platform", + "semver", + "serde", + "serde_json", +] + [[package]] name = "cargo_metadata" version = "0.18.1" @@ -1891,6 +1910,15 @@ dependencies = [ "libc", ] +[[package]] +name = "error-chain" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d2f06b9cac1506ece98fe3231e3cc9c4410ec3d5b1f24ae1c8946f0742cdefc" +dependencies = [ + "version_check", +] + [[package]] name = "error-code" version = "2.3.1" @@ -3826,6 +3854,33 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8835116a5c179084a830efb3adc117ab007512b535bc1a21c991d3b32a6b44dd" +[[package]] +name = "peg" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f76678828272f177ac33b7e2ac2e3e73cc6c1cd1e3e387928aa69562fa51367" +dependencies = [ + "peg-macros", + "peg-runtime", +] + +[[package]] +name = "peg-macros" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "636d60acf97633e48d266d7415a9355d4389cea327a193f87df395d88cd2b14d" +dependencies = [ + "peg-runtime", + "proc-macro2", + "quote", +] + +[[package]] +name = "peg-runtime" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9555b1514d2d99d78150d3c799d4c357a3e2c2a8062cd108e93a06d9057629c5" + [[package]] name = "percent-encoding" version = "2.2.0" @@ -3903,6 +3958,18 @@ dependencies = [ "plotters-backend", ] +[[package]] +name = "ply-rs" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbadf9cb4a79d516de4c64806fe64ffbd8161d1ac685d000be789fb628b88963" +dependencies = [ + "byteorder", + "linked-hash-map", + "peg", + "skeptic", +] + [[package]] name = "png" version = "0.17.8" @@ -4447,7 +4514,7 @@ name = "re_build_tools" version = "0.12.0-alpha.1+dev" dependencies = [ "anyhow", - "cargo_metadata", + "cargo_metadata 0.18.1", "glob", "sha2", "time", @@ -4460,7 +4527,7 @@ name = "re_build_web_viewer" version = "0.12.0-alpha.1+dev" dependencies = [ "anyhow", - "cargo_metadata", + "cargo_metadata 0.18.1", "re_error", "wasm-bindgen-cli-support", ] @@ -5071,9 +5138,11 @@ dependencies = [ "image", "infer", "itertools 0.11.0", + "linked-hash-map", "mime_guess2", "ndarray", "once_cell", + "ply-rs", "rand", "rayon", "re_build_tools", @@ -6039,6 +6108,21 @@ dependencies = [ "similar", ] +[[package]] +name = "skeptic" +version = "0.13.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16d23b015676c90a0f01c197bfdc786c20342c73a0afdda9025adb0bc42940a8" +dependencies = [ + "bytecount", + "cargo_metadata 0.14.2", + "error-chain", + "glob", + "pulldown-cmark", + "tempfile", + "walkdir", +] + [[package]] name = "slab" version = "0.4.8" diff --git a/Cargo.toml b/Cargo.toml index 806a3f76a2fa..50842815dd5d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -146,6 +146,7 @@ itertools = "0.11" # updating it js-sys = "0.3" # No lazy_static - use `std::sync::OnceLock` or `once_cell` instead libc = "0.2" +linked-hash-map = { version = "0.5", default-features = false } log = "0.4" log-once = "0.4" lz4_flex = "0.11" @@ -167,6 +168,7 @@ ordered-float = "4.2" parking_lot = "0.12" pathdiff = "0.2" pico-args = "0.5" +ply-rs = { version = "0.1", default-features = false } polars-core = "0.29" polars-lazy = "0.29" polars-ops = "0.29" diff --git a/crates/re_data_source/src/data_loader/loader_archetype.rs b/crates/re_data_source/src/data_loader/loader_archetype.rs index dafbde2c6c17..f903e78b9dff 100644 --- a/crates/re_data_source/src/data_loader/loader_archetype.rs +++ b/crates/re_data_source/src/data_loader/loader_archetype.rs @@ -80,22 +80,25 @@ impl DataLoader for ArchetypeLoader { let mut rows = Vec::new(); - if crate::SUPPORTED_MESH_EXTENSIONS.contains(&extension.as_str()) { - re_log::debug!(?filepath, loader = self.name(), "Loading 3D model…",); - rows.extend(load_mesh( - filepath, + if crate::SUPPORTED_IMAGE_EXTENSIONS.contains(&extension.as_str()) { + re_log::debug!(?filepath, loader = self.name(), "Loading image…",); + rows.extend(load_image( + &filepath, timepoint, entity_path, contents.into_owned(), )?); - } else if crate::SUPPORTED_IMAGE_EXTENSIONS.contains(&extension.as_str()) { - re_log::debug!(?filepath, loader = self.name(), "Loading image…",); - rows.extend(load_image( - &filepath, + } else if crate::SUPPORTED_MESH_EXTENSIONS.contains(&extension.as_str()) { + re_log::debug!(?filepath, loader = self.name(), "Loading 3D model…",); + rows.extend(load_mesh( + filepath, timepoint, entity_path, contents.into_owned(), )?); + } else if crate::SUPPORTED_POINT_CLOUD_EXTENSIONS.contains(&extension.as_str()) { + re_log::debug!(?filepath, loader = self.name(), "Loading 3D point cloud…",); + rows.extend(load_point_cloud(timepoint, entity_path, &contents)?); } else if crate::SUPPORTED_TEXT_EXTENSIONS.contains(&extension.as_str()) { re_log::debug!(?filepath, loader = self.name(), "Loading text document…",); rows.extend(load_text_document( @@ -118,6 +121,28 @@ impl DataLoader for ArchetypeLoader { // --- +fn load_image( + filepath: &std::path::Path, + timepoint: TimePoint, + entity_path: EntityPath, + contents: Vec, +) -> Result, DataLoaderError> { + re_tracing::profile_function!(); + + let rows = [ + { + let arch = re_types::archetypes::Image::from_file_contents( + contents, + image::ImageFormat::from_path(filepath).ok(), + )?; + DataRow::from_archetype(RowId::new(), timepoint, entity_path, &arch)? + }, + // + ]; + + Ok(rows.into_iter()) +} + fn load_mesh( filepath: std::path::PathBuf, timepoint: TimePoint, @@ -140,21 +165,18 @@ fn load_mesh( Ok(rows.into_iter()) } -fn load_image( - filepath: &std::path::Path, +fn load_point_cloud( timepoint: TimePoint, entity_path: EntityPath, - contents: Vec, + contents: &[u8], ) -> Result, DataLoaderError> { re_tracing::profile_function!(); let rows = [ { - let arch = re_types::archetypes::Image::from_file_contents( - contents, - image::ImageFormat::from_path(filepath).ok(), - )?; - DataRow::from_archetype(RowId::new(), timepoint, entity_path, &arch)? + // TODO(#4532): `.ply` data loader should support 2D point cloud & meshes + let points3d = re_types::archetypes::Points3D::from_file_contents(contents)?; + DataRow::from_archetype(RowId::new(), timepoint, entity_path, &points3d)? }, // ]; diff --git a/crates/re_data_source/src/lib.rs b/crates/re_data_source/src/lib.rs index 791c80712ae1..014f225d9906 100644 --- a/crates/re_data_source/src/lib.rs +++ b/crates/re_data_source/src/lib.rs @@ -45,6 +45,9 @@ pub const SUPPORTED_IMAGE_EXTENSIONS: &[&str] = &[ pub const SUPPORTED_MESH_EXTENSIONS: &[&str] = &["glb", "gltf", "obj"]; +// TODO(#4532): `.ply` data loader should support 2D point cloud & meshes +pub const SUPPORTED_POINT_CLOUD_EXTENSIONS: &[&str] = &["ply"]; + pub const SUPPORTED_RERUN_EXTENSIONS: &[&str] = &["rrd"]; // TODO(#4555): Add catch-all builtin `DataLoader` for text files @@ -64,6 +67,7 @@ pub fn supported_extensions() -> impl Iterator { pub fn is_supported_file_extension(extension: &str) -> bool { SUPPORTED_IMAGE_EXTENSIONS.contains(&extension) || SUPPORTED_MESH_EXTENSIONS.contains(&extension) + || SUPPORTED_POINT_CLOUD_EXTENSIONS.contains(&extension) || SUPPORTED_RERUN_EXTENSIONS.contains(&extension) || SUPPORTED_TEXT_EXTENSIONS.contains(&extension) } diff --git a/crates/re_types/Cargo.toml b/crates/re_types/Cargo.toml index ff94e96cbd2a..80117e1a6b6b 100644 --- a/crates/re_types/Cargo.toml +++ b/crates/re_types/Cargo.toml @@ -62,12 +62,14 @@ arrow2 = { workspace = true, features = [ ] } bytemuck = { workspace = true, features = ["derive", "extern_crate_alloc"] } document-features.workspace = true +half = { workspace = true, features = ["bytemuck"] } infer.workspace = true itertools.workspace = true -half = { workspace = true, features = ["bytemuck"] } +linked-hash-map.workspace = true mime_guess2.workspace = true ndarray.workspace = true once_cell.workspace = true +ply-rs.workspace = true smallvec.workspace = true thiserror.workspace = true uuid = { workspace = true, features = ["serde", "v4", "js"] } @@ -100,8 +102,6 @@ re_types_builder.workspace = true rayon.workspace = true # `machete` is not a fan of `build-dependencies`. - - [package.metadata.cargo-machete] ignored = ["rayon", "re_build_tools", "re_types_builder"] diff --git a/crates/re_types/src/archetypes/mod.rs b/crates/re_types/src/archetypes/mod.rs index f940a8594486..42f6f9e505ba 100644 --- a/crates/re_types/src/archetypes/mod.rs +++ b/crates/re_types/src/archetypes/mod.rs @@ -23,6 +23,7 @@ mod pinhole; mod pinhole_ext; mod points2d; mod points3d; +mod points3d_ext; mod segmentation_image; mod segmentation_image_ext; mod tensor; diff --git a/crates/re_types/src/archetypes/points3d_ext.rs b/crates/re_types/src/archetypes/points3d_ext.rs new file mode 100644 index 000000000000..03fbc6de2080 --- /dev/null +++ b/crates/re_types/src/archetypes/points3d_ext.rs @@ -0,0 +1,218 @@ +use super::Points3D; + +impl Points3D { + /// Creates a new [`Points3D`] from a `.ply` file. + /// + /// ## Supported properties + /// + /// This expects the following property names: + /// - (Required) Positions of the points: `"x"`, `"y"` & `"z"`. + /// - (Optional) Colors of the points: `"red"`, `"green"` & `"blue"`. + /// - (Optional) Radii of the points: `"radius"`. + /// - (Optional) Labels of the points: `"label"`. + /// + /// The media type will be inferred from the path (extension), or the contents if that fails. + #[cfg(not(target_arch = "wasm32"))] + pub fn from_file_path(filepath: &std::path::Path) -> anyhow::Result { + use anyhow::Context as _; + + let file = std::fs::File::open(filepath) + .with_context(|| format!("Failed to open file {filepath:?}"))?; + let mut file = std::io::BufReader::new(file); + + let parser = ply_rs::parser::Parser::::new(); + let ply = parser.read_ply(&mut file)?; + + Ok(from_ply(&ply)) + } + + /// Creates a new [`Points3D`] from the contents of a `.ply` file. + /// + /// If unspecified, he media type will be inferred from the contents. + pub fn from_file_contents(contents: &[u8]) -> anyhow::Result { + let parser = ply_rs::parser::Parser::::new(); + let mut contents = std::io::Cursor::new(contents); + let ply = parser.read_ply(&mut contents)?; + Ok(from_ply(&ply)) + } +} + +fn from_ply(ply: &ply_rs::ply::Ply) -> Points3D { + re_tracing::profile_function!(); + + use std::borrow::Cow; + + use linked_hash_map::LinkedHashMap; + use ply_rs::ply::Property; + + use crate::components::{Color, Position3D, Radius, Text}; + + fn f32(prop: &Property) -> Option { + match *prop { + Property::Short(v) => Some(v as f32), + Property::UShort(v) => Some(v as f32), + Property::Int(v) => Some(v as f32), + Property::UInt(v) => Some(v as f32), + Property::Float(v) => Some(v), + Property::Double(v) => Some(v as f32), + Property::Char(_) + | Property::UChar(_) + | Property::ListChar(_) + | Property::ListUChar(_) + | Property::ListShort(_) + | Property::ListUShort(_) + | Property::ListInt(_) + | Property::ListUInt(_) + | Property::ListFloat(_) + | Property::ListDouble(_) => None, + } + } + + fn u8(prop: &Property) -> Option { + match *prop { + Property::Short(v) => Some(v as u8), + Property::UShort(v) => Some(v as u8), + Property::Int(v) => Some(v as u8), + Property::UInt(v) => Some(v as u8), + Property::Float(v) => Some((v * 255.0) as u8), + Property::Double(v) => Some((v * 255.0) as u8), + Property::Char(v) => Some(v as u8), + Property::UChar(v) => Some(v), + Property::ListChar(_) + | Property::ListUChar(_) + | Property::ListShort(_) + | Property::ListUShort(_) + | Property::ListInt(_) + | Property::ListUInt(_) + | Property::ListFloat(_) + | Property::ListDouble(_) => None, + } + } + + fn string(prop: &Property) -> Option> { + match prop { + Property::ListUChar(chars) => Some(String::from_utf8_lossy(chars)), + Property::ListChar(_) + | Property::ListShort(_) + | Property::ListUShort(_) + | Property::ListInt(_) + | Property::ListUInt(_) + | Property::ListFloat(_) + | Property::ListDouble(_) + | Property::Char(_) + | Property::UChar(_) + | Property::Short(_) + | Property::UShort(_) + | Property::Int(_) + | Property::UInt(_) + | Property::Float(_) + | Property::Double(_) => None, + } + } + + struct Vertex { + position: Position3D, + color: Option, + radius: Option, + label: Option, + } + + // TODO(cmc): This could be optimized by using custom property accessors. + impl Vertex { + fn from_props(props: &LinkedHashMap) -> Option { + // NOTE: Empirical evidence points to these being de-facto standard… + const PROP_X: &str = "x"; + const PROP_Y: &str = "y"; + const PROP_Z: &str = "z"; + const PROP_RED: &str = "red"; + const PROP_GREEN: &str = "green"; + const PROP_BLUE: &str = "blue"; + const PROP_ALPHA: &str = "alpha"; + const PROP_RADIUS: &str = "radius"; + const PROP_LABEL: &str = "label"; + + let (Some(x), Some(y), Some(z)) = ( + props.get(PROP_X).and_then(f32), + props.get(PROP_Y).and_then(f32), + props.get(PROP_Z).and_then(f32), + ) else { + return None; + }; + + let mut this = Self { + position: Position3D::new(x, y, z), + color: None, + radius: None, + label: None, + }; + + if let (Some(r), Some(g), Some(b)) = ( + props.get(PROP_RED).and_then(u8), + props.get(PROP_GREEN).and_then(u8), + props.get(PROP_BLUE).and_then(u8), + ) { + let a = props.get(PROP_ALPHA).and_then(u8).unwrap_or(255); + this.color = Some(Color::new((r, g, b, a))); + }; + + if let Some(radius) = props.get(PROP_RADIUS).and_then(f32) { + this.radius = Some(Radius(radius)); + } + + if let Some(label) = props.get(PROP_LABEL).and_then(string) { + this.label = Some(Text(label.to_string().into())); + } + + Some(this) + } + } + + let mut positions = Vec::new(); + let mut colors = Vec::new(); + let mut radii = Vec::new(); + let mut labels = Vec::new(); + + for all_props in ply.payload.values() { + for props in all_props { + if let Some(vertex) = Vertex::from_props(props) { + let Vertex { + position, + color, + radius, + label, + } = vertex; + positions.push(position); + colors.push(color); // opt + radii.push(radius); // opt + labels.push(label); // opt + } + } + } + + colors.truncate(positions.len()); + radii.truncate(positions.len()); + labels.truncate(positions.len()); + + let mut arch = crate::archetypes::Points3D::new(positions); + if colors.iter().any(|opt| opt.is_some()) { + // If some colors have been specified but not others, default the unspecified ones to white. + let colors = colors + .into_iter() + .map(|opt| opt.unwrap_or(Color::from_rgb(255, 255, 255))); + arch = arch.with_colors(colors); + } + if radii.iter().any(|opt| opt.is_some()) { + // If some radii have been specified but not others, default the unspecified ones to 1.0. + let radii = radii.into_iter().map(|opt| opt.unwrap_or(Radius(1.0))); + arch = arch.with_radii(radii); + } + if labels.iter().any(|opt| opt.is_some()) { + // If some labels have been specified but not others, default the unspecified ones to "undef". + let labels = labels + .into_iter() + .map(|opt| opt.unwrap_or(Text("undef".into()))); + arch = arch.with_labels(labels); + } + + arch +} diff --git a/deny.toml b/deny.toml index f985979d014d..f0c763bb33c6 100644 --- a/deny.toml +++ b/deny.toml @@ -48,6 +48,7 @@ deny = [ skip = [ { name = "ahash" }, # Popular crate + fast release schedule = lots of crates still using old versions { name = "base64" }, # Too popular + { name = "cargo_metadata" }, # Older version used by ply-rs. It's small, and it's build-time only! { name = "foreign-types" }, # used for cocoa bindings. wgpu uses newer than eframe. { name = "foreign-types-shared" }, # used for cocoa bindings. wgpu uses newer than eframe. { name = "hashbrown" }, # Old version used by polar-rs diff --git a/examples/assets/example.ply b/examples/assets/example.ply new file mode 100644 index 000000000000..be5aeaa80585 --- /dev/null +++ b/examples/assets/example.ply @@ -0,0 +1,51 @@ +ply +comment Source: . +format ascii 1.0 +element vertex 36 +property float x +property float y +property float z +property float nx +property float ny +property float nz +property float intensity +property uchar red +property uchar green +property uchar blue +end_header +13.32 12.84 3.06 -0.352745 -0.230493 0.906887 1 255 243 245 +13.44 12.84 3.06 0.135449 -0.0514792 0.989446 1 255 244 242 +13.56 12.84 3.024 0.26937 -0.00564067 0.96302 0.988235 252 232 218 +13.68 12.84 3 0.023836 0.341656 0.939523 0.980392 250 222 199 +13.8 12.84 3 0.17265 0.491562 0.853557 0.980392 250 222 196 +13.92 12.84 2.94 0.271418 0.421348 0.865331 0.960784 245 205 181 +13.32 12.96 3.048 -0.113205 -0.0506456 0.99228 0.996078 254 240 240 +13.44 12.96 3.036 0.316732 0.242606 0.916964 0.992157 253 232 220 +13.56 12.96 2.976 0.305802 0.351274 0.884925 0.972549 248 217 195 +13.68 12.96 2.94 0.123776 0.286629 0.950012 0.960784 245 207 182 +13.8 12.96 2.928 0.166562 0.236305 0.957297 0.956863 244 199 175 +13.92 12.96 2.916 0.300154 0.375882 0.87671 0.952941 243 173 147 +13.32 13.08 3.06 0.256866 0.12211 0.958702 1 255 236 225 +13.44 13.08 2.976 0.445059 0.33052 0.832274 0.972549 248 217 195 +13.56 13.08 2.94 0.108126 0.123257 0.986467 0.960784 245 197 168 +13.68 13.08 2.94 0.154002 0.120018 0.980754 0.960784 245 185 157 +13.8 13.08 2.916 0.347761 0.334486 0.875889 0.952941 243 169 149 +13.92 13.08 2.796 0.601865 0.623676 0.498785 0.913725 233 144 121 +13.32 13.2 3.024 0.468064 0.297601 0.832075 0.988235 252 228 206 +13.44 13.2 2.928 0.257091 0.200496 0.94536 0.956863 244 207 178 +13.56 13.2 2.952 0.005533 -0.0206843 0.999771 0.964706 246 190 155 +13.68 13.2 2.892 0.407234 0.363634 0.837813 0.945098 241 161 133 +13.8 13.2 2.856 0.559132 0.580269 0.592165 0.933333 238 144 115 +13.92 13.2 2.556 0.724107 0.541258 0.427443 0.835294 213 119 99 +13.32 13.32 2.964 0.398726 0.298406 0.867163 0.968627 247 215 189 +13.44 13.32 2.916 -0.0611859 -0.0315522 0.997628 0.952941 243 198 169 +13.56 13.32 2.976 0.225468 0.0427099 0.973314 0.972549 248 181 148 +13.68 13.32 2.784 0.653282 0.527581 0.543029 0.909804 232 144 116 +13.8 13.32 2.664 0.648986 0.607185 0.458414 0.870588 222 124 101 +13.92 13.32 2.484 0.71337 0.436706 0.54808 0.811765 207 105 86 +13.32 13.44 2.94 0.11495 -0.00345421 0.993365 0.960784 245 197 166 +13.44 13.44 2.94 -0.124913 -0.113648 0.985637 0.960784 245 191 161 +13.56 13.44 2.952 0.517879 0.102231 0.849323 0.964706 246 170 139 +13.68 13.44 2.64 0.825371 0.336651 0.453242 0.862745 220 128 104 +13.8 13.44 2.472 0.631464 0.52381 0.571731 0.807843 206 110 92 +13.92 13.44 2.412 0.592844 0.469536 0.654272 0.788235 201 89 72