@ai-game-assets/dev 0.8.4 → 0.9.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 +11 -0
- package/dist/asset-store.d.ts +1 -0
- package/dist/asset-store.d.ts.map +1 -1
- package/dist/asset-store.js +9 -2
- package/dist/asset-store.js.map +1 -1
- package/dist/image-generation-sizes.d.ts +1 -1
- package/dist/image-generation-sizes.d.ts.map +1 -1
- package/dist/image-generation-sizes.js +4 -3
- package/dist/image-generation-sizes.js.map +1 -1
- package/dist/provider-image-processing.d.ts +3 -52
- package/dist/provider-image-processing.d.ts.map +1 -1
- package/dist/provider-image-processing.js +4 -485
- package/dist/provider-image-processing.js.map +1 -1
- package/dist/provider.d.ts +0 -2
- package/dist/provider.d.ts.map +1 -1
- package/dist/provider.js +46 -74
- package/dist/provider.js.map +1 -1
- package/dist/server.d.ts +2 -0
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +6 -1
- package/dist/server.js.map +1 -1
- package/dist/tileset-sheet-processing.d.ts +2 -4
- package/dist/tileset-sheet-processing.d.ts.map +1 -1
- package/dist/tileset-sheet-processing.js +10 -18
- package/dist/tileset-sheet-processing.js.map +1 -1
- package/package.json +2 -2
|
@@ -1,48 +1,5 @@
|
|
|
1
1
|
import { PNG } from "pngjs";
|
|
2
2
|
import sharp from "sharp";
|
|
3
|
-
const DEFAULT_CHROMA_KEY = { red: 255, green: 0, blue: 255 };
|
|
4
|
-
const CHROMA_KEY_CANDIDATES = [
|
|
5
|
-
DEFAULT_CHROMA_KEY,
|
|
6
|
-
{ red: 0, green: 255, blue: 0 },
|
|
7
|
-
{ red: 0, green: 255, blue: 255 },
|
|
8
|
-
{ red: 255, green: 255, blue: 0 },
|
|
9
|
-
{ red: 0, green: 0, blue: 255 }
|
|
10
|
-
];
|
|
11
|
-
const CHROMA_MATCH_TOLERANCE = 120;
|
|
12
|
-
const CHROMA_EDGE_TOLERANCE = 170;
|
|
13
|
-
const CHROMA_EDGE_FILL_TOLERANCE = 150;
|
|
14
|
-
const STRICT_CHROMA_MATCH_TOLERANCE = 90;
|
|
15
|
-
const COHERENT_EDGE_MATTE_TOLERANCE = 96;
|
|
16
|
-
const COHERENT_EDGE_MATTE_MIN_COVERAGE = 0.55;
|
|
17
|
-
const OPAQUE_EDGE_ALPHA_THRESHOLD = 224;
|
|
18
|
-
export function selectChromaKey(request) {
|
|
19
|
-
const samples = [...(request.references ?? []), ...(request.styleReferences ?? [])]
|
|
20
|
-
.flatMap((reference) => referenceColorSamples(reference));
|
|
21
|
-
const prompt = [
|
|
22
|
-
request.prompt,
|
|
23
|
-
request.asset.prompt,
|
|
24
|
-
request.stylePrompt,
|
|
25
|
-
...(request.asset.tileset?.tiles?.map((tile) => tile.prompt) ?? [])
|
|
26
|
-
].filter(Boolean).join(" ").toLowerCase();
|
|
27
|
-
if (!samples.length) {
|
|
28
|
-
return CHROMA_KEY_CANDIDATES.find((candidate) => !promptMentionsChromaFamily(prompt, candidate)) ??
|
|
29
|
-
DEFAULT_CHROMA_KEY;
|
|
30
|
-
}
|
|
31
|
-
return CHROMA_KEY_CANDIDATES
|
|
32
|
-
.filter((candidate) => !promptMentionsChromaFamily(prompt, candidate))
|
|
33
|
-
.map((candidate) => ({
|
|
34
|
-
candidate,
|
|
35
|
-
score: Math.min(...samples.map((sample) => colorDistance(candidate, sample)))
|
|
36
|
-
}))
|
|
37
|
-
.sort((left, right) => right.score - left.score)[0]?.candidate ??
|
|
38
|
-
CHROMA_KEY_CANDIDATES
|
|
39
|
-
.map((candidate) => ({
|
|
40
|
-
candidate,
|
|
41
|
-
score: Math.min(...samples.map((sample) => colorDistance(candidate, sample)))
|
|
42
|
-
}))
|
|
43
|
-
.sort((left, right) => right.score - left.score)[0]?.candidate ??
|
|
44
|
-
DEFAULT_CHROMA_KEY;
|
|
45
|
-
}
|
|
46
3
|
export function referenceLockPromptLines(references) {
|
|
47
4
|
const analyses = references
|
|
48
5
|
.map((reference) => analyzeReferenceImage(reference))
|
|
@@ -70,28 +27,6 @@ export function referenceLockPromptLines(references) {
|
|
|
70
27
|
return lines;
|
|
71
28
|
});
|
|
72
29
|
}
|
|
73
|
-
export function referenceColorSamples(reference) {
|
|
74
|
-
if (reference.mimeType !== "image/png")
|
|
75
|
-
return [];
|
|
76
|
-
let png;
|
|
77
|
-
try {
|
|
78
|
-
png = PNG.sync.read(Buffer.from(reference.image));
|
|
79
|
-
}
|
|
80
|
-
catch {
|
|
81
|
-
return [];
|
|
82
|
-
}
|
|
83
|
-
const bins = new Map();
|
|
84
|
-
for (let offset = 0; offset < png.data.length; offset += 4) {
|
|
85
|
-
const alpha = png.data[offset + 3] ?? 255;
|
|
86
|
-
if (alpha < 64)
|
|
87
|
-
continue;
|
|
88
|
-
const red = png.data[offset] ?? 0;
|
|
89
|
-
const green = png.data[offset + 1] ?? 0;
|
|
90
|
-
const blue = png.data[offset + 2] ?? 0;
|
|
91
|
-
addColorBin(bins, red, green, blue);
|
|
92
|
-
}
|
|
93
|
-
return topColorBinValues(bins, 8);
|
|
94
|
-
}
|
|
95
30
|
export function analyzeReferenceImage(reference) {
|
|
96
31
|
if (reference.mimeType !== "image/png")
|
|
97
32
|
return undefined;
|
|
@@ -116,8 +51,6 @@ export function analyzeReferenceImage(reference) {
|
|
|
116
51
|
const red = png.data[offset] ?? 0;
|
|
117
52
|
const green = png.data[offset + 1] ?? 0;
|
|
118
53
|
const blue = png.data[offset + 2] ?? 0;
|
|
119
|
-
if (isChromaRgb(red, green, blue))
|
|
120
|
-
continue;
|
|
121
54
|
visiblePixels += 1;
|
|
122
55
|
const max = Math.max(red, green, blue);
|
|
123
56
|
const min = Math.min(red, green, blue);
|
|
@@ -181,9 +114,6 @@ export function topColorBinValues(bins, count) {
|
|
|
181
114
|
export function quantizeColor(value) {
|
|
182
115
|
return Math.round(value / 32) * 32;
|
|
183
116
|
}
|
|
184
|
-
export function isChromaRgb(red, green, blue) {
|
|
185
|
-
return CHROMA_KEY_CANDIDATES.some((chromaKey) => (colorDistance({ red, green, blue }, chromaKey) < 90));
|
|
186
|
-
}
|
|
187
117
|
export function isBrightSaturatedColor(red, green, blue) {
|
|
188
118
|
const max = Math.max(red, green, blue);
|
|
189
119
|
const min = Math.min(red, green, blue);
|
|
@@ -210,16 +140,6 @@ export function colorName(red, green, blue) {
|
|
|
210
140
|
return blue - red > 40 ? "blue" : "purple";
|
|
211
141
|
return "color";
|
|
212
142
|
}
|
|
213
|
-
export function colorDistance(left, right) {
|
|
214
|
-
const redDelta = left.red - right.red;
|
|
215
|
-
const greenDelta = left.green - right.green;
|
|
216
|
-
const blueDelta = left.blue - right.blue;
|
|
217
|
-
return Math.sqrt(redDelta * redDelta + greenDelta * greenDelta + blueDelta * blueDelta);
|
|
218
|
-
}
|
|
219
|
-
export function hexColor(color) {
|
|
220
|
-
const channel = (value) => value.toString(16).padStart(2, "0");
|
|
221
|
-
return `#${channel(color.red)}${channel(color.green)}${channel(color.blue)}`;
|
|
222
|
-
}
|
|
223
143
|
export function rgbColor(color) {
|
|
224
144
|
return `rgb(${color.red}, ${color.green}, ${color.blue})`;
|
|
225
145
|
}
|
|
@@ -232,8 +152,8 @@ export function variationDirectionPromptLine(index) {
|
|
|
232
152
|
];
|
|
233
153
|
return variants[index % variants.length];
|
|
234
154
|
}
|
|
235
|
-
export function
|
|
236
|
-
if (
|
|
155
|
+
export function shouldRequestTransparency(request, context) {
|
|
156
|
+
if (context.outputFormat === "jpeg") {
|
|
237
157
|
return false;
|
|
238
158
|
}
|
|
239
159
|
if (context.requestedBackground === "opaque") {
|
|
@@ -254,270 +174,6 @@ export function resolveRequestedBackground(request, options) {
|
|
|
254
174
|
}
|
|
255
175
|
return options.background ?? "transparent";
|
|
256
176
|
}
|
|
257
|
-
export function shouldPostprocessTransparency(request, context) {
|
|
258
|
-
return shouldRequestRgbaPng(request, context);
|
|
259
|
-
}
|
|
260
|
-
export function removeChromaBackground(image, chromaKey) {
|
|
261
|
-
const png = PNG.sync.read(Buffer.from(image));
|
|
262
|
-
const backgroundRemoval = detectBackgroundRemoval(png, chromaKey);
|
|
263
|
-
if (!backgroundRemoval) {
|
|
264
|
-
return Buffer.from(image);
|
|
265
|
-
}
|
|
266
|
-
removeDetectedBackground(png, backgroundRemoval);
|
|
267
|
-
return PNG.sync.write(png);
|
|
268
|
-
}
|
|
269
|
-
export function removeTilesetChromaBackground(image, tileset, chromaKey) {
|
|
270
|
-
const png = PNG.sync.read(Buffer.from(image));
|
|
271
|
-
const tileCapacity = tileset.columns * tileset.rows;
|
|
272
|
-
const tileCount = Math.min(tileset.tileCount ?? tileCapacity, tileCapacity);
|
|
273
|
-
const cellPixels = new Uint8Array(png.width * png.height);
|
|
274
|
-
for (let index = 0; index < tileCapacity; index += 1) {
|
|
275
|
-
const bounds = scaledTilesetCellBounds(png, tileset, index);
|
|
276
|
-
if (!bounds)
|
|
277
|
-
continue;
|
|
278
|
-
markPngRect(cellPixels, png.width, bounds.x, bounds.y, bounds.width, bounds.height);
|
|
279
|
-
if (index >= tileCount) {
|
|
280
|
-
clearPngRect(png, bounds.x, bounds.y, bounds.width, bounds.height);
|
|
281
|
-
continue;
|
|
282
|
-
}
|
|
283
|
-
const cell = copyPngRect(png, bounds.x, bounds.y, bounds.width, bounds.height);
|
|
284
|
-
if (removeKnownChromaPixels(cell, chromaKey)) {
|
|
285
|
-
pastePngRect(png, cell, bounds.x, bounds.y);
|
|
286
|
-
}
|
|
287
|
-
}
|
|
288
|
-
clearUnmarkedPngPixels(png, cellPixels);
|
|
289
|
-
return PNG.sync.write(png);
|
|
290
|
-
}
|
|
291
|
-
function markPngRect(pixels, imageWidth, x, y, width, height) {
|
|
292
|
-
for (let localY = 0; localY < height; localY += 1) {
|
|
293
|
-
const start = (y + localY) * imageWidth + x;
|
|
294
|
-
pixels.fill(1, start, start + width);
|
|
295
|
-
}
|
|
296
|
-
}
|
|
297
|
-
function clearUnmarkedPngPixels(png, markedPixels) {
|
|
298
|
-
for (let index = 0; index < markedPixels.length; index += 1) {
|
|
299
|
-
if (markedPixels[index])
|
|
300
|
-
continue;
|
|
301
|
-
const offset = index * 4;
|
|
302
|
-
png.data[offset] = 0;
|
|
303
|
-
png.data[offset + 1] = 0;
|
|
304
|
-
png.data[offset + 2] = 0;
|
|
305
|
-
png.data[offset + 3] = 0;
|
|
306
|
-
}
|
|
307
|
-
}
|
|
308
|
-
function removeDetectedBackground(png, backgroundRemoval) {
|
|
309
|
-
const width = png.width;
|
|
310
|
-
const height = png.height;
|
|
311
|
-
const visited = new Uint8Array(width * height);
|
|
312
|
-
const transparent = new Uint8Array(width * height);
|
|
313
|
-
const queue = [];
|
|
314
|
-
let queueCursor = 0;
|
|
315
|
-
const enqueue = (x, y) => {
|
|
316
|
-
if (x < 0 || y < 0 || x >= width || y >= height)
|
|
317
|
-
return;
|
|
318
|
-
const index = y * width + x;
|
|
319
|
-
if (visited[index])
|
|
320
|
-
return;
|
|
321
|
-
visited[index] = 1;
|
|
322
|
-
if (isRemovableEdgeBackgroundPixel(png, index, backgroundRemoval)) {
|
|
323
|
-
queue.push(index);
|
|
324
|
-
}
|
|
325
|
-
};
|
|
326
|
-
for (let x = 0; x < width; x += 1) {
|
|
327
|
-
enqueue(x, 0);
|
|
328
|
-
enqueue(x, height - 1);
|
|
329
|
-
}
|
|
330
|
-
for (let y = 1; y < height - 1; y += 1) {
|
|
331
|
-
enqueue(0, y);
|
|
332
|
-
enqueue(width - 1, y);
|
|
333
|
-
}
|
|
334
|
-
while (queueCursor < queue.length) {
|
|
335
|
-
const index = queue[queueCursor];
|
|
336
|
-
queueCursor += 1;
|
|
337
|
-
transparent[index] = 1;
|
|
338
|
-
const x = index % width;
|
|
339
|
-
const y = Math.floor(index / width);
|
|
340
|
-
enqueue(x + 1, y);
|
|
341
|
-
enqueue(x - 1, y);
|
|
342
|
-
enqueue(x, y + 1);
|
|
343
|
-
enqueue(x, y - 1);
|
|
344
|
-
}
|
|
345
|
-
for (let index = 0; index < transparent.length; index += 1) {
|
|
346
|
-
if (transparent[index] ||
|
|
347
|
-
(backgroundRemoval.kind === "chroma" &&
|
|
348
|
-
backgroundRemoval.removeDisconnected &&
|
|
349
|
-
isHighConfidenceChromaPixel(png, index, backgroundRemoval.chromaKey))) {
|
|
350
|
-
transparent[index] = 1;
|
|
351
|
-
setAlpha(png, index, 0);
|
|
352
|
-
}
|
|
353
|
-
}
|
|
354
|
-
featherBackgroundEdges(png, transparent, backgroundRemoval);
|
|
355
|
-
}
|
|
356
|
-
function removeKnownChromaPixels(png, chromaKey) {
|
|
357
|
-
const transparent = new Uint8Array(png.width * png.height);
|
|
358
|
-
let found = false;
|
|
359
|
-
for (let index = 0; index < transparent.length; index += 1) {
|
|
360
|
-
if (isChromaPixel(png, index, CHROMA_MATCH_TOLERANCE, chromaKey) ||
|
|
361
|
-
isKeyTintedPixel(rgbAt(png, index), chromaKey, 0.86)) {
|
|
362
|
-
transparent[index] = 1;
|
|
363
|
-
setAlpha(png, index, 0);
|
|
364
|
-
found = true;
|
|
365
|
-
}
|
|
366
|
-
}
|
|
367
|
-
if (found) {
|
|
368
|
-
featherBackgroundEdges(png, transparent, {
|
|
369
|
-
kind: "chroma",
|
|
370
|
-
chromaKey,
|
|
371
|
-
removeDisconnected: true
|
|
372
|
-
});
|
|
373
|
-
}
|
|
374
|
-
return found;
|
|
375
|
-
}
|
|
376
|
-
function scaledTilesetCellBounds(png, tileset, index) {
|
|
377
|
-
const margin = tileset.margin ?? 0;
|
|
378
|
-
const spacing = tileset.spacing ?? 0;
|
|
379
|
-
const sheetWidth = margin * 2 + tileset.columns * tileset.tileWidth +
|
|
380
|
-
Math.max(0, tileset.columns - 1) * spacing;
|
|
381
|
-
const sheetHeight = margin * 2 + tileset.rows * tileset.tileHeight +
|
|
382
|
-
Math.max(0, tileset.rows - 1) * spacing;
|
|
383
|
-
if (sheetWidth <= 0 || sheetHeight <= 0 || index < 0 || index >= tileset.columns * tileset.rows) {
|
|
384
|
-
return undefined;
|
|
385
|
-
}
|
|
386
|
-
const column = index % tileset.columns;
|
|
387
|
-
const row = Math.floor(index / tileset.columns);
|
|
388
|
-
const sourceX = margin + column * (tileset.tileWidth + spacing);
|
|
389
|
-
const sourceY = margin + row * (tileset.tileHeight + spacing);
|
|
390
|
-
const x = Math.round((sourceX / sheetWidth) * png.width);
|
|
391
|
-
const y = Math.round((sourceY / sheetHeight) * png.height);
|
|
392
|
-
const right = Math.round(((sourceX + tileset.tileWidth) / sheetWidth) * png.width);
|
|
393
|
-
const bottom = Math.round(((sourceY + tileset.tileHeight) / sheetHeight) * png.height);
|
|
394
|
-
return {
|
|
395
|
-
x,
|
|
396
|
-
y,
|
|
397
|
-
width: Math.max(1, right - x),
|
|
398
|
-
height: Math.max(1, bottom - y)
|
|
399
|
-
};
|
|
400
|
-
}
|
|
401
|
-
function copyPngRect(png, x, y, width, height) {
|
|
402
|
-
const target = new PNG({ width, height });
|
|
403
|
-
PNG.bitblt(png, target, x, y, width, height, 0, 0);
|
|
404
|
-
return target;
|
|
405
|
-
}
|
|
406
|
-
function pastePngRect(png, source, x, y) {
|
|
407
|
-
PNG.bitblt(source, png, 0, 0, source.width, source.height, x, y);
|
|
408
|
-
}
|
|
409
|
-
export function detectBackgroundRemoval(png, chromaKey) {
|
|
410
|
-
if (hasRequestedChromaKey(png, chromaKey)) {
|
|
411
|
-
return {
|
|
412
|
-
kind: "chroma",
|
|
413
|
-
chromaKey,
|
|
414
|
-
removeDisconnected: hasRequestedChromaKeyOnEdges(png, chromaKey)
|
|
415
|
-
};
|
|
416
|
-
}
|
|
417
|
-
const matteColor = detectNeutralEdgeMatte(png);
|
|
418
|
-
if (matteColor) {
|
|
419
|
-
return { kind: "edge-matte", matteColor };
|
|
420
|
-
}
|
|
421
|
-
const coherentMatteColor = detectCoherentEdgeMatte(png);
|
|
422
|
-
return coherentMatteColor
|
|
423
|
-
? { kind: "coherent-edge-matte", matteColor: coherentMatteColor }
|
|
424
|
-
: undefined;
|
|
425
|
-
}
|
|
426
|
-
export function hasRequestedChromaKey(png, chromaKey) {
|
|
427
|
-
if (hasRequestedChromaKeyOnEdges(png, chromaKey))
|
|
428
|
-
return true;
|
|
429
|
-
let matches = 0;
|
|
430
|
-
const threshold = Math.max(256, Math.floor(png.width * png.height * 0.005));
|
|
431
|
-
for (let index = 0; index < png.width * png.height; index += 1) {
|
|
432
|
-
if (isChromaPixel(png, index, STRICT_CHROMA_MATCH_TOLERANCE, chromaKey) ||
|
|
433
|
-
isKeyTintedPixel(rgbAt(png, index), chromaKey, 0.8)) {
|
|
434
|
-
matches += 1;
|
|
435
|
-
if (matches >= threshold)
|
|
436
|
-
return true;
|
|
437
|
-
}
|
|
438
|
-
}
|
|
439
|
-
return false;
|
|
440
|
-
}
|
|
441
|
-
export function hasRequestedChromaKeyOnEdges(png, chromaKey) {
|
|
442
|
-
const edgeStats = sampleImageEdges(png, (index) => (isChromaPixel(png, index, STRICT_CHROMA_MATCH_TOLERANCE, chromaKey) ||
|
|
443
|
-
isKeyTintedPixel(rgbAt(png, index), chromaKey, 0.8)));
|
|
444
|
-
return edgeStats.matches / edgeStats.total >= 0.18;
|
|
445
|
-
}
|
|
446
|
-
export function detectNeutralEdgeMatte(png) {
|
|
447
|
-
let edgePixelCount = 0;
|
|
448
|
-
let neutralEdgePixelCount = 0;
|
|
449
|
-
let redTotal = 0;
|
|
450
|
-
let greenTotal = 0;
|
|
451
|
-
let blueTotal = 0;
|
|
452
|
-
sampleImageEdges(png, (index) => {
|
|
453
|
-
const color = rgbAt(png, index);
|
|
454
|
-
edgePixelCount += 1;
|
|
455
|
-
if (isNeutralMatteCandidate(color)) {
|
|
456
|
-
neutralEdgePixelCount += 1;
|
|
457
|
-
redTotal += color.red;
|
|
458
|
-
greenTotal += color.green;
|
|
459
|
-
blueTotal += color.blue;
|
|
460
|
-
}
|
|
461
|
-
return false;
|
|
462
|
-
});
|
|
463
|
-
if (neutralEdgePixelCount === 0 ||
|
|
464
|
-
neutralEdgePixelCount / edgePixelCount < 0.65 ||
|
|
465
|
-
neutralCornerCount(png) < 3) {
|
|
466
|
-
return undefined;
|
|
467
|
-
}
|
|
468
|
-
const matteColor = {
|
|
469
|
-
red: Math.round(redTotal / neutralEdgePixelCount),
|
|
470
|
-
green: Math.round(greenTotal / neutralEdgePixelCount),
|
|
471
|
-
blue: Math.round(blueTotal / neutralEdgePixelCount)
|
|
472
|
-
};
|
|
473
|
-
const matteStats = sampleImageEdges(png, (index) => (isNeutralMattePixel(rgbAt(png, index), matteColor)));
|
|
474
|
-
return matteStats.matches / matteStats.total >= 0.55 ? matteColor : undefined;
|
|
475
|
-
}
|
|
476
|
-
export function detectCoherentEdgeMatte(png) {
|
|
477
|
-
const corners = [
|
|
478
|
-
{ x: 0, y: 0 },
|
|
479
|
-
{ x: png.width - 1, y: 0 },
|
|
480
|
-
{ x: 0, y: png.height - 1 },
|
|
481
|
-
{ x: png.width - 1, y: png.height - 1 }
|
|
482
|
-
].map(({ x, y }) => y * png.width + x)
|
|
483
|
-
.filter((index) => alphaAt(png, index) >= OPAQUE_EDGE_ALPHA_THRESHOLD)
|
|
484
|
-
.map((index) => rgbAt(png, index));
|
|
485
|
-
if (corners.length < 3)
|
|
486
|
-
return undefined;
|
|
487
|
-
const cornerCluster = corners
|
|
488
|
-
.map((seed) => corners.filter((color) => (colorDistance(color, seed) <= COHERENT_EDGE_MATTE_TOLERANCE)))
|
|
489
|
-
.sort((left, right) => right.length - left.length)[0];
|
|
490
|
-
if (!cornerCluster || cornerCluster.length < 3)
|
|
491
|
-
return undefined;
|
|
492
|
-
const cornerColor = averageColors(cornerCluster);
|
|
493
|
-
const matchingEdgeColors = [];
|
|
494
|
-
const edgeStats = sampleImageEdges(png, (index) => {
|
|
495
|
-
if (alphaAt(png, index) < OPAQUE_EDGE_ALPHA_THRESHOLD)
|
|
496
|
-
return false;
|
|
497
|
-
const color = rgbAt(png, index);
|
|
498
|
-
const matches = colorDistance(color, cornerColor) <= COHERENT_EDGE_MATTE_TOLERANCE;
|
|
499
|
-
if (matches)
|
|
500
|
-
matchingEdgeColors.push(color);
|
|
501
|
-
return matches;
|
|
502
|
-
});
|
|
503
|
-
if (matchingEdgeColors.length === 0 ||
|
|
504
|
-
edgeStats.matches / edgeStats.total < COHERENT_EDGE_MATTE_MIN_COVERAGE) {
|
|
505
|
-
return undefined;
|
|
506
|
-
}
|
|
507
|
-
const matteColor = averageColors(matchingEdgeColors);
|
|
508
|
-
const refinedStats = sampleImageEdges(png, (index) => (alphaAt(png, index) >= OPAQUE_EDGE_ALPHA_THRESHOLD &&
|
|
509
|
-
colorDistance(rgbAt(png, index), matteColor) <= COHERENT_EDGE_MATTE_TOLERANCE));
|
|
510
|
-
return refinedStats.matches / refinedStats.total >= COHERENT_EDGE_MATTE_MIN_COVERAGE
|
|
511
|
-
? matteColor
|
|
512
|
-
: undefined;
|
|
513
|
-
}
|
|
514
|
-
function averageColors(colors) {
|
|
515
|
-
return {
|
|
516
|
-
red: Math.round(colors.reduce((sum, color) => sum + color.red, 0) / colors.length),
|
|
517
|
-
green: Math.round(colors.reduce((sum, color) => sum + color.green, 0) / colors.length),
|
|
518
|
-
blue: Math.round(colors.reduce((sum, color) => sum + color.blue, 0) / colors.length)
|
|
519
|
-
};
|
|
520
|
-
}
|
|
521
177
|
export function resizePngToDimensions(image, dimensions) {
|
|
522
178
|
const source = PNG.sync.read(Buffer.from(image));
|
|
523
179
|
if (source.width === dimensions.width && source.height === dimensions.height) {
|
|
@@ -593,6 +249,8 @@ export async function resizeRasterToDimensions(image, dimensions, outputFormat)
|
|
|
593
249
|
fit: "fill",
|
|
594
250
|
kernel: sharp.kernel.nearest
|
|
595
251
|
});
|
|
252
|
+
if (outputFormat === "png")
|
|
253
|
+
return resized.png().toBuffer();
|
|
596
254
|
return outputFormat === "webp"
|
|
597
255
|
? resized.webp({ quality: 100 }).toBuffer()
|
|
598
256
|
: resized.jpeg({ quality: 95 }).toBuffer();
|
|
@@ -744,143 +402,4 @@ function copyShiftedPngRect(png, source, options) {
|
|
|
744
402
|
}
|
|
745
403
|
}
|
|
746
404
|
}
|
|
747
|
-
export function featherBackgroundEdges(png, transparent, backgroundRemoval) {
|
|
748
|
-
const width = png.width;
|
|
749
|
-
const height = png.height;
|
|
750
|
-
for (let y = 0; y < height; y += 1) {
|
|
751
|
-
for (let x = 0; x < width; x += 1) {
|
|
752
|
-
const index = y * width + x;
|
|
753
|
-
if (transparent[index] || !touchesTransparentNeighbor(transparent, width, height, x, y)) {
|
|
754
|
-
continue;
|
|
755
|
-
}
|
|
756
|
-
const distance = backgroundDistance(png, index, backgroundRemoval);
|
|
757
|
-
if (distance > CHROMA_EDGE_TOLERANCE) {
|
|
758
|
-
continue;
|
|
759
|
-
}
|
|
760
|
-
const alpha = Math.max(0, Math.min(255, Math.round((distance / CHROMA_EDGE_TOLERANCE) * 255)));
|
|
761
|
-
setAlpha(png, index, Math.min(alphaAt(png, index), alpha));
|
|
762
|
-
}
|
|
763
|
-
}
|
|
764
|
-
}
|
|
765
|
-
export function touchesTransparentNeighbor(transparent, width, height, x, y) {
|
|
766
|
-
return ((x > 0 && transparent[y * width + x - 1] === 1) ||
|
|
767
|
-
(x < width - 1 && transparent[y * width + x + 1] === 1) ||
|
|
768
|
-
(y > 0 && transparent[(y - 1) * width + x] === 1) ||
|
|
769
|
-
(y < height - 1 && transparent[(y + 1) * width + x] === 1));
|
|
770
|
-
}
|
|
771
|
-
export function isChromaPixel(png, index, tolerance, chromaKey) {
|
|
772
|
-
return chromaDistance(png, index, chromaKey) <= tolerance;
|
|
773
|
-
}
|
|
774
|
-
export function isRemovableEdgeBackgroundPixel(png, index, backgroundRemoval) {
|
|
775
|
-
if (backgroundRemoval.kind === "chroma") {
|
|
776
|
-
return (isChromaPixel(png, index, CHROMA_EDGE_FILL_TOLERANCE, backgroundRemoval.chromaKey) ||
|
|
777
|
-
isKeyTintedPixel(rgbAt(png, index), backgroundRemoval.chromaKey, 0.86));
|
|
778
|
-
}
|
|
779
|
-
if (backgroundRemoval.kind === "coherent-edge-matte") {
|
|
780
|
-
return colorDistance(rgbAt(png, index), backgroundRemoval.matteColor) <=
|
|
781
|
-
COHERENT_EDGE_MATTE_TOLERANCE;
|
|
782
|
-
}
|
|
783
|
-
return isNeutralMattePixel(rgbAt(png, index), backgroundRemoval.matteColor);
|
|
784
|
-
}
|
|
785
|
-
export function isHighConfidenceChromaPixel(png, index, chromaKey) {
|
|
786
|
-
return (isChromaPixel(png, index, STRICT_CHROMA_MATCH_TOLERANCE, chromaKey) ||
|
|
787
|
-
isKeyTintedPixel(rgbAt(png, index), chromaKey, 0.86));
|
|
788
|
-
}
|
|
789
|
-
export function isKeyTintedPixel(color, chromaKey, strength) {
|
|
790
|
-
const keyChannels = [
|
|
791
|
-
{ color: color.red, key: chromaKey.red },
|
|
792
|
-
{ color: color.green, key: chromaKey.green },
|
|
793
|
-
{ color: color.blue, key: chromaKey.blue }
|
|
794
|
-
];
|
|
795
|
-
const activeChannels = keyChannels.filter((channel) => channel.key > 128);
|
|
796
|
-
const inactiveChannels = keyChannels.filter((channel) => channel.key <= 128);
|
|
797
|
-
return (activeChannels.every((channel) => channel.color >= 150 * strength) &&
|
|
798
|
-
inactiveChannels.every((channel) => channel.color <= 190 * (1.1 - strength)));
|
|
799
|
-
}
|
|
800
|
-
export function promptMentionsChromaFamily(prompt, chromaKey) {
|
|
801
|
-
if (chromaKey.red === 255 && chromaKey.green === 0 && chromaKey.blue === 255) {
|
|
802
|
-
return /\b(red|pink|purple|magenta|violet|crimson|fuchsia)\b/.test(prompt);
|
|
803
|
-
}
|
|
804
|
-
if (chromaKey.red === 0 && chromaKey.green === 255 && chromaKey.blue === 0) {
|
|
805
|
-
return /\b(green|lime|emerald|neon green|chartreuse)\b/.test(prompt);
|
|
806
|
-
}
|
|
807
|
-
if (chromaKey.red === 0 && chromaKey.green === 255 && chromaKey.blue === 255) {
|
|
808
|
-
return /\b(cyan|aqua|teal|turquoise|blue)\b/.test(prompt);
|
|
809
|
-
}
|
|
810
|
-
if (chromaKey.red === 255 && chromaKey.green === 255 && chromaKey.blue === 0) {
|
|
811
|
-
return /\b(yellow|gold|golden|amber)\b/.test(prompt);
|
|
812
|
-
}
|
|
813
|
-
if (chromaKey.red === 0 && chromaKey.green === 0 && chromaKey.blue === 255) {
|
|
814
|
-
return /\b(blue|navy|cobalt|azure|indigo|cyan)\b/.test(prompt);
|
|
815
|
-
}
|
|
816
|
-
return false;
|
|
817
|
-
}
|
|
818
|
-
export function chromaDistance(png, index, chromaKey) {
|
|
819
|
-
return colorDistance(rgbAt(png, index), chromaKey);
|
|
820
|
-
}
|
|
821
|
-
export function backgroundDistance(png, index, backgroundRemoval) {
|
|
822
|
-
if (backgroundRemoval.kind === "chroma") {
|
|
823
|
-
return chromaDistance(png, index, backgroundRemoval.chromaKey);
|
|
824
|
-
}
|
|
825
|
-
return colorDistance(rgbAt(png, index), backgroundRemoval.matteColor);
|
|
826
|
-
}
|
|
827
|
-
export function isNeutralMattePixel(color, matteColor) {
|
|
828
|
-
const max = Math.max(color.red, color.green, color.blue);
|
|
829
|
-
const min = Math.min(color.red, color.green, color.blue);
|
|
830
|
-
const saturation = max === 0 ? 0 : (max - min) / max;
|
|
831
|
-
const luminance = relativeLuminance(color.red, color.green, color.blue);
|
|
832
|
-
const matteLuminance = relativeLuminance(matteColor.red, matteColor.green, matteColor.blue);
|
|
833
|
-
return (saturation <= 0.22 &&
|
|
834
|
-
luminance >= 80 &&
|
|
835
|
-
Math.abs(luminance - matteLuminance) <= 70 &&
|
|
836
|
-
colorDistance(color, matteColor) <= 92);
|
|
837
|
-
}
|
|
838
|
-
export function isNeutralMatteCandidate(color) {
|
|
839
|
-
const max = Math.max(color.red, color.green, color.blue);
|
|
840
|
-
const min = Math.min(color.red, color.green, color.blue);
|
|
841
|
-
const saturation = max === 0 ? 0 : (max - min) / max;
|
|
842
|
-
const luminance = relativeLuminance(color.red, color.green, color.blue);
|
|
843
|
-
return saturation <= 0.22 && luminance >= 75;
|
|
844
|
-
}
|
|
845
|
-
export function neutralCornerCount(png) {
|
|
846
|
-
return [
|
|
847
|
-
{ x: 0, y: 0 },
|
|
848
|
-
{ x: png.width - 1, y: 0 },
|
|
849
|
-
{ x: 0, y: png.height - 1 },
|
|
850
|
-
{ x: png.width - 1, y: png.height - 1 }
|
|
851
|
-
].filter(({ x, y }) => isNeutralMatteCandidate(rgbAt(png, y * png.width + x))).length;
|
|
852
|
-
}
|
|
853
|
-
export function sampleImageEdges(png, predicate) {
|
|
854
|
-
let total = 0;
|
|
855
|
-
let matches = 0;
|
|
856
|
-
const sample = (x, y) => {
|
|
857
|
-
total += 1;
|
|
858
|
-
if (predicate(y * png.width + x)) {
|
|
859
|
-
matches += 1;
|
|
860
|
-
}
|
|
861
|
-
};
|
|
862
|
-
for (let x = 0; x < png.width; x += 1) {
|
|
863
|
-
sample(x, 0);
|
|
864
|
-
sample(x, png.height - 1);
|
|
865
|
-
}
|
|
866
|
-
for (let y = 1; y < png.height - 1; y += 1) {
|
|
867
|
-
sample(0, y);
|
|
868
|
-
sample(png.width - 1, y);
|
|
869
|
-
}
|
|
870
|
-
return { total, matches };
|
|
871
|
-
}
|
|
872
|
-
export function rgbAt(png, index) {
|
|
873
|
-
const offset = index * 4;
|
|
874
|
-
return {
|
|
875
|
-
red: png.data[offset] ?? 0,
|
|
876
|
-
green: png.data[offset + 1] ?? 0,
|
|
877
|
-
blue: png.data[offset + 2] ?? 0
|
|
878
|
-
};
|
|
879
|
-
}
|
|
880
|
-
export function alphaAt(png, index) {
|
|
881
|
-
return png.data[index * 4 + 3] ?? 255;
|
|
882
|
-
}
|
|
883
|
-
export function setAlpha(png, index, alpha) {
|
|
884
|
-
png.data[index * 4 + 3] = alpha;
|
|
885
|
-
}
|
|
886
405
|
//# sourceMappingURL=provider-image-processing.js.map
|