• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

dstndstn / tractor / c78418bc-2e37-424f-896b-f3a08e40f85b

19 Feb 2025 03:51AM UTC coverage: 45.592%. First build
c78418bc-2e37-424f-896b-f3a08e40f85b

push

circleci

craigwarner-ufastro
Brought craig_factored_merge inline with craig_factored debugging
changes and now CPU, factored, and factored vectorized all match to
np.allclose!

Merging in changes from Dustin and Craig - things like
smarter_dense_optimizer and patch.py for the CPU, and differences
between mogpatch - selecting the proper means and getting varcopy
correct; also brought changes to IM, IF masks, mogweights, fftweights,
and amp inline to vectorized version.

702 of 2177 branches covered (32.25%)

11 of 14 new or added lines in 2 files covered. (78.57%)

3367 of 7385 relevant lines covered (45.59%)

0.46 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

56.23
/tractor/patch.py
1
import numpy as np
1✔
2

3
class ModelMask(object):
1✔
4
    def __init__(self, x0, y0, *args):
1✔
5
        '''
6
        ModelMask(x0, y0, w, h)
7
        ModelMask(x0, y0, mask)
8

9
        *mask* is assumed to be a binary image, True for pixels we're
10
         interested in.
11
        '''
12
        self.x0 = x0
1✔
13
        self.y0 = y0
1✔
14
        if len(args) == 2:
1✔
15
            self.w, self.h = args
1✔
16
            self.mask = None
1✔
17
        elif len(args) == 1:
1!
18
            self.mask = args[0]
1✔
19
            self.h, self.w = self.mask.shape
1✔
20
        else:
21
            raise ValueError('Wrong number of arguments')
×
22

23
    def __str__(self):
1✔
24
        s = ('ModelMask: origin (%i,%i), w=%i, h=%i' %
×
25
             (self.x0, self.y0, self.w, self.h))
26
        if self.mask is None:
×
27
            return s
×
28
        return s + ', has mask'
×
29

30
    def __repr__(self):
1✔
31
        if self.mask is None:
×
32
            return ('ModelMask(%i,%i, w=%i, h=%i)' %
×
33
                    (self.x0, self.y0, self.w, self.h))
34
        else:
35
            return ('ModelMask(%i,%i, mask of shape w=%i, h=%i)' %
×
36
                    (self.x0, self.y0, self.w, self.h))
37

38
    @staticmethod
1✔
39
    def fromExtent(x0, x1, y0, y1):
1✔
40
        return ModelMask(x0, y0, x1 - x0, y1 - y0)
1✔
41

42
    @property
1✔
43
    def shape(self):
1✔
44
        return (self.h, self.w)
1✔
45

46
    @property
1✔
47
    def x1(self):
1✔
48
        return self.x0 + self.w
1✔
49

50
    @property
1✔
51
    def y1(self):
1✔
52
        return self.y0 + self.h
1✔
53

54
    @property
1✔
55
    def extent(self):
1✔
56
        return (self.x0, self.x0 + self.w, self.y0, self.y0 + self.h)
×
57

58
# Adds two patches, handling the case when one is None
59

60

61
def add_patches(pa, pb):
1✔
62
    p = pa
1✔
63
    if pb is not None:
1✔
64
        if p is None:
1!
65
            p = pb
×
66
        else:
67
            p += pb
1✔
68
    return p
1✔
69

70

71
class Patch(object):
1✔
72
    '''
73
    An image patch; a subimage.  In the Tractor we use these to hold
74
    synthesized (ie, model) images.  The patch is a rectangular grid
75
    of pixels and it knows its offset (2-d position) in some larger
76
    image.
77

78
    This class overloads arithmetic operations (like add and multiply)
79
    relevant to synthetic image patches.
80
    '''
81

82
    def __init__(self, x0, y0, patch):
1✔
83
        self.x0 = x0
1✔
84
        self.y0 = y0
1✔
85
        self.patch = patch
1✔
86
        self.name = ''
1✔
87
        if patch is not None:
1!
88
            try:
1✔
89
                H, W = patch.shape
1✔
90
                self.size = H * W
1✔
91
            except:
×
92
                pass
×
93

94
    @property
1✔
95
    def y1(self):
1✔
96
        return self.y0 + self.patch.shape[0]
×
97

98
    @property
1✔
99
    def x1(self):
1✔
100
        return self.x0 + self.patch.shape[1]
×
101

102
    def __str__(self):
1✔
103
        s = 'Patch: '
1✔
104
        name = getattr(self, 'name', '')
1✔
105
        if len(name):
1✔
106
            s += name + ' '
1✔
107
        s += 'origin (%i,%i) ' % (self.x0, self.y0)
1✔
108
        if self.patch is not None:
1!
109
            (H, W) = self.patch.shape
1✔
110
            s += 'size (%i x %i)' % (W, H)
1✔
111
        else:
112
            s += '(no image)'
×
113
        return s
1✔
114

115
    def set(self, other):
1✔
116
        self.x0 = other.x0
×
117
        self.y0 = other.y0
×
118
        self.patch = other.patch
×
119
        self.name = other.name
×
120

121
    def trimToNonZero(self):
1✔
122
        if self.patch is None:
×
123
            return
×
124
        H, W = self.patch.shape
×
125
        if W == 0 or H == 0:
×
126
            return
×
127
        for x in range(W):
×
128
            if not np.all(self.patch[:, x] == 0):
×
129
                break
×
130
        x0 = x
×
131
        for x in range(W, 0, -1):
×
132
            if not np.all(self.patch[:, x - 1] == 0):
×
133
                break
×
134
            if x <= x0:
×
135
                break
×
136
        x1 = x
×
137

138
        for y in range(H):
×
139
            if not np.all(self.patch[y, :] == 0):
×
140
                break
×
141
        y0 = y
×
142
        for y in range(H, 0, -1):
×
143
            if not np.all(self.patch[y - 1, :] == 0):
×
144
                break
×
145
            if y <= y0:
×
146
                break
×
147
        y1 = y
×
148

149
        if x0 == 0 and y0 == 0 and x1 == W and y1 == H:
×
150
            return
×
151

152
        self.patch = self.patch[y0:y1, x0:x1]
×
153
        H, W = self.patch.shape
×
154
        if H == 0 or W == 0:
×
155
            self.patch = None
×
156
        self.x0 += x0
×
157
        self.y0 += y0
×
158

159
    def overlapsBbox(self, bbox):
1✔
160
        ext = self.getExtent()
1✔
161
        (x0, x1, y0, y1) = ext
1✔
162
        (ox0, ox1, oy0, oy1) = bbox
1✔
163
        if x0 >= ox1 or ox0 >= x1 or y0 >= oy1 or oy0 >= y1:
1!
164
            return False
×
165
        return True
1✔
166

167
    def hasBboxOverlapWith(self, other):
1✔
168
        oext = other.getExtent()
×
169
        return self.overlapsBbox(oext)
×
170

171
    def hasNonzeroOverlapWith(self, other):
1✔
172
        from astrometry.util.miscutils import get_overlapping_region
×
173
        if not self.hasBboxOverlapWith(other):
×
174
            return False
×
175
        ext = self.getExtent()
×
176
        (x0, x1, y0, y1) = ext
×
177
        oext = other.getExtent()
×
178
        (ox0, ox1, oy0, oy1) = oext
×
179
        ix, ox = get_overlapping_region(ox0, ox1 - 1, x0, x1 - 1)
×
180
        iy, oy = get_overlapping_region(oy0, oy1 - 1, y0, y1 - 1)
×
181
        ix = slice(ix.start - x0, ix.stop - x0)
×
182
        iy = slice(iy.start - y0, iy.stop - y0)
×
183
        sub = self.patch[iy, ix]
×
184
        osub = other.patch[oy, ox]
×
185
        assert(sub.shape == osub.shape)
×
186
        return np.sum(sub * osub) > 0.
×
187

188
    def getNonZeroMask(self):
1✔
189
        nz = (self.patch != 0)
×
190
        return Patch(self.x0, self.y0, nz)
×
191

192
    def __repr__(self):
1✔
193
        return str(self)
1✔
194

195
    def setName(self, name):
1✔
196
        self.name = name
1✔
197

198
    def getName(self):
1✔
199
        return self.name
×
200

201
    def copy(self):
1✔
202
        if self.patch is None:
×
203
            return Patch(self.x0, self.y0, None)
×
204
        return Patch(self.x0, self.y0, self.patch.copy())
×
205

206
    def getExtent(self, margin=0):
1✔
207
        '''
208
        Return (x0, x1, y0, y1) of the region covered by this patch;
209
        NON-inclusive upper bounds, ie, [x0, x1), [y0, y1).
210
        '''
211
        (h, w) = self.shape
1✔
212
        return (self.x0 - margin, self.x0 + w + margin,
1✔
213
                self.y0 - margin, self.y0 + h + margin)
214

215
    @property
1✔
216
    def extent(self):
1✔
NEW
217
        (h, w) = self.shape
×
NEW
218
        return (self.x0, self.x0 + w, self.y0, self.y0 + h)
×
219

220
    @property
1✔
221
    def shape(self):
1✔
222
        if self.patch is None:
1!
NEW
223
            return 0,0
×
224
        return self.patch.shape
1✔
225

226
    def getOrigin(self):
1✔
227
        return (self.x0, self.y0)
×
228

