Coverage for tests/test_box_conversions.py: 96%

62 statements  

« prev     ^ index     » next       coverage.py v7.14.1, created at 2026-06-15 03:23 +0000

1# This file is part of meas_extensions_scarlet. 

2# 

3# Developed for the LSST Data Management System. 

4# This product includes software developed by the LSST Project 

5# (https://www.lsst.org). 

6# See the COPYRIGHT file at the top-level directory of this distribution 

7# for details of code ownership. 

8# 

9# This program is free software: you can redistribute it and/or modify 

10# it under the terms of the GNU General Public License as published by 

11# the Free Software Foundation, either version 3 of the License, or 

12# (at your option) any later version. 

13# 

14# This program is distributed in the hope that it will be useful, 

15# but WITHOUT ANY WARRANTY; without even the implied warranty of 

16# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 

17# GNU General Public License for more details. 

18# 

19# You should have received a copy of the GNU General Public License 

20# along with this program. If not, see <https://www.gnu.org/licenses/>. 

21 

22"""Round-trip tests for ``scarletBoxToBBox`` / ``bboxToScarletBox``.""" 

23 

24import unittest 

25 

26import lsst.geom as geom 

27import lsst.meas.extensions.scarlet as mes 

28import lsst.scarlet.lite as scl 

29import lsst.utils.tests 

30 

31 

32class TestBoxConversions(lsst.utils.tests.TestCase): 

33 """Round-trip tests for the box conversions in 

34 ``lsst.meas.extensions.scarlet.utils``. 

35 

36 Scarlet stores boxes as ``(..., y, x)``-ordered shape and origin 

37 tuples; afw uses ``Box2I`` with separate `(x, y)` accessors. The 

38 conversion functions also accept an ``xy0`` offset so the same call 

39 can hop between a blend-local frame and an exposure-global frame. 

40 """ 

41 

42 def test_scarletBoxToBBox_roundtrip(self): 

43 """``bboxToScarletBox(scarletBoxToBBox(box)) == box`` over a 

44 sweep of origins and shapes. 

45 """ 

46 cases = [ 

47 # Zero origin — the most common parent-blend case. 

48 ((5, 8), (0, 0)), 

49 # Small positive origin with an asymmetric (y, x) shape. 

50 ((25, 30), (17, 5)), 

51 # Degenerate 1×1 box far from the origin. 

52 ((1, 1), (100, 200)), 

53 # Negative origin — exercises sub-image offsets where the 

54 # blend extends below (0, 0). 

55 ((50, 50), (-10, -20)), 

56 # Origin pinned to a single axis (y == 0, x large). 

57 ((3, 17), (0, 99)), 

58 ] 

59 for shape, origin in cases: 

60 with self.subTest(shape=shape, origin=origin): 

61 box = scl.Box(shape, origin) 

62 bbox = mes.utils.scarletBoxToBBox(box) 

63 roundtripped = mes.utils.bboxToScarletBox(bbox) 

64 self.assertTupleEqual(roundtripped.origin, origin) 

65 self.assertTupleEqual(roundtripped.shape, shape) 

66 

67 def test_bboxToScarletBox_roundtrip(self): 

68 """``scarletBoxToBBox(bboxToScarletBox(bbox)) == bbox`` over a 

69 sweep of corners and extents. 

70 """ 

71 cases = [ 

72 # Zero-origin Box2I — the trivial case. 

73 (geom.Point2I(0, 0), geom.Extent2I(5, 8)), 

74 # Positive corner; mirrors the scarlet test above with 

75 # axes swapped to confirm the (x, y) / (y, x) ordering 

76 # convention is consistent in both directions. 

77 (geom.Point2I(17, 5), geom.Extent2I(30, 25)), 

78 # Negative corner — afw allows it; this pins that 

79 # ``bboxToScarletBox`` doesn't clamp it to zero. 

80 (geom.Point2I(-20, -10), geom.Extent2I(50, 50)), 

81 # Single-axis offset, asymmetric extent. 

82 (geom.Point2I(99, 0), geom.Extent2I(17, 3)), 

83 ] 

84 for minPoint, extent in cases: 

85 # ``subTest`` kwargs are serialized through ``execnet`` when 

86 # the suite runs under ``pytest-xdist`` (e.g. on Jenkins), 

87 # and ``execnet`` can't dump pybind11 objects. Label the 

88 # subtest with plain int tuples instead. 

89 with self.subTest( 

90 minPoint=(minPoint.getX(), minPoint.getY()), 

91 extent=(extent.getX(), extent.getY()), 

92 ): 

93 bbox = geom.Box2I(minPoint, extent) 

94 box = mes.utils.bboxToScarletBox(bbox) 

95 roundtripped = mes.utils.scarletBoxToBBox(box) 

96 self.assertEqual(roundtripped, bbox) 

97 

98 def test_box_negative_origin(self): 

99 """Both conversions handle origins below ``(0, 0)``.""" 

100 box = scl.Box((10, 8), (-5, -3)) 

101 bbox = mes.utils.scarletBoxToBBox(box) 

102 self.assertEqual(bbox.getMinY(), -5) 

103 self.assertEqual(bbox.getMinX(), -3) 

104 self.assertEqual(bbox.getHeight(), 10) 

105 self.assertEqual(bbox.getWidth(), 8) 

106 roundtripped = mes.utils.bboxToScarletBox(bbox) 

107 self.assertTupleEqual(roundtripped.origin, box.origin) 

108 self.assertTupleEqual(roundtripped.shape, box.shape) 

109 

110 def test_box_with_xy0_offset(self): 

111 """A non-zero ``xy0`` shifts forward and back symmetrically.""" 

112 box = scl.Box((25, 30), (17, 5)) 

113 xy0 = geom.Point2I(100, 200) 

114 bbox = mes.utils.scarletBoxToBBox(box, xy0) 

115 self.assertEqual(bbox.getMinX(), 5 + 100) 

116 self.assertEqual(bbox.getMinY(), 17 + 200) 

117 self.assertEqual(bbox.getWidth(), 30) 

118 self.assertEqual(bbox.getHeight(), 25) 

119 roundtripped = mes.utils.bboxToScarletBox(bbox, xy0) 

120 self.assertTupleEqual(roundtripped.origin, box.origin) 

121 self.assertTupleEqual(roundtripped.shape, box.shape) 

122 

123 def test_box_transforms(self): 

124 """Backward-continuity lift of ``TestUtils.test_box_transforms`` 

125 from the pre-refactor ``test_deblend.py``. 

126 """ 

127 box = scl.Box((25, 30), (17, 5)) 

128 bbox = mes.utils.scarletBoxToBBox(box) 

129 x0, y0 = bbox.getMin() 

130 width = bbox.getWidth() 

131 height = bbox.getHeight() 

132 self.assertTupleEqual((y0, x0), box.origin) 

133 self.assertTupleEqual((height, width), box.shape) 

134 

135 newBox = mes.utils.bboxToScarletBox(bbox) 

136 self.assertTupleEqual(newBox.origin, box.origin) 

137 self.assertTupleEqual(newBox.shape, box.shape) 

138 

139 

140def setup_module(module): 

141 lsst.utils.tests.init() 

142 

143 

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

145 pass 

146 

147 

148if __name__ == "__main__": 148 ↛ 149line 148 didn't jump to line 149 because the condition on line 148 was never true

149 lsst.utils.tests.init() 

150 unittest.main()