Skip to content

Feat/multithread - #232

Closed
Krasner wants to merge 3 commits into
Ryan-Millard:feat/contour-tracingfrom
Krasner:feat/multithread
Closed

Feat/multithread#232
Krasner wants to merge 3 commits into
Ryan-Millard:feat/contour-tracingfrom
Krasner:feat/multithread

Conversation

@Krasner

@Krasner Krasner commented Jan 18, 2026

Copy link
Copy Markdown
Collaborator

⚠️ Are you using the correct pull request template?
Please choose one of the following:

If none of these fit, you may use this default to describe your change manually.

If this is the right template, go ahead and complete it below 👇


📌 Description

Please describe the changes made in this PR and why they are necessary.

Fixes #issue-number (if applicable)

✅ Type of Change

Place an "x" in the brackets below:

  • Bug fix 🐛
  • New feature ✨
  • Refactor 🔧
  • Documentation 📚
  • Build/dependency update 🧱
  • Other (describe):

🧪 How Has This Been Tested?

Please describe how you tested your changes (e.g., unit tests, manual testing, screenshots, etc.)

🧩 Checklist

Place an "x" in the brackets below:

  • I’ve followed the contribution guidelines.
  • My code follows the code style of this project.
  • I’ve added tests where necessary.
  • I’ve updated the documentation where applicable.
  • I’ve linked related issues or discussions (if any).
  • I’ve checked for breaking changes and backwards compatibility.

📸 Screenshots / Demo (if applicable)

Paste images, GIFs, or demo links here.

💬 Additional Context

Anything else relevant to the PR.

Summary by CodeRabbit

  • New Features

    • Contour detection and graph-based region workflow added
    • Multi-threaded bilateral filter for faster image processing
    • K-means now returns per-pixel labels alongside clustered pixels
  • Documentation

    • New comprehensive WASM, worker, Vite and build guides
  • Removals

    • Deprecated in-place region-merge API (replaced by contour/graph workflow)
  • Security

    • Dev server applies COOP and COEP headers for stronger isolation

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

@Krasner
Krasner requested a review from Ryan-Millard January 18, 2026 04:32
@coderabbitai

coderabbitai Bot commented Jan 18, 2026

Copy link
Copy Markdown
Contributor
📝 Walkthrough

Walkthrough

Added documentation and WASM/Vite tooling pages; replaced mergeSmallRegionsInPlace with a graph+contours-based pipeline (new Node/Graph/kmeans_graph/contours C++ APIs), made bilateral filter and kmeans multi-threaded, updated JS worker and hook exports to use findContours and label outputs, removed legacy mergeSmallRegionsInPlace implementation and tests, and added security headers to Vite dev server.

Changes

Cohort / File(s) Summary of changes (attention areas)
Docs (new & updated categories)
docs/docs/reference/react/workers/_category_.json, docs/docs/reference/react/hooks/useWasmWorker/index.md, docs/docs/reference/react/workers/wasmWorker.md, docs/docs/reference/react/vite-config.md, docs/docs/reference/wasm/modules/cmake-root-orchestrator.md, docs/docs/reference/wasm/modules/image/cmake.md
Added comprehensive docs for workers, useWasmWorker hook, Vite WASM config, CMake orchestrator and image module build. Review for accuracy vs code (API names, arg lists, build flows).
Docs (position metadata removals/changes)
docs/docs/reference/react/components/_category_.json, .../react/css/_category_.json, .../react/hooks/_category_.json, .../hooks/useTheme/_category_.json, .../wasm/modules/image/_category_.json, multiple .../_category_.json under image subdirs
Removed or adjusted position fields in many category files — affects generated ordering in docs site.
Frontend - React & Hook changes
src/components/WasmImageProcessor.jsx, src/hooks/useWasmWorker.js, src/hooks/useWasmWorker.test.js
Switched pipeline to consume kmeans labels + findContours; kmeans now returns { pixels, labels }; mergeSmallRegionsInPlace removed and replaced by findContours; tests updated (removed coverage for mergeSmallRegionsInPlace). Check updated hook signature and component destructuring.
Worker runtime (JS)
src/workers/wasmWorker.js
Improved allocation metadata tracking and TypedArray handling for WASM heap read/write. Ensure type mapping matches new C++ buffers (labels, out_data) and error paths.
Vite config
vite.config.js, docs/docs/reference/react/vite-config.md
Added Cross-Origin-Opener-Policy and Cross-Origin-Embedder-Policy via server.headers and a dev-server plugin. Validate dev-server behavior and WASM loading (COOP/COEP requirements).
WASM build flags
src/wasm/modules/image/CMakeLists.txt
Enabled pthreads, exported HEAP32 in runtime methods. Confirm Emscripten build flags and thread support compatibility.
New/removed public headers
src/wasm/modules/image/include/contours.h, graph.h, kmeans_graph.h, node.h, mergeSmallRegionsInPlace.h (removed), kmeans.h (signature change), bilateral_filter.h (signature change)
Added contour/graph/node/kmeans_graph headers and removed mergeSmallRegionsInPlace public header; updated kmeans and bilateral_filter signatures (new params and outputs). These change the WASM ABI — verify exported names and JS call signatures match.
WASM C++ implementations added/changed
src/wasm/modules/image/src/contours.cpp, graph.cpp, kmeans.cpp, kmeans_graph.cpp, node.cpp, bilateral_filter.cpp, mergeSmallRegionsInPlace.cpp (removed)
Major new implementations for contours, graph, node, kmeans_graph; bilateral_filter and kmeans converted to multithreaded and new APIs; mergeSmallRegionsInPlace implementation removed. High-impact on algorithm correctness, threading, and memory use — review concurrency, bounds checks, and ABI.
Docs category for Workers
docs/docs/reference/react/workers/_category_.json, docs/docs/reference/react/workers/wasmWorker.md
New category and WASM worker documentation; ensure it aligns with updated worker and hook behavior.

Sequence Diagram(s)

sequenceDiagram
  participant React as React (WasmImageProcessor)
  participant Hook as useWasmWorker Hook
  participant Worker as WASM Worker (WebWorker)
  participant WASM as Image WASM module (C++)
  participant Graph as Graph/Contours subsystems (C++)

  React->>Hook: requestProcess(image)
  Hook->>Worker: postMessage({ cmd: 'bilateralFilter', ... })
  Worker->>WASM: call exported bilateral_filter(..., n_threads)
  WASM-->>Worker: returns filtered pixels

  Hook->>Worker: postMessage({ cmd: 'kmeans', pixels, out_buffers })
  Worker->>WASM: call exported kmeans(..., out_data, out_labels, n_threads)
  WASM-->>Worker: returns { out_data, out_labels }

  Hook->>Worker: postMessage({ cmd: 'kmeans_clustering_graph', data, labels, min_area, draw_contours })
  Worker->>WASM: call exported kmeans_clustering_graph(...)
  WASM->>Graph: build graph, discover_edges, merge_small_area_nodes
  Graph->>WASM: returns merged regions + contours
  WASM-->>Worker: returns final RGBA and contours
  Worker-->>Hook: postMessage(result)
  Hook-->>React: deliver processed pixels + contours/labels
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested labels

C/C++, BREAKING CHANGE, docs

Suggested reviewers

  • Ryan-Millard

"I'm a rabbit in a wasm-filled glen,
Threads and contours hop now and then.
I nibbled old merges, planted a graph,
Kmeans and filters do a joyful dance.
🐇✨ New docs and code — hop to review, friend!"

🚥 Pre-merge checks | ✅ 1 | ❌ 2
❌ Failed checks (1 warning, 1 inconclusive)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 24.39% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Title check ❓ Inconclusive The PR title 'Feat/multithread' is vague and uses a branch-naming convention instead of a descriptive summary. It doesn't clearly convey the main change or which specific multithreading implementation was added. Revise the title to be more descriptive and specific, such as 'Add multi-threading support to bilateral filter and k-means clustering' or similar, to clearly indicate the scope of the changes.
✅ Passed checks (1 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ 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.

@Krasner Krasner mentioned this pull request Jan 18, 2026
@Krasner

Krasner commented Jan 18, 2026

Copy link
Copy Markdown
Collaborator Author

continues from #219 - check last commit

@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: 14

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
src/wasm/modules/image/src/bilateral_filter.cpp (1)

107-109: Unconditional CIELAB lookup even in RGB mode.

Lines 107-109 read from cie_image regardless of color_space. In RGB mode, cie_image is empty, causing undefined behavior (reading from empty vector).

🐛 Proposed fix - conditionally access cie_image
-          double L{cie_image[neighbor_idx]};
-          double A{cie_image[neighbor_idx + 1]};
-          double B{cie_image[neighbor_idx + 2]};
+          double L{0}, A{0}, B{0};
+          if (color_space == COLOR_SPACE_OPTION_CIELAB) {
+            L = cie_image[neighbor_idx];
+            A = cie_image[neighbor_idx + 1];
+            B = cie_image[neighbor_idx + 2];
+          }
src/wasm/modules/image/src/kmeans.cpp (1)

207-212: Alpha channel not set in output.

Unlike kmeans (line 100) which explicitly sets alpha to 255, this function doesn't write the alpha channel. If the input data has non-opaque alpha values or if the buffer is newly allocated, the output may have incorrect transparency. This inconsistency could be the "poorly indexed channel" bug referenced in the comment at line 108.

Proposed fix
   for (int32_t i = 0; i < num_pixels; ++i) {
     int32_t cluster = labels[i];
     data[i * 4 + 0] = static_cast<uint8_t>(centroids[cluster].r * 255);
     data[i * 4 + 1] = static_cast<uint8_t>(centroids[cluster].g * 255);
     data[i * 4 + 2] = static_cast<uint8_t>(centroids[cluster].b * 255);
+    data[i * 4 + 3] = 255;
   }
🤖 Fix all issues with AI agents
In `@docs/docs/reference/react/hooks/useWasmWorker/index.md`:
- Around line 73-95: Fix the malformed Markdown code fence inside the TabItem
that contains the C++ snippet: remove the extra indentation before the
declaration and replace the orphaned closing backticks so the block starts with
a proper fenced code block label (cpp) and ends with four backticks that close
the outer fenced region; specifically adjust the block containing EXPORTED void
add_arrays(int* a, int* b, int* out, int length); and ensure the TabItem/ Tabs
wrappers remain balanced so the following TabItem value="emscripten-method"
renders correctly.

In `@docs/docs/reference/wasm/modules/cmake-root-orchestrator.md`:
- Around line 147-148: The diagram/text claiming "Hot-reload WASM modules on
source changes" is misleading; update the copy near nodes K/L (the "Hot-reload
WASM with dev server" label) to clarify that Vite HMR only hot-reloads the
JavaScript integration layer and that native C/C++ -> WASM artifacts must be
rebuilt via CMake manually (e.g., rephrase to "Hot-reload JS integration with
dev server — rebuild WASM via CMake on source changes" or similar).
- Line 162: Update the explanatory text for the "**Development:** Hot-reloads
WASM modules on source changes." bullet to include the same clarification as
elsewhere: define what "hot-reloads" means (e.g., it rebuilds the module and
replaces the running instance without restarting the whole orchestrator), state
whether module state is preserved or reset, and note how to enable/disable this
behavior (matching the phrasing used in the other explanation). Target the
sentence containing "**Development:** Hot-reloads WASM modules on source
changes." to ensure consistency and clarity.

In `@src/wasm/modules/image/include/bilateral_filter.h`:
- Around line 18-20: In bilateral_filter, add validation to clamp the n_threads
parameter into a safe range to avoid division by zero and unsigned loop issues:
compute a max_threads = std::thread::hardware_concurrency() (fallback to 1 if it
returns 0), then set n_threads = std::max(1, std::min(n_threads,
static_cast<int>(max_threads))); use this validated n_threads when computing
rows_per_thread and any loop bounds (ensure casts to signed/size_t are
deliberate), and replace any unsigned loop comparisons that rely on possibly
negative n_threads with iterations based on the validated positive n_threads to
prevent undefined behavior.

In `@src/wasm/modules/image/include/graph.h`:
- Line 54: The size() method in graph.h currently declares the return type as
const (inline const size_t size()) which is meaningless for a value type; change
the signature of the Graph::size() method to put the const qualifier on the
method instead (e.g., inline size_t size() const) so it is a const-qualified
accessor; update the declaration/definition of size() in graph.h to remove the
leading const on the return type and append const after the parameter list to
ensure const-correctness.

In `@src/wasm/modules/image/src/bilateral_filter.cpp`:
- Line 29: The global std::mutex write_mutex and any locking in _process should
be removed because threads write disjoint ranges (use start_row/end_row) into
the result buffer; locate the write_mutex declaration and delete it, then remove
the lock_guard/lock/unlock usage inside the _process function (and any includes
only used for the mutex) so that each worker thread writes directly to result
for its assigned rows without synchronization.
- Line 191: The current calculation int rows_per_thread =
static_cast<int>(height) / n_threads can yield zero when n_threads > height;
clamp the thread count first (e.g., used_threads =
std::min(static_cast<int>(height), n_threads)) and then compute rows_per_thread
= static_cast<int>(height) / used_threads so each thread gets at least one row;
update all uses of n_threads in the scheduling logic (e.g., where
rows_per_thread and loop bounds are used) to use used_threads to avoid
division-by-zero and incorrect distribution.

In `@src/wasm/modules/image/src/graph.cpp`:
- Around line 120-147: In merge_small_area_nodes, the neighbors vector is
incorrectly initialized with size n->num_edges() and then appended to (creating
default elements) and the code can index past the end if all neighbor areas are
zero; fix by creating neighbors as an empty vector and calling
neighbors.reserve(n->num_edges()) before copying edges, then replace the manual
idx loop with a safe search (e.g., std::find_if on neighbors for area()>0) and
if no such neighbor exists, skip this node (continue) or otherwise handle the
case without accessing neighbors[idx]; update references in this function
(neighbors, n, merge_nodes, get_nodes) accordingly.

In `@src/wasm/modules/image/src/kmeans_graph.cpp`:
- Around line 143-146: The multiplication width * height in
kmeans_clustering_graph is done as int and assigned to int32_t num_pixels,
risking integer overflow for large images; change the calculation to perform the
multiplication in an unsigned wider type (e.g., size_t) by casting operands
before multiplying and assign to a size_t (or uint64_t) variable (e.g., size_t
num_pixels = static_cast<size_t>(width) * static_cast<size_t>(height)); also
validate width/height are non-negative and optionally check that num_pixels fits
expected bounds before later casts/uses to prevent silent overflow.

In `@src/wasm/modules/image/src/kmeans.cpp`:
- Around line 1-9: Add the missing <cstring> include so std::memcpy is declared:
update the include block at the top of kmeans.cpp (near the existing includes in
this file) to add `#include` <cstring>; this fixes the use of std::memcpy
(referenced in the code around the memcpy call) and avoids relying on transitive
includes.

In `@src/wasm/modules/image/src/node.cpp`:
- Around line 22-39: The Node::color method divides by m_pixels_size without
guarding against an empty m_pixels, risking division by zero; update Node::color
to check that m_pixels is non-null and not empty (e.g., if m_pixels->empty() or
m_pixels_size == 0) before performing the average, and return a sensible default
color (such as {0,0,0}) or handle the empty case similarly to centroid() to
avoid the division; reference m_pixels, m_pixels_size and Node::color when
making the change.
- Around line 8-20: Node::centroid computes an average without guarding against
an empty m_pixels, causing division by zero; update Node::centroid to check
m_pixels->empty() (or m_pixels_size == 0) before dividing and return a safe
default XY (e.g., {0,0} or another agreed sentinel) or handle the empty case
appropriately, ensuring all references to m_pixels and the centroid calculation
(centroid.x/centroid.y and m_pixels_size) are skipped when empty.
- Around line 41-65: In Node::bounding_box_xywh(), x_max and y_max are
initialized to 0 which yields incorrect boxes when all pixel coordinates are
negative; change their initialization to INT_MIN (matching x_min/y_min using
INT_MAX) so the loop correctly computes extremes for negative coordinates and
the returned x,y,w,h are valid.

In `@vite.config.js`:
- Around line 116-126: Prettier formatting is off in vite.config.js; run the
formatter and ensure the code around the plugin named "force-security-headers"
(the configureServer function that calls server.middlewares.use and sets
Cross-Origin-Opener-Policy and Cross-Origin-Embedder-Policy headers) is properly
formatted—run `prettier --write vite.config.js` and commit the reformatted file
so the configureServer middleware block and surrounding server.headers remain
consistently styled.
🧹 Nitpick comments (18)
src/wasm/modules/image/include/bilateral_filter.h (1)

9-20: Add documentation for the new n_threads parameter.

The comment block documents all parameters except the newly added n_threads. Consider adding:

 //  - sigma_range: Gaussian standard deviation for intensity difference
 //  (radiometric decay)
+//  - n_threads: Number of threads for parallel processing (default: 8)
 void bilateral_filter(uint8_t *image, size_t width, size_t height,
src/wasm/modules/image/CMakeLists.txt (1)

45-47: Consider making PTHREAD_POOL_SIZE configurable via CMake variable.

The hardcoded value of 8 matches the default in bilateral_filter.h but is inflexible across deployment environments. Best practices recommend matching pool size to actual concurrent thread usage or device capabilities (via navigator.hardwareConcurrency). Each precreated thread consumes memory and stack; oversizing can increase page memory overhead on memory-constrained devices (mobile) while undersizing may under-utilize multi-core machines.

Consider:

  • Exposing PTHREAD_POOL_SIZE as a CMake variable with a sensible default (e.g., 4–8 depending on target).
  • Using -sPTHREAD_POOL_DELAY_LOAD=1 to defer worker creation and avoid startup blocking.

Also verify your deployment environment supports SharedArrayBuffer (requires Cross-Origin-Opener-Policy and Cross-Origin-Embedder-Policy headers in secure contexts).

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

76-104: Documentation is missing the new pthread/multithreading flags.

According to the AI summary, the actual CMakeLists.txt now includes pthread-related flags (USE_PTHREADS=1, PTHREAD_POOL_SIZE=8, -pthread) to enable multithreading for the bilateral filter. This documentation should be updated to reflect those additions.

Consider adding a section documenting:

  • -pthread compile/link flags
  • USE_PTHREADS=1
  • PTHREAD_POOL_SIZE=8
docs/docs/reference/react/vite-config.md (2)

122-136: Documentation is missing the new security headers for multithreading.

The actual vite.config.js now includes server.headers with Cross-Origin-Opener-Policy and Cross-Origin-Embedder-Policy headers required for SharedArrayBuffer/pthread support. This section should be updated to document these headers.

Suggested addition
### Security Headers (Multithreading)

```javascript
server: {
  headers: {
    "Cross-Origin-Opener-Policy": "same-origin",
    "Cross-Origin-Embedder-Policy": "require-corp",
  },
},
  • Required for SharedArrayBuffer support in browsers.
  • Enables pthread-based multithreading in WASM modules.
</details>

---

`197-241`: **Missing documentation for the `force-security-headers` plugin.**

The actual `vite.config.js` includes a new plugin named `force-security-headers` that applies COOP/COEP headers via middleware. This plugin should be documented alongside the other custom plugins for completeness.

</blockquote></details>
<details>
<summary>src/wasm/modules/image/src/contours.cpp (1)</summary><blockquote>

`112-119`: **Consider using `get` instead of `set` for reading pixel values.**

At line 116, `set(y3, x3) == 1` works because `set` returns `int&`, but semantically this is a read operation. Using `get(y3, x3) == 1` would be clearer and more consistent with the rest of the code.

<details>
<summary>Suggested change</summary>

```diff
     if (rightZeroExamined) {
       set(y3, x3) = -nbd;
     } else {
-      if (set(y3, x3) == 1) {
+      if (get(y3, x3) == 1) {
         set(y3, x3) = nbd;
       }
     }
src/wasm/modules/image/include/contours.h (1)

4-9: Consider removing unused includes from the header.

The includes <cmath> and <cstdlib> are not used by any declarations in this header. They appear to be needed only by the implementation in contours.cpp. Moving them to the .cpp file would reduce header dependencies and improve compile times.

Suggested change
 `#include` <array>
-#include <cmath>
 `#include` <cstdint>
-#include <cstdlib>
 `#include` <stdexcept>
 `#include` <vector>

The static analysis error about 'array' file not found is a false positive—standard library headers are available in the Emscripten toolchain.

src/wasm/modules/image/include/kmeans.h (1)

8-10: Type mismatch with implementation.

The header declares width, height, k, max_iter as int, but the implementation in kmeans.cpp uses int32_t. While these are typically identical, using consistent types avoids subtle ABI or toolchain issues.

🔧 Suggested fix for consistency
-EXPORTED void kmeans(const uint8_t *data, uint8_t *out_data, int *out_labels,
-                     const int width, const int height, const int k,
-                     const int max_iter);
+EXPORTED void kmeans(const uint8_t *data, uint8_t *out_data, int *out_labels,
+                     const int32_t width, const int32_t height, const int32_t k,
+                     const int32_t max_iter);
src/wasm/modules/image/src/kmeans_graph.cpp (3)

1-3: Acknowledge the TODO; consider tracking.

The TODO about alpha channel handling is noted. This could lead to unexpected behavior for images with transparency. Consider creating an issue to track this.

Would you like me to open an issue to track the alpha channel handling improvement?


92-96: Unused variable counts.

The variable counts returned from flood_fill is computed but never used. The num_pixels variable is also computed separately but unused.

🧹 Remove unused variables
-        int counts = flood_fill(labels, regions, data, i, j, label, r_lbl,
+        flood_fill(labels, regions, data, i, j, label, r_lbl,
                                 width, height, p_ptr);
-        int num_pixels = p_ptr->size();

-        // num_pixels == counts always
         Node_ptr n_ptr = std::make_shared<Node>(r_lbl, p_ptr);

102-109: Static RNG may cause non-reproducible results across calls.

Using static for rng and dist means the state persists across calls to visualize_contours. This could be intentional for variety, but may cause non-deterministic debugging. If reproducibility is needed, consider seeding with a fixed value or making it non-static.

src/hooks/useWasmWorker.js (2)

45-49: Consider dynamically determining thread count.

The default n_threads = 8 may not be optimal for all systems. Consider using navigator.hardwareConcurrency to determine available cores, with a fallback.

♻️ Suggested improvement
   const bilateralFilter = async ({
     pixels,
     width,
     height,
     sigma_spatial = 3.0,
     sigma_range = 50.0,
     color_space = 0,
-    n_threads = 8,
+    n_threads = navigator.hardwareConcurrency || 4,
   }) => {

75-82: Function name findContours may be misleading.

The function calls kmeans_clustering_graph which performs region merging and optionally draws contours. The name findContours suggests only contour detection. Consider renaming to better reflect its purpose (e.g., processRegionsWithContours or kmeansClusteringGraph).

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

49-63: Parameter n_threads is unused in _process.

The n_threads parameter is passed to _process but never used within the function.

🧹 Remove unused parameter
 void _process(
   const uint8_t* image, 
   const std::vector<double>& cie_image, 
   std::vector<uint8_t>& result, 
   const std::vector<double>& spatial_weights,
   const std::vector<double>& range_lut,
   int radius,
   double sigma_range,
   int start_row, 
   int end_row, 
   size_t height, 
   size_t width, 
-  uint8_t color_space, 
-  int n_threads
+  uint8_t color_space
 ) {

And update call sites accordingly.

src/wasm/modules/image/include/node.h (1)

4-4: Use quotes for project-local headers.

Static analysis indicates RGBPixel.h should use quotes ("RGBPixel.h") instead of angle brackets (<RGBPixel.h>) since it's a project header, not a system header. Angle brackets search system include paths first.

♻️ Proposed fix
-#include <RGBPixel.h>
+#include "RGBPixel.h"
src/wasm/modules/image/src/graph.cpp (2)

9-14: Fix incorrect complexity comment.

The comment states std::unordered_map lookup is O(log(N)), but it's actually O(1) average case (hash table). O(log(N)) would be for std::map (balanced tree).

📝 Fix comment
 /*
  *To quickly search m_nodes (std::vector) for the index of a node id
  *create an std::unordered_map of node id - index pairs
  *indexing time of std::vector by value is O(N)
- *lookup time of std::unordered_map by key is O(log(N))
+ *lookup time of std::unordered_map by key is O(1) average
  */

15-20: Potential signed/unsigned comparison warning.

Comparing int32_t i with m_nodes->size() (which returns size_t) may produce a compiler warning on some platforms.

♻️ Use consistent types
 void Graph::hash_node_ids() {
-  for (int32_t i{0}; i < m_nodes->size(); i++) {
+  for (size_t i{0}; i < m_nodes->size(); ++i) {
     const int32_t key{m_nodes->at(i)->id()};
-    m_node_ids[key] = i;
+    m_node_ids[key] = static_cast<int32_t>(i);
   }
 }
src/wasm/modules/image/src/kmeans.cpp (1)

29-29: Consider using consistent integer types.

labels and counts vectors use int, while loop indices and other variables use int32_t. While these are typically equivalent, using int32_t consistently would improve clarity and ensure portability.

Proposed fix
-  std::vector<int> labels(num_pixels, 0);
+  std::vector<int32_t> labels(num_pixels, 0);
-    std::vector<int> counts(k, 0);
+    std::vector<int32_t> counts(k, 0);

Also applies to: 71-71

Comment on lines +73 to +95
````cpp
#include "exported.h"

EXPORTED void add_arrays(int* a, int* b, int* out, int length);
```

</TabItem>

<TabItem value="emscripten-method" label="Emscripten Method">
:::caution Don't use this method in Img2Num!
This method leads to bloat in the CMake file, is difficult to maintain and tough to debug.

Use the Img2Num method instead because it provides convenience, reduces boilerplate, and saves you from numerous possible bugs.
:::
Add the function to the CMake file's `EXPORTED_FUNCTIONS` flag:
```cmake
"SHELL:-s EXPORTED_FUNCTIONS=['_malloc','_free','_add_arrays']"
```

</TabItem>
</Tabs>

````

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.

⚠️ Potential issue | 🟡 Minor

Markdown formatting issue in code block.

The code block within the TabItem has indentation issues and orphaned backticks that may cause rendering problems:

  1. Lines 73-77: The code block content is incorrectly indented inside the TabItem
  2. Line 95: Orphaned closing backticks
🔧 Suggested fix
     ````cpp
     `#include` "exported.h"
 
-        EXPORTED void add_arrays(int* a, int* b, int* out, int length);
-        ```
+    EXPORTED void add_arrays(int* a, int* b, int* out, int length);
+    ````
 
       </TabItem>
 
       <TabItem value="emscripten-method" label="Emscripten Method">
         :::caution Don't use this method in Img2Num!
         This method leads to bloat in the CMake file, is difficult to maintain and tough to debug.
 
         Use the Img2Num method instead because it provides convenience, reduces boilerplate, and saves you from numerous possible bugs.
         :::
         Add the function to the CMake file's `EXPORTED_FUNCTIONS` flag:
         ```cmake
         "SHELL:-s EXPORTED_FUNCTIONS=['_malloc','_free','_add_arrays']"
         ```
 
       </TabItem>
     </Tabs>
-
-    ````
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
````cpp
#include "exported.h"
EXPORTED void add_arrays(int* a, int* b, int* out, int length);
```
</TabItem>
<TabItem value="emscripten-method" label="Emscripten Method">
:::caution Don't use this method in Img2Num!
This method leads to bloat in the CMake file, is difficult to maintain and tough to debug.
Use the Img2Num method instead because it provides convenience, reduces boilerplate, and saves you from numerous possible bugs.
:::
Add the function to the CMake file's `EXPORTED_FUNCTIONS` flag:
```cmake
"SHELL:-s EXPORTED_FUNCTIONS=['_malloc','_free','_add_arrays']"
```
</TabItem>
</Tabs>
````
🤖 Prompt for AI Agents
In `@docs/docs/reference/react/hooks/useWasmWorker/index.md` around lines 73 - 95,
Fix the malformed Markdown code fence inside the TabItem that contains the C++
snippet: remove the extra indentation before the declaration and replace the
orphaned closing backticks so the block starts with a proper fenced code block
label (cpp) and ends with four backticks that close the outer fenced region;
specifically adjust the block containing EXPORTED void add_arrays(int* a, int*
b, int* out, int length); and ensure the TabItem/ Tabs wrappers remain balanced
so the following TabItem value="emscripten-method" renders correctly.

Comment on lines +147 to +148
K -->|Development| L["Hot-reload WASM with dev server"]
K -->|Production| M["Use prebuilt WASM modules"]

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.

⚠️ Potential issue | 🟡 Minor

Clarify the hot-reload claim for WASM modules.

The statement "Hot-reload WASM modules on source changes" may be misleading. Vite's HMR doesn't automatically rebuild C++ source to WASM on changes—developers must manually rerun the CMake build. Consider rephrasing to clarify that hot-reload applies to the JavaScript integration layer, while WASM modules require manual rebuilds.

-    K -->|Development| L["Hot-reload WASM with dev server"]
+    K -->|Development| L["Serve rebuilt WASM via dev server"]
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
K -->|Development| L["Hot-reload WASM with dev server"]
K -->|Production| M["Use prebuilt WASM modules"]
K -->|Development| L["Serve rebuilt WASM via dev server"]
K -->|Production| M["Use prebuilt WASM modules"]
🤖 Prompt for AI Agents
In `@docs/docs/reference/wasm/modules/cmake-root-orchestrator.md` around lines 147
- 148, The diagram/text claiming "Hot-reload WASM modules on source changes" is
misleading; update the copy near nodes K/L (the "Hot-reload WASM with dev
server" label) to clarify that Vite HMR only hot-reloads the JavaScript
integration layer and that native C/C++ -> WASM artifacts must be rebuilt via
CMake manually (e.g., rephrase to "Hot-reload JS integration with dev server —
rebuild WASM via CMake on source changes" or similar).

5. **Compilation:** Each module is added via `add_subdirectory` and compiled to WASM.
6. **WASM Output:** Compiled files go to `module/build`.
7. **Vite Integration:**
- **Development:** Hot-reloads WASM modules on source changes.

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.

⚠️ Potential issue | 🟡 Minor

Same clarification needed in the explanation.

-   - **Development:** Hot-reloads WASM modules on source changes.
+   - **Development:** Serves WASM modules via dev server after rebuild.
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
- **Development:** Hot-reloads WASM modules on source changes.
- **Development:** Serves WASM modules via dev server after rebuild.
🤖 Prompt for AI Agents
In `@docs/docs/reference/wasm/modules/cmake-root-orchestrator.md` at line 162,
Update the explanatory text for the "**Development:** Hot-reloads WASM modules
on source changes." bullet to include the same clarification as elsewhere:
define what "hot-reloads" means (e.g., it rebuilds the module and replaces the
running instance without restarting the whole orchestrator), state whether
module state is preserved or reset, and note how to enable/disable this behavior
(matching the phrasing used in the other explanation). Target the sentence
containing "**Development:** Hot-reloads WASM modules on source changes." to
ensure consistency and clarity.

Comment on lines 18 to +20
void bilateral_filter(uint8_t *image, size_t width, size_t height,
double sigma_spatial, double sigma_range,
uint8_t color_space);
uint8_t color_space, int n_threads=8);

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.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Search for n_threads validation in the codebase
rg -n -A5 'n_threads' --type=cpp

Repository: Ryan-Millard/Img2Num

Length of output: 4313


Add validation for n_threads parameter to prevent undefined behavior.

The function does not validate n_threads, which allows negative values that cause issues: division by zero at line 191 (rows_per_thread = height / n_threads), and undefined behavior in the unsigned loop comparison at line 248. Clamp n_threads to a valid range (e.g., [1, std::thread::hardware_concurrency()]) in the validation block at the start of the function.

🤖 Prompt for AI Agents
In `@src/wasm/modules/image/include/bilateral_filter.h` around lines 18 - 20, In
bilateral_filter, add validation to clamp the n_threads parameter into a safe
range to avoid division by zero and unsigned loop issues: compute a max_threads
= std::thread::hardware_concurrency() (fallback to 1 if it returns 0), then set
n_threads = std::max(1, std::min(n_threads, static_cast<int>(max_threads))); use
this validated n_threads when computing rows_per_thread and any loop bounds
(ensure casts to signed/size_t are deliberate), and replace any unsigned loop
comparisons that rely on possibly negative n_threads with iterations based on
the validated positive n_threads to prevent undefined behavior.

inline const std::vector<Node_ptr> &get_nodes() const { return *m_nodes; }

bool all_areas_bigger_than(int32_t min_area);
inline const size_t size() { return m_nodes->size(); }

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.

🛠️ Refactor suggestion | 🟠 Major

Fix const-correctness on size() method.

The const qualifier should be on the method (after the parameter list), not on the return type. const size_t as a return type is meaningless for value types.

♻️ Proposed fix
-  inline const size_t size() { return m_nodes->size(); }
+  inline size_t size() const { return m_nodes->size(); }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
inline const size_t size() { return m_nodes->size(); }
inline size_t size() const { return m_nodes->size(); }
🤖 Prompt for AI Agents
In `@src/wasm/modules/image/include/graph.h` at line 54, The size() method in
graph.h currently declares the return type as const (inline const size_t size())
which is meaningless for a value type; change the signature of the Graph::size()
method to put the const qualifier on the method instead (e.g., inline size_t
size() const) so it is a const-qualified accessor; update the
declaration/definition of size() in graph.h to remove the leading const on the
return type and append const after the parameter list to ensure
const-correctness.

Comment on lines 1 to +9
#include "kmeans.h"

float colorDistance(const RGB &a, const RGB &b) {
return std::sqrt((a.r - b.r) * (a.r - b.r) + (a.g - b.g) * (a.g - b.g) +
(a.b - b.b) * (a.b - b.b));
}

float colorDistance(const ImageLib::RGBAPixel<float> &a, const RGB &b) {
return std::sqrt((a.red - b.r) * (a.red - b.r) +
(a.green - b.g) * (a.green - b.g) +
(a.blue - b.b) * (a.blue - b.b));
}

float colorDistance(const ImageLib::RGBAPixel<float> &a,
const ImageLib::RGBAPixel<float> &b) {
#include "Image.h"
#include "PixelConverters.h"
#include "RGBAPixel.h"
#include <cmath>
#include <cstdlib>
#include <ctime>
#include <limits>
#include <vector>

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.

⚠️ Potential issue | 🟠 Major

Missing <cstring> header for std::memcpy.

Line 104 uses std::memcpy, but <cstring> is not included. This may compile on some toolchains due to transitive includes, but it's not guaranteed and violates the standard.

Proposed fix
 `#include` <cstdlib>
+#include <cstring>
 `#include` <ctime>
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
#include "kmeans.h"
float colorDistance(const RGB &a, const RGB &b) {
return std::sqrt((a.r - b.r) * (a.r - b.r) + (a.g - b.g) * (a.g - b.g) +
(a.b - b.b) * (a.b - b.b));
}
float colorDistance(const ImageLib::RGBAPixel<float> &a, const RGB &b) {
return std::sqrt((a.red - b.r) * (a.red - b.r) +
(a.green - b.g) * (a.green - b.g) +
(a.blue - b.b) * (a.blue - b.b));
}
float colorDistance(const ImageLib::RGBAPixel<float> &a,
const ImageLib::RGBAPixel<float> &b) {
#include "Image.h"
#include "PixelConverters.h"
#include "RGBAPixel.h"
#include <cmath>
#include <cstdlib>
#include <ctime>
#include <limits>
#include <vector>
`#include` "kmeans.h"
`#include` "Image.h"
`#include` "PixelConverters.h"
`#include` "RGBAPixel.h"
`#include` <cmath>
`#include` <cstdlib>
`#include` <cstring>
`#include` <ctime>
`#include` <limits>
`#include` <vector>
🧰 Tools
🪛 Clang (14.0.6)

[error] 1-1: 'kmeans.h' file not found

(clang-diagnostic-error)

🤖 Prompt for AI Agents
In `@src/wasm/modules/image/src/kmeans.cpp` around lines 1 - 9, Add the missing
<cstring> include so std::memcpy is declared: update the include block at the
top of kmeans.cpp (near the existing includes in this file) to add `#include`
<cstring>; this fixes the use of std::memcpy (referenced in the code around the
memcpy call) and avoids relying on transitive includes.

Comment on lines +8 to +20
XY Node::centroid() const {
XY centroid{0, 0};
for (auto &[_, pos] : *m_pixels) {
centroid.x += pos.x;
centroid.y += pos.y;
}

const int32_t m_pixels_size{static_cast<int32_t>(m_pixels->size())};
centroid.x /= m_pixels_size;
centroid.y /= m_pixels_size;

return centroid;
}

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.

⚠️ Potential issue | 🟠 Major

Division by zero if m_pixels is empty.

If m_pixels is empty, m_pixels_size will be 0, causing undefined behavior on lines 16-17.

🔧 Suggested fix
 XY Node::centroid() const {
+  if (m_pixels->empty()) {
+    return XY{0, 0};
+  }
+
   XY centroid{0, 0};
   for (auto &[_, pos] : *m_pixels) {
     centroid.x += pos.x;
     centroid.y += pos.y;
   }
 
   const int32_t m_pixels_size{static_cast<int32_t>(m_pixels->size())};
   centroid.x /= m_pixels_size;
   centroid.y /= m_pixels_size;
 
   return centroid;
 }
🤖 Prompt for AI Agents
In `@src/wasm/modules/image/src/node.cpp` around lines 8 - 20, Node::centroid
computes an average without guarding against an empty m_pixels, causing division
by zero; update Node::centroid to check m_pixels->empty() (or m_pixels_size ==
0) before dividing and return a safe default XY (e.g., {0,0} or another agreed
sentinel) or handle the empty case appropriately, ensuring all references to
m_pixels and the centroid calculation (centroid.x/centroid.y and m_pixels_size)
are skipped when empty.

Comment on lines +22 to +39
ImageLib::RGBPixel<uint8_t> Node::color() const {
float r{0};
float g{0};
float b{0};
for (auto &[color, _] : *m_pixels) {
r += color.red;
g += color.green;
b += color.blue;
}

const int32_t m_pixels_size{static_cast<int32_t>(m_pixels->size())};
r /= m_pixels_size;
g /= m_pixels_size;
b /= m_pixels_size;

return {static_cast<uint8_t>(r), static_cast<uint8_t>(g),
static_cast<uint8_t>(b)};
}

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.

⚠️ Potential issue | 🟠 Major

Same division by zero risk in color().

This function has the same vulnerability as centroid() when m_pixels is empty.

🔧 Suggested fix
 ImageLib::RGBPixel<uint8_t> Node::color() const {
+  if (m_pixels->empty()) {
+    return {0, 0, 0};
+  }
+
   float r{0};
   float g{0};
   float b{0};
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
ImageLib::RGBPixel<uint8_t> Node::color() const {
float r{0};
float g{0};
float b{0};
for (auto &[color, _] : *m_pixels) {
r += color.red;
g += color.green;
b += color.blue;
}
const int32_t m_pixels_size{static_cast<int32_t>(m_pixels->size())};
r /= m_pixels_size;
g /= m_pixels_size;
b /= m_pixels_size;
return {static_cast<uint8_t>(r), static_cast<uint8_t>(g),
static_cast<uint8_t>(b)};
}
ImageLib::RGBPixel<uint8_t> Node::color() const {
if (m_pixels->empty()) {
return {0, 0, 0};
}
float r{0};
float g{0};
float b{0};
for (auto &[color, _] : *m_pixels) {
r += color.red;
g += color.green;
b += color.blue;
}
const int32_t m_pixels_size{static_cast<int32_t>(m_pixels->size())};
r /= m_pixels_size;
g /= m_pixels_size;
b /= m_pixels_size;
return {static_cast<uint8_t>(r), static_cast<uint8_t>(g),
static_cast<uint8_t>(b)};
}
🤖 Prompt for AI Agents
In `@src/wasm/modules/image/src/node.cpp` around lines 22 - 39, The Node::color
method divides by m_pixels_size without guarding against an empty m_pixels,
risking division by zero; update Node::color to check that m_pixels is non-null
and not empty (e.g., if m_pixels->empty() or m_pixels_size == 0) before
performing the average, and return a sensible default color (such as {0,0,0}) or
handle the empty case similarly to centroid() to avoid the division; reference
m_pixels, m_pixels_size and Node::color when making the change.

Comment on lines +41 to +65
std::array<int32_t, 4> Node::bounding_box_xywh() const {
int32_t x_min{INT_MAX};
int32_t y_min{INT_MAX};
int32_t x_max{0};
int32_t y_max{0};
for (auto &[_, p] : *m_pixels) {
if (p.x < x_min) {
x_min = p.x;
}
if (p.x > x_max) {
x_max = p.x;
}
if (p.y < y_min) {
y_min = p.y;
}
if (p.y > y_max) {
y_max = p.y;
}
}

const int32_t w{x_max - x_min + 1};
const int32_t h{y_max - y_min + 1};

return std::array<int32_t, 4>{x_min, y_min, w, h};
}

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.

⚠️ Potential issue | 🟠 Major

Bounding box computation may fail for negative coordinates.

x_max and y_max are initialized to 0, but if all pixel coordinates are negative, the computed bounding box will be incorrect. Initialize them to INT_MIN for correctness.

🔧 Suggested fix
 std::array<int32_t, 4> Node::bounding_box_xywh() const {
+  if (m_pixels->empty()) {
+    return {0, 0, 0, 0};
+  }
+
   int32_t x_min{INT_MAX};
   int32_t y_min{INT_MAX};
-  int32_t x_max{0};
-  int32_t y_max{0};
+  int32_t x_max{INT_MIN};
+  int32_t y_max{INT_MIN};
   for (auto &[_, p] : *m_pixels) {
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
std::array<int32_t, 4> Node::bounding_box_xywh() const {
int32_t x_min{INT_MAX};
int32_t y_min{INT_MAX};
int32_t x_max{0};
int32_t y_max{0};
for (auto &[_, p] : *m_pixels) {
if (p.x < x_min) {
x_min = p.x;
}
if (p.x > x_max) {
x_max = p.x;
}
if (p.y < y_min) {
y_min = p.y;
}
if (p.y > y_max) {
y_max = p.y;
}
}
const int32_t w{x_max - x_min + 1};
const int32_t h{y_max - y_min + 1};
return std::array<int32_t, 4>{x_min, y_min, w, h};
}
std::array<int32_t, 4> Node::bounding_box_xywh() const {
if (m_pixels->empty()) {
return {0, 0, 0, 0};
}
int32_t x_min{INT_MAX};
int32_t y_min{INT_MAX};
int32_t x_max{INT_MIN};
int32_t y_max{INT_MIN};
for (auto &[_, p] : *m_pixels) {
if (p.x < x_min) {
x_min = p.x;
}
if (p.x > x_max) {
x_max = p.x;
}
if (p.y < y_min) {
y_min = p.y;
}
if (p.y > y_max) {
y_max = p.y;
}
}
const int32_t w{x_max - x_min + 1};
const int32_t h{y_max - y_min + 1};
return std::array<int32_t, 4>{x_min, y_min, w, h};
}
🤖 Prompt for AI Agents
In `@src/wasm/modules/image/src/node.cpp` around lines 41 - 65, In
Node::bounding_box_xywh(), x_max and y_max are initialized to 0 which yields
incorrect boxes when all pixel coordinates are negative; change their
initialization to INT_MIN (matching x_min/y_min using INT_MAX) so the loop
correctly computes extremes for negative coordinates and the returned x,y,w,h
are valid.

Comment thread vite.config.js
Comment on lines +116 to +126
// for multithreading
{
name: 'force-security-headers',
configureServer(server) {
server.middlewares.use((_req, res, next) => {
res.setHeader("Cross-Origin-Opener-Policy", "same-origin");
res.setHeader("Cross-Origin-Embedder-Policy", "require-corp");
next();
});
}
},

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.

⚠️ Potential issue | 🟡 Minor

Redundant header setting, but intentional for robustness.

The same COOP/COEP headers are set both in server.headers (lines 49-52) and via this middleware plugin. While this appears redundant, the middleware approach ensures headers are applied to all responses including HMR websocket upgrades, which server.headers may not cover consistently in all Vite versions. This is acceptable.

However, the pipeline indicates a Prettier formatting issue in this file. Please run prettier --write vite.config.js to fix formatting before merging.

🤖 Prompt for AI Agents
In `@vite.config.js` around lines 116 - 126, Prettier formatting is off in
vite.config.js; run the formatter and ensure the code around the plugin named
"force-security-headers" (the configureServer function that calls
server.middlewares.use and sets Cross-Origin-Opener-Policy and
Cross-Origin-Embedder-Policy headers) is properly formatted—run `prettier
--write vite.config.js` and commit the reformatted file so the configureServer
middleware block and surrounding server.headers remain consistently styled.

@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/kmeans.cpp`:
- Around line 61-65: The mutex around writes in _apply_labels is unnecessary
because each thread writes exclusively to its own range
labels[start_pixel..end_pixel) so remove the std::unique_lock<std::mutex>
lock(write_mutex); and the explicit lock.unlock(); and simply assign labels[i] =
best_cluster when labels[i] != best_cluster; keep the conditional and variable
references (labels, best_cluster, start_pixel, end_pixel, _apply_labels) intact
to preserve logic and avoid synchronization overhead.
- Around line 149-153: The code unconditionally sets changed = true after the
threaded label assignment, preventing early termination; modify the threaded
label update so _apply_labels reports whether it changed any labels (e.g.,
return a bool or increment a shared atomic<int> changed_count), run each thread
to produce a per-thread changed flag or increment the atomic, join/aggregate
those results in kmeans() and then set changed = (changed_count > 0) (or any
thread flag OR), and only then clear threads(); ensure to use an atomic or
mutex-safe per-thread storage when aggregating to avoid races.
♻️ Duplicate comments (1)
src/wasm/modules/image/src/kmeans.cpp (1)

1-13: Missing <mutex> header for std::mutex.

Line 22 uses std::mutex, but the <mutex> header is not included. This will cause a compilation error on strict toolchains.

Proposed fix
 `#include` <thread>
+#include <mutex>
 `#include` <functional>

Also, as noted in a prior review, <cstring> is needed for std::memcpy on line 222.

🧹 Nitpick comments (5)
src/wasm/modules/image/src/kmeans.cpp (5)

35-37: Redundant lock.unlock() with std::unique_lock.

std::unique_lock automatically releases the mutex when it goes out of scope at loop iteration end. The explicit unlock() is unnecessary.

Proposed simplification
-    std::unique_lock<std::mutex> lock(write_mutex);
-    std::copy(_res.begin(), _res.end(), output[j].begin());
-    lock.unlock();
+    {
+      std::lock_guard<std::mutex> lock(write_mutex);
+      std::copy(_res.begin(), _res.end(), output[j].begin());
+    }

22-22: Global mutex may cause contention with concurrent kmeans calls.

If kmeans is ever called concurrently (e.g., from multiple workers or contexts), this global mutex would serialize unrelated operations. Consider making the mutex local to each kmeans invocation and passing it to helper functions.


83-84: Edge case: when k < n_threads, most threads process zero centroids.

If k=3 and n_threads=8, then centroids_per_thread=0. Most threads will have start_c == end_c and do no work. Consider capping effective threads: int effective_threads = std::min(n_threads, k);


87-91: Consider rand_r or <random> for reproducibility and quality.

While srand/rand are safe here (called before threading), consider using <random> facilities for better distribution quality. This is optional since k-means is generally tolerant of initialization quality.


225-231: Noted: Spatial variant is marked as buggy.

The comment indicates known issues with channel indexing. This appears to be pre-existing code. Consider opening a tracking issue if not already done.

Would you like me to help identify the channel indexing bug mentioned in the comment, or open an issue to track this?

Comment thread src/wasm/modules/image/src/kmeans.cpp
Comment thread src/wasm/modules/image/src/kmeans.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/kmeans.cpp`:
- Around line 112-116: Loop variable type and thread count validation are wrong:
change the loop in the centroid-splitting logic to use a signed int for i
(matching n_threads), validate n_threads before use (ensure n_threads > 0 and
clamp/limit it to at most k), and recompute centroids_per_thread accordingly so
you don't get zero when k < n_threads (e.g., distribute at least one centroid to
the first k threads or compute start/end by integer division with remainders).
Update references in this block (n_threads, centroids_per_thread, k, and the for
loop that assigns start_c and end_c) to use the validated n_threads and the
corrected partitioning strategy.
- Around line 230-236: The exported function kmeans_clustering_spatial is buggy
and unused; either remove it or fix alpha handling: update the RGBXY struct (or
create RGBA-XY) to include an alpha field, change kmeans_clustering_spatial to
read input alpha from the source buffer when building pixel records, preserve
and carry alpha through any copies/assignments during clustering, and finally
write the original/preserved alpha values back into the output buffer; also
ensure the function's exported signature/documentation reflects RGBA
input/output if you keep it.
♻️ Duplicate comments (1)
src/wasm/modules/image/src/kmeans.cpp (1)

1-14: Missing <cstring> header for std::memcpy.

Line 227 uses std::memcpy, but <cstring> is not included. This was flagged in a previous review and remains unaddressed.

Proposed fix
 `#include` <cstdlib>
+#include <cstring>
 `#include` <ctime>
🧹 Nitpick comments (4)
src/wasm/modules/image/src/kmeans.cpp (3)

33-39: Unnecessary mutex in _process_dist_per_centroid - each thread writes to disjoint rows.

Each thread writes to output[j] for j ∈ [start_centroid, end_centroid), which are non-overlapping ranges. The mutex is pure overhead since there's no data race.

Proposed fix
   for (int j{start_centroid}; j < end_centroid; ++j) {
     std::transform(pixels.begin(), pixels.end(), _res.begin(), [&centroids, j](const ImageLib::RGBAPixel<float>& p) { return colorDistance(p, centroids[j]); });
-    std::unique_lock<std::mutex> lock(write_mutex);
     std::copy(_res.begin(), _res.end(), output[j].begin());
-    lock.unlock();
   }

63-68: Remove commented-out dead code.

The mutex lock was correctly removed per previous review feedback. Clean up the leftover commented code.

Proposed fix
     if (labels[i] != best_cluster) {
-      //std::unique_lock<std::mutex> lock(write_mutex);
       labels[i] = best_cluster;
       changed.store(true, std::memory_order_relaxed);
-      //lock.unlock();
     }

131-132: Remove debug artifacts.

Commented-out debug statements should be cleaned up before merging.

Also applies to: 183-185

src/components/WasmImageProcessor.jsx (1)

82-88: Remove debug timing statements before merge.

console.time/console.timeEnd calls are useful for development but should be removed or conditionally disabled for production builds. Additionally, the pipeline indicates Prettier formatting issues—these lines are missing semicolons.

Proposed fix
       step(20);
       // NOTE: Gaussian blur destroys the sharp outlines first, preventing the Bilateral filter from detecting and preserving them
-      console.time("bilateralFilter")
       const imgBilateralFiltered = await bilateralFilter({
         pixels: fileData.pixels,
         width,
         height,
       });
-      console.timeEnd("bilateralFilter")

       step(45);
       step(70);
-      console.time("kmeans")
       const { pixels: kmeansed, labels } = await kmeans({
         ...fileData,
         pixels: thresholded,
         num_colors: 16,
       });
-      console.timeEnd("kmeans")

Also applies to: 98-104

Comment on lines +112 to +116
for (unsigned int i = 0; i < n_threads; ++i) {

int start_c = i * centroids_per_thread;
int end_c = (i == n_threads - 1 ) ? k : (i + 1) * centroids_per_thread;

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.

⚠️ Potential issue | 🟡 Minor

Type mismatch between loop variable and n_threads.

n_threads is int but the loop uses unsigned int i. If n_threads is negative (invalid input), the comparison behaves unexpectedly. Also, if k < n_threads, centroids_per_thread is 0, so all threads except the last process nothing.

Proposed fix - add validation and fix type
+  const int effective_threads = std::min(n_threads, std::max(1, k));
-  for (unsigned int i = 0; i < n_threads; ++i) {
+  for (int i = 0; i < effective_threads; ++i) {
🤖 Prompt for AI Agents
In `@src/wasm/modules/image/src/kmeans.cpp` around lines 112 - 116, Loop variable
type and thread count validation are wrong: change the loop in the
centroid-splitting logic to use a signed int for i (matching n_threads),
validate n_threads before use (ensure n_threads > 0 and clamp/limit it to at
most k), and recompute centroids_per_thread accordingly so you don't get zero
when k < n_threads (e.g., distribute at least one centroid to the first k
threads or compute start/end by integer division with remainders). Update
references in this block (n_threads, centroids_per_thread, k, and the for loop
that assigns start_c and end_c) to use the validated n_threads and the corrected
partitioning strategy.

Comment on lines +230 to +236
/*
*BUGGY CODE BELOW - one of the channels is poorly indexed
*
* TODO: The kmeans algorithm actually ignores the values of alpha where it
*should actually be taken into account.
*/

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.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check if kmeans_clustering_spatial is used or exported anywhere
echo "=== Checking for usages of kmeans_clustering_spatial ==="
rg -n "kmeans_clustering_spatial" --type cpp --type h

echo ""
echo "=== Checking if it's exported via Emscripten bindings ==="
rg -n "kmeans_clustering_spatial" -g "*.js" -g "*.ts" -g "CMakeLists.txt"

Repository: Ryan-Millard/Img2Num

Length of output: 419


🏁 Script executed:

#!/bin/bash
# Read the kmeans_clustering_spatial function implementation
echo "=== Reading kmeans_clustering_spatial function in kmeans.cpp ==="
sed -n '230,290p' src/wasm/modules/image/src/kmeans.cpp

echo ""
echo "=== Check what EXPORTED means in this codebase ==="
rg -n "EXPORTED" src/wasm/modules/image/include/kmeans.h -A 2 -B 2

echo ""
echo "=== Check if EXPORTED is used elsewhere ==="
rg -n "define EXPORTED" --type cpp --type h

Repository: Ryan-Millard/Img2Num

Length of output: 3188


🏁 Script executed:

#!/bin/bash
# Search for kmeans_clustering_spatial in all JS/TS files and bindings
echo "=== Searching for kmeans_clustering_spatial in JS/TS files ==="
find . -type f \( -name "*.js" -o -name "*.ts" -o -name "*.jsx" -o -name "*.tsx" \) -exec grep -l "kmeans_clustering_spatial" {} \;

echo ""
echo "=== Search entire repo for kmeans_clustering_spatial (excluding node_modules) ==="
rg "kmeans_clustering_spatial" --type-list | head -20
rg "kmeans_clustering_spatial" -g "!node_modules" 2>/dev/null | head -30

Repository: Ryan-Millard/Img2Num

Length of output: 849


🏁 Script executed:

#!/bin/bash
# Get more of the function to see the actual buggy indexing
sed -n '252,320p' src/wasm/modules/image/src/kmeans.cpp

Repository: Ryan-Millard/Img2Num

Length of output: 2330


🏁 Script executed:

#!/bin/bash
# Get the end of the function to see the buggy indexing and alpha handling
sed -n '320,360p' src/wasm/modules/image/src/kmeans.cpp

Repository: Ryan-Millard/Img2Num

Length of output: 766


Remove or fix the kmeans_clustering_spatial function before merge.

This function is marked as EXPORTED (public WASM API) but has confirmed bugs: it ignores the alpha channel entirely when reading input data, and never writes it back, causing data corruption. Additionally, no C++ or JavaScript code in the repository calls this function, making it dead/unused code. Either fix the alpha channel handling (include it in the RGBXY struct and write it back to output) or remove the function entirely.

🤖 Prompt for AI Agents
In `@src/wasm/modules/image/src/kmeans.cpp` around lines 230 - 236, The exported
function kmeans_clustering_spatial is buggy and unused; either remove it or fix
alpha handling: update the RGBXY struct (or create RGBA-XY) to include an alpha
field, change kmeans_clustering_spatial to read input alpha from the source
buffer when building pixel records, preserve and carry alpha through any
copies/assignments during clustering, and finally write the original/preserved
alpha values back into the output buffer; also ensure the function's exported
signature/documentation reflects RGBA input/output if you keep it.

@Ryan-Millard

Copy link
Copy Markdown
Owner

Hi. Please see my comment on the related issue. Please also start using the PR templates or at least filling in the body of the PR so I can understand everything that has changed - it makes it a bit difficult for me to understand the changes when all I have to look at is code and minimal documentation.

@Ryan-Millard

Copy link
Copy Markdown
Owner

continues from #219 - check last commit

You should target #219's branch then. I thought this was a standalone feature.😂😂

This is a completely different feature, so I'd like this to not be a continuation of #219. Once #219 is merged, this branch can be updated and reviewed.

@Ryan-Millard

Copy link
Copy Markdown
Owner

@Krasner please also see #223 and #230.

@Krasner Krasner mentioned this pull request Jan 18, 2026
@Krasner
Krasner changed the base branch from main to feat/contour-tracing January 18, 2026 14:24
@Krasner

Krasner commented Jan 18, 2026

Copy link
Copy Markdown
Collaborator Author

@Ryan-Millard updated to target feat/contour-tracing you can see the changes much better now

@Ryan-Millard
Ryan-Millard deleted the branch Ryan-Millard:feat/contour-tracing January 18, 2026 20:44
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.

2 participants