quantize static method

QuantizerResult quantize(
  1. Iterable<int> inputPixels,
  2. int maxColors, {
  3. List<int> startingClusters = const [],
  4. PointProvider pointProvider = const PointProviderLab(),
  5. int maxIterations = 5,
  6. bool returnInputPixelToClusterPixel = false,
})

Implementation

static QuantizerResult quantize(
  Iterable<int> inputPixels,
  int maxColors, {
  List<int> startingClusters = const [],
  PointProvider pointProvider = const PointProviderLab(),
  int maxIterations = 5,
  bool returnInputPixelToClusterPixel = false,
}) {
  if (pointProvider is PointProviderLab) {
    return _quantizeLab(
      inputPixels,
      maxColors,
      startingClusters: startingClusters,
      maxIterations: maxIterations,
      returnInputPixelToClusterPixel: returnInputPixelToClusterPixel,
    );
  }

  final pixelToIndex = HashMap<int, int>();
  final points = <List<double>>[];
  final pixels = <int>[];
  final countValues = <int>[];
  for (var inputPixel in inputPixels) {
    final index = pixelToIndex[inputPixel];
    if (index == null) {
      pixelToIndex[inputPixel] = pixels.length;
      points.add(pointProvider.fromInt(inputPixel));
      pixels.add(inputPixel);
      countValues.add(1);
    } else {
      countValues[index]++;
    }
  }

  final pointCount = pixels.length;
  final counts = countValues;

  var clusterCount = math.min(maxColors, pointCount);

  final clusters = startingClusters
      .map((e) => pointProvider.fromInt(e))
      .toList();
  final additionalClustersNeeded = clusterCount - clusters.length;
  if (additionalClustersNeeded > 0) {
    final random = math.Random(0x42688);
    final indices = <int>{};
    for (var i = 0; i < additionalClustersNeeded; i++) {
      // Use existing points rather than generating random centroids.
      //
      // KMeans is extremely sensitive to initial clusters. This quantizer
      // is meant to be used with a Wu quantizer that provides initial
      // centroids, but Wu is very slow on unscaled images and when extracting
      // more than 256 colors.
      //
      // Here, we can safely assume that more than 256 colors were requested
      // for extraction. Generating random centroids tends to lead to many
      // "empty" centroids, as the random centroids are nowhere near any pixels
      // in the image, and the centroids from Wu are very refined and close
      // to pixels in the image.
      //
      // Rather than generate random centroids, we'll pick centroids that
      // are actual pixels in the image, and avoid duplicating centroids.

      // Safeguard: if we've exhausted all unique points, stop adding clusters.
      if (indices.length >= points.length) {
        break;
      }
      var index = random.nextInt(points.length);
      while (indices.contains(index)) {
        index = random.nextInt(points.length);
      }
      indices.add(index);
    }

    for (var index in indices) {
      clusters.add(points[index]);
    }
  }
  debugLog(
    'have ${clusters.length} starting clusters, ${points.length} points',
  );
  final clusterIndices = List<int>.generate(
    pointCount,
    (index) => index % clusterCount,
  );

  final clusterDistanceMatrix = Float64List(clusterCount * clusterCount);
  final neighborIndexMatrix = Int32List(clusterCount * clusterCount);
  for (var i = 0; i < clusterCount; i++) {
    final rowOffset = i * clusterCount;
    for (var j = 0; j < clusterCount; j++) {
      neighborIndexMatrix[rowOffset + j] = j;
    }
  }
  final neighborDistanceMatrix = Float64List(clusterCount * clusterCount);
  final neighborLimits = Int32List(clusterCount);
  final previousDistances = Float64List(pointCount);
  final maxPreviousDistancesByCluster = Float64List(clusterCount);

  final pixelCountSums = List<int>.filled(clusterCount, 0);
  final componentASums = List<double>.filled(clusterCount, 0);
  final componentBSums = List<double>.filled(clusterCount, 0);
  final componentCSums = List<double>.filled(clusterCount, 0);
  for (var iteration = 0; iteration < maxIterations; iteration++) {
    if (debug) {
      for (var i = 0; i < clusterCount; i++) {
        pixelCountSums[i] = 0;
      }
      for (var i = 0; i < pointCount; i++) {
        final clusterIndex = clusterIndices[i];
        final count = counts[i];
        pixelCountSums[clusterIndex] += count;
      }
      var emptyClusters = 0;
      for (var cluster = 0; cluster < clusterCount; cluster++) {
        if (pixelCountSums[cluster] == 0) {
          emptyClusters++;
        }
      }
      debugLog(
        'starting iteration ${iteration + 1}; $emptyClusters clusters are empty of $clusterCount',
      );
    }

    var pointsMoved = 0;
    for (var i = 0; i < clusterCount; i++) {
      maxPreviousDistancesByCluster[i] = 0;
    }
    for (var i = 0; i < pointCount; i++) {
      final clusterIndex = clusterIndices[i];
      final distance = pointProvider.distance(
        points[i],
        clusters[clusterIndex],
      );
      previousDistances[i] = distance;
      if (distance > maxPreviousDistancesByCluster[clusterIndex]) {
        maxPreviousDistancesByCluster[clusterIndex] = distance;
      }
    }
    if (iteration == 0) {
      for (var i = 0; i < pointCount; i++) {
        final point = points[i];
        final previousClusterIndex = clusterIndices[i];
        var minimumDistance = previousDistances[i];
        var newClusterIndex = -1;
        for (
          var candidateClusterIndex = 0;
          candidateClusterIndex < clusterCount;
          candidateClusterIndex++
        ) {
          if (candidateClusterIndex == previousClusterIndex) {
            continue;
          }
          final distance = pointProvider.distance(
            point,
            clusters[candidateClusterIndex],
          );
          if (distance <= minimumDistance) {
            minimumDistance = distance;
            newClusterIndex = candidateClusterIndex;
          }
        }
        if (newClusterIndex != -1) {
          pointsMoved++;
          clusterIndices[i] = newClusterIndex;
        }
      }
    } else {
      for (var i = 0; i < clusterCount; i++) {
        final distanceRowOffset = i * clusterCount;
        for (var j = i + 1; j < clusterCount; j++) {
          final distance = pointProvider.distance(clusters[i], clusters[j]);
          clusterDistanceMatrix[j * clusterCount + i] = distance;
          clusterDistanceMatrix[distanceRowOffset + j] = distance;
        }
        final neighborLimit = _partitionNeighborRow(
          neighborIndexMatrix,
          neighborDistanceMatrix,
          distanceRowOffset,
          clusterDistanceMatrix,
          distanceRowOffset,
          clusterCount,
          4 * maxPreviousDistancesByCluster[i],
        );
        neighborLimits[i] = neighborLimit;
        _sortNeighborRow(
          neighborIndexMatrix,
          neighborDistanceMatrix,
          distanceRowOffset,
          neighborLimit,
        );
      }

      for (var i = 0; i < pointCount; i++) {
        final point = points[i];
        final previousClusterIndex = clusterIndices[i];
        final previousDistance = previousDistances[i];
        final neighborDistanceThreshold = 4 * previousDistance;
        var minimumDistance = previousDistance;
        var newClusterIndex = -1;
        final previousClusterNeighborRowOffset =
            previousClusterIndex * clusterCount;
        final neighborLimit = neighborLimits[previousClusterIndex];
        for (var j = 0; j < neighborLimit; j++) {
          final neighborOffset = previousClusterNeighborRowOffset + j;
          final candidateClusterIndex = neighborIndexMatrix[neighborOffset];
          if (neighborDistanceMatrix[neighborOffset] >=
              neighborDistanceThreshold) {
            break;
          }
          if (candidateClusterIndex == previousClusterIndex) {
            continue;
          }
          final distance = pointProvider.distance(
            point,
            clusters[candidateClusterIndex],
          );
          if (distance <= minimumDistance) {
            minimumDistance = distance;
            newClusterIndex = candidateClusterIndex;
          }
        }
        if (newClusterIndex != -1) {
          pointsMoved++;
          clusterIndices[i] = newClusterIndex;
        }
      }
    }

    if (pointsMoved == 0 && iteration > 0) {
      debugLog('terminated after $iteration k-means iterations');
      break;
    }

    debugLog('iteration ${iteration + 1} moved $pointsMoved');
    for (var i = 0; i < clusterCount; i++) {
      pixelCountSums[i] = 0;
      componentASums[i] = 0;
      componentBSums[i] = 0;
      componentCSums[i] = 0;
    }
    for (var i = 0; i < pointCount; i++) {
      final clusterIndex = clusterIndices[i];
      final point = points[i];
      final count = counts[i];
      pixelCountSums[clusterIndex] += count;
      componentASums[clusterIndex] += (point[0] * count);
      componentBSums[clusterIndex] += (point[1] * count);
      componentCSums[clusterIndex] += (point[2] * count);
    }
    for (var i = 0; i < clusterCount; i++) {
      final count = pixelCountSums[i];
      if (count == 0) {
        clusters[i] = [0.0, 0.0, 0.0];
        continue;
      }
      final a = componentASums[i] / count;
      final b = componentBSums[i] / count;
      final c = componentCSums[i] / count;
      clusters[i] = [a, b, c];
    }
  }

  final clusterArgbs = <int>[];
  final clusterPopulations = <int>[];
  for (var i = 0; i < clusterCount; i++) {
    final count = pixelCountSums[i];
    if (count == 0) {
      continue;
    }

    final possibleNewCluster = pointProvider.toInt(clusters[i]);
    if (clusterArgbs.contains(possibleNewCluster)) {
      continue;
    }

    clusterArgbs.add(possibleNewCluster);
    clusterPopulations.add(count);
  }
  debugLog(
    'kmeans finished and generated ${clusterArgbs.length} clusters; $clusterCount were requested',
  );

  final inputPixelToClusterPixel = <int, int>{};
  if (returnInputPixelToClusterPixel) {
    final stopwatch = Stopwatch()..start();
    for (var i = 0; i < pixels.length; i++) {
      final inputPixel = pixels[i];
      final clusterIndex = clusterIndices[i];
      final cluster = clusters[clusterIndex];
      final clusterPixel = pointProvider.toInt(cluster);
      inputPixelToClusterPixel[inputPixel] = clusterPixel;
    }
    debugLog(
      'took ${stopwatch.elapsedMilliseconds} ms to create input to cluster map',
    );
  }

  return QuantizerResult(
    Map.fromIterables(clusterArgbs, clusterPopulations),
    inputPixelToClusterPixel: inputPixelToClusterPixel,
    lstarToCount: {},
  );
}