Gaussian and Median Filtering

Every camera sensor captures noise along with signal. Every downstream vision task suffers for it. A single hot pixel or a burst of sensor read noise can derail an edge detector. It can also corrupt a feature descriptor or confuse a stereo matcher. Before any interesting computer vision happens — before edges are found, corners detected, or objects recognized — the image usually passes through a smoothing stage. This stage suppresses noise while preserving the structure that matters. This article studies two of the most widely used smoothing operators: the Gaussian filter, a linear convolution that blurs an image with a bell-shaped kernel, and the median filter, a nonlinear operator that replaces each pixel with the median of its neighborhood. They solve the same practical problem — denoising — with fundamentally different mathematical character. The Gaussian filter is separable, rotationally symmetric, and has a clean frequency-domain interpretation as a low-pass filter. It is the workhorse behind scale-space theory and image pyramids. The median filter sacrifices linearity but gains something the Gaussian cannot offer. It removes impulsive "salt-and-pepper" noise without smearing sharp edges. Understanding both filters matters. So does understanding why one is linear-optimal for Gaussian noise while the other is robust to outliers. This builds the mathematical vocabulary needed for every later filtering-based technique in vision: gradient estimation, corner detection, bilateral and anisotropic diffusion filters, and the construction of Gaussian pyramids for scale-invariant feature detection. This article builds both filters from first principles. It gives working pseudocode and derives their time and space complexity precisely, including the separable speedup. It also shows a worked numerical trace so the mechanics are never a black box.

Key takeaways
  • The Gaussian filter convolves an image with a 2D Gaussian kernel, giving a linear, separable, rotationally symmetric low-pass filter.
  • Separability lets an n×nn \times n Gaussian blur run in O(WHn)O(WHn) time instead of O(WHn2)O(WHn^2), via two 1D passes.
  • The median filter replaces each pixel with the neighborhood median — nonlinear, edge-preserving, and highly robust to salt-and-pepper (impulsive) noise.
  • Gaussian filtering is the maximum-likelihood-optimal smoother under additive Gaussian noise but blurs edges; the median filter resists outliers but is O(n2log⁡n2)O(n^2 \log n^2) per pixel without extra tricks.
  • Kernel size σ\sigma (Gaussian) or window size kk (median) trades noise suppression against loss of fine detail — there is no free lunch.
  • Both are foundational preprocessing steps that feed into Image Gradients, edge detection, and Image Pyramids for scale-space analysis.
The core insight

Gaussian filtering averages linearly and optimally for Gaussian noise but blurs edges, while median filtering picks a robust order statistic that kills outlier noise while keeping edges sharp.

The idea

A digital image I(x,y)I(x,y) is a discrete sampling of a scene corrupted by acquisition noise, quantization, and sometimes transmission artifacts. Smoothing (or denoising) seeks an estimate I^\hat{I} of the true underlying signal that suppresses noise while retaining meaningful structure such as edges and textures. This is fundamentally a local estimation problem: at each pixel, combine the values in a neighborhood into a single output value, trusting that nearby pixels carry correlated information about the true scene. The Gaussian filter performs this combination via Linear Filtering and Convolution with a kernel derived from the 2D Gaussian density,

Gσ(x,y)=12πσ2exp⁡(−x2+y22σ2).G_\sigma(x,y) = \frac{1}{2\pi\sigma^2} \exp\left(-\frac{x^2+y^2}{2\sigma^2}\right).

The output is the convolution I^=I∗Gσ\hat{I} = I * G_\sigma, a weighted average where nearer pixels receive exponentially larger weight. Because the kernel is a genuine probability density (it integrates to 1, after discretization and renormalization), the filter cannot create new intensity extrema; it can only average existing values, which is precisely why it is a low-pass filter — it attenuates high spatial frequencies (fine detail, noise) while passing low frequencies (smooth regions) largely unchanged. The median filter replaces the linear weighted average with an order statistic: at each pixel, sort the intensities inside a window (typically k×kk \times k, kk odd) and output the middle value. Because a median is a robust statistic — it is insensitive to any single extreme value in the window, unlike a mean — the median filter can remove impulsive noise (isolated corrupted pixels with arbitrary intensity, so-called "salt-and-pepper" noise) while leaving step edges essentially untouched, since one side of an edge always dominates the window and supplies the median. This nonlinearity means the median filter has no convolution representation, no frequency response in the usual sense, and no superposition property — but it buys edge preservation that no linear filter can match.

How it works

Gaussian filtering. The 2D Gaussian kernel is separable: Gσ(x,y)=gσ(x) gσ(y)G_\sigma(x,y) = g_\sigma(x)\, g_\sigma(y) where gσg_\sigma is the 1D Gaussian. This algebraic fact follows directly from the exponential splitting e−(x2+y2)/2σ2=e−x2/2σ2⋅e−y2/2σ2e^{-(x^2+y^2)/2\sigma^2} = e^{-x^2/2\sigma^2}\cdot e^{-y^2/2\sigma^2}. Consequently, a 2D convolution can be computed as two sequential 1D convolutions — one along rows, one along columns — which is both simpler to implement and dramatically faster. The kernel is truncated to a finite support, typically a half-width of r=⌈3σ⌉r = \lceil 3\sigma \rceil (capturing more than 99.7% of the Gaussian's mass), and normalized so its discrete entries sum to 1 (preserving average brightness). ``` function GAUSSIAN_KERNEL_1D(sigma): r = ceil(3 sigma) k = array of size (2r + 1) for i in -r .. r: k[i + r] = exp(-(i i) / (2 sigma sigma)) normalize k so sum(k) == 1 return k function CONVOLVE_1D_HORIZONTAL(I, k, r):

# I: H x W image, k: kernel of half-width r O = array H x W, zero-initialized for y in 0 .. H-1: for x in 0 .. W-1: acc = 0 for i in -r .. r: xi = clamp(x + i, 0, W - 1) # border handling acc += I[y][xi] * k[i + r] O[y][x] = acc return O function CONVOLVE_1D_VERTICAL(I, k, r):

# symmetric to the horizontal pass, sums over rows O = array H x W, zero-initialized for y in 0 .. H-1: for x in 0 .. W-1: acc = 0 for i in -r .. r: yi = clamp(y + i, 0, H - 1) acc += I[yi][x] * k[i + r] O[y][x] = acc return O function GAUSSIAN_BLUR(I, sigma): k = GAUSSIAN_KERNEL_1D(sigma) r = (length(k) - 1) / 2 T = CONVOLVE_1D_HORIZONTAL(I, k, r) O = CONVOLVE_1D_VERTICAL(T, k, r) return O ``` Border handling (`clamp`, reflect, or wrap) is an implementation choice; clamping (replicate edge pixels) is simplest and avoids introducing artificial dark/bright borders.

Median filtering. The naive algorithm collects the k2k^2 neighborhood values for every pixel, sorts them, and picks the middle element. ``` function MEDIAN_FILTER(I, k):

# k odd, window is k x k, r = (k-1)/2 r = (k - 1) / 2 O = array H x W for y in 0 .. H-1: for x in 0 .. W-1: window = [] for dy in -r .. r: for dx in -r .. r: yi = clamp(y + dy, 0, H - 1) xi = clamp(x + dx, 0, W - 1) window.append(I[yi][xi]) sort(window) O[y][x] = window[length(window) / 2] # middle element return O ``` For a fixed small window (e.g. 3×33\times3 or 5×55\times5), this is fast enough in practice. For large windows, the classic Huang running-median algorithm exploits the fact that as the window slides one column at a time, only one column of pixels enters and one leaves; a per-column histogram (256 bins for 8-bit images) is updated incrementally, and the median is tracked via a running count rather than resorting from scratch. ``` function HUANG_MEDIAN_FILTER(I, k): r = (k - 1) / 2 O = array H x W for y in 0 .. H-1: hist = zeros(256) for dy in -r .. r: for dx in -r .. r: yi = clamp(y + dy, 0, H - 1) hist[I[yi][clamp(dx, 0, W-1)]] += 1 median_idx = FIND_MEDIAN(hist, kk) O[y][0] = median_idx for x in 1 .. W-1: for dy in -r .. r: yi = clamp(y + dy, 0, H - 1) out_col = clamp(x - r - 1, 0, W - 1) in_col = clamp(x + r, 0, W - 1) hist[I[yi][out_col]] -= 1 hist[I[yi][in_col]] += 1 median_idx = FIND_MEDIAN(hist, kk) # incremental scan, amortized O(1) O[y][x] = median_idx return O ``` `FIND_MEDIAN` tracks a running cumulative count and a pointer into the histogram; because the histogram changes by only 2k2k entries per column step, the median pointer moves only a small, bounded amount on average, giving an amortized near-constant update per pixel.

Figure
Gaussian separable convolution vs. median sliding window
Gaussian: separable 1D passes input I → rows horiz. blur T → cols vert. blur O O = ( I * g_sigma^T ) * g_sigma — same result as 2D conv, cost O(n) not O(n^2) Median: sliding window, sorted order statistic 3x3 window at (x,y) 1224014 151316 111210 sorted: 10 11 12 12 [13] 14 15 16 240 median = 13 (outlier 240 discarded) 13 Effect on a step edge (1D profile) solid = true edge blue dashed = Gaussian (smeared) red dotted = median (sharp)

Complexity & correctness

Gaussian filter — time and space. For an H×WH \times W image and a kernel with half-width rr (so kernel diameter n=2r+1n = 2r+1):

  • Direct 2D convolution: each output pixel costs O(n2)O(n^2) multiply-adds, so total time is O(HWn2)O(H W n^2).
  • Separable implementation: each output pixel of the horizontal pass costs O(n)O(n), and likewise for the vertical pass, giving total time O(HWn)O(H W n) — a factor of nn speedup, which matters enormously since n=O(σ)n = O(\sigma) and large σ\sigma (needed for heavy denoising) makes n2n^2 prohibitive.
  • Space: O(HW)O(HW) for the image buffers, plus O(n)O(n) for the 1D kernel; the intermediate buffer TT adds another O(HW)O(HW), so total space is O(HW)O(HW) (constant-factor overhead).

Why separability is correct, not just faster: 2D convolution is associative and the 2D Gaussian factors as an outer product Gσ(x,y)=gσ(x)gσ(y)G_\sigma(x,y) = g_\sigma(x) g_\sigma(y). Substituting into the convolution integral,

(I∗Gσ)(x,y)=∬I(x−u,y−v) gσ(u) gσ(v) du dv=∫gσ(v)[∫I(x−u,y−v)gσ(u) du]dv,(I * G_\sigma)(x,y) = \iint I(x-u,y-v)\, g_\sigma(u)\, g_\sigma(v)\, du\, dv = \int g_\sigma(v)\left[\int I(x-u,y-v) g_\sigma(u)\, du\right] dv,

which is exactly "convolve each row with gσg_\sigma, then convolve each resulting column with gσg_\sigma." The inner integral is the horizontal pass; the outer is the vertical pass. No approximation is involved — the separable computation produces the exact same result as direct 2D convolution (up to floating-point rounding), not merely a fast approximation. Why Gaussian smoothing is the "right" linear filter: under the model that each pixel is the true signal plus i.i.d. Gaussian noise, a local weighted average is the maximum-likelihood estimate of the mean, and the Gaussian weights are the unique kernel that is simultaneously separable, rotationally symmetric, and infinitely smooth — properties needed for consistent multi-scale (scale-space) analysis, since Gaussian blurring is the unique linear, shift-invariant smoothing that does not create new local extrema as σ\sigma increases (the scale-space causality property). This is why the Gaussian, rather than a box or triangular filter, underlies the construction of Image Pyramids.

Median filter — time and space. For a k×kk \times k window (n=k2n = k^2 samples per pixel):

  • Naive (collect + sort per pixel): each pixel costs O(nlog⁡n)O(n \log n) for sorting, so total time is O(HWk2log⁡(k2))=O(HWk2log⁡k)O(HW k^2 \log(k^2)) = O(HW k^2 \log k).
  • Selection-based (median-of-medians or quickselect instead of full sort): O(n)O(n) expected (or worst-case with median-of-medians) per pixel, giving O(HWk2)O(HW k^2) total.
  • Huang's running-histogram method: maintaining a per-column histogram and updating it incrementally as the window slides costs O(k)O(k) to add/remove a column and amortized small cost to relocate the median pointer, giving O(HWk)O(HW k) total for 8-bit images (histogram bins bounded by a constant, 256) — a substantial improvement for large kk.
  • Space: O(HW)O(HW) for image buffers; O(256)O(256) or O(bit-depth range)O(\text{bit-depth range}) for the histogram in Huang's method, versus O(k2)O(k^2) for the naive per-pixel window buffer.

Why the median is correct (robust) where the mean is not: the breakdown point of an estimator is the fraction of arbitrarily corrupted samples it can tolerate before producing an arbitrarily wrong output. The mean has breakdown point 00: a single sample driven to +∞+\infty drags the mean to +∞+\infty. The median has breakdown point 12\tfrac{1}{2}: up to (just under) half the samples in the window can be arbitrarily corrupted and the median remains bounded by the true values, because the median is defined purely by rank, not magnitude. This is exactly why median filtering removes salt-and-pepper noise (where a small fraction of pixels take extreme values) essentially perfectly for reasonable noise densities, while a mean/Gaussian filter only attenuates such noise proportionally to kernel weight. Edge preservation, argued precisely: consider a 1D step edge with true values aa on the left and b>ab > a on the right, and a window of odd length kk centered at position xx. If more than half the window lies on one side of the edge, the median equals exactly aa or exactly bb — the true value — with zero blur, regardless of kk. A Gaussian filter, in contrast, always produces a weighted blend of aa and bb whenever any nonzero-weight kernel taps straddle the edge, producing a smeared transition whose width scales with σ\sigma. This is the formal sense in which median filtering is "edge-preserving" and Gaussian filtering is not.

Honest tradeoffs. The Gaussian filter is linear (compositions of Gaussian blurs are themselves Gaussian, Gσ1∗Gσ2=Gσ12+σ22G_{\sigma_1} * G_{\sigma_2} = G_{\sqrt{\sigma_1^2+\sigma_2^2}}), has a clean frequency response (a Gaussian in the spatial domain transforms to a Gaussian in frequency, so it is a smooth low-pass filter with no ringing), and is separable and fast — but it blurs every edge and cannot remove impulsive noise without an impractically small kernel. The median filter preserves edges and kills outliers, but it is nonlinear (no frequency-domain shortcut, no simple composition rule), can erode thin structures (a line thinner than the window's "majority" width gets removed, since the majority of window entries then come from the background), and its fast implementation (Huang's algorithm) is more complex to code than a straightforward separable convolution.

Worked example

Take a 5×55\times5 8-bit image with a clean vertical step edge (left half = 20, right half = 200), and inject one salt-noise pixel (value 255) at position (2,2)(2,2) (0-indexed, row, col): ``` 20 20 20 200 200 20 20 20 200 200 20 20 255 200 200 20 20 20 200 200 20 20 20 200 200 ```

Gaussian filter, 3×33\times3 kernel from σ=1\sigma = 1 (unnormalized weights, rounded, then normalized to sum 1):

K≈116(121242121)K \approx \frac{1}{16}\begin{pmatrix}1&2&1\\2&4&2\\1&2&1\end{pmatrix}

Applying KK centered at (2,2)(2,2) (using clamped borders where needed), the weighted sum over the 3×33\times3 neighborhood ``` 20 20 200 20 255 200 20 20 200 ``` is

1(20)+2(20)+1(200)+2(20)+4(255)+2(200)+1(20)+2(20)+1(200)16=20+40+200+40+1020+400+20+40+20016=198016≈123.75.\frac{1(20)+2(20)+1(200)+2(20)+4(255)+2(200)+1(20)+2(20)+1(200)}{16} = \frac{20+40+200+40+1020+400+20+40+200}{16} = \frac{1980}{16} \approx 123.75.

The Gaussian output at (2,2)(2,2) is about 124 — a value that is neither the clean background (20) nor the clean foreground (200) nor the noise spike (255); the noise has been blended into the result and the edge nearby is softened over roughly a ±3σ\pm 3\sigma band.

Median filter, same 3×33\times3 window at (2,2)(2,2): sort the nine values ``` 20 20 20 20 20 20 200 200 255 ``` sorted: 20,20,20,20,20,20,200,200,25520,20,20,20,20,20,200,200,255. The median (5th of 9) is 20. The noise spike is completely eliminated, and the output at (2,2)(2,2) correctly reflects that the majority of the neighborhood is background. Away from the spike — say at pixel (2,3)(2,3), whose 3×33\times3 window is entirely on the "200" side except for one column touching the edge — the median filter also reproduces a clean value near 200, whereas the Gaussian filter at the same location would show a smooth, gradual transition spanning several pixels across the true edge.

LocationTrue valueGaussian (σ=1\sigma=1) outputMedian (3×33\times3) output
(2,2)(2,2) (noise pixel, should be 20)20≈124 (corrupted by blend)20 (fully corrected)
(2,1)(2,1) (clean background, near noise)20≈45 (leaked from spike)20 (unaffected)
(0,2)(0,2)–(0,3)(0,3) edge boundary20 → 200smooth ramp over ~3 pxsharp step, ≤1 px transition

This trace demonstrates concretely: the linear Gaussian filter spreads the single bad sample's influence to its neighbors (since convolution is a weighted sum that cannot ignore any input), while the nonlinear median filter's rank-based selection discards the outlier outright.

Common pitfalls & traps

PitfallWhy it happensFix
Using an even-sized median windowNo unique middle element; ad hoc averaging of the two central values reintroduces some non-robustnessAlways use odd k×kk \times k windows (e.g., 3, 5, 7)
Forgetting to normalize the Gaussian kernelUnnormalized weights change overall image brightnessDivide kernel entries by their sum before convolving
Confusing kernel size with σ\sigmaA truncated kernel that is too small clips the Gaussian's tails, distorting the frequency responseUse half-width r≥3σr \ge 3\sigma (captures >99.7% of mass)
Ignoring border effectsNaive convolution at image edges either reads out-of-bounds or implicitly zero-pads, darkening edgesUse clamp/replicate or reflect padding, not zero padding, for photometric filtering
Applying median filter expecting it to remove Gaussian noiseMedian filtering is optimized for impulsive (outlier) noise, not distributed additive Gaussian noise, where the mean is statistically more efficientUse Gaussian (or Wiener) filtering for additive Gaussian noise; reserve median for salt-and-pepper/impulse noise
Large median windows eroding thin lines/textA thin bright line thinner than roughly k/2k/2 becomes a minority in its own window and is replaced by backgroundUse the smallest window that removes the target noise density; consider adaptive or weighted median variants
Re-sorting the entire window from scratch every pixel for large kkNaive implementation costs O(k2log⁡k)O(k^2 \log k) per pixel, intractable for large windows/videoUse Huang's running histogram (or a selection algorithm) for large kk

Pioneers & history

The Gaussian kernel's mathematical roots trace to Carl Friedrich Gauss's work on the normal distribution and least-squares estimation, but its systematic use as an image-smoothing and multiscale analysis tool emerged from 20th-century signal processing and the computer vision community's development of scale-space theory, where Gaussian blurring was shown to be the unique kernel satisfying causality (no new spurious extrema) across scales. The median filter's development is more directly tied to robust statistics and digital signal processing.

Origins · 1 minJohn Wilder Tukey and how this was discovered
JW
John Wilder Tukey
1915–2000 · American statistician who pioneered robust statistics and the median filter for smoothing time series and, later, images

History
1795
— Gauss develops the normal distribution and least-squares methods underlying the Gaussian kernel.
1971
— John Tukey introduces running-median smoothing for robust time-series analysis.
1978
— Median filtering is adapted and analyzed for two-dimensional image denoising.
1981
— Huang, Yang, and Tang publish the efficient running-histogram algorithm for fast median filtering.
1983
— Burt and Adelson formalize the Gaussian pyramid, cementing Gaussian smoothing's role in multiscale vision.

Where this matters

Nearly every camera image-signal processor (ISP) applies denoising — often a combination of Gaussian-like and median-like filters — before demosaicing, sharpening, and compression. Photo and video editing software (Adobe Photoshop, GIMP, mobile camera pipelines) expose both "Gaussian Blur" and "Median" or "Despeckle" filters directly as user-facing tools. In computer vision pipelines, Gaussian smoothing is the mandatory precursor to gradient-based operators such as Sobel or Canny edge detection (since numerical differentiation amplifies high-frequency noise, a topic developed further in Image Gradients), and it is the core recursive operation used to build Image Pyramids for scale-invariant keypoint detectors such as SIFT and for multiresolution blending and coarse-to-fine optical flow. Median filtering is used in astronomical imaging to remove cosmic-ray hits, in medical imaging to suppress impulsive sensor artifacts, and in classic salt-and-pepper noise removal for scanned documents and old photographs. Both filters also serve as pedagogical anchors: the Gaussian illustrates the power and limits of linear, shift-invariant systems, while the median illustrates why nonlinear, order-statistic-based methods are indispensable whenever real-world noise is not well modeled by a Gaussian distribution.

Ask the tutor: How does the bilateral filter modify the Gaussian kernel to preserve edges, and why does that break separability?

Why does Gaussian blurring reduce noise if it doesn't remove any pixel values?

Gaussian blurring is a weighted average, and averaging independent random noise reduces its variance without removing signal structure. If each pixel is the true value plus independent zero-mean noise, a weighted sum over nn effectively independent samples reduces the noise's standard deviation roughly by a factor related to 1/neff1/\sqrt{n_{\text{eff}}} (exactly, for i.i.d. noise and normalized weights ∑wi2\sum w_i^2 controls the variance reduction). The true underlying signal, if locally smooth, changes little under this averaging, so the ratio of retained signal to reduced noise improves — this is exactly why the Gaussian filter behaves as a low-pass filter, and why the underlying assumption (local smoothness of the true image) matters for its correctness.

How do I choose sigma for a Gaussian filter?

Sigma controls the tradeoff between noise suppression and detail loss: larger σ\sigma suppresses more noise (and more high-frequency detail like textures and fine edges) at the cost of blurring structure you may want to keep. A common heuristic ties σ\sigma to the expected noise level (larger noise standard deviation warrants larger σ\sigma) or to the scale of features you want to preserve versus discard, since the Gaussian's frequency response attenuates spatial frequencies above roughly 1/σ1/\sigma. In practice, σ\sigma between 0.5 and 2 pixels handles mild sensor noise, while multiscale applications (like image pyramids) use a geometric sequence of σ\sigma values to build a scale-space.

Can the median filter be made as fast as Gaussian filtering?

For large windows, the naive per-pixel sort is much slower than separable Gaussian convolution, but Huang's running-histogram algorithm brings median filtering down to roughly linear time in the number of pixels, independent of window size for fixed bit depth, by incrementally updating a histogram as the window slides. This still involves more bookkeeping and a less cache-friendly access pattern than the two simple 1D passes of separable Gaussian convolution, so in practice median filtering typically remains somewhat slower per pixel, but the asymptotic gap in window-size dependence (O(k)O(k) vs. O(k2log⁡k)O(k^2 \log k) naive) is what makes large-window median filtering practical at all.

Is there a filter that combines Gaussian smoothing with edge preservation?

Yes — the bilateral filter and related edge-preserving smoothers weight neighboring pixels by both spatial distance (as the Gaussian does) and intensity similarity, so pixels across a strong edge receive near-zero weight even though they are spatially close. This retains much of the Gaussian filter's noise-averaging power while approximating the median filter's edge preservation, at the cost of losing the clean separability and linear-system guarantees of the plain Gaussian filter, and requiring a nonlinear, per-pixel adaptive computation.

Why does the Gaussian filter never create new local maxima or minima as sigma increases?

This is the causality property central to scale-space theory: convolving a signal with a Gaussian is equivalent to running the heat/diffusion equation forward in time, and diffusion equations are known to smooth monotonically — new extrema cannot spontaneously appear, only existing ones can merge or disappear as σ\sigma grows. This uniqueness result (that the Gaussian is essentially the only kernel with this non-creation property under mild axioms) is why Gaussian pyramids, rather than pyramids built from other blur kernels, are the standard foundation for scale-invariant feature detection.

What happens if you apply the median filter repeatedly?

Repeated median filtering on the same image tends to converge to a stable "root signal" after a finite number of iterations — regions of the image stop changing once every local window has a well-defined majority-consistent value, producing a piecewise-constant-like image with flat regions separated by preserved edges. This is fundamentally different from repeated Gaussian filtering, which never converges to a fixed point and instead approaches a spatially constant (fully blurred) image as iteration count or effective σ\sigma grows without bound, since Gaussian convolution composes into ever-wider Gaussians.

Does color complicate either filter?

For Gaussian filtering, color is straightforward: apply the same 1D separable convolution independently to each channel (R, G, B, or in a perceptual space like Lab), since the filter is linear and channels are processed identically. For median filtering, there is a subtlety — computing the median of each channel independently can produce a color not present anywhere in the original neighborhood (color fringing), so robust implementations often use a vector median, choosing the actual neighborhood pixel (as a full RGB triple) whose sum of distances to all others in the window is smallest, preserving valid colors at the cost of extra computation.

Before this

Where this leads