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-2023 Wilhelm Burger, Mark J. Burge. All rights reserved.
007 * Visit https://imagingbook.com for additional details.
008 ******************************************************************************/
009
010package imagingbook.common.color.quantize;
011
012import imagingbook.common.color.RgbUtils;
013
014import java.util.LinkedList;
015import java.util.List;
016
017/**
018 * <p>
019 * This class implements color quantization based on the octree method. It is a re-factored version of an implementation
020 * ({@code OctTreeOpImage.java}) used in Sun's JAI (Java Advanced Imaging) framework, which &ndash; in turn &ndash; is
021 * based on a C implementation ({@code quantize.c}) by John Cristy in 1992 as part of <a
022 * href="http://www.imagemagick.org/">ImageMagick</a>. The associated source code can be found <a href=
023 * "https://github.com/ImageMagick/ImageMagick/blob/main/MagickCore/quantize.c"> here</a>, the original license note is
024 * provided at the bottom of this source file. This implementation is similar but not identical to the original octree
025 * quantization algorithm described in [1].
026 * </p>
027 * <p>
028 * This implementation uses a modified strategy for pruning the octree for better efficiency. "Quick quantization" means
029 * that original colors are mapped to their color index by simply finding the containing octree node. Otherwise, the
030 * closest quantized color (in terms of Euclidean distance) is searched for, which is naturally slower. See Sec. 13.4 of
031 * [2] for more details.
032 * </p>
033 * <p>
034 * [1] M. Gervautz and W. Purgathofer, "A simple method for color quantization: octree quantization", in A. Glassner
035 * (editor), Graphics Gems I, pp. 287–293. Academic Press, New York (1990).<br> [2] W. Burger, M.J. Burge, <em>Digital
036 * Image Processing &ndash; An Algorithmic Introduction</em>, 3rd ed, Springer (2022).
037 * </p>
038 *
039 * @author WB
040 * @version 2022/11/05
041 */
042public class OctreeQuantizer implements ColorQuantizer {
043
044//      private final static int MAX_RGB = 255;
045        private final static int MAX_NODES = 262144 - 1; // = 2^18 - 1, was 266817;
046        private final static int MAX_TREE_DEPTH = 8;    // check, 7 enough?
047
048        private final int maxColors;    // max. number of distinct colors after quantization
049        private final TreeNode root;    // root node of the tree
050        private final float[][] colorMap;
051        private final boolean quickQuantization;
052        @SuppressWarnings("unused")
053        private final int nColors;              // final number of colors
054        
055        private int depth;                              // current depth of the tree
056        private int nodeCnt = 0;                // counts the number of nodes in the tree
057        private int colorCnt = 0;               // counts the number of colors in the cube (used for temp. counting)
058
059        // -------------------------------------------------------------------------
060
061        /**
062         * Constructor, creates a new {@link OctreeQuantizer} with up to K colors, but never more than the number of colors
063         * found in the supplied image. Quick quantization is turned off by default.
064         *
065         * @param pixels an image as a aRGB-encoded int array
066         * @param K the desired number of colors (1 or more)
067         */
068        public OctreeQuantizer(int[] pixels, int K) {
069                this(pixels, K, false);
070        }
071
072        /**
073         * Constructor, creates a new {@link OctreeQuantizer} with up to K colors, but never more than the number of colors
074         * found in the supplied image. "Quick" quantization can be selected, which means that original colors are mapped to
075         * their color index by simply finding the containing octree node. Otherwise, the closest quantized color (in terms
076         * of Euclidean distance) is searched for, which is naturally slower but usually gives better results. This setting
077         * only affects the final assignment of input colors to the nearest reference colors, the reference colors
078         * themselves are not changed.
079         *
080         * @param pixels an image as a aRGB-encoded int array
081         * @param K the desired number of colors (1 or more)
082         * @param quick turns "quick quantization" on or off
083         */
084        public OctreeQuantizer(int[] pixels, int K, boolean quick) {
085                this.maxColors = K;
086                this.root = new TreeNode(null, 0);
087                this.depth = Math.min(Math.max(log2(maxColors) - 1, 2), MAX_TREE_DEPTH);// 2 <= depth <= maxTreeDepth           
088                int initColorCnt = addPixels(pixels);
089                this.nColors = reduceTree(initColorCnt, pixels.length);
090                this.colorMap = makeColorMap();
091                this.quickQuantization = quick;
092        }
093
094        
095        // -------------------------------------------------------------------------
096        
097        private int addPixels(int[] pixels) {
098                for (int p : pixels) {
099                        addPixel(p);
100                        if (nodeCnt > MAX_NODES) { // a hard limit on the number of nodes in the tree
101                                root.pruneTree();
102                                depth--;
103                        }       
104                }
105                return colorCnt;
106        }
107
108        // walk the tree to depth, increasing the 'npixels' count in each node
109        private void addPixel(int p) {
110                TreeNode node = root;
111                node.nPixels++;
112                int[] rgb = RgbUtils.intToRgb(p);
113                for (int level = 1; level <= depth; level++) {
114                        int id = node.getChildId(rgb);
115                        if (node.childs[id] == null) {
116                                node = new TreeNode(node, id);  // create a new node at next level
117                        } 
118                        else {  // step to next level
119                                node = node.childs[id];
120                        }
121                        node.nPixels++;
122                }
123
124                // at level 'depth': update color statistics of this node
125                node.nUnique++;
126                
127                node.totalR += rgb[0];
128                node.totalG += rgb[1];
129                node.totalB += rgb[2];
130        }
131
132        /**
133         * Repeatedly prunes the tree until the number of nodes with unique &gt; 0 is less than or equal to the maximum
134         * number of colors allowed in the output image. When a node to be pruned has offspring, the pruning procedure
135         * invokes itself recursively in order to prune the tree from the leaves upward. The statistics of the node being
136         * pruned are always added to the corresponding data in that node's parent. This retains the pruned node's color
137         * characteristics for later averaging.
138         *
139         * @param initColorCnt The initial number of colors (leaves) in the octree.
140         * @param nSamples The total number of color samples used for creating the tree.
141         * @return the number of colors
142         */
143        private int reduceTree(int initColorCnt, int nSamples) {
144                int minPixelCnt = Math.max(1,  nSamples / (maxColors * 8));
145                colorCnt = initColorCnt;
146                while (colorCnt > maxColors) {
147                        colorCnt = 0;   // for recounting the number of colors (leaves)
148                        minPixelCnt = root.reduceSparseNodes(minPixelCnt, Integer.MAX_VALUE);
149                }
150                return colorCnt;
151        }
152
153        private float[][] makeColorMap() {
154                List<float[]> colList = new LinkedList<>();
155                colorCnt = 0;   // used to store the color index in each node
156                root.collectColors(colList);
157                return colList.toArray(new float[0][]);
158        }
159        
160        /**
161         * Lists the octree nodes to System.out (for debugging only).
162         */
163        public void listNodes() {
164                root.listNodes();
165        }
166
167        // ------------------------------------------------------------------------------------
168        
169        /**
170         * Represents a node of the octree.
171         */
172        private class TreeNode {
173                private final TreeNode parent;          // reference to the parent node (null for the root node)
174                private final TreeNode[] childs;        // references to child nodes
175                private final int id;                           // branch index of this node at the parent node (0,...,7)
176                private final int level;                        // level of this node within the tree (root has level 0)
177
178                private final int midRed;                       // RGB color midpoint (center of this node in color space)
179                private final int midGrn;
180                private final int midBlu;
181                
182                private int nChilds = 0;                        // number of child nodes
183                private int nPixels = 0;                        // number of pixels represented by this node and all child nodes
184                private int nUnique = 0;                        // number of pixels represented by this node but none of the children
185                
186                private int totalR = 0;                         // sum of all pixel component values represented by this node
187                private int totalG = 0;
188                private int totalB = 0;
189                
190                int colorIdx = -1;                                      // the index of the associated color in the final color table
191
192                private TreeNode(TreeNode parent, int id) {
193                        this.parent = parent;
194                        this.id = id;
195                        this.childs = new TreeNode[8];
196                        nodeCnt++;
197
198                        if (parent == null) {   // this is the root node
199                                this.level = 0;
200                                // set this node's center
201                                int mid = (MAX_RGB + 1) / 2;
202                                midRed = mid;
203                                midGrn = mid;
204                                midBlu = mid;
205                        }
206                        else {
207                                this.level = parent.level + 1;
208                                // attach this node to its parent
209                                parent.nChilds++;
210                                parent.childs[id] = this;
211                                // set this node's midpoint
212                                //int bi = (1 << (MAX_TREE_DEPTH - level)) >> 1;
213                                int bi = 256 >> (level + 1);
214                                midRed = parent.midRed + ((id & 1) > 0 ? bi : -bi);
215                                midGrn = parent.midGrn + ((id & 2) > 0 ? bi : -bi);
216                                midBlu = parent.midBlu + ((id & 4) > 0 ? bi : -bi);
217                                if (level == depth) {   // this is a leaf node
218                                        colorCnt++;
219                                }
220                        }
221                }
222
223                /**
224                 * Prune all nodes at the lowest level (= depth) of the tree.
225                 */
226                private void pruneTree() {
227                        if (nChilds != 0) {     
228                                // move recursively to the lowest level
229                                for (int i = 0; i < childs.length; i++) {
230                                        if (childs[i] != null) {
231                                                childs[i].pruneTree();
232                                        }
233                                }
234                        }
235                        // work on the actual node now
236                        if (level == depth) {   // we are at a node on the lowest level
237                                delete();                       // remove THIS node!
238                        }
239                }
240
241                /**
242                 * Removes any nodes that hold fewer than 'minPixelCount' pixels and finds the minimum population of all
243                 * remaining nodes.
244                 *
245                 * @param minPixelCount The minimum population required for surviving nodes.
246                 * @param newMinPixelCnt The minimum population found so far (during recursive tree walk).
247                 * @return
248                 */
249                private int reduceSparseNodes(int minPixelCount, int newMinPixelCnt) {
250                        // visit all child nodes first
251                        if (nChilds > 0) {
252                                for (TreeNode ch : childs) {
253                                        if (ch != null) {
254                                                newMinPixelCnt = ch.reduceSparseNodes(minPixelCount, newMinPixelCnt);
255                                        }
256                                }
257                        }
258                        // work on the actual node now
259                        if (nPixels <= minPixelCount) {
260                                this.delete();
261                        } 
262                        else {
263                                if (nUnique > 0) {
264                                        colorCnt++;
265                                }
266                                newMinPixelCnt = Math.min(nPixels, newMinPixelCnt); // find smallest population
267                        }
268                        return newMinPixelCnt;
269                }
270                
271                /**
272                 * Remove this node, and push all pixel statistics to its parent.
273                 */
274                private void delete() {
275                        parent.nChilds--;
276                        parent.nUnique += nUnique;
277                        parent.totalR += totalR;
278                        parent.totalG += totalG;
279                        parent.totalB += totalB;
280                        parent.childs[id] = null;       // unlink
281                        nodeCnt--;
282                }
283
284                /**
285                 * Calculates the branch index [0,...,7] out of this node for the specified color.
286                 */
287                private int getChildId(int[] rgb) {
288                        int idx = 0;
289                        if (rgb[0] > this.midRed) idx = idx | 0X01;
290                        if (rgb[1] > this.midGrn) idx = idx | 0X02;
291                        if (rgb[2] > this.midBlu) idx = idx | 0X04;
292                        return idx;
293                }
294
295                /**
296                 * Collects the color entries for the color map. Any node with a non-zero number of unique colors creates a
297                 * color map entry. The representative color for the node is calculated as the average color vector over all
298                 * contributing pixels.
299                 *
300                 * @param colList List of colors to add to.
301                 */
302                private void collectColors(List<float[]> colList) {
303                        // visit all children first
304                        if (nChilds > 0) {
305                                for (TreeNode ch : childs) {
306                                        if (ch != null) {
307                                                ch.collectColors(colList);
308                                        }
309                                }
310                        }               
311                        // process this node
312                        if (nUnique > 0) {                      
313                                float avgRed = (float) totalR / nUnique;
314                                float avgGrn = (float) totalG / nUnique;
315                                float avgBlu = (float) totalB / nUnique;
316                                colList.add(new float[] {avgRed, avgGrn, avgBlu});
317                                this.colorIdx = colorCnt;       // store the color table index for this node
318                                colorCnt++;
319                        }
320                }
321
322                private void listNodes() {
323                        if (nChilds > 0) {      
324                                for (TreeNode ch : childs) {
325                                        if (ch != null) {
326                                                ch.listNodes();
327                                        }
328                                }
329                        }
330                        // bottom of recursion
331                }
332                
333        }
334        
335        // ------- methods required by abstract super class -----------------------
336        
337        @Override
338        public float[][] getColorMap() {
339                return this.colorMap;
340        }
341        
342        @Override
343        public int findColorIndex(int p, float[][] colormap) {
344                if (quickQuantization) {
345                        return getNodeIndex(p);
346                }
347                else {
348                        return ColorQuantizer.super.findColorIndex(p, colormap);
349                }
350        }
351
352        /**
353         * Finds the associated color table index for the supplied RGB color by traversing the octree.
354         */
355        private int getNodeIndex(int p) {
356                int[] rgb = RgbUtils.intToRgb(p);
357                TreeNode node = root;
358                while(true) {
359                        int id = node.getChildId(rgb);  // which of the child nodes?
360                        if (node.childs[id] == null) {  // there is no finer-grained child node, so current 'node' is the one
361                                break;
362                        }
363                        node = node.childs[id];
364                }
365                // 'node' is associated with color p
366                if (node.colorIdx < 0) {
367                        throw new RuntimeException("cannot assign color " + p);
368                }
369                return node.colorIdx;
370        }
371        
372        private int log2(int n){
373                if (n <= 0) throw new IllegalArgumentException();
374                return 31 - Integer.numberOfLeadingZeros(n);
375        }
376
377}
378