Skip to content

Commit

Permalink
Support displaying 1D tensors (#5837)
Browse files Browse the repository at this point in the history
### What

Technically two separate fixes:
* visualizability for 1 x n and n x 1 was blocked
* real single dimension tensors are now supported


Test code:

```py
import rerun as rr
import numpy as np

rr.init("1dtensor", spawn=True)
x = np.linspace(0.0, 100.0, 100)
rr.log("n", rr.Tensor(x))
rr.log("n_x_1", rr.Tensor(np.reshape(x, (100, 1))))
rr.log("1_x_n", rr.Tensor(np.reshape(x, (1, 100))))
```

Before:
<img width="1683" alt="Screenshot 2024-04-08 at 10 00 40"
src="https://github.com/rerun-io/rerun/assets/1220815/2f2850dd-2d17-40af-ab3b-d5b86fa20f04">
After first commit:
<img width="1698" alt="Screenshot 2024-04-08 at 10 02 53"
src="https://github.com/rerun-io/rerun/assets/1220815/ae4108f2-199e-4c92-9575-13d232146efd">

After second commit:


https://github.com/rerun-io/rerun/assets/1220815/18b3fc7a-c0d6-4fff-8d3d-ab7d26d5d2fe


### Checklist
* [x] I have read and agree to [Contributor
Guide](https://github.com/rerun-io/rerun/blob/main/CONTRIBUTING.md) and
the [Code of
Conduct](https://github.com/rerun-io/rerun/blob/main/CODE_OF_CONDUCT.md)
* [x] I've included a screenshot or gif (if applicable)
* [x] I have tested the web demo (if applicable):
* Using newly built examples:
[rerun.io/viewer](https://rerun.io/viewer/pr/5837)
* Using examples from latest `main` build:
[rerun.io/viewer](https://rerun.io/viewer/pr/5837?manifest_url=https://app.rerun.io/version/main/examples_manifest.json)
* Using full set of examples from `nightly` build:
[rerun.io/viewer](https://rerun.io/viewer/pr/5837?manifest_url=https://app.rerun.io/version/nightly/examples_manifest.json)
* [x] The PR title and labels are set such as to maximize their
usefulness for the next release's CHANGELOG
* [x] If applicable, add a new check to the [release
checklist](https://github.com/rerun-io/rerun/blob/main/tests/python/release_checklist)!

- [PR Build Summary](https://build.rerun.io/pr/5837)
- [Recent benchmark results](https://build.rerun.io/graphs/crates.html)
- [Wasm size tracking](https://build.rerun.io/graphs/sizes.html)
  • Loading branch information
Wumpf authored Apr 8, 2024
1 parent b9da1b1 commit d7a672e
Show file tree
Hide file tree
Showing 6 changed files with 111 additions and 29 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
26 changes: 24 additions & 2 deletions crates/re_space_view_bar_chart/src/space_view_class.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -66,6 +67,27 @@ impl SpaceViewClass for BarChartSpaceView {
None
}

fn choose_default_visualizers(
&self,
entity_path: &EntityPath,
visualizable_entities_per_visualizer: &PerVisualizer<VisualizableEntities>,
_indicated_entities_per_visualizer: &PerVisualizer<IndicatedEntities>,
) -> 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<'_>,
Expand Down
4 changes: 2 additions & 2 deletions crates/re_space_view_tensor/src/dimension_mapping.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
35 changes: 27 additions & 8 deletions crates/re_space_view_tensor/src/space_view_class.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::<Vec<_>>();
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;
Expand Down
18 changes: 1 addition & 17 deletions crates/re_space_view_tensor/src/visualizer_system.rs
Original file line number Diff line number Diff line change
@@ -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)]
Expand All @@ -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::<Tensor>()
}

fn applicability_filter(&self) -> Option<Box<dyn VisualizerAdditionalApplicabilityFilter>> {
Some(Box::new(TensorVisualizerEntityFilter))
}

fn execute(
&mut self,
ctx: &ViewerContext<'_>,
Expand Down
56 changes: 56 additions & 0 deletions tests/python/release_checklist/check_1d_tensor_data.py
Original file line number Diff line number Diff line change
@@ -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)

0 comments on commit d7a672e

Please sign in to comment.