# -*- coding: latin-1 -*- ######################################################################################## # # Description : # ------------- # # Create an image from a numpy 2D array # # Prerequisites : # --------------- # # - python >= 2.5 # - numpy >= 1.2.1 # - pyhdf >= 0.8.3 # - Python Imaging Library (PIL) >= # # Earlier versions are probably supported but not tested # # Author : # -------- # # CGTD-ICARE/UDEV Nicolas PASCAL # # License : # --------- # # This file must be used under the terms of the CeCILL. # This source file is licensed as described in the file COPYING, which # you should have received as part of this distribution. The terms # are also available at # http://www.cecill.info/licences/Licence_CeCILL_V2-en.txt # # TODO : # ------ # # Treat correctly palette indexes arrays without computing a scaling, min/max etc # # History : # -------- # # v1.0.0 : 2010/02/10 # - packaging for release on the web # # v0.0.2 : 21/09/2007 # - Add colormap choice in arguments # # 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 ValueError ( "No data given" ) 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 are 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 #palette_data = palette_data.reshape ( palette_data.shape[1], palette_data.shape[0] ) # PIL requires a (width,height) ordered image, and numpy gives it in ( y, x ) #self.pil_img=Image.fromstring('P', palette_data.shape, palette_data.tostring( ) ) #palette_data = palette_data.reshape ( ) # PIL requires a (width,height) ordered image, and numpy gives it in ( y, x ) self.pil_img=Image.fromstring('P', ( palette_data.shape[1], palette_data.shape[0] ), palette_data.tostring( None ) ) #---Apply the colormap to it self.pil_img.putpalette(self.colortable.get_palette(self.colormap)) #self.pil_img.show() 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): """ @brief saves the image under the given filename. @param filename the output filename, with directory. The format is determined from the filename extension @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). """ if filename is None : raise ValueError, "Output filename can't be None" outdir = os.path.dirname(filename) if outdir == '': raise ValueError, "No output directory specified" if not os.path.exists ( outdir ): raise ValueError, "The output directory "+outdir+" doesn't exist" if self.pil_img==None: raise ValueError, "The PIL image has not been built yet" self.pil_img.save(filename) def test(): """ build an array, then run the prog on it """ #---create an array INC=0.5 sz_y = 1000 sz_x = 10 data=numpy.arange ( 0., sz_y * INC, INC, dtype=numpy.float32 ) data.shape = ( data.shape[0], 1 ) data=numpy.repeat(data,sz_x,0) data.shape = ( sz_y, sz_x ) #---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, sz_y * INC, ( sz_y * INC ) /2 ) #---run the prog itself time_start=time.time() img = data2img ( data, colormap = colormap, val_min = val_min, val_max = val_max, invalid_val = invalid_val ) time_end=time.time() print "Exec time : "+str(time_end-time_start) #img.save( "./test.png" ) img.show() if __name__ == '__main__': #main() # uncomment it for command line mode test()