Документ взят из кэша поисковой машины. Адрес оригинального документа : http://www.mrao.cam.ac.uk/~dag/CUBEHELIX/Colormaps.py
Дата изменения: Sat Sep 3 10:52:29 2011
Дата индексирования: Tue Oct 2 04:09:11 2012
Кодировка: IBM-866

Поисковые слова: rainbow
# -*- coding: utf-8 -*-
#
#% $Id$
#
#
# Copyright (C) 2002-2011
# The MeqTree Foundation &
# ASTRON (Netherlands Foundation for Research in Astronomy)
# P.O.Box 2, 7990 AA Dwingeloo, The Netherlands
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, see ,
# or write to the Free Software Foundation, Inc.,
# 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
#

from PyQt4.Qt import *
from PyQt4.Qwt5 import *

import math
import numpy
import numpy.ma
from scipy.ndimage import measurements

import Kittens.utils
import copy

_verbosity = Kittens.utils.verbosity(name="colormap");
dprint = _verbosity.dprint;
dprintf = _verbosity.dprintf;

class IntensityMap (object):
"""An IntensityMap maps a float array into a 0...1 range."""
def __init__ (self,dmin=None,dmax=None):
"""Constructor. An optional data range may be supplied.""";
self.range = None;
if dmin is not None:
if dmax is None:
raise TypeError,"both dmin and dmax must be specified, or neither.""";
self.setDataRange(dmin,dmax);

def copy (self):
return copy.copy(self);

def setDataRange (self,dmin,dmax):
"""Sets the data range.""";
self.range = dmin,dmax;

def setDataSubset (self,subset,minmax=None):
"""Sets the data subset.""";
self.subset = subset;
self.subset_minmax = minmax;

def getDataSubset (self):
return self.subset,self.subset_minmax;

def getDataRange (self,data):
"""Returns the set data range, or uses data min/max if it is not set""";
# use data min/max if no explicit ranges are set
return self.range or measurements.extrema(data)[:2];

def remap (self,data):
"""Remaps data into 0...1 range""";
raise RuntimeError,"remap() not implemented in "+str(type(self));

class LinearIntensityMap (IntensityMap):
"""This scales data linearly between preset min and max values."""
def remap (self,data):
d0,d1 = self.getDataRange(data);
dd = d1 - d0;
if dd:
return ((data-d0)/dd).clip(0,1);
else:
return numpy.zeros(data.shape,float);

class LogIntensityMap (IntensityMap):
"""This scales data linearly between preset min and max values."""
def __init__ (self):
self.log_cycles = 6;

def remap (self,data):
# d0,d1 is current data range
d0,d1 = self.getDataRange(data);
if d0 == d1:
return numpy.zeros(data.shape,float);
dmax = d1 - d0;
data = data - d0;
dmin = dmax*(10**(-self.log_cycles));
# clip data to between dmin and dmax, and take log
data = numpy.ma.log10(data.clip(dmin,dmax));
# now rescale
return (data - math.log10(dmin))/(math.log10(dmax) - math.log10(dmin));


class HistEqIntensityMap (IntensityMap):
def __init__ (self,nbins=256):
"""Creates intensity mapper which uses histogram equalization.""";
IntensityMap.__init__(self);
self._nbins = nbins;
self._cdf = self._bins = self.subset = None;

def setDataSubset (self,subset,minmax=None):
IntensityMap.setDataSubset(self,subset,minmax);
self._bins = None; # to recompute the CDF

def setDataRange (self,*range):
IntensityMap.setDataRange(self,*range);
self._bins = None; # to recompute the CDF

def _computeCDF (self,data):
"""Recomputes the CDF using the current data subset and range""";
dmin,dmax = self.getDataRange(self.subset if self.subset is not None else data);
if dmin == dmax:
self._cdf = None;
else:
dprint(1,"computing CDF for range",dmin,dmax);
# make cumulative histogram, normalize to 0...1
hist = measurements.histogram(self.subset if self.subset is not None else data,dmin,dmax,self._nbins);
cdf = numpy.cumsum(hist);
cdf = cdf/float(cdf[-1]);
# append 0 at beginning, as left side of bin
self._cdf = numpy.zeros(len(cdf)+1,float);
self._cdf[1:] = cdf[...];
# make array of bin edges
self._bins = dmin + (dmax-dmin)*numpy.arange(self._nbins+1)/float(self._nbins);

def remap (self,data):
if self._bins is None:
self._computeCDF(data);
if self._cdf is None:
return numpy.zeros(data.shape,float);
values = numpy.interp(data.ravel(),self._bins,self._cdf).reshape(data.shape);
if hasattr(data,'mask'):
values = numpy.ma.masked_array(values,data.mask);
return values;

class Colormap (QObject):
"""A Colormap provides operations for turning normalized float arrays into QImages. The default implementation is a linear colormap between two colors.
""";
def __init__ (self,name,color0=QColor("black"),color1=QColor("white"),alpha=(1,1)):
QObject.__init__(self);
self.name = name;
# color is either specified as one argument (which should then be a [3,n] or [4,n] array),
# or as two QColors orstring names.
if isinstance(color0,(list,tuple)):
self._rgb = numpy.array(color0);
if self._rgb.shape[1] != 3 or self._rgb.shape[0] < 2:
raise TypeError,"expected [N,3] (N>=2) array as first argument";
else:
if isinstance(color0,str):
color0 = QColor(color0);
if isinstance(color1,str):
color1 = QColor(color1);
self._rgb = numpy.array([[color0.red(),color0.green(),color0.blue()],
[color1.red(),color1.green(),color1.blue()]])/255.;
self._rgb_arg = numpy.arange(self._rgb.shape[0])/(self._rgb.shape[0]-1.0)
# alpha array
self._alpha = numpy.array(alpha).astype(float);
self._alpha_arg = numpy.arange(len(alpha))/(len(alpha)-1.0);
# background brush
self._brush = None;

def makeQImage (self,width,height):
data = numpy.zeros((width,height),float);
data[...] = (numpy.arange(width)/(width-1.))[:,numpy.newaxis];
# make brush image -- diag background, with colormap on top
img = QImage(width,height,QImage.Format_RGB32);
painter = QPainter(img);
painter.fillRect(0,0,width,height,QBrush(QColor("white")));
painter.fillRect(0,0,width,height,QBrush(Qt.BDiagPattern));
painter.drawImage(0,0,self.colorize(data));
painter.end();
return img;

def makeQPixmap (self,width,height):
data = numpy.zeros((width,height),float);
data[...] = (numpy.arange(width)/(width-1.))[:,numpy.newaxis];
# make brush image -- diag background, with colormap on top
img = QPixmap(width,height);
painter = QPainter(img);
painter.fillRect(0,0,width,height,QBrush(QColor("white")));
painter.fillRect(0,0,width,height,QBrush(Qt.BDiagPattern));
painter.drawImage(0,0,self.colorize(data));
painter.end();
return img;

def makeBrush (self,width,height):
return QBrush(self.makeQImage(width,height));

def colorize (self,data,alpha=None):
"""Converts normalized data (0...1) array into a QImage of the same dimensions.
'alpha', if set, is a 0...1 array of the same size, which is mapped to the alpha channel
(i.e. 0 for fully transparent and 1 for fully opaque).
If data is a masked array, masked pixels will be fully transparent.""";
# setup alpha channel
if alpha is None:
alpha = numpy.interp(data.ravel(),self._alpha_arg,self._alpha).reshape(data.shape);
alpha = numpy.round(255*alpha).astype(numpy.int32).clip(0,255);
# make RGB arrays
rgbs = [ (numpy.interp(data.ravel(),self._rgb_arg,self._rgb[:,i]).
reshape(data.shape)*255).round().astype(numpy.int32).clip(0,255)
for i in range(3) ];
# add data mask
mask = getattr(data,'mask',None);
if mask is not None and mask is not False:
alpha[mask] = 0;
for x in rgbs:
x[mask] = 0;
# do the deed
return self.QARGBImage(alpha,*rgbs);

def makeControlWidgets (self,parent):
"""Creates control widgets for the colormap's internal parameters.
"parent" is a parent widget.
Returns None if no controls are required""";
return None;

class QARGBImage (QImage):
"""This is a QImage which is constructed from an A,R,G,B arrays.""";
def __init__ (self,a,r,g,b):
nx,ny = r.shape;
argb = (a<<24) | (r<<16) | (g<<8) | b;
# transpose array, as it is in column-major (C order), while QImages are in row-major order
dprint(5,"making qimage of size",nx,ny);
self._buffer = argb.transpose().tostring();
QImage.__init__(self,self._buffer,nx,ny,QImage.Format_ARGB32);

class ColormapWithControls (Colormap):
"""This is a base class for a colormap with controls knobs""";
class SliderControl (QObject):
"""This class implements a slider control for a colormap""";
def __init__ (self,name,value,minval,maxval,step,format="%s: %.1f"):
QObject.__init__(self);
self.name,self.value,self.minval,self.maxval,self.step,self.format = \
name,value,minval,maxval,step,format;
self._default = value;
self._wlabel = None;

def makeControlWidgets (self,parent,gridlayout,row,column):
toprow = QWidget(parent);
gridlayout.addWidget(toprow,row*2,column);
top_lo = QHBoxLayout(toprow);
top_lo.setContentsMargins(0,0,0,0);
self._wlabel = QLabel(self.format%(self.name,self.value),toprow);
top_lo.addWidget(self._wlabel);
self._wreset = QToolButton(toprow);
self._wreset.setText("reset");
self._wreset.setToolButtonStyle(Qt.ToolButtonTextOnly);
self._wreset.setAutoRaise(True);
self._wreset.setEnabled(self.value != self._default);
QObject.connect(self._wreset,SIGNAL("clicked()"),self._resetValue);
top_lo.addWidget(self._wreset);
self._wslider = QwtSlider(parent);
# This works around a stupid bug in QwtSliders -- see comments on histogram zoom wheel above
self._wslider_timer = QTimer(parent);
self._wslider_timer.setSingleShot(True);
self._wslider_timer.setInterval(500);
QObject.connect(self._wslider_timer,SIGNAL("timeout()"),self.setValue);
gridlayout.addWidget(self._wslider,row*2+1,column);
self._wslider.setRange(self.minval,self.maxval);
self._wslider.setStep(self.step);
self._wslider.setValue(self.value);
self._wslider.setTracking(False);
QObject.connect(self._wslider,SIGNAL("valueChanged(double)"),self.setValue);
QObject.connect(self._wslider,SIGNAL("sliderMoved(double)"),self._previewValue);

def _resetValue (self):
self._wslider.setValue(self._default);
self.setValue(self._default);

def setValue (self,value=None,notify=True):
# only update widgets if already created
self.value = value;
if self._wlabel is not None:
if value is None:
self.value = value = self._wslider.value();
self._wreset.setEnabled(value != self._default);
self._wlabel.setText(self.format%(self.name,self.value));
# stop timer if being called to finalize the change in value
if notify:
self._wslider_timer.stop();
self.emit(SIGNAL("valueChanged"),self.value);

def _previewValue (self,value):
self.setValue(notify=False);
self._wslider_timer.start(500);
self.emit(SIGNAL("valueMoved"),self.value);

def emitChange (self,*dum):
self.emit(SIGNAL("colormapChanged"));

def emitPreview (self,*dum):
self.emit(SIGNAL("colormapPreviewed"));

def loadConfig (self,config):
pass;

def saveConfig (self,config):
pass;

class CubeHelixColormap (ColormapWithControls):
"""This implements the "cubehelix" colour scheme proposed by Dave Green:
D. Green 2011, Bull. Astr. Soc. India (2011) 39, 289тАУ295
http://arxiv.org/pdf/1108.5083v1
"""
def __init__(self,gamma=1,rgb=0.5,rots=-1.5,hue=1.2,name="CubeHelix"):
ColormapWithControls.__init__(self,name);
self.gamma = self.SliderControl("Gamma",gamma,0,6,.1);
self.color = self.SliderControl("Colour",rgb,0,3,.1);
self.cycles = self.SliderControl("Cycles",rots,-10,10,.1);
self.hue = self.SliderControl("Hue",hue,0,2,.1);

def colorize (self,data,alpha=None):
"""Converts normalized data (0...1) array into a QImage of the same dimensions.
'alpha', if set, is a 0...1 array of the same size, which is mapped to the alpha channel
(i.e. 0 for fully transparent and 1 for fully opaque).
If data is a masked array, masked pixels will be fully transparent.""";
# setup alpha channel
if alpha is None:
alpha = numpy.zeros(data.shape,dtype=numpy.int32);
alpha[...] = 255;
else:
alpha = numpy.round(255*alpha).astype(numpy.int32).clip(0,255);
# make RGB arrays
dg = data**self.gamma.value;
a = self.hue.value*dg*(1-dg)/2;
phi = 2*math.pi*(self.color.value/3 + self.cycles.value*data);
cosphi = a*numpy.cos(phi);
sinphi = a*numpy.sin(phi);
r = dg - 0.14861*cosphi + 1.78277*sinphi;
g = dg - 0.29227*cosphi - 0.90649*sinphi;
b = dg + 1.97249*cosphi;
rgbs = [ (x*255).round().astype(numpy.int32).clip(0,255) for x in r,g,b ];
# add data mask
mask = getattr(data,'mask',None);
if mask is not None and mask is not False:
alpha[mask] = 0;
for x in rgbs:
x[mask] = 0;
# do the deed
return self.QARGBImage(alpha,*rgbs);

def makeControlWidgets (self,parent):
"""Creates control widgets for the colormap's internal parameters.
"parent" is a parent widget.
Returns None if no controls are required""";
top = QWidget(parent);
layout = QGridLayout(top);
layout.setContentsMargins(0,0,0,0);
for irow,icol,control in ((0,0,self.gamma),(0,1,self.color),(1,0,self.cycles),(1,1,self.hue)):
control.makeControlWidgets(top,layout,irow,icol);
QObject.connect(control,SIGNAL("valueChanged"),self.emitChange);
QObject.connect(control,SIGNAL("valueMoved"),self.emitPreview);
return top;

def loadConfig (self,config):
for name in "gamma","color","cycles","hue":
control = getattr(self,name);
value = config.getfloat("cubehelix-colourmap-%s"%name,control.value);
control.setValue(value,notify=False);

def saveConfig (self,config):
for name in "gamma","color","cycles","hue":
control = getattr(self,name);
config.set("cubehelix-colourmap-%s"%name,control.value);


# instantiate "static" colormaps (i.e. those that have no internal parameters, and thus can be
# shared among images without instantiating a new Colormap object for each)
GreyscaleColormap = Colormap("Greyscale");
TransparentFuchsiaColormap = Colormap("Transparent Fuchsia",color0="fuchsia",color1="fuchsia",alpha=(0,1));

from ColormapTables import Karma
_karma_colormaps = [
Colormap(cmap,getattr(Karma,cmap))
for cmap in [
"Background",
"Heat",
"Isophot",
"Mousse",
"Rainbow",
"RGB",
"RGB2",
"Smooth",
"Staircase",
"Mirp",
"Random" ]
];

def getColormapList ():
"""Returns list of Colormap instances."""

# Some colormaps need a unique instantiation (because they have parameters)
# For the rest, use the static objects
return [ GreyscaleColormap,
CubeHelixColormap(),
TransparentFuchsiaColormap ] + _karma_colormaps;