Loading [MathJax]/extensions/tex2jax.js
LSST Applications 28.0.0,g1653933729+a8ce1bb630,g1a997c3884+a8ce1bb630,g28da252d5a+5bd70b7e6d,g2bbee38e9b+638fca75ac,g2bc492864f+638fca75ac,g3156d2b45e+07302053f8,g347aa1857d+638fca75ac,g35bb328faa+a8ce1bb630,g3a166c0a6a+638fca75ac,g3e281a1b8c+7bbb0b2507,g4005a62e65+17cd334064,g414038480c+5b5cd4fff3,g41af890bb2+4ffae9de63,g4e1a3235cc+0f1912dca3,g6249c6f860+3c3976f90c,g80478fca09+46aba80bd6,g82479be7b0+77990446f6,g858d7b2824+78ba4d1ce1,g89c8672015+f667a5183b,g9125e01d80+a8ce1bb630,ga5288a1d22+2a6264e9ca,gae0086650b+a8ce1bb630,gb58c049af0+d64f4d3760,gc22bb204ba+78ba4d1ce1,gc28159a63d+638fca75ac,gcf0d15dbbd+32ddb6096f,gd6b7c0dfd1+3e339405e9,gda3e153d99+78ba4d1ce1,gda6a2b7d83+32ddb6096f,gdaeeff99f8+1711a396fd,gdd5a9049c5+b18c39e5e3,ge2409df99d+a5e4577cdc,ge33fd446bb+78ba4d1ce1,ge79ae78c31+638fca75ac,gf0baf85859+64e8883e75,gf5289d68f6+e1b046a8d7,gfa443fc69c+91d9ed1ecf,gfda6b12a05+8419469a56
LSST Data Management Base Package
All Classes Namespaces Files Functions Variables Typedefs Enumerations Enumerator Properties Friends Macros Modules Pages
ampOffset.py
Go to the documentation of this file.
1# This file is part of ip_isr.
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__all__ = ["AmpOffsetConfig", "AmpOffsetTask"]
23
24import warnings
25
26import numpy as np
27from lsst.afw.math import MEANCLIP, StatisticsControl, makeStatistics
28from lsst.afw.table import SourceTable
29from lsst.meas.algorithms import SourceDetectionTask, SubtractBackgroundTask
30from lsst.pex.config import Config, ConfigurableField, Field
31from lsst.pipe.base import Struct, Task
32
33
35 """Configuration parameters for AmpOffsetTask."""
36
37 def setDefaults(self):
38 self.background.algorithm = "AKIMA_SPLINE"
39 self.background.useApprox = False
40 self.background.ignoredPixelMask = [
41 "BAD",
42 "SAT",
43 "INTRP",
44 "CR",
45 "EDGE",
46 "DETECTED",
47 "DETECTED_NEGATIVE",
48 "SUSPECT",
49 "NO_DATA",
50 ]
51 self.detection.reEstimateBackground = False
52
53 # This maintains existing behavior and test values after DM-39796.
54 self.detection.thresholdType = "stdev"
55
56 ampEdgeInset = Field(
57 doc="Number of pixels the amp edge strip is inset from the amp edge. A thin strip of pixels running "
58 "parallel to the edge of the amp is used to characterize the average flux level at the amp edge.",
59 dtype=int,
60 default=5,
61 )
62 ampEdgeWidth = Field(
63 doc="Pixel width of the amp edge strip, starting at ampEdgeInset and extending inwards.",
64 dtype=int,
65 default=64,
66 )
67 ampEdgeMinFrac = Field(
68 doc="Minimum allowed fraction of viable pixel rows along an amp edge. No amp offset estimate will be "
69 "generated for amp edges that do not have at least this fraction of unmasked pixel rows.",
70 dtype=float,
71 default=0.5,
72 )
73 ampEdgeMaxOffset = Field(
74 doc="Maximum allowed amp offset ADU value. If a measured amp offset value is larger than this, the "
75 "result will be discarded and therefore not used to determine amp pedestal corrections.",
76 dtype=float,
77 default=5.0,
78 )
79 doWindowSmoothing = Field(
80 doc="Smooth amp edge differences by taking a rolling average.",
81 dtype=bool,
82 default=True,
83 )
84 ampEdgeWindowFrac = Field(
85 doc="Fraction of the amp edge lengths utilized as the sliding window for generating rolling average "
86 "amp offset values. It should be reconfigured for every instrument (HSC, LSSTCam, etc.) and should "
87 "not exceed 1. If not provided, it defaults to the fraction that recovers the pixel size of the "
88 "sliding window used in obs_subaru for compatibility with existing HSC data. Only relevant if "
89 "`doWindowSmoothing` is set to True.",
90 dtype=float,
91 default=512 / 4176,
92 )
93 doBackground = Field(
94 doc="Estimate and subtract background prior to amp offset estimation?",
95 dtype=bool,
96 default=True,
97 )
98 background = ConfigurableField(
99 doc="An initial background estimation step run prior to amp offset calculation.",
100 target=SubtractBackgroundTask,
101 )
102 backgroundFractionSample = Field(
103 doc="The fraction of the shorter side of the amplifier used for background binning.",
104 dtype=float,
105 default=1.0,
106 )
107 doDetection = Field(
108 doc="Detect sources and update cloned exposure prior to amp offset estimation?",
109 dtype=bool,
110 default=True,
111 )
112 detection = ConfigurableField(
113 doc="Source detection to add temporary detection footprints prior to amp offset calculation.",
114 target=SourceDetectionTask,
115 )
116 applyWeights = Field(
117 doc="Weights the amp offset calculation by the length of the interface between amplifiers. Applying "
118 "weights does not affect outcomes for amplifiers in a 2D grid with square-shaped amplifiers or in "
119 "any 1D layout on a detector, regardless of whether the amplifiers are square.",
120 dtype=bool,
121 default=True,
122 )
123 doApplyAmpOffset = Field(
124 doc="Apply amp offset corrections to the input exposure?",
125 dtype=bool,
126 default=False,
127 )
128
129
130class AmpOffsetTask(Task):
131 """Calculate and apply amp offset corrections to an exposure."""
132
133 ConfigClass = AmpOffsetConfig
134 _DefaultName = "isrAmpOffset"
135
136 def __init__(self, *args, **kwargs):
137 super().__init__(*args, **kwargs)
138 # Always load background subtask, even if doBackground=False;
139 # this allows for default plane bit masks to be defined.
140 self.makeSubtask("background")
141 if self.config.doDetection:
142 self.makeSubtask("detection")
143 # Initialize all of the instance variables here.
145
146 def run(self, exposure):
147 """Calculate amp offset values, determine corrective pedestals for each
148 amp, and update the input exposure in-place.
149
150 Parameters
151 ----------
152 exposure: `lsst.afw.image.Exposure`
153 Exposure to be corrected for amp offsets.
154 """
155
156 # Generate an exposure clone to work on and establish the bit mask.
157 exp = exposure.clone()
158 bitMask = exp.mask.getPlaneBitMask(self.background.config.ignoredPixelMask)
159 amps = exp.getDetector().getAmplifiers()
160
161 # Check that all amps have the same gemotry.
162 ampDims = [amp.getBBox().getDimensions() for amp in amps]
163 if not all(dim == ampDims[0] for dim in ampDims):
164 raise RuntimeError("All amps should have the same geometry.")
165 else:
166 # The zeroth amp is representative of all amps in the detector.
167 self.ampDims = ampDims[0]
168 # Dictionary mapping side numbers to interface lengths.
169 # See `getAmpAssociations()` for details about sides.
170 self.interfaceLengthLookupBySide = {i: self.ampDims[i % 2] for i in range(4)}
171
172 # Determine amplifier geometry.
173 ampWidths = {amp.getBBox().getWidth() for amp in amps}
174 ampHeights = {amp.getBBox().getHeight() for amp in amps}
175 if len(ampWidths) > 1 or len(ampHeights) > 1:
176 raise NotImplementedError(
177 "Amp offset correction is not yet implemented for detectors with differing amp sizes."
178 )
179
180 # Assuming all the amps have the same geometry.
181 self.shortAmpSide = np.min(ampDims[0])
182
183 # Check that the edge width and inset are not too large.
184 if self.config.ampEdgeWidth >= self.shortAmpSide - 2 * self.config.ampEdgeInset:
185 raise RuntimeError(
186 f"The edge width ({self.config.ampEdgeWidth}) plus insets ({self.config.ampEdgeInset}) "
187 f"exceed the amp's short side ({self.shortAmpSide}). This setup leads to incorrect results."
188 )
189
190 # Fit and subtract background.
191 if self.config.doBackground:
192 maskedImage = exp.getMaskedImage()
193 # Assuming all the detectors are the same.
194 nX = exp.getWidth() // (self.shortAmpSide * self.config.backgroundFractionSample) + 1
195 nY = exp.getHeight() // (self.shortAmpSide * self.config.backgroundFractionSample) + 1
196 # This ensures that the `binSize` is as large as possible,
197 # preventing background subtraction from inadvertently removing the
198 # amp offset signature. Here it's set to the shorter dimension of
199 # the amplifier by default (`backgroundFractionSample` = 1), which
200 # seems reasonable.
201 bg = self.background.fitBackground(maskedImage, nx=int(nX), ny=int(nY))
202 bgImage = bg.getImageF(self.background.config.algorithm, self.background.config.undersampleStyle)
203 maskedImage -= bgImage
204
205 # Detect sources and update cloned exposure mask planes in-place.
206 if self.config.doDetection:
207 schema = SourceTable.makeMinimalSchema()
208 table = SourceTable.make(schema)
209 # Detection sigma, used for smoothing and to grow detections, is
210 # normally measured from the PSF of the exposure. As the PSF hasn't
211 # been measured at this stage of processing, sigma is instead
212 # set to an approximate value here (which should be sufficient).
213 _ = self.detection.run(table=table, exposure=exp, sigma=2)
214
215 # Safety check: do any pixels remain for amp offset estimation?
216 if (exp.mask.array & bitMask).all():
217 log_fn = self.log.warning if self.config.doApplyAmpOffset else self.log.debug
218 log_fn(
219 "All pixels masked: cannot calculate any amp offset corrections. All pedestals are being set "
220 "to zero."
221 )
222 pedestals = np.zeros(len(amps))
223 else:
224 # Set up amp offset inputs.
225 im = exp.image
226 im.array[(exp.mask.array & bitMask) > 0] = np.nan
227
228 if self.config.ampEdgeWindowFrac > 1:
229 raise RuntimeError(
230 f"The specified fraction (`ampEdgeWindowFrac`={self.config.ampEdgeWindowFrac}) of the "
231 "edge length exceeds 1. This leads to complications downstream, after convolution in "
232 "the `getInterfaceOffset()` method. Please modify the `ampEdgeWindowFrac` value in the "
233 "config to be 1 or less and rerun."
234 )
235
236 # Obtain association and offset matrices.
237 A, sides = self.getAmpAssociations(amps)
238 B = self.getAmpOffsets(im, amps, A, sides)
239
240 # If least-squares minimization fails, convert NaNs to zeroes,
241 # ensuring that no values are erroneously added/subtracted.
242 pedestals = np.nan_to_num(np.linalg.lstsq(A, B, rcond=None)[0])
243
244 metadata = exposure.getMetadata() # Exposure metadata.
245 self.metadata["AMPOFFSET_PEDESTALS"] = {} # Task metadata.
246 for amp, pedestal in zip(amps, pedestals):
247 ampName = amp.getName()
248 # Add the amp pedestal to the exposure metadata.
249 metadata.set(
250 f"LSST ISR AMPOFFSET PEDESTAL {ampName}",
251 float(pedestal),
252 f"Pedestal level calculated for amp {ampName}",
253 )
254 if self.config.doApplyAmpOffset:
255 ampIm = exposure.image[amp.getBBox()].array
256 ampIm -= pedestal
257 # Add the amp pedestal to the "Task" metadata as well.
258 # Needed for Sasquatch/Chronograf!
259 self.metadata["AMPOFFSET_PEDESTALS"][ampName] = float(pedestal)
260 if self.config.doApplyAmpOffset:
261 status = "subtracted from exposure"
262 metadata.set(
263 "LSST ISR AMPOFFSET PEDESTAL SUBTRACTED", True, "Amp pedestals have been subtracted"
264 )
265 else:
266 status = "not subtracted from exposure"
267 metadata.set(
268 "LSST ISR AMPOFFSET PEDESTAL SUBTRACTED", False, "Amp pedestals have not been subtracted"
269 )
270 self.log.info(f"amp pedestal values ({status}): {', '.join([f'{x:.4f}' for x in pedestals])}")
271
272 return Struct(pedestals=pedestals)
273
274 def getAmpAssociations(self, amps):
275 """Determine amp geometry and amp associations from a list of
276 amplifiers.
277
278 Parse an input list of amplifiers to determine the layout of amps
279 within a detector, and identify all amp sides (i.e., the
280 horizontal and vertical junctions between amps).
281
282 Returns a matrix with a shape corresponding to the geometry of the amps
283 in the detector.
284
285 Parameters
286 ----------
287 amps : `list` [`lsst.afw.cameraGeom.Amplifier`]
288 List of amplifier objects used to deduce associations.
289
290 Returns
291 -------
292 ampAssociations : `numpy.ndarray`
293 An N x N matrix (N = number of amplifiers) that illustrates the
294 connections between amplifiers within the detector layout. Each row
295 and column index corresponds to the ampIds of a specific pair of
296 amplifiers, and the matrix elements indicate their associations as
297 follows:
298
299 * 0: No association
300 * -1: Association exists (direction specified in the ampSides
301 matrix)
302 * n >= 1: Diagonal elements indicate the number of neighboring
303 amplifiers for the corresponding ampId==row==column number.
304
305 ampSides : `numpy.ndarray`
306 An N x N matrix (N = the number of amplifiers) representing the amp
307 side information corresponding to the `ampAssociations`
308 matrix. The elements are integers defined as below:
309
310 * -1: No side due to no association or the same amp (diagonals)
311 * 0: Side on the bottom
312 * 1: Side on the right
313 * 2: Side on the top
314 * 3: Side on the left
315 """
316 xCenters = [amp.getBBox().getCenterX() for amp in amps]
317 yCenters = [amp.getBBox().getCenterY() for amp in amps]
318 xIndices = np.ceil(xCenters / np.min(xCenters) / 2).astype(int) - 1
319 yIndices = np.ceil(yCenters / np.min(yCenters) / 2).astype(int) - 1
320
321 nAmps = len(amps)
322 ampIds = np.zeros((len(set(yIndices)), len(set(xIndices))), dtype=int)
323
324 for ampId, xIndex, yIndex in zip(np.arange(nAmps), xIndices, yIndices):
325 ampIds[yIndex, xIndex] = ampId
326
327 ampAssociations = np.zeros((nAmps, nAmps), dtype=int)
328 ampSides = np.full_like(ampAssociations, -1)
329
330 for ampId in ampIds.ravel():
331 neighbors, sides = self.getNeighbors(ampIds, ampId)
332 interfaceWeights = (
333 1
334 if not self.config.applyWeights
335 else np.array([self.interfaceLengthLookupBySide[side] for side in sides])
336 )
337 ampAssociations[ampId, neighbors] = -1 * interfaceWeights
338 ampSides[ampId, neighbors] = sides
339 ampAssociations[ampId, ampId] = -ampAssociations[ampId].sum()
340
341 if ampAssociations.sum() != 0:
342 raise RuntimeError("The `ampAssociations` array does not sum to zero.")
343
344 if not np.all(ampAssociations == ampAssociations.T):
345 raise RuntimeError("The `ampAssociations` is not symmetric about the diagonal.")
346
347 self.log.debug("amp associations:\n%s", ampAssociations)
348 self.log.debug("amp sides:\n%s", ampSides)
349
350 return ampAssociations, ampSides
351
352 def getNeighbors(self, ampIds, ampId):
353 """Get the neighbor amplifiers and their sides for a given
354 amplifier.
355
356 Parameters
357 ----------
358 ampIds : `numpy.ndarray`
359 Matrix with amp side association information.
360 ampId : `int`
361 The amplifier ID for which neighbor amplifiers and side IDs
362 are to be found.
363
364 Returns
365 -------
366 neighbors : `list` [`int`]
367 List of neighbor amplifier IDs.
368 sides : `list` [`int`]
369 List of side IDs, with each ID corresponding to its respective
370 neighbor amplifier.
371 """
372 m, n = ampIds.shape
373 r, c = np.ravel(np.where(ampIds == ampId))
374 neighbors, sides = [], []
375 sideLookup = {
376 0: (r + 1, c),
377 1: (r, c + 1),
378 2: (r - 1, c),
379 3: (r, c - 1),
380 }
381 for side, (row, column) in sideLookup.items():
382 if 0 <= row < m and 0 <= column < n:
383 neighbors.append(ampIds[row][column])
384 sides.append(side)
385 return neighbors, sides
386
387 def getAmpOffsets(self, im, amps, associations, sides):
388 """Calculate the amp offsets for all amplifiers.
389
390 Parameters
391 ----------
392 im : `lsst.afw.image._image.ImageF`
393 Amplifier image to extract data from.
394 amps : `list` [`lsst.afw.cameraGeom.Amplifier`]
395 List of amplifier objects.
396 associations : numpy.ndarray
397 An N x N matrix containing amp association information, where N is
398 the number of amplifiers.
399 sides : numpy.ndarray
400 An N x N matrix containing amp side information, where N is the
401 number of amplifiers.
402
403 Returns
404 -------
405 ampsOffsets : `numpy.ndarray`
406 1D float array containing the calculated amp offsets for all
407 amplifiers.
408 """
409 ampsOffsets = np.zeros(len(amps))
410 ampsEdges = self.getAmpEdges(im, amps, sides)
411 interfaceOffsetLookup = {}
412
413 for ampId, ampAssociations in enumerate(associations):
414 ampNeighbors = np.ravel(np.where(ampAssociations < 0))
415 for ampNeighbor in ampNeighbors:
416 ampSide = sides[ampId][ampNeighbor]
417 interfaceWeight = (
418 1 if not self.config.applyWeights else self.interfaceLengthLookupBySide[ampSide]
419 )
420 edgeA = ampsEdges[ampId][ampSide]
421 edgeB = ampsEdges[ampNeighbor][(ampSide + 2) % 4]
422 if ampId < ampNeighbor:
423 interfaceOffset = self.getInterfaceOffset(ampId, ampNeighbor, edgeA, edgeB)
424 interfaceOffsetLookup[f"{ampId}{ampNeighbor}"] = interfaceOffset
425 else:
426 interfaceOffset = -interfaceOffsetLookup[f"{ampNeighbor}{ampId}"]
427 ampsOffsets[ampId] += interfaceWeight * interfaceOffset
428 return ampsOffsets
429
430 def getAmpEdges(self, im, amps, ampSides):
431 """Calculate the amp edges for all amplifiers.
432
433 Parameters
434 ----------
435 im : `lsst.afw.image._image.ImageF`
436 Amplifier image to extract data from.
437 amps : `list` [`lsst.afw.cameraGeom.Amplifier`]
438 List of amplifier objects.
439 ampSides : `numpy.ndarray`
440 An N x N matrix containing amp side information, where N is the
441 number of amplifiers.
442
443 Returns
444 -------
445 ampEdges : `dict` [`int`, `dict` [`int`, `numpy.ndarray`]]
446 A dictionary containing amp edge(s) for each amplifier,
447 corresponding to one or more potential sides, where each edge is
448 associated with a side. The outer dictionary has integer keys
449 representing amplifier IDs, and the inner dictionary has integer
450 keys representing side IDs for each amplifier and values that are
451 1D arrays of floats representing the 1D medianified strips from the
452 amp image, referred to as "amp edge":
453 {ampID: {sideID: numpy.ndarray}, ...}
454 """
455 ampEdgeOuter = self.config.ampEdgeInset + self.config.ampEdgeWidth
456 ampEdges = {}
457 slice_map = {
458 0: (slice(-ampEdgeOuter, -self.config.ampEdgeInset), slice(None)),
459 1: (slice(None), slice(-ampEdgeOuter, -self.config.ampEdgeInset)),
460 2: (slice(self.config.ampEdgeInset, ampEdgeOuter), slice(None)),
461 3: (slice(None), slice(self.config.ampEdgeInset, ampEdgeOuter)),
462 }
463 for ampId, (amp, ampSides) in enumerate(zip(amps, ampSides)):
464 ampEdges[ampId] = {}
465 ampIm = im[amp.getBBox()].array
466 # Loop over identified sides.
467 for ampSide in ampSides:
468 if ampSide < 0:
469 continue
470 strip = ampIm[slice_map[ampSide]]
471 # Catch warnings to prevent all-NaN slice RuntimeWarning.
472 with warnings.catch_warnings():
473 warnings.filterwarnings("ignore", r"All-NaN (slice|axis) encountered")
474 ampEdges[ampId][ampSide] = np.nanmedian(strip, axis=ampSide % 2) # 1D medianified strip
475 return ampEdges
476
477 def getInterfaceOffset(self, ampIdA, ampIdB, edgeA, edgeB):
478 """Calculate the amp offset for a given interface between two
479 amplifiers.
480
481 Parameters
482 ----------
483 ampIdA : int
484 ID of the first amplifier.
485 ampIdB : int
486 ID of the second amplifier.
487 edgeA : numpy.ndarray
488 Amp edge for the first amplifier.
489 edgeB : numpy.ndarray
490 Amp edge for the second amplifier.
491
492 Returns
493 -------
494 interfaceOffset : float
495 The calculated amp offset value for the given interface between
496 amps A and B.
497 """
498 interfaceId = f"{ampIdA}{ampIdB}"
499 sctrl = StatisticsControl()
500 # NOTE: Taking the difference with the order below fixes the sign flip
501 # in the B matrix.
502 edgeDiff = edgeA - edgeB
503 if self.config.doWindowSmoothing:
504 # Compute rolling averages.
505 window = int(self.config.ampEdgeWindowFrac * len(edgeDiff))
506 edgeDiffSum = np.convolve(np.nan_to_num(edgeDiff), np.ones(window), "same")
507 edgeDiffNum = np.convolve(~np.isnan(edgeDiff), np.ones(window), "same")
508 edgeDiffAvg = edgeDiffSum / np.clip(edgeDiffNum, 1, None)
509 else:
510 # Directly use the difference.
511 edgeDiffAvg = edgeDiff.copy()
512 edgeDiffAvg[np.isnan(edgeDiff)] = np.nan
513 # Take clipped mean of rolling average data as amp offset value.
514 interfaceOffset = makeStatistics(edgeDiffAvg, MEANCLIP, sctrl).getValue()
515 ampEdgeGoodFrac = 1 - (np.sum(np.isnan(edgeDiffAvg)) / len(edgeDiffAvg))
516 # Perform a couple of do-no-harm safety checks:
517 # a) The fraction of unmasked pixel rows is > ampEdgeMinFrac,
518 # b) The absolute offset ADU value is < ampEdgeMaxOffset.
519 minFracFail = ampEdgeGoodFrac < self.config.ampEdgeMinFrac
520 maxOffsetFail = np.abs(interfaceOffset) > self.config.ampEdgeMaxOffset
521 if minFracFail or maxOffsetFail:
522 log_fn = self.log.warning if self.config.doApplyAmpOffset else self.log.debug
523 if minFracFail:
524 log_fn(
525 f"The fraction of unmasked pixels for amp interface {interfaceId} is below the "
526 f"threshold ({ampEdgeGoodFrac:.2f} < {self.config.ampEdgeMinFrac}). Resetting the "
527 f"interface offset from {interfaceOffset} to 0."
528 )
529 if maxOffsetFail:
530 log_fn(
531 "The absolute offset value exceeds the limit "
532 f"({np.abs(interfaceOffset):.2f} > {self.config.ampEdgeMaxOffset} ADU). Resetting "
533 f"the interface offset from {interfaceOffset} to 0."
534 )
535 interfaceOffset = 0
536 self.log.debug(
537 f"amp interface {interfaceId} : "
538 f"viable edge difference frac = {ampEdgeGoodFrac}, "
539 f"interface offset = {interfaceOffset:.3f}"
540 )
541 return interfaceOffset
Pass parameters to a Statistics object.
Definition Statistics.h:83
getAmpOffsets(self, im, amps, associations, sides)
Definition ampOffset.py:387
__init__(self, *args, **kwargs)
Definition ampOffset.py:136
getAmpEdges(self, im, amps, ampSides)
Definition ampOffset.py:430
getNeighbors(self, ampIds, ampId)
Definition ampOffset.py:352
getInterfaceOffset(self, ampIdA, ampIdB, edgeA, edgeB)
Definition ampOffset.py:477