UNPKG

@poupe/material-color-utilities

Version:

Algorithms and utilities that power the Material Design 3 (M3) color system, including choosing theme colors from images and creating tones of colors; all in a new color space.

210 lines 9.03 kB
/** * @license * Copyright 2021 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ import { LabPointProvider } from './lab_point_provider.js'; const MAX_ITERATIONS = 10; const MIN_MOVEMENT_DISTANCE = 3.0; /** * An image quantizer that improves on the speed of a standard K-Means algorithm * by implementing several optimizations, including deduping identical pixels * and a triangle inequality rule that reduces the number of comparisons needed * to identify which cluster a point should be moved to. * * Wsmeans stands for Weighted Square Means. * * This algorithm was designed by M. Emre Celebi, and was found in their 2011 * paper, Improving the Performance of K-Means for Color Quantization. * https://arxiv.org/abs/1101.0395 */ // material_color_utilities is designed to have a consistent API across // platforms and modular components that can be moved around easily. Using a // class as a namespace facilitates this. // // tslint:disable-next-line:class-as-namespace export class QuantizerWsmeans { /** * @param inputPixels Colors in ARGB format. * @param startingClusters Defines the initial state of the quantizer. Passing * an empty array is fine, the implementation will create its own initial * state that leads to reproducible results for the same inputs. * Passing an array that is the result of Wu quantization leads to higher * quality results. * @param maxColors The number of colors to divide the image into. A lower * number of colors may be returned. * @return Colors in ARGB format. */ static quantize(inputPixels, startingClusters, maxColors) { const pixelToCount = new Map(); const points = new Array(); const pixels = new Array(); const pointProvider = new LabPointProvider(); let pointCount = 0; for (let i = 0; i < inputPixels.length; i++) { const inputPixel = inputPixels[i]; const pixelCount = pixelToCount.get(inputPixel); if (pixelCount === undefined) { pointCount++; points.push(pointProvider.fromInt(inputPixel)); pixels.push(inputPixel); pixelToCount.set(inputPixel, 1); } else { pixelToCount.set(inputPixel, pixelCount + 1); } } const counts = new Array(); for (let i = 0; i < pointCount; i++) { const pixel = pixels[i]; const count = pixelToCount.get(pixel); if (count !== undefined) { counts[i] = count; } } let clusterCount = Math.min(maxColors, pointCount); if (startingClusters.length > 0) { clusterCount = Math.min(clusterCount, startingClusters.length); } const clusters = new Array(); for (let i = 0; i < startingClusters.length; i++) { clusters.push(pointProvider.fromInt(startingClusters[i])); } const additionalClustersNeeded = clusterCount - clusters.length; if (startingClusters.length === 0 && additionalClustersNeeded > 0) { for (let i = 0; i < additionalClustersNeeded; i++) { const l = Math.random() * 100.0; const a = Math.random() * (100.0 - (-100.0) + 1) + -100; const b = Math.random() * (100.0 - (-100.0) + 1) + -100; clusters.push(new Array(l, a, b)); } } const clusterIndices = new Array(); for (let i = 0; i < pointCount; i++) { clusterIndices.push(Math.floor(Math.random() * clusterCount)); } const indexMatrix = new Array(); for (let i = 0; i < clusterCount; i++) { indexMatrix.push(new Array()); for (let j = 0; j < clusterCount; j++) { indexMatrix[i].push(0); } } const distanceToIndexMatrix = new Array(); for (let i = 0; i < clusterCount; i++) { distanceToIndexMatrix.push(new Array()); for (let j = 0; j < clusterCount; j++) { distanceToIndexMatrix[i].push(new DistanceAndIndex()); } } const pixelCountSums = new Array(); for (let i = 0; i < clusterCount; i++) { pixelCountSums.push(0); } for (let iteration = 0; iteration < MAX_ITERATIONS; iteration++) { for (let i = 0; i < clusterCount; i++) { for (let j = i + 1; j < clusterCount; j++) { const distance = pointProvider.distance(clusters[i], clusters[j]); distanceToIndexMatrix[j][i].distance = distance; distanceToIndexMatrix[j][i].index = i; distanceToIndexMatrix[i][j].distance = distance; distanceToIndexMatrix[i][j].index = j; } distanceToIndexMatrix[i].sort(); for (let j = 0; j < clusterCount; j++) { indexMatrix[i][j] = distanceToIndexMatrix[i][j].index; } } let pointsMoved = 0; for (let i = 0; i < pointCount; i++) { const point = points[i]; const previousClusterIndex = clusterIndices[i]; const previousCluster = clusters[previousClusterIndex]; const previousDistance = pointProvider.distance(point, previousCluster); let minimumDistance = previousDistance; let newClusterIndex = -1; for (let j = 0; j < clusterCount; j++) { if (distanceToIndexMatrix[previousClusterIndex][j].distance >= 4 * previousDistance) { continue; } const distance = pointProvider.distance(point, clusters[j]); if (distance < minimumDistance) { minimumDistance = distance; newClusterIndex = j; } } if (newClusterIndex !== -1) { const distanceChange = Math.abs((Math.sqrt(minimumDistance) - Math.sqrt(previousDistance))); if (distanceChange > MIN_MOVEMENT_DISTANCE) { pointsMoved++; clusterIndices[i] = newClusterIndex; } } } if (pointsMoved === 0 && iteration !== 0) { break; } const componentASums = new Array(clusterCount).fill(0); const componentBSums = new Array(clusterCount).fill(0); const componentCSums = new Array(clusterCount).fill(0); for (let i = 0; i < clusterCount; i++) { pixelCountSums[i] = 0; } for (let i = 0; i < pointCount; i++) { const clusterIndex = clusterIndices[i]; const point = points[i]; const count = counts[i]; pixelCountSums[clusterIndex] += count; componentASums[clusterIndex] += (point[0] * count); componentBSums[clusterIndex] += (point[1] * count); componentCSums[clusterIndex] += (point[2] * count); } for (let i = 0; i < clusterCount; i++) { const count = pixelCountSums[i]; if (count === 0) { clusters[i] = [0.0, 0.0, 0.0]; continue; } const a = componentASums[i] / count; const b = componentBSums[i] / count; const c = componentCSums[i] / count; clusters[i] = [a, b, c]; } } const argbToPopulation = new Map(); for (let i = 0; i < clusterCount; i++) { const count = pixelCountSums[i]; if (count === 0) { continue; } const possibleNewCluster = pointProvider.toInt(clusters[i]); if (argbToPopulation.has(possibleNewCluster)) { continue; } argbToPopulation.set(possibleNewCluster, count); } return argbToPopulation; } } /** * A wrapper for maintaining a table of distances between K-Means clusters. */ class DistanceAndIndex { constructor() { this.distance = -1; this.index = -1; } } //# sourceMappingURL=quantizer_wsmeans.js.map