smart-downscaler 0.6.0 → 0.6.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -4,7 +4,7 @@ A high-performance Rust library for intelligent image downscaling with pixel art
4
4
 
5
5
  **Available as a native Rust library and WebAssembly module for browser/Node.js.**
6
6
 
7
- [![Version](https://img.shields.io/badge/version-0.5.0-blue.svg)]()
7
+ [![Version](https://img.shields.io/badge/version-0.6.0-blue.svg)]()
8
8
  [![License](https://img.shields.io/badge/license-MIT-green.svg)]()
9
9
 
10
10
  ---
@@ -35,6 +35,7 @@ A high-performance Rust library for intelligent image downscaling with pixel art
35
35
  | **Edge-Aware Processing** | Sobel/Scharr detection preserves boundaries |
36
36
  | **Spatial Coherence** | Neighbor and region voting for smooth results |
37
37
  | **K-Centroid Tile Logic** | Advanced dominant color extraction per tile |
38
+ | **Rare-Color Preservation** | Saliency weighting + slot reservation keeps small, important colors (lips, eyes) |
38
39
  | **Performance Preprocessing** | Resolution capping and color pre-quantization |
39
40
  | **WebAssembly Support** | Full browser compatibility with near-native speed |
40
41
 
@@ -46,7 +47,7 @@ A high-performance Rust library for intelligent image downscaling with pixel art
46
47
 
47
48
  ```toml
48
49
  [dependencies]
49
- smart-downscaler = "0.5.0"
50
+ smart-downscaler = "0.6.0"
50
51
  ```
51
52
 
52
53
  ### WebAssembly (npm)
@@ -59,7 +60,7 @@ npm install smart-downscaler
59
60
 
60
61
  ```html
61
62
  <script type="module">
62
- import init, { downscale_rgba, WasmDownscaleConfig } from 'https://unpkg.com/smart-downscaler@0.5.0/smart_downscaler.js';
63
+ import init, { downscale_rgba, WasmDownscaleConfig } from 'https://unpkg.com/smart-downscaler@0.6.0/smart_downscaler.js';
63
64
  </script>
64
65
  ```
65
66
 
@@ -168,8 +169,12 @@ println!("Palette: {} colors", result.palette.len());
168
169
  | `max_resolution_mp` | `f32` | `1.6` | `0.0-10.0` | Resolution cap in megapixels (0 = disabled) |
169
170
  | `max_color_preprocess` | `usize` | `16384` | `0-65536` | Pre-quantization limit (0 = disabled) |
170
171
  | **Tile Processing** |||||
171
- | `k_centroid` | `usize` | `1` | `1`, `2`, `3` | Tile color extraction mode |
172
+ | `k_centroid` | `usize` | `1` | `1`, `2`, `3`, `4` | Tile color extraction mode |
172
173
  | `k_centroid_iterations` | `usize` | `0` | `0-10` | K-Means iterations for tile color |
174
+ | **Rare-Color Preservation** |||||
175
+ | `color_rarity` | `f32` | `0.0` | `0.0-1.0` | Damps frequency vote so large flat areas stop dominating (`0`=area, `1`=`count^0.5`) |
176
+ | `detail_boost` | `f32` | `0.0` | `0.0-2.0` | Boosts colors in detail-rich regions via a local-contrast saliency map (`0`=off) |
177
+ | `reserve_colors` | `usize` | `0` | `0 - palette_size` | Reserve N palette slots for distinct, important, under-represented source colors (`0`=off) |
173
178
 
174
179
  ---
175
180
 
@@ -224,6 +229,9 @@ Controls how each source tile is reduced to a single representative color:
224
229
  | **Average** | `1` | Simple weighted average of all pixels | Smooth gradients, noise reduction |
225
230
  | **Dominant** | `2` | K-Means (k=2), uses largest cluster | Sharp edges, foreground/background separation |
226
231
  | **Foremost** | `3` | K-Means (k=3), finer dominant detection | Complex textures, detailed sprites |
232
+ | **Salient** | `4` | K-Means (k=2), keeps a distinctly-more-chromatic minority | Thin colorful features (lips, eyes, makeup) |
233
+
234
+ > **Note:** Mode `2` (Dominant) snaps a mixed tile to its *larger* cluster, which discards thin minority colors — a tile that is 60% skin / 40% lip becomes pure skin. For faces and other artwork with small colorful features, prefer mode `4`, which keeps the colorful minority when it is non-trivial (≥22% of the tile) and clearly more chromatic than the majority.
227
235
 
228
236
  ```javascript
229
237
  // Mode 1: Average (default) - smooth results
@@ -237,6 +245,10 @@ config.k_centroid_iterations = 2;
237
245
  // Mode 3: Foremost - detailed preservation
238
246
  config.k_centroid = 3;
239
247
  config.k_centroid_iterations = 3;
248
+
249
+ // Mode 4: Salient - preserve thin colorful features (lips, eyes)
250
+ config.k_centroid = 4;
251
+ config.k_centroid_iterations = 2;
240
252
  ```
241
253
 
242
254
  ---
@@ -447,7 +459,7 @@ const dist = color_distance(255, 0, 0, 0, 255, 0); // Red vs Green
447
459
  Get library version.
448
460
 
449
461
  ```javascript
450
- console.log(version()); // "0.5.0"
462
+ console.log(version()); // "0.6.0"
451
463
  ```
452
464
 
453
465
  ---
@@ -496,18 +508,59 @@ const exact = WasmDownscaleConfig.exact_colors();
496
508
 
497
509
  ### Preset Comparison
498
510
 
499
- | Preset | Palette | K-Means | Segmentation | K-Centroid | Speed |
500
- |--------|---------|---------|--------------|------------|-------|
501
- | `fast()` | 16 | 3 | none | 1 (avg) | ⚡⚡⚡ |
502
- | `default` | 16 | 5 | hierarchy_fast | 1 (avg) | ⚡⚡ |
503
- | `vibrant()` | 24 | 8 | hierarchy_fast | 2 (dom) | ⚡ |
504
- | `quality()` | 32 | 10 | hierarchy | 2 (dom) | 🐢 |
505
- | `exact_colors()` | 16 | 0 | hierarchy_fast | 1 (avg) | ⚡⚡ |
511
+ | Preset | Palette | K-Means | Segmentation | K-Centroid | Rare-Color¹ | Speed |
512
+ |--------|---------|---------|--------------|------------|-------------|-------|
513
+ | `fast()` | 16 | 3 | none | 1 (avg) | off | ⚡⚡⚡ |
514
+ | `default` | 16 | 5 | hierarchy_fast | 1 (avg) | off | ⚡⚡ |
515
+ | `vibrant()` | 24 | 8 | hierarchy_fast | 2 (dom) | on (3 slots) | ⚡ |
516
+ | `quality()` | 32 | 10 | hierarchy | 2 (dom) | on (4 slots) | 🐢 |
517
+ | `exact_colors()` | 16 | 0 | hierarchy_fast | 1 (avg) | off | ⚡⚡ |
518
+
519
+ ¹ `vibrant()` and `quality()` enable `reserve_colors` + `detail_boost` + `color_rarity`. For the strongest thin-feature retention (faces), additionally set `k_centroid = 4`.
506
520
 
507
521
  ---
508
522
 
509
523
  ## Advanced Usage
510
524
 
525
+ ### Preserving Rare / Important Colors
526
+
527
+ Palette extraction is **area-weighted**: a color that covers a large region gets
528
+ many "votes", so small but perceptually important colors — lips, eyes, a logo,
529
+ a colored highlight — are easily merged into the dominant tones (e.g. lips into
530
+ skin) at low palette sizes (16–64). Three knobs counteract this:
531
+
532
+ | Knob | What it does | When to raise it |
533
+ |------|--------------|------------------|
534
+ | `reserve_colors` | **Hard guarantee.** Reserves N slots, filled with exact source colors that are far from the rest of the palette *and* important. The most reliable fix. | Always, for portraits/artwork with small features. ~`palette_size / 8`. |
535
+ | `detail_boost` | Weights extraction toward colors in **detail-rich regions** using a local-contrast saliency map (measured at the downscale radius, so it targets features thin enough to be averaged away — and picks their *clean* interior color, not muddy edge blends). | When the important color sits in a high-detail area (most facial features). `0.8–1.0`. |
536
+ | `color_rarity` | Damps the frequency vote (`count^p`, `p = 1 − 0.5·rarity`) so large flat regions stop monopolizing the palette. | Mild assist alongside the above. `0.3–0.4`. |
537
+
538
+ Pair these with `k_centroid = 4` (Salient), which stops the *tile* stage from
539
+ discarding the same minority colors. A correct palette is wasted if a 60% skin /
540
+ 40% lip tile is still snapped to skin.
541
+
542
+ ```javascript
543
+ // Recommended for faces / portraits at 16–64 colors
544
+ const config = new WasmDownscaleConfig();
545
+ config.palette_size = 32;
546
+ config.palette_strategy = 'oklab'; // honors color_rarity / detail_boost in median-cut
547
+ config.k_centroid = 4; // salient tile color (keep colorful minorities)
548
+ config.k_centroid_iterations = 2;
549
+ config.reserve_colors = 4; // ~palette_size / 8 — the guarantee
550
+ config.detail_boost = 0.9; // saliency targeting
551
+ config.color_rarity = 0.35; // frequency damping
552
+ config.neighbor_weight = 0.18; // lower = less erosion of thin features
553
+ ```
554
+
555
+ > **Trade-off:** at very small palettes, reserving slots for rare colors costs
556
+ > some gradient smoothness elsewhere. Scale `reserve_colors` with `palette_size`
557
+ > (16→2, 32→4, 64→8) and lean on it harder at the high end.
558
+ >
559
+ > **Strategy note:** `bitmask` ignores frequency weights during its initial
560
+ > split, so with `bitmask` only `reserve_colors` is active. Use `oklab` (or
561
+ > `saturation`) if you also want `color_rarity` / `detail_boost` to shape the
562
+ > main palette.
563
+
511
564
  ### Custom Palette Workflow
512
565
 
513
566
  ```javascript
@@ -705,7 +758,7 @@ smart-downscaler input.png --extract-palette palette.hex -c 16
705
758
  | `--colors` | `-c` | `16` | Palette size |
706
759
  | `--strategy` | `-s` | `oklab` | Palette strategy |
707
760
  | `--segmentation` | | `hierarchy_fast` | Segmentation method |
708
- | `--k-centroid` | | `1` | Tile color mode |
761
+ | `--k-centroid` | | `1` | Tile color mode (1=avg, 2=dominant, 3=foremost, 4=salient) |
709
762
  | `--k-centroid-iterations` | | `0` | Tile refinement |
710
763
  | `--no-refinement` | | false | Disable two-pass |
711
764
  | `--preset` | `-p` | | Use preset (fast/quality/vibrant) |
package/package.json CHANGED
@@ -5,7 +5,7 @@
5
5
  "Pixagram"
6
6
  ],
7
7
  "description": "Intelligent pixel art downscaler with region-aware color quantization",
8
- "version": "0.6.0",
8
+ "version": "0.6.2",
9
9
  "license": "MIT",
10
10
  "repository": {
11
11
  "type": "git",
@@ -30,4 +30,4 @@
30
30
  "quantization",
31
31
  "wasm"
32
32
  ]
33
- }
33
+ }
@@ -33,6 +33,14 @@ export class WasmDownscaleConfig {
33
33
  constructor();
34
34
  static quality(): WasmDownscaleConfig;
35
35
  static vibrant(): WasmDownscaleConfig;
36
+ /**
37
+ * Rare-color preservation: 0.0 = pure area weighting, 1.0 = strong (count^0.5).
38
+ */
39
+ color_rarity: number;
40
+ /**
41
+ * Detail-color boost via local-contrast saliency. 0.0 = off.
42
+ */
43
+ detail_boost: number;
36
44
  edge_weight: number;
37
45
  hierarchy_min_size: number;
38
46
  hierarchy_threshold: number;
@@ -45,6 +53,10 @@ export class WasmDownscaleConfig {
45
53
  palette_size: number;
46
54
  refinement_iterations: number;
47
55
  region_weight: number;
56
+ /**
57
+ * Reserve N palette slots for distinct, important, under-represented colors. 0 = off.
58
+ */
59
+ reserve_colors: number;
48
60
  slic_compactness: number;
49
61
  slic_superpixels: number;
50
62
  two_pass_refinement: boolean;
@@ -154,6 +154,22 @@ export class WasmDownscaleConfig {
154
154
  const ptr = this.__destroy_into_raw();
155
155
  wasm.__wbg_wasmdownscaleconfig_free(ptr, 0);
156
156
  }
157
+ /**
158
+ * Rare-color preservation: 0.0 = pure area weighting, 1.0 = strong (count^0.5).
159
+ * @returns {number}
160
+ */
161
+ get color_rarity() {
162
+ const ret = wasm.__wbg_get_wasmdownscaleconfig_color_rarity(this.__wbg_ptr);
163
+ return ret;
164
+ }
165
+ /**
166
+ * Detail-color boost via local-contrast saliency. 0.0 = off.
167
+ * @returns {number}
168
+ */
169
+ get detail_boost() {
170
+ const ret = wasm.__wbg_get_wasmdownscaleconfig_detail_boost(this.__wbg_ptr);
171
+ return ret;
172
+ }
157
173
  /**
158
174
  * @returns {number}
159
175
  */
@@ -238,6 +254,14 @@ export class WasmDownscaleConfig {
238
254
  const ret = wasm.__wbg_get_wasmdownscaleconfig_region_weight(this.__wbg_ptr);
239
255
  return ret;
240
256
  }
257
+ /**
258
+ * Reserve N palette slots for distinct, important, under-represented colors. 0 = off.
259
+ * @returns {number}
260
+ */
261
+ get reserve_colors() {
262
+ const ret = wasm.__wbg_get_wasmdownscaleconfig_reserve_colors(this.__wbg_ptr);
263
+ return ret >>> 0;
264
+ }
241
265
  /**
242
266
  * @returns {number}
243
267
  */
@@ -259,6 +283,20 @@ export class WasmDownscaleConfig {
259
283
  const ret = wasm.__wbg_get_wasmdownscaleconfig_two_pass_refinement(this.__wbg_ptr);
260
284
  return ret !== 0;
261
285
  }
286
+ /**
287
+ * Rare-color preservation: 0.0 = pure area weighting, 1.0 = strong (count^0.5).
288
+ * @param {number} arg0
289
+ */
290
+ set color_rarity(arg0) {
291
+ wasm.__wbg_set_wasmdownscaleconfig_color_rarity(this.__wbg_ptr, arg0);
292
+ }
293
+ /**
294
+ * Detail-color boost via local-contrast saliency. 0.0 = off.
295
+ * @param {number} arg0
296
+ */
297
+ set detail_boost(arg0) {
298
+ wasm.__wbg_set_wasmdownscaleconfig_detail_boost(this.__wbg_ptr, arg0);
299
+ }
262
300
  /**
263
301
  * @param {number} arg0
264
302
  */
@@ -331,6 +369,13 @@ export class WasmDownscaleConfig {
331
369
  set region_weight(arg0) {
332
370
  wasm.__wbg_set_wasmdownscaleconfig_region_weight(this.__wbg_ptr, arg0);
333
371
  }
372
+ /**
373
+ * Reserve N palette slots for distinct, important, under-represented colors. 0 = off.
374
+ * @param {number} arg0
375
+ */
376
+ set reserve_colors(arg0) {
377
+ wasm.__wbg_set_wasmdownscaleconfig_reserve_colors(this.__wbg_ptr, arg0);
378
+ }
334
379
  /**
335
380
  * @param {number} arg0
336
381
  */
Binary file