def getPointSourcePatch(self, px, py, minval=0., modelMask=None, radius=None, **kwargs): from astrometry.util.miscutils import get_overlapping_region img = self.getImage(px, py) assert (np.all(np.isfinite(img))) if radius is not None: R = int(np.ceil(radius)) H, W = img.shape cx = W // 2 cy = H // 2 img = img[max(cy - R, 0):min(cy + R + 1, H - 1), max(cx - R, 0):min(cx + R + 1, W - 1)] H, W = img.shape ix = int(np.round(px)) iy = int(np.round(py)) dx = px - ix dy = py - iy x0 = ix - W // 2 y0 = iy - H // 2 if modelMask is None: outimg = lanczos_shift_image(img, dx, dy) return Patch(x0, y0, outimg) mh, mw = modelMask.shape mx0, my0 = modelMask.x0, modelMask.y0 # print 'PixelizedPSF + modelMask' # print 'mx0,my0', mx0,my0, '+ mw,mh', mw,mh # print 'PSF image x0,y0', x0,y0, '+ W,H', W,H if ((mx0 >= x0 + W) or (mx0 + mw <= x0) or (my0 >= y0 + H) or (my0 + mh <= y0)): # No overlap return None # Otherwise, we'll just produce the Lanczos-shifted PSF # image as usual, and then copy it into the modelMask # space. L = 3 padding = L # Create a modelMask + padding sized stamp and insert PSF image into it mm = np.zeros((mh + 2 * padding, mw + 2 * padding), np.float32) yi, yo = get_overlapping_region(my0 - y0 - padding, my0 - y0 + mh - 1 + padding, 0, H - 1) xi, xo = get_overlapping_region(mx0 - x0 - padding, mx0 - x0 + mw - 1 + padding, 0, W - 1) mm[yo, xo] = img[yi, xi] mm = lanczos_shift_image(mm, dx, dy) mm = mm[padding:-padding, padding:-padding] assert (np.all(np.isfinite(mm))) return Patch(mx0, my0, mm)
def getDerivs(self): ''' Computes model-image derivatives for each parameter. Returns a nested list of tuples: allderivs: [ (param0:) [ (deriv, img), (deriv, img), ... ], (param1:) [], (param2:) [ (deriv, img), ], ] Where the *derivs* are *Patch* objects and *imgs* are *Image* objects. ''' allderivs = [] if self.isParamFrozen('catalog'): srcs = [] else: srcs = list(self.catalog.getThawedSources()) allsrcs = self.catalog if not self.isParamFrozen('images'): for i in self.images.getThawedParamIndices(): img = self.images[i] derivs = img.getParamDerivatives(self, allsrcs) mod0 = None for di, deriv in enumerate(derivs): if deriv is False: if mod0 is None: mod0 = self.getModelImage(img) p0 = img.getParams() stepsizes = img.getStepSizes() paramnames = img.getParamNames() oldval = img.setParam(di, p0[di] + stepsizes[di]) mod = self.getModelImage(img) img.setParam(di, oldval) deriv = Patch(0, 0, (mod - mod0) / stepsizes[di]) deriv.name = 'd(im%i)/d(%s)' % (i, paramnames[di]) allderivs.append([(deriv, img)]) del mod0 for src in srcs: srcderivs = [[] for i in range(src.numberOfParams())] for img in self.images: derivs = self._getSourceDerivatives(src, img) for k, deriv in enumerate(derivs): if deriv is None: continue srcderivs[k].append((deriv, img)) allderivs.extend(srcderivs) #print('allderivs:', len(allderivs)) #print('N params:', self.numberOfParams()) assert (len(allderivs) == self.numberOfParams()) return allderivs
def __init__(self, real, brights, tim, ps): from tractor.patch import Patch ''' *real*: The real source whose derivatives are my profiles. ''' # This a subclass of MultiParams and we pass the brightnesses # as our params. super(SourceDerivatives,self).__init__(*brights) self.real = real self.brights = brights self.umods = None # Get the current source profile and take pixel-space # derivatives by hand, for speed and to get symmetric # derivatives. realmod = real.getUnitFluxModelPatch(tim) p = realmod.patch dx = np.zeros_like(p) dy = np.zeros_like(p) # Omit a boundary of 1 pixel on all sides in both derivatives dx[1:-1, 1:-1] = (p[1:-1, :-2] - p[1:-1, 2:]) / 2. dy[1:-1, 1:-1] = (p[:-2, 1:-1] - p[2:, 1:-1]) / 2. # Convert from pixel-space to RA,Dec derivatives via CD matrix px, py = tim.getWcs().positionToPixel(real.pos, real) cdi = tim.getWcs().cdInverseAtPixel(px, py) dra = dx * cdi[0, 0] + dy * cdi[1, 0] ddec = dx * cdi[0, 1] + dy * cdi[1, 1] if ps is not None: import pylab as plt mx = p.max() plt.clf() plt.subplot(2,3,1) plt.imshow(p, interpolation='nearest', origin='lower', vmin=0, vmax=mx) mx *= 0.25 plt.subplot(2,3,2) plt.imshow(dx, interpolation='nearest', origin='lower', vmin=-mx, vmax=mx) plt.title('dx') plt.subplot(2,3,3) plt.imshow(dy, interpolation='nearest', origin='lower', vmin=-mx, vmax=mx) plt.title('dy') plt.subplot(2,3,5) sc = 3600/0.262 plt.imshow(dra/sc, interpolation='nearest', origin='lower', vmin=-mx, vmax=mx) plt.title('dra') plt.subplot(2,3,6) plt.imshow(ddec/sc, interpolation='nearest', origin='lower', vmin=-mx, vmax=mx) plt.title('ddec') ps.savefig() # These are our "unit-flux" models self.umods = [Patch(realmod.x0, realmod.y0, dra), Patch(realmod.x0, realmod.y0, ddec)]
def _checkModelMask(self, patch, mask): if self.expectModelMasks: if patch is not None: assert (mask is not None) if patch is not None and mask is not None: # not strictly required? but a good idea! assert (patch.patch.shape == mask.patch.shape) if patch is not None and mask is not None and patch.patch is not None: nonzero = Patch(patch.x0, patch.y0, patch.patch != 0) #print('nonzero type:', nonzero.patch.dtype) unmasked = Patch(mask.x0, mask.y0, np.logical_not(mask.mask)) #print('unmasked type:', unmasked.patch.dtype) bad = nonzero.performArithmetic(unmasked, '__iand__', otype=bool) assert (np.all(bad.patch == False))
def evaluate_grid_masked(self, x0, y0, mask, fx, fy, derivs=False): ''' mask: np array of booleans (NOT Patch object!) ''' from tractor.mix import c_gauss_2d_masked h, w = mask.shape result = np.zeros((h, w), np.float32) xderiv = yderiv = None if derivs: xderiv = np.zeros_like(result) yderiv = np.zeros_like(result) # print('gauss_2d_masked:', int(x0), int(y0), int(w), int (h), float(fx), float(fy),) # print(' ', self.amp.astype(np.float32),) # print(' ', self.mean.astype(np.float32),) # print(' ', self.var.astype(np.float32),) # print(' ', 'res', (result.shape, result.dtype),) # if xderiv is not None: # print(' ', 'xd', (xderiv.shape, xderiv.dtype),) # if yderiv is not None: # print(' ', 'yd', (yderiv.shape, yderiv.dtype),) # print(' ', 'mask', mask.shape, mask.dtype) rtn = c_gauss_2d_masked(int(x0), int(y0), int(w), int(h), float(fx), float(fy), self.amp.astype(np.float32), self.mean.astype(np.float32), self.var.astype(np.float32), result, xderiv, yderiv, mask) # print('gauss_2d_masked returned.') assert(rtn == 0) if derivs: return (Patch(x0, y0, result), Patch(x0, y0, xderiv), Patch(x0, y0, yderiv)) return Patch(x0, y0, result)
def evaluate_grid_dstn(self, x0, x1, y0, y1, cx, cy): ''' [x0,x1): (int) X values to evaluate [y0,y1): (int) Y values to evaluate (cx,cy): (float) pixel center of the MoG ''' from tractor.mix import c_gauss_2d_grid assert(self.D == 2) result = np.zeros((y1 - y0, x1 - x0)) rtn = c_gauss_2d_grid(int(x0), int(x1), int(y0), int(y1), cx, cy, self.amp, self.mean, self.var, result) if rtn == -1: raise RuntimeError('c_gauss_2d_grid failed') return Patch(x0, y0, result)
def galaxy_norm(self, tim, x=None, y=None): # Galaxy-detection norm from tractor.galaxy import ExpGalaxy from tractor.ellipses import EllipseE from tractor.patch import Patch h, w = tim.shape band = tim.band if x is None: x = w / 2. if y is None: y = h / 2. pos = tim.wcs.pixelToPosition(x, y) gal = ExpGalaxy(pos, NanoMaggies(**{band: 1.}), EllipseE(0.45, 0., 0.)) S = 32 mm = Patch(int(x - S), int(y - S), np.ones((2 * S + 1, 2 * S + 1), bool)) galmod = gal.getModelPatch(tim, modelMask=mm).patch galmod = np.maximum(0, galmod) galmod /= galmod.sum() galnorm = np.sqrt(np.sum(galmod**2)) return galnorm
def _realGetUnitFluxModelPatch(self, img, px, py, minval, modelMask=None): if modelMask is not None: x0, y0 = modelMask.x0, modelMask.y0 else: # choose the patch size halfsize = self._getUnitFluxPatchSize(img, px=px, py=py, minval=minval) # find overlapping pixels to render (outx, inx) = get_overlapping_region(int(np.floor(px - halfsize)), int(np.ceil(px + halfsize + 1)), 0, img.getWidth()) (outy, iny) = get_overlapping_region(int(np.floor(py - halfsize)), int(np.ceil(py + halfsize + 1)), 0, img.getHeight()) if inx == [] or iny == []: # no overlap return None x0, x1 = outx.start, outx.stop y0, y1 = outy.start, outy.stop psf = img.getPsf() # We have two methods of rendering profile galaxies: If the # PSF can be represented as a mixture of Gaussians, then we do # the analytic Gaussian convolution, producing a larger # mixture of Gaussians, and we render that. Otherwise # (pixelized PSFs), we FFT the PSF, multiply by the analytic # FFT of the galaxy, and IFFT back to get the rendered # profile. # The "HybridPSF" class is just a marker to indicate whether this # code should treat the PSF as a hybrid. from tractor.psf import HybridPSF hybrid = isinstance(psf, HybridPSF) def run_mog(amix=None, mm=None): ''' This runs the mixture-of-Gaussians convolution method. ''' if amix is None: amix = self._getAffineProfile(img, px, py) if mm is None: mm = modelMask # now convolve with the PSF, analytically # (note that the psf's center is *not* set to px,py; that's just # the position to use for spatially-varying PSFs) psfmix = psf.getMixtureOfGaussians(px=px, py=py) cmix = amix.convolve(psfmix) if mm is None: #print('Mixture to patch: amix', amix, 'psfmix', psfmix, 'cmix', cmix) return mp.mixture_to_patch(cmix, x0, x1, y0, y1, minval) # The convolved mixture *already* has the px,py offset added # (via px,py to amix) so set px,py=0,0 in this call. if mm.mask is not None: p = cmix.evaluate_grid_masked(mm.x0, mm.y0, mm.mask, 0., 0.) else: p = cmix.evaluate_grid(mm.x0, mm.x1, mm.y0, mm.y1, 0., 0.) assert (p.shape == mm.shape) return p if hasattr(psf, 'getMixtureOfGaussians') and not hybrid: return run_mog(mm=modelMask) # Otherwise, FFT: imh, imw = img.shape if modelMask is None: # Avoid huge galaxies -> huge halfsize in a tiny image (blob) imsz = max(imh, imw) halfsize = min(halfsize, imsz) # FIXME -- should take some kind of combination of # modelMask, PSF, and Galaxy sizes! else: # ModelMask sets the sizes. mh, mw = modelMask.shape x1 = x0 + mw y1 = y0 + mh halfsize = max(mh / 2., mw / 2.) # How far from the source center to furthest modelMask edge? # FIXME -- add 1 for Lanczos margin? halfsize = max( halfsize, max(max(1 + px - x0, 1 + x1 - px), max(1 + py - y0, 1 + y1 - py))) psfh, psfw = psf.shape halfsize = max(halfsize, max(psfw / 2., psfh / 2.)) #print('Halfsize:', halfsize) # is the source center outside the modelMask? sourceOut = (px < x0 or px > x1 - 1 or py < y0 or py > y1 - 1) # print('mh,mw', mh,mw, 'sourceout?', sourceOut) if sourceOut: if hybrid: return run_mog(mm=modelMask) # Super Yuck -- FFT, modelMask, source is outside the # box. neardx, neardy = 0., 0. if px < x0: neardx = x0 - px if px > x1: neardx = px - x1 if py < y0: neardy = y0 - py if py > y1: neardy = py - y1 nearest = np.hypot(neardx, neardy) #print('Nearest corner:', nearest, 'vs radius', self.getRadius()) if nearest > self.getRadius(): return None # how far is the furthest point from the source center? farw = max(abs(x0 - px), abs(x1 - px)) farh = max(abs(y0 - py), abs(y1 - py)) bigx0 = int(np.floor(px - farw)) bigx1 = int(np.ceil(px + farw)) bigy0 = int(np.floor(py - farh)) bigy1 = int(np.ceil(py + farh)) bigw = 1 + bigx1 - bigx0 bigh = 1 + bigy1 - bigy0 boffx = x0 - bigx0 boffy = y0 - bigy0 assert (bigw >= mw) assert (bigh >= mh) assert (boffx >= 0) assert (boffy >= 0) bigMask = np.zeros((bigh, bigw), bool) if modelMask.mask is not None: bigMask[boffy:boffy + mh, boffx:boffx + mw] = modelMask.mask else: bigMask[boffy:boffy + mh, boffx:boffx + mw] = True bigMask = ModelMask(bigx0, bigy0, bigMask) # print('Recursing:', self, ':', (mh,mw), 'to', (bigh,bigw)) bigmodel = self._realGetUnitFluxModelPatch(img, px, py, minval, modelMask=bigMask) return Patch( x0, y0, bigmodel.patch[boffy:boffy + mh, boffx:boffx + mw]) # print('Getting Fourier transform of PSF at', px,py) # print('Tim shape:', img.shape) P, (cx, cy), (pH, pW), (v, w) = psf.getFourierTransform(px, py, halfsize) dx = px - cx dy = py - cy if modelMask is not None: # the Patch we return *must* have this origin. ix0 = x0 iy0 = y0 # the difference that we have to handle by shifting the model image mux = dx - ix0 muy = dy - iy0 # we will handle the integer portion by computing a shifted image # and copying it into the result sx = int(np.round(mux)) sy = int(np.round(muy)) # the subpixel portion will be handled with a Lanczos interpolation mux -= sx muy -= sy else: # Put the integer portion of the offset into Patch x0,y0 ix0 = int(np.round(dx)) iy0 = int(np.round(dy)) # the subpixel portion will be handled with a Lanczos interpolation mux = dx - ix0 muy = dy - iy0 # At this point, mux,muy are both in [-0.5, 0.5] assert (np.abs(mux) <= 0.5) assert (np.abs(muy) <= 0.5) amix = self._getShearedProfile(img, px, py) fftmix = amix mogmix = None #print('Galaxy affine profile:', amix) if hybrid: # Split "amix" into terms that we will evaluate using MoG # vs FFT. vv = amix.var[:, 0, 0] + amix.var[:, 1, 1] nsigma = 3. # Terms that will wrap-around significantly... I = ((vv * nsigma**2) > (pW**2)) if np.any(I): # print('Evaluating', np.sum(I), 'terms as MoGs') mogmix = mp.MixtureOfGaussians( amix.amp[I], amix.mean[I, :] + np.array([px, py])[np.newaxis, :], amix.var[I, :, :], quick=True) I = np.logical_not(I) if np.any(I): # print('Evaluating', np.sum(I), 'terms with FFT') fftmix = mp.MixtureOfGaussians(amix.amp[I], amix.mean[I, :], amix.var[I, :, :], quick=True) else: fftmix = None if fftmix is not None: Fsum = fftmix.getFourierTransform(v, w, zero_mean=True) # In Intel's mkl_fft library, the irfftn code path is faster than irfft2 # (the irfft2 version sets args (to their default values) which triggers padding # behavior, changing the FFT size and copying behavior) #G = np.fft.irfft2(Fsum * P, s=(pH, pW)) G = np.fft.irfftn(Fsum * P) assert (G.shape == (pH, pW)) # FIXME -- we could try to be sneaky and Lanczos-interp # after cutting G down to nearly its final size... tricky # tho # Lanczos-3 interpolation in ~the same way we do for # pixelized PSFs. from tractor.psf import lanczos_shift_image G = G.astype(np.float32) lanczos_shift_image(G, mux, muy, inplace=True) else: G = np.zeros((pH, pW), np.float32) if modelMask is not None: gh, gw = G.shape if sx != 0 or sy != 0: yi, yo = get_overlapping_region(-sy, -sy + mh - 1, 0, gh - 1) xi, xo = get_overlapping_region(-sx, -sx + mw - 1, 0, gw - 1) # shifted # FIXME -- are yo,xo always the whole image? If so, optimize shG = np.zeros((mh, mw), G.dtype) shG[yo, xo] = G[yi, xi] if debug_ps is not None: _fourier_galaxy_debug_plots(G, shG, xi, yi, xo, yo, P, Fsum, pW, pH, psf) G = shG if gh > mh or gw > mw: G = G[:mh, :mw] assert (G.shape == modelMask.shape) else: # Clip down to suggested "halfsize" if x0 > ix0: G = G[:, x0 - ix0:] ix0 = x0 if y0 > iy0: G = G[y0 - iy0:, :] iy0 = y0 gh, gw = G.shape if gw + ix0 > x1: G = G[:, :x1 - ix0] if gh + iy0 > y1: G = G[:y1 - iy0, :] if mogmix is not None: if modelMask is not None: mogpatch = run_mog(amix=mogmix, mm=modelMask) else: gh, gw = G.shape mogpatch = run_mog(amix=mogmix, mm=ModelMask(ix0, iy0, gw, gh)) assert (mogpatch.patch.shape == G.shape) G += mogpatch.patch return Patch(ix0, iy0, G)
def evaluate_grid_approx3(self, x0, x1, y0, y1, fx, fy, minval, derivs=False, minradius=3, doslice=True, maxmargin=100): ''' minval: small value at which to stop evaluating [x0,x1): (int) X values to evaluate [y0,y1): (int) Y values to evaluate (fx,fy): (float) pixel offset of the MoG; ie, evaluate MoG shifted by this amount. 'maxmargin': don't render sources more than this distance outside the box. If 'doslice' is True, slices the images down to the non-zero bounding-box. If 'derivs' is True, computes and returns x and y derivatives too. Unlike evaluate_grid_approx, returns a Patch object. ''' from tractor.mix import c_gauss_2d_approx3 result = np.zeros((y1 - y0, x1 - x0)) xderiv = yderiv = None if derivs: xderiv = np.zeros_like(result) yderiv = np.zeros_like(result) # guess: cx = int(self.mean[0, 0] + fx) cy = int(self.mean[0, 1] + fy) if (cx < x0 - maxmargin or cx > x1 + maxmargin or cy < y0 - maxmargin or cy > y1 + maxmargin): return None try: rtn, sx0, sx1, sy0, sy1 = c_gauss_2d_approx3( int(x0), int(x1), int(y0), int(y1), float(fx), float(fy), float(minval), self.amp, self.mean, self.var, result, xderiv, yderiv, cx, cy, int(minradius)) except: print('failure calling c_gauss_2d_approx3:') print(x0, x1, y0, y1) print(fx, fy, minval) print(cx, cy, minradius) print('-->', int(x0), int(x1), int(y0), int(y1)) print('-->', float(fx), float(fy), float(minval)) print('-->', cx, cy, int(minradius)) raise assert(rtn == 0) if doslice: slc = slice(sy0, sy1), slice(sx0, sx1) result = result[slc].copy() if derivs: xderiv = xderiv[slc].copy() yderiv = yderiv[slc].copy() x0 += sx0 y0 += sy0 if derivs: return (Patch(x0, y0, result), Patch(x0, y0, xderiv), Patch(x0, y0, yderiv)) return Patch(x0, y0, result)
def getParamDerivatives(self, tractor, img, srcs): import numpy as np p = Patch(0, 0, np.ones_like(img.getImage())) p.setName('dsky') return [p]