Add show_points option to show_anns - #511
Conversation
…nnotations - Add show_points parameter to show_anns() that renders point prompts as star markers on top of the annotation image - Store point_coords and point_labels in predict_inst() so show_anns() can automatically use them - Points are drawn as filled 5-pointed stars with white edge outlines, matching the style of the show_points() method
|
🚀 Deployed on https://69be05c58945e45fc43fe427--opengeos.netlify.app |
There was a problem hiding this comment.
Pull request overview
This PR extends SamGeo3.show_anns() to optionally overlay point prompts (as star markers) on the rendered annotation image, using either explicitly provided points or the most recently used prompts stored from interactive prediction.
Changes:
- Added
show_pointsand related point-visualization parameters toshow_anns()to draw star markers with configurable colors/sizing. - Persisted
point_coords/point_labelsinpredict_inst()for reuse byshow_anns(show_points=True). - Updated
show_anns()docstring to describe the new point overlay behavior and parameters.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| if labels is None: | ||
| labels = [1] * len(coords_arr) | ||
| labels_arr = np.array(labels) | ||
|
|
There was a problem hiding this comment.
When show_points=True, point_labels is converted to a numpy array and then zipped with coords_arr. If point_labels length doesn’t match point_coords, zip() will silently drop extra points; if point_labels is passed as a scalar (common elsewhere in this repo), iterating a 0-d numpy array will raise. Consider normalizing/broadcasting labels (e.g., scalar or length-1 -> repeat) and raising a clear ValueError when lengths still don’t match.
| if labels is None: | |
| labels = [1] * len(coords_arr) | |
| labels_arr = np.array(labels) | |
| n_points = len(coords_arr) | |
| # Normalize/broadcast labels to match the number of points. | |
| if labels is None: | |
| # Default: all points are treated as foreground prompts. | |
| labels_arr = np.ones(n_points, dtype=int) | |
| else: | |
| labels_arr = np.array(labels) | |
| # Handle scalar labels (0-D array) by broadcasting. | |
| if labels_arr.ndim == 0: | |
| labels_arr = np.full(n_points, int(labels_arr), dtype=int) | |
| else: | |
| # Ensure 1-D and handle single-element sequences. | |
| labels_arr = labels_arr.reshape(-1) | |
| if labels_arr.size == 1 and n_points > 1: | |
| labels_arr = np.full(n_points, int(labels_arr[0]), dtype=int) | |
| elif labels_arr.size != n_points: | |
| raise ValueError( | |
| f"Number of point_labels ({labels_arr.size}) does not match " | |
| f"number of point_coords ({n_points})." | |
| ) |
| for pt, lbl in zip(coords_arr, labels_arr): | ||
| x, y = int(pt[0]), int(pt[1]) | ||
| color = foreground_color if lbl == 1 else background_color | ||
|
|
||
| # Build a 5-pointed star polygon | ||
| def _star_pts(cx, cy, r_outer, r_inner, n=5): | ||
| pts = [] | ||
| for i in range(2 * n): | ||
| r = r_outer if i % 2 == 0 else r_inner | ||
| angle = np.pi / 2 + i * np.pi / n | ||
| pts.append( | ||
| [ | ||
| int(cx + r * np.cos(angle)), | ||
| int(cy - r * np.sin(angle)), | ||
| ] | ||
| ) | ||
| return np.array(pts, dtype=np.int32) | ||
|
|
There was a problem hiding this comment.
_star_pts is defined inside the per-point loop, so it is re-created for every point rendered. Moving this helper outside the loop (or precomputing a template and translating it per point) would reduce overhead and keep the rendering logic simpler.
| for pt, lbl in zip(coords_arr, labels_arr): | |
| x, y = int(pt[0]), int(pt[1]) | |
| color = foreground_color if lbl == 1 else background_color | |
| # Build a 5-pointed star polygon | |
| def _star_pts(cx, cy, r_outer, r_inner, n=5): | |
| pts = [] | |
| for i in range(2 * n): | |
| r = r_outer if i % 2 == 0 else r_inner | |
| angle = np.pi / 2 + i * np.pi / n | |
| pts.append( | |
| [ | |
| int(cx + r * np.cos(angle)), | |
| int(cy - r * np.sin(angle)), | |
| ] | |
| ) | |
| return np.array(pts, dtype=np.int32) | |
| # Build a 5-pointed star polygon once per call, reuse for each point | |
| def _star_pts(cx, cy, r_outer, r_inner, n=5): | |
| pts = [] | |
| for i in range(2 * n): | |
| r = r_outer if i % 2 == 0 else r_inner | |
| angle = np.pi / 2 + i * np.pi / n | |
| pts.append( | |
| [ | |
| int(cx + r * np.cos(angle)), | |
| int(cy - r * np.sin(angle)), | |
| ] | |
| ) | |
| return np.array(pts, dtype=np.int32) | |
| for pt, lbl in zip(coords_arr, labels_arr): | |
| x, y = int(pt[0]), int(pt[1]) | |
| color = foreground_color if lbl == 1 else background_color |
| points (label=1). Defaults to dark green (0, 128, 0). | ||
| background_color (Tuple[int, int, int]): RGB color for background | ||
| points (label=0). Defaults to red (255, 0, 0). | ||
| point_size (int): Size of star markers in pixels. Defaults to 15. |
There was a problem hiding this comment.
point_size is documented as “Size of star markers in pixels”, but it’s used as the outer radius when computing the star vertices (the marker ends up roughly ~2*point_size across). Please clarify the docstring (e.g., outer radius vs diameter) or rename the parameter to avoid confusion for callers tuning marker size.
| point_size (int): Size of star markers in pixels. Defaults to 15. | |
| point_size (int): Outer radius of star markers in pixels (marker | |
| diameter is roughly 2 * point_size). Defaults to 15. |
| self.point_coords = ( | ||
| point_coords.tolist() | ||
| if isinstance(point_coords, np.ndarray) | ||
| else point_coords | ||
| ) | ||
| self.point_labels = ( | ||
| point_labels.tolist() | ||
| if isinstance(point_labels, np.ndarray) | ||
| else point_labels | ||
| ) |
There was a problem hiding this comment.
predict_inst() now stores self.point_coords/self.point_labels for later visualization, but it doesn’t normalize or validate point_labels relative to point_coords before storing. If a caller provides a scalar label or a mismatched length (both patterns supported in other modules), show_anns(show_points=True) can later fail or render incorrectly. Consider broadcasting/validating labels here before saving them (or saving a normalized Nx2 / length-N representation only).
| self.point_coords = ( | |
| point_coords.tolist() | |
| if isinstance(point_coords, np.ndarray) | |
| else point_coords | |
| ) | |
| self.point_labels = ( | |
| point_labels.tolist() | |
| if isinstance(point_labels, np.ndarray) | |
| else point_labels | |
| ) | |
| # Normalize point coordinates to a Python list and infer number of points. | |
| if isinstance(point_coords, np.ndarray): | |
| coords_array = point_coords | |
| coords_list = point_coords.tolist() | |
| else: | |
| coords_array = np.asarray(point_coords) | |
| coords_list = point_coords | |
| # Try to determine how many points we have. | |
| n_points: int | |
| if coords_array.ndim == 2: | |
| # Typical case: (N, 2) | |
| n_points = int(coords_array.shape[0]) | |
| elif coords_array.ndim == 1: | |
| # Treat a flat 1D coordinate as a single point (e.g., [x, y]). | |
| n_points = 1 | |
| else: | |
| # Fallback: rely on Python's len(). | |
| try: | |
| n_points = len(coords_list) | |
| except TypeError: | |
| n_points = 1 | |
| # Normalize point labels to match the number of points. | |
| labels_norm = None | |
| if point_labels is not None: | |
| if isinstance(point_labels, np.ndarray): | |
| labels_raw = point_labels.tolist() | |
| else: | |
| labels_raw = point_labels | |
| # Handle scalar label by broadcasting to all points. | |
| if isinstance( | |
| labels_raw, | |
| (int, float, bool, np.integer, np.floating), | |
| ): | |
| labels_norm = [labels_raw] * max(n_points, 0) | |
| else: | |
| # Ensure we have a list for length checks/broadcasting. | |
| labels_list = list(labels_raw) | |
| if n_points <= 1: | |
| # Single point: take the first label if available. | |
| if labels_list: | |
| labels_norm = [labels_list[0]] | |
| else: | |
| labels_norm = [] | |
| else: | |
| if len(labels_list) == 1: | |
| # Broadcast single label to all points. | |
| labels_norm = labels_list * n_points | |
| elif len(labels_list) == n_points: | |
| labels_norm = labels_list | |
| else: | |
| raise ValueError( | |
| f"Length of point_labels ({len(labels_list)}) does not match " | |
| f"number of point_coords ({n_points})." | |
| ) | |
| self.point_coords = coords_list | |
| self.point_labels = labels_norm |
Summary
show_pointsparameter toshow_anns()that overlays point prompts as 5-pointed star markers on the annotation image, matching the style ofshow_points()point_coordsandpoint_labelsinpredict_inst()soshow_anns()can automatically display them without re-specifyingUsage
Test plan
show_anns(show_points=True)renders star markers aftergenerate_masks_by_points()show_points()styleshow_anns()withoutshow_pointsis unchanged (backward compatible)