Use center weighted sampling and fix pattern border

This commit is contained in:
wuyanwanwu
2026-08-14 01:18:23 +08:00
parent a5d4850728
commit 1bd885f5b0
15 changed files with 40 additions and 32 deletions
+1 -1
View File
@@ -79,7 +79,7 @@ input[type="range"] { accent-color: var(--green); }
.canvas-viewport { position: relative; width: 100%; aspect-ratio: 1 / 1; height: auto; min-height: 0; flex: 0 0 auto; align-self: stretch; contain: size layout; overflow: hidden; }
.canvas-stage { position: absolute; inset: 0; width: auto; height: auto; overflow: scroll; display: block; padding: 12px; cursor: grab; touch-action: none; overscroll-behavior: contain; user-select: none; scrollbar-gutter: stable; }
.canvas-stage.is-dragging { cursor: grabbing; }
.canvas-wrap { width: max-content; line-height: 0; position: relative; margin: auto; border: 1px solid var(--ink); box-shadow: 10px 10px 0 rgba(32,39,36,.13); background: white; }
.canvas-wrap { width: max-content; line-height: 0; position: relative; margin: auto; border: 1px solid var(--ink); background: white; }
.canvas-wrap canvas { cursor: inherit; image-rendering: pixelated; }
.pixel-tooltip { position: absolute; z-index: 5; width: max-content; max-width: 260px; padding: 9px 11px; display: grid; grid-template-columns: 14px auto; align-items: center; gap: 3px 8px; color: white; background: rgba(22, 30, 27, .94); border: 1px solid rgba(255,255,255,.2); box-shadow: 4px 4px 0 rgba(0,0,0,.2); border-radius: 3px; line-height: 1.35; pointer-events: none; }
.tooltip-swatch { width: 14px; height: 14px; grid-row: 1 / 3; border-radius: 50%; border: 1px solid rgba(255,255,255,.6); }
+22 -14
View File
@@ -253,9 +253,15 @@ function sampleDominantRegions(
cropX: number,
cropY: number,
) {
// An odd sampling grid has a real center point and avoids directional ties
// at object boundaries (the former 4x4 grid favored one side on exact ties).
const scale = 5;
// Center-weighted 3x3 sampling preserves geometric corners better than a
// flat vote: center=4, orthogonal neighbors=2, diagonal neighbors=1.
const scale = 3;
const sampleWeights = [
[1, 2, 1],
[2, 4, 2],
[1, 2, 1],
];
const totalSampleWeight = 16;
const sample = document.createElement("canvas");
sample.width = width * scale;
sample.height = height * scale;
@@ -269,13 +275,14 @@ function sampleDominantRegions(
for (let row = 0; row < height; row++) {
for (let column = 0; column < width; column++) {
const groups: Array<{ count: number; lab: Oklab; samples: Array<{ rgb: [number, number, number]; lab: Oklab }>; containsCenter: boolean }> = [];
let transparentCount = 0;
const groups: Array<{ count: number; weight: number; lab: Oklab; samples: Array<{ rgb: [number, number, number]; lab: Oklab }>; containsCenter: boolean }> = [];
let transparentWeight = 0;
for (let offsetY = 0; offsetY < scale; offsetY++) {
for (let offsetX = 0; offsetX < scale; offsetX++) {
const sourceIndex = ((row * scale + offsetY) * sample.width + column * scale + offsetX) * 4;
const sampleWeight = sampleWeights[offsetY][offsetX];
if (source[sourceIndex + 3] < TRANSPARENT_ALPHA_THRESHOLD) {
transparentCount += 1;
transparentWeight += sampleWeight;
continue;
}
const rgb: [number, number, number] = [source[sourceIndex], source[sourceIndex + 1], source[sourceIndex + 2]];
@@ -291,23 +298,24 @@ function sampleDominantRegions(
}
}
if (closest && closestDistance <= localMergeThreshold) {
const total = closest.count + 1;
closest.lab = closest.lab.map((value, channel) => (value * closest.count + lab[channel]) / total) as Oklab;
closest.count = total;
const totalWeight = closest.weight + sampleWeight;
closest.lab = closest.lab.map((value, channel) => (value * closest.weight + lab[channel] * sampleWeight) / totalWeight) as Oklab;
closest.weight = totalWeight;
closest.count += 1;
closest.samples.push({ rgb, lab });
closest.containsCenter ||= isCenter;
} else {
groups.push({ count: 1, lab: [...lab], samples: [{ rgb, lab }], containsCenter: isCenter });
groups.push({ count: 1, weight: sampleWeight, lab: [...lab], samples: [{ rgb, lab }], containsCenter: isCenter });
}
}
}
const targetIndex = (row * width + column) * 4;
if (transparentCount >= scale * scale / 2 || groups.length === 0) {
if (transparentWeight >= totalSampleWeight / 2 || groups.length === 0) {
result[targetIndex + 3] = 0;
confidence[row * width + column] = transparentCount / (scale * scale);
confidence[row * width + column] = transparentWeight / totalSampleWeight;
continue;
}
const dominant = groups.sort((a, b) => b.count - a.count || Number(b.containsCenter) - Number(a.containsCenter))[0];
const dominant = groups.sort((a, b) => b.weight - a.weight || Number(b.containsCenter) - Number(a.containsCenter))[0];
const representative = dominant.samples.reduce((best, current) =>
oklabDistance(current.lab, dominant.lab) < oklabDistance(best.lab, dominant.lab) ? current : best,
);
@@ -315,7 +323,7 @@ function sampleDominantRegions(
result[targetIndex + 1] = representative.rgb[1];
result[targetIndex + 2] = representative.rgb[2];
result[targetIndex + 3] = 255;
confidence[row * width + column] = dominant.count / Math.max(1, scale * scale - transparentCount);
confidence[row * width + column] = dominant.weight / Math.max(1, totalSampleWeight - transparentWeight);
}
}
return { data: result, confidence };