@mcptoolshop/style-dataset-lab 3.3.0 → 3.5.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/CHANGELOG.md +60 -0
- package/README.es.md +117 -55
- package/README.fr.md +125 -65
- package/README.hi.md +117 -68
- package/README.it.md +119 -58
- package/README.ja.md +124 -74
- package/README.md +73 -7
- package/README.pt-BR.md +128 -63
- package/README.zh.md +116 -65
- package/bin/sdlab.js +100 -4
- package/lib/adapters/ai-toolkit.js +224 -35
- package/lib/adapters/comfyui-runner.js +79 -26
- package/lib/adapters/comfyui-workflows.js +94 -11
- package/lib/adapters/diffusers-lora.js +37 -5
- package/lib/adapters/generic-image-caption.js +43 -5
- package/lib/args.js +16 -1
- package/lib/asset-ingest.js +575 -0
- package/lib/asset-masks.js +323 -0
- package/lib/asset-source.js +1176 -0
- package/lib/batch-modes.js +29 -8
- package/lib/batch-runs.js +29 -1
- package/lib/batch-sheet-render.js +13 -3
- package/lib/brief-compiler.js +19 -0
- package/lib/candidate-sheet.js +327 -0
- package/lib/canon-author.js +350 -0
- package/lib/canon-build/build.js +146 -93
- package/lib/canon-review-gate.js +205 -0
- package/lib/captions.js +57 -0
- package/lib/card.js +74 -2
- package/lib/colab-launcher-guards.js +285 -0
- package/lib/comfyui.js +116 -18
- package/lib/config.js +116 -12
- package/lib/critique-engine.js +36 -2
- package/lib/eligibility.js +121 -9
- package/lib/eval-pack.js +14 -14
- package/lib/eval-runs.js +33 -0
- package/lib/export.js +122 -34
- package/lib/freeze-gate.js +16 -3
- package/lib/freeze-stamp.js +28 -3
- package/lib/generated-provenance.js +194 -6
- package/lib/implementation-packs.js +95 -17
- package/lib/ingest.js +396 -0
- package/lib/judgment-provenance.js +220 -0
- package/lib/lane1-render.js +668 -0
- package/lib/measure.js +422 -0
- package/lib/memory-store.js +663 -0
- package/lib/model-licences.js +305 -0
- package/lib/npy-meta.js +130 -0
- package/lib/paths.js +196 -7
- package/lib/png-meta.js +512 -0
- package/lib/records.js +54 -8
- package/lib/reingest-selected.js +113 -17
- package/lib/reingest.js +27 -3
- package/lib/run-manifest.js +159 -25
- package/lib/run-summary.js +12 -6
- package/lib/runtime-runs.js +45 -4
- package/lib/snapshot.js +114 -15
- package/lib/split.js +364 -48
- package/lib/train-runs.js +214 -0
- package/lib/training-manifests.js +114 -21
- package/lib/training-packages.js +147 -15
- package/lib/verifier.js +3 -0
- package/lib/verify-chain.js +367 -0
- package/lib/workflow-profiles.js +24 -3
- package/package.json +4 -3
- package/runtime/comfyui/txt2img-qwen.json +38 -0
- package/runtime/comfyui/txt2img-standard.json +21 -0
- package/scripts/_resume.js +62 -0
- package/scripts/_shared-args.js +44 -0
- package/scripts/asset-ingest.js +160 -0
- package/scripts/brief-compile.js +2 -1
- package/scripts/brief-show.js +2 -1
- package/scripts/canon-bind.js +2 -2
- package/scripts/canon-build.js +27 -20
- package/scripts/canon-draft.js +82 -0
- package/scripts/canon-drift.js +16 -7
- package/scripts/canon-freeze-status.js +14 -6
- package/scripts/canon-freeze.js +31 -11
- package/scripts/canon-unfreeze.js +17 -9
- package/scripts/card.js +2 -2
- package/scripts/colab/REAL_SERVICE_ASSUMPTIONS.md +14 -0
- package/scripts/colab/ai-toolkit-override-py315.txt +45 -0
- package/scripts/colab/colab_window.sh +587 -0
- package/scripts/colab/lane1_render.py +970 -0
- package/scripts/colab/launch_guards.py +564 -0
- package/scripts/colab/launch_lane1_stub.sh +309 -0
- package/scripts/colab/launch_lane3_stub.sh +366 -0
- package/scripts/colab/remote_runner.py +59 -0
- package/scripts/compare.js +2 -2
- package/scripts/critique.js +16 -1
- package/scripts/curate.js +2 -2
- package/scripts/doctor.js +182 -55
- package/scripts/eligibility.js +42 -2
- package/scripts/eval-pack.js +2 -2
- package/scripts/eval-run.js +2 -2
- package/scripts/export.js +2 -2
- package/scripts/generate-controlnet.js +45 -25
- package/scripts/generate-identity.js +53 -23
- package/scripts/generate-ipadapter.js +44 -23
- package/scripts/generate.js +86 -17
- package/scripts/implementation-pack.js +2 -2
- package/scripts/ingest.js +145 -0
- package/scripts/init.js +73 -13
- package/scripts/measure.js +170 -0
- package/scripts/measure_image.py +458 -0
- package/scripts/migrate-records.js +142 -0
- package/scripts/painterly-test.js +4 -3
- package/scripts/painterly.js +7 -3
- package/scripts/qwen_generate.py +138 -20
- package/scripts/reingest.js +2 -2
- package/scripts/sheet.js +142 -0
- package/scripts/snapshot.js +2 -2
- package/scripts/split.js +2 -2
- package/scripts/training-manifest.js +2 -2
- package/scripts/training-package.js +2 -2
- package/scripts/training-profile.js +2 -2
- package/scripts/verify.js +67 -0
- package/scripts/workflow-list.js +2 -1
- package/scripts/workflow-show.js +2 -1
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Asset-lane color math — the palette-conformance gate ported
|
|
3
|
+
* formula-faithfully from facet's `tools/palette_gate.py` (read-only port,
|
|
4
|
+
* 2026-08-04), plus categorical class-share measurement for declared
|
|
5
|
+
* render-space channels.
|
|
6
|
+
*
|
|
7
|
+
* Port fidelity notes (checked against the source, not paraphrased):
|
|
8
|
+
* - sRGB → linear → XYZ(D65) → Lab with the exact constants palette_gate.py
|
|
9
|
+
* uses (which itself matches facet's e08_deltaE.py).
|
|
10
|
+
* - C* = hypot(a*, b*); hue = atan2(b*, a*) in degrees, mod 360.
|
|
11
|
+
* - A band with lo <= hi is [lo, hi]; a band with lo > hi WRAPS AROUND 0°.
|
|
12
|
+
* - off-palette = (C* > min_chroma) AND (not in any band) AND inside the
|
|
13
|
+
* EXACT silhouette (mask value > 0.5 of full scale — a keyed mask would
|
|
14
|
+
* let background into the measurement; the E01 / E08-A2 failure).
|
|
15
|
+
* - TWO numbers, because they are two different failures: total off-palette
|
|
16
|
+
* (an antialiasing-spread diagnostic — its percentage bound is WITHDRAWN
|
|
17
|
+
* per facet 2026-08-04 and gates nothing unless the palette declares one)
|
|
18
|
+
* and the LARGEST 4-CONNECTED COMPONENT (an invented garment is one big
|
|
19
|
+
* blob), which is the load-bearing gate.
|
|
20
|
+
* - The chroma floor is load-bearing, not a tuning knob: hue is undefined
|
|
21
|
+
* at low chroma (W3's steel greatsword sits at C* 1.6–2.8 inside the
|
|
22
|
+
* blue band; without the floor it would flag on every view).
|
|
23
|
+
*
|
|
24
|
+
* Pure functions over decoded images (lib/png-meta.js output). No I/O, no
|
|
25
|
+
* channel semantics — the palette, bands, classes and tolerances all arrive
|
|
26
|
+
* from the asset's manifest. (Parnas boundary: this module knows color math;
|
|
27
|
+
* lib/asset-source.js knows the contract.)
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
import { inputError } from './errors.js';
|
|
31
|
+
|
|
32
|
+
// ─── sRGB → Lab (exact palette_gate.py constants) ────────────────────
|
|
33
|
+
|
|
34
|
+
const XYZ_M = [
|
|
35
|
+
[0.4124564, 0.3575761, 0.1804375],
|
|
36
|
+
[0.2126729, 0.7151522, 0.0721750],
|
|
37
|
+
[0.0193339, 0.1191920, 0.9503041],
|
|
38
|
+
];
|
|
39
|
+
const WHITE = [0.95047, 1.0, 1.08883];
|
|
40
|
+
const LAB_E = 216 / 24389;
|
|
41
|
+
const LAB_K = 24389 / 27;
|
|
42
|
+
|
|
43
|
+
function srgbToLinear(c) {
|
|
44
|
+
return c <= 0.04045 ? c / 12.92 : ((c + 0.055) / 1.055) ** 2.4;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function labF(t) {
|
|
48
|
+
return t > LAB_E ? Math.cbrt(t) : (LAB_K * t + 16) / 116;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Convert one sRGB pixel (0–255 ints) to Lab.
|
|
53
|
+
* @returns {[number, number, number]} [L, a, b]
|
|
54
|
+
*/
|
|
55
|
+
export function srgbToLab(r8, g8, b8) {
|
|
56
|
+
const r = srgbToLinear(r8 / 255);
|
|
57
|
+
const g = srgbToLinear(g8 / 255);
|
|
58
|
+
const b = srgbToLinear(b8 / 255);
|
|
59
|
+
const x = (XYZ_M[0][0] * r + XYZ_M[0][1] * g + XYZ_M[0][2] * b) / WHITE[0];
|
|
60
|
+
const y = (XYZ_M[1][0] * r + XYZ_M[1][1] * g + XYZ_M[1][2] * b) / WHITE[1];
|
|
61
|
+
const z = (XYZ_M[2][0] * r + XYZ_M[2][1] * g + XYZ_M[2][2] * b) / WHITE[2];
|
|
62
|
+
const fx = labF(x);
|
|
63
|
+
const fy = labF(y);
|
|
64
|
+
const fz = labF(z);
|
|
65
|
+
return [116 * fy - 16, 500 * (fx - fy), 200 * (fy - fz)];
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// ─── Decoded-image pixel access ──────────────────────────────────────
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Read pixel i of a decoded PNG as [r, g, b] (0–255), independent of the
|
|
72
|
+
* source color type (grayscale, RGB, RGBA, or indexed via its PLTE).
|
|
73
|
+
*/
|
|
74
|
+
export function rgbAt(img, i) {
|
|
75
|
+
switch (img.colorType) {
|
|
76
|
+
case 0: {
|
|
77
|
+
const v = img.data[i];
|
|
78
|
+
return [v, v, v];
|
|
79
|
+
}
|
|
80
|
+
case 2: {
|
|
81
|
+
const o = i * 3;
|
|
82
|
+
return [img.data[o], img.data[o + 1], img.data[o + 2]];
|
|
83
|
+
}
|
|
84
|
+
case 6: {
|
|
85
|
+
const o = i * 4;
|
|
86
|
+
return [img.data[o], img.data[o + 1], img.data[o + 2]];
|
|
87
|
+
}
|
|
88
|
+
case 3: {
|
|
89
|
+
const entry = img.palette[img.data[i]];
|
|
90
|
+
return [entry[0], entry[1], entry[2]];
|
|
91
|
+
}
|
|
92
|
+
default:
|
|
93
|
+
throw inputError('PNG_UNSUPPORTED', `rgbAt: unsupported color type ${img.colorType}.`);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** Alpha of pixel i (255 when the image has no alpha channel). */
|
|
98
|
+
export function alphaAt(img, i) {
|
|
99
|
+
return img.colorType === 6 ? img.data[i * 4 + 3] : 255;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Binarize a decoded grayscale silhouette: value > 127 (i.e. > 0.5 of full
|
|
104
|
+
* scale, matching palette_gate.py's `> 0.5` on a /255 float) = figure.
|
|
105
|
+
* @returns {Uint8Array} 0/1 per pixel
|
|
106
|
+
*/
|
|
107
|
+
export function silhouetteToMask(img, label = 'silhouette') {
|
|
108
|
+
if (img.colorType !== 0) {
|
|
109
|
+
throw inputError(
|
|
110
|
+
'ASSET_ENCODING_MISMATCH',
|
|
111
|
+
`${label}: silhouette masks must be grayscale PNGs (color type 0), got ${img.colorType}.`
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
const mask = new Uint8Array(img.width * img.height);
|
|
115
|
+
for (let i = 0; i < mask.length; i++) mask[i] = img.data[i] > 127 ? 1 : 0;
|
|
116
|
+
return mask;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// ─── Connected components (4-connectivity, matching scipy.ndimage.label
|
|
120
|
+
// with its default cross-shaped structure) ────────────────────────
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Size in pixels of the largest 4-connected component of a 0/1 mask.
|
|
124
|
+
*/
|
|
125
|
+
export function largestComponent4(mask, width, height) {
|
|
126
|
+
const visited = new Uint8Array(mask.length);
|
|
127
|
+
const stack = new Int32Array(mask.length);
|
|
128
|
+
let largest = 0;
|
|
129
|
+
for (let start = 0; start < mask.length; start++) {
|
|
130
|
+
if (!mask[start] || visited[start]) continue;
|
|
131
|
+
let top = 0;
|
|
132
|
+
stack[top++] = start;
|
|
133
|
+
visited[start] = 1;
|
|
134
|
+
let size = 0;
|
|
135
|
+
while (top > 0) {
|
|
136
|
+
const i = stack[--top];
|
|
137
|
+
size++;
|
|
138
|
+
const x = i % width;
|
|
139
|
+
const y = (i - x) / width;
|
|
140
|
+
if (x > 0 && mask[i - 1] && !visited[i - 1]) { visited[i - 1] = 1; stack[top++] = i - 1; }
|
|
141
|
+
if (x < width - 1 && mask[i + 1] && !visited[i + 1]) { visited[i + 1] = 1; stack[top++] = i + 1; }
|
|
142
|
+
if (y > 0 && mask[i - width] && !visited[i - width]) { visited[i - width] = 1; stack[top++] = i - width; }
|
|
143
|
+
if (y < height - 1 && mask[i + width] && !visited[i + width]) { visited[i + width] = 1; stack[top++] = i + width; }
|
|
144
|
+
}
|
|
145
|
+
if (size > largest) largest = size;
|
|
146
|
+
}
|
|
147
|
+
return largest;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// ─── The palette gate ────────────────────────────────────────────────
|
|
151
|
+
|
|
152
|
+
function median(values) {
|
|
153
|
+
if (values.length === 0) return 0;
|
|
154
|
+
const sorted = Float64Array.from(values).sort();
|
|
155
|
+
const mid = sorted.length >> 1;
|
|
156
|
+
return sorted.length % 2 ? sorted[mid] : (sorted[mid - 1] + sorted[mid]) / 2;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function round(v, places) {
|
|
160
|
+
const f = 10 ** places;
|
|
161
|
+
return Math.round(v * f) / f;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Run the ported palette-conformance gate on one render.
|
|
166
|
+
*
|
|
167
|
+
* @param {Object} renderImg — decoded render (lib/png-meta.js decodePng)
|
|
168
|
+
* @param {Object} silhouetteImg — decoded grayscale silhouette, same dims
|
|
169
|
+
* @param {Object} palette — the manifest's palette block:
|
|
170
|
+
* {min_chroma, allowed_bands: [{name, hue_deg: [lo, hi]}],
|
|
171
|
+
* gate: {max_offpalette_pct: number|null, max_offpalette_blob_px: number}}
|
|
172
|
+
* @param {string} [label] — for error messages
|
|
173
|
+
* @returns {{figure_px:number, offpalette_px:number, offpalette_pct:number,
|
|
174
|
+
* largest_blob_px:number, pass:boolean, withdrawn_pct_bound:boolean,
|
|
175
|
+
* dominant:Object|null}}
|
|
176
|
+
*/
|
|
177
|
+
export function paletteGate(renderImg, silhouetteImg, palette, label = 'render') {
|
|
178
|
+
if (renderImg.width !== silhouetteImg.width || renderImg.height !== silhouetteImg.height) {
|
|
179
|
+
// The parallel-mask ANDON from palette_gate.py: a mask that does not
|
|
180
|
+
// match its image measures the wrong pixels silently.
|
|
181
|
+
throw inputError(
|
|
182
|
+
'ASSET_MASK_MISMATCH',
|
|
183
|
+
`${label}: silhouette is ${silhouetteImg.width}x${silhouetteImg.height} but the render is ${renderImg.width}x${renderImg.height}.`
|
|
184
|
+
);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const mask = silhouetteToMask(silhouetteImg, `${label} silhouette`);
|
|
188
|
+
const bands = palette.allowed_bands.map((b) => ({
|
|
189
|
+
lo: Number(b.hue_deg[0]),
|
|
190
|
+
hi: Number(b.hue_deg[1]),
|
|
191
|
+
}));
|
|
192
|
+
const cmin = Number(palette.min_chroma);
|
|
193
|
+
const maxPct = palette.gate.max_offpalette_pct == null ? null : Number(palette.gate.max_offpalette_pct);
|
|
194
|
+
const maxBlob = Number(palette.gate.max_offpalette_blob_px);
|
|
195
|
+
|
|
196
|
+
const n = renderImg.width * renderImg.height;
|
|
197
|
+
const off = new Uint8Array(n);
|
|
198
|
+
let figurePx = 0;
|
|
199
|
+
let offPx = 0;
|
|
200
|
+
const offChroma = [];
|
|
201
|
+
const offHue = [];
|
|
202
|
+
const offRgb = [];
|
|
203
|
+
|
|
204
|
+
for (let i = 0; i < n; i++) {
|
|
205
|
+
if (!mask[i]) continue;
|
|
206
|
+
figurePx++;
|
|
207
|
+
const [r, g, b] = rgbAt(renderImg, i);
|
|
208
|
+
const [, a, bb] = srgbToLab(r, g, b);
|
|
209
|
+
const chroma = Math.hypot(a, bb);
|
|
210
|
+
if (chroma <= cmin) continue; // hue undefined below the floor
|
|
211
|
+
let hue = (Math.atan2(bb, a) * 180) / Math.PI;
|
|
212
|
+
hue = ((hue % 360) + 360) % 360;
|
|
213
|
+
let inBand = false;
|
|
214
|
+
for (const { lo, hi } of bands) {
|
|
215
|
+
if (lo <= hi ? hue >= lo && hue <= hi : hue >= lo || hue <= hi) { inBand = true; break; }
|
|
216
|
+
}
|
|
217
|
+
if (inBand) continue;
|
|
218
|
+
off[i] = 1;
|
|
219
|
+
offPx++;
|
|
220
|
+
offChroma.push(chroma);
|
|
221
|
+
offHue.push(hue);
|
|
222
|
+
offRgb.push([r, g, b]);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
const pct = (100 * offPx) / Math.max(figurePx, 1);
|
|
226
|
+
const blob = offPx > 0 ? largestComponent4(off, renderImg.width, renderImg.height) : 0;
|
|
227
|
+
const pass = blob <= maxBlob && (maxPct === null || pct <= maxPct);
|
|
228
|
+
|
|
229
|
+
// Dominant off-palette mass, so a failure is diagnosable, not just flagged
|
|
230
|
+
// (same 36-bin histogram + medians as the source).
|
|
231
|
+
let dominant = null;
|
|
232
|
+
if (offPx > 0) {
|
|
233
|
+
const hist = new Array(36).fill(0);
|
|
234
|
+
for (const h of offHue) hist[Math.min(35, Math.floor(h / 10))]++;
|
|
235
|
+
let top = 0;
|
|
236
|
+
for (let i = 1; i < 36; i++) if (hist[i] > hist[top]) top = i;
|
|
237
|
+
dominant = {
|
|
238
|
+
hue_lo: top * 10,
|
|
239
|
+
hue_hi: (top + 1) * 10,
|
|
240
|
+
share_of_offpalette: round((hist[top] / offPx) * 100, 1),
|
|
241
|
+
median_chroma: round(median(offChroma), 1),
|
|
242
|
+
median_rgb: [
|
|
243
|
+
Math.round(median(offRgb.map((p) => p[0]))),
|
|
244
|
+
Math.round(median(offRgb.map((p) => p[1]))),
|
|
245
|
+
Math.round(median(offRgb.map((p) => p[2]))),
|
|
246
|
+
],
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
return {
|
|
251
|
+
figure_px: figurePx,
|
|
252
|
+
offpalette_px: offPx,
|
|
253
|
+
offpalette_pct: round(pct, 4),
|
|
254
|
+
largest_blob_px: blob,
|
|
255
|
+
pass,
|
|
256
|
+
withdrawn_pct_bound: maxPct === null,
|
|
257
|
+
dominant,
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// ─── Categorical class shares ────────────────────────────────────────
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* Measure per-class coverage of a categorical render-space channel inside
|
|
265
|
+
* the silhouette. Shares are computed for EVERY declared class — this module
|
|
266
|
+
* never knows which class matters; the manifest's consumers do.
|
|
267
|
+
*
|
|
268
|
+
* `filter: "nearest"` ⇒ exact color match (tolerance 0).
|
|
269
|
+
* `filter: "linear"` ⇒ nearest declared class within
|
|
270
|
+
* `classification_tolerance` (RGB euclidean); pixels
|
|
271
|
+
* beyond it are honestly `unclassified`.
|
|
272
|
+
* Fully transparent pixels (alpha 0) are excluded — background, not class.
|
|
273
|
+
*
|
|
274
|
+
* @param {Object} channelImg — decoded channel instance
|
|
275
|
+
* @param {Object} silhouetteImg — decoded grayscale silhouette, same dims
|
|
276
|
+
* @param {Object} decl — the channel declaration:
|
|
277
|
+
* {palette: [{name, rgb: [r,g,b]}], filter, classification_tolerance?}
|
|
278
|
+
* @param {string} [label]
|
|
279
|
+
* @returns {{figure_px:number,
|
|
280
|
+
* classes:Object<string,{px:number, share:number}>,
|
|
281
|
+
* unclassified:{px:number, share:number}}}
|
|
282
|
+
*/
|
|
283
|
+
export function classShares(channelImg, silhouetteImg, decl, label = 'channel') {
|
|
284
|
+
if (channelImg.width !== silhouetteImg.width || channelImg.height !== silhouetteImg.height) {
|
|
285
|
+
throw inputError(
|
|
286
|
+
'ASSET_MASK_MISMATCH',
|
|
287
|
+
`${label}: channel is ${channelImg.width}x${channelImg.height} but the silhouette is ${silhouetteImg.width}x${silhouetteImg.height}.`
|
|
288
|
+
);
|
|
289
|
+
}
|
|
290
|
+
const mask = silhouetteToMask(silhouetteImg, `${label} silhouette`);
|
|
291
|
+
const tolerance = decl.filter === 'nearest' ? 0 : Number(decl.classification_tolerance ?? 0);
|
|
292
|
+
const tol2 = tolerance * tolerance;
|
|
293
|
+
const classes = decl.palette.map((c) => ({ name: c.name, rgb: c.rgb, px: 0 }));
|
|
294
|
+
|
|
295
|
+
let figurePx = 0;
|
|
296
|
+
let unclassified = 0;
|
|
297
|
+
const n = channelImg.width * channelImg.height;
|
|
298
|
+
|
|
299
|
+
for (let i = 0; i < n; i++) {
|
|
300
|
+
if (!mask[i]) continue;
|
|
301
|
+
if (alphaAt(channelImg, i) === 0) continue; // transparent = background
|
|
302
|
+
figurePx++;
|
|
303
|
+
const [r, g, b] = rgbAt(channelImg, i);
|
|
304
|
+
let best = -1;
|
|
305
|
+
let bestD = Infinity;
|
|
306
|
+
for (let c = 0; c < classes.length; c++) {
|
|
307
|
+
const [cr, cg, cb] = classes[c].rgb;
|
|
308
|
+
const d = (r - cr) * (r - cr) + (g - cg) * (g - cg) + (b - cb) * (b - cb);
|
|
309
|
+
if (d < bestD) { bestD = d; best = c; }
|
|
310
|
+
}
|
|
311
|
+
if (best >= 0 && bestD <= tol2) classes[best].px++;
|
|
312
|
+
else unclassified++;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
const share = (px) => round((100 * px) / Math.max(figurePx, 1), 4);
|
|
316
|
+
const out = {};
|
|
317
|
+
for (const c of classes) out[c.name] = { px: c.px, share: share(c.px) };
|
|
318
|
+
return {
|
|
319
|
+
figure_px: figurePx,
|
|
320
|
+
classes: out,
|
|
321
|
+
unclassified: { px: unclassified, share: share(unclassified) },
|
|
322
|
+
};
|
|
323
|
+
}
|