Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Expose tensor slice selection to blueprint #6590

Merged
merged 24 commits into from
Jun 24, 2024
Merged
Show file tree
Hide file tree
Changes from 13 commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
0ec1e03
Model tensor slice selection
Wumpf Jun 17, 2024
1960c7e
use blueprint state directly in tensor view
Wumpf Jun 18, 2024
7f7c72b
fill in missing fallback descriptions
Wumpf Jun 18, 2024
952e914
wip snippet update and blueprint
Wumpf Jun 18, 2024
4ea906d
codegen more struct serialization, manual serialization for some of t…
Wumpf Jun 18, 2024
25fe156
finish snippet update
Wumpf Jun 18, 2024
405d26b
bring back tensor dimension mapping reset buttons
Wumpf Jun 18, 2024
37ac353
fix completely unrelated ci issue
Wumpf Jun 18, 2024
f143cd8
fix reset to default blueprint not working for cleared components of …
Wumpf Jun 18, 2024
4ea5028
fix & improve handling for single dimension tensors
Wumpf Jun 18, 2024
54324ed
change view fit default to keep aspect ratio
Wumpf Jun 18, 2024
18d07f1
update tensor view image
Wumpf Jun 18, 2024
f06fdd2
add another exception to roundtrips.py
Wumpf Jun 18, 2024
2224400
improve load_tensor_slice_selection_and_make_valid based on feedback
Wumpf Jun 19, 2024
fe46cd1
improve tensor_slice_selection docs based on feedback
Wumpf Jun 19, 2024
59be4a0
more fbs documentation fixes
Wumpf Jun 19, 2024
a9c4440
improve view properties based on feedback
Wumpf Jun 19, 2024
66770cb
improve snippets and init extension based on feedback
Wumpf Jun 19, 2024
6359a88
Merge remote-tracking branch 'origin/main' into andreas/tensor-slice-…
Wumpf Jun 19, 2024
07b40fa
Merge remote-tracking branch 'origin/main' into andreas/tensor-slice-…
Wumpf Jun 24, 2024
ab6c0d8
make code nicer based on pr feedback
Wumpf Jun 24, 2024
54609c6
call dimensions x and y in tensor view snippet to make it harder to c…
Wumpf Jun 24, 2024
651d013
add unit test for tensor slice selection
Wumpf Jun 24, 2024
523554b
split load_tensor_slice_selection_and_make_valid into parts and unit …
Wumpf Jun 24, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion crates/re_log_encoding/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ default = []
decoder = ["dep:rmp-serde", "dep:lz4_flex", "re_log_types/serde"]

## Enable encoding of log messages to an .rrd file/stream.
encoder = ["dep:rmp-serde", "dep:lz4_flex"]
encoder = ["dep:rmp-serde", "dep:lz4_flex", "re_log_types/serde"]

