227 """Find glint trails in a catalog.
231 catalog : `lsst.afw.table.SourceCatalog`
232 Catalog to search for glint trails.
236 result : `lsst.pipe.base.Struct`
237 Results as a struct with attributes:
240 Catalog subsets containing sources in each trail that was found.
241 (`list` [`lsst.afw.table.SourceCatalog`])
243 Ids of all the sources that were included in any fit trail.
246 Parameters of all the trails that were found.
247 (`list` [`GlintTrailParameters`])
252 per_id = collections.defaultdict(list)
253 for match
in matches:
254 per_id[match.first[
"id"]].append(match)
255 counts = {id: len(value)
for id, value
in per_id.items()}
261 for id
in dict(sorted(counts.items(), key=
lambda item: item[1], reverse=
True)):
265 if counts[id] + 1 < self.config.min_points
or id
in trailed_ids:
268 self.log.debug(
"id=%d at %.1f,%.1f has %d matches within %d pixels.",
270 per_id[id][0].first.getX(),
271 per_id[id][0].first.getY(),
274 if (trail := self.
_search_one(per_id[id], good_catalog))
is not None:
275 trail, result = trail
277 n_new = len(set(trail[
"id"]).difference(trailed_ids))
279 self.log.info(
"Found %.1f pixel length trail with %d points, "
280 "%d not in any other trail (angle=%.2f deg, center=%.1f,%.1f)",
281 result.length, len(trail), n_new, math.degrees(result.angle),
282 result.center_x, result.center_y)
284 trailed_ids.update(trail[
"id"])
285 parameters.append(result)
287 self.log.info(
"Found %d glint trails containing %d total sources.",
288 len(trails), len(trailed_ids))
289 return lsst.pipe.base.Struct(trails=trails,
290 trailed_ids=trailed_ids,
291 parameters=parameters)
313 """Search one set of matches for a possible trail.
317 matches : `list` [`lsst.afw.table.Match`]
318 Matches for one anchor source to search for lines.
319 catalog : `lsst.afw.SourceCatalog`
320 Catalog of all sources, to refit lines to.
324 trail, result : `tuple` or None
325 If the no trails matching the criteria are found, return None,
326 otherwise return a tuple of the sources in the trail and the
329 components = collections.defaultdict(list)
331 xy_deltas = {pair.second[
"id"]: (pair.second.getX() - pair.first.getX(),
332 pair.second.getY() - pair.first.getY())
for pair
in matches}
335 for i, (id1, pair1)
in enumerate(xy_deltas.items()):
336 distance = math.sqrt(pair1[0]**2 + pair1[1]**2)
337 for j, (id2, pair2)
in enumerate(xy_deltas.items()):
340 delta = abs(pair1[0] * pair2[1] - pair1[1] * pair2[0])
342 if delta / distance < 2 * self.config.threshold:
343 components[i].append(j)
346 if len(components) == 0:
349 longest, value = max(components.items(), key=
lambda x: len(x[1]))
350 n_points = len(value)
352 if n_points < self.config.min_points:
355 candidate = [longest] + components[longest]
356 trail, result = self.
_other_points(n_points, candidate, matches, catalog)
358 if trail
is None or len(trail) < self.config.min_points:
363 """Find all catalog records that could lie on this line.
368 Number of sources in this candidate trail.
369 indexes : `list` [`int`]
370 Indexes into matches on this candidate trail.
371 matches : `list` [`lsst.afw.table.Match`]
372 Matches for one anchor sources to search for lines.
373 catalog : `lsst.afw.SourceCatalog`
374 Catalog of all sources, to refit lines to.
378 trail : `lsst.afw.table.SourceCatalog`
379 Sources that are in the fitted trail.
380 result : `GlintTrailParameters`
381 Parameters of the fitted trail.
384 def extract(line, inliers, x, y, prefix=""):
385 """Extract values from the fit and log and return them."""
388 stderr = math.sqrt(np.mean(line.distance(x, y)**2))
389 self.log.debug(
"%s fit: stderr=%.6f, inliers/total=%d/%d",
390 prefix, stderr, len(x), len(inliers))
393 length = max(scipy.spatial.distance.pdist(np.column_stack((x, y))), default=0)
394 dx, dy = line.direction
397 m, b = math.inf, math.nan
400 b = line.center[1] - m*line.center[0]
403 center_x=line.center[0], center_y=line.center[1])
405 x = np.empty(n_points)
406 x[0] = matches[0].first.getX()
407 x[1:] = [matches[i].second.getX()
for i
in indexes]
408 y = np.empty(n_points)
409 y[0] = matches[0].first.getY()
410 y[1:] = [matches[i].second.getY()
for i
in indexes]
412 line, inliers =
_ransac_line(x, y, self.config.threshold, min_samples=2, seed=self.config.seed)
414 self.log.info(
"Glint trail interpolation could not find a valid fit.")
416 extract(line, inliers, x, y, prefix=
"preliminary")
418 if (n_inliers := np.sum(inliers)) < self.config.min_points:
419 self.log.debug(
"Candidate trail rejected with %d < %d points.",
420 n_inliers, self.config.min_points)
424 x = catalog[
"slot_Centroid_x"]
425 y = catalog[
"slot_Centroid_y"]
428 candidates = line.distance(x, y) < 2*self.config.threshold
432 line, inliers =
_ransac_line(x, y, self.config.threshold, min_samples=3, seed=self.config.seed)
434 self.log.info(
"Glint trail interpolation could not find a valid fit.")
436 result = extract(line, inliers, x, y, prefix=
"final")
438 return catalog[candidates][inliers], result