@trishchuk/coolors-mcp 1.0.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/.claude/settings.local.json +39 -0
- package/.env +2 -0
- package/.github/ISSUE_TEMPLATE/bug_report.md +73 -0
- package/.github/ISSUE_TEMPLATE/feature_request.md +71 -0
- package/.github/pull_request_template.md +97 -0
- package/.github/workflows/ci.yml +127 -0
- package/.github/workflows/deploy-docs.yml +56 -0
- package/.github/workflows/release.yml +99 -0
- package/.mcp.json +12 -0
- package/.prettierignore +1 -0
- package/CLAUDE.md +201 -0
- package/DOCUMENTATION.md +274 -0
- package/GEMINI.md +54 -0
- package/LICENSE +21 -0
- package/README.md +401 -0
- package/demo/content_based_color.png +0 -0
- package/demo/music-player.html +621 -0
- package/demo/podcast-player.html +903 -0
- package/dist/bin/coolors-mcp.d.ts +1 -0
- package/dist/bin/coolors-mcp.js +154 -0
- package/dist/bin/coolors-mcp.js.map +1 -0
- package/dist/bin/server.d.ts +1 -0
- package/dist/bin/server.js +3292 -0
- package/dist/bin/server.js.map +1 -0
- package/dist/chunk-IQ7NN26V.js +114 -0
- package/dist/chunk-IQ7NN26V.js.map +1 -0
- package/dist/chunk-P3ARRKLS.js +1214 -0
- package/dist/chunk-P3ARRKLS.js.map +1 -0
- package/dist/color/index.d.ts +716 -0
- package/dist/color/index.js +153 -0
- package/dist/color/index.js.map +1 -0
- package/dist/coolors-mcp.d.ts +136 -0
- package/dist/coolors-mcp.js +7 -0
- package/dist/coolors-mcp.js.map +1 -0
- package/docs/.vitepress/cache/deps/@braintree_sanitize-url.js +93 -0
- package/docs/.vitepress/cache/deps/@braintree_sanitize-url.js.map +7 -0
- package/docs/.vitepress/cache/deps/_metadata.json +127 -0
- package/docs/.vitepress/cache/deps/chunk-BUSYA2B4.js +9 -0
- package/docs/.vitepress/cache/deps/chunk-BUSYA2B4.js.map +7 -0
- package/docs/.vitepress/cache/deps/chunk-JD3CXNQ6.js +12683 -0
- package/docs/.vitepress/cache/deps/chunk-JD3CXNQ6.js.map +7 -0
- package/docs/.vitepress/cache/deps/chunk-SYPOPCWC.js +9719 -0
- package/docs/.vitepress/cache/deps/chunk-SYPOPCWC.js.map +7 -0
- package/docs/.vitepress/cache/deps/cytoscape-cose-bilkent.js +4710 -0
- package/docs/.vitepress/cache/deps/cytoscape-cose-bilkent.js.map +7 -0
- package/docs/.vitepress/cache/deps/cytoscape.js +30278 -0
- package/docs/.vitepress/cache/deps/cytoscape.js.map +7 -0
- package/docs/.vitepress/cache/deps/dayjs.js +285 -0
- package/docs/.vitepress/cache/deps/dayjs.js.map +7 -0
- package/docs/.vitepress/cache/deps/debug.js +468 -0
- package/docs/.vitepress/cache/deps/debug.js.map +7 -0
- package/docs/.vitepress/cache/deps/package.json +3 -0
- package/docs/.vitepress/cache/deps/prismjs.js +1466 -0
- package/docs/.vitepress/cache/deps/prismjs.js.map +7 -0
- package/docs/.vitepress/cache/deps/prismjs_components_prism-bash.js +228 -0
- package/docs/.vitepress/cache/deps/prismjs_components_prism-bash.js.map +7 -0
- package/docs/.vitepress/cache/deps/prismjs_components_prism-javascript.js +142 -0
- package/docs/.vitepress/cache/deps/prismjs_components_prism-javascript.js.map +7 -0
- package/docs/.vitepress/cache/deps/prismjs_components_prism-json.js +27 -0
- package/docs/.vitepress/cache/deps/prismjs_components_prism-json.js.map +7 -0
- package/docs/.vitepress/cache/deps/prismjs_components_prism-python.js +65 -0
- package/docs/.vitepress/cache/deps/prismjs_components_prism-python.js.map +7 -0
- package/docs/.vitepress/cache/deps/prismjs_components_prism-typescript.js +53 -0
- package/docs/.vitepress/cache/deps/prismjs_components_prism-typescript.js.map +7 -0
- package/docs/.vitepress/cache/deps/prismjs_components_prism-yaml.js +73 -0
- package/docs/.vitepress/cache/deps/prismjs_components_prism-yaml.js.map +7 -0
- package/docs/.vitepress/cache/deps/vitepress___@vue_devtools-api.js +4507 -0
- package/docs/.vitepress/cache/deps/vitepress___@vue_devtools-api.js.map +7 -0
- package/docs/.vitepress/cache/deps/vitepress___@vueuse_core.js +584 -0
- package/docs/.vitepress/cache/deps/vitepress___@vueuse_core.js.map +7 -0
- package/docs/.vitepress/cache/deps/vitepress___@vueuse_integrations_useFocusTrap.js +1146 -0
- package/docs/.vitepress/cache/deps/vitepress___@vueuse_integrations_useFocusTrap.js.map +7 -0
- package/docs/.vitepress/cache/deps/vitepress___mark__js_src_vanilla__js.js +1667 -0
- package/docs/.vitepress/cache/deps/vitepress___mark__js_src_vanilla__js.js.map +7 -0
- package/docs/.vitepress/cache/deps/vitepress___minisearch.js +1814 -0
- package/docs/.vitepress/cache/deps/vitepress___minisearch.js.map +7 -0
- package/docs/.vitepress/cache/deps/vue.js +344 -0
- package/docs/.vitepress/cache/deps/vue.js.map +7 -0
- package/docs/.vitepress/components/ClientGrid.vue +125 -0
- package/docs/.vitepress/components/CodeBlock.vue +231 -0
- package/docs/.vitepress/components/ConfigModal.vue +477 -0
- package/docs/.vitepress/components/DiagramModal.vue +528 -0
- package/docs/.vitepress/components/TroubleshootingModal.vue +472 -0
- package/docs/.vitepress/config.js +162 -0
- package/docs/.vitepress/theme/FundingLayout.vue +251 -0
- package/docs/.vitepress/theme/Layout.vue +134 -0
- package/docs/.vitepress/theme/components/AdBanner.vue +317 -0
- package/docs/.vitepress/theme/components/AdPlaceholder.vue +78 -0
- package/docs/.vitepress/theme/components/FundingEffects.vue +322 -0
- package/docs/.vitepress/theme/components/FundingHero.vue +345 -0
- package/docs/.vitepress/theme/components/SupportSection.vue +511 -0
- package/docs/.vitepress/theme/custom-app.css +339 -0
- package/docs/.vitepress/theme/custom.css +699 -0
- package/docs/.vitepress/theme/index.js +25 -0
- package/docs/README.md +198 -0
- package/docs/concepts/accessibility.md +473 -0
- package/docs/concepts/color-spaces.md +222 -0
- package/docs/concepts/distance-metrics.md +384 -0
- package/docs/concepts/hct.md +261 -0
- package/docs/concepts/image-analysis.md +396 -0
- package/docs/concepts/material-design.md +306 -0
- package/docs/concepts/theme-matching.md +399 -0
- package/docs/examples/basic-colors.md +490 -0
- package/docs/examples/creating-themes.md +898 -0
- package/docs/examples/css-refactoring.md +824 -0
- package/docs/examples/image-extraction.md +882 -0
- package/docs/getting-started.md +366 -0
- package/docs/index.md +190 -0
- package/docs/installation.md +157 -0
- package/docs/tools/README.md +234 -0
- package/docs/tools/accessibility.md +614 -0
- package/docs/tools/color-operations.md +374 -0
- package/docs/tools/image-extraction.md +624 -0
- package/docs/tools/material-design.md +347 -0
- package/docs/tools/theme-matching.md +552 -0
- package/eslint.config.ts +14 -0
- package/examples/theme-matching.md +113 -0
- package/jsr.json +7 -0
- package/mcp-config.json +8 -0
- package/note.md +35 -0
- package/package.json +122 -0
- package/research_results.md +53 -0
- package/src/bin/coolors-mcp.ts +194 -0
- package/src/bin/server.ts +61 -0
- package/src/color/__tests__/conversions-argb.test.ts +198 -0
- package/src/color/__tests__/extract-colors.test.ts +360 -0
- package/src/color/__tests__/image-utils.test.ts +242 -0
- package/src/color/__tests__/reference-colors.test.ts +278 -0
- package/src/color/__tests__/round-trip.test.ts +197 -0
- package/src/color/conversions.test.ts +402 -0
- package/src/color/conversions.ts +393 -0
- package/src/color/dislike/__tests__/dislike-analyzer.test.ts +248 -0
- package/src/color/dislike/dislike-analyzer.ts +114 -0
- package/src/color/extract-colors.ts +228 -0
- package/src/color/hct/__tests__/hct-class.test.ts +232 -0
- package/src/color/hct/harmonization.ts +204 -0
- package/src/color/hct/hct-class.ts +109 -0
- package/src/color/hct/hct-solver.ts +168 -0
- package/src/color/hct/index.ts +39 -0
- package/src/color/hct/tonal-palette.ts +211 -0
- package/src/color/hct/types.ts +88 -0
- package/src/color/image-utils.ts +79 -0
- package/src/color/index.ts +87 -0
- package/src/color/material-theme.ts +157 -0
- package/src/color/metrics.test.ts +276 -0
- package/src/color/metrics.ts +281 -0
- package/src/color/quantize/__tests__/quantizer_celebi.test.ts +195 -0
- package/src/color/quantize/lab_point_provider.ts +55 -0
- package/src/color/quantize/point_provider.ts +27 -0
- package/src/color/quantize/quantizer_celebi.ts +51 -0
- package/src/color/quantize/quantizer_celebi_test.ts +71 -0
- package/src/color/quantize/quantizer_map.ts +47 -0
- package/src/color/quantize/quantizer_wsmeans.ts +232 -0
- package/src/color/quantize/quantizer_wu.ts +472 -0
- package/src/color/score/__tests__/score.test.ts +224 -0
- package/src/color/score/score.ts +175 -0
- package/src/color/types.ts +151 -0
- package/src/color/utils/color_utils.ts +292 -0
- package/src/color/utils/math_utils.ts +145 -0
- package/src/color/utils.test.ts +403 -0
- package/src/color/utils.ts +315 -0
- package/src/constants.ts +5 -0
- package/src/coolors-mcp.ts +37 -0
- package/src/examples/addition.ts +333 -0
- package/src/examples/color-demo.ts +125 -0
- package/src/examples/custom-logger.ts +201 -0
- package/src/examples/oauth-server.ts +113 -0
- package/src/examples/session-context.ts +269 -0
- package/src/session.ts +116 -0
- package/src/theme/__tests__/matcher.test.ts +180 -0
- package/src/theme/__tests__/parser.test.ts +148 -0
- package/src/theme/__tests__/refactor.test.ts +224 -0
- package/src/theme/index.ts +34 -0
- package/src/theme/matcher.ts +395 -0
- package/src/theme/parser.ts +392 -0
- package/src/theme/refactor.ts +360 -0
- package/src/theme/types.ts +152 -0
- package/src/tools/__tests__/gradient-generator.test.ts +206 -0
- package/src/tools/__tests__/palette-with-locks.test.ts +109 -0
- package/src/tools/color-conversion.tool.ts +54 -0
- package/src/tools/color-distance.tool.ts +41 -0
- package/src/tools/colors.ts +31 -0
- package/src/tools/contrast-checker.tool.ts +37 -0
- package/src/tools/dislike-analyzer.tool.ts +247 -0
- package/src/tools/gradient-generator.tool.ts +250 -0
- package/src/tools/image-extraction.tools.ts +289 -0
- package/src/tools/index.ts +39 -0
- package/src/tools/material-theme.tools.ts +250 -0
- package/src/tools/palette-generator.tool.ts +135 -0
- package/src/tools/palette-with-locks.tool.ts +221 -0
- package/src/tools/registry.ts +142 -0
- package/src/tools/simple-tools.ts +37 -0
- package/src/tools/theme-matching.tools.ts +334 -0
- package/src/types.ts +182 -0
- package/src/utils.ts +22 -0
- package/tsconfig.json +8 -0
- package/vitest.config.js +15 -0
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for Celebi quantizer
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { describe, expect, it } from "vitest";
|
|
6
|
+
|
|
7
|
+
import * as utils from "../../utils/color_utils";
|
|
8
|
+
import { QuantizerCelebi } from "../quantizer_celebi";
|
|
9
|
+
|
|
10
|
+
describe("QuantizerCelebi", () => {
|
|
11
|
+
it("should quantize a single color", () => {
|
|
12
|
+
const pixels = [
|
|
13
|
+
utils.argbFromRgb(255, 0, 0),
|
|
14
|
+
utils.argbFromRgb(255, 0, 0),
|
|
15
|
+
utils.argbFromRgb(255, 0, 0),
|
|
16
|
+
];
|
|
17
|
+
|
|
18
|
+
const result = QuantizerCelebi.quantize(pixels, 1);
|
|
19
|
+
|
|
20
|
+
expect(result.size).toBe(1);
|
|
21
|
+
// Should have red color
|
|
22
|
+
const colors = Array.from(result.keys());
|
|
23
|
+
const rgb = {
|
|
24
|
+
b: utils.blueFromArgb(colors[0]),
|
|
25
|
+
g: utils.greenFromArgb(colors[0]),
|
|
26
|
+
r: utils.redFromArgb(colors[0]),
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
// Should be close to red
|
|
30
|
+
expect(rgb.r).toBeGreaterThan(250);
|
|
31
|
+
expect(rgb.g).toBeLessThan(5);
|
|
32
|
+
expect(rgb.b).toBeLessThan(5);
|
|
33
|
+
|
|
34
|
+
// Population should be 3
|
|
35
|
+
expect(result.get(colors[0])).toBe(3);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it("should quantize multiple distinct colors", () => {
|
|
39
|
+
const pixels = [
|
|
40
|
+
utils.argbFromRgb(255, 0, 0), // Red
|
|
41
|
+
utils.argbFromRgb(0, 255, 0), // Green
|
|
42
|
+
utils.argbFromRgb(0, 0, 255), // Blue
|
|
43
|
+
utils.argbFromRgb(255, 255, 0), // Yellow
|
|
44
|
+
];
|
|
45
|
+
|
|
46
|
+
const result = QuantizerCelebi.quantize(pixels, 4);
|
|
47
|
+
|
|
48
|
+
// Should preserve distinct colors when maxColors is sufficient
|
|
49
|
+
expect(result.size).toBeLessThanOrEqual(4);
|
|
50
|
+
expect(result.size).toBeGreaterThan(0);
|
|
51
|
+
|
|
52
|
+
// Check total population
|
|
53
|
+
let totalPopulation = 0;
|
|
54
|
+
for (const pop of result.values()) {
|
|
55
|
+
totalPopulation += pop;
|
|
56
|
+
}
|
|
57
|
+
expect(totalPopulation).toBe(4);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it("should reduce colors when maxColors is limited", () => {
|
|
61
|
+
const pixels = [
|
|
62
|
+
utils.argbFromRgb(255, 0, 0),
|
|
63
|
+
utils.argbFromRgb(254, 0, 0), // Very similar to red
|
|
64
|
+
utils.argbFromRgb(0, 255, 0),
|
|
65
|
+
utils.argbFromRgb(0, 254, 0), // Very similar to green
|
|
66
|
+
utils.argbFromRgb(0, 0, 255),
|
|
67
|
+
utils.argbFromRgb(0, 0, 254), // Very similar to blue
|
|
68
|
+
];
|
|
69
|
+
|
|
70
|
+
const result = QuantizerCelebi.quantize(pixels, 3);
|
|
71
|
+
|
|
72
|
+
// Should reduce to approximately 3 colors
|
|
73
|
+
expect(result.size).toBeLessThanOrEqual(3);
|
|
74
|
+
expect(result.size).toBeGreaterThan(0);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it("should handle grayscale colors", () => {
|
|
78
|
+
const pixels = [
|
|
79
|
+
utils.argbFromRgb(0, 0, 0), // Black
|
|
80
|
+
utils.argbFromRgb(128, 128, 128), // Gray
|
|
81
|
+
utils.argbFromRgb(255, 255, 255), // White
|
|
82
|
+
];
|
|
83
|
+
|
|
84
|
+
const result = QuantizerCelebi.quantize(pixels, 3);
|
|
85
|
+
|
|
86
|
+
expect(result.size).toBeLessThanOrEqual(3);
|
|
87
|
+
expect(result.size).toBeGreaterThan(0);
|
|
88
|
+
|
|
89
|
+
// Total population should be preserved
|
|
90
|
+
let totalPopulation = 0;
|
|
91
|
+
for (const pop of result.values()) {
|
|
92
|
+
totalPopulation += pop;
|
|
93
|
+
}
|
|
94
|
+
expect(totalPopulation).toBe(3);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it("should handle empty pixel array", () => {
|
|
98
|
+
const result = QuantizerCelebi.quantize([], 10);
|
|
99
|
+
|
|
100
|
+
expect(result.size).toBe(0);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it("should handle single pixel", () => {
|
|
104
|
+
const pixel = utils.argbFromRgb(123, 45, 67);
|
|
105
|
+
const result = QuantizerCelebi.quantize([pixel], 10);
|
|
106
|
+
|
|
107
|
+
expect(result.size).toBe(1);
|
|
108
|
+
expect(result.has(pixel)).toBe(true);
|
|
109
|
+
expect(result.get(pixel)).toBe(1);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it("should handle maxColors of 1", () => {
|
|
113
|
+
const pixels = [
|
|
114
|
+
utils.argbFromRgb(255, 0, 0),
|
|
115
|
+
utils.argbFromRgb(0, 255, 0),
|
|
116
|
+
utils.argbFromRgb(0, 0, 255),
|
|
117
|
+
];
|
|
118
|
+
|
|
119
|
+
const result = QuantizerCelebi.quantize(pixels, 1);
|
|
120
|
+
|
|
121
|
+
expect(result.size).toBe(1);
|
|
122
|
+
|
|
123
|
+
// Population should be total
|
|
124
|
+
const population = Array.from(result.values())[0];
|
|
125
|
+
expect(population).toBe(3);
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it("should handle large number of similar colors", () => {
|
|
129
|
+
const pixels: number[] = [];
|
|
130
|
+
|
|
131
|
+
// Add many similar shades of blue
|
|
132
|
+
for (let i = 0; i < 100; i++) {
|
|
133
|
+
const variation = Math.floor(i / 10);
|
|
134
|
+
pixels.push(utils.argbFromRgb(0, 0, 200 + variation));
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const result = QuantizerCelebi.quantize(pixels, 5);
|
|
138
|
+
|
|
139
|
+
// Should reduce to fewer colors
|
|
140
|
+
expect(result.size).toBeLessThanOrEqual(5);
|
|
141
|
+
expect(result.size).toBeGreaterThan(0);
|
|
142
|
+
|
|
143
|
+
// Total population should be preserved
|
|
144
|
+
let totalPopulation = 0;
|
|
145
|
+
for (const pop of result.values()) {
|
|
146
|
+
totalPopulation += pop;
|
|
147
|
+
}
|
|
148
|
+
expect(totalPopulation).toBe(100);
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
it("should produce consistent results for same input", () => {
|
|
152
|
+
const pixels = [
|
|
153
|
+
utils.argbFromRgb(100, 150, 200),
|
|
154
|
+
utils.argbFromRgb(150, 100, 200),
|
|
155
|
+
utils.argbFromRgb(200, 100, 150),
|
|
156
|
+
];
|
|
157
|
+
|
|
158
|
+
const result1 = QuantizerCelebi.quantize(pixels, 2);
|
|
159
|
+
const result2 = QuantizerCelebi.quantize(pixels, 2);
|
|
160
|
+
|
|
161
|
+
// Results should be deterministic
|
|
162
|
+
expect(result1.size).toBe(result2.size);
|
|
163
|
+
|
|
164
|
+
// Check that the same colors are produced
|
|
165
|
+
const colors1 = Array.from(result1.keys()).sort();
|
|
166
|
+
const colors2 = Array.from(result2.keys()).sort();
|
|
167
|
+
|
|
168
|
+
expect(colors1).toEqual(colors2);
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
it("should handle gradient properly", () => {
|
|
172
|
+
const pixels: number[] = [];
|
|
173
|
+
|
|
174
|
+
// Create a gradient from red to blue
|
|
175
|
+
for (let i = 0; i < 100; i++) {
|
|
176
|
+
const r = Math.floor(255 * (1 - i / 100));
|
|
177
|
+
const b = Math.floor(255 * (i / 100));
|
|
178
|
+
pixels.push(utils.argbFromRgb(r, 0, b));
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const result = QuantizerCelebi.quantize(pixels, 10);
|
|
182
|
+
|
|
183
|
+
// Should extract representative colors from gradient
|
|
184
|
+
expect(result.size).toBeLessThanOrEqual(10);
|
|
185
|
+
expect(result.size).toBeGreaterThan(1); // Should find multiple colors
|
|
186
|
+
|
|
187
|
+
// Check that we have both reddish and bluish colors
|
|
188
|
+
const colors = Array.from(result.keys());
|
|
189
|
+
const hasReddish = colors.some((c) => utils.redFromArgb(c) > 200);
|
|
190
|
+
const hasBluish = colors.some((c) => utils.blueFromArgb(c) > 200);
|
|
191
|
+
|
|
192
|
+
expect(hasReddish).toBe(true);
|
|
193
|
+
expect(hasBluish).toBe(true);
|
|
194
|
+
});
|
|
195
|
+
});
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2021 Google LLC
|
|
4
|
+
*
|
|
5
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
* you may not use this file except in compliance with the License.
|
|
7
|
+
* You may obtain a copy of the License at
|
|
8
|
+
*
|
|
9
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
*
|
|
11
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
* See the License for the specific language governing permissions and
|
|
15
|
+
* limitations under the License.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import * as utils from "../utils/color_utils.js";
|
|
19
|
+
import { PointProvider } from "./point_provider.js";
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Provides conversions needed for K-Means quantization. Converting input to
|
|
23
|
+
* points, and converting the final state of the K-Means algorithm to colors.
|
|
24
|
+
*/
|
|
25
|
+
export class LabPointProvider implements PointProvider {
|
|
26
|
+
/**
|
|
27
|
+
* Standard CIE 1976 delta E formula also takes the square root, unneeded
|
|
28
|
+
* here. This method is used by quantization algorithms to compare distance,
|
|
29
|
+
* and the relative ordering is the same, with or without a square root.
|
|
30
|
+
*
|
|
31
|
+
* This relatively minor optimization is helpful because this method is
|
|
32
|
+
* called at least once for each pixel in an image.
|
|
33
|
+
*/
|
|
34
|
+
distance(from: number[], to: number[]): number {
|
|
35
|
+
const dL = from[0] - to[0];
|
|
36
|
+
const dA = from[1] - to[1];
|
|
37
|
+
const dB = from[2] - to[2];
|
|
38
|
+
return dL * dL + dA * dA + dB * dB;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Convert a color represented in ARGB to a 3-element array of L*a*b*
|
|
43
|
+
* coordinates of the color.
|
|
44
|
+
*/
|
|
45
|
+
fromInt(argb: number): number[] {
|
|
46
|
+
return utils.labFromArgb(argb);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Convert a 3-element array to a color represented in ARGB.
|
|
51
|
+
*/
|
|
52
|
+
toInt(point: number[]): number {
|
|
53
|
+
return utils.argbFromLab(point[0], point[1], point[2]);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2021 Google LLC
|
|
4
|
+
*
|
|
5
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
* you may not use this file except in compliance with the License.
|
|
7
|
+
* You may obtain a copy of the License at
|
|
8
|
+
*
|
|
9
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
*
|
|
11
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
* See the License for the specific language governing permissions and
|
|
15
|
+
* limitations under the License.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* An interface to allow use of different color spaces by
|
|
20
|
+
* quantizers.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
export declare interface PointProvider {
|
|
24
|
+
distance(from: number[], to: number[]): number;
|
|
25
|
+
fromInt(argb: number): number[];
|
|
26
|
+
toInt(point: number[]): number;
|
|
27
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2021 Google LLC
|
|
4
|
+
*
|
|
5
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
* you may not use this file except in compliance with the License.
|
|
7
|
+
* You may obtain a copy of the License at
|
|
8
|
+
*
|
|
9
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
*
|
|
11
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
* See the License for the specific language governing permissions and
|
|
15
|
+
* limitations under the License.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { QuantizerWsmeans } from "./quantizer_wsmeans.js";
|
|
19
|
+
import { QuantizerWu } from "./quantizer_wu.js";
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* An image quantizer that improves on the quality of a standard K-Means
|
|
23
|
+
* algorithm by setting the K-Means initial state to the output of a Wu
|
|
24
|
+
* quantizer, instead of random centroids. Improves on speed by several
|
|
25
|
+
* optimizations, as implemented in Wsmeans, or Weighted Square Means, K-Means
|
|
26
|
+
* with those optimizations.
|
|
27
|
+
*
|
|
28
|
+
* This algorithm was designed by M. Emre Celebi, and was found in their 2011
|
|
29
|
+
* paper, Improving the Performance of K-Means for Color Quantization.
|
|
30
|
+
* https://arxiv.org/abs/1101.0395
|
|
31
|
+
*/
|
|
32
|
+
// material_color_utilities is designed to have a consistent API across
|
|
33
|
+
// platforms and modular components that can be moved around easily. Using a
|
|
34
|
+
// class as a namespace facilitates this.
|
|
35
|
+
//
|
|
36
|
+
// tslint:disable-next-line:class-as-namespace
|
|
37
|
+
export class QuantizerCelebi {
|
|
38
|
+
/**
|
|
39
|
+
* @param pixels Colors in ARGB format.
|
|
40
|
+
* @param maxColors The number of colors to divide the image into. A lower
|
|
41
|
+
* number of colors may be returned.
|
|
42
|
+
* @return Map with keys of colors in ARGB format, and values of number of
|
|
43
|
+
* pixels in the original image that correspond to the color in the
|
|
44
|
+
* quantized image.
|
|
45
|
+
*/
|
|
46
|
+
static quantize(pixels: number[], maxColors: number): Map<number, number> {
|
|
47
|
+
const wu = new QuantizerWu();
|
|
48
|
+
const wuResult = wu.quantize(pixels, maxColors);
|
|
49
|
+
return QuantizerWsmeans.quantize(pixels, wuResult, maxColors);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2021 Google LLC
|
|
4
|
+
*
|
|
5
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
* you may not use this file except in compliance with the License.
|
|
7
|
+
* You may obtain a copy of the License at
|
|
8
|
+
*
|
|
9
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
*
|
|
11
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
* See the License for the specific language governing permissions and
|
|
15
|
+
* limitations under the License.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import "jasmine";
|
|
19
|
+
|
|
20
|
+
import { QuantizerCelebi } from "./quantizer_celebi.js";
|
|
21
|
+
|
|
22
|
+
const RED = 0xffff0000;
|
|
23
|
+
const GREEN = 0xff00ff00;
|
|
24
|
+
const BLUE = 0xff0000ff;
|
|
25
|
+
|
|
26
|
+
describe("QuantizerCelebi", () => {
|
|
27
|
+
it("1R", () => {
|
|
28
|
+
const answer = QuantizerCelebi.quantize([RED], 128);
|
|
29
|
+
expect(answer.size).toBe(1);
|
|
30
|
+
expect(answer.get(RED)).toBe(1);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it("1G", () => {
|
|
34
|
+
const answer = QuantizerCelebi.quantize([GREEN], 128);
|
|
35
|
+
expect(answer.size).toBe(1);
|
|
36
|
+
expect(answer.get(GREEN)).toBe(1);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it("1B", () => {
|
|
40
|
+
const answer = QuantizerCelebi.quantize([BLUE], 128);
|
|
41
|
+
expect(answer.size).toBe(1);
|
|
42
|
+
expect(answer.get(BLUE)).toBe(1);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it("5B", () => {
|
|
46
|
+
const answer = QuantizerCelebi.quantize(
|
|
47
|
+
[BLUE, BLUE, BLUE, BLUE, BLUE],
|
|
48
|
+
128,
|
|
49
|
+
);
|
|
50
|
+
expect(answer.size).toBe(1);
|
|
51
|
+
expect(answer.get(BLUE)).toBe(5);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it("2R 3G", () => {
|
|
55
|
+
const answer = QuantizerCelebi.quantize(
|
|
56
|
+
[RED, RED, GREEN, GREEN, GREEN],
|
|
57
|
+
128,
|
|
58
|
+
);
|
|
59
|
+
expect(answer.size).toBe(2);
|
|
60
|
+
expect(answer.get(RED)).toBe(2);
|
|
61
|
+
expect(answer.get(GREEN)).toBe(3);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it("1R 1G 1B", () => {
|
|
65
|
+
const answer = QuantizerCelebi.quantize([RED, GREEN, BLUE], 128);
|
|
66
|
+
expect(answer.size).toBe(3);
|
|
67
|
+
expect(answer.get(RED)).toBe(1);
|
|
68
|
+
expect(answer.get(GREEN)).toBe(1);
|
|
69
|
+
expect(answer.get(BLUE)).toBe(1);
|
|
70
|
+
});
|
|
71
|
+
});
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2021 Google LLC
|
|
4
|
+
*
|
|
5
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
* you may not use this file except in compliance with the License.
|
|
7
|
+
* You may obtain a copy of the License at
|
|
8
|
+
*
|
|
9
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
*
|
|
11
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
* See the License for the specific language governing permissions and
|
|
15
|
+
* limitations under the License.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import * as utils from "../utils/color_utils.js";
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Quantizes an image into a map, with keys of ARGB colors, and values of the
|
|
22
|
+
* number of times that color appears in the image.
|
|
23
|
+
*/
|
|
24
|
+
// material_color_utilities is designed to have a consistent API across
|
|
25
|
+
// platforms and modular components that can be moved around easily. Using a
|
|
26
|
+
// class as a namespace facilitates this.
|
|
27
|
+
//
|
|
28
|
+
// tslint:disable-next-line:class-as-namespace
|
|
29
|
+
export class QuantizerMap {
|
|
30
|
+
/**
|
|
31
|
+
* @param pixels Colors in ARGB format.
|
|
32
|
+
* @return A Map with keys of ARGB colors, and values of the number of times
|
|
33
|
+
* the color appears in the image.
|
|
34
|
+
*/
|
|
35
|
+
static quantize(pixels: number[]): Map<number, number> {
|
|
36
|
+
const countByColor = new Map<number, number>();
|
|
37
|
+
for (let i = 0; i < pixels.length; i++) {
|
|
38
|
+
const pixel = pixels[i];
|
|
39
|
+
const alpha = utils.alphaFromArgb(pixel);
|
|
40
|
+
if (alpha < 255) {
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
countByColor.set(pixel, (countByColor.get(pixel) ?? 0) + 1);
|
|
44
|
+
}
|
|
45
|
+
return countByColor;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2021 Google LLC
|
|
4
|
+
*
|
|
5
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
* you may not use this file except in compliance with the License.
|
|
7
|
+
* You may obtain a copy of the License at
|
|
8
|
+
*
|
|
9
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
*
|
|
11
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
* See the License for the specific language governing permissions and
|
|
15
|
+
* limitations under the License.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { LabPointProvider } from "./lab_point_provider.js";
|
|
19
|
+
|
|
20
|
+
const MAX_ITERATIONS = 10;
|
|
21
|
+
const MIN_MOVEMENT_DISTANCE = 3.0;
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* A wrapper for maintaining a table of distances between K-Means clusters.
|
|
25
|
+
*/
|
|
26
|
+
class DistanceAndIndex {
|
|
27
|
+
distance: number = -1;
|
|
28
|
+
index: number = -1;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* An image quantizer that improves on the speed of a standard K-Means algorithm
|
|
33
|
+
* by implementing several optimizations, including deduping identical pixels
|
|
34
|
+
* and a triangle inequality rule that reduces the number of comparisons needed
|
|
35
|
+
* to identify which cluster a point should be moved to.
|
|
36
|
+
*
|
|
37
|
+
* Wsmeans stands for Weighted Square Means.
|
|
38
|
+
*
|
|
39
|
+
* This algorithm was designed by M. Emre Celebi, and was found in their 2011
|
|
40
|
+
* paper, Improving the Performance of K-Means for Color Quantization.
|
|
41
|
+
* https://arxiv.org/abs/1101.0395
|
|
42
|
+
*/
|
|
43
|
+
// material_color_utilities is designed to have a consistent API across
|
|
44
|
+
// platforms and modular components that can be moved around easily. Using a
|
|
45
|
+
// class as a namespace facilitates this.
|
|
46
|
+
//
|
|
47
|
+
// tslint:disable-next-line:class-as-namespace
|
|
48
|
+
export class QuantizerWsmeans {
|
|
49
|
+
/**
|
|
50
|
+
* @param inputPixels Colors in ARGB format.
|
|
51
|
+
* @param startingClusters Defines the initial state of the quantizer. Passing
|
|
52
|
+
* an empty array is fine, the implementation will create its own initial
|
|
53
|
+
* state that leads to reproducible results for the same inputs.
|
|
54
|
+
* Passing an array that is the result of Wu quantization leads to higher
|
|
55
|
+
* quality results.
|
|
56
|
+
* @param maxColors The number of colors to divide the image into. A lower
|
|
57
|
+
* number of colors may be returned.
|
|
58
|
+
* @return Colors in ARGB format.
|
|
59
|
+
*/
|
|
60
|
+
static quantize(
|
|
61
|
+
inputPixels: number[],
|
|
62
|
+
startingClusters: number[],
|
|
63
|
+
maxColors: number,
|
|
64
|
+
): Map<number, number> {
|
|
65
|
+
const pixelToCount = new Map<number, number>();
|
|
66
|
+
const points = new Array<number[]>();
|
|
67
|
+
const pixels = new Array<number>();
|
|
68
|
+
const pointProvider = new LabPointProvider();
|
|
69
|
+
let pointCount = 0;
|
|
70
|
+
for (let i = 0; i < inputPixels.length; i++) {
|
|
71
|
+
const inputPixel = inputPixels[i];
|
|
72
|
+
const pixelCount = pixelToCount.get(inputPixel);
|
|
73
|
+
if (pixelCount === undefined) {
|
|
74
|
+
pointCount++;
|
|
75
|
+
points.push(pointProvider.fromInt(inputPixel));
|
|
76
|
+
pixels.push(inputPixel);
|
|
77
|
+
pixelToCount.set(inputPixel, 1);
|
|
78
|
+
} else {
|
|
79
|
+
pixelToCount.set(inputPixel, pixelCount + 1);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const counts = new Array<number>();
|
|
84
|
+
for (let i = 0; i < pointCount; i++) {
|
|
85
|
+
const pixel = pixels[i];
|
|
86
|
+
const count = pixelToCount.get(pixel);
|
|
87
|
+
if (count !== undefined) {
|
|
88
|
+
counts[i] = count;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
let clusterCount = Math.min(maxColors, pointCount);
|
|
93
|
+
if (startingClusters.length > 0) {
|
|
94
|
+
clusterCount = Math.min(clusterCount, startingClusters.length);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const clusters = new Array<number[]>();
|
|
98
|
+
for (let i = 0; i < startingClusters.length; i++) {
|
|
99
|
+
clusters.push(pointProvider.fromInt(startingClusters[i]));
|
|
100
|
+
}
|
|
101
|
+
const additionalClustersNeeded = clusterCount - clusters.length;
|
|
102
|
+
if (startingClusters.length === 0 && additionalClustersNeeded > 0) {
|
|
103
|
+
for (let i = 0; i < additionalClustersNeeded; i++) {
|
|
104
|
+
const l = Math.random() * 100.0;
|
|
105
|
+
const a = Math.random() * (100.0 - -100.0 + 1) + -100;
|
|
106
|
+
const b = Math.random() * (100.0 - -100.0 + 1) + -100;
|
|
107
|
+
|
|
108
|
+
clusters.push([l, a, b]);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const clusterIndices = new Array<number>();
|
|
113
|
+
for (let i = 0; i < pointCount; i++) {
|
|
114
|
+
clusterIndices.push(Math.floor(Math.random() * clusterCount));
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const indexMatrix = new Array<number[]>();
|
|
118
|
+
for (let i = 0; i < clusterCount; i++) {
|
|
119
|
+
indexMatrix.push(new Array<number>());
|
|
120
|
+
for (let j = 0; j < clusterCount; j++) {
|
|
121
|
+
indexMatrix[i].push(0);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const distanceToIndexMatrix = new Array<DistanceAndIndex[]>();
|
|
126
|
+
for (let i = 0; i < clusterCount; i++) {
|
|
127
|
+
distanceToIndexMatrix.push(new Array<DistanceAndIndex>());
|
|
128
|
+
for (let j = 0; j < clusterCount; j++) {
|
|
129
|
+
distanceToIndexMatrix[i].push(new DistanceAndIndex());
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const pixelCountSums = new Array<number>();
|
|
134
|
+
for (let i = 0; i < clusterCount; i++) {
|
|
135
|
+
pixelCountSums.push(0);
|
|
136
|
+
}
|
|
137
|
+
for (let iteration = 0; iteration < MAX_ITERATIONS; iteration++) {
|
|
138
|
+
for (let i = 0; i < clusterCount; i++) {
|
|
139
|
+
for (let j = i + 1; j < clusterCount; j++) {
|
|
140
|
+
const distance = pointProvider.distance(clusters[i], clusters[j]);
|
|
141
|
+
distanceToIndexMatrix[j][i].distance = distance;
|
|
142
|
+
distanceToIndexMatrix[j][i].index = i;
|
|
143
|
+
distanceToIndexMatrix[i][j].distance = distance;
|
|
144
|
+
distanceToIndexMatrix[i][j].index = j;
|
|
145
|
+
}
|
|
146
|
+
distanceToIndexMatrix[i].sort();
|
|
147
|
+
for (let j = 0; j < clusterCount; j++) {
|
|
148
|
+
indexMatrix[i][j] = distanceToIndexMatrix[i][j].index;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
let pointsMoved = 0;
|
|
153
|
+
for (let i = 0; i < pointCount; i++) {
|
|
154
|
+
const point = points[i];
|
|
155
|
+
const previousClusterIndex = clusterIndices[i];
|
|
156
|
+
const previousCluster = clusters[previousClusterIndex];
|
|
157
|
+
const previousDistance = pointProvider.distance(point, previousCluster);
|
|
158
|
+
let minimumDistance = previousDistance;
|
|
159
|
+
let newClusterIndex = -1;
|
|
160
|
+
for (let j = 0; j < clusterCount; j++) {
|
|
161
|
+
if (
|
|
162
|
+
distanceToIndexMatrix[previousClusterIndex][j].distance >=
|
|
163
|
+
4 * previousDistance
|
|
164
|
+
) {
|
|
165
|
+
continue;
|
|
166
|
+
}
|
|
167
|
+
const distance = pointProvider.distance(point, clusters[j]);
|
|
168
|
+
if (distance < minimumDistance) {
|
|
169
|
+
minimumDistance = distance;
|
|
170
|
+
newClusterIndex = j;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
if (newClusterIndex !== -1) {
|
|
174
|
+
const distanceChange = Math.abs(
|
|
175
|
+
Math.sqrt(minimumDistance) - Math.sqrt(previousDistance),
|
|
176
|
+
);
|
|
177
|
+
if (distanceChange > MIN_MOVEMENT_DISTANCE) {
|
|
178
|
+
pointsMoved++;
|
|
179
|
+
clusterIndices[i] = newClusterIndex;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
if (pointsMoved === 0 && iteration !== 0) {
|
|
185
|
+
break;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const componentASums = new Array<number>(clusterCount).fill(0);
|
|
189
|
+
const componentBSums = new Array<number>(clusterCount).fill(0);
|
|
190
|
+
const componentCSums = new Array<number>(clusterCount).fill(0);
|
|
191
|
+
|
|
192
|
+
for (let i = 0; i < clusterCount; i++) {
|
|
193
|
+
pixelCountSums[i] = 0;
|
|
194
|
+
}
|
|
195
|
+
for (let i = 0; i < pointCount; i++) {
|
|
196
|
+
const clusterIndex = clusterIndices[i];
|
|
197
|
+
const point = points[i];
|
|
198
|
+
const count = counts[i];
|
|
199
|
+
pixelCountSums[clusterIndex] += count;
|
|
200
|
+
componentASums[clusterIndex] += point[0] * count;
|
|
201
|
+
componentBSums[clusterIndex] += point[1] * count;
|
|
202
|
+
componentCSums[clusterIndex] += point[2] * count;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
for (let i = 0; i < clusterCount; i++) {
|
|
206
|
+
const count = pixelCountSums[i];
|
|
207
|
+
if (count === 0) {
|
|
208
|
+
clusters[i] = [0.0, 0.0, 0.0];
|
|
209
|
+
continue;
|
|
210
|
+
}
|
|
211
|
+
const a = componentASums[i] / count;
|
|
212
|
+
const b = componentBSums[i] / count;
|
|
213
|
+
const c = componentCSums[i] / count;
|
|
214
|
+
clusters[i] = [a, b, c];
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
const argbToPopulation = new Map<number, number>();
|
|
219
|
+
for (let i = 0; i < clusterCount; i++) {
|
|
220
|
+
const count = pixelCountSums[i];
|
|
221
|
+
if (count === 0) {
|
|
222
|
+
continue;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
const possibleNewCluster = pointProvider.toInt(clusters[i]);
|
|
226
|
+
// Accumulate population counts when multiple clusters converge to same color
|
|
227
|
+
const existingCount = argbToPopulation.get(possibleNewCluster) || 0;
|
|
228
|
+
argbToPopulation.set(possibleNewCluster, existingCount + count);
|
|
229
|
+
}
|
|
230
|
+
return argbToPopulation;
|
|
231
|
+
}
|
|
232
|
+
}
|