ImageProcessor.java
35.4 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
/*
* ESO Archive
*
* $Id: ImageProcessor.java,v 1.3 2009/03/04 11:13:45 abrighto Exp $
*
* who when what
* -------------- ---------- ----------------------------------------
* Allan Brighton 1999/11/17 Created
*/
package jsky.image;
import com.sun.media.jai.codec.ImageCodec;
import java.awt.RenderingHints;
import java.awt.geom.Rectangle2D;
import java.awt.image.DataBuffer;
import java.awt.image.SampleModel;
import java.awt.image.IndexColorModel;
import javax.media.jai.Histogram;
import javax.media.jai.Interpolation;
import javax.media.jai.InterpolationNearest;
import javax.media.jai.JAI;
import javax.media.jai.LookupTableJAI;
import javax.media.jai.PlanarImage;
import javax.media.jai.ROI;
import javax.media.jai.ROIShape;
import javax.media.jai.operator.TransposeDescriptor;
import javax.swing.event.ChangeListener;
import javax.swing.event.EventListenerList;
import jsky.image.fits.codec.FITSCodec;
import jsky.image.fits.codec.FITSImage;
import jsky.image.operator.CutLevelDescriptor;
import jsky.image.operator.ImageOps;
import jsky.image.operator.MinMaxDescriptor;
import jsky.util.gui.DialogUtil;
/**
* Responsible for processing images before they are
* displayed, including setting cut levels and manipulating colors
* and lookup tables.
*
* @version $Revision: 1.3 $
* @author Allan Brighton
*/
public class ImageProcessor {
/** The default number of X pixels to skip for histograms and statistics */
public static final int DEFAULT_X_PERIOD = 4;
/** The default number of Y pixels to skip for histograms and statistics */
public static final int DEFAULT_Y_PERIOD = 4;
// The original source image
private PlanarImage _sourceImage;
// The original source image after applying BZERO and BSCALE, if needed
private PlanarImage _rescaledSourceImage;
// For grayscale images: The source image, scaled to short range (if needed),
// so that a cut levels lookup table may be applied
private PlanarImage _shortImage;
// The color image, after applying any necessary transformations
// (scale, rotate, translate, ...)
private PlanarImage _displayImage;
// If set, this is the region of interest in the image (normally the currently visible area)
private ROI _roi = null;
// If set, this is the region of interest in the image (normally the currently visible area)
private Rectangle2D.Double _region;
// Used to get a histogram of the image data in the visible area.
private ImageHistogram _imageHistogram = new ImageHistogram();
// The number of bands in the source image
private int _numBands;
// The object managing the image colormap
private ImageColormap _colormap = new ImageColormap();
// min and max pixel values
private double _minValue = 0., _maxValue = 0.;
// low and high image cut levels
private double _lowCut = 0., _highCut = 0.;
// The number of X pixels to skip for histograms and statistics
private int _xPeriod = DEFAULT_X_PERIOD;
// The number of Y pixels to skip for histograms and statistics
private int _yPeriod = DEFAULT_Y_PERIOD;
// list of listeners for change events
private EventListenerList _listenerList = new EventListenerList();
// Generic event fired whenever the image is updated
private ImageChangeEvent _imageChangeEvent = new ImageChangeEvent(this);
// True if the user has set the cut levels by hand (then we don't change them)
private boolean _userSetCutLevels = false;
// Rotation angle in radians
private double _angle = 0.0;
// if true, flip the X axis
private boolean _flipX = false;
// if true, flip the Y axis
private boolean _flipY = false;
// if true, reverse the meaning of flipY (for FITS images that were not flipped while reading)
private boolean _reverseY = false;
// Set to true if the Y axis of the (FITS) image was inverted already while reading.
private boolean _invertedYAxis = false;
// type of interpolation to use for rotating
private Interpolation _interpolation = new InterpolationNearest();
// Used to reduce image data to byte range
private LookupTableJAI _scaleLookupTable;
// Algorithm used to scale image to byte range
private int _scaleAlgorithm = ImageLookup.LINEAR_SCALE;
// A name for this object (for diagnostic purposes)
private String _name = "";
// Value for bad pixels
private float _blank = Float.NaN;
// Value of the DATAMIN property, if defined
private double _dataMin = 0.;
// Value of the DATAMAX property, if defined
private double _dataMax = 0.;
// Value of the DATAMEAN property, if defined
private double _dataMean = 0.;
// Value of the BZERO property, if defined
private double _bzero = 0.;
// Value of the BSCALE property, if defined
private double _bscale = 1.;
// Value of the BITPIX property, if defined
private double _bitpix = 1.;
// Set to true if something was changed and a call to update() is needed.
private boolean _updatePending = false;
// static initializer
static {
// make sure we have the correct JAI version
String expectedJAIVersion = "jai-1_1";
try {
// this method is only available starting with jai-1_1 - allow newer versions
JAI.getBuildVersion();
} catch (NoSuchMethodError e) {
DialogUtil.error("Error: Incompatible JAI (Java Advanced Imaging) version. Expected "
+ expectedJAIVersion);
System.exit(1);
}
// add FITS support
ImageCodec.registerCodec(new FITSCodec());
// used in some cases to get the min and max pixel values
MinMaxDescriptor.register();
// used to calculate the image cut levels
CutLevelDescriptor.register();
}
/**
* Default constructor.
*
* Call setSourceImage(PlanarImage, Rectangle2D.Double) to set the image to process
* and the region of interest.
*/
public ImageProcessor() {
}
/**
* Constructor.
*
* @param sourceImage The source image to process.
* @param region the region of interest in the image (usually the visible area)
* in the coordinates of the source image (values will be clipped).
*/
public ImageProcessor(PlanarImage sourceImage, Rectangle2D.Double region) {
setSourceImage(sourceImage, region);
}
/** Returns the current source image. */
public PlanarImage getSourceImage() {
return _sourceImage;
}
/** Returns the current source image, after applying BZERO and BSCALE, if needed. */
public PlanarImage getRescaledSourceImage() {
return _rescaledSourceImage;
}
/**
* Set the source image and copy all of the image processing settings from the
* given ImageProcessor object. This method may be used, for example, for a pan window
* that displays a separate, prescaled image, that should have the same
* image processing settings as the main image.
*/
public void setSourceImage(PlanarImage sourceImage, ImageProcessor imageProcessor) {
this._sourceImage = sourceImage;
_updatePending = true;
if (_sourceImage == null) {
_displayImage = _rescaledSourceImage = _sourceImage;
return;
}
SampleModel sampleModel = _sourceImage.getSampleModel();
if (sampleModel == null) {
return;
}
_numBands = sampleModel.getNumBands();
// copy the settings from the master image processor
if (imageProcessor != this)
copySettings(imageProcessor);
// apply bscale and bzero if needed
_rescaledSourceImage = rescaleImage(_sourceImage);
// get image into short range
ImageLookup imageLookup = new ImageLookup();
_shortImage = imageLookup.scaleToShortRange(_rescaledSourceImage, _lowCut, _highCut);
}
/**
* Set the source image and the region of interest and perform any requested
* image processing to make the display image.
*
* @param region the region of interest in the image (usually the visible area)
* in the coordinates of the source image.
*/
public void setSourceImage(PlanarImage sourceImage, Rectangle2D.Double region) {
this._sourceImage = sourceImage;
_updatePending = true;
if (_sourceImage == null) {
_displayImage = _rescaledSourceImage = _sourceImage;
return;
}
SampleModel sampleModel = _sourceImage.getSampleModel();
if (sampleModel == null) {
return;
}
_numBands = sampleModel.getNumBands();
_minValue = _maxValue = 0.;
// if it is a FITS file, reverse the Y axis
// (XXX should be done in FITS codec? But transpose is accelerated in JAI)
Object o = _sourceImage.getProperty("#fits_image");
_reverseY = false;
_invertedYAxis = false;
if (o instanceof FITSImage) {
FITSImage fitsImage = (FITSImage) o;
_invertedYAxis = fitsImage.isYFlipped();
_reverseY = !_invertedYAxis;
// check for BZERO and BSCALE keywords and, if needed, apply the values
_bzero = fitsImage.getKeywordValue("BZERO", 0.);
_bscale = fitsImage.getKeywordValue("BSCALE", 1.);
_bitpix = fitsImage.getKeywordValue("BITPIX", 0);
_rescaledSourceImage = rescaleImage(_sourceImage);
// check for grayscale images
// (Note: GIF images can have _numBands == 1 and IndexColorModel, which is already color)
if (_numBands == 1 && ! (_sourceImage.getColorModel() instanceof IndexColorModel)) {
// get value of blank or bad pixels
_blank = fitsImage.getKeywordValue("BLANK", Float.NaN);
if (Float.isNaN(_blank))
_blank = fitsImage.getKeywordValue("BADPIXEL", Float.NaN);
if (!Float.isNaN(_blank)) {
// assume blank value needs to be rescaled in the same way as the image
// (the resulting image is float data, so make sure the "blank" value is treated the same)
_blank = _blank * (float) _bscale + (float) _bzero;
}
// min/max pixel values, if specified in image properties
_dataMin = fitsImage.getKeywordValue("DATAMIN", 0.);
_dataMax = fitsImage.getKeywordValue("DATAMAX", 0.);
_dataMean = fitsImage.getKeywordValue("DATAMEAN", Double.NaN);
}
} else {
_bzero = 0.;
_bscale = 1.;
_bitpix = 0;
_blank = Float.NaN;
_dataMin = 0.;
_dataMax = 0.;
_dataMean = Double.NaN;
_rescaledSourceImage = _sourceImage;
}
// check for grayscale images...
if (_numBands == 1 && ! (_sourceImage.getColorModel() instanceof IndexColorModel)) {
// set the image cut levels, preserving user's choice if needed
if (!_userSetCutLevels) {
autoSetCutLevels(region);
// // 95% seems to be a better default than median filter
// autoSetCutLevels(95., region);
} else {
setRegionOfInterest(region);
setCutLevels(_lowCut, _highCut, true);
}
}
}
/** Returns the current display image. */
public PlanarImage getDisplayImage() {
return _displayImage;
}
/**
* Process the source image, adding a lookup table and setting appropriate
* cut levels where needed to improve visibility.
*/
public void update() {
if (!_updatePending)
return;
_updatePending = false;
if (_sourceImage == null) {
_displayImage = _rescaledSourceImage = _sourceImage;
// notify listeners of changes in the image
fireChange(_imageChangeEvent);
return;
}
if (_rescaledSourceImage == null) {
return;
}
try {
PlanarImage colorImage;
if (_numBands == 1 && ! (_sourceImage.getColorModel() instanceof IndexColorModel)) {
if (_shortImage == null || _scaleLookupTable == null) {
return;
}
// perform a lookup operation to scale short to byte range.
// (_shortImage should have been already prepared when cut levels were set)
PlanarImage byteImage = ImageOps.lookup(_shortImage, _scaleLookupTable);
// add a color RGB lookup table so we can manipulate the image colors
colorImage = ImageOps.lookup(byteImage, _colormap.getColorLookupTable());
} else {
// source image is already color
colorImage = _rescaledSourceImage;
}
// apply flipX, flipY, if needed
_displayImage = setTrans(colorImage);
// apply rotation, if needed
_displayImage = rotate(_displayImage);
} catch (Exception e) {
e.printStackTrace();
return;
}
//System.out.println("XXX ImageProcessor: " + _name + ": updateImage()");
// notify listeners of changes in the image
fireChange(_imageChangeEvent);
}
/**
* Rescale the given image, if needed, using the given factor and offset and return the result.
* The resulting image is converted to float, to avoid loosing data.
*/
protected PlanarImage rescaleImage(PlanarImage im) {
if (_bscale != 1. || _bzero != 0.) {
RenderingHints hints = ImageUtil.getSampleModelHint(im.getTileWidth(),
im.getTileHeight(),
DataBuffer.TYPE_FLOAT);
return ImageOps.rescale(im, _bscale, _bzero, hints);
}
return im;
}
/**
* Perform a transpose operation on the given image using the current
* rotate, flipX and flipY settings and return the resulting image.
*/
protected PlanarImage setTrans(PlanarImage im) {
if (_flipX)
im = ImageOps.transpose(im, TransposeDescriptor.FLIP_HORIZONTAL);
if (_flipY != _reverseY) {
im = ImageOps.transpose(im, TransposeDescriptor.FLIP_VERTICAL);
}
return im;
}
/**
* Rotate the image about the center by the specified angle (in radians).
*/
protected PlanarImage rotate(PlanarImage im) {
if (_angle != 0.0F) {
double x = _sourceImage.getWidth() / 2.0;
double y = _sourceImage.getHeight() / 2.0;
im = ImageOps.rotate(im, (float) x, (float) y, (float) _angle, _interpolation, null);
}
return im;
}
/**
* Set the current region of interest in the image (normally the
* currently visible area). This is used by operations that need
* to gather statistics on the image to avoid having to scan the
* entire image.
*
* @param region the region of interest in the image
*/
protected void setRegionOfInterest(Rectangle2D.Double region) {
if (_roi != null && this._region != null && region.equals(this._region))
return;
this._region = region;
// clip to image boundary to form the region of interest
_region = new Rectangle2D.Double((int) _region.getX(), (int) _region.getY(),
(int) _region.getWidth(), (int) _region.getHeight());
_region = (Rectangle2D.Double) _region.createIntersection(new Rectangle2D.Double(_xPeriod, _yPeriod,
_sourceImage.getWidth() - _xPeriod,
_sourceImage.getHeight() - _yPeriod));
_roi = new ROIShape(_region);
}
/**
* Examine the given region of the source image to determine the min
* and max pixel values as well as the default cut levels (using median filter).
* As a result, the minValue, maxValue, lowCut, and highCut member
* variables are set.
* This method should be called before calling getMinValue(), getMaxValue(),
* getLowCut() or getHighCut(). If the same region is specified as in the
* previous call, nothing is done.
*
* @param region the region of interest in the image
*/
protected void calculateImageStatistics(Rectangle2D.Double region) {
// clip to image boundary to form the region of interest
setRegionOfInterest(region);
// Get the min and max pixel values in the region (or for the image, if known)
if (_dataMin != _dataMax) {
// Use property values, if set
_minValue = _dataMin;
_maxValue = _dataMax;
} else if (Float.isNaN(_blank) && _bitpix != -32 && _bitpix != -64) {
// No blank pixel was defined, use extrema operation, which should be faster, since
// it is implemented in native code. The JAI extrema operation doesn't handle NaNs though...
try {
// the extrema op seems to have problems with very small images? (like dummy, blank images?)
double[][] extrema = ImageOps.extrema(_rescaledSourceImage, _roi, _xPeriod, _yPeriod);
_minValue = extrema[0][0];
_maxValue = extrema[1][0];
} catch (Exception e) {
_minValue = _maxValue = 0.;
}
} else {
// A blank pixel was defined, use our MinMax operator
double[] minMax = ImageOps.minMax(_rescaledSourceImage, _roi, _xPeriod, _yPeriod, _blank);
_minValue = minMax[0];
_maxValue = minMax[1];
// the mean is also calculated for float images, to avoid NaN issues
_dataMean = minMax[2];
}
if (_minValue > _maxValue) {
throw new IllegalArgumentException("min value > max value.");
}
// Try to guess the best cut levels for the image
if (Double.isNaN(_dataMean)) {
_dataMean = ImageOps.mean(_rescaledSourceImage, _roi, _xPeriod, _yPeriod)[0];
}
double[] cutLevels = ImageOps.cutLevel(_rescaledSourceImage, _roi, _blank, _dataMean);
_lowCut = cutLevels[0];
_highCut = cutLevels[1];
}
/**
* Copy the settings from the given ImageProcessor to this one.
*/
public void copySettings(ImageProcessor ip) {
// Note: this method tends to be called more often than needed, due to
// change events being fired and propagated. Try to avoid unnecessary
// calls to update() by checking if any of the publicly accessible settings
// have changed.
if (_lowCut != ip._lowCut
|| _highCut != ip._highCut
|| (!_colormap.equals(ip._colormap))
|| _scaleAlgorithm != ip._scaleAlgorithm
|| _flipX != ip._flipX
|| _flipY != ip._flipY
|| _reverseY != ip._reverseY
|| _invertedYAxis != ip._invertedYAxis
|| _angle != ip._angle) {
_updatePending = true;
_minValue = ip._minValue;
_maxValue = ip._maxValue;
_dataMin = ip._minValue;
_dataMax = ip._maxValue;
_dataMean = ip._dataMean;
_blank = ip._blank;
_bzero = ip._bzero;
_bscale = ip._bscale;
_lowCut = ip._lowCut;
_highCut = ip._highCut;
_userSetCutLevels = true;
// clone a shallow copy of the colormap object, so that we can compare
// it later to see if anything changed, and avoid unnecessary updates.
try {
_colormap = (ImageColormap) ip._colormap.clone();
} catch (CloneNotSupportedException e) {
throw new RuntimeException(e); // should not happen
}
_scaleAlgorithm = ip._scaleAlgorithm;
_scaleLookupTable = ip._scaleLookupTable;
_flipX = ip._flipX;
_flipY = ip._flipY;
_reverseY = ip._reverseY;
_invertedYAxis = ip._invertedYAxis;
_angle = ip._angle;
}
}
/**
* Set the image cutoff levels.
*
* @param lowCut the low cut value
* @param highCut the high cut value
*/
public void setCutLevels(double lowCut, double highCut) {
setCutLevels(lowCut, highCut, true);
}
/**
* Set the image cutoff levels.
*
* @param lowCut the low cut value
* @param highCut the high cut value
* @param userSetCutLevels set to true if the cut levels were set by the user
* (meaning they should not be changed automatically)
*/
public void setCutLevels(double lowCut, double highCut, boolean userSetCutLevels) {
if (lowCut > highCut) {
return;
}
this._lowCut = lowCut;
this._highCut = highCut;
_updatePending = true;
this._userSetCutLevels = userSetCutLevels;
// reduce the image data to byte range with a lookup table
ImageLookup imageLookup = new ImageLookup();
_shortImage = imageLookup.scale(_rescaledSourceImage, _scaleAlgorithm, _lowCut, _highCut,
_roi, _imageHistogram);
_scaleLookupTable = imageLookup.getLookupTable();
_imageChangeEvent.setNewCutLevels(true);
}
/**
* Set the low cutoff level
*/
public void setLowCut(double lowCut) {
setCutLevels(lowCut, _highCut, true);
}
/**
* Set the high cutoff level
*/
public void setHighCut(double highCut) {
setCutLevels(_lowCut, highCut, true);
}
/**
* Get the low cutoff level
*/
public double getLowCut() {
return _lowCut;
}
/**
* Get the high cutoff level
*/
public double getHighCut() {
return _highCut;
}
/**
* Set the image cut levels automatically using median filtering on the given
* area of the image.
*
* @param region the region of interest in the image
*/
public void autoSetCutLevels(Rectangle2D.Double region) {
calculateImageStatistics(region);
setCutLevels(_lowCut, _highCut, false);
}
/**
* Set the cut levels so that the given percent of pixels
* in the given region of the image are within the low
* and high cut values.
*
* @param percent value between 0. and 100. indicating percent
* of image pixels within the cut levels.
* @param region the region of interest in the image
*/
public void autoSetCutLevels(double percent, Rectangle2D.Double region) {
_userSetCutLevels = false;
double lowCut = _minValue, highCut = _maxValue;
calculateImageStatistics(region);
// get a histogram of the image data
int dataType = _rescaledSourceImage.getSampleModel().getDataType();
int numBins = 2048;
double n = highCut - lowCut;
if (n <= 0)
return;
if (n < numBins && dataType != DataBuffer.TYPE_FLOAT && dataType != DataBuffer.TYPE_DOUBLE)
numBins = (int) n;
if (numBins <= 0) {
return;
}
Histogram histogram = _imageHistogram.getHistogram(_rescaledSourceImage, numBins, _minValue, _maxValue, _roi, _xPeriod, _yPeriod);
// find out how many pixel we actually counted (may be significant numbers of blanks)
int npixels = 0;
int[] bins = histogram.getBins(0);
double binWidth = (_maxValue - _minValue) / numBins;
for (int i = 0; i < numBins; i++) {
npixels += bins[i];
}
if (npixels > 0) {
// change to percent to cut off and split between lowCut and highCut
int cutoff = (int) ((npixels * (100.0 - percent) / 100.0) / 2.0);
// set low cut value
npixels = 0;
int nprev;
for (int i = 0; i < numBins; i++) {
nprev = npixels;
npixels += bins[i];
if (npixels >= cutoff) {
lowCut = _minValue + i * binWidth;
if (i != 0) {
// Interpolate between the relevant bins.
double interp = (double) (cutoff - nprev) / (npixels - nprev);
double d = _minValue + (i - 1) * binWidth;
lowCut = d + (lowCut - d) * interp;
}
break;
}
}
// set high cut value
npixels = 0;
// nprev = 0;
for (int i = numBins - 1; i > 0; i--) {
nprev = npixels;
npixels += bins[i];
if (npixels >= cutoff) {
highCut = _minValue + i * binWidth;
if (i != numBins - 1) {
// Interpolate between the relevant bins.
double interp = (double) (cutoff - nprev) / (npixels - nprev);
double d = _minValue + (i + 1) * binWidth;
highCut = d + (d - highCut) * interp;
}
break;
}
}
}
setCutLevels(lowCut, highCut, false);
}
/** Return true if the user has set the cut levels and they were not automatically set. */
public boolean isUserSetCutLevels() {
return _userSetCutLevels;
}
/**
* Set to true if the user has set the cut levels and they were not automatically set.
* This has the effect that the cut levels will not be automatically set if a new image
* is loaded.
*/
public void setUserSetCutLevels(boolean b) {
_userSetCutLevels = b;
}
/**
* register to receive change events from this object whenever the
* image or cut levels are changed.
*/
public void addChangeListener(ChangeListener l) {
_listenerList.add(ChangeListener.class, l);
}
/**
* Stop receiving change events from this object.
*/
public void removeChangeListener(ChangeListener l) {
_listenerList.remove(ChangeListener.class, l);
}
/**
* Notify any listeners of a change in the image or cut levels.
*/
protected void fireChange(ImageChangeEvent changeEvent) {
Object[] listeners = _listenerList.getListenerList();
for (int i = listeners.length - 2; i >= 0; i -= 2) {
if (listeners[i] == ChangeListener.class) {
((ChangeListener) listeners[i + 1]).stateChanged(changeEvent);
}
}
changeEvent.reset();
}
/**
* Return the min pixel value in the region specified in the last call
* to calculateImageStatistics.
*/
public double getMinValue() {
return _minValue;
}
/**
* Return the max pixel value in the region specified in the last call
* to calculateImageStatistics.
*/
public double getMaxValue() {
return _maxValue;
}
/**
* Return the value used for bad or blank pixels (taken from "BLANK" image property).
*/
public float getBlank() {
return _blank;
}
/**
* Return the rotation angle.
*/
public double getAngle() {
return _angle;
}
/**
* Set the rotation angle.
*/
public void setAngle(double angle) {
this._angle = angle;
_imageChangeEvent.setNewAngle(true);
_updatePending = true;
}
/** Return the type of interpolation to use (for rotating). */
public Interpolation getInterpolation() {
return _interpolation;
}
/** Set the type of interpolation to use (for rotating). */
public void setInterpolation(Interpolation i) {
_interpolation = i;
_updatePending = true;
}
/**
* Toggle the flipping of the X axis
*/
public void toggleFlipX() {
_flipX = !_flipX;
_imageChangeEvent.setNewAngle(true);
_updatePending = true;
}
/**
* Set the value of the flipX flag.
*/
public void setFlipX(boolean flipX) {
if (this._flipX != flipX) {
this._flipX = flipX;
_imageChangeEvent.setNewAngle(true);
_updatePending = true;
}
}
/**
* Return the value of the flipX flag.
*/
public boolean getFlipX() {
return _flipX;
}
/**
* Toggle the flipping of the Y axis
*/
public void toggleFlipY() {
_flipY = !_flipY;
_imageChangeEvent.setNewAngle(true);
_updatePending = true;
}
/**
* Set the value of the flipY flag.
*/
public void setFlipY(boolean flipY) {
if (this._flipY != flipY) {
this._flipY = flipY;
_imageChangeEvent.setNewAngle(true);
_updatePending = true;
}
}
/**
* Return the value of the flipY flag.
*/
public boolean getFlipY() {
return _flipY;
}
/**
* Set the value of the reverseY flag.
* If this flag is set to true, the value of flipY is reversed (used for FITS images).
*/
public void setReverseY(boolean reverseY) {
if (this._reverseY != reverseY) {
this._reverseY = reverseY;
_imageChangeEvent.setNewAngle(true);
_updatePending = true;
}
}
/**
* Return the value of the reverseY flag.
* If this flag is true, the value of flipY is reversed (used for FITS images).
*/
public boolean getReverseY() {
return _reverseY;
}
/**
* Set to true if the Y axis of the image was inverted while reading.
* <p>
* This means that the image doesn't need to be flipped before displaying,
* but the image coordinate system still needs to be inverted in the Y axis.
*/
public void setInvertedYAxis(boolean invertedYAxis) {
if (_invertedYAxis != invertedYAxis) {
_invertedYAxis = invertedYAxis;
_imageChangeEvent.setNewAngle(true);
_updatePending = true;
}
}
/** Return true if the Y axis of the image was inverted while reading. */
public boolean isInvertedYAxis() {
return _invertedYAxis;
}
/**
* Return a histogram for the image with the given size (number of bins)
* and region of interest and default settings for the other arguments.
*/
public Histogram getHistogram(int size, ROI roi) {
return _imageHistogram.getHistogram(_rescaledSourceImage, size, _lowCut, _highCut, roi, _xPeriod, _yPeriod);
}
/** Set the name to use for this object (for testing and debugging). */
public void setName(String name) {
this._name = name;
}
/** Return the name of this object */
public String getName() {
return _name;
}
/** Return the lookup table used to scale the image to byte range */
public LookupTableJAI getScaleLookupTable() {
return _scaleLookupTable;
}
/**
* Set the algorithm to use to scale the image to byte range.
*
* @param scaleAlgorithm one of the public constants defined in the ImageLookup class
* (default: ImageLookup.LINEAR_SCALE).
*/
public void setScaleAlgorithm(int scaleAlgorithm) {
this._scaleAlgorithm = scaleAlgorithm;
// reduce the image data to byte range with a lookup table
ImageLookup imageLookup = new ImageLookup();
_shortImage = imageLookup.scale(_rescaledSourceImage, _scaleAlgorithm, _lowCut, _highCut, _roi, _imageHistogram);
_scaleLookupTable = imageLookup.getLookupTable();
_imageChangeEvent.setNewColormap(true);
_updatePending = true;
}
/**
* Return the current image scaling algorithm (one of the constants defined in the
* ImageLookup class (default: ImageLookup.LINEAR_SCALE).
*/
public int getScaleAlgorithm() {
return _scaleAlgorithm;
}
/**
* Create a color RGB lookup table that can be added to the image processing chain,
* so that we can manipulate the image colors.
*
* @param name the name of the colormap table to use. This is currently
* One of: "Background", "Blue", "Heat", "Isophot", "Light", "Pastel",
* "Ramp", "Real", "Smooth", "Staircase", "Standard".
* User defined maps will be implemented in a later release.
*/
public void setColorLookupTable(String name) {
_colormap.setColorLookupTable(name);
_imageChangeEvent.setNewColormap(true);
_updatePending = true;
}
/** Return the current lookup table used to add color to a grayscale image. */
public LookupTableJAI getColorLookupTable() {
return _colormap.getColorLookupTable();
}
/** Return the name of the current color lookup table */
public String getColorLookupTableName() {
return _colormap.getColorLookupTableName();
}
/** Return the name of the current intensity lookup table */
public String getIntensityLookupTableName() {
return _colormap.getIntensityLookupTableName();
}
/**
* Create an intensity lookup table that can be added to the image processing chain
* to rearrange the order of the colors in the colormap.
*
* @param name the name of the intensity lookup table to use. This is currently
* One of: "Equal", "Exponential", "Gamma", "Jigsaw", "Lasritt", "Logarithmic",
* "Negative", "Negative Log", "Ramp", "Staircase".
*
* User defined intensity lookup tables will be implemented in a later release.
*/
public void setIntensityLookupTable(String name) {
_colormap.setIntensityLookupTable(name);
_imageChangeEvent.setNewColormap(true);
_updatePending = true;
}
/**
* Rotate the colormap by the given amount.
*/
public void rotateColormap(int amount) {
_colormap.rotateColormap(amount);
_imageChangeEvent.setNewColormap(true);
_updatePending = true;
}
/**
* Shift the colormap by the given amount.
*/
public void shiftColormap(int amount) {
_colormap.shiftColormap(amount);
_imageChangeEvent.setNewColormap(true);
_updatePending = true;
}
/**
* Scale the colormap by the given amount.
*/
public void scaleColormap(int amount) {
_colormap.scaleColormap(amount);
_imageChangeEvent.setNewColormap(true);
_updatePending = true;
}
/**
* Save the current colormap state for the next shift, rotate or scale operation.
*/
public void saveColormap() {
_colormap.saveColormap();
}
/**
* Reset the colormap to the default.
*/
public void resetColormap() {
_colormap.resetColormap();
_imageChangeEvent.setNewColormap(true);
_updatePending = true;
}
/**
* Reset the colormap to the default.
*/
public void setDefaultColormap() {
_colormap.setDefaultColormap();
setScaleAlgorithm(ImageLookup.LINEAR_SCALE);
_imageChangeEvent.setNewColormap(true);
_updatePending = true;
}
/** Set to true if something was changed and a call to update() is needed. */
protected void setUpdatePending(boolean b) {
_updatePending = b;
}
/** Return true if something was changed and a call to update() is needed. */
protected boolean isUpdatePending() {
return _updatePending;
}
}