#! /usr/bin/env python # -*- coding: latin-1 -*- """ This module creates an image of a numpy 2D array History: -------- v0.0.2 : 9/01/09 - adaptations for uint8 SDS. v0.0.1 : 6/10/2006 - Adaptations to the new version of the numpy package : numpy.arange -> numpy.arange v0.0.0 : 07/2006 - creation """ import os import time import glob import sys import os.path import numpy import Image, ImagePalette, ImageChops from ColorTable import * __TRACE__=False class data2img: """ This class transforms a data array into a PIL image """ # allowed colormap scales v_colormap_scale = ["linear","log","log10","exp","invert"] def __init__(self,data, val_min=None,val_max=None, invalid_val=None,invalid_val_idx=0, missing_val=None,missing_val_idx=1, colormap="rainbow", colormap_length=256, colormap_scale="linear", slope=1., offset=0., calibration_equation=0, is_palette_index=False ): """ build the image of a data array @param data the input data array (numpy one) @param val_min the minimal valid value to use ( default: the min data one ) @param val_max the maximal valid value (default: the min data one) @param invalid_val the invalid value to use @param invalid_val_idx the index in the colormap of the color to use for the invalid value @param missing_val the missing value to use @param missing_val_idx the index in the colormap of the color to use for the missing value @param colormap name of the colormap to use. See ColorTable.py for definitions @param colormap_length number of indexes of the colormap (invalid and missing indexes included) @param slope calibration slope @param offset calibration offset @param calibration_equation code of the calibration to apply : 0->(slope*x+offset) ; 1->(slope*x-offset) @param scale type of colormap scale top use : linear (default), log, exp, invert """ if data is None: raise InvalidData if data.dtype=='|S1' : shape = data.shape data = numpy.frombuffer(data.data,dtype=numpy.uint8) data.shape=shape self.data=data # the input array self.colortable=ColorTable() # the available colortables self.colormap=colormap # name of the colormap self.colormap_length=colormap_length self.colormap_scale=colormap_scale if self.colormap_scale not in self.v_colormap_scale: msg="Invalid Colormap Scale "+colormap_scale+" Must be one of "+str(self.v_colormap_scale) raise ValueError(msg) # if given, set the invalid value if invalid_val!=None: self.invalid_val=float(invalid_val) self.colormap_length-=1 else : self.invalid_val=None self.invalid_val_idx=invalid_val_idx # if given, set the missing value if missing_val!=None: self.missing_val=float(missing_val) self.colormap_length-=1 else : self.missing_val=None self.missing_val_idx=missing_val_idx self.is_palette_index = is_palette_index # set the min, max threshold. If not given, use the minimum and maximum valid values if __TRACE__: print "start setting min max data" if val_min is None and val_max is None: # for optimization purpose, compute only one time the valid data indexes valid_data=self.get_valid_data(self.data,self.invalid_val,self.missing_val) self.val_min=self.data[valid_data].min() self.val_max=self.data[valid_data].max() else: if val_min!=None: self.val_min=float(val_min) else: self.val_min=self.data[self.get_valid_data(self.data,self.invalid_val,self.missing_val)].min() if val_max!=None: self.val_max=float(val_max) else: self.val_max=self.data[self.get_valid_data(self.data,self.invalid_val,self.missing_val)].max() # the calibration coefficients self.slope=slope # calibration slope self.offset=offset # calibration offset self.calibration_equation=calibration_equation # The calibration equation to use : 0->(slope*x+offset) ; 1->(slope*x-offset) #---build the mask of the invalid and missing values if any if __TRACE__: print "start setting invalid and missing data mask" invalid_data_mask=None if self.invalid_val is not None: invalid_data_mask=(self.data==self.invalid_val) missing_data_mask=None if self.missing_val is not None: missing_data_mask=(self.data==self.missing_val) valid_data_mask=numpy.where(numpy.logical_and(missing_data_mask!=True,invalid_data_mask!=True)) #---apply the calibration if self.calibration_equation==0: # -------------------- TODO --------------------------------------------------------------- # calculer une calibration ne sert à rien si la donnée contient déjà des indices de palette # ----------------------------------------------------------------------------------------- if self.is_palette_index: self.data=self.offset+self.slope*self.data.astype(numpy.int32) else: self.data=self.offset+self.slope*self.data else : raise ValueError, "Invalid calibration equation" #---turn the data into 8bits indexes for applying the color palette if self.val_min is None: self.val_min=numpy.min(self.data) if self.val_max is None: self.val_max=numpy.max(self.data) #---eventually apply non linear scaling if self.colormap_scale == "log": self.val_min=numpy.log(self.val_min) self.val_max=numpy.log(self.val_max) self.data[valid_data_mask]=numpy.log(self.data[valid_data_mask]) elif self.colormap_scale == "log10": self.val_min=numpy.log10(self.val_min) self.val_max=numpy.log10(self.val_max) self.data[valid_data_mask]=numpy.log10(self.data[valid_data_mask]) elif self.colormap_scale == "exp": self.val_min=numpy.exp(self.val_min) self.val_max=numpy.exp(self.val_max) self.data[valid_data_mask]=numpy.exp(self.data[valid_data_mask]) elif self.colormap_scale == "invert": self.val_min=1./self.val_min self.val_max=1./self.val_max self.data[valid_data_mask]=1./(self.data[valid_data_mask]) if __TRACE__: print "start clipping the data" # apply a (min,max) threshold if needed (ie if val_min or val_max is given) self.data[valid_data_mask]=numpy.clip(self.data[valid_data_mask],self.val_min,self.val_max) #---build the palette data array if __TRACE__: print "start setting the palette data" # set the step between 2 values of indexes # -------------------- TODO --------------------------------------------------------------- # calculer un incrément n'a pas de sens si la donnée contient des indices de palette : # l'incrément sert à calculer les indices de palette, et self.data les contient déjà # ----------------------------------------------------------------------------------------- inc = 1 if not self.is_palette_index : inc=float(self.val_max-self.val_min)/float(self.colormap_length-1) palette_data=numpy.zeros(self.data.shape,dtype=numpy.uint8) # if an invalid value index is set, initialize the palette data to it if self.invalid_val is not None: palette_data[:]=self.invalid_val_idx # number of palette indexes reserved for invalid or missing data nb_reserved_palette_indexes=256-self.colormap_length # compute the palette indexes of valid data # -------------------- TODO --------------------------------------------------------------- # rien à faire ici si la donnée contient des indices de palette. Ce sera directement : # if self.is_palette_index : # palette_data = self.data.astype(numpy.uint8) # ----------------------------------------------------------------------------------------- palette_data[valid_data_mask]=nb_reserved_palette_indexes+((self.data[valid_data_mask]-self.val_min)/inc).astype(numpy.uint8) # Apply the mask of missing datas if self.missing_val is not None: numpy.putmask(palette_data,missing_data_mask,self.missing_val_idx) if __TRACE__: print "start creating the PIL image" #---Build the 8bits palette image self.pil_img=Image.fromstring('P', ( palette_data.shape[1],palette_data.shape[0]) , palette_data.tostring()) #---Apply the colormap to it self.pil_img.putpalette(self.colortable.get_palette(self.colormap)) #self.pil_img=self.pil_img.transpose(Image.FLIP_TOP_BOTTOM) def get_valid_data(self,data,invalid_val,missing_val): """ returns the mask of the valid data. If missing None, ignored. The same for the invalid values """ # build invalid and missing data mask if any not_invalid_data=None if self.invalid_val!=None: not_invalid_data=numpy.not_equal(self.data,self.invalid_val).astype(numpy.bool) not_missing_data=None if self.missing_val!=None: not_missing_data=numpy.not_equal(self.data,self.missing_val).astype(numpy.bool) if self.invalid_val is not None and self.missing_val is not None: return numpy.logical_and( not_invalid_data, not_missing_data) elif self.invalid_val is not None and self.missing_val is None: return not_invalid_data elif self.invalid_val is None and self.missing_val is not None: return not_missing_data # if here, all data are valid return numpy.ones( data.shape,dtype=numpy.bool ) def show(self): """ display the image build : uses xv on linux, and imaging on windows """ if self.pil_img==None: raise ValueError, "The PIL image isn't set" self.pil_img.show() def save(self,filename,format=None): """ Saves the image under the given filename. If format is omitted, the format is determined from the filename extension, if possible @param filename the output filename @param format the output format. @param overwrite set it to True to overwrite an existing file The available output formats are : GIF, JPEG, PNG, BMP and many others. See the Python Imaging Library handbook for them (http://www.pythonware.com/library/pil/handbook/index.htm). TODO add format options support for compression and other stuffs like this... """ if self.pil_img==None: raise ValueError, "The PIL image isn't set" if not os.path.exists(os.path.dirname(filename)): raise ValueError, "The output directory "+os.path.dirname(filename)+"doesn't exist" self.pil_img.save(filename,format) def test(): """ build an array, then run the prog on it """ #---create an array NB_DATA=5000 INC=0.5 data=numpy.arange(0.,NB_DATA*INC,INC, "f") data=numpy.repeat(data,10,0) data=data.reshape((10,NB_DATA)) #---name of the colormap to use (set in the Colormap.py module) colormap="blue_red" #---array characteristics : thresholds and invalid values (val_min,val_max,invalid_val)=(0.5,5000.,0.) #---run the prog itself time_start=time.time() img=data2img(data,colormap,val_min,val_max,invalid_val) time_end=time.time() print "Exec time : "+str(time_end-time_start) img.show() def main(): """ Main function, deals with arguments and launch program""" # run program prog=data2img() if __name__ == '__main__': #main() # uncomment it for command line mode test()