diff --git a/CHANGELOG.md b/CHANGELOG.md index c7ccf1aa869f..2901d7e2d834 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -108,6 +108,7 @@ Blueprints are currently only supported in the Python API, with C++ and Rust sup - Entity path query now shows simple statistics and warns if nothing is displayed [#5693](https://github.com/rerun-io/rerun/pull/5693) - Go back to example page with browser Back-button [#5750](https://github.com/rerun-io/rerun/pull/5750) - On Web, implement navigating back/forward with mouse buttons [#5792](https://github.com/rerun-io/rerun/pull/5792) +- Support displaying 1D tensors [#5837](https://github.com/rerun-io/rerun/pull/5837) #### 🧑‍🏫 Examples - New `incremental_logging` example [#5462](https://github.com/rerun-io/rerun/pull/5462) diff --git a/crates/re_space_view_bar_chart/src/space_view_class.rs b/crates/re_space_view_bar_chart/src/space_view_class.rs index 2ee86651d9e0..fe408e003fae 100644 --- a/crates/re_space_view_bar_chart/src/space_view_class.rs +++ b/crates/re_space_view_bar_chart/src/space_view_class.rs @@ -4,8 +4,9 @@ use re_log_types::EntityPath; use re_space_view::{controls, suggest_space_view_for_each_entity}; use re_types::datatypes::TensorBuffer; use re_viewer_context::{ - auto_color, SpaceViewClass, SpaceViewClassIdentifier, SpaceViewClassRegistryError, SpaceViewId, - SpaceViewState, SpaceViewSystemExecutionError, ViewQuery, ViewerContext, + auto_color, IdentifiedViewSystem as _, IndicatedEntities, PerVisualizer, SpaceViewClass, + SpaceViewClassIdentifier, SpaceViewClassRegistryError, SpaceViewId, SpaceViewState, + SpaceViewSystemExecutionError, ViewQuery, ViewerContext, VisualizableEntities, }; use super::visualizer_system::BarChartVisualizerSystem; @@ -66,6 +67,27 @@ impl SpaceViewClass for BarChartSpaceView { None } + fn choose_default_visualizers( + &self, + entity_path: &EntityPath, + visualizable_entities_per_visualizer: &PerVisualizer, + _indicated_entities_per_visualizer: &PerVisualizer, + ) -> re_viewer_context::SmallVisualizerSet { + // Default implementation would not suggest the BarChart visualizer for tensors and 1D images, + // since they're not indicated with a BarChart indicator. + // (and as of writing, something needs to be both visualizable and indicated to be shown in a visualizer) + + // Keeping this implementation simple: We know there's only a single visualizer here. + if visualizable_entities_per_visualizer + .get(&BarChartVisualizerSystem::identifier()) + .map_or(false, |entities| entities.contains(entity_path)) + { + std::iter::once(BarChartVisualizerSystem::identifier()).collect() + } else { + Default::default() + } + } + fn spawn_heuristics( &self, ctx: &ViewerContext<'_>, diff --git a/crates/re_space_view_tensor/src/dimension_mapping.rs b/crates/re_space_view_tensor/src/dimension_mapping.rs index 2f3d89427db7..208cecee2bb6 100644 --- a/crates/re_space_view_tensor/src/dimension_mapping.rs +++ b/crates/re_space_view_tensor/src/dimension_mapping.rs @@ -45,8 +45,8 @@ impl DimensionMapping { }, 1 => DimensionMapping { - selectors: vec![DimensionSelector::new(0)], - width: None, + selectors: Default::default(), + width: Some(0), height: None, invert_width: false, invert_height: false, diff --git a/crates/re_space_view_tensor/src/space_view_class.rs b/crates/re_space_view_tensor/src/space_view_class.rs index eb6eec99447c..022840280a8b 100644 --- a/crates/re_space_view_tensor/src/space_view_class.rs +++ b/crates/re_space_view_tensor/src/space_view_class.rs @@ -565,18 +565,37 @@ pub fn selected_tensor_slice<'a, T: Copy>( assert!(dimension_mapping.is_valid(tensor.ndim())); - // TODO(andreas) - shouldn't just give up here - if dimension_mapping.width.is_none() || dimension_mapping.height.is_none() { - return tensor.view(); - } + let (width, height) = + if let (Some(width), Some(height)) = (dimension_mapping.width, dimension_mapping.height) { + (width, height) + } else if let Some(width) = dimension_mapping.width { + // If height is missing, create a 1D row. + (width, 1) + } else if let Some(height) = dimension_mapping.height { + // If width is missing, create a 1D column. + (1, height) + } else { + // If both are missing, give up. + return tensor.view(); + }; + + let view = if tensor.shape().len() == 1 { + // We want 2D slices, so for "pure" 1D tensors add a dimension. + // This is important for above width/height conversion to work since this assumes at least 2 dimensions. + tensor + .view() + .into_shape(ndarray::IxDyn(&[tensor.len(), 1])) + .unwrap() + } else { + tensor.view() + }; - let axis = dimension_mapping - .height + #[allow(clippy::tuple_array_conversions)] + let axis = [height, width] .into_iter() - .chain(dimension_mapping.width) .chain(dimension_mapping.selectors.iter().map(|s| s.dim_idx)) .collect::>(); - let mut slice = tensor.view().permuted_axes(axis); + let mut slice = view.permuted_axes(axis); for DimensionSelector { dim_idx, .. } in &dimension_mapping.selectors { let selector_value = selector_values.get(dim_idx).copied().unwrap_or_default() as usize; diff --git a/crates/re_space_view_tensor/src/visualizer_system.rs b/crates/re_space_view_tensor/src/visualizer_system.rs index 11807f00afd2..bfad099e7614 100644 --- a/crates/re_space_view_tensor/src/visualizer_system.rs +++ b/crates/re_space_view_tensor/src/visualizer_system.rs @@ -1,12 +1,10 @@ use re_data_store::{LatestAtQuery, VersionedComponent}; use re_entity_db::EntityPath; use re_log_types::RowId; -use re_space_view::diff_component_filter; use re_types::{archetypes::Tensor, components::TensorData, tensor_data::DecodedTensor}; use re_viewer_context::{ IdentifiedViewSystem, SpaceViewSystemExecutionError, TensorDecodeCache, ViewContextCollection, - ViewQuery, ViewerContext, VisualizerAdditionalApplicabilityFilter, VisualizerQueryInfo, - VisualizerSystem, + ViewQuery, ViewerContext, VisualizerQueryInfo, VisualizerSystem, }; #[derive(Default)] @@ -20,25 +18,11 @@ impl IdentifiedViewSystem for TensorSystem { } } -struct TensorVisualizerEntityFilter; - -impl VisualizerAdditionalApplicabilityFilter for TensorVisualizerEntityFilter { - fn update_applicability(&mut self, event: &re_data_store::StoreEvent) -> bool { - diff_component_filter(event, |tensor: &re_types::components::TensorData| { - !tensor.is_vector() - }) - } -} - impl VisualizerSystem for TensorSystem { fn visualizer_query_info(&self) -> VisualizerQueryInfo { VisualizerQueryInfo::from_archetype::() } - fn applicability_filter(&self) -> Option> { - Some(Box::new(TensorVisualizerEntityFilter)) - } - fn execute( &mut self, ctx: &ViewerContext<'_>, diff --git a/tests/python/release_checklist/check_1d_tensor_data.py b/tests/python/release_checklist/check_1d_tensor_data.py new file mode 100644 index 000000000000..ed334dede2b9 --- /dev/null +++ b/tests/python/release_checklist/check_1d_tensor_data.py @@ -0,0 +1,56 @@ +from __future__ import annotations + +import os +from argparse import Namespace +from uuid import uuid4 + +import numpy as np +import rerun as rr + +README = """ +# 1D Image/Tensor/BarChart + +This checks the different ways 1D arrays can be visualized. + +### Actions + +You should see: +* a tensor view with 1D data +* an image view with a 1D image +* a bar chart + +Bonus actions: +* use the ui to create a tensor/bar-chart with each of the entities no matter how it was logged + * TODO(#5847): Right now tensors & bar charts can not be reinterpreted as 2D images. + In this example, image is correctly not suggested for the `tensor` and `image` entities, + since they are of 1D shape, but this would be relevant if they were 1xN or Nx1. + +""" + + +def log_readme() -> None: + rr.log("readme", rr.TextDocument(README, media_type=rr.MediaType.MARKDOWN), timeless=True) + + +def log_1d_data() -> None: + x = np.linspace(0.0, 100.0, 100) + rr.log("tensor", rr.Tensor(x)) + rr.log("barchart", rr.BarChart(x)) + # We're not allowing "real" 1D here and force users to be explicit about width/height + rr.log("image", rr.Image(np.reshape(x, (1, 100)))) + + +def run(args: Namespace) -> None: + rr.script_setup(args, f"{os.path.basename(__file__)}", recording_id=uuid4()) + + log_readme() + log_1d_data() + + +if __name__ == "__main__": + import argparse + + parser = argparse.ArgumentParser(description="Interactive release checklist") + rr.script_add_args(parser) + args = parser.parse_args() + run(args)