#!/usr/bin/env python
# import libraries
from sklearn.base import BaseEstimator, TransformerMixin
import numpy as np
import matplotlib.pyplot as plt
[docs]class image_cropper( BaseEstimator, TransformerMixin ):
"""Implements a transformer that cuts out only the non-null regions of an
IRIS image (be it SJI or spectra). Null pixels of the image are encoded as
-200.
The bounds are found by moving in lines from all sides towards the center
until the number of nonzero pixels stops increasing. To make sure everything
worked well, the cropped image is checked for negative pixels at the end,
throwing an error if more than 5% of the image border pixels or the whole image
are negative. In this way, bounds for the image are determined with the `fit`
method while the `transform` method returns a the cropped image.
Parameters
----------
offset : integer
Number of pixels that are removed as a safety border from all sides
after the cropping.
check_coverage : boolean
Whether to check the coverage of the cropped image. It can happen that
there are patches of negative values in images, either due to loss of
data during transmission (typically a band or a large rectangular patch
of negative data) or due to overall low data counts (missing data is no
data).
image_cropper labels an image as corrupt if >5% of its pixels are still
negative after cropping. This might be problematic for lines with low
data counts (and therefore many missing pixels) and the user is advised
to disable the coverage check for such lines.
A method that is able to distinguish missing data arising from
transmission errors from missing data due to low data counts could be
helpful here.
"""
# constructor
def __init__( self, offset=0, check_coverage=True ):
self._xmin = self._xmax = self._ymin = self._ymax = 0
self._image_ref = None
self._offset = offset
self._check_coverage = check_coverage
# fit method: find boundaries
[docs] def fit( self, X, y=None ):
"""
Determines bounds for the supplied image with the sliding lines approach
mentioned in the class description.
Parameters
----------
X : numpy.ndarray
2D image data that is to be cropped, format [y,x]
Returns
-------
image_cropper
Returns this object with fitted variables.
"""
# set image reference for access outside the fit function
self._image_ref = X
# raise exception if the image is NULL (-200) everywhere
if np.all( self._image_ref < 0 ):
raise NullImageException("Null image cannot be cropped")
# functions to get bounds on the image from all sides:
# A line is moved from the outside towards the center until the number
# of nonzero pixels stops increasing
def get_lower_bound( image ):
previous_nonzero_pixels = 0
for i in range( image.shape[1] ):
nonzero_pixels = np.sum( image[:,i] >= 0 )
if nonzero_pixels > 0 and previous_nonzero_pixels-nonzero_pixels>=0 and i>0:
return i
previous_nonzero_pixels = nonzero_pixels
return image.shape[1]
def get_upper_bound( image ):
previous_nonzero_pixels = 0
for i in range( image.shape[1] ):
nonzero_pixels = np.sum( image[:,image.shape[1]-i-1] >= 0 )
if nonzero_pixels > 0 and previous_nonzero_pixels-nonzero_pixels>=0 and i>0:
return image.shape[1]-i
previous_nonzero_pixels = nonzero_pixels
return image.shape[1]
def get_left_bound( image ):
previous_nonzero_pixels = 0
for i in range( image.shape[0] ):
nonzero_pixels = np.sum( image[i,:] >= 0 )
if nonzero_pixels > 0 and previous_nonzero_pixels-nonzero_pixels>=0 and i>0:
return i
previous_nonzero_pixels = nonzero_pixels
return image.shape[0]
def get_right_bound( image ):
previous_nonzero_pixels = 0
for i in range( image.shape[0] ):
nonzero_pixels = np.sum( image[image.shape[0]-i-1,:] >= 0 )
if nonzero_pixels > 0 and previous_nonzero_pixels-nonzero_pixels>=0 and i>0:
return image.shape[0]-i
previous_nonzero_pixels = nonzero_pixels
return image.shape[0]
# set image boundaries (plus add a possible defined offset)
self._xmin = get_lower_bound( self._image_ref ) + self._offset
self._xmax = get_upper_bound( self._image_ref ) - self._offset
self._ymin = get_left_bound( self._image_ref ) + self._offset
self._ymax = get_right_bound( self._image_ref ) - self._offset
# check whether image has some extent at all
min_extent = 10 # TODO: this needs to be on better theoretical foundation
if self._xmax - self._xmin < min_extent or self._ymax - self._ymin < min_extent:
raise CorruptImageException("This image contains almost no data after cropping! (less than 10 pixels on at least one axis)")
# raise a corrupt image exception if more than 5% of the image or the
# image border are still negative
# This check can be disable with check_coverage = False
if self._check_coverage:
if np.mean( self._image_ref[self._ymin:self._ymax,self._xmin:self._xmax] < 0 ) > 0.05:
raise CorruptImageException("Image might contain a corrupt patch, more than 5% have a negative pixel value!")
if np.mean( self._image_ref[self._ymin,self._xmin:self._xmax] < 0 ) > 0.05 or np.mean( self._image_ref[self._ymax,self._xmin:self._xmax] < 0 ) > 0.05 or np.mean( self._image_ref[self._ymin:self._ymax,self._xmin] < 0 ) > 0.05 or np.mean( self._image_ref[self._ymin:self._ymax,self._xmax] < 0 ) > 0.05:
raise CorruptImageException("Image border contains more than 5% negative pixels!")
return self
# transform method: return bounded image
# function to return bounds computed by the fit function
[docs] def get_bounds( self ):
"""
Gets the bounds computed by image_cropper.
Returns
-------
int
List [ xmin, xmax, ymin, ymax ] for the given image.
"""
if self._image_ref is None:
raise ValueError("Please pass an image first using the fit method.")
else:
return [self._xmin, self._xmax, self._ymin, self._ymax]
# helper function: plot image with boundary
[docs] def plot_bounding_boxed( self ):
"""Plots the supplied image and places a bounding box around it."""
if self._image_ref is None:
raise ValueError("Please pass an image first using the fit method.")
else:
plt.imshow( self._image_ref.clip(min=0)**0.4, cmap="gist_heat", vmax=10, origin="lower" )
plt.plot( [self._xmin,self._xmax], [self._ymin,self._ymin], color='white' ); plt.plot( [self._xmin,self._xmax], [self._ymax,self._ymax], color='white' )
plt.plot( [self._xmax,self._xmax], [self._ymin,self._ymax], color='white' ); plt.plot( [self._xmin, self._xmin], [self._ymin,self._ymax], color='white' )
plt.show()
# Exception classes for null images and corrupt images
class NullImageException(Exception):
pass
class CorruptImageException(Exception):
pass
# Test code
if __name__ == "__main__":
from irisreader import observation
obs = observation( "/home/chuwyler/Desktop/FITS/20140906_112339_3820259253/" )
X = obs.sji[0].get_image_step( 0 )
plt.imshow( X.clip(min=0)**0.4, cmap="gist_heat", origin="lower", vmax=7 )
cropper = image_cropper()
X_cropped = cropper.fit_transform( X )
plt.imshow( X_cropped.clip(min=0)**0.4, cmap="gist_heat", origin="lower", vmax=7 )