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