1#!/usr/bin/env python
2from __future__ import absolute_import, division, print_function
3"""Code to handle imager windowing (subimage) and binning.
4
5Note: uses the expansionist or inclusive philosophy.
6The new window is grown to the maximum possible extent
7whenever binning or unbinning. For example, assuming 1-based and inclusive:
8- An unbinned window of [1/2/3, 1/2/3, 4/5/6, 4/5/6] will bin 3x3 to [1, 1, 2, 2]
9- A 3x3 binned window of [1, 1, 2, 2] will unbin to [1, 1, 6, 6]
10
112008-11-06 ROwen
122008-12-15 ROwen    Bug fix: binWindow was mis-computing limits.
132009-02-25 ROwen    Added getFullBinWindow method.
14"""
15import math
16
17__all__ = ["ImageWindow"]
18
19class ImageWindow(object):
20    """Class to handle imager windowing and binning.
21
22    Users typically prefer to specify windows (subregions) in binned coordinates,
23    but then what happens if the user changes the bin factor? This class offers
24    functions that help handle such changes.
25
26    Inputs:
27    - imSize        image size (x, y unbinned pixels)
28    - isOneBased    if True, the address of the lower left pixel is (1,1), else it is (0, 0)
29    - isInclusive   if True, the upper right portion of the image window is included in the data,
30                    else it is omitted.
31    """
32    def __init__(self,
33        imSize,
34        isOneBased = True,
35        isInclusive = True,
36    ):
37        if len(imSize) != 2:
38            raise ValueError("imSize must be two integers; imSize = %r" % (imSize,))
39        self.imSize = tuple([int(mc) for mc in imSize])
40        self.isInclusive = bool(isInclusive)
41
42        if self.isInclusive:
43            urPosAdj = 0
44            ubUROff = 0
45            binUROff = -1
46        else:
47            urPosAdj = 1
48            ubUROff = 1
49            binUROff = 0
50
51        def posToWin(xyPos, urOff):
52            return tuple(xyPos) + tuple([val + urOff for val in xyPos])
53
54        if isOneBased:
55            imLL = (1, 1)
56        else:
57            imLL = (0, 0)
58        imUR = [(imLL[ind] + imSize[ind] - 1) for ind in range(2)]
59        self.minWin = posToWin(imLL, urPosAdj)
60        self.maxUBWin = posToWin(imUR, urPosAdj)
61
62        self.binWinOffset = posToWin(imLL, ubUROff)
63        self.ubWinOffset = posToWin(imLL, binUROff)
64
65    def binWindow(self, ubWin, binFac):
66        """Converts unbinned window to binned.
67
68        The output is constrained to be in range for the given bin factor,
69        though this is only an issue if ubWin is out of range.
70
71        Inputs:
72        - ubWin: unbinned window coords (LL x, LL y, UR x, UR y)
73
74        Returns binned window coords (LL x, LL y, UR x, UR y)
75
76        If any element of ubWin or binFac is None, all returned elements are None.
77        """
78        if None in ubWin or None in binFac:
79            return (None,)*4
80        ubWin = self._getWin(ubWin, "ubWin")
81        binXYXY = self._getBinXYXY(binFac)
82
83        # bin window, ignoring limits
84        binWin = [int(math.floor(self.binWinOffset[ind] + ((ubWin[ind] - self.binWinOffset[ind]) / float(binXYXY[ind]))))
85            for ind in range(4)]
86
87        # apply limits
88        binWin = [min(max(binWin[ind], self.minWin[ind]), self.maxUBWin[ind] // binXYXY[ind]) for ind in range(4)]
89
90#       print "binWindow(ubWin=%r, binFac=%r) = %r" % (ubWin, binFac, binWin)
91        return binWin
92
93    def unbinWindow(self, binWin, binFac):
94        """Converts binned window to unbinned.
95
96        The output is constrained to be in range for the given bin factor.
97
98        Inputs:
99        - binWin: binned window coords (LL x, LL y, UR x, UR y)
100
101        Returns unbinned window coords: (LL x, LL y, UR x, UR y)
102
103        If any element of ubWin or binFac is None, all returned elements are None.
104        """
105        if None in binWin or None in binFac:
106            return (None,)*4
107        binWin = self._getWin(binWin, "binWin")
108        binXYXY = self._getBinXYXY(binFac)
109
110        binWin = [int(val) for val in binWin]
111
112        # unbin window, ignoring limits
113        ubWin = [((binWin[ind] - self.ubWinOffset[ind]) * binXYXY[ind]) + self.ubWinOffset[ind]
114            for ind in range(len(binWin))]
115
116        # apply limits
117        ubWin = [min(max(ubWin[ind], self.minWin[ind]), self.maxUBWin[ind])
118            for ind in range(4)]
119#       print "unbinWindow(binWin=%r, binFac=%r) = %r" % (binWin, binFac, ubWin)
120        return ubWin
121
122    def getMinWindow(self):
123        """Return the minimum window coords (which is independent of bin factor).
124
125        Returns [LL x, LL y, UR x, UR y]
126        """
127        return list(self.minWin)
128
129    def getMaxBinWindow(self, binFac=(1,1)):
130        """Return the maximum binned window coords, given a bin factor.
131        Note: the minimum window coords are the same binned or unbinned: self.minWin
132
133        Returns [LL x, LL y, UR x, UR y]
134        """
135        binXYXY = self._getBinXYXY(binFac)
136        return [int(math.floor(self.binWinOffset[ind] + ((self.maxUBWin[ind] - self.binWinOffset[ind]) / float(binXYXY[ind]))))
137            for ind in range(4)]
138
139    def getFullBinWindow(self, binFac=(1,1)):
140        """Return the full binned window coords
141
142        Returns [LL x, LL y, UR x, UR y]
143        """
144        return self.getMinWindow()[0:2] + self.getMaxBinWindow(binFac)[2:]
145
146    def _getBinXYXY(self, binFac):
147        """Check bin factor and return as 4 ints: x, y, x, y"""
148        if len(binFac) != 2:
149            raise ValueError("binFac=%r; must have 2 elements" % (binFac,))
150        try:
151            binXY = [int(bf) for bf in binFac]
152        except Exception:
153            raise ValueError("binFac=%r; must have 2 integers" % (binFac,))
154        if min(binXY) < 1:
155            raise ValueError("binFac=%r; must be >= 1" % (binFac,))
156        return binXY * 2
157
158    def _getWin(self, win, winName="window"):
159        """Check window argument and return as 4 ints"""
160        if len(win) != 4:
161            raise ValueError("%s=%r; must have 4 elements" % (winName, win))
162        try:
163            return [int(w) for w in win]
164        except Exception:
165            raise ValueError("%s=%r; must have 4 integers" % (winName, win))
166
167
168if __name__ == "__main__":
169    ci = ImageWindow(imSize=(2045, 1024))
170    for bfx in range(1, 4):
171        binFac = (bfx, bfx)
172        for begInd in range(1, bfx + 1):
173            for endInd in range(begInd, bfx + 1):
174                unbWin = [begInd, begInd, endInd, endInd]
175                binWin = tuple(ci.binWindow(unbWin, binFac))
176                if binWin != (1, 1, 1, 1):
177                    raise RuntimeError("Test failed; unbWin=%s != (1, 1, 1, 1), binWin=%s; binFac=%s" % \
178                        (unbWin, binWin, binFac))
179                newUnbWin = tuple(ci.unbinWindow(binWin, binFac))
180                if newUnbWin != (1, 1, bfx, bfx):
181                    raise RuntimeError("Test failed; newUnbWin=%s != (1, 1, %s, %s), binWin=%s; binFac=%s" % \
182                        (newUnbWin, bfx, bfx, binWin, binFac))
183    print ("OK")
184