Source code for pdsspect.roi

"""Region of interest creation"""
import abc
import six
import warnings
from functools import wraps
from contextlib import contextmanager

import numpy as np
from ginga.canvas.types import basic


[docs]@six.add_metaclass(abc.ABCMeta) class ROIBase(basic.Polygon): """Base class for all ROI shapes""" def __init__(self, image_set, view_canvas, color='red', linewidth=1, linestyle='solid', showcap=False, fill=True, fillcolor=None, alpha=1.0, drawdims=False, font='Sans Serif', fillalpha=1.0, **kwargs): self.image_set = image_set self.view_canvas = view_canvas self.color = color self.linewidth = linewidth self.linestyle = linestyle self.showcap = showcap self.fill = True self.fillcolor = fillcolor self.alpha = alpha self.drawdims = drawdims self.font = font self.fillalpha = fillalpha self.kwargs = kwargs self._has_temp_point = False self._current_path = None
[docs] @staticmethod def draw_after(func): """Wrapper to redraw canvas after function""" @wraps(func) def wrapper(self, *args, **kwargs): run_func = func(self, *args, **kwargs) self.view_canvas.redraw() return run_func return wrapper
[docs] def lock_coords_to_pixel(self, data_x, data_y): """Lock the coordinates to the pixel The coordinate of the pixel is located at the bottom left corner of the pixel square while the center of the pixel .5 units up and to the right of the corner. So if the given coordinates are (2.3, 3.7), the pixel coordinates will be (2, 3) and the center of the pixel is (2.5, 3.5). This method locks the given coordinates to the pixel's coordinates Parameters ---------- data_x : :obj:`float` The given x coordinate data_y : :obj:`float` The given y coordinate Returns ------- point_x : :obj:`float` The corresponding x pixel coordinate point_y : :obj:`float` The corresponding y pixel coordinate """ point_x = point_y = None # Set default values for data points outside the pan if data_x <= 0: point_x = -.5 if data_y <= 0: point_y = -.5 if data_x >= (self.image_set.x_radius * 2 - 1): point_x = (self.image_set.x_radius * 2 - 1.5) if data_y >= (self.image_set.y_radius * 2 - 1): point_y = self.image_set.y_radius * 2 - 1.5 if None not in (point_x, point_y): return point_x, point_y X, Y = np.ceil(data_x), np.ceil(data_y) x, y = np.floor(data_x), np.floor(data_y) if point_x is None: if X - data_x <= .5: point_x = x + .5 else: point_x = x - .5 if point_y is None: if Y - data_y <= .5: point_y = y + .5 else: point_y = y - .5 return point_x, point_y
[docs] @staticmethod def lock_coords_to_pixel_wrapper(func): """Wrapper to lock data coordinates to the corresponding pixels""" @wraps(func) def wrapper(self, data_x, data_y): point_x, point_y = self.lock_coords_to_pixel(data_x, data_y) return func(self, point_x, point_y) return wrapper
[docs] @abc.abstractmethod def start_ROI(self, data_x, data_y): """Abstract method to start the ROI process""" pass
[docs] @abc.abstractmethod def continue_ROI(self, data_x, data_y): """Abstract method to continue the ROI process""" pass
[docs] @abc.abstractmethod def extend_ROI(self, data_x, data_y): """Abstract method to extend the ROI process""" pass
[docs] @abc.abstractmethod def stop_ROI(self, data_x, data_y): """Abstract method to stop the ROI process""" pass
[docs] def create_ROI(self, points=None): """Create a Region of interest Parameters ---------- points : :obj:`list` of :obj:`tuple` of two :obj:`int` Points that make up the vertices of the ROI Returns ------- coordinates : :class:`numpy.ndarray` ``m x 2`` array of coordinates. """ points = self._current_path.get_points() if points is None else points super(ROIBase, self).__init__( points, color=self.color, linewidth=self.linewidth, linestyle=self.linestyle, showcap=self.showcap, fill=self.fill, fillcolor=self.color, alpha=self.alpha, drawdims=self.drawdims, font=self.font, fillalpha=self.fillalpha, **self.kwargs) self.view_canvas.add(self) coords = self._get_roi_coords() self.view_canvas.deleteObject(self) coordinates = np.stack(coords, axis=-1) return coordinates
[docs] def contains_arr(self, x_arr, y_arr): """Determine whether the points in the ROI are in arrays The arrays must be the same shape. The arrays should be result of ``np.mgrid[y1:y2:1, x1:x2:1]`` Parameters ---------- x_arr : :class:`numpy.ndarray` Array of x coodinates y_arr : :class:`numpy.ndarray` Array of y coordinates Returns ------- result : :class:`numpy.ndarray` Boolean array where coordinates that are in ROI are True """ # NOTE: we use a version of the ray casting algorithm # See: http://alienryderflex.com/polygon/ xa, ya = x_arr, y_arr # promote input arrays dimension cardinality, if necessary promoted = False if len(x_arr.shape) == 1: x_arr = x_arr.reshape(1, -1) promoted = True if len(y_arr.shape) == 1: y_arr = y_arr.reshape(-1, 1) promoted = True result = np.zeros(y_arr.shape, dtype=np.bool) result1 = np.zeros(y_arr.shape, dtype=np.bool) result2 = np.zeros(y_arr.shape, dtype=np.bool) points = self.get_data_points() xj, yj = points[-1] for point in points: xi, yi = point tf = np.logical_and( np.logical_or(np.logical_and(yi < ya, yj >= ya), np.logical_and(yj < ya, yi >= ya)), np.logical_or(xi <= xa, xj <= xa) ) rs, cs = np.where(tf) cross1 = np.zeros(ya.shape, dtype=bool) cross2 = np.zeros(ya.shape, dtype=bool) mask1 = ( (xi + (ya[rs, cs] - yi) / (yj - yi) * (xj - xi)) < xa[rs, cs] ) mask2 = ( (xi + (ya[rs, cs] - yi) / (yj - yi) * (xj - xi)) <= xa[rs, cs] ) cross1[rs, cs] = mask1 cross2[rs, cs] = mask2 result1[tf] ^= cross1[tf] result2[tf] ^= cross2[tf] xj, yj = xi, yi result = np.logical_or(result1, result2) if promoted: # de-promote result result = result[np.eye(len(y_arr), len(x_arr), dtype=np.bool)] return result
def _get_mask_from_roi(self, roi, mask=None): """Get mask array from ROI Parameters ---------- roi : :class:`ROIBase` The region of interest mask : :class:`numpy.ndarray` Boolean array of the image Returns ------- mask : :class:`numpy.ndarray` Boolean array of the image with ROI coordinates as ``True`` """ if mask is None: mask = np.zeros(self.image_set.current_image.shape, dtype=np.bool) x1, y1, x2, y2 = roi.get_llur() x1, y1 = np.floor([x1, y1]).astype(int) x2, y2 = np.ceil([x2, y2]).astype(int) X, Y = np.mgrid[x1:x2, y1:y2] rows, cols = Y, X coords = roi.contains_arr(X, Y) mask[rows, cols] = coords return mask @contextmanager def _temporary_move_by_delta(self, delta): """Context manager to move the ROI by delta temporarily Parameters ---------- delta : :obj:`tuple` of two :obj:`float` Change the roi position by x and y Example ------- >>> with _temporary_move_by_delta((10, 15) as moved_roi: ... moved_roi.get_points() """ delta_x, delta_y = delta self.move_delta(delta_x, delta_y) yield self self.move_delta(-delta_x, -delta_y) def _get_roi_coords(self): """Get the coordinates in the region of interest""" delta = self.image_set.map_zoom_to_full_view() with self._temporary_move_by_delta(delta) as moved_roi: mask = self._get_mask_from_roi(moved_roi) roi_coords = np.where(mask) return roi_coords
[docs]class Polygon(ROIBase): """Polygon Region of Interest"""
[docs] @ROIBase.draw_after @ROIBase.lock_coords_to_pixel_wrapper def start_ROI(self, data_x, data_y): """Start the ROI process The ROI will be a :class:`ginga.canvas.types.basic.Path` object Parameters ---------- data_x : :obj:`float` The x coordinate data_y : :obj:`float` The y coordinate """ self._current_path = basic.Path( [(data_x, data_y)], color=self.color ) self.view_canvas.add(self._current_path)
[docs] @ROIBase.draw_after @ROIBase.lock_coords_to_pixel_wrapper def continue_ROI(self, data_x, data_y): """Create new vertex on the polygon on left click Parameters ---------- data_x : :obj:`float` The x coordinate data_y : :obj:`float` The y coordinate """ self._current_path.insert_pt(0, (data_x, data_y)) self._has_temp_point = False
[docs] @ROIBase.draw_after @ROIBase.lock_coords_to_pixel_wrapper def extend_ROI(self, data_x, data_y): """Extend the current edge of the polygon on mouse motion Parameters ---------- data_x : :obj:`float` The x coordinate data_y : :obj:`float` The y coordinate """ self._current_path.insert_pt(0, (data_x, data_y)) if self._current_path.get_num_points() > 2 and self._has_temp_point: self._current_path.delete_pt(1) self._has_temp_point = True
[docs] @ROIBase.draw_after @ROIBase.lock_coords_to_pixel_wrapper def stop_ROI(self, data_x, data_y): """Close the polygon on right click The polygon will close based on last left click and not on the right click. There must be more than 2 points to formulate a polygon Parameters ---------- data_x : :obj:`float` The x coordinate data_y : :obj:`float` The y coordinate """ if self._has_temp_point: self._current_path.delete_pt(0) if self._current_path.get_num_points() <= 2: warnings.warn("Must have more than 2 points for a polygon") coords = [] else: coords = self.create_ROI(self._current_path.get_points()) self.view_canvas.deleteObject(self._current_path) return coords
[docs]class Rectangle(ROIBase): """Rectangle Region of interest"""
[docs] @ROIBase.draw_after @ROIBase.lock_coords_to_pixel_wrapper def start_ROI(self, data_x, data_y): """Start the region of interest on left click Parameters ---------- data_x : :obj:`float` The x coordinate data_y : :obj:`float` The y coordinate """ self._current_path = basic.Rectangle( data_x, data_y, data_x + 1, data_y + 1, color=self.color) self.view_canvas.add(self._current_path)
def continue_ROI(self, data_x, data_y): pass
[docs] @ROIBase.draw_after @ROIBase.lock_coords_to_pixel_wrapper def extend_ROI(self, data_x, data_y): """Exend the rectangle on region of interest on mouse motion Parameters ---------- data_x : :obj:`float` The x coordinate data_y : :obj:`float` The y coordinate """ if data_x >= self._current_path.x1: data_x += 1 if data_y >= self._current_path.y1: data_y += 1 self._current_path.x2 = data_x self._current_path.y2 = data_y
[docs] @ROIBase.draw_after @ROIBase.lock_coords_to_pixel_wrapper def stop_ROI(self, data_x, data_y): """Stop the region of interest on right click Parameters ---------- data_x : :obj:`float` The x coordinate data_y : :obj:`float` The y coordinate """ coords = self.create_ROI(self._current_path.get_points()) self.view_canvas.deleteObject(self._current_path) return coords
[docs]class Pencil(ROIBase): """Select individual pixels""" point_radius = center_shift = .5 def __init__(self, *args, **kwargs): super(Pencil, self).__init__(*args, **kwargs) self._current_path = []
[docs] @ROIBase.draw_after def start_ROI(self, data_x, data_y): """Start choosing pixels on left click Parameters ---------- data_x : :obj:`float` The x coordinate data_y : :obj:`float` The y coordinate """ self._add_point(data_x, data_y)
@ROIBase.lock_coords_to_pixel_wrapper def _add_point(self, data_x, data_y): """Add a point to the current path list Parameters ---------- data_x : :obj:`float` The x coordinate data_y : :obj:`float` The y coordinate """ next_point = basic.Point( data_x + self.center_shift, data_y + self.center_shift, self.point_radius, color=self.color) self.view_canvas.add(next_point) self._current_path.append(next_point)
[docs] @ROIBase.draw_after def continue_ROI(self, data_x, data_y): """Add another pixel on left click Parameters ---------- data_x : :obj:`float` The x coordinate data_y : :obj:`float` The y coordinate """ self._add_point(data_x, data_y)
# @ROIBase.draw_after # def extend_ROI(self, data_x, data_y): # self._add_point(data_x, data_y) def extend_ROI(self, data_x, data_y): pass
[docs] def move_delta(self, delta_x, delta_y): """Override the move_delta function to move all the points Parameters ---------- delta_x : :obj:`float` Change in the x direction delta_y : :obj:`float` Change in the y direction """ for point in self._current_path: point.move_delta(delta_x, delta_y)
[docs] @ROIBase.draw_after def stop_ROI(self, data_x, data_y): """Set all pixels as roi cooridinates on right click Parameters ---------- data_x : :obj:`float` The x coordinate data_y : :obj:`float` The y coordinate Returns ------- coordinates : :class:`numpy.ndarray` Coordinates of points selected """ delta = self.image_set.map_zoom_to_full_view() with self._temporary_move_by_delta(delta) as moved: pixels = list(set([(p.x, p.y) for p in moved._current_path])) self.view_canvas.delete_objects(self._current_path) coords = [(int(y), int(x)) for x, y in pixels] coordinates = np.array(coords) return coordinates