-
-
Notifications
You must be signed in to change notification settings - Fork 47
feat(WASM): add bilateral filter #176
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Ryan-Millard
merged 15 commits into
Ryan-Millard:feat/kmean-preprocessing/bilateral-filter
from
fransafu:feature/bilateral-filter
Jan 3, 2026
Merged
Changes from all commits
Commits
Show all changes
15 commits
Select commit
Hold shift + click to select a range
9df4747
feat(WASM): add bilateral filter
fransafu 073e6f8
test: add call method test, and custom parameters test
fransafu 227dd6a
refactor: rename image parameter to pixels to match lib conventions
fransafu 5c3b6c4
test: add bilateralFilter as part of helper methods return test
fransafu 009e0a7
refactor: remove unused variable MAX_PIXEL_VAL
fransafu df40601
feat: add headers to be used by WASM (best practice)
fransafu 9f30fb0
docs(WASM): add bilateral_filter documentation (overview, explained, …
fransafu a8ff686
feat(bilateral_filter): add upper bound validation for sigma_spatial
fransafu a16b186
docs: improve bilateral headers documentation
fransafu 1bd0962
docs(bilateral filter): explain use of Gaussian kernels inside formula
Ryan-Millard 6dc5097
docs(bilateral filter): better styling
Ryan-Millard 35f4e0d
docs(bilateral filter): better styling
Ryan-Millard ebefadb
docs(bilateral filter): correct sidebar_position
Ryan-Millard e5a0c9b
docs(bilateral filter): mobile accessibility
Ryan-Millard 39718f4
docs(bilateral filter): explicit description in module overview
Ryan-Millard File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
10 changes: 10 additions & 0 deletions
10
docs/docs/reference/wasm/modules/image/bilateral_filter/_category_.json
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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" | ||
| } | ||
| } |
31 changes: 31 additions & 0 deletions
31
docs/docs/reference/wasm/modules/image/bilateral_filter/api.md
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,31 @@ | ||
| --- | ||
| id: api | ||
| title: Bilateral Filter — API & Reference | ||
| sidebar_label: API / Usage | ||
| sidebar_position: 5 | ||
| --- | ||
|
|
||
| # Bilateral Filter — API & Reference | ||
|
|
||
| Quick reference for the function implemented in the header. | ||
|
|
||
| ```cpp title="Applies a bilateral filter to an RGBA image (modified in-place)." | ||
| void bilateral_filter(uint8_t *image, | ||
| size_t width, size_t height, | ||
| double sigma_spatial, | ||
| double sigma_range) | ||
|
|
||
| ## 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). | | ||
|
|
||
| :::info Implementation Details | ||
| - **Namespace**: `bilateral` (C++) | ||
| - **Export**: Exposed to WASM via `extern "C"` wrapper as `bilateral_filter`. | ||
| ::: |
78 changes: 78 additions & 0 deletions
78
docs/docs/reference/wasm/modules/image/bilateral_filter/explained.md
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,78 @@ | ||
| --- | ||
| id: explained | ||
| title: Implementation Explained | ||
| sidebar_position: 6 | ||
| --- | ||
|
|
||
| # Bilateral Filter — Implementation Explained | ||
|
|
||
| This section explains the inner workings of the **bilateral filter** implementation. | ||
|
|
||
| ## Overview | ||
|
|
||
| The bilateral filter smoothes an image while **preserving edges**. It achieves this by weighting neighboring pixels based on two criteria: | ||
| 1. **Spatial Distance**: Pixels closer to the center have higher weight. | ||
| 2. **Range (Color) Difference**: Pixels with similar colors to the center have higher weight. | ||
|
|
||
| This prevents the "blurring" from crossing strong edges, where the color difference is large. | ||
|
|
||
| ## How It Works | ||
|
|
||
| For each pixel in the image, we look at a local window (kernel) around it. The new pixel value is a weighted average of its neighbors: | ||
|
|
||
| $$ | ||
| I_{new}(x) = \frac{1}{W_p} \sum_{x_i \in \Omega} I(x_i) \cdot w_{spatial}(\|x_i - x\|) \cdot w_{range}(|I(x_i) - I(x)|) | ||
| $$ | ||
|
|
||
| Where: | ||
| - $w_{spatial}$ is a Gaussian function of the distance. | ||
| - $w_{range}$ is a Gaussian function of the intensity difference. | ||
| - $W_p$ is the normalization factor (sum of all weights). | ||
|
|
||
|
|
||
|
|
||
|
|
||
| :::info | ||
| In this implementation, both weighting terms are **Gaussian kernels**: | ||
|
|
||
| $$ | ||
| w_{\text{spatial}}(d) = \exp!\left(-\frac{d^2}{2\sigma_s^2}\right), | ||
| \quad | ||
| w_{\text{range}}(d) = \exp!\left(-\frac{d^2}{2\sigma_r^2}\right) | ||
| $$ | ||
|
|
||
| where $ \sigma_s$ controls spatial smoothing and $\sigma_r$ controls edge sensitivity. | ||
| ::: | ||
| ## Implementation Details | ||
|
|
||
| Our implementation uses a **naive sliding window** approach with **Look-Up Table (LUT) optimizations** to improve performance in WebAssembly. | ||
|
|
||
| ### 1. Precomputed Look-Up Tables | ||
|
|
||
| Calculating `std::exp()` inside the inner loop is expensive. We precompute the two Gaussian functions: | ||
| - **Spatial Weights**: A 2D grid of weights based on the kernel radius. Since the spatial distance between a neighbor and the center never changes, this is calculated once per filter application. | ||
| - **Range Weights**: A 1D array mapping squared color distance ($0$ to $255^2 \times 3$) to a weight. This allows O(1) lookups for the "edge preservation" factor. | ||
|
|
||
| ```cpp title="Precomputing Range Weights" | ||
| std::vector<double> range_lut(MAX_RGB_DIST_SQ + 1); | ||
| for (int i = 0; i <= MAX_RGB_DIST_SQ; ++i) { | ||
| range_lut[i] = std::exp(-static_cast<double>(i) / two_sigma_range_sq); | ||
| } | ||
| ``` | ||
|
|
||
| ### 2. The Loop | ||
|
|
||
| We iterate over every pixel `(y, x)` and then over every neighbor `(ky, kx)` within the kernel radius: | ||
|
|
||
| 1. **Load Neighbor**: Get RGB values of the neighbor. | ||
| 2. **Spatial Weight**: Look up precomputed $G_{\sigma_{spatial}}$. | ||
| 3. **Range Weight**: Calculate squared color distance $\|C_p - C_q\|^2$ and look up precomputed $G_{\sigma_{range}}$. | ||
| 4. **Accumulate**: `pixel_acc += neighbor_rgb * (spatial_w * range_w)`. | ||
| 5. **Normalize**: Divide by probability sum. | ||
|
|
||
| ### Complexity | ||
|
|
||
| - **Time Complexity**: $O(W \cdot H \cdot R^2)$, where $R$ is the kernel radius. | ||
| - **Space Complexity**: $O(W \cdot H)$ for the output buffer. | ||
|
|
||
| This complexity is why the filter can be slow for large radii ($\sigma_{spatial} > 5.0$), but we currently parameterize the radius to be small ($\sigma_{spatial} \leq 3.0$) so it is not a problem. | ||
73 changes: 73 additions & 0 deletions
73
docs/docs/reference/wasm/modules/image/bilateral_filter/implementation.md
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,73 @@ | ||
| --- | ||
| id: implementation | ||
| title: Bilateral Filter — Implementation details | ||
| sidebar_label: Implementation | ||
| sidebar_position: 4 | ||
| --- | ||
|
|
||
| # Bilateral Filter — Implementation details | ||
|
|
||
| This page maps the conceptual steps of the Bilateral Filter to the concrete functions and loops in the implementation. | ||
|
|
||
| ## 1. Parameters & Window Size | ||
|
|
||
| The filter first calculates the kernel size based on the spatial standard deviation ($\sigma_{spatial}$). | ||
|
|
||
| ```cpp | ||
| const int radius = static_cast<int>(std::ceil(SIGMA_RADIUS_FACTOR * sigma_spatial)); | ||
| const int kernel_width = 2 * radius + 1; | ||
| ``` | ||
|
|
||
| We primarily use $\sigma_{spatial} \approx 3.0$, which results in a kernel radius of 9 (width 19x19). | ||
|
|
||
| ## 2. Precomputing Weights (Optimization) | ||
|
|
||
| To avoid computing `std::exp` millions of times per frame, we precalculate the weights. | ||
|
|
||
| ### Spatial Weights (constant per kernel) | ||
| The distance pattern is the same for every pixel, so we calculate the distance-based weights once at the start. | ||
|
|
||
| ```cpp | ||
| spatial_weights[(ky + radius) * kernel_width + (kx + radius)] = | ||
| std::exp(-dist2 / two_sigma_space_sq); | ||
| ``` | ||
|
|
||
| ### Range Weights (LUT) | ||
| We calculate the `similarity score` for every possible color difference ahead of time. We just measure the color difference and look up the precomputed weight in the table. | ||
|
|
||
| ```cpp | ||
| for (int i = 0; i <= MAX_RGB_DIST_SQ; ++i) { | ||
| range_lut[i] = std::exp(-static_cast<double>(i) / two_sigma_range_sq); | ||
| } | ||
| ``` | ||
|
|
||
| ## 3. Sliding Window Loop | ||
|
|
||
| The core processing happens in a nested loop over every pixel $(y, x)$. For each pixel, we: | ||
|
|
||
| 1. **Iterate** over the window (from $-radius$ to $+radius$). | ||
| 2. **Fetch** neighbor RGB values. | ||
| 3. **Calculate** color difference (squared Euclidean distance). | ||
| 4. **Lookup** spatial weight (from array) and range weight (from LUT). | ||
| 5. **Accumulate** the weighted sum and the sum of weights. | ||
|
|
||
| ```cpp | ||
| double w_space = spatial_weights[...]; | ||
| double w_range = range_lut[dist_sq]; | ||
| double w = w_space * w_range; | ||
|
|
||
| r_acc += r * w; | ||
| g_acc += g * w; | ||
| b_acc += b * w; | ||
| weight_acc += w; | ||
| ``` | ||
|
|
||
| ## 4. Normalization | ||
|
|
||
| Finally, we normalize the accumulated color values by the total weight to get the filtered pixel value: | ||
|
|
||
| ```cpp | ||
| result[center_idx] = static_cast<uint8_t>(std::clamp(r_acc / weight_acc, 0.0, 255.0)); | ||
| ``` | ||
|
|
||
| This ensures the pixel brightness remains consistent with the local area. |
31 changes: 31 additions & 0 deletions
31
docs/docs/reference/wasm/modules/image/bilateral_filter/overview.md
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,31 @@ | ||
| --- | ||
| id: overview | ||
| title: Bilateral Filter | ||
| sidebar_label: Overview | ||
| sidebar_position: 2 | ||
| --- | ||
|
|
||
| # Bilateral Filter | ||
|
|
||
| This section introduces the **bilateral filter** used in the Img2Num project | ||
| (see [`bilateral_filter.h`](https://github.com/Ryan-Millard/Img2Num/blob/main/src/wasm/modules/image/include/bilateral_filter.h) | ||
| & [`bilateral_filter.cpp`](https://github.com/Ryan-Millard/Img2Num/blob/main/src/wasm/modules/image/src/bilateral_filter.cpp)). | ||
| It focuses on how the algorithm is implemented, why each step is necessary, | ||
| and where the corresponding code lives so you can jump straight into the implementation. | ||
|
|
||
| ## At a glance | ||
| - **Algorithm:** Bilateral Filter (Non-linear, edge-preserving). | ||
| - **Data type:** `uint8_t` (8-bit unsigned integer channels). | ||
| - **Key steps:** | ||
| 1. For each pixel, inspect neighbors in radius $R$. | ||
| 2. Weight neighbors by **spatial distance** (Gaussian). | ||
| 3. Weight neighbors by **intensity difference** (Gaussian). | ||
| 4. Normalize and average. | ||
|
|
||
| ## Pages in this mini-guide | ||
|
|
||
| * **Overview** (this page) | ||
| * **Implementation details** — step-by-step mapping between theory and the actual C++ code. | ||
| * **API & reference** — brief function signatures and purpose for quick lookup. | ||
|
|
||
| Jump to implementation: [Implementation details](../implementation/) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,21 @@ | ||
| #ifndef BILATERAL_FILTER_H | ||
| #define BILATERAL_FILTER_H | ||
|
|
||
| #include <cstddef> // for size_t | ||
| #include <cstdint> // for uint8_t | ||
|
|
||
| namespace bilateral { | ||
|
|
||
| // Apply bilateral filter to an image. | ||
| // The filter modifies the image buffer in-place. | ||
| // Parameters: | ||
| // - image: Pointer to RGBA pixel buffer | ||
| // - width, height: Image dimensions (px) | ||
| // - sigma_spatial: Gaussian standard deviation for spatial proximity (spatial decay) | ||
| // - sigma_range: Gaussian standard deviation for intensity difference (radiometric decay) | ||
| void bilateral_filter(uint8_t *image, size_t width, size_t height, | ||
| double sigma_spatial, double sigma_range); | ||
|
|
||
| } // namespace bilateral | ||
|
|
||
| #endif // BILATERAL_FILTER_H |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.