2 * Copyright (c) 2021 Samsung Electronics Co., Ltd.
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
8 * http://www.apache.org/licenses/LICENSE-2.0
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
19 * Copyright (C) 2017 The Android Open Source Project
21 * Modified by Woochan Lee(wc0917.lee@samsung.com)
25 using System.Collections;
26 using System.Collections.Generic;
31 internal sealed class ColorCutQuantizer
33 private const float blackMaxLightness = 0.05f;
34 private const float whiteMinLightness = 0.95f;
35 private const int componentRed = -3;
36 private const int componentGreen = -2;
37 private const int componentBlue = -1;
39 private static Dictionary<int, int> colorPopulations;
40 private static int[] colors;
41 private List<Palette.Swatch> quantizedColors;
42 private float[] tempHsl = new float[3];
44 private ColorCutQuantizer(ColorHistogram colorHistogram, int maxColors)
46 if (colorHistogram == null)
48 throw new ArgumentNullException(nameof(colorHistogram), "colorHistogram should not be null.");
53 throw new ArgumentNullException(nameof(maxColors), "maxColors should not be null.");
56 int rawColorCount = colorHistogram.GetNumberOfColors();
57 int[] rawColors = colorHistogram.GetColors();
58 int[] rawColorCounts = colorHistogram.GetColorCounts();
60 // First, lets pack the populations into a SparseIntArray so that they can be easily
61 // retrieved without knowing a color's index
62 colorPopulations = new Dictionary<int, int>();
64 for (int i = 0; i < rawColors.Length; i++)
66 colorPopulations.Add(rawColors[i], rawColorCounts[i]);
69 // Now go through all of the colors and keep those which we do not want to ignore
70 colors = new int[rawColorCount];
71 int validColorCount = 0;
73 foreach (int color in rawColors)
75 if (!ShouldIgnoreColor(color))
77 colors[validColorCount++] = color;
81 Tizen.Log.Info("Palette", "ValidColorCount = " + validColorCount + "\n");
82 if (validColorCount <= maxColors)
84 // The image has fewer colors than the maximum requested, so just return the colors
85 quantizedColors = new List<Palette.Swatch>();
87 for (int i = 0; i < validColorCount; i++)
89 quantizedColors.Add(new Palette.Swatch(colors[i], colorPopulations[colors[i]]));
95 Tizen.Log.Info("Palette", "validColorCount = " + validColorCount + " maxColors = " + maxColors + "\n");
96 // We need use quantization to reduce the number of colors
97 quantizedColors = QuantizePixels(validColorCount - 1, maxColors);
102 /// Factory-method to generate a ColorCutQuantizer from a PixelBuffer object.
104 public static ColorCutQuantizer FromBitmap(PixelBuffer pixelBuffer, Rectangle region, int maxColors)
113 width = (int)pixelBuffer.GetWidth(); height = (int)pixelBuffer.GetHeight(); i = 0; j = 0;
118 width = region.Width; height = region.Height; i = region.X; j = region.Y;
121 Tizen.Log.Info("Palette", "Get pixels rawdata from (" + i + " " + j + " " + width + " " + height + ")" + "\n");
123 pixels = new int[width * height];
124 PixelFormat format = pixelBuffer.GetPixelFormat();
125 int pixelLength = (int)ColorUtils.GetBytesPerPixel(format);
126 IntPtr bufferIntPtr = pixelBuffer.GetBuffer();
130 byte *rawdata = (byte *)bufferIntPtr.ToPointer();
131 int totalLength = width * height * pixelLength;
132 for (i = 0; i < totalLength; i += pixelLength)
135 if (pixelLength == 3)
136 pixels[index++] = (255 & 0xff) << 24 | (rawdata[i] & 0xff) << 16 | (rawdata[i+1] & 0xff) << 8 | (rawdata[i+2] & 0xff);
139 pixels[index++] = (rawdata[i + 3] & 0xff) << 24 | (rawdata[i] & 0xff) << 16 | (rawdata[i+1] & 0xff) << 8 | (rawdata[i+2] & 0xff);
143 return new ColorCutQuantizer(new ColorHistogram(pixels), maxColors);
147 /// return the list of quantized colors
149 public List<Palette.Swatch> GetQuantizedColors()
151 return quantizedColors;
154 private List<Palette.Swatch> QuantizePixels(int maxColorIndex, int maxColors)
156 // Create the priority queue which is sorted by volume descending. This means we always
157 // split the largest box in the queue
158 CustomHeap<Vbox> customHeap = new CustomHeap<Vbox>(new VboxComparatorVolume());
159 // To start, offer a box which contains all of the colors
160 customHeap.Offer(new Vbox(0, maxColorIndex));
161 // Now go through the boxes, splitting them until we have reached maxColors or there are no
162 // more boxes to split
163 SplitBoxes(customHeap, maxColors);
164 // Finally, return the average colors of the color boxes
165 return GenerateAverageColors(customHeap);
169 /// Iterate through the queue, popping
170 /// ColorCutQuantizer.Vbox objects from the queue
171 /// and splitting them. Once split, the new box and the remaining box are offered back to the
174 /// param queue PriorityQueue to poll for boxes
175 /// param maxSize Maximum amount of boxes to split
177 private void SplitBoxes(CustomHeap<Vbox> queue, int maxSize)
180 while (queue.count < maxSize)
183 Vbox vbox = queue.Poll();
184 if (vbox != null && vbox.CanSplit())
186 // First split the box, and offer the result
187 queue.Offer(vbox.SplitBox());
188 // Then offer the box back
193 // If we get here then there are no more boxes to split, so return
199 private List<Palette.Swatch> GenerateAverageColors(CustomHeap<Vbox> vboxes)
201 List<Palette.Swatch> colors = new List<Palette.Swatch>(vboxes.count);
202 foreach (Vbox vbox in vboxes)
204 Palette.Swatch color = vbox.GetAverageColor();
205 if (!ShouldIgnoreColor(color))
207 // As we're averaging a color box, we can still get colors which we do not want, so
208 // we check again here
213 Tizen.Log.Info("Palette", "Final generated color count = " + colors.Count + "\n");
217 private Boolean ShouldIgnoreColor(int color)
219 ColorUtils.RgbToHsl((color >> 16) & 0xff, (color >> 8) & 0xff, color & 0xff, tempHsl);
220 return ShouldIgnoreColor(tempHsl);
223 private static Boolean ShouldIgnoreColor(Palette.Swatch color)
225 return ShouldIgnoreColor(color.GetHsl());
228 private static Boolean ShouldIgnoreColor(float[] hslColor)
230 return IsWhite(hslColor) || IsBlack(hslColor) || IsNearRedILine(hslColor);
234 ///return true if the color represents a color which is close to black.
236 private static Boolean IsBlack(float[] hslColor)
238 return hslColor[2] <= blackMaxLightness;
242 /// return true if the color represents a color which is close to white.
244 private static Boolean IsWhite(float[] hslColor)
246 return hslColor[2] >= whiteMinLightness;
250 /// return true if the color lies close to the red side of the I line.
252 private static Boolean IsNearRedILine(float[] hslColor)
254 return hslColor[0] >= 10f && hslColor[0] <= 37f && hslColor[1] <= 0.82f;
257 private sealed class CustomHeap<T> : IEnumerable<T>
259 private const int initialcapacity = 0;
260 private const int growFactor = 2;
261 private const int minGrow = 1;
263 private int capacity = initialcapacity;
264 private int tail = 0;
265 private T[] heap = Array.Empty<T>();
267 public CustomHeap(Comparer<T> comparer)
269 if (comparer == null) throw new ArgumentNullException(nameof(comparer), "comparer is null");
273 private Comparer<T> Comparer { get; set; }
275 public IEnumerator<T> GetEnumerator()
277 return heap.Take(count).GetEnumerator();
280 IEnumerator IEnumerable.GetEnumerator()
282 return GetEnumerator();
285 public int count { get { return tail; } }
287 public void Offer(T item)
289 if (count == capacity)
298 if (count == 0) throw new InvalidOperationException("CustomHeap is empty");
304 if (count == 0) throw new InvalidOperationException("CustomHeap is empty");
314 private void BubbleDown(int i)
316 int dominatingNode = Dominating(i);
318 if (dominatingNode == i) return;
319 Swap(i, dominatingNode);
320 BubbleDown(dominatingNode);
323 private void BubbleUp(int i)
325 if (i == 0 || Dominates(heap[Parent(i)], heap[i]))
332 private int Dominating(int i)
334 int dominatingNode = i;
336 dominatingNode = GetDominating(YoungChild(i), dominatingNode);
337 dominatingNode = GetDominating(OldChild(i), dominatingNode);
338 return dominatingNode;
341 private int GetDominating(int newNode, int dominatingNode)
343 if (newNode < tail && !Dominates(heap[dominatingNode], heap[newNode]))
346 return dominatingNode;
349 private void Swap(int i, int j)
357 private static int Parent(int i)
359 return (i + 1) / 2 - 1;
362 private static int YoungChild(int i)
364 return (i + 1) * 2 - 1;
367 private static int OldChild(int i)
369 return YoungChild(i) + 1;
374 int newcapacity = capacity * growFactor + minGrow;
375 var newHeap = new T[newcapacity];
377 Array.Copy(heap, newHeap, capacity);
379 capacity = newcapacity;
382 private bool Dominates(T x, T y)
384 return Comparer.Compare(x, y) <= 0;
390 /// Comparator which sorts Vbox instances based on their volume, in descending order
392 private sealed class VboxComparatorVolume : Comparer<Vbox>
394 public override int Compare(Vbox lhs, Vbox rhs)
396 return rhs.GetVolume() - lhs.GetVolume();
401 /// Represents a tightly fitting box around a color space.
403 private sealed class Vbox
405 private int lowerIndex;
406 private int upperIndex;
407 private int minRed, maxRed, minGreen, maxGreen, minBlue, maxBlue;
409 public Vbox(int lowerIndex, int upperIndex)
411 this.lowerIndex = lowerIndex;
412 this.upperIndex = upperIndex;
416 public int GetVolume()
418 return (maxRed - minRed + 1) * (maxGreen - minGreen + 1) * (maxBlue - minBlue + 1);
421 public Boolean CanSplit()
423 return GetColorCount() > 1;
426 public int GetColorCount()
428 return upperIndex - lowerIndex;
432 /// Recomputes the boundaries of this box to tightly fit the colors within the box.
436 // Reset the min and max to opposite values
437 minRed = minGreen = minBlue = 0xff;
438 maxRed = maxGreen = maxBlue = 0x0;
439 for (int i = lowerIndex; i <= upperIndex; i++)
441 int red = (colors[i] >> 16) & 0xff;
442 int green = (colors[i] >> 8) & 0xff;
443 int blue = colors[i] & 0xff;
445 maxRed = red > maxRed ? red : maxRed;
446 minRed = red < minRed ? red : minRed;
447 maxGreen = green > maxGreen ? green : maxGreen;
448 minGreen = green < minGreen ? green : minGreen;
449 maxBlue = blue > maxBlue ? blue : maxBlue;
450 minBlue = blue < minBlue ? blue : minBlue;
455 /// Split this color box at the mid-point along it's longest dimension
457 /// return the new ColorBox
459 public Vbox SplitBox()
463 throw new InvalidOperationException("Can not split a box with only 1 color");
466 // find median along the longest dimension
467 int splitPoint = FindSplitPoint();
469 Vbox newBox = new Vbox(splitPoint + 1, upperIndex);
470 // Now change this box's upperIndex and recompute the color boundaries
471 upperIndex = splitPoint;
478 /// return the dimension which this box is largest in
480 public int GetLongestColorDimension()
482 int redLength = maxRed - minRed;
483 int greenLength = maxGreen - minGreen;
484 int blueLength = maxBlue - minBlue;
486 if (redLength >= greenLength && redLength >= blueLength)
490 else if (greenLength >= redLength && greenLength >= blueLength)
492 return componentGreen;
496 return componentBlue;
501 /// Finds the point within this box's lowerIndex and upperIndex index of where to split.
503 /// This is calculated by finding the longest color dimension, and then sorting the
504 /// sub-array based on that dimension value in each color. The colors are then iterated over
505 /// until a color is found with at least the midpoint of the whole box's dimension midpoint.
507 /// return the index of the colors array to split from
509 public int FindSplitPoint()
511 int longestDimension = GetLongestColorDimension();
513 // We need to sort the colors in this box based on the longest color dimension.
514 // As we can't use a Comparator to define the sort logic, we modify each color so that
515 // it's most significant is the desired dimension
516 ModifySignificantOctet(longestDimension, lowerIndex, upperIndex);
518 Array.Sort(colors, lowerIndex, upperIndex + 1 - lowerIndex);
520 // Now revert all of the colors so that they are packed as RGB again
521 ModifySignificantOctet(longestDimension, lowerIndex, upperIndex);
523 int dimensionMidPoint = MidPoint(longestDimension);
524 for (int i = lowerIndex; i < upperIndex; i++)
526 switch (longestDimension)
529 if (((colors[i] >> 16) & 0xff) >= dimensionMidPoint)
535 if (((colors[i] >> 8) & 0xff) >= dimensionMidPoint)
541 if ((colors[i] &0xff) > dimensionMidPoint)
553 /// return the average color of this box.
555 public Palette.Swatch GetAverageColor()
560 int totalPopulation = 0;
562 for (int i = lowerIndex; i <= upperIndex; i++)
564 int colorPopulation = colorPopulations[colors[i]];
565 totalPopulation += colorPopulation;
566 redSum += colorPopulation * ((colors[i] >> 16) & 0xff);
567 greenSum += colorPopulation * ((colors[i] >> 8) & 0xff);
568 blueSum += colorPopulation * (colors[i] & 0xff);
571 int redAverage = (int)Math.Round(redSum / (float)totalPopulation);
572 int greenAverage = (int)Math.Round(greenSum / (float)totalPopulation);
573 int blueAverage = (int)Math.Round(blueSum / (float)totalPopulation);
575 return new Palette.Swatch(redAverage, greenAverage, blueAverage, totalPopulation);
579 /// return the midpoint of this box in the given dimension
581 private int MidPoint(int dimension)
587 return (minRed + maxRed) / 2;
589 return (minGreen + maxGreen) / 2;
591 return (minBlue + maxBlue) / 2;
596 /// Modify the significant octet in a packed color int. Allows sorting based on the value of a
597 /// single color component.
599 /// see Vbox#findSplitPoint()
601 private void ModifySignificantOctet(int dimension, int lowIndex, int highIndex)
606 // Already in RGB, no need to do anything
609 // We need to do a RGB to GRB swap, or vice-versa
610 for (int i = lowIndex; i <= highIndex; i++)
612 int color = colors[i];
613 colors[i] = 255 << 24 | (color >> 8 & 0xff) << 16 | (color >> 16 & 0xff) << 8 | (color & 0xff);
618 // We need to do a RGB to BGR swap, or vice-versa
619 for (int i = lowIndex; i <= highIndex; i++)
621 int color = colors[i];
622 colors[i] = 255 << 24 | (color & 0xff) << 16 | (color >> 8 & 0xff) << 8 | (color >> 16 & 0xff);