Clean low-confidence isolated bead colors

This commit is contained in:
wuyanwanwu
2026-08-14 00:38:13 +08:00
parent 1a02e27740
commit b81613e598
12 changed files with 76 additions and 19 deletions
+62 -5
View File
@@ -209,6 +209,7 @@ function sampleDominantRegions(
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);
const confidence = new Float32Array(width * height);
const localMergeThreshold = 0.04;
for (let row = 0; row < height; row++) {
@@ -247,9 +248,54 @@ function sampleDominantRegions(
result[targetIndex + 1] = representative.rgb[1];
result[targetIndex + 2] = representative.rgb[2];
result[targetIndex + 3] = 255;
confidence[row * width + column] = dominant.count / (scale * scale);
}
}
return result;
return { data: result, confidence };
}
function cleanLowConfidenceIsolatedColors(
colors: BeadColor[],
confidence: Float32Array,
width: number,
height: number,
) {
const cleaned = [...colors];
let changed = 0;
const offsets = [-1, 0, 1];
for (let row = 0; row < height; row++) {
for (let column = 0; column < width; column++) {
const index = row * width + column;
const current = colors[index];
if (confidence[index] >= 0.625) continue;
const chroma = Math.hypot(current.lab[1], current.lab[2]);
const protectedDetail = current.lab[0] < 0.32 || current.lab[0] > 0.96 || chroma > 0.14;
if (protectedDetail) continue;
const neighborCounts = new Map<string, { color: BeadColor; count: number }>();
let neighborTotal = 0;
for (const rowOffset of offsets) {
for (const columnOffset of offsets) {
if (rowOffset === 0 && columnOffset === 0) continue;
const neighborRow = row + rowOffset;
const neighborColumn = column + columnOffset;
if (neighborRow < 0 || neighborColumn < 0 || neighborRow >= height || neighborColumn >= width) continue;
const neighbor = colors[neighborRow * width + neighborColumn];
const entry = neighborCounts.get(neighbor.code) ?? { color: neighbor, count: 0 };
entry.count += 1;
neighborCounts.set(neighbor.code, entry);
neighborTotal += 1;
}
}
const majority = [...neighborCounts.values()].sort((a, b) => b.count - a.count)[0];
if (!majority || majority.color.code === current.code) continue;
const requiredCount = neighborTotal >= 7 ? 5 : Math.max(2, Math.ceil(neighborTotal * 0.67));
if (majority.count < requiredCount) continue;
cleaned[index] = majority.color;
changed += 1;
}
}
return { colors: cleaned, changed };
}
function drawFittedImage(
@@ -367,8 +413,11 @@ export default function Home() {
const width = Math.max(8, Math.min(256, requestedWidth));
const height = Math.max(8, Math.min(256, requestedHeight));
let data: Uint8ClampedArray;
let dominantConfidence: Float32Array | null = null;
if (samplingStrategy === "dominant") {
data = sampleDominantRegions(image, width, height, fitMode, crop.zoom, crop.x, crop.y);
const sampled = sampleDominantRegions(image, width, height, fitMode, crop.zoom, crop.x, crop.y);
data = sampled.data;
dominantConfidence = sampled.confidence;
} else {
const sample = document.createElement("canvas");
sample.width = width;
@@ -382,15 +431,23 @@ export default function Home() {
const { pixelKeys, clusters, binClusters } = mergePerceptualColors(data);
const limitedPalette = chooseDistinctMardColors(clusters, Math.max(2, Math.min(colorLimit, PALETTE.length)));
const clusterMatches = clusters.map((cluster) => nearestColor(cluster.lab, limitedPalette));
const converted = pixelKeys.map((key) => ({ color: clusterMatches[binClusters.get(key) ?? 0] }));
let matchedColors = pixelKeys.map((key) => clusterMatches[binClusters.get(key) ?? 0]);
let cleanedCount = 0;
if (dominantConfidence) {
const cleaned = cleanLowConfidenceIsolatedColors(matchedColors, dominantConfidence, width, height);
matchedColors = cleaned.colors;
cleanedCount = cleaned.changed;
}
const converted = matchedColors.map((color) => ({ color }));
setGridWidth(width);
setGridHeight(height);
setRequestedWidth(width);
setRequestedHeight(height);
setPixels(converted);
setSelectedCodes(new Set());
const strategyName = samplingStrategy === "dominant" ? "区域主色" : "平滑取色";
setStatus(`已转换为 ${width} × ${height} · ${strategyName} · ${limitedPalette.length} 种差异色`);
const strategyName = samplingStrategy === "dominant" ? `区域主色 · 清理 ${cleanedCount} 个低可信孤立格` : "平滑取色";
const actualColorCount = new Set(matchedColors.map((color) => color.code)).size;
setStatus(`已转换为 ${width} × ${height} · ${strategyName} · ${actualColorCount} 种差异色`);
};
useEffect(() => {