/*
* Copyright (c) 2012 European Synchrotron Radiation Facility,
* Diamond Light Source Ltd.
*
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*/
package fable.imageviewer.model;
import java.awt.Dimension;
import java.util.Arrays;
import java.util.EventListener;
import java.util.Vector;
import javax.swing.event.EventListenerList;
import jep.JepException;
import org.dawb.fabio.FabioFile;
import org.eclipse.swt.graphics.Rectangle;
import org.embl.cca.utils.imageviewer.Histogram;
import org.embl.cca.utils.imageviewer.PointWithValueIIF;
import org.embl.cca.utils.sorting.QuickSort;
import org.embl.cca.utils.imageviewer.RangeWithValuesFFV;
import org.embl.cca.utils.imageviewer.Statistics;
import org.slf4j.Logger;
/**
* This class implements a simple image model that stores the the width, height,
* and the pixel data. The data are stored as a float[index] with index = col +
* row * width. It calculates the statistics (min, max, and mean) when requested
* and then stores the values.
*
* @author evans
*
*/
public class ImageModel implements Cloneable {
// Note: Change the ImageInfoAction if more fields are added
private EventListenerList listenerList = null;
private String fileName = null;
private int width = 0;
private int height = 0;
private float[] data = null;
private Statistics statistics = null;
private long time;
// Property change names
/**
* Denotes that the data and statistics changed but not the other
* parameters.
*/
public static final String DATA_CHANGED = ImageModel.class.getName()
+ ".DataChanged";
/**
* Denotes the data and parameters changed.
*/
public static final String RESET = ImageModel.class.getName() + ".Reset";
/**
* Empty constructor. Sets the listenerList.
*/
public ImageModel() {
listenerList = new EventListenerList();
}
/**
* Constructor that sets the model based on the given FabioFile. Calls
* reset(fabioFile). Note that any events fired will have no listeners, yet.
* If you need to be informed of events, create an ImageModel, add the
* listeners, and use reset instead of constructing a new ImageModel.
*
* @param fabioFile
* @throws JepException
*/
public ImageModel(FabioFile fabioFile) throws Throwable {
this();
set(fabioFile);
}
/**
* Constructor that sets the model based on the given parameters. Calls
* reset(fileName, width, height, data). Note that any events fired will
* have no listeners, yet. If you need to be informed of events, create an
* ImageModel, add the listeners, and use reset instead of constructing a
* new ImageModel.
*
* @param fileName
* @param width
* @param height
* @param data
* @param time
*/
public ImageModel(String fileName, int width, int height, float[] data, long time) {
this();
reset(fileName, width, height, data);
this.time = time;
}
public ImageModel clone() {
ImageModel clone = new ImageModel();
if( fileName != null )
clone.fileName = fileName;
clone.width = width;
clone.height = height;
if( data != null )
clone.data = data.clone();
if( statistics != null )
clone.statistics = statistics.clone();
clone.time = time;
return clone;
}
/**
* Adds the listener.
*
* @param l
*/
public void addImageModelListener(ImageModelListener l) {
listenerList.add(ImageModelListener.class, l);
}
/**
* Removes the listener.
*
* @param l
*/
public void removeImageModelListener(ImageModelListener l) {
listenerList.remove(ImageModelListener.class, l);
}
/**
* Removes all the listeners.
*
* @param l
*/
public void removeAllImageModelListeners(ImageModelListener l) {
EventListener[] listeners = listenerList
.getListeners(ImageModelListener.class);
for (EventListener listener : listeners) {
listenerList.remove(ImageModelListener.class,
(ImageModelListener) listener);
}
}
/**
* Fires an ImageModelEvent with the given parameters.
*
* @param name
* Should be one of the ImageModel.xxx_CHANGED names.
* @param oldValue
* @param newValue
*/
protected void fireImageModelEvent(String name, Object oldValue,
Object newValue) {
EventListener[] listeners = listenerList
.getListeners(ImageModelListener.class);
for (EventListener listener : listeners) {
ImageModelEvent imageModelEvent = new ImageModelEvent(this, name,
oldValue, newValue);
((ImageModelListener) listener).propertyChange(imageModelEvent);
}
}
/**
* Resets the model based on the given FabioFile;
*
* @param fabioFile
* @throws JepException
*/
public void set(FabioFile fabioFile) throws Throwable {
try {
statistics = null;
this.fileName = fabioFile.getFileName();
this.data = fabioFile.getImageAsFloat();
this.width = fabioFile.getWidth();
this.height = fabioFile.getHeight();
this.time = fabioFile.getTimeToReadImage();
} finally {
fireImageModelEvent(RESET, this, this);
}
}
/**
* Resets the model based on the given parameters. Will cause a RESET
* ImageModelEvent but not a DATA_CHANGED event to be fired.
*
* @param fileName
* @param width
* @param height
* @param mean
* @param data
*/
public void reset(String fileName, int width, int height, float[] data) {
statistics = null;
this.fileName = fileName;
this.width = width;
this.height = height;
this.data = data;
fireImageModelEvent(RESET, this, this);
}
/**
* Calculates the statistics (min, max, mean) for the whole image and stores
* it.
*/
private void calculateStatistics() {
statistics = getStatistics(new Rectangle(0,0,width,height));
}
// Getters and setters
/**
* @return the fileName
*/
public String getFileName() {
return fileName;
}
/**
* @return the width
*/
public int getWidth() {
return width;
}
/**
* @return the height
*/
public int getHeight() {
return height;
}
public Rectangle getRect() {
return new Rectangle(0, 0, width, height);
}
/**
* Get the statistics (min, max, mean) for the whole image. The values are
* cached after the first time they are calculated.
*
* @return The statistics
* @remark as float[3] = {min, max, mean}.
*/
public Statistics getStatistics() {
if (statistics == null) {
calculateStatistics();
}
return statistics;
}
/**
* Searches the specified bins vector for the bin (range of keys)
* containing the specified key using the binary search algorithm. The
* vector must be sorted into ascending order prior to making this call.
* If it is not sorted, the results are undefined. If the vector contains
* multiple elements containing the specified key, there is no guarantee
* which one will be found.
*
* <p>This method runs in log(n) time for a "random access" vector (which
* provides near-constant-time positional access).
*
* @param bins the bins vector to be searched.
* @param key the value of which bin to be searched for.
* @return the index of the search key, if it is contained in the vector;
* otherwise, <tt>(-(<i>insertion point</i>) - 1)</tt>. The
* <i>insertion point</i> is defined as the point at which the
* key would be inserted into the vector: the index of the first
* element greater than the key, or <tt>vector.size()</tt> if all
* elements in the vector are less than the specified key. Note
* that this guarantees that the return value will be >= 0 if
* and only if the key is found.
*/
public int searchBin( Vector<RangeWithValuesFFV<Float>> bins, float key ) {
int low = 0;
int high = bins.size() - 1;
while (low <= high) {
int mid = (low + high) >>> 1;
if( key < bins.get(mid).rangeStart )
high = mid - 1;
else {
if( key >= bins.get(mid).rangeEnd )
low = mid + 1;
else
return mid; // key found
}
}
return -(low + 1); // key not found
}
public Statistics getStatistics(Rectangle rect) {
final Logger logger = org.slf4j.LoggerFactory.getLogger(ImageModel.class);
Rectangle imageRect = new Rectangle( 0, 0, width, height );
Rectangle constrained = imageRect.intersection(rect);
int iMax = constrained.x + constrained.width;
int jMax = constrained.y + constrained.height;
float[] rectData = new float[ constrained.width * constrained.height ];
int d = 0;
int s = constrained.y * width + constrained.x;
float min = Float.MAX_VALUE;
float max = -Float.MAX_VALUE;
float sum = 0.0f;
long t0 = System.nanoTime();
//Copying data of selected rectangle, in the meantime calculating statistics
for( int j = constrained.y; j < jMax; j++ ) {
int sj = s;
for( int i = constrained.x; i < iMax; i++ ) {
float val = data[ s++ ];
rectData[ d++ ] = val;
sum += val;
if (val < min) min = val;
if (val > max) max = val;
}
s = sj + width;
}
long t1 = System.nanoTime();
// logger.debug( "cut rect.dt [msec]= " + ( t1 - t0 ) / 1000000 ); //around 37 msec
final int topAmountMin = 100000; //Value by experience
int topAmount = Math.min( Math.max( (int)( rectData.length * 0.1f ), topAmountMin ), rectData.length ); //The top 10% of points will be PSF-ed, anyway this could be configurable
int topFrom = rectData.length - topAmount;
// float[] sortedData = QuickSort.sort( rectData );
float[] sortedData = QuickSort.sortTop( rectData, topFrom );
long t2 = System.nanoTime();
logger.debug( "QuickSort.dt [msec]= " + ( t2 - t1 ) / 1000000 ); //around 760 msec
float topFromValue = sortedData[ topFrom ];
//Find first other than value@topFrom, because there can be more value@topFrom below topFrom we do not count
while( topFrom < rectData.length && sortedData[ topFrom ] == topFromValue )
topFrom++;
topAmount = rectData.length - topFrom;
for( int i = topFrom + 1; i < sortedData.length; i++ )
if( sortedData[ i - 1 ] > sortedData[ i ] )
System.out.println( "QuickSort failure!" );
// final Dimension dataDim = new Dimension( width, height );
Statistics minMaxMean = new Statistics( min, max, sum / sortedData.length, false );
PointWithValueIIF[] psfPoints = new PointWithValueIIF[ topAmount ];
if( topAmount > 0 ) {
final float highlightValueMin = sortedData[ topFrom ];
// Searching for the values >= highlightValueMin to be highlighted by PSF
int iH = 0;
iMax = constrained.width;
jMax = constrained.height;
for( int j = 0; j < jMax; j++ ) {
int xyOffset = (constrained.y + j) * width + constrained.x;
for( int i = 0; i < iMax; i++ ) {
float val = data[ xyOffset++ ];
if( val >= highlightValueMin ) {
// try {
psfPoints[ iH++ ] = new PointWithValueIIF( i, j, val );
// } catch( Exception e ) {
// int aaa = 0;
// }
}
}
}
}
long t3 = System.nanoTime();
// logger.debug( "histograming.dt [msec]= " + ( t3 - t2 ) / 1000000 ); //around 153 msec
Arrays.sort( psfPoints );
final int valueAmountTotal = constrained.width * constrained.height;
//TODO passing dynHistogram instead of(?) histogram
float binWidth = 0; //TODO Set value, Width of bins
minMaxMean.setHistogram( new Histogram( null, minMaxMean.getMinimum(), binWidth, valueAmountTotal ) );
minMaxMean.setPSFPoints( psfPoints );
minMaxMean.setReadOnly( true );
return minMaxMean;
/*
final int binAmountMax = 100;
final int binHeightLimiter = Math.max( rectData.length / binAmountMax, 1 ); //Expression by experience
// Creating histogram
// At most 1% of maximum amount of bins can be the bin width
// final int binWidthMax = binAmountMax / 100; //Value by experience
// The preferred bin size
int binIndex = 0;
int binStart = 0;
// int binValueAmount = 0;
PointWithValueFFF[] dynHistogram = new PointWithValueFFF[ rectData.length ];
int sumBins = 0;
int sumBinsPrev = 0;
int prevIH = 0;
int iHMax = rectData.length;
int iH;
for( iH = 0; iH < iHMax; iH++ ) {
if( sortedData[ iH ] != sortedData[ prevIH ] ) {
prevIH = iH - 1;
sumBinsPrev = sumBins;
}
final int val = 1;
if( ( sumBinsPrev > 0 && sumBins + val > binHeightLimiter ) ) {
dynHistogram[ binIndex++ ] = new PointWithValueFFF( sortedData[ binStart ], sortedData[ prevIH ], sumBinsPrev );
binStart = iH;
prevIH = iH;
sumBinsPrev = 0;
sumBins = val;
} else
sumBins += val;
}
dynHistogram[ binIndex++ ] = new PointWithValueFFF( sortedData[ binStart ], sortedData[ iH - 1 ], sumBins );
PointWithValueFFF[] dynHistogramPacked = new PointWithValueFFF[ binIndex ];
System.arraycopy( dynHistogram, 0, dynHistogramPacked, 0, binIndex );
// for( int g = 0; g < dynHistogramPacked.length; g++ )
// System.out.println( "DHP[" + g + "] = " + dynHistogramPacked[ g ].toString() );
Vector<RangeWithValuesFFV<Float>> bins = new Vector<RangeWithValuesFFV<Float>>(binAmountMax);
bins.add( new RangeWithValuesFFV<Float>(Float.MIN_VALUE, Float.MAX_VALUE, binHeightLimiter + 1 ) );
iMax = constrained.width;
jMax = constrained.height;
for( int j = 0; j < jMax; j++ ) {
int xyOffset = j * constrained.width;
for ( int i = 0; i < iMax; i++ ) {
float curData = rectData[ xyOffset++ ];
int binIndex = searchBin( bins, curData ); //We are sure it finds a bin
RangeWithValuesFFV<Float> bin = bins.get(binIndex);
bin.values.add(new Float(curData));
int binSize = bin.values.size();
int binHalfSize = ( binSize + 1 ) >>> 1;
if( ( binSize % binHeightLimiter ) == 0 ) { //Split the bin
QuickSort.sortInItself( bin.values );
int findHalf = -1;
Float fPrev = 0f;
while( ++findHalf < binSize ) {
Float f = bin.values.get( findHalf );
if( findHalf >= binHalfSize && f != fPrev )
break;
fPrev = f;
}
if( findHalf < binSize ) {
RangeWithValuesFFV<Float> binNew = new RangeWithValuesFFV<Float>(bin.values.get( findHalf ), bin.rangeEnd, binHeightLimiter );
bins.insertElementAt( binNew, binIndex + 1 );
while( binSize > findHalf++ ) {
Float f = bin.values.remove( 0 );
binNew.values.add( f );
findHalf++;
}
bin.rangeEnd = binNew.rangeStart;
}
}
}
}
// float min = minMaxMean.getMinimum();
// int[] histogram = new int[ (int) ( minMaxMean.getMaximum() - min + 1 ) ];
// for (int j = 0; j < rect.height; j++) {
// int xyOffset = (rect.y + j) * width + rect.x;
// for (int i = 0; i < rect.width; i++) {
// histogram[ (int)( data[ xyOffset++ ] - min ) ]++;
// }
// }
// Searching for the 1% (but >=valueAmountMin) of values to be highlighted by PSF
final int valueAmountTotal = rect.width * rect.height;
final int valueAmountMin = 10000; //Value by experience
int valueAmountMax = Math.max( valueAmountTotal / 100, valueAmountMin );
int valueAmountPartial = 0;
for( iH = dynHistogramPacked.length - 1; iH >= 0; iH-- ) {
valueAmountPartial += dynHistogramPacked[ iH ].z;
if( valueAmountPartial > valueAmountMax )
break;
}
if( iH >= 0 )
valueAmountPartial -= dynHistogramPacked[ iH ].z;
PointWithValueIIF[] psfPoints = new PointWithValueIIF[ valueAmountPartial ];
// Searching for the values >= highlightValueMin to be highlighted by PSF
if( iH + 1 < dynHistogramPacked.length ) { //else way too many values in last bin
float highlightValueMin = dynHistogramPacked[ iH + 1 ].x;
iH = 0;
for( int j = 0; j < rect.height; j++ ) {
int xyOffset = (rect.y + j) * width + rect.x;
for( int i = 0; i < rect.width; i++ ) {
float val = data[ xyOffset++ ];
if( val >= highlightValueMin ) {
try {
psfPoints[ iH++ ] = new PointWithValueIIF( i, j, val );
} catch( Exception e ) {
int aaa = 0;
}
}
}
}
}
long t3 = System.nanoTime();
// logger.debug( "histograming.dt [msec]= " + ( t3 - t2 ) / 1000000 ); //around 153 msec
Arrays.sort( psfPoints );
//TODO passing dynHistogram instead of(?) histogram
float binWidth = 0; //TODO Set value, Width of bins
minMaxMean.setHistogram( new Histogram( null, minMaxMean.getMinimum(), binWidth, valueAmountTotal ) );
minMaxMean.setPSFPoints( psfPoints );
minMaxMean.setReadOnly( true );
return minMaxMean;
*/
}
/**
* Get the statistics (min, max, mean) for a sub Rectangle. These are
* calculated each time this method is called.
*
* @param rect
* @return The statistics
* @remark as float[3] = {min, max, mean}.
*/
public Statistics getStatisticsPrev(Rectangle rect) {
final Dimension dataDim = new Dimension( width, height );
Statistics minMaxMean = Statistics.calculateMinMaxMean( data, dataDim, rect, false );
final int binAmountMax = 0x100000;
// float[] normData = minMaxMean.normalize( data, dataDim, rect, binAmountMax ); //Maybe in the future
/**
* Creating a histogram for a wide range, that is binAmountMax bins.
* To make it fast, array is used which takes a lot of space. Then it
* is packed in a dynamic histogram.
* How? Using a binWidthMax and binHeightLimiter (anyway binWidthMin=1).
* If adding next bin to this bin would exceed binHeightLimiter, then next bin
* must be a separated bin, and this bin is closed. However if adding does
* not exceed binAmountLimiter, but the united binWidth would exceed binWidthMax,
* then next bin must be a separated bin, and this bin is closed.
* For example, binWidthMax = binAmountMax/100, binHeightLimiter = binAmountMax/binWidthMax.
* To store this dynamic histogram, the min and max of bin, and amount of values
* in that bin must be stored for each bin. Searching a bin can be done by binary
* search, assuming the bins are stored ordered.
*/
float min = minMaxMean.getMinimum();
int[] histogram = new int[ (int) ( minMaxMean.getMaximum() - min + 1 ) ];
for (int j = 0; j < rect.height; j++) {
int xyOffset = (rect.y + j) * width + rect.x;
for (int i = 0; i < rect.width; i++) {
histogram[ (int)( data[ xyOffset++ ] - min ) ]++;
}
}
// Creating histogram
// At most 1% of maximum amount of bins can be the bin width
final int binWidthMax = binAmountMax / 100; //Value by experience
// The preferred bin size
final int binHeightLimiter = binAmountMax / binWidthMax; //Expression by experience
int binIndex = 0;
int binStart = 0;
// int binValueAmount = 0;
PointWithValueIIF[] dynHistogram = null;
dynHistogram = new PointWithValueIIF[ histogram.length ];
int sum = 0;
int iH;
for( iH = 0; iH < histogram.length; iH++ ) {
int val = histogram[ iH ];
if( ( sum > 0 && sum + val > binHeightLimiter ) || iH - binStart > binWidthMax ) {
dynHistogram[ binIndex++ ] = new PointWithValueIIF( binStart, iH - 1, sum );
binStart = iH;
sum = val;
} else
sum += val;
}
dynHistogram[ binIndex++ ] = new PointWithValueIIF( binStart, iH - 1, sum );
PointWithValueIIF[] dynHistogramPacked = new PointWithValueIIF[ binIndex ];
System.arraycopy( dynHistogram, 0, dynHistogramPacked, 0, binIndex );
/**
*/
// Searching for the 1% (but >=valueAmountMin) of values to be highlighted by PSF
final int valueAmountTotal = rect.width * rect.height;
final int valueAmountMin = 10000; //Value by experience
int valueAmountMax = Math.max( valueAmountTotal / 100, valueAmountMin );
int valueAmountPartial = 0;
for( iH = dynHistogramPacked.length - 1; iH >= 0; iH-- ) {
valueAmountPartial += dynHistogramPacked[ iH ].z;
if( valueAmountPartial > valueAmountMax )
break;
}
if( iH >= 0 )
valueAmountPartial -= dynHistogramPacked[ iH ].z;
PointWithValueIIF[] psfPoints = new PointWithValueIIF[ valueAmountPartial ];
// Searching for the values >= highlightValueMin to be highlighted by PSF
if( iH + 1 < dynHistogramPacked.length ) { //else way too many values in last bin
float highlightValueMin = dynHistogramPacked[ iH + 1 ].x + minMaxMean.getMinimum();
iH = 0;
for( int j = 0; j < rect.height; j++ ) {
int xyOffset = (rect.y + j) * width + rect.x;
for( int i = 0; i < rect.width; i++ ) {
float val = data[ xyOffset++ ];
if( val >= highlightValueMin ) {
psfPoints[ iH++ ] = new PointWithValueIIF( i, j, val );
}
}
}
}
Arrays.sort( psfPoints );
//TODO passing dynHistogram instead of(?) histogram
float binWidth = 0; //TODO Set value, Width of bins
minMaxMean.setHistogram( new Histogram( histogram, minMaxMean.getMinimum(), binWidth, valueAmountTotal ) );
minMaxMean.setPSFPoints( psfPoints );
minMaxMean.setReadOnly( true );
return minMaxMean;
}
/**
* @return the data
*/
public float[] getData() {
return data;
}
/**
* Return the value of the data corresponding to the given row and column of
* the stored data.
*
* @param row
* @param col
* @return
*/
public float getData(int row, int col) {
if (data == null) {
return Float.NaN;
}
return data[col + row * width];
}
/**
* return the value of the data corresponding to given row and column of the
* given Rectangle.
*
* @param row
* @param col
* @param rect
* @return
*/
public float getData(int row, int col, Rectangle rect) {
if (data == null) {
return Float.NaN;
}
int index1 = col + row * rect.width;
int col1 = index1 % width;
int row1 = index1 / width;
return data[col1 + row1 * width];
}
/**
* Returns a sub array of the data corresponding to the given Rectangle.
*
* @param rect
* @return
*/
public float[] getData(Rectangle rect) {
if (data == null) {
return null;
}
float[] array = new float[rect.width * rect.height];
for (int j = 0; j < rect.height; j++) {
for (int i = 0; i < rect.width; i++) {
array[i + j * rect.width] = data[rect.x + i + (rect.y + j)
* width];
}
}
return array;
}
/**
* Sets a new value for the data and cause a DATA_CHANGED ImageModelEvent to
* be fired.
*
* @param data
* the data to set
*/
public void setData(float[] data) {
float[] oldValue = this.data;
if (data != oldValue) {
statistics = null;
this.data = data;
fireImageModelEvent(DATA_CHANGED, oldValue, data);
}
}
public long getTimeToReadImage() {
return time;
}
//Assuming the width and height is same in this and in imageModel
public void addImageModel( ImageModel imageModel ) {
float[] fthisdata = getData();
float[] fdata = imageModel.getData();
int jMax = Math.min( fthisdata.length, fdata.length ); //If assumption is right, the two lengths are same
for( int j = 0; j < jMax; j++ )
fthisdata[j] += fdata[j];
}
//Assuming the width and height is same in this and in imageModel
public void subImageModel( ImageModel imageModel ) {
float[] fsetdata = getData();
float[] fdata = imageModel.getData();
int jMax = Math.min( fsetdata.length, fdata.length ); //If assumption is right, the two lengths are same
for( int j = 0; j < jMax; j++ )
fsetdata[j] -= fdata[j];
}
}