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.2 563 */ 564 public <T extends Dataset> T createGreyDataset(final Class<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 * @return a grey-scale dataset of given class 575 * @since 2.2 576 */ 577 @SuppressWarnings("unchecked") 578 public <T extends Dataset> T createGreyDataset(final Class<T> clazz, final double red, final double green, final double blue) { 579 return (T) createGreyDataset(red, green, blue, DTypeUtils.getDType(clazz)); 580 } 581 582 /** 583 * Convert colour dataset to a grey-scale one using the NTSC formula, aka ITU-R BT.601, for RGB to luma mapping 584 * @param dtype 585 * @return a grey-scale dataset of given class 586 * @deprecated Use {@link RGBDataset#createGreyDataset(Class)} 587 */ 588 @Deprecated 589 public Dataset createGreyDataset(final int dtype) { 590 return createGreyDataset(Wr, Wg, Wb, dtype); 591 } 592 593 /** 594 * Convert colour dataset to a grey-scale one using given RGB to luma mapping 595 * @param red weight 596 * @param green weight 597 * @param blue weight 598 * @param dtype 599 * @return a grey-scale dataset of given type 600 * @deprecated Use {@link RGBDataset#createGreyDataset(Class, double, double, double)} 601 */ 602 @Deprecated 603 public Dataset createGreyDataset(final double red, final double green, final double blue, final int dtype) { 604 final Dataset grey = DatasetFactory.zeros(shape, dtype); 605 final IndexIterator it = getIterator(); 606 607 int i = 0; 608 while (it.hasNext()) { 609 grey.setObjectAbs(i++, red*data[it.index] + green*data[it.index + 1] + blue*data[it.index + 2]); 610 } 611 return grey; 612 } 613 614 /** 615 * Extract red colour channel 616 * @param clazz 617 * @return a dataset of given class 618 * @since 2.1 619 */ 620 public <T extends Dataset> T createRedDataset(final T clazz) { 621 return createColourChannelDataset(0, clazz, "red"); 622 } 623 624 /** 625 * Extract green colour channel 626 * @param clazz 627 * @return a dataset of given class 628 * @since 2.1 629 */ 630 public <T extends Dataset> T createGreenDataset(final T clazz) { 631 return createColourChannelDataset(1, clazz, "green"); 632 } 633 634 /** 635 * Extract blue colour channel 636 * @param clazz 637 * @return a dataset of given class 638 * @since 2.1 639 */ 640 public <T extends Dataset> T createBlueDataset(final T clazz) { 641 return createColourChannelDataset(2, clazz, "blue"); 642 } 643 644 /** 645 * Extract red colour channel 646 * @param dtype 647 * @return a dataset of given type 648 * @deprecated Use {@link #createRedDataset} 649 */ 650 @Deprecated 651 public Dataset createRedDataset(final int dtype) { 652 return createColourChannelDataset(0, dtype, "red"); 653 } 654 655 /** 656 * Extract green colour channel 657 * @param dtype 658 * @return a dataset of given type 659 * @deprecated Use {@link #createGreenDataset} 660 */ 661 @Deprecated 662 public Dataset createGreenDataset(final int dtype) { 663 return createColourChannelDataset(1, dtype, "green"); 664 } 665 666 /** 667 * Extract blue colour channel 668 * @param dtype 669 * @return a dataset of given type 670 * @deprecated Use {@link #createBlueDataset} 671 */ 672 @Deprecated 673 public Dataset createBlueDataset(final int dtype) { 674 return createColourChannelDataset(2, dtype, "blue"); 675 } 676 677 @SuppressWarnings("unchecked") 678 private <T extends Dataset> T createColourChannelDataset(int channelOffset, T clazz, String cName) { 679 return (T) createColourChannelDataset(channelOffset, DTypeUtils.getDType(clazz), cName); 680 } 681 682 683 @Deprecated 684 private Dataset createColourChannelDataset(final int channelOffset, final int dtype, final String cName) { 685 final Dataset channel = DatasetFactory.zeros(shape, dtype); 686 687 final StringBuilder cname = name == null ? new StringBuilder() : new StringBuilder(name); 688 if (cname.length() > 0) { 689 cname.append('.'); 690 } 691 cname.append(cName); 692 channel.setName(cname.toString()); 693 694 final IndexIterator it = getIterator(); 695 696 int i = 0; 697 while (it.hasNext()) { 698 channel.setObjectAbs(i++, data[it.index + channelOffset]); 699 } 700 701 return channel; 702 } 703 704 /** 705 * @return red view 706 */ 707 public ShortDataset getRedView() { 708 return getColourChannelView(0, "red"); 709 } 710 711 /** 712 * @return green view 713 */ 714 public ShortDataset getGreenView() { 715 return getColourChannelView(1, "green"); 716 } 717 718 /** 719 * @return blue view 720 */ 721 public ShortDataset getBlueView() { 722 return getColourChannelView(2, "blue"); 723 } 724 725 private ShortDataset getColourChannelView(final int channelOffset, final String cName) { 726 ShortDataset view = getElements(channelOffset); 727 view.setName(cName); 728 return view; 729 } 730 731 @Override 732 public Number max(boolean... ignored) { 733 short max = Short.MIN_VALUE; 734 final IndexIterator it = getIterator(); 735 736 while (it.hasNext()) { 737 for (int i = 0; i < ISIZE; i++) { 738 final short value = data[it.index + i]; 739 if (value > max) 740 max = value; 741 } 742 } 743 return max; 744 } 745 746 @Override 747 public Number min(boolean... ignored) { 748 short max = Short.MAX_VALUE; 749 final IndexIterator it = getIterator(); 750 751 while (it.hasNext()) { 752 for (int i = 0; i < ISIZE; i++) { 753 final short value = data[it.index + i]; 754 if (value < max) 755 max = value; 756 } 757 } 758 return max; 759 } 760}