diff --git a/Cargo.lock b/Cargo.lock index 962d25303729..29ec8d763cef 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5572,6 +5572,7 @@ dependencies = [ "re_chunk_store", "re_data_ui", "re_log_types", + "re_query", "re_renderer", "re_space_view", "re_tracing", diff --git a/crates/store/re_types/definitions/rerun/archetypes/depth_image.fbs b/crates/store/re_types/definitions/rerun/archetypes/depth_image.fbs index 8c2365b73ac7..24d63fa7c9e3 100644 --- a/crates/store/re_types/definitions/rerun/archetypes/depth_image.fbs +++ b/crates/store/re_types/definitions/rerun/archetypes/depth_image.fbs @@ -5,7 +5,7 @@ namespace rerun.archetypes; /// /// Each pixel corresponds to a depth value in units specified by [components.DepthMeter]. /// -/// \cpp Since the underlying `rerun::datatypes::TensorData` uses `rerun::Collection` internally, +/// \cpp Since the underlying `rerun::datatypes::ImageBuffer` uses `rerun::Collection` internally, /// \cpp data can be passed in without a copy from raw pointers or by reference from `std::vector`/`std::array`/c-arrays. /// \cpp If needed, this "borrow-behavior" can be extended by defining your own `rerun::CollectionAdapter`. /// @@ -41,6 +41,19 @@ table DepthImage ( /// If not set, the depth image will be rendered using the Turbo colormap. colormap: rerun.components.Colormap ("attr.rerun.component_optional", nullable, order: 3200); + /// The expected range of depth values. + /// + /// This is typically the expected range of valid values. + /// Everything outside of the range is clamped to the range for the purpose of colormpaping. + /// Note that point clouds generated from this image will still display all points, regardless of this range. + /// + /// If not specified, the range will be automatically estimated from the data. + /// Note that the Viewer may try to guess a wider range than the minimum/maximum of values + /// in the contents of the depth image. + /// E.g. if all values are positive, some bigger than 1.0 and all smaller than 255.0, + /// the Viewer will guess that the data likely came from an 8bit image, thus assuming a range of 0-255. + depth_range: rerun.components.ValueRange ("attr.rerun.component_optional", nullable, order: 3300); + /// Scale the radii of the points in the point cloud generated from this image. /// /// A fill ratio of 1.0 (the default) means that each point is as big as to touch the center of its neighbor @@ -48,10 +61,10 @@ table DepthImage ( /// A fill ratio of 0.5 means that each point touches the edge of its neighbor if it has the same depth. /// /// TODO(#6744): This applies only to 3D views! - point_fill_ratio: rerun.components.FillRatio ("attr.rerun.component_optional", nullable, order: 3300); + point_fill_ratio: rerun.components.FillRatio ("attr.rerun.component_optional", nullable, order: 3400); /// An optional floating point value that specifies the 2D drawing order, used only if the depth image is shown as a 2D image. /// /// Objects with higher values are drawn on top of those with lower values. - draw_order: rerun.components.DrawOrder ("attr.rerun.component_optional", nullable, order: 3400); + draw_order: rerun.components.DrawOrder ("attr.rerun.component_optional", nullable, order: 3500); } diff --git a/crates/store/re_types/definitions/rerun/archetypes/tensor.fbs b/crates/store/re_types/definitions/rerun/archetypes/tensor.fbs index abf8e5150b21..00ea0344f112 100644 --- a/crates/store/re_types/definitions/rerun/archetypes/tensor.fbs +++ b/crates/store/re_types/definitions/rerun/archetypes/tensor.fbs @@ -19,4 +19,19 @@ table Tensor ( ) { /// The tensor data data: rerun.components.TensorData ("attr.rerun.component_required", order: 1000); + + // --- Optional --- + + /// The expected range of values. + /// + /// This is typically the expected range of valid values. + /// Everything outside of the range is clamped to the range for the purpose of colormpaping. + /// Any colormap applied for display, will map this range. + /// + /// If not specified, the range will be automatically estimated from the data. + /// Note that the Viewer may try to guess a wider range than the minimum/maximum of values + /// in the contents of the tensor. + /// E.g. if all values are positive, some bigger than 1.0 and all smaller than 255.0, + /// the Viewer will guess that the data likely came from an 8bit image, thus assuming a range of 0-255. + value_range: rerun.components.ValueRange ("attr.rerun.component_optional", nullable, order: 2000); } diff --git a/crates/store/re_types/definitions/rerun/blueprint/archetypes/tensor_scalar_mapping.fbs b/crates/store/re_types/definitions/rerun/blueprint/archetypes/tensor_scalar_mapping.fbs index def7226b2ef4..ff772e6abdfc 100644 --- a/crates/store/re_types/definitions/rerun/blueprint/archetypes/tensor_scalar_mapping.fbs +++ b/crates/store/re_types/definitions/rerun/blueprint/archetypes/tensor_scalar_mapping.fbs @@ -17,10 +17,8 @@ table TensorScalarMapping ( /// /// Raises the normalized values to the power of this value before mapping to color. /// Acts like an inverse brightness. Defaults to 1.0. + /// + /// The final value for display is set as: + /// `colormap( ((value - data_display_range.min) / (data_display_range.max - data_display_range.min)) ** gamma )` gamma: rerun.components.GammaCorrection ("attr.rerun.component_optional", nullable, order: 1200); - - // TODO(andreas): explicit scalar ranges should go in here as well! - // Overall we should communicate scalar mapping to work like this here - // https://matplotlib.org/stable/api/_as_gen/matplotlib.colors.PowerNorm.html#matplotlib.colors.PowerNorm - // (value - vmin) ** gamma / (vmax - vmin) ** gamma } diff --git a/crates/store/re_types/definitions/rerun/components.fbs b/crates/store/re_types/definitions/rerun/components.fbs index b5916c757898..b212c946a50d 100644 --- a/crates/store/re_types/definitions/rerun/components.fbs +++ b/crates/store/re_types/definitions/rerun/components.fbs @@ -51,6 +51,7 @@ include "./components/transform_mat3x3.fbs"; include "./components/transform_relation.fbs"; include "./components/translation3d.fbs"; include "./components/triangle_indices.fbs"; +include "./components/value_range.fbs"; include "./components/vector2d.fbs"; include "./components/vector3d.fbs"; include "./components/video_timestamp.fbs"; diff --git a/crates/store/re_types/definitions/rerun/components/value_range.fbs b/crates/store/re_types/definitions/rerun/components/value_range.fbs new file mode 100644 index 000000000000..b238f2be856a --- /dev/null +++ b/crates/store/re_types/definitions/rerun/components/value_range.fbs @@ -0,0 +1,11 @@ +namespace rerun.components; + +// --- + +/// Range of expected or valid values, specifying a lower and upper bound. +struct ValueRange ( + "attr.rust.derive": "Copy, PartialEq, bytemuck::Pod, bytemuck::Zeroable", + "attr.rust.repr": "transparent" +) { + range: rerun.datatypes.Range1D (order: 100); +} diff --git a/crates/store/re_types/src/archetypes/depth_image.rs b/crates/store/re_types/src/archetypes/depth_image.rs index e446993e3328..9032c30bd030 100644 --- a/crates/store/re_types/src/archetypes/depth_image.rs +++ b/crates/store/re_types/src/archetypes/depth_image.rs @@ -86,6 +86,19 @@ pub struct DepthImage { /// If not set, the depth image will be rendered using the Turbo colormap. pub colormap: Option, + /// The expected range of depth values. + /// + /// This is typically the expected range of valid values. + /// Everything outside of the range is clamped to the range for the purpose of colormpaping. + /// Note that point clouds generated from this image will still display all points, regardless of this range. + /// + /// If not specified, the range will be automatically estimated from the data. + /// Note that the Viewer may try to guess a wider range than the minimum/maximum of values + /// in the contents of the depth image. + /// E.g. if all values are positive, some bigger than 1.0 and all smaller than 255.0, + /// the Viewer will guess that the data likely came from an 8bit image, thus assuming a range of 0-255. + pub depth_range: Option, + /// Scale the radii of the points in the point cloud generated from this image. /// /// A fill ratio of 1.0 (the default) means that each point is as big as to touch the center of its neighbor @@ -108,6 +121,7 @@ impl ::re_types_core::SizeBytes for DepthImage { + self.format.heap_size_bytes() + self.meter.heap_size_bytes() + self.colormap.heap_size_bytes() + + self.depth_range.heap_size_bytes() + self.point_fill_ratio.heap_size_bytes() + self.draw_order.heap_size_bytes() } @@ -118,6 +132,7 @@ impl ::re_types_core::SizeBytes for DepthImage { && ::is_pod() && >::is_pod() && >::is_pod() + && >::is_pod() && >::is_pod() && >::is_pod() } @@ -134,17 +149,18 @@ static REQUIRED_COMPONENTS: once_cell::sync::Lazy<[ComponentName; 2usize]> = static RECOMMENDED_COMPONENTS: once_cell::sync::Lazy<[ComponentName; 1usize]> = once_cell::sync::Lazy::new(|| ["rerun.components.DepthImageIndicator".into()]); -static OPTIONAL_COMPONENTS: once_cell::sync::Lazy<[ComponentName; 4usize]> = +static OPTIONAL_COMPONENTS: once_cell::sync::Lazy<[ComponentName; 5usize]> = once_cell::sync::Lazy::new(|| { [ "rerun.components.DepthMeter".into(), "rerun.components.Colormap".into(), + "rerun.components.ValueRange".into(), "rerun.components.FillRatio".into(), "rerun.components.DrawOrder".into(), ] }); -static ALL_COMPONENTS: once_cell::sync::Lazy<[ComponentName; 7usize]> = +static ALL_COMPONENTS: once_cell::sync::Lazy<[ComponentName; 8usize]> = once_cell::sync::Lazy::new(|| { [ "rerun.components.ImageBuffer".into(), @@ -152,14 +168,15 @@ static ALL_COMPONENTS: once_cell::sync::Lazy<[ComponentName; 7usize]> = "rerun.components.DepthImageIndicator".into(), "rerun.components.DepthMeter".into(), "rerun.components.Colormap".into(), + "rerun.components.ValueRange".into(), "rerun.components.FillRatio".into(), "rerun.components.DrawOrder".into(), ] }); impl DepthImage { - /// The total number of components in the archetype: 2 required, 1 recommended, 4 optional - pub const NUM_COMPONENTS: usize = 7usize; + /// The total number of components in the archetype: 2 required, 1 recommended, 5 optional + pub const NUM_COMPONENTS: usize = 8usize; } /// Indicator component for the [`DepthImage`] [`::re_types_core::Archetype`] @@ -258,6 +275,15 @@ impl ::re_types_core::Archetype for DepthImage { } else { None }; + let depth_range = if let Some(array) = arrays_by_name.get("rerun.components.ValueRange") { + ::from_arrow_opt(&**array) + .with_context("rerun.archetypes.DepthImage#depth_range")? + .into_iter() + .next() + .flatten() + } else { + None + }; let point_fill_ratio = if let Some(array) = arrays_by_name.get("rerun.components.FillRatio") { ::from_arrow_opt(&**array) @@ -282,6 +308,7 @@ impl ::re_types_core::Archetype for DepthImage { format, meter, colormap, + depth_range, point_fill_ratio, draw_order, }) @@ -302,6 +329,9 @@ impl ::re_types_core::AsComponents for DepthImage { self.colormap .as_ref() .map(|comp| (comp as &dyn ComponentBatch).into()), + self.depth_range + .as_ref() + .map(|comp| (comp as &dyn ComponentBatch).into()), self.point_fill_ratio .as_ref() .map(|comp| (comp as &dyn ComponentBatch).into()), @@ -329,6 +359,7 @@ impl DepthImage { format: format.into(), meter: None, colormap: None, + depth_range: None, point_fill_ratio: None, draw_order: None, } @@ -356,6 +387,26 @@ impl DepthImage { self } + /// The expected range of depth values. + /// + /// This is typically the expected range of valid values. + /// Everything outside of the range is clamped to the range for the purpose of colormpaping. + /// Note that point clouds generated from this image will still display all points, regardless of this range. + /// + /// If not specified, the range will be automatically estimated from the data. + /// Note that the Viewer may try to guess a wider range than the minimum/maximum of values + /// in the contents of the depth image. + /// E.g. if all values are positive, some bigger than 1.0 and all smaller than 255.0, + /// the Viewer will guess that the data likely came from an 8bit image, thus assuming a range of 0-255. + #[inline] + pub fn with_depth_range( + mut self, + depth_range: impl Into, + ) -> Self { + self.depth_range = Some(depth_range.into()); + self + } + /// Scale the radii of the points in the point cloud generated from this image. /// /// A fill ratio of 1.0 (the default) means that each point is as big as to touch the center of its neighbor diff --git a/crates/store/re_types/src/archetypes/depth_image_ext.rs b/crates/store/re_types/src/archetypes/depth_image_ext.rs index a1ab8eac0dde..242d99c8eb3f 100644 --- a/crates/store/re_types/src/archetypes/depth_image_ext.rs +++ b/crates/store/re_types/src/archetypes/depth_image_ext.rs @@ -42,6 +42,7 @@ impl DepthImage { meter: None, colormap: None, point_fill_ratio: None, + depth_range: None, }) } } diff --git a/crates/store/re_types/src/archetypes/tensor.rs b/crates/store/re_types/src/archetypes/tensor.rs index b5e277447d63..5d8ad391a76a 100644 --- a/crates/store/re_types/src/archetypes/tensor.rs +++ b/crates/store/re_types/src/archetypes/tensor.rs @@ -52,17 +52,31 @@ use ::re_types_core::{DeserializationError, DeserializationResult}; pub struct Tensor { /// The tensor data pub data: crate::components::TensorData, + + /// The expected range of values. + /// + /// This is typically the expected range of valid values. + /// Everything outside of the range is clamped to the range for the purpose of colormpaping. + /// Any colormap applied for display, will map this range. + /// + /// If not specified, the range will be automatically estimated from the data. + /// Note that the Viewer may try to guess a wider range than the minimum/maximum of values + /// in the contents of the tensor. + /// E.g. if all values are positive, some bigger than 1.0 and all smaller than 255.0, + /// the Viewer will guess that the data likely came from an 8bit image, thus assuming a range of 0-255. + pub value_range: Option, } impl ::re_types_core::SizeBytes for Tensor { #[inline] fn heap_size_bytes(&self) -> u64 { - self.data.heap_size_bytes() + self.data.heap_size_bytes() + self.value_range.heap_size_bytes() } #[inline] fn is_pod() -> bool { ::is_pod() + && >::is_pod() } } @@ -72,20 +86,21 @@ static REQUIRED_COMPONENTS: once_cell::sync::Lazy<[ComponentName; 1usize]> = static RECOMMENDED_COMPONENTS: once_cell::sync::Lazy<[ComponentName; 1usize]> = once_cell::sync::Lazy::new(|| ["rerun.components.TensorIndicator".into()]); -static OPTIONAL_COMPONENTS: once_cell::sync::Lazy<[ComponentName; 0usize]> = - once_cell::sync::Lazy::new(|| []); +static OPTIONAL_COMPONENTS: once_cell::sync::Lazy<[ComponentName; 1usize]> = + once_cell::sync::Lazy::new(|| ["rerun.components.ValueRange".into()]); -static ALL_COMPONENTS: once_cell::sync::Lazy<[ComponentName; 2usize]> = +static ALL_COMPONENTS: once_cell::sync::Lazy<[ComponentName; 3usize]> = once_cell::sync::Lazy::new(|| { [ "rerun.components.TensorData".into(), "rerun.components.TensorIndicator".into(), + "rerun.components.ValueRange".into(), ] }); impl Tensor { - /// The total number of components in the archetype: 1 required, 1 recommended, 0 optional - pub const NUM_COMPONENTS: usize = 2usize; + /// The total number of components in the archetype: 1 required, 1 recommended, 1 optional + pub const NUM_COMPONENTS: usize = 3usize; } /// Indicator component for the [`Tensor`] [`::re_types_core::Archetype`] @@ -153,7 +168,16 @@ impl ::re_types_core::Archetype for Tensor { .ok_or_else(DeserializationError::missing_data) .with_context("rerun.archetypes.Tensor#data")? }; - Ok(Self { data }) + let value_range = if let Some(array) = arrays_by_name.get("rerun.components.ValueRange") { + ::from_arrow_opt(&**array) + .with_context("rerun.archetypes.Tensor#value_range")? + .into_iter() + .next() + .flatten() + } else { + None + }; + Ok(Self { data, value_range }) } } @@ -164,6 +188,9 @@ impl ::re_types_core::AsComponents for Tensor { [ Some(Self::indicator()), Some((&self.data as &dyn ComponentBatch).into()), + self.value_range + .as_ref() + .map(|comp| (comp as &dyn ComponentBatch).into()), ] .into_iter() .flatten() @@ -177,6 +204,29 @@ impl Tensor { /// Create a new `Tensor`. #[inline] pub fn new(data: impl Into) -> Self { - Self { data: data.into() } + Self { + data: data.into(), + value_range: None, + } + } + + /// The expected range of values. + /// + /// This is typically the expected range of valid values. + /// Everything outside of the range is clamped to the range for the purpose of colormpaping. + /// Any colormap applied for display, will map this range. + /// + /// If not specified, the range will be automatically estimated from the data. + /// Note that the Viewer may try to guess a wider range than the minimum/maximum of values + /// in the contents of the tensor. + /// E.g. if all values are positive, some bigger than 1.0 and all smaller than 255.0, + /// the Viewer will guess that the data likely came from an 8bit image, thus assuming a range of 0-255. + #[inline] + pub fn with_value_range( + mut self, + value_range: impl Into, + ) -> Self { + self.value_range = Some(value_range.into()); + self } } diff --git a/crates/store/re_types/src/archetypes/tensor_ext.rs b/crates/store/re_types/src/archetypes/tensor_ext.rs index 9be45f97550b..c7063b0b6264 100644 --- a/crates/store/re_types/src/archetypes/tensor_ext.rs +++ b/crates/store/re_types/src/archetypes/tensor_ext.rs @@ -16,7 +16,10 @@ impl Tensor { pub fn try_from>(data: T) -> Result { let data: TensorData = data.try_into()?; - Ok(Self { data: data.into() }) + Ok(Self { + data: data.into(), + value_range: None, + }) } /// Update the `names` of the contained [`TensorData`] dimensions. @@ -50,6 +53,7 @@ impl Tensor { buffer: self.data.0.buffer, } .into(), + value_range: None, } } } @@ -62,7 +66,10 @@ impl Tensor { pub fn from_image( image: impl Into, ) -> Result { - TensorData::from_image(image).map(|data| Self { data: data.into() }) + TensorData::from_image(image).map(|data| Self { + data: data.into(), + value_range: None, + }) } /// Construct a tensor from [`image::DynamicImage`]. @@ -71,7 +78,10 @@ impl Tensor { pub fn from_dynamic_image( image: image::DynamicImage, ) -> Result { - TensorData::from_dynamic_image(image).map(|data| Self { data: data.into() }) + TensorData::from_dynamic_image(image).map(|data| Self { + data: data.into(), + value_range: None, + }) } } diff --git a/crates/store/re_types/src/blueprint/archetypes/tensor_scalar_mapping.rs b/crates/store/re_types/src/blueprint/archetypes/tensor_scalar_mapping.rs index 3192b0a4a708..a0877142b5a2 100644 --- a/crates/store/re_types/src/blueprint/archetypes/tensor_scalar_mapping.rs +++ b/crates/store/re_types/src/blueprint/archetypes/tensor_scalar_mapping.rs @@ -33,6 +33,9 @@ pub struct TensorScalarMapping { /// /// Raises the normalized values to the power of this value before mapping to color. /// Acts like an inverse brightness. Defaults to 1.0. + /// + /// The final value for display is set as: + /// `colormap( ((value - data_display_range.min) / (data_display_range.max - data_display_range.min)) ** gamma )` pub gamma: Option, } @@ -231,6 +234,9 @@ impl TensorScalarMapping { /// /// Raises the normalized values to the power of this value before mapping to color. /// Acts like an inverse brightness. Defaults to 1.0. + /// + /// The final value for display is set as: + /// `colormap( ((value - data_display_range.min) / (data_display_range.max - data_display_range.min)) ** gamma )` #[inline] pub fn with_gamma(mut self, gamma: impl Into) -> Self { self.gamma = Some(gamma.into()); diff --git a/crates/store/re_types/src/components/.gitattributes b/crates/store/re_types/src/components/.gitattributes index ce1cd46d7000..bfc236554ca9 100644 --- a/crates/store/re_types/src/components/.gitattributes +++ b/crates/store/re_types/src/components/.gitattributes @@ -59,6 +59,7 @@ transform_mat3x3.rs linguist-generated=true transform_relation.rs linguist-generated=true translation3d.rs linguist-generated=true triangle_indices.rs linguist-generated=true +value_range.rs linguist-generated=true vector2d.rs linguist-generated=true vector3d.rs linguist-generated=true video_timestamp.rs linguist-generated=true diff --git a/crates/store/re_types/src/components/mod.rs b/crates/store/re_types/src/components/mod.rs index e65ab7eed0c0..b2010e7147e2 100644 --- a/crates/store/re_types/src/components/mod.rs +++ b/crates/store/re_types/src/components/mod.rs @@ -102,6 +102,8 @@ mod translation3d; mod translation3d_ext; mod triangle_indices; mod triangle_indices_ext; +mod value_range; +mod value_range_ext; mod vector2d; mod vector2d_ext; mod vector3d; @@ -168,6 +170,7 @@ pub use self::transform_mat3x3::TransformMat3x3; pub use self::transform_relation::TransformRelation; pub use self::translation3d::Translation3D; pub use self::triangle_indices::TriangleIndices; +pub use self::value_range::ValueRange; pub use self::vector2d::Vector2D; pub use self::vector3d::Vector3D; pub use self::video_timestamp::VideoTimestamp; diff --git a/crates/store/re_types/src/components/range1d_ext.rs b/crates/store/re_types/src/components/range1d_ext.rs index 9ff1fe448295..7ca451ff1df5 100644 --- a/crates/store/re_types/src/components/range1d_ext.rs +++ b/crates/store/re_types/src/components/range1d_ext.rs @@ -35,13 +35,6 @@ impl Range1D { } } -impl From for emath::Rangef { - #[inline] - fn from(range2d: Range1D) -> Self { - Self::from(range2d.0) - } -} - impl Display for Range1D { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "[{}, {}]", self.start(), self.end(),) diff --git a/crates/store/re_types/src/components/value_range.rs b/crates/store/re_types/src/components/value_range.rs new file mode 100644 index 000000000000..b5c44c0a28ff --- /dev/null +++ b/crates/store/re_types/src/components/value_range.rs @@ -0,0 +1,113 @@ +// DO NOT EDIT! This file was auto-generated by crates/build/re_types_builder/src/codegen/rust/api.rs +// Based on "crates/store/re_types/definitions/rerun/components/value_range.fbs". + +#![allow(unused_imports)] +#![allow(unused_parens)] +#![allow(clippy::clone_on_copy)] +#![allow(clippy::cloned_instead_of_copied)] +#![allow(clippy::map_flatten)] +#![allow(clippy::needless_question_mark)] +#![allow(clippy::new_without_default)] +#![allow(clippy::redundant_closure)] +#![allow(clippy::too_many_arguments)] +#![allow(clippy::too_many_lines)] + +use ::re_types_core::external::arrow2; +use ::re_types_core::ComponentName; +use ::re_types_core::SerializationResult; +use ::re_types_core::{ComponentBatch, MaybeOwnedComponentBatch}; +use ::re_types_core::{DeserializationError, DeserializationResult}; + +/// **Component**: Range of expected or valid values, specifying a lower and upper bound. +#[derive(Clone, Debug, Copy, PartialEq, bytemuck::Pod, bytemuck::Zeroable)] +#[repr(transparent)] +pub struct ValueRange(pub crate::datatypes::Range1D); + +impl ::re_types_core::SizeBytes for ValueRange { + #[inline] + fn heap_size_bytes(&self) -> u64 { + self.0.heap_size_bytes() + } + + #[inline] + fn is_pod() -> bool { + ::is_pod() + } +} + +impl> From for ValueRange { + fn from(v: T) -> Self { + Self(v.into()) + } +} + +impl std::borrow::Borrow for ValueRange { + #[inline] + fn borrow(&self) -> &crate::datatypes::Range1D { + &self.0 + } +} + +impl std::ops::Deref for ValueRange { + type Target = crate::datatypes::Range1D; + + #[inline] + fn deref(&self) -> &crate::datatypes::Range1D { + &self.0 + } +} + +impl std::ops::DerefMut for ValueRange { + #[inline] + fn deref_mut(&mut self) -> &mut crate::datatypes::Range1D { + &mut self.0 + } +} + +::re_types_core::macros::impl_into_cow!(ValueRange); + +impl ::re_types_core::Loggable for ValueRange { + type Name = ::re_types_core::ComponentName; + + #[inline] + fn name() -> Self::Name { + "rerun.components.ValueRange".into() + } + + #[inline] + fn arrow_datatype() -> arrow2::datatypes::DataType { + crate::datatypes::Range1D::arrow_datatype() + } + + fn to_arrow_opt<'a>( + data: impl IntoIterator>>>, + ) -> SerializationResult> + where + Self: Clone + 'a, + { + crate::datatypes::Range1D::to_arrow_opt(data.into_iter().map(|datum| { + datum.map(|datum| match datum.into() { + ::std::borrow::Cow::Borrowed(datum) => ::std::borrow::Cow::Borrowed(&datum.0), + ::std::borrow::Cow::Owned(datum) => ::std::borrow::Cow::Owned(datum.0), + }) + })) + } + + fn from_arrow_opt( + arrow_data: &dyn arrow2::array::Array, + ) -> DeserializationResult>> + where + Self: Sized, + { + crate::datatypes::Range1D::from_arrow_opt(arrow_data) + .map(|v| v.into_iter().map(|v| v.map(Self)).collect()) + } + + #[inline] + fn from_arrow(arrow_data: &dyn arrow2::array::Array) -> DeserializationResult> + where + Self: Sized, + { + crate::datatypes::Range1D::from_arrow(arrow_data).map(bytemuck::cast_vec) + } +} diff --git a/crates/store/re_types/src/components/value_range_ext.rs b/crates/store/re_types/src/components/value_range_ext.rs new file mode 100644 index 000000000000..90b3f103d930 --- /dev/null +++ b/crates/store/re_types/src/components/value_range_ext.rs @@ -0,0 +1,49 @@ +use crate::datatypes; +use std::fmt::Display; + +use super::ValueRange; + +impl ValueRange { + /// Create a new range. + #[inline] + pub fn new(start: f64, end: f64) -> Self { + Self(datatypes::Range1D([start, end])) + } + + /// The start of the range. + #[inline] + pub fn start(&self) -> f64 { + self.0 .0[0] + } + + /// The end of the range. + #[inline] + pub fn end(&self) -> f64 { + self.0 .0[1] + } + + /// The start of the range. + #[inline] + pub fn start_mut(&mut self) -> &mut f64 { + &mut self.0 .0[0] + } + + /// The end of the range. + #[inline] + pub fn end_mut(&mut self) -> &mut f64 { + &mut self.0 .0[1] + } +} + +impl Display for ValueRange { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "[{}, {}]", self.start(), self.end(),) + } +} + +impl Default for ValueRange { + #[inline] + fn default() -> Self { + Self::new(0.0, 1.0) + } +} diff --git a/crates/store/re_types/src/datatypes/channel_datatype_ext.rs b/crates/store/re_types/src/datatypes/channel_datatype_ext.rs index d4dfb0104196..d0991c90a68c 100644 --- a/crates/store/re_types/src/datatypes/channel_datatype_ext.rs +++ b/crates/store/re_types/src/datatypes/channel_datatype_ext.rs @@ -46,7 +46,7 @@ impl ChannelDatatype { } } - /// What is the minimum value representable by this datatype? + /// What is the minimum finite value representable by this datatype? #[inline] pub fn min_value(&self) -> f64 { match self { @@ -66,7 +66,7 @@ impl ChannelDatatype { } } - /// What is the maximum value representable by this datatype? + /// What is the maximum finite value representable by this datatype? #[inline] pub fn max_value(&self) -> f64 { match self { diff --git a/crates/store/re_types/src/tensor_data.rs b/crates/store/re_types/src/tensor_data.rs index 6b299f6678bc..08b0d2d5b67e 100644 --- a/crates/store/re_types/src/tensor_data.rs +++ b/crates/store/re_types/src/tensor_data.rs @@ -146,7 +146,7 @@ impl TensorDataType { } } - /// What is the minimum value representable by this datatype? + /// What is the minimum finite value representable by this datatype? #[inline] pub fn min_value(&self) -> f64 { match self { @@ -166,7 +166,7 @@ impl TensorDataType { } } - /// What is the maximum value representable by this datatype? + /// What is the maximum finite value representable by this datatype? #[inline] pub fn max_value(&self) -> f64 { match self { diff --git a/crates/store/re_types/tests/types/depth_image.rs b/crates/store/re_types/tests/types/depth_image.rs index 63d443304c56..f43b836472b2 100644 --- a/crates/store/re_types/tests/types/depth_image.rs +++ b/crates/store/re_types/tests/types/depth_image.rs @@ -26,6 +26,7 @@ fn depth_image_roundtrip() { draw_order: None, colormap: None, point_fill_ratio: None, + depth_range: None, }]; let all_arch_serialized = [ diff --git a/crates/store/re_types/tests/types/tensor.rs b/crates/store/re_types/tests/types/tensor.rs index 8c72dfb4611d..d440b7fd3b85 100644 --- a/crates/store/re_types/tests/types/tensor.rs +++ b/crates/store/re_types/tests/types/tensor.rs @@ -26,6 +26,7 @@ fn tensor_roundtrip() { buffer: TensorBuffer::U8(vec![1, 2, 3, 4, 5, 6].into()), } .into(), + value_range: None, }]; let all_arch_serialized = [Tensor::try_from(ndarray::array![[1u8, 2, 3], [4, 5, 6]]) diff --git a/crates/viewer/re_component_ui/src/datatype_uis/mod.rs b/crates/viewer/re_component_ui/src/datatype_uis/mod.rs index 32f9a76078c1..7d40859af397 100644 --- a/crates/viewer/re_component_ui/src/datatype_uis/mod.rs +++ b/crates/viewer/re_component_ui/src/datatype_uis/mod.rs @@ -1,6 +1,7 @@ mod bool_toggle; mod enum_combobox; mod float_drag; +mod range1d; mod singleline_string; mod vec; mod view_id; @@ -8,6 +9,7 @@ mod view_id; pub use bool_toggle::edit_bool; pub use enum_combobox::edit_view_enum; pub use float_drag::{edit_f32_min_to_max_float, edit_f32_zero_to_max, edit_f32_zero_to_one}; +pub use range1d::edit_view_range1d; pub use singleline_string::{ display_name_ui, display_text_ui, edit_multiline_string, edit_singleline_string, }; diff --git a/crates/viewer/re_component_ui/src/range1d.rs b/crates/viewer/re_component_ui/src/datatype_uis/range1d.rs similarity index 65% rename from crates/viewer/re_component_ui/src/range1d.rs rename to crates/viewer/re_component_ui/src/datatype_uis/range1d.rs index 5c1adef2817e..a1f109085f7e 100644 --- a/crates/viewer/re_component_ui/src/range1d.rs +++ b/crates/viewer/re_component_ui/src/datatype_uis/range1d.rs @@ -1,15 +1,26 @@ use egui::NumExt as _; -use re_types::components::Range1D; +use re_types::datatypes::Range1D; use re_viewer_context::MaybeMutRef; -pub fn edit_range1d( +pub fn edit_view_range1d( _ctx: &re_viewer_context::ViewerContext<'_>, + ui: &mut egui::Ui, + value: &mut MaybeMutRef<'_, impl std::ops::DerefMut>, +) -> egui::Response { + let mut value: MaybeMutRef<'_, Range1D> = match value { + MaybeMutRef::Ref(value) => MaybeMutRef::Ref(value), + MaybeMutRef::MutRef(value) => MaybeMutRef::MutRef(value), + }; + edit_view_range1d_impl(ui, &mut value) +} + +fn edit_view_range1d_impl( ui: &mut egui::Ui, value: &mut MaybeMutRef<'_, Range1D>, ) -> egui::Response { if let Some(value) = value.as_mut() { - let [min, max] = &mut value.0 .0; + let [min, max] = &mut value.0; let range = (*max - *min).abs(); let speed = (range * 0.01).at_least(0.001); @@ -29,7 +40,7 @@ pub fn edit_range1d( response_min | response_max } else { - let [min, max] = value.0 .0; + let [min, max] = value.0; ui.label(format!( "{} - {}", re_format::format_f64(min), diff --git a/crates/viewer/re_component_ui/src/lib.rs b/crates/viewer/re_component_ui/src/lib.rs index 95102cc3ab08..dd390bc96c3d 100644 --- a/crates/viewer/re_component_ui/src/lib.rs +++ b/crates/viewer/re_component_ui/src/lib.rs @@ -12,7 +12,6 @@ mod line_strip; mod marker_shape; mod pinhole; mod radius; -mod range1d; mod resolution; mod response_utils; mod timeline; @@ -24,7 +23,7 @@ mod visual_bounds2d; use datatype_uis::{ display_name_ui, display_text_ui, edit_bool, edit_f32_min_to_max_float, edit_f32_zero_to_max, edit_f32_zero_to_one, edit_multiline_string, edit_or_view_vec3d, edit_singleline_string, - edit_view_enum, view_view_id, + edit_view_enum, edit_view_range1d, view_view_id, }; use re_types::{ @@ -32,7 +31,8 @@ use re_types::{ components::{ AggregationPolicy, AlbedoFactor, AxisLength, Color, DepthMeter, DrawOrder, FillMode, FillRatio, GammaCorrection, ImagePlaneDistance, MagnificationFilter, MarkerSize, Name, - Opacity, Scale3D, ShowLabels, StrokeWidth, Text, TransformRelation, Translation3D, + Opacity, Range1D, Scale3D, ShowLabels, StrokeWidth, Text, TransformRelation, Translation3D, + ValueRange, }, Loggable as _, }; @@ -102,6 +102,10 @@ pub fn create_component_ui_registry() -> re_viewer_context::ComponentUiRegistry registry.add_singleline_edit_or_view::(view_view_id); registry.add_singleline_edit_or_view::(view_view_id); + // Range1D components: + registry.add_singleline_edit_or_view::(edit_view_range1d); + registry.add_singleline_edit_or_view::(edit_view_range1d); + // -------------------------------------------------------------------------------- // All other special components: // -------------------------------------------------------------------------------- @@ -123,7 +127,6 @@ pub fn create_component_ui_registry() -> re_viewer_context::ComponentUiRegistry registry.add_singleline_edit_or_view(radius::edit_radius_ui); registry.add_singleline_edit_or_view(marker_shape::edit_marker_shape_ui); - registry.add_singleline_edit_or_view(range1d::edit_range1d); registry.add_multiline_edit_or_view(visual_bounds2d::multiline_edit_visual_bounds2d); registry.add_singleline_edit_or_view(visual_bounds2d::singleline_edit_visual_bounds2d); diff --git a/crates/viewer/re_data_ui/src/blob.rs b/crates/viewer/re_data_ui/src/blob.rs index a6a4caf6d3e1..2327487596cb 100644 --- a/crates/viewer/re_data_ui/src/blob.rs +++ b/crates/viewer/re_data_ui/src/blob.rs @@ -109,7 +109,8 @@ pub fn blob_preview_and_save_ui( .ok() }); if let Some(image) = &image { - image_preview_ui(ctx, ui, ui_layout, query, entity_path, image); + let colormap = None; // TODO(andreas): Rely on default here for now. + image_preview_ui(ctx, ui, ui_layout, query, entity_path, image, colormap); } // Try to treat it as a video if treating it as image didn't work: else if let Some(blob_row_id) = blob_row_id { @@ -153,12 +154,11 @@ pub fn blob_preview_and_save_ui( let image_stats = ctx .cache .entry(|c: &mut re_viewer_context::ImageStatsCache| c.entry(&image)); - if let Ok(data_range) = re_viewer_context::gpu_bridge::image_data_range_heuristic( + let data_range = re_viewer_context::gpu_bridge::image_data_range_heuristic( &image_stats, &image.format, - ) { - crate::image::copy_image_button_ui(ui, &image, data_range); - } + ); + crate::image::copy_image_button_ui(ui, &image, data_range); } }); } diff --git a/crates/viewer/re_data_ui/src/image.rs b/crates/viewer/re_data_ui/src/image.rs index 56dd270e1455..caacd42acd80 100644 --- a/crates/viewer/re_data_ui/src/image.rs +++ b/crates/viewer/re_data_ui/src/image.rs @@ -3,7 +3,7 @@ use egui::{NumExt as _, Vec2}; use re_renderer::renderer::ColormappedTexture; use re_viewer_context::{ gpu_bridge::{self, image_to_gpu}, - ImageInfo, ImageStatsCache, UiLayout, ViewerContext, + ColormapWithRange, ImageInfo, ImageStatsCache, UiLayout, ViewerContext, }; /// Show a button letting the user copy the image @@ -37,12 +37,21 @@ pub fn image_preview_ui( query: &re_chunk_store::LatestAtQuery, entity_path: &re_log_types::EntityPath, image: &ImageInfo, + colormap_with_range: Option<&ColormapWithRange>, ) -> Option<()> { let render_ctx = ctx.render_ctx?; let image_stats = ctx.cache.entry(|c: &mut ImageStatsCache| c.entry(image)); let annotations = crate::annotations(ctx, query, entity_path); let debug_name = entity_path.to_string(); - let texture = image_to_gpu(render_ctx, &debug_name, image, &image_stats, &annotations).ok()?; + let texture = image_to_gpu( + render_ctx, + &debug_name, + image, + &image_stats, + &annotations, + colormap_with_range, + ) + .ok()?; texture_preview_ui(render_ctx, ui, ui_layout, &debug_name, texture); Some(()) } diff --git a/crates/viewer/re_data_ui/src/instance_path.rs b/crates/viewer/re_data_ui/src/instance_path.rs index 9affa0b25aa8..1f51324a7fb3 100644 --- a/crates/viewer/re_data_ui/src/instance_path.rs +++ b/crates/viewer/re_data_ui/src/instance_path.rs @@ -1,3 +1,4 @@ +use egui::Rangef; use nohash_hasher::IntMap; use re_chunk_store::UnitChunkShared; @@ -11,8 +12,8 @@ use re_types::{ }; use re_ui::{ContextExt as _, UiExt as _}; use re_viewer_context::{ - gpu_bridge::image_data_range_heuristic, HoverHighlight, ImageInfo, ImageStatsCache, Item, - UiLayout, ViewerContext, + gpu_bridge::image_data_range_heuristic, ColormapWithRange, HoverHighlight, ImageInfo, + ImageStatsCache, Item, UiLayout, ViewerContext, }; use crate::{blob::blob_preview_and_save_ui, image::image_preview_ui}; @@ -287,40 +288,58 @@ fn preview_if_image_ui( ImageKind::Color }; - let colormap = component_map - .get(&components::Colormap::name()) - .and_then(|colormap| { - colormap - .component_mono::() - .transpose() - .ok() - .flatten() - }); - let image = ImageInfo { buffer_row_id, buffer: image_buffer.0, format: image_format.0, kind, - colormap, }; + let image_stats = ctx.cache.entry(|c: &mut ImageStatsCache| c.entry(&image)); - image_preview_ui(ctx, ui, ui_layout, query, entity_path, &image); + let colormap = component_map + .get(&components::Colormap::name()) + .and_then(|colormap| colormap.component_mono::()?.ok()); + let value_range = component_map + .get(&components::Range1D::name()) + .and_then(|colormap| colormap.component_mono::()?.ok()); + let colormap_with_range = colormap.map(|colormap| ColormapWithRange { + colormap, + value_range: value_range + .map(|r| [r.start() as _, r.end() as _]) + .unwrap_or_else(|| { + if kind == ImageKind::Depth { + ColormapWithRange::default_range_for_depth_images(&image_stats) + } else { + let (min, max) = image_stats.finite_range; + [min as _, max as _] + } + }), + }); + + image_preview_ui( + ctx, + ui, + ui_layout, + query, + entity_path, + &image, + colormap_with_range.as_ref(), + ); if ui_layout.is_single_line() || ui_layout == UiLayout::Tooltip { return Some(()); // no more ui } - let image_stats = ctx.cache.entry(|c: &mut ImageStatsCache| c.entry(&image)); - - if let Ok(data_range) = image_data_range_heuristic(&image_stats, &image.format) { - ui.horizontal(|ui| { - image_download_button_ui(ctx, ui, entity_path, &image, data_range); + let data_range = value_range.map_or_else( + || image_data_range_heuristic(&image_stats, &image.format), + |r| Rangef::new(r.start() as _, r.end() as _), + ); + ui.horizontal(|ui| { + image_download_button_ui(ctx, ui, entity_path, &image, data_range); - #[cfg(not(target_arch = "wasm32"))] - crate::image::copy_image_button_ui(ui, &image, data_range); - }); - } + #[cfg(not(target_arch = "wasm32"))] + crate::image::copy_image_button_ui(ui, &image, data_range); + }); // TODO(emilk): we should really support histograms for all types of images if image.format.pixel_format.is_none() diff --git a/crates/viewer/re_data_ui/src/tensor.rs b/crates/viewer/re_data_ui/src/tensor.rs index 62b7e3c892ad..afe680f47228 100644 --- a/crates/viewer/re_data_ui/src/tensor.rs +++ b/crates/viewer/re_data_ui/src/tensor.rs @@ -133,10 +133,11 @@ pub fn tensor_summary_ui_grid_contents( ui.end_row(); } // Show finite range only if it is different from the actual range. - if let (true, Some((min, max))) = (range != finite_range, finite_range) { + if range != &Some(*finite_range) { ui.label("Finite data range").on_hover_text( "The finite values (ignoring all NaN & -Inf/+Inf) of the tensor range within these bounds" ); + let (min, max) = finite_range; ui.monospace(format!( "[{} - {}]", re_format::format_f64(*min), diff --git a/crates/viewer/re_renderer/shader/depth_cloud.wgsl b/crates/viewer/re_renderer/shader/depth_cloud.wgsl index d5874562c651..b2784acc7bb4 100644 --- a/crates/viewer/re_renderer/shader/depth_cloud.wgsl +++ b/crates/viewer/re_renderer/shader/depth_cloud.wgsl @@ -42,8 +42,8 @@ struct DepthCloudInfo { /// Point radius is calculated as world-space depth times this value. point_radius_from_world_depth: f32, - /// The maximum depth value in world-space, for use with the colormap. - max_depth_in_world: f32, + /// The minimum & maximum depth value in world-space, for use with the colormap. + min_max_depth_in_world: vec2f, /// Configures color mapping mode, see `colormap.wgsl`. colormap: u32, @@ -120,7 +120,10 @@ fn compute_point_data(quad_idx: u32) -> PointData { if 0.0 < world_space_depth && world_space_depth < f32max { // TODO(cmc): albedo textures - let color = vec4f(colormap_linear(depth_cloud_info.colormap, world_space_depth / depth_cloud_info.max_depth_in_world), 1.0); + let normalized_depth = + (world_space_depth - depth_cloud_info.min_max_depth_in_world.x) / + (depth_cloud_info.min_max_depth_in_world.y - depth_cloud_info.min_max_depth_in_world.x); + let color = vec4f(colormap_linear(depth_cloud_info.colormap, normalized_depth), 1.0); // TODO(cmc): This assumes a pinhole camera; need to support other kinds at some point. let intrinsics = depth_cloud_info.depth_camera_intrinsics; diff --git a/crates/viewer/re_renderer/src/renderer/depth_cloud.rs b/crates/viewer/re_renderer/src/renderer/depth_cloud.rs index 8a15628c2154..a856299cdfea 100644 --- a/crates/viewer/re_renderer/src/renderer/depth_cloud.rs +++ b/crates/viewer/re_renderer/src/renderer/depth_cloud.rs @@ -62,20 +62,20 @@ mod gpu_data { /// Point radius is calculated as world-space depth times this value. pub point_radius_from_world_depth: f32, - /// The maximum depth value in world-space, for use with the colormap. - pub max_depth_in_world: f32, + /// The minimum and maximum depth value in world-space, for use with the colormap. + pub min_max_depth_in_world: [f32; 2], + // --- /// Which colormap should be used. pub colormap: u32, - // --- /// One of `SAMPLE_TYPE_*`. pub sample_type: u32, /// Changes over different draw-phases. pub radius_boost_in_ui_points: f32, - pub _row_padding: [f32; 2], + pub _row_padding: [f32; 1], // --- pub _end_padding: [wgpu_buffer_types::PaddingRow; 16 - 4 - 3 - 1 - 1 - 1], @@ -91,7 +91,7 @@ mod gpu_data { depth_camera_intrinsics, world_depth_from_texture_depth, point_radius_from_world_depth, - max_depth_in_world, + min_max_depth_in_world, depth_dimensions: _, depth_texture, colormap, @@ -117,7 +117,7 @@ mod gpu_data { outline_mask_id: outline_mask_id.0.unwrap_or_default().into(), world_depth_from_texture_depth: *world_depth_from_texture_depth, point_radius_from_world_depth: *point_radius_from_world_depth, - max_depth_in_world: *max_depth_in_world, + min_max_depth_in_world: *min_max_depth_in_world, colormap: *colormap as u32, sample_type, radius_boost_in_ui_points, @@ -145,8 +145,8 @@ pub struct DepthCloud { /// Point radius is calculated as world-space depth times this value. pub point_radius_from_world_depth: f32, - /// The maximum depth value in world-space, for use with the colormap. - pub max_depth_in_world: f32, + /// The minimum and maximum depth value in world-space, for use with the colormap. + pub min_max_depth_in_world: [f32; 2], /// The dimensions of the depth texture in pixels. pub depth_dimensions: glam::UVec2, @@ -168,8 +168,11 @@ pub struct DepthCloud { impl DepthCloud { /// World-space bounding-box. + /// + /// Assumes max extent to be the maximum depth used for colormapping + /// but ignores the minimum depth, using the frustum's origin instead. pub fn world_space_bbox(&self) -> re_math::BoundingBox { - let max_depth = self.max_depth_in_world; + let max_depth = self.min_max_depth_in_world[1]; let w = self.depth_dimensions.x as f32; let h = self.depth_dimensions.y as f32; let corners = [ diff --git a/crates/viewer/re_renderer_examples/depth_cloud.rs b/crates/viewer/re_renderer_examples/depth_cloud.rs index 1e0ab23ab3ed..a6bf6af6183e 100644 --- a/crates/viewer/re_renderer_examples/depth_cloud.rs +++ b/crates/viewer/re_renderer_examples/depth_cloud.rs @@ -177,7 +177,7 @@ impl RenderDepthClouds { depth_camera_intrinsics: *intrinsics, world_depth_from_texture_depth: 1.0, point_radius_from_world_depth: *point_radius_from_world_depth, - max_depth_in_world: 5.0, + min_max_depth_in_world: [0.0, 5.0], depth_dimensions: depth.dimensions, depth_texture: depth.texture.clone(), colormap: re_renderer::Colormap::Turbo, diff --git a/crates/viewer/re_space_view_spatial/src/mesh_loader.rs b/crates/viewer/re_space_view_spatial/src/mesh_loader.rs index 304685ec10d1..1482486d258b 100644 --- a/crates/viewer/re_space_view_spatial/src/mesh_loader.rs +++ b/crates/viewer/re_space_view_spatial/src/mesh_loader.rs @@ -211,7 +211,6 @@ fn try_get_or_create_albedo_texture( buffer: albedo_texture_buffer.0.clone(), format: albedo_texture_format.0, kind: re_types::image::ImageKind::Color, - colormap: None, }; if re_viewer_context::gpu_bridge::required_shader_decode( diff --git a/crates/viewer/re_space_view_spatial/src/visualizers/depth_images.rs b/crates/viewer/re_space_view_spatial/src/visualizers/depth_images.rs index 7788d774e69d..244a058d49b4 100644 --- a/crates/viewer/re_space_view_spatial/src/visualizers/depth_images.rs +++ b/crates/viewer/re_space_view_spatial/src/visualizers/depth_images.rs @@ -6,15 +6,16 @@ use re_renderer::renderer::{ColormappedTexture, DepthCloud, DepthClouds}; use re_types::{ archetypes::DepthImage, components::{ - self, Colormap, DepthMeter, DrawOrder, FillRatio, ImageBuffer, ImageFormat, ViewCoordinates, + self, Colormap, DepthMeter, DrawOrder, FillRatio, ImageBuffer, ImageFormat, ValueRange, + ViewCoordinates, }, image::ImageKind, Loggable as _, }; use re_viewer_context::{ - ApplicableEntities, IdentifiedViewSystem, ImageInfo, QueryContext, SpaceViewClass, - SpaceViewSystemExecutionError, TypedComponentFallbackProvider, ViewContext, - ViewContextCollection, ViewQuery, VisualizableEntities, VisualizableFilterContext, + ApplicableEntities, ColormapWithRange, IdentifiedViewSystem, ImageInfo, ImageStatsCache, + QueryContext, SpaceViewClass, SpaceViewSystemExecutionError, TypedComponentFallbackProvider, + ViewContext, ViewContextCollection, ViewQuery, VisualizableEntities, VisualizableFilterContext, VisualizerQueryInfo, VisualizerSystem, }; @@ -48,6 +49,8 @@ struct DepthImageComponentData { image: ImageInfo, depth_meter: Option, fill_ratio: Option, + colormap: Option, + value_range: Option<[f64; 2]>, } impl DepthImageVisualizer { @@ -68,15 +71,31 @@ impl DepthImageVisualizer { for data in images { let DepthImageComponentData { - mut image, + image, depth_meter, fill_ratio, + colormap, + value_range, } = data; let depth_meter = depth_meter.unwrap_or_else(|| self.fallback_for(ctx)); // All depth images must have a colormap: - image.colormap = Some(image.colormap.unwrap_or_else(|| self.fallback_for(ctx))); + let colormap = colormap.unwrap_or_else(|| self.fallback_for(ctx)); + let value_range = value_range + .map(|r| [r[0] as f32, r[1] as f32]) + .unwrap_or_else(|| { + // Don't use fallback provider since it has to query information we already have. + let image_stats = ctx + .viewer_ctx + .cache + .entry(|c: &mut ImageStatsCache| c.entry(&image)); + ColormapWithRange::default_range_for_depth_images(&image_stats) + }); + let colormap_with_range = ColormapWithRange { + colormap, + value_range, + }; // First try to create a textured rect for this image. // Even if we end up only showing a depth cloud, @@ -86,6 +105,7 @@ impl DepthImageVisualizer { entity_path, ent_context, &image, + Some(&colormap_with_range), re_renderer::Rgba::WHITE, "DepthImage", &mut self.data, @@ -185,12 +205,17 @@ impl DepthImageVisualizer { let pixel_width_from_depth = (0.5 * fov_y).tan() / (0.5 * dimensions.y as f32); let point_radius_from_world_depth = *radius_scale.0 * pixel_width_from_depth; + let min_max_depth_in_world = [ + world_depth_from_texture_depth * depth_texture.range[0], + world_depth_from_texture_depth * depth_texture.range[1], + ]; + Ok(DepthCloud { world_from_rdf, depth_camera_intrinsics: intrinsics.image_from_camera.0.into(), world_depth_from_texture_depth, point_radius_from_world_depth, - max_depth_in_world: world_depth_from_texture_depth * depth_texture.range[1], + min_max_depth_in_world, depth_dimensions: dimensions, depth_texture: depth_texture.texture.clone(), colormap: match depth_texture.color_mapper { @@ -261,18 +286,20 @@ impl VisualizerSystem for DepthImageVisualizer { ImageFormat::name(), ); let all_colormaps = results.iter_as(timeline, Colormap::name()); + let all_value_ranges = results.iter_as(timeline, ValueRange::name()); let all_depth_meters = results.iter_as(timeline, DepthMeter::name()); let all_fill_ratios = results.iter_as(timeline, FillRatio::name()); - let mut data = re_query::range_zip_1x4( + let mut data = re_query::range_zip_1x5( all_buffers_indexed, all_formats_indexed, all_colormaps.component::(), + all_value_ranges.primitive_array::<2, f64>(), all_depth_meters.primitive::(), all_fill_ratios.primitive::(), ) .filter_map( - |(index, buffers, format, colormap, depth_meter, fill_ratio)| { + |(index, buffers, format, colormap, value_range, depth_meter, fill_ratio)| { let buffer = buffers.first()?; Some(DepthImageComponentData { @@ -281,10 +308,11 @@ impl VisualizerSystem for DepthImageVisualizer { buffer: buffer.clone().into(), format: first_copied(format.as_deref())?.0, kind: ImageKind::Depth, - colormap: first_copied(colormap.as_deref()), }, depth_meter: first_copied(depth_meter).map(Into::into), fill_ratio: first_copied(fill_ratio).map(Into::into), + colormap: first_copied(colormap.as_deref()), + value_range: first_copied(value_range).map(Into::into), }) }, ); @@ -335,9 +363,46 @@ impl VisualizerSystem for DepthImageVisualizer { } } +impl TypedComponentFallbackProvider for DepthImageVisualizer { + fn fallback_for(&self, _ctx: &QueryContext<'_>) -> DrawOrder { + DrawOrder::DEFAULT_DEPTH_IMAGE + } +} + +impl TypedComponentFallbackProvider for DepthImageVisualizer { + fn fallback_for( + &self, + ctx: &re_viewer_context::QueryContext<'_>, + ) -> re_types::components::ValueRange { + if let Some(((_time, buffer_row_id), image_buffer)) = ctx + .recording() + .latest_at_component::(ctx.target_entity_path, ctx.query) + { + // TODO(andreas): What about overrides on the image format? + if let Some((_, format)) = ctx + .recording() + .latest_at_component::(ctx.target_entity_path, ctx.query) + { + let image = ImageInfo { + buffer_row_id, + buffer: image_buffer.0, + format: format.0, + kind: ImageKind::Depth, + }; + let cache = ctx.viewer_ctx.cache; + let image_stats = cache.entry(|c: &mut ImageStatsCache| c.entry(&image)); + let default_range = ColormapWithRange::default_range_for_depth_images(&image_stats); + return [default_range[0] as f64, default_range[1] as f64].into(); + } + } + + [0.0, f64::MAX].into() + } +} + impl TypedComponentFallbackProvider for DepthImageVisualizer { fn fallback_for(&self, _ctx: &re_viewer_context::QueryContext<'_>) -> Colormap { - Colormap::Turbo + ColormapWithRange::DEFAULT_DEPTH_COLORMAP } } @@ -352,13 +417,7 @@ impl TypedComponentFallbackProvider for DepthImageVisualizer { } } -impl TypedComponentFallbackProvider for DepthImageVisualizer { - fn fallback_for(&self, _ctx: &QueryContext<'_>) -> DrawOrder { - DrawOrder::DEFAULT_DEPTH_IMAGE - } -} - -re_viewer_context::impl_component_fallback_provider!(DepthImageVisualizer => [Colormap, DepthMeter, DrawOrder]); +re_viewer_context::impl_component_fallback_provider!(DepthImageVisualizer => [Colormap, ValueRange, DepthMeter, DrawOrder]); fn first_copied(slice: Option<&[T]>) -> Option { slice.and_then(|element| element.first()).copied() diff --git a/crates/viewer/re_space_view_spatial/src/visualizers/encoded_image.rs b/crates/viewer/re_space_view_spatial/src/visualizers/encoded_image.rs index 881c9d514591..9b094ec9e17f 100644 --- a/crates/viewer/re_space_view_spatial/src/visualizers/encoded_image.rs +++ b/crates/viewer/re_space_view_spatial/src/visualizers/encoded_image.rs @@ -180,12 +180,14 @@ impl EncodedImageVisualizer { let opacity = opacity.copied().unwrap_or_else(|| self.fallback_for(ctx)); let multiplicative_tint = re_renderer::Rgba::from_white_alpha(opacity.0.clamp(0.0, 1.0)); + let colormap = None; if let Some(textured_rect) = textured_rect_from_image( ctx.viewer_ctx, entity_path, spatial_ctx, &image, + colormap, multiplicative_tint, "EncodedImage", &mut self.data, diff --git a/crates/viewer/re_space_view_spatial/src/visualizers/images.rs b/crates/viewer/re_space_view_spatial/src/visualizers/images.rs index 78bfee579b73..fb8227bc9a26 100644 --- a/crates/viewer/re_space_view_spatial/src/visualizers/images.rs +++ b/crates/viewer/re_space_view_spatial/src/visualizers/images.rs @@ -151,7 +151,6 @@ impl ImageVisualizer { buffer: buffer.clone().into(), format: first_copied(formats.as_deref())?.0, kind: ImageKind::Color, - colormap: None, }, opacity: first_copied(opacities).map(Into::into), }) @@ -161,12 +160,14 @@ impl ImageVisualizer { let opacity = opacity.unwrap_or_else(|| self.fallback_for(ctx)); let multiplicative_tint = re_renderer::Rgba::from_white_alpha(opacity.0.clamp(0.0, 1.0)); + let colormap = None; if let Some(textured_rect) = textured_rect_from_image( ctx.viewer_ctx, entity_path, spatial_ctx, &image, + colormap, multiplicative_tint, "Image", &mut self.data, diff --git a/crates/viewer/re_space_view_spatial/src/visualizers/segmentation_images.rs b/crates/viewer/re_space_view_spatial/src/visualizers/segmentation_images.rs index 7e1f9b203d2b..d19aaa64c3f9 100644 --- a/crates/viewer/re_space_view_spatial/src/visualizers/segmentation_images.rs +++ b/crates/viewer/re_space_view_spatial/src/visualizers/segmentation_images.rs @@ -109,7 +109,6 @@ impl VisualizerSystem for SegmentationImageVisualizer { buffer: buffer.clone().into(), format: first_copied(formats.as_deref())?.0, kind: ImageKind::Segmentation, - colormap: None, }, opacity: first_copied(opacity).map(Into::into), }) @@ -121,12 +120,14 @@ impl VisualizerSystem for SegmentationImageVisualizer { let opacity = opacity.unwrap_or_else(|| self.fallback_for(ctx)); let multiplicative_tint = re_renderer::Rgba::from_white_alpha(opacity.0.clamp(0.0, 1.0)); + let colormap = None; if let Some(textured_rect) = textured_rect_from_image( ctx.viewer_ctx, entity_path, spatial_ctx, &image, + colormap, multiplicative_tint, "SegmentationImage", &mut self.data, diff --git a/crates/viewer/re_space_view_spatial/src/visualizers/utilities/textured_rect.rs b/crates/viewer/re_space_view_spatial/src/visualizers/utilities/textured_rect.rs index b2e72cb089e9..1a09222abcf7 100644 --- a/crates/viewer/re_space_view_spatial/src/visualizers/utilities/textured_rect.rs +++ b/crates/viewer/re_space_view_spatial/src/visualizers/utilities/textured_rect.rs @@ -3,18 +3,20 @@ use glam::Vec3; use re_log_types::EntityPath; use re_renderer::renderer; use re_viewer_context::{ - gpu_bridge, ImageInfo, ImageStatsCache, SpaceViewClass as _, ViewerContext, + gpu_bridge, ColormapWithRange, ImageInfo, ImageStatsCache, SpaceViewClass as _, ViewerContext, }; use crate::{contexts::SpatialSceneEntityContext, SpatialSpaceView2D}; use super::SpatialViewVisualizerData; +#[allow(clippy::too_many_arguments)] pub fn textured_rect_from_image( ctx: &ViewerContext<'_>, ent_path: &EntityPath, ent_context: &SpatialSceneEntityContext<'_>, image: &ImageInfo, + colormap: Option<&ColormapWithRange>, multiplicative_tint: egui::Rgba, visualizer_name: &'static str, visualizer_data: &mut SpatialViewVisualizerData, @@ -30,6 +32,7 @@ pub fn textured_rect_from_image( image, &tensor_stats, &ent_context.annotations, + colormap, ) { Ok(colormapped_texture) => { // TODO(emilk): let users pick texture filtering. diff --git a/crates/viewer/re_space_view_tensor/Cargo.toml b/crates/viewer/re_space_view_tensor/Cargo.toml index 03b01fe484b9..1d7ea1ec349d 100644 --- a/crates/viewer/re_space_view_tensor/Cargo.toml +++ b/crates/viewer/re_space_view_tensor/Cargo.toml @@ -22,6 +22,7 @@ all-features = true re_chunk_store.workspace = true re_data_ui.workspace = true re_log_types.workspace = true +re_query.workspace = true re_renderer.workspace = true re_space_view.workspace = true re_tracing.workspace = true @@ -29,6 +30,7 @@ re_types.workspace = true re_ui.workspace = true re_viewer_context.workspace = true re_viewport_blueprint.workspace = true + anyhow.workspace = true bytemuck.workspace = true egui.workspace = true diff --git a/crates/viewer/re_space_view_tensor/src/space_view_class.rs b/crates/viewer/re_space_view_tensor/src/space_view_class.rs index fa7933e1b491..1b3c770a135e 100644 --- a/crates/viewer/re_space_view_tensor/src/space_view_class.rs +++ b/crates/viewer/re_space_view_tensor/src/space_view_class.rs @@ -2,7 +2,6 @@ use egui::{epaint::TextShape, Align2, NumExt as _, Vec2}; use ndarray::Axis; use re_space_view::{suggest_space_view_for_each_entity, view_property_ui}; -use re_chunk_store::RowId; use re_data_ui::tensor_summary_ui_grid_contents; use re_log_types::EntityPath; use re_types::{ @@ -16,16 +15,17 @@ use re_types::{ }; use re_ui::{list_item, ContextExt as _, UiExt as _}; use re_viewer_context::{ - gpu_bridge, ApplicableEntities, IdentifiedViewSystem as _, IndicatedEntities, PerVisualizer, - SpaceViewClass, SpaceViewClassRegistryError, SpaceViewId, SpaceViewState, - SpaceViewStateExt as _, SpaceViewSystemExecutionError, TensorStatsCache, + gpu_bridge, ApplicableEntities, ColormapWithRange, IdentifiedViewSystem as _, + IndicatedEntities, PerVisualizer, SpaceViewClass, SpaceViewClassRegistryError, SpaceViewId, + SpaceViewState, SpaceViewStateExt as _, SpaceViewSystemExecutionError, TensorStatsCache, TypedComponentFallbackProvider, ViewQuery, ViewerContext, VisualizableEntities, }; use re_viewport_blueprint::ViewProperty; use crate::{ dimension_mapping::load_tensor_slice_selection_and_make_valid, - tensor_dimension_mapper::dimension_mapping_ui, visualizer_system::TensorSystem, + tensor_dimension_mapper::dimension_mapping_ui, + visualizer_system::{TensorSystem, TensorView}, }; #[derive(Default)] @@ -37,7 +37,7 @@ type ViewType = re_types::blueprint::views::TensorView; pub struct ViewTensorState { /// Last viewed tensor, copied each frame. /// Used for the selection view. - tensor: Option<(RowId, TensorData)>, + tensor: Option, } impl SpaceViewState for ViewTensorState { @@ -125,10 +125,15 @@ Note: select the space view to configure which dimensions are shown." // TODO(andreas): Listitemify ui.selection_grid("tensor_selection_ui").show(ui, |ui| { - if let Some((tensor_data_row_id, tensor)) = &state.tensor { + if let Some(TensorView { + tensor, + tensor_row_id, + .. + }) = &state.tensor + { let tensor_stats = ctx .cache - .entry(|c: &mut TensorStatsCache| c.entry(*tensor_data_row_id, tensor)); + .entry(|c: &mut TensorStatsCache| c.entry(*tensor_row_id, tensor)); tensor_summary_ui_grid_contents(ui, tensor, &tensor_stats); } @@ -140,7 +145,7 @@ Note: select the space view to configure which dimensions are shown." }); // TODO(#6075): Listitemify - if let Some((_, tensor)) = &state.tensor { + if let Some(TensorView { tensor, .. }) = &state.tensor { let slice_property = ViewProperty::from_archetype::( ctx.blueprint_db(), ctx.blueprint_query, @@ -213,9 +218,9 @@ Note: select the space view to configure which dimensions are shown." tensors.len() )); }); - } else if let Some((tensor_data_row_id, tensor)) = tensors.first() { - state.tensor = Some((*tensor_data_row_id, tensor.0.clone())); - self.view_tensor(ctx, ui, state, query.space_view_id, tensor)?; + } else if let Some(tensor_view) = tensors.first() { + state.tensor = Some(tensor_view.clone()); + self.view_tensor(ctx, ui, state, query.space_view_id, &tensor_view.tensor)?; } else { ui.centered_and_justified(|ui| ui.label("(empty)")); } @@ -314,9 +319,14 @@ impl TensorSpaceView { ) -> anyhow::Result<(egui::Response, egui::Painter, egui::Rect)> { re_tracing::profile_function!(); - let Some((tensor_data_row_id, tensor)) = state.tensor.as_ref() else { + let Some(tensor_view) = state.tensor.as_ref() else { anyhow::bail!("No tensor data available."); }; + let TensorView { + tensor_row_id, + tensor, + data_range, + } = &tensor_view; let scalar_mapping = ViewProperty::from_archetype::( ctx.blueprint_db(), @@ -331,17 +341,16 @@ impl TensorSpaceView { let Some(render_ctx) = ctx.render_ctx else { return Err(anyhow::Error::msg("No render context available.")); }; - - let tensor_stats = ctx - .cache - .entry(|c: &mut TensorStatsCache| c.entry(*tensor_data_row_id, tensor)); + let colormap = ColormapWithRange { + colormap, + value_range: [data_range.start() as f32, data_range.end() as f32], + }; let colormapped_texture = super::tensor_slice_to_gpu::colormapped_texture( render_ctx, - *tensor_data_row_id, + *tensor_row_id, tensor, - &tensor_stats, slice_selection, - colormap, + &colormap, gamma, )?; let [width, height] = colormapped_texture.width_height(); diff --git a/crates/viewer/re_space_view_tensor/src/tensor_slice_to_gpu.rs b/crates/viewer/re_space_view_tensor/src/tensor_slice_to_gpu.rs index f3a3e5957fdf..4855479c7ea1 100644 --- a/crates/viewer/re_space_view_tensor/src/tensor_slice_to_gpu.rs +++ b/crates/viewer/re_space_view_tensor/src/tensor_slice_to_gpu.rs @@ -5,13 +5,13 @@ use re_renderer::{ }; use re_types::{ blueprint::archetypes::TensorSliceSelection, - components::{Colormap, GammaCorrection}, + components::GammaCorrection, datatypes::TensorData, tensor_data::{TensorCastError, TensorDataType}, }; use re_viewer_context::{ - gpu_bridge::{self, colormap_to_re_renderer, tensor_data_range_heuristic, RangeError}, - TensorStats, + gpu_bridge::{self, colormap_to_re_renderer}, + ColormapWithRange, }; use crate::space_view_class::selected_tensor_slice; @@ -23,35 +23,29 @@ pub enum TensorUploadError { #[error("Expected a 2D slice")] Not2D, - - #[error(transparent)] - RangeError(#[from] RangeError), } pub fn colormapped_texture( render_ctx: &re_renderer::RenderContext, tensor_data_row_id: RowId, tensor: &TensorData, - tensor_stats: &TensorStats, slice_selection: &TensorSliceSelection, - colormap: Colormap, + colormap: &ColormapWithRange, gamma: GammaCorrection, ) -> Result> { re_tracing::profile_function!(); - let range = tensor_data_range_heuristic(tensor_stats, tensor.dtype()) - .map_err(|err| TextureManager2DError::DataCreation(err.into()))?; let texture = upload_texture_slice_to_gpu(render_ctx, tensor_data_row_id, tensor, slice_selection)?; Ok(ColormappedTexture { texture, - range, + range: colormap.value_range, decode_srgb: false, multiply_rgb_with_alpha: false, gamma: *gamma.0, color_mapper: re_renderer::renderer::ColorMapper::Function(colormap_to_re_renderer( - colormap, + colormap.colormap, )), shader_decoding: None, }) diff --git a/crates/viewer/re_space_view_tensor/src/visualizer_system.rs b/crates/viewer/re_space_view_tensor/src/visualizer_system.rs index 6740f3b63567..95e6d5015403 100644 --- a/crates/viewer/re_space_view_tensor/src/visualizer_system.rs +++ b/crates/viewer/re_space_view_tensor/src/visualizer_system.rs @@ -1,13 +1,26 @@ use re_chunk_store::{LatestAtQuery, RowId}; -use re_types::{archetypes::Tensor, components::TensorData}; +use re_space_view::{latest_at_with_blueprint_resolved_data, RangeResultsExt}; +use re_types::{ + archetypes::Tensor, + components::{TensorData, ValueRange}, + Loggable as _, +}; use re_viewer_context::{ - IdentifiedViewSystem, SpaceViewSystemExecutionError, ViewContext, ViewContextCollection, - ViewQuery, VisualizerQueryInfo, VisualizerSystem, + IdentifiedViewSystem, SpaceViewSystemExecutionError, TensorStats, TensorStatsCache, + TypedComponentFallbackProvider, ViewContext, ViewContextCollection, ViewQuery, + VisualizerQueryInfo, VisualizerSystem, }; +#[derive(Clone)] +pub struct TensorView { + pub tensor_row_id: RowId, + pub tensor: TensorData, + pub data_range: ValueRange, +} + #[derive(Default)] pub struct TensorSystem { - pub tensors: Vec<(RowId, TensorData)>, + pub tensors: Vec, } impl IdentifiedViewSystem for TensorSystem { @@ -32,11 +45,50 @@ impl VisualizerSystem for TensorSystem { for data_result in query.iter_visible_data_results(ctx, Self::identifier()) { let timeline_query = LatestAtQuery::new(query.timeline, query.latest_at); - if let Some(((_time, row_id), tensor)) = ctx - .recording() - .latest_at_component::(&data_result.entity_path, &timeline_query) + let annotations = None; + let query_shadowed_defaults = false; + let results = latest_at_with_blueprint_resolved_data( + ctx, + annotations, + &timeline_query, + data_result, + [TensorData::name(), ValueRange::name()].into_iter(), + query_shadowed_defaults, + ); + + let Some(all_tensor_chunks) = results.get_required_chunks(&TensorData::name()) else { + continue; + }; + + let timeline = query.timeline; + let all_tensors_indexed = all_tensor_chunks.iter().flat_map(move |chunk| { + chunk + .iter_component_indices(&timeline, &TensorData::name()) + .zip(chunk.iter_component::()) + }); + let all_ranges = results.iter_as(timeline, ValueRange::name()); + + for ((_, tensor_row_id), tensors, data_ranges) in + re_query::range_zip_1x1(all_tensors_indexed, all_ranges.component::()) { - self.tensors.push((row_id, tensor)); + let Some(tensor) = tensors.first() else { + continue; + }; + let data_range = data_ranges + .and_then(|ranges| ranges.first().copied()) + .unwrap_or_else(|| { + let tensor_stats = ctx + .viewer_ctx + .cache + .entry(|c: &mut TensorStatsCache| c.entry(tensor_row_id, tensor)); + tensor_data_range_heuristic(&tensor_stats, tensor.dtype()) + }); + + self.tensors.push(TensorView { + tensor_row_id, + tensor: tensor.clone(), + data_range, + }); } } @@ -52,4 +104,50 @@ impl VisualizerSystem for TensorSystem { } } -re_viewer_context::impl_component_fallback_provider!(TensorSystem => []); +/// Get a valid, finite range for the gpu to use. +pub fn tensor_data_range_heuristic( + tensor_stats: &TensorStats, + data_type: re_types::tensor_data::TensorDataType, +) -> ValueRange { + let (min, max) = tensor_stats.finite_range; + + // Apply heuristic for ranges that are typically expected depending on the data type and the finite (!) range. + // (we ignore NaN/Inf values heres, since they are usually there by accident!) + #[allow(clippy::tuple_array_conversions)] + ValueRange::from(if data_type.is_float() && 0.0 <= min && max <= 1.0 { + // Float values that are all between 0 and 1, assume that this is the range. + [0.0, 1.0] + } else if 0.0 <= min && max <= 255.0 { + // If all values are between 0 and 255, assume this is the range. + // (This is very common, independent of the data type) + [0.0, 255.0] + } else if min == max { + // uniform range. This can explode the colormapping, so let's map all colors to the middle: + [min - 1.0, max + 1.0] + } else { + // Use range as is if nothing matches. + [min, max] + }) +} + +impl TypedComponentFallbackProvider for TensorSystem { + fn fallback_for( + &self, + ctx: &re_viewer_context::QueryContext<'_>, + ) -> re_types::components::ValueRange { + if let Some(((_time, row_id), tensor)) = ctx + .recording() + .latest_at_component::(ctx.target_entity_path, ctx.query) + { + let tensor_stats = ctx + .viewer_ctx + .cache + .entry(|c: &mut TensorStatsCache| c.entry(row_id, &tensor)); + tensor_data_range_heuristic(&tensor_stats, tensor.dtype()) + } else { + ValueRange::new(0.0, 1.0) + } + } +} + +re_viewer_context::impl_component_fallback_provider!(TensorSystem => [re_types::components::ValueRange]); diff --git a/crates/viewer/re_viewer/src/reflection/mod.rs b/crates/viewer/re_viewer/src/reflection/mod.rs index 12842c1e4ae7..fa649070bbf6 100644 --- a/crates/viewer/re_viewer/src/reflection/mod.rs +++ b/crates/viewer/re_viewer/src/reflection/mod.rs @@ -664,6 +664,13 @@ fn generate_component_reflection() -> Result::name(), + ComponentReflection { + docstring_md: "Range of expected or valid values, specifying a lower and upper bound.", + placeholder: Some(ValueRange::default().to_arrow()?), + }, + ), ( ::name(), ComponentReflection { @@ -952,6 +959,10 @@ fn generate_archetype_reflection() -> ArchetypeReflectionMap { docstring_md : "Colormap to use for rendering the depth image.\n\nIf not set, the depth image will be rendered using the Turbo colormap.", is_required : false, }, ArchetypeFieldReflection { component_name : + "rerun.components.ValueRange".into(), display_name : "Depth range", + docstring_md : + "The expected range of depth values.\n\nThis is typically the expected range of valid values.\nEverything outside of the range is clamped to the range for the purpose of colormpaping.\nNote that point clouds generated from this image will still display all points, regardless of this range.\n\nIf not specified, the range will be automatically estimated from the data.\nNote that the Viewer may try to guess a wider range than the minimum/maximum of values\nin the contents of the depth image.\nE.g. if all values are positive, some bigger than 1.0 and all smaller than 255.0,\nthe Viewer will guess that the data likely came from an 8bit image, thus assuming a range of 0-255.", + is_required : false, }, ArchetypeFieldReflection { component_name : "rerun.components.FillRatio".into(), display_name : "Point fill ratio", docstring_md : "Scale the radii of the points in the point cloud generated from this image.\n\nA fill ratio of 1.0 (the default) means that each point is as big as to touch the center of its neighbor\nif it is at the same depth, leaving no gaps.\nA fill ratio of 0.5 means that each point touches the edge of its neighbor if it has the same depth.\n\nTODO(#6744): This applies only to 3D views!", @@ -1388,6 +1399,11 @@ fn generate_archetype_reflection() -> ArchetypeReflectionMap { ArchetypeFieldReflection { component_name : "rerun.components.TensorData".into(), display_name : "Data", docstring_md : "The tensor data", is_required : true, }, + ArchetypeFieldReflection { component_name : + "rerun.components.ValueRange".into(), display_name : "Value range", + docstring_md : + "The expected range of values.\n\nThis is typically the expected range of valid values.\nEverything outside of the range is clamped to the range for the purpose of colormpaping.\nAny colormap applied for display, will map this range.\n\nIf not specified, the range will be automatically estimated from the data.\nNote that the Viewer may try to guess a wider range than the minimum/maximum of values\nin the contents of the tensor.\nE.g. if all values are positive, some bigger than 1.0 and all smaller than 255.0,\nthe Viewer will guess that the data likely came from an 8bit image, thus assuming a range of 0-255.", + is_required : false, }, ], }, ), @@ -1682,7 +1698,7 @@ fn generate_archetype_reflection() -> ArchetypeReflectionMap { false, }, ArchetypeFieldReflection { component_name : "rerun.components.GammaCorrection".into(), display_name : "Gamma", docstring_md : - "Gamma exponent applied to normalized values before mapping to color.\n\nRaises the normalized values to the power of this value before mapping to color.\nActs like an inverse brightness. Defaults to 1.0.", + "Gamma exponent applied to normalized values before mapping to color.\n\nRaises the normalized values to the power of this value before mapping to color.\nActs like an inverse brightness. Defaults to 1.0.\n\nThe final value for display is set as:\n`colormap( ((value - data_display_range.min) / (data_display_range.max - data_display_range.min)) ** gamma )`", is_required : false, }, ], }, diff --git a/crates/viewer/re_viewer_context/src/cache/image_decode_cache.rs b/crates/viewer/re_viewer_context/src/cache/image_decode_cache.rs index efbcc865548f..289d01577cba 100644 --- a/crates/viewer/re_viewer_context/src/cache/image_decode_cache.rs +++ b/crates/viewer/re_viewer_context/src/cache/image_decode_cache.rs @@ -103,7 +103,6 @@ fn decode_image( buffer: buffer.0, format: format.0, kind: ImageKind::Color, - colormap: None, }) } diff --git a/crates/viewer/re_viewer_context/src/gpu_bridge/image_to_gpu.rs b/crates/viewer/re_viewer_context/src/gpu_bridge/image_to_gpu.rs index ad848dc9c9e7..1f3feb6e4d6c 100644 --- a/crates/viewer/re_viewer_context/src/gpu_bridge/image_to_gpu.rs +++ b/crates/viewer/re_viewer_context/src/gpu_bridge/image_to_gpu.rs @@ -13,13 +13,16 @@ use re_renderer::{ resource_managers::Texture2DCreationDesc, RenderContext, }; -use re_types::components::{ClassId, Colormap}; +use re_types::components::ClassId; use re_types::datatypes::{ChannelDatatype, ColorModel, ImageFormat, PixelFormat}; use re_types::image::ImageKind; -use crate::{gpu_bridge::colormap::colormap_to_re_renderer, Annotations, ImageInfo, ImageStats}; +use crate::{ + gpu_bridge::colormap::colormap_to_re_renderer, image_info::ColormapWithRange, Annotations, + ImageInfo, ImageStats, +}; -use super::{get_or_create_texture, RangeError}; +use super::get_or_create_texture; // ---------------------------------------------------------------------------- @@ -34,19 +37,19 @@ fn generate_texture_key(image: &ImageInfo) -> u64 { format, kind, - - colormap: _, // No need to upload new texture when this changes } = image; hash((blob_row_id, format, kind)) } +/// `colormap` is currently only used for depth images. pub fn image_to_gpu( render_ctx: &RenderContext, debug_name: &str, image: &ImageInfo, image_stats: &ImageStats, annotations: &Annotations, + colormap: Option<&ColormapWithRange>, ) -> anyhow::Result { re_tracing::profile_function!(); @@ -56,9 +59,14 @@ pub fn image_to_gpu( ImageKind::Color => { color_image_to_gpu(render_ctx, debug_name, texture_key, image, image_stats) } - ImageKind::Depth => { - depth_image_to_gpu(render_ctx, debug_name, texture_key, image, image_stats) - } + ImageKind::Depth => depth_image_to_gpu( + render_ctx, + debug_name, + texture_key, + image, + image_stats, + colormap, + ), ImageKind::Segmentation => segmentation_image_to_gpu( render_ctx, debug_name, @@ -92,25 +100,23 @@ fn color_image_to_gpu( // TODO(emilk): let the user specify the color space. let decode_srgb = texture_format == TextureFormat::Rgba8Unorm - || image_decode_srgb_gamma_heuristic(image_stats, image_format)?; + || image_decode_srgb_gamma_heuristic(image_stats, image_format); // Special casing for normalized textures used above: let range = if matches!( texture_format, TextureFormat::R8Unorm | TextureFormat::Rgba8Unorm | TextureFormat::Bgra8Unorm ) { - [0.0, 1.0] + emath::Rangef::new(0.0, 1.0) } else if texture_format == TextureFormat::R8Snorm { - [-1.0, 1.0] + emath::Rangef::new(-1.0, 1.0) } else if let Some(shader_decoding) = shader_decoding { match shader_decoding { - ShaderDecoding::Nv12 | ShaderDecoding::Yuy2 => [0.0, 1.0], - ShaderDecoding::Bgr => image_data_range_heuristic(image_stats, &image_format) - .map(|range| [range.min, range.max])?, + ShaderDecoding::Nv12 | ShaderDecoding::Yuy2 => emath::Rangef::new(0.0, 1.0), + ShaderDecoding::Bgr => image_data_range_heuristic(image_stats, &image_format), } } else { image_data_range_heuristic(image_stats, &image_format) - .map(|range| [range.min, range.max])? }; let color_mapper = if let Some(shader_decoding) = shader_decoding { @@ -148,7 +154,7 @@ fn color_image_to_gpu( Ok(ColormappedTexture { texture: texture_handle, - range, + range: [range.min, range.max], decode_srgb, multiply_rgb_with_alpha, gamma, @@ -158,12 +164,9 @@ fn color_image_to_gpu( } /// Get a valid, finite range for the gpu to use. -// TODO(#2341): The range should be determined by a `DataRange` component. In absence this, heuristics apply. -pub fn image_data_range_heuristic( - image_stats: &ImageStats, - image_format: &ImageFormat, -) -> Result { - let (min, max) = image_stats.finite_range.ok_or(RangeError::MissingRange)?; +// TODO(#4624): The range should be determined by a `DataRange` component. In absence this, heuristics apply. +pub fn image_data_range_heuristic(image_stats: &ImageStats, image_format: &ImageFormat) -> Rangef { + let (min, max) = image_stats.finite_range; let min = min as f32; let max = max as f32; @@ -172,41 +175,38 @@ pub fn image_data_range_heuristic( // (we ignore NaN/Inf values heres, since they are usually there by accident!) if image_format.is_float() && 0.0 <= min && max <= 1.0 { // Float values that are all between 0 and 1, assume that this is the range. - Ok(Rangef::new(0.0, 1.0)) + Rangef::new(0.0, 1.0) } else if 0.0 <= min && max <= 255.0 { // If all values are between 0 and 255, assume this is the range. // (This is very common, independent of the data type) - Ok(Rangef::new(0.0, 255.0)) + Rangef::new(0.0, 255.0) } else if min == max { // uniform range. This can explode the colormapping, so let's map all colors to the middle: - Ok(Rangef::new(min - 1.0, max + 1.0)) + Rangef::new(min - 1.0, max + 1.0) } else { // Use range as is if nothing matches. - Ok(Rangef::new(min, max)) + Rangef::new(min, max) } } /// Return whether an image should be assumed to be encoded in sRGB color space ("gamma space", no EOTF applied). -fn image_decode_srgb_gamma_heuristic( - image_stats: &ImageStats, - image_format: ImageFormat, -) -> Result { +fn image_decode_srgb_gamma_heuristic(image_stats: &ImageStats, image_format: ImageFormat) -> bool { if let Some(pixel_format) = image_format.pixel_format { match pixel_format { - PixelFormat::NV12 | PixelFormat::YUY2 => Ok(true), + PixelFormat::NV12 | PixelFormat::YUY2 => true, } } else { - let (min, max) = image_stats.finite_range.ok_or(RangeError::MissingRange)?; + let (min, max) = image_stats.finite_range; #[allow(clippy::if_same_then_else)] if 0.0 <= min && max <= 255.0 { // If the range is suspiciously reminding us of a "regular image", assume sRGB. - Ok(true) + true } else if image_format.datatype().is_float() && 0.0 <= min && max <= 1.0 { // Floating point images between 0 and 1 are often sRGB as well. - Ok(true) + true } else { - Ok(false) + false } } } @@ -354,6 +354,7 @@ fn depth_image_to_gpu( texture_key: u64, image: &ImageInfo, image_stats: &ImageStats, + colormap_with_range: Option<&ColormapWithRange>, ) -> anyhow::Result { re_tracing::profile_function!(); @@ -370,7 +371,12 @@ fn depth_image_to_gpu( let datatype = image.format.datatype(); - let range = data_range(image_stats, datatype); + let ColormapWithRange { + value_range, + colormap, + } = colormap_with_range + .cloned() + .unwrap_or_else(|| ColormapWithRange::default_for_depth_images(image_stats)); let texture = get_or_create_texture(render_ctx, texture_key, || { general_texture_creation_desc_from_image(debug_name, image, ColorModel::L, datatype) @@ -379,13 +385,11 @@ fn depth_image_to_gpu( Ok(ColormappedTexture { texture, - range, + range: value_range, decode_srgb: false, multiply_rgb_with_alpha: false, gamma: 1.0, - color_mapper: ColorMapper::Function(colormap_to_re_renderer( - image.colormap.unwrap_or(Colormap::Turbo), - )), + color_mapper: ColorMapper::Function(colormap_to_re_renderer(colormap)), shader_decoding: None, }) } @@ -467,34 +471,6 @@ fn segmentation_image_to_gpu( }) } -fn data_range(image_stats: &ImageStats, datatype: ChannelDatatype) -> [f32; 2] { - let default_min = 0.0; - let default_max = if datatype.is_float() { - 1.0 - } else { - datatype.max_value() - }; - - let range = image_stats - .finite_range - .unwrap_or((default_min, default_max)); - let (mut min, mut max) = range; - - if !min.is_finite() { - min = default_min; - } - if !max.is_finite() { - max = default_max; - } - - if max <= min { - min = default_min; - max = default_max; - } - - [min as f32, max as f32] -} - /// Uploads the image to a texture in a format that closely resembled the input. /// Uses no `Unorm/Snorm` formats. fn general_texture_creation_desc_from_image<'a>( diff --git a/crates/viewer/re_viewer_context/src/gpu_bridge/mod.rs b/crates/viewer/re_viewer_context/src/gpu_bridge/mod.rs index a0f64954417e..29daaf789d97 100644 --- a/crates/viewer/re_viewer_context/src/gpu_bridge/mod.rs +++ b/crates/viewer/re_viewer_context/src/gpu_bridge/mod.rs @@ -25,62 +25,26 @@ use re_renderer::{ // ---------------------------------------------------------------------------- -/// Errors that can happen when supplying a tensor range to the GPU. -#[derive(thiserror::Error, Debug, PartialEq, Eq)] -pub enum RangeError { - /// This is weird. Should only happen with JPEGs, and those should have been decoded already - #[error("Missing a range.")] - MissingRange, -} - -/// Get a valid, finite range for the gpu to use. -pub fn tensor_data_range_heuristic( - tensor_stats: &TensorStats, - data_type: re_types::tensor_data::TensorDataType, -) -> Result<[f32; 2], RangeError> { - let (min, max) = tensor_stats.finite_range.ok_or(RangeError::MissingRange)?; - - let min = min as f32; - let max = max as f32; - - // Apply heuristic for ranges that are typically expected depending on the data type and the finite (!) range. - // (we ignore NaN/Inf values heres, since they are usually there by accident!) - if data_type.is_float() && 0.0 <= min && max <= 1.0 { - // Float values that are all between 0 and 1, assume that this is the range. - Ok([0.0, 1.0]) - } else if 0.0 <= min && max <= 255.0 { - // If all values are between 0 and 255, assume this is the range. - // (This is very common, independent of the data type) - Ok([0.0, 255.0]) - } else if min == max { - // uniform range. This can explode the colormapping, so let's map all colors to the middle: - Ok([min - 1.0, max + 1.0]) - } else { - // Use range as is if nothing matches. - Ok([min, max]) - } -} - /// Return whether a tensor should be assumed to be encoded in sRGB color space ("gamma space", no EOTF applied). pub fn tensor_decode_srgb_gamma_heuristic( tensor_stats: &TensorStats, data_type: re_types::tensor_data::TensorDataType, channels: u32, -) -> Result { +) -> bool { if matches!(channels, 1 | 3 | 4) { - let (min, max) = tensor_stats.finite_range.ok_or(RangeError::MissingRange)?; + let (min, max) = tensor_stats.finite_range; #[allow(clippy::if_same_then_else)] if 0.0 <= min && max <= 255.0 { // If the range is suspiciously reminding us of a "regular image", assume sRGB. - Ok(true) + true } else if data_type.is_float() && 0.0 <= min && max <= 1.0 { // Floating point images between 0 and 1 are often sRGB as well. - Ok(true) + true } else { - Ok(false) + false } } else { - Ok(false) + false } } diff --git a/crates/viewer/re_viewer_context/src/image_info.rs b/crates/viewer/re_viewer_context/src/image_info.rs index b144cf50473e..0a514018da59 100644 --- a/crates/viewer/re_viewer_context/src/image_info.rs +++ b/crates/viewer/re_viewer_context/src/image_info.rs @@ -8,9 +8,34 @@ use re_types::{ tensor_data::TensorElement, }; -/// Represents an `Image`, `SegmentationImage` or `DepthImage`. +/// Colormap together with the range of image values that is mapped to the colormap's range. /// -/// It has enough information to render the image on the screen. +/// The range is used to linearly re-map the image values to a normalized range (of 0-1) +/// to which the colormap is applied. +#[derive(Clone)] +pub struct ColormapWithRange { + pub colormap: Colormap, + pub value_range: [f32; 2], +} + +impl ColormapWithRange { + pub const DEFAULT_DEPTH_COLORMAP: Colormap = Colormap::Turbo; + + pub fn default_range_for_depth_images(image_stats: &crate::ImageStats) -> [f32; 2] { + // Use 0.0 as default minimum depth value, even if it doesn't show up in the data. + // (since logically, depth usually starts at zero) + [0.0, image_stats.finite_range.1 as _] + } + + pub fn default_for_depth_images(image_stats: &crate::ImageStats) -> Self { + Self { + colormap: Self::DEFAULT_DEPTH_COLORMAP, + value_range: Self::default_range_for_depth_images(image_stats), + } + } +} + +/// Represents the contents of an `Image`, `SegmentationImage` or `DepthImage`. #[derive(Clone)] pub struct ImageInfo { /// The row id that contained the blob. @@ -26,9 +51,6 @@ pub struct ImageInfo { /// Color, Depth, or Segmentation? pub kind: ImageKind, - - /// Primarily for depth images atm - pub colormap: Option, } impl ImageInfo { @@ -373,7 +395,6 @@ mod tests { buffer: image.buffer.0, format: image.format.0, kind: re_types::image::ImageKind::Color, - colormap: None, } } diff --git a/crates/viewer/re_viewer_context/src/lib.rs b/crates/viewer/re_viewer_context/src/lib.rs index fbe1c360a267..0cd14c563bfb 100644 --- a/crates/viewer/re_viewer_context/src/lib.rs +++ b/crates/viewer/re_viewer_context/src/lib.rs @@ -51,7 +51,7 @@ pub use component_fallbacks::{ }; pub use component_ui_registry::{ComponentUiRegistry, ComponentUiTypes, UiLayout}; pub use contents::{blueprint_id_to_tile_id, Contents, ContentsName}; -pub use image_info::ImageInfo; +pub use image_info::{ColormapWithRange, ImageInfo}; pub use item::Item; pub use maybe_mut_ref::MaybeMutRef; pub use query_context::{ diff --git a/crates/viewer/re_viewer_context/src/tensor/image_stats.rs b/crates/viewer/re_viewer_context/src/tensor/image_stats.rs index 72d9bcaad5e2..c4254feb78fe 100644 --- a/crates/viewer/re_viewer_context/src/tensor/image_stats.rs +++ b/crates/viewer/re_viewer_context/src/tensor/image_stats.rs @@ -14,8 +14,9 @@ pub struct ImageStats { /// Like `range`, but ignoring all `NaN`/inf values. /// - /// `None` if there are no finite values at all. - pub finite_range: Option<(f64, f64)>, + /// If no finite values are present, this takes the maximum finite range + /// of the underlying data type. + pub finite_range: (f64, f64), } impl ImageStats { @@ -124,7 +125,7 @@ impl ImageStats { // We do the lazy thing here: return Self { range: Some((0.0, 255.0)), - finite_range: Some((0.0, 255.0)), + finite_range: (0.0, 255.0), }; } None => image.format.datatype(), @@ -150,13 +151,13 @@ impl ImageStats { // Empty image return Self { range: None, - finite_range: None, + finite_range: (datatype.min_value(), datatype.max_value()), }; } let finite_range = if range.0.is_finite() && range.1.is_finite() { // Already finite - Some(range) + range } else { let finite_range = match datatype { ChannelDatatype::U8 @@ -175,9 +176,9 @@ impl ImageStats { // Ensure it actually is finite: if finite_range.0.is_finite() && finite_range.1.is_finite() { - Some(finite_range) + finite_range } else { - None + (datatype.min_value(), datatype.max_value()) } }; diff --git a/crates/viewer/re_viewer_context/src/tensor/tensor_stats.rs b/crates/viewer/re_viewer_context/src/tensor/tensor_stats.rs index ebac7631a181..20be75b4ede7 100644 --- a/crates/viewer/re_viewer_context/src/tensor/tensor_stats.rs +++ b/crates/viewer/re_viewer_context/src/tensor/tensor_stats.rs @@ -13,8 +13,9 @@ pub struct TensorStats { /// Like `range`, but ignoring all `NaN`/inf values. /// - /// `None` if there are no finite values at all. - pub finite_range: Option<(f64, f64)>, + /// If no finite values are present, this takes the maximum finite range + /// of the underlying data type. + pub finite_range: (f64, f64), } impl TensorStats { @@ -132,7 +133,7 @@ impl TensorStats { // Empty tensor return Self { range: None, - finite_range: None, + finite_range: (tensor.dtype().min_value(), tensor.dtype().max_value()), }; } } @@ -172,7 +173,8 @@ impl TensorStats { None } }) - }; + } + .unwrap_or_else(|| (tensor.dtype().min_value(), tensor.dtype().max_value())); Self { range, diff --git a/docs/content/reference/types/archetypes/depth_image.md b/docs/content/reference/types/archetypes/depth_image.md index b2ec72e6b1d2..e647416b782a 100644 --- a/docs/content/reference/types/archetypes/depth_image.md +++ b/docs/content/reference/types/archetypes/depth_image.md @@ -11,7 +11,7 @@ Each pixel corresponds to a depth value in units specified by [`components.Depth **Required**: [`ImageBuffer`](../components/image_buffer.md), [`ImageFormat`](../components/image_format.md) -**Optional**: [`DepthMeter`](../components/depth_meter.md), [`Colormap`](../components/colormap.md), [`FillRatio`](../components/fill_ratio.md), [`DrawOrder`](../components/draw_order.md) +**Optional**: [`DepthMeter`](../components/depth_meter.md), [`Colormap`](../components/colormap.md), [`ValueRange`](../components/value_range.md), [`FillRatio`](../components/fill_ratio.md), [`DrawOrder`](../components/draw_order.md) ## Shown in * [Spatial2DView](../views/spatial2d_view.md) diff --git a/docs/content/reference/types/archetypes/tensor.md b/docs/content/reference/types/archetypes/tensor.md index a0bff263a4fa..b28f6d62b61f 100644 --- a/docs/content/reference/types/archetypes/tensor.md +++ b/docs/content/reference/types/archetypes/tensor.md @@ -9,6 +9,8 @@ An N-dimensional array of numbers. **Required**: [`TensorData`](../components/tensor_data.md) +**Optional**: [`ValueRange`](../components/value_range.md) + ## Shown in * [TensorView](../views/tensor_view.md) * [BarChartView](../views/bar_chart_view.md) (for 1D tensors) diff --git a/docs/content/reference/types/components.md b/docs/content/reference/types/components.md index 772ccf32b4bc..11aba202b4d0 100644 --- a/docs/content/reference/types/components.md +++ b/docs/content/reference/types/components.md @@ -71,6 +71,7 @@ on [Entities and Components](../../concepts/entity-component.md). * [`TransformRelation`](components/transform_relation.md): Specifies relation a spatial transform describes. * [`Translation3D`](components/translation3d.md): A translation vector in 3D space. * [`TriangleIndices`](components/triangle_indices.md): The three indices of a triangle in a triangle mesh. +* [`ValueRange`](components/value_range.md): Range of expected or valid values, specifying a lower and upper bound. * [`Vector2D`](components/vector2d.md): A vector in 2D space. * [`Vector3D`](components/vector3d.md): A vector in 3D space. * [`VideoTimestamp`](components/video_timestamp.md): Timestamp inside a [`archetypes.AssetVideo`](https://rerun.io/docs/reference/types/archetypes/asset_video?speculative-link). diff --git a/docs/content/reference/types/components/.gitattributes b/docs/content/reference/types/components/.gitattributes index 25cfd40ed1e7..c07d94eee94f 100644 --- a/docs/content/reference/types/components/.gitattributes +++ b/docs/content/reference/types/components/.gitattributes @@ -59,6 +59,7 @@ transform_mat3x3.md linguist-generated=true transform_relation.md linguist-generated=true translation3d.md linguist-generated=true triangle_indices.md linguist-generated=true +value_range.md linguist-generated=true vector2d.md linguist-generated=true vector3d.md linguist-generated=true video_timestamp.md linguist-generated=true diff --git a/docs/content/reference/types/components/value_range.md b/docs/content/reference/types/components/value_range.md new file mode 100644 index 000000000000..e13c0fc619a2 --- /dev/null +++ b/docs/content/reference/types/components/value_range.md @@ -0,0 +1,21 @@ +--- +title: "ValueRange" +--- + + +Range of expected or valid values, specifying a lower and upper bound. + +## Fields + +* range: [`Range1D`](../datatypes/range1d.md) + +## API reference links + * 🌊 [C++ API docs for `ValueRange`](https://ref.rerun.io/docs/cpp/stable/structrerun_1_1components_1_1ValueRange.html) + * 🐍 [Python API docs for `ValueRange`](https://ref.rerun.io/docs/python/stable/common/components#rerun.components.ValueRange) + * 🦀 [Rust API docs for `ValueRange`](https://docs.rs/rerun/latest/rerun/components/struct.ValueRange.html) + + +## Used by + +* [`DepthImage`](../archetypes/depth_image.md) +* [`Tensor`](../archetypes/tensor.md) diff --git a/docs/content/reference/types/datatypes/range1d.md b/docs/content/reference/types/datatypes/range1d.md index 82e2d8551947..b485ac3f3cd1 100644 --- a/docs/content/reference/types/datatypes/range1d.md +++ b/docs/content/reference/types/datatypes/range1d.md @@ -19,3 +19,4 @@ A 1D range, specifying a lower and upper bound. * [`Range1D`](../components/range1d.md) * [`Range2D`](../datatypes/range2d.md) +* [`ValueRange`](../components/value_range.md) diff --git a/examples/python/signed_distance_fields/signed_distance_fields/__main__.py b/examples/python/signed_distance_fields/signed_distance_fields/__main__.py index 17adac1a5357..739c6eab15f0 100755 --- a/examples/python/signed_distance_fields/signed_distance_fields/__main__.py +++ b/examples/python/signed_distance_fields/signed_distance_fields/__main__.py @@ -117,7 +117,12 @@ def log_sampled_sdf(points: npt.NDArray[np.float32], sdf: npt.NDArray[np.float32 def log_volumetric_sdf(voxvol: npt.NDArray[np.float32]) -> None: names = ["width", "height", "depth"] - rr.log("tensor", rr.Tensor(voxvol, dim_names=names)) + # Use a symmetric value range, so that the `cyantoyellow` colormap centers around zero. + # Either positive or negative range might be quite small, so don't exceed 1.5x the minimum range. + negative_range = abs(cast(float, np.min(voxvol))) + positive_range = abs(cast(float, np.max(voxvol))) + range = min(max(negative_range, positive_range), 1.5 * min(negative_range, positive_range)) + rr.log("tensor", rr.Tensor(voxvol, dim_names=names, value_range=[-range, range])) @log_timing_decorator("global/log_mesh", "DEBUG") # type: ignore[misc] @@ -197,7 +202,14 @@ def main() -> None: rrb.Vertical( rrb.Horizontal( rrb.Spatial3DView(name="Input Mesh", origin="/world/mesh"), - rrb.TensorView(name="SDF", origin="/tensor"), + rrb.TensorView( + # The cyan to yellow colormap changes its color at the mid point of its range. + # By combining this with a the `value_range` parameter on the tensor, + # we can visualize negative & positive values effectively. + name="SDF", + origin="/tensor", + scalar_mapping=rrb.TensorScalarMapping(colormap="cyantoyellow"), + ), ), rrb.TextLogView(name="Execution Log"), ), diff --git a/rerun_cpp/src/rerun/archetypes/depth_image.cpp b/rerun_cpp/src/rerun/archetypes/depth_image.cpp index 593fb02bbb34..f392dcd19c83 100644 --- a/rerun_cpp/src/rerun/archetypes/depth_image.cpp +++ b/rerun_cpp/src/rerun/archetypes/depth_image.cpp @@ -14,7 +14,7 @@ namespace rerun { ) { using namespace archetypes; std::vector cells; - cells.reserve(7); + cells.reserve(8); { auto result = ComponentBatch::from_loggable(archetype.buffer); @@ -36,6 +36,11 @@ namespace rerun { RR_RETURN_NOT_OK(result.error); cells.push_back(std::move(result.value)); } + if (archetype.depth_range.has_value()) { + auto result = ComponentBatch::from_loggable(archetype.depth_range.value()); + RR_RETURN_NOT_OK(result.error); + cells.push_back(std::move(result.value)); + } if (archetype.point_fill_ratio.has_value()) { auto result = ComponentBatch::from_loggable(archetype.point_fill_ratio.value()); RR_RETURN_NOT_OK(result.error); diff --git a/rerun_cpp/src/rerun/archetypes/depth_image.hpp b/rerun_cpp/src/rerun/archetypes/depth_image.hpp index c1168019870c..66775eb9b8ab 100644 --- a/rerun_cpp/src/rerun/archetypes/depth_image.hpp +++ b/rerun_cpp/src/rerun/archetypes/depth_image.hpp @@ -12,6 +12,7 @@ #include "../components/fill_ratio.hpp" #include "../components/image_buffer.hpp" #include "../components/image_format.hpp" +#include "../components/value_range.hpp" #include "../image_utils.hpp" #include "../indicator_component.hpp" #include "../result.hpp" @@ -26,7 +27,7 @@ namespace rerun::archetypes { /// /// Each pixel corresponds to a depth value in units specified by `components::DepthMeter`. /// - /// Since the underlying `rerun::datatypes::TensorData` uses `rerun::Collection` internally, + /// Since the underlying `rerun::datatypes::ImageBuffer` uses `rerun::Collection` internally, /// data can be passed in without a copy from raw pointers or by reference from `std::vector`/`std::array`/c-arrays. /// If needed, this "borrow-behavior" can be extended by defining your own `rerun::CollectionAdapter`. /// @@ -94,6 +95,19 @@ namespace rerun::archetypes { /// If not set, the depth image will be rendered using the Turbo colormap. std::optional colormap; + /// The expected range of depth values. + /// + /// This is typically the expected range of valid values. + /// Everything outside of the range is clamped to the range for the purpose of colormpaping. + /// Note that point clouds generated from this image will still display all points, regardless of this range. + /// + /// If not specified, the range will be automatically estimated from the data. + /// Note that the Viewer may try to guess a wider range than the minimum/maximum of values + /// in the contents of the depth image. + /// E.g. if all values are positive, some bigger than 1.0 and all smaller than 255.0, + /// the Viewer will guess that the data likely came from an 8bit image, thus assuming a range of 0-255. + std::optional depth_range; + /// Scale the radii of the points in the point cloud generated from this image. /// /// A fill ratio of 1.0 (the default) means that each point is as big as to touch the center of its neighbor @@ -201,6 +215,23 @@ namespace rerun::archetypes { RR_WITH_MAYBE_UNINITIALIZED_DISABLED(return std::move(*this);) } + /// The expected range of depth values. + /// + /// This is typically the expected range of valid values. + /// Everything outside of the range is clamped to the range for the purpose of colormpaping. + /// Note that point clouds generated from this image will still display all points, regardless of this range. + /// + /// If not specified, the range will be automatically estimated from the data. + /// Note that the Viewer may try to guess a wider range than the minimum/maximum of values + /// in the contents of the depth image. + /// E.g. if all values are positive, some bigger than 1.0 and all smaller than 255.0, + /// the Viewer will guess that the data likely came from an 8bit image, thus assuming a range of 0-255. + DepthImage with_depth_range(rerun::components::ValueRange _depth_range) && { + depth_range = std::move(_depth_range); + // See: https://github.com/rerun-io/rerun/issues/4027 + RR_WITH_MAYBE_UNINITIALIZED_DISABLED(return std::move(*this);) + } + /// Scale the radii of the points in the point cloud generated from this image. /// /// A fill ratio of 1.0 (the default) means that each point is as big as to touch the center of its neighbor diff --git a/rerun_cpp/src/rerun/archetypes/tensor.cpp b/rerun_cpp/src/rerun/archetypes/tensor.cpp index 07db511be330..124deaf6e2ab 100644 --- a/rerun_cpp/src/rerun/archetypes/tensor.cpp +++ b/rerun_cpp/src/rerun/archetypes/tensor.cpp @@ -14,13 +14,18 @@ namespace rerun { ) { using namespace archetypes; std::vector cells; - cells.reserve(2); + cells.reserve(3); { auto result = ComponentBatch::from_loggable(archetype.data); RR_RETURN_NOT_OK(result.error); cells.push_back(std::move(result.value)); } + if (archetype.value_range.has_value()) { + auto result = ComponentBatch::from_loggable(archetype.value_range.value()); + RR_RETURN_NOT_OK(result.error); + cells.push_back(std::move(result.value)); + } { auto indicator = Tensor::IndicatorComponent(); auto result = ComponentBatch::from_loggable(indicator); diff --git a/rerun_cpp/src/rerun/archetypes/tensor.hpp b/rerun_cpp/src/rerun/archetypes/tensor.hpp index 88bfafd2d2a4..881c6f34bb60 100644 --- a/rerun_cpp/src/rerun/archetypes/tensor.hpp +++ b/rerun_cpp/src/rerun/archetypes/tensor.hpp @@ -4,12 +4,15 @@ #pragma once #include "../collection.hpp" +#include "../compiler_utils.hpp" #include "../component_batch.hpp" #include "../components/tensor_data.hpp" +#include "../components/value_range.hpp" #include "../indicator_component.hpp" #include "../result.hpp" #include +#include #include #include @@ -53,6 +56,19 @@ namespace rerun::archetypes { /// The tensor data rerun::components::TensorData data; + /// The expected range of values. + /// + /// This is typically the expected range of valid values. + /// Everything outside of the range is clamped to the range for the purpose of colormpaping. + /// Any colormap applied for display, will map this range. + /// + /// If not specified, the range will be automatically estimated from the data. + /// Note that the Viewer may try to guess a wider range than the minimum/maximum of values + /// in the contents of the tensor. + /// E.g. if all values are positive, some bigger than 1.0 and all smaller than 255.0, + /// the Viewer will guess that the data likely came from an 8bit image, thus assuming a range of 0-255. + std::optional value_range; + public: static constexpr const char IndicatorComponentName[] = "rerun.components.TensorIndicator"; @@ -90,6 +106,23 @@ namespace rerun::archetypes { Tensor(Tensor&& other) = default; explicit Tensor(rerun::components::TensorData _data) : data(std::move(_data)) {} + + /// The expected range of values. + /// + /// This is typically the expected range of valid values. + /// Everything outside of the range is clamped to the range for the purpose of colormpaping. + /// Any colormap applied for display, will map this range. + /// + /// If not specified, the range will be automatically estimated from the data. + /// Note that the Viewer may try to guess a wider range than the minimum/maximum of values + /// in the contents of the tensor. + /// E.g. if all values are positive, some bigger than 1.0 and all smaller than 255.0, + /// the Viewer will guess that the data likely came from an 8bit image, thus assuming a range of 0-255. + Tensor with_value_range(rerun::components::ValueRange _value_range) && { + value_range = std::move(_value_range); + // See: https://github.com/rerun-io/rerun/issues/4027 + RR_WITH_MAYBE_UNINITIALIZED_DISABLED(return std::move(*this);) + } }; } // namespace rerun::archetypes diff --git a/rerun_cpp/src/rerun/blueprint/archetypes/tensor_scalar_mapping.hpp b/rerun_cpp/src/rerun/blueprint/archetypes/tensor_scalar_mapping.hpp index e9370fdc226a..4f85781fefec 100644 --- a/rerun_cpp/src/rerun/blueprint/archetypes/tensor_scalar_mapping.hpp +++ b/rerun_cpp/src/rerun/blueprint/archetypes/tensor_scalar_mapping.hpp @@ -32,6 +32,9 @@ namespace rerun::blueprint::archetypes { /// /// Raises the normalized values to the power of this value before mapping to color. /// Acts like an inverse brightness. Defaults to 1.0. + /// + /// The final value for display is set as: + /// `colormap( ((value - data_display_range.min) / (data_display_range.max - data_display_range.min)) ** gamma )` std::optional gamma; public: @@ -65,6 +68,9 @@ namespace rerun::blueprint::archetypes { /// /// Raises the normalized values to the power of this value before mapping to color. /// Acts like an inverse brightness. Defaults to 1.0. + /// + /// The final value for display is set as: + /// `colormap( ((value - data_display_range.min) / (data_display_range.max - data_display_range.min)) ** gamma )` TensorScalarMapping with_gamma(rerun::components::GammaCorrection _gamma) && { gamma = std::move(_gamma); // See: https://github.com/rerun-io/rerun/issues/4027 diff --git a/rerun_cpp/src/rerun/components.hpp b/rerun_cpp/src/rerun/components.hpp index 3e05b8095a17..e11d74969590 100644 --- a/rerun_cpp/src/rerun/components.hpp +++ b/rerun_cpp/src/rerun/components.hpp @@ -60,6 +60,7 @@ #include "components/transform_relation.hpp" #include "components/translation3d.hpp" #include "components/triangle_indices.hpp" +#include "components/value_range.hpp" #include "components/vector2d.hpp" #include "components/vector3d.hpp" #include "components/video_timestamp.hpp" diff --git a/rerun_cpp/src/rerun/components/.gitattributes b/rerun_cpp/src/rerun/components/.gitattributes index ef90d02b74d5..a63cd0bb87b8 100644 --- a/rerun_cpp/src/rerun/components/.gitattributes +++ b/rerun_cpp/src/rerun/components/.gitattributes @@ -68,6 +68,7 @@ transform_relation.cpp linguist-generated=true transform_relation.hpp linguist-generated=true translation3d.hpp linguist-generated=true triangle_indices.hpp linguist-generated=true +value_range.hpp linguist-generated=true vector2d.hpp linguist-generated=true vector3d.hpp linguist-generated=true video_timestamp.hpp linguist-generated=true diff --git a/rerun_cpp/src/rerun/components/value_range.hpp b/rerun_cpp/src/rerun/components/value_range.hpp new file mode 100644 index 000000000000..840045a3f58e --- /dev/null +++ b/rerun_cpp/src/rerun/components/value_range.hpp @@ -0,0 +1,74 @@ +// DO NOT EDIT! This file was auto-generated by crates/build/re_types_builder/src/codegen/cpp/mod.rs +// Based on "crates/store/re_types/definitions/rerun/components/value_range.fbs". + +#pragma once + +#include "../datatypes/range1d.hpp" +#include "../result.hpp" + +#include +#include +#include + +namespace rerun::components { + /// **Component**: Range of expected or valid values, specifying a lower and upper bound. + struct ValueRange { + rerun::datatypes::Range1D range; + + public: + ValueRange() = default; + + ValueRange(rerun::datatypes::Range1D range_) : range(range_) {} + + ValueRange& operator=(rerun::datatypes::Range1D range_) { + range = range_; + return *this; + } + + ValueRange(std::array range_) : range(range_) {} + + ValueRange& operator=(std::array range_) { + range = range_; + return *this; + } + + /// Cast to the underlying Range1D datatype + operator rerun::datatypes::Range1D() const { + return range; + } + }; +} // namespace rerun::components + +namespace rerun { + static_assert(sizeof(rerun::datatypes::Range1D) == sizeof(components::ValueRange)); + + /// \private + template <> + struct Loggable { + static constexpr const char Name[] = "rerun.components.ValueRange"; + + /// Returns the arrow data type this type corresponds to. + static const std::shared_ptr& arrow_datatype() { + return Loggable::arrow_datatype(); + } + + /// Serializes an array of `rerun::components::ValueRange` into an arrow array. + static Result> to_arrow( + const components::ValueRange* instances, size_t num_instances + ) { + if (num_instances == 0) { + return Loggable::to_arrow(nullptr, 0); + } else if (instances == nullptr) { + return rerun::Error( + ErrorCode::UnexpectedNullArgument, + "Passed array instances is null when num_elements> 0." + ); + } else { + return Loggable::to_arrow( + &instances->range, + num_instances + ); + } + } + }; +} // namespace rerun diff --git a/rerun_py/rerun_sdk/rerun/archetypes/depth_image.py b/rerun_py/rerun_sdk/rerun/archetypes/depth_image.py index 14b8da0ac6a9..5fc1d1ed023c 100644 --- a/rerun_py/rerun_sdk/rerun/archetypes/depth_image.py +++ b/rerun_py/rerun_sdk/rerun/archetypes/depth_image.py @@ -70,6 +70,7 @@ def __attrs_clear__(self) -> None: format=None, # type: ignore[arg-type] meter=None, # type: ignore[arg-type] colormap=None, # type: ignore[arg-type] + depth_range=None, # type: ignore[arg-type] point_fill_ratio=None, # type: ignore[arg-type] draw_order=None, # type: ignore[arg-type] ) @@ -123,6 +124,25 @@ def _clear(cls) -> DepthImage: # # (Docstring intentionally commented out to hide this field from the docs) + depth_range: components.ValueRangeBatch | None = field( + metadata={"component": "optional"}, + default=None, + converter=components.ValueRangeBatch._optional, # type: ignore[misc] + ) + # The expected range of depth values. + # + # This is typically the expected range of valid values. + # Everything outside of the range is clamped to the range for the purpose of colormpaping. + # Note that point clouds generated from this image will still display all points, regardless of this range. + # + # If not specified, the range will be automatically estimated from the data. + # Note that the Viewer may try to guess a wider range than the minimum/maximum of values + # in the contents of the depth image. + # E.g. if all values are positive, some bigger than 1.0 and all smaller than 255.0, + # the Viewer will guess that the data likely came from an 8bit image, thus assuming a range of 0-255. + # + # (Docstring intentionally commented out to hide this field from the docs) + point_fill_ratio: components.FillRatioBatch | None = field( metadata={"component": "optional"}, default=None, diff --git a/rerun_py/rerun_sdk/rerun/archetypes/depth_image_ext.py b/rerun_py/rerun_sdk/rerun/archetypes/depth_image_ext.py index 31416fdc5f79..68238558e785 100644 --- a/rerun_py/rerun_sdk/rerun/archetypes/depth_image_ext.py +++ b/rerun_py/rerun_sdk/rerun/archetypes/depth_image_ext.py @@ -5,6 +5,8 @@ import numpy as np import numpy.typing as npt +from rerun.datatypes.range1d import Range1DLike + from ..components import Colormap, ImageFormat from ..datatypes import ChannelDatatype, Float32Like @@ -45,7 +47,57 @@ def __init__( *, meter: Float32Like | None = None, colormap: Colormap | None = None, + depth_range: Range1DLike | None = None, + point_fill_ratio: Float32Like | None = None, + draw_order: Float32Like | None = None, ): + """ + Create a new instance of the DepthImage archetype. + + Parameters + ---------- + image: + A numpy array or tensor with the depth image data. + Leading and trailing unit-dimensions are ignored, so that + `1x480x640x1` is treated as a `480x640`. + meter: + An optional floating point value that specifies how long a meter is in the native depth units. + + For instance: with uint16, perhaps meter=1000 which would mean you have millimeter precision + and a range of up to ~65 meters (2^16 / 1000). + + Note that the only effect on 2D views is the physical depth values shown when hovering the image. + In 3D views on the other hand, this affects where the points of the point cloud are placed. + colormap: + Colormap to use for rendering the depth image. + + If not set, the depth image will be rendered using the Turbo colormap. + depth_range: + The expected range of depth values. + + This is typically the expected range of valid values. + Everything outside of the range is clamped to the range for the purpose of colormpaping. + Note that point clouds generated from this image will still display all points, regardless of this range. + + If not specified, the range will be automatically be estimated from the data. + Note that the Viewer may try to guess a wider range than the minimum/maximum of values + in the contents of the depth image. + E.g. if all values are positive, some bigger than 1.0 and all smaller than 255.0, + the Viewer will guess that the data likely came from an 8bit image, thus assuming a range of 0-255. + point_fill_ratio: + Scale the radii of the points in the point cloud generated from this image. + + A fill ratio of 1.0 (the default) means that each point is as big as to touch the center of its neighbor + if it is at the same depth, leaving no gaps. + A fill ratio of 0.5 means that each point touches the edge of its neighbor if it has the same depth. + + TODO(#6744): This applies only to 3D views! + draw_order: + An optional floating point value that specifies the 2D drawing order, used only if the depth image is shown as a 2D image. + + Objects with higher values are drawn on top of those with lower values. + + """ image = _to_numpy(image) shape = image.shape @@ -74,4 +126,7 @@ def __init__( ), meter=meter, colormap=colormap, + depth_range=depth_range, + point_fill_ratio=point_fill_ratio, + draw_order=draw_order, ) diff --git a/rerun_py/rerun_sdk/rerun/archetypes/tensor.py b/rerun_py/rerun_sdk/rerun/archetypes/tensor.py index 8a0529a7bd9e..9c9a90ec5cda 100644 --- a/rerun_py/rerun_sdk/rerun/archetypes/tensor.py +++ b/rerun_py/rerun_sdk/rerun/archetypes/tensor.py @@ -57,6 +57,7 @@ def __attrs_clear__(self) -> None: """Convenience method for calling `__attrs_init__` with all `None`s.""" self.__attrs_init__( data=None, # type: ignore[arg-type] + value_range=None, # type: ignore[arg-type] ) @classmethod @@ -74,5 +75,24 @@ def _clear(cls) -> Tensor: # # (Docstring intentionally commented out to hide this field from the docs) + value_range: components.ValueRangeBatch | None = field( + metadata={"component": "optional"}, + default=None, + converter=components.ValueRangeBatch._optional, # type: ignore[misc] + ) + # The expected range of values. + # + # This is typically the expected range of valid values. + # Everything outside of the range is clamped to the range for the purpose of colormpaping. + # Any colormap applied for display, will map this range. + # + # If not specified, the range will be automatically estimated from the data. + # Note that the Viewer may try to guess a wider range than the minimum/maximum of values + # in the contents of the tensor. + # E.g. if all values are positive, some bigger than 1.0 and all smaller than 255.0, + # the Viewer will guess that the data likely came from an 8bit image, thus assuming a range of 0-255. + # + # (Docstring intentionally commented out to hide this field from the docs) + __str__ = Archetype.__str__ __repr__ = Archetype.__repr__ # type: ignore[assignment] diff --git a/rerun_py/rerun_sdk/rerun/archetypes/tensor_ext.py b/rerun_py/rerun_sdk/rerun/archetypes/tensor_ext.py index 845e0d1f5976..0ee17ce0edab 100644 --- a/rerun_py/rerun_sdk/rerun/archetypes/tensor_ext.py +++ b/rerun_py/rerun_sdk/rerun/archetypes/tensor_ext.py @@ -2,6 +2,8 @@ from typing import TYPE_CHECKING, Any, Sequence +from rerun.datatypes.range1d import Range1DLike + from ..error_utils import catch_and_log_exceptions if TYPE_CHECKING: @@ -17,6 +19,7 @@ def __init__( data: TensorDataLike | TensorLike | None = None, *, dim_names: Sequence[str | None] | None = None, + value_range: Range1DLike | None = None, ): """ Construct a `Tensor` archetype. @@ -37,6 +40,10 @@ def __init__( A TensorData object, or type that can be converted to a numpy array. dim_names: Sequence[str] | None The names of the tensor dimensions when generating the shape from an array. + value_range: Sequence[float] | None + The range of values to use for colormapping. + + If not specified, the range will be estimated from the data. """ from ..datatypes import TensorData @@ -47,7 +54,7 @@ def __init__( elif dim_names is not None: data = TensorData(buffer=data.buffer, dim_names=dim_names) - self.__attrs_init__(data=data) + self.__attrs_init__(data=data, value_range=value_range) return self.__attrs_clear__() diff --git a/rerun_py/rerun_sdk/rerun/blueprint/archetypes/tensor_scalar_mapping.py b/rerun_py/rerun_sdk/rerun/blueprint/archetypes/tensor_scalar_mapping.py index 5111432a38af..f0a1157f5462 100644 --- a/rerun_py/rerun_sdk/rerun/blueprint/archetypes/tensor_scalar_mapping.py +++ b/rerun_py/rerun_sdk/rerun/blueprint/archetypes/tensor_scalar_mapping.py @@ -46,6 +46,9 @@ def __init__( Raises the normalized values to the power of this value before mapping to color. Acts like an inverse brightness. Defaults to 1.0. + The final value for display is set as: + `colormap( ((value - data_display_range.min) / (data_display_range.max - data_display_range.min)) ** gamma )` + """ # You can define your own __init__ function as a member of TensorScalarMappingExt in tensor_scalar_mapping_ext.py @@ -99,6 +102,9 @@ def _clear(cls) -> TensorScalarMapping: # Raises the normalized values to the power of this value before mapping to color. # Acts like an inverse brightness. Defaults to 1.0. # + # The final value for display is set as: + # `colormap( ((value - data_display_range.min) / (data_display_range.max - data_display_range.min)) ** gamma )` + # # (Docstring intentionally commented out to hide this field from the docs) __str__ = Archetype.__str__ diff --git a/rerun_py/rerun_sdk/rerun/components/.gitattributes b/rerun_py/rerun_sdk/rerun/components/.gitattributes index bb7b80d1486c..6541e94fa6eb 100644 --- a/rerun_py/rerun_sdk/rerun/components/.gitattributes +++ b/rerun_py/rerun_sdk/rerun/components/.gitattributes @@ -60,6 +60,7 @@ transform_mat3x3.py linguist-generated=true transform_relation.py linguist-generated=true translation3d.py linguist-generated=true triangle_indices.py linguist-generated=true +value_range.py linguist-generated=true vector2d.py linguist-generated=true vector3d.py linguist-generated=true video_timestamp.py linguist-generated=true diff --git a/rerun_py/rerun_sdk/rerun/components/__init__.py b/rerun_py/rerun_sdk/rerun/components/__init__.py index 8855fcf59d3c..7fa9bf8b8c02 100644 --- a/rerun_py/rerun_sdk/rerun/components/__init__.py +++ b/rerun_py/rerun_sdk/rerun/components/__init__.py @@ -88,6 +88,7 @@ ) from .translation3d import Translation3D, Translation3DBatch, Translation3DType from .triangle_indices import TriangleIndices, TriangleIndicesBatch, TriangleIndicesType +from .value_range import ValueRange, ValueRangeBatch, ValueRangeType from .vector2d import Vector2D, Vector2DBatch, Vector2DType from .vector3d import Vector3D, Vector3DBatch, Vector3DType from .video_timestamp import VideoTimestamp, VideoTimestampBatch, VideoTimestampType @@ -286,6 +287,9 @@ "TriangleIndices", "TriangleIndicesBatch", "TriangleIndicesType", + "ValueRange", + "ValueRangeBatch", + "ValueRangeType", "Vector2D", "Vector2DBatch", "Vector2DType", diff --git a/rerun_py/rerun_sdk/rerun/components/value_range.py b/rerun_py/rerun_sdk/rerun/components/value_range.py new file mode 100644 index 000000000000..b45afdf2ec1e --- /dev/null +++ b/rerun_py/rerun_sdk/rerun/components/value_range.py @@ -0,0 +1,36 @@ +# DO NOT EDIT! This file was auto-generated by crates/build/re_types_builder/src/codegen/python/mod.rs +# Based on "crates/store/re_types/definitions/rerun/components/value_range.fbs". + +# You can extend this class by creating a "ValueRangeExt" class in "value_range_ext.py". + +from __future__ import annotations + +from .. import datatypes +from .._baseclasses import ( + ComponentBatchMixin, + ComponentMixin, +) + +__all__ = ["ValueRange", "ValueRangeBatch", "ValueRangeType"] + + +class ValueRange(datatypes.Range1D, ComponentMixin): + """**Component**: Range of expected or valid values, specifying a lower and upper bound.""" + + _BATCH_TYPE = None + # You can define your own __init__ function as a member of ValueRangeExt in value_range_ext.py + + # Note: there are no fields here because ValueRange delegates to datatypes.Range1D + pass + + +class ValueRangeType(datatypes.Range1DType): + _TYPE_NAME: str = "rerun.components.ValueRange" + + +class ValueRangeBatch(datatypes.Range1DBatch, ComponentBatchMixin): + _ARROW_TYPE = ValueRangeType() + + +# This is patched in late to avoid circular dependencies. +ValueRange._BATCH_TYPE = ValueRangeBatch # type: ignore[assignment] diff --git a/rerun_py/tests/unit/test_depth_image.py b/rerun_py/tests/unit/test_depth_image.py index 14221c96ea40..3413901fc401 100644 --- a/rerun_py/tests/unit/test_depth_image.py +++ b/rerun_py/tests/unit/test_depth_image.py @@ -1,13 +1,14 @@ from __future__ import annotations +import itertools from typing import Any import numpy as np import pytest import rerun as rr import torch -from rerun.components import DepthMeter -from rerun.datatypes import Float32Like +from rerun.components import DepthMeter, ImageFormat +from rerun.datatypes import ChannelDatatype, Float32Like rng = np.random.default_rng(12345) RANDOM_IMAGE_SOURCE = rng.uniform(0.0, 1.0, (10, 20)) @@ -25,13 +26,32 @@ def depth_image_expected() -> Any: return rr.DepthImage(RANDOM_IMAGE_SOURCE, meter=1000) -def test_image() -> None: - expected = depth_image_expected() - - for img, meter in zip(IMAGE_INPUTS, METER_INPUTS): - arch = rr.DepthImage(img, meter=meter) - - assert arch == expected +def test_depth_image() -> None: + ranges = [None, [0.0, 1.0], (1000, 1000)] + + for img, meter, depth_range in itertools.zip_longest(IMAGE_INPUTS, METER_INPUTS, ranges): + if img is None: + img = IMAGE_INPUTS[0] + + print( + f"rr.DepthImage(\n" # + f" {img}\n" + f" meter={meter!r}\n" + f" depth_range={depth_range!r}\n" + f")" + ) + arch = rr.DepthImage(img, meter=meter, depth_range=depth_range) + + assert arch.buffer == rr.components.ImageBufferBatch._optional(img.tobytes()) + assert arch.format == rr.components.ImageFormatBatch._optional( + ImageFormat( + width=img.shape[1], + height=img.shape[0], + channel_datatype=ChannelDatatype.from_np_dtype(img.dtype), + ) + ) + assert arch.meter == rr.components.DepthMeterBatch._optional(meter) + assert arch.depth_range == rr.components.ValueRangeBatch._optional(depth_range) GOOD_IMAGE_INPUTS: list[Any] = [ diff --git a/tests/python/release_checklist/check_all_components_ui.py b/tests/python/release_checklist/check_all_components_ui.py index af638f024364..9d2f17ad221d 100644 --- a/tests/python/release_checklist/check_all_components_ui.py +++ b/tests/python/release_checklist/check_all_components_ui.py @@ -216,6 +216,7 @@ def alternatives(self) -> list[Any] | None: ), "Translation3DBatch": TestCase(batch=[(1, 2, 3), (4, 5, 6), (7, 8, 9)]), "TriangleIndicesBatch": TestCase(batch=[(0, 1, 2), (3, 4, 5), (6, 7, 8)]), + "ValueRangeBatch": TestCase((0, 5)), "Vector2DBatch": TestCase(batch=[(0, 1), (2, 3), (4, 5)]), "Vector3DBatch": TestCase(batch=[(0, 3, 4), (1, 4, 5), (2, 5, 6)]), "VideoTimestampBatch": TestCase(rr.components.VideoTimestamp(seconds=0.0)),