Source code for pypeit.sampling

# Licensed under a 3-clause BSD style license - see LICENSE.rst
# -*- coding: utf-8 -*-
"""
Provides a set of functions to handle resampling.

.. include common links, assuming primary doc root is up one directory
.. include:: ../include/links.rst

"""

import warnings
import numpy
from scipy import interpolate
import astropy.constants

from pypeit.core import moment

[docs]def spectral_coordinate_step(wave, log=False, base=10.0): """ Return the sampling step for the input wavelength vector. If the sampling is logarithmic, return the change in the logarithm of the wavelength; otherwise, return the linear step in angstroms. Args: wave (`numpy.ndarray`_): Wavelength coordinates of each spectral channel in angstroms. log (bool): (**Optional**) Input spectrum has been sampled geometrically. base (float): (**Optional**) If sampled geometrically, the sampling is done using a logarithm with this base. For natural logarithm, use numpy.exp(1). Returns: float: Spectral sampling step in either angstroms (log=False) or the step in log(angstroms). """ dw = numpy.diff(numpy.log(wave))/numpy.log(base) if log else numpy.diff(wave) if numpy.any( numpy.absolute(numpy.diff(dw)) > 100*numpy.finfo(dw.dtype).eps): raise ValueError('Wavelength vector is not uniformly sampled to numerical accuracy.') return numpy.mean(dw)
[docs]def spectrum_velocity_scale(wave): """ Determine the velocity sampling of an input wavelength vector when log sampled .. note:: The wavelength vector is assumed to be geometrically sampled! However, the input units expected to be in angstroms, not, e.g., log(angstrom). Args: wave (`numpy.ndarray`_): Wavelength coordinates of each spectral channel in angstroms. It is expected that the spectrum has been sampled geometrically Returns: float: Velocity scale of the spectrum in km/s. """ return astropy.constants.c.to('km/s').value*spectral_coordinate_step(wave, log=True, base=numpy.exp(1.))
[docs]def angstroms_per_pixel(wave, log=False, base=10.0, regular=True): """ Return a vector with the angstroms per pixel at each channel. When `regular=True`, the function assumes that the wavelengths are either sampled linearly or geometrically. Otherwise, it calculates the size of each pixel as the difference between the wavelength coordinates. The first and last pixels are assumed to have a width as determined by assuming the coordinate is at its center. Args: wave (`numpy.ndarray`_): (Geometric) centers of the spectrum pixels in angstroms. log (`numpy.ndarray`_, optional): The vector is geometrically sampled. base (:obj:`float`, optional): Base of the logarithm used in the geometric sampling. regular (:obj:`bool`, optional): The vector is regularly sampled. Returns: `numpy.ndarray`_: The angstroms per pixel. """ if regular: ang_per_pix = spectral_coordinate_step(wave, log=log, base=base) if log: ang_per_pix *= wave*numpy.log(base) else: ang_per_pix = numpy.diff([(3*wave[0]-wave[1])/2] + ((wave[1:] + wave[:-1])/2).tolist() + [(3*wave[-1]-wave[-2])/2]) return ang_per_pix
[docs]def _pixel_centers(xlim, npix, log=False, base=10.0): """ Determine the centers of pixels in a linearly or geometrically sampled vector given first, last and number of pixels Args: xlim (`numpy.ndarray`_) : (Geometric) Centers of the first and last pixel in the vector. npix (int) : Number of pixels in the vector. log (bool) : (**Optional**) The input range is (to be) logarithmically sampled. base (float) : (**Optional**) The base of the logarithmic sampling. The default is 10.0; use numpy.exp(1.) for the natural logarithm. Returns: `numpy.ndarray`_, float: A vector with the npix centres of the pixels and the sampling rate. If logarithmically binned, the sampling is the step in :math`\log x`. """ if log: logRange = numpy.log(xlim)/numpy.log(base) dlogx = numpy.diff(logRange)/(npix-1.) centers = numpy.power(base, numpy.linspace(*(logRange/dlogx), num=npix)*dlogx) return centers, dlogx dx = numpy.diff(xlim)/(npix-1.) centers = numpy.linspace(*(xlim/dx), num=npix)*dx return centers, dx
[docs]def _pixel_borders(xlim, npix, log=False, base=10.0): """ Determine the borders of the pixels in a vector given the first, last and number of pixels Args: xlim (`numpy.ndarray`_) : (Geometric) Centers of the first and last pixel in the vector. npix (int) : Number of pixels in the vector. log (bool) : (**Optional**) The input range is (to be) logarithmically sampled. base (float) : (**Optional**) The base of the logarithmic sampling. The default is 10.0; use numpy.exp(1.) for the natural logarithm. Returns: `numpy.ndarray`_, float: A vector with the (npix+1) borders of the pixels and the sampling rate. If logarithmically binned, the sampling is the step in :math`\log x`. """ if log: logRange = numpy.log(xlim)/numpy.log(base) dlogx = numpy.diff(logRange)/(npix-1.) borders = numpy.power(base, numpy.linspace(*(logRange/dlogx + [-0.5, 0.5]), num=npix+1)*dlogx) return borders, dlogx dx = numpy.diff(xlim)/(npix-1.) borders = numpy.linspace(*(xlim/dx + numpy.array([-0.5, 0.5])), num=npix+1)*dx return borders, dx
[docs]def resample_vector_npix(outRange=None, dx=None, log=False, base=10.0, default=None): """ Determine the number of pixels needed to resample a vector given first, last pixel and dx Args: outRange (list, `numpy.ndarray`_) : Two-element array with the starting and ending x coordinate of the pixel centers to divide into pixels of a given width. If *log* is True, this must still be the linear value of the x coordinate, not log(x)!. dx (float) : Linear or logarithmic pixel width. log (bool) : Flag that the range should be logarithmically binned. base (float) : Base for the logarithm default (int) : Default number of pixels to use. The default is returned if either *outRange* or *dx* are not provided. Returns: :obj:`tuple`: Returns two objects: The number of pixels to cover *outRange* with pixels of width *dx* and the adjusted range such that number of pixels of size dx is the exact integer. Raises: ValueError: Raised if the range is not a two-element vector """ # If the range or sampling are not provided, the number of pixels is # already set if outRange is None or dx is None: return default, outRange if len(outRange) != 2: raise ValueError('Output range must be a 2-element vector.') _outRange = numpy.atleast_1d(outRange).copy() npix = int( numpy.diff(numpy.log(_outRange))/numpy.log(base) / dx) + 1 if log else \ int(numpy.diff(_outRange)/dx) + 1 _outRange[1] = numpy.power(base, numpy.log(_outRange[0])/numpy.log(base) + dx*(npix-1)) \ if log else _outRange[0] + dx*(npix-1) return npix, _outRange
[docs]class Resample: r""" Resample regularly or irregularly sampled data to a new grid using integration. This is a generalization of the routine ``ppxf_util.log_rebin`` from from the `ppxf`_ package by Michele Cappellari. The abscissa coordinates (`x`) or the pixel borders (`xBorders`) for the data (`y`) should be provided for irregularly sampled data. If the input data is linearly or geometrically sampled (`inLog=True`), the abscissa coordinates can be generated using the input range for the (geometric) center of each grid point. If `x`, `xBorders`, and `xRange` are all None, the function assumes grid coordinates of `x=numpy.arange(y.shape[-1])`. The function resamples the data by constructing the borders of the output grid using the `new*` keywords and integrating the input function between those borders. The output data will be set to `ext_value` for any data beyond the abscissa limits of the input data. The data to resample (`y`) can be a 1D or 2D vector; the abscissa coordinates must always be 1D. If (`y`) is 2D, the resampling is performed along the last axis (i.e., `axis=-1`). The nominal assumption is that the provided function is a step function based on the provided input (i.e., `step=True`). If the output grid is substantially finer than the input grid, the assumption of a step function will be very apparent. To assume the function is instead linearly interpolated between each provided point, choose `step=False`; higher-order interpolations are not provided. If errors are provided, a nominal error propagation is performed to provide the errors in the resampled data. .. warning:: Depending on the details of the resampling, the output errors are likely highly correlated. Any later analysis of the resampled function should account for this. A covariance calculation will be provided in the future on a best-effort basis. The `conserve` keyword sets how the units of the input data should be treated. If `conserve=False`, the input data are expected to be in density units (i.e., per `x` coordinate unit) such that the integral over :math:`dx` is independent of the units of :math:`x` (i.e., flux per unit angstrom, or flux density). If `conserve=True`, the value of the data is assumed to have been integrated over the size of each pixel (i.e., units of flux). If `conserve=True`, :math:`y` is converted to units of per step in :math:`x` such that the integral before and after the resample is the same. For example, if :math:`y` is a spectrum in units of flux, the function first converts the units to flux density and then computes the integral over each new pixel to produce the new spectra with units of flux. .. todo:: - Allow the user to provide the output pixel borders directly. - Allow for higher order interpolations. - Allow for a covariance matrix calculation. Args: y (`numpy.ndarray`_): Data values to resample. Can be a numpy.ma.MaskedArray, and the shape can be 1 or 2D. If 1D, the shape must be :math:`(N_{\rm pix},)`; otherwise, it must be :math:`(N_y,N_{\rm pix})`. I.e., the length of the last axis must match the input coordinates. e (`numpy.ndarray`_, optional): Errors in the data that should be resampled. Can be a numpy.ma.MaskedArray, and the shape must match the input `y` array. These data are used to perform a nominal calculation of the error in the resampled array. mask (`numpy.ndarray`_, optional): A boolean array (masked values are True) indicating values in `y` that should be ignored during the resampling. The mask used during the resampling is the union of this object and the masks of `y` and `e`, if they are provided as numpy.ma.MaskedArrays. x (`numpy.ndarray`_, optional): Abcissa coordinates for the data, which do not need to be regularly sampled. If the pixel borders are not provided, they are assumed to be half-way between adjacent pixels, and the first and last borders are assumed to be equidistant about the provided value. If these coordinates are not provided, they are determined by the input borders, the input range, or just assumed to be the indices, :math:`0..N_{\rm pix}-1`. xRange (array-like, optional): A two-element array with the starting and ending value for the coordinates of the centers of the first and last pixels in y. Default is :math:`[0,N_{\rm pix}-1]`. xBorders (`numpy.ndarray`_, optional): An array with the borders of each pixel that must have a length of :math:`N_{\rm pix}+1`. inLog (:obj:`bool`, optional): Flag that the input is logarithmically binned, primarily meaning that the coordinates are at the geometric center of each pixel and the centers are spaced logarithmically. If false, the sampling is expected to be linear. newRange (array-like, optional): A two-element array with the (geometric) centers of the first and last pixel in the output vector. If not provided, assumed to be the same as the input range. newpix (:obj:`int`, optional): Number of pixels for the output vector. If not provided, assumed to be the same as the input vector. newLog (:obj:`bool`, optional): The output vector should be logarithmically binned. newdx (:obj:`float`, optional): The sampling step for the output vector. If `newLog=True`, this has to be the change in the logarithm of x for the output vector! If not provided, the sampling is set by the output range (see `newRange` above) and number of pixels (see `newpix` above). base (:obj:`float`, optional): The base of the logarithm used for both input and output sampling, if specified. The default is 10; use `numpy.exp(1)` for natural logarithm. ext_value (:obj:`float`, optional): Set extrapolated values to the provided float. By default, extrapolated values are set to 0. If set to None, values are just set to the linear exatrapolation of the data beyond the provided limits; use `ext_value=None` with caution! conserve (:obj:`bool`, optional): Conserve the integral of the input vector. For example, if the input vector is a spectrum in flux units, you should conserve the flux in the resampling; if the spectrum is in units of flux density, you do not want to conserve the integral. step (:obj:`bool`, optional): Treat the input function as a step function during the resampling integration. If False, use a linear interpolation between pixel samples. Attributes: x (`numpy.ndarray`_): The coordinates of the function on input. xborders (`numpy.ndarray`_): The borders of the input pixel samples. y (`numpy.ndarray`_): The function to resample. e (`numpy.ndarray`_): The 1-sigma errors in the function to resample. m (`numpy.ndarray`_): The boolean mask for the input function. outx (`numpy.ndarray`_): The coordinates of the function on output. outborders (`numpy.ndarray`_): The borders of the output pixel samples. outy (`numpy.ndarray`_): The resampled function. oute (`numpy.ndarray`_): The resampled 1-sigma errors. outf (`numpy.ndarray`_): The fraction of each output pixel that includes valid data from the input function. Raises: ValueError: Raised if *y* is not of type `numpy.ndarray`_, if *y* is not one-dimensional, or if *xRange* is not provided and the input vector is logarithmically binned (see *inLog* above). """ def __init__(self, y, e=None, mask=None, x=None, xRange=None, xBorders=None, inLog=False, newRange=None, newpix=None, newLog=True, newdx=None, base=10.0, ext_value=0.0, conserve=False, step=True): # Check operation can be performed if not isinstance(y, numpy.ndarray): raise ValueError('Input vector must be a numpy.ndarray!') if len(y.shape) > 2: raise ValueError('Input must be a 1D or 2D array!') # Setup the data, errors, and mask self.y = y.filled(0.0) if isinstance(y, numpy.ma.MaskedArray) else y.copy() self.twod = self.y.ndim == 2 self.e = None if e is None \ else e.filled(0.0) if isinstance(e, numpy.ma.MaskedArray) else e.copy() self.m = numpy.zeros(self.y.shape, dtype=bool) if mask is None else mask # Check the shapes if self.e is not None and self.e.shape != self.y.shape: raise ValueError('Error array shape mismatched!') if self.m.shape != self.y.shape: raise ValueError('Mask array shape mismatched!') # Get the union of all the relevant masks if isinstance(y, numpy.ma.MaskedArray): self.m |= y.mask if e is not None and isinstance(e, numpy.ma.MaskedArray): self.m |= e.mask # Get the input coordinates self.x = None self.xborders = None # this defines the self.x and self.xborders self._input_coordinates(x, xRange, xBorders, inLog, base) # If conserving integral, assume input is integrated over pixel # width and convert to a density function (divide by pixel size) if conserve: self.y /= (numpy.diff(self.xborders)[None,:] if self.twod \ else numpy.diff(self.xborders)) # Get the output coordinates self.outx = None self.outborders = None # this defines the self.outx and self.outborders self._output_coordinates(newRange, newpix, newLog, newdx, base) # Perform the resampling self.outy = self._resample_step(self.y) if step else self._resample_linear(self.y) # The mask and errors are always interpolated as a step function self.oute = None if self.e is None else self._resample_step(self.e, quad=True) self.outf = self._resample_step(numpy.invert(self.m).astype(int)) \ / numpy.diff(self.outborders) # Do not conserve the integral over the size of the pixel if not conserve: self.outy /= (numpy.diff(self.outborders)[None,:] if self.twod \ else numpy.diff(self.outborders)) if self.oute is not None: self.oute /= (numpy.diff(self.outborders)[None,:] if self.twod \ else numpy.diff(self.outborders)) # Set values for extrapolated regions if ext_value is not None: indx = (self.outborders[:-1] < self.xborders[0]) \ | (self.outborders[1:] > self.xborders[-1]) if numpy.sum(indx) > 0: if self.twod: self.outy[:,indx] = ext_value self.outf[:,indx] = 0. if self.oute is not None: self.oute[:,indx] = 0. else: self.outy[indx] = ext_value self.outf[indx] = 0. if self.oute is not None: self.oute[indx] = 0.
[docs] def _input_coordinates(self, x, xRange, xBorders, inLog, base): """ Determine the centers and pixel borders of the input coordinates. """ if (x is not None or xBorders is not None) and xRange is not None: warnings.warn('Provided both x or x borders and the x range. Ignoring range.') _xRange = xRange if x is None and xBorders is None else None if x is not None: if x.ndim != 1: raise ValueError('Coordinate vector must be 1D.') if x.size != self.y.shape[-1]: raise ValueError('Coordinate vector must match last dimension of value array.') if xBorders is not None: if xBorders.ndim != 1: raise ValueError('Coordinate borders must be 1D.') if xBorders.size != self.y.shape[-1]+1: raise ValueError('Coordinate borders must match last dimension of value array.') if x is None: if xBorders is not None: self.x = numpy.sqrt(xBorders[:-1]*xBorders[1:]) if inLog \ else (xBorders[:-1]+xBorders[1:])/2.0 elif xRange is not None: self.x = _pixel_centers(xRange, self.y.shape[-1], log=inLog, base=base)[0] else: self.x = numpy.arange(self.y.shape[-1]) + 0.5 else: self.x = x if xBorders is None: dx = numpy.diff(numpy.log(self.x)) if inLog else numpy.diff(self.x) self.xborders = numpy.exp(numpy.append(numpy.log(self.x[:-1]) - dx/2, numpy.log(self.x[-1]) + numpy.array([-1,1])*dx[-1]/2)) \ if inLog \ else numpy.append(self.x[:-1] - dx/2, self.x[-1] + numpy.array([-1,1])*dx[-1]/2) else: self.xborders = xBorders
[docs] def _output_coordinates(self, newRange, newpix, newLog, newdx, base): """Set the output coordinates.""" # Set the output range and number of pixels outRange = numpy.array([self.x[0], self.x[-1]]) if newRange is None \ else numpy.array(newRange) m, _outRange = resample_vector_npix(outRange=outRange, log=newLog, base=base, dx=newdx, default=(self.y.shape[-1] if newpix is None else newpix)) outRange = outRange if _outRange is None else _outRange # Get the output pixel borders self.outborders = _pixel_borders(outRange, m, log=newLog, base=base)[0] # Get the output coordinate vector self.outx = numpy.sqrt(self.outborders[:-1]*self.outborders[1:]) if newLog \ else (self.outborders[:-1]+self.outborders[1:])/2.0
[docs] def _resample_linear(self, v, quad=False): """Resample the vectors.""" # Combine the input coordinates and the output borders combinedX = numpy.append(self.outborders, self.x) srt = numpy.argsort(combinedX) combinedX = combinedX[srt] # Get the indices where the data should be reduced border = numpy.ones(combinedX.size, dtype=bool) border[self.outborders.size:] = False k = numpy.arange(combinedX.size)[border[srt]] # Calculate the integrand if self.twod: # Linearly interpolate the input function at the output border positions interp = interpolate.interp1d(self.x, v, axis=-1, assume_sorted=True, fill_value='extrapolate') combinedY = numpy.append(interp(self.outborders), v, axis=-1)[:,srt] integrand = (combinedY[:,1:]+combinedY[:,:-1])*numpy.diff(combinedX)[None,:]/2.0 else: # Linearly interpolate the input function at the output border positions interp = interpolate.interp1d(self.x, v, assume_sorted=True, fill_value='extrapolate') combinedY = numpy.append(interp(self.outborders), v)[srt] integrand = (combinedY[1:]+combinedY[:-1])*numpy.diff(combinedX)/2.0 if quad: integrand = numpy.square(integrand) # Use reduceat to calculate the integral out = numpy.add.reduceat(integrand, k[:-1], axis=-1) if k[-1] == combinedX.size-1 \ else numpy.add.reduceat(integrand, k, axis=-1)[...,:-1] return numpy.sqrt(out) if quad else out
[docs] def _resample_step(self, v, quad=False): """Resample the vectors.""" # Convert y to a step function # - repeat each element of the input vector twice _v = numpy.repeat(v, 2, axis=1) if self.twod else numpy.repeat(v, 2) # - repeat each element of the border array twice, and remove # the first and last elements _x = numpy.repeat(self.xborders, 2)[1:-1] # Combine the input coordinates and the output borders into a # single vector indx = numpy.searchsorted(_x, self.outborders) combinedX = numpy.insert(_x, indx, self.outborders) # Insert points at the borders of the output function v_indx = indx.copy() v_indx[indx >= _v.shape[-1]] = -1 combinedY = numpy.array([ numpy.insert(__v, indx, __v[v_indx]) for __v in _v ]) \ if self.twod else numpy.insert(_v, indx, _v[v_indx]) # Calculate the integrand integrand = combinedY[:,1:]*numpy.diff(combinedX)[None,:] if self.twod else \ combinedY[1:]*numpy.diff(combinedX) if quad: integrand = numpy.square(integrand) # Get the indices where the data should be reduced border = numpy.insert(numpy.zeros(_x.size, dtype=bool), indx, numpy.ones(self.outborders.size, dtype=bool)) k = numpy.arange(combinedX.size)[border] # Use reduceat to calculate the integral out = numpy.add.reduceat(integrand, k[:-1], axis=-1) if k[-1] == combinedX.size-1 \ else numpy.add.reduceat(integrand, k, axis=-1)[...,:-1] # if self.twod: # out = numpy.array(numpy.add.reduceat(integrand, k[:-1], axis=-1)) \ # if k[-1] == combinedX.size-1 else \ # numpy.array(numpy.add.reduceat(integrand, k, axis=-1))[:, 0:-1] # # else: # out = numpy.add.reduceat(integrand, k[:-1], axis=-1) if k[-1] == combinedX.size-1 \ # else numpy.add.reduceat(integrand, k, axis=-1)[:-1] return numpy.sqrt(out) if quad else out
[docs]def rectify_image(img, col, bpm=None, ocol=None, max_ocol=None, extract_width=None, mask_threshold=0.5): r""" Rectify the image by shuffling flux along columns using the provided column mapping. The image recification is one dimensional, treating each image row independently. It can be done either by a direct resampling of the image columns using the provided mapping of output to input column location (see `col` and :class:`Resample`) or by an extraction along the provided column locations (see `extract_width`). The latter is generally faster; however, when resampling each row, the flux is explicitly conserved (see the `conserve` argument of :class:`Resample`). Args: img (`numpy.ndarray`_): The 2D image to rectify. Shape is :math:`(N_{\rm row}, N_{\rm col})`. col (`numpy.ndarray`_): The array mapping each output column to its location in the input image. That is, e.g., `col[:,0]` provides the column coordinate in `img` that should be rectified to column 0 in the output image. Shape is :math:`(N_{\rm row}, N_{\rm map})`. bpm (`numpy.ndarray`_, optional): Boolean bad-pixel mask for pixels to ignore in input image. If None, no pixels are masked in the rectification. If provided, shape must match `img`. ocol (`numpy.ndarray`_, optional): The column in the output image for each column in `col`. If None, assume:: ocol = numpy.arange(col.shape[1]) These coordinates can fall off the output image (i.e., :math:`<0` or :math:`\geq N_{\rm out,col}`), but those columns are removed from the output). max_ocol (:obj:`int`, optional): The last viable column *index* to include in the output image; ie., for an image with `ncol` columns, this should be `ncol-1`. If None, assume `max(ocol)`. extract_width (:obj:`float`, optional): The width of the extraction aperture to use for the image rectification. If None, the image recification is performed using :class:`Resample` along each row. mask_threshold (:obj:`float`, optional): Either due to `bpm` or the bounds of the provided `img`, pixels in the rectified image may not be fully covered by valid pixels in `img`. Pixels in the output image with less than this fractional coverage of an input pixel are flagged in the output. Returns: Two `numpy.ndarray`_ objects are returned both with shape `(nrow,max_ocol+1)`, the rectified image and its boolean bad-pixel mask. """ # Check the input if img.ndim != 2: raise ValueError('Input image must be 2D.') if bpm is not None and bpm.shape != img.shape: raise ValueError('Image bad-pixel mask must match image shape.') _img = numpy.ma.MaskedArray(img, mask=bpm) nrow, ncol = _img.shape if col.ndim != 2: raise ValueError('Column mapping array must be 2D.') if col.shape[0] != nrow: raise ValueError('Number of rows in column mapping array must match image to rectify.') _ocol = numpy.arange(col.shape[1]) if ocol is None else numpy.atleast_1d(ocol) if _ocol.ndim != 1: raise ValueError('Output column indices must be provided as a vector.') if _ocol.size != col.shape[1]: raise ValueError('Output column indices must match columns in column mapping array.') _max_ocol = numpy.amax(_ocol) if max_ocol is None else max_ocol # Use an aperture extraction to rectify the image if extract_width is not None: # Select viable columns indx = (_ocol >= 0) & (_ocol <= _max_ocol) # Initialize the output image as all masked out_img = numpy.ma.masked_all((nrow,_max_ocol+1), dtype=float) # Perform the extraction out_img[:,_ocol[indx]] = moment.moment1d(_img.data, col[:,indx], extract_width, bpm=_img.mask)[0] # Determine what fraction of the extraction fell off the image coo = col[:,indx,None] + numpy.arange(numpy.ceil(extract_width)).astype(int)[None,None,:] \ - extract_width/2 in_image = (coo >= 0) | (coo < ncol) out_bpm = numpy.sum(in_image, axis=2)/extract_width < mask_threshold # Return the filled numpy.ndarray and boolean mask return out_img.filled(0.0)/extract_width, out_img.mask | out_bpm # Directly resample the image # # `col` provides the column in the input image that should # resampled to a given position in the output image: the value of # the flux at img[:,col[:,0]] should be rectified to `outimg[:,0]`. # To run the resampling algorithm we need to invert this. That is, # instead of having a function that provides the output column as a # function of the input column, we want a function that provies the # input column as a function of the output column. # Instantiate the output image out_img = numpy.zeros((nrow,_max_ocol+1), dtype=float) out_bpm = numpy.zeros((nrow,_max_ocol+1), dtype=bool) icol = numpy.arange(ncol) for i in range(nrow): # Get the coordinate vector of the output column _icol = interpolate.interp1d(col[i,:], _ocol, copy=False, bounds_error=False, fill_value='extrapolate', assume_sorted=True)(icol) # Resample it r = Resample(_img[i,:], x=_icol, newRange=[0,_max_ocol], newpix=_max_ocol+1, newLog=False, conserve=True) # Save the resampled data out_img[i,:] = r.outy # Flag pixels out_bpm[i,:] = r.outf < mask_threshold return out_img, out_bpm