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