/*
* Copyright 2004-2010 Information & Software Engineering Group (188/1)
* Institute of Software Technology and Interactive Systems
* Vienna University of Technology, Austria
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.ifs.tuwien.ac.at/dm/somtoolbox/license.html
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package at.tuwien.ifs.somtoolbox.visualization;
import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.Graphics2D;
import java.awt.GraphicsEnvironment;
import java.awt.Stroke;
import java.awt.image.BufferedImage;
import java.util.HashMap;
import java.util.Map;
import java.util.logging.Logger;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JSpinner;
import javax.swing.SpinnerNumberModel;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;
import org.apache.commons.math.util.MathUtils;
import at.tuwien.ifs.somtoolbox.SOMToolboxException;
import at.tuwien.ifs.somtoolbox.layers.LayerAccessException;
import at.tuwien.ifs.somtoolbox.layers.metrics.MetricException;
import at.tuwien.ifs.somtoolbox.models.GrowingSOM;
import at.tuwien.ifs.somtoolbox.util.ImageUtils;
import at.tuwien.ifs.somtoolbox.util.StdErrProgressWriter;
import at.tuwien.ifs.somtoolbox.util.StringUtils;
/**
* This class provides two visualizations:
* <ul>
* <li>Flow visualization</li>
* <li>Borderline visualizations.</li>
* </ul>
* both described in:<br>
* <i><b>Georg Poelzlbauer, Michael Dittenbach, Andreas Rauber</b>. Advanced visualization of Self-Organizing Maps with
* vector fields. <a href="http://www.sciencedirect.com/science?_ob=GatewayURL&_method=citationSearch&_urlVersion=4&_origin=SDVIALERTHTML&_version=1&_uoikey=B6T08-4K7166Y-4&md5=f8260f66027afdb1d445893049cad9d6"
* >Neural Networks, 19(6-7):911-922</a>, July-August 2006.</i>
*
* @author Dominik Schnitzer
* @author Peter Widhalm
* @version $Id: FlowBorderlineVisualizer.java 3883 2010-11-02 17:13:23Z frank $
*/
public class FlowBorderlineVisualizer extends AbstractBackgroundImageVisualizer {
public static final String[] FLOWBORDER_SHORT_NAMES = new String[] { "Flow", "Border", "FlowBorder" };
// Visualization Parameters
private double sigma = 1.5;
private double stretchConst = 0.7;
private GrowingSOM gsom;
private double[][] ax; // x-komponente des flow
private double[][] ay; // y-komponente des flow
private double maxa; // maximale flow-Komponente (fuer Normalisierung bei Darstellung)
public FlowBorderlineVisualizer() {
NUM_VISUALIZATIONS = 3;
VISUALIZATION_NAMES = new String[] { "Flow", "Borderline", "Flow & Borderline" };
VISUALIZATION_SHORT_NAMES = FLOWBORDER_SHORT_NAMES;
VISUALIZATION_DESCRIPTIONS = new String[] {
"Inplementation of Flow Visualization as described in \""
+ "G. Poelzlbauer, M. Dittenbach, A. Rauber. \n Advanced "
+ "visualization of Self-Organizing Maps with vector fields.\n"
+ "Neural Networks, 19(6-7):911-922, July-August 2006.\"", "Borderline Visualization Variant",
"Combined Flow/Borderline Visualization Variant" };
if (!GraphicsEnvironment.isHeadless()) {
controlPanel = new FlowBorderlineControlPanel();
}
// Scale for the FlowBorderlineVisualizer needs to be smaller, as the visualisation is made of lines, which
// cannot be scaled too much.
preferredScaleFactor = 2;
}
protected class FlowBorderlineControlPanel extends VisualizationControlPanel {
private static final long serialVersionUID = 1L;
protected JPanel sigmaPanel;
public JSpinner sigmaSpinner;
public FlowBorderlineControlPanel() {
super("Flow/Borderline Control");
sigmaSpinner = new JSpinner(new SpinnerNumberModel(sigma, 0.5, 30.0, 0.1));
sigmaSpinner.addChangeListener(new ChangeListener() {
@Override
public void stateChanged(ChangeEvent e) {
sigma = ((Double) ((JSpinner) e.getSource()).getValue()).doubleValue();
if (visualizationUpdateListener != null) {
clearFlows();
visualizationUpdateListener.updateVisualization();
}
}
});
sigmaPanel = new JPanel();
sigmaPanel.add(new JLabel("Sigma: "));
sigmaPanel.add(sigmaSpinner);
add(sigmaPanel, c);
}
}
private void clearFlows() {
// FIXME: consider having a cache for ax, ay, maxa, instead of always computing it
ax = null;
ay = null;
maxa = 0;
}
@Override
public BufferedImage createVisualization(int index, GrowingSOM gsom, int width, int height)
throws SOMToolboxException {
checkVariantIndex(index, getClass());
// max-value for sigma = max diagonal
double maxSigma = gsom.getLayer().maxNeighbourhoodRadius();
((FlowBorderlineControlPanel) controlPanel).sigmaSpinner.setModel(new SpinnerNumberModel(sigma, 0.5, maxSigma,
0.1));
((FlowBorderlineControlPanel) controlPanel).sigmaSpinner.setToolTipText("Value for the neighbourhood radius. Ranges from 0.5 to "
+ maxSigma + ", the maximum value for the diagonal distance.");
this.gsom = gsom;
if (ax == null || ay == null) { // only calculate flow when really needed
try {
calculateFlows();
} catch (Exception ex) {
Logger.getLogger("at.tuwien.ifs.somtoolbox").severe(ex.getMessage());
return null;
}
}
BufferedImage res = ImageUtils.createEmptyImage(width, height);
Graphics2D g = (Graphics2D) res.getGraphics();
g.setColor(Color.blue);
double unitWidth = width / gsom.getLayer().getXSize();
double stretchBorder = unitWidth / maxa * stretchConst;
// draw thicker lines, but save existing stroke first
Stroke oldStroke = g.getStroke();
g.setStroke(new BasicStroke((int) Math.round(unitWidth / 7)));
for (int x = 0; x < gsom.getLayer().getXSize(); x++) {
for (int y = 0; y < gsom.getLayer().getYSize(); y++) {
draw(index, g, x, y, unitWidth, stretchBorder);
}
}
g.setStroke(oldStroke); // reset to original stroke
return res;
}
/**
* Formel 1 distance in feature space
*/
private double df(int x1, int y1, int x2, int y2) throws LayerAccessException, MetricException {
// Formel 1
return gsom.getLayer().getMetric().distance(gsom.getLayer().getUnit(x1, y1).getWeightVector(),
gsom.getLayer().getUnit(x2, y2).getWeightVector());
}
/**
* Formel 2 distance in output space
*/
private double dout(int x1, int y1, int x2, int y2) {
// Formel 2
return Math.sqrt((x1 - x2) * (x1 - x2) + (y1 - y2) * (y1 - y2));
}
/**
* Formel 3
*/
private double kernel(double dout) {
// Formel 3
// return Math.exp(dout*dout/(2.0*sigma));
// FIXME: why not the function above, which is in the paper ?
return Math.exp(-1.0 * dout * dout / (2 * sigma * sigma));
}
/**
* Formel 8, 9 10, 11, 12, 13, 14, 15, 16
*/
private void calculateFlows() throws LayerAccessException, MetricException {
int xSize = gsom.getLayer().getXSize();
int ySize = gsom.getLayer().getYSize();
ax = new double[xSize][ySize];
ay = new double[xSize][ySize];
maxa = 0.0;
StdErrProgressWriter progress = new StdErrProgressWriter(xSize * ySize, "Calculating flows for unit ", xSize
* ySize / 10);
for (int x = 0; x < xSize; x++) {
for (int y = 0; y < ySize; y++) {
double roxplus = 0.0;
double roxminus = 0.0;
double royplus = 0.0;
double royminus = 0.0;
double omegaxplus = 0.0;
double omegaxminus = 0.0;
double omegayplus = 0.0;
double omegayminus = 0.0;
for (int x2 = 0; x2 < xSize; x2++) {
for (int y2 = 0; y2 < ySize; y2++) {
if (x != x2 || y != y2) {
// Formel 8
int doutx = x2 - x;
int douty = y2 - y;
// Formel 9
double alpha = Math.atan2(douty, doutx);
// Formel 10
double h = kernel(dout(x, y, x2, y2));
double omegax = Math.cos(alpha) * h;
double omegay = Math.sin(alpha) * h;
// Formel 11, 12, 13, 14, 15
double df = df(x, y, x2, y2);
if (omegax > 0.0) {
roxplus += omegax * df;
omegaxplus += omegax;
} else {
roxminus += -omegax * df;
omegaxminus += -omegax;
}
if (omegay > 0.0) {
royplus += omegay * df;
omegayplus += omegay;
} else {
royminus += -omegay * df;
omegayminus += -omegay;
}
}
}
}
// Formel 16
ax[x][y] = (-roxminus * omegaxplus + roxplus * omegaxminus) / (roxplus + roxminus);
ay[x][y] = (-royminus * omegayplus + royplus * omegayminus) / (royplus + royminus);
if (ax[x][y] > maxa) {
maxa = ax[x][y];
}
if (ay[x][y] > maxa) {
maxa = ay[x][y];
}
progress.progress();
}
}
}
private void draw(int mode, Graphics2D g, int x, int y, double unitWidth, double stretchBorder) {
if (mode == 1 || mode == 2) {
int bx1 = (int) (x * unitWidth + unitWidth / 2.0 - ay[x][y] * stretchBorder);
int by1 = (int) (y * unitWidth + unitWidth / 2.0 + ax[x][y] * stretchBorder);
int bx2 = (int) (x * unitWidth + unitWidth / 2.0 + ay[x][y] * stretchBorder);
int by2 = (int) (y * unitWidth + unitWidth / 2.0 - ax[x][y] * stretchBorder);
float w = (float) ((float) Math.sqrt(Math.pow(bx1 - bx2, 2) + Math.pow(by1 - by2, 2)) * .2 / 3);
g.setStroke(new BasicStroke(w));
g.setColor(Color.red);
g.drawLine(bx1, by1, bx2, by2);
}
if (mode == 0 || mode == 2) {
double lengthx = ax[x][y] * stretchBorder;
double lengthy = ay[x][y] * stretchBorder;
int bx1 = (int) (x * unitWidth + unitWidth / 2.0);
int by1 = (int) (y * unitWidth + unitWidth / 2.0);
int bx2 = (int) (x * unitWidth + unitWidth / 2.0 - lengthx);
int by2 = (int) (y * unitWidth + unitWidth / 2.0 - lengthy);
int deltaX = bx2 - bx1;
int deltaY = by2 - by1;
double frac = 0.2;
int headTipX = bx2 + (int) (deltaX * 0.2);
int headTipY = by2 + (int) (deltaY * 0.2);
int headEndX1 = bx1 + (int) ((1 - frac) * deltaX + frac * deltaY);
int headEndY1 = by1 + (int) ((1 - frac) * deltaY - frac * deltaX);
int headEndX2 = bx1 + (int) ((1 - frac) * deltaX - frac * deltaY);
int headEndY2 = by1 + (int) ((1 - frac) * deltaY + frac * deltaX);
float w = (float) (Math.sqrt(Math.pow(headEndX1 - headEndX2, 2) + Math.pow(headEndY1 - headEndY2, 2)) / 3f);
g.setStroke(new BasicStroke(w));
g.setColor(Color.blue);
g.drawLine(bx1, by1, bx2, by2);
g.fillPolygon(new int[] { headTipX, headEndX1, headEndX2 }, new int[] { headTipY, headEndY1, headEndY2 }, 3);
}
}
/**
* Scale for the {@link FlowBorderlineVisualizer} needs to be smaller, as the visualisation is made of lines, which
* cannot be scaled too much.
*/
@Override
public int getPreferredScaleFactor() {
return 2;
}
@Override
protected String getCacheKey(GrowingSOM gsom, int index, int width, int height) {
return appendToCacheKey(gsom, index, width, height, CACHE_KEY_SECTION_SEPARATOR + "sigma:" + sigma);
}
@Override
public HashMap<String, BufferedImage> getVisualizationFlavours(int index, GrowingSOM gsom, int width, int height)
throws SOMToolboxException {
return getVisualizationFlavours(index, gsom, width, height, -1);
}
@Override
public HashMap<String, BufferedImage> getVisualizationFlavours(int index, GrowingSOM gsom, int width, int height,
int maxFlavours) throws SOMToolboxException {
// create images from sigma = 0.1 to 3.0 in steps of 0.1, then in steps o 0.2 till 10, then in steps of 0.5
// or until maxFlavours is reached
HashMap<String, BufferedImage> images = new HashMap<String, BufferedImage>();
double maxNeighbourhoodRadius = gsom.getLayer().maxNeighbourhoodRadius();
int numberOfDigits = at.tuwien.ifs.commons.util.MathUtils.numberOfDigits(Math.round(maxNeighbourhoodRadius));
double currentSigma = sigma; // save old value
sigma = 0.5;
while (MathUtils.round(sigma, 1) <= maxNeighbourhoodRadius
&& (maxFlavours == -1 || images.size() <= maxFlavours)) {
clearFlows();
images.put("_sigma_" + StringUtils.format(sigma, 1, true, numberOfDigits), getVisualization(index, gsom,
width, height));
if (MathUtils.round(sigma, 1) < 1.0) {
sigma += 0.1;
} else if (MathUtils.round(sigma, 1) < 30) {
sigma += 0.5;
} else {
sigma += 1;
}
}
sigma = currentSigma;
return images;
}
@Override
public HashMap<String, BufferedImage> getVisualizationFlavours(int index, GrowingSOM gsom, int width, int height,
Map<String, String> flavourParameters) throws SOMToolboxException {
// FIXME: implement this
return getVisualizationFlavours(index, gsom, width, height);
}
}