Source code for topo.plotting.plot

"""
Plot class.
"""

import copy
from math import sin, cos

import numpy as np
import param

from holoviews.core import NdMapping, SheetCoordinateSystem, Slice
from bitmap import HSVBitmap, RGBBitmap, Bitmap, DrawBitmap



### JCALERT!
### - Re-write the test file, taking the new changes into account.
### - I have to change the order: situate, plot_bb and (normalize)
### - There should be a way to associate the density explicitly
###   with the sheet_views, because it must match all SheetViews
###   in that dictionary.  Maybe as a tuple?
### - Fix the plot name handling along with the view_info sheetview attribute
### - Get rid of release_sheetviews.


[docs]class Plot(param.Parameterized): """ Simple Plot object constructed from a specified PIL image. """ staleness_warning=param.Number(default=10,bounds=(0,None),doc=""" Time length allowed between bitmaps making up a single plot before warning. If the difference between the Image with the earliest timestamp and the one with the latest timestamp is larger than this parameter's value, produce a warning. """) def __init__(self,image=None,**params): super(Plot,self).__init__(**params) self._orig_bitmap = Bitmap(image) self.bitmap = self._orig_bitmap # Possibly scaled copy (at first identical) self.scale_factor=1.0 self.plot_src_name = '' self.precedence = 0.0 self.row_precedence = 0.5 # If False, this plot should be left in its native size # pixel-for-pixel, (e.g. for a color key or similar static # image), rather than being resized as necessary. self.resize=False # Time at which the bitmaps were created self.timestamp = -1
[docs] def rescale(self,scale_factor): """ Change the size of this image by the specified numerical factor. The original image is kept as-is in _orig_bitmap; the scaled image is stored in bitmap. The scale_factor argument is taken as relative to the current scaling of the bitmap. For instance, calling scale(1.5) followed by scale(2.0) will yield a final scale of 3.0, not 2.0. """ self.scale_factor *= scale_factor if (self._orig_bitmap): self.bitmap = copy.copy(self._orig_bitmap) self.bitmap.image = self._orig_bitmap.zoom(self.scale_factor)
[docs] def set_scale(self,scale_factor): """ Specify the numerical value of the scaling factor for this image. The original image is kept as-is in _orig_bitmap; the scaled image is stored in bitmap. The scale_factor argument is taken as relative to the original size of the bitmap. For instance, calling scale(1.5) followed by scale(2.0) will yield a final scale of 2.0, not 3.0. """ self.scale_factor = scale_factor if (self._orig_bitmap): self.bitmap = copy.copy(self._orig_bitmap) self.bitmap.image = self._orig_bitmap.zoom(self.scale_factor)
[docs] def label(self): """Return a label for this plot.""" return self.plot_src_name + '\n' + self.name
def _sane_plot_data(channels,sheet_views): # CEBALERT: was sf.net tracker item 1860837 # (Avoid plotting only hue+confidence for a weights plot.) s_chan = channels.get('Strength') if s_chan is not None and len(s_chan)>0 and s_chan[0]=='Weights': return channels['Strength'] in sheet_views else: return True # JABALERT: How can we handle joint normalization, where a set of # plots (e.g. a CFProjectionPlotGroup, or the jointly normalized # subset of a ConnectionFields plot) is all scaled by the same amount, # so that relative strengths can be determined? Maybe we can have # make_template_plot and the various TemplatePlot types accept a # parameter 'range_only' that makes them simply calculate a pair # (min,max) with the values to use for scaling, and then the caller # (e.g. CFProjectionPlotGroup._create_plots) would run through # everything twice, first to get the ranges, and then the next time it # would supply an explicit range for scaling (overriding the default # single-plot normalization)? See the commented-out code for # value_range below for a start. I *think* that would work, but maybe # there is some simpler way?
[docs]def make_template_plot(channels,sheet_views,density=None, plot_bounding_box=None,normalize='None', name='None',range_=False): """ Factory function for constructing a Plot object whose type is not yet known. Typically, a TemplatePlot will be constructed through this call, because it selects the appropriate type automatically, rather than calling one of the Plot subclasses automatically. See TemplatePlot.__init__ for a description of the arguments. """ if _sane_plot_data(channels,sheet_views): plot_types=[SHCPlot,RGBPlot,PalettePlot,MultiOrPlot] for pt in plot_types: plot = pt(channels,sheet_views,density,plot_bounding_box,normalize, name=name,range_=range_) if plot.bitmap is not None or range_ is None: # range_ is None means we're calculating the range return plot param.Parameterized(name="make_template_plot").verbose('No %s plot constructed for this Sheet',name) return None
[docs]class TemplatePlot(Plot): """ A bitmap-based plot as specified by a plot template (or plot channels). """ # Not sure why, but this has to be a Parameter to avoid spurious complaints warn_time=param.Number(-2,precedence=-1,doc="Time last warned about stale plots") def __init__(self,channels,sheet_views,density, plot_bounding_box,normalize, range_=False,**params): """ Build a plot out of a set of SheetViews as determined by a plot_template. channels is a plot_template, i.e. a dictionary with keys (i.e. 'Strength','Hue','Confidence' ...). Each key typically has a string value naming specifies a Image in sheet_views, though specific channels may contain other types of information as required by specific Plot subclasses. channels that are not used by a particular Plot subclass will silently be ignored. sheet_views is a dictionary of SheetViews, generally (but not necessarily) belonging to a Sheet object. density is the density of the Sheet whose sheet_views was passed. plot_bounding_box is the outer bounding_box of the plot to apply if specified. If not, the bounds of the smallest Image are used. normalize specifies how the Plot should be normalized: any value of normalize other than 'None' will result in normalization according to the value of the range argument: range=(A,B) - scale plot so that A is 0 and B is 1 range=False - scale plot so that min(plot) is 0 and max(plot) is 1 (i.e. fill the maximim dynamic range) range=None - calculate value_range only name (which is inherited from Parameterized) specifies the name to use for this plot. """ super(TemplatePlot,self).__init__(**params) # for a template plot, resize is True by default self.resize=True self.bitmap = None self.channels = channels self.view_dict = copy.copy(sheet_views) # bounds of the situated plotting area self.plot_bounding_box = plot_bounding_box ### JCALERT ! The problem of displaying the right plot name is still reviewed ### at the moment we have the plot_src_name and name attribute that are used for the label. ### generally the name is set to the plot_template name, except for connection # set the name of the sheet that provides the SheetViews # combined with the self.name parameter when creating the plot (which is generally # the name of the plot_template), it provides the necessary information for displaying plot label self._set_plot_src_name() # # Eventually: support other type of plots (e.g vector fields...) using # # something like: # def annotated_bitmap(self): # enable other construction.... def _get_sv(self, key): sheet_view_key = self.channels.get(key, None) try: sv = self.view_dict.get(key,{}).get(sheet_view_key, None) except: sv = None else: if isinstance(sv, NdMapping): sv = sv.last return sv def _get_matrix(self,key): """ Retrieve the matrix view associated with a given key, if any. If the key is found in self.channels and the corresponding sheetview is found in self.view_dict, the view's matrix is returned; otherwise None is returned (with no error). If the sheet_view derives from a cyclic distribution, and it will be used as Hue, the matrix is normalized in range 0..1 """ sv = self._get_sv(key) if sv == None: matrix = None else: matrix = sv.data.copy() if key=='Hue' and sv.vdims[0].cyclic: cyclic_range = sv.vdims[0].range[1] - sv.vdims[0].range[0] matrix /= cyclic_range # Calculate timestamp for this plot timestamp = sv.metadata.timestamp if timestamp >=0: if self.timestamp < 0: self.timestamp = timestamp elif abs(timestamp - self.timestamp) > self.staleness_warning: if TemplatePlot.warn_time != min(timestamp, self.timestamp): self.warning("Combining SheetViews from different times (%s,%s) for plot %s; see staleness_warning" % (timestamp, self.timestamp,self.name)) TemplatePlot.warn_time = min(timestamp, self.timestamp) return matrix def _set_plot_src_name(self): """ Set the Plot plot_src_name. Called when Plot is created""" for key in self.channels: sheet_view_key = self.channels.get(key,None) try: sv = self.view_dict.get(key,{}).get(sheet_view_key) except: sv = None if sv != None: self.plot_src_name = sv.metadata.src_name self.precedence = sv.metadata.precedence self.row_precedence = sv.metadata.row_precedence if hasattr(sv.metadata,'proj_src_name'): self.proj_src_name=sv.metadata.proj_src_name ### JCALERT: This could be inserted in the code of get_matrix def _get_shape_and_box(self): """ Sub-function used by plot: get the matrix shape and the bounding box of the SheetViews that constitute the TemplatePlot. """ for channel, name in self.channels.items(): try: sv = self.view_dict.get(channel,{}).get(name, None) except: sv = None else: if isinstance(sv, NdMapping): sv = sv.last if sv != None: shape = sv.data.shape box = sv.bounds return shape, box # CEBALERT: needs simplification! (To begin work on joint # normalization, I didn't want to interfere with the existing # normalization calculations.) Also need to update this # docstring. # # range=None - calculate value_range; don't scale a # range=(A,B) - scale a so that A is 0 and B is 1 # range=False - scale a so that min(array) is 0 and max(array) is 1 def _normalize(self,a,range_): """ Normalize an array s to be in the range 0 to 1.0. For an array of identical elements, returns an array of ones if the elements are greater than zero, and zeros if the elements are less than or equal to zero. """ if range_: # i.e. not False, not None (expecting a tuple) range_min = float(range_[0]) range_max = float(range_[1]) if range_min==range_max: if range_min>0: resu = np.ones(a.shape) else: resu = np.zeros(a.shape) else: a_offset = a - range_min resu = a_offset/(range_max-range_min) return resu else: if range_ is None: if not hasattr(self,'value_range'): self.value_range=(a.min(),a.max()) else: # If normalizing multiple matrices, take the largest values self.value_range=(min(self.value_range[0],a.min()), max(self.value_range[1],a.max())) return None # (indicate that array was not scaled) else: # i.e. range_ is False a_offset = a-a.min() max_a_offset = a_offset.max() if max_a_offset>0: a = np.divide(a_offset,float(max_a_offset)) else: if min(a.ravel())<=0: a=np.zeros(a.shape,dtype=np.float) else: a=np.ones(a.shape,dtype=np.float) return a ### JC: maybe density can become an attribute of the TemplatePlot? def _re_bound(self,plot_bounding_box,mat,box,density): # CEBHACKALERT: for Julien... # If plot_bounding_box is that of a Sheet, it will already have been # setup so that the density in the x direction and the density in the # y direction are equal. # If plot_bounding_box comes from elsewhere (i.e. you create it from # arbitrary bounds), it might need to be adjusted to ensure the density # in both directions is the same (see Sheet.__init__()). I don't know where # you want to do that; presumably the code should be common to Sheet and # where it's used in the plotting? # # It's possible we can move some of the functionality # into SheetCoordinateSystem. if plot_bounding_box.containsbb_exclusive(box): ct = SheetCoordinateSystem(plot_bounding_box,density,density) new_mat = np.zeros(ct.shape,dtype=np.float) r1,r2,c1,c2 = Slice(box,ct) new_mat[r1:r2,c1:c2] = mat else: scs = SheetCoordinateSystem(box,density,density) s=Slice(plot_bounding_box,scs) s.crop_to_sheet(scs) new_mat = s.submatrix(mat) return new_mat
[docs]class SHCPlot(TemplatePlot): """ Bitmap plot based on Strength, Hue, and Confidence matrices. Constructs an HSV (hue, saturation, and value) plot by choosing the appropriate matrix for each channel. """ def __init__(self,channels,sheet_views,density, plot_bounding_box,normalize, range_=False,**params): super(SHCPlot,self).__init__(channels,sheet_views,density, plot_bounding_box,normalize,**params) # catching the empty plot exception s_mat = self._get_matrix('Strength') h_mat = self._get_matrix('Hue') c_mat = self._get_matrix('Confidence') # If it is an empty plot: self.bitmap=None if (s_mat is None and c_mat is None and h_mat is None): self.debug('Empty plot.') # Otherwise, we construct self.bitmap according to what is specified by the channels. else: shape,box = self._get_shape_and_box() hue,sat,val = self.__make_hsv_matrices((s_mat,h_mat,c_mat),shape,normalize,range_) if range_ is None: return ############################## if self.plot_bounding_box == None: self.plot_bounding_box = box hue = self._re_bound(self.plot_bounding_box,hue,box,density) sat = self._re_bound(self.plot_bounding_box,sat,box,density) val = self._re_bound(self.plot_bounding_box,val,box,density) self.bitmap = HSVBitmap(hue,sat,val) self._orig_bitmap=self.bitmap def __make_hsv_matrices(self,hsc_matrices,shape,normalize,range_=False): """ Sub-function of plot() that return the h,s,v matrices corresponding to the current matrices in sliced_matrices_dict. The shape of the matrices in the dict is passed, as well as the normalize boolean parameter. The result specified a bitmap in hsv coordinate. Applies normalizing and cropping if required. """ zero=np.zeros(shape,dtype=np.float) one=np.ones(shape,dtype=np.float) s,h,c = hsc_matrices # Determine appropriate defaults for each matrix if s is None: s=one # Treat as full strength by default if c is None: c=one # Treat as full confidence by default if h is None: # No color, gray-scale plot. h=zero c=zero # If normalizing, offset the matrix so that the minimum # value is 0.0 and then scale to make the maximum 1.0 if normalize!='None': s=self._normalize(s,range_=range_) # CEBALERT: I meant False, right? c=self._normalize(c,range_=False) # This translation from SHC to HSV is valid only for black backgrounds; # it will need to be extended also to support white backgrounds. hue,sat,val=h,c,s return (hue,sat,val)
[docs]class RGBPlot(TemplatePlot): """ Bitmap plot based on Red, Green, and Blue matrices. Construct an RGB (red, green, and blue) plot from the Red, Green, and Blue channels. """ def __init__(self,channels,sheet_views,density, plot_bounding_box,normalize, range_=False,**params): super(RGBPlot,self).__init__(channels,sheet_views,density, plot_bounding_box,normalize,**params) # catching the empty plot exception r_mat = self._get_matrix('Red') g_mat = self._get_matrix('Green') b_mat = self._get_matrix('Blue') # If it is an empty plot: self.bitmap=None if (r_mat==None and g_mat==None and b_mat==None): self.debug('Empty plot.') # Otherwise, we construct self.bitmap according to what is specified by the channels. else: shape,box = self._get_shape_and_box() red,green,blue = self.__make_rgb_matrices((r_mat,g_mat,b_mat),shape, normalize,range_=range_) if range_ is None: return ############################ if self.plot_bounding_box == None: self.plot_bounding_box = box red = self._re_bound(self.plot_bounding_box,red,box,density) green = self._re_bound(self.plot_bounding_box,green,box,density) blue = self._re_bound(self.plot_bounding_box,blue,box,density) self.bitmap = RGBBitmap(red,green,blue) self._orig_bitmap=self.bitmap def __make_rgb_matrices(self, rgb_matrices,shape,normalize,range_=False): """ Sub-function of plot() that return the h,s,v matrices corresponding to the current matrices in sliced_matrices_dict. The shape of the matrices in the dict is passed, as well as the normalize boolean parameter. The result specified a bitmap in hsv coordinate. Applies normalizing and cropping if required. """ zero=np.zeros(shape,dtype=np.float) r,g,b = rgb_matrices # Determine appropriate defaults for each matrix if r is None: r=zero if g is None: g=zero if b is None: b=zero # CEBALERT: have I checked this works? if normalize!='None': r = self._normalize(r,range_=range_) g = self._normalize(g,range_=range_) b = self._normalize(b,range_=range_) return (r,g,b)
[docs]class PalettePlot(TemplatePlot): """ Bitmap plot based on a Strength matrix, with optional colorization. Not yet implemented. When implemented, construct an RGB plot from a Strength channel, optionally colorized using a specified Palette. """ def __init__(self,channels,sheet_views,density, plot_bounding_box,normalize,**params): super(PalettePlot,self).__init__(channels,sheet_views,density, plot_bounding_box,normalize,**params) ### JABHACKALERT: To implement the class: If Strength is present, ### ask for Palette if it's there, and make a PaletteBitmap.
[docs]class MultiOrPlot(TemplatePlot): """ Bitmap plot with oriented lines draws for every units, representing the most preferred orientations. Constructs a matrix of drawing directives displaying oriented lines in each unit, colored according to the order or preference, and selectivity This plot expects channels named "OrX" "SelX", with "X" the number ranking the preferred orientations. """ unit_size = param.Number(default=25,bounds=(9,None),doc="box size of a single unit") min_brightness = param.Number(default=30,bounds=(0,50),doc="min brightness of lines") max_brightness = param.Number(default=90,bounds=(50,100),doc="max brightness of lines") def __init__(self,channels,sheet_views,density, plot_bounding_box,normalize, range_=False,**params): super(MultiOrPlot,self).__init__(channels,sheet_views,density, plot_bounding_box,normalize,**params) n = len( channels.keys() ) if density > 10: self.unit_size = int( density ) # there should be an even number of channels if n % 2: self.debug('Empty plot.') return if ( self.unit_size % 2 ) == 0: self.unit_size = self.unit_size + 1 n = n / 2 m = [] for i in range( n ): o = self._get_matrix( "Or%d" % (i+1) ) s = self._get_matrix( "Sel%d" % (i+1) ) if ( o==None or s==None ): self.debug('Empty plot.') return m.append( ( o, s ) ) shape,box = self._get_shape_and_box() dm = self.__make_lines_from_or_matrix( m, shape ) box_size = self.unit_size self.bitmap = DrawBitmap( dm, box_size ) self._orig_bitmap = self.bitmap def __vertices_from_or( self, o ): """ help function for generating coordinates of line vertices from normalized orientation value. Return a list with two tuples, the coordinates of the segment with the given orientation, in the normalized range [ 0...1 ]. Orientation is expected in range [ 0..pi ]. Space representation is in ordinary image convention: first coordinate is X, from left to right, second coordinate Y, from top to bottom. """ s = 0.5 * sin( o ) c = 0.5 * cos( o ) return [ ( 0.5 - c, 0.5 + s ), ( 0.5 + c, 0.5 - s ) ] def __make_line_directive( self, os_list ): """ help function for composing the list of line directives for a single unit. """ d_hue = 360 / len( os_list ) hue = 0 p = [] n = self.max_brightness - self.min_brightness for o,s in os_list: if s > 0.: f = "hsl(%d,100%%,%2d%%)" % ( hue, max( self.min_brightness, n * ( 1. - s ) ) ) p.append( { "line": [ o, { "fill": f } ] } ) hue = hue + d_hue return p def __make_lines_from_or_matrix( self, matrices, shape ): """ return a matrix of line drawing directives for each unit, derived from the given list of tuples ( o, s ), where o is the orientation view and s is the selectivity. The list is ordered by the orientation preference. """ vertices_from_or = np.vectorize( self.__vertices_from_or, otypes=[np.object_] ) mat_list = [] for o, s in matrices: a = s.mean() d = s.std() ad = a + d if isinstance( ad, np.number ) and ad > 0: mat_list.append( ( vertices_from_or( o ), ( s - d ) / ad ) ) lines = np.empty( shape, np.object_ ) for x in range( shape[ 0 ] ): for y in range( shape[ 1 ] ): os_list = [] for o, s in mat_list: os_list.append( ( o[ x, y ], s[ x, y ] ) ) lines[ x, y ] = self.__make_line_directive( os_list ) return lines