Depth Estimation

Estimate depth from a photo or webcam with ONNX Runtime Web on WebGPU. Shade its GPU-resident output beside the input through a zero-copy vgpu buffer wrap.

Open fullscreen
// Draws the input beside its depth estimate: colour on the left, depth as
// grayscale on the right, brighter meaning nearer.
//
// Both halves read plain row-major storage buffers. The depth buffer is the
// model's output memory, wrapped without a copy, which is the whole point of
// the example -- 250k f32 values per frame are shaded straight out of the
// tensor ONNX Runtime wrote. The colour buffer is the same frame packed as
// RGBA8, uploaded once per inference.
//
// Grayscale rather than a colour ramp on purpose: a rainbow palette makes a
// depth map look richer than it is by spending hue on small differences, and
// the point of showing the two halves together is to let the viewer judge how
// much of the scene the model actually recovered.
 
struct Uniforms {
  resolution: vec2f,
  depth_size: vec2f,
  mode: f32,
  near_meters: f32,
  far_meters: f32,
  has_result: f32,
};
 
@group(0) @binding(0) var<uniform> uniforms: Uniforms;
@group(0) @binding(1) var<storage, read> depth: array<f32>;
@group(0) @binding(2) var<storage, read> range: array<u32>;
@group(0) @binding(3) var<storage, read> colour: array<u32>;
 
const PRESENTATION_LOG_METRIC: f32 = 0.0;
const PRESENTATION_AUTO_RANGE: f32 = 1.0;
 
const BG: vec3f = vec3f(0.039, 0.039, 0.039);       // #0a0a0a  page
const PANEL: vec3f = vec3f(0.098, 0.098, 0.098);    // #191919  surface
const EDGE: vec3f = vec3f(0.180, 0.184, 0.192);     // divider
 
/// Undoes the order-preserving key the reduction wrote.
fn value_of(key: u32) -> f32 {
  let bits = select(~key, key & 0x7fffffffu, (key & 0x80000000u) != 0u);
  return bitcast<f32>(bits);
}
 
fn depth_at(texel: vec2i) -> f32 {
  let limit = vec2i(i32(uniforms.depth_size.x) - 1, i32(uniforms.depth_size.y) - 1);
  let c = clamp(texel, vec2i(0, 0), limit);
  return depth[u32(c.y) * u32(uniforms.depth_size.x) + u32(c.x)];
}
 
/// Maps one model's output to nearness in 0..1, where 1 is closest.
///
/// FastDepth returns metres, so it takes a fixed logarithmic range: stable
/// frame to frame, which stops the image from pumping as the scene changes.
/// The transformers return relative inverse depth with no scale of their own,
/// so they are rescaled against the min and max the GPU reduction just found.
fn nearness(value: f32) -> f32 {
  if (uniforms.mode == PRESENTATION_AUTO_RANGE) {
    let lo = value_of(range[0]);
    let hi = value_of(range[1]);
    let span = hi - lo;
    if (span <= 0.0) {
      return 0.0;
    }
    return clamp((value - lo) / span, 0.0, 1.0);
  }
 
  // Log scale because depth resolution matters far more up close: the near
  // metre of a room deserves as much of the ramp as the far five.
  let near = max(uniforms.near_meters, 1e-3);
  let far = max(uniforms.far_meters, near * 1.001);
  let d = clamp(value, near, far);
  return clamp(1.0 - log(d / near) / log(far / near), 0.0, 1.0);
}
 
fn nearness_at(texel: vec2i) -> f32 {
  return nearness(depth_at(texel));
}
 
/// Bilinear nearness. The tensor is a fraction of the canvas size, so nearest
/// texel lookups would draw the depth grid itself as stair steps.
fn nearness_sample(uv: vec2f) -> f32 {
  let p = uv * uniforms.depth_size - vec2f(0.5);
  let base = floor(p);
  let f = p - base;
  let i = vec2i(base);
  let top = mix(nearness_at(i), nearness_at(i + vec2i(1, 0)), f.x);
  let bottom = mix(nearness_at(i + vec2i(0, 1)), nearness_at(i + vec2i(1, 1)), f.x);
  return mix(top, bottom, f.y);
}
 
fn colour_at(texel: vec2i) -> vec3f {
  let limit = vec2i(i32(uniforms.depth_size.x) - 1, i32(uniforms.depth_size.y) - 1);
  let c = clamp(texel, vec2i(0, 0), limit);
  let packed = colour[u32(c.y) * u32(uniforms.depth_size.x) + u32(c.x)];
  return vec3f(
    f32(packed & 0xffu),
    f32((packed >> 8u) & 0xffu),
    f32((packed >> 16u) & 0xffu),
  ) / 255.0;
}
 
fn colour_sample(uv: vec2f) -> vec3f {
  let p = uv * uniforms.depth_size - vec2f(0.5);
  let base = floor(p);
  let f = p - base;
  let i = vec2i(base);
  let top = mix(colour_at(i), colour_at(i + vec2i(1, 0)), f.x);
  let bottom = mix(colour_at(i + vec2i(0, 1)), colour_at(i + vec2i(1, 1)), f.x);
  return mix(top, bottom, f.y);
}
 
@fragment
fn fs_main(@builtin(position) position: vec4f) -> @location(0) vec4f {
  let resolution = max(uniforms.resolution, vec2f(1.0));
  let px = position.xy;
 
  // Two panels of equal width, each fitting the frame whole rather than
  // cropping it, so the halves stay pixel-aligned with each other.
  let half_width = resolution.x * 0.5;
  let right = px.x >= half_width;
  let panel = vec2f(half_width, resolution.y);
  let scale = min(panel.x / uniforms.depth_size.x, panel.y / uniforms.depth_size.y);
  let drawn = uniforms.depth_size * scale;
  let origin = (panel - drawn) * 0.5;
  let local = vec2f(px.x - select(0.0, half_width, right), px.y) - origin;
  let uv = local / drawn;
 
  let inside = uv.x >= 0.0 && uv.x <= 1.0 && uv.y >= 0.0 && uv.y <= 1.0;
 
  var content = PANEL;
  if (uniforms.has_result < 0.5) {
    // No estimate yet: show the colour half and leave the depth half blank, so
    // the first frame is not a black canvas.
    content = select(PANEL, colour_sample(clamp(uv, vec2f(0.0), vec2f(1.0))), !right);
  } else if (right) {
    // Nearness straight to brightness. The floor keeps the far field off pure
    // black so the panel edge stays visible.
    let n = nearness_sample(clamp(uv, vec2f(0.0), vec2f(1.0)));
    content = vec3f(0.04 + 0.92 * n);
  } else {
    content = colour_sample(clamp(uv, vec2f(0.0), vec2f(1.0)));
  }
 
  var colour_out = select(BG, content, inside);
 
  // Hairline between the halves.
  let divider = 1.0 - smoothstep(0.0, 1.0, abs(px.x - half_width));
  colour_out = mix(colour_out, EDGE, divider * 0.9);
 
  return vec4f(clamp(colour_out, vec3f(0.0), vec3f(1.0)), 1.0);
}