Skip to content

Feat/svg builder3 - #254

Closed
Krasner wants to merge 34 commits into
Ryan-Millard:mainfrom
Krasner:feat/svg-builder3
Closed

Feat/svg builder3#254
Krasner wants to merge 34 commits into
Ryan-Millard:mainfrom
Krasner:feat/svg-builder3

Conversation

@Krasner

@Krasner Krasner commented Feb 6, 2026

Copy link
Copy Markdown
Collaborator

Note: rebasing is a mess. but this should be up-to-date with main.

A few updates to get smoother contours possibly:

  1. SavitzkyGolay new method filter_wrap_with_constraints tries to prevent points on the borders from moving. https://github.com/Krasner/Img2Num/blob/0c610818b03a25a51e7321f40b29ebc1bc3b2742/src/wasm/modules/image/src/contours.cpp#L781C32-L781C60
    SG filter has some ringing to it, so further apply binomial smoothing
    https://github.com/Krasner/Img2Num/blob/0c610818b03a25a51e7321f40b29ebc1bc3b2742/src/wasm/modules/image/src/contours.cpp#L782
  2. Support of Cubic Bezier curves. They add a 2nd control point, so they can make more complex shapes. Potentially helping with loops. https://github.com/Krasner/Img2Num/blob/0c610818b03a25a51e7321f40b29ebc1bc3b2742/src/wasm/modules/image/include/contours.h#L22
  3. Since contours are all loops update any iteration over contour points to wrap around. Some previous code had
    for (int i = 1; i < pts.size() - 1; i++) where looking back would be i-1 and looking ahead is i+1. now all those loops are for (int i = 0; i < pts.size() ; i++) with the correct logic to wrap around. This prevents some distortion on the end points.

Summary by CodeRabbit

  • Documentation

    • Expanded graph module docs with a new Diagram section, multiple illustrative slides, and richer explanatory text.
  • New Features

    • Added cubic Bezier curve fitting and SVG export alongside existing quadratic support.
    • Enhanced contour processing: improved smoothing, broader neighbor/circular handling, corner detection, and constrained smoothing that respects locked points.

