1'''
2:class:`~spectral.SpyFile` is the base class for creating objects to read
3hyperspectral data files.  When a :class:`~spectral.SpyFile` object is created,
4it provides an interface to read data from a corresponding file.  When an image
5is opened, the actual object returned will be a subclass of
6:class:`~spectral.SpyFile` (BipFile, BilFile, or BsqFile) corresponding to the
7interleave of the data within the image file.
8
9Let's open our sample image.
10
11.. ipython::
12
13    In [1]: from spectral import *
14
15    In [2]: img = open_image('92AV3C.lan')
16
17    In [3]: img.__class__
18    Out[3]: spectral.io.bilfile.BilFile
19
20    In [4]: print(img)
21            Data Source:   '/Users/thomas/spectral_data/92AV3C.lan'
22            # Rows:            145
23            # Samples:         145
24            # Bands:           220
25            Interleave:        BIL
26            Quantization:  16 bits
27            Data format:     int16
28
29The image was not located in the working directory but it was still opened
30because it was in a directory specified by the *SPECTRAL_DATA* environment
31variable.  Because the image pixel data are interleaved by line, the *image*
32function returned a *BilFile* instance.
33
34Since hyperspectral image files can be quite large, only
35metadata are read from the file when the :class:`~spectral.SpyFile` object is
36first created. Image data values are only read when specifically requested via
37:class:`~spectral.SpyFile` methods.  The :class:`~spectral.SpyFile` class
38provides a subscript operator that behaves much like the numpy array subscript
39operator. The :class:`~spectral.SpyFile` object is subscripted as an *MxNxB*
40array where *M* is the number of rows in the image, *N* is the number of
41columns, and *B* is thenumber of bands.
42
43.. ipython::
44
45    In [5]: img.shape
46    Out[5]: (145, 145, 220)
47
48    In [6]: pixel = img[50,100]
49
50    In [7]: pixel.shape
51    Out[7]: (220,)
52
53    In [8]: band6 = img[:,:,5]
54
55    In [9]: band6.shape
56    Out[9]: (145, 145, 1)
57
58The image data values were not read from the file until the subscript operator
59calls were performed.  Note that since Python indices start at 0,
60``img[50,100]`` refers to the pixel at 51st row and 101st column of the image.
61Similarly, ``img[:,:,5]`` refers to all the rows and columns for the 6th band
62of the image.
63
64:class:`~spectral.SpyFile` subclass instances returned for particular image
65files will also provide the following methods:
66
67==============   ===============================================================
68   Method                               Description
69==============   ===============================================================
70read_band        Reads a single band into an *MxN* array
71read_bands       Reads multiple bands into an *MxNxC* array
72read_pixel       Reads a single pixel into a length *B* array
73read_subregion   Reads multiple bands from a rectangular sub-region of the image
74read_subimage    Reads specified rows, columns, and bands
75==============   ===============================================================
76
77:class:`~spectral.SpyFile` objects have a ``bands`` member, which is an
78instance of a :class:`~spectral.BandInfo` object that contains optional
79information about the images spectral bands.
80'''
81
82from __future__ import absolute_import, division, print_function, unicode_literals
83
84import array
85import numpy as np
86import os
87import warnings
88
89import spectral as spy
90from .. import SpyException
91from ..image import Image, ImageArray
92from ..utilities.errors import has_nan, NaNValueWarning
93from ..utilities.python23 import typecode, tobytes, frombytes
94
95
96class FileNotFoundError(SpyException):
97    pass
98
99class InvalidFileError(SpyException):
100    '''Raised when file contents are invalid for the exepected file type.'''
101    pass
102
103def find_file_path(filename):
104    '''
105    Search cwd and SPECTRAL_DATA directories for the given file.
106    '''
107    pathname = None
108    dirs = [os.curdir]
109    if 'SPECTRAL_DATA' in os.environ:
110        dirs += os.environ['SPECTRAL_DATA'].split(os.pathsep)
111    for d in dirs:
112        testpath = os.path.join(d, filename)
113        if os.path.isfile(testpath):
114            pathname = testpath
115            break
116    if not pathname:
117        msg = 'Unable to locate file "%s". If the file exists, ' \
118          'use its full path or place its directory in the ' \
119          'SPECTRAL_DATA environment variable.'  % filename
120        raise FileNotFoundError(msg)
121    return pathname
122
123
124class SpyFile(Image):
125    '''A base class for accessing spectral image files'''
126
127    def __init__(self, params, metadata=None):
128        Image.__init__(self, params, metadata)
129        # Number by which to divide values read from file.
130        self.scale_factor = 1.0
131
132    def set_params(self, params, metadata):
133        Image.set_params(self, params, metadata)
134
135        try:
136            self.filename = params.filename
137            self.offset = params.offset
138            self.byte_order = params.byte_order
139            if spy.byte_order != self.byte_order:
140                self.swap = 1
141            else:
142                self.swap = 0
143            self.sample_size = np.dtype(params.dtype).itemsize
144
145            self.fid = open(find_file_path(self.filename), "rb")
146
147            # So that we can use this more like a Numeric array
148            self.shape = (self.nrows, self.ncols, self.nbands)
149
150        except:
151            raise
152
153    def transform(self, xform):
154        '''Returns a SpyFile image with the linear transform applied.'''
155        # This allows a LinearTransform object to take the SpyFile as an arg.
156        return transform_image(xform, self)
157
158    def __str__(self):
159        '''Prints basic parameters of the associated file.'''
160        s = '\tData Source:   \'%s\'\n' % self.filename
161        s += '\t# Rows:         %6d\n' % (self.nrows)
162        s += '\t# Samples:      %6d\n' % (self.ncols)
163        s += '\t# Bands:        %6d\n' % (self.shape[2])
164        if self.interleave == spy.BIL:
165            interleave = 'BIL'
166        elif self.interleave == spy.BIP:
167            interleave = 'BIP'
168        else:
169            interleave = 'BSQ'
170        s += '\tInterleave:     %6s\n' % (interleave)
171        s += '\tQuantization: %3d bits\n' % (self.sample_size * 8)
172
173        s += '\tData format:  %8s' % np.dtype(self.dtype).name
174        return s
175
176    def load(self, **kwargs):
177        '''Loads entire image into memory in a :class:`spectral.image.ImageArray`.
178
179        Keyword Arguments:
180
181            `dtype` (numpy.dtype):
182
183                An optional dtype to which the loaded array should be cast.
184
185            `scale` (bool, default True):
186
187                Specifies whether any applicable scale factor should be applied
188                to the data after loading.
189
190        :class:`spectral.image.ImageArray` is derived from both
191        :class:`spectral.image.Image` and :class:`numpy.ndarray` so it supports the
192        full :class:`numpy.ndarray` interface.  The returns object will have
193        shape `(M,N,B)`, where `M`, `N`, and `B` are the numbers of rows,
194        columns, and bands in the image.
195        '''
196        for k in list(kwargs.keys()):
197            if k not in ('dtype', 'scale'):
198                raise ValueError('Invalid keyword %s.' % str(k))
199        dtype = kwargs.get('dtype', ImageArray.format)
200        data = array.array(typecode('b'))
201        self.fid.seek(self.offset)
202        data.fromfile(self.fid, self.nrows * self.ncols *
203                      self.nbands * self.sample_size)
204        npArray = np.frombuffer(tobytes(data), dtype=self.dtype)
205        if self.interleave == spy.BIL:
206            npArray.shape = (self.nrows, self.nbands, self.ncols)
207            npArray = npArray.transpose([0, 2, 1])
208        elif self.interleave == spy.BSQ:
209            npArray.shape = (self.nbands, self.nrows, self.ncols)
210            npArray = npArray.transpose([1, 2, 0])
211        else:
212            npArray.shape = (self.nrows, self.ncols, self.nbands)
213        npArray = npArray.astype(dtype)
214        if self.scale_factor != 1 and kwargs.get('scale', True):
215            npArray = npArray / float(self.scale_factor)
216        imarray = ImageArray(npArray, self)
217        if has_nan(imarray):
218            warnings.warn('Image data contains NaN values.', NaNValueWarning)
219        return imarray
220
221    def __getitem__(self, args):
222        '''Subscripting operator that provides a numpy-like interface.
223        Usage::
224
225            x = img[i, j]
226            x = img[i, j, k]
227
228        Arguments:
229
230            `i`, `j`, `k` (int or :class:`slice` object)
231
232                Integer subscript indices or slice objects.
233
234        The subscript operator emulates the :class:`numpy.ndarray` subscript
235        operator, except data are read from the corresponding image file
236        instead of an array object in memory.  For frequent access or when
237        accessing a large fraction of the image data, consider calling
238        :meth:`spectral.SpyFile.load` to load the data into an
239        :meth:`spectral.image.ImageArray` object and using its subscript operator
240        instead.
241
242        Examples:
243
244            Read the pixel at the 30th row and 51st column of the image::
245
246                pixel = img[29, 50]
247
248            Read the 10th band::
249
250                band = img[:, :, 9]
251
252            Read the first 30 bands for a square sub-region of the image::
253
254                region = img[50:100, 50:100, :30]
255        '''
256
257        atypes = [type(a) for a in args]
258
259        if len(args) < 2:
260            raise IndexError('Too few subscript indices.')
261
262        fix_negative_indices = self._fix_negative_indices
263
264        if atypes[0] == atypes[1] == int and len(args) == 2:
265            row = fix_negative_indices(args[0], 0)
266            col = fix_negative_indices(args[1], 1)
267            return self.read_pixel(row, col)
268        elif len(args) == 3 and atypes[0] == atypes[1] == atypes[2] == int:
269            row = fix_negative_indices(args[0], 0)
270            col = fix_negative_indices(args[1], 1)
271            band = fix_negative_indices(args[2], 2)
272            return self.read_datum(row, col, band)
273        else:
274            #  At least one arg should be a slice
275            if atypes[0] == slice:
276                (xstart, xstop, xstep) = (args[0].start, args[0].stop,
277                                          args[0].step)
278                if xstart is None:
279                    xstart = 0
280                if xstop is None:
281                    xstop = self.nrows
282                if xstep is None:
283                    xstep = 1
284                rows = list(range(xstart, xstop, xstep))
285            else:
286                rows = [args[0]]
287            if atypes[1] == slice:
288                (ystart, ystop, ystep) = (args[1].start, args[1].stop,
289                                          args[1].step)
290                if ystart is None:
291                    ystart = 0
292                if ystop is None:
293                    ystop = self.ncols
294                if ystep is None:
295                    ystep = 1
296                cols = list(range(ystart, ystop, ystep))
297            else:
298                cols = [args[1]]
299
300        if len(args) == 2 or args[2] is None:
301            bands = None
302        elif atypes[2] == slice:
303            (zstart, zstop, zstep) = (args[2].start, args[2].stop,
304                                      args[2].step)
305            if zstart == zstop == zstep == None:
306                bands = None
307            else:
308                if zstart is None:
309                    zstart = 0
310                if zstop is None:
311                    zstop = self.nbands
312                if zstep is None:
313                    zstep = 1
314                bands = list(range(zstart, zstop, zstep))
315        elif atypes[2] == int:
316            bands = [args[2]]
317        else:
318            # Band indices should be in a list
319            bands = args[2]
320
321        if atypes[0] == slice and xstep == 1 \
322          and atypes[1] == slice and ystep == 1 \
323          and (bands is None or type(bands) == list):
324            xstart = fix_negative_indices(xstart, 0)
325            xstop = fix_negative_indices(xstop, 0)
326            ystart = fix_negative_indices(ystart, 0)
327            ystop = fix_negative_indices(ystop, 0)
328            bands = fix_negative_indices(bands, 2)
329            return self.read_subregion((xstart, xstop), (ystart, ystop), bands)
330
331        rows = fix_negative_indices(rows, 0)
332        cols = fix_negative_indices(cols, 1)
333        bands = fix_negative_indices(bands, 2)
334        return self.read_subimage(rows, cols, bands)
335
336    def _fix_negative_indices(self, indices, dim):
337        if not indices:
338            return indices
339
340        dim_len = self.shape[dim]
341        try:
342            return [i if i >= 0 else dim_len + i
343                    for i in indices]
344        except:
345            return indices if indices >= 0 else dim_len + indices
346
347    def params(self):
348        '''Return an object containing the SpyFile parameters.'''
349        p = Image.params(self)
350
351        p.filename = self.filename
352        p.offset = self.offset
353        p.byte_order = self.byte_order
354        p.sample_size = self.sample_size
355
356        return p
357
358    def __del__(self):
359        self.fid.close()
360
361
362class SubImage(SpyFile):
363    '''
364    Represents a rectangular sub-region of a larger SpyFile object.
365    '''
366    def __init__(self, image, row_range, col_range):
367        '''Creates a :class:`Spectral.SubImage` for a rectangular sub-region.
368
369        Arguments:
370
371            `image` (SpyFile):
372
373                The image for which to define the sub-image.
374
375            `row_range` (2-tuple):
376
377                Integers [i, j) defining the row limits of the sub-region.
378
379            `col_range` (2-tuple):
380
381                Integers [i, j) defining the col limits of the sub-region.
382
383        Returns:
384
385            A :class:`spectral.SubImage` object providing a
386            :class:`spectral.SpyFile` interface to a sub-region of the image.
387
388        Raises:
389
390            :class:`IndexError`
391
392        Row and column ranges must be 2-tuples (i,j) where i >= 0 and i < j.
393
394        '''
395        if row_range[0] < 0 or \
396            row_range[1] > image.nrows or \
397            col_range[0] < 0 or \
398                col_range[1] > image.ncols:
399            raise IndexError('SubImage index out of range.')
400
401        p = image.params()
402
403        SpyFile.__init__(self, p, image.metadata)
404        self.parent = image
405        self.row_offset = row_range[0]
406        self.col_offset = col_range[0]
407        self.nrows = row_range[1] - row_range[0]
408        self.ncols = col_range[1] - col_range[0]
409        self.shape = (self.nrows, self.ncols, self.nbands)
410
411    def read_band(self, band):
412        '''Reads a single band from the image.
413
414        Arguments:
415
416            `band` (int):
417
418                Index of band to read.
419
420        Returns:
421
422           :class:`numpy.ndarray`
423
424                An `MxN` array of values for the specified band.
425        '''
426        return self.parent.read_subregion([self.row_offset,
427                                           self.row_offset + self.nrows - 1],
428                                          [self.col_offset,
429                                           self.col_offset + self.ncols - 1],
430                                          [band])
431
432    def read_bands(self, bands):
433        '''Reads multiple bands from the image.
434
435        Arguments:
436
437            `bands` (list of ints):
438
439                Indices of bands to read.
440
441        Returns:
442
443           :class:`numpy.ndarray`
444
445                An `MxNxL` array of values for the specified bands. `M` and `N`
446                are the number of rows & columns in the image and `L` equals
447                len(`bands`).
448        '''
449        return self.parent.read_subregion([self.row_offset,
450                                           self.row_offset + self.nrows - 1],
451                                          [self.col_offset,
452                                           self.col_offset + self.ncols - 1],
453                                          bands)
454
455    def read_pixel(self, row, col):
456        '''Reads the pixel at position (row,col) from the file.
457
458        Arguments:
459
460            `row`, `col` (int):
461
462                Indices of the row & column for the pixel
463
464        Returns:
465
466           :class:`numpy.ndarray`
467
468                A length-`B` array, where `B` is the number of image bands.
469        '''
470        return self.parent.read_pixel(row + self.row_offset,
471                                      col + self.col_offset)
472
473    def read_subimage(self, rows, cols, bands=[]):
474        '''
475        Reads arbitrary rows, columns, and bands from the image.
476
477        Arguments:
478
479            `rows` (list of ints):
480
481                Indices of rows to read.
482
483            `cols` (list of ints):
484
485                Indices of columns to read.
486
487            `bands` (list of ints):
488
489                Optional list of bands to read.  If not specified, all bands
490                are read.
491
492        Returns:
493
494           :class:`numpy.ndarray`
495
496                An `MxNxL` array, where `M` = len(`rows`), `N` = len(`cols`),
497                and `L` = len(bands) (or # of image bands if `bands` == None).
498        '''
499        return self.parent.read_subimage(list(array.array(rows) \
500                                              + self.row_offset),
501                                         list(array.array(cols) \
502                                              + self.col_offset),
503                                         bands)
504
505    def read_subregion(self, row_bounds, col_bounds, bands=None):
506        '''
507        Reads a contiguous rectangular sub-region from the image.
508
509        Arguments:
510
511            `row_bounds` (2-tuple of ints):
512
513                (a, b) -> Rows a through b-1 will be read.
514
515            `col_bounds` (2-tuple of ints):
516
517                (a, b) -> Columnss a through b-1 will be read.
518
519            `bands` (list of ints):
520
521                Optional list of bands to read.  If not specified, all bands
522                are read.
523
524        Returns:
525
526           :class:`numpy.ndarray`
527
528                An `MxNxL` array.
529        '''
530        return self.parent.read_subimage(list(np.array(row_bounds) \
531                                              + self.row_offset),
532                                         list(np.array(col_bounds) \
533                                              + self.col_offset),
534                                         bands)
535
536
537def tile_image(im, nrows, ncols):
538    '''
539    Break an image into nrows x ncols tiles.
540
541    USAGE: tiles = tile_image(im, nrows, ncols)
542
543    ARGUMENTS:
544        im              The SpyFile to tile.
545        nrows           Number of tiles in the veritical direction.
546        ncols           Number of tiles in the horizontal direction.
547
548    RETURN VALUE:
549        tiles           A list of lists of SubImage objects. tiles
550                        contains nrows lists, each of which contains
551                        ncols SubImage objects.
552    '''
553    x = (np.array(list(range(nrows + 1))) * float(im.nrows) / nrows).astype(int)
554    y = (np.array(list(range(ncols + 1))) * float(im.ncols) / ncols).astype(int)
555    x[-1] = im.nrows
556    y[-1] = im.ncols
557
558    tiles = []
559    for r in range(len(x) - 1):
560        row = []
561        for c in range(len(y) - 1):
562            si = SubImage(im, [x[r], x[r + 1]], [y[c], y[c + 1]])
563            row.append(si)
564        tiles.append(row)
565    return tiles
566
567def transform_image(transform, img):
568    '''Applies a linear transform to an image.
569
570    Arguments:
571
572        `transform` (ndarray or LinearTransform):
573
574            The `CxB` linear transform to apply.
575
576        `img` (ndarray or :class:`spectral.SpyFile`):
577
578            The `MxNxB` image to be transformed.
579
580    Returns (ndarray or :class:spectral.spyfile.TransformedImage`):
581
582        The transformed image.
583
584    If `img` is an ndarray, then a `MxNxC` ndarray is returned.  If `img` is
585    a :class:`spectral.SpyFile`, then a
586    :class:`spectral.spyfile.TransformedImage` is returned.
587    '''
588    from ..algorithms.transforms import LinearTransform
589    if isinstance(img, np.ndarray):
590        if isinstance(transform, LinearTransform):
591            return transform(img)
592        ret = np.empty(img.shape[:2] + (transform.shape[0],), img.dtype)
593        for i in range(img.shape[0]):
594            for j in range(img.shape[1]):
595                ret[i, j] = np.dot(transform, img[i, j])
596        return ret
597    else:
598        return TransformedImage(transform, img)
599
600
601class TransformedImage(Image):
602    '''
603    An image with a linear transformation applied to each pixel spectrum.
604    The transformation is not applied until data is read from the image file.
605    '''
606    dtype = np.dtype('f4').char
607
608    def __init__(self, transform, img):
609        from ..algorithms.transforms import LinearTransform
610        if not isinstance(img, Image):
611            raise Exception(
612                'Invalid image argument to to TransformedImage constructor.')
613
614        if isinstance(transform, np.ndarray):
615            transform = LinearTransform(transform)
616        self.transform = transform
617
618        if self.transform.dim_in not in (None, img.shape[-1]):
619            raise Exception('Number of bands in image (%d) do not match the '
620                            ' input dimension of the transform (%d).'
621                            % (img.shape[-1], transform.dim_in))
622
623        params = img.params()
624        self.set_params(params, params.metadata)
625
626        # If img is also a TransformedImage, then just modify the transform
627        if isinstance(img, TransformedImage):
628            self.transform = self.transform.chain(img.transform)
629            self.image = img.image
630        else:
631            self.image = img
632        if self.transform.dim_out is not None:
633            self.shape = self.image.shape[:2] + (self.transform.dim_out,)
634            self.nbands = self.transform.dim_out
635        else:
636            self.shape = self.image.shape
637            self.nbands = self.image.nbands
638
639    @property
640    def bands(self):
641        return self.image.bands
642
643    def __getitem__(self, args):
644        '''
645        Get data from the image and apply the transform.
646        '''
647        if len(args) < 2:
648            raise Exception('Must pass at least two subscript arguments')
649
650        # Note that band indices are wrt transformed features
651        if len(args) == 2 or args[2] is None:
652            bands = list(range(self.nbands))
653        elif type(args[2]) == slice:
654            (zstart, zstop, zstep) = (args[2].start, args[2].stop,
655                                      args[2].step)
656            if zstart is None:
657                zstart = 0
658            if zstop is None:
659                zstop = self.nbands
660            if zstep is None:
661                zstep = 1
662            bands = list(range(zstart, zstop, zstep))
663        elif isinstance(args[2], int):
664            bands = [args[2]]
665        else:
666            # Band indices should be in a list
667            bands = args[2]
668
669        orig = self.image.__getitem__(args[:2])
670        if len(orig.shape) == 1:
671            orig = orig[np.newaxis, np.newaxis, :]
672        elif len(orig.shape) == 2:
673            orig = orig[np.newaxis, :]
674        transformed_xy = np.zeros(orig.shape[:2] + (self.shape[2],),
675                                  self.transform.dtype)
676        for i in range(transformed_xy.shape[0]):
677            for j in range(transformed_xy.shape[1]):
678                transformed_xy[i, j] = self.transform(orig[i, j])
679        # Remove unnecessary dimensions
680
681        transformed = np.take(transformed_xy, bands, 2)
682
683        return transformed.squeeze()
684
685    def __str__(self):
686        s = '\tTransformedImage object with output dimensions:\n'
687        s += '\t# Rows:         %6d\n' % (self.nrows)
688        s += '\t# Samples:      %6d\n' % (self.ncols)
689        s += '\t# Bands:        %6d\n\n' % (self.shape[2])
690        s += '\tThe linear transform is applied to the following image:\n\n'
691        s += str(self.image)
692        return s
693
694    def read_pixel(self, row, col):
695        return self.transform(self.image.read_pixel(row, col))
696
697    def load(self):
698        '''Loads all image data, transforms it, and returns an ndarray).'''
699        data = self.image.load()
700        return self.transform(data)
701
702    def read_subregion(self, row_bounds, col_bounds, bands=None):
703        '''
704        Reads a contiguous rectangular sub-region from the image. First
705        arg is a 2-tuple specifying min and max row indices.  Second arg
706        specifies column min and max.  If third argument containing list
707        of band indices is not given, all bands are read.
708        '''
709        data = self.image.read_subregion(row_bounds, col_bounds)
710        xdata = self.transform(data)
711        if bands:
712            return np.take(xdata, bands, 2)
713        else:
714            return xdata
715
716    def read_subimage(self, rows, cols, bands=None):
717        '''
718        Reads a sub-image from a rectangular region within the image.
719        First arg is a 2-tuple specifying min and max row indices.
720        Second arg specifies column min and max. If third argument
721        containing list of band indices is not given, all bands are read.
722        '''
723        data = self.image.read_subimage(rows, cols)
724        xdata = self.transform(data)
725        if bands:
726            return np.take(xdata, bands, 2)
727        else:
728            return xdata
729
730    def read_datum(self, i, j, k):
731        return self.read_pixel(i, j)[k]
732
733    def read_bands(self, bands):
734        shape = (self.image.nrows, self.image.ncols, len(bands))
735        data = np.zeros(shape, float)
736        for i in range(shape[0]):
737            for j in range(shape[1]):
738                data[i, j] = self.read_pixel(i, j)[bands]
739        return data
740
741class MemmapFile(object):
742    '''Interface class for SpyFile subclasses using `numpy.memmap` objects.'''
743
744    def _disable_memmap(self):
745        '''Disables memmap and reverts to direct file reads (slower).'''
746        self._memmap = None
747
748    @property
749    def using_memmap(self):
750        '''Returns True if object is using a `numpy.memmap` to read data.'''
751        return self._memmap is not None
752
753    def open_memmap(self, **kwargs):
754        '''Returns a new `numpy.memmap` object for image file data access.
755
756        Keyword Arguments:
757
758            `interleave` (str, default 'bip'):
759
760                Specifies the shape/interleave of the returned object. Must be
761                one of ['bip', 'bil', 'bsq', 'source']. If not specified, the
762                memmap will be returned as 'bip'. If the interleave is
763                'source', the interleave of the memmap will be the same as the
764                source data file. If the number of rows, columns, and bands in
765                the file are R, C, and B, the shape of the returned memmap
766                array will be as follows:
767
768                .. table::
769
770                    ========== ===========
771                    interleave array shape
772                    ========== ===========
773                    'bip'      (R, C, B)
774                    'bil'      (R, B, C)
775                    'bsq'      (B, R, C)
776                    ========== ===========
777
778            `writable` (bool, default False):
779
780                If `writable` is True, modifying values in the returned memmap
781                will result in corresponding modification to the image data
782                file.
783        '''
784        src_inter = {spy.BIL: 'bil',
785                     spy.BIP: 'bip',
786                     spy.BSQ: 'bsq'}[self.interleave]
787        dst_inter = kwargs.get('interleave', 'bip').lower()
788        if dst_inter not in ['bip', 'bil', 'bsq', 'source']:
789            raise ValueError('Invalid interleave specified.')
790        if kwargs.get('writable', False) is True:
791            mode = 'r+'
792        else:
793            mode = 'r'
794        memmap = self._open_memmap(mode)
795        if dst_inter == 'source':
796            dst_inter = src_inter
797        if src_inter == dst_inter:
798            return memmap
799        else:
800            return np.transpose(memmap, interleave_transpose(src_inter,
801                                                             dst_inter))
802
803    def asarray(self, writable=False):
804        '''Returns an object with a standard numpy array interface.
805
806        The function returns a numpy memmap created with the
807        `open_memmap` method.
808
809        This function is for compatibility with ImageArray objects.
810
811        Keyword Arguments:
812
813            `writable` (bool, default False):
814
815                If `writable` is True, modifying values in the returned
816                memmap will result in corresponding modification to the
817                image data file.
818        '''
819        return self.open_memmap(writable=writable)
820
821def interleave_transpose(int1, int2):
822    '''Returns the 3-tuple of indices to transpose between interleaves.
823
824    Arguments:
825
826        `int1`, `int2` (string):
827
828            The input and output interleaves.  Each should be one of "bil",
829            "bip", or "bsq".
830
831    Returns:
832
833        A 3-tuple of integers that can be passed to `numpy.transpose` to
834        convert and RxCxB image between the two interleaves.
835    '''
836    if int1.lower() not in ('bil', 'bip', 'bsq'):
837        raise ValueError('Invalid interleave: %s' % str(int1))
838    if int2.lower() not in ('bil', 'bip', 'bsq'):
839        raise ValueError('Invalid interleave: %s' % str(int2))
840    int1 = int1.lower()
841    int2 = int2.lower()
842    if int1 == 'bil':
843        if int2 == 'bil':
844            return (1, 1, 1)
845        elif int2 == 'bip':
846            return (0, 2, 1)
847        else:
848            return (1, 0, 2)
849    elif int1 == 'bip':
850        if int2 == 'bil':
851            return (0, 2, 1)
852        elif int2 == 'bip':
853            return (1, 1, 1)
854        else:
855            return (2, 0, 1)
856    else:  # bsq
857        if int2 == 'bil':
858            return (1, 0, 2)
859        elif int2 == 'bip':
860            return (1, 2, 0)
861        else:
862            return (1, 1, 1)
863