Source code for pysis.cubefile

# -*- coding: utf-8 -*-

import numpy
import pvl

from six import string_types
from six.moves import range

from .specialpixels import SPECIAL_PIXELS


[docs]class CubeFile(object): """A Isis Cube file reader.""" PIXEL_TYPES = { 'UnsignedByte': numpy.dtype('uint8'), 'SignedByte': numpy.dtype('int8'), 'UnsignedWord': numpy.dtype('uint16'), 'SignedWord': numpy.dtype('int16'), 'UnsignedInteger': numpy.dtype('uint32'), 'SignedInteger': numpy.dtype('int32'), 'Real': numpy.dtype('float32'), 'Double': numpy.dtype('float64') } BYTE_ORDERS = { 'NoByteOrder': '=', # system 'Lsb': '<', # little-endian 'Msb': '>' # big-endian } SPECIAL_PIXELS = SPECIAL_PIXELS @classmethod
[docs] def open(cls, filename): """Read an Isis Cube file from disk. :param filename: name of file to read as an isis file """ with open(filename, 'rb') as fp: return cls(fp, filename)
def __init__(self, stream_or_fname, filename=None): """Create an Isis Cube file. :param stream: file object to read as an isis cube file :param filename: an optional filename to attach to the object """ if isinstance(stream_or_fname, string_types): self.filename = stream_or_fname stream = open(stream_or_fname, 'rb') else: #: The filename if given, otherwise none. self.filename = filename stream = stream_or_fname #: The parsed label header in dictionary form. self.label = self._parse_label(stream) #: A numpy array representing the image data. self.data = self._parse_data(stream)
[docs] def apply_scaling(self, copy=True): """Scale pixel values to there true DN. :param copy: whether to apply the scalling to a copy of the pixel data and leave the orginial unaffected :returns: a scalled version of the pixel data """ if copy: return self.multiplier * self.data + self.base if self.multiplier != 1: self.data *= self.multiplier if self.base != 0: self.data += self.base return self.data
[docs] def apply_numpy_specials(self, copy=True): """Convert isis special pixel values to numpy special pixel values. ======= ======= Isis Numpy ======= ======= Null nan Lrs -inf Lis -inf His inf Hrs inf ======= ======= :param copy: whether to apply the new special values to a copy of the pixel data and leave the orginial unaffected :returns: a numpy array with special values converted to numpy's nan, inf and -inf """ if copy: data = self.data.astype(numpy.float64) elif self.data.dtype != numpy.float64: data = self.data = self.data.astype(numpy.float64) else: data = self.data data[data == self.specials['Null']] = numpy.nan with numpy.errstate(invalid='ignore'): # we can do this here, because we know that the operations do the right thing # which is, where there's a numpy.nan the indexing returns False, # so no new value will be set there. That's what we want. data[data < self.specials['Min']] = numpy.NINF data[data > self.specials['Max']] = numpy.inf return data
[docs] def specials_mask(self): """Create a pixel map for special pixels. :returns: an array where the value is `False` if the pixel is special and `True` otherwise """ mask = self.data >= self.specials['Min'] mask &= self.data <= self.specials['Max'] return mask
[docs] def get_image_array(self): """Create an array for use in making an image. Creates a linear stretch of the image and scales it to between `0` and `255`. `Null`, `Lis` and `Lrs` pixels are set to `0`. `His` and `Hrs` pixels are set to `255`. Usage:: from pysis import CubeFile from PIL import Image # Read in the image and create the image data image = CubeFile.open('test.cub') data = image.get_image_array() # Save the first band to a new file Image.fromarray(data[0]).save('test.png') :returns: A uint8 array of pixel values. """ specials_mask = self.specials_mask() data = self.data.copy() data[specials_mask] -= data[specials_mask].min() data[specials_mask] *= 255 / data[specials_mask].max() data[data == self.specials['His']] = 255 data[data == self.specials['Hrs']] = 255 return data.astype(numpy.uint8)
@property def bands(self): """Number of image bands.""" return self.label['IsisCube']['Core']['Dimensions']['Bands'] @property def lines(self): """Number of lines per band.""" return self.label['IsisCube']['Core']['Dimensions']['Lines'] @property def samples(self): """Number of samples per line.""" return self.label['IsisCube']['Core']['Dimensions']['Samples'] @property def tile_lines(self): """Number of lines per tile.""" if self.format != 'Tile': return None return self.label['IsisCube']['Core']['TileLines'] @property def tile_samples(self): """Number of samples per tile.""" if self.format != 'Tile': return None return self.label['IsisCube']['Core']['TileSamples'] @property def format(self): return self.label['IsisCube']['Core']['Format'] @property def dtype(self): """Pixel data type.""" pixels_group = self.label['IsisCube']['Core']['Pixels'] byte_order = self.BYTE_ORDERS[pixels_group['ByteOrder']] pixel_type = self.PIXEL_TYPES[pixels_group['Type']] return pixel_type.newbyteorder(byte_order) @property def specials(self): pixel_type = self.label['IsisCube']['Core']['Pixels']['Type'] return self.SPECIAL_PIXELS[pixel_type] @property def base(self): """An additive factor by which to offset pixel DN.""" return self.label['IsisCube']['Core']['Pixels']['Base'] @property def multiplier(self): """A multiplicative factor by which to scale pixel DN.""" return self.label['IsisCube']['Core']['Pixels']['Multiplier'] @property def start_byte(self): """Index of the start of the image data (zero indexed).""" return self.label['IsisCube']['Core']['StartByte'] - 1 @property def shape(self): """Tuple of images bands, lines and samples.""" return (self.bands, self.lines, self.samples) @property def size(self): """Total number of pixels.""" return self.bands * self.lines * self.samples def _parse_label(self, stream): return pvl.load(stream) def _parse_data(self, stream): stream.seek(self.start_byte) if self.format == 'BandSequential': return self._parse_band_sequential_data(stream) if self.format == 'Tile': return self._parse_tile_data(stream) raise Exception('Unkown Isis Cube format (%s)' % self.format) def _parse_band_sequential_data(self, stream): data = numpy.fromfile(stream, self.dtype, self.size) return data.reshape(self.shape) def _parse_tile_data(self, stream): tile_lines = self.tile_lines tile_samples = self.tile_samples tile_size = tile_lines * tile_samples lines = range(0, self.lines, self.tile_lines) samples = range(0, self.samples, self.tile_samples) dtype = self.dtype data = numpy.empty(self.shape, dtype=dtype) for band in data: for line in lines: for sample in samples: sample_end = sample + tile_samples line_end = line + tile_lines chunk = band[line:line_end, sample:sample_end] tile = numpy.fromfile(stream, dtype, tile_size) tile = tile.reshape((tile_lines, tile_samples)) chunk_lines, chunk_samples = chunk.shape chunk[:] = tile[:chunk_lines, :chunk_samples] return data