## Enable streaming of .rrd files from HTTP.
stream_from_http = [
Expand Down
226 changes: 126 additions & 100 deletions crates/re_space_view_tensor/src/dimension_mapping.rs
Original file line number Diff line number Diff line change
@@ -1,115 +1,141 @@
use re_types::datatypes::TensorDimension;

#[derive(Clone, Debug, PartialEq, Eq, Hash, serde::Deserialize, serde::Serialize)]
pub struct DimensionSelector {
pub visible: bool,
pub dim_idx: usize,
}
use egui::NumExt as _;
use re_types::{
blueprint::{archetypes::TensorSliceSelection, components::TensorDimensionIndexSlider},
components::{TensorDimensionIndexSelection, TensorHeightDimension, TensorWidthDimension},
datatypes::{TensorDimension, TensorDimensionSelection},
};
use re_viewport_blueprint::ViewProperty;

/// Loads slice selection from blueprint and makes modifications (without writing back) such that it is valid
/// for the given tensor shape.
///
/// This is a best effort function and will insert fallbacks as needed.
/// Note that fallbacks are defined on the spot here and don't use the component fallback system.
/// We don't need the fallback system here since we're also not using generic ui either.
///
/// General rules for scrubbing the input data:
/// * out of bounds dimensions and indices are clamped to valid
/// * missing width/height is filled in if there's at least 2 dimensions.
pub fn load_tensor_slice_selection_and_make_valid(
slice_selection: &ViewProperty<'_>,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This could really use a unit-test or two.

For instance, what happens if your dimensions are named height, width and the input is height=1, width=null? From just reading the code, it looks like maybe the output will be height=1, width=1.

shape: &[TensorDimension],
) -> Result<TensorSliceSelection, re_types::DeserializationError> {
re_tracing::profile_function!();

let max_valid_dim = shape.len().saturating_sub(1) as u32;

let mut width = slice_selection.component_or_empty::<TensorWidthDimension>()?;
let mut height = slice_selection.component_or_empty::<TensorHeightDimension>()?;

// Clamp width and height to valid dimensions.
if let Some(width) = width.as_mut() {
width.dimension = width.dimension.at_most(max_valid_dim);
}
if let Some(height) = height.as_mut() {
height.dimension = height.dimension.at_most(max_valid_dim);
}

impl DimensionSelector {
pub fn new(dim_idx: usize) -> Self {
Self {
visible: true,
dim_idx,
// If there's more than two dimensions, force width and height to be set.
if shape.len() >= 2 && (width.is_none() || height.is_none()) {
let (default_width, default_height) = find_width_height_dim_indices(shape);
if width.is_none() {
width = Some(
TensorDimensionSelection {
dimension: default_width as u32,
invert: shape[default_width]
.name
.as_ref()
.map_or(false, |name| name.to_lowercase().eq("left")),
}
.into(),
);
}
if height.is_none() {
height = Some(
TensorDimensionSelection {
dimension: default_height as u32,
invert: shape[default_height]
.name
.as_ref()
.map_or(false, |name| name.to_lowercase().eq("up")),
}
.into(),
);
}
}
Wumpf marked this conversation as resolved.
Show resolved Hide resolved
}

#[derive(Default, Clone, Debug, PartialEq, Eq, Hash, serde::Deserialize, serde::Serialize)]
pub struct DimensionMapping {
/// Which dimensions have selectors, and are they visible?
pub selectors: Vec<DimensionSelector>,
// If there's one dimension, force at least with or height to be set.
else if shape.len() == 1 && width.is_none() && height.is_none() {
width = Some(
TensorDimensionSelection {
dimension: 0,
invert: false,
}
.into(),
);
}

// Which dim?
pub width: Option<usize>,
let width_dim = width.map_or(u32::MAX, |w| w.0.dimension);
let height_dim = height.map_or(u32::MAX, |h| h.0.dimension);

// Which dim?
pub height: Option<usize>,
// -----

/// Flip the width
pub invert_width: bool,
let mut indices =
slice_selection.component_array_or_empty::<TensorDimensionIndexSelection>()?;

/// Flip the height
pub invert_height: bool,
}
// Remove any index selection that uses a dimension that is out of bounds or equal to width/height.
indices.retain(|index| {
index.dimension < shape.len() as u32
&& index.dimension != width_dim
&& index.dimension != height_dim
});

impl DimensionMapping {
pub fn create(shape: &[TensorDimension]) -> Self {
match shape.len() {
0 => Self {
selectors: Default::default(),
width: None,
height: None,
invert_width: false,
invert_height: false,
},

1 => Self {
selectors: Default::default(),
width: Some(0),
height: None,
invert_width: false,
invert_height: false,
},

_ => {
let (width, height) = find_width_height_dim_indices(shape);
let selectors = (0..shape.len())
.filter(|i| *i != width && *i != height)
.map(DimensionSelector::new)
.collect();

let invert_width = shape[width]
.name
.as_ref()
.map(|name| name.to_lowercase().eq("left"))
.unwrap_or_default();
let invert_height = shape[height]
.name
.as_ref()
.map(|name| name.to_lowercase().eq("up"))
.unwrap_or_default();

Self {
selectors,
width: Some(width),
height: Some(height),
invert_width,
invert_height,
}
}
}
// Clamp indices to valid dimension extent.
let mut covered_dims = vec![false; shape.len()];
for dim_index_selection in &mut indices {
dim_index_selection.index = dim_index_selection
.index
.at_most(shape[dim_index_selection.dimension as usize].size - 1);
covered_dims[dim_index_selection.dimension as usize] = true;
Wumpf marked this conversation as resolved.
Show resolved Hide resolved
}

/// Protect against old serialized data that is not up-to-date with the new tensor
pub fn is_valid(&self, num_dim: usize) -> bool {
fn is_in_range(dim_selector: &Option<usize>, num_dim: usize) -> bool {
if let Some(dim) = dim_selector {
*dim < num_dim
} else {
true
// Fill in missing indices for dimensions that aren't covered with the middle index.
width.inspect(|w| covered_dims[w.dimension as usize] = true);
Wumpf marked this conversation as resolved.
Show resolved Hide resolved
height.inspect(|h| covered_dims[h.dimension as usize] = true);
for (i, _) in covered_dims.into_iter().enumerate().filter(|(_, b)| !b) {
indices.push(
re_types::datatypes::TensorDimensionIndexSelection {
dimension: i as u32,
index: shape[i].size / 2,
}
}

let mut used_dimensions: ahash::HashSet<usize> =
self.selectors.iter().map(|s| s.dim_idx).collect();
if let Some(width) = self.width {
used_dimensions.insert(width);
}
if let Some(height) = self.height {
used_dimensions.insert(height);
}
if used_dimensions.len() != num_dim {
return false;
}

// we should have both width and height set…
(num_dim < 2 || (self.width.is_some() && self.height.is_some()))

// …and all dimensions should be in range
&& is_in_range(&self.width, num_dim)
&& is_in_range(&self.height, num_dim)
.into(),
);
}

// -----

let slider = if let Some(mut slider) =
slice_selection.component_array::<TensorDimensionIndexSlider>()?
{
// Remove any slider selection that uses a dimension that is out of bounds or equal to width/height.
slider.retain(|slider| {
slider.dimension < shape.len() as u32
&& slider.dimension != width_dim
&& slider.dimension != height_dim
});
slider
} else {
// If no slider were specified, create a default one for each dimension that isn't covered by width/height
indices.iter().map(|index| index.dimension.into()).collect()
};

// -----

Ok(TensorSliceSelection {
width,
height,
indices: Some(indices),
slider: Some(slider),
})
}

#[allow(clippy::collapsible_else_if)]
Expand Down
Loading
Loading