Beispiel #1
0
    avg_stack = avg_stack + img
if max_int_stack_file is not None:
    max_int_stack = np.zeros((h,w,c), dtype=img.dtype)
    max_int_stack = max_int_stack + img
    # Maximum intensities need to be 32-bit to prevent overflow
    max_pix_intensities = np.zeros((h,w), dtype='uint32')
    max_pix_intensities = max_pix_intensities + img.sum(axis=2)
if max_dev_stack_file is not None:
    max_dev_stack = np.zeros((h,w,c), dtype=img.dtype)
    max_dev_stack = max_dev_stack + img
    max_pix_deviations = np.zeros((h,w), dtype='uint16')
    max_pix_deviations = max_pix_deviations + img.std(axis=2)
if max_col_diff_stack_file is not None:
    max_col_diff_stack = np.zeros((h,w,c), dtype=img.dtype)
    max_col_diff_stack = max_col_diff_stack + img
    max_col_diffs = img.max(axis=2)-img.min(axis=2)
if median_stack_file is not None:
    # Import HDF5 library
    import h5py
    median_stack = np.zeros((h,w,c), dtype=img.dtype)
    # Create a HDF5 file for temporary storage space for median stack
    # calculations. The file is memorymapped, so access from the
    # program is easy, but amount of disk-I/O will be huge.
    median_hdf_file = median_stack_file[:-3]
    median_hdf_file = median_hdf_file + 'H5'
    median_hdf_fid = h5py.File(median_hdf_file,'w')
    # Datasets will be h-high, w-wide and n_files-deep with the same
    # bit-depth as the input images
    hdf_R = median_hdf_fid.create_dataset('R', (h,w,n_files), dtype=img.dtype)
    hdf_G = median_hdf_fid.create_dataset('G', (h,w,n_files), dtype=img.dtype)
    hdf_B = median_hdf_fid.create_dataset('B', (h,w,n_files), dtype=img.dtype)
Beispiel #2
0
class Image(object):
    '''Class for handling images.

    :param img: array holding image data
    :type img: ndarray or None
    :param fname: image filename
    :type fname: str or None
    :param enhancements: image processing applied to the image
    :type enhancements: dictionary or None
    :param nprocs: number or parallel processes
    :type nprocs: int
    '''
    def __init__(self, img=None, fname=None, enhancements=None, nprocs=1):
        self.img = img
        self.fname = fname
        self._nprocs = nprocs

        self._pool = None

        if fname is not None:
            self._read()
        if enhancements:
            LOGGER.info("Preprocessing image.")
            self.enhance(enhancements)
        self.shape = None
        self._to_numpy()

    def __add__(self, img):
        self._to_numpy()
        if isinstance(img, Image):
            img.to_numpy()
            return Image(img=self.img + img.img, nprocs=self._nprocs)
        else:
            # Assume a numpy array or scalar
            return Image(img=self.img + img, nprocs=self._nprocs)

    def __radd__(self, img):
        return self.__add__(img)

    def __sub__(self, img):
        self._to_numpy()
        if isinstance(img, Image):
            img.to_numpy()
            return Image(img=self.img - img.img, nprocs=self._nprocs)
        else:
            # Assume a numpy array or scalar
            return Image(img=self.img - img, nprocs=self._nprocs)

    def __rsub__(self, img):
        return self.__sub__(img)

    def __isub__(self, img):
        self.img = self.__sub__(img)

    def __mul__(self, img):
        self._to_numpy()
        if isinstance(img, Image):
            img.to_numpy()
            return Image(img=self.img * img.img, nprocs=self._nprocs)
        else:
            # Assume a numpy array or scalar
            return Image(img=self.img * img, nprocs=self._nprocs)

    def __rmul__(self, img):
        return self.__mul__(img)

    def __div__(self, img):
        self._to_numpy()
        if isinstance(img, Image):
            self._to_numpy()
            img.to_numpy()
            return Image(img=self.img / img.img, nprocs=self._nprocs)
        else:
            # Assume a numpy array or scalar
            return Image(img=self.img / img, nprocs=self._nprocs)

    def __abs__(self):
        self._to_numpy()
        return Image(img=np.abs(self.img), nprocs=self._nprocs)

    def __lt__(self, img):
        self._to_numpy()
        if isinstance(img, Image):
            img.to_numpy()
            img = img.img
        return self.img < img

    def __le__(self, img):
        self._to_numpy()
        if isinstance(img, Image):
            img.to_numpy()
            img = img.img
        return self.img <= img

    def __gt__(self, img):
        self._to_numpy()
        if isinstance(img, Image):
            img.to_numpy()
            img = img.img
        return self.img > img

    def __ge__(self, img):
        self._to_numpy()
        if isinstance(img, Image):
            img.to_numpy()
            img = img.img
        return self.img >= img

    def __eq__(self, img):
        self._to_numpy()
        if isinstance(img, Image):
            img.to_numpy()
            img = img.img
        return self.img == img

    def __getitem__(self, idx):
        self._to_numpy()
        return self.img[idx]

    def __setitem__(self, idx, val):
        self._to_numpy()
        self.img[idx] = val

    def _read(self):
        '''Read the image.
        '''
        LOGGER.info("Reading image %s.", self.fname)
        self.img = PMImage(self.fname)

    def to_numpy(self):
        '''Convert from PMImage to Numpy ndarray.
        '''
        self._to_numpy()

    def _to_numpy(self):
        '''Convert from PMImage to numpy.
        '''
        if isinstance(self.img, PMImage):
            self.img = to_numpy(self.img).astype(np.float64)
            self.shape = self.img.shape

    def _to_imagemagick(self, bits=16):
        '''Convert from numpy to PMImage.
        '''
        self.img = to_imagemagick(self.img, bits=bits)

    def save(self, fname, bits=16, enhancements=None):
        '''Save the image data.

        :param fname: output filename
        :type fname: str
        :param bits: output bit-depth
        :type bits: int
        :param enhancements: image processing applied to the image before saving
        :type enhancements: dictionary or None
        '''

        if enhancements:
            LOGGER.info("Postprocessing output image.")
            self.enhance(enhancements)
        self._to_imagemagick(bits=bits)
        LOGGER.info("Saving %s.", fname)
        self.img.write(fname)

    def min(self):
        '''Return the minimum value in the image.

        :rtype: float
        '''
        self._to_numpy()
        return np.min(self.img)

    def max(self):
        '''Return the maximum value in the image.

        :rtype: float
        '''
        self._to_numpy()
        return np.max(self.img)

    def luminance(self):
        '''Return luminance (channel average) as Numpy ndarray.

        :rtype: Numpy ndarray
        '''
        self._to_numpy()
        if len(self.img.shape) == 3:
            return Image(img=np.mean(self.img, 2), nprocs=self._nprocs)
        else:
            return Image(img=self.img, nprocs=self._nprocs)

    def enhance(self, enhancements):
        '''Enhance the image with the given function(s) and argument(s).

        :param enhancements: image processing methods
        :type enhancements: dictionary

        Available image processing methods:

        * ``br``: Blue - Red

          * possible calls:

            * ``{'br': None}``
            * ``{'br': float}``

          * optional arguments:

            * ``float``: multiplier for blue channel [``mean(red/green)``]

        * ``emboss``: emboss image using *ImageMagick*

          * possible calls:

            * ``{'emboss': None}``
            * ``{'emboss': float}``
            * ``{'emboss': [float, float]}``

          * optional arguments:

            * ``float``: light source azimuth in degrees [``90``]
            * ``float``: light source elevation in degrees [``45``]

        * ``gamma``: gamma correction using *ImageMagick*

          * possible calls:

            * ``{'gamma': float}``

          * required arguments:

            * ``float``: gamma value

        * ``gradient``: remove image gradient

          * possible calls:

            * ``{'gradient': None}``
            * ``{'gradient': float}``

          * optional arguments:

            * ``float`` (blur radius) [``min(image dimensions)/20``]

        * ``gr``: Green - Red

          * possible calls:

            * ``{'gr': None}``
            * ``{'gr': float}``

          * optional arguments:

            * ``float``: multiplier for red channel [``mean(green/red)``]

        * ``rgb_sub``: Subtract luminance from each color channel

          * possible calls:

            ``{'rgb_sub': None}``

        * ``rgb_mix``: Subtract luminance from each color channel and mix it
          back to the original image

          * possible calls:

            ``{'rgb_mix': None}``
            ``{'rgb_mix': float}``

          * optional arguments:

            * ``float``: mixing ratio [``0.7``]

        * ``stretch``: linear histogram stretch

          * possible calls:

            * ``{'stretch': None}``
            * ``{'stretch': float}``
            * ``{'stretch': [float, float]}``

          * optional arguments:

            * ``float``: low cut threshold [``0.01``]
            * ``float``: high cut threshold [``1 - <low cut threshold>``]

        * ``usm``: unsharp mask using *ImageMagick*

          * possible calls:

            * ``{'usm': [float, float]}``
            * ``{'usm': [float, float, float]}``
            * ``{'usm': [float, float, float, float]}``

          * required arguments:

            * ``float``: radius
            * ``float``: amount

          * optional arguments:

            * ``float``: standard deviation of the gaussian [``sqrt(radius)``]
            * ``float``: threshold [``0``]

        '''

        functions = {
            'usm': self._usm,
            'emboss': self._emboss,
            'blur': self._blur,
            'gamma': self._gamma,
            'br': self._blue_red_subtract,
            'gr': self._green_red_subtract,
            'rgb_sub': self._rgb_subtract,
            'rgb_mix': self._rgb_mix,
            'gradient': self._remove_gradient,
            'stretch': self._stretch
        }

        for key in enhancements:
            LOGGER.info("Apply \"%s\".", key)
            func = functions[key]
            func(enhancements[key])

    def _channel_difference(self, chan1, chan2, multiplier=None):
        '''Calculate channel difference: chan1 * multiplier - chan2.
        '''
        self._to_numpy()
        chan1 = self.img[:, :, chan1].copy()
        chan2 = self.img[:, :, chan2].copy()
        if multiplier is None:
            idxs = np.logical_and(
                np.logical_and(1.5 * chan1 < chan2, 2.5 * chan1 > chan2),
                chan1 > 0)
            if np.all(np.invert(idxs)):
                multiplier = 2
            else:
                multiplier = np.mean(chan2[idxs] / chan1[idxs])
        else:
            if isinstance(multiplier, list):
                multiplier = multiplier[0]
        LOGGER.debug("Multiplier: %.3lf", multiplier)
        self.img = multiplier * chan1 - chan2

    def _blue_red_subtract(self, args):
        '''Subtract red channel from the blue channel after scaling
        the blue channel using the supplied multiplier.
        '''
        LOGGER.debug("Calculating channel difference, Blue - Red.")
        self._channel_difference(2, 0, multiplier=args)

    def _green_red_subtract(self, args):
        '''Subtract red channel from the green channel after scaling
        the green channel using the supplied multiplier.
        '''
        LOGGER.debug("Calculating channel difference, Green - Red.")
        self._channel_difference(1, 0, multiplier=args)

    def _rgb_subtract(self, args):
        '''Subtract mean(r,g,b) from all the channels.
        '''
        LOGGER.debug("Subtracting luminance from color channels.")
        del args
        self._to_numpy()
        luminance = self.luminance().img
        for i in range(3):
            self.img[:, :, i] -= luminance
        self.img -= self.img.min()

    def _rgb_mix(self, args):
        '''Subtract mean(r,g,b) from all the channels, and blend it
        back to the original image.

        :param args: mixing factor [0.7]
        :type args: float
        '''

        if args is None:
            args = 0.7
        else:
            args = args[0]

        LOGGER.debug("Mixing factor: %.2lf", args)
        self._to_numpy()
        img = Image(img=self.img.copy(), nprocs=self._nprocs)
        img.enhance({'rgb_sub': None})

        self.img *= (1 - args)
        self.img += args * img.img

    def _stretch(self, args):
        '''Apply a linear stretch to the image.
        '''

        self._to_numpy()
        self.img -= self.img.min()

        if args is None:
            args = []
        if not isinstance(args, list):
            args = [args]
        if len(args) == 0:
            args.append(0.01)
        if len(args) == 1:
            args.append(1 - args[0])

        LOGGER.debug("low cut: %.0f %%, high cut: %.0f %%", 100 * args[0],
                     100 * args[1])

        hist_num_points = 2**16 - 1

        # Use luminance
        if len(self.img.shape) == 3:
            lumin = np.mean(self.img, 2)
        else:
            lumin = self.img.copy()

        # histogram
        hist, _ = np.histogram(lumin.flatten(), hist_num_points, normed=True)
        # cumulative distribution function
        cdf = hist.cumsum()
        # normalize to image maximum
        cdf = self.img.max() * cdf / cdf[-1]

        # find lower end truncation point
        start = 0
        csum = 0
        while csum < cdf[-1] * args[0]:
            csum = cdf[start]
            start += 1
        # higher end truncation point
        end = cdf.size - 1
        csum = cdf[-1]
        while csum > cdf[-1] * args[1]:
            csum = cdf[end]
            end -= 1

        LOGGER.debug("Truncation points: %d and %d", start, end)

        # calculate the corresponding data values
        start_val = start * self.img.max() / hist_num_points
        end_val = end * self.img.max() / hist_num_points

        # truncate
        self.img[self.img < start_val] = start_val
        self.img[self.img > end_val] = end_val

    def _remove_gradient(self, args):
        '''Calculate the gradient from the image, subtract from the
        original, scale back to full bit depth and return the result.
        '''
        self._to_numpy()

        args = {'method': {'blur': args}}

        gradient = self._calculate_gradient(args)
        self.img -= gradient.img

        if self.img.min() < 0:
            self.img -= self.img.min()

    def _calculate_gradient(self, args):
        '''Calculate gradient from the image using the given method.
        :param method: name of the method for calculating the gradient
        '''
        methods = {
            'blur': self._gradient_blur,
            'random': self._gradient_random_points,
            'grid': self._gradient_grid_points
        }
        # 'user': self._gradient_get_user_points,
        # 'mask': self._gradient_mask_points,
        # 'all': self._gradient_all_points

        LOGGER.debug("Calculating gradient.")
        try:
            func = methods[args['method'].keys()[0]]
        except TypeError:
            LOGGER.error("Method \"%s\" not available, using method \"blur\"",
                         args['method'].keys()[0])
            args = {}
            args['method'] = {'blur': None}
            func = methods['blur']
        result = func(args['method'])
        shape = self.img.shape

        if args['method'].keys()[0] in ['blur']:
            return result

        x_pts, y_pts = result
        if len(shape) == 2:
            return Image(img=self._gradient_fit_surface(x_pts,
                                                        y_pts,
                                                        order=args['order']),
                         nprocs=self._nprocs)
        else:
            gradient = np.empty(shape)
            for i in range(shape[2]):
                gradient[:, :, i] = \
                    self._gradient_fit_surface(x_pts, y_pts,
                                               order=args['order'],
                                               chan=i)
            return Image(img=gradient, nprocs=self._nprocs)

    def _gradient_blur(self, args):
        '''Blur the image to get the approximation of the background
        gradient.
        '''
        gradient = Image(img=self.img.copy(), nprocs=self._nprocs)
        gradient.enhance(args)

        return gradient

    def _gradient_random_points(self, args):
        '''Automatically extract background points for gradient estimation.
        '''
        shape = self.img.shape
        y_pts = np.random.randint(shape[0], size=(args['points'], ))
        x_pts = np.random.randint(shape[1], size=(args['points'], ))

        return (x_pts, y_pts)

    def _gradient_grid_points(self, args):
        '''Get uniform sampling of image locations.
        '''
        shape = self.img.shape
        y_locs = np.arange(0, shape[0], args['points'])
        x_locs = np.arange(0, shape[1], args['points'])
        x_mat, y_mat = np.meshgrid(x_locs, y_locs, indexing='ij')

        return (x_mat.ravel(), y_mat.ravel())

    def _gradient_fit_surface(self, x_pts, y_pts, order=2, chan=None):
        '''Fit a surface to the given channel.
        '''
        shape = self.img.shape
        x_locs, y_locs = np.meshgrid(np.arange(shape[0]), np.arange(shape[1]))
        if chan is not None:
            poly = polyfit2d(x_pts,
                             y_pts,
                             self.img[y_pts, x_pts, chan].ravel(),
                             order=order)
            return polyval2d(x_locs, y_locs, poly)
        else:
            poly = polyfit2d(x_pts,
                             y_pts,
                             self.img[y_pts, x_pts].ravel(),
                             order=order)
            return polyval2d(x_locs, y_locs, poly).T

    def _rotate(self, *args):
        '''Rotate image.
        '''
        # use scipy.ndimage.interpolation.rotate()
        del args
        LOGGER.error("Image rotation not implemented.")

    def _usm(self, args):
        '''Use unsharp mask to enhance the image contrast.  Uses ImageMagick.
        '''

        self._to_imagemagick()
        if len(args) == 2:
            args.append(np.sqrt(args[0]))
        if len(args) == 3:
            args.append(0)
        LOGGER.debug(
            "Radius: %.0lf, amount: %.1lf, "
            "sigma: %.1lf, threshold: %.0lf.", args[0], args[1], args[2],
            args[3])
        self.img.unsharpmask(*args)

    def _emboss(self, args):
        '''Emboss filter the image. Actually uses shade() from
        ImageMagick.
        '''

        if args is None:
            args = []
        if len(args) == 0:
            args.append(90)
        if len(args) == 1:
            args.append(10)
        LOGGER.debug("Azimuth: %.1lf, elevation: %.1lf.", args[0], args[1])
        self._to_imagemagick()
        self.img.shade(*args)

    def _blur_im(self, args):
        '''Blur the image using ImageMagick.
        '''
        self._to_imagemagick()
        # args['radius'], args['weight'])
        self.img.blur(*args)

    def _blur(self, args):
        '''Blur the image using 1D convolution for each column and
        row. Data borders are padded with mean of the border area
        before convolution to reduce the edge effects.
        '''
        self._to_numpy()

        shape = self.img.shape
        if args is None:
            radius = int(np.min(shape[:2]) / 20.)
            sigma = radius / 3.
        else:
            radius = args[0]
            if len(args) > 1:
                sigma = args[1]
            else:
                sigma = radius / 3.

        def form_blur_data(data, radius):
            '''Form vectors for blur.
            '''
            vect = np.zeros(2 * radius + data.size - 1, dtype=data.dtype)
            vect[:radius] = np.mean(data[:radius])
            vect[radius:radius + data.size] = data
            vect[-radius:] = np.mean(data[-radius:])

            return vect

        def gaussian_kernel(radius, sigma):
            ''' Generate a gaussian convolution kernel.

            'param radius: kernel radius in pixels
            'type radisu: int
            'param sigma': standard deviation of the gaussian in pixels
            'type sigma: float
            '''

            sigma2 = sigma**2

            half_kernel = map(lambda x: 1/(2 * np.pi * sigma2) * \
                                  np.exp(-x**2 / (2 * sigma2)),
                              range(radius+1))
            kernel = np.zeros(2 * radius + 1)
            kernel[radius:] = half_kernel
            kernel[:radius + 1] = half_kernel[::-1]
            kernel /= np.sum(kernel)

            print sigma2

            return kernel

        kernel = gaussian_kernel(radius, sigma)

        LOGGER.debug("Blur radius is %.0lf pixels and sigma is %.3lf.", radius,
                     sigma)
        LOGGER.debug("Using %d threads.", self._nprocs)

        if self._nprocs > 1:
            self._pool = Pool(self._nprocs)

        for i in range(shape[-1]):
            # rows
            data = []
            for j in range(shape[0]):
                data.append(
                    [kernel, form_blur_data(self.img[j, :, i], radius)])
            if self._nprocs > 1:
                result = self._pool.map(_blur_worker, data)
            else:
                result = map(_blur_worker, data)

            # compile result data
            for j in range(shape[0]):
                self.img[j, :, i] = result[j][2 * radius:2 * radius + shape[1]]

            data = []
            # columns
            for j in range(shape[1]):
                data.append(
                    [kernel, form_blur_data(self.img[:, j, i], radius)])
            if self._nprocs > 1:
                result = self._pool.map(_blur_worker, data)
            else:
                result = map(_blur_worker, data)

            # compile result data
            for j in range(shape[1]):
                self.img[:, j, i] = result[j][2 * radius:2 * radius + shape[0]]

        self.img -= np.min(self.img)

    def _gamma(self, args):
        '''Apply gamma correction to the image.
        '''
        self._to_imagemagick()
        LOGGER.debug("Apply gamma correction, gamma: %.2lf.", args[0])
        self.img.gamma(args[0])