BookNotesSlidesP-sets Syllabusdraft · v0.2.49.1
◀▲⌂▶
💬Comments welcome. To leave a note, select any text and click the note / highlight button that pops up — or open the panel with the tab at the top-right (‹). Notes are visible only inside our private review group.
💡 In a hurry? Jump to this chapter’s 4 big lessons ↓

10.1 Bilateral filtering⧉

📎 Problem set

PS2 implements the bilateral filter (with a YUV chroma variant). → Chapter 1.5, Problem sets (appendix).

A blur mixes two sides of an edge and makes it soft. That is useful for some operations, but harmful when we want to remove noise while preserving sharpness or to smooth texture while keeping the boundary. The big idea of the bilateral filter is to only take into account pixels that are similar enough. This notion of affinity between pixels will come back over and over for edge-preserving techniques.

We illustrate bilateral filtering for the task of denoising, but it also underlies many algorithms because it enables the factoring of images into large-scale structure and local detail while preserving strong edges. This proves powerful for applications such as high-dynamic-range tone mapping (Chapter 18.3, Two-scale and bilateral tone mapping), upsampling low-resolution results, and combining images with different illumination, where the edges come from a second image (#Cross / joint bilateral).

10.1.1 Bilateral filter⧉

Consider an input signal composed of a step edge with noise (Figure 10.1.1). The goal of bilateral filtering is to remove the noise by averaging nearby pixels without blurring the edge.

fig-bilateral-1d
Figure 10.1.1. The bilateral filter on a 1-D scanline, in three stages (interactive). The figure builds the argument of this section one step at a time: first the noisy step edge, one row of an image; then a plain Gaussian, which averages across the step and smears it into a ramp; then the bilateral, which denoises each plateau and keeps the step crisp, because at the edge the range weight kills every neighbor on the far side. Each stage brings the controls that matter at that point, $\sigma_s$ with the blur and $\sigma_r$ with the bilateral.

Start from the Gaussian blur. It replaces each pixel with a weighted average of its neighbors, where the weight only depends on distance. It is a spatial Gaussian $g_s(\lVert p - q\rVert)$ of the distance between the center pixel $p$ and a neighbor $q$: nearby pixels count for a lot, distant ones taper off. It is easiest to watch this on a 1-D scanline (a single row of the image), where the picture becomes a wiggly curve of value against position (Figure 10.1.1).

fig-bilateral-1d
Figure 10.1.1, stage 2. The same figure at its second stage: a plain Gaussian blur of the noisy scanline. The blur cleans both plateaus, and at the step it averages across the boundary, so the edge arrives as a ramp. $\sigma_s$ sets how wide that reach is.

The trouble, once more, is the edge. A step edge on the scanline, with values low on the left and high on the right, gets smeared by the Gaussian, because right at the step the averaging window reaches across the boundary and blends the two plateaus together. The same bleed, in two dimensions and on a compressed base layer, is what paints halos along high-contrast edges in tone mapping (Chapter 18.3, Two-scale and bilateral tone mapping).

The fix is a single extra factor. Keep the spatial Gaussian, but multiply in a second Gaussian, this one on the value difference between the center pixel and the neighbor, $g_r(\lvert I_p - I_q\rvert)$. This range weight asks "are these two pixels similar in value?" and down-weights neighbors that are too different. A neighbor that is close in space but far in value, one on the other side of an edge, now earns a weight near zero, however spatially near it sits. The filter, all by itself, declines to average across the edge.

A practical wrinkle follows. The range weight asks whether two pixels are similar. But on a badly corrupted image the raw values are an unreliable judge: noise can make two same-region pixels look different, or two different ones look alike, so the weights themselves come out noisy. The weights need not be read from the original image at all. We can compute $g_r$ from a pre-cleaned version (a quick median filter, say, or any cheap denoise) and then apply those steadier weights when averaging the original pixels. Decoupling what decides the affinity from what actually gets averaged is exactly the move the cross / joint bilateral (below) makes with a separate guide image; using a median-filtered copy of the same image as that guide is just the in-place version of the trick.

Putting the two Gaussians together gives the bilateral filter (Tomasi & Manduchi 1998). The weight a neighbor $q$ receives at pixel $p$ is the product of the spatial and range terms,

$$ w(p,q) \;=\; g_s\!\big(\lVert p - q\rVert\big)\; g_r\!\big(\lvert I_p - I_q\rvert\big), \qquad g_s(d) = e^{-d^2/2\sigma_s^2}, \quad g_r(\Delta) = e^{-\Delta^2/2\sigma_r^2}, $$

and the output is the normalized weighted average of the neighbors,

$$ I^{\mathrm{bf}}_p \;=\; \frac{1}{W_p}\sum_q w(p,q)\, I_q, \qquad W_p = \sum_q w(p,q). $$

In words: "average my neighbors, but only those that are both nearby and similar to me." The two widths do different jobs. $\sigma_s$, in pixels, sets how large a region we pool over, the reach of the spatial Gaussian. $\sigma_r$, in intensity units, sets what counts as "too different": the value gap beyond which a neighbor stops counting. Because $\sigma_r$ is measured in the same units as $I$, we must fix the encoding before we can quote it: for HDR tone mapping the filter runs on log luminance (Durand & Dorsey 2002, where a range $\sigma_r \approx 0.4$ in $\log_{10}$ is typical); other tasks may use linear values or a specified output encoding. There is no single required domain for every bilateral application.

fig-bilateral-1d
Figure 10.1.1, stage 3. The same figure at its third stage: the bilateral against the Gaussian. Both plateaus are as clean as the blur made them, and the step survives, because for a pixel beside the edge the range weight has already set every neighbor on the far side to nearly zero. $\sigma_r$ is what counts as too different.

As code, the brute-force bilateral is a double loop with a per-pixel normalization, and one subtlety distinguishes it from convolution: you may precompute the spatial weight $g_s$ (it is the same window everywhere), but the range weight $g_r$ must be recomputed for every pair, because it depends on the value difference at that pixel. The range function can be tabulated for quantized differences; it is the resulting image-dependent kernel that cannot be reused as one convolution. Specify spatial truncation and boundary handling, include the center sample, and accumulate in a sufficiently wide type.

Algorithm 10.1.1
precompute g_s over the window          # spatial weights, same for every pixel
for each output pixel p:
    sum ← 0;  W ← 0                      # accumulator and normalizer
    for each neighbour q in the window around p:
        w ← g_s(‖p − q‖) · g_r(|I[p] − I[q]|)   # range term recomputed per pair
        sum ← sum + w · I[q]
        W   ← W + w
    I_bf[p] ← sum / W                    # per-pixel normalization

For a color image the range distance $|I_p - I_q|$ is the 3-D distance in RGB (or YUV); everything else is unchanged.

The filter also travels under other names in editing software. Photoshop's Surface Blur is a bilateral filter: its radius is the spatial width and its threshold is the range width, over a box spatial kernel rather than a Gaussian. The name is worth knowing: those two sliders are the two widths of this section, and a tool many readers already use is a direct way to feel what each one does.

The 1-D picture carries straight to the 2-D image: the bilateral smooths flat texture and noise inside each region while holding the edges, so a portrait emerges with clean skin and intact eyelashes, and an HDR base layer stops cleanly at the silhouette instead of ramping across it (Figure 10.1.2).

fig-bilateral-2d
Figure 10.1.2. The bilateral filter on a real photograph. Left, the input: a white egret against busy foliage. Center, a plain Gaussian blur of the same radius: it smooths the foliage texture but also smears the bird's edges into the background. Right, the bilateral filter: texture and noise are smoothed away within each region while the strong edges, the bird's silhouette and the beak, stay crisp. The same behavior as the 1-D scanline, now on a 2-D image.
fig-bilateral-diagram
Figure 10.1.3. One filter, three views, and the equation it comes from (interactive). The same data is drawn as a 1-D scanline, as the 2-D image that scanline is a row of, and as a height field, where the picture is a surface and the filter's weights are a second surface sitting over it. Each row reads left to right in the order of the equation at the top: the output, the weights at the selected pixel, then the input. The weights can be split into the spatial Gaussian $f$ and the range Gaussian $g$, which is where the height field earns its place: $f$ is the same hump everywhere, $g$ is a cliff that follows the image, and their product is the kernel actually applied, different at every pixel. Step the selected pixel across the edge, or let it sweep, and watch that kernel lose the far side. The filter can be switched to a plain Gaussian, which drops $g$, or to the unnormalized form that filters the difference from the center pixel and never divides. Interactive: choose a noisy step edge (its height and noise) or a photograph, set $\sigma_s$ and $\sigma_r$, split the weights, switch the filter, and move the selected pixel by clicking, stepping or animating.

The range Gaussian $g_r$ is more than a clever weight; it is a similarity, an affinity between two pixels: a number that says how much these two pixels belong together, computed from their value difference. The bilateral averages pixels in proportion to that affinity. Every later method in this chapter, and several beyond it, is a different answer to one question: how should we measure whether two pixels belong together?

💡 Big lesson (L10.1) — edge-preserving = affinity

Use the color / intensity difference between two pixels as how much they belong together, an affinity, then average (or connect) pixels in proportion to it. Pixels with high affinity are treated as measurements of the same underlying thing and get averaged together; pixels with low affinity, across an edge, are kept apart. The bilateral filter's range weight $g_r$ is the first full instance, but the same affinity drives the bilateral grid, the guided filter, non-local means (NL-means), colorization, and matting / segmentation that follow. Edge-preserving is affinity. (This is L10.1, registered in Part 13, Denoising where the bilateral first appeared for noise; this is its full edge-preserving treatment.)

fig-bilateral-image
Figure 10.1.4. The bilateral filter on a real image. Unlike a Gaussian blur, which smooths across edges and softens the whole image, the bilateral filter weights neighbors by both spatial distance and intensity similarity, so it smooths flat regions while largely preserving strong edges. Compare the input, the bilateral result, and a plain Gaussian of the same spatial extent; vary the range and spatial widths to see the trade-off, and note that a very large range width turns the bilateral back into that Gaussian. In this demo the color channels are filtered with weights computed from their gray intensity, so an edge between two colors of the same brightness is not protected; the range Gaussian reduces mixing across an edge rather than forbidding it, and the spatial kernel is cut off at three σ on a downsampled working copy of the image.
fig-bilateral-kernels
Figure 10.1.5. The bilateral filter's kernel is different at every pixel; this figure makes that visible. The image is tiled into patches about $4\sigma_s$ wide (the filter itself cuts its support off at ±2σ_s); for each patch the middle panel paints the raw weight its center pixel gives every pixel of the patch, the spatial Gaussian times the range Gaussian $g_s\cdot g_r$ (bright = high weight). These weights are not normalized, so brightness shows relative preference, not each neighbor's share of the final average. In a flat patch the weight map is just the round spatial Gaussian, an ordinary blur kernel. Straddling an edge, the range term suppresses the far side and the kernel shrinks onto the center's own region. That space-varying reshaping is why the output (right) smooths texture yet averages very little across an edge: the affinity of L10.1 made into a picture. Pick the image and $\sigma_s,\sigma_r$, and switch the range term between gray and RGB, both computed on encoded pixel values.
fig-bilateral-filter-tomasi-3
Figure 10.1.6. The bilateral filter in the paper that named it. The original demonstration that combining a domain (spatial) Gaussian with a range (intensity-similarity) Gaussian smooths within regions while leaving strong edges intact: the affinity idea this whole chapter develops. After Tomasi & Manduchi 1998.

Debugging: hand-checkable limits⧉

The bilateral has two limits you can predict by hand, and together they make an excellent correctness test for an implementation (Figure 10.1.7). Push the range width to each extreme:

A middle $\sigma_r$ interpolates between these two: enough to smooth away noise and texture, not so much that it bleeds across real edges. If your implementation does not reproduce a Gaussian at large $\sigma_r$ and the identity at small $\sigma_r$, it is wrong. These are the first two things to check (and the recipe in the debugging sidebar above).

fig-bilateral-limits
Figure 10.1.7. The two debugging limits. The same input filtered across a sweep of range widths $\sigma_r$. At large $\sigma_r$ the range weight does nothing and the bilateral is a plain Gaussian blur (edges washed out); at tiny $\sigma_r$ only the pixel itself counts and the output is the identity (input untouched); in between, the filter smooths within regions while holding edges. The scanline plot underneath makes it concrete: the identity tracks the input exactly, the Gaussian washes it out, and a middle $\sigma_r$ denoises the plateaus while keeping the step.

The same two knobs guide tuning. Pick $\sigma_s$ from the spatial scale of the structure you mean to remove (how large a region should be pooled). Pick $\sigma_r$ from the contrast of the edges you mean to preserve: set it just below the value jump of a real edge, so the filter smooths everything gentler than that and stops at anything sharper. And state the encoding first: the same numeric $\sigma_r$ means something completely different in linear light, in gamma, and in log luminance.

10.1.2 Robustness⧉

With the Gaussians defined above and the center included, $w(p,p)=1$, so $W_p\geq1$. The normalized bilateral is a convex average and cannot blow up from division by a near-zero normalizer. An isolated value can still be poorly denoised because it assigns little weight to other samples. This is a limitation of the affinity, not proof that normalization is numerically invalid.

The deeper weakness is that the range term reads the noisy image itself. At an outlier the center's own corrupted value decides which neighbors count as similar, so the pixel that most needs help is the one whose weights are least trustworthy: it finds nothing that resembles it and averages with almost nothing but itself. A hot pixel survives the filter for the same reason an edge does.

A cheap repair is to compute the range term from a pre-filtered copy instead of from the input. Run a small median filter first, then use that image as the reference for the value differences while still averaging the original samples. The median removes impulses without moving an edge (Chapter 13.3, Gaussian, median, and bilateral filtering), so the weights are decided by something close to the clean signal while the output remains an average of real measurements. Mechanically this is the cross-bilateral construction of the next section, with the guide derived from the input rather than captured separately.

The cost is that the guide's failures become the filter's. A median erases thin lines, small dots and isolated highlights, and a structure the guide has removed has no range weight left to defend it: the bilateral will happily average it away. Check the guide on the structures you care about before trusting the result, and keep the median's window small enough that it removes impulses rather than detail.

An alternative discussed by Aubry et al. is an unnormalized correction Aubry et al. 2014. Its scale must be defined. With these unnormalized spatial weights, introduce a step size $\eta$:

$$ I^{\mathrm{bf}}_p \;=\; I_p \;+\; \eta\sum_q w(p,q)\,\big(I_q - I_p\big). $$

Each neighbor contributes a weighted difference from the center. A sufficient condition for this update to remain a convex combination is $0\leq\eta\sum_{q\ne p}w(p,q)\leq1$. One practical convention normalizes the spatial kernel to sum to one and uses $0\leq\eta\leq1$. Without a scale convention, adding more neighbors can cause overshoot. The update generally differs from the normalized bilateral; neither is a universal replacement for the other.

This unnormalized form is also the bilateral cousin of the local Laplacian filters: working in deltas from the input, scale by scale, is exactly the move that makes the multiscale local Laplacian fast and robust (Paris / Aubry; cross-ref Chapter 10.4, Local Laplacian filters and Chapter 6.6, Pyramids and wavelets).

💡 Big lesson — define the scale of an unnormalized update

Removing the bilateral normalizer changes the filter. Specify a normalized spatial kernel or an explicit step size, then test constants, impulses and edges. A small total affinity makes a small correction; it does not establish that an isolated value is correct.

10.1.3 Cross / joint bilateral⧉

So far the affinity has been read off the same image we are smoothing (the range weight uses $I$ itself). But there is no reason it must. The cross (or joint) bilateral filter reads the range weight from a different image, a guide $J$, while still averaging the target $I$:

$$ I^{\mathrm{cb}}_p \;=\; \frac{1}{W_p}\sum_q g_s\!\big(\lVert p-q\rVert\big)\, g_r\!\big(\lvert J_p - J_q\rvert\big)\, I_q, \qquad W_p = \sum_q g_s\!\big(\lVert p-q\rVert\big)\, g_r\!\big(\lvert J_p - J_q\rvert\big). $$

The averaging runs over the target $I$; the affinity comes from the guide $J$. Why want that? Because sometimes the image you wish to smooth is too noisy or too dark for its own edges to be trusted. If you read the affinity off $I$, you are reading off the noise. If you hold a cleaner signal that shares the same scene geometry, borrow its edges instead.

The headline application is flash / no-flash photography (Petschnigg et al. 2004; Eisemann & Durand 2004). Shoot a dim scene twice: once without flash (the right mood and color, but dark and noisy) and once with flash (clean and well-exposed, but flat, harsh lighting). After registration, many object boundaries correspond. Motion, occlusion, flash shadows and specularities can introduce mismatched edges, so a practical pipeline needs confidence masks or fallback behavior. So smooth the noisy no-flash image using the flash image as the guide: the flash image's clean edges drive the range weight, telling the filter where it may and may not average, while the values being averaged come from the ambient no-flash shot. The result keeps the no-flash mood and kills the no-flash noise (Figure 10.1.8). We develop the full flash/no-flash pipeline later (forward-ref → Flash / no flash, computational illumination).

fig-cross-bilateral-flash
Figure 10.1.8. The cross bilateral filter on a real flash / no-flash pair, Eisemann and Durand's own photographs Eisemann & Durand 2004. Top: the no-flash frame, the image to filter, shown brightened, and the flash frame, the guide. Bottom: the no-flash frame filtered with range weights read from itself (the ordinary bilateral) and read from the flash (the cross bilateral), with the same $\sigma_s$ and $\sigma_r$. Around the candles the no-flash frame is broken into JPEG blocks and noise: the ordinary bilateral takes the blocks for edges and keeps them, while the cross bilateral smooths inside the flash's clean edges. It fails where the no-flash frame has an edge the flash lacks. The flames light only the no-flash frame, so they spread into a glow; and an intensity guide cannot see a color edge between two surfaces equally bright under the flash, such as a blue candle against pink icing. "Each channel" compares each color channel of the guide separately, as Petschnigg et al. 2004 do. On the toy, whose no-flash frame has real signal, the cross bilateral mostly loses what the ordinary one keeps, such as the white of the eye, which the flash shows as white as the fur. Click a picture to move the zoom.

The full flash/no-flash story, including Petschnigg's detail transfer, Eisemann & Durand's intrinsic-relighting pipeline, flash-shadow removal, and their end-to-end results, belongs with the rest of computational illumination, where capturing the scene under two lights is the whole subject (Flash / no flash).

The cross bilateral is the first instance of a more general joint / guided idea (filter one image using structure taken from another) that culminates in the guided filter (later in the part).

10.1.4 Bilateral grid⧉

The bilateral filter is non-linear: the weights depend on the pixel values, so they differ at every output pixel, and the whole thing is not a convolution. That is why you cannot reuse your fast Gaussian-blur code, and why a brute-force bilateral is a slow double loop. The bilateral grid (Chen, Paris & Durand 2007) is the observation that you can turn it back into an ordinary convolution: add a dimension (Figure 10.1.9).

The key picture: the product of a spatial Gaussian (in $x, y$) and a range Gaussian (in $I$) is just a single 3-D Gaussian in the space $(x, y, I)$. So lift every pixel out of the 2-D plane into a 3-D space by using its value as a third coordinate. Pixel $(x, y)$ with value $I(x, y)$ goes to grid position $(x, y, I(x,y))$. In this lifted space the nonlinear 2-D bilateral becomes a plain, separable 3-D Gaussian blur, that is, an ordinary linear convolution, and the edge-preserving behavior is automatic: two pixels on opposite sides of an edge have very different values, so they land in different layers of the grid, which reduces their interaction according to the range bandwidth. A Gaussian still has nonzero tails, and discretization adds approximation error.

💡 Big lesson (L10.2) — discretize the range to accelerate a non-linear filter

The bilateral is awkward because it is non-linear: its weights depend on the pixel values, so they differ at every output pixel and the filter is not a convolution. The bilateral grid's deeper trick, beyond "add a dimension", is to discretize the intensity (range) axis into a small set of levels, and then do cheap, ordinary linear work at each level. Quantize $I$ into grid layers, run a plain blur within and across layers (distant layers interact less, with accuracy controlled by grid resolution and the blur), and interpolate back to each pixel's exact value. The per-pixel nonlinearity becomes "a few linear passes plus a lookup." This is a general recipe, not a bilateral-only hack: the fast local Laplacian filter (Chapter 10.4, Local Laplacian filters, Aubry et al. 2014) accelerates its per-pixel nonlinearity the same way: evaluate a handful of discrete intensity levels of a slow operator and interpolate between them. Whenever a filter's cost comes from a continuous, value-dependent response, discretizing that response axis turns it into reusable, parallel, convolution-shaped work. (Registered in Big Lessons as L10.2; recurs in Chapter 10.4, Local Laplacian filters and, as an algorithm-level speedup, in Chapter 29.1, Algorithmic speedups.)

The pipeline has three steps (splat, blur, slice):

The whole pipeline is short, and every step is an ordinary operation (accumulate, blur, look up):

Algorithm 10.1.2
# splat — build the coarse grid (downsample by σ_s in space, σ_r in range)
for each pixel (x, y):
    c ← (x/σ_s, y/σ_s, I[x,y]/σ_r)      # the cell this pixel lands in
    splat(grid, c, I[x,y])              # distribute to surrounding vertices
    splat(weight, c, 1)                 # same interpolation weights

# blur — plain separable 3-D Gaussian, on BOTH grid and weight
blur grid, weight along x, then y, then I    # e.g. [1 4 6 4 1]/16 per axis

# slice — read back at each pixel's own (x, y, I), normalized
for each pixel (x, y):
    c ← (x/σ_s, y/σ_s, I[x,y]/σ_r)
    out[x, y] ← interpolate(grid, c) / interpolate(weight, c)      # trilinear interpolation at c
return out

Why is this fast? Because the grid is coarse. The right resolution is set by the filter's own widths (a grid cell about $\sigma_s$ wide in space and $\sigma_r$ deep in range), so the grid is heavily downsampled in both space and value, often by large factors. A small grid, a separable blur, a trilinear read: the whole thing runs in real time on a GPU. This is the pipeline that Halide schedules for live bilateral filtering (see Chapter 29.3, Halide at a high level: Decoupling Algorithms from Schedules).

💡 Big lesson (L10.3) — an extra dimension lets you downsample space without losing edges, and makes big kernels cheap

Lifting the image into the higher-dimensional grid $(x, y, I)$ is what makes the coarse grid safe. Because pixels on opposite sides of an edge sit at different intensity heights, they are already separated along the range axis, so you can downsample the spatial axes aggressively while retaining range information that ordinary spatial downsampling would lose; coarse sampling can still distort boundaries. Two gains follow: high-resolution edges survive despite a coarse grid; and a large spatial kernel becomes a small blur on the coarse grid, so larger spatial widths may reduce grid work, although full-resolution splatting and slicing still cost at least one pass over the image. Paying one extra dimension for cheap, edge-aware, large-scale smoothing is a trade that recurs: NL-means' patch space, the permutohedral lattice, and the learned bilateral grid (HDRnet) below all live on it.

fig-bilateral-grid
Figure 10.1.9. The bilateral grid: lift $(x, y, I)$ → splat · blur · slice. A nonlinear 2-D bilateral becomes a linear 3-D blur by promoting the value $I$ to a third axis. Splat the pixels into a coarse 3-D grid (each cell accumulates a value and a weight); blur with a plain separable 3-D Gaussian (an ordinary convolution: edges are automatic because dissimilar values sit in different grid layers); slice the result back by trilinear interpolation at each pixel's $(x, y, I)$, dividing value by weight to normalize. A plain blur that will not cross edges.
fig-bilateral-grid-3d
Figure 10.1.10. The bilateral grid, in 3-D, orbit it yourself. A grayscale image is splatted into a grid whose axes are image $x$, image $y$, and intensity (height); each cell is drawn with an opacity proportional to the sum of weights that landed in it (the transparent cubes are sorted strictly back-to-front so the layering reads correctly from any angle, and the input is overlaid as a light height-field wireframe (height = intensity) you can see the grid through). A flat region piles its votes into one column of cells; an edge splits its votes between a low and a high intensity layer, leaving the cells between them empty. Switch from the original grid to the 3-D-Gaussian-blurred one and the cells spread and soften, but that empty edge-gap survives, which is why a single ordinary 3-D blur smooths texture yet mixes very little across an edge. The grid is a coarse approximation of the bilateral filter. Its controls set $\sigma_s$ in pixels and $\sigma_r$ in intensity, and the grid follows them as in Chen, Paris and Durand: each cell is $\sigma_s$ pixels wide and $\sigma_r$ deep, the blur is one cell wide, and the sliders show the resulting number of cells and bins. Cell opacity is scaled for visibility. Hover the side image to light up the matching 3-D point; toggle the panel between input and bilateral output. The built-in scenes include three real high-dynamic-range photographs (a vine sunset, Antelope Canyon, and a design center) for which the height axis is log luminance, exactly as HDR tone mapping uses the bilateral (Durand & Dorsey 2002): the grid layers are then spaced in stops, and you can watch the canyon's bright slot stay cleanly separated from its deep-shadow walls across the empty gap (a tiny epsilon is added before the log so the darkest pixels stay finite). HDR radiance maps courtesy of Paul Debevec (vine sunset) and Byong Mok Oh (design center).

On a single scanline the grid has only two axes, position and intensity, so each step of the pipeline can be followed with its numbers and its cost counted against the brute-force filter (Figure 10.1.11).

fig-bilateral-grid-1d
Figure 10.1.11. The bilateral grid on one scanline, in four stages (interactive). With one image row the grid is a plane of position $x$ and intensity $I$, each cell $\sigma_s$ wide and $\sigma_r$ deep. Splat: each pixel adds its value to the value grid and 1 to the weight grid, shared bilinearly among the four vertices around $(x, I(x))$; most cells stay empty, and a step leaves two bands with nothing between them. Blur: both grids get $[1\ 4\ 6\ 4\ 1]/16$ along $x$, then along $I$, a Gaussian of one cell on each axis. Slice: each pixel reads both blurred grids at its own $(x, I(x))$, which gives $V(x)$ and $W(x)$; $W$ dips beside the step, where fewer neighbors are similar, as the normalizer $W_p$ does. Divide: $V/W$ is the output, drawn over the input with the brute-force bilateral; a constant input comes back exactly, since the same weights sit above and below the fraction. The readouts count the work from the grid's size: at the default widths on this 200-sample row the grid does more work than the brute force (9,000 operations against 5,960 window samples), and on a 200 × 200 image with the same widths about 30 times less, because in 2-D the brute-force window holds $(2r+1)^2$ samples per pixel while the grid stays a few cells per $\sigma_s$. The downsample toggle removes the intensity axis: the same blur then spreads the step over 16 samples between 10% and 90% of its height, where the grid and the brute force leave none. Pick a scanline (a noisy step, a noisy ramp, a row of the egret photograph of Figure 10.1.2, or a constant), set $\sigma_s$ and $\sigma_r$, and click or drag on any panel to move the probe pixel: its cell column and grid point light up in both grids, and in the last stage its brute-force window and weights appear under the scanline. After Paris & Durand 2009 and Chen et al. 2007.

The grid also makes the affinity lesson geometric: the third axis is the affinity, drawn as a coordinate. Two pixels far apart in value are literally far apart in the grid, so a local blur, which only mixes nearby cells, reduces their contribution according to distance and the blur support. The bilateral's "do these pixels belong together?" becomes, in the grid, simply "are these cells close?"

Because the grid is coarse and the blur is a plain convolution, the whole edge-aware filter runs fast enough to be interactive, and the authors put that speed to work on a range of applications.

fig-bilateral-grid-1
Figure 10.1.12. Edge-aware manipulation in real time. Running the splat–blur–slice pipeline on the GPU makes local tone mapping, and other edge-respecting adjustments, fast enough to apply live as the user works. The result has the local contrast and halo-free edges of a bilateral decomposition, but at frame rate rather than the seconds a brute-force double loop would cost. (Chen et al. 2007.)
fig-bilateral-grid-6
Figure 10.1.13. Interactive edge-aware editing on the grid. Because slicing is just a trilinear lookup, the grid supports painting and other direct manipulation: the user brushes an adjustment, and the edge-aware structure carries it across smooth regions while stopping it at object boundaries, so a stroke fills a region without bleeding into its neighbors, with no manual masking. (Chen et al. 2007.)
fig-bilateral-grid-9
Figure 10.1.14. The grid generalizes beyond a single average, shown here in 1-D. Each grid cell does not have to hold just a value and a weight; it can hold a small local histogram, so the per-cell operation can be a full histogram equalization rather than a mean. The 1-D diagram makes the bookkeeping legible: pixels splat into range cells, each region accumulates its own distribution, and slicing reads back a per-region remapping. (Chen et al. 2007.)
fig-bilateral-grid-10
Figure 10.1.15. Local histogram equalization on a real image. Applying the per-region histogram operation of the previous figure across a 2-D photograph equalizes contrast region by region (each part of the image gets the stretch its own local distribution calls for) while the grid's edge-awareness keeps the adjustment from bleeding across boundaries. The effect is a strong, spatially varying contrast enhancement without the halos a naive local operator would produce. (Chen et al. 2007.)
Video 10.1.1. Video: Real-time Edge-Aware Image Processing with the Bilateral Grid, Chen, Paris & Durand 2007 (Chen et al. 2007). The original paper video, showing the splat–blur–slice pipeline running live: lifting pixels into the 3-D grid by value turns the nonlinear bilateral into a plain convolution, fast enough for the real-time local tone mapping, painting, and local histogram equalization above.

A caveat: the curse of dimensionality. The grid buys speed by coarsening space and range, but its memory grows with the dimension of the cells. A grayscale bilateral needs a 3-D grid (two spatial axes plus one range axis); a color bilateral, or a cross-bilateral keyed on several guide channels, needs a range axis per channel, a 5-D grid for RGB, or more dimensions for additional guide channels whose cell count grows exponentially. And most of those cells are empty: real images occupy only a thin sheet in the space (exactly what the 3-D grid above makes visible: the occupied cells hug the image's intensity surface), so a dense array is enormously wasteful. Andrew Adams and colleagues answer this with the permutohedral lattice (Adams, Baek & Davis 2010 (permutohedral lattice)), a sparse, simplex-based tessellation of high-dimensional space that splats, blurs and slices without allocating a dense Cartesian grid exponential in the feature dimension; runtime still depends on dimension, samples and occupied vertices, making high-dimensional (color, cross-, even non-local) bilateral filtering practical. Read the other way, the bilateral grid is just the low-dimensional, dense special case of this general high-dimensional Gaussian filtering.

How much the grid saves depends on both widths; timing the two filters on the same image shows where it overtakes the brute force (Figure 10.1.16). The brute force visits a window whose area grows as $\sigma_s^2$, so its cost climbs with the spatial width. The grid's cost does the opposite: a wider $\sigma_s$ means fewer, larger cells, and once the grid is small, what remains is the splat and the slice, which visit every pixel once whatever the widths. With both filters compiled from Halide and run on one thread, the grid overtakes the brute force at a spatial width of only one or two pixels, and at $\sigma_s=8$ it is about 67 times faster on the machine we measured, at a cost of a small error concentrated at strong edges, where the coarse cells blur most.

fig-bilateral-speed
Figure 10.1.16. Brute force against the bilateral grid, timed in your browser (interactive). Both filters run on the log luminance of a radiance map, 512 pixels on the long side, as Halide pipelines compiled to WebAssembly with SIMD, on one thread (WebAssembly threads need a cross-origin-isolated page, which the book's pages are not). The brute force sums a window of $\pm 2\sigma_s$ on each axis, so its cost grows as $\sigma_s^2$; its fastest schedule reads the range weight from a table instead of evaluating an exponential, and runs 3.5 times faster than plain C++ compiled with the same flags. The grid follows Halide's bilateral-grid application, with cells $\sigma_s$ wide and $\sigma_r$ deep. Each point is the median of five timings. Hollow points are brute-force runs too long to time whole: they are timed on a band of rows across the middle of the image and scaled by the pixel count, which leaves out the slower clamped rows near the top and bottom, so the full-image run in the readout comes out somewhat longer. On the machine that made the static image (an Apple M4 Max, headless Chrome), with the office window at $\sigma_r = 0.4$, the grid wins from $\sigma_s \approx 1.4$ pixels, is about 70 times faster at $\sigma_s = 8$, and stops getting cheaper above $\sigma_s = 4$, because its splat and slice still visit every pixel. A smaller $\sigma_r$ means more range bins (133 for the cathedral nave at $\sigma_r = 0.1$) and moves the crossover to larger $\sigma_s$. For the selected $\sigma_s$ the readouts give the brute-force window, the grid's bins along $x$, $y$ and range, and the two full-image outputs with their difference. They do not match exactly: the splat and the slice widen the grid's kernel a little beyond its nominal $\sigma_s$ and $\sigma_r$, and the outputs differ by a few hundredths of a log10 unit on average at $\sigma_r = 0.4$, most at strong edges. The live figure measures the machine it runs on. After Chen et al. 2007 and Ragan-Kelley et al. 2013. Radiance maps courtesy of Paul Debevec and Byong Mok Oh. Interactive: pick a radiance map and $\sigma_r$, then a $\sigma_s$ to inspect; measure again repeats the timings.

10.1.5 Bilateral upsampling and learning⧉

Many operations are too expensive to run on a full-resolution photograph: a stereo matcher, an optimization, a large network. A common answer is to run them on a small copy and bring the result back to full resolution. Ordinary interpolation blurs the result across edges, so a depth map or a mask bleeds from the foreground onto the background. The full-resolution image is still available, though, and its edges say where the result should change. The bilateral filter's range weight is the tool for using them, and this section follows that idea from simple upsampling to layers inside neural networks.

Joint bilateral upsampling. Kopf and colleagues upsample a low-resolution solution $S$ with the full-resolution image as the guide $J$ Kopf et al. 2007 (joint bilateral upsampling). Each output pixel $p$ averages the low-resolution values near its position $p_\downarrow$ in the small image, weighted by distance in the small image and by similarity in the guide:

$$\tilde S_p=\frac{1}{W_p}\sum_{q_\downarrow}S_{q_\downarrow}\;g_s\big(\|p_\downarrow-q_\downarrow\|\big)\;g_r\big(|J_p-J_q|\big).$$

It is the cross bilateral filter of the previous section with the two inputs at different resolutions. A value from the other side of an edge in the guide gets almost no weight, so the result changes where the photograph does. The guide is not a measurement of the quantity being upsampled, though: a painted stripe is a color edge with no depth edge, and a foreground object the color of its background has a depth edge the guide cannot see. Chapter 29.1, Algorithmic speedups gives the procedure in pseudocode and these failure cases in detail.

Upsampling an operator rather than an image. Chen and colleagues upsample the effect of an operator instead of its output Chen et al. 2016 (bilateral guided upsampling). They run the operator on a small copy of the input and fit, in a coarse bilateral grid, a local affine color transform per cell: the one that best maps the small input to the small output. The grid is then sliced at full resolution, with each pixel's own position and intensity, and the sliced transform is applied to the full-resolution input. Because the transform is applied to the original pixels, the output keeps all of their detail, and the operator only has to get the large-scale changes right. This works for tone adjustments, color grading and many stylizations, anything a smoothly varying color transform can express, and fails for operations that move or create pixels, such as warps or added texture.

Learning the grid: HDRnet. HDRnet goes one step further and predicts the grid of affine coefficients with a network instead of fitting it to an operator's output (Gharbi et al. 2017, Deep Bilateral Learning). A convolutional network runs on a low-resolution copy of the image and predicts, per grid cell, the coefficients of a local affine color transform. At full resolution the grid is sliced at each pixel's position and at a guide value, itself a learned per-pixel function of the color, and the sliced transform is applied to the pixel. The expensive network stays at low resolution, where it can see the whole image, and the edge-aware slice carries its decision back to every pixel. The bilateral grid becomes a learned, edge-preserving upsampling layer, fast enough to run in a phone's viewfinder (Figure 7.3.4, Figure 7.3.5). Chapter 7.3, Deep learning shows more of its results.

The bilateral solver. Upsampling averages the low-resolution values; some problems instead need to clean them. A network's depth map or segmentation is often noisy, has holes, and comes with a confidence that varies from pixel to pixel. Barron and Poole's fast bilateral solver finds the image $x$ that stays close to a target $t$ where the confidence $c$ is high and is smooth under the bilateral affinity $\hat W$ of the guide everywhere Barron & Poole 2016 (fast bilateral solver):

$$\min_x\;\frac{\lambda}{2}\sum_{i,j}\hat W_{ij}\,(x_i-x_j)^2\;+\;\sum_i c_i\,(x_i-t_i)^2.$$

Solved pixel by pixel this would be a huge system, but the affinity is a bilateral filter, so the problem can be splatted into a bilateral grid, solved there with a small linear system, and sliced back. The solution is differentiable with respect to the target and the confidence, so the solver can sit inside a network and be trained through. It has been used to refine stereo depth, to upsample depth and semantic segmentation, and for colorization.

Trainable edge-aware layers. Two lines of work made the bilateral filter itself a network layer. Jampani, Kiefel and Gehler learned the filter's weights on the permutohedral lattice, the high-dimensional generalization of the grid Adams, Baek & Davis 2010 (permutohedral lattice), instead of fixing them to a Gaussian Jampani, Kiefel & Gehler 2016. Wu and colleagues made the guided filter of Chapter 10.6, Guided image filtering differentiable, with a guide image learned by a small network, so that a network can run at low resolution and be upsampled by a guided filter trained end to end with it Wu et al. 2018 (deep guided filter).

Sharpening segmentations. Semantic segmentation networks produce coarse labels whose boundaries do not follow the objects. A fully connected conditional random field, solved with bilateral filtering in its inner loop, snaps the labels to the image's edges: pixels with similar color and position are encouraged to share a label Krähenbühl & Koltun 2011 (dense CRF). Zheng and colleagues unrolled that inference as layers of a recurrent network so that it could be trained together with the network that produces the labels Zheng et al. 2015 (CRF-as-RNN).

Bilateral grids in neural rendering. Reconstructions of a scene from many photographs, such as neural radiance fields and Gaussian splatting, assume that the same point has the same color in every view. A phone's processing breaks that assumption: exposure, white balance and local tone mapping change from frame to frame, and the reconstruction absorbs the differences as floating artifacts. Wang and colleagues give each view a small learned bilateral grid of affine color transforms, which models that processing and is discarded after training, so the reconstructed scene stays consistent Wang et al. 2024 (BilaRF).

The pattern is the same throughout. A computation too expensive or too coarse for the full image runs at low resolution or on low-quality data, and an edge-aware structure built from the full-resolution image carries the result back, keeping its edges where the photograph's are. What the guide cannot supply is information it does not contain.

fig-bilateral-upsampling
Figure 10.1.17. Bilateral upsampling, four ways (interactive). A result computed from the photograph $J$ is reduced to a small $S$, brought back to full resolution, and scored against the result at full resolution, over the whole picture and in two magnified crops. Two of the results come from networks run on the full photograph and averaged down by the factor, as if the network had run on a small copy: a relative depth map from Depth Anything V2 Small, the second version of Yang et al. 2024, shown near in yellow and far in purple, and a subject mask from U²-Net-p (Qin et al. 2020). The third, a two-scale tone adjustment that compresses the base and amplifies the detail Durand & Dorsey 2002, runs on the small photograph itself. Bilinear interpolation ignores the photograph. Joint bilateral upsampling weights the small pixels by $g_s$ in the small image and by $g_r$ on their color difference in $J$ Kopf et al. 2007 (joint bilateral upsampling). Bilateral guided upsampling fits an affine color transform per vertex of a grid over position and luma, $\sigma_s$ small pixels wide and $\sigma_r$ deep, slices it at each pixel and applies it to that pixel's own color Chen et al. 2016 (bilateral guided upsampling). The bilateral solver minimizes the energy above on the same grid, with $t$ the bilinear upsampling of $S$ and $c$ the confidence upsampled the same way Barron & Poole 2016 (fast bilateral solver). Its stage adds noise to $S$ whose deviation triples from left to right, a confidence equal to the inverse variance (from 1 down to 0.11), and four holes of zero confidence; every method then weights the small pixels by $c$, and magenta marks a hole or a pixel left without a value. The red crop opens on the strongest depth edge, the blue one on the strongest texture where the depth is flat. On the iguana's depth at 1/8, the red crop's RMS error falls from 0.164 with bilinear to 0.122 with joint bilateral and 0.113 with guided upsampling, while the blue crop's rises from 0.006 to 0.016 and 0.055: the scales of the skin appear in the depth. On the tone adjustment, guided upsampling has the lowest error, 0.036 against 0.128 for bilinear, because it applies the transform to the full-resolution pixels and keeps their detail. With noise and holes, the solver has the lowest error over the whole picture (0.083, bilinear 0.094) and in both crops (0.150 and 0.027, against 0.204 and 0.052), and gives a value to every pixel. Interactive: step through the four stages with the arrows; pick a photograph, a result and the downsampling factor; set $\sigma_s$, $\sigma_r$ and $\lambda$; click or drag on any full-resolution panel to move the nearer crop.
fig-hdrnet-1
Figure 7.3.4 (reproduced). Learned bilateral-grid enhancement, in real time. HDRnet predicts a tone/color adjustment from a low-resolution view of the photo and applies it at full resolution through the grid's edge-aware slicing: the input on the left, the network's enhanced output on the right, produced fast enough to run live on a phone. After Gharbi et al. 2017.
fig-hdrnet-2
Figure 7.3.5 (reproduced). The HDRnet architecture, which is exactly the paragraph above made into a diagram. A convolutional network runs on a low-resolution copy of the image and predicts, per bilateral-grid cell, the coefficients of an affine color/tone transform; at full resolution the grid is sliced at each pixel's $(x, y, \text{luma})$ (edge-aware trilinear lookup) and the sliced affine map is applied to the original pixel. A heavy net on a tiny image, carried back to full resolution by edge-aware upsampling. After Gharbi et al. 2017.

10.1.6 Anisotropic diffusion and robust estimators⧉

The bilateral filter is one member of a family, and the other two members were derived from different starting points and arrive at nearly the same arithmetic. Seeing them together explains what the range weight is really doing.

Read the filter as an estimator first. Averaging a neighborhood is what you do when you believe every neighbor is a measurement of the same value corrupted by Gaussian noise, which is the quadratic penalty $\rho(x)=x^2$. A quadratic grows without bound, so one very different neighbor, the pixel across an edge, pulls the estimate as far as it likes. A robust error norm saturates instead: past some difference a neighbor stops mattering more the further away it is. Edge preservation and outlier rejection are then the same operation, and the bilateral range weight is a particular choice of robust norm (Figure 10.1.18).

fig-robust-diffusion
Figure 10.1.18. Edge-preserving smoothing as robust estimation. Left: a robust error norm $\rho$ (which saturates) versus the quadratic $\rho(x)=x^2$ (which grows without bound). Right: the influence function $\psi=\rho'$: for the quadratic it grows linearly, so large differences pull hardest and blur edges; for the robust norm it rises and then returns to zero, so a neighbor across a strong edge is treated as an outlier and exerts no smoothing force. The bilateral range weight, the Perona–Malik conductance $g(|\nabla I|)$, and this influence function are the same curve. After Perona & Malik (1990), Black et al. (1998), and Durand & Dorsey (2002).

A weighted-difference update resembles one discrete diffusion step. For a symmetric pairwise energy, its force is proportional to the penalty derivative $\psi(t)=\rho'(t)$, while the weight is proportional to $\psi(t)/t$. Weight and influence are therefore different curves: a Gaussian weight yields $\psi(t)=t\exp(-t^2/(2\sigma_r^2))$, up to scale. Perona–Malik diffusion uses a local gradient-dependent conductance Perona & Malik 1990; a finite-neighborhood bilateral update is related but not the same discretization. Some penalties saturate, while others, such as the Lorentzian, grow without bound despite a diminishing influence Black et al. 1998.

Perona and Malik started from diffusion rather than from estimation Perona & Malik 1990. Blurring with a Gaussian is the same as running the heat equation $\partial_\tau I = \operatorname{div}(\nabla I)$ for a time $\tau$ that grows with the blur's variance: intensity flows from brighter to darker neighbors until they agree, across edges as readily as within regions. Their change was to let the flow depend on the local gradient,

$$\partial_\tau I = \operatorname{div}\big(g(|\nabla I|)\,\nabla I\big),$$

with a conductance $g$ near one for small gradients and near zero for large ones, for example $g(s)=1/(1+(s/\kappa)^2)$ or $g(s)=e^{-(s/\kappa)^2}$ with an edge scale $\kappa$. Heat then flows freely inside a region and barely crosses a strong edge, so iterating the equation smooths regions while boundaries survive. Two refinements matter in practice. With these conductances the flux $g(s)\,s$ decreases once $s$ exceeds a value near $\kappa$, so diffusion can steepen an edge rather than merely preserve it, and the continuous equation is ill-posed in that regime; practical schemes measure the gradient on a slightly smoothed image and take explicit time steps small enough to stay stable. A bilateral pass is not a discretization of this equation: it averages over a whole neighborhood in one large step, where diffusion takes many small nearest-neighbor steps.

A tensor can express directional diffusion. Diffusion-tensor imaging estimates physical water diffusion from measurements; it is not an image-smoothing algorithm that recovers anatomy by running Perona–Malik.

fig-diffusion-tensor
Figure 10.1.19. A diffusion tensor field, as measured by diffusion-tensor imaging (DTI). Each location carries a tensor, drawn as an ellipsoid: elongated where water diffuses preferentially in one direction (along a fiber bundle), round where diffusion is isotropic. In DTI the tensors are measured physical quantities, and tractography follows their long axes to trace white-matter pathways. The same object, used as a conductance in image smoothing, lets intensity flow along structures but not across them, the direction-dependent generalization of the scalar Perona–Malik conductance. (Visualization by Thomas Schultz, CC BY-SA 3.0, via Wikimedia Commons.)

The practical consequence is that these are three vocabularies for one design decision: how much should a dissimilar neighbor count? The bilateral answers with a range Gaussian, Perona and Malik with a conductance that falls with the gradient Perona & Malik 1990, robust statistics with an influence function. Chapter 13.3, Gaussian, median, and bilateral filtering compares their behavior on noise, and Chapter 10.4, Local Laplacian filters takes the same question to a multiscale decomposition.

Recap: big lessons of this chapter

💡 Big lesson (L10.1) — edge-preserving = affinity

Use the color / intensity difference between two pixels as how much they belong together, an affinity, then average (or connect) pixels in proportion to it. Pixels with high affinity are treated as measurements of the same underlying thing and get averaged together; pixels with low affinity, across an edge, are kept apart. The bilateral filter's range weight $g_r$ is the first full instance, but the same affinity drives the bilateral grid, the guided filter, non-local means (NL-means), colorization, and matting / segmentation that follow. Edge-preserving is affinity. (This is L10.1, registered in Part 13, Denoising where the bilateral first appeared for noise; this is its full edge-preserving treatment.)

💡 Big lesson — define the scale of an unnormalized update

Removing the bilateral normalizer changes the filter. Specify a normalized spatial kernel or an explicit step size, then test constants, impulses and edges. A small total affinity makes a small correction; it does not establish that an isolated value is correct.

💡 Big lesson (L10.2) — discretize the range to accelerate a non-linear filter

The bilateral is awkward because it is non-linear: its weights depend on the pixel values, so they differ at every output pixel and the filter is not a convolution. The bilateral grid's deeper trick, beyond "add a dimension", is to discretize the intensity (range) axis into a small set of levels, and then do cheap, ordinary linear work at each level. Quantize $I$ into grid layers, run a plain blur within and across layers (distant layers interact less, with accuracy controlled by grid resolution and the blur), and interpolate back to each pixel's exact value. The per-pixel nonlinearity becomes "a few linear passes plus a lookup." This is a general recipe, not a bilateral-only hack: the fast local Laplacian filter (Chapter 10.4, Local Laplacian filters, Aubry et al. 2014) accelerates its per-pixel nonlinearity the same way: evaluate a handful of discrete intensity levels of a slow operator and interpolate between them. Whenever a filter's cost comes from a continuous, value-dependent response, discretizing that response axis turns it into reusable, parallel, convolution-shaped work. (Registered in Big Lessons as L10.2; recurs in Chapter 10.4, Local Laplacian filters and, as an algorithm-level speedup, in Chapter 29.1, Algorithmic speedups.)

💡 Big lesson (L10.3) — an extra dimension lets you downsample space without losing edges, and makes big kernels cheap

Lifting the image into the higher-dimensional grid $(x, y, I)$ is what makes the coarse grid safe. Because pixels on opposite sides of an edge sit at different intensity heights, they are already separated along the range axis, so you can downsample the spatial axes aggressively while retaining range information that ordinary spatial downsampling would lose; coarse sampling can still distort boundaries. Two gains follow: high-resolution edges survive despite a coarse grid; and a large spatial kernel becomes a small blur on the coarse grid, so larger spatial widths may reduce grid work, although full-resolution splatting and slicing still cost at least one pass over the image. Paying one extra dimension for cheap, edge-aware, large-scale smoothing is a trade that recurs: NL-means' patch space, the permutohedral lattice, and the learned bilateral grid (HDRnet) below all live on it.