ditherImage function

Image ditherImage(
  1. Image image, {
  2. Quantizer? quantizer,
  3. DitherKernel kernel = DitherKernel.floydSteinberg,
  4. @Deprecated('Use scanOrder: DitherScanOrder.serpentine instead. ' 'This parameter will be removed in a future release.') bool serpentine = false,
  5. DitherScanOrder scanOrder = DitherScanOrder.zigzag,
  6. double bayerStrength = 1.0,
})

Dither an image to reduce banding patterns when reducing the number of colors. Derived from http://jsbin.com/iXofIji/2/edit

quantizer is the color reducer used to map each pixel to the palette; if null a NeuralQuantizer is built from image.

kernel selects the dithering algorithm: error-diffusion kernels (e.g. DitherKernel.floydSteinberg) propagate quantization error to neighbors, while the ordered Bayer kernels (DitherKernel.bayer2x2, DitherKernel.bayer4x4, DitherKernel.bayer8x8) and blue noise (DitherKernel.blueNoise) use a fixed position-based threshold matrix.

scanOrder selects the order in which pixels are visited by the error-diffusion kernels (DitherScanOrder.raster, DitherScanOrder.serpentine, the diagonal DitherScanOrder.zigzag, or the space-filling DitherScanOrder.hilbert curve). It has no effect on the Bayer kernels.

bayerStrength scales the dither offset and is only used for the Bayer kernels; it is ignored by the error-diffusion kernels.

Implementation

img.Image ditherImage(
  img.Image image, {
  img.Quantizer? quantizer,
  DitherKernel kernel = DitherKernel.floydSteinberg,
  @Deprecated(
    'Use scanOrder: DitherScanOrder.serpentine instead. '
    'This parameter will be removed in a future release.',
  )
  bool serpentine = false,
  DitherScanOrder scanOrder = DitherScanOrder.zigzag,
  double bayerStrength = 1.0,
}) {
  quantizer ??= img.NeuralQuantizer(image);

  if (kernel == DitherKernel.none) {
    return quantizer.getIndexImage(image);
  }

  final orderedMatrix = _orderedDitherMatrix(kernel);
  if (orderedMatrix != null) {
    return ditherImageOrdered(image, quantizer, orderedMatrix, bayerStrength);
  }

  final order = serpentine
      // ignore: deprecated_member_use_from_same_package
      ? DitherScanOrder.serpentine
      : scanOrder;

  final q = quantizer;
  final ds = _errorDiffusionKernels[kernel]!;
  final height = image.height;
  final width = image.width;

  final palette = quantizer.palette;
  final indexedImage = img.Image(
    width: width,
    height: height,
    numChannels: 1,
    palette: palette,
  );

  final imageCopy = image.clone();

  // Quantizes the pixel at [x],[y] and diffuses its error to the neighbors.
  // [direction] is the horizontal scan direction (1 or -1) and controls the
  // order in which the kernel taps are applied.
  void diffusePixel(int x, int y, int direction) {
    // Get original color
    final pc = imageCopy.getPixel(x, y);
    final r1 = pc[0].toInt();
    final g1 = pc[1].toInt();
    final b1 = pc[2].toInt();

    // Get converted color
    final idx = q.getColorIndexRgb(r1, g1, b1);
    indexedImage.setPixelIndex(x, y, idx);

    final r2 = palette.get(idx, 0);
    final g2 = palette.get(idx, 1);
    final b2 = palette.get(idx, 2);

    final er = r1 - r2;
    final eg = g1 - g2;
    final eb = b1 - b2;

    if (er == 0 && eg == 0 && eb == 0) {
      return;
    }

    final i0 = direction == 1 ? 0 : ds.length - 1;
    final i1 = direction == 1 ? ds.length : 0;
    for (var i = i0; i != i1; i += direction) {
      final x1 = ds[i][1].toInt();
      final y1 = ds[i][2].toInt();
      if ((x1 + x) >= 0 &&
          (x1 + x) < width &&
          (y1 + y) >= 0 &&
          (y1 + y) < height) {
        final d = ds[i][0];
        final nx = x + x1;
        final ny = y + y1;
        final p2 = imageCopy.getPixel(nx, ny);
        p2
          ..r = p2.r + er * d
          ..g = p2.g + eg * d
          ..b = p2.b + eb * d;
      }
    }
  }

  if (order == DitherScanOrder.zigzag) {
    // Walk the anti-diagonals x + y == d, alternating their direction.
    final numDiagonals = width + height - 1;
    for (var d = 0; d < numDiagonals; d++) {
      final xMin = d < height ? 0 : d - height + 1;
      final xMax = d < width ? d : width - 1;
      if (d.isEven) {
        for (var x = xMin; x <= xMax; x++) {
          diffusePixel(x, d - x, 1);
        }
      } else {
        for (var x = xMax; x >= xMin; x--) {
          diffusePixel(x, d - x, 1);
        }
      }
    }
    return indexedImage;
  }

  if (order == DitherScanOrder.hilbert) {
    // Walk pixels in Hilbert space-filling curve order.
    // The curve is defined on a power-of-2 square; pixels outside the image
    // bounds are simply skipped.
    final maxDim = max(width, height);
    var n = 1;
    while (n < maxDim) {
      n <<= 1;
    }
    final xy = [0, 0];
    final total = n * n;
    for (var d = 0; d < total; d++) {
      _hilbertDtoXY(n, d, xy);
      if (xy[0] < width && xy[1] < height) {
        diffusePixel(xy[0], xy[1], 1);
      }
    }
    return indexedImage;
  }

  final isSerpentine = order == DitherScanOrder.serpentine;
  var direction = isSerpentine ? -1 : 1;

  for (var y = 0; y < height; y++) {
    if (isSerpentine) {
      direction = direction * -1;
    }

    final x0 = direction == 1 ? 0 : width - 1;
    final x1 = direction == 1 ? width : 0;
    for (var x = x0; x != x1; x += direction) {
      diffusePixel(x, y, direction);
    }
  }

  return indexedImage;
}