# -*- coding: utf-8 -*-
import os
import gzip
import bz2
import six
import pvl
import numpy
[docs]class PlanetaryImage(object):
"""A generic image reader. Parent object for PDS3Image and CubeFile
Parameters
----------
stream
file object to read as an image file
filename : string
an optional filename to attach to the object
compression : string
an optional string that indicate the compression type 'bz2' or 'gz'
Attributes
----------
compression : string
Compression type (i.e. 'gz', 'bz2', or None).
data : numpy array
A numpy array representing the image.
filename : string
The filename given.
label : pvl module
The image's label in dictionary form.
Examples
--------
>>> from planetaryimage import PDS3Image
>>> testfile = 'tests/mission_data/2p129641989eth0361p2600r8m1.img'
>>> image = PDS3Image.open(testfile)
>>> # Examples of attributes
>>> image.bands
1
>>> image.lines
64
>>> image.samples
64
>>> str(image.format)
'BAND_SEQUENTIAL'
>>> image.data_filename
>>> image.dtype
dtype('>i2')
>>> image.start_byte
34304
>>> image.shape
(1, 64, 64)
>>> image.size
4096
See https://planetaryimage.readthedocs.io/en/latest/usage.html to see how
to open images to view them and make manipulations.
"""
@classmethod
[docs] def open(cls, filename):
""" Read an image file from disk
Parameters
----------
filename : string
Name of file to read as an image file. This file may be gzip
(``.gz``) or bzip2 (``.bz2``) compressed.
"""
if filename.endswith('.gz'):
fp = gzip.open(filename, 'rb')
try:
return cls(fp, filename, compression='gz')
finally:
fp.close()
elif filename.endswith('.bz2'):
fp = bz2.BZ2File(filename, 'rb')
try:
return cls(fp, filename, compression='bz2')
finally:
fp.close()
else:
with open(filename, 'rb') as fp:
return cls(fp, filename)
def __init__(self, stream_string_or_array, filename=None, compression=None):
"""
Create an Image object.
"""
if isinstance(stream_string_or_array, six.string_types):
error_msg = (
'A file like object is expected for stream. '
'Use %s.open(filename) to open a image file.'
)
raise TypeError(error_msg % type(self).__name__)
if isinstance(stream_string_or_array, numpy.ndarray):
self.filename = None
self.compression = None
self.data = stream_string_or_array
self.label = self._create_label(stream_string_or_array)
else:
#: The filename if given, otherwise none.
self.filename = filename
self.compression = compression
# TODO: rename to header and add footer?
#: The parsed label header in dictionary form.
self.label = self._load_label(stream_string_or_array)
#: A numpy array representing the image
self.data = self._load_data(stream_string_or_array)
def __repr__(self):
# TODO: pick a better repr
return self.filename
def save(self, file_to_write=None, overwrite=False):
self._save(file_to_write, overwrite)
@property
def image(self):
"""An Image like array of ``self.data`` convenient for image processing tasks
* 2D array for single band, grayscale image data
* 3D array for three band, RGB image data
Enables working with ``self.data`` as if it were a PIL image.
See https://planetaryimage.readthedocs.io/en/latest/usage.html to see
how to open images to view them and make manipulations.
"""
if self.bands == 1:
return self.data.squeeze()
elif self.bands == 3:
return numpy.dstack(self.data)
# TODO: what about multiband images with 2, and 4+ bands?
@property
def bands(self):
"""Number of image bands."""
return self._bands
@property
def lines(self):
"""Number of lines per band."""
return self._lines
@property
def samples(self):
"""Number of samples per line."""
return self._samples
@property
def format(self):
"""Image format."""
return self._format
_data_filename = None
@property
def data_filename(self):
"""Return detached filename else None."""
return self._data_filename
@property
def dtype(self):
"""Pixel data type."""
return self._dtype
@property
def start_byte(self):
"""Index of the start of the image data (zero indexed)."""
return self._start_byte
@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 _load_label(self, stream):
return pvl.load(stream)
def _load_data(self, stream):
if self.data_filename is not None:
return self._load_detached_data()
stream.seek(self.start_byte)
return self._decode(stream)
def create_label(self, array):
self._create_label(array)
def _decode(self, stream):
return self._decoder.decode(stream)
def _load_detached_data(self):
dirpath = os.path.dirname(self.filename)
filename = os.path.abspath(os.path.join(dirpath, self.data_filename))
with open(filename, 'rb') as stream:
return self._decode(stream)