diff --git a/docs/docs/reference/wasm/modules/image/mergeSmallRegionsInPlace/_category_.json b/docs/docs/reference/wasm/modules/image/mergeSmallRegionsInPlace/_category_.json new file mode 100644 index 000000000..3b9af70b1 --- /dev/null +++ b/docs/docs/reference/wasm/modules/image/mergeSmallRegionsInPlace/_category_.json @@ -0,0 +1,10 @@ +{ + "label": "mergeSmallRegionsInPlace.h", + "position": 3, + "link": { + "type": "generated-index", + "title": "Merging Small Pixel Regions", + "description": "A single-pass C++ routine that finds connected-color regions in an RGBA image, computes per-region bounding-box metadata, and replaces pixels belonging to small regions by sampling colors from adjacent large regions (effectively merging small islands into nearby large blobs).", + "slug": "/reference/wasm/modules/image/mergeSmallRegionsInPlace" + } +} diff --git a/docs/docs/reference/wasm/modules/image/mergeSmallRegionsInPlace/complexity-and-memory.md b/docs/docs/reference/wasm/modules/image/mergeSmallRegionsInPlace/complexity-and-memory.md new file mode 100644 index 000000000..1375201e6 --- /dev/null +++ b/docs/docs/reference/wasm/modules/image/mergeSmallRegionsInPlace/complexity-and-memory.md @@ -0,0 +1,22 @@ +--- +id: complexity-and-memory +title: Complexity and Memory +sidebar_label: Complexity and Memory +sidebar_position: 6 +description: A breakdown of the time complexity, memory usage, and cache behavior of the mergeSmallRegionsInPlace function. +--- + +$$ +\text{Let } N = width \times height +$$ + +## Time +The flood-fill visits each pixel once and tests 4 neighbours ($$4 \cdot N$$), so the dominant cost is $$O(N)$$ with a small constant +(neighbour checks and byte comparisons). The merge pass is another similar $$O(N)$$ scan, so overall $$O(N)$$. + +## Memory +The implementation allocates a `labels` array of $N$ integers and a `regions` vector whose size equals number of components +(at most $$N$$). So memory is $$O(N)$$ additional to the image buffer. + +## Cache behaviour +BFS queue can cause random access inside large components; using a scanline two-pass connected-component algorithm (or union-find) can improve cache locality and throughput on big images. diff --git a/docs/docs/reference/wasm/modules/image/mergeSmallRegionsInPlace/connected-components.md b/docs/docs/reference/wasm/modules/image/mergeSmallRegionsInPlace/connected-components.md new file mode 100644 index 000000000..4ebc33f45 --- /dev/null +++ b/docs/docs/reference/wasm/modules/image/mergeSmallRegionsInPlace/connected-components.md @@ -0,0 +1,120 @@ +--- +id: connected-components +title: Connected Components & Small Region Merging +sidebar_label: Connected Components +sidebar_position: 3 +description: > + Step-by-step explanation of the connected-component labeling algorithm (mergeSmallRegionsInPlace) with flood-fill, + small-region removal, and merging into larger neighboring regions. Includes mathematical background and visual examples. +--- + +## High-Level Algorithm + +1. **Labeling / connected-component flood-fill**: iterate pixels, perform a breadth-first (queue) flood-fill +whenever an unlabeled pixel is found. Two pixels are considered connected if they are 4-neighbours (up, down, left, right) +and their RGBA values are exactly equal. +
+
+
+
+ Center pixel (being flood-filled) +
+
+
+ Other pixels (unlabeled neighbor candidates) +
+
+
+ Other pixels (unlabeled non-neighbor candidates) +
+
+ + + + + + + + + + + + +
+ +2. While flood-filling, `Region` metadata is collected (`size`, `minX`, `maxX`, `minY`, `maxY`) and labeled according to an index (`labels`). +3. After all components are labeled and regions metadata computed, +iterate pixels again. For a pixel whose region is considered *small* +(fails `isBigEnough(minArea,minWidth,minHeight)`), +check its four immediate neighbors. If any neighbor belongs to a *big* region, +copy that neighbor's RGBA into the small pixel (effectively assigning the small pixel to the big region; +over time, the small region is consumed by *bigger neighboring regions*). + +:::note +This merges only small-region pixels which are adjacent to large regions. +The order of iteration means small pixels near large regions are captured first — +the implementation stops at first qualifying neighbour. +::: + +## Mathematical Background + +### Connected components + +The algorithm computes **connected components** on a planar grid using 4-connectivity. +Formally, we can describe the image as a function: +$$ +\begin{align*} +I &: \mathbb{Z}^2 \to \mathcal{C} \\ +(x, y) &\mapsto I(x, y) +\end{align*} +$$ + +:::important +- $I$ is the image function. +- $\mathbb{Z}^2$ is the set of all integer pairs $(x, y)$ representing pixel coordinates. +- $\mathcal{C}$ is the set of all possible RGBA values: + $$ + \mathcal{C} = \{ (R, G, B, A) \mid R,G,B,A \in [0,255] \} + $$ +- $I(x, y) \in \mathcal{C}$ is the color of the pixel at coordinates $(x, y)$. + +> In simple terms, each pixel at position $(x, y)$ has a color given by $I(x, y)$. +::: + +Two pixels, $$p=(x,y)$$ and $$q=(x',y')$$, are **4-adjacent** if $$|x-x'| + |y-y'| = 1$$. +A connected component is a maximal set of pixels, $$S$$, such that any two pixels in $$S$$ are connected by a path of 4-adjacent pixels with identical colors. + +A flood-fill (BFS / DFS) computes these components exactly. + +### Bounding box & geometric heuristics + +For each component we compute an axis-aligned bounding box with integer coordinates: +$$ +[minX,maxX]\times[minY,maxY] +$$ +The bounding-box width and height are: +$$ +\begin{align*} +W &= maxX - minX + 1 \\ +H &= maxY - minY + 1 +\end{align*} +$$ + +:::tip +See the source code in the [Region struct](https://github.com/Ryan-Millard/Img2Num/blob/main/src/wasm/modules/image/src/mergeSmallRegionsInPlace.cpp) +to understand how this is used. +::: + +The area (component size) is simply the number of pixels in the component, $$|S|$$. +The heuristics used to classify *small* vs *big* regions rely on thresholds on both area and bounding box dimensions. +This avoids keeping long thin noise (e.g. a long 1-pixel-wide arm) even if its area is above `minArea`. + +### Why 4-connectivity, not 8? + +4-connectivity treats diagonally touching pixels as disconnected. +This is stricter and avoids connecting components that only meet at a corner. +Depending on the data, 8-connectivity (connect diagonally too) may be preferred — see the [Variants & Improvements page](../variants-and-improvements). + +### What does merging mean here? + +The code does not merge region graphs with union operations. Instead, it performs a **pixel-wise recoloring** of small-region pixels to the color of an adjacent big region. That has the practical effect of attaching each small pixel to a neighboring large region. This is a cheap and local merge — it won’t always choose the most semantically correct neighbor if multiple are present. diff --git a/docs/docs/reference/wasm/modules/image/mergeSmallRegionsInPlace/faq.md b/docs/docs/reference/wasm/modules/image/mergeSmallRegionsInPlace/faq.md new file mode 100644 index 000000000..8f9b6aa3a --- /dev/null +++ b/docs/docs/reference/wasm/modules/image/mergeSmallRegionsInPlace/faq.md @@ -0,0 +1,363 @@ +--- +id: faq +title: FAQ +sidebar_label: FAQ +sidebar_position: 10 +description: Frequently asked questions related to the mergeSmallRegionsInPlace function. +--- + +# Frequently Asked Questions + +## What problem does `mergeSmallRegionsInPlace` solve? + +This function removes **tiny connected regions** in an RGBA image by merging them into neighboring, sufficiently large regions **in-place**. + +It is intended for post-processing steps after: +- k-means color quantization +- segmentation + +Or as a pre-processing step before: +- image-to-vector (SVG) pipelines + +Small regions often appear as visual noise and make downstream geometry extraction harder. + +## What exactly is a “region” in this context? +A **region** is a *4-connected component* of pixels where: +- Each pixel is connected via **up, down, left, or right** +- All pixels have **exactly the same RGBA values** + +Diagonal adjacency **does not** count. + +## Why use 4-connectivity instead of 8-connectivity? +4-connectivity: +- Matches most raster algorithms (flood-fill, contour tracing) +- Avoids diagonal “corner-touch” artifacts +- Produces cleaner, grid-aligned regions + +Using 8-connectivity would merge diagonally touching pixels that visually are not part of the same region. + +## Why is the image indexed as `idx(x, y, width) * 4`? + +Each pixel consists of **4 bytes**: `R`, `G`, `B`, `A` + +So the linear index into the pixel buffer is: + +$$ +(\text{y} \cdot \text{width} + \text{x}) \times 4 +$$ + +
+Visual Example: 5×5 RGBA Image Indexing + + + + + + + + (0,0) idx*4=0 + + + (1,0) idx*4=4 + + + (2,0) idx*4=8 + + + (3,0) idx*4=12 + + + (4,0) idx*4=16 + + + + (0,1) idx*4=20 + + + (1,1) idx*4=24 + + + (2,1) idx*4=28 + + + (3,1) idx*4=32 + + + (4,1) idx*4=36 + + + + (0,2) idx*4=40 + + + (1,2) idx*4=44 + + + (2,2) idx*4=48 + + + (3,2) idx*4=52 + + + (4,2) idx*4=56 + + + + (0,3) idx*4=60 + + + (1,3) idx*4=64 + + + (2,3) idx*4=68 + + + (3,3) idx*4=72 + + + (4,3) idx*4=76 + + + + (0,4) idx*4=80 + + + (1,4) idx*4=84 + + + (2,4) idx*4=88 + + + (3,4) idx*4=92 + + + (4,4) idx*4=96 + + + Each pixel = 4 bytes (R,G,B,A) + Byte offset = idx(x,y,width) * 4 + + +
+ +:::note Assumptions this layout has +- Row-major order +- No padding between rows +::: + +## What does `sameColor(...)` check? + +It compares **all four RGBA channels** for equality: + +- Red +- Green +- Blue +- Alpha + +Two pixels are considered connected **only if all channels match exactly**. + +This is important for correctness when alpha is meaningful (e.g. transparency). + +## Why does the algorithm do two passes? + +The function is split into **two distinct phases**: + +1. **Labeling pass** + - Flood-fills each region + - Assigns a label to every pixel + - Computes region metadata (area + bounding box) + +2. **Merge pass** + - Iterates pixels again + - Replaces pixels belonging to “small” regions + - Copies color from a neighboring “big enough” region + +This separation keeps the logic simpler and avoids modifying regions while still discovering them. + +## What makes a region “big enough”? + +A region must satisfy **all three** conditions: + +- `size >= minArea` +- `width() >= minWidth` +- `height() >= minHeight` + +Where: +- `size` = number of pixels +- `width()` = bounding box width +- `height()` = bounding box height + +This prevents: +- Thin lines +- Long but narrow artifacts +- Small blobs + +## Why use a bounding box instead of checking shape quality? + +Bounding boxes are: +- Fast to compute +- Memory cheap +- Conservative + +They don’t capture holes or concavities, but they are a good heuristic for filtering obvious noise. + +There is a TODO noting that **internal gaps** could reduce effective width/height even if the bounding box looks valid. + +## Can a region pass the bounding box test but still be “bad”? + +Yes. + +Example: +- A hollow ring +- A U-shaped region +- A region with internal gaps + +The bounding box may be large, but the actual filled area might be sparse. + +This implementation prioritizes speed and simplicity over perfect geometric validity. + +## Why are regions merged pixel-by-pixel instead of as a whole? + +Because: +- The function operates **in-place** +- It avoids reallocating buffers +- It keeps memory usage predictable + +Each pixel independently: +- Checks its neighbors +- Copies the color of a valid region + +This makes the merge phase linear and simple. + +## Why only check immediate neighbors during merging? + +Only **4 immediate neighbors** are checked because: +- The merge should respect spatial adjacency +- Copying from distant pixels could create visual artifacts + +This ensures merges look natural and locally consistent. + +## What happens if a small region has no big neighbors? + +Nothing. + +Pixels in that region remain unchanged. + +This avoids: +- Arbitrary color assignment +- Unexpected long-range merges + +If this is undesirable, a second pass or fallback strategy can be added. + +## Does this update region metadata after merging? + +No. + +Once the merge phase starts: +- `regions` is treated as read-only +- Labels may change per pixel +- Region sizes are **not recomputed** + +This is intentional to keep runtime linear and avoid cascading changes. + +## What is the time complexity? + +Overall complexity is **linear**: + +- Flood-fill labeling: $O(n)$ +- Merge pass: $O(n)$ + +Where $$ n = \text{width} \times \text{height} $$ + +Each pixel is: +- Visited once in flood-fill +- Checked against at most 4 neighbors + +## What is the memory overhead? + +Additional memory used: +- `labels`: one `int` per pixel +- `regions`: one entry per connected component +- BFS queue (temporary) + +No additional image buffers are allocated. + +## Why use BFS (`std::queue`) instead of DFS? + +BFS: +- Avoids deep recursion +- Prevents stack overflow on large regions +- Has predictable memory usage + +DFS would require either recursion (unsafe) or an explicit stack (no advantage here). + +## Can this function be parallelized? + +Not easily in its current form. + +Reasons: +- Flood-fill has data dependencies +- Label assignment is sequential +- Merge step mutates shared data + +Parallel versions would require: +- Tiled processing +- Boundary reconciliation +- More complex region merging logic + +## Is this suitable for SVG generation pipelines? + +Yes — especially as a **cleanup step** before: +- Boundary tracing +- Polygon extraction +- Path simplification + +Removing small regions early greatly simplifies vector geometry later. + +## What are common improvements or extensions? + +Possible enhancements include: +- Detecting holes inside regions + + + + + + + + +- Merging based on color similarity instead of equality + + + + + + + +- Multi-pass merging +- Choosing the *largest neighboring region* instead of the first valid one +- Detecting localised width/height - regions often have large tentacle-like protrusions. + +The current design favors **clarity and predictability** over heuristics. + +## Is this function deterministic? + +Yes. + +Given the same input image and parameters, the output is always identical. + +No randomness is involved. + +## When should I *not* use this? + +Avoid this function if: +- You need exact topology preservation +- You rely on diagonal connectivity +- You require sub-pixel or fuzzy color matching + +It is designed for **grid-aligned, exact-color regions**. + +# Summary + +`mergeSmallRegionsInPlace` is a fast, predictable cleanup step for raster segmentation pipelines. +It trades geometric perfection for simplicity, performance, and ease of reasoning — which is often exactly what you want before vectorization. diff --git a/docs/docs/reference/wasm/modules/image/mergeSmallRegionsInPlace/limitations-and-pitfalls.md b/docs/docs/reference/wasm/modules/image/mergeSmallRegionsInPlace/limitations-and-pitfalls.md new file mode 100644 index 000000000..45e7e2dcd --- /dev/null +++ b/docs/docs/reference/wasm/modules/image/mergeSmallRegionsInPlace/limitations-and-pitfalls.md @@ -0,0 +1,40 @@ +--- +id: limitations-and-pitfalls +title: Limitations & Pitfalls +sidebar_label: Limitations & Pitfalls +sidebar_position: 7 +description: Limitations and pitfalls commonly encountered when using the mergeSmallRegionsInPlace function. +--- + +## Exact RGBA equality +The algorithm treats colors as equal only when all 4 bytes match exactly. +If you need fuzzy color merging (e.g. colors within a Euclidean distance in RGB), you must replace `sameColor` with a colour-distance test. +:::note Extending functionality +This could be extended in the future by allowing callers to pass a custom equality-checker function. +::: + +## Only 4-connectivity +Components connected only diagonally will be considered separate. If your semantics require 8-connectivity, adapt the neighbour set. + +## Local merge heuristic +A small region is colored using the first adjacent large neighbour encountered. +If a small island touches multiple large regions, the chosen one depends on neighbour scan order +(right, left, down, up in the reference code). This can occasionally lead to confusion as it is not an `intelligent check`. + +## Holes / concavities +The bounding-box test may be fooled by shapes with large bounding boxes but containing many holes. +**The TODO in the source code remains valid**: you could compute convex-hull, morphological closing, +or compute the ratio `size / (width*height)` (occupancy) to detect sparse shapes. +```cpp title="The TODO comment" +// TODO: check for gaps inside regions - its possible their dimensions are fine, +// but inner gaps reduce effective width and height +``` + +## Order sensitivity +Since pixels are recolored in place and have their `labels` updated when a merge is done, +subsequent small pixels that were adjacent to that pixel may now see a different neighbour label; +this actually helps the merge flood (small pixels adjacent to a merged pixel can be recoloured to the same large region), +but it means the behaviour is implementation-order dependent. + +## Performance +Allocation of `std::vector` and `std::queue` can be optimized for very large images. diff --git a/docs/docs/reference/wasm/modules/image/mergeSmallRegionsInPlace/overview.md b/docs/docs/reference/wasm/modules/image/mergeSmallRegionsInPlace/overview.md new file mode 100644 index 000000000..2f6842a8b --- /dev/null +++ b/docs/docs/reference/wasm/modules/image/mergeSmallRegionsInPlace/overview.md @@ -0,0 +1,67 @@ +--- +id: overview +title: mergeSmallRegionsInPlace — Overview +sidebar_label: Overview +sidebar_position: 2 +--- + +This page documents the algorithm, the math and topology behind the mergeSmallRegionsInPlace function, +the provided implementation, usage examples, complexity, limitations, and suggested improvements. + +## Why this exists + +Small spurious pixel clusters (`"speckles"` or thin "arms") are common after colour quantization, k-means-based color clustering, +thresholding, or other segmentation steps. They make vectorization (e.g. converting raster -> SVG) +and any downstream shape-based processing noisy. +This routine cleans those small clusters by merging them into neighboring, larger regions so the image looks cleaner and shapes are more blob-like. + +Think of it as a domain-specific `despeckle` that uses connected-component analysis and a simple adjacency-based merge rule. + +The merge rule is important as it avoids leaving gaps in the image (a problem faced by several other similar libraries). + +## When to use it + +- With an RGBA image stored in a tightly-packed `uint8_t*` pixels buffer (4 bytes per pixel, row-major). +- To remove very small connected components while preserving large components. +- You are OK with replacing a small-region pixel by the color of an *adjacent* large region. + +:::important +Not suitable when you need to preserve small but semantically important details (e.g. text strokes), +or when color matching should be fuzzy (this implementation checks exact RGBA equality). +::: + +## Function signature & API + +```cpp +void mergeSmallRegionsInPlace( + uint8_t *pixels, // pointer to RGBA buffer, length = width * height * 4 + int width, // image's width + int height, // image's height + int minArea, // minimum pixel count for a region to be "big enough" + int minWidth, // minimum bounding-box width for a region to be "big enough" + int minHeight // minimum bounding-box height for a region to be "big enough" +); +``` + +### Parameters + +- `pixels` — row-major RGBA buffer with 4 bytes per pixel: + +$$ +\text{pixels} = [ \overbrace{255}^{R},\overbrace{255}^{G},\overbrace{255}^{B},\overbrace{255}^{A}, \overbrace{0}^{R},\overbrace{0}^{G},\overbrace{0}^{B},\overbrace{255}^{A}, \cdots ] +$$ + +- `width`, `height` — image dimensions in pixels. +- Evaluation Criteria: + - `minArea` — threshold on the number of pixels in a connected component; components with `size < minArea` are considered too small. + - `minWidth`, `minHeight` — thresholds on the component's bounding-box width/height. A component is considered big enough when **all three** conditions pass (area and both bbox dimensions). This helps avoid long thin components being considered large just by area. + +### Preconditions + +* `pixels != nullptr` and `width>0` and `height>0`. +* The buffer length must be at least `width * height * 4` bytes. + +### Postconditions + +* The `pixels` buffer may be modified in-place: small regions will have their pixels recolored to match an adjacent large region (if found). +* The function uses an internal label map and region metadata; it does not return labels. diff --git a/docs/docs/reference/wasm/modules/image/mergeSmallRegionsInPlace/testing-and-debugging-suggestions.md b/docs/docs/reference/wasm/modules/image/mergeSmallRegionsInPlace/testing-and-debugging-suggestions.md new file mode 100644 index 000000000..19b6b8550 --- /dev/null +++ b/docs/docs/reference/wasm/modules/image/mergeSmallRegionsInPlace/testing-and-debugging-suggestions.md @@ -0,0 +1,23 @@ +--- +id: testing-and-debugging-suggestions +title: Testing & Debugging Suggestions +sidebar_label: Testing & Debugging Suggestions +sidebar_position: 9 +description: Testing and debugging suggestions for the mergeSmallRegionsInPlace function. +--- + +## Synthetic test images +Create synthetic test images that exercise corner cases: +- Single pixel islands +- Long 1-pixel-wide arms (test minWidth/minHeight) +- Two large regions separated by thin small islands +- Diagonal-touching shapes (to test 4 vs 8 connectivity) + +## Visualization of labels map +Visualize the `labels` map (map labels to colors) to ensure connected components are being formed as expected. + +## Unit tests +Assert that the number of pixels of a known large blob is unchanged; assert that small isolated pixels have been recolored. + +## Instrumentation +Collect histogram of region sizes to choose appropriate `minArea`. diff --git a/docs/docs/reference/wasm/modules/image/mergeSmallRegionsInPlace/variants-and-improvements.md b/docs/docs/reference/wasm/modules/image/mergeSmallRegionsInPlace/variants-and-improvements.md new file mode 100644 index 000000000..be81f8985 --- /dev/null +++ b/docs/docs/reference/wasm/modules/image/mergeSmallRegionsInPlace/variants-and-improvements.md @@ -0,0 +1,58 @@ +--- +id: variants-and-improvements +title: Variants & Improvements +sidebar_label: Variants & Improvements +sidebar_position: 8 +description: Several ways mergeSmallRegionsInPlace could be improved or customized. +--- + +This section explains several ways to improve or customize the behavior of `mergeSmallRegionsInPlace`. + +:::tip Limitations & Pitfalls +Also see the previous section, [Limitations & Pitfalls](../limitations-and-pitfalls), for a better understanding of the below. +::: + +## 8-connectivity + +Change the neighbor offsets from 4 to 8 directions (include diagonals). This connects diagonally touching pixels. + +## Two-pass connected-component + +> A.K.A. scanline or union-find labeling + +A two-pass algorithm is usually faster (lower memory footprint and better cache locality). +It scans rows, assigns provisional labels and records equivalences, then resolves equivalences and computes final region stats in the second pass. + +## Use colour distance instead of exact equality + +```cpp title="Replace sameColor with something like the below" +inline bool similarColor(const uint8_t *img, int w, int h, int x1, int y1, int x2, int y2, int tol) { + int i1 = idx(x1,y1,w)*4; int i2 = idx(x2,y2,w)*4; + int dr = int(img[i1]) - img[i2]; + int dg = int(img[i1+1]) - img[i2+1]; + int db = int(img[i1+2]) - img[i2+2]; + return (dr*dr + dg*dg + db*db) <= tol*tol; +} +``` + +This treats colors within `tol` distance as identical. + +## Merge-by-nearest-large-region (instead of neighbour sampling) + +For each small region, find the nearest big region (e.g. by computing region adjacency graph or distance transform) +and recolor the whole small region at once. This reduces order dependence and makes merging decisions global. + +## Use morphological operations to fill small gaps + +Applying a morphological closing (dilate then erode) before connected-component labeling can fill thin gaps and reduce speckles without per-component heuristics. + +## Occupancy / solidity test + +Compute $solidity = \frac{size}{width \times height}$ + +If `solidity` is low (i.e., bounding box large but region sparse), treat as small/noisy and merge. + +## Parallel labeling + +Use segmented image tiling with boundary stitching for multicore scaling or specialized GPU approaches. + diff --git a/docs/docs/reference/wasm/modules/image/mergeSmallRegionsInPlace/walkthrough-and-usage.md b/docs/docs/reference/wasm/modules/image/mergeSmallRegionsInPlace/walkthrough-and-usage.md new file mode 100644 index 000000000..9a423ef10 --- /dev/null +++ b/docs/docs/reference/wasm/modules/image/mergeSmallRegionsInPlace/walkthrough-and-usage.md @@ -0,0 +1,104 @@ +--- +id: walkthrough-and-usage +title: Walkthrough & Usage +sidebar_label: Walkthrough & Usage +sidebar_position: 4 +description: Code walkthrough of mergeSmallRegionsInPlace and how it can be used in real code. +--- + +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + +## Line-by-line code walk-through + +:::tip +See the source code ([mergeSmallRegionsInPlace.h](https://github.com/Ryan-Millard/Img2Num/blob/main/src/wasm/modules/image/include/mergeSmallRegionsInPlace.h) and +[mergeSmallRegionsInPlace.cpp](https://github.com/Ryan-Millard/Img2Num/blob/main/src/wasm/modules/image/src/mergeSmallRegionsInPlace.cpp)) +for the full source listing. Here we explain the important parts. +::: + +- `struct Pixel { int x,y; };` — small POD for BFS queue. +- `inline int idx(int x, int y, int width)` — maps (x,y) to a linear index into `labels` (not into pixel bytes; bytes index uses `*4`). +This makes it simpler to index 2D data in a 1D array. +- `sameColor(...)` — compares 4 bytes (in RGBA form) at two pixel coordinates for exact equality. + :::caution It uses *exact* equality + Compression or anti-aliased edges will produce many colors that are visually similar but not equal. + ::: +- `struct Region` — collects `size`, `minX`, `maxX`, `minY`, `maxY`, provides convenience `width()`, `height()`, and `isBigEnough(...)`. +- **Flood-fill labeling loop** — For every unlabeled pixel, perform a BFS: + + + + 1. Push initial pixel + 2. Mark its `labels[...] = nextLabel` + 3. Add pixel to region: `r.add(x,y)` + 4. While queue non-empty + - Pop and consider 4 neighbours + - If their label is `-1` and `sameColor(...)` holds + - label them and push to queue + 5. Now done with nth region: + - Add region to list of regions: `regions.push_back(r)` + - Increment `nextLabel` + + + + ```mermaid + flowchart TD + A[Start: For every pixel] --> B{Is pixel unlabeled?} + B -- No --> A + B -- Yes --> C[Push initial pixel to queue] + C --> D[Mark labels = nextLabel] + D --> E[Add pixel to region r] + E --> F{Queue not empty?} + F -- No --> G[Region done: add r to regions list] + G --> H[Increment nextLabel] + H --> A + F -- Yes --> I[Pop pixel from queue] + I --> J[Check 4 neighbours] + J --> K{Neighbour unlabeled AND sameColor?} + K -- No --> F + K -- Yes --> L[Label neighbour = nextLabel] + L --> M[Push neighbour to queue] + M --> F + + ``` + + + +- **Merge phase**: iterate every pixel; if its region is too small (`isBigEnough(...) == false`), +check the 4 immediate neighbours; if any neighbour is in a different label `nl` and `regions[nl].isBigEnough(...)` +is true, copy the neighbour's color bytes into the small pixel and set its label to `nl`. + :::note + The merge phase uses the `labels` array to pick neighbour region IDs and the `regions` metadata to determine which regions are "big". + ::: + +## Example usage + +```cpp title="Load an image with stb_image, run merge, write out with stb_image_write" +#include "mergeSmallRegionsInPlace.h" +#include +#include + +int main() { + int w,h,comp; + unsigned char *img = stbi_load("segmented.png", &w, &h, &comp, 4); + if (!img) return 1; + + // Remove tiny islands smaller than 50px and require bbox at least 3x3 + mergeSmallRegionsInPlace(img, w, h, 50, 3, 3); + + stbi_write_png("cleaned.png", w, h, 4, img, w*4); + stbi_image_free(img); + return 0; +} +``` +:::tip +Tweak the thresholds to match your use-case. +::: +