Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
56 commits
Select commit Hold shift + click to select a range
57863c4
bilateral filter with CIELAB distance
Krasner Dec 31, 2025
d32d1cf
fix nomenclature
Krasner Dec 31, 2025
a6ee113
fix bug
Krasner Jan 1, 2026
60893f5
bug fix
Krasner Jan 1, 2026
04a7ff9
variable name fix
Krasner Jan 1, 2026
0021a76
incorporate git actions advice
Krasner Jan 1, 2026
e97becb
cleanup
Krasner Jan 1, 2026
d7065e6
Fix indexing problem in kmeans_clustering_spatial. Verify that it wor…
Krasner Jan 1, 2026
ba234a8
kernel range fix
Krasner Jan 1, 2026
0d42005
feat(WASM): add bilateral filter
fransafu Dec 31, 2025
cd4da10
test: add call method test, and custom parameters test
fransafu Dec 31, 2025
8d296f1
refactor: rename image parameter to pixels to match lib conventions
fransafu Dec 31, 2025
c21ef21
test: add bilateralFilter as part of helper methods return test
fransafu Dec 31, 2025
a39db16
refactor: remove unused variable MAX_PIXEL_VAL
fransafu Dec 31, 2025
6317a81
feat: add headers to be used by WASM (best practice)
fransafu Dec 31, 2025
4719995
docs(WASM): add bilateral_filter documentation (overview, explained, …
fransafu Dec 31, 2025
9e71927
feat(bilateral_filter): add upper bound validation for sigma_spatial
fransafu Dec 31, 2025
fc1af42
docs: improve bilateral headers documentation
fransafu Dec 31, 2025
7e2c942
docs(bilateral filter): explain use of Gaussian kernels inside formula
Ryan-Millard Jan 3, 2026
bf2fb0a
docs(bilateral filter): better styling
Ryan-Millard Jan 3, 2026
457bc28
docs(bilateral filter): better styling
Ryan-Millard Jan 3, 2026
7df3173
docs(bilateral filter): correct sidebar_position
Ryan-Millard Jan 3, 2026
273f207
docs(bilateral filter): mobile accessibility
Ryan-Millard Jan 3, 2026
6dad78d
docs(bilateral filter): explicit description in module overview
Ryan-Millard Jan 3, 2026
a4c3849
=== end of commits from #176 ===
Ryan-Millard Jan 3, 2026
b0e3f17
Merge branch 'feat/kmean-preprocessing/bilateral-filter' into dev/bil…
Ryan-Millard Jan 3, 2026
c0e9d07
fix(duplicate symbol: bilateral_filter): temp rename cielab -> bilate…
Ryan-Millard Jan 3, 2026
8062a13
Merge pull request #177 from Krasner/dev/bilateral
Ryan-Millard Jan 3, 2026
7604897
docs(bilateral filter): fix api.md styling
Ryan-Millard Jan 3, 2026
31887a7
feat(bilateral filter): combine CIELAB & RGB implementations into sin…
Ryan-Millard Jan 3, 2026
e3299c5
refactor(cielab.h): split into .h & .cpp, include guard
Ryan-Millard Jan 3, 2026
a482a16
fix(bilateral filter): guard against unknown color_space param
Ryan-Millard Jan 3, 2026
4425cce
fix(cielab.{h,cpp} includes): properly structured
Ryan-Millard Jan 3, 2026
0ee78a6
debugging memory and run time...
Krasner Jan 3, 2026
dcb4cb7
Convert full RGB image to CIELAB then look up during convolution step
Krasner Jan 4, 2026
8d267d7
put in missing if
Krasner Jan 4, 2026
60a6e30
updates
Krasner Jan 4, 2026
583b324
update bilateral filter docs to include cielab color space
Krasner Jan 4, 2026
56bd792
for cielab apply bilateral filter weights on LAB components then conv…
Krasner Jan 5, 2026
1d7a5a4
fix bug
Krasner Jan 5, 2026
9a7580a
docs(bilateral filter): update for better clarity
Ryan-Millard Jan 5, 2026
85fd528
refactor(cielab): use constexpr functions and improve naming
Ryan-Millard Jan 5, 2026
ef96ab4
refactor(bilateral_filter): improve readability and use brace initial…
Ryan-Millard Jan 5, 2026
fc770b0
condense 2 switch cases
Krasner Jan 5, 2026
65d4b54
refactor(bilateral filter): inline gaussian function
Ryan-Millard Jan 5, 2026
16c0097
fix(cpp: cielab): gaussian function inlined now
Ryan-Millard Jan 5, 2026
6e4cd68
RGB-CIELAB conversion write up
Krasner Jan 6, 2026
7baacc8
docs(bilateral filter): add color space docs
Ryan-Millard Jan 6, 2026
1f59365
Merge remote-tracking branch 'origin/main' into feat/kmean-preprocess…
Ryan-Millard Jan 6, 2026
fd3cff0
style(formatting): fix formatting issues on all files
Ryan-Millard Jan 6, 2026
9538f6e
style(docs files): fix display styles
Ryan-Millard Jan 6, 2026
c89d3ca
feat(cielab): improve RGB to Lab documentation and matrix precision
Ryan-Millard Jan 6, 2026
32f483e
style(docs: cielab): fix comment formatting - line 47
Ryan-Millard Jan 6, 2026
14a3aa6
docs(cpp: bilateral filter): document the difference between sigma_ra…
Ryan-Millard Jan 6, 2026
45f6274
style(docs: bilateral filter): fix formatting
Ryan-Millard Jan 6, 2026
abb0402
fix(cpp: cielab.h): add clamp values to protect against bad data
Ryan-Millard Jan 6, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"label": "bilateral_filter.h",
"position": 4,
"link": {
"type": "generated-index",
"title": "Bilateral Filter",
"description": "Documentation for the Bilateral Filter in the Image WebAssembly (WASM) module in Img2Num.",
"slug": "/reference/wasm/modules/image/bilateral_filter"
}
}
53 changes: 53 additions & 0 deletions docs/docs/reference/wasm/modules/image/bilateral_filter/api.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
---
id: api
title: Bilateral Filter β€” API & Reference
sidebar_label: API / Usage
sidebar_position: 4
---

# Bilateral Filter β€” API & Reference

Quick reference for the function implemented in the header.

```cpp title="Applies a bilateral filter to an RGBA uint8_t* image (modified in-place)."
void bilateral_filter(uint8_t *image, size_t width, size_t height, double sigma_spatial, double sigma_range, uint8_t color_space)
```

:::important Alpha Channel Preservation
The alpha channel, `image[i + 3]`, is left untouched - it is not part of the bilateral filter implementation.
:::

## Parameters

| Parameter | Type | Description |
| :-------------- | :--------- | :--------------------------------------------------------------------------------------------------------------------------------------------- |
| `image` | `uint8_t*` | Pointer to the RGBA image data (4 bytes per pixel). Modified in-place. |
| `width` | `size_t` | Width of the image in pixels. |
| `height` | `size_t` | Height of the image in pixels. |
| `sigma_spatial` | `double` | Spatial standard deviation ($\sigma_s$). Controls how far pixels influence each other spatially. |
| `sigma_range` | `double` | Range standard deviation ($\sigma_r$). Controls how much color definition is preserved (edge preservation). |
| `color_space` | `uint8_t` | Toggle color space to use for range distance (0 - CIELAB, 1 - RGB). CIELAB produces perceptually better results but requires more computation. |

:::info Implementation Details

- **Namespace**: `bilateral` (C++)
- **Export**: Exposed to WASM via `extern "C"` wrapper as `bilateral_filter`.
:::

:::tip Color Space Discrepancies
As noted on the
[Color Space Selection page](../color-spaces/#why-the-scaling-factor-exists-and-why-418-works),
the bilateral filter will produce **different results** depending on the selected
`color_space`, even with identical parameters.

To achieve **visually equivalent filtering behavior** between CIELAB and RGB,
treat CIELAB as the reference space and scale `sigma_range` for RGB:

$$
\sigma_{\text{range, RGB}} \approx 4.18 \times \sigma_{\text{range, CIELAB}}
$$

> The factor **4.18** is empirically derived for natural images and equalizes bilateral
> range weights across color spaces. Any value in the range **[4.1, 4.3]** will typically
> produce comparable results. This is a recommended default, not a universal constant.
> :::
Original file line number Diff line number Diff line change
@@ -0,0 +1,332 @@
---
Comment thread
coderabbitai[bot] marked this conversation as resolved.
id: color-spaces
title: Color Space Selection β€” RGB vs CIELAB
sidebar_label: Color Space Selection
sidebar_position: 6
---

# Color Space Selection β€” RGB vs CIELAB

The bilateral filter in Img2Num supports two color spaces for computing range (color) distances: **RGB** and **CIELAB**. This guide explains the differences, trade-offs, and when to use each.

## Quick Comparison

| Aspect | RGB | CIELAB |
| :---------------------- | :-------------------------------------------------------------------------------------- | :------------------------------------------------------ |
| **Perceptual accuracy** | Lower β€” equal Euclidean distances don't correspond to equal perceived color differences | Higher β€” designed to be perceptually uniform |
| **Performance** | Faster β€” uses precomputed LUT | Slower β€” requires conversion and on-the-fly computation |
| **Edge preservation** | Good for most images | Better for images with subtle color transitions |
| **Best for** | General purpose, real-time applications | High-quality processing, perceptual accuracy |

## When to Use Each Color Space

### Use RGB when:

- **Performance is critical** β€” RGB processing is significantly faster due to LUT optimization
- **Working with high-contrast images** β€” where edge preservation is less sensitive to color space choice
- **Real-time processing** β€” where milliseconds matter
- **Sigma_range values are well-tuned** β€” and visual results are satisfactory

### Use CIELAB when:

- **Perceptual uniformity matters** β€” you want visually equal smoothing across different hues
- **Working with skin tones or subtle gradients** β€” where human perception is sensitive
- **Quality over speed** β€” when processing time is less critical than output quality
- **Processing medical or scientific imagery** β€” where perceptual accuracy is important

## Mathematical Differences

### Distance Metrics

Both color spaces compute the Euclidean distance between color vectors, but the ranges differ significantly.

#### RGB Color Space

RGB channels are bounded `[0, 255]` per channel:

$$
\text{distance}_{\text{RGB}} = \sqrt{\Delta R^2 + \Delta G^2 + \Delta B^2}
$$

Maximum possible distance:

$$
\text{max}_{\text{RGB}} = \sqrt{255^2 + 255^2 + 255^2} \approx 441.67
$$

#### CIELAB Color Space

CIELAB channels have different ranges:

- **L\***: `[0, 100]` (lightness)
- **a\***: approximately `[-128, 127]` (green-red)
- **b\***: approximately `[-128, 127]` (blue-yellow)

$$
\text{distance}_{\text{LAB}} = \sqrt{\Delta L^2 + \Delta a^2 + \Delta b^2}
$$

Maximum theoretical distance:

$$
\text{max}_{\text{LAB}} = \sqrt{100^2 + 255^2 + 255^2} \approx 373.56
$$

:::important Key Insight
In practice, most real-world pixel differences are **much smaller** than the maximum possible distance. CIELAB distances for neighboring pixels are typically smaller than RGB distances due to:

1. **Numerical compression** from the RGB→LAB conversion
2. **Perceptual scaling** β€” LAB is designed to reflect human vision, which perceives smaller differences
:::

## Sigma_range Behavior Differences

The `sigma_range` parameter controls edge preservation by weighting color similarity. However, the same `sigma_range` value produces **different visual results** in RGB vs CIELAB.

### The Range Weight Formula

The bilateral filter computes range weights using a Gaussian:

$$
w_{\text{range}} = \exp\left(-\frac{\text{distance}^2}{2\sigma_{\text{range}}^2}\right)
$$

- When distance is **small**, weight is **high** (β‰ˆ1) β†’ strong contribution
- When distance is **large**, weight is **low** (β‰ˆ0) β†’ weak contribution

### Why the Same Sigma Produces Different Results

import RgbVsLabRangeKernel from '@site/src/components/docs/reference/wasm/modules/image/bilateral_filter/RgbVsLabRangeKernel';

<RgbVsLabRangeKernel />

**With `sigma_range = 50`**:

- **RGB**: Typical neighboring pixel distances are small relative to 50, so many neighbors contribute significantly β†’ **moderate blur**
- **CIELAB**: Typical neighboring pixel distances are even smaller, so almost all neighbors contribute strongly β†’ **stronger blur**

### Sigma_range Scaling for Visual Consistency

To achieve **visually similar** blur between RGB and CIELAB, you can scale `sigma_range`:

```javascript
// Example: Scaling RGB sigma_range to match CIELAB visual output
const sigma_range_base = 50.0; // Target CIELAB sigma_range

let sigma_range_actual;
if (color_space === COLOR_SPACE_RGB) {
// Scale RGB sigma_range to match CIELAB perceptually
sigma_range_actual = sigma_range_base * 4.18;
} else {
sigma_range_actual = sigma_range_base;
}
```

:::important Scaling Factor
The scaling factor of **~4.18** is empirically derived and works well for natural images. However:

- It's **not universal** β€” depends on image statistics
- It's **not mandatory** β€” the different behaviors are valid features of each color space
- **Advanced users** may want different sigma_range values for each space
:::

### Visual Example

Using the same `sigma_range = 50`:

| Color Space | Visual Result |
| :--------------- | :-------------------------------------------------------------------------- |
| **CIELAB** | Stronger smoothing, better edge preservation in perceptually uniform manner |
| **RGB** | Moderate smoothing, adequate edge preservation for most use cases |
| **RGB (scaled)** | Similar smoothing to CIELAB when `sigma_range β‰ˆ 209` |

## Why the Scaling Factor Exists (and Why ~4.18 Works)

RGB and CIELAB do **not** measure color differences on the same numeric scale. As a result, identical `sigma_range` values will generally not produce equivalent range weights or visual results.

### What β€œequivalent behavior” means mathematically

The bilateral filter’s range weight is defined as:

$$
w_{\text{range}} = \exp!\left(-\frac{d^2}{2\sigma_{\text{range}}^2}\right)
$$

For RGB and CIELAB to behave equivalently, they must produce **the same range weight** for corresponding color differences:

$$
\exp\left(-\frac{d_{\text{RGB}}^2}{2\sigma_{\text{RGB}}^2}\right)
\approx
\exp\left(-\frac{d_{\text{LAB}}^2}{2\sigma_{\text{LAB}}^2}\right)
$$

Taking the logarithm and simplifying yields:

$$
\frac{d_{\text{RGB}}}{\sigma_{\text{RGB}}}
\approx
\frac{d_{\text{LAB}}}{\sigma_{\text{LAB}}}
$$

This implies the required relationship:

$$
\sigma_{RGB} \approx
\frac{d_{\text{RGB}}}{d_{\text{LAB}}}
\sigma_{LAB}
$$

So the scaling factor is **not arbitrary** β€” it is the **ratio of typical RGB distances to LAB distances** for the same pixel differences.

### Where the value ~4.18 comes from

For natural images (photographic content, sRGB, D65):

1. Sample many _local_ pixel pairs (neighbors).
2. Measure:
- $$d_{\text{RGB}} = \sqrt{\Delta R^2 + \Delta G^2 + \Delta B^2}$$
- $$d_{\text{LAB}} = \sqrt{\Delta L^2 + \Delta a^2 + \Delta b^2}$$

3. Compute the ratio $\frac{d_{RGB}}{d_{LAB}}$.
4. Aggregate (mean or median).

Across a wide range of natural images, this ratio consistently clusters around:

$$
\boxed{4.1 \text{ to } 4.3}
$$

The value **4.18** lies near the center of this empirical range and provides a strong default for matching bilateral range behavior between RGB and CIELAB.

### Why this ratio is stable (but not universal)

The factor remains stable for natural images because:

- **LAB compresses perceptual differences**
Equal perceived color changes produce smaller numeric deltas than in RGB.
- **RGB channels are highly correlated**
Euclidean RGB distance accumulates redundant energy across channels.
- **Bilateral filters operate locally**
In the small-delta regime, the RGB→LAB transform is locally quasi-linear.

However, the factor may vary if:

- Images are synthetic or heavily quantized
- A different RGB color space or white point is used
- LAB components are re-weighted or normalized differently

### Practical guidance

- **Recommended default**

For visually comparable smoothing on natural images, use:

$$
\sigma_{range_{RGB}} \approx 4.18 \times \sigma_{range_{CIELAB}}
$$

- **Advanced usage**
For strict equivalence, compute the ratio
$$
k = \frac{\mathbb{E}[d_{RGB}]}{\mathbb{E}[d_{LAB}]}
$$
on your image set and scale `sigma_range` accordingly.

## Performance Considerations

### RGB Performance

- **Precomputed LUT**: All 195,075 possible squared distances are precomputed
- **O(1) lookup**: Range weight retrieval is extremely fast
- **Memory**: ~1.5 MB for LUT (acceptable for most applications)

### CIELAB Performance

- **Full image conversion**: RGB→LAB conversion for entire image upfront
- **On-the-fly computation**: Range weights computed using `exp()` for each neighbor
- **Slower but optimized**: Conversion is done once; only distance calculation repeated

**Performance Impact**: CIELAB is typically **2-4Γ— slower** than RGB, depending on image size and kernel radius.

:::tip Optimization Note
Future optimizations may include:

- Taylor/Horner polynomial approximations for `exp(-xΒ²)`
- SIMD vectorization for distance calculations
- Adaptive LUT for CIELAB (with quantization)
:::

## Implementation Details

### RGB Range Weights (LUT)

```cpp
// Precompute all possible RGB distances
std::vector<double> range_lut(MAX_RGB_DIST_SQ + 1);
for (int i = 0; i <= MAX_RGB_DIST_SQ; ++i) {
range_lut[i] = gaussian(std::sqrt(i), sigma_range);
}

// Later, during filtering:
const int dr = r_neighbor - r_center;
const int dg = g_neighbor - g_center;
const int db = b_neighbor - b_center;
const int dist_sq = dr*dr + dg*dg + db*db;
double w_range = range_lut[dist_sq]; // O(1) lookup
```

### CIELAB Range Weights (On-the-fly)

```cpp
// Precompute full-image RGB β†’ LAB conversion
std::vector<double> cie_image(width * height * 4);
for (each pixel) {
rgb_to_lab(r, g, b, L, A, B);
cie_image[idx] = L; cie_image[idx+1] = A; cie_image[idx+2] = B;
}

// Later, during filtering:
double dL = L_neighbor - L_center;
double dA = A_neighbor - A_center;
double dB = B_neighbor - B_center;
double dist = std::sqrt(dL*dL + dA*dA + dB*dB);
double w_range = gaussian(dist, sigma_range); // Computed on-the-fly
```

## Recommendations

### Default Choice

For most applications, **RGB** is the recommended default:

- βœ… Faster processing
- βœ… Good results for general images
- βœ… Predictable behavior

### When to Switch to CIELAB

Consider CIELAB when you observe:

- Inconsistent smoothing across different hues
- Need for perceptually uniform processing
- Working with images where color accuracy is critical
- Willing to accept 2-4Γ— performance cost

### Parameter Tuning

**Starting values**:

- `sigma_spatial = 3.0` (both color spaces)
- `sigma_range = 50.0` (CIELAB) or `sigma_range = 200.0` (RGB for similar visual effect)

**Adjustment guidelines**:

- Increase `sigma_range` β†’ more blur, less edge preservation
- Decrease `sigma_range` β†’ sharper edges, less smoothing
- Test with your specific images β€” optimal values vary by content

## See Also

- [Implementation Details](./implementation.md#range-weights) β€” Deep dive into LUT vs on-the-fly computation
- [API Reference](./api.md) β€” `color_space` parameter documentation
- [Keywords](./keywords.md) β€” Understanding range and spatial components
Loading