Preserve broad gradients without edge artifacts
This commit is contained in:
+26
-11
@@ -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,
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user