smart-downscaler 0.2.1 → 0.3.0
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 +205 -161
- package/package.json +2 -2
- package/smart_downscaler.d.ts +18 -12
- package/smart_downscaler_bg.js +58 -51
- package/smart_downscaler_bg.wasm +0 -0
package/README.md
CHANGED
|
@@ -4,9 +4,39 @@ A sophisticated Rust library for intelligent image downscaling with focus on pix
|
|
|
4
4
|
|
|
5
5
|
**Available as both a native Rust library and a WebAssembly module for browser/Node.js usage.**
|
|
6
6
|
|
|
7
|
+
## What's New in v0.2
|
|
8
|
+
|
|
9
|
+
### 🎨 Oklab Color Space
|
|
10
|
+
|
|
11
|
+
Version 0.2 introduces **Oklab color space** for all color operations, solving the common problem of desaturated, muddy colors:
|
|
12
|
+
|
|
13
|
+
| Before (RGB Median Cut) | After (Oklab Median Cut) |
|
|
14
|
+
|-------------------------|--------------------------|
|
|
15
|
+
| Colors appear tanned/darkened | True color preservation |
|
|
16
|
+
| Saturated colors become muddy | Vibrant colors maintained |
|
|
17
|
+
| RGB averaging loses chroma | Perceptually uniform blending |
|
|
18
|
+
|
|
19
|
+
### Palette Strategies
|
|
20
|
+
|
|
21
|
+
Choose the best strategy for your use case:
|
|
22
|
+
|
|
23
|
+
```javascript
|
|
24
|
+
const config = new WasmDownscaleConfig();
|
|
25
|
+
config.palette_strategy = 'saturation'; // For vibrant pixel art
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
| Strategy | Best For | Description |
|
|
29
|
+
|----------|----------|-------------|
|
|
30
|
+
| `oklab` | General use | Default, best overall quality |
|
|
31
|
+
| `saturation` | Vibrant art | Preserves highly saturated colors |
|
|
32
|
+
| `medoid` | Exact colors | Only uses colors from source image |
|
|
33
|
+
| `kmeans` | Small palettes | K-Means++ clustering |
|
|
34
|
+
| `legacy` | Comparison | Original RGB (not recommended) |
|
|
35
|
+
|
|
7
36
|
## Features
|
|
8
37
|
|
|
9
|
-
- **
|
|
38
|
+
- **Oklab Color Space**: Modern perceptual color space with superior hue linearity
|
|
39
|
+
- **Global Palette Extraction**: Median Cut + K-Means++ refinement
|
|
10
40
|
- **Multiple Segmentation Methods**:
|
|
11
41
|
- SLIC superpixels for fast, balanced regions
|
|
12
42
|
- VTracer-style hierarchical clustering for content-aware boundaries
|
|
@@ -14,7 +44,6 @@ A sophisticated Rust library for intelligent image downscaling with focus on pix
|
|
|
14
44
|
- **Edge-Aware Processing**: Sobel/Scharr edge detection to preserve boundaries
|
|
15
45
|
- **Neighbor-Coherent Assignment**: Spatial coherence through neighbor and region voting
|
|
16
46
|
- **Two-Pass Refinement**: Iterative optimization for smooth results
|
|
17
|
-
- **Graph-Cut Optimization**: Optional MRF energy minimization for advanced refinement
|
|
18
47
|
- **WebAssembly Support**: Run in browsers with full performance
|
|
19
48
|
|
|
20
49
|
## Installation
|
|
@@ -25,7 +54,7 @@ Add to your `Cargo.toml`:
|
|
|
25
54
|
|
|
26
55
|
```toml
|
|
27
56
|
[dependencies]
|
|
28
|
-
smart-downscaler = "0.
|
|
57
|
+
smart-downscaler = "0.2"
|
|
29
58
|
```
|
|
30
59
|
|
|
31
60
|
### WebAssembly (npm)
|
|
@@ -49,11 +78,24 @@ Or use directly in browser:
|
|
|
49
78
|
|
|
50
79
|
```rust
|
|
51
80
|
use smart_downscaler::prelude::*;
|
|
81
|
+
use smart_downscaler::palette::PaletteStrategy;
|
|
52
82
|
|
|
53
83
|
fn main() {
|
|
54
84
|
let img = image::open("input.png").unwrap().to_rgb8();
|
|
85
|
+
|
|
86
|
+
// Simple usage
|
|
55
87
|
let result = downscale(&img, 64, 64, 16);
|
|
56
88
|
result.save("output.png").unwrap();
|
|
89
|
+
|
|
90
|
+
// Advanced: preserve vibrant colors
|
|
91
|
+
let config = DownscaleConfig {
|
|
92
|
+
palette_size: 24,
|
|
93
|
+
palette_strategy: PaletteStrategy::SaturationWeighted,
|
|
94
|
+
..Default::default()
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
let pixels: Vec<Rgb> = img.pixels().map(|&p| p.into()).collect();
|
|
98
|
+
let result = smart_downscale(&pixels, img.width() as usize, img.height() as usize, 64, 64, &config);
|
|
57
99
|
}
|
|
58
100
|
```
|
|
59
101
|
|
|
@@ -62,111 +104,128 @@ fn main() {
|
|
|
62
104
|
```javascript
|
|
63
105
|
import init, { WasmDownscaleConfig, downscale_rgba } from 'smart-downscaler';
|
|
64
106
|
|
|
65
|
-
// Initialize WASM
|
|
66
107
|
await init();
|
|
67
108
|
|
|
68
|
-
// Get image data from canvas
|
|
69
109
|
const ctx = canvas.getContext('2d');
|
|
70
110
|
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
|
|
71
111
|
|
|
72
|
-
//
|
|
73
|
-
const config =
|
|
112
|
+
// Use vibrant preset for saturated colors
|
|
113
|
+
const config = WasmDownscaleConfig.vibrant();
|
|
74
114
|
config.palette_size = 16;
|
|
75
|
-
config.neighbor_weight = 0.3;
|
|
76
|
-
config.segmentation_method = 'hierarchy_fast';
|
|
77
115
|
|
|
78
|
-
//
|
|
116
|
+
// Or configure manually
|
|
117
|
+
const config2 = new WasmDownscaleConfig();
|
|
118
|
+
config2.palette_size = 16;
|
|
119
|
+
config2.palette_strategy = 'saturation';
|
|
120
|
+
config2.neighbor_weight = 0.3;
|
|
121
|
+
|
|
79
122
|
const result = downscale_rgba(
|
|
80
123
|
imageData.data,
|
|
81
124
|
canvas.width,
|
|
82
125
|
canvas.height,
|
|
83
|
-
64, 64,
|
|
126
|
+
64, 64,
|
|
84
127
|
config
|
|
85
128
|
);
|
|
86
129
|
|
|
87
|
-
// Use result
|
|
88
130
|
const outputData = new ImageData(result.data, result.width, result.height);
|
|
89
131
|
outputCtx.putImageData(outputData, 0, 0);
|
|
90
|
-
|
|
91
|
-
// Access palette
|
|
92
|
-
console.log(`Used ${result.palette_size} colors`);
|
|
93
132
|
```
|
|
94
133
|
|
|
95
|
-
###
|
|
96
|
-
|
|
97
|
-
For an even simpler API, use the included JavaScript wrapper:
|
|
134
|
+
### Configuration Presets
|
|
98
135
|
|
|
99
136
|
```javascript
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
await init();
|
|
137
|
+
// Speed optimized
|
|
138
|
+
const fast = WasmDownscaleConfig.fast();
|
|
103
139
|
|
|
104
|
-
//
|
|
105
|
-
const
|
|
106
|
-
paletteSize: 16,
|
|
107
|
-
segmentation: 'hierarchy_fast'
|
|
108
|
-
});
|
|
140
|
+
// Best quality
|
|
141
|
+
const quality = WasmDownscaleConfig.quality();
|
|
109
142
|
|
|
110
|
-
//
|
|
111
|
-
const
|
|
112
|
-
const gbResult = downscaleWithPalette(canvas, 64, 64, gbPalette);
|
|
143
|
+
// Preserve vibrant colors
|
|
144
|
+
const vibrant = WasmDownscaleConfig.vibrant();
|
|
113
145
|
|
|
114
|
-
//
|
|
115
|
-
const
|
|
146
|
+
// Use only exact source colors
|
|
147
|
+
const exact = WasmDownscaleConfig.exact_colors();
|
|
116
148
|
```
|
|
117
149
|
|
|
118
|
-
##
|
|
150
|
+
## Why Oklab?
|
|
119
151
|
|
|
120
|
-
|
|
152
|
+
Traditional RGB-based palette extraction has fundamental problems:
|
|
121
153
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
154
|
+
### The Problem: RGB Averaging Desaturates Colors
|
|
155
|
+
|
|
156
|
+
```
|
|
157
|
+
Red [255, 0, 0] + Cyan [0, 255, 255]
|
|
158
|
+
RGB Average → Gray [127, 127, 127] ❌
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
When you average colors in RGB space, saturated colors get pulled toward gray. This is why downscaled images often look "washed out" or "tanned."
|
|
125
162
|
|
|
126
|
-
|
|
127
|
-
./build-wasm.sh
|
|
163
|
+
### The Solution: Oklab Color Space
|
|
128
164
|
|
|
129
|
-
|
|
130
|
-
|
|
165
|
+
Oklab is a **perceptually uniform** color space where:
|
|
166
|
+
- Euclidean distance = perceived color difference
|
|
167
|
+
- Averaging preserves hue and saturation
|
|
168
|
+
- Interpolations look natural
|
|
169
|
+
|
|
170
|
+
```
|
|
171
|
+
Red (Oklab) + Cyan (Oklab)
|
|
172
|
+
Oklab Average → Preserves colorfulness ✓
|
|
131
173
|
```
|
|
132
174
|
|
|
133
|
-
|
|
175
|
+
### Visual Comparison
|
|
134
176
|
|
|
135
|
-
|
|
177
|
+
| Issue | RGB Median Cut | Oklab Median Cut |
|
|
178
|
+
|-------|----------------|------------------|
|
|
179
|
+
| Saturated colors | Become muddy | Stay vibrant |
|
|
180
|
+
| Gradients | Shift in hue | Stay consistent |
|
|
181
|
+
| Dark colors | Get darker | Accurate lightness |
|
|
182
|
+
| Overall look | Desaturated | True to source |
|
|
136
183
|
|
|
137
|
-
|
|
138
|
-
|----------|-------------|
|
|
139
|
-
| `downscale(data, w, h, tw, th, config?)` | Downscale RGBA image data |
|
|
140
|
-
| `downscale_rgba(data, w, h, tw, th, config?)` | Same, for Uint8ClampedArray |
|
|
141
|
-
| `downscale_simple(data, w, h, tw, th, colors)` | Simple API with color count |
|
|
142
|
-
| `downscale_with_palette(data, w, h, tw, th, palette, config?)` | Use custom palette |
|
|
143
|
-
| `extract_palette_from_image(data, w, h, colors, iters)` | Extract palette only |
|
|
144
|
-
| `quantize_to_palette(data, w, h, palette)` | Quantize without resizing |
|
|
184
|
+
## API Reference
|
|
145
185
|
|
|
146
186
|
### WasmDownscaleConfig
|
|
147
187
|
|
|
148
188
|
```javascript
|
|
149
189
|
const config = new WasmDownscaleConfig();
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
config.
|
|
153
|
-
config.
|
|
154
|
-
config.
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
config.
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
190
|
+
|
|
191
|
+
// Palette settings
|
|
192
|
+
config.palette_size = 16; // Number of output colors
|
|
193
|
+
config.palette_strategy = 'oklab'; // 'oklab', 'saturation', 'medoid', 'kmeans', 'legacy'
|
|
194
|
+
config.kmeans_iterations = 5; // Refinement iterations
|
|
195
|
+
|
|
196
|
+
// Spatial coherence
|
|
197
|
+
config.neighbor_weight = 0.3; // [0-1] Prefer neighbor colors
|
|
198
|
+
config.region_weight = 0.2; // [0-1] Prefer region colors
|
|
199
|
+
|
|
200
|
+
// Refinement
|
|
201
|
+
config.two_pass_refinement = true;
|
|
202
|
+
config.refinement_iterations = 3;
|
|
203
|
+
|
|
204
|
+
// Edge detection
|
|
205
|
+
config.edge_weight = 0.5;
|
|
206
|
+
|
|
207
|
+
// Segmentation
|
|
208
|
+
config.segmentation_method = 'hierarchy_fast'; // 'none', 'slic', 'hierarchy', 'hierarchy_fast'
|
|
162
209
|
```
|
|
163
210
|
|
|
211
|
+
### Available Functions
|
|
212
|
+
|
|
213
|
+
| Function | Description |
|
|
214
|
+
|----------|-------------|
|
|
215
|
+
| `downscale(data, w, h, tw, th, config?)` | Main downscale function |
|
|
216
|
+
| `downscale_rgba(data, w, h, tw, th, config?)` | For Uint8ClampedArray input |
|
|
217
|
+
| `downscale_simple(data, w, h, tw, th, colors)` | Simple API |
|
|
218
|
+
| `downscale_with_palette(...)` | Use custom palette |
|
|
219
|
+
| `extract_palette_from_image(data, w, h, colors, iters, strategy?)` | Extract palette only |
|
|
220
|
+
| `quantize_to_palette(data, w, h, palette)` | Quantize without resizing |
|
|
221
|
+
| `get_palette_strategies()` | List available strategies |
|
|
222
|
+
|
|
164
223
|
### WasmDownscaleResult
|
|
165
224
|
|
|
166
225
|
```javascript
|
|
167
226
|
result.width // Output width
|
|
168
|
-
result.height // Output height
|
|
169
|
-
result.data // Uint8ClampedArray (RGBA
|
|
227
|
+
result.height // Output height
|
|
228
|
+
result.data // Uint8ClampedArray (RGBA)
|
|
170
229
|
result.rgb_data() // Uint8Array (RGB only)
|
|
171
230
|
result.palette // Uint8Array (RGB, 3 bytes per color)
|
|
172
231
|
result.indices // Uint8Array (palette index per pixel)
|
|
@@ -179,143 +238,129 @@ result.palette_size // Number of colors
|
|
|
179
238
|
# Basic usage
|
|
180
239
|
smart-downscaler input.png output.png -w 64 -h 64
|
|
181
240
|
|
|
182
|
-
# With
|
|
183
|
-
smart-downscaler input.png output.png -w
|
|
241
|
+
# With saturation preservation
|
|
242
|
+
smart-downscaler input.png output.png -w 64 -h 64 --palette-strategy saturation
|
|
184
243
|
|
|
185
|
-
# Using
|
|
186
|
-
smart-downscaler input.png output.png -
|
|
244
|
+
# Using exact source colors only
|
|
245
|
+
smart-downscaler input.png output.png -w 64 -h 64 --palette-strategy medoid -p 24
|
|
187
246
|
|
|
188
|
-
#
|
|
189
|
-
smart-downscaler input.png output.png
|
|
247
|
+
# Full options
|
|
248
|
+
smart-downscaler input.png output.png \
|
|
249
|
+
-w 128 -h 128 \
|
|
250
|
+
-p 32 \
|
|
251
|
+
--palette-strategy saturation \
|
|
252
|
+
--segmentation hierarchy-fast \
|
|
253
|
+
--neighbor-weight 0.4
|
|
190
254
|
```
|
|
191
255
|
|
|
192
256
|
## Algorithm Details
|
|
193
257
|
|
|
194
|
-
### 1.
|
|
258
|
+
### 1. Palette Extraction (Oklab Median Cut)
|
|
195
259
|
|
|
196
|
-
|
|
260
|
+
1. Convert all pixels to Oklab color space
|
|
261
|
+
2. Build weighted color histogram
|
|
262
|
+
3. Apply Median Cut to partition Oklab space
|
|
263
|
+
4. For each bucket, compute centroid in Oklab
|
|
264
|
+
5. Convert centroids back to RGB
|
|
265
|
+
6. Refine with K-Means++ in Oklab space
|
|
197
266
|
|
|
198
|
-
|
|
199
|
-
2. Apply Median Cut to partition the color space, finding initial centroids with good distribution
|
|
200
|
-
3. Refine centroids using K-Means++ in CIE Lab space for perceptual accuracy
|
|
267
|
+
### 2. Saturation-Weighted Strategy
|
|
201
268
|
|
|
202
|
-
|
|
269
|
+
When using `saturation` strategy:
|
|
270
|
+
```
|
|
271
|
+
effective_weight = pixel_count × (1 + chroma × 2)
|
|
272
|
+
```
|
|
203
273
|
|
|
204
|
-
|
|
274
|
+
This boosts the influence of highly saturated colors during Median Cut partitioning.
|
|
205
275
|
|
|
206
|
-
|
|
207
|
-
- Iteratively clusters pixels by color and spatial proximity
|
|
208
|
-
- Produces compact, regular regions
|
|
209
|
-
- Fast and predictable
|
|
276
|
+
### 3. Medoid Strategy
|
|
210
277
|
|
|
211
|
-
|
|
212
|
-
- Bottom-up merging of similar adjacent pixels
|
|
213
|
-
- Content-aware boundaries that follow natural edges
|
|
214
|
-
- Configurable merge threshold and minimum region size
|
|
278
|
+
Instead of computing centroids (averages), selects the actual image color closest to the centroid. Guarantees output palette contains only exact source colors.
|
|
215
279
|
|
|
216
|
-
|
|
217
|
-
- O(α(n)) per operation using union by rank + path compression
|
|
218
|
-
- Best for large images where full hierarchical is too slow
|
|
280
|
+
### 4. Region Pre-Segmentation
|
|
219
281
|
|
|
220
|
-
|
|
282
|
+
Before downscaling, identifies coherent regions:
|
|
283
|
+
- **SLIC**: Fast, regular superpixels
|
|
284
|
+
- **Hierarchy**: Content-aware boundaries
|
|
285
|
+
- **Hierarchy Fast**: O(α(n)) union-find
|
|
221
286
|
|
|
222
|
-
|
|
287
|
+
### 5. Edge-Aware Tile Computation
|
|
223
288
|
|
|
224
289
|
```
|
|
225
|
-
weight(pixel) = 1 / (1 + edge_strength
|
|
290
|
+
weight(pixel) = 1 / (1 + edge_strength × edge_weight)
|
|
226
291
|
```
|
|
227
292
|
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
### 4. Neighbor-Coherent Assignment
|
|
293
|
+
Reduces influence of transitional edge pixels.
|
|
231
294
|
|
|
232
|
-
|
|
295
|
+
### 6. Neighbor-Coherent Assignment
|
|
233
296
|
|
|
234
|
-
- **Color distance** to the tile's weighted average (primary factor)
|
|
235
|
-
- **Neighbor votes**: already-assigned neighbors bias toward their colors
|
|
236
|
-
- **Region membership**: tiles in the same source region prefer consistent colors
|
|
237
|
-
|
|
238
|
-
The scoring function:
|
|
239
297
|
```
|
|
240
|
-
score(color) =
|
|
298
|
+
score(color) = oklab_distance(color, tile_avg) × (1 - neighbor_bias - region_bias)
|
|
241
299
|
```
|
|
242
300
|
|
|
243
|
-
### 5. Two-Pass Refinement
|
|
244
|
-
|
|
245
|
-
After initial assignment, we iteratively refine:
|
|
246
|
-
|
|
247
|
-
1. For each pixel, gather all 8 neighbors
|
|
248
|
-
2. Re-evaluate the best palette color considering neighbor votes
|
|
249
|
-
3. Update if a better assignment is found
|
|
250
|
-
4. Repeat until convergence or max iterations
|
|
251
|
-
|
|
252
|
-
This smooths isolated outliers while preserving intentional edges.
|
|
253
|
-
|
|
254
|
-
### 6. Graph-Cut Optimization (Optional)
|
|
255
|
-
|
|
256
|
-
For highest quality, we offer MRF energy minimization:
|
|
257
|
-
|
|
258
|
-
- **Data term**: color distance between tile and palette color
|
|
259
|
-
- **Smoothness term**: penalty for different labels between neighbors
|
|
260
|
-
- **Alpha-expansion**: iteratively try changing each pixel to each label
|
|
261
|
-
|
|
262
|
-
## Comparison with Existing Tools
|
|
263
|
-
|
|
264
|
-
| Feature | Smart Downscaler | Per-Tile K-Means | Simple Resize |
|
|
265
|
-
|---------|------------------|------------------|---------------|
|
|
266
|
-
| Global color consistency | ✓ | ✗ | ✗ |
|
|
267
|
-
| Edge preservation | ✓ | Partial | ✗ |
|
|
268
|
-
| Region awareness | ✓ | ✗ | ✗ |
|
|
269
|
-
| Spatial coherence | ✓ | ✗ | ✗ |
|
|
270
|
-
| Two-pass refinement | ✓ | ✗ | ✗ |
|
|
271
|
-
| Custom palette support | ✓ | ✓ | ✗ |
|
|
272
|
-
| Perceptual color space | ✓ (Lab) | Often RGB | N/A |
|
|
273
|
-
|
|
274
301
|
## Performance
|
|
275
302
|
|
|
276
|
-
Typical performance
|
|
303
|
+
Typical performance (single-threaded):
|
|
277
304
|
|
|
278
305
|
| Image Size | Target Size | Palette | Time |
|
|
279
306
|
|------------|-------------|---------|------|
|
|
280
307
|
| 256×256 | 32×32 | 16 | ~50ms |
|
|
281
308
|
| 512×512 | 64×64 | 32 | ~200ms |
|
|
282
309
|
| 1024×1024 | 128×128 | 32 | ~800ms |
|
|
283
|
-
| 2048×2048 | 256×256 | 64 | ~3s |
|
|
284
310
|
|
|
285
|
-
Enable
|
|
311
|
+
Enable `parallel` feature for multi-threaded processing.
|
|
286
312
|
|
|
287
313
|
## Configuration Reference
|
|
288
314
|
|
|
289
|
-
### DownscaleConfig
|
|
315
|
+
### DownscaleConfig (Rust)
|
|
290
316
|
|
|
291
317
|
| Field | Type | Default | Description |
|
|
292
318
|
|-------|------|---------|-------------|
|
|
293
|
-
| `palette_size` | usize | 16 |
|
|
294
|
-
| `
|
|
295
|
-
| `
|
|
296
|
-
| `
|
|
297
|
-
| `
|
|
298
|
-
| `
|
|
299
|
-
| `
|
|
300
|
-
| `
|
|
319
|
+
| `palette_size` | usize | 16 | Output colors |
|
|
320
|
+
| `palette_strategy` | PaletteStrategy | OklabMedianCut | Extraction method |
|
|
321
|
+
| `kmeans_iterations` | usize | 5 | Refinement iterations |
|
|
322
|
+
| `neighbor_weight` | f32 | 0.3 | Neighbor coherence |
|
|
323
|
+
| `region_weight` | f32 | 0.2 | Region coherence |
|
|
324
|
+
| `two_pass_refinement` | bool | true | Enable refinement |
|
|
325
|
+
| `refinement_iterations` | usize | 3 | Max iterations |
|
|
326
|
+
| `segmentation` | SegmentationMethod | Hierarchy | Pre-segmentation |
|
|
327
|
+
| `edge_weight` | f32 | 0.5 | Edge influence |
|
|
328
|
+
|
|
329
|
+
### PaletteStrategy
|
|
330
|
+
|
|
331
|
+
| Value | Description |
|
|
332
|
+
|-------|-------------|
|
|
333
|
+
| `OklabMedianCut` | Default, best general quality |
|
|
334
|
+
| `SaturationWeighted` | Preserves vibrant colors |
|
|
335
|
+
| `Medoid` | Exact source colors only |
|
|
336
|
+
| `KMeansPlusPlus` | K-Means++ clustering |
|
|
337
|
+
| `LegacyRgb` | Original RGB (not recommended) |
|
|
338
|
+
|
|
339
|
+
## Troubleshooting
|
|
340
|
+
|
|
341
|
+
### Colors still look desaturated
|
|
342
|
+
|
|
343
|
+
Try increasing palette size or using `saturation` strategy:
|
|
344
|
+
```javascript
|
|
345
|
+
config.palette_size = 24; // Up from 16
|
|
346
|
+
config.palette_strategy = 'saturation';
|
|
347
|
+
```
|
|
301
348
|
|
|
302
|
-
###
|
|
349
|
+
### Want exact source colors
|
|
303
350
|
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
| `spatial_weight` | f32 | 0.1 | Spatial proximity influence |
|
|
351
|
+
Use medoid strategy with no K-Means refinement:
|
|
352
|
+
```javascript
|
|
353
|
+
config.palette_strategy = 'medoid';
|
|
354
|
+
config.kmeans_iterations = 0;
|
|
355
|
+
```
|
|
310
356
|
|
|
311
|
-
###
|
|
357
|
+
### Output looks noisy
|
|
312
358
|
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
| `convergence_threshold` | f32 | 1.0 | Early termination threshold |
|
|
359
|
+
Increase neighbor weight for smoother results:
|
|
360
|
+
```javascript
|
|
361
|
+
config.neighbor_weight = 0.5; // Up from 0.3
|
|
362
|
+
config.two_pass_refinement = true;
|
|
363
|
+
```
|
|
319
364
|
|
|
320
365
|
## License
|
|
321
366
|
|
|
@@ -323,8 +368,7 @@ MIT
|
|
|
323
368
|
|
|
324
369
|
## Credits
|
|
325
370
|
|
|
326
|
-
|
|
327
|
-
- VTracer's hierarchical clustering approach
|
|
371
|
+
- Oklab color space by Björn Ottosson
|
|
328
372
|
- SLIC superpixel algorithm
|
|
329
373
|
- K-Means++ initialization
|
|
330
|
-
-
|
|
374
|
+
- VTracer hierarchical clustering approach
|
package/package.json
CHANGED
|
@@ -5,11 +5,11 @@
|
|
|
5
5
|
"Pixagram"
|
|
6
6
|
],
|
|
7
7
|
"description": "Intelligent pixel art downscaler with region-aware color quantization",
|
|
8
|
-
"version": "0.
|
|
8
|
+
"version": "0.3.0",
|
|
9
9
|
"license": "MIT",
|
|
10
10
|
"repository": {
|
|
11
11
|
"type": "git",
|
|
12
|
-
"url": "https://github.com/pixagram
|
|
12
|
+
"url": "https://github.com/pixagram/smart-downscaler"
|
|
13
13
|
},
|
|
14
14
|
"files": [
|
|
15
15
|
"smart_downscaler_bg.wasm",
|
package/smart_downscaler.d.ts
CHANGED
|
@@ -16,6 +16,14 @@ export class WasmDownscaleConfig {
|
|
|
16
16
|
* Create configuration optimized for quality
|
|
17
17
|
*/
|
|
18
18
|
static quality(): WasmDownscaleConfig;
|
|
19
|
+
/**
|
|
20
|
+
* Create configuration optimized for vibrant colors
|
|
21
|
+
*/
|
|
22
|
+
static vibrant(): WasmDownscaleConfig;
|
|
23
|
+
/**
|
|
24
|
+
* Create configuration that uses only exact image colors (medoid)
|
|
25
|
+
*/
|
|
26
|
+
static exact_colors(): WasmDownscaleConfig;
|
|
19
27
|
/**
|
|
20
28
|
* Number of colors in the output palette (default: 16)
|
|
21
29
|
*/
|
|
@@ -64,6 +72,10 @@ export class WasmDownscaleConfig {
|
|
|
64
72
|
* Get the segmentation method
|
|
65
73
|
*/
|
|
66
74
|
segmentation_method: string;
|
|
75
|
+
/**
|
|
76
|
+
* Get the palette extraction strategy
|
|
77
|
+
*/
|
|
78
|
+
palette_strategy: string;
|
|
67
79
|
}
|
|
68
80
|
|
|
69
81
|
export class WasmDownscaleResult {
|
|
@@ -104,17 +116,6 @@ export class WasmDownscaleResult {
|
|
|
104
116
|
readonly palette_size: number;
|
|
105
117
|
}
|
|
106
118
|
|
|
107
|
-
/**
|
|
108
|
-
* Highly optimized color analysis using a custom linear-probing hash table.
|
|
109
|
-
*
|
|
110
|
-
* OPTIMIZATIONS:
|
|
111
|
-
* 1. Casts u8 slice to u32 slice (Zero Copy, 4x read speed)
|
|
112
|
-
* 2. Uses Power-of-Two capacity for bitwise masking (vs modulo)
|
|
113
|
-
* 3. FxHash-style integer mixer
|
|
114
|
-
* 4. Linear probing with localized memory access
|
|
115
|
-
*/
|
|
116
|
-
export function analyze_colors(image_data: Uint8Array, max_colors: number, sort_method: string): any;
|
|
117
|
-
|
|
118
119
|
/**
|
|
119
120
|
* Main downscaling function for WebAssembly
|
|
120
121
|
*
|
|
@@ -149,7 +150,12 @@ export function downscale_with_palette(image_data: Uint8Array, width: number, he
|
|
|
149
150
|
/**
|
|
150
151
|
* Extract a color palette from an image without downscaling
|
|
151
152
|
*/
|
|
152
|
-
export function extract_palette_from_image(image_data: Uint8Array, _width: number, _height: number, num_colors: number, kmeans_iterations: number): Uint8Array;
|
|
153
|
+
export function extract_palette_from_image(image_data: Uint8Array, _width: number, _height: number, num_colors: number, kmeans_iterations: number, strategy?: string | null): Uint8Array;
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Get available palette strategies
|
|
157
|
+
*/
|
|
158
|
+
export function get_palette_strategies(): Array<any>;
|
|
153
159
|
|
|
154
160
|
/**
|
|
155
161
|
* Initialize panic hook for better error messages in browser console
|
package/smart_downscaler_bg.js
CHANGED
|
@@ -31,13 +31,6 @@ function isLikeNone(x) {
|
|
|
31
31
|
return x === undefined || x === null;
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
-
function passArray8ToWasm0(arg, malloc) {
|
|
35
|
-
const ptr = malloc(arg.length * 1, 1) >>> 0;
|
|
36
|
-
getUint8ArrayMemory0().set(arg, ptr / 1);
|
|
37
|
-
WASM_VECTOR_LEN = arg.length;
|
|
38
|
-
return ptr;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
34
|
function passStringToWasm0(arg, malloc, realloc) {
|
|
42
35
|
if (realloc === undefined) {
|
|
43
36
|
const buf = cachedTextEncoder.encode(arg);
|
|
@@ -338,6 +331,31 @@ export class WasmDownscaleConfig {
|
|
|
338
331
|
wasm.__wbindgen_free(deferred1_0, deferred1_1, 1);
|
|
339
332
|
}
|
|
340
333
|
}
|
|
334
|
+
/**
|
|
335
|
+
* Set the palette extraction strategy
|
|
336
|
+
* @param {string} strategy
|
|
337
|
+
*/
|
|
338
|
+
set palette_strategy(strategy) {
|
|
339
|
+
const ptr0 = passStringToWasm0(strategy, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
|
|
340
|
+
const len0 = WASM_VECTOR_LEN;
|
|
341
|
+
wasm.wasmdownscaleconfig_set_palette_strategy(this.__wbg_ptr, ptr0, len0);
|
|
342
|
+
}
|
|
343
|
+
/**
|
|
344
|
+
* Get the palette extraction strategy
|
|
345
|
+
* @returns {string}
|
|
346
|
+
*/
|
|
347
|
+
get palette_strategy() {
|
|
348
|
+
let deferred1_0;
|
|
349
|
+
let deferred1_1;
|
|
350
|
+
try {
|
|
351
|
+
const ret = wasm.wasmdownscaleconfig_palette_strategy(this.__wbg_ptr);
|
|
352
|
+
deferred1_0 = ret[0];
|
|
353
|
+
deferred1_1 = ret[1];
|
|
354
|
+
return getStringFromWasm0(ret[0], ret[1]);
|
|
355
|
+
} finally {
|
|
356
|
+
wasm.__wbindgen_free(deferred1_0, deferred1_1, 1);
|
|
357
|
+
}
|
|
358
|
+
}
|
|
341
359
|
/**
|
|
342
360
|
* Create configuration optimized for speed
|
|
343
361
|
* @returns {WasmDownscaleConfig}
|
|
@@ -354,6 +372,22 @@ export class WasmDownscaleConfig {
|
|
|
354
372
|
const ret = wasm.wasmdownscaleconfig_quality();
|
|
355
373
|
return WasmDownscaleConfig.__wrap(ret);
|
|
356
374
|
}
|
|
375
|
+
/**
|
|
376
|
+
* Create configuration optimized for vibrant colors
|
|
377
|
+
* @returns {WasmDownscaleConfig}
|
|
378
|
+
*/
|
|
379
|
+
static vibrant() {
|
|
380
|
+
const ret = wasm.wasmdownscaleconfig_vibrant();
|
|
381
|
+
return WasmDownscaleConfig.__wrap(ret);
|
|
382
|
+
}
|
|
383
|
+
/**
|
|
384
|
+
* Create configuration that uses only exact image colors (medoid)
|
|
385
|
+
* @returns {WasmDownscaleConfig}
|
|
386
|
+
*/
|
|
387
|
+
static exact_colors() {
|
|
388
|
+
const ret = wasm.wasmdownscaleconfig_exact_colors();
|
|
389
|
+
return WasmDownscaleConfig.__wrap(ret);
|
|
390
|
+
}
|
|
357
391
|
}
|
|
358
392
|
if (Symbol.dispose) WasmDownscaleConfig.prototype[Symbol.dispose] = WasmDownscaleConfig.prototype.free;
|
|
359
393
|
|
|
@@ -446,31 +480,6 @@ export class WasmDownscaleResult {
|
|
|
446
480
|
}
|
|
447
481
|
if (Symbol.dispose) WasmDownscaleResult.prototype[Symbol.dispose] = WasmDownscaleResult.prototype.free;
|
|
448
482
|
|
|
449
|
-
/**
|
|
450
|
-
* Highly optimized color analysis using a custom linear-probing hash table.
|
|
451
|
-
*
|
|
452
|
-
* OPTIMIZATIONS:
|
|
453
|
-
* 1. Casts u8 slice to u32 slice (Zero Copy, 4x read speed)
|
|
454
|
-
* 2. Uses Power-of-Two capacity for bitwise masking (vs modulo)
|
|
455
|
-
* 3. FxHash-style integer mixer
|
|
456
|
-
* 4. Linear probing with localized memory access
|
|
457
|
-
* @param {Uint8Array} image_data
|
|
458
|
-
* @param {number} max_colors
|
|
459
|
-
* @param {string} sort_method
|
|
460
|
-
* @returns {any}
|
|
461
|
-
*/
|
|
462
|
-
export function analyze_colors(image_data, max_colors, sort_method) {
|
|
463
|
-
const ptr0 = passArray8ToWasm0(image_data, wasm.__wbindgen_malloc);
|
|
464
|
-
const len0 = WASM_VECTOR_LEN;
|
|
465
|
-
const ptr1 = passStringToWasm0(sort_method, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
|
|
466
|
-
const len1 = WASM_VECTOR_LEN;
|
|
467
|
-
const ret = wasm.analyze_colors(ptr0, len0, max_colors, ptr1, len1);
|
|
468
|
-
if (ret[2]) {
|
|
469
|
-
throw takeFromExternrefTable0(ret[1]);
|
|
470
|
-
}
|
|
471
|
-
return takeFromExternrefTable0(ret[0]);
|
|
472
|
-
}
|
|
473
|
-
|
|
474
483
|
/**
|
|
475
484
|
* Main downscaling function for WebAssembly
|
|
476
485
|
*
|
|
@@ -577,16 +586,28 @@ export function downscale_with_palette(image_data, width, height, target_width,
|
|
|
577
586
|
* @param {number} _height
|
|
578
587
|
* @param {number} num_colors
|
|
579
588
|
* @param {number} kmeans_iterations
|
|
589
|
+
* @param {string | null} [strategy]
|
|
580
590
|
* @returns {Uint8Array}
|
|
581
591
|
*/
|
|
582
|
-
export function extract_palette_from_image(image_data, _width, _height, num_colors, kmeans_iterations) {
|
|
583
|
-
|
|
592
|
+
export function extract_palette_from_image(image_data, _width, _height, num_colors, kmeans_iterations, strategy) {
|
|
593
|
+
var ptr0 = isLikeNone(strategy) ? 0 : passStringToWasm0(strategy, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
|
|
594
|
+
var len0 = WASM_VECTOR_LEN;
|
|
595
|
+
const ret = wasm.extract_palette_from_image(image_data, _width, _height, num_colors, kmeans_iterations, ptr0, len0);
|
|
584
596
|
if (ret[2]) {
|
|
585
597
|
throw takeFromExternrefTable0(ret[1]);
|
|
586
598
|
}
|
|
587
599
|
return takeFromExternrefTable0(ret[0]);
|
|
588
600
|
}
|
|
589
601
|
|
|
602
|
+
/**
|
|
603
|
+
* Get available palette strategies
|
|
604
|
+
* @returns {Array<any>}
|
|
605
|
+
*/
|
|
606
|
+
export function get_palette_strategies() {
|
|
607
|
+
const ret = wasm.get_palette_strategies();
|
|
608
|
+
return ret;
|
|
609
|
+
}
|
|
610
|
+
|
|
590
611
|
/**
|
|
591
612
|
* Initialize panic hook for better error messages in browser console
|
|
592
613
|
*/
|
|
@@ -650,11 +671,6 @@ export function __wbg_log_1d990106d99dacb7(arg0) {
|
|
|
650
671
|
console.log(arg0);
|
|
651
672
|
};
|
|
652
673
|
|
|
653
|
-
export function __wbg_new_1ba21ce319a06297() {
|
|
654
|
-
const ret = new Object();
|
|
655
|
-
return ret;
|
|
656
|
-
};
|
|
657
|
-
|
|
658
674
|
export function __wbg_new_25f239778d6112b9() {
|
|
659
675
|
const ret = new Array();
|
|
660
676
|
return ret;
|
|
@@ -684,12 +700,9 @@ export function __wbg_prototypesetcall_dfe9b766cdc1f1fd(arg0, arg1, arg2) {
|
|
|
684
700
|
Uint8Array.prototype.set.call(getArrayU8FromWasm0(arg0, arg1), arg2);
|
|
685
701
|
};
|
|
686
702
|
|
|
687
|
-
export function
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
export function __wbg_set_7df433eea03a5c14(arg0, arg1, arg2) {
|
|
692
|
-
arg0[arg1 >>> 0] = arg2;
|
|
703
|
+
export function __wbg_push_7d9be8f38fc13975(arg0, arg1) {
|
|
704
|
+
const ret = arg0.push(arg1);
|
|
705
|
+
return ret;
|
|
693
706
|
};
|
|
694
707
|
|
|
695
708
|
export function __wbindgen_cast_2241b6af4c4b2941(arg0, arg1) {
|
|
@@ -698,12 +711,6 @@ export function __wbindgen_cast_2241b6af4c4b2941(arg0, arg1) {
|
|
|
698
711
|
return ret;
|
|
699
712
|
};
|
|
700
713
|
|
|
701
|
-
export function __wbindgen_cast_d6cd19b81560fd6e(arg0) {
|
|
702
|
-
// Cast intrinsic for `F64 -> Externref`.
|
|
703
|
-
const ret = arg0;
|
|
704
|
-
return ret;
|
|
705
|
-
};
|
|
706
|
-
|
|
707
714
|
export function __wbindgen_init_externref_table() {
|
|
708
715
|
const table = wasm.__wbindgen_externrefs;
|
|
709
716
|
const offset = table.grow(4);
|
package/smart_downscaler_bg.wasm
CHANGED
|
Binary file
|