Preserve broad gradients without edge artifacts

This commit is contained in:
wuyanwanwu
2026-08-14 01:41:38 +08:00
parent 1de77c904e
commit 02a17f5283
12 changed files with 40 additions and 25 deletions
+26 -11
View File
@@ -254,10 +254,14 @@ function chooseDistinctMardColors(clusters: ColorCluster[], maximum: number) {
}
const separated: BeadColor[] = [];
const totalPixels = clusters.reduce((total, cluster) => total + cluster.count, 0);
const minimumMardDistance = 0.035;
const broadGradientDistance = 0.02;
const broadColorThreshold = Math.max(12, totalPixels * 0.008);
const ranked = [...candidates.values()].sort((a, b) => b.score - a.score || a.error / a.count - b.error / b.count);
for (const candidate of ranked) {
if (separated.every((selected) => oklabDistance(candidate.color.lab, selected.lab) >= minimumMardDistance)) {
const requiredDistance = candidate.count >= broadColorThreshold ? broadGradientDistance : minimumMardDistance;
if (separated.every((selected) => oklabDistance(candidate.color.lab, selected.lab) >= requiredDistance)) {
separated.push(candidate.color);
}
}
@@ -273,24 +277,22 @@ function sampleDominantRegions(
cropX: number,
cropY: number,
) {
// Keep the previously stable center-weighted 3x3 sampler. The canvas
// interpolation is important for broad gradients (sky, shadows, skin tones),
// while the later transition-band cleanup handles one-cell anti-aliased
// colors at hard object boundaries.
// A mildly center-weighted 3x3 vote keeps hard object boundaries clean.
// Broad gradients are preserved later when the MARD palette is selected,
// rather than by blending pixels across object edges here.
const scale = 3;
const sampleWeights = [
[1, 1, 1],
[1, 2, 1],
[2, 4, 2],
[1, 2, 1],
[1, 1, 1],
];
const totalSampleWeight = 16;
const totalSampleWeight = 10;
const sample = document.createElement("canvas");
sample.width = width * scale;
sample.height = height * scale;
const ctx = sample.getContext("2d", { willReadFrequently: true })!;
ctx.clearRect(0, 0, sample.width, sample.height);
ctx.imageSmoothingEnabled = true;
ctx.imageSmoothingQuality = "high";
ctx.imageSmoothingEnabled = false;
drawFittedImage(ctx, image, sample.width, sample.height, fitMode, cropZoom, cropX, cropY);
const source = ctx.getImageData(0, 0, sample.width, sample.height).data;
const result = new Uint8ClampedArray(width * height * 4);
@@ -339,7 +341,20 @@ function sampleDominantRegions(
confidence[row * width + column] = transparentWeight / totalSampleWeight;
continue;
}
const dominant = groups.sort((a, b) => b.weight - a.weight || Number(b.containsCenter) - Number(a.containsCenter))[0];
const rankedGroups = groups.map((group) => {
let effectiveWeight = group.weight;
if (group.count <= 2) {
for (let first = 0; first < groups.length; first++) {
for (let second = first + 1; second < groups.length; second++) {
if (groups[first] === group || groups[second] === group) continue;
if (groups[first].weight + groups[second].weight < group.weight) continue;
if (isPerceptualBridge(group.lab, groups[first].lab, groups[second].lab)) effectiveWeight = 0;
}
}
}
return { group, effectiveWeight };
});
const dominant = rankedGroups.sort((a, b) => b.effectiveWeight - a.effectiveWeight || b.group.weight - a.group.weight || Number(b.group.containsCenter) - Number(a.group.containsCenter))[0].group;
const representative = dominant.samples.reduce((best, current) =>
oklabDistance(current.lab, dominant.lab) < oklabDistance(best.lab, dominant.lab) ? current : best,
);