229
    def getPatch(self):
1✔
230
        return self.patch
×
231

232
    def getImage(self):
1✔
233
        return self.patch
1✔
234

235
    def getX0(self):
1✔
236
        return self.x0
1✔
237

238
    def getY0(self):
1✔
239
        return self.y0
1✔
240

241
    def clipTo(self, W, H):
1✔
242
        if self.patch is None:
1!
243
            return False
×
244
        if self.x0 >= W:
1!
245
            # empty
246
            self.patch = None
×
247
            return False
×
248
        if self.y0 >= H:
1!
249
            self.patch = None
×
250
            return False
×
251
        # debug
252
        #o0 = (self.x0, self.y0, self.patch.shape)
253
        if self.x0 < 0:
1!
254
            self.patch = self.patch[:, -self.x0:]
×
255
            self.x0 = 0
×
256
        if self.y0 < 0:
1!
257
            self.patch = self.patch[-self.y0:, :]
×
258
            self.y0 = 0
×
259
        # debug
260
        # S = self.patch.shape
261
        # if len(S) != 2:
262
        #     print 'clipTo: shape', self.patch.shape
263
        #     print 'original offset and patch shape:', o0
264
        #     print 'current offset and patch shape:', self.x0, self.y0, self.patch.shape
265

266
        (h, w) = self.patch.shape
1✔
267
        if (self.x0 + w) > W:
1!
268
            self.patch = self.patch[:, :(W - self.x0)]
×
269
        if (self.y0 + h) > H:
1!
270
            self.patch = self.patch[:(H - self.y0), :]
×
271

272
        assert(self.x0 >= 0)
1✔
273
        assert(self.y0 >= 0)
1✔
274
        (h, w) = self.shape
1✔
275
        assert(w <= W)
1✔
276
        assert(h <= H)
1✔
277
        assert(self.shape == self.patch.shape)
1✔
278
        return True
1✔
279

280
    def clipToRoi(self, x0, x1, y0, y1):
1✔
281
        if self.patch is None:
×
282
            return False
×
283
        if ((self.x0 >= x1) or (self.x1 <= x0) or
×
284
                (self.y0 >= y1) or (self.y1 <= y0)):
285
            # empty
286
            self.patch = None
×
287
            return False
×
288

289
        if self.x0 < x0:
×
290
            self.patch = self.patch[:, x0 - self.x0:]
×
291
            self.x0 = x0
×
292
        if self.y0 < y0:
×
293
            self.patch = self.patch[(y0 - self.y0):, :]
×
294
            self.y0 = y0
×
295
        (h, w) = self.shape
×
296
        if (self.x0 + w) > x1:
×
297
            self.patch = self.patch[:, :(x1 - self.x0)]
×
298
        if (self.y0 + h) > y1:
×
299
            self.patch = self.patch[:(y1 - self.y0), :]
×
300
        return True
×
301

302
    def getSlice(self, parent=None):
1✔
303
        if self.patch is None:
1!
304
            return ([], [])
×
305
        (ph, pw) = self.patch.shape
1✔
306
        if parent is not None:
1!
307
            (H, W) = parent.shape
1✔
308
            return (slice(np.clip(self.y0, 0, H), np.clip(self.y0 + ph, 0, H)),
1✔
309
                    slice(np.clip(self.x0, 0, W), np.clip(self.x0 + pw, 0, W)))
310
        return (slice(self.y0, self.y0 + ph),
×
311
                slice(self.x0, self.x0 + pw))
312

313
    def getSlices(self, shape):
1✔
314
        '''
315
        shape = (H,W).
316

317
        Returns (spatch, sparent), slices that yield the overlapping regions
318
        in this Patch and the given image.
319
        '''
320
        from astrometry.util.miscutils import get_overlapping_region
×
321
        (ph, pw) = self.shape
×
322
        (ih, iw) = shape
×
323
        (outx, inx) = get_overlapping_region(
×
324
            self.x0, self.x0 + pw - 1, 0, iw - 1)
325
        (outy, iny) = get_overlapping_region(
×
326
            self.y0, self.y0 + ph - 1, 0, ih - 1)
327
        if inx == [] or iny == []:
×
328
            return (slice(0, 0), slice(0, 0)), (slice(0, 0), slice(0, 0))
×
329
        return (iny, inx), (outy, outx)
×
330

331
    def getPixelIndices(self, parent, dtype=np.int32):
1✔
332
        if self.patch is None:
1!
333
            return np.array([], dtype)
×
334
        (h, w) = self.shape
1✔
335
        (H, W) = parent.shape
1✔
336
        return ( (np.arange(w, dtype=dtype) + dtype(self.x0))[np.newaxis, :] +
1✔
337
                ((np.arange(h, dtype=dtype) + dtype(self.y0)) * dtype(W))[:, np.newaxis]).ravel()
338

339
    plotnum = 0
1✔
340

341
    def addTo(self, img, scale=1.):
1✔
342
        from astrometry.util.miscutils import get_overlapping_region
1✔
343
        if self.patch is None:
1!
344
            return
×
345
        (ih, iw) = img.shape
1✔
346
        (ph, pw) = self.shape
1✔
347
        (outx, inx) = get_overlapping_region(
1✔
348
            self.x0, self.x0 + pw - 1, 0, iw - 1)
349
        (outy, iny) = get_overlapping_region(
1✔
350
            self.y0, self.y0 + ph - 1, 0, ih - 1)
351
        if inx == [] or iny == []:
1!
352
            return
×
353
        p = self.patch[iny, inx]
1✔
354
        img[outy, outx] += p * scale
1✔
355

356
        # if False:
357
        #   tmpimg = np.zeros_like(img)
358
        #   tmpimg[outy,outx] = p * scale
359
        #   plt.clf()
360
        #   plt.imshow(tmpimg, interpolation='nearest', origin='lower')
361
        #   plt.hot()
362
        #   plt.colorbar()
363
        #   fn = 'addto-%03i.png' % Patch.plotnum
364
        #   plt.savefig(fn)
365
        #   print 'Wrote', fn
366
        #
367
        #   plt.clf()
368
        #   plt.imshow(p, interpolation='nearest', origin='lower')
369
        #   plt.hot()
370
        #   plt.colorbar()
371
        #   fn = 'addto-%03i-p.png' % Patch.plotnum
372
        #   plt.savefig(fn)
373
        #   print 'Wrote', fn
374
        #
375
        #   Patch.plotnum += 1
376

377
    # Implement *=, /= for numeric types
378
    def __imul__(self, f):
1✔
379
        if self.patch is not None:
1!
380
            self.patch *= f
1✔
381
        return self
1✔
382

383
    def __idiv__(self, f):
1✔
384
        if self.patch is not None:
×
385
            self.patch /= f
×
386
        return self
×
387

388
    # Implement *, / for numeric types
389
    def __mul__(self, f):
1✔
390
        if self.patch is None:
1!
391
            return Patch(self.x0, self.y0, None)
×
392
        return Patch(self.x0, self.y0, self.patch * f)
1✔
393

394
    def __div__(self, f):
1✔
395
        if self.patch is None:
×
396
            return Patch(self.x0, self.y0, None)
×
397
        return Patch(self.x0, self.y0, self.patch / f)
×
398

399
    def performArithmetic(self, other, opname, otype=float):
1✔
400
        assert(isinstance(other, Patch))
1✔
401
        if (self.x0 == other.getX0() and self.y0 == other.getY0() and
1✔
402
                self.shape == other.shape):
403
            assert(self.x0 == other.getX0())
1✔
404
            assert(self.y0 == other.getY0())
1✔
405
            assert(self.shape == other.shape)
1✔
406
            if self.patch is None or other.patch is None:
1!
407
                return Patch(self.x0, self.y0, None)
×
408
            pcopy = self.patch.copy()
1✔
409
            op = getattr(pcopy, opname)
1✔
410
            return Patch(self.x0, self.y0, op(other.patch))
1✔
411

412
        (ph, pw) = self.patch.shape
1✔
413
        (ox0, oy0) = other.getX0(), other.getY0()
1✔
414
        (oh, ow) = other.shape
1✔
415

416
        # Find the union of the regions.
417
        ux0 = min(ox0, self.x0)
1✔
418
        uy0 = min(oy0, self.y0)
1✔
419
        ux1 = max(ox0 + ow, self.x0 + pw)
1✔
420
        uy1 = max(oy0 + oh, self.y0 + ph)
1✔
421

422
        # Set the "self" portion of the union
423
        p = np.zeros((uy1 - uy0, ux1 - ux0), dtype=otype)
1✔
424
        p[self.y0 - uy0: self.y0 - uy0 + ph,
1✔
425
          self.x0 - ux0: self.x0 - ux0 + pw] = self.patch
426

427
        # Get a slice for the "other"'s portion of the union
428
        psub = p[oy0 - uy0: oy0 - uy0 + oh,
1✔
429
                 ox0 - ux0: ox0 - ux0 + ow]
430
        # Perform the in-place += or -= operation on "psub"
431
        op = getattr(psub, opname)
1✔
432
        op(other.getImage())
1✔
433
        return Patch(ux0, uy0, p)
1✔
434

435
    def __add__(self, other):
1✔
436
        return self.performArithmetic(other, '__iadd__')
1✔
437

438
    def __sub__(self, other):
1✔
439
        return self.performArithmetic(other, '__isub__')
1✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc