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 }