001    /*
002     * AutoDetectColorType
003     * 
004     * Copyright (c) 2001, 2002, 2003 Marco Schmidt.
005     * All rights reserved.
006     */
007    
008    package net.sourceforge.jiu.color.reduction;
009    
010    import net.sourceforge.jiu.color.data.Histogram3D;
011    import net.sourceforge.jiu.color.data.OnDemandHistogram3D;
012    import net.sourceforge.jiu.data.BilevelImage;
013    import net.sourceforge.jiu.data.Gray16Image;
014    import net.sourceforge.jiu.data.Gray8Image;
015    import net.sourceforge.jiu.data.IntegerImage;
016    import net.sourceforge.jiu.data.MemoryBilevelImage;
017    import net.sourceforge.jiu.data.MemoryGray8Image;
018    import net.sourceforge.jiu.data.MemoryPaletted8Image;
019    import net.sourceforge.jiu.data.MemoryRGB24Image;
020    import net.sourceforge.jiu.data.Palette;
021    import net.sourceforge.jiu.data.Paletted8Image;
022    import net.sourceforge.jiu.data.PixelImage;
023    import net.sourceforge.jiu.data.RGB24Image;
024    import net.sourceforge.jiu.data.RGB48Image;
025    import net.sourceforge.jiu.data.RGBIndex;
026    import net.sourceforge.jiu.data.RGBIntegerImage;
027    import net.sourceforge.jiu.ops.MissingParameterException;
028    import net.sourceforge.jiu.ops.Operation;
029    import net.sourceforge.jiu.ops.WrongParameterException;
030    
031    /**
032     * Detects the minimum (in terms of memory) color type of an image.
033     * Can convert the original image to that new input type on demand.
034     * <p>
035     * Input parameters: image to be examined, boolean that specifies whether
036     * conversion will be performed (default is true, conversion is performed).
037     * Output parameters: converted image, boolean that expresses whether
038     * a conversion was possible.
039     * <p>
040     * Supported types for input image: RGB24Image, Gray8Image, Paletted8Image.
041     * <p>
042     * BilevelImage is not supported because there is no smaller image type,
043     * so bilevel images cannot be reduced.
044     * <p>
045     * This operation is not a {@link net.sourceforge.jiu.ops.ImageToImageOperation} because this 
046     * class need not necessarily produce a new image 
047     * (with {@link #setConversion}(false)).
048     * <h3>Usage example</h3>
049     * This code snippet loads an image and attempts to reduce it to the
050     * minimum color type that will hold it.
051     * <pre>
052     * PixelImage image = ImageLoader.load("test.bmp");
053     * AutoDetectColorType op = new AutoDetectColorType();
054     * op.setInputImage(image);
055     * op.process();
056     * if (op.isReducible())
057     * {
058     *   image = op.getOutputImage();
059     * }
060     * </pre>
061     *
062     * @author Marco Schmidt
063     */
064    public class AutoDetectColorType extends Operation
065    {
066            public static final int TYPE_UNKNOWN = -1;
067            public static final int TYPE_BILEVEL = 0;
068            public static final int TYPE_GRAY16 = 4;
069            public static final int TYPE_GRAY8 = 1;
070            public static final int TYPE_PALETTED8 = 2;
071            public static final int TYPE_RGB24 = 3;
072            public static final int TYPE_RGB48 = 5;
073    
074            private PixelImage inputImage;
075            private PixelImage outputImage;
076            private boolean doConvert;
077            private int type;
078            private Histogram3D hist;
079    
080            public AutoDetectColorType()
081            {
082                    doConvert = true;
083                    type = TYPE_UNKNOWN;
084            }
085    
086            /**
087             * Creates a bilevel image from any grayscale (or RGB) image
088             * that has been checked to be bilevel.
089             */
090            private void createBilevelFromGrayOrRgb(IntegerImage in)
091            {
092                    MemoryBilevelImage out = new MemoryBilevelImage(in.getWidth(), in.getHeight());
093                    out.clear(BilevelImage.BLACK);
094                    for (int y = 0; y < in.getHeight(); y++)
095                    {
096                            for (int x = 0; x < in.getWidth(); x++)
097                            {
098                                    if (in.getSample(0, x, y) != 0)
099                                    {
100                                            out.putWhite(x, y);
101                                    }
102                            }
103                            setProgress(y, in.getHeight());
104                    }
105                    outputImage = out;
106            }
107    
108            private void createBilevelFromPaletted(Paletted8Image in)
109            {
110                    Palette palette = in.getPalette();
111                    MemoryBilevelImage out = new MemoryBilevelImage(in.getWidth(), in.getHeight());
112                    out.clear(BilevelImage.BLACK);
113                    for (int y = 0; y < in.getHeight(); y++)
114                    {
115                            for (int x = 0; x < in.getWidth(); x++)
116                            {
117                                    if (palette.getSample(RGBIndex.INDEX_RED, in.getSample(0, x, y)) != 0)
118                                    {
119                                            out.putWhite(x, y);
120                                    }
121                            }
122                            setProgress(y, in.getHeight());
123                    }
124                    outputImage = out;
125            }
126    
127            // works for RGB24 and RGB48 input image, assumed that
128            // a matching output image type was chosen (Gray8 for RGB24, Gray16 for
129            // RGB48)
130            private void createGrayFromRgb(IntegerImage in, IntegerImage out)
131            {
132                    for (int y = 0; y < in.getHeight(); y++)
133                    {
134                            for (int x = 0; x < in.getWidth(); x++)
135                            {
136                                    out.putSample(0, x, y, in.getSample(0, x, y));
137                            }
138                            setProgress(y, in.getHeight());
139                    }
140                    outputImage = out;
141            }
142    
143            private void createGray8FromGray16(Gray16Image in)
144            {
145                    Gray8Image out = new MemoryGray8Image(in.getWidth(), in.getHeight());
146                    for (int y = 0; y < in.getHeight(); y++)
147                    {
148                            for (int x = 0; x < in.getWidth(); x++)
149                            {
150                                    out.putSample(0, x, y, in.getSample(0, x, y) & 0xff);
151                            }
152                            setProgress(y, in.getHeight());
153                    }
154                    outputImage = out;
155            }
156    
157            // assumes that in fact has a palette with gray colors only
158            private void createGray8FromPaletted8(Paletted8Image in, Gray8Image out)
159            {
160                    Palette palette = in.getPalette();
161                    for (int y = 0; y < in.getHeight(); y++)
162                    {
163                            for (int x = 0; x < in.getWidth(); x++)
164                            {
165                                    out.putSample(0, x, y, palette.getSample(0, in.getSample(0, x, y)));
166                            }
167                            setProgress(y, in.getHeight());
168                    }
169                    outputImage = out;
170            }
171    
172            private void createPaletted8FromRgb24(RGB24Image in)
173            {
174                    // create palette from histogram
175                    int uniqueColors = hist.getNumUsedEntries();
176                    Palette palette = new Palette(uniqueColors, 255);
177                    int index = 0;
178                    for (int r = 0; r < 256; r++)
179                    {
180                            for (int g = 0; g < 256; g++)
181                            {
182                                    for (int b = 0; b < 256; b++)
183                                    {
184                                            if (hist.getEntry(r, g, b) != 0)
185                                            {
186                                                    hist.setEntry(r, g, b, index);
187                                                    palette.putSample(RGBIndex.INDEX_RED, index, r);
188                                                    palette.putSample(RGBIndex.INDEX_GREEN, index, g);
189                                                    palette.putSample(RGBIndex.INDEX_BLUE, index, b);
190                                                    index++;
191                                            }
192                                    }
193                            }
194                    }
195                    Paletted8Image out = new MemoryPaletted8Image(in.getWidth(), in.getHeight(), palette);
196                    for (int y = 0; y < in.getHeight(); y++)
197                    {
198                            for (int x = 0; x < in.getWidth(); x++)
199                            {
200                                    int red = in.getSample(RGBIndex.INDEX_RED, x, y);
201                                    int green = in.getSample(RGBIndex.INDEX_GREEN, x, y);
202                                    int blue = in.getSample(RGBIndex.INDEX_BLUE, x, y);
203                                    out.putSample(0, x, y, hist.getEntry(red, green, blue));
204                            }
205                            setProgress(y, in.getHeight());
206                    }
207                    outputImage = out;
208            }
209    
210            private void createPaletted8FromRgb48(RGB48Image in)
211            {
212                    // create palette from histogram
213                    int uniqueColors = hist.getNumUsedEntries();
214                    Palette palette = new Palette(uniqueColors, 255);
215                    int index = 0;
216                    for (int r = 0; r < 256; r++)
217                    {
218                            for (int g = 0; g < 256; g++)
219                            {
220                                    for (int b = 0; b < 256; b++)
221                                    {
222                                            if (hist.getEntry(r, g, b) != 0)
223                                            {
224                                                    hist.setEntry(r, g, b, index);
225                                                    palette.putSample(RGBIndex.INDEX_RED, index, r);
226                                                    palette.putSample(RGBIndex.INDEX_GREEN, index, g);
227                                                    palette.putSample(RGBIndex.INDEX_BLUE, index, b);
228                                                    index++;
229                                            }
230                                    }
231                            }
232                    }
233                    Paletted8Image out = new MemoryPaletted8Image(in.getWidth(), in.getHeight(), palette);
234                    for (int y = 0; y < in.getHeight(); y++)
235                    {
236                            for (int x = 0; x < in.getWidth(); x++)
237                            {
238                                    int red = in.getSample(RGBIndex.INDEX_RED, x, y) >> 8;
239                                    int green = in.getSample(RGBIndex.INDEX_GREEN, x, y) >> 8;
240                                    int blue = in.getSample(RGBIndex.INDEX_BLUE, x, y) >> 8;
241                                    out.putSample(0, x, y, hist.getEntry(red, green, blue));
242                            }
243                            setProgress(y, in.getHeight());
244                    }
245                    outputImage = out;
246            }
247    
248            private void createRgb24FromRgb48(RGB48Image in, RGB24Image out)
249            {
250                    for (int y = 0; y < in.getHeight(); y++)
251                    {
252                            for (int x = 0; x < in.getWidth(); x++)
253                            {
254                                    out.putSample(RGBIndex.INDEX_RED, x, y, in.getSample(RGBIndex.INDEX_RED, x, y) >> 8);
255                                    out.putSample(RGBIndex.INDEX_GREEN, x, y, in.getSample(RGBIndex.INDEX_GREEN, x, y) >> 8);
256                                    out.putSample(RGBIndex.INDEX_BLUE, x, y, in.getSample(RGBIndex.INDEX_BLUE, x, y) >> 8);
257                            }
258                            setProgress(y, in.getHeight());
259                    }
260                    outputImage = out;
261            }
262    
263    
264            /**
265             * Returns the reduced output image if one was created in {@link #process()}.
266             * @return newly-created output image
267             */
268            public PixelImage getOutputImage()
269            {
270                    return outputImage;
271            }
272    
273            /**
274             * Returns the type of the minimum image type found (one of the TYPE_xyz constants
275             * of this class).
276             * Can only be called after a successful call to process.
277             */
278            public int getType()
279            {
280                    return type;
281            }
282    
283            /**
284             * This method can be called after {@link #process()} to find out if the input
285             * image in fact can be reduced to a "smaller" image type.
286             * If this method returns <code>true</code> and if conversion was desired by the
287             * user (can be specified via {@link #setConversion}), the reduced image can 
288             * be retrieved via {@link #getOutputImage()}.
289             * @return if image was found to be reducible in process()
290             */
291            public boolean isReducible()
292            {
293                    return type != TYPE_UNKNOWN;
294            }
295    
296            // works for Gray8 and Gray16
297            private boolean isGrayBilevel(IntegerImage in)
298            {
299                    final int HEIGHT = in.getHeight();
300                    final int MAX = in.getMaxSample(0);
301                    for (int y = 0; y < HEIGHT; y++)
302                    {
303                            for (int x = 0; x < in.getWidth(); x++)
304                            {
305                                    int value = in.getSample(0, x, y);
306                                    if (value != 0 && value != MAX)
307                                    {
308                                            return false; // not a grayscale image
309                                    }
310                            }
311                    }
312                    return true;
313            }
314    
315            private boolean isGray16Gray8(Gray16Image in)
316            {
317                    final int HEIGHT = in.getHeight();
318                    final int MAX = in.getMaxSample(0);
319                    for (int y = 0; y < HEIGHT; y++)
320                    {
321                            for (int x = 0; x < in.getWidth(); x++)
322                            {
323                                    int value = in.getSample(0, x, y);
324                                    int lsb = value & 0xff;
325                                    int msb = (value >> 8) & 0xff;
326                                    if (lsb != msb)
327                                    {
328                                            return false;
329                                    }
330                            }
331                    }
332                    return true;
333            }
334    
335            private boolean isRgb48Gray8(RGB48Image in)
336            {
337                    final int HEIGHT = in.getHeight();
338                    for (int y = 0; y < HEIGHT; y++)
339                    {
340                            for (int x = 0; x < in.getWidth(); x++)
341                            {
342                                    int red = in.getSample(RGBIndex.INDEX_RED, x, y);
343                                    int green = in.getSample(RGBIndex.INDEX_GREEN, x, y);
344                                    int blue = in.getSample(RGBIndex.INDEX_BLUE, x, y);
345                                    if (red != green || green != blue)
346                                    {
347                                            return false;
348                                    }
349                                    int lsb = red & 0xff;
350                                    int msb = red >> 8;
351                                    if (lsb != msb)
352                                    {
353                                            return false;
354                                    }
355                            }
356                    }
357                    return true;
358            }
359    
360            /**
361             * Assumes that it has already been verified that the input 48 bpp
362             * RGB image is also a 24 bpp RGB image.
363             * @param in input image to be checked
364             * @return if this image can be losslessly converted to a Paletted8Image
365             */
366            private boolean isRgb48Paletted8(RGB48Image in)
367            {
368                    int uniqueColors = 0;
369                    hist = new OnDemandHistogram3D(255);
370                    for (int y = 0; y < in.getHeight(); y++)
371                    {
372                            for (int x = 0; x < in.getWidth(); x++)
373                            {
374                                    int red = in.getSample(RGBIndex.INDEX_RED, x, y) >> 8;
375                                    int green = in.getSample(RGBIndex.INDEX_GREEN, x, y) >> 8;
376                                    int blue = in.getSample(RGBIndex.INDEX_BLUE, x, y) >> 8;
377                                    if (hist.getEntry(red, green, blue) == 0)
378                                    {
379                                            hist.increaseEntry(red, green, blue);
380                                            uniqueColors++;
381                                            if (uniqueColors > 256)
382                                            {
383                                                    return false;
384                                            }
385                                    }
386                            }
387                    }
388                    return true;
389            }
390    
391            private boolean isRgb48Rgb24(RGB48Image in)
392            {
393                    final int HEIGHT = in.getHeight();
394                    for (int y = 0; y < HEIGHT; y++)
395                    {
396                            for (int x = 0; x < in.getWidth(); x++)
397                            {
398                                    for (int channel = 0; channel < 3; channel++)
399                                    {
400                                            int sample = in.getSample(channel, x, y);
401                                            if ((sample & 0xff) != ((sample & 0xff00) >> 8))
402                                            {
403                                                    return false; 
404                                            }
405                                    }
406                            }
407                    }
408                    return true;
409            }
410    
411            // works for RGB24 and RGB48
412            private boolean isRgbBilevel(IntegerImage in)
413            {
414                    final int HEIGHT = in.getHeight();
415                    final int MAX = in.getMaxSample(0);
416                    for (int y = 0; y < HEIGHT; y++)
417                    {
418                            for (int x = 0; x < in.getWidth(); x++)
419                            {
420                                    int red = in.getSample(RGBIndex.INDEX_RED, x, y);
421                                    int green = in.getSample(RGBIndex.INDEX_GREEN, x, y);
422                                    int blue = in.getSample(RGBIndex.INDEX_BLUE, x, y);
423                                    if (red != green || green != blue || (blue != 0  && blue != MAX))
424                                    {
425                                            return false;
426                                    }
427                            }
428                    }
429                    return true;
430            }
431    
432            /**
433             * Returns if the input RGB image can be losslessly converted to 
434             * a grayscale image.
435             * @param in RGB image to be checked
436             * @return true if input is gray, false otherwise
437             */
438            private boolean isRgbGray(RGBIntegerImage in)
439            {
440                    final int HEIGHT = in.getHeight();
441                    for (int y = 0; y < HEIGHT; y++)
442                    {
443                            for (int x = 0; x < in.getWidth(); x++)
444                            {
445                                    int red = in.getSample(RGBIndex.INDEX_RED, x, y);
446                                    int green = in.getSample(RGBIndex.INDEX_GREEN, x, y);
447                                    int blue = in.getSample(RGBIndex.INDEX_BLUE, x, y);
448                                    if (red != green || green != blue)
449                                    {
450                                            return false;
451                                    }
452                            }
453                    }
454                    return true;
455            }
456    
457            private boolean isRgb24Paletted8(RGB24Image in)
458            {
459                    int uniqueColors = 0;
460                    hist = new OnDemandHistogram3D(255);
461                    for (int y = 0; y < in.getHeight(); y++)
462                    {
463                            for (int x = 0; x < in.getWidth(); x++)
464                            {
465                                    int red = in.getSample(RGBIndex.INDEX_RED, x, y);
466                                    int green = in.getSample(RGBIndex.INDEX_GREEN, x, y);
467                                    int blue = in.getSample(RGBIndex.INDEX_BLUE, x, y);
468                                    if (hist.getEntry(red, green, blue) == 0)
469                                    {
470                                            hist.increaseEntry(red, green, blue);
471                                            uniqueColors++;
472                                            if (uniqueColors > 256)
473                                            {
474                                                    return false;
475                                            }
476                                    }
477                            }
478                    }
479                    return true;
480            }
481    
482            public void process() throws 
483                    MissingParameterException,
484                    WrongParameterException
485            {
486                    if (inputImage == null)
487                    {
488                            throw new MissingParameterException("No input image available");
489                    }
490                    // GRAY8
491                    if (inputImage instanceof Gray8Image)
492                    {
493                            if (isGrayBilevel((Gray8Image)inputImage))
494                            {
495                                    type = TYPE_BILEVEL;
496                                    if (doConvert)
497                                    {
498                                            createBilevelFromGrayOrRgb((Gray8Image)inputImage);
499                                    }
500                            }
501                    }
502                    else
503                    // GRAY16
504                    if (inputImage instanceof Gray16Image)
505                    {
506                            if (isGrayBilevel((Gray16Image)inputImage))
507                            {
508                                    type = TYPE_BILEVEL;
509                                    if (doConvert)
510                                    {
511                                            createBilevelFromGrayOrRgb((Gray16Image)inputImage);
512                                    }
513                            }
514                            else
515                            if (isGray16Gray8((Gray16Image)inputImage))
516                            {
517                                    type = TYPE_GRAY16;
518                                    if (doConvert)
519                                    {
520                                            createGray8FromGray16((Gray16Image)inputImage);
521                                    }
522                            }
523                    }
524                    else
525                    // RGB24
526                    if (inputImage instanceof RGB24Image)
527                    {
528                            if (isRgbBilevel((RGB24Image)inputImage))
529                            {
530                                    type = TYPE_BILEVEL;
531                                    if (doConvert)
532                                    {
533                                            createBilevelFromGrayOrRgb((RGB24Image)inputImage);
534                                    }
535                            }
536                            else
537                            if (isRgbGray((RGB24Image)inputImage))
538                            {
539                                    type = TYPE_GRAY8;
540                                    if (doConvert)
541                                    {
542                                            outputImage = new MemoryGray8Image(inputImage.getWidth(), inputImage.getHeight());
543                                            createGrayFromRgb((RGB24Image)inputImage, (Gray8Image)outputImage);
544                                    }
545                            }
546                            else
547                            if (isRgb24Paletted8((RGB24Image)inputImage))
548                            {
549                                    type = TYPE_PALETTED8;
550                                    if (doConvert)
551                                    {
552                                            createPaletted8FromRgb24((RGB24Image)inputImage);
553                                    }
554                            }
555                    }
556                    else
557                    // RGB48
558                    if (inputImage instanceof RGB48Image)
559                    {
560                            if (isRgbBilevel((RGB48Image)inputImage))
561                            {
562                                    type = TYPE_BILEVEL;
563                                    if (doConvert)
564                                    {
565                                            createBilevelFromGrayOrRgb((RGB48Image)inputImage);
566                                    }
567                            }
568                            else
569                            if (isRgb48Gray8((RGB48Image)inputImage))
570                            {
571                                    type = TYPE_GRAY8;
572                                    if (doConvert)
573                                    {
574                                            outputImage = new MemoryGray8Image(inputImage.getWidth(), inputImage.getHeight());
575                                            // this create method works because it works with int and the least significant 8
576                                            // bits are equal to the most significant 8 bits if isRgb48Gray8 returned true
577                                            createGrayFromRgb((RGB48Image)inputImage, (Gray8Image)outputImage);
578                                    }
579                            }
580                            else
581                            if (isRgbGray((RGB48Image)inputImage))
582                            {
583                                    type = TYPE_GRAY16;
584                                    if (doConvert)
585                                    {
586                                            outputImage = new MemoryGray8Image(inputImage.getWidth(), inputImage.getHeight());
587                                            createGrayFromRgb((RGB24Image)inputImage, (Gray8Image)outputImage);
588                                    }
589                            }
590                            else
591                            if (isRgb48Rgb24((RGB48Image)inputImage))
592                            {
593                                    // RGB48 input is RGB24; is it also Paletted8?
594                                    if (isRgb48Paletted8((RGB48Image)inputImage))
595                                    {
596                                            type = TYPE_PALETTED8;
597                                            if (doConvert)
598                                            {
599                                                    createPaletted8FromRgb48((RGB48Image)inputImage);
600                                            }
601                                    }
602                                    else
603                                    {
604                                            type = TYPE_RGB24;
605                                            if (doConvert)
606                                            {
607                                                    outputImage = new MemoryRGB24Image(inputImage.getWidth(), inputImage.getHeight());
608                                                    createRgb24FromRgb48((RGB48Image)inputImage, (RGB24Image)outputImage);
609                                            }
610                                    }
611                            }
612                    }
613                    else
614                    // PALETTED8
615                    if (inputImage instanceof Paletted8Image)
616                    {
617                            Paletted8Image in = (Paletted8Image)inputImage;
618                            Palette palette = in.getPalette();
619                            if (palette.isBlackAndWhite())
620                            {
621                                    type = TYPE_BILEVEL;
622                                    if (doConvert)
623                                    {
624                                            createBilevelFromPaletted(in);
625                                    }
626                            }
627                            else
628                            if (palette.isGray())
629                            {
630                                    type = TYPE_GRAY8;
631                                    if (doConvert)
632                                    {
633                                            Gray8Image out = new MemoryGray8Image(in.getWidth(), in.getHeight());
634                                            createGray8FromPaletted8(in, out);
635                                    }
636                            }
637                    }
638                    else
639                    {
640                            throw new WrongParameterException("Not a supported or reducible image type: " + inputImage.toString());
641                    }
642            }
643    
644            /**
645             * This method can be used to specify whether the input image is to be converted
646             * to the minimum image type if it is clear that such a conversion is possible.
647             * The default value is <code>true</code>.
648             * If this is set to <code>false</code>, it can still be 
649             * @param convert if true, the conversion will be performed
650             */
651            public void setConversion(boolean convert)
652            {
653                    doConvert = convert;
654            }
655    
656            /**
657             * This method must be used to specify the mandatory input image.
658             * @param image PixelImage object to be examined
659             */
660            public void setInputImage(PixelImage image)
661            {
662                    inputImage = image;
663            }
664    }