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}