Skip to content

Commit

Permalink
Use GPU colormapping when showing images in the GUI (#1865)
Browse files Browse the repository at this point in the history
* Cleanup: move Default close to the struct definition

* Simplify code: use if-let-else-return

* Simplify code: no need for Arc

* Add EntityDataUi so that the Tensor ui function knows entity path

* Better naming: selection -> item

* Simplify code: no optional tensor stats

* Less use of anyhow

* Use GPU colormapping when showing tensors in GUI

* Link to issue

* Optimize pad_to_four_elements for debug builds

* Refactor: simpler arguments to show_zoomed_image_region_area_outline

* Fix missing meter argument

* Refactor: break up long function

* Less use of Arc

* Pipe annotation context to the hover preview

* Simplify `AnnotationMap::find`

* Use new GPU colormapper for the hover-zoom-in tooltip

* Refactor

* Add helper function for turning a Tensor into an image::DynamicImage

* Fix warning on web builds

* Add helper function `Tensor::could_be_dynamic_image`

* Implement click-to-copy and click-to-save for tensors without egui

* Convert histogram to the new system

* Remove the TensorImageCache

* Fix TODO formatting

* bug fixes and cleanups

* Rename some stuff

* Build-fix

* Simplify some code
  • Loading branch information
emilk authored Apr 17, 2023
1 parent adf9856 commit c7f5cb6
Show file tree
Hide file tree
Showing 20 changed files with 773 additions and 907 deletions.
32 changes: 16 additions & 16 deletions crates/re_data_store/src/entity_properties.rs
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,22 @@ pub struct EntityProperties {
pub backproject_radius_scale: EditableAutoValue<f32>,
}

#[cfg(feature = "serde")]
impl Default for EntityProperties {
fn default() -> Self {
Self {
visible: true,
visible_history: ExtraQueryHistory::default(),
interactive: true,
color_mapper: EditableAutoValue::default(),
pinhole_image_plane_distance: EditableAutoValue::default(),
backproject_depth: EditableAutoValue::Auto(true),
depth_from_world_scale: EditableAutoValue::default(),
backproject_radius_scale: EditableAutoValue::Auto(1.0),
}
}
}

#[cfg(feature = "serde")]
impl EntityProperties {
/// Multiply/and these together.
Expand Down Expand Up @@ -97,22 +113,6 @@ impl EntityProperties {
}
}

#[cfg(feature = "serde")]
impl Default for EntityProperties {
fn default() -> Self {
Self {
visible: true,
visible_history: ExtraQueryHistory::default(),
interactive: true,
color_mapper: EditableAutoValue::default(),
pinhole_image_plane_distance: EditableAutoValue::default(),
backproject_depth: EditableAutoValue::Auto(true),
depth_from_world_scale: EditableAutoValue::default(),
backproject_radius_scale: EditableAutoValue::Auto(1.0),
}
}
}

// ----------------------------------------------------------------------------

/// When showing an entity in the history view, add this much history to it.
Expand Down
2 changes: 1 addition & 1 deletion crates/re_log_types/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ ecolor = ["dep:ecolor"]
glam = ["dep:glam", "dep:macaw"]

## Integration with the [`image`](https://crates.io/crates/image/) crate.
image = ["dep:image"]
image = ["dep:ecolor", "dep:image"]

## Enable (de)serialization using serde.
serde = [
Expand Down
4 changes: 2 additions & 2 deletions crates/re_log_types/src/component_types/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -58,11 +58,11 @@ pub use radius::Radius;
pub use rect::Rect2D;
pub use scalar::{Scalar, ScalarPlotProps};
pub use size::Size3D;
#[cfg(feature = "image")]
pub use tensor::TensorImageError;
pub use tensor::{
Tensor, TensorCastError, TensorData, TensorDataMeaning, TensorDimension, TensorId,
};
#[cfg(feature = "image")]
pub use tensor::{TensorImageLoadError, TensorImageSaveError};
pub use text_entry::TextEntry;
pub use transform::{Pinhole, Rigid3, Transform};
pub use vec::{Vec2D, Vec3D, Vec4D};
Expand Down
161 changes: 154 additions & 7 deletions crates/re_log_types/src/component_types/tensor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,23 @@ impl TensorData {
pub fn is_empty(&self) -> bool {
self.size_in_bytes() == 0
}

pub fn is_compressed_image(&self) -> bool {
match self {
Self::U8(_)
| Self::U16(_)
| Self::U32(_)
| Self::U64(_)
| Self::I8(_)
| Self::I16(_)
| Self::I32(_)
| Self::I64(_)
| Self::F32(_)
| Self::F64(_) => false,

Self::JPEG(_) => true,
}
}
}

impl std::fmt::Debug for TensorData {
Expand Down Expand Up @@ -588,9 +605,10 @@ impl<'a> TryFrom<&'a Tensor> for ::ndarray::ArrayViewD<'a, half::f16> {

// ----------------------------------------------------------------------------

/// Errors when loading [`Tensor`] from the [`image`] crate.
#[cfg(feature = "image")]
#[derive(thiserror::Error, Debug)]
pub enum TensorImageError {
pub enum TensorImageLoadError {
#[error(transparent)]
Image(#[from] image::ImageError),

Expand All @@ -604,6 +622,20 @@ pub enum TensorImageError {
ReadError(#[from] std::io::Error),
}

/// Errors when converting [`Tensor`] to [`image`] images.
#[cfg(feature = "image")]
#[derive(thiserror::Error, Debug)]
pub enum TensorImageSaveError {
#[error("Expected image-shaped tensor, got {0:?}")]
ShapeNotAnImage(Vec<TensorDimension>),

#[error("Cannot convert tensor with {0} channels and datatype {1} to an image")]
UnsupportedChannelsDtype(u64, TensorDataType),

#[error("The tensor data did not match tensor dimensions")]
BadData,
}

impl Tensor {
pub fn new(
tensor_id: TensorId,
Expand All @@ -630,20 +662,20 @@ impl Tensor {
#[cfg(not(target_arch = "wasm32"))]
pub fn tensor_from_jpeg_file(
image_path: impl AsRef<std::path::Path>,
) -> Result<Self, TensorImageError> {
) -> Result<Self, TensorImageLoadError> {
let jpeg_bytes = std::fs::read(image_path)?;
Self::tensor_from_jpeg_bytes(jpeg_bytes)
}

/// Construct a tensor from the contents of a JPEG file.
///
/// Requires the `image` feature.
pub fn tensor_from_jpeg_bytes(jpeg_bytes: Vec<u8>) -> Result<Self, TensorImageError> {
pub fn tensor_from_jpeg_bytes(jpeg_bytes: Vec<u8>) -> Result<Self, TensorImageLoadError> {
use image::ImageDecoder as _;
let jpeg = image::codecs::jpeg::JpegDecoder::new(std::io::Cursor::new(&jpeg_bytes))?;
if jpeg.color_type() != image::ColorType::Rgb8 {
// TODO(emilk): support gray-scale jpeg as well
return Err(TensorImageError::UnsupportedJpegColorType(
return Err(TensorImageLoadError::UnsupportedJpegColorType(
jpeg.color_type(),
));
}
Expand All @@ -665,14 +697,14 @@ impl Tensor {
/// Construct a tensor from something that can be turned into a [`image::DynamicImage`].
///
/// Requires the `image` feature.
pub fn from_image(image: impl Into<image::DynamicImage>) -> Result<Self, TensorImageError> {
pub fn from_image(image: impl Into<image::DynamicImage>) -> Result<Self, TensorImageLoadError> {
Self::from_dynamic_image(image.into())
}

/// Construct a tensor from [`image::DynamicImage`].
///
/// Requires the `image` feature.
pub fn from_dynamic_image(image: image::DynamicImage) -> Result<Self, TensorImageError> {
pub fn from_dynamic_image(image: image::DynamicImage) -> Result<Self, TensorImageLoadError> {
let (w, h) = (image.width(), image.height());

let (depth, data) = match image {
Expand Down Expand Up @@ -706,7 +738,9 @@ impl Tensor {
}
_ => {
// It is very annoying that DynamicImage is #[non_exhaustive]
return Err(TensorImageError::UnsupportedImageColorType(image.color()));
return Err(TensorImageLoadError::UnsupportedImageColorType(
image.color(),
));
}
};

Expand All @@ -722,6 +756,119 @@ impl Tensor {
meter: None,
})
}

/// Predicts if [`Self::to_dynamic_image`] is likely to succeed, without doing anything expensive
pub fn could_be_dynamic_image(&self) -> bool {
self.is_shaped_like_an_image()
&& matches!(
self.dtype(),
TensorDataType::U8
| TensorDataType::U16
| TensorDataType::F16
| TensorDataType::F32
| TensorDataType::F64
)
}

/// Try to convert an image-like tensor into an [`image::DynamicImage`].
pub fn to_dynamic_image(&self) -> Result<image::DynamicImage, TensorImageSaveError> {
use ecolor::{gamma_u8_from_linear_f32, linear_u8_from_linear_f32};
use image::{DynamicImage, GrayImage, RgbImage, RgbaImage};

type Rgb16Image = image::ImageBuffer<image::Rgb<u16>, Vec<u16>>;
type Rgba16Image = image::ImageBuffer<image::Rgba<u16>, Vec<u16>>;
type Gray16Image = image::ImageBuffer<image::Luma<u16>, Vec<u16>>;

let [h, w, channels] = self
.image_height_width_channels()
.ok_or_else(|| TensorImageSaveError::ShapeNotAnImage(self.shape.clone()))?;
let w = w as u32;
let h = h as u32;

let dyn_img_result =
match (channels, &self.data) {
(1, TensorData::U8(buf)) => {
GrayImage::from_raw(w, h, buf.as_slice().to_vec()).map(DynamicImage::ImageLuma8)
}
(1, TensorData::U16(buf)) => Gray16Image::from_raw(w, h, buf.as_slice().to_vec())
.map(DynamicImage::ImageLuma16),
// TODO(emilk) f16
(1, TensorData::F32(buf)) => {
let pixels = buf
.iter()
.map(|pixel| gamma_u8_from_linear_f32(*pixel))
.collect();
GrayImage::from_raw(w, h, pixels).map(DynamicImage::ImageLuma8)
}
(1, TensorData::F64(buf)) => {
let pixels = buf
.iter()
.map(|&pixel| gamma_u8_from_linear_f32(pixel as f32))
.collect();
GrayImage::from_raw(w, h, pixels).map(DynamicImage::ImageLuma8)
}

(3, TensorData::U8(buf)) => {
RgbImage::from_raw(w, h, buf.as_slice().to_vec()).map(DynamicImage::ImageRgb8)
}
(3, TensorData::U16(buf)) => Rgb16Image::from_raw(w, h, buf.as_slice().to_vec())
.map(DynamicImage::ImageRgb16),
(3, TensorData::F32(buf)) => {
let pixels = buf.iter().copied().map(gamma_u8_from_linear_f32).collect();
RgbImage::from_raw(w, h, pixels).map(DynamicImage::ImageRgb8)
}
(3, TensorData::F64(buf)) => {
let pixels = buf
.iter()
.map(|&comp| gamma_u8_from_linear_f32(comp as f32))
.collect();
RgbImage::from_raw(w, h, pixels).map(DynamicImage::ImageRgb8)
}

(4, TensorData::U8(buf)) => {
RgbaImage::from_raw(w, h, buf.as_slice().to_vec()).map(DynamicImage::ImageRgba8)
}
(4, TensorData::U16(buf)) => Rgba16Image::from_raw(w, h, buf.as_slice().to_vec())
.map(DynamicImage::ImageRgba16),
(4, TensorData::F32(buf)) => {
let rgba: &[[f32; 4]] = bytemuck::cast_slice(buf.as_slice());
let pixels: Vec<u8> = rgba
.iter()
.flat_map(|&[r, g, b, a]| {
let r = gamma_u8_from_linear_f32(r);
let g = gamma_u8_from_linear_f32(g);
let b = gamma_u8_from_linear_f32(b);
let a = linear_u8_from_linear_f32(a);
[r, g, b, a]
})
.collect();
RgbaImage::from_raw(w, h, pixels).map(DynamicImage::ImageRgba8)
}
(4, TensorData::F64(buf)) => {
let rgba: &[[f64; 4]] = bytemuck::cast_slice(buf.as_slice());
let pixels: Vec<u8> = rgba
.iter()
.flat_map(|&[r, g, b, a]| {
let r = gamma_u8_from_linear_f32(r as _);
let g = gamma_u8_from_linear_f32(g as _);
let b = gamma_u8_from_linear_f32(b as _);
let a = linear_u8_from_linear_f32(a as _);
[r, g, b, a]
})
.collect();
RgbaImage::from_raw(w, h, pixels).map(DynamicImage::ImageRgba8)
}

(_, _) => {
return Err(TensorImageSaveError::UnsupportedChannelsDtype(
channels,
self.data.dtype(),
))
}
};

dyn_img_result.ok_or(TensorImageSaveError::BadData)
}
}

// ----------------------------------------------------------------------------
Expand Down
2 changes: 2 additions & 0 deletions crates/re_renderer/src/renderer/rectangles.rs
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ pub enum TextureFilterMin {
}

/// Describes a texture and how to map it to a color.
#[derive(Clone)]
pub struct ColormappedTexture {
pub texture: GpuTexture2DHandle,

Expand All @@ -71,6 +72,7 @@ pub struct ColormappedTexture {
}

/// How to map the normalized `.r` component to a color.
#[derive(Clone)]
pub enum ColorMapper {
/// Apply the given function.
Function(Colormap),
Expand Down
5 changes: 5 additions & 0 deletions crates/re_renderer/src/resource_managers/texture_manager.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,11 @@ impl GpuTexture2DHandle {
pub fn height(&self) -> u32 {
self.0.as_ref().map_or(1, |t| t.texture.height())
}

/// Width and height of the texture.
pub fn width_height(&self) -> [u32; 2] {
[self.width(), self.height()]
}
}

/// Data required to create a texture 2d resource.
Expand Down
4 changes: 2 additions & 2 deletions crates/re_renderer/src/view_builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ use crate::{
},
global_bindings::FrameUniformBuffer,
renderer::{CompositorDrawData, DebugOverlayDrawData, DrawData, Renderer},
wgpu_resources::{GpuBindGroup, GpuTexture, TextureDesc},
wgpu_resources::{GpuBindGroup, GpuTexture, PoolError, TextureDesc},
DebugLabel, IntRect, Rgba, Size,
};

Expand Down Expand Up @@ -498,7 +498,7 @@ impl ViewBuilder {
&mut self,
ctx: &RenderContext,
clear_color: Rgba,
) -> anyhow::Result<wgpu::CommandBuffer> {
) -> Result<wgpu::CommandBuffer, PoolError> {
crate::profile_function!();

let setup = &self.setup;
Expand Down
19 changes: 16 additions & 3 deletions crates/re_viewer/src/gpu_bridge/tensor_to_gpu.rs
Original file line number Diff line number Diff line change
Expand Up @@ -414,9 +414,22 @@ fn narrow_f64_to_f32s(slice: &[f64]) -> Cow<'static, [u8]> {

fn pad_to_four_elements<T: Copy>(data: &[T], pad: T) -> Vec<T> {
crate::profile_function!();
data.chunks_exact(3)
.flat_map(|chunk| [chunk[0], chunk[1], chunk[2], pad])
.collect::<Vec<T>>()
if cfg!(debug_assertions) {
// fastest version in debug builds.
// 5x faster in debug builds, but 2x slower in release
let mut padded = vec![pad; data.len() / 3 * 4];
for i in 0..(data.len() / 3) {
padded[4 * i] = data[3 * i];
padded[4 * i + 1] = data[3 * i + 1];
padded[4 * i + 2] = data[3 * i + 2];
}
padded
} else {
// fastest version in optimized builds
data.chunks_exact(3)
.flat_map(|chunk| [chunk[0], chunk[1], chunk[2], pad])
.collect()
}
}

fn pad_and_cast<T: Copy + Pod>(data: &[T], pad: T) -> Cow<'static, [u8]> {
Expand Down
Loading

1 comment on commit c7f5cb6

@github-actions
Copy link

Choose a reason for hiding this comment

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

Rust Benchmark

Benchmark suite Current: c7f5cb6 Previous: adf9856 Ratio
datastore/num_rows=1000/num_instances=1000/packed=false/insert/default 3910006 ns/iter (± 258968) 3884493 ns/iter (± 224684) 1.01
datastore/num_rows=1000/num_instances=1000/packed=false/latest_at/default 370 ns/iter (± 1) 370 ns/iter (± 1) 1
datastore/num_rows=1000/num_instances=1000/packed=false/latest_at_missing/primary/default 261 ns/iter (± 0) 261 ns/iter (± 0) 1
datastore/num_rows=1000/num_instances=1000/packed=false/latest_at_missing/secondaries/default 420 ns/iter (± 0) 420 ns/iter (± 0) 1
datastore/num_rows=1000/num_instances=1000/packed=false/range/default 3829057 ns/iter (± 184303) 3839235 ns/iter (± 208720) 1.00
datastore/num_rows=1000/num_instances=1000/gc/default 2398158 ns/iter (± 5289) 2386181 ns/iter (± 7225) 1.01
mono_points_arrow/generate_message_bundles 31241270 ns/iter (± 493329) 31121592 ns/iter (± 444364) 1.00
mono_points_arrow/generate_messages 128841598 ns/iter (± 1163017) 127961340 ns/iter (± 1094579) 1.01
mono_points_arrow/encode_log_msg 163234030 ns/iter (± 2103568) 161906054 ns/iter (± 2440212) 1.01
mono_points_arrow/encode_total 324093156 ns/iter (± 2241102) 319505901 ns/iter (± 2471660) 1.01
mono_points_arrow/decode_log_msg 192126769 ns/iter (± 1198237) 193377167 ns/iter (± 1176387) 0.99
mono_points_arrow/decode_message_bundles 71645418 ns/iter (± 748245) 71953200 ns/iter (± 627613) 1.00
mono_points_arrow/decode_total 262243580 ns/iter (± 1637296) 262749089 ns/iter (± 1904379) 1.00
mono_points_arrow_batched/generate_message_bundles 25213628 ns/iter (± 1280912) 25979718 ns/iter (± 1444642) 0.97
mono_points_arrow_batched/generate_messages 4771532 ns/iter (± 448125) 5046246 ns/iter (± 489858) 0.95
mono_points_arrow_batched/encode_log_msg 1401615 ns/iter (± 14794) 1395661 ns/iter (± 4692) 1.00
mono_points_arrow_batched/encode_total 32892767 ns/iter (± 2185005) 32890333 ns/iter (± 1815439) 1.00
mono_points_arrow_batched/decode_log_msg 781794 ns/iter (± 3333) 786757 ns/iter (± 3628) 0.99
mono_points_arrow_batched/decode_message_bundles 7787474 ns/iter (± 294582) 8010830 ns/iter (± 467979) 0.97
mono_points_arrow_batched/decode_total 9005564 ns/iter (± 426309) 9320700 ns/iter (± 528144) 0.97
batch_points_arrow/generate_message_bundles 192718 ns/iter (± 515) 193675 ns/iter (± 435) 1.00
batch_points_arrow/generate_messages 5187 ns/iter (± 11) 5140 ns/iter (± 10) 1.01
batch_points_arrow/encode_log_msg 258731 ns/iter (± 1661) 266170 ns/iter (± 1730) 0.97
batch_points_arrow/encode_total 494700 ns/iter (± 3508) 495605 ns/iter (± 3605) 1.00
batch_points_arrow/decode_log_msg 212663 ns/iter (± 953) 213925 ns/iter (± 1642) 0.99
batch_points_arrow/decode_message_bundles 1878 ns/iter (± 7) 1870 ns/iter (± 8) 1.00
batch_points_arrow/decode_total 221211 ns/iter (± 1069) 223284 ns/iter (± 1391) 0.99
arrow_mono_points/insert 2544958905 ns/iter (± 3786039) 2520560471 ns/iter (± 5545821) 1.01
arrow_mono_points/query 1196089 ns/iter (± 23127) 1219187 ns/iter (± 15121) 0.98
arrow_batch_points/insert 1156597 ns/iter (± 2349) 1153495 ns/iter (± 10456) 1.00
arrow_batch_points/query 14929 ns/iter (± 83) 15049 ns/iter (± 70) 0.99
arrow_batch_vecs/insert 26539 ns/iter (± 123) 26690 ns/iter (± 82) 0.99
arrow_batch_vecs/query 325977 ns/iter (± 814) 325241 ns/iter (± 830) 1.00
tuid/Tuid::random 34 ns/iter (± 0) 34 ns/iter (± 0) 1

This comment was automatically generated by workflow using github-action-benchmark.

Please sign in to comment.