Ryan-Millard and others added 29 commits February 4, 2026 02:52
… labels (Ryan-Millard#248)

* ci(stale.yml): Configure stale workflow with new settings

Updated stale issue and PR messages, added days before stale and close settings, and exempt labels.

* style(stale-workflow): format properly

* deps(stale-workflow): bump to newer v10

* ci(stale.yml): fix label inputs

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>

* docs(PR-template): update PR template

* ci: add PR auto-label workflow and labeler config

* ci: PR heading and body checker

* fix: workflow does not contain permissions

Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>

* style(workflows): format new workflow files

* ci(deps): bump labeler to v6

* ci(pr-check): fix permissions

---------

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
- simplify useWasmWorker convenience functions
- add type checks for arguments and return types
- add better typing and guarding system against bad values
	- no TypeScript as its benefit here would not be felt
@Krasner
Krasner requested a review from Ryan-Millard February 6, 2026 04:20
@github-actions github-actions Bot added docs c/c++ Changes to C or C++ files labels Feb 6, 2026
@coderabbitai

coderabbitai Bot commented Feb 6, 2026

Copy link
Copy Markdown
Contributor
📝 Walkthrough

Walkthrough

Adds cubic Bezier support and constrained smoothing to the image pipeline: new data structures and fitting for CubicBezier, constrained Savitzky–Golay filtering with corner/locked-point handling, updated contour processing and SVG generation, plus minor JS formatting and docs additions.

Changes

Cohort / File(s) Summary
Documentation
docs/docs/reference/wasm/modules/image/graph/explained.md
Added Diagram section with multiple slides and explanatory text; duplicated slide/figure blocks and an extra PR checkout snippet.
JS hook formatting
src/hooks/useWasmWorker.js
Reformatted bilateralFilter parameter destructuring to multi-line; no behavioral change.
Savitzky–Golay header
src/wasm/modules/image/include/SavitskyGolay.h
Declared solveQuadraticAtZero and new public filter_wrap_with_constraints API for constrained wrap-around filtering.
Savitzky–Golay impl
src/wasm/modules/image/src/SavitskyGolay.cpp
Implemented filter_wrap_with_constraints and solveQuadraticAtZero to apply per-sample locked/corner constraints during smoothing.
Bezier headers
src/wasm/modules/image/include/bezier.h
Added overload for fit_curve_reduction returning std::vector<std::vector<CubicBezier>> (new CubicBezier path declaration).
Contours header
src/wasm/modules/image/include/contours.h
Added CubicBezier struct, ContoursResult.ccurves, included <numeric>, and changed coupled_smooth to accept pairRadiusSq.
Bezier implementation
src/wasm/modules/image/src/bezier.cpp
Added evalCubic, generateCubicBezier, cubic fitRecursive, and cubic fit_curve_reduction overloads; adjusted quad base-case handling.
Contours implementation
src/wasm/modules/image/src/contours.cpp
Expanded corner detection neighborhood, added radius parameter to selectiveSmooth, added constrained SG smoothing and cornerMasks plumbing, adjusted coupling logic and circular indexing.
Graph computation
src/wasm/modules/image/src/graph.cpp
Updated compute_contours to produce both quad and cubic curve sets (all_curves + all_ccurves), adjusted smoothing parameter, and replaced element-wise copies with direct assignments.
SVG generation
src/wasm/modules/image/src/labels_to_svg.cpp
Added contourToSVGCurve overload for std::vector<CubicBezier>, ensured M/C command generation and closing Z, and aggregated cubic contours into output.
Node management
src/wasm/modules/image/src/node.cpp
Clears and resizes new m_contours.ccurves alongside existing contour containers during clear/compute.

Sequence Diagram

sequenceDiagram
    participant Client as Graph::compute_contours
    participant Contours as Contour Pipeline
    participant SG as SavitskyGolay
    participant Bezier as Bezier Fitting
    participant SVG as labels_to_svg

    Client->>Contours: provide raw contours + locked masks
    Contours->>Contours: detectCorners (2-step neighbors)
    Contours->>SG: filter_wrap_with_constraints(contours, locked, cornerMasks)
    SG->>SG: solveQuadraticAtZero per-sample constrained solve
    SG->>Contours: return smoothed contours
    Contours->>Contours: coupledSmooth / selectiveSmooth (radius, pairRadiusSq)
    Contours->>Bezier: fit_curve_reduction (quad & cubic overloads)
    Bezier->>Bezier: generateCubicBezier / fitRecursive
    Bezier->>Client: return QuadBezier & CubicBezier sets
    Client->>SVG: contourToSVGCurve(all_curves, all_ccurves)
    SVG->>SVG: emit path data (Q and C commands) and finalize SVG
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

  • PR #238: Strongly related — modifies the same image→SVG pipeline (labels_to_svg, bezier/contours, graph, node) and overlaps on contour/curve handling.
  • PR #219: Related — touches contour/graph processing components (contours, graph, node) that this change extends with cubic support and constrained smoothing.
  • PR #191: Related — modifies the bilateralFilter surface that was reformatted in this change (JS hook).

Suggested labels

C/C++, wasm, enhancement

Suggested reviewers

  • Ryan-Millard

Poem

🐇 I hop through points both locked and free,

I smooth the curves where corners be,
Cubic loops now join the song,
SVG sings the path along,
Hooray — the rabbit drew them strong! 🎨

🚥 Pre-merge checks | ✅ 1 | ❌ 2
❌ Failed checks (1 warning, 1 inconclusive)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 40.00% 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 title 'Feat/svg builder3' is vague and generic, using a branch-naming convention rather than a clear description of the main changes. It does not convey meaningful information about the substantive work. Consider revising the title to something more descriptive, such as 'Add cubic Bezier curves and constrained smoothing for contours' or 'Improve contour rendering with cubic curves and corner-aware smoothing.'
✅ 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
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

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: 13

Caution

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

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

169-189: ⚠️ Potential issue | 🟠 Major

The contourToSVGCurve(CubicBezier) overload is dead code and ccurves are never rendered.

contoursResultToSVG (line 198) loops over result.curves (QuadBezier) only. Meanwhile, result.ccurves (CubicBezier) are accumulated at lines 283–284 but never passed to any rendering function. The cubic Bézier overload at line 169 is never called.

Either update contoursResultToSVG to use ccurves (replacing or preferring them over curves), or remove the unused accumulation and overload.

🤖 Fix all issues with AI agents
In `@docs/docs/reference/wasm/modules/image/graph/explained.md`:
- Around line 39-67: Delete the duplicated block that repeats the "Diagram",
graph visualization, "Small Area Node Merging (Small Region Pruning)" and
"Example" sections (the repeated Slide1/Slide2/Slide3 discussion and images),
remove the consecutive duplicate image reference (![slide1] to Slide1.SVG),
normalize image filenames to lowercase (.svg) so they match the original
references, and fix the typo "Similarily" → "Similarly" in the Small Area Node
Merging paragraph so only the first occurrence of these sections and images
remain (look for headings "Diagram" and "Small Area Node Merging" and image
names Slide1, Slide2, Slide3).
- Around line 135-146: Remove the duplicated <details> block that contains the
"Pull the code locally from PR `#238`" summary and the bash snippet; keep only one
instance of that block (the original on lines earlier in the file) so the
instructions aren’t repeated, and ensure the surrounding Markdown remains valid
(matching opening and closing <details> and ``` fences).

In `@docs/docs/reference/wasm/modules/image/graph/explained2.md`:
- Around line 105-130: Add documentation for the newly introduced CubicBezier
struct and the ContoursResult::ccurves member: insert a "### `struct
CubicBezier`" section after the QuadBezier section that documents members `p0`,
`p1`, `p2`, `p3` (each as Point with start/control/control/end descriptions),
and add a row to the ContoursResult member table describing `ccurves` with type
`vector<vector<CubicBezier>>` and a short description like "Vectorized cubic
Bezier representation of contours; ccurves[k] contains the cubic segments for
the k-th contour."

In `@src/components/WasmImageProcessor.jsx`:
- Around line 96-99: The code contains a duplicated progress update call
step(95) in the WasmImageProcessor component — remove the redundant second
invocation so progress is only set once; locate the duplicate step(95) calls in
the function where step is used (e.g., the progress update block inside
WasmImageProcessor.jsx) and delete the repeated call, leaving a single step(95)
to avoid the no-op and clarify intent.

In `@src/hooks/useWasmWorker.js`:
- Around line 61-69: Prettier CI is failing for src/hooks/useWasmWorker.js due
to formatting in the bilateralFilter parameter destructuring; run the formatter
(e.g., prettier --write src/hooks/useWasmWorker.js) or reformat the
bilateralFilter async ({ ... }) block to match the project's Prettier style used
elsewhere (see kmeans function parameter formatting) so the file passes CI.

In `@src/wasm/modules/image/src/bezier.cpp`:
- Around line 289-306: fit_curve_reduction currently calls chains[i].front()
unconditionally which is UB for empty chains; modify fit_curve_reduction to
check if chains[i].empty() before accessing front(): if empty, either push an
empty vector<CubicBezier> into results (to preserve indexing) or continue to the
next chain, otherwise build c by copying points and appending chains[i].front()
and then call fitRecursive(c, tolerance, result); ensure you refer to
fit_curve_reduction, chains, results, fitRecursive, Point and CubicBezier when
making the change.

In `@src/wasm/modules/image/src/contours.cpp`:
- Around line 742-744: Remove the debug stdout prints inside selectiveSmooth
that print kernel coefficients; locate the loop that outputs coeff[k] and the
trailing std::endl (the std::cout << coeff[k] << " "; and std::cout <<
std::endl; statements) and delete them so no kernel coefficients are printed to
stdout on each call.
- Around line 700-710: The loop uses size_t i causing unsigned underflow when
computing previ = i - 2; change the loop to use a signed index (e.g., for (int i
= 0; i < (int)pts.size(); ++i)) or cast i to int before arithmetic (int ii =
(int)i; int previ = ii - 2; int nexti = ii + 2;) and keep the existing wrap
logic for previ/nexti so negative values are handled correctly; adjust all uses
of i in this loop to the signed index (ii) or the new int i.
- Around line 806-810: The next-index conditional is inverted and the prev/next
Point variables are dead: change the nextp computation to use "p <
contours[c].size() - 1 ? p + 1 : 0" instead of the current comparison, and then
either remove the unused Point prev and Point next declarations (prevp/nextp may
remain if indexes are used) or keep them if you will reference prev/next later;
specifically update the variables shown (Point myPos, int prevp, int nextp,
Point prev, Point next) so there are no out-of-bounds index calculations and no
unused variables left.

In `@src/wasm/modules/image/src/labels_to_svg.cpp`:
- Around line 283-285: The loop that pushes node_contours.ccurves into
all_contours.ccurves is doing unnecessary work because contoursResultToSVG does
not use all_contours.ccurves; remove the accumulation to avoid wasted work
(delete the for-loop that copies node_contours.ccurves into
all_contours.ccurves) or, if you prefer to keep the intent, wrap it behind a
clear TODO/feature flag and comment referencing future cubic curve rendering;
update or remove any dependent comments and ensure contoursResultToSVG continues
to operate on the used contours fields only.

In `@src/wasm/modules/image/src/SavitskyGolay.cpp`:
- Around line 82-84: The comment above the weight declarations in
SavitskyGolay.cpp is inconsistent: it says "Fixed = 10000.0 (Hard constraint)"
while the code defines float wFixed = 100.0; update either the comment or the
value so they match—preferably change the comment to "Fixed = 100.0 (Hard
constraint)" or, if the intention was 10000.0, change the initializer for wFixed
to 10000.0; ensure you update the adjacent comment referencing wNormal and
wFixed so the message and the actual values remain consistent.
- Around line 86-98: The loop uses size_t i and int j then computes int k = i +
j, causing signed/unsigned mismatch and UB; fix by converting i to a signed type
before arithmetic (e.g., int idx = static_cast<int>(i); then use int k = idx +
j) and apply the same change in the other occurrence in filter_wrap; update uses
of i in this loop (and any wrap-around adjustments) to use idx to avoid implicit
conversions.
- Around line 214-236: The function solveQuadraticAtZero currently returns 0.0
on near-singular matrices which maps the point to the origin; change its
signature to accept a fallback value (e.g., float fallback) and when
std::abs(det) < 1e-9 return that fallback instead of 0.0, and update all call
sites that invoke solveQuadraticAtZero to pass the center sample (original data
value) as the fallback so ill-conditioned solves preserve the original point
rather than teleporting it to (0,0).
🧹 Nitpick comments (10)
src/components/WasmImageProcessor.jsx (1)

107-107: Commented-out step(100) — clean up or restore.

Leaving commented-out code in the pipeline is a maintenance smell. If the intent is to skip the 100% step because navigate fires immediately after findContours, remove the line entirely and add a brief comment explaining why. If it was disabled temporarily for debugging, restore it.

Proposed fix (if intentionally skipped)
-     //step(100);
+     // Progress intentionally skipped — navigate fires immediately after findContours
src/wasm/modules/image/include/SavitskyGolay.h (1)

25-25: corner parameter is accepted but unused in the implementation.

Looking at the implementation in SavitskyGolay.cpp (lines 68–137 in the relevant snippets), the corner vector is never read — both usages are commented out (// if (locked[i] || corner[i]) { continue; } and // || corner[k]). If corner detection is planned for later, consider documenting that intent; otherwise this is dead surface area in the public API.

Also, the implementation comment says "Fixed = 10000.0 (Hard constraint)" but the code sets wFixed = 100.0.

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

14-14: Move #include <numeric> from the header to contours.cpp.

The header does not use any <numeric> symbols, but contours.cpp uses std::accumulate at line 739 and currently relies on the transitive include. Moving the include directly to the translation unit that needs it reduces unnecessary header dependencies.

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

200-201: Remove commented-out code.

These lines are debug leftovers. If the QuadBezier path is the intended code path, remove the commented lines to reduce noise.

🧹 Proposed cleanup
   for (size_t i = 0; i < result.curves.size(); ++i) {
     std::string pathData = contourToSVGCurve(result.curves[i]);
-  //for (size_t i = 0; i < result.contours.size(); ++i) {
-    //std::string pathData = contourToSVGPath(result.contours[i]);
src/wasm/modules/image/src/graph.cpp (1)

295-307: Redundant .resize() before direct assignment; clean up commented-out code.

Lines 299 and 304 call .resize() on c0->curves[i] and c0->ccurves[i] respectively, but the immediately following = assignment (lines 302, 307) replaces the entire vector, making the resize unnecessary.

Also, the commented-out std::copy blocks (lines 295–296, 300–301, 305–306) should be removed.

🧹 Proposed cleanup
     for (size_t i = 0; i < c0->contours.size(); ++i) {
-      //std::copy(all_contours[j].begin(), all_contours[j].end(),
-      //          c0->contours[i].begin());
       c0->contours[i] = all_contours[j];
 
-      c0->curves[i].resize(all_curves[j].size());
-      //std::copy(all_curves[j].begin(), all_curves[j].end(),
-      //          c0->curves[i].begin());
       c0->curves[i] = all_curves[j];
 
-      c0->ccurves[i].resize(all_ccurves[j].size());
-      //std::copy(all_ccurves[j].begin(), all_ccurves[j].end(),
-      //          c0->ccurves[i].begin());
       c0->ccurves[i] = all_ccurves[j];
       j++;
     }
src/wasm/modules/image/src/SavitskyGolay.cpp (1)

69-73: corner parameter is unused.

The corner vector is accepted but never referenced in the function body (the check at line 91 is commented out). Either use it or remove it from the signature to avoid confusion.

src/wasm/modules/image/src/contours.cpp (4)

725-738: Recursive factorial via std::function is fragile and slow.

std::function has overhead from type-erasure. More importantly, the integer factorial will overflow int for inputs ≥ 13 (and int is typically 32-bit). Currently safe with radius=2 (window=5, max factorial(4)=24), but any future caller passing a larger radius will silently overflow.

Consider a simple iterative approach or just precompute binomial coefficients directly (e.g., Pascal's triangle row), which avoids factorial entirely.

♻️ Alternative: build binomial row iteratively
-  std::function<int(int)> factorial = [&factorial](int x) -> int {
-    if (x <= 1) return 1;
-    return x * factorial(x-1);
-  };
-
   std::vector<Point> original = pts;
   int window = 2 * radius + 1;
 
   std::vector<float> coeff(window);
-  // binomial coefficients
-  for (int k = 0; k < window; ++k){
-    //n!/((n-k)! * k!)
-    coeff[k] = static_cast<float>(factorial(window-1)/ (factorial(k) * factorial(window-1-k)));
-  }
+  // Binomial coefficients via iterative construction (no overflow risk for moderate window)
+  coeff[0] = 1.0f;
+  for (int k = 1; k < window; ++k) {
+    coeff[k] = coeff[k - 1] * static_cast<float>(window - k) / static_cast<float>(k);
+  }

763-767: Remove commented-out code.

The old Laplacian smoothing block is commented out. Clean it up to reduce noise.


776-783: SavitzkyGolay(5, 2) constructor computes coefficients that filter_wrap_with_constraints ignores.

The constructor at line 776 precomputes smoothing coefficients (coeffs_) for radius=5, poly_order=2. However, filter_wrap_with_constraints builds its own per-sample normal equations using only window_radius_ for the loop range and hardcodes a 3×3 (quadratic) system. The precomputed coeffs_ are never used by this code path.

This is not a bug, but the wasted computation and the coupling between the constructor's polynomial order and the hardcoded 3×3 matrix in filter_wrap_with_constraints is a maintainability concern. If someone changes poly_order_ expecting it to affect the constrained filter, it won't.


846-847: Commented-out code in partner target calculation.

Lines 846–847 are remnants of the old direct-neighbor approach. Remove them.

Comment on lines +39 to +67
## Diagram
Assume a region partition image as shown. Each partition (and the pixels contained within) are represented as a Node. Nodes exist in the heap. A Graph is a collection of shared_ptr(s) to these Nodes. A Graph has unique ownership over these Nodes pointers.
![slide1](./diagrams/Slide1.SVG)

![slide1](./diagrams/Slide1.SVG)
_Figure 1: Partitioned Image_

Below is a visualization of the Graph with its connections forming a undirected graph (a). In reality each `Node` contains a list of pointers to their neighbors, represented as edges (b). Note that adjacent nodes point to each other. Edge management is handled by the Graph.
![slide2](./diagrams/Slide2.SVG)
_Figure 2: Visualizing a Graph. Note in (b) only 3 nodes are displayed 0,1,3. The remaining nodes have a similar structure_

## Small Area Node Merging (Small Region Pruning)

One of the Graph's main operations is Node merging. Suppose we want to absorb/merge region 0 (Node 0) into region 2 (Node 2). Node 2 will assume ownership of Nodes 0's pixels and neighbors. The following figure shows the step-by-step process that happens in Graph.

![slide3](./diagrams/Slide3.SVG)
_Figure 3: Step-by-step visualization of node merging_

**a**. Consider merging Node 0 into Node 2

**b**. Disconnect edges to Node 0 neighbors. This requires iterating over Nodes 1, 2, and 3, and removing Node 0 from their edge set.

**c**. Transferring edges to absorbing node (Node 2). Again, iterate over Nodes 1 and 3 to assign Node 2 as an edge. Similarily Node 2 adds Node 1 and 3 as edges. In this case Node 2 and Node 3 already share an edge, but Node 1 gets a new edge.

**d**. Pixels owned by Node 0 are passed to Node 2. Node 0 is removed from the graph.

**e**. In image space, Node 2 (region 2) now contains the area that used to be Node 0's.

## Example

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

Remove duplicated content sections.

Lines 39–67 duplicate the existing content from lines 9–36 (Diagram, Graph visualization, Small Area Node Merging, and Example headings). This looks like a rebase artifact. Additionally:

  • Line 41/43: .SVG (uppercase) vs .svg (lowercase, line 13) — will break on case-sensitive filesystems.
  • Line 43: ![slide1] image is rendered twice consecutively.
  • Line 61: Typo — "Similarily" → "Similarly".
  • Lines 50 and 67: Duplicate headings trigger markdownlint MD024.

Remove the entire block (lines 39–67) to eliminate the duplication.

🧰 Tools
🪛 LanguageTool

[grammar] ~61-~61: Ensure spelling is correct
Context: ...es 1 and 3 to assign Node 2 as an edge. Similarily Node 2 adds Node 1 and 3 as edges. In t...

(QB_NEW_EN_ORTHOGRAPHY_ERROR_IDS_1)

🪛 markdownlint-cli2 (0.20.0)

[warning] 50-50: Multiple headings with the same content

(MD024, no-duplicate-heading)


[warning] 67-67: Multiple headings with the same content

(MD024, no-duplicate-heading)

🤖 Prompt for AI Agents
In `@docs/docs/reference/wasm/modules/image/graph/explained.md` around lines 39 -
67, Delete the duplicated block that repeats the "Diagram", graph visualization,
"Small Area Node Merging (Small Region Pruning)" and "Example" sections (the
repeated Slide1/Slide2/Slide3 discussion and images), remove the consecutive
duplicate image reference (![slide1] to Slide1.SVG), normalize image filenames
to lowercase (.svg) so they match the original references, and fix the typo
"Similarily" → "Similarly" in the Small Area Node Merging paragraph so only the
first occurrence of these sections and images remain (look for headings
"Diagram" and "Small Area Node Merging" and image names Slide1, Slide2, Slide3).

Comment on lines +135 to +146
<details>
<summary>Pull the code locally from PR #238</summary>
```bash
# 1. Clone the repo (if you haven't already)
git clone https://github.com/Ryan-Millard/Img2Num.git
cd Img2Num
# 2. Fetch the specific commit from the PR
git fetch origin 9eb23f9a56edaeec95e2dfcfc8389b11bfd777b6
# 3. Create a local branch pointing at it
git checkout -b try-pr-238 9eb23f9a56edaeec95e2dfcfc8389b11bfd777b6
```
</details>

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

Remove duplicated PR checkout instructions.

This <details> block is identical to the one on lines 122–133. Likely another rebase artifact.

Proposed fix
-<details>
-<summary>Pull the code locally from PR `#238`</summary>
-```bash
-# 1. Clone the repo (if you haven't already)
-git clone https://github.com/Ryan-Millard/Img2Num.git
-cd Img2Num
-# 2. Fetch the specific commit from the PR
-git fetch origin 9eb23f9a56edaeec95e2dfcfc8389b11bfd777b6
-# 3. Create a local branch pointing at it
-git checkout -b try-pr-238 9eb23f9a56edaeec95e2dfcfc8389b11bfd777b6
-```
-</details>
-
📝 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
<details>
<summary>Pull the code locally from PR #238</summary>
```bash
# 1. Clone the repo (if you haven't already)
git clone https://github.com/Ryan-Millard/Img2Num.git
cd Img2Num
# 2. Fetch the specific commit from the PR
git fetch origin 9eb23f9a56edaeec95e2dfcfc8389b11bfd777b6
# 3. Create a local branch pointing at it
git checkout -b try-pr-238 9eb23f9a56edaeec95e2dfcfc8389b11bfd777b6
```
</details>
🤖 Prompt for AI Agents
In `@docs/docs/reference/wasm/modules/image/graph/explained.md` around lines 135 -
146, Remove the duplicated <details> block that contains the "Pull the code
locally from PR `#238`" summary and the bash snippet; keep only one instance of
that block (the original on lines earlier in the file) so the instructions
aren’t repeated, and ensure the surrounding Markdown remains valid (matching
opening and closing <details> and ``` fences).

Comment on lines +105 to +130
### `struct QuadBezier`

Represents a Quadratic Bezier curve segment. This is used when the raw pixel contours are approximated or smoothed into vector paths.

| Member | Type | Description |
| :----- | :------ | :--------------------------------------------------------------------------------------------------------------------------------- |
| `p0` | `Point` | **Start Point:** The anchor point where the curve begins. |
| `p1` | `Point` | **Control Point:** The handle that determines the curve's tangent and shape. The curve generally does not pass through this point. |
| `p2` | `Point` | **End Point:** The anchor point where the curve ends. |

---

## 2. Contour Results

### `struct ContoursResult`

The primary container for the output of the contour extraction algorithm (e.g., Suzuki-Abe). It separates the raw pixel data from the topological relationship data.

#### Member Variables

| Member Variable | Type | Description |
| :-------------- | :--------------------------- | :-------------------------------------------------------------------------------------------------------------------- |
| `contours` | `vector<vector<Point>>` | A list of contours. `contours[k]` is a vector of `Point`s tracing the boundary of the $k$-th region. |
| `curves` | `vector<vector<QuadBezier>>` | A vectorized representation of `contours`. `curves[k]` contains the Bezier segments approximating the $k$-th contour. |
| `hierarchy` | `vector<array<int, 4>>` | Topological tree structure describing how contours nest within each other (see **Hierarchy Structure** below). |
| `is_hole` | `vector<bool>` | Flags the type of border. `true` if `contours[k]` is an internal hole; `false` if it is an external boundary. |

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

Documentation is missing the new CubicBezier struct and ccurves member.

This PR adds struct CubicBezier and ContoursResult::ccurves in contours.h, but this documentation page only covers QuadBezier and omits ccurves from the ContoursResult member table (line 125–130). Consider adding:

  1. A ### struct CubicBezier section (after QuadBezier) documenting p0, p1, p2, p3.
  2. A row for ccurves (vector<vector<CubicBezier>>) in the ContoursResult table.
🤖 Prompt for AI Agents
In `@docs/docs/reference/wasm/modules/image/graph/explained2.md` around lines 105
- 130, Add documentation for the newly introduced CubicBezier struct and the
ContoursResult::ccurves member: insert a "### `struct CubicBezier`" section
after the QuadBezier section that documents members `p0`, `p1`, `p2`, `p3` (each
as Point with start/control/control/end descriptions), and add a row to the
ContoursResult member table describing `ccurves` with type
`vector<vector<CubicBezier>>` and a short description like "Vectorized cubic
Bezier representation of contours; ccurves[k] contains the cubic segments for
the k-th contour."

Comment thread src/components/WasmImageProcessor.jsx Outdated
Comment on lines +61 to +69
bilateralFilter: async ({
pixels,
width,
height,
sigma_spatial = 3,
sigma_range = 50,
color_space = 0,
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 | 🟡 Minor

Formatting change looks good, but Prettier CI is failing on this file.

The multi-line destructuring aligns with other functions in this file (e.g., kmeans at Line 87). However, the CI pipeline reports a Prettier formatting issue on this file. Please run prettier --write to resolve it before merging.

🤖 Prompt for AI Agents
In `@src/hooks/useWasmWorker.js` around lines 61 - 69, Prettier CI is failing for
src/hooks/useWasmWorker.js due to formatting in the bilateralFilter parameter
destructuring; run the formatter (e.g., prettier --write
src/hooks/useWasmWorker.js) or reformat the bilateralFilter async ({ ... })
block to match the project's Prettier style used elsewhere (see kmeans function
parameter formatting) so the file passes CI.

Comment thread src/wasm/modules/image/src/contours.cpp
Comment on lines +283 to +285
for (auto &c : node_contours.ccurves) {
all_contours.ccurves.push_back(c);
}

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

Accumulating ccurves with no consumer.

As noted above, all_contours.ccurves is populated here but contoursResultToSVG does not use it. This is unnecessary work until cubic curve rendering is wired up.

🤖 Prompt for AI Agents
In `@src/wasm/modules/image/src/labels_to_svg.cpp` around lines 283 - 285, The
loop that pushes node_contours.ccurves into all_contours.ccurves is doing
unnecessary work because contoursResultToSVG does not use all_contours.ccurves;
remove the accumulation to avoid wasted work (delete the for-loop that copies
node_contours.ccurves into all_contours.ccurves) or, if you prefer to keep the
intent, wrap it behind a clear TODO/feature flag and comment referencing future
cubic curve rendering; update or remove any dependent comments and ensure
contoursResultToSVG continues to operate on the used contours fields only.

Comment on lines +82 to +84
// Weights: Normal = 1.0, Fixed = 10000.0 (Hard constraint)
float wNormal = 1.0;
float wFixed = 100.0;

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

Misleading comment: wFixed is 100.0, not 10000.0.

The comment reads "Hard constraint" with 10000.0, but the actual value is 100.0. Either update the comment or the value.

✏️ Fix comment
-  // Weights: Normal = 1.0, Fixed = 10000.0 (Hard constraint)
+  // Weights: Normal = 1.0, Fixed = 100.0 (Hard constraint)
   float wNormal = 1.0;
   float wFixed = 100.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
// Weights: Normal = 1.0, Fixed = 10000.0 (Hard constraint)
float wNormal = 1.0;
float wFixed = 100.0;
// Weights: Normal = 1.0, Fixed = 100.0 (Hard constraint)
float wNormal = 1.0;
float wFixed = 100.0;
🤖 Prompt for AI Agents
In `@src/wasm/modules/image/src/SavitskyGolay.cpp` around lines 82 - 84, The
comment above the weight declarations in SavitskyGolay.cpp is inconsistent: it
says "Fixed = 10000.0 (Hard constraint)" while the code defines float wFixed =
100.0; update either the comment or the value so they match—preferably change
the comment to "Fixed = 100.0 (Hard constraint)" or, if the intention was
10000.0, change the initializer for wFixed to 10000.0; ensure you update the
adjacent comment referencing wNormal and wFixed so the message and the actual
values remain consistent.

Comment on lines +86 to +98
for (size_t i = 0; i < data.size(); ++i) {
Point val{0.0, 0.0};
float A[3][3] = {0};
float Bx[3] = {0};
float By[3] = {0};
// if (locked[i] || corner[i]) { continue; }
for (int j = -window_radius_; j <= window_radius_; ++j) {
int k = i + j;
if (k < 0) {
k = data.size() + k;
} else if (k >= data.size()) {
k = k - data.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.

⚠️ Potential issue | 🟡 Minor

Signed/unsigned mismatch in wrap-around index arithmetic.

int k = i + j; where i is size_t and j is int. When j is negative, j is implicitly converted to size_t (a huge unsigned value), producing an incorrect intermediate before truncation to int. This is technically undefined behavior if the result doesn't fit in int.

The same pattern exists in filter_wrap (line 55), but it's worth fixing here. Cast i to int first.

🛡️ Proposed fix
-      int k = i + j;
+      int k = static_cast<int>(i) + j;
📝 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
for (size_t i = 0; i < data.size(); ++i) {
Point val{0.0, 0.0};
float A[3][3] = {0};
float Bx[3] = {0};
float By[3] = {0};
// if (locked[i] || corner[i]) { continue; }
for (int j = -window_radius_; j <= window_radius_; ++j) {
int k = i + j;
if (k < 0) {
k = data.size() + k;
} else if (k >= data.size()) {
k = k - data.size();
}
for (size_t i = 0; i < data.size(); ++i) {
Point val{0.0, 0.0};
float A[3][3] = {0};
float Bx[3] = {0};
float By[3] = {0};
// if (locked[i] || corner[i]) { continue; }
for (int j = -window_radius_; j <= window_radius_; ++j) {
int k = static_cast<int>(i) + j;
if (k < 0) {
k = data.size() + k;
} else if (k >= data.size()) {
k = k - data.size();
}
🤖 Prompt for AI Agents
In `@src/wasm/modules/image/src/SavitskyGolay.cpp` around lines 86 - 98, The loop
uses size_t i and int j then computes int k = i + j, causing signed/unsigned
mismatch and UB; fix by converting i to a signed type before arithmetic (e.g.,
int idx = static_cast<int>(i); then use int k = idx + j) and apply the same
change in the other occurrence in filter_wrap; update uses of i in this loop
(and any wrap-around adjustments) to use idx to avoid implicit conversions.

Comment on lines +214 to +236
float SavitzkyGolay::solveQuadraticAtZero(float A[3][3], float B[3]) {
// Determinant of A (Symmetric)
float det = A[0][0] * (A[1][1] * A[2][2] - A[1][2] * A[2][1]) -
A[0][1] * (A[1][0] * A[2][2] - A[1][2] * A[2][0]) +
A[0][2] * (A[1][0] * A[2][1] - A[1][1] * A[2][0]);

if (std::abs(det) < 1e-9) return 0.0; // Singularity check

// We only need the first element of the solution vector (c0),
// which corresponds to the polynomial value at x=0.
// Cramer's Rule for x[0]: Replace first column of A with B, divide det by main det.

// Matrix A_0:
// [ B0 A01 A02 ]
// [ B1 A11 A12 ]
// [ B2 A21 A22 ]

float det0 = B[0] * (A[1][1] * A[2][2] - A[1][2] * A[2][1]) -
A[0][1] * (B[1] * A[2][2] - A[1][2] * B[2]) +
A[0][2] * (B[1] * A[2][1] - A[1][1] * B[2]);

return det0 / det;
} No newline at end of file

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

Returning 0.0 on singular matrix silently maps the point to the origin.

When solveQuadraticAtZero encounters a near-singular matrix (line 220), it returns 0.0 for both x and y coordinates, effectively teleporting the point to (0, 0). This would produce a visible spike/artifact in the contour.

A safer fallback is to return the original data value (the center sample), preserving the point when the system is ill-conditioned.

🐛 Proposed fix — pass the original value as fallback

Change the function signature to accept a fallback value:

-float SavitzkyGolay::solveQuadraticAtZero(float A[3][3], float B[3]) {
+float SavitzkyGolay::solveQuadraticAtZero(float A[3][3], float B[3], float fallback) {
     // ...
-    if (std::abs(det) < 1e-9) return 0.0; // Singularity check
+    if (std::abs(det) < 1e-9) return fallback; // Singularity: keep original
     // ...

And at the call sites (lines 131–132):

-    val.x = solveQuadraticAtZero(A, Bx);
-    val.y = solveQuadraticAtZero(A, By);
+    val.x = solveQuadraticAtZero(A, Bx, data[i].x);
+    val.y = solveQuadraticAtZero(A, By, data[i].y);
🤖 Prompt for AI Agents
In `@src/wasm/modules/image/src/SavitskyGolay.cpp` around lines 214 - 236, The
function solveQuadraticAtZero currently returns 0.0 on near-singular matrices
which maps the point to the origin; change its signature to accept a fallback
value (e.g., float fallback) and when std::abs(det) < 1e-9 return that fallback
instead of 0.0, and update all call sites that invoke solveQuadraticAtZero to
pass the center sample (original data value) as the fallback so ill-conditioned
solves preserve the original point rather than teleporting it to (0,0).

@Krasner

Krasner commented Feb 6, 2026

Copy link
Copy Markdown
Collaborator Author

rebase is so bad 😠
anyways the only changes are in some c++ headers and cpp files. There isn't much.

This is also optional... if you think it's not worth the merge then i'm ok with that.

@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

Caution

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

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

835-852: ⚠️ Potential issue | 🟡 Minor

Remove endpoint guard to align with closed-loop treatment.

The SG filter filter_wrap_with_constraints (line 774) processes all indices including endpoints using wrap-around neighbors, producing valid smoothed targets for all contour points. The main loop (line 794) now iterates all points including endpoints. However, the partner target calculation (line 835) still excludes endpoints (op > 0 && op < (int)otherContour.size() - 1), forcing partners at endpoints to snap to their current position (line 850) instead of using their smoothed target. This guard is inconsistent with the closed-loop design and prevents endpoint partners from contributing their computed smoothed targets.

🤖 Fix all issues with AI agents
In `@src/wasm/modules/image/src/contours.cpp`:
- Around line 730-759: The smoothing loop mistakenly reads from the in-place
vector pts (causing Gauss-Seidel order dependence) even though an unmodified
snapshot original is created; update the inner convolution to read from original
(use original[k] instead of pts[k] when computing val) so the filter is
Jacobi-style and independent of index order while keeping the existing checks
(isLocked, Corner logic) and using radius/coeff/Point as before.
- Around line 744-756: The loop mixes signed and unsigned types causing UB when
adding negative j to size_t i; change the loop index to a signed type (e.g., int
or ptrdiff_t) and compute a signed container length once (e.g., int n =
static_cast<int>(pts.size()) or ptrdiff_t n = pts.size()), then replace uses of
pts.size() in the wrap logic with that signed n and compute k as a signed index
(int k = i + j) so expressions like pts.size() + k and k >= pts.size() become
well-defined; update the for loop header (i), the n variable, and the wrap
branches accordingly for variables pts, isLocked, radius, and k.
🧹 Nitpick comments (3)
src/wasm/modules/image/src/contours.cpp (3)

10-10: Unused <iostream> include.

The debug std::cout statements were removed per a previous review, but the <iostream> header is still included. No std::cout/std::cerr usage remains in this file.

🧹 Remove unused include
-#include <iostream>

725-738: Recursive std::function factorial is slow and fragile.

std::function involves heap allocation and virtual dispatch on every call. More importantly, int overflow occurs at factorial(13), so any radius ≥ 7 silently produces wrong coefficients. A simple iterative binomial coefficient computation avoids both problems.

♻️ Proposed replacement — compute binomial coefficients iteratively
-  std::function<int(int)> factorial = [&factorial](int x) -> int {
-    if (x <= 1) return 1;
-    return x * factorial(x-1);
-  };
-
   std::vector<Point> original = pts;
   int window = 2 * radius + 1;

   std::vector<float> coeff(window);
-  // binomial coefficients
-  for (int k = 0; k < window; ++k){
-    //n!/((n-k)! * k!)
-    coeff[k] = static_cast<float>(factorial(window-1)/ (factorial(k) * factorial(window-1-k)));
-  }
+  // Compute binomial coefficients iteratively (overflow-safe for reasonable radii)
+  coeff[0] = 1.0f;
+  for (int k = 1; k < window; ++k) {
+    coeff[k] = coeff[k - 1] * static_cast<float>(window - 1 - (k - 1)) / static_cast<float>(k);
+  }

796-840: Remove commented-out code.

Several commented-out fragments remain (lines 701–702, 796, 836, 839–840). These are dead vestiges from development iterations. If the logic may be needed again, it can be recovered from version control.

Comment on lines 730 to +759
std::vector<Point> original = pts;
for (size_t i = 1; i < pts.size() - 1; ++i) {
int window = 2 * radius + 1;

std::vector<float> coeff(window);
// binomial coefficients
for (int k = 0; k < window; ++k){
//n!/((n-k)! * k!)
coeff[k] = static_cast<float>(factorial(window-1)/ (factorial(k) * factorial(window-1-k)));
}
float sum = std::accumulate(coeff.begin(), coeff.end(), 0.0f);
for (int k = 0; k < window; ++k){
coeff[k] /= sum;
}

for (size_t i = 0; i < pts.size(); ++i) {
// DO NOT move if it's a Corner OR if it's Locked on the boundary
if (isLocked[i])
continue;

Point val{0,0};
for (int j = -radius; j <= radius; ++j) {
int k = i + j;
if (k < 0) {
k = pts.size() + k;
} else if (k >= pts.size()) {
k = k - pts.size();
}
val += pts[k] * coeff[j+radius];
}
pts[i] = val;

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

Bug: smoothing reads from in-place modified pts instead of the original snapshot.

original is created on line 730 as a snapshot but is never referenced. The inner loop on line 757 reads pts[k], which has already been modified for indices < i, producing order-dependent (Gauss-Seidel) smoothing instead of the intended Jacobi-style filter. This introduces directional bias — especially problematic on closed contours where the seam at the iteration start point will be visible.

🐛 Fix: read from `original` instead of `pts`
     Point val{0,0};
     for (int j = -radius; j <= radius; ++j) {
       int k = i + j;
       if (k < 0) {
         k = pts.size() + k;
       } else if (k >= pts.size()) {
         k = k - pts.size();
       }
-      val += pts[k] * coeff[j+radius];
+      val += original[k] * coeff[j+radius];
     }
     pts[i] = val;
🤖 Prompt for AI Agents
In `@src/wasm/modules/image/src/contours.cpp` around lines 730 - 759, The
smoothing loop mistakenly reads from the in-place vector pts (causing
Gauss-Seidel order dependence) even though an unmodified snapshot original is
created; update the inner convolution to read from original (use original[k]
instead of pts[k] when computing val) so the filter is Jacobi-style and
independent of index order while keeping the existing checks (isLocked, Corner
logic) and using radius/coeff/Point as before.

Comment on lines +744 to +756
for (size_t i = 0; i < pts.size(); ++i) {
// DO NOT move if it's a Corner OR if it's Locked on the boundary
if (isLocked[i])
continue;

Point val{0,0};
for (int j = -radius; j <= radius; ++j) {
int k = i + j;
if (k < 0) {
k = pts.size() + k;
} else if (k >= pts.size()) {
k = k - pts.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.

⚠️ Potential issue | 🟡 Minor

Signed/unsigned mixing: size_t i + int jint k is implementation-defined.

On line 751, j (negative int) is promoted to size_t before adding to i, producing a wrapped unsigned value. Assigning that to int k is implementation-defined. Same pattern on line 753 (pts.size() + k where k is negative). Use a signed loop index to avoid this:

🛡️ Proposed fix
- for (size_t i = 0; i < pts.size(); ++i) {
+ int n = static_cast<int>(pts.size());
+ for (int i = 0; i < n; ++i) {
     if (isLocked[i])
       continue;
     
     Point val{0,0};
     for (int j = -radius; j <= radius; ++j) {
       int k = i + j;
       if (k < 0) {
-        k = pts.size() + k;
-      } else if (k >= pts.size()) {
-        k = k - pts.size();
+        k = n + k;
+      } else if (k >= n) {
+        k = k - n;
       }
🤖 Prompt for AI Agents
In `@src/wasm/modules/image/src/contours.cpp` around lines 744 - 756, The loop
mixes signed and unsigned types causing UB when adding negative j to size_t i;
change the loop index to a signed type (e.g., int or ptrdiff_t) and compute a
signed container length once (e.g., int n = static_cast<int>(pts.size()) or
ptrdiff_t n = pts.size()), then replace uses of pts.size() in the wrap logic
with that signed n and compute k as a signed index (int k = i + j) so
expressions like pts.size() + k and k >= pts.size() become well-defined; update
the for loop header (i), the n variable, and the wrap branches accordingly for
variables pts, isLocked, radius, and k.

@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/contours.cpp`:
- Around line 700-710: The loop uses pts.size() (size_t) mixed with signed ints
causing warnings; cache the container size into an int (e.g., int n =
static_cast<int>(pts.size())) at the start and use n for all wrap arithmetic and
comparisons for previ/nexti (variables in this loop), then replace expressions
like if (previ < 0) previ = pts.size() + previ and if (nexti >= pts.size()) ...
with logic using n (e.g., add n when previ is negative, subtract n when nexti >=
n) so all math and comparisons are performed with signed ints.
🧹 Nitpick comments (3)
src/wasm/modules/image/src/contours.cpp (3)

10-10: Unused <iostream> include.

The debug std::cout statements were removed (per prior review), but the <iostream> header was left behind. It's no longer referenced anywhere in this file.

🧹 Proposed fix
-#include <iostream>

725-738: Factorial via std::function is fragile and inefficient.

std::function incurs type-erasure overhead for every call. More importantly, int overflow occurs for radius ≥ 7 (factorial(13) > INT_MAX), and the intermediate product factorial(k) * factorial(window-1-k) can overflow even sooner.

Since the binomial coefficients are small here, consider an iterative multiplicative formula that avoids large intermediate factorials entirely, or at minimum use a plain function:

♻️ Suggested replacement — iterative binomial coefficient
-  std::function<int(int)> factorial = [&factorial](int x) -> int {
-    if (x <= 1) return 1;
-    return x * factorial(x-1);
-  };
-
   std::vector<Point> original = pts;
   int window = 2 * radius + 1;

   std::vector<float> coeff(window);
-  // binomial coefficients
-  for (int k = 0; k < window; ++k){
-    //n!/((n-k)! * k!)
-    coeff[k] = static_cast<float>(factorial(window-1)/ (factorial(k) * factorial(window-1-k)));
-  }
+  // Binomial coefficients via multiplicative recurrence (no overflow for small window)
+  coeff[0] = 1.0f;
+  for (int k = 1; k < window; ++k) {
+    coeff[k] = coeff[k - 1] * static_cast<float>(window - 1 - (k - 1)) / static_cast<float>(k);
+  }

This also lets you remove the #include <functional> on Line 9 if it's not used elsewhere.


796-842: Clean up commented-out code.

Several commented-out lines remain across this function (Lines 701–702, 796, 836–837, 841–842). Version control preserves the history — shipping dead comments adds noise and makes review harder.

Comment on lines +700 to +710
for (int i = 0; i < pts.size(); ++i) {
// int previ = i > 0 ? i - 1 : pts.size() - 1;
// int nexti = i < pts.size() - 1 ? i + 1 : 0;
int previ = i - 2;
int nexti = i + 2;
if (previ < 0) {
previ = pts.size() + previ;
}
if (nexti >= pts.size()) {
nexti = nexti - pts.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.

⚠️ Potential issue | 🟡 Minor

Signed/unsigned mixing in wrap-around arithmetic.

pts.size() returns size_t. On Line 706, adding a negative previ to it triggers implicit unsigned conversion; on Line 708, nexti >= pts.size() is a signed-vs-unsigned comparison. Both produce compiler warnings and rely on implementation-defined behavior.

Cache the size as int once and use it throughout:

🛡️ Proposed fix
+  int n = static_cast<int>(pts.size());
-  for (int i = 0; i < pts.size(); ++i) {
+  for (int i = 0; i < n; ++i) {
     int previ = i - 2;
     int nexti = i + 2;
     if (previ < 0) {
-      previ = pts.size() + previ;
+      previ = n + previ;
     } 
-    if (nexti >= pts.size()) {
-      nexti = nexti - pts.size();
+    if (nexti >= n) {
+      nexti = nexti - n;
     }
🤖 Prompt for AI Agents
In `@src/wasm/modules/image/src/contours.cpp` around lines 700 - 710, The loop
uses pts.size() (size_t) mixed with signed ints causing warnings; cache the
container size into an int (e.g., int n = static_cast<int>(pts.size())) at the
start and use n for all wrap arithmetic and comparisons for previ/nexti
(variables in this loop), then replace expressions like if (previ < 0) previ =
pts.size() + previ and if (nexti >= pts.size()) ... with logic using n (e.g.,
add n when previ is negative, subtract n when nexti >= n) so all math and
comparisons are performed with signed ints.

@Ryan-Millard

Copy link
Copy Markdown
Owner

rebase is so bad 😠 anyways the only changes are in some c++ headers and cpp files. There isn't much.

This is also optional... if you think it's not worth the merge then i'm ok with that.

rebase is so bad 😠 anyways the only changes are in some c++ headers and cpp files. There isn't much.

This is also optional... if you think it's not worth the merge then i'm ok with that.

Yeah.😂😂

When I first used it, I messed up horribly because I didn't realize that the commit hashes changed.

@Ryan-Millard

Copy link
Copy Markdown
Owner

@Krasner, what exactly is the impact of this PR?

The visual changes aren't overtly obvious to me, but the paths look slightly rounder now. Is that correct?

@Krasner

Krasner commented Feb 6, 2026

Copy link
Copy Markdown
Collaborator Author

Yep just that. This is optional

@Ryan-Millard

Copy link
Copy Markdown
Owner

This doesn't look like a good addition to our code because it makes straight edges rounder, which pushes use closer to lossier output. We need to try to match the image as precisely as possible without creating bad SVGs.

I also want to take a moment to thank you for your sustained interest in this project. It's really nice to have someone else working on it and your contributions are always wonderful.

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

Labels

c/c++ Changes to C or C++ files

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants