dsh-drawai 0.2.4 → 0.2.6
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 +4 -4
- package/lib/client.js +72 -33
- package/lib/index.js +249 -40
- package/lib/raster.js +55 -7
- package/lib/trace-image.js +539 -75
- package/package.json +1 -1
package/lib/trace-image.js
CHANGED
|
@@ -5,11 +5,12 @@
|
|
|
5
5
|
* 这里只有**像素进、SVG 出**,不碰文件系统、不碰 ctx:宿主工具与命令行脚本共用同一份。
|
|
6
6
|
*
|
|
7
7
|
* 管线(每一步都能在返回的 metrics 里看到数字):
|
|
8
|
-
* ①
|
|
9
|
-
* ②
|
|
10
|
-
* ③
|
|
11
|
-
* ④
|
|
12
|
-
* ⑤
|
|
8
|
+
* ① 工作图尺寸 = src/scale,再套 grid(宽)/ gridH(高)上限;透明像素合成到 bgColor
|
|
9
|
+
* ② 四边泛洪判背景(近白 / 近灰 / 全透明)→ 不画截图底色
|
|
10
|
+
* ③ 分层 k-means(colorsDark 暗色 + colorsFill 填充)定调色板;暗像素进保护区
|
|
11
|
+
* ④ 3× 超采样归类 → 条件众数(高对比跳过)→ despeckle / minRegionArea(保护区不并)
|
|
12
|
+
* ⑤ 每色按连通域取闭合轮廓:像素**格边**上的有向图 + 左转跟随(区域恒在左侧)
|
|
13
|
+
* ⑥ RDP + 预算阶梯(受保护环不因 minLoopArea 丢弃)→ 每色一条 <path fill-rule="nonzero">
|
|
13
14
|
*
|
|
14
15
|
* 为什么是"格边跟随"而不是 marching squares:标签图在鞍点(对角相接)处 marching squares
|
|
15
16
|
* 要人为定连通性,而格边有向图本身就无歧义 —— 每条边只属于一个区域,走完一圈恰好回到起点。
|
|
@@ -21,6 +22,11 @@ function clamp(v, lo, hi) {
|
|
|
21
22
|
return v < lo ? lo : v > hi ? hi : v
|
|
22
23
|
}
|
|
23
24
|
|
|
25
|
+
/** 相对亮度(0..255),用于线稿/保护区与众数对比。 */
|
|
26
|
+
function lumaRgb(r, g, b) {
|
|
27
|
+
return 0.299 * r + 0.587 * g + 0.114 * b
|
|
28
|
+
}
|
|
29
|
+
|
|
24
30
|
/* ────────────────────────── ① 背景 ────────────────────────── */
|
|
25
31
|
|
|
26
32
|
/**
|
|
@@ -305,12 +311,21 @@ function traceLoops(comp, width, height, regionCount) {
|
|
|
305
311
|
* · 并进**颜色最接近**的邻色,而不是"面积最多的"—— 边缘的过渡色应该并回它看起来最像的那一侧;
|
|
306
312
|
* · **不连锁**:一轮里对固定快照判一次,避免小块并进小块滚雪球。
|
|
307
313
|
*/
|
|
308
|
-
|
|
314
|
+
/**
|
|
315
|
+
* 按颜色分块合并碎块。
|
|
316
|
+
* @param {Uint8Array|null} protectMask 非空时:块内受保护像素过半,或该 label 本身是保护色 → 不合并
|
|
317
|
+
* @param {Uint8Array|null} protectLabels 长度为 palette.length;1 = 该色永不被合并
|
|
318
|
+
*/
|
|
319
|
+
function despeckle(labels, width, height, palette, minArea, protectMask, protectLabels) {
|
|
320
|
+
if (!(minArea > 0)) {
|
|
321
|
+
return { mergedPixels: 0, blocksMerged: 0, blocks: 0, protectedSkipped: 0 }
|
|
322
|
+
}
|
|
309
323
|
const colorCount = palette.length
|
|
310
324
|
const comp = new Int32Array(width * height).fill(-1)
|
|
311
325
|
const stack = []
|
|
312
326
|
let mergedPixels = 0
|
|
313
327
|
let blocksMerged = 0
|
|
328
|
+
let protectedSkipped = 0
|
|
314
329
|
|
|
315
330
|
// 先把每块 4 连通区域找出来(一次,不随合并改变)
|
|
316
331
|
const blocks = []
|
|
@@ -353,6 +368,18 @@ function despeckle(labels, width, height, palette, minArea) {
|
|
|
353
368
|
if (block.pixels.length >= minArea) continue
|
|
354
369
|
// 背景(label 0)不参与:它是"没画东西的地方",并它等于把图啃出洞
|
|
355
370
|
if (palette[block.label] === undefined) continue
|
|
371
|
+
if (protectLabels !== null && protectLabels !== undefined && protectLabels[block.label] === 1) {
|
|
372
|
+
protectedSkipped += 1
|
|
373
|
+
continue
|
|
374
|
+
}
|
|
375
|
+
if (protectMask !== null && protectMask !== undefined) {
|
|
376
|
+
let prot = 0
|
|
377
|
+
for (let k = 0; k < block.pixels.length; k += 1) if (protectMask[block.pixels[k]] === 1) prot += 1
|
|
378
|
+
if (prot * 2 >= block.pixels.length) {
|
|
379
|
+
protectedSkipped += 1
|
|
380
|
+
continue
|
|
381
|
+
}
|
|
382
|
+
}
|
|
356
383
|
hist.fill(0)
|
|
357
384
|
for (let k = 0; k < block.pixels.length; k += 1) {
|
|
358
385
|
const p = block.pixels[k]
|
|
@@ -392,7 +419,109 @@ function despeckle(labels, width, height, palette, minArea) {
|
|
|
392
419
|
mergedPixels += pixels.length
|
|
393
420
|
blocksMerged += 1
|
|
394
421
|
}
|
|
395
|
-
return {
|
|
422
|
+
return {
|
|
423
|
+
mergedPixels: mergedPixels,
|
|
424
|
+
blocksMerged: blocksMerged,
|
|
425
|
+
blocks: blocks.length,
|
|
426
|
+
protectedSkipped: protectedSkipped,
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
/**
|
|
431
|
+
* 分层调色板:暗像素单独 k-means(colorsDark),其余走 colorsFill。
|
|
432
|
+
* colorsDark=0 时退回单次 k-means(与旧行为一致)。
|
|
433
|
+
*/
|
|
434
|
+
function buildPalette(work, fgIndex, colors, colorsDark, colorsFill, protectLuma, seeds) {
|
|
435
|
+
const darkK = Math.max(0, Math.min(colors - 1, colorsDark))
|
|
436
|
+
const fillK = Math.max(1, colors - darkK)
|
|
437
|
+
// 显式 colorsFill 时以它为准(仍保证总和 = colors)
|
|
438
|
+
const useFill = colorsFill > 0 ? Math.max(1, Math.min(colors, colorsFill)) : fillK
|
|
439
|
+
const useDark = colorsFill > 0 ? Math.max(0, colors - useFill) : darkK
|
|
440
|
+
|
|
441
|
+
if (useDark === 0 || !(protectLuma > 0)) {
|
|
442
|
+
const samples = new Float64Array(fgIndex.length * 3)
|
|
443
|
+
for (let i = 0; i < fgIndex.length; i += 1) {
|
|
444
|
+
const o = fgIndex[i] * 4
|
|
445
|
+
samples[i * 3] = work[o]
|
|
446
|
+
samples[i * 3 + 1] = work[o + 1]
|
|
447
|
+
samples[i * 3 + 2] = work[o + 2]
|
|
448
|
+
}
|
|
449
|
+
let best = null
|
|
450
|
+
for (let s = 1; s <= seeds; s += 1) {
|
|
451
|
+
const got = kmeans(samples, colors, 24, s * 2654435761)
|
|
452
|
+
if (best === null || got.inertia < best.inertia) best = got
|
|
453
|
+
}
|
|
454
|
+
return {
|
|
455
|
+
cents: best.cent,
|
|
456
|
+
colorsDark: 0,
|
|
457
|
+
colorsFill: colors,
|
|
458
|
+
darkSamples: 0,
|
|
459
|
+
fillSamples: fgIndex.length,
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
const darkIdx = []
|
|
464
|
+
const fillIdx = []
|
|
465
|
+
for (let i = 0; i < fgIndex.length; i += 1) {
|
|
466
|
+
const p = fgIndex[i]
|
|
467
|
+
const o = p * 4
|
|
468
|
+
if (lumaRgb(work[o], work[o + 1], work[o + 2]) <= protectLuma) darkIdx.push(p)
|
|
469
|
+
else fillIdx.push(p)
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
// 某一侧为空时,把配额并给另一侧,避免空簇
|
|
473
|
+
let kDark = useDark
|
|
474
|
+
let kFill = useFill
|
|
475
|
+
if (darkIdx.length === 0) {
|
|
476
|
+
kFill = colors
|
|
477
|
+
kDark = 0
|
|
478
|
+
} else if (fillIdx.length === 0) {
|
|
479
|
+
kDark = colors
|
|
480
|
+
kFill = 0
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
const cents = []
|
|
484
|
+
function runK(indexes, k, seedBase) {
|
|
485
|
+
if (k <= 0 || indexes.length === 0) return
|
|
486
|
+
const samples = new Float64Array(indexes.length * 3)
|
|
487
|
+
for (let i = 0; i < indexes.length; i += 1) {
|
|
488
|
+
const o = indexes[i] * 4
|
|
489
|
+
samples[i * 3] = work[o]
|
|
490
|
+
samples[i * 3 + 1] = work[o + 1]
|
|
491
|
+
samples[i * 3 + 2] = work[o + 2]
|
|
492
|
+
}
|
|
493
|
+
const kk = Math.min(k, indexes.length)
|
|
494
|
+
let best = null
|
|
495
|
+
for (let s = 1; s <= seeds; s += 1) {
|
|
496
|
+
const got = kmeans(samples, kk, 24, seedBase + s * 2654435761)
|
|
497
|
+
if (best === null || got.inertia < best.inertia) best = got
|
|
498
|
+
}
|
|
499
|
+
for (let c = 0; c < best.cent.length; c += 1) cents.push(best.cent[c])
|
|
500
|
+
}
|
|
501
|
+
runK(darkIdx, kDark, 1)
|
|
502
|
+
runK(fillIdx, kFill, 100)
|
|
503
|
+
// 极端情况下簇数不足:用全体像素补满
|
|
504
|
+
if (cents.length < colors) {
|
|
505
|
+
const samples = new Float64Array(fgIndex.length * 3)
|
|
506
|
+
for (let i = 0; i < fgIndex.length; i += 1) {
|
|
507
|
+
const o = fgIndex[i] * 4
|
|
508
|
+
samples[i * 3] = work[o]
|
|
509
|
+
samples[i * 3 + 1] = work[o + 1]
|
|
510
|
+
samples[i * 3 + 2] = work[o + 2]
|
|
511
|
+
}
|
|
512
|
+
const need = colors - cents.length
|
|
513
|
+
const got = kmeans(samples, Math.min(colors, fgIndex.length), 24, 999 * 2654435761)
|
|
514
|
+
for (let c = 0; c < got.cent.length && cents.length < colors; c += 1) cents.push(got.cent[c])
|
|
515
|
+
void need
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
return {
|
|
519
|
+
cents: cents.slice(0, colors),
|
|
520
|
+
colorsDark: kDark,
|
|
521
|
+
colorsFill: kFill,
|
|
522
|
+
darkSamples: darkIdx.length,
|
|
523
|
+
fillSamples: fillIdx.length,
|
|
524
|
+
}
|
|
396
525
|
}
|
|
397
526
|
|
|
398
527
|
/* ────────────────────────── ⑤ 简化与编码 ────────────────────────── */
|
|
@@ -525,50 +654,249 @@ function encodeLoop(points, precision) {
|
|
|
525
654
|
|
|
526
655
|
/* ────────────────────────── 门面 ────────────────────────── */
|
|
527
656
|
|
|
657
|
+
/** 把 bgColor 规范成 [r,g,b](数组或 #rrggbb;认不出就白)。 */
|
|
658
|
+
function parseBgColor(raw) {
|
|
659
|
+
if (Array.isArray(raw) && raw.length >= 3) {
|
|
660
|
+
return [
|
|
661
|
+
clamp(Math.round(Number(raw[0])), 0, 255),
|
|
662
|
+
clamp(Math.round(Number(raw[1])), 0, 255),
|
|
663
|
+
clamp(Math.round(Number(raw[2])), 0, 255),
|
|
664
|
+
]
|
|
665
|
+
}
|
|
666
|
+
if (typeof raw === 'string') {
|
|
667
|
+
const m = /^#?([0-9a-fA-F]{6})$/.exec(raw.trim())
|
|
668
|
+
if (m) {
|
|
669
|
+
const n = parseInt(m[1], 16)
|
|
670
|
+
return [(n >> 16) & 255, (n >> 8) & 255, n & 255]
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
return [255, 255, 255]
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
/**
|
|
677
|
+
* 工作图尺寸:先按 scale 降,再用 grid(宽)/ gridH(高,0=不限)做上限;超限等比压。
|
|
678
|
+
* @returns {{width:number,height:number,scaleRequested:number,scaleApplied:number,grid:number,gridH:number,sizeNotes:string}}
|
|
679
|
+
*/
|
|
680
|
+
function resolveWorkSize(srcWidth, srcHeight, options) {
|
|
681
|
+
const scaleRequested = Number(options.scale)
|
|
682
|
+
const scaleRounded = Math.round(scaleRequested)
|
|
683
|
+
const scaleApplied = scaleRounded >= 1 ? scaleRounded : 1
|
|
684
|
+
const grid = Math.max(16, Math.round(Number(options.grid)))
|
|
685
|
+
const gridHRaw = Number(options.gridH)
|
|
686
|
+
const gridH = gridHRaw > 0 ? Math.max(16, Math.round(gridHRaw)) : 0
|
|
687
|
+
|
|
688
|
+
const notes = []
|
|
689
|
+
if (!(scaleRequested >= 1) || scaleRounded !== scaleApplied) {
|
|
690
|
+
notes.push('scale clamped to ' + scaleApplied + ' (requested ' + scaleRequested + ')')
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
let width = Math.max(1, Math.round(srcWidth / scaleApplied))
|
|
694
|
+
let height = Math.max(1, Math.round(srcHeight / scaleApplied))
|
|
695
|
+
if (scaleApplied > 1) notes.push('scaleApplied ' + scaleApplied)
|
|
696
|
+
|
|
697
|
+
const capH = gridH > 0 ? gridH : Infinity
|
|
698
|
+
if (width > grid || height > capH) {
|
|
699
|
+
const factor = Math.min(grid / width, capH / height)
|
|
700
|
+
const nextW = Math.max(16, Math.round(width * factor))
|
|
701
|
+
const nextH = Math.max(16, Math.round(height * factor))
|
|
702
|
+
const parts = []
|
|
703
|
+
if (width > grid) parts.push('width')
|
|
704
|
+
if (height > capH) parts.push('height')
|
|
705
|
+
notes.push('grid capped ' + parts.join('+') + ' (' + width + '×' + height + ' → ' + nextW + '×' + nextH + ')')
|
|
706
|
+
width = nextW
|
|
707
|
+
height = nextH
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
return {
|
|
711
|
+
width: width,
|
|
712
|
+
height: height,
|
|
713
|
+
scaleRequested: scaleRequested,
|
|
714
|
+
scaleApplied: scaleApplied,
|
|
715
|
+
grid: grid,
|
|
716
|
+
gridH: gridH,
|
|
717
|
+
sizeNotes: notes.join('; '),
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
|
|
528
721
|
/** 矢量化的默认参数(宿主工具与命令行共用)。 */
|
|
529
722
|
export const TRACE_DEFAULTS = {
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
723
|
+
preset: 'default', // 命名预设,见 TRACE_PRESETS;显式字段覆盖预设
|
|
724
|
+
colors: 12, // 前景调色板色数(背景另占 1 色)= colorsDark + colorsFill(auto)
|
|
725
|
+
colorsDark: 3, // 暗色配额(线稿);0 = 不分层,全体一次 k-means
|
|
726
|
+
colorsFill: 0, // 填充色配额;0 = 自动 colors - colorsDark
|
|
727
|
+
grid: 499, // 工作图宽度上限(px);与 gridH 一起做等比封顶
|
|
728
|
+
gridH: 0, // 工作图高度上限(px);0 = 不限高(只卡宽,兼容旧行为)
|
|
729
|
+
scale: 1, // 真降采样因子:work ≈ src/scale,再套 grid/gridH(=1 表示不按 scale 降)
|
|
533
730
|
epsilon: 2.0, // RDP 简化容差(像素)
|
|
534
731
|
precision: 1, // 坐标小数位
|
|
535
|
-
minArea: 10, //
|
|
732
|
+
minArea: 10, // despeckle:小于这个面积(像素²)的碎块并进邻色;0 = 关
|
|
733
|
+
minRegionArea: 0, // despeckle 之后再按面积并区域;0 = 关(与 minArea 分开调)
|
|
734
|
+
protectLuma: 110, // 亮度 ≤ 此值的前景像素受保护(不 despeckle / 不 ladder 丢环);0 = 关
|
|
735
|
+
majorityContrast: 40, // 局部亮度差 ≥ 此值则跳过众数滤波(保线稿);0 = 全图滤波(旧行为)
|
|
536
736
|
bgBright: 238, // 判"近白"的亮度下限
|
|
537
737
|
bgTol: 10, // 判"近灰"的通道差上限
|
|
738
|
+
bgColor: [255, 255, 255], // 透明/半透明像素合成底色(再进泛洪与 k-means)
|
|
538
739
|
seeds: 3, // k-means 重启次数(取 inertia 最小)
|
|
539
|
-
|
|
740
|
+
// SVG 上限(换算成 drawio style 里的 data URI 要塞得下)。宿主的硬上限是 250000 字符,
|
|
741
|
+
// 且它自己会再留 2000 的余量 —— 所以这里直接对齐到 248000:整图的精细度就由这个预算决定
|
|
742
|
+
// (预算越小,管线越要抬 epsilon、丢小环)。命令行想更省字符可显式传 --budget。
|
|
743
|
+
maxBytes: 248000,
|
|
540
744
|
includeLabels: false, // 额外返回"去碎块后"的标签图(调试/比对用;会额外占 width*height 字节的 base64)
|
|
541
745
|
}
|
|
542
746
|
|
|
747
|
+
/**
|
|
748
|
+
* 命名预设:AI 先选场景,再按需覆盖单个字段。
|
|
749
|
+
* 合并顺序:TRACE_DEFAULTS ← preset ← 显式字段。
|
|
750
|
+
*/
|
|
751
|
+
export const TRACE_PRESETS = {
|
|
752
|
+
default: {},
|
|
753
|
+
/** 插画 / 线稿:多留暗色、严保细线、轮廓更细 */
|
|
754
|
+
lineart: {
|
|
755
|
+
colorsDark: 4,
|
|
756
|
+
protectLuma: 130,
|
|
757
|
+
majorityContrast: 32,
|
|
758
|
+
minArea: 4,
|
|
759
|
+
epsilon: 1.5,
|
|
760
|
+
precision: 2,
|
|
761
|
+
},
|
|
762
|
+
/** 照片 / 渐变:少暗色配额、碎块并多一点、轮廓可粗 */
|
|
763
|
+
photo: {
|
|
764
|
+
colors: 14,
|
|
765
|
+
colorsDark: 1,
|
|
766
|
+
protectLuma: 80,
|
|
767
|
+
majorityContrast: 55,
|
|
768
|
+
minArea: 16,
|
|
769
|
+
epsilon: 2.5,
|
|
770
|
+
},
|
|
771
|
+
/** 图标 / 简单截图:少色 + 降采样,压字符数 */
|
|
772
|
+
simple: {
|
|
773
|
+
colors: 6,
|
|
774
|
+
colorsDark: 2,
|
|
775
|
+
scale: 2,
|
|
776
|
+
grid: 320,
|
|
777
|
+
epsilon: 3,
|
|
778
|
+
minArea: 20,
|
|
779
|
+
protectLuma: 100,
|
|
780
|
+
},
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
/** 合法预设名列表(给报错 / 提示用)。 */
|
|
784
|
+
export const TRACE_PRESET_NAMES = Object.keys(TRACE_PRESETS)
|
|
785
|
+
|
|
786
|
+
/**
|
|
787
|
+
* 合并默认值 + 预设 + 显式覆盖。未知 preset 抛错(让 AI 立刻改,而不是静默当 default)。
|
|
788
|
+
* @returns {{options:object, preset:string}}
|
|
789
|
+
*/
|
|
790
|
+
export function resolveTraceOptions(rawOptions) {
|
|
791
|
+
const raw = rawOptions === undefined || rawOptions === null ? {} : rawOptions
|
|
792
|
+
const presetName = typeof raw.preset === 'string' && raw.preset.length > 0 ? raw.preset : 'default'
|
|
793
|
+
if (TRACE_PRESETS[presetName] === undefined) {
|
|
794
|
+
throw new Error(
|
|
795
|
+
'未知 trace preset "' + presetName + '";可用:' + TRACE_PRESET_NAMES.join(', '),
|
|
796
|
+
)
|
|
797
|
+
}
|
|
798
|
+
const options = Object.assign({}, TRACE_DEFAULTS, TRACE_PRESETS[presetName], raw, { preset: presetName })
|
|
799
|
+
return { options: options, preset: presetName }
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
/**
|
|
803
|
+
* 根据 metrics 生成一句调参建议(给 AI 读,不是给用户的长文)。
|
|
804
|
+
*/
|
|
805
|
+
export function hintTraceTune(metrics) {
|
|
806
|
+
if (metrics === undefined || metrics === null) return ''
|
|
807
|
+
const hints = []
|
|
808
|
+
const budget = Number(metrics.budget) || TRACE_DEFAULTS.maxBytes
|
|
809
|
+
const chars = Number(metrics.svgChars) || 0
|
|
810
|
+
if (chars > budget * 0.9 || Number(metrics.attempts) > 1) {
|
|
811
|
+
hints.push('字符紧/触发预算阶梯 → 试 scale:2、加大 epsilon、减小 colors/grid,或 preset:"simple"')
|
|
812
|
+
}
|
|
813
|
+
if (Number(metrics.protectPixels) === 0 && metrics.preset !== 'photo') {
|
|
814
|
+
hints.push('无暗像素保护区 → 插画线稿可试 preset:"lineart" 或提高 protectLuma')
|
|
815
|
+
}
|
|
816
|
+
if (Number(metrics.colorsDark) === 0 && Number(metrics.darkSamples) === 0 && metrics.preset === 'lineart') {
|
|
817
|
+
hints.push('lineart 但没分到暗样本 → 提高 protectLuma 或检查原图对比度')
|
|
818
|
+
}
|
|
819
|
+
if (Number(metrics.majoritySkipped) === 0 && Number(metrics.majorityContrast) > 0) {
|
|
820
|
+
hints.push('众数未跳过任何像素 → 图对比偏低,可减小 majorityContrast 或改 preset')
|
|
821
|
+
}
|
|
822
|
+
if (chars < budget * 0.35 && Number(metrics.scaleApplied) >= 2) {
|
|
823
|
+
hints.push('字符很富余且已降采样 → 可 scale:1 或减小 epsilon 换保真')
|
|
824
|
+
}
|
|
825
|
+
if (hints.length === 0) {
|
|
826
|
+
hints.push('指标正常;线稿糊/断可试 preset:"lineart";要更省字符用 preset:"simple"')
|
|
827
|
+
}
|
|
828
|
+
return hints.join(';')
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
/**
|
|
832
|
+
* 压缩给 AI 的结构化调参回执(diagram_apply.trace[])。
|
|
833
|
+
*/
|
|
834
|
+
export function summarizeTraceForAi(metrics, extra) {
|
|
835
|
+
const base = {
|
|
836
|
+
preset: metrics.preset,
|
|
837
|
+
image: extra && extra.image !== undefined ? extra.image : undefined,
|
|
838
|
+
nodeId: extra && extra.nodeId !== undefined ? extra.nodeId : undefined,
|
|
839
|
+
// 只描了原图的一块时才有:那块在原图里的位置与尺寸(像素)——
|
|
840
|
+
// 模型据此知道"这次只覆盖了局部",以及该按原图坐标把它摆到哪。
|
|
841
|
+
crop: metrics.crop,
|
|
842
|
+
source: metrics.source,
|
|
843
|
+
work: metrics.work,
|
|
844
|
+
scaleApplied: metrics.scaleApplied,
|
|
845
|
+
sizeNotes: metrics.sizeNotes,
|
|
846
|
+
colors: metrics.colors,
|
|
847
|
+
colorsDark: metrics.colorsDark,
|
|
848
|
+
colorsFill: metrics.colorsFill,
|
|
849
|
+
protectPixels: metrics.protectPixels,
|
|
850
|
+
majoritySkipped: metrics.majoritySkipped,
|
|
851
|
+
paths: metrics.paths,
|
|
852
|
+
loopsKept: metrics.loopsKept,
|
|
853
|
+
loopsProtectedKept: metrics.loopsProtectedKept,
|
|
854
|
+
regionsProtected: metrics.regionsProtected,
|
|
855
|
+
svgChars: metrics.svgChars,
|
|
856
|
+
budget: metrics.budget,
|
|
857
|
+
attempts: metrics.attempts,
|
|
858
|
+
exactPixelCoverage: metrics.exactPixelCoverage,
|
|
859
|
+
options: metrics.optionsApplied,
|
|
860
|
+
hint: metrics.tuneHint,
|
|
861
|
+
}
|
|
862
|
+
return base
|
|
863
|
+
}
|
|
864
|
+
|
|
543
865
|
/**
|
|
544
866
|
* 位图 → SVG 标记。
|
|
545
867
|
*
|
|
546
868
|
* @param {{width:number,height:number,data:Buffer|Uint8Array}} raster RGBA8
|
|
547
|
-
* @param {object} [rawOptions] 见 TRACE_DEFAULTS
|
|
869
|
+
* @param {object} [rawOptions] 见 TRACE_DEFAULTS / TRACE_PRESETS(可用 preset)
|
|
548
870
|
* @returns {{svg:string, metrics:object}}
|
|
549
871
|
*/
|
|
550
872
|
export function traceRasterToSvg(raster, rawOptions) {
|
|
551
|
-
const
|
|
873
|
+
const resolved = resolveTraceOptions(rawOptions)
|
|
874
|
+
const options = resolved.options
|
|
875
|
+
const preset = resolved.preset
|
|
552
876
|
const colors = Math.max(2, Math.min(24, Math.round(options.colors)))
|
|
553
|
-
const
|
|
554
|
-
const
|
|
555
|
-
const minArea = Math.max(
|
|
877
|
+
const colorsDark = Math.max(0, Math.min(colors, Math.round(options.colorsDark)))
|
|
878
|
+
const colorsFillOpt = Math.max(0, Math.min(colors, Math.round(options.colorsFill)))
|
|
879
|
+
const minArea = Math.max(0, Math.round(options.minArea))
|
|
880
|
+
const minRegionArea = Math.max(0, Math.round(options.minRegionArea))
|
|
881
|
+
const protectLuma = Math.max(0, Number(options.protectLuma))
|
|
882
|
+
const majorityContrast = Math.max(0, Number(options.majorityContrast))
|
|
556
883
|
const seeds = Math.max(1, Math.round(options.seeds))
|
|
884
|
+
const bgColor = parseBgColor(options.bgColor)
|
|
557
885
|
|
|
558
886
|
const srcWidth = raster.width
|
|
559
887
|
const srcHeight = raster.height
|
|
560
888
|
const rgba = raster.data
|
|
561
889
|
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
width = grid
|
|
567
|
-
height = Math.max(16, Math.round((srcHeight * grid) / srcWidth))
|
|
568
|
-
}
|
|
890
|
+
const sized = resolveWorkSize(srcWidth, srcHeight, options)
|
|
891
|
+
const width = sized.width
|
|
892
|
+
const height = sized.height
|
|
893
|
+
const scale = sized.scaleApplied
|
|
569
894
|
|
|
570
|
-
// 降采样(面积平均)+
|
|
895
|
+
// 降采样(面积平均)+ 把每个源像素先合成到 bgColor:一次写完,后面只认不透明的 width×height 工作图
|
|
571
896
|
const work = new Uint8Array(width * height * 4)
|
|
897
|
+
const bgR = bgColor[0]
|
|
898
|
+
const bgG = bgColor[1]
|
|
899
|
+
const bgB = bgColor[2]
|
|
572
900
|
for (let y = 0; y < height; y += 1) {
|
|
573
901
|
const y0 = Math.floor((y * srcHeight) / height)
|
|
574
902
|
const y1 = Math.max(y0 + 1, Math.floor(((y + 1) * srcHeight) / height))
|
|
@@ -583,10 +911,14 @@ export function traceRasterToSvg(raster, rawOptions) {
|
|
|
583
911
|
for (let yy = y0; yy < y1; yy += 1) {
|
|
584
912
|
for (let xx = x0; xx < x1; xx += 1) {
|
|
585
913
|
const o = (yy * srcWidth + xx) * 4
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
914
|
+
const srcA = rgba[o + 3]
|
|
915
|
+
const af = srcA / 255
|
|
916
|
+
const inv = 1 - af
|
|
917
|
+
// RGB 先合成到 bgColor,避免半透明边缘带着脏 RGB;alpha 原样平均留给 nearWhite(a<16)
|
|
918
|
+
r += rgba[o] * af + bgR * inv
|
|
919
|
+
g += rgba[o + 1] * af + bgG * inv
|
|
920
|
+
b += rgba[o + 2] * af + bgB * inv
|
|
921
|
+
a += srcA
|
|
590
922
|
n += 1
|
|
591
923
|
}
|
|
592
924
|
}
|
|
@@ -622,31 +954,47 @@ export function traceRasterToSvg(raster, rawOptions) {
|
|
|
622
954
|
for (let i = 0; i < width * height; i += 1) if (isBg[i] === 0) fgIndex.push(i)
|
|
623
955
|
if (fgIndex.length === 0) throw new Error('这张图整幅都是背景色(近白/近灰),没有可画的像素')
|
|
624
956
|
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
957
|
+
// 保护区:暗前景像素(线稿)—— despeckle / ladder 都不动它们
|
|
958
|
+
const protectMask = new Uint8Array(width * height)
|
|
959
|
+
let protectPixels = 0
|
|
960
|
+
if (protectLuma > 0) {
|
|
628
961
|
for (let i = 0; i < fgIndex.length; i += 1) {
|
|
629
|
-
const
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
const got = kmeans(samples, colors, 24, s * 2654435761)
|
|
636
|
-
if (best === null || got.inertia < best.inertia) best = got
|
|
962
|
+
const p = fgIndex[i]
|
|
963
|
+
const o = p * 4
|
|
964
|
+
if (lumaRgb(work[o], work[o + 1], work[o + 2]) <= protectLuma) {
|
|
965
|
+
protectMask[p] = 1
|
|
966
|
+
protectPixels += 1
|
|
967
|
+
}
|
|
637
968
|
}
|
|
638
969
|
}
|
|
970
|
+
|
|
971
|
+
const built = buildPalette(work, fgIndex, colors, colorsDark, colorsFillOpt, protectLuma, seeds)
|
|
639
972
|
const palette = [bgMean]
|
|
640
|
-
for (let c = 0; c <
|
|
641
|
-
palette.push([
|
|
973
|
+
for (let c = 0; c < built.cents.length; c += 1) {
|
|
974
|
+
palette.push([
|
|
975
|
+
clamp(Math.round(built.cents[c][0]), 0, 255),
|
|
976
|
+
clamp(Math.round(built.cents[c][1]), 0, 255),
|
|
977
|
+
clamp(Math.round(built.cents[c][2]), 0, 255),
|
|
978
|
+
])
|
|
642
979
|
}
|
|
643
980
|
const colorCount = palette.length
|
|
981
|
+
const protectLabels = new Uint8Array(colorCount)
|
|
982
|
+
if (protectLuma > 0) {
|
|
983
|
+
for (let c = 1; c < colorCount; c += 1) {
|
|
984
|
+
const p = palette[c]
|
|
985
|
+
if (lumaRgb(p[0], p[1], p[2]) <= protectLuma) protectLabels[c] = 1
|
|
986
|
+
}
|
|
987
|
+
}
|
|
644
988
|
|
|
645
|
-
// ③ 超采样归类(双线性)→
|
|
989
|
+
// ③ 超采样归类(双线性)→ 条件众数滤波 → 降回原分辨率
|
|
646
990
|
const ss = 3
|
|
647
991
|
const sw = width * ss
|
|
648
992
|
const sh = height * ss
|
|
649
993
|
const gridLabels = new Uint8Array(sw * sh)
|
|
994
|
+
const paletteLuma = new Float64Array(colorCount)
|
|
995
|
+
for (let c = 0; c < colorCount; c += 1) {
|
|
996
|
+
paletteLuma[c] = lumaRgb(palette[c][0], palette[c][1], palette[c][2])
|
|
997
|
+
}
|
|
650
998
|
const px = [0, 0, 0]
|
|
651
999
|
for (let y = 0; y < sh; y += 1) {
|
|
652
1000
|
const fy = (y + 0.5) / ss - 0.5
|
|
@@ -687,8 +1035,32 @@ export function traceRasterToSvg(raster, rawOptions) {
|
|
|
687
1035
|
|
|
688
1036
|
const scratch = new Uint8Array(gridLabels.length)
|
|
689
1037
|
const hist = new Int32Array(colorCount + 1)
|
|
1038
|
+
let majoritySkipped = 0
|
|
1039
|
+
let majorityApplied = 0
|
|
690
1040
|
for (let y = 0; y < sh; y += 1) {
|
|
691
1041
|
for (let x = 0; x < sw; x += 1) {
|
|
1042
|
+
const self = gridLabels[y * sw + x]
|
|
1043
|
+
// 局部对比高(线稿边缘)→ 跳过众数,避免细线被邻色吃掉
|
|
1044
|
+
if (majorityContrast > 0) {
|
|
1045
|
+
let minL = paletteLuma[self]
|
|
1046
|
+
let maxL = minL
|
|
1047
|
+
for (let dy = -1; dy <= 1; dy += 1) {
|
|
1048
|
+
const yy = y + dy
|
|
1049
|
+
if (yy < 0 || yy >= sh) continue
|
|
1050
|
+
for (let dx = -1; dx <= 1; dx += 1) {
|
|
1051
|
+
const xx = x + dx
|
|
1052
|
+
if (xx < 0 || xx >= sw) continue
|
|
1053
|
+
const L = paletteLuma[gridLabels[yy * sw + xx]]
|
|
1054
|
+
if (L < minL) minL = L
|
|
1055
|
+
if (L > maxL) maxL = L
|
|
1056
|
+
}
|
|
1057
|
+
}
|
|
1058
|
+
if (maxL - minL >= majorityContrast) {
|
|
1059
|
+
scratch[y * sw + x] = self
|
|
1060
|
+
majoritySkipped += 1
|
|
1061
|
+
continue
|
|
1062
|
+
}
|
|
1063
|
+
}
|
|
692
1064
|
hist.fill(0)
|
|
693
1065
|
for (let dy = -1; dy <= 1; dy += 1) {
|
|
694
1066
|
const yy = y + dy
|
|
@@ -699,7 +1071,7 @@ export function traceRasterToSvg(raster, rawOptions) {
|
|
|
699
1071
|
hist[gridLabels[yy * sw + xx]] += 1
|
|
700
1072
|
}
|
|
701
1073
|
}
|
|
702
|
-
let bi =
|
|
1074
|
+
let bi = self
|
|
703
1075
|
let bc = 0
|
|
704
1076
|
for (let c = 0; c < colorCount; c += 1) {
|
|
705
1077
|
if (hist[c] > bc) {
|
|
@@ -708,6 +1080,7 @@ export function traceRasterToSvg(raster, rawOptions) {
|
|
|
708
1080
|
}
|
|
709
1081
|
}
|
|
710
1082
|
scratch[y * sw + x] = bi
|
|
1083
|
+
majorityApplied += 1
|
|
711
1084
|
}
|
|
712
1085
|
}
|
|
713
1086
|
gridLabels.set(scratch)
|
|
@@ -732,7 +1105,40 @@ export function traceRasterToSvg(raster, rawOptions) {
|
|
|
732
1105
|
}
|
|
733
1106
|
}
|
|
734
1107
|
|
|
735
|
-
|
|
1108
|
+
// 背景色(label 0)只给泛洪底色用,且最终不画 path。
|
|
1109
|
+
// 白衣白底时,角色内部的白也会被最近邻归到 palette[0] —— 若不改回前景色,
|
|
1110
|
+
// 跳过绘制 label 0 就会把裙子/头发抠成透明洞。
|
|
1111
|
+
let fgRelabeledFromBg = 0
|
|
1112
|
+
let bgForced = 0
|
|
1113
|
+
for (let i = 0; i < width * height; i += 1) {
|
|
1114
|
+
if (isBg[i] === 1) {
|
|
1115
|
+
if (labels[i] !== 0) bgForced += 1
|
|
1116
|
+
labels[i] = 0
|
|
1117
|
+
continue
|
|
1118
|
+
}
|
|
1119
|
+
if (labels[i] !== 0) continue
|
|
1120
|
+
const o = i * 4
|
|
1121
|
+
let bi = 1
|
|
1122
|
+
let bd = Infinity
|
|
1123
|
+
for (let c = 1; c < colorCount; c += 1) {
|
|
1124
|
+
const p = palette[c]
|
|
1125
|
+
const dr = work[o] - p[0]
|
|
1126
|
+
const dg = work[o + 1] - p[1]
|
|
1127
|
+
const db = work[o + 2] - p[2]
|
|
1128
|
+
const d = dr * dr + dg * dg + db * db
|
|
1129
|
+
if (d < bd) {
|
|
1130
|
+
bd = d
|
|
1131
|
+
bi = c
|
|
1132
|
+
}
|
|
1133
|
+
}
|
|
1134
|
+
labels[i] = bi
|
|
1135
|
+
fgRelabeledFromBg += 1
|
|
1136
|
+
}
|
|
1137
|
+
|
|
1138
|
+
const maskOrNull = protectPixels > 0 ? protectMask : null
|
|
1139
|
+
const labelsOrNull = protectLuma > 0 ? protectLabels : null
|
|
1140
|
+
const speckle = despeckle(labels, width, height, palette, minArea, maskOrNull, labelsOrNull)
|
|
1141
|
+
const regionMerge = despeckle(labels, width, height, palette, minRegionArea, maskOrNull, labelsOrNull)
|
|
736
1142
|
const labelDump = options.includeLabels === true
|
|
737
1143
|
? {
|
|
738
1144
|
width: width, height: height, colors: colorCount,
|
|
@@ -741,21 +1147,24 @@ export function traceRasterToSvg(raster, rawOptions) {
|
|
|
741
1147
|
}
|
|
742
1148
|
: null
|
|
743
1149
|
|
|
744
|
-
// ④ 连通域 +
|
|
1150
|
+
// ④ 连通域 + 轮廓(记下区域是否受保护,供 ladder 保留细环)
|
|
745
1151
|
const comp = new Int32Array(width * height).fill(-1)
|
|
746
1152
|
const regionLabel = []
|
|
747
1153
|
const regionArea = []
|
|
1154
|
+
const regionProtected = []
|
|
748
1155
|
const stack = []
|
|
749
1156
|
for (let i = 0; i < width * height; i += 1) {
|
|
750
1157
|
if (comp[i] !== -1) continue
|
|
751
1158
|
const label = labels[i]
|
|
752
1159
|
const id = regionLabel.length
|
|
753
1160
|
let area = 0
|
|
1161
|
+
let prot = protectLabels[label] === 1 ? 1 : 0
|
|
754
1162
|
comp[i] = id
|
|
755
1163
|
stack.push(i)
|
|
756
1164
|
while (stack.length > 0) {
|
|
757
1165
|
const p = stack.pop()
|
|
758
1166
|
area += 1
|
|
1167
|
+
if (protectMask[p] === 1) prot = 1
|
|
759
1168
|
const x = p % width
|
|
760
1169
|
const y = (p - x) / width
|
|
761
1170
|
for (let dy = -1; dy <= 1; dy += 1) {
|
|
@@ -774,19 +1183,26 @@ export function traceRasterToSvg(raster, rawOptions) {
|
|
|
774
1183
|
}
|
|
775
1184
|
regionLabel.push(label)
|
|
776
1185
|
regionArea.push(area)
|
|
1186
|
+
regionProtected.push(prot)
|
|
777
1187
|
}
|
|
778
1188
|
|
|
779
1189
|
const loopsByRegion = traceLoops(comp, width, height, regionLabel.length)
|
|
780
1190
|
|
|
781
|
-
// ⑤ 每色一条 path:外环(面积<0,顺时针)+ 洞(>0
|
|
1191
|
+
// ⑤ 每色一条 path:外环(面积<0,顺时针)+ 洞(>0,反向);环带 protected 标记
|
|
782
1192
|
const byColor = []
|
|
783
1193
|
for (let c = 0; c < colorCount; c += 1) byColor.push({ outer: [], hole: [], area: 0 })
|
|
784
1194
|
loopsByRegion.forEach(function (loops, id) {
|
|
785
1195
|
const label = regionLabel[id]
|
|
786
1196
|
const bucket = byColor[label]
|
|
1197
|
+
const protectedLoop = regionProtected[id] === 1
|
|
787
1198
|
for (let i = 0; i < loops.length; i += 1) {
|
|
788
|
-
|
|
789
|
-
|
|
1199
|
+
const tagged = {
|
|
1200
|
+
points: loops[i].points,
|
|
1201
|
+
area: loops[i].area,
|
|
1202
|
+
protected: protectedLoop,
|
|
1203
|
+
}
|
|
1204
|
+
if (loops[i].area < 0) bucket.outer.push(tagged)
|
|
1205
|
+
else bucket.hole.push(tagged)
|
|
790
1206
|
}
|
|
791
1207
|
bucket.area += regionArea[id]
|
|
792
1208
|
})
|
|
@@ -806,8 +1222,9 @@ export function traceRasterToSvg(raster, rawOptions) {
|
|
|
806
1222
|
let attempts = 0
|
|
807
1223
|
let pathCount = 0
|
|
808
1224
|
let loopCount = 0
|
|
1225
|
+
let loopsProtectedKept = 0
|
|
809
1226
|
let svg = ''
|
|
810
|
-
// 超预算就逐步收紧:先抬 RDP
|
|
1227
|
+
// 超预算就逐步收紧:先抬 RDP 容差,再丢小洞(受保护环永不因 minLoopArea 丢弃)
|
|
811
1228
|
const ladder = [
|
|
812
1229
|
{ epsilon: options.epsilon, minLoopArea: 4 },
|
|
813
1230
|
{ epsilon: Math.max(options.epsilon, 2.5), minLoopArea: 4 },
|
|
@@ -821,19 +1238,25 @@ export function traceRasterToSvg(raster, rawOptions) {
|
|
|
821
1238
|
let body = ''
|
|
822
1239
|
pathCount = 0
|
|
823
1240
|
loopCount = 0
|
|
1241
|
+
loopsProtectedKept = 0
|
|
824
1242
|
for (let k = 0; k < order.length; k += 1) {
|
|
1243
|
+
// label 0 = 泛洪标出的截图底色:不画(SVG 该处透明)。
|
|
1244
|
+
// 若画成 path,角色左右两块互不连通的底色会进同一条 nonzero path,
|
|
1245
|
+
// 绕向抵消时会出现"一侧透明、一侧实心白"的假背景(插画发丝顶天立地时必现)。
|
|
1246
|
+
if (order[k] === 0) continue
|
|
825
1247
|
const bucket = byColor[order[k]]
|
|
826
1248
|
let d = ''
|
|
827
1249
|
const all = bucket.outer.concat(bucket.hole)
|
|
828
1250
|
for (let i = 0; i < all.length; i += 1) {
|
|
829
1251
|
const loop = all[i]
|
|
830
|
-
if (Math.abs(loop.area) < plan.minLoopArea) continue
|
|
1252
|
+
if (!loop.protected && Math.abs(loop.area) < plan.minLoopArea) continue
|
|
831
1253
|
// 洞反向 → nonzero 里挖空
|
|
832
1254
|
const points = loop.area > 0 ? loop.points.slice().reverse() : loop.points
|
|
833
1255
|
const seg = encodeLoop(simplifyClosed(points, plan.epsilon), options.precision)
|
|
834
1256
|
if (seg === null) continue
|
|
835
1257
|
d += seg
|
|
836
1258
|
loopCount += 1
|
|
1259
|
+
if (loop.protected) loopsProtectedKept += 1
|
|
837
1260
|
}
|
|
838
1261
|
if (d === '') continue
|
|
839
1262
|
body += "<path fill='" + hex(palette[order[k]]) + "' fill-rule='nonzero' d='" + d + "'/>"
|
|
@@ -854,33 +1277,74 @@ export function traceRasterToSvg(raster, rawOptions) {
|
|
|
854
1277
|
let paletteArea = 0
|
|
855
1278
|
for (let c = 0; c < colorCount; c += 1) paletteArea += byColor[c].area
|
|
856
1279
|
|
|
1280
|
+
const optionsApplied = {
|
|
1281
|
+
preset: preset,
|
|
1282
|
+
colors: colors,
|
|
1283
|
+
colorsDark: colorsDark,
|
|
1284
|
+
colorsFill: colorsFillOpt,
|
|
1285
|
+
grid: sized.grid,
|
|
1286
|
+
gridH: sized.gridH,
|
|
1287
|
+
scale: scale,
|
|
1288
|
+
epsilon: options.epsilon,
|
|
1289
|
+
precision: options.precision,
|
|
1290
|
+
minArea: minArea,
|
|
1291
|
+
minRegionArea: minRegionArea,
|
|
1292
|
+
protectLuma: protectLuma,
|
|
1293
|
+
majorityContrast: majorityContrast,
|
|
1294
|
+
}
|
|
1295
|
+
|
|
1296
|
+
const metrics = {
|
|
1297
|
+
preset: preset,
|
|
1298
|
+
source: { width: srcWidth, height: srcHeight },
|
|
1299
|
+
work: { width: width, height: height, scale: scale },
|
|
1300
|
+
scaleRequested: sized.scaleRequested,
|
|
1301
|
+
scaleApplied: sized.scaleApplied,
|
|
1302
|
+
grid: sized.grid,
|
|
1303
|
+
gridH: sized.gridH,
|
|
1304
|
+
bgColor: bgColor.slice(),
|
|
1305
|
+
sizeNotes: sized.sizeNotes,
|
|
1306
|
+
colors: colorCount - 1,
|
|
1307
|
+
colorsDark: built.colorsDark,
|
|
1308
|
+
colorsFill: built.colorsFill,
|
|
1309
|
+
darkSamples: built.darkSamples,
|
|
1310
|
+
fillSamples: built.fillSamples,
|
|
1311
|
+
protectLuma: protectLuma,
|
|
1312
|
+
protectPixels: protectPixels,
|
|
1313
|
+
fgRelabeledFromBg: fgRelabeledFromBg,
|
|
1314
|
+
bgForced: bgForced,
|
|
1315
|
+
majorityContrast: majorityContrast,
|
|
1316
|
+
majoritySkipped: majoritySkipped,
|
|
1317
|
+
majorityApplied: majorityApplied,
|
|
1318
|
+
palette: palette.map(hex),
|
|
1319
|
+
backgroundPixels: (function () {
|
|
1320
|
+
let n = 0
|
|
1321
|
+
for (let i = 0; i < width * height; i += 1) if (isBg[i] === 1) n += 1
|
|
1322
|
+
return n
|
|
1323
|
+
})(),
|
|
1324
|
+
foregroundPixels: fgIndex.length,
|
|
1325
|
+
despeckle: speckle,
|
|
1326
|
+
regionMerge: regionMerge,
|
|
1327
|
+
regions: regionLabel.length,
|
|
1328
|
+
regionsProtected: regionProtected.reduce(function (n, v) { return n + v }, 0),
|
|
1329
|
+
loops: loopsTotal.value,
|
|
1330
|
+
paths: pathCount,
|
|
1331
|
+
loopsKept: loopCount,
|
|
1332
|
+
loopsProtectedKept: loopsProtectedKept,
|
|
1333
|
+
drawnPixels: covered,
|
|
1334
|
+
totalPixels: width * height,
|
|
1335
|
+
exactPixelCoverage: covered === width * height,
|
|
1336
|
+
svgChars: svg.length,
|
|
1337
|
+
budget: options.maxBytes,
|
|
1338
|
+
attempts: attempts,
|
|
1339
|
+
paletteAreaSum: paletteArea,
|
|
1340
|
+
optionsApplied: optionsApplied,
|
|
1341
|
+
}
|
|
1342
|
+
metrics.tuneHint = hintTraceTune(metrics)
|
|
1343
|
+
|
|
857
1344
|
return {
|
|
858
1345
|
svg: svg,
|
|
859
1346
|
labels: labelDump,
|
|
860
|
-
metrics:
|
|
861
|
-
source: { width: srcWidth, height: srcHeight },
|
|
862
|
-
work: { width: width, height: height, scale: scale },
|
|
863
|
-
colors: colorCount - 1,
|
|
864
|
-
palette: palette.map(hex),
|
|
865
|
-
backgroundPixels: (function () {
|
|
866
|
-
let n = 0
|
|
867
|
-
for (let i = 0; i < width * height; i += 1) if (isBg[i] === 1) n += 1
|
|
868
|
-
return n
|
|
869
|
-
})(),
|
|
870
|
-
foregroundPixels: fgIndex.length,
|
|
871
|
-
despeckle: speckle,
|
|
872
|
-
regions: regionLabel.length,
|
|
873
|
-
loops: loopsTotal.value,
|
|
874
|
-
paths: pathCount,
|
|
875
|
-
loopsKept: loopCount,
|
|
876
|
-
drawnPixels: covered,
|
|
877
|
-
totalPixels: width * height,
|
|
878
|
-
exactPixelCoverage: covered === width * height,
|
|
879
|
-
svgChars: svg.length,
|
|
880
|
-
budget: options.maxBytes,
|
|
881
|
-
attempts: attempts,
|
|
882
|
-
paletteAreaSum: paletteArea,
|
|
883
|
-
},
|
|
1347
|
+
metrics: metrics,
|
|
884
1348
|
}
|
|
885
1349
|
}
|
|
886
1350
|
|