001/*******************************************************************************
002 * This software is provided as a supplement to the authors' textbooks on digital
003 *  image processing published by Springer-Verlag in various languages and editions.
004 * Permission to use and distribute this software is granted under the BSD 2-Clause 
005 * "Simplified" License (see http://opensource.org/licenses/BSD-2-Clause). 
006 * Copyright (c) 2006-2016 Wilhelm Burger, Mark J. Burge. All rights reserved. 
007 * Visit http://imagingbook.com for additional details.
008 *******************************************************************************/
009
010package imagingbook.pub.color.quantize;
011
012
013import java.util.ArrayList;
014import java.util.Arrays;
015import java.util.Comparator;
016import java.util.List;
017
018import ij.IJ;
019import ij.process.ColorProcessor;
020import imagingbook.pub.color.statistics.ColorHistogram;
021
022/**
023 * This is an implementation of Heckbert's median-cut color quantization algorithm 
024 * (Heckbert P., "Color Image Quantization for Frame Buffer Display", ACM Transactions
025 * on Computer Graphics (SIGGRAPH), pp. 297-307, 1982).
026 * Unlike in the original algorithm, no initial uniform (scalar) quantization is used to
027 * for reducing the number of image colors. Instead, all colors contained in the original
028 * image are considered in the quantization process. After the set of representative
029 * colors has been found, each image color is mapped to the closest representative
030 * in RGB color space using the Euclidean distance.
031 * The quantization process has two steps: first a ColorQuantizer object is created from
032 * a given image using one of the constructor methods provided. Then this ColorQuantizer
033 * can be used to quantize the original image or any other image using the same set of 
034 * representative colors (color table).
035 * 
036 * @author WB
037 * @version 2017/01/03
038 */
039public class MedianCutQuantizer extends ColorQuantizer {
040        
041        private final ColorNode[] imageColors;  // original (unique) image colors
042        private final ColorNode[] quantColors;  // quantized colors
043        
044        private final int[][] colormap;
045        private final Parameters params;
046        
047        public static class Parameters {
048                /** Maximum number of quantized colors. */
049                public int maxColors = 16;
050                
051                void check() {
052                        if (maxColors < 2 || maxColors > 256) {
053                                throw new IllegalArgumentException();
054                        }
055                }
056        }
057
058        // quick fix, better use a lambda expression?
059        @Deprecated
060        private static Parameters makeParameters(int Kmax) {
061                Parameters p = new Parameters();
062                p.maxColors = Kmax;
063                return p;
064        }
065        
066        @Deprecated
067        public MedianCutQuantizer(ColorProcessor ip, int Kmax) {
068                this((int[]) ip.getPixels(), makeParameters(Kmax));
069        }
070        
071        @Deprecated
072        public MedianCutQuantizer(int[] pixels, int Kmax) {
073                this(pixels, makeParameters(Kmax));
074        }
075        
076         
077        public MedianCutQuantizer(int[] pixels, Parameters params) {
078                this.params = params;
079                System.out.println("Kmax = " + this.params.maxColors);
080                imageColors = makeImageColors(pixels);
081                //listColors(imageColors);
082                quantColors = findRepresentativeColors(this.params.maxColors);
083                listColors(quantColors);
084                colormap = makeColorMap();
085        }
086        
087        void listColors(ColorNode[] colors) {
088                for (ColorNode nd : colors) {
089                        IJ.log(nd.toString());
090                }
091        }
092        
093        private ColorNode[] makeImageColors(int[] pixels) {
094                ColorHistogram colorHist = new ColorHistogram(pixels);
095                final int K = colorHist.getNumberOfColors();
096                ColorNode[] imgColors = new ColorNode[K];
097                for (int i = 0; i < K; i++) {
098                        int rgb = colorHist.getColor(i);
099                        int cnt = colorHist.getCount(i);
100                        imgColors[i] = new ColorNode(rgb, cnt);
101                }
102                return imgColors;
103        }
104
105        ColorNode[] findRepresentativeColors(int Kmax) {
106                final int n = imageColors.length;
107                if (n <= Kmax) {// image has fewer colors than Kmax
108                        return imageColors;
109                }
110                else {
111                        System.out.println("splitting  " + n);
112                        ColorBox initialBox = new ColorBox(0, n - 1, 0);
113                        List<ColorBox> colorSet = new ArrayList<ColorBox>();
114                        colorSet.add(initialBox);
115                        int k = 1;
116                        boolean done = false;
117                        while (k < Kmax && !done) {
118                                ColorBox nextBox = findBoxToSplit(colorSet);
119                                if (nextBox != null) {
120                                        ColorBox newBox = nextBox.splitBox();
121                                        colorSet.add(newBox);
122                                        k = k + 1;
123                                } else {
124                                        done = true;
125                                }
126                        }
127                        return getAvgColors(colorSet);
128                }
129        }
130        
131        private int[][] makeColorMap() {
132                int[][] map = new int[quantColors.length][3];
133                for (int i = 0; i < quantColors.length; i++) {
134                        map[i][0] = quantColors[i].red;
135                        map[i][1] = quantColors[i].grn;
136                        map[i][2] = quantColors[i].blu;
137                }
138                return map;
139        }
140        
141        private ColorBox findBoxToSplit(List<ColorBox> colorBoxes) {
142                ColorBox boxToSplit = null;
143                // from the set of splitable color boxes
144                // select the one with the minimum level
145                int minLevel = Integer.MAX_VALUE;
146                for (ColorBox box : colorBoxes) {
147                        if (box.colorCount() >= 2) {    // box can be split
148                                if (box.level < minLevel) {
149                                        boxToSplit = box;
150                                        minLevel = box.level;
151                                }
152                        }
153                }
154                return boxToSplit;
155        }
156        
157        private ColorNode[] getAvgColors(List<ColorBox> colorBoxes) {
158                int n = colorBoxes.size();
159                ColorNode[] avgColors = new ColorNode[n];
160                int i = 0;
161                for (ColorBox box : colorBoxes) {
162                        avgColors[i] = box.getAvgColor();
163                        i = i + 1;
164                }
165                return avgColors;
166        }
167        
168
169        
170        // ------- methods required by abstract super class -----------------------
171        
172        @Override
173        public int[][] getColorMap() {
174                return colormap;
175        }
176        
177        // ------- obsolete methods -----------------------
178        
179        @Deprecated
180        public int countQuantizedColors() {
181                return quantColors.length;
182        }
183        
184        @Deprecated
185        public ColorNode[] getQuantizedColors() {
186                return quantColors;
187        }
188        
189        // -------------- class ColorNode -------------------------------------------
190
191        private class ColorNode {
192                private final int rgb;
193                private final int red, grn, blu;
194                private final int cnt;
195                
196                ColorNode (int rgb, int cnt) {
197                        this.rgb = (rgb & 0xFFFFFF);
198                        int[] c = intToRgb(rgb);
199                        this.red = c[0];
200                        this.grn = c[1];
201                        this.blu = c[2];
202                        this.cnt = cnt;
203                }
204                
205                ColorNode (int red, int grn, int blu, int cnt) {
206                        this.rgb = rgbToInt(red, grn, blu);
207                        this.red = red;
208                        this.grn = grn;
209                        this.blu = blu;
210                        this.cnt = cnt;
211                }
212                
213                public String toString() {
214                        String s = ColorNode.class.getSimpleName();
215                        s = s + " red=" + red + " green=" + grn + " blue=" + blu + " rgb=" + rgb + " count=" + cnt;
216                        return s;
217                }
218        }
219        
220        // -------------- inner class ColorBox -------------------------------------------
221
222        private class ColorBox { 
223                int lower;              // lower index into 'imageColors'
224                int upper;              // upper index into 'imageColors'
225                int level;              // split level of this color box
226                int count = 0;  // number of pixels represented by this color box
227                int rmin, rmax; // range of contained colors in red dimension
228                int gmin, gmax; // range of contained colors in green dimension
229                int bmin, bmax; // range of contained colors in blue dimension
230                
231                ColorBox(int lower, int upper, int level) {
232                        this.lower = lower;
233                        this.upper = upper;
234                        this.level = level;
235                        this.trim();
236                }
237                
238                int colorCount() {
239                        return upper - lower;
240                }
241                
242                /**
243                 * Recalculates the boundaries and population of this color box.
244                 */
245                void trim() {
246                        count = 0;      
247                        rmin = gmin = bmin = MAX_RGB;   
248                        rmax = gmax = bmax = 0;
249                        for (int i = lower; i <= upper; i++) {
250                                ColorNode color = imageColors[i];
251                                count = count + color.cnt;
252                                rmax = Math.max(color.red, rmax);
253                                rmin = Math.min(color.red, rmin);
254                                gmax = Math.max(color.grn, gmax);
255                                gmin = Math.min(color.grn, gmin);
256                                bmax = Math.max(color.blu, bmax);
257                                bmin = Math.min(color.blu, bmin);
258                        }
259                }
260                
261                /**
262                 * Splits this color box at the median point along its 
263                 * longest color dimension. Modifies the original color
264                 * box and creates a new one, which is returned.
265                 * @return A new box.
266                 */
267                ColorBox splitBox() {   
268                        if (this.colorCount() < 2)      // this box cannot be split
269                                return null;
270                        else {
271                                // find longest dimension of this box:
272                                ColorDimension dim = getLongestColorDimension();
273                                // find median along dim
274                                int med = findMedian(dim);
275                                // now split this box at the median return the resulting new box
276                                int nextLevel = level + 1;
277                                ColorBox newBox = new ColorBox(med + 1, upper, nextLevel);
278                                this.upper = med;
279                                this.level = nextLevel;
280                                this.trim();
281                                return newBox;
282                        }
283                }
284                
285                /**
286                 * Finds the longest dimension of this color box (RED, GREEN, or BLUE)
287                 * @return The color dimension of the longest box side.
288                 */
289                ColorDimension getLongestColorDimension() {
290                        final int rLength = rmax - rmin;
291                        final int gLength = gmax - gmin;
292                        final int bLength = bmax - bmin;
293                        if (bLength >= rLength && bLength >= gLength)
294                                return ColorDimension.BLUE;
295                        else if (gLength >= rLength && gLength >= bLength)
296                                return ColorDimension.GREEN;
297                        else 
298                                return ColorDimension.RED;
299                }
300                                
301                /**
302                 * Finds the position of the median of this color box in RGB space along
303                 * the red, green or blue dimension, respectively.
304                 * @param dim Color dimension.
305                 * @return The median value.
306                 */
307                int findMedian(ColorDimension dim) {
308                        // sort color in this box along dimension dim:
309                        Arrays.sort(imageColors, lower, upper + 1, dim.comparator);
310                        // find the median point:
311                        int nPixels, median;
312                        for (median = lower, nPixels = 0; median < upper; median++) {
313                                nPixels = nPixels + imageColors[median].cnt;
314                                if (nPixels >= count / 2)
315                                        break;
316                        }                       
317                        return median;
318                }
319                
320                ColorNode getAvgColor() {
321                        int rSum = 0;
322                        int gSum = 0;
323                        int bSum = 0;
324                        int n = 0;
325                        for (int i = lower; i <= upper; i++) {
326                                ColorNode cn = imageColors[i];
327                                int cnt = cn.cnt;
328                                rSum = rSum + cnt * cn.red;
329                                gSum = gSum + cnt * cn.grn;
330                                bSum = bSum + cnt * cn.blu;
331                                n = n + cnt;
332                        }
333                        int avgRed = (rSum + (n / 2)) / n;
334                        int avgGrn = (gSum + (n / 2)) / n;
335                        int avgBlu = (bSum + (n / 2)) / n;
336                        return new ColorNode(avgRed, avgGrn, avgBlu, n);
337                }
338
339                public String toString() {
340                        String s = this.getClass().getSimpleName();
341                        s = s + " lower=" + lower + " upper=" + upper;
342                        s = s + " count=" + count + " level=" + level;
343                        s = s + " rmin=" + rmin + " rmax=" + rmax;
344                        s = s + " gmin=" + gmin + " gmax=" + gmax;
345                        s = s + " bmin=" + bmin + " bmax=" + bmax;
346                        s = s + " bmin=" + bmin + " bmax=" + bmax;
347                        return s;
348                }
349        }
350                
351        /**
352         * The main purpose of this inner enumeration class is to associate the
353         * color dimensions RED, GREEN, BLUE with the corresponding comparators.
354         * Implementation uses anonymous classes.
355         */
356        private enum ColorDimension {
357                RED (new Comparator<ColorNode>() {
358                        public int compare(ColorNode colA, ColorNode colB) {
359                                return colA.red - colB.red;
360                        }}), 
361                GREEN (new Comparator<ColorNode>() {
362                        public int compare(ColorNode colA, ColorNode colB) {
363                                return colA.grn - colB.grn;
364                        }}), 
365                BLUE (new Comparator<ColorNode>() {
366                        public int compare(ColorNode colA, ColorNode colB) {
367                                return colA.blu - colB.blu;
368                        }});
369
370                final Comparator<ColorNode> comparator;
371
372                ColorDimension(Comparator<ColorNode> cmp) {
373                        this.comparator = cmp;
374                }
375        }
376        
377} 
378