    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 dtype
    561         * @return a grey-scale dataset of given type
    562         */
    563        public Dataset createGreyDataset(final int dtype) {
    564                return createGreyDataset(Wr, Wg, Wb, dtype);
    565        }
    566
    567        /**
    568         * Convert colour dataset to a grey-scale one using given RGB to luma mapping
    569         * @param red weight
    570         * @param green weight
    571         * @param blue weight
    572         * @param dtype
    573         * @return a grey-scale dataset of given type
    574         */
    575        public Dataset createGreyDataset(final double red, final double green, final double blue, final int dtype) {
    576                @SuppressWarnings("deprecation")
    577                final Dataset grey = DatasetFactory.zeros(shape, dtype);
    578                final IndexIterator it = getIterator();
    579
    580                int i = 0;
    581                while (it.hasNext()) {
    582                        grey.setObjectAbs(i++, red*data[it.index] + green*data[it.index + 1] + blue*data[it.index + 2]);
    583                }
    584                return grey;
    585        }
    586
    587        /**
    588         * Extract red colour channel
    589         * @param dtype
    590         * @return a dataset of given type
    591         */
    592        public Dataset createRedDataset(final int dtype) {
    593                return createColourChannelDataset(0, dtype, "red");
    594        }
    595
    596        /**
    597         * Extract green colour channel
    598         * @param dtype
    599         * @return a dataset of given type
    600         */
    601        public Dataset createGreenDataset(final int dtype) {
    602                return createColourChannelDataset(1, dtype, "green");
    603        }
    604
    605        /**
    606         * Extract blue colour channel
    607         * @param dtype
    608         * @return a dataset of given type
    609         */
    610        public Dataset createBlueDataset(final int dtype) {
    611                return createColourChannelDataset(2, dtype, "blue");
    612        }
    613
    614        private Dataset createColourChannelDataset(final int channelOffset, final int dtype, final String cName) {
    615                @SuppressWarnings("deprecation")
    616                final Dataset channel = DatasetFactory.zeros(shape, dtype);
    617
    618                final StringBuilder cname = name == null ? new StringBuilder() : new StringBuilder(name);
    619                if (cname.length() > 0) {
    620                        cname.append('.');
    621                }
    622                cname.append(cName);
    623                channel.setName(cname.toString());
    624
    625                final IndexIterator it = getIterator();
    626
    627                int i = 0;
    628                while (it.hasNext()) {
    629                        channel.setObjectAbs(i++, data[it.index + channelOffset]);
    630                }
    631
    632                return channel;
    633        }
    634
    635        /**
    636         * @return red view
    637         */
    638        public ShortDataset getRedView() {
    639                return getColourChannelView(0, "red");
    640        }
    641
    642        /**
    643         * @return green view
    644         */
    645        public ShortDataset getGreenView() {
    646                return getColourChannelView(1, "green");
    647        }
    648
    649        /**
    650         * @return blue view
    651         */
    652        public ShortDataset getBlueView() {
    653                return getColourChannelView(2, "blue");
    654        }
    655
    656        private ShortDataset getColourChannelView(final int channelOffset, final String cName) {
    657                ShortDataset view = getElements(channelOffset);
    658                view.setName(cName);
    659                return view;
    660        }
    661
    662        @Override
    663        public Number max(boolean... ignored) {
    664                short max = Short.MIN_VALUE;
    665                final IndexIterator it = getIterator();
    666
    667                while (it.hasNext()) {
    668                        for (int i = 0; i < ISIZE; i++) {
    669                                final short value = data[it.index + i];
    670                                if (value > max)
    671                                        max = value;
    672                        }
    673                }
    674                return max;
    675        }
    676
    677        @Override
    678        public Number min(boolean... ignored) {
    679                short max = Short.MAX_VALUE;
    680                final IndexIterator it = getIterator();
    681
    682                while (it.hasNext()) {
    683                        for (int i = 0; i < ISIZE; i++) {
    684                                final short value = data[it.index + i];
    685                                if (value < max)
    686                                        max = value;
    687                        }
    688                }
    689                return max;
    690        }
    691}