Source code for galsim.bounds

# Copyright (c) 2012-2023 by the GalSim developers team on GitHub
# https://github.com/GalSim-developers
#
# This file is part of GalSim: The modular galaxy image simulation toolkit.
# https://github.com/GalSim-developers/GalSim
#
# GalSim is free software: redistribution and use in source and binary forms,
# with or without modification, are permitted provided that the following
# conditions are met:
#
# 1. Redistributions of source code must retain the above copyright notice, this
#    list of conditions, and the disclaimer given in the accompanying LICENSE
#    file.
# 2. Redistributions in binary form must reproduce the above copyright notice,
#    this list of conditions, and the disclaimer given in the documentation
#    and/or other materials provided with the distribution.
#

__all__ = [ 'Bounds', 'BoundsI', 'BoundsD', '_BoundsI', '_BoundsD', ]

import math

from . import _galsim
from .position import *
from .errors import GalSimUndefinedBoundsError

[docs]class Bounds: """A class for representing image bounds as 2D rectangles. Bounds is a base class for two slightly different kinds of bounds: `BoundsD` describes bounds with floating point values in x and y. `BoundsI` described bounds with integer values in x and y. The bounds are stored as four numbers in each instance, (xmin, xmax, ymin, ymax), with an additional boolean switch to say whether or not the Bounds rectangle has been defined. The rectangle is undefined if the min value > the max value in either direction. *Initialization*: A `BoundsI` or `BoundsD` instance can be initialized in a variety of ways. The most direct is via four scalars:: >>> bounds = galsim.BoundsD(xmin, xmax, ymin, ymax) >>> bounds = galsim.BoundsI(imin, imax, jmin, jmax) In the `BoundsI` example above, ``imin``, ``imax``, ``jmin`` and ``jmax`` must all be integers to avoid a TypeError exception. Another way to initialize a `Bounds` instance is using two `Position` instances, the first for ``(xmin,ymin)`` and the second for ``(xmax,ymax)``:: >>> bounds = galsim.BoundsD(galsim.PositionD(xmin, ymin), galsim.PositionD(xmax, ymax)) >>> bounds = galsim.BoundsI(galsim.PositionI(imin, jmin), galsim.PositionI(imax, jmax)) In both the examples above, the I/D type of `PositionI`/`PositionD` must match that of `BoundsI`/`BoundsD`. Finally, there are a two ways to lazily initialize a bounds instance with ``xmin = xmax``, ``ymin = ymax``, which will have an undefined rectangle and the instance method isDefined() will return False. The first sets ``xmin = xmax = ymin = ymax = 0``:: >>> bounds = galsim.BoundsD() >>> bounds = galsim.BoundsI() The second method sets both upper and lower rectangle bounds to be equal to some position:: >>> bounds = galsim.BoundsD(galsim.PositionD(xmin, ymin)) >>> bounds = galsim.BoundsI(galsim.PositionI(imin, jmin)) Once again, the I/D type of `PositionI`/`PositionD` must match that of `BoundsI`/`BoundsD`. For the latter two initializations, you would typically then add to the bounds with:: >>> bounds += pos1 >>> bounds += pos2 >>> [etc.] Then the bounds will end up as the bounding box of all the positions that were added to it. You can also find the intersection of two bounds with the & operator:: >>> overlap = bounds1 & bounds2 This is useful for adding one image to another when part of the first image might fall off the edge of the other image:: >>> overlap = stamp.bounds & image.bounds >>> image[overlap] += stamp[overlap] """ def __init__(self): raise NotImplementedError("Cannot instantiate the base class. " "Use either BoundsD or BoundsI.") def _parse_args(self, *args, **kwargs): if len(kwargs) == 0: if len(args) == 4: self._isdefined = True self.xmin, self.xmax, self.ymin, self.ymax = args elif len(args) == 0: self._isdefined = False self.xmin = self.xmax = self.ymin = self.ymax = 0 elif len(args) == 1: if isinstance(args[0], (Bounds, _galsim.BoundsD, _galsim.BoundsI)): self._isdefined = True self.xmin = args[0].xmin self.xmax = args[0].xmax self.ymin = args[0].ymin self.ymax = args[0].ymax elif isinstance(args[0], (Position, _galsim.PositionD, _galsim.PositionI)): self._isdefined = True self.xmin = self.xmax = args[0].x self.ymin = self.ymax = args[0].y else: raise TypeError("Single argument to %s must be either a Bounds or a Position"%( self.__class__.__name__)) self._isdefined = True elif len(args) == 2: if (isinstance(args[0], (Position, _galsim.PositionD, _galsim.PositionI)) and isinstance(args[1], (Position, _galsim.PositionD, _galsim.PositionI))): self._isdefined = True self.xmin = min(args[0].x, args[1].x) self.xmax = max(args[0].x, args[1].x) self.ymin = min(args[0].y, args[1].y) self.ymax = max(args[0].y, args[1].y) else: raise TypeError("Two arguments to %s must be Positions"%( self.__class__.__name__)) else: raise TypeError("%s takes either 1, 2, or 4 arguments (%d given)"%( self.__class__.__name__,len(args))) elif len(args) != 0: raise TypeError("Cannot provide both keyword and non-keyword arguments to %s"%( self.__class__.__name__)) else: try: self._isdefined = True self.xmin = kwargs.pop('xmin') self.xmax = kwargs.pop('xmax') self.ymin = kwargs.pop('ymin') self.ymax = kwargs.pop('ymax') except KeyError: raise TypeError("Keyword arguments, xmin, xmax, ymin, ymax are required for %s"%( self.__class__.__name__)) from None if kwargs: raise TypeError("Got unexpected keyword arguments %s"%kwargs.keys()) if not (float(self.xmin) <= float(self.xmax) and float(self.ymin) <= float(self.ymax)): self._isdefined = False
[docs] def area(self): """Return the area of the enclosed region. The area is a bit different for integer-type `BoundsI` and float-type `BoundsD` instances. For floating point types, it is simply ``(xmax-xmin)*(ymax-ymin)``. However, for integer types, we add 1 to each size to correctly count the number of pixels being described by the bounding box. """ return self._area()
[docs] def withBorder(self, dx, dy=None): """Return a new `Bounds` object that expands the current bounds by the specified width. If two arguments are given, then these are separate dx and dy borders. """ self._check_scalar(dx, "dx") if dy is None: dy = dx else: self._check_scalar(dy, "dy") return self.__class__(self.xmin-dx, self.xmax+dx, self.ymin-dy, self.ymax+dy)
@property def origin(self): "The lower left position of the `Bounds`." return self._pos_class(self.xmin, self.ymin) @property def center(self): """The central position of the `Bounds`. For a `BoundsI`, this will return an integer `PositionI`, which will be above and/or to the right of the true center if the x or y ranges have an even number of pixels. For a `BoundsD`, this is equivalent to true_center. """ if not self.isDefined(): raise GalSimUndefinedBoundsError("center is invalid for an undefined Bounds") return self._center @property def true_center(self): """The central position of the `Bounds` as a `PositionD`. This is always (xmax + xmin)/2., (ymax + ymin)/2., even for integer `BoundsI`, where this may not necessarily be an integer `PositionI`. """ if not self.isDefined(): raise GalSimUndefinedBoundsError("true_center is invalid for an undefined Bounds") return _PositionD((self.xmax + self.xmin)/2., (self.ymax + self.ymin)/2.)
[docs] def includes(self, *args): """Test whether a supplied ``(x,y)`` pair, `Position`, or `Bounds` lie within a defined `Bounds` rectangle of this instance. Examples:: >>> bounds = galsim.BoundsD(0., 100., 0., 100.) >>> bounds.includes(50., 50.) True >>> bounds.includes(galsim.PositionD(50., 50.)) True >>> bounds.includes(galsim.BoundsD(-50., -50., 150., 150.)) False The type of the `PositionI`/`PositionD` and `BoundsI`/`BoundsD` instances (i.e. integer or float type) should match that of the bounds instance. """ if len(args) == 1: if isinstance(args[0], Bounds): b = args[0] return (self.isDefined() and b.isDefined() and self.xmin <= b.xmin and self.xmax >= b.xmax and self.ymin <= b.ymin and self.ymax >= b.ymax) elif isinstance(args[0], Position): p = args[0] return (self.isDefined() and self.xmin <= p.x <= self.xmax and self.ymin <= p.y <= self.ymax) else: raise TypeError("Invalid argument %s"%args[0]) elif len(args) == 2: x, y = args return (self.isDefined() and self.xmin <= float(x) <= self.xmax and self.ymin <= float(y) <= self.ymax) elif len(args) == 0: raise TypeError("include takes at least 1 argument (0 given)") else: raise TypeError("include takes at most 2 arguments (%d given)"%len(args))
[docs] def expand(self, factor_x, factor_y=None): """Grow the `Bounds` by the supplied factor about the center. If two arguments are given, then these are separate x and y factors to expand by. """ if factor_y is None: factor_y = factor_x dx = (self.xmax - self.xmin) * 0.5 * (factor_x-1.) dy = (self.ymax - self.ymin) * 0.5 * (factor_y-1.) if isinstance(self, BoundsI): dx = int(math.ceil(dx)) dy = int(math.ceil(dy)) return self.withBorder(dx,dy)
[docs] def isDefined(self): "Test whether `Bounds` rectangle is defined." return self._isdefined
[docs] def getXMin(self): "Get the value of xmin." return self.xmin
[docs] def getXMax(self): "Get the value of xmax." return self.xmax
[docs] def getYMin(self): "Get the value of ymin." return self.ymin
[docs] def getYMax(self): "Get the value of ymax." return self.ymax
[docs] def shift(self, delta): """Shift the `Bounds` instance by a supplied `Position`. Examples: The shift method takes either a `PositionI` or `PositionD` instance, which must match the type of the `Bounds` instance:: >>> bounds = BoundsI(1,32,1,32) >>> bounds = bounds.shift(galsim.PositionI(3, 2)) >>> bounds = BoundsD(0, 37.4, 0, 49.9) >>> bounds = bounds.shift(galsim.PositionD(3.9, 2.1)) """ if not isinstance(delta, self._pos_class): raise TypeError("delta must be a %s instance"%self._pos_class) return self.__class__(self.xmin + delta.x, self.xmax + delta.x, self.ymin + delta.y, self.ymax + delta.y)
def __and__(self, other): if not isinstance(other, self.__class__): raise TypeError("other must be a %s instance"%self.__class__.__name__) if not self.isDefined() or not other.isDefined(): return self.__class__() else: xmin = max(self.xmin, other.xmin) xmax = min(self.xmax, other.xmax) ymin = max(self.ymin, other.ymin) ymax = min(self.ymax, other.ymax) if xmin > xmax or ymin > ymax: return self.__class__() else: return self.__class__(xmin, xmax, ymin, ymax) def __add__(self, other): if isinstance(other, self.__class__): if not other.isDefined(): return self elif self.isDefined(): xmin = min(self.xmin, other.xmin) xmax = max(self.xmax, other.xmax) ymin = min(self.ymin, other.ymin) ymax = max(self.ymax, other.ymax) return self.__class__(xmin, xmax, ymin, ymax) else: return other elif isinstance(other, self._pos_class): if self.isDefined(): xmin = min(self.xmin, other.x) xmax = max(self.xmax, other.x) ymin = min(self.ymin, other.y) ymax = max(self.ymax, other.y) return self.__class__(xmin, xmax, ymin, ymax) else: return self.__class__(other) else: raise TypeError("other must be either a %s or a %s"%( self.__class__.__name__, self._pos_class.__name__)) def __repr__(self): if self.isDefined(): return "galsim.%s(xmin=%r, xmax=%r, ymin=%r, ymax=%r)"%( self.__class__.__name__, self.xmin, self.xmax, self.ymin, self.ymax) else: return "galsim.%s()"%(self.__class__.__name__) def __str__(self): if self.isDefined(): return "galsim.%s(%s,%s,%s,%s)"%( self.__class__.__name__, self.xmin, self.xmax, self.ymin, self.ymax) else: return "galsim.%s()"%(self.__class__.__name__) def _getinitargs(self): if self.isDefined(): return (self.xmin, self.xmax, self.ymin, self.ymax) else: return () def __eq__(self, other): return (self is other or (isinstance(other, self.__class__) and self._getinitargs() == other._getinitargs())) def __ne__(self, other): return not self.__eq__(other) def __hash__(self): return hash((self.__class__.__name__, self._getinitargs()))
[docs]class BoundsD(Bounds): """A `Bounds` that takes floating point values. See the `Bounds` doc string for more details. """ _pos_class = PositionD def __init__(self, *args, **kwargs): self._parse_args(*args, **kwargs) self.xmin = float(self.xmin) self.xmax = float(self.xmax) self.ymin = float(self.ymin) self.ymax = float(self.ymax) @property def _b(self): return _galsim.BoundsD(float(self.xmin), float(self.xmax), float(self.ymin), float(self.ymax)) def _check_scalar(self, x, name): try: if x == float(x): return except (TypeError, ValueError): pass raise TypeError("%s must be a float value"%name) def _area(self): return (self.xmax - self.xmin) * (self.ymax - self.ymin) @property def _center(self): return _PositionD( (self.xmax + self.xmin)/2., (self.ymax + self.ymin)/2. )
[docs]class BoundsI(Bounds): """A `Bounds` that takes only integer values. Typically used to define the bounding box of an image. See the `Bounds` doc string for more details. """ _pos_class = PositionI def __init__(self, *args, **kwargs): self._parse_args(*args, **kwargs) if (self.xmin != int(self.xmin) or self.xmax != int(self.xmax) or self.ymin != int(self.ymin) or self.ymax != int(self.ymax)): raise TypeError("BoundsI must be initialized with integer values") # Now make sure they are all ints self.xmin = int(self.xmin) self.xmax = int(self.xmax) self.ymin = int(self.ymin) self.ymax = int(self.ymax) @property def _b(self): return _galsim.BoundsI(self.xmin, self.xmax, self.ymin, self.ymax) def _check_scalar(self, x, name): try: if x == int(x): return except (TypeError, ValueError): pass raise TypeError("%s must be an integer value"%name)
[docs] def numpyShape(self): "A simple utility function to get the numpy shape that corresponds to this `Bounds` object." if self.isDefined(): return self.ymax-self.ymin+1, self.xmax-self.xmin+1 else: return 0,0
def _area(self): # Remember the + 1 this time to include the pixels on both edges of the bounds. if not self.isDefined(): return 0 else: return (self.xmax - self.xmin + 1) * (self.ymax - self.ymin + 1) @property def _center(self): # Write it this way to make sure the integer rounding goes the same way regardless # of whether the values are positive or negative. # e.g. (1,10,1,10) -> (6,6) # (-10,-1,-10,-1) -> (-5,-5) # Just up and to the right of the true center in both cases. return _PositionI(self.xmin + (self.xmax - self.xmin + 1)//2, self.ymin + (self.ymax - self.ymin + 1)//2)
[docs]def _BoundsD(xmin, xmax, ymin, ymax): """Equivalent to `BoundsD` constructor, but skips some sanity checks and argument parsing. This requires that the four values be float types. """ ret = BoundsD.__new__(BoundsD) ret._isdefined = True ret.xmin = float(xmin) ret.xmax = float(xmax) ret.ymin = float(ymin) ret.ymax = float(ymax) return ret
[docs]def _BoundsI(xmin, xmax, ymin, ymax): """Equivalent to `BoundsI` constructor, but skips some sanity checks and argument parsing. This requires that the four values be int types. """ ret = BoundsI.__new__(BoundsI) ret._isdefined = True ret.xmin = int(xmin) ret.xmax = int(xmax) ret.ymin = int(ymin) ret.ymax = int(ymax) return ret