Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

DM-14690: Add ability to construct centered boxes #4

Merged
merged 3 commits into from
Jun 26, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ _build.*
*.cfgc
*.pyc
.cache
.coverage
.pytest_cache
pytest_session.txt
config.log
Expand Down
40 changes: 34 additions & 6 deletions include/lsst/geom/Box.h
Original file line number Diff line number Diff line change
Expand Up @@ -73,14 +73,16 @@ class Box2I {
Box2I(Point2I const& minimum, Point2I const& maximum, bool invert = true);

/**
* Construct a box from its minimum point and dimensions.
* Construct a box from one corner and dimensions.
*
* @param[in] minimum Minimum (lower left) coordinate.
* @param[in] corner Reference coordinate. This is the lower left corner if both
* dimensions are positive, but a right corner or upper corner if
* the corresponding dimension is negative and `invert` is set.
* @param[in] dimensions Box dimensions. If either dimension coordinate is 0, the box will be empty.
* @param[in] invert If true (default), invert any negative dimensions instead of creating
* an empty box.
*/
Box2I(Point2I const& minimum, Extent2I const& dimensions, bool invert = true);
Box2I(Point2I const& corner, Extent2I const& dimensions, bool invert = true);

/**
* Construct an integer box from a floating-point box.
Expand All @@ -104,6 +106,18 @@ class Box2I {
Box2I(Box2I&&) = default;
~Box2I() = default;

/**
* Create a box centered as closely as possible on a particular point.
*
* @param center The desired center of the box.
* @param size The desired width and height (in that order) of the box.
*
* @returns if `size` is positive, a box with size `size`; otherwise,
* an empty box. If the returned box is not empty, its center
* shall be within half a pixel of `center` in either dimension.
*/
static Box2I makeCenteredBox(Point2D const& center, Extent const& size);

void swap(Box2I& other) {
_minimum.swap(other._minimum);
_dimensions.swap(other._dimensions);
Expand Down Expand Up @@ -293,14 +307,16 @@ class Box2D {
Box2D(Point2D const& minimum, Point2D const& maximum, bool invert = true);

/**
* Construct a box from its minimum point and dimensions.
* Construct a box from one corner and dimensions.
*
* @param[in] minimum Minimum (lower left) coordinate (inclusive).
* @param[in] corner Reference coordinate (inclusive). This is the lower left corner if
* both dimensions are positive, but a right corner or upper corner if
* the corresponding dimension is negative and `invert` is set.
* @param[in] dimensions Box dimensions. If either dimension coordinate is 0, the box will be empty.
* @param[in] invert If true (default), invert any negative dimensions instead of creating
* an empty box.
*/
Box2D(Point2D const& minimum, Extent2D const& dimensions, bool invert = true);
Box2D(Point2D const& corner, Extent2D const& dimensions, bool invert = true);

/**
* Construct a floating-point box from an integer box.
Expand All @@ -319,6 +335,18 @@ class Box2D {

~Box2D() = default;

/**
* Create a box centered on a particular point.
*
* @param center The desired center of the box.
* @param size The desired width and height (in that order) of the box.
*
* @returns if `size` is positive, a box with size `size`; otherwise,
* an empty box. If the returned box is not empty, it shall be
* centered on `center`.
*/
static Box2D makeCenteredBox(Point2D const& center, Extent const& size);

void swap(Box2D& other) {
_minimum.swap(other._minimum);
_maximum.swap(other._maximum);
Expand Down
6 changes: 4 additions & 2 deletions python/lsst/geom/box.cc
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ PYBIND11_PLUGIN(box) {
clsBox2I.def(py::init<>());
clsBox2I.def(py::init<Point2I const &, Point2I const &, bool>(), "minimum"_a, "maximum"_a,
"invert"_a = true);
clsBox2I.def(py::init<Point2I const &, Extent2I const &, bool>(), "minimum"_a, "dimensions"_a,
clsBox2I.def(py::init<Point2I const &, Extent2I const &, bool>(), "corner"_a, "dimensions"_a,
"invert"_a = true);
clsBox2I.def(py::init<Box2D const &, Box2I::EdgeHandlingEnum>(), "other"_a,
"edgeHandling"_a = Box2I::EXPAND);
Expand All @@ -65,6 +65,7 @@ PYBIND11_PLUGIN(box) {
clsBox2I.def("__ne__", [](Box2I const &self, Box2I const &other) { return self != other; },
py::is_operator());

clsBox2I.def_static("makeCenteredBox", &Box2I::makeCenteredBox, "center"_a, "size"_a);
clsBox2I.def("swap", &Box2I::swap);
clsBox2I.def("getMin", &Box2I::getMin);
clsBox2I.def("getMinX", &Box2I::getMinX);
Expand Down Expand Up @@ -127,7 +128,7 @@ PYBIND11_PLUGIN(box) {
clsBox2D.def(py::init<>());
clsBox2D.def(py::init<Point2D const &, Point2D const &, bool>(), "minimum"_a, "maximum"_a,
"invert"_a = true);
clsBox2D.def(py::init<Point2D const &, Extent2D const &, bool>(), "minimum"_a, "dimensions"_a,
clsBox2D.def(py::init<Point2D const &, Extent2D const &, bool>(), "corner"_a, "dimensions"_a,
"invert"_a = true);
clsBox2D.def(py::init<Box2I const &>());
clsBox2D.def(py::init<Box2D const &>());
Expand All @@ -137,6 +138,7 @@ PYBIND11_PLUGIN(box) {
clsBox2D.def("__ne__", [](Box2D const &self, Box2D const &other) { return self != other; },
py::is_operator());

clsBox2D.def_static("makeCenteredBox", &Box2D::makeCenteredBox, "center"_a, "size"_a);
clsBox2D.def("swap", &Box2D::swap);
clsBox2D.def("getMin", &Box2D::getMin);
clsBox2D.def("getMinX", &Box2D::getMinX);
Expand Down
22 changes: 18 additions & 4 deletions src/Box.cc
Original file line number Diff line number Diff line change
Expand Up @@ -43,8 +43,8 @@ Box2I::Box2I(Point2I const& minimum, Point2I const& maximum, bool invert)
_dimensions += Extent2I(1);
}

Box2I::Box2I(Point2I const& minimum, Extent2I const& dimensions, bool invert)
: _minimum(minimum), _dimensions(dimensions) {
Box2I::Box2I(Point2I const& corner, Extent2I const& dimensions, bool invert)
: _minimum(corner), _dimensions(dimensions) {
for (int n = 0; n < 2; ++n) {
if (_dimensions[n] == 0) {
*this = Box2I();
Expand Down Expand Up @@ -92,6 +92,14 @@ Box2I::Box2I(Box2D const& other, EdgeHandlingEnum edgeHandling) : _minimum(), _d
}
}

Box2I Box2I::makeCenteredBox(Point2D const& center, Box2I::Extent const& size) {
lsst::geom::Point2D corner(center);
corner.shift(-0.5 * lsst::geom::Extent2D(size));
// compensate for Box2I's coordinate conventions (where max = min + size - 1)
corner.shift(lsst::geom::Extent2D(0.5, 0.5));
return lsst::geom::Box2I(lsst::geom::Point2I(corner), size, false);
}

ndarray::View<boost::fusion::vector2<ndarray::index::Range, ndarray::index::Range> > Box2I::getSlices()
const {
return ndarray::view(getBeginY(), getEndY())(getBeginX(), getEndX());
Expand Down Expand Up @@ -238,8 +246,8 @@ Box2D::Box2D(Point2D const& minimum, Point2D const& maximum, bool invert)
}
}

Box2D::Box2D(Point2D const& minimum, Extent2D const& dimensions, bool invert)
: _minimum(minimum), _maximum(minimum + dimensions) {
Box2D::Box2D(Point2D const& corner, Extent2D const& dimensions, bool invert)
: _minimum(corner), _maximum(corner + dimensions) {
for (int n = 0; n < 2; ++n) {
if (_minimum[n] == _maximum[n]) {
*this = Box2D();
Expand All @@ -261,6 +269,12 @@ Box2D::Box2D(Box2I const& other)
if (other.isEmpty()) *this = Box2D();
}

Box2D Box2D::makeCenteredBox(Point2D const& center, Box2D::Extent const& size) {
lsst::geom::Point2D corner(center);
corner.shift(-0.5 * size);
return lsst::geom::Box2D(corner, size, false);
}

bool Box2D::contains(Point2D const& point) const {
return all(point.ge(this->getMin())) && all(point.lt(this->getMax()));
}
Expand Down
86 changes: 86 additions & 0 deletions tests/test_box.py
Original file line number Diff line number Diff line change
Expand Up @@ -513,6 +513,92 @@ def testFlipD(self):
self.assertAlmostEqual(box1.getMaxY(), tby11, places=6)


class SharedBoxTestCase(lsst.utils.tests.TestCase):
"""Tests of Box2I and Box2D where the code for both classes is the same,
and only the test fixtures need be different.
"""
def setUp(self):
np.random.seed(1)

def testMakeCenteredBox(self):
dimensionsI = [geom.Extent2I(100, 50), geom.Extent2I(15, 15),
geom.Extent2I(0, 10), geom.Extent2I(25, 30),
geom.Extent2I(15, -5)]
dimensionsD = [geom.Extent2D(d) for d in dimensionsI] \
+ [geom.Extent2D(1.5, 2.1), geom.Extent2D(4, 3.7),
geom.Extent2D(-0.1, -0.1), geom.Extent2D(5.5, 5.5)]
locations = [geom.Point2D(0, 0), geom.Point2D(0.2, 0.7),
geom.Point2D(1, 1.5),
geom.Point2D(-0.5 + 1e-4, -0.5 + 1e-4),
geom.Point2D(-0.5 - 1e-4, -0.5 - 1e-4)]

for center in locations:
for size in dimensionsI:
self._checkBoxConstruction(geom.Box2I, size, center, np.sqrt(0.5))
for size in dimensionsD:
self._checkBoxConstruction(geom.Box2D, size, center, 1e-10)

def _checkBoxConstruction(self, boxClass, size, center, precision):
"""Test attempts to create a centered box of a particular type
and parameters.

Parameters
----------
boxClass : `type`
One of `lsst.geom.Box2I` or `lsst.geom.Box2D`.
size : ``boxClass.Extent``
The desired dimensions of the box.
center : `lsst.geom.Point2D`
The desired center of the box.
precision : `float`
The maximum distance by which the box can be offset from ``center``.
"""
msg = 'Box size = %s, location = %s' % (size, center)
box = boxClass.makeCenteredBox(center, size)

if all(size.gt(0)):
self._checkBoxProperties(box, size, center, precision, msg)
else:
self.assertTrue(box.isEmpty(), msg=msg)

def _checkBoxProperties(self, box, size, center, precision, msg):
"""Test whether a box has the desired size and position.

Parameters
----------
box : `lsst.geom.Box2I` or `lsst.geom.Box2D`
The box to test.
size : ``box.Extent``
The expected dimensions of ``box``.
center : `lsst.geom.Point2D`
The expected center of ``box``.
precision : `float`
The maximum distance between the center of ``box`` and ``center``.
msg : `str`
An error message suffix describing test parameters.
"""
newCenter = self._getBoxCenter(box)
self.assertIsNotNone(box, msg=msg)
self.assertPairsAlmostEqual(newCenter, center, maxDiff=precision, msg=msg)
self.assertPairsAlmostEqual(box.getDimensions(), size, msg=msg)

def _getBoxCenter(self, box):
"""Return the coordinates of a Box's center.

Parameters
----------
box : `lsst.geom.Box2I` or `lsst.geom.Box2D`
The box whose center is desired.

Returns
-------
center : `lsst.geom.Point2D`
The position at the center of ``box``. If ``box`` is a ``Box2I``,
this will always have integer or half-integer coordinates.
"""
return geom.Box2D(box).getCenter()


class MemoryTester(lsst.utils.tests.MemoryTestCase):
pass

Expand Down