Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Use Isolate to Quantize #12

Closed
wants to merge 6 commits into from
Closed
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
114 changes: 75 additions & 39 deletions packages/palette_generator/lib/palette_generator.dart
Original file line number Diff line number Diff line change
Expand Up @@ -182,15 +182,16 @@ class PaletteGenerator extends Diagnosticable {
ImageConfiguration(size: size, devicePixelRatio: 1.0),
);
final Completer<ui.Image> imageCompleter = Completer<ui.Image>();
Timer loadFailureTimeout;
void imageListener(ImageInfo info, bool synchronousCall) {
loadFailureTimeout?.cancel();
stream.removeListener(imageListener);
imageCompleter.complete(info.image);
}

final ImageStreamListener imageListener = ImageStreamListener(
(ImageInfo info, bool synchronouscall) {
imageCompleter.complete(info.image);
},
onError: (dynamic error, StackTrace stackTrace) {
imageCompleter.completeError(error);
},
);
if (timeout != Duration.zero) {
loadFailureTimeout = Timer(timeout, () {
Timer(timeout, () {
stream.removeListener(imageListener);
imageCompleter.completeError(
TimeoutException(
Expand Down Expand Up @@ -1040,8 +1041,8 @@ class _ColorCutQuantizer {
final Rect region;
final List<PaletteFilter> filters;

Iterable<Color> _getImagePixels(ByteData pixels, int width, int height,
{Rect region}) sync* {
static Iterable<Color> _getImagePixels(
ByteData pixels, int width, int height, Rect region) sync* {
final int rowStride = width * 4;
int rowStart;
int rowEnd;
Expand Down Expand Up @@ -1076,7 +1077,7 @@ class _ColorCutQuantizer {
assert(byteCount == ((rowEnd - rowStart) * (colEnd - colStart) * 4));
}

bool _shouldIgnoreColor(Color color) {
static bool _shouldIgnoreColor(Color color, List<PaletteFilter> filters) {
final HSLColor hslColor = HSLColor.fromColor(color);
if (filters != null && filters.isNotEmpty) {
for (PaletteFilter filter in filters) {
Expand All @@ -1089,31 +1090,47 @@ class _ColorCutQuantizer {
}

Future<List<PaletteColor>> _quantizeColors(ui.Image image) async {
const int quantizeWordWidth = 5;
const int quantizeChannelWidth = 8;
const int quantizeShift = quantizeChannelWidth - quantizeWordWidth;
const int quantizeWordMask =
((1 << quantizeWordWidth) - 1) << quantizeShift;

Color quantizeColor(Color color) {
return Color.fromARGB(
color.alpha,
color.red & quantizeWordMask,
color.green & quantizeWordMask,
color.blue & quantizeWordMask,
);
}

final ByteData imageData =
await image.toByteData(format: ui.ImageByteFormat.rawRgba);
if (filters == null)
return await compute(_quantizefromByte, {
'byteData': imageData,
'maxColors': maxColors,
'width': image.width,
'height': image.height,
'region': region,
'paletteColors': _paletteColors,
'filters': null
});
else
return _quantizefromByte({
'byteData': imageData,
'maxColors': maxColors,
'width': image.width,
'height': image.height,
'region': region,
'paletteColors': _paletteColors,
'filters': filters
});
}

static List<PaletteColor> _quantizefromByte(Map<String, dynamic> map) {
ByteData bd = map['byteData'];
int maxColors = map['maxColors'];
int width = map['width'];
int height = map['height'];
Rect region = map['region'];
List<PaletteFilter> filters = map['filters'];
List<PaletteColor> _paletteColors = map['paletteColors'];
final ByteData imageData = bd;
final Iterable<Color> pixels =
_getImagePixels(imageData, image.width, image.height, region: region);
_getImagePixels(imageData, width, height, region);
final Map<Color, int> hist = <Color, int>{};
for (Color pixel in pixels) {
// Update the histogram, but only for non-zero alpha values, and for the
// ones we do add, make their alphas opaque so that we can use a Color as
// the histogram key.
final Color quantizedColor = quantizeColor(pixel);
final Color quantizedColor = _quantizeColor(pixel);
final Color colorKey = quantizedColor.withAlpha(0xff);
// Skip pixels that are entirely transparent.
if (quantizedColor.alpha != 0x0) {
Expand All @@ -1122,7 +1139,7 @@ class _ColorCutQuantizer {
}
// Now let's remove any colors that the filters want to ignore.
hist.removeWhere((Color color, int _) {
return _shouldIgnoreColor(color);
return _shouldIgnoreColor(color, filters);
});
if (hist.length <= maxColors) {
// The image has fewer colors than the maximum requested, so just return
Expand All @@ -1134,15 +1151,31 @@ class _ColorCutQuantizer {
} else {
// We need use quantization to reduce the number of colors
_paletteColors.clear();
_paletteColors.addAll(_quantizePixels(maxColors, hist));
_paletteColors.addAll(_quantizePixels(
{'maxColors': maxColors, 'histogram': hist}, filters));
}
return _paletteColors;
}

List<PaletteColor> _quantizePixels(
int maxColors,
Map<Color, int> histogram,
) {
static Color _quantizeColor(Color color) {
const int quantizeWordWidth = 5;
const int quantizeChannelWidth = 8;
const int quantizeShift = quantizeChannelWidth - quantizeWordWidth;
const int quantizeWordMask =
((1 << quantizeWordWidth) - 1) << quantizeShift;
return Color.fromARGB(
color.alpha,
color.red & quantizeWordMask,
color.green & quantizeWordMask,
color.blue & quantizeWordMask,
);
}

static List<PaletteColor> _quantizePixels(
Map args, List<PaletteFilter> filters) {
int maxColors = args['maxColors'];
Map<Color, int> histogram = args['histogram'];

int volumeComparator(_ColorVolumeBox a, _ColorVolumeBox b) {
return b.getVolume().compareTo(a.getVolume());
}
Expand All @@ -1158,15 +1191,16 @@ class _ColorCutQuantizer {
// or there are no more boxes to split
_splitBoxes(priorityQueue, maxColors);
// Finally, return the average colors of the color boxes.
return _generateAverageColors(priorityQueue);
return _generateAverageColors(priorityQueue, filters);
}

// Iterate through the [PriorityQueue], popping [_ColorVolumeBox] objects
// from the queue and splitting them. Once split, the new box and the
// remaining box are offered back to the queue.
//
// The `maxSize` is the maximum number of boxes to split.
void _splitBoxes(PriorityQueue<_ColorVolumeBox> queue, final int maxSize) {
static void _splitBoxes(
PriorityQueue<_ColorVolumeBox> queue, final int maxSize) {
while (queue.length < maxSize) {
final _ColorVolumeBox colorVolumeBox = queue.removeFirst();
if (colorVolumeBox != null && colorVolumeBox.canSplit()) {
Expand All @@ -1182,12 +1216,14 @@ class _ColorCutQuantizer {
}

// Generates the average colors from each of the boxes in the queue.
List<PaletteColor> _generateAverageColors(
PriorityQueue<_ColorVolumeBox> colorVolumeBoxes) {
static List<PaletteColor> _generateAverageColors(
PriorityQueue<_ColorVolumeBox> colorVolumeBoxes,
List<PaletteFilter> filters) {
final List<PaletteColor> colors = <PaletteColor>[];
for (_ColorVolumeBox colorVolumeBox in colorVolumeBoxes.toList()) {
final PaletteColor paletteColor = colorVolumeBox.getAverageColor();
if (!_shouldIgnoreColor(paletteColor.color)) {

if (!_shouldIgnoreColor(paletteColor.color, filters)) {
colors.add(paletteColor);
}
}
Expand Down