Skip to content

feat(bilateral filter): implement bilateral filter for denoising before K-Means in RGB & CIELAB - #191

Merged
Ryan-Millard merged 56 commits into
mainfrom
feat/kmean-preprocessing/bilateral-filter
Jan 6, 2026
Merged

feat(bilateral filter): implement bilateral filter for denoising before K-Means in RGB & CIELAB#191
Ryan-Millard merged 56 commits into
mainfrom
feat/kmean-preprocessing/bilateral-filter

Conversation

@Ryan-Millard

@Ryan-Millard Ryan-Millard commented Jan 3, 2026

Copy link
Copy Markdown
Owner

✨ Feature Pull Request

Proposal for new features or enhancements

📌 Description

  • What: Bilateral filter that accepts a flag that decides between CIELAB or RGB implementation
  • Why: Necessary to denoise images before K-Means - less aggressive denoising than Gaussian blur

🔗 Issue / PR

No issue, see this Reddit post.

Derived from #176 & #177

📦 Type of Change

  • New feature
  • Enhancement

🧪 How Has This Been Tested?

N/A - C++ has no tests yet.

Screenshots

Original

image

Processed

image

Left: CIELAB
Right: RGB

Testing this locally (manual test)

Copy the code below and paste it into src/components/WasmImageProcessor.jsx:

Click here to reveal the code

import { useEffect, useState, useId, useRef, useCallback, useMemo } from 'react';
import { Upload } from 'lucide-react';
import { loadImageToUint8Array, uint8ClampedArrayToSVG } from '@utils/image-utils';
import { useWasmWorker } from '@hooks/useWasmWorker';
import GlassCard from '@components/GlassCard';
import styles from './WasmImageProcessor.module.css';
import { useNavigate } from 'react-router-dom';
import LoadingHedgehog from '@components/LoadingHedgehog';
import Tooltip from '@components/Tooltip';

const WasmImageProcessor = () => {
  const navigate = useNavigate();
  const inputId = useId();
  const inputRef = useRef(null);

  const { bilateralFilter, blackThreshold, kmeans, mergeSmallRegionsInPlace } = useWasmWorker();

  const [originalSrc, setOriginalSrc] = useState(null);
  const [fileData, setFileData] = useState(null);
  const [isProcessing, setIsProcessing] = useState(false);
  const [progress, setProgress] = useState(0);
  const blurredCanvasRef = useRef(null);

  /* Cleanup object URLs on unmount or src change */
  useEffect(() => {
    return () => {
      if (originalSrc) URL.revokeObjectURL(originalSrc);
    };
  }, [originalSrc]);

  /* Stable loader for images */
  const loadOriginal = useCallback(async (file) => {
    if (!file) return;

    const url = URL.createObjectURL(file);
    setOriginalSrc(url);

    const { pixels, width, height } = await loadImageToUint8Array(file);
    setFileData({ pixels, width, height });
  }, []);

  /* Paste support */
  useEffect(() => {
    const handlePaste = (e) => {
      for (const item of e.clipboardData?.items || []) {
        if (item.type.startsWith('image/')) {
          loadOriginal(item.getAsFile());
          break;
        }
      }
    };

    document.addEventListener('paste', handlePaste);
    return () => document.removeEventListener('paste', handlePaste);
  }, [loadOriginal]);

  /* Drag & drop */
  const handleDrop = useCallback(
    (e) => {
      e.preventDefault();
      loadOriginal(e.dataTransfer.files[0]);
    },
    [loadOriginal]
  );

  const handleSelect = useCallback(
    (e) => loadOriginal(e.target.files[0]),
    [loadOriginal]
  );

  /* Hashed steps to keep pipeline aligned */
  const step = useCallback((p) => setProgress(p), []);

  /* Main pipeline */
  const processImage = useCallback(async () => {
  if (!fileData) return;

  setIsProcessing(true);
  step(5);

  try {
    const { width, height } = fileData;

    step(20);

    // Run filter in color_space = 1
    const filtered1 = await bilateralFilter({ ...fileData, color_space: 0 });

    // Run filter in color_space = 2
    const filtered2 = await bilateralFilter({ ...fileData, color_space: 1 });

    step(70);

    // Draw both results side by side on canvas
    if (blurredCanvasRef.current) {
      const ctx = blurredCanvasRef.current.getContext('2d');
      blurredCanvasRef.current.width = width * 2; // two images side by side
      blurredCanvasRef.current.height = height;

      const imageData1 = new ImageData(filtered1, width, height);
      const imageData2 = new ImageData(filtered2, width, height);

      ctx.putImageData(imageData1, 0, 0);
      ctx.putImageData(imageData2, width, 0); // draw second image to the right
    }

    step(100);
  } catch (err) {
    console.error(err);
  } finally {
    setTimeout(() => {
      setIsProcessing(false);
      step(0);
    }, 800);
  }
}, [fileData, bilateralFilter, step]);



  /* Memo'd UI fragments */
  const EmptyState = useMemo(
    () => (
      <>
        <Tooltip content="Upload an image from your device">
          <Upload className={`anchor-style ${styles.uploadIcon}`} />
        </Tooltip>

        <p className={`text-center ${styles.dragDropText}`}>
          Drag & Drop or{' '}
          <Tooltip content="Select an image file from your computer">
            <span className={`anchor-style ${styles.noTextWrap}`}>
              Choose File
            </span>
          </Tooltip>
        </p>
      </>
    ),
    []
  );

  const LoadedState = useMemo(() => {
  if (!originalSrc) return null;

  return (
    <>
      <img src={originalSrc} alt="Original" className={styles.preview} />

      <canvas
        ref={blurredCanvasRef}
        className={styles.preview} // reuse same styling
      />

      {!isProcessing ? (
        <Tooltip content="Process the image and convert it to numbers">
          <button
            className="uppercase button"
            onClick={(e) => {
              e.stopPropagation();
              processImage();
            }}
          >
            Ok
          </button>
        </Tooltip>
      ) : (
        <LoadingHedgehog
          progress={progress}
          text={`Processing – ${Math.round(progress)}%`}
        />
      )}
    </>
  );
}, [originalSrc, isProcessing, progress, processImage]);



  return (
    <GlassCard
      className={`flex-center flex-column ${styles.dropZone}`}
      onDrop={handleDrop}
      onDragOver={(e) => e.preventDefault()}
      onClick={() => {
        if (!originalSrc) inputRef.current?.click();
      }}
      data-image-loaded={!!originalSrc}
    >
      {originalSrc ? LoadedState : EmptyState}

      <input
        ref={inputRef}
        id={inputId}
        type="file"
        accept="image/*"
        hidden
        onChange={handleSelect}
      />
    </GlassCard>
  );
};

export default WasmImageProcessor;

Summary by CodeRabbit

  • New Features

    • App now applies a bilateral filter for edge-preserving smoothing (replacing prior Gaussian blur) with tunable spatial and range controls and selectable RGB vs CIELAB color modes.
  • Documentation

    • Added comprehensive docs: API, implementation notes, color-space guidance, examples, and visual comparisons.
  • Tests

    • Added tests covering the bilateral filter and its parameter handling.

✏️ Tip: You can customize this high-level summary in your review settings.

@coderabbitai

coderabbitai Bot commented Jan 3, 2026

Copy link
Copy Markdown
Contributor
📝 Walkthrough

Walkthrough

Adds a Bilateral Filter feature to the WASM image module: new C++ implementation and headers (RGB/CIELAB support and conversions), WASM worker hook and tests, React integration replacing Gaussian blur, k-means adjustments, docs, and a docs SVG visualization component.

Changes

Cohort / File(s) Summary
Bilateral Filter — C++ Core
src/wasm/modules/image/include/bilateral_filter.h, src/wasm/modules/image/src/bilateral_filter.cpp
New exported API and WASM-exported implementation applying an in-place RGBA bilateral filter with RGB LUT and CIELAB modes, spatial kernel, per-pixel accumulation, and alpha preservation.
CIELAB Utilities — C++
src/wasm/modules/image/include/cielab.h, src/wasm/modules/image/src/cielab.cpp
New rgb_to_lab / lab_to_rgb declarations and implementation for sRGB ↔ CIELAB conversions (D65, linearization/gamma).
JS Hook & Tests
src/hooks/useWasmWorker.js, src/hooks/useWasmWorker.test.js
Added bilateralFilter wrapper calling worker bilateral_filter (defaults: sigma_spatial=3.0, sigma_range=50.0, color_space=0); updated hook return shape and tests to assert messages and parameter overrides.
React Integration
src/components/WasmImageProcessor.jsx
Replaced gaussianBlur usage with bilateralFilter output; thresholding now consumes bilateral-filtered image.
K-means Adjustments
src/wasm/modules/image/include/kmeans.h, src/wasm/modules/image/src/kmeans.cpp
Added default spatial_weight = 1.0 and refactored clustering data to normalized RGBXY with 4-channel back-projection.
Documentation — Bilateral Filter
docs/docs/reference/wasm/modules/image/bilateral_filter/_category_.json, .../api.md, .../explained.md, .../implementation.md, .../overview.md, .../color-spaces.md, .../keywords.md
New category and comprehensive docs: API, algorithm explanation, implementation notes, color-space guidance, LUT vs on-the-fly strategies, sigma scaling, and keywords.
Documentation — CIELAB
docs/docs/reference/wasm/modules/image/cielab/_category_.json, .../api.md, .../implementation-explained.md
New CIELAB docs: category, API reference, and detailed conversion pipeline explanation.
Docs Visualization Component
docs/src/components/docs/reference/wasm/modules/image/bilateral_filter/RgbVsLabRangeKernel.jsx
New React SVG component visualizing RGB LUT vs CIELAB range kernels (default-exported).
Module Overview Update
docs/docs/reference/wasm/modules/image/overview.md
Image module overview updated to list bilateral_filter under PixelConverters.

Sequence Diagram(s)

sequenceDiagram
    actor User
    participant React as WasmImageProcessor
    participant Hook as useWasmWorker
    participant Worker as WebWorker
    participant WASM as bilateral_filter (WASM)

    User->>React: supply image + params
    React->>Hook: bilateralFilter(pixels,width,height,σs,σr,color_space)
    Hook->>Worker: postMessage({funcName: "bilateral_filter", args, bufferKeys:["pixels"]})
    Worker->>WASM: call bilateral_filter(image, width, height, σs, σr, color_space)

    alt RGB mode
        WASM->>WASM: build spatial kernel
        WASM->>WASM: precompute RGB range LUT
        WASM->>WASM: apply weighted sums using RGB distances
    else CIELAB mode
        WASM->>WASM: build spatial kernel
        WASM->>WASM: convert image RGB→Lab buffer
        WASM->>WASM: compute range weights on-the-fly (Lab distances)
    end

    WASM->>WASM: normalize & write RGBA output (preserve alpha)
    Worker->>Hook: postMessage({output:{pixels:filtered}})
    Hook->>React: return filtered pixels
    React->>React: pass filtered pixels to thresholding
    React->>User: display result
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

Suggested labels

C/C++

Poem

🐰 In kernels I hop, weighing color and space,
Edges kept safe while noise I erase.
Lab or RGB, I nibble with care,
WASM-speed whiskers and docs to share. ✨

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 41.18% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main change: implementing a bilateral filter for image denoising in two color spaces (RGB & CIELAB) before K-Means clustering.
✨ Finishing touches
  • 📝 Generate docstrings

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 7

🧹 Nitpick comments (4)
docs/docs/reference/wasm/modules/image/bilateral_filter/explained.md (2)

39-41: Non-standard LaTeX syntax: Remove exclamation marks after \exp.

The \exp! syntax is non-standard. Standard LaTeX uses \exp without the exclamation mark. While some renderers may handle it, it's better to use the conventional form.

🔎 Proposed fix
 $$
-w_{\text{spatial}}(d) = \exp!\left(-\frac{d^2}{2\sigma_s^2}\right),
+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)
+w_{\text{range}}(d) = \exp\left(-\frac{d^2}{2\sigma_r^2}\right)
 $$

54-54: Define MAX_RGB_DIST_SQ constant in documentation.

The constant MAX_RGB_DIST_SQ is referenced but not defined. For reader clarity, add a brief explanation (e.g., "where MAX_RGB_DIST_SQ = 255² × 3 = 195,075 represents the maximum squared Euclidean distance in RGB space").

docs/docs/reference/wasm/modules/image/bilateral_filter/implementation.md (2)

40-40: Define MAX_RGB_DIST_SQ constant in documentation.

Similar to the other documentation file, MAX_RGB_DIST_SQ is referenced here but not defined. Consider adding its value and meaning (e.g., 195,075, the maximum squared RGB distance).


17-21: Define SIGMA_RADIUS_FACTOR in the documentation for clarity.

The documentation references SIGMA_RADIUS_FACTOR (line 17) without defining it. While the radius calculation is correct (ceil(3.0 × 3.0) = 9 with the actual constant value being 3.0 from the implementation), readers should not need to consult the source code to understand the documentation. Either define the constant's value inline or replace it with the numerical value directly.

📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between f897186 and a4c3849.

📒 Files selected for processing (12)
  • docs/docs/reference/wasm/modules/image/bilateral_filter/_category_.json
  • docs/docs/reference/wasm/modules/image/bilateral_filter/api.md
  • docs/docs/reference/wasm/modules/image/bilateral_filter/explained.md
  • docs/docs/reference/wasm/modules/image/bilateral_filter/implementation.md
  • docs/docs/reference/wasm/modules/image/bilateral_filter/overview.md
  • docs/docs/reference/wasm/modules/image/overview.md
  • src/components/WasmImageProcessor.jsx
  • src/hooks/useWasmWorker.js
  • src/hooks/useWasmWorker.test.js
  • src/wasm/modules/image/include/bilateral_filter.h
  • src/wasm/modules/image/include/image_utils.h
  • src/wasm/modules/image/src/bilateral_filter.cpp
🧰 Additional context used
🧠 Learnings (3)
📓 Common learnings
Learnt from: fransafu
Repo: Ryan-Millard/Img2Num PR: 176
File: src/wasm/modules/image/src/bilateral_filter.cpp:98-101
Timestamp: 2025-12-31T17:46:54.476Z
Learning: In bilateral filter implementations, the center pixel always contributes a weight of exactly 1.0 to the weight accumulator (spatial weight = exp(0) = 1.0 and range weight = exp(0) = 1.0), which inherently prevents division by zero during normalization without requiring explicit guards.
📚 Learning: 2025-12-31T17:46:54.476Z
Learnt from: fransafu
Repo: Ryan-Millard/Img2Num PR: 176
File: src/wasm/modules/image/src/bilateral_filter.cpp:98-101
Timestamp: 2025-12-31T17:46:54.476Z
Learning: In bilateral filter implementations, the center pixel always contributes a weight of exactly 1.0 to the weight accumulator (spatial weight = exp(0) = 1.0 and range weight = exp(0) = 1.0), which inherently prevents division by zero during normalization without requiring explicit guards.

Applied to files:

  • src/wasm/modules/image/include/image_utils.h
  • docs/docs/reference/wasm/modules/image/bilateral_filter/implementation.md
  • src/wasm/modules/image/include/bilateral_filter.h
  • docs/docs/reference/wasm/modules/image/bilateral_filter/api.md
  • docs/docs/reference/wasm/modules/image/bilateral_filter/explained.md
  • docs/docs/reference/wasm/modules/image/bilateral_filter/overview.md
📚 Learning: 2025-12-31T17:46:54.476Z
Learnt from: fransafu
Repo: Ryan-Millard/Img2Num PR: 176
File: src/wasm/modules/image/src/bilateral_filter.cpp:98-101
Timestamp: 2025-12-31T17:46:54.476Z
Learning: In bilateral_filter.cpp (src/wasm/modules/image/src/bilateral_filter.cpp), rely on the fact that the center pixel contributes a weight of exactly 1.0 to both spatial and range components (exp(0) = 1) so the normalization sum cannot be zero. This implies you do not need explicit guards against division by zero for normalization in this implementation.

Applied to files:

  • src/wasm/modules/image/src/bilateral_filter.cpp
🧬 Code graph analysis (4)
src/wasm/modules/image/include/image_utils.h (1)
src/wasm/modules/image/src/bilateral_filter.cpp (4)
  • void (114-117)
  • bilateral_filter (28-109)
  • bilateral_filter (28-29)
  • bilateral_filter (114-115)
src/wasm/modules/image/include/bilateral_filter.h (1)
src/wasm/modules/image/src/bilateral_filter.cpp (3)
  • bilateral_filter (28-109)
  • bilateral_filter (28-29)
  • bilateral_filter (114-115)
src/hooks/useWasmWorker.test.js (3)
src/workers/wasmWorker.js (1)
  • result (42-42)
src/components/WasmImageProcessor.jsx (1)
  • useWasmWorker (16-16)
src/hooks/useWasmWorker.js (1)
  • useWasmWorker (4-53)
src/components/WasmImageProcessor.jsx (2)
src/hooks/useWasmWorker.js (5)
  • useWasmWorker (4-53)
  • bilateralFilter (38-40)
  • blackThreshold (41-43)
  • kmeans (44-46)
  • mergeSmallRegionsInPlace (47-50)
src/wasm/modules/image/src/mergeSmallRegionsInPlace.cpp (2)
  • mergeSmallRegionsInPlace (51-117)
  • mergeSmallRegionsInPlace (51-52)
🪛 LanguageTool
docs/docs/reference/wasm/modules/image/bilateral_filter/implementation.md

[style] ~13-~13: Consider placing the discourse marker ‘first’ at the beginning of the sentence for more clarity.
Context: ...ation. ## 1. Parameters & Window Size The filter first calculates the kernel size based on the...

(SENT_START_FIRST_PREMIUM)

🔇 Additional comments (13)
src/wasm/modules/image/include/image_utils.h (1)

24-25: LGTM!

The function declaration is correctly formed and consistent with the existing header structure. The signature matches the implementation in bilateral_filter.cpp.

docs/docs/reference/wasm/modules/image/overview.md (1)

28-28: LGTM!

The bilateral filter is appropriately integrated into the module overview documentation. The structure listing and description are clear and consistent with the existing documentation pattern.

Also applies to: 42-42

docs/docs/reference/wasm/modules/image/bilateral_filter/overview.md (1)

1-31: LGTM!

The bilateral filter overview documentation is well-structured and clearly written. The algorithm summary is concise, and the navigation structure helps readers find detailed information.

src/hooks/useWasmWorker.test.js (1)

207-258: LGTM! Comprehensive test coverage for bilateralFilter.

The test suite thoroughly validates:

  • The presence of bilateralFilter in the hook's return value
  • Correct funcName: 'bilateral_filter' in posted messages
  • Default parameters (sigma_spatial: 3.0, sigma_range: 50.0)
  • Custom parameter handling
  • Proper buffer key passing

The tests follow established patterns from other helper methods like gaussianBlur.

src/components/WasmImageProcessor.jsx (3)

16-16: LGTM! Clean integration of bilateralFilter.

The destructuring correctly replaces gaussianBlur with bilateralFilter from the useWasmWorker hook, aligning with the updated processing pipeline.


84-96: LGTM! Bilateral filter integration preserves edge information.

The switch from Gaussian blur to bilateral filter is well-motivated (the comment on line 84 explains why Gaussian blur was problematic). The bilateral filter output correctly feeds into the thresholding step.

The processing flow is now:

  1. fileData.pixels → bilateral filter
  2. imgBilateralFiltered → black threshold
  3. Continue to k-means and region merging

This provides edge-preserving denoising before clustering, which should improve the quality of the final output.


149-149: LGTM! Dependency array correctly updated.

The effect dependency array now references bilateralFilter instead of gaussianBlur, ensuring the effect re-runs correctly when the hook reference changes.

src/hooks/useWasmWorker.js (1)

52-52: LGTM!

The function is correctly added to the hook's public API alongside other helper methods.

src/wasm/modules/image/src/bilateral_filter.cpp (5)

1-11: LGTM!

Includes are appropriate and the namespace organization is clean.


33-36: LGTM!

Kernel radius calculation and clamping logic are correct, and the result buffer allocation is appropriate for RGBA images.


38-48: LGTM!

The spatial Gaussian kernel precomputation is correct and efficient. The formula and indexing are both accurate.


50-56: LGTM!

The range weight LUT precomputation is an effective optimization. The formula is correct and the table size (~1.5 MB) is reasonable.


108-117: LGTM!

The result copy back to the input buffer and the WASM export wrapper are both correctly implemented.

Comment thread docs/docs/reference/wasm/modules/image/bilateral_filter/api.md Outdated
Comment thread src/hooks/useWasmWorker.js Outdated
Comment thread src/wasm/modules/image/include/bilateral_filter.h
Comment thread src/wasm/modules/image/src/bilateral_filter.cpp Outdated
Comment thread src/wasm/modules/image/src/bilateral_filter.cpp Outdated
Comment thread src/wasm/modules/image/src/bilateral_filter.cpp Outdated
Comment thread src/wasm/modules/image/src/bilateral_filter.cpp Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 9

🤖 Fix all issues with AI Agents
In @docs/docs/reference/wasm/modules/image/bilateral_filter/color-spaces.md:
- Line 1: The file
docs/docs/reference/wasm/modules/image/bilateral_filter/color-spaces.md has
Prettier formatting issues; run Prettier to reformat it (e.g., execute prettier
--write docs/docs/reference/wasm/modules/image/bilateral_filter/color-spaces.md
or use your editor’s Prettier formatter), verify the file is reformatted, and
commit the updated file to fix the CI failure.

In @docs/docs/reference/wasm/modules/image/bilateral_filter/explained.md:
- Line 1: There are Prettier formatting errors in the markdown file; run the
formatter on the file (prettier --write
docs/docs/reference/wasm/modules/image/bilateral_filter/explained.md), stage and
commit the updated file, and push the commit so the CI pipeline can re-run and
pass.
- Around line 85-92: The fenced code block containing the C++ snippet (lines
using dL, dA, dB, cie_image, neighbor_idx, L0, A0, B0, dist, gaussian,
sigma_range) lacks a language identifier; update the opening fence from ``` to
```cpp (or ```c++) so the block is marked as C++ to enable proper syntax
highlighting. Ensure only the opening fence is changed and the rest of the
snippet (dL = cie_image[neighbor_idx] - L0; etc.) remains unchanged.

In @docs/docs/reference/wasm/modules/image/bilateral_filter/implementation.md:
- Around line 94-95: The LaTeX formula contains a stray exclamation mark:
replace the occurrence of "\exp!" with the correct "\exp" in the formula (e.g.,
change "$\exp!\left(-\frac{x^2}{2\sigma_{spatial}^2}\right)$" to
"$\exp\left(-\frac{x^2}{2\sigma_{spatial}^2}\right)$") so the expression renders
correctly.
- Around line 175-177: Replace the mistaken LaTeX command `\exp!` with the
correct `\exp` in the inline formula used in the sentence "During filtering, we
simply **look up the weight**..." so the expression reads
`\exp\!\left(-\frac{x^2}{2\sigma_{spatial}^2}\right)` (or simply
`\exp\left(-\frac{x^2}{2\sigma_{spatial}^2}\right)`) to remove the stray
exclamation mark; update the text containing the LaTeX snippet
`\exp!(-\frac{x^2}{2\sigma_{spatial}^2})` accordingly.

In @docs/docs/reference/wasm/modules/image/bilateral_filter/keywords.md:
- Line 1: Run Prettier to fix formatting for the markdown file that failed CI by
executing the suggested command (prettier --write
docs/docs/reference/wasm/modules/image/bilateral_filter/keywords.md), then stage
and commit the resulting changes so the pipeline will pass; ensure you use the
repository’s Prettier config (or npm/yarn script) so formatting matches CI
settings.

In @docs/docs/reference/wasm/modules/image/cielab/api.md:
- Line 1: Run Prettier to fix formatting in the failing markdown by running the
formatter on the file docs/docs/reference/wasm/modules/image/cielab/api.md
(e.g., `prettier --write docs/docs/reference/wasm/modules/image/cielab/api.md`),
then stage and commit the changes so the CI pipeline passes.

In @src/wasm/modules/image/CMakeLists.txt:
- Line 52: The current target_compile_options call forces -O3 for all builds
which breaks Debug debuggability; update the call for ${MODULE_NAME}_wasm to set
optimization per-configuration (e.g. use generator expressions) so Debug uses
-O0 and preserves debug info while Release keeps -O3; for example replace the
single flag list with config-specific flags via
target_compile_options(${MODULE_NAME}_wasm PRIVATE $<$<CONFIG:Debug>:-O0 -g4>
$<$<CONFIG:Release>:-O3 -g4> -ffast-math) or equivalent generator-expression
variants.
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 4425cce and 7baacc8.

📒 Files selected for processing (14)
  • docs/docs/reference/wasm/modules/image/bilateral_filter/api.md
  • docs/docs/reference/wasm/modules/image/bilateral_filter/color-spaces.md
  • docs/docs/reference/wasm/modules/image/bilateral_filter/explained.md
  • docs/docs/reference/wasm/modules/image/bilateral_filter/implementation.md
  • docs/docs/reference/wasm/modules/image/bilateral_filter/keywords.md
  • docs/docs/reference/wasm/modules/image/bilateral_filter/overview.md
  • docs/docs/reference/wasm/modules/image/cielab/_category_.json
  • docs/docs/reference/wasm/modules/image/cielab/api.md
  • docs/docs/reference/wasm/modules/image/cielab/index.md
  • docs/src/components/docs/reference/wasm/modules/image/bilateral_filter/RgbVsLabRangeKernel.jsx
  • src/wasm/modules/image/CMakeLists.txt
  • src/wasm/modules/image/include/cielab.h
  • src/wasm/modules/image/src/bilateral_filter.cpp
  • src/wasm/modules/image/src/cielab.cpp
✅ Files skipped from review due to trivial changes (2)
  • docs/docs/reference/wasm/modules/image/cielab/index.md
  • docs/docs/reference/wasm/modules/image/cielab/category.json
🚧 Files skipped from review as they are similar to previous changes (4)
  • docs/docs/reference/wasm/modules/image/bilateral_filter/api.md
  • src/wasm/modules/image/src/bilateral_filter.cpp
  • docs/docs/reference/wasm/modules/image/bilateral_filter/overview.md
  • src/wasm/modules/image/include/cielab.h
🧰 Additional context used
🧠 Learnings (3)
📚 Learning: 2026-01-04T15:34:04.654Z
Learnt from: Ryan-Millard
Repo: Ryan-Millard/Img2Num PR: 146
File: docs/docs/reference/react/components/ThemeSwitch/tests.md:123-124
Timestamp: 2026-01-04T15:34:04.654Z
Learning: In Docusaurus documentation, relative directory links (e.g., ../ or ../../../hooks/useTheme) resolve to index.md within the target directory. Do not require or force linking to index.md explicitly; such relative links remain valid and should not be changed when reviewing docs under the docs directory.

Applied to files:

  • docs/docs/reference/wasm/modules/image/cielab/api.md
  • docs/docs/reference/wasm/modules/image/bilateral_filter/color-spaces.md
  • docs/docs/reference/wasm/modules/image/bilateral_filter/keywords.md
  • docs/docs/reference/wasm/modules/image/bilateral_filter/explained.md
  • docs/docs/reference/wasm/modules/image/bilateral_filter/implementation.md
📚 Learning: 2025-12-31T17:46:54.476Z
Learnt from: fransafu
Repo: Ryan-Millard/Img2Num PR: 176
File: src/wasm/modules/image/src/bilateral_filter.cpp:98-101
Timestamp: 2025-12-31T17:46:54.476Z
Learning: In bilateral filter implementations, the center pixel always contributes a weight of exactly 1.0 to the weight accumulator (spatial weight = exp(0) = 1.0 and range weight = exp(0) = 1.0), which inherently prevents division by zero during normalization without requiring explicit guards.

Applied to files:

  • docs/docs/reference/wasm/modules/image/bilateral_filter/keywords.md
  • docs/docs/reference/wasm/modules/image/bilateral_filter/explained.md
  • docs/docs/reference/wasm/modules/image/bilateral_filter/implementation.md
📚 Learning: 2025-12-31T17:46:54.476Z
Learnt from: fransafu
Repo: Ryan-Millard/Img2Num PR: 176
File: src/wasm/modules/image/src/bilateral_filter.cpp:98-101
Timestamp: 2025-12-31T17:46:54.476Z
Learning: In bilateral_filter.cpp (src/wasm/modules/image/src/bilateral_filter.cpp), rely on the fact that the center pixel contributes a weight of exactly 1.0 to both spatial and range components (exp(0) = 1) so the normalization sum cannot be zero. This implies you do not need explicit guards against division by zero for normalization in this implementation.

Applied to files:

  • src/wasm/modules/image/src/cielab.cpp
🪛 GitHub Actions: CI
docs/src/components/docs/reference/wasm/modules/image/bilateral_filter/RgbVsLabRangeKernel.jsx

[error] 1-1: Prettier formatting check failed. Run 'prettier --write' to fix code style issues in this file.

docs/docs/reference/wasm/modules/image/cielab/api.md

[error] 1-1: Prettier formatting check failed. Run 'prettier --write' to fix code style issues in this file.

docs/docs/reference/wasm/modules/image/bilateral_filter/color-spaces.md

[error] 1-1: Prettier formatting check failed. Run 'prettier --write' to fix code style issues in this file.

docs/docs/reference/wasm/modules/image/bilateral_filter/keywords.md

[error] 1-1: Prettier formatting check failed. Run 'prettier --write' to fix code style issues in this file.

docs/docs/reference/wasm/modules/image/bilateral_filter/explained.md

[error] 1-1: Prettier formatting check failed. Run 'prettier --write' to fix code style issues in this file.

docs/docs/reference/wasm/modules/image/bilateral_filter/implementation.md

[error] 1-1: Prettier formatting check failed. Run 'prettier --write' to fix code style issues in this file.

🪛 LanguageTool
docs/docs/reference/wasm/modules/image/bilateral_filter/implementation.md

[style] ~17-~17: Consider placing the discourse marker ‘first’ at the beginning of the sentence for more clarity.
Context: ...d. ::: ## 1. Parameters & Kernel Size The filter first calculates the kernel size based on the...

(SENT_START_FIRST_PREMIUM)

🪛 markdownlint-cli2 (0.18.1)
docs/docs/reference/wasm/modules/image/bilateral_filter/explained.md

85-85: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

docs/docs/reference/wasm/modules/image/bilateral_filter/implementation.md

53-53: Code block style
Expected: fenced; Actual: indented

(MD046, code-block-style)

🔇 Additional comments (13)
docs/docs/reference/wasm/modules/image/bilateral_filter/color-spaces.md (1)

136-138: The LUT memory size claim has been verified and is accurate. The implementation uses std::vector<double> range_lut with MAX_RGB_DIST_SQ + 1 elements (195,076 total). This calculates to 195,076 × 8 bytes = 1,560,608 bytes ≈ 1.49 MB, which matches the documentation's claim of "~1.5 MB". No changes needed.

docs/src/components/docs/reference/wasm/modules/image/bilateral_filter/RgbVsLabRangeKernel.jsx (2)

1-60: Run Prettier to fix formatting.

The CI pipeline reports a Prettier formatting failure for this file. Run prettier --write on this file to resolve the style issues before merging.


13-41: LGTM!

The Gaussian weight calculation and SVG path generation logic is correct. The visualization accurately represents the decay curves for both RGB (σ=20) and CIELAB (σ=15) range kernels.

docs/docs/reference/wasm/modules/image/bilateral_filter/implementation.md (3)

1-214: Run Prettier to fix formatting.

The CI pipeline reports a Prettier formatting failure for this file. Run prettier --write to resolve the style issues before merging.


27-77: LGTM!

The embedded SVG visualization correctly illustrates the 19×19 kernel with the center pixel highlighted and radius labels. The grid generation logic is accurate.


83-158: LGTM!

The documentation accurately explains the weight computation strategies for both RGB (LUT-based) and CIELAB (on-the-fly) approaches. The rationale for why CIELAB cannot use a precomputed LUT is well-articulated.

src/wasm/modules/image/src/cielab.cpp (7)

5-48: LGTM!

The color space conversion constants are correctly defined for sRGB with D65 illuminant. The matrix coefficients and LAB factors match the standard specifications.


50-55: LGTM!

The xyz_to_lab helper correctly implements the standard CIE f(t) function with appropriate handling for negative floating-point errors.


57-63: LGTM!

The srgb_to_linear function correctly implements the standard sRGB inverse gamma correction with proper thresholding.


65-94: LGTM!

The rgb_to_lab function correctly implements the standard RGB→XYZ→Lab conversion pipeline. The decision to clamp only L* while leaving a* and b* unclamped is appropriate for maintaining precision in the bilateral filter context.


96-99: LGTM!

The inverse_xyz_to_lab function correctly implements the inverse of the CIE f(t) function using the appropriate threshold (δ rather than δ³) for the transformed domain.


101-105: LGTM!

The gamma_encode function correctly implements sRGB gamma encoding with the proper threshold derived from the linearization constants.


107-133: LGTM!

The lab_to_rgb function correctly implements the Lab→XYZ→RGB conversion pipeline. The use of std::round before casting ensures proper rounding to the nearest integer, and the final clamp handles out-of-gamut colors appropriately.

Comment thread docs/docs/reference/wasm/modules/image/bilateral_filter/explained.md Outdated
Comment thread docs/docs/reference/wasm/modules/image/bilateral_filter/implementation.md Outdated
Comment thread docs/docs/reference/wasm/modules/image/bilateral_filter/keywords.md
Comment thread docs/docs/reference/wasm/modules/image/cielab/api.md
Comment thread src/wasm/modules/image/CMakeLists.txt Outdated
@Ryan-Millard

Copy link
Copy Markdown
Owner Author

One more thing left before merging this: We need to document the difference between sigma_range in the color spaces and recommend a value.

That should go inside api.md for the bilateral filter.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Fix all issues with AI Agents
In @src/wasm/modules/image/src/cielab.cpp:
- Around line 31-39: The XYZ→RGB inverse matrix constants (SRGB_X_TO_R,
SRGB_Y_TO_R, SRGB_Z_TO_R, SRGB_X_TO_G, SRGB_Y_TO_G, SRGB_Z_TO_G, SRGB_X_TO_B,
SRGB_Y_TO_B, SRGB_Z_TO_B) use only 4 decimal places; update these constants to
the higher-precision ITU-R BT.709 / sRGB inverse values (R: 3.240970, −1.537383,
−0.498611; G: −0.969244, 1.875968, 0.041555; B: 0.055630, −0.203977, 1.056972)
so the precision matches the forward sRGB→XYZ matrix and reduces rounding drift
in color conversions.

In @src/wasm/modules/image/src/kmeans.cpp:
- Around line 128-134: The designated-initializer syntax in kmeans.cpp (the
RGBXY initializer used when assigning pixels[idx]) requires C++20 but the
project uses C++17; replace the designated initializer with a C++17-compatible
aggregate initializer that follows the RGBXY field order (e.g., RGBXY{ r_value,
g_value, b_value, x_value, y_value }) using the same static_cast expressions, or
alternatively update the project CMake CMAKE_CXX_STANDARD to 20 if you
intentionally want C++20 features across the repo.
🧹 Nitpick comments (3)
src/components/WasmImageProcessor.jsx (1)

81-86: Consider exposing bilateral filter parameters for fine-tuning.

The bilateral filter is called with default parameters (sigma_spatial = 3.0, sigma_range = 50.0, color_space = 0). Since the PR objectives mention the need to document sigma_range differences between color spaces and there are known performance/correctness trade-offs between RGB and CIELAB modes, consider exposing these parameters through UI controls in a future iteration to allow users to optimize the filter for different image types.

Note: The comment on line 81 effectively explains why Gaussian blur was removed. You might consider rephrasing to make it clearer this is a historical note: "NOTE: Bilateral filter is used instead of Gaussian blur, which would destroy sharp outlines and prevent edge preservation."

src/wasm/modules/image/src/bilateral_filter.cpp (1)

47-56: Input validation is correct, but consider warning users when kernel size is clamped.

The validation logic correctly handles invalid inputs and the clamping strategy at line 55 effectively limits kernel size. However, when sigma_spatial produces a radius exceeding MAX_KERNEL_RADIUS, the filter silently clamps without notification.

Consider adding a debug warning or returning an error code when clamping occurs, so users understand their requested sigma_spatial was reduced. This would improve developer experience when tuning parameters.

Based on past review discussion about upper-bound validation.

src/wasm/modules/image/src/cielab.cpp (1)

101-103: Consider using inline instead of constexpr for clarity.

While constexpr is technically valid here, inverse_xyz_to_lab is not used in constant expressions. Using inline would more clearly express the intent of avoiding call overhead without implying compile-time evaluation.

🔎 Proposed change
-constexpr double inverse_xyz_to_lab(double t) {
+inline double inverse_xyz_to_lab(double t) {
   return (t > DELTA) ? (t * t * t) : (3 * DELTA * DELTA * (t - EPSILON));
 }
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 7baacc8 and 9538f6e.

📒 Files selected for processing (18)
  • docs/docs/reference/wasm/modules/image/bilateral_filter/_category_.json
  • docs/docs/reference/wasm/modules/image/bilateral_filter/api.md
  • docs/docs/reference/wasm/modules/image/bilateral_filter/color-spaces.md
  • docs/docs/reference/wasm/modules/image/bilateral_filter/explained.md
  • docs/docs/reference/wasm/modules/image/bilateral_filter/implementation.md
  • docs/docs/reference/wasm/modules/image/bilateral_filter/keywords.md
  • docs/docs/reference/wasm/modules/image/bilateral_filter/overview.md
  • docs/docs/reference/wasm/modules/image/cielab/api.md
  • docs/docs/reference/wasm/modules/image/cielab/index.md
  • docs/docs/reference/wasm/modules/image/overview.md
  • docs/src/components/docs/reference/wasm/modules/image/bilateral_filter/RgbVsLabRangeKernel.jsx
  • src/components/WasmImageProcessor.jsx
  • src/hooks/useWasmWorker.js
  • src/wasm/modules/image/include/bilateral_filter.h
  • src/wasm/modules/image/include/cielab.h
  • src/wasm/modules/image/src/bilateral_filter.cpp
  • src/wasm/modules/image/src/cielab.cpp
  • src/wasm/modules/image/src/kmeans.cpp
🚧 Files skipped from review as they are similar to previous changes (7)
  • docs/docs/reference/wasm/modules/image/overview.md
  • docs/docs/reference/wasm/modules/image/bilateral_filter/api.md
  • docs/src/components/docs/reference/wasm/modules/image/bilateral_filter/RgbVsLabRangeKernel.jsx
  • docs/docs/reference/wasm/modules/image/cielab/index.md
  • docs/docs/reference/wasm/modules/image/bilateral_filter/color-spaces.md
  • src/wasm/modules/image/include/bilateral_filter.h
  • docs/docs/reference/wasm/modules/image/bilateral_filter/overview.md
🧰 Additional context used
🧠 Learnings (4)
📓 Common learnings
Learnt from: fransafu
Repo: Ryan-Millard/Img2Num PR: 176
File: src/wasm/modules/image/src/bilateral_filter.cpp:98-101
Timestamp: 2025-12-31T17:46:54.476Z
Learning: In bilateral filter implementations, the center pixel always contributes a weight of exactly 1.0 to the weight accumulator (spatial weight = exp(0) = 1.0 and range weight = exp(0) = 1.0), which inherently prevents division by zero during normalization without requiring explicit guards.
📚 Learning: 2025-12-31T17:46:54.476Z
Learnt from: fransafu
Repo: Ryan-Millard/Img2Num PR: 176
File: src/wasm/modules/image/src/bilateral_filter.cpp:98-101
Timestamp: 2025-12-31T17:46:54.476Z
Learning: In bilateral_filter.cpp (src/wasm/modules/image/src/bilateral_filter.cpp), rely on the fact that the center pixel contributes a weight of exactly 1.0 to both spatial and range components (exp(0) = 1) so the normalization sum cannot be zero. This implies you do not need explicit guards against division by zero for normalization in this implementation.

Applied to files:

  • src/wasm/modules/image/src/kmeans.cpp
  • src/wasm/modules/image/src/cielab.cpp
  • src/wasm/modules/image/src/bilateral_filter.cpp
📚 Learning: 2025-12-31T17:46:54.476Z
Learnt from: fransafu
Repo: Ryan-Millard/Img2Num PR: 176
File: src/wasm/modules/image/src/bilateral_filter.cpp:98-101
Timestamp: 2025-12-31T17:46:54.476Z
Learning: In bilateral filter implementations, the center pixel always contributes a weight of exactly 1.0 to the weight accumulator (spatial weight = exp(0) = 1.0 and range weight = exp(0) = 1.0), which inherently prevents division by zero during normalization without requiring explicit guards.

Applied to files:

  • docs/docs/reference/wasm/modules/image/bilateral_filter/explained.md
  • docs/docs/reference/wasm/modules/image/bilateral_filter/keywords.md
  • docs/docs/reference/wasm/modules/image/bilateral_filter/implementation.md
📚 Learning: 2026-01-04T15:34:04.654Z
Learnt from: Ryan-Millard
Repo: Ryan-Millard/Img2Num PR: 146
File: docs/docs/reference/react/components/ThemeSwitch/tests.md:123-124
Timestamp: 2026-01-04T15:34:04.654Z
Learning: In Docusaurus documentation, relative directory links (e.g., ../ or ../../../hooks/useTheme) resolve to index.md within the target directory. Do not require or force linking to index.md explicitly; such relative links remain valid and should not be changed when reviewing docs under the docs directory.

Applied to files:

  • docs/docs/reference/wasm/modules/image/bilateral_filter/explained.md
  • docs/docs/reference/wasm/modules/image/bilateral_filter/keywords.md
  • docs/docs/reference/wasm/modules/image/cielab/api.md
  • docs/docs/reference/wasm/modules/image/bilateral_filter/implementation.md
🧬 Code graph analysis (3)
src/components/WasmImageProcessor.jsx (1)
src/hooks/useWasmWorker.js (5)
  • useWasmWorker (4-62)
  • bilateralFilter (38-49)
  • blackThreshold (50-52)
  • kmeans (53-55)
  • mergeSmallRegionsInPlace (56-59)
src/wasm/modules/image/include/cielab.h (1)
src/wasm/modules/image/src/cielab.cpp (4)
  • rgb_to_lab (70-99)
  • rgb_to_lab (70-71)
  • lab_to_rgb (112-137)
  • lab_to_rgb (112-113)
src/wasm/modules/image/src/bilateral_filter.cpp (1)
src/wasm/modules/image/src/cielab.cpp (4)
  • rgb_to_lab (70-99)
  • rgb_to_lab (70-71)
  • lab_to_rgb (112-137)
  • lab_to_rgb (112-113)
🪛 LanguageTool
docs/docs/reference/wasm/modules/image/bilateral_filter/implementation.md

[style] ~17-~17: Consider placing the discourse marker ‘first’ at the beginning of the sentence for more clarity.
Context: ...d. ::: ## 1. Parameters & Kernel Size The filter first calculates the kernel size based on the...

(SENT_START_FIRST_PREMIUM)

🪛 markdownlint-cli2 (0.18.1)
docs/docs/reference/wasm/modules/image/bilateral_filter/implementation.md

53-53: Code block style
Expected: fenced; Actual: indented

(MD046, code-block-style)

🔇 Additional comments (19)
src/wasm/modules/image/include/cielab.h (1)

1-11: LGTM! Clean header with proper guards.

The header follows best practices with include guards, proper includes, and clear function declarations. All previously identified issues (include guards, inline requirements, missing <cstdint>) have been addressed.

docs/docs/reference/wasm/modules/image/bilateral_filter/explained.md (1)

1-118: LGTM! Comprehensive and accurate documentation.

The documentation thoroughly explains the bilateral filter implementation, including:

  • Clear mathematical foundations with proper notation
  • Detailed explanation of spatial and range weighting
  • LUT optimization strategy for RGB mode
  • On-the-fly computation approach for CIELAB mode
  • Complexity analysis and performance considerations

All previously flagged formatting issues have been resolved.

docs/docs/reference/wasm/modules/image/cielab/api.md (1)

1-218: LGTM! Excellent API documentation.

This is thorough and well-structured documentation covering:

  • Complete function signatures and parameter descriptions
  • Detailed transformation pipelines for both directions
  • Important out-of-gamut handling warnings
  • Practical usage examples
  • Technical specifications with proper standards references
  • Performance considerations and optimization guidance
  • Proper cross-references to related documentation

All previously flagged formatting issues have been resolved.

docs/docs/reference/wasm/modules/image/bilateral_filter/_category_.json (1)

1-10: LGTM! Standard category configuration.

The category file follows Docusaurus conventions with appropriate label, position, and generated-index configuration.

src/components/WasmImageProcessor.jsx (2)

16-16: LGTM! Correct hook destructuring.

The component properly destructures bilateralFilter from useWasmWorker to replace the previous gaussianBlur implementation.


141-141: LGTM! Correct dependency update.

The effect dependency array properly includes bilateralFilter instead of the removed gaussianBlur.

docs/docs/reference/wasm/modules/image/bilateral_filter/keywords.md (1)

1-27: LGTM!

The keyword definitions are clear, accurate, and well-structured. The documentation appropriately covers all key bilateral filter concepts with correct mathematical notation and explanations.

src/wasm/modules/image/src/kmeans.cpp (1)

193-199: LGTM!

The rescaling logic correctly converts normalized centroid values [0,1] back to 8-bit RGB [0,255]. The added comment clarifies the intent, and the implementation is consistent with the normalized input data at lines 128-134.

src/hooks/useWasmWorker.js (2)

38-49: Implementation is correct; consider documenting the default parameter choices.

The bilateralFilter function is correctly implemented with proper async/await handling and buffer transfer. The sigma_spatial = 3.0 default aligns with implementation constraints.

However, the sigma_range = 50.0 default is notably higher than typical industry defaults (HALCON: 20.0, mild denoising: 5-30) and may produce stronger smoothing than expected. Based on the PR objectives, you've noted the need to document sigma_range differences between color spaces—consider including guidance on these defaults in that documentation.

Based on past review feedback and the PR objective to document sigma_range differences.


61-61: LGTM!

The bilateralFilter function is correctly added to the return object alongside existing functions.

src/wasm/modules/image/src/bilateral_filter.cpp (3)

82-106: LGTM! Efficient CIELAB preprocessing strategy.

The full-image RGB→CIELAB conversion before filtering is an excellent optimization. Converting once upfront (rather than repeatedly during kernel loops) significantly reduces the computational cost, as noted in PR #192 discussions.

Based on PR comments about CIELAB optimization.


137-186: LGTM! Core filtering logic is correct for both color spaces.

The implementation correctly:

  • Iterates over the kernel with proper boundary clamping
  • Uses precomputed LUT for RGB range weights (efficient)
  • Computes CIELAB range weights on-the-fly (necessary due to continuous LAB space)
  • Accumulates weighted sums for all channels

The normalization is safe—the center pixel always contributes weight 1.0 (spatial and range distances = 0), preventing division by zero.

Based on retrieved learnings about center pixel weight.


188-216: LGTM! Normalization and color space conversion are correctly implemented.

Both color space paths properly:

  • Normalize accumulated values by total weight
  • RGB: Clamp to valid [0,255] range
  • CIELAB: Convert LAB→RGB using the appropriate conversion function
  • Preserve alpha channel
  • Copy results back to input buffer
docs/docs/reference/wasm/modules/image/bilateral_filter/implementation.md (1)

1-219: LGTM! Comprehensive and accurate implementation documentation.

The documentation thoroughly explains:

  • Parameter-driven kernel sizing with helpful visualization
  • Weight precomputation strategies for both color spaces
  • RGB LUT vs CIELAB on-the-fly computation trade-offs
  • Complete filtering and normalization workflow

All technical details accurately reflect the implementation in bilateral_filter.cpp. The embedded code snippets, mathematical notation, and visual aids effectively communicate the algorithm.

src/wasm/modules/image/src/cielab.cpp (5)

52-57: LGTM: Proper defensive handling of floating-point errors.

The std::max(0.0, t) guard prevents negative values from tiny floating-point errors, and the piecewise formula correctly implements the CIE Lab transformation function.


61-68: LGTM: Correct sRGB gamma correction implementation.

The function properly clamps input to [0.0, 1.0] and implements the standard sRGB to linear RGB transformation with correct threshold and exponent values.


105-110: LGTM: Correct sRGB gamma encoding implementation.

The threshold and formula correctly implement the sRGB gamma encoding specification. The lack of input clamping is acceptable here since the final RGB values are clamped in lab_to_rgb before conversion to 8-bit.


112-137: LGTM: Proper Lab to RGB conversion with appropriate clamping and rounding.

The function correctly implements the Lab→XYZ→linear RGB→sRGB→8-bit pipeline with proper clamping at the final stage and std::round for accurate conversion to uint8_t. The implementation handles out-of-gamut colors gracefully by clamping RGB to [0, 1] before quantization.


70-99: Add clamping for a and b values to match L* normalization.**

While L* is correctly clamped to [0, 100] on line 98, the a* and b* values remain unclamped. This creates an asymmetry in the bilateral filter implementation: the RGB mode operates on bounded values [0, 255], while the CIELAB mode uses unbounded a* and b* values for distance calculations (bilateral_filter.cpp, line 174). This inconsistency can affect the range weighting behavior between the two color spaces.

For sRGB inputs, a* and b* values are theoretically unbounded but should be constrained. Standard CIELAB implementations commonly clamp a* and b* to approximately [-128, 127] or [-100, 100] to ensure consistent behavior across different processing pipelines.

Consider clamping both a* and b* after their computation to maintain consistency with L* normalization and align with bilateral filter expectations.

Comment thread src/wasm/modules/image/src/cielab.cpp Outdated
Comment thread src/wasm/modules/image/src/kmeans.cpp
- Add Mermaid diagrams for RGB → Lab and Lab → RGB
- Update RGB↔XYZ and XYZ↔RGB matrices to high-precision Khronos/W3C values
- Add explanatory comments for matrices in cielab.cpp

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Fix all issues with AI Agents
In @src/wasm/modules/image/src/cielab.cpp:
- Around line 43-57: There is a clang-format style violation in the CIE XYZ →
sRGB comment block in cielab.cpp; fix it by running the formatter on that file
(e.g., run `clang-format -i src/wasm/modules/image/src/cielab.cpp`) or, if
caching causes issues, run `./img2num clean-wasm` then re-run clang-format,
ensuring the comment block and surrounding code are reformatted to satisfy
clang-format.
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 9538f6e and c89d3ca.

📒 Files selected for processing (2)
  • docs/docs/reference/wasm/modules/image/cielab/index.md
  • src/wasm/modules/image/src/cielab.cpp
🧰 Additional context used
🧠 Learnings (5)
📓 Common learnings
Learnt from: fransafu
Repo: Ryan-Millard/Img2Num PR: 176
File: src/wasm/modules/image/src/bilateral_filter.cpp:98-101
Timestamp: 2025-12-31T17:46:54.476Z
Learning: In bilateral filter implementations, the center pixel always contributes a weight of exactly 1.0 to the weight accumulator (spatial weight = exp(0) = 1.0 and range weight = exp(0) = 1.0), which inherently prevents division by zero during normalization without requiring explicit guards.
📚 Learning: 2026-01-06T05:10:03.461Z
Learnt from: Ryan-Millard
Repo: Ryan-Millard/Img2Num PR: 191
File: src/wasm/modules/image/src/cielab.cpp:31-39
Timestamp: 2026-01-06T05:10:03.461Z
Learning: When reviewing color space transformation code in C++ (specifically in src/wasm/modules/image/src/*.cpp), flag and require checks for precision inconsistencies between forward and inverse matrix coefficients. Ensure that round-trip conversions do not accumulate unacceptable rounding errors by comparing forward and inverse results within a defined numerical tolerance, documenting tolerances, and adding tests that verify symmetry (forward then inverse).

Applied to files:

  • src/wasm/modules/image/src/cielab.cpp
📚 Learning: 2025-12-31T17:46:54.476Z
Learnt from: fransafu
Repo: Ryan-Millard/Img2Num PR: 176
File: src/wasm/modules/image/src/bilateral_filter.cpp:98-101
Timestamp: 2025-12-31T17:46:54.476Z
Learning: In bilateral_filter.cpp (src/wasm/modules/image/src/bilateral_filter.cpp), rely on the fact that the center pixel contributes a weight of exactly 1.0 to both spatial and range components (exp(0) = 1) so the normalization sum cannot be zero. This implies you do not need explicit guards against division by zero for normalization in this implementation.

Applied to files:

  • src/wasm/modules/image/src/cielab.cpp
📚 Learning: 2026-01-06T04:56:57.269Z
Learnt from: Ryan-Millard
Repo: Ryan-Millard/Img2Num PR: 191
File: src/wasm/modules/image/src/kmeans.cpp:128-134
Timestamp: 2026-01-06T04:56:57.269Z
Learning: In C++ sources compiled for WASM with Emscripten/Clang, designated initializers (e.g., RGBXY{.r = ..., .g = ...}) are allowed as a C++17 extension. When reviewing code that relies on designated initializers, verify that the target toolchain enables CXX_STANDARD 17 or higher and that the build system (CMake/emsdk) uses Emscripten with a compatible clang. If not, avoid such initializers or provide portable alternatives.

Applied to files:

  • src/wasm/modules/image/src/cielab.cpp
📚 Learning: 2026-01-04T15:34:04.654Z
Learnt from: Ryan-Millard
Repo: Ryan-Millard/Img2Num PR: 146
File: docs/docs/reference/react/components/ThemeSwitch/tests.md:123-124
Timestamp: 2026-01-04T15:34:04.654Z
Learning: In Docusaurus documentation, relative directory links (e.g., ../ or ../../../hooks/useTheme) resolve to index.md within the target directory. Do not require or force linking to index.md explicitly; such relative links remain valid and should not be changed when reviewing docs under the docs directory.

Applied to files:

  • docs/docs/reference/wasm/modules/image/cielab/index.md
🪛 GitHub Actions: CI
src/wasm/modules/image/src/cielab.cpp

[error] 47-47: clang-format check failed. Code should be clang-formatted [-Wclang-format-violations]. Command failed: clang-format --dry-run --Werror "src/wasm/modules/image/src/cielab.cpp"

🔇 Additional comments (8)
docs/docs/reference/wasm/modules/image/cielab/index.md (1)

1-187: LGTM! Comprehensive and accurate documentation.

The documentation clearly explains the RGB ↔ CIELAB conversion pipeline with appropriate visual aids (Mermaid diagrams), mathematical formulas, and references to industry standards. The transformation matrices match the high-precision values used in the implementation.

src/wasm/modules/image/src/cielab.cpp (7)

1-4: LGTM! Includes are appropriate.

The necessary headers are included for the color conversion implementation.


5-10: LGTM! Standard CIELAB constants.

The constants for XYZ to Lab transformation are correctly defined and well-documented.


11-18: LGTM! Standard sRGB gamma correction constants.

The constants for sRGB linearization and gamma encoding are correctly defined per the sRGB standard.


19-42: LGTM! High-precision forward transformation matrix.

The sRGB → XYZ matrix uses high-precision coefficients (7 decimal places) matching the Khronos/W3C specification. The ASCII matrix diagram in the comments provides excellent inline documentation.


43-67: LGTM! High-precision inverse transformation matrix.

The XYZ → sRGB inverse matrix now uses high-precision coefficients (6 decimal places) matching the ITU-R BT.709 / sRGB standard, addressing the previous precision concern.


68-77: LGTM! Standard D65 white point and Lab factors.

The D65 illuminant reference white point and CIELAB conversion factors are correctly defined per the CIE 1976 Lab* specification.


79-137: LGTM! Helper functions implement standard conversions correctly.

The helper functions correctly implement:

  • Non-linear XYZ → Lab transformation with floating-point safety (std::max)
  • sRGB gamma expansion/compression with proper thresholds and clamping
  • Inverse Lab → XYZ transformation

The logic aligns with the sRGB and CIELAB standards.

Comment thread src/wasm/modules/image/src/cielab.cpp

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Fix all issues with AI Agents
In @src/wasm/modules/image/src/cielab.cpp:
- Around line 133-138: gamma_encode can return NaN when given negative linear
RGB values from lab_to_rgb; clamp negative inputs to zero before applying the
fractional pow. Modify gamma_encode to guard negatives (e.g., if (u <= 0.0)
return 0.0;) or compute with u_clamped = std::max(u, 0.0) and use u_clamped for
the pow/threshold logic so SRGB_GAMMA_INV and SRGB_LINEAR_THRESHOLD branches
never call std::pow on a negative value.
🧹 Nitpick comments (1)
src/wasm/modules/image/src/cielab.cpp (1)

129-131: Use inline instead of constexpr for consistency.

The function inverse_xyz_to_lab is marked constexpr, while other helper functions (xyz_to_lab, srgb_to_linear, gamma_encode) use inline. For consistency and clarity, use inline here as well—the function is not evaluated at compile time in practice.

🔎 Proposed fix
-constexpr double inverse_xyz_to_lab(double t) {
+inline double inverse_xyz_to_lab(const double t) {
   return (t > DELTA) ? (t * t * t) : (3 * DELTA * DELTA * (t - EPSILON));
 }
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between c89d3ca and 45f6274.

📒 Files selected for processing (5)
  • docs/docs/reference/wasm/modules/image/bilateral_filter/api.md
  • docs/docs/reference/wasm/modules/image/bilateral_filter/color-spaces.md
  • docs/docs/reference/wasm/modules/image/cielab/api.md
  • docs/docs/reference/wasm/modules/image/cielab/implementation-explained.md
  • src/wasm/modules/image/src/cielab.cpp
✅ Files skipped from review due to trivial changes (1)
  • docs/docs/reference/wasm/modules/image/cielab/implementation-explained.md
🚧 Files skipped from review as they are similar to previous changes (2)
  • docs/docs/reference/wasm/modules/image/bilateral_filter/api.md
  • docs/docs/reference/wasm/modules/image/cielab/api.md
🧰 Additional context used
🧠 Learnings (6)
📚 Learning: 2026-01-06T05:10:03.461Z
Learnt from: Ryan-Millard
Repo: Ryan-Millard/Img2Num PR: 191
File: src/wasm/modules/image/src/cielab.cpp:31-39
Timestamp: 2026-01-06T05:10:03.461Z
Learning: When reviewing color space transformation code in C++ (specifically in src/wasm/modules/image/src/*.cpp), flag and require checks for precision inconsistencies between forward and inverse matrix coefficients. Ensure that round-trip conversions do not accumulate unacceptable rounding errors by comparing forward and inverse results within a defined numerical tolerance, documenting tolerances, and adding tests that verify symmetry (forward then inverse).

Applied to files:

  • src/wasm/modules/image/src/cielab.cpp
📚 Learning: 2026-01-06T21:06:24.476Z
Learnt from: Ryan-Millard
Repo: Ryan-Millard/Img2Num PR: 191
File: src/wasm/modules/image/src/cielab.cpp:43-57
Timestamp: 2026-01-06T21:06:24.476Z
Learning: In the Img2Num project, prefer recommending and using the provided docker/script wrappers (e.g., ./img2num format-wasm, ./img2num clean-wasm) instead of invoking local tools directly (e.g., clang-format -i). This reduces dependency requirements for users and ensures consistent tooling across environments. Apply this guidance to C++ source files under the project when reviewing changes.

Applied to files:

  • src/wasm/modules/image/src/cielab.cpp
📚 Learning: 2025-12-20T20:11:28.422Z
Learnt from: Ryan-Millard
Repo: Ryan-Millard/Img2Num PR: 0
File: :0-0
Timestamp: 2025-12-20T20:11:28.422Z
Learning: In the Img2Num repository, all documentation should be properly organized in the docs/docs/ folder structure following the Docusaurus conventions, either as a dedicated category or integrated into existing categories like project-scripts.

Applied to files:

  • src/wasm/modules/image/src/cielab.cpp
📚 Learning: 2025-12-31T17:46:54.476Z
Learnt from: fransafu
Repo: Ryan-Millard/Img2Num PR: 176
File: src/wasm/modules/image/src/bilateral_filter.cpp:98-101
Timestamp: 2025-12-31T17:46:54.476Z
Learning: In bilateral_filter.cpp (src/wasm/modules/image/src/bilateral_filter.cpp), rely on the fact that the center pixel contributes a weight of exactly 1.0 to both spatial and range components (exp(0) = 1) so the normalization sum cannot be zero. This implies you do not need explicit guards against division by zero for normalization in this implementation.

Applied to files:

  • src/wasm/modules/image/src/cielab.cpp
📚 Learning: 2026-01-06T04:56:57.269Z
Learnt from: Ryan-Millard
Repo: Ryan-Millard/Img2Num PR: 191
File: src/wasm/modules/image/src/kmeans.cpp:128-134
Timestamp: 2026-01-06T04:56:57.269Z
Learning: In C++ sources compiled for WASM with Emscripten/Clang, designated initializers (e.g., RGBXY{.r = ..., .g = ...}) are allowed as a C++17 extension. When reviewing code that relies on designated initializers, verify that the target toolchain enables CXX_STANDARD 17 or higher and that the build system (CMake/emsdk) uses Emscripten with a compatible clang. If not, avoid such initializers or provide portable alternatives.

Applied to files:

  • src/wasm/modules/image/src/cielab.cpp
📚 Learning: 2026-01-04T15:34:04.654Z
Learnt from: Ryan-Millard
Repo: Ryan-Millard/Img2Num PR: 146
File: docs/docs/reference/react/components/ThemeSwitch/tests.md:123-124
Timestamp: 2026-01-04T15:34:04.654Z
Learning: In Docusaurus documentation, relative directory links (e.g., ../ or ../../../hooks/useTheme) resolve to index.md within the target directory. Do not require or force linking to index.md explicitly; such relative links remain valid and should not be changed when reviewing docs under the docs directory.

Applied to files:

  • docs/docs/reference/wasm/modules/image/bilateral_filter/color-spaces.md
🔇 Additional comments (3)
docs/docs/reference/wasm/modules/image/bilateral_filter/color-spaces.md (2)

1-80: Content comprehensively addresses PR objectives.

This documentation successfully fulfills the stated requirements: it clearly documents how sigma_range behaves differently in RGB vs CIELAB color spaces, explains the mathematical underpinnings, provides the empirically-derived scaling factor (4.18), and offers practical parameter guidance for users.

The mathematical explanations, performance trade-offs, and decision-tree guidance (lines 21–36) are well-structured and accessible.


99-99: All component imports and cross-document references are correctly resolvable. The RgbVsLabRangeKernel component exists at the expected location, and all referenced documentation files (implementation.md, api.md, keywords.md) are present in the same directory with correct relative path syntax.

src/wasm/modules/image/src/cielab.cpp (1)

98-165: Verify round-trip conversion accuracy between RGB and CIELAB, and document numerical tolerances.

The transformation pipeline involves multiple floating-point operations with potential accumulated rounding errors. Based on the code review:

  1. No round-trip conversion tests exist — Add tests to verify RGB→Lab→RGB conversions preserve values within acceptable tolerance (e.g., within 1 bit-level accuracy ±1 on uint8 output).

  2. Matrix coefficient precision needs verification — The forward (SRGB_R_TO_X, etc.) and inverse (SRGB_X_TO_R, etc.) matrix coefficients should be mathematically verified as true inverses to within acceptable floating-point error bounds, as precision mismatches can accumulate in round-trip conversions.

  3. Document numerical tolerances — Add comments or constants defining acceptable error ranges for:

    • Round-trip conversions (RGB→Lab→RGB)
    • Bilateral filter range weight calculations in both color spaces
    • The impact of the existing clamping strategy (L* clamped in rgb_to_lab, a*/b* unclamped, final RGB clamped in lab_to_rgb)
  4. Bilateral filter consistency — Verify that CIELAB and RGB modes produce visually equivalent results at equivalent sigma values. (Note: Documentation mentions a 4.18x sigma scaling factor, but quantitative test validation is needed.)

Comment thread src/wasm/modules/image/src/cielab.cpp
Comment thread src/wasm/modules/image/src/cielab.cpp Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Fix all issues with AI Agents
In @src/wasm/modules/image/src/cielab.cpp:
- Around line 159-161: The three assignments to r, g, b call gamma_encode with
std::clamp but are missing the closing parentheses; fix each to call
gamma_encode(std::clamp(...)); e.g., update the r, g, and b lines so the
std::clamp(...) is closed and then the gamma_encode(...) call is closed (use
gamma_encode(std::clamp(r, 0.0, 1.0)); and similarly for g and b), ensuring
parentheses correctly nest around std::clamp and gamma_encode.
🧹 Nitpick comments (1)
src/wasm/modules/image/src/cielab.cpp (1)

154-166: Consider simplifying the clamping strategy.

The current implementation clamps linear RGB values before gamma encoding (lines 159-161) and again after gamma encoding (lines 164-166). While this defensive approach is correct, it's slightly redundant:

  • gamma_encode already guards against negative inputs (line 135)
  • Pre-gamma clamping ensures out-of-gamut values are bounded to [0.0, 1.0]
  • Post-gamma clamping catches any values that exceed 1.0 after gamma correction

You could simplify by clamping once before gamma encoding and relying on gamma_encode's monotonicity, though the current approach is safer if gamma_encode behavior changes. Given the PR context mentions consistency concerns (PR #195), keeping the defensive double-clamping may be prudent for now.

📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 45f6274 and 2ee37a3.

📒 Files selected for processing (1)
  • src/wasm/modules/image/src/cielab.cpp
🧰 Additional context used
🧠 Learnings (5)
📚 Learning: 2026-01-06T05:10:03.461Z
Learnt from: Ryan-Millard
Repo: Ryan-Millard/Img2Num PR: 191
File: src/wasm/modules/image/src/cielab.cpp:31-39
Timestamp: 2026-01-06T05:10:03.461Z
Learning: When reviewing color space transformation code in C++ (specifically in src/wasm/modules/image/src/*.cpp), flag and require checks for precision inconsistencies between forward and inverse matrix coefficients. Ensure that round-trip conversions do not accumulate unacceptable rounding errors by comparing forward and inverse results within a defined numerical tolerance, documenting tolerances, and adding tests that verify symmetry (forward then inverse).

Applied to files:

  • src/wasm/modules/image/src/cielab.cpp
📚 Learning: 2026-01-06T21:06:24.476Z
Learnt from: Ryan-Millard
Repo: Ryan-Millard/Img2Num PR: 191
File: src/wasm/modules/image/src/cielab.cpp:43-57
Timestamp: 2026-01-06T21:06:24.476Z
Learning: In the Img2Num project, prefer recommending and using the provided docker/script wrappers (e.g., ./img2num format-wasm, ./img2num clean-wasm) instead of invoking local tools directly (e.g., clang-format -i). This reduces dependency requirements for users and ensures consistent tooling across environments. Apply this guidance to C++ source files under the project when reviewing changes.

Applied to files:

  • src/wasm/modules/image/src/cielab.cpp
📚 Learning: 2025-12-20T20:11:28.422Z
Learnt from: Ryan-Millard
Repo: Ryan-Millard/Img2Num PR: 0
File: :0-0
Timestamp: 2025-12-20T20:11:28.422Z
Learning: In the Img2Num repository, all documentation should be properly organized in the docs/docs/ folder structure following the Docusaurus conventions, either as a dedicated category or integrated into existing categories like project-scripts.

Applied to files:

  • src/wasm/modules/image/src/cielab.cpp
📚 Learning: 2025-12-31T17:46:54.476Z
Learnt from: fransafu
Repo: Ryan-Millard/Img2Num PR: 176
File: src/wasm/modules/image/src/bilateral_filter.cpp:98-101
Timestamp: 2025-12-31T17:46:54.476Z
Learning: In bilateral_filter.cpp (src/wasm/modules/image/src/bilateral_filter.cpp), rely on the fact that the center pixel contributes a weight of exactly 1.0 to both spatial and range components (exp(0) = 1) so the normalization sum cannot be zero. This implies you do not need explicit guards against division by zero for normalization in this implementation.

Applied to files:

  • src/wasm/modules/image/src/cielab.cpp
📚 Learning: 2026-01-06T04:56:57.269Z
Learnt from: Ryan-Millard
Repo: Ryan-Millard/Img2Num PR: 191
File: src/wasm/modules/image/src/kmeans.cpp:128-134
Timestamp: 2026-01-06T04:56:57.269Z
Learning: In C++ sources compiled for WASM with Emscripten/Clang, designated initializers (e.g., RGBXY{.r = ..., .g = ...}) are allowed as a C++17 extension. When reviewing code that relies on designated initializers, verify that the target toolchain enables CXX_STANDARD 17 or higher and that the build system (CMake/emsdk) uses Emscripten with a compatible clang. If not, avoid such initializers or provide portable alternatives.

Applied to files:

  • src/wasm/modules/image/src/cielab.cpp
🪛 Cppcheck (2.19.0)
src/wasm/modules/image/src/cielab.cpp

[error] 161-161: Unmatched '('. Configuration

(syntaxError)

🔇 Additional comments (5)
src/wasm/modules/image/src/cielab.cpp (5)

1-4: LGTM!

The includes are appropriate for the mathematical operations and algorithms used throughout the file.


5-77: LGTM!

The constants are well-organized with clear documentation. The transformation matrices include helpful ASCII diagrams and references to the ITU-R BT.709/sRGB standard, making the implementation easy to verify and maintain.


79-96: LGTM!

The helper functions correctly implement the non-linear transformations with appropriate guards:

  • xyz_to_lab handles the piecewise CIE Lab function with protection against negative inputs
  • srgb_to_linear properly implements inverse gamma correction with input clamping

98-127: LGTM!

The rgb_to_lab function correctly implements the full conversion pipeline: sRGB normalization → linearization → XYZ transformation → D65 normalization → Lab conversion. The clamping of L* to [0.0, 100.0] is appropriate.


129-140: LGTM!

The inverse transformation helpers are correctly implemented:

  • inverse_xyz_to_lab properly inverts the piecewise Lab→XYZ function
  • gamma_encode now guards against negative inputs (line 135), preventing NaN from std::pow with fractional exponents on out-of-gamut colors

Comment thread src/wasm/modules/image/src/cielab.cpp Outdated
@Ryan-Millard
Ryan-Millard force-pushed the feat/kmean-preprocessing/bilateral-filter branch from 2ee37a3 to abb0402 Compare January 6, 2026 21:36
@Ryan-Millard

Copy link
Copy Markdown
Owner Author

Thank you @Krasner and @fransafu for the amazing work! It has been wonderful to collaborate on this feature and I hope to see both of you around some time!

Have a great day!

@Ryan-Millard Ryan-Millard changed the title WIP: feat(bilateral filter): implement bilateral filter for denoising before K-Means in RGB & CIELAB feat(bilateral filter): implement bilateral filter for denoising before K-Means in RGB & CIELAB Jan 6, 2026
@Ryan-Millard
Ryan-Millard merged commit c399755 into main Jan 6, 2026
3 checks passed
@Ryan-Millard
Ryan-Millard deleted the feat/kmean-preprocessing/bilateral-filter branch January 6, 2026 21:54
@Ryan-Millard

Copy link
Copy Markdown
Owner Author

Huge thanks to @Krasner and @fransafu - this filter work landed because of both of your contributions.

You each approached the problem differently, and the combined result is stronger for it. I really appreciate the time, thought, and iteration you both put into this.

I’d like to follow up by offering you both an access upgrade to better reflect your contributions. I’ll comment on your individual PRs with details.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants