Skip to content

Commit 2843e9c

Browse files
Wumpfemilk
authored andcommitted
Automatically determine image/tensor color mapping & need for srgb decoding (#2342)
<!-- Open the PR up as a draft until you feel it is ready for a proper review. Do not make PR:s from your own `main` branch, as that makes it difficult for reviewers to add their own fixes. Add any improvements to the branch as new commits to make it easier for reviewers to follow the progress. All commits will be squashed to a single commit once the PR is merged into `main`. Make sure you mention any issues that this PR closes in the description, as well as any other related issues. To get an auto-generated PR description you can put "copilot:summary" or "copilot:walkthrough" anywhere. --> Fixes #2274 * #2274 Will need to be further worked on via: * #2341 Introduces 3 new things actually: * finite range, determined if the range of a tensor was in fact not finite * heuristic for color mapping range * heuristic for srgb * ⚠️ does this break things? Range & sRGB: This ``` import rerun as rr import numpy as np import matplotlib.pyplot as plt rr.init("image color", spawn=True) img = np.random.random((300, 300, 3)) * 0.2 rr.log_image("image", img) print(img.min(), img.max()) # to check they are in range [0,1] plt.imshow(img) plt.show() ``` Gives now the expected image: ![image](https://github.com/rerun-io/rerun/assets/1220815/9fcda125-f762-4fd4-ae24-ce6dcaaa496a) When setting a pixel to inf, we show the "finite range" as well, and behave correctly: <img width="1026" alt="image" src="https://github.com/rerun-io/rerun/assets/1220815/7d3037e8-f562-4b6c-8f08-ec4f5627c21a"> * [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) <!-- This line will get updated when the PR build summary job finishes. --> PR Build Summary: https://build.rerun.io/pr/2342 <!-- pr-link-docs:start --> Docs preview: https://rerun.io/preview/93697e6/docs Examples preview: https://rerun.io/preview/93697e6/examples <!-- pr-link-docs:end -->
1 parent 7419dfe commit 2843e9c

File tree

10 files changed

+223
-58
lines changed

10 files changed

+223
-58
lines changed

crates/re_data_ui/src/image.rs

+15-1
Original file line numberDiff line numberDiff line change
@@ -282,7 +282,10 @@ pub fn tensor_summary_ui_grid_contents(
282282
}
283283
}
284284

285-
let TensorStats { range } = tensor_stats;
285+
let TensorStats {
286+
range,
287+
finite_range,
288+
} = tensor_stats;
286289

287290
if let Some((min, max)) = range {
288291
ui.label("Data range")
@@ -294,6 +297,17 @@ pub fn tensor_summary_ui_grid_contents(
294297
));
295298
ui.end_row();
296299
}
300+
// Show finite range only if it is different from the actual range.
301+
if let (true, Some((min, max))) = (range != finite_range, finite_range) {
302+
ui.label("Finite data range")
303+
.on_hover_text("The finite values (ignoring all NaN & -Inf/+Inf) of the tensor range within these bounds.");
304+
ui.monospace(format!(
305+
"[{} - {}]",
306+
re_format::format_f64(*min),
307+
re_format::format_f64(*max)
308+
));
309+
ui.end_row();
310+
}
297311
}
298312

299313
pub fn tensor_summary_ui(

crates/re_log_types/src/component_types/tensor.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -566,7 +566,7 @@ impl Component for Tensor {
566566
}
567567
}
568568

569-
#[derive(thiserror::Error, Debug, PartialEq)]
569+
#[derive(thiserror::Error, Debug, PartialEq, Clone)]
570570
pub enum TensorCastError {
571571
#[error("ndarray type mismatch with tensor storage")]
572572
TypeMismatch,

crates/re_renderer/shader/rectangle.wgsl

+3
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,9 @@ struct UniformBuffer {
5353

5454
/// Boolean: decode 0-1 sRGB gamma to linear space before filtering?
5555
decode_srgb: u32,
56+
57+
/// Boolean: multiply RGB with alpha before filtering
58+
multiply_rgb_with_alpha: u32,
5659
};
5760

5861
@group(1) @binding(0)

crates/re_renderer/shader/rectangle_fs.wgsl

+39-32
Original file line numberDiff line numberDiff line change
@@ -14,78 +14,85 @@ fn tex_filter(pixel_coord: Vec2) -> u32 {
1414
}
1515
}
1616

17-
fn decode_color(rgba_arg: Vec4) -> Vec4 {
18-
var rgba = rgba_arg;
17+
fn normalize_range(sampled_value: Vec4) -> Vec4 {
18+
let range = rect_info.range_min_max;
19+
return (sampled_value - range.x) / (range.y - range.x);
20+
}
21+
22+
fn decode_color(sampled_value: Vec4) -> Vec4 {
23+
// Normalize the value first, otherwise premultiplying alpha and linear space conversion won't make sense.
24+
var rgba = normalize_range(sampled_value);
1925

20-
// Convert to linear space:
26+
// Convert to linear space
2127
if rect_info.decode_srgb != 0u {
22-
rgba = linear_from_srgba(rgba);
28+
if all(0.0 <= rgba.rgb) && all(rgba.rgb <= 1.0) {
29+
rgba = linear_from_srgba(rgba);
30+
} else {
31+
rgba = ERROR_RGBA; // out of range
32+
}
2333
}
2434

25-
// Premultiply alpha:
26-
rgba = vec4(rgba.xyz * rgba.a, rgba.a);
35+
// Premultiply alpha.
36+
if rect_info.multiply_rgb_with_alpha != 0u {
37+
rgba = vec4(rgba.xyz * rgba.a, rgba.a);
38+
}
2739

2840
return rgba;
2941
}
3042

43+
fn filter_bilinear(coord: Vec2, v00: Vec4, v01: Vec4, v10: Vec4, v11: Vec4) -> Vec4 {
44+
let top = mix(v00, v10, fract(coord.x - 0.5));
45+
let bottom = mix(v01, v11, fract(coord.x - 0.5));
46+
return mix(top, bottom, fract(coord.y - 0.5));
47+
}
48+
3149
@fragment
3250
fn fs_main(in: VertexOut) -> @location(0) Vec4 {
3351
// Sample the main texture:
34-
var sampled_value: Vec4;
52+
var normalized_value: Vec4;
3553
if rect_info.sample_type == SAMPLE_TYPE_FLOAT {
3654
let coord = in.texcoord * Vec2(textureDimensions(texture_float).xy);
3755
if tex_filter(coord) == FILTER_NEAREST {
3856
// nearest
39-
sampled_value = decode_color(textureLoad(texture_float, IVec2(coord), 0));
57+
normalized_value = decode_color(textureLoad(texture_float, IVec2(coord), 0));
4058
} else {
4159
// bilinear
4260
let v00 = decode_color(textureLoad(texture_float, IVec2(coord + vec2(-0.5, -0.5)), 0));
4361
let v01 = decode_color(textureLoad(texture_float, IVec2(coord + vec2(-0.5, 0.5)), 0));
4462
let v10 = decode_color(textureLoad(texture_float, IVec2(coord + vec2( 0.5, -0.5)), 0));
4563
let v11 = decode_color(textureLoad(texture_float, IVec2(coord + vec2( 0.5, 0.5)), 0));
46-
let top = mix(v00, v10, fract(coord.x - 0.5));
47-
let bottom = mix(v01, v11, fract(coord.x - 0.5));
48-
sampled_value = mix(top, bottom, fract(coord.y - 0.5));
64+
normalized_value = filter_bilinear(coord, v00, v01, v10, v11);
4965
}
5066
} else if rect_info.sample_type == SAMPLE_TYPE_SINT {
5167
let coord = in.texcoord * Vec2(textureDimensions(texture_sint).xy);
5268
if tex_filter(coord) == FILTER_NEAREST {
5369
// nearest
54-
sampled_value = Vec4(textureLoad(texture_sint, IVec2(coord), 0));
70+
normalized_value = decode_color(Vec4(textureLoad(texture_sint, IVec2(coord), 0)));
5571
} else {
5672
// bilinear
57-
let v00 = Vec4(textureLoad(texture_sint, IVec2(coord + vec2(-0.5, -0.5)), 0));
58-
let v01 = Vec4(textureLoad(texture_sint, IVec2(coord + vec2(-0.5, 0.5)), 0));
59-
let v10 = Vec4(textureLoad(texture_sint, IVec2(coord + vec2( 0.5, -0.5)), 0));
60-
let v11 = Vec4(textureLoad(texture_sint, IVec2(coord + vec2( 0.5, 0.5)), 0));
61-
let top = mix(v00, v10, fract(coord.x - 0.5));
62-
let bottom = mix(v01, v11, fract(coord.x - 0.5));
63-
sampled_value = mix(top, bottom, fract(coord.y - 0.5));
73+
let v00 = decode_color(Vec4(textureLoad(texture_sint, IVec2(coord + vec2(-0.5, -0.5)), 0)));
74+
let v01 = decode_color(Vec4(textureLoad(texture_sint, IVec2(coord + vec2(-0.5, 0.5)), 0)));
75+
let v10 = decode_color(Vec4(textureLoad(texture_sint, IVec2(coord + vec2( 0.5, -0.5)), 0)));
76+
let v11 = decode_color(Vec4(textureLoad(texture_sint, IVec2(coord + vec2( 0.5, 0.5)), 0)));
77+
normalized_value = filter_bilinear(coord, v00, v01, v10, v11);
6478
}
6579
} else if rect_info.sample_type == SAMPLE_TYPE_UINT {
66-
// TODO(emilk): support premultiplying alpha on this path. Requires knowing the alpha range (255, 65535, …).
6780
let coord = in.texcoord * Vec2(textureDimensions(texture_uint).xy);
6881
if tex_filter(coord) == FILTER_NEAREST {
6982
// nearest
70-
sampled_value = Vec4(textureLoad(texture_uint, IVec2(coord), 0));
83+
normalized_value = decode_color(Vec4(textureLoad(texture_uint, IVec2(coord), 0)));
7184
} else {
7285
// bilinear
73-
let v00 = Vec4(textureLoad(texture_uint, IVec2(coord + vec2(-0.5, -0.5)), 0));
74-
let v01 = Vec4(textureLoad(texture_uint, IVec2(coord + vec2(-0.5, 0.5)), 0));
75-
let v10 = Vec4(textureLoad(texture_uint, IVec2(coord + vec2( 0.5, -0.5)), 0));
76-
let v11 = Vec4(textureLoad(texture_uint, IVec2(coord + vec2( 0.5, 0.5)), 0));
77-
let top = mix(v00, v10, fract(coord.x - 0.5));
78-
let bottom = mix(v01, v11, fract(coord.x - 0.5));
79-
sampled_value = mix(top, bottom, fract(coord.y - 0.5));
86+
let v00 = decode_color(Vec4(textureLoad(texture_uint, IVec2(coord + vec2(-0.5, -0.5)), 0)));
87+
let v01 = decode_color(Vec4(textureLoad(texture_uint, IVec2(coord + vec2(-0.5, 0.5)), 0)));
88+
let v10 = decode_color(Vec4(textureLoad(texture_uint, IVec2(coord + vec2( 0.5, -0.5)), 0)));
89+
let v11 = decode_color(Vec4(textureLoad(texture_uint, IVec2(coord + vec2( 0.5, 0.5)), 0)));
90+
normalized_value = filter_bilinear(coord, v00, v01, v10, v11);
8091
}
8192
} else {
8293
return ERROR_RGBA; // unknown sample type
8394
}
8495

85-
// Normalize the sample:
86-
let range = rect_info.range_min_max;
87-
var normalized_value: Vec4 = (sampled_value - range.x) / (range.y - range.x);
88-
8996
// Apply gamma:
9097
normalized_value = vec4(pow(normalized_value.rgb, vec3(rect_info.gamma)), normalized_value.a);
9198

crates/re_renderer/src/renderer/rectangles.rs

+17-4
Original file line numberDiff line numberDiff line change
@@ -53,14 +53,23 @@ pub enum TextureFilterMin {
5353
pub struct ColormappedTexture {
5454
pub texture: GpuTexture2D,
5555

56+
/// Min/max range of the values in the texture.
57+
///
58+
/// Used to normalize the input values (squash them to the 0-1 range).
59+
/// The normalization is applied before sRGB gamma decoding and alpha pre-multiplication
60+
/// (this transformation is also applied to alpha!).
61+
pub range: [f32; 2],
62+
5663
/// Decode 0-1 sRGB gamma values to linear space before filtering?
5764
///
5865
/// Only applies to [`wgpu::TextureFormat::Rgba8Unorm`] and float textures.
5966
pub decode_srgb: bool,
6067

61-
/// Min/max range of the values in the texture.
62-
/// Used to normalize the input values (squash them to the 0-1 range).
63-
pub range: [f32; 2],
68+
/// Multiply color channels with the alpha channel before filtering?
69+
///
70+
/// Set this to false for textures that don't have an alpha channel or are already pre-multiplied.
71+
/// Applied after range normalization and srgb decoding, before filtering.
72+
pub multiply_rgb_with_alpha: bool,
6473

6574
/// Raise the normalized values to this power (before any color mapping).
6675
/// Acts like an inverse brightness.
@@ -102,6 +111,7 @@ impl ColormappedTexture {
102111
decode_srgb,
103112
range: [0.0, 1.0],
104113
gamma: 1.0,
114+
multiply_rgb_with_alpha: true,
105115
color_mapper: None,
106116
}
107117
}
@@ -224,7 +234,8 @@ mod gpu_data {
224234
magnification_filter: u32,
225235

226236
decode_srgb: u32,
227-
_row_padding: [u32; 3],
237+
multiply_rgb_with_alpha: u32,
238+
_row_padding: [u32; 2],
228239

229240
_end_padding: [wgpu_buffer_types::PaddingRow; 16 - 7],
230241
}
@@ -251,6 +262,7 @@ mod gpu_data {
251262
range,
252263
gamma,
253264
color_mapper,
265+
multiply_rgb_with_alpha,
254266
} = colormapped_texture;
255267

256268
let super::RectangleOptions {
@@ -322,6 +334,7 @@ mod gpu_data {
322334
minification_filter,
323335
magnification_filter,
324336
decode_srgb: *decode_srgb as _,
337+
multiply_rgb_with_alpha: *multiply_rgb_with_alpha as _,
325338
_row_padding: Default::default(),
326339
_end_padding: Default::default(),
327340
})

crates/re_viewer/src/ui/view_tensor/tensor_slice_to_gpu.rs

+5-4
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ use re_renderer::{
44
resource_managers::{GpuTexture2D, Texture2DCreationDesc, TextureManager2DError},
55
};
66
use re_viewer_context::{
7-
gpu_bridge::{self, range, RangeError},
7+
gpu_bridge::{self, tensor_data_range_heuristic, RangeError},
88
TensorStats,
99
};
1010

@@ -33,16 +33,17 @@ pub fn colormapped_texture(
3333
) -> Result<ColormappedTexture, TextureManager2DError<TensorUploadError>> {
3434
crate::profile_function!();
3535

36-
let range =
37-
range(tensor_stats).map_err(|err| TextureManager2DError::DataCreation(err.into()))?;
36+
let range = tensor_data_range_heuristic(tensor_stats, tensor.dtype())
37+
.map_err(|err| TextureManager2DError::DataCreation(err.into()))?;
3838
let texture = upload_texture_slice_to_gpu(render_ctx, tensor, state.slice())?;
3939

4040
let color_mapping = state.color_mapping();
4141

4242
Ok(ColormappedTexture {
4343
texture,
44-
decode_srgb: false,
4544
range,
45+
decode_srgb: false,
46+
multiply_rgb_with_alpha: false,
4647
gamma: color_mapping.gamma,
4748
color_mapper: Some(re_renderer::renderer::ColorMapper::Function(
4849
color_mapping.map,

crates/re_viewer/src/ui/view_tensor/ui.rs

+2-1
Original file line numberDiff line numberDiff line change
@@ -326,8 +326,9 @@ fn paint_colormap_gradient(
326326

327327
let colormapped_texture = re_renderer::renderer::ColormappedTexture {
328328
texture: horizontal_gradient,
329-
decode_srgb: false,
330329
range: [0.0, 1.0],
330+
decode_srgb: false,
331+
multiply_rgb_with_alpha: false,
331332
gamma: 1.0,
332333
color_mapper: Some(re_renderer::renderer::ColorMapper::Function(colormap)),
333334
};

crates/re_viewer_context/src/gpu_bridge/mod.rs

+38-7
Original file line numberDiff line numberDiff line change
@@ -25,28 +25,59 @@ pub enum RangeError {
2525
/// This is weird. Should only happen with JPEGs, and those should have been decoded already
2626
#[error("Missing a range.")]
2727
MissingRange,
28-
29-
#[error("Non-finite range of values")]
30-
NonfiniteRange,
3128
}
3229

3330
/// Get a valid, finite range for the gpu to use.
34-
pub fn range(tensor_stats: &TensorStats) -> Result<[f32; 2], RangeError> {
35-
let (min, max) = tensor_stats.range.ok_or(RangeError::MissingRange)?;
31+
pub fn tensor_data_range_heuristic(
32+
tensor_stats: &TensorStats,
33+
data_type: re_log_types::TensorDataType,
34+
) -> Result<[f32; 2], RangeError> {
35+
let (min, max) = tensor_stats.finite_range.ok_or(RangeError::MissingRange)?;
3636

3737
let min = min as f32;
3838
let max = max as f32;
3939

40-
if !min.is_finite() || !max.is_finite() {
41-
Err(RangeError::NonfiniteRange)
40+
// Apply heuristic for ranges that are typically expected depending on the data type and the finite (!) range.
41+
// (we ignore NaN/Inf values heres, since they are usually there by accident!)
42+
if data_type.is_float() && 0.0 <= min && max <= 1.0 {
43+
// Float values that are all between 0 and 1, assume that this is the range.
44+
Ok([0.0, 1.0])
45+
} else if 0.0 <= min && max <= 255.0 {
46+
// If all values are between 0 and 255, assume this is the range.
47+
// (This is very common, independent of the data type)
48+
Ok([0.0, 255.0])
4249
} else if min == max {
4350
// uniform range. This can explode the colormapping, so let's map all colors to the middle:
4451
Ok([min - 1.0, max + 1.0])
4552
} else {
53+
// Use range as is if nothing matches.
4654
Ok([min, max])
4755
}
4856
}
4957

58+
/// Return whether a tensor should be assumed to be encoded in sRGB color space ("gamma space", no EOTF applied).
59+
pub fn tensor_decode_srgb_gamma_heuristic(
60+
tensor_stats: &TensorStats,
61+
data_type: re_log_types::TensorDataType,
62+
channels: u32,
63+
) -> Result<bool, RangeError> {
64+
if matches!(channels, 1 | 3 | 4) {
65+
let (min, max) = tensor_stats.finite_range.ok_or(RangeError::MissingRange)?;
66+
#[allow(clippy::if_same_then_else)]
67+
if 0.0 <= min && max <= 255.0 {
68+
// If the range is suspiciously reminding us of a "regular image", assume sRGB.
69+
Ok(true)
70+
} else if data_type.is_float() && 0.0 <= min && max <= 1.0 {
71+
// Floating point images between 0 and 1 are often sRGB as well.
72+
Ok(true)
73+
} else {
74+
Ok(false)
75+
}
76+
} else {
77+
Ok(false)
78+
}
79+
}
80+
5081
// ----------------------------------------------------------------------------
5182

5283
pub fn viewport_resolution_in_pixels(clip_rect: egui::Rect, pixels_from_point: f32) -> [u32; 2] {

0 commit comments

Comments
 (0)