001/*- 002 ******************************************************************************* 003 * Copyright (c) 2011, 2016 Diamond Light Source Ltd. 004 * All rights reserved. This program and the accompanying materials 005 * are made available under the terms of the Eclipse Public License v1.0 006 * which accompanies this distribution, and is available at 007 * http://www.eclipse.org/legal/epl-v10.html 008 * 009 * Contributors: 010 * Peter Chang - initial API and implementation and/or initial documentation 011 *******************************************************************************/ 012 013package org.eclipse.january.dataset; 014 015import java.util.Arrays; 016 017/** 018 * Class to hold colour datasets as red, green, blue tuples of short integers 019 */ 020public class RGBDataset extends CompoundShortDataset implements Cloneable { 021 // pin UID to base class 022 private static final long serialVersionUID = Dataset.serialVersionUID; 023 024 private static final int ISIZE = 3; // number of elements per item 025 026 @Override 027 public int getDType() { 028 return Dataset.RGB; 029 } 030 031 /** 032 * Create a null dataset 033 */ 034 public RGBDataset() { 035 super(ISIZE); 036 } 037 038 public RGBDataset(final int... shape) { 039 super(ISIZE, shape); 040 } 041 042 public RGBDataset(final short[] data, final int... shape) { 043 super(ISIZE, data, shape); 044 } 045 046 /** 047 * Copy a dataset 048 * @param dataset 049 */ 050 public RGBDataset(final RGBDataset dataset) { 051 super(dataset); 052 } 053 054 @Override 055 public RGBDataset clone() { 056 return new RGBDataset(this); 057 } 058 059 /** 060 * Create a dataset using given data (red, green and blue parts are given separately) 061 * @param redData 062 * @param greenData 063 * @param blueData 064 * @param shape (can be null to create 1D dataset) 065 */ 066 public RGBDataset(final int[] redData, final int[] greenData, final int[] blueData, int... shape) { 067 int dsize = redData.length > greenData.length ? greenData.length : redData.length; 068 dsize = dsize > blueData.length ? blueData.length : dsize; 069 if (shape == null || shape.length == 0) { 070 shape = new int[] {dsize}; 071 } 072 isize = ISIZE; 073 size = ShapeUtils.calcSize(shape); 074 if (size != dsize) { 075 logger.error("Shape is not compatible with size of data array"); 076 throw new IllegalArgumentException("Shape is not compatible with size of data array"); 077 } 078 this.shape = shape.clone(); 079 080 try { 081 odata = data = createArray(size); 082 } catch (Throwable t) { 083 logger.error("Could not create a dataset of shape {}", Arrays.toString(shape), t); 084 throw new IllegalArgumentException(t); 085 } 086 087 for (int i = 0, n = 0; i < size; i++) { 088 data[n++] = (short) redData[i]; 089 data[n++] = (short) greenData[i]; 090 data[n++] = (short) blueData[i]; 091 } 092 } 093 094 /** 095 * Create a dataset using given data (red, green and blue parts are given separately) 096 * @param redData 097 * @param greenData 098 * @param blueData 099 * @param shape (can be null to create 1D dataset) 100 */ 101 public RGBDataset(final short[] redData, final short[] greenData, final short[] blueData, int... shape) { 102 int dsize = redData.length > greenData.length ? greenData.length : redData.length; 103 dsize = dsize > blueData.length ? blueData.length : dsize; 104 if (shape == null || shape.length == 0) { 105 shape = new int[] {dsize}; 106 } 107 isize = ISIZE; 108 size = ShapeUtils.calcSize(shape); 109 if (size != dsize) { 110 logger.error("Shape is not compatible with size of data array"); 111 throw new IllegalArgumentException("Shape is not compatible with size of data array"); 112 } 113 this.shape = shape.clone(); 114 115 try { 116 odata = data = createArray(size); 117 } catch (Throwable t) { 118 logger.error("Could not create a dataset of shape {}", Arrays.toString(shape), t); 119 throw new IllegalArgumentException(t); 120 } 121 122 for (int i = 0, n = 0; i < size; i++) { 123 data[n++] = redData[i]; 124 data[n++] = greenData[i]; 125 data[n++] = blueData[i]; 126 } 127 } 128 129 /** 130 * Create a dataset using given data (red, green and blue parts are given separately) 131 * @param redData 132 * @param greenData 133 * @param blueData 134 * @param shape (can be null to create 1D dataset) 135 */ 136 public RGBDataset(final byte[] redData, final byte[] greenData, final byte[] blueData, int... shape) { 137 int dsize = redData.length > greenData.length ? greenData.length : redData.length; 138 dsize = dsize > blueData.length ? blueData.length : dsize; 139 if (shape == null || shape.length == 0) { 140 shape = new int[] {dsize}; 141 } 142 isize = ISIZE; 143 size = ShapeUtils.calcSize(shape); 144 if (size != dsize) { 145 logger.error("Shape is not compatible with size of data array"); 146 throw new IllegalArgumentException("Shape is not compatible with size of data array"); 147 } 148 this.shape = shape.clone(); 149 150 try { 151 odata = data = createArray(size); 152 } catch (Throwable t) { 153 logger.error("Could not create a dataset of shape {}", Arrays.toString(shape), t); 154 throw new IllegalArgumentException(t); 155 } 156 157 for (int i = 0, n = 0; i < size; i++) { 158 data[n++] = (short) (0xff & redData[i]); 159 data[n++] = (short) (0xff & greenData[i]); 160 data[n++] = (short) (0xff & blueData[i]); 161 } 162 } 163 164 /** 165 * Create a dataset using given colour data (colour components are given separately) 166 * @param red 167 * @param green 168 * @param blue 169 */ 170 public RGBDataset(final Dataset red, final Dataset green, final Dataset blue) { 171 super(ISIZE, red.getShapeRef()); 172 red.checkCompatibility(green); 173 red.checkCompatibility(blue); 174 175 if (red.max().doubleValue() > Short.MAX_VALUE || red.min().doubleValue() < Short.MIN_VALUE || 176 green.max().doubleValue() > Short.MAX_VALUE || green.min().doubleValue() < Short.MIN_VALUE || 177 blue.max().doubleValue() > Short.MAX_VALUE || blue.min().doubleValue() < Short.MIN_VALUE) { 178 logger.warn("Some values are out of range and will be "); 179 } 180 181 IndexIterator riter = red.getIterator(); 182 IndexIterator giter = green.getIterator(); 183 IndexIterator biter = blue.getIterator(); 184 185 for (int i = 0; riter.hasNext() && giter.hasNext() && biter.hasNext();) { 186 data[i++] = (short) red.getElementLongAbs(riter.index); 187 data[i++] = (short) green.getElementLongAbs(riter.index); 188 data[i++] = (short) blue.getElementLongAbs(riter.index); 189 } 190 } 191 192 /** 193 * Create a dataset using given grey data 194 * @param grey 195 */ 196 public RGBDataset(final Dataset grey) { 197 super(ISIZE, grey.getShapeRef()); 198 199 IndexIterator giter = grey.getIterator(); 200 201 for (int i = 0; giter.hasNext();) { 202 final short g = (short) grey.getElementLongAbs(giter.index); 203 data[i++] = g; 204 data[i++] = g; 205 data[i++] = g; 206 } 207 } 208 209 /** 210 * Create a RGB dataset from an object which could be a Java list, array (of arrays...) or Number. Ragged 211 * sequences or arrays are padded with zeros. The item size is the last dimension of the corresponding 212 * elemental dataset 213 * 214 * @param obj 215 * @return dataset with contents given by input 216 */ 217 public static RGBDataset createFromObject(final Object obj) { 218 CompoundShortDataset result = (CompoundShortDataset) DatasetUtils.createCompoundDataset(ShortDataset.createFromObject(obj), ISIZE); 219 return new RGBDataset(result.data, result.shape); 220 } 221 222 /** 223 * Create a RGB dataset from a compound dataset (no normalisation performed) 224 * @param a 225 * @return RGB dataset (grey if input dataset has less than 3 elements per item) 226 */ 227 public static RGBDataset createFromCompoundDataset(final CompoundDataset a) { 228 if (a instanceof RGBDataset) 229 return (RGBDataset) a; 230 final int is = a.getElementsPerItem(); 231 if (is < 3) { 232 return new RGBDataset(a); 233 } 234 235 if (a instanceof CompoundShortDataset && is == 3) { 236 return new RGBDataset((short[]) a.getBuffer(), a.getShapeRef()); 237 } 238 239 final RGBDataset rgb = new RGBDataset(a.getShapeRef()); 240 final IndexIterator it = a.getIterator(); 241 242 int n = 0; 243 while (it.hasNext()) { 244 rgb.data[n++] = (short) a.getElementLongAbs(it.index); 245 rgb.data[n++] = (short) a.getElementLongAbs(it.index + 1); 246 rgb.data[n++] = (short) a.getElementLongAbs(it.index + 2); 247 } 248 249 return rgb; 250 } 251 252 /** 253 * Create a RGB dataset from hue, saturation and value dataset 254 * @param hue (in degrees from -360 to 360) 255 * @param saturation (from 0 to 1), can be null to denote 1 256 * @param value (from 0 to 1) 257 * @return RGB dataset 258 */ 259 public static RGBDataset createFromHSV(final Dataset hue, final Dataset saturation, final Dataset value) { 260 if ((saturation != null && !hue.isCompatibleWith(saturation)) || !hue.isCompatibleWith(value)) { 261 throw new IllegalArgumentException("Hue, saturation and value datasets must have the same shape"); 262 } 263 264 RGBDataset result = new RGBDataset(hue.getShapeRef()); 265 IndexIterator it = result.getIterator(true); 266 int[] pos = it.getPos(); 267 short[] rgb = new short[3]; 268 269 if (saturation == null) { 270 while (it.hasNext()) { 271 convertHSVToRGB(hue.getDouble(pos), 1, value.getDouble(pos), rgb); 272 result.setAbs(it.index, rgb); 273 } 274 } else { 275 while (it.hasNext()) { 276 convertHSVToRGB(hue.getDouble(pos), saturation.getDouble(pos), value.getDouble(pos), rgb); 277 result.setAbs(it.index, rgb); 278 } 279 } 280 281 return result; 282 } 283 284 /** 285 * Create a RGB dataset from hue, saturation and lightness dataset 286 * @param hue (in degrees from -360 to 360) 287 * @param saturation (from 0 to 1), can be null to denote 1 288 * @param lightness (from 0 to 1) 289 * @return RGB dataset 290 */ 291 public static RGBDataset createFromHSL(final Dataset hue, final Dataset saturation, final Dataset lightness) { 292 if ((saturation != null && !hue.isCompatibleWith(saturation)) || !hue.isCompatibleWith(lightness)) { 293 throw new IllegalArgumentException("Hue, saturation and lightness datasets must have the same shape"); 294 } 295 296 RGBDataset result = new RGBDataset(hue.getShapeRef()); 297 IndexIterator it = result.getIterator(true); 298 int[] pos = it.getPos(); 299 short[] rgb = new short[3]; 300 301 if (saturation == null) { 302 while (it.hasNext()) { 303 convertHSLToRGB(hue.getDouble(pos), 1, lightness.getDouble(pos), rgb); 304 result.setAbs(it.index, rgb); 305 } 306 } else { 307 while (it.hasNext()) { 308 convertHSLToRGB(hue.getDouble(pos), saturation.getDouble(pos), lightness.getDouble(pos), rgb); 309 result.setAbs(it.index, rgb); 310 } 311 } 312 313 return result; 314 } 315 316 private static void convertHSVToRGB(double h, double s, double v, short[] rgb) { 317 double m = 255 * v; 318 double chroma = s * m; 319 m -= chroma; 320 double hprime = h / 60.; 321 if (hprime < 0) { 322 hprime += 6; 323 } 324 short sx = (short) (chroma * (1 - Math.abs((hprime % 2) - 1)) + m); 325 short sc = (short) (chroma + m); 326 short sm = (short) m; 327 328 if (hprime < 1) { 329 rgb[0] = sc; 330 rgb[1] = sx; 331 rgb[2] = sm; 332 } else if (hprime < 2) { 333 rgb[0] = sx; 334 rgb[1] = sc; 335 rgb[2] = sm; 336 } else if (hprime < 3) { 337 rgb[0] = sm; 338 rgb[1] = sc; 339 rgb[2] = sx; 340 } else if (hprime < 4) { 341 rgb[0] = sm; 342 rgb[1] = sx; 343 rgb[2] = sc; 344 } else if (hprime < 5) { 345 rgb[0] = sx; 346 rgb[1] = sm; 347 rgb[2] = sc; 348 } else if (hprime < 6) { 349 rgb[0] = sc; 350 rgb[1] = sm; 351 rgb[2] = sx; 352 } else { // if hue is outside domain 353 rgb[0] = sm; 354 rgb[1] = sm; 355 rgb[2] = sm; 356 } 357 } 358 359 private static void convertHSLToRGB(double h, double s, double l, short[] rgb) { 360 double m = l; 361 double chroma = s * (1 - Math.abs(2 * m - 1)); 362 m -= chroma * 0.5; 363 m *= 255; 364 chroma *= 255; 365 double hprime = h / 60.; 366 if (hprime < 0) { 367 hprime += 6; 368 } 369 short sx = (short) (chroma * (1 - Math.abs((hprime % 2) - 1)) + m); 370 short sc = (short) (chroma + m); 371 short sm = (short) m; 372 373 if (hprime < 1) { 374 rgb[0] = sc; 375 rgb[1] = sx; 376 rgb[2] = sm; 377 } else if (hprime < 2) { 378 rgb[0] = sx; 379 rgb[1] = sc; 380 rgb[2] = sm; 381 } else if (hprime < 3) { 382 rgb[0] = sm; 383 rgb[1] = sc; 384 rgb[2] = sx; 385 } else if (hprime < 4) { 386 rgb[0] = sm; 387 rgb[1] = sx; 388 rgb[2] = sc; 389 } else if (hprime < 5) { 390 rgb[0] = sx; 391 rgb[1] = sm; 392 rgb[2] = sc; 393 } else if (hprime < 6) { 394 rgb[0] = sc; 395 rgb[1] = sm; 396 rgb[2] = sx; 397 } else { // if hue is outside domain 398 rgb[0] = sm; 399 rgb[1] = sm; 400 rgb[2] = sm; 401 } 402 } 403 404 @Override 405 public RGBDataset getSlice(SliceIterator siter) { 406 CompoundShortDataset base = super.getSlice(siter); 407 408 RGBDataset slice = new RGBDataset(); 409 copyToView(base, slice, false, false); 410 slice.setData(); 411 return slice; 412 } 413 414 @Override 415 public RGBDataset getView(boolean deepCopyMetadata) { 416 RGBDataset view = new RGBDataset(); 417 copyToView(this, view, true, deepCopyMetadata); 418 view.setData(); 419 return view; 420 } 421 422 /** 423 * @return red value in the first position 424 * @since 2.0 425 */ 426 public short getRed() { 427 return data[getFirst1DIndex()]; 428 } 429 430 /** 431 * @param i 432 * @return red value in given position 433 */ 434 public short getRed(final int i) { 435 return data[get1DIndex(i)]; 436 } 437 438 /** 439 * @param i 440 * @param j 441 * @return red value in given position 442 */ 443 public short getRed(final int i, final int j) { 444 return data[get1DIndex(i, j)]; 445 } 446 447 /** 448 * @param pos 449 * @return red value in given position 450 */ 451 public short getRed(final int... pos) { 452 return data[get1DIndex(pos)]; 453 } 454 455 /** 456 * @return green value in the first position 457 * @since 2.0 458 */ 459 public short getGreen() { 460 return data[getFirst1DIndex() + 1]; 461 } 462 463 /** 464 * @param i 465 * @return green value in given position 466 */ 467 public short getGreen(final int i) { 468 return data[get1DIndex(i) + 1]; 469 } 470 471 /** 472 * @param i 473 * @param j 474 * @return green value in given position 475 */ 476 public short getGreen(final int i, final int j) { 477 return data[get1DIndex(i, j) + 1]; 478 } 479 480 /** 481 * @param pos 482 * @return green value in given position 483 */ 484 public short getGreen(final int... pos) { 485 return data[get1DIndex(pos) + 1]; 486 } 487 488 /** 489 * @return blue value in the first position 490 * @since 2.0 491 */ 492 public short getBlue() { 493 return data[getFirst1DIndex() + 2]; 494 } 495 496 /** 497 * @param i 498 * @return blue value in given position 499 */ 500 public short getBlue(final int i) { 501 return data[get1DIndex(i) + 2]; 502 } 503 504 /** 505 * @param i 506 * @param j 507 * @return blue value in given position 508 */ 509 public short getBlue(final int i, final int j) { 510 return data[get1DIndex(i, j) + 2]; 511 } 512 513 /** 514 * @param pos 515 * @return blue value in given position 516 */ 517 public short getBlue(final int... pos) { 518 return data[get1DIndex(pos) + 2]; 519 } 520 521 /** 522 * Get a red value from given absolute index as a short - note this index does not 523 * take in account the item size so be careful when using with multi-element items 524 * 525 * @param n 526 * @return red value 527 */ 528 public short getRedAbs(int n) { 529 return data[n*isize]; 530 } 531 532 /** 533 * Get a green value from given absolute index as a short - note this index does not 534 * take in account the item size so be careful when using with multi-element items 535 * 536 * @param n 537 * @return green value 538 */ 539 public short getGreenAbs(int n) { 540 return data[n*isize + 1]; 541 } 542 543 /** 544 * Get a blue value from given absolute index as a short - note this index does not 545 * take in account the item size so be careful when using with multi-element items 546 * 547 * @param n 548 * @return blue value 549 */ 550 public short getBlueAbs(int n) { 551 return data[n*isize + 2]; 552 } 553 554 555 // weights from NTSC formula aka ITU-R BT.601 for mapping RGB to luma 556 private static final double Wr = 0.299, Wg = 0.587, Wb = 0.114; 557 558 /** 559 * Convert colour dataset to a grey-scale one using the NTSC formula, aka ITU-R BT.601, for RGB to luma mapping 560 * @param clazz 561 * @return a grey-scale dataset of given type 562 * @since 2.1 563 */ 564 public <T extends Dataset> T createGreyDataset(final T clazz) { 565 return (T) createGreyDataset(clazz, Wr, Wg, Wb); 566 } 567 568 /** 569 * Convert colour dataset to a grey-scale one using given RGB to luma mapping 570 * @param clazz 571 * @param red weight 572 * @param green weight 573 * @param blue weight 574 * @param dtype 575 * @return a grey-scale dataset of given class 576 * @since 2.1 577 */ 578 @SuppressWarnings("unchecked") 579 public <T extends Dataset> T createGreyDataset(final T clazz, final double red, final double green, final double blue) { 580 return (T) createGreyDataset(red, green, blue, DTypeUtils.getDType(clazz)); 581 } 582 583 /** 584 * Convert colour dataset to a grey-scale one using the NTSC formula, aka ITU-R BT.601, for RGB to luma mapping 585 * @param dtype 586 * @return a grey-scale dataset of given class 587 * @deprecated Use {@link RGBDataset#createGreyDataset(Class)} 588 */ 589 @Deprecated 590 public Dataset createGreyDataset(final int dtype) { 591 return createGreyDataset(Wr, Wg, Wb, dtype); 592 } 593 594 /** 595 * Convert colour dataset to a grey-scale one using given RGB to luma mapping 596 * @param red weight 597 * @param green weight 598 * @param blue weight 599 * @param dtype 600 * @return a grey-scale dataset of given type 601 * @deprecated Use {@link RGBDataset#createGreyDataset(Class, double, double, double)} 602 */ 603 @Deprecated 604 public Dataset createGreyDataset(final double red, final double green, final double blue, final int dtype) { 605 final Dataset grey = DatasetFactory.zeros(shape, dtype); 606 final IndexIterator it = getIterator(); 607 608 int i = 0; 609 while (it.hasNext()) { 610 grey.setObjectAbs(i++, red*data[it.index] + green*data[it.index + 1] + blue*data[it.index + 2]); 611 } 612 return grey; 613 } 614 615 /** 616 * Extract red colour channel 617 * @param clazz 618 * @return a dataset of given class 619 * @since 2.1 620 */ 621 public <T extends Dataset> T createRedDataset(final T clazz) { 622 return createColourChannelDataset(0, clazz, "red"); 623 } 624 625 /** 626 * Extract green colour channel 627 * @param clazz 628 * @return a dataset of given class 629 * @since 2.1 630 */ 631 public <T extends Dataset> T createGreenDataset(final T clazz) { 632 return createColourChannelDataset(1, clazz, "green"); 633 } 634 635 /** 636 * Extract blue colour channel 637 * @param clazz 638 * @return a dataset of given class 639 * @since 2.1 640 */ 641 public <T extends Dataset> T createBlueDataset(final T clazz) { 642 return createColourChannelDataset(2, clazz, "blue"); 643 } 644 645 /** 646 * Extract red colour channel 647 * @param dtype 648 * @return a dataset of given type 649 * @deprecated Use {@link RGBDataset#createRedDataset(Class)} 650 */ 651 @Deprecated 652 public Dataset createRedDataset(final int dtype) { 653 return createColourChannelDataset(0, dtype, "red"); 654 } 655 656 /** 657 * Extract green colour channel 658 * @param dtype 659 * @return a dataset of given type 660 * @deprecated Use {@link RGBDataset#createGreenDataset(Class)} 661 */ 662 @Deprecated 663 public Dataset createGreenDataset(final int dtype) { 664 return createColourChannelDataset(1, dtype, "green"); 665 } 666 667 /** 668 * Extract blue colour channel 669 * @param dtype 670 * @return a dataset of given type 671 * @deprecated Use {@link RGBDataset#createBlueDataset(Class)} 672 */ 673 @Deprecated 674 public Dataset createBlueDataset(final int dtype) { 675 return createColourChannelDataset(2, dtype, "blue"); 676 } 677 678 @SuppressWarnings("unchecked") 679 private <T extends Dataset> T createColourChannelDataset(int channelOffset, T clazz, String cName) { 680 return (T) createColourChannelDataset(channelOffset, DTypeUtils.getDType(clazz), cName); 681 } 682 683 684 @Deprecated 685 private Dataset createColourChannelDataset(final int channelOffset, final int dtype, final String cName) { 686 final Dataset channel = DatasetFactory.zeros(shape, dtype); 687 688 final StringBuilder cname = name == null ? new StringBuilder() : new StringBuilder(name); 689 if (cname.length() > 0) { 690 cname.append('.'); 691 } 692 cname.append(cName); 693 channel.setName(cname.toString()); 694 695 final IndexIterator it = getIterator(); 696 697 int i = 0; 698 while (it.hasNext()) { 699 channel.setObjectAbs(i++, data[it.index + channelOffset]); 700 } 701 702 return channel; 703 } 704 705 /** 706 * @return red view 707 */ 708 public ShortDataset getRedView() { 709 return getColourChannelView(0, "red"); 710 } 711 712 /** 713 * @return green view 714 */ 715 public ShortDataset getGreenView() { 716 return getColourChannelView(1, "green"); 717 } 718 719 /** 720 * @return blue view 721 */ 722 public ShortDataset getBlueView() { 723 return getColourChannelView(2, "blue"); 724 } 725 726 private ShortDataset getColourChannelView(final int channelOffset, final String cName) { 727 ShortDataset view = getElements(channelOffset); 728 view.setName(cName); 729 return view; 730 } 731 732 @Override 733 public Number max(boolean... ignored) { 734 short max = Short.MIN_VALUE; 735 final IndexIterator it = getIterator(); 736 737 while (it.hasNext()) { 738 for (int i = 0; i < ISIZE; i++) { 739 final short value = data[it.index + i]; 740 if (value > max) 741 max = value; 742 } 743 } 744 return max; 745 } 746 747 @Override 748 public Number min(boolean... ignored) { 749 short max = Short.MAX_VALUE; 750 final IndexIterator it = getIterator(); 751 752 while (it.hasNext()) { 753 for (int i = 0; i < ISIZE; i++) { 754 final short value = data[it.index + i]; 755 if (value < max) 756 max = value; 757 } 758 } 759 return max; 760 } 761}