001: /*
002: * @(#) $Header: /cvs/jai-operators/src/main/ca/forklabs/media/jai/operator/KMeansDescriptor.java,v 1.2 2007/09/07 18:09:41 forklabs Exp $
003: *
004: * Copyright (C) 2007 Forklabs Daniel Léonard
005: *
006: * This program is free software; you can redistribute it and/or
007: * modify it under the terms of the GNU General Public License
008: * as published by the Free Software Foundation; either version 2
009: * of the License, or (at your option) any later version.
010: *
011: * This program is distributed in the hope that it will be useful,
012: * but WITHOUT ANY WARRANTY; without even the implied warranty of
013: * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
014: * GNU General Public License for more details.
015: *
016: * You should have received a copy of the GNU General Public License
017: * along with this program; if not, write to the Free Software
018: * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
019: */
020:
021: package ca.forklabs.media.jai.operator;
022:
023: import java.awt.Point;
024: import java.awt.RenderingHints;
025: import java.awt.image.RenderedImage;
026: import java.awt.image.SampleModel;
027: import java.awt.image.renderable.ParameterBlock;
028: import java.util.Arrays;
029: import javax.media.jai.JAI;
030: import javax.media.jai.OperationDescriptor;
031: import javax.media.jai.OperationDescriptorImpl;
032: import javax.media.jai.ParameterBlockJAI;
033: import javax.media.jai.RenderedOp;
034: import javax.media.jai.iterator.RandomIter;
035: import javax.media.jai.registry.RenderedRegistryMode;
036: import javax.media.jai.util.Range;
037: import ca.forklabs.baselib.util.BinaryFunction;
038:
039: /**
040: * Class {@code KMeansDescriptor} is an {@link OperationDescriptor} describing
041: * the <em>kmeans</em> operation. This operation performs applies the
042: * <a href="http://en.wikipedia.org/wiki/K-means_algorithm">k-means</a> on the
043: * given source images.
044: * <p>
045: * The <em>kmeans</em> operation takes two parameter: - the number of cluster,
046: * - and optional evaluation function (defaults to taking the color components
047: * for the vector to partition), - an optional number of iteration (defaults to
048: * <em>100</em>) and - an optional color map (defaults to the center of the
049: * cluster).
050: *
051: * <table border=1>
052: * <caption>Resource List</caption>
053: * <tr><th>Name</th> <th>Value</th></tr>
054: * <tr><td>GlobalName</td> <td>kmeans</td></tr>
055: * <tr><td>LocalName</td> <td>kmeans</td></tr>
056: * <tr><td>Vendor</td> <td>ca.forklabs.media.jai.opimage</td></tr>
057: * <tr><td>Description</td> <td>K-means algorithm</td></tr>
058: * <tr><td>DocURL</td> <td>n/a</td></tr>
059: * <tr><td>Version</td> <td>$Version$</td></tr>
060: * <tr><td>Arg0Desct</td> <td>The number of clusters</td></tr>
061: * <tr><td>Arg1Desct</td> <td>The evaluation function (optional)</td></tr>
062: * <tr><td>Arg2Desct</td> <td>The maximum number of iteration (optional)</td></tr>
063: * <tr><td>Arg3Desct</td> <td>A new color map (optional)</td></tr>
064: * </table>
065: *
066: * <table border=1>
067: * <caption>Parameter List</caption>
068: * <tr><th>Name</th> <th>Class Type</th> <th>Default Value</th></tr>
069: * <tr><td>clusters</td> <td>Integer</td> <td>NO_PARAMETER_DEFAULT</td>
070: * <tr><td>function</td> <td>KMeansDescriptor.EvaluationFunction</td> <td>new KMeansDescriptor.ColorEvaluationFunction</td>
071: * <tr><td>iterations</td> <td>Integer</td> <td>100</td>
072: * <tr><td>colormap</td> <td>int[][]</td> <td>the cluster order</td>
073: * </table>
074: *
075: * This operators works in {@code rendered} mode only.
076: *
077: * @author <a href="mailto:forklabs at dev.java.net?subject=ca.forklabs.media.jai.operator.KMeansDescriptor">Daniel Léonard</a>
078: * @version $Revision: 1.2 $
079: */
080: public class KMeansDescriptor extends OperationDescriptorImpl {
081:
082: //---------------------------
083: // Inner class
084: //---------------------------
085:
086: /**
087: * Interface {@code EvaluationFunction} represents an evaluation function
088: * that gives a vector to partition for the given pixel.
089: *
090: * @author <a href="mailto:forklabs at dev.java.net?subject=ca.forklabs.media.jai.operator.KMeansDescriptor$EvaluationFunction">Daniel Léonard</a>
091: * @version $Revision: 1.2 $
092: */
093: public static interface EvaluationFunction extends
094: BinaryFunction<double[], RandomIter, Point> {
095:
096: /**
097: * Gives the vector to clusterize. Values that must be retained between
098: * calls to this method are copied or cloned by the implementation,
099: * meaning that the same array can be returned call after call.
100: * @param iterator a random iterator on all the pixels of the source
101: * image.
102: * @param position the coordinate of the pixel.
103: * @return the vector.
104: */
105: public double[] invoke(RandomIter iterator, Point position);
106:
107: }
108:
109: /**
110: * Class {@code ColorEvaluationFunction} extracts the color components and
111: * gives them as the vector to partition.
112: *
113: * @author <a href="mailto:forklabs at dev.java.net?subject=ca.forklabs.media.jai.operator.KMeansDescriptor$ColorEvaluationFunction">Daniel Léonard</a>
114: * @version $Revision: 1.2 $
115: */
116: public static class ColorEvaluationFunction implements
117: EvaluationFunction {
118: /** The array to extract the pixel. */
119: private double[] color = null;
120:
121: /**
122: * Extracts the color component of the given pixel and gives them as the
123: * vector to partition.
124: * @param iterator a random iterator on all the pixels of the source
125: * image.
126: * @param position the coordinate of the pixel.
127: * @return the vector.
128: */
129: @Override
130: public double[] invoke(RandomIter iterator, Point position) {
131: int x = position.x;
132: int y = position.y;
133:
134: this .color = iterator.getPixel(x, y, this .color);
135:
136: return this .color;
137: }
138:
139: }
140:
141: //---------------------------
142: // Class variables
143: //---------------------------
144:
145: /** <em>serialVersionUID</em>. */
146: private static final long serialVersionUID = 7829097636253278365L;
147:
148: /** The name of this operator. */
149: @SuppressWarnings("nls")
150: public static final String NAME = "kmeans";
151:
152: /** The name of the number of cluster parameter. */
153: @SuppressWarnings("nls")
154: public static final String CLUSTER_PARAMETER_NAME = "clusters";
155: /** The name of the number of evaluation function parameter. */
156: @SuppressWarnings("nls")
157: public static final String EVALUATION_FUNCTION_PARAMETER_NAME = "evaluationFunction";
158: /** The name of the number of iterations parameter. */
159: @SuppressWarnings("nls")
160: public static final String ITERATIONS_PARAMETER_NAME = "iterations";
161: /** The name of the color map parameter. */
162: @SuppressWarnings("nls")
163: public static final String COLOR_MAP_PARAMETER_NAME = "colormap";
164:
165: /** The index in the parameter block of the number of cluster parameter. */
166: public static final int CLUSTER_PARAMETER_INDEX = 0;
167: /** The index in the parameter block of the evaluation function parameter. */
168: public static final int EVALUATION_FUNCTION_PARAMETER_INDEX = 1;
169: /** The index in the parameter block of the iterations parameter. */
170: public static final int ITERATIONS_PARAMETER_INDEX = 2;
171: /** The index in the parameter block of the color map parameter. */
172: public static final int COLOR_MAP_PARAMETER_INDEX = 3;
173:
174: /** The default value for the number of iterations, arbitrarily <em>100</em>. */
175: public static final int ITERATIONS_DEFAULT_VALUE = 100;
176:
177: /**
178: * The resource strings that provide the general documentation and specify
179: * the parameter list for this operation.
180: */
181: @SuppressWarnings("nls")
182: private static final String[][] RESOURCES = {
183: { "GlobalName", KMeansDescriptor.NAME, },
184: { "LocalName", KMeansDescriptor.NAME, },
185: { "Vendor", "ca.forklabs.media.jai.opimage", },
186: { "Description", KMeansDescriptor.getDescription(), },
187: { "DocURL", "n/a", }, { "Version", "$Version$", },
188: { "arg0Desc", KMeansDescriptor.getArg0Description(), },
189: { "arg1Desc", KMeansDescriptor.getArg1Description(), },
190: { "arg2Desc", KMeansDescriptor.getArg2Description(), },
191: { "arg3Desc", KMeansDescriptor.getArg3Description(), }, };
192:
193: /** The supported modes. */
194: private static final String[] SUPPORTED_MODES = { RenderedRegistryMode.MODE_NAME, };
195:
196: /** The parameter class list for this operation. */
197: private static final Class<?>[] PARAMETER_CLASSES = new Class<?>[] {
198: Integer.class, EvaluationFunction.class, Integer.class,
199: int[][].class, };
200:
201: /** The parameter name list for this operation. */
202: private static final String[] PARAMETER_NAMES = new String[] {
203: KMeansDescriptor.CLUSTER_PARAMETER_NAME,
204: KMeansDescriptor.EVALUATION_FUNCTION_PARAMETER_NAME,
205: KMeansDescriptor.ITERATIONS_PARAMETER_NAME,
206: KMeansDescriptor.COLOR_MAP_PARAMETER_NAME, };
207:
208: /** The default parameters. */
209: @SuppressWarnings({"boxing","static-access"})
210: private static final Object[] PARAMETER_DEFAULTS = new Object[] {
211: KMeansDescriptor.NO_PARAMETER_DEFAULT, null,
212: KMeansDescriptor.ITERATIONS_DEFAULT_VALUE, null, };
213:
214: /** Valid parameter values. */
215: @SuppressWarnings("boxing")
216: private static final Object[] VALID_PARAMETERS = new Object[] {
217: new Range(Integer.class, 1, Integer.MAX_VALUE), null,
218: new Range(Integer.class, 1, Integer.MAX_VALUE), null, };
219:
220: //---------------------------
221: // Constructor
222: //---------------------------
223:
224: /**
225: * Constructor.
226: */
227: public KMeansDescriptor() {
228: super (RESOURCES, SUPPORTED_MODES, 1, PARAMETER_NAMES,
229: PARAMETER_CLASSES, PARAMETER_DEFAULTS, VALID_PARAMETERS);
230: }
231:
232: //---------------------------
233: // Instance methods
234: //---------------------------
235:
236: /**
237: * Creates the default color map.
238: * @param clusters the number of clusters.
239: * @param bands the number of bands.
240: * @return the default color map.
241: */
242: protected int[][] createDefaultColorMap(int clusters, int bands) {
243: int[][] color_map = new int[clusters][bands];
244: for (int c = 0; c < clusters; c++) {
245: Arrays.fill(color_map[c], c);
246: }
247: return color_map;
248: }
249:
250: /**
251: * Validates the evaluation function. That is, put the default evaluation
252: * function in the parameter block if the value of the parameter is
253: * {@code null}.
254: * @param mode ignored.
255: * @param pb the parameter block.
256: * @param sb ignored.
257: * @return always {@code true}.
258: */
259: @SuppressWarnings("unused")
260: protected boolean validateEvaluationFunction(String mode,
261: ParameterBlock pb, StringBuffer sb) {
262: // if no evaluation function is supplied,
263: // put the default evaluation function
264: EvaluationFunction function = (EvaluationFunction) pb
265: .getObjectParameter(KMeansDescriptor.EVALUATION_FUNCTION_PARAMETER_INDEX);
266: if (null == function) {
267: function = new ColorEvaluationFunction();
268: pb
269: .set(
270: function,
271: KMeansDescriptor.EVALUATION_FUNCTION_PARAMETER_INDEX);
272: }
273: return true;
274: }
275:
276: /**
277: * Validates the color map. It checks that the color map has the same number
278: * of color as the number of clusters and that each color has the same number
279: * of components as the source image.
280: * @param mode ignored.
281: * @param pb the parameter block.
282: * @param sb the error message buffer.
283: * @return {@code true} if the color map is correct,
284: * {@code false} otherwise.
285: */
286: @SuppressWarnings("unused")
287: protected boolean validateColorMap(String mode, ParameterBlock pb,
288: StringBuffer sb) {
289: boolean all_ok = true;
290:
291: int expected_clusters = pb
292: .getIntParameter(KMeansDescriptor.CLUSTER_PARAMETER_INDEX);
293:
294: RenderedImage source = (RenderedImage) pb.getSource(0);
295: SampleModel sample_model = source.getSampleModel();
296: int expected_bands = sample_model.getNumBands();
297:
298: // if no color map exist, create
299: // the default color map
300: int[][] color_map = (int[][]) pb
301: .getObjectParameter(KMeansDescriptor.COLOR_MAP_PARAMETER_INDEX);
302: if (null == color_map) {
303: color_map = this .createDefaultColorMap(expected_clusters,
304: expected_bands);
305: pb.set(color_map,
306: KMeansDescriptor.COLOR_MAP_PARAMETER_INDEX);
307: }
308:
309: // check that the number of color
310: // in the color map matches the
311: // number of cluster: one color per cluster
312: int color_map_clusters = color_map.length;
313: all_ok = (expected_clusters == color_map_clusters);
314: if (false == all_ok) {
315: String message = this .getClustersDoNotMatchErrorMessage(
316: expected_clusters, color_map_clusters);
317: sb.append(message);
318: return all_ok;
319: }
320:
321: // check that each color in the color
322: // map have the same number of components
323: int color_map_bands = color_map[0].length;
324: for (int i = 1, len = color_map.length; i < len; i++) {
325: all_ok = (color_map[i].length == color_map_bands);
326: if (false == all_ok) {
327: String message = this
328: .getColorMapNotRectangularErrorMessage(
329: color_map_bands, color_map[i].length);
330: sb.append(message);
331: return all_ok;
332: }
333: }
334:
335: // and finally, check that the color
336: // have the expected number of components
337: all_ok = (expected_bands == color_map_bands);
338: if (false == all_ok) {
339: String message = this .getBandsDoNotMatchErrorMessage(
340: expected_bands, color_map_bands);
341: sb.append(message);
342: return all_ok;
343: }
344:
345: return all_ok;
346: }
347:
348: //---------------------------
349: // Overriden methods from javax.media.jai.OperationDescriptorImpl
350: //---------------------------
351:
352: /**
353: * Validates that the <em>colormap</em> parameter matches the number of bands
354: * in the image and the number of clusters.
355: * @param mode the rendering mode.
356: * @param pb the parameter block.
357: * @param sb the error message buffer.
358: * @return {@code true} if all is OK, {@code false} otherwise.
359: */
360: @Override
361: @SuppressWarnings("boxing")
362: public boolean validateParameters(String mode, ParameterBlock pb,
363: StringBuffer sb) {
364: boolean all_ok = super .validateParameters(mode, pb, sb);
365: if (false == all_ok) {
366: return all_ok;
367: }
368:
369: all_ok = this .validateEvaluationFunction(mode, pb, sb);
370: if (false == all_ok) {
371: return all_ok;
372: }
373:
374: all_ok = this .validateColorMap(mode, pb, sb);
375: if (false == all_ok) {
376: return all_ok;
377: }
378:
379: return all_ok;
380: }
381:
382: //---------------------------
383: // Class methods
384: //---------------------------
385:
386: /**
387: * Creates and fills a new parameter block.
388: * @param mode the rendering mode.
389: * @param source the source image.
390: * @param clusters the number of clusters.
391: * @param function the evaluation function.
392: * @param iterations the maximum number of iterations (<em>0</em> will
393: * use the default value).
394: * @param colormap the color map ({@code null} will use the default color
395: * map).
396: * @return a new parameter block.
397: */
398: public static ParameterBlockJAI createParameterBlock(String mode,
399: Object source, int clusters,
400: KMeansDescriptor.EvaluationFunction function,
401: int iterations, int[][] colormap) {
402: String name = KMeansDescriptor.NAME;
403: ParameterBlockJAI pb = new ParameterBlockJAI(name, mode);
404: if (null != source) {
405: pb.addSource(source);
406: }
407: pb.setParameter(KMeansDescriptor.CLUSTER_PARAMETER_NAME,
408: clusters);
409: pb.setParameter(
410: KMeansDescriptor.EVALUATION_FUNCTION_PARAMETER_NAME,
411: function);
412: if (0 != iterations) {
413: pb.setParameter(KMeansDescriptor.ITERATIONS_PARAMETER_NAME,
414: iterations);
415: }
416: if (null != colormap) {
417: pb.setParameter(KMeansDescriptor.COLOR_MAP_PARAMETER_NAME,
418: colormap);
419: }
420: return pb;
421: }
422:
423: /**
424: * Creates a rendered image.
425: * @param source the source image.
426: * @param clusters the number of clusters.
427: * @param function the evaluation function.
428: * @param iterations the maximum number of iterations (<em>0</em> will
429: * use the default value).
430: * @param colormap the color map ({@code null} will use the default color
431: * map).
432: * @param hints the rendering hints, may be {@code null}.
433: * @return the new image.
434: */
435: public static RenderedOp create(RenderedImage source, int clusters,
436: KMeansDescriptor.EvaluationFunction function,
437: int iterations, int[][] colormap, RenderingHints hints) {
438: String operation = KMeansDescriptor.NAME;
439: String mode = RenderedRegistryMode.MODE_NAME;
440: ParameterBlockJAI pb = KMeansDescriptor.createParameterBlock(
441: mode, source, clusters, function, iterations, colormap);
442: RenderedOp image = JAI.create(operation, pb, hints);
443: return image;
444: }
445:
446: //---------------------------
447: // External resources methods
448: //---------------------------
449:
450: /**
451: * Gets the description of this operation.
452: * @return the description message.
453: */
454: protected static String getDescription() {
455: String key = Resources.KMEANS_DESCRIPTION;
456: String message = Resources.getLocalizedString(key);
457: return message;
458: }
459:
460: /**
461: * Gets the description for the first argument, the number of clusters.
462: * @return the description message.
463: */
464: protected static String getArg0Description() {
465: String key = Resources.KMEANS_ARG0_DESCRIPTION;
466: String message = Resources.getLocalizedString(key);
467: return message;
468: }
469:
470: protected static String getArg1Description() {
471: String key = Resources.KMEANS_ARG1_DESCRIPTION;
472: String message = Resources.getLocalizedString(key);
473: return message;
474: }
475:
476: /**
477: * Gets the description for the second argument, the maximum number of
478: * iterations.
479: * @return the description message.
480: */
481: protected static String getArg2Description() {
482: String key = Resources.KMEANS_ARG2_DESCRIPTION;
483: String message = Resources.getLocalizedString(key);
484: return message;
485: }
486:
487: /**
488: * Gets the description for the third argument, the color map.
489: * @return the description message.
490: */
491: protected static String getArg3Description() {
492: String key = Resources.KMEANS_ARG3_DESCRIPTION;
493: String message = Resources.getLocalizedString(key);
494: return message;
495: }
496:
497: /**
498: * Gets the error message telling that the number of clusters in the color
499: * map do not match the number of clusters.
500: * @param expected the number of clusters.
501: * @param got the number of clusters in the color map.
502: * @return the error message.
503: */
504: @SuppressWarnings("boxing")
505: protected String getClustersDoNotMatchErrorMessage(int expected,
506: int got) {
507: String key = Resources.KMEANS_BAD_CLUSTERS;
508: String message = Resources.getLocalizedString(key, expected,
509: got);
510: return message;
511: }
512:
513: /**
514: * Gets the error message telling that not all colors in the color map have
515: * the same number of bands.
516: * @param expected one number of bands.
517: * @param got another number of bands.
518: * @return the error message.
519: */
520: @SuppressWarnings("boxing")
521: protected String getColorMapNotRectangularErrorMessage(
522: int expected, int got) {
523: String key = Resources.KMEANS_COLOR_MAP_NOT_RECTANGULAR;
524: String message = Resources.getLocalizedString(key, expected,
525: got);
526: return message;
527: }
528:
529: /**
530: * Gets the error message telling that the number of bands in the color
531: * map do not match the number of bands in the source image.
532: * @param expected the number of bands in the image.
533: * @param got the number of bands in the color map.
534: * @return the error message.
535: */
536: @SuppressWarnings("boxing")
537: protected String getBandsDoNotMatchErrorMessage(int expected,
538: int got) {
539: String key = Resources.KMEANS_BAD_BANDS;
540: String message = Resources.getLocalizedString(key, expected,
541: got);
542: return message;
543: }
544:
545: }
546:
547: /*
548: * $Log: KMeansDescriptor.java,v $
549: * Revision 1.2 2007/09/07 18:09:41 forklabs
550: * Refactored K-Means to add a new parameter, the evaluation function.
551: *
552: * Revision 1.1 2007/08/16 21:25:51 forklabs
553: * Operator kmeans.
554: *
555: */
|