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

Tensor view ui improvements #1022

Merged
merged 7 commits into from
Feb 1, 2023
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
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
26 changes: 21 additions & 5 deletions crates/re_tensor_ops/src/dimension_mapping.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,24 @@
use re_log_types::component_types;

#[derive(Clone, Debug, PartialEq, Eq, serde::Deserialize, serde::Serialize)]
pub struct DimensionSelector {
pub visible: bool,
pub dim_idx: usize,
}

impl DimensionSelector {
pub fn new(dim_idx: usize) -> Self {
DimensionSelector {
visible: true,
dim_idx,
}
}
}

#[derive(Clone, Debug, PartialEq, Eq, serde::Deserialize, serde::Serialize)]
pub struct DimensionMapping {
/// Which dimensions have selectors?
pub selectors: Vec<usize>,
/// Which dimensions have selectors and are visible?
Wumpf marked this conversation as resolved.
Show resolved Hide resolved
Wumpf marked this conversation as resolved.
Show resolved Hide resolved
pub selectors: Vec<DimensionSelector>,

// Which dim?
pub width: Option<usize>,
Expand All @@ -30,7 +45,7 @@ impl DimensionMapping {
},

1 => DimensionMapping {
selectors: vec![0],
selectors: vec![DimensionSelector::new(0)],
width: None,
height: None,
invert_width: false,
Expand All @@ -40,7 +55,7 @@ impl DimensionMapping {
_ => {
let (width, height) = find_width_height_dim_indices(shape);
let selectors = (0..shape.len())
.filter(|&i| i != width && i != height)
.filter_map(|i| (i != width && i != height).then(|| DimensionSelector::new(i)))
Wumpf marked this conversation as resolved.
Show resolved Hide resolved
.collect();

let invert_width = shape[width]
Expand Down Expand Up @@ -75,7 +90,8 @@ impl DimensionMapping {
}
}

let mut used_dimensions: ahash::HashSet<usize> = self.selectors.iter().copied().collect();
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);
}
Expand Down
2 changes: 1 addition & 1 deletion crates/re_viewer/src/ui/space_view.rs
Original file line number Diff line number Diff line change
Expand Up @@ -167,7 +167,7 @@ impl SpaceView {
) {
self.data_blueprint.on_frame_start();

let Some(space_info) = spaces_info.get(&self.space_path) else {
let Some(space_info) = spaces_info.get(&self.space_path) else {
return;
};

Expand Down
60 changes: 39 additions & 21 deletions crates/re_viewer/src/ui/view_tensor/tensor_dimension_mapper.rs
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
use re_log_types::component_types::TensorDimension;
use re_tensor_ops::dimension_mapping::DimensionMapping;
use re_tensor_ops::dimension_mapping::{DimensionMapping, DimensionSelector};

#[derive(Clone, Copy, PartialEq, Eq)]
enum DragDropAddress {
None,
Width,
Height,
Selector(usize),
// TODO(andreas): Unused right now but we'll add it back once we have more ways of pushing dimensions.
#[allow(dead_code)]
NewSelector,
}

Expand All @@ -21,7 +23,7 @@ impl DragDropAddress {
DragDropAddress::Width => dimension_mapping.width,
DragDropAddress::Height => dimension_mapping.height,
DragDropAddress::Selector(selector_idx) => {
Some(dimension_mapping.selectors[*selector_idx])
Some(dimension_mapping.selectors[*selector_idx].dim_idx)
}
DragDropAddress::NewSelector => None,
}
Expand All @@ -34,13 +36,15 @@ impl DragDropAddress {
DragDropAddress::Height => dimension_mapping.height = dim_idx,
DragDropAddress::Selector(selector_idx) => {
if let Some(dim_idx) = dim_idx {
dimension_mapping.selectors[*selector_idx] = dim_idx;
dimension_mapping.selectors[*selector_idx] = DimensionSelector::new(dim_idx);
} else {
dimension_mapping.selectors.remove(*selector_idx);
}
}
// NewSelector can only be a drop *target*, therefore dim_idx can't be None!
DragDropAddress::NewSelector => dimension_mapping.selectors.push(dim_idx.unwrap()),
DragDropAddress::NewSelector => dimension_mapping
.selectors
.push(DimensionSelector::new(dim_idx.unwrap())),
};
}
}
Expand Down Expand Up @@ -169,6 +173,7 @@ fn drop_target_ui<R>(
}

pub fn dimension_mapping_ui(
re_ui: &re_ui::ReUi,
ui: &mut egui::Ui,
dim_mapping: &mut DimensionMapping,
shape: &[TensorDimension],
Expand All @@ -185,7 +190,7 @@ pub fn dimension_mapping_ui(
ui.memory(|mem| mem.is_being_dragged(drag_source_ui_id(drag_context_id, dim_idx)))
});

ui.horizontal(|ui| {
ui.vertical(|ui| {
ui.vertical(|ui| {
ui.strong("Image:");
egui::Grid::new("imagegrid").num_columns(2).show(ui, |ui| {
Expand Down Expand Up @@ -223,39 +228,52 @@ pub fn dimension_mapping_ui(
});
});

ui.add_space(24.0);
ui.add_space(4.0);

ui.vertical(|ui| {
ui.strong("Selectors:");
// Use Grid instead of Vertical layout to match styling of the parallel Grid for
egui::Grid::new("selectiongrid")
.num_columns(1)
.num_columns(2)
.show(ui, |ui| {
for (selector_idx, &mut dim_idx) in dim_mapping.selectors.iter_mut().enumerate()
{
for (selector_idx, selector) in dim_mapping.selectors.iter_mut().enumerate() {
tensor_dimension_ui(
ui,
drag_context_id,
can_accept_dragged,
Some(dim_idx),
Some(selector.dim_idx),
DragDropAddress::Selector(selector_idx),
shape,
&mut drop_source,
&mut drop_target,
);
let visibility_button = if selector.visible {
re_ui
.small_icon_button(ui, &re_ui::icons::VISIBLE)
.on_hover_text("Hide selector ui from the Space View.")
} else {
re_ui
.small_icon_button(ui, &re_ui::icons::INVISIBLE)
.on_hover_text("Show selector ui in the Space View.")
};
if visibility_button.clicked() {
selector.visible = !selector.visible;
}
Wumpf marked this conversation as resolved.
Show resolved Hide resolved
ui.end_row();
}
tensor_dimension_ui(
ui,
drag_context_id,
can_accept_dragged,
None,
DragDropAddress::NewSelector,
shape,
&mut drop_source,
&mut drop_target,
);
ui.end_row();
// Don't expose `NewSelector` for the moment since it doesn't add any value.
// We might need it again though if there is a way to park a selector somewhere else than width/height/selector!
// tensor_dimension_ui(
Wumpf marked this conversation as resolved.
Show resolved Hide resolved
// ui,
// drag_context_id,
// can_accept_dragged,
// None,
// DragDropAddress::NewSelector,
// shape,
// &mut drop_source,
// &mut drop_target,
// );
// ui.end_row();
});
});
});
Expand Down
72 changes: 44 additions & 28 deletions crates/re_viewer/src/ui/view_tensor/ui.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ use half::f16;
use ndarray::{Axis, Ix2};

use re_log_types::{component_types, ClassicTensor, TensorDataType};
use re_tensor_ops::dimension_mapping::DimensionMapping;
use re_tensor_ops::dimension_mapping::{DimensionMapping, DimensionSelector};

use super::dimension_mapping_ui;

Expand Down Expand Up @@ -45,24 +45,32 @@ impl ViewTensorState {

pub(crate) fn ui(&mut self, ctx: &mut crate::misc::ViewerContext<'_>, ui: &mut egui::Ui) {
if let Some(tensor) = &self.tensor {
ui.collapsing("Dimension Mapping", |ui| {
crate::ui::data_ui::image::tensor_dtype_and_shape_ui(ui, tensor);
ui.add_space(12.0);

dimension_mapping_ui(ui, &mut self.dimension_mapping, tensor.shape());

let default_mapping = DimensionMapping::create(tensor.shape());
if ui
.add_enabled(
self.dimension_mapping != default_mapping,
egui::Button::new("Auto-map"),
)
.on_disabled_hover_text("The default is already set up")
.clicked()
{
self.dimension_mapping = DimensionMapping::create(tensor.shape());
}
});
egui::CollapsingHeader::new("Dimension Mapping")
.default_open(true)
.show(ui, |ui| {
crate::ui::data_ui::image::tensor_dtype_and_shape_ui(ui, tensor);
ui.add_space(12.0);

let default_mapping = DimensionMapping::create(tensor.shape());
if ui
.add_enabled(
self.dimension_mapping != default_mapping,
egui::Button::new("Reset mapping"),
)
.on_disabled_hover_text("The default is already set up.")
.on_hover_text("Reset dimension mapping to the default.")
.clicked()
{
self.dimension_mapping = DimensionMapping::create(tensor.shape());
}

dimension_mapping_ui(
ctx.re_ui,
ui,
&mut self.dimension_mapping,
tensor.shape(),
);
});
}

self.texture_settings.show(ui);
Expand Down Expand Up @@ -570,11 +578,11 @@ fn selected_tensor_slice<'a, T: Copy>(
.height
.into_iter()
.chain(dim_mapping.width.into_iter())
.chain(dim_mapping.selectors.iter().copied())
.chain(dim_mapping.selectors.iter().map(|s| s.dim_idx))
.collect::<Vec<_>>();
let mut slice = tensor.view().permuted_axes(axis);

for dim_idx in &dim_mapping.selectors {
for DimensionSelector { dim_idx, .. } in &dim_mapping.selectors {
let selector_value = state
.selector_values
.get(dim_idx)
Expand Down Expand Up @@ -727,13 +735,17 @@ fn image_ui(
}

fn selectors_ui(ui: &mut egui::Ui, state: &mut ViewTensorState, tensor: &ClassicTensor) {
for &dim_idx in &state.dimension_mapping.selectors {
let dim = &tensor.shape()[dim_idx];
for selector in &state.dimension_mapping.selectors {
if !selector.visible {
continue;
}

let dim = &tensor.shape()[selector.dim_idx];
let size = dim.size;

let selector_value = state
.selector_values
.entry(dim_idx)
.entry(selector.dim_idx)
.or_insert_with(|| size / 2); // start in the middle

if size > 0 {
Expand All @@ -743,11 +755,12 @@ fn selectors_ui(ui: &mut egui::Ui, state: &mut ViewTensorState, tensor: &Classic
if size > 1 {
ui.horizontal(|ui| {
let name = dim.name.as_ref().map_or_else(
|| format!("dimension {dim_idx}"),
|| format!("dimension {}", selector.dim_idx),
|name| format!("{name:?}"),
);

ui.weak(format!("Slice selector for {name}:"));
let slider_tooltip = format!("Adjust the selected slice for the {name} dimension");
ui.weak(format!("{name}:")).on_hover_text(&slider_tooltip);

// If the range is big (say, 2048) then we would need
// a slider that is 2048 pixels wide to get the good precision.
Expand All @@ -757,15 +770,18 @@ fn selectors_ui(ui: &mut egui::Ui, state: &mut ViewTensorState, tensor: &Classic
.clamp_range(0..=size - 1)
.speed(0.5),
)
.on_hover_text("Drag to precisely control the slice index");
.on_hover_text(format!(
"Drag to precisely control the slice index of the {name} dimension"
));

// Make the slider as big as needed:
const MIN_SLIDER_WIDTH: f32 = 64.0;
if ui.available_width() >= MIN_SLIDER_WIDTH {
ui.spacing_mut().slider_width = (size as f32 * 2.0)
.at_least(MIN_SLIDER_WIDTH)
.at_most(ui.available_width());
ui.add(egui::Slider::new(selector_value, 0..=size - 1).show_value(false));
ui.add(egui::Slider::new(selector_value, 0..=size - 1).show_value(false))
.on_hover_text(slider_tooltip);
}
});
}
Expand Down
11 changes: 10 additions & 1 deletion crates/re_viewer/src/ui/viewport.rs
Original file line number Diff line number Diff line change
Expand Up @@ -414,7 +414,16 @@ impl Viewport {
for (category, entity_paths) in
SpaceView::default_queries_entities_by_category(ctx, spaces_info, space_info)
{
space_views.push(SpaceView::new(category, space_info, &entity_paths));
// For tensors create one space view for each tensor.
if category == ViewCategory::Tensor {
for entity_path in entity_paths {
let mut space_view = SpaceView::new(category, space_info, &[entity_path]);
space_view.allow_auto_adding_more_entities = false;
space_views.push(space_view);
}
} else {
space_views.push(SpaceView::new(category, space_info, &entity_paths));
}
}
}

Expand Down