class CameraDisplay: """ Camera Display using matplotlib. Parameters ---------- geometry : `~ctapipe.io.CameraGeometry` Definition of the Camera/Image image: array_like array of values corresponding to the pixels in the CameraGeometry. ax : `matplotlib.axes.Axes` A matplotlib axes object to plot on, or None to create a new one title : str (default "Camera") Title to put on camera plot norm : str or `matplotlib.color.Normalize` instance (default 'lin') Normalization for the color scale. Supported str arguments are - 'lin': linear scale - 'log': logarithmic scale (base 10) cmap : str or `matplotlib.colors.Colormap` (default 'hot') Color map to use (see `matplotlib.cm`) allow_pick : bool (default False) if True, allow user to click and select a pixel autoupdate : bool (default True) redraw automatically (otherwise need to call plt.draw()) autoscale : bool (default True) rescale the vmin/vmax values when the image changes. This is set to False if `set_limits_*` is called to explicity set data limits. antialiased : bool (default True) whether to draw in antialiased mode or not. Notes ----- Speed: CameraDisplay is not intended to be very fast (matplotlib is not a very speed performant graphics library, it is intended for nice output plots). However, most of the slowness of CameraDisplay is in the constructor. Once one is displayed, changing the image that is displayed is relatively fast and efficient. Therefore it is best to initialize an instance, and change the data, rather than generating new CameraDisplays. Pixel Implementation: Pixels are rendered as a `matplotlib.collections.PatchCollection` of Polygons (either 6 or 4 sided). You can access the PatchCollection directly (to e.g. change low-level style parameters) via `CameraDisplay.pixels` Output: Since CameraDisplay uses matplotlib, any display can be saved to any output file supported via plt.savefig(filename). This includes `.pdf` and `.png`. """ def __init__( self, geometry, image=None, ax=None, title="Camera", norm="lin", cmap="hot", allow_pick=False, autoupdate=True, autoscale=True, antialiased=True, ): self.axes = ax if ax is not None else plt.gca() self.geom = geometry self.pixels = None self.colorbar = None self.autoupdate = autoupdate self.autoscale = autoscale self._active_pixel = None self._active_pixel_label = None # initialize the plot and generate the pixels as a # RegularPolyCollection patches = [] for xx, yy, aa in zip( u.Quantity(self.geom.pix_x).value, u.Quantity(self.geom.pix_y).value, u.Quantity(np.array(self.geom.pix_area))): if self.geom.pix_type.startswith("hex"): rr = sqrt(aa * 2 / 3 / sqrt(3)) poly = RegularPolygon( (xx, yy), 6, radius=rr, orientation=self.geom.pix_rotation.rad, fill=True, ) else: rr = sqrt(aa) poly = Rectangle( (xx - rr / 2., yy - rr / 2.), width=rr, height=rr, angle=self.geom.pix_rotation.deg, fill=True, ) patches.append(poly) self.pixels = PatchCollection(patches, cmap=cmap, linewidth=0) self.axes.add_collection(self.pixels) self.pixel_highlighting = copy.copy(self.pixels) self.pixel_highlighting.set_facecolor('none') self.pixel_highlighting.set_linewidth(0) self.axes.add_collection(self.pixel_highlighting) # Set up some nice plot defaults self.axes.set_aspect('equal', 'datalim') self.axes.set_title(title) self.axes.set_xlabel("X position ({})".format(self.geom.pix_x.unit)) self.axes.set_ylabel("Y position ({})".format(self.geom.pix_y.unit)) self.axes.autoscale_view() # set up a patch to display when a pixel is clicked (and # pixel_picker is enabled): self._active_pixel = copy.copy(patches[0]) self._active_pixel.set_facecolor('r') self._active_pixel.set_alpha(0.5) self._active_pixel.set_linewidth(2.0) self._active_pixel.set_visible(False) self.axes.add_patch(self._active_pixel) self._active_pixel_label = self.axes.text(self._active_pixel.xy[0], self._active_pixel.xy[1], "0", horizontalalignment='center', verticalalignment='center') self._active_pixel_label.set_visible(False) # enable ability to click on pixel and do something (can be # enabled on-the-fly later as well: if allow_pick: self.enable_pixel_picker() if image is not None: self.image = image else: self.image = np.zeros_like(self.geom.pix_id, dtype=np.float) self.norm = norm def highlight_pixels(self, pixels, color='g', linewidth=1, alpha=0.75): ''' Highlight the given pixels with a colored line around them Parameters ---------- pixels : index-like The pixels to highlight. Can either be a list or array of integers or a boolean mask of length number of pixels color: a matplotlib conform color the color for the pixel highlighting linewidth: float linewidth of the highlighting in points alpha: 0 <= alpha <= 1 The transparency ''' l = np.zeros_like(self.image) l[pixels] = linewidth self.pixel_highlighting.set_linewidth(l) self.pixel_highlighting.set_alpha(alpha) self.pixel_highlighting.set_edgecolor(color) self.update() def enable_pixel_picker(self): """ enable ability to click on pixels """ self.pixels.set_picker(True) # enable click self.pixels.set_pickradius( sqrt(u.Quantity(self.geom.pix_area[0]).value) / np.pi) self.pixels.set_snap(True) # snap cursor to pixel center self.axes.figure.canvas.mpl_connect('pick_event', self._on_pick) def set_limits_minmax(self, zmin, zmax): """ set the color scale limits from min to max """ self.pixels.set_clim(zmin, zmax) self.autoscale = False self.update() def set_limits_percent(self, percent=95): """ auto-scale the color range to percent of maximum """ zmin = self.pixels.get_array().min() zmax = self.pixels.get_array().max() dz = zmax - zmin frac = percent / 100.0 self.autoscale = False self.set_limits_minmax(zmin, zmax - (1.0 - frac) * dz) @property def norm(self): ''' The norm instance of the Display Possible values: - "lin": linear scale - "log": log scale - any matplotlib.colors.Normalize instance, e. g. PowerNorm(gamma=-2) ''' return self.pixels.norm @norm.setter def norm(self, norm): if norm == 'lin': self.pixels.norm = Normalize() elif norm == 'log': self.pixels.norm = LogNorm() self.pixels.autoscale() # this is to handle matplotlib bug #5424 elif isinstance(norm, Normalize): self.pixels.norm = norm else: raise ValueError('Unsupported norm: {}'.format(norm)) self.update(force=True) self.pixels.autoscale() @property def cmap(self): """ Color map to use. Either a name or `matplotlib.colors.ColorMap` instance, e.g. from `matplotlib.pyplot.cm` """ return self.pixels.get_cmap() @cmap.setter def cmap(self, cmap): self.pixels.set_cmap(cmap) self.update() @property def image(self): """The image displayed on the camera (1D array of pixel values)""" return self.pixels.get_array() @image.setter def image(self, image): """ Change the image displayed on the Camera. Parameters ---------- image: array_like array of values corresponding to the pixels in the CameraGeometry. """ image = np.asanyarray(image) if image.shape != self.geom.pix_x.shape: raise ValueError("Image has a different shape {} than the" "given CameraGeometry {}".format( image.shape, self.geom.pix_x.shape)) self.pixels.set_array(image) self.pixels.changed() if self.autoscale: self.pixels.autoscale() self.update() def update(self, force=False): """ signal a redraw if necessary """ if self.autoupdate: if self.colorbar is not None: if force is True: self.colorbar.update_bruteforce(self.pixels) else: self.colorbar.update_normal(self.pixels) self.colorbar.draw_all() self.axes.figure.canvas.draw() def add_colorbar(self, **kwargs): """ add a colobar to the camera plot kwargs are passed to `figure.colorbar(self.pixels, **kwargs)` See matplotlib documentation for the supported kwargs: http://matplotlib.org/api/figure_api.html#matplotlib.figure.Figure.colorbar """ if self.colorbar is not None: raise ValueError( 'There is already a colorbar attached to this CameraDisplay') else: self.colorbar = self.axes.figure.colorbar(self.pixels, **kwargs) self.update() def add_ellipse(self, centroid, length, width, angle, asymmetry=0.0, **kwargs): """ plot an ellipse on top of the camera Parameters ---------- centroid: (float, float) position of centroid length: float major axis width: float minor axis angle: float rotation angle wrt "up" about the centroid, clockwise, in radians asymmetry: float 3rd-order moment for directionality if known kwargs: any MatPlotLib style arguments to pass to the Ellipse patch """ ellipse = Ellipse(xy=centroid, width=width, height=length, angle=np.degrees(angle), fill=False, **kwargs) self.axes.add_patch(ellipse) self.update() return ellipse def overlay_moments(self, momparams, **kwargs): """helper to overlay ellipse from a `reco.MomentParameters` structure Parameters ---------- momparams: `reco.MomentParameters` structuring containing Hillas-style parameterization kwargs: key=value any style keywords to pass to matplotlib (e.g. color='red' or linewidth=6) """ el = self.add_ellipse(centroid=(momparams.cen_x.value, momparams.cen_y.value), length=momparams.length.value, width=momparams.width.value, angle=momparams.psi.to(u.rad).value, **kwargs) self.axes.text(momparams.cen_x.value, momparams.cen_y.value, ("({:.02f},{:.02f})\n" "[w={:.02f},l={:.02f}]").format( momparams.cen_x, momparams.cen_y, momparams.width, momparams.length), color=el.get_edgecolor()) def _on_pick(self, event): """ handler for when a pixel is clicked """ pix_id = event.ind[-1] xx, yy, aa = u.Quantity(self.geom.pix_x[pix_id]).value, \ u.Quantity(self.geom.pix_y[pix_id]).value, \ u.Quantity(np.array(self.geom.pix_area)[pix_id]) if self.geom.pix_type.startswith("hex"): self._active_pixel.xy = (xx, yy) else: rr = sqrt(aa) self._active_pixel.xy = (xx - rr / 2., yy - rr / 2.) self._active_pixel.set_visible(True) self._active_pixel_label.set_x(xx) self._active_pixel_label.set_y(yy) self._active_pixel_label.set_text("{:003d}".format(pix_id)) self._active_pixel_label.set_visible(True) self.update() self.on_pixel_clicked(pix_id) # call user-function def on_pixel_clicked(self, pix_id): """virtual function to overide in sub-classes to do something special when a pixel is clicked """ print("Clicked pixel_id {}".format(pix_id)) def show(self): self.axes.figure.show()
class CameraDisplay: """ Camera Display using matplotlib. Parameters ---------- geometry : `~ctapipe.instrument.CameraGeometry` Definition of the Camera/Image image: array_like array of values corresponding to the pixels in the CameraGeometry. ax : `matplotlib.axes.Axes` A matplotlib axes object to plot on, or None to create a new one title : str (default "Camera") Title to put on camera plot norm : str or `matplotlib.color.Normalize` instance (default 'lin') Normalization for the color scale. Supported str arguments are - 'lin': linear scale - 'log': logarithmic scale (base 10) cmap : str or `matplotlib.colors.Colormap` (default 'hot') Color map to use (see `matplotlib.cm`) allow_pick : bool (default False) if True, allow user to click and select a pixel autoupdate : bool (default True) redraw automatically (otherwise need to call plt.draw()) autoscale : bool (default True) rescale the vmin/vmax values when the image changes. This is set to False if `set_limits_*` is called to explicity set data limits. Notes ----- Speed: CameraDisplay is not intended to be very fast (matplotlib is not a very speed performant graphics library, it is intended for nice output plots). However, most of the slowness of CameraDisplay is in the constructor. Once one is displayed, changing the image that is displayed is relatively fast and efficient. Therefore it is best to initialize an instance, and change the data, rather than generating new CameraDisplays. Pixel Implementation: Pixels are rendered as a `matplotlib.collections.PatchCollection` of Polygons (either 6 or 4 sided). You can access the PatchCollection directly (to e.g. change low-level style parameters) via `CameraDisplay.pixels` Output: Since CameraDisplay uses matplotlib, any display can be saved to any output file supported via plt.savefig(filename). This includes ``.pdf`` and ``.png``. """ def __init__( self, geometry, image=None, ax=None, title=None, norm="lin", cmap=None, allow_pick=False, autoupdate=True, autoscale=True, ): self.axes = ax if ax is not None else plt.gca() self.pixels = None self.colorbar = None self.autoupdate = autoupdate self.autoscale = autoscale self._active_pixel = None self._active_pixel_label = None self._axes_overlays = [] self.geom = geometry if title is None: title = geometry.camera_name # initialize the plot and generate the pixels as a # RegularPolyCollection patches = [] if hasattr(self.geom, "mask"): self.mask = self.geom.mask else: self.mask = np.ones_like(self.geom.pix_x.value, dtype=bool) pix_x = self.geom.pix_x.value[self.mask] pix_y = self.geom.pix_y.value[self.mask] pix_area = self.geom.pix_area.value[self.mask] for x, y, area in zip(pix_x, pix_y, pix_area): if self.geom.pix_type.startswith("hex"): r = sqrt(area * 2 / 3 / sqrt(3)) + 2 * PIXEL_EPSILON poly = RegularPolygon( (x, y), 6, radius=r, orientation=self.geom.pix_rotation.to_value(u.rad), fill=True, ) else: r = sqrt(area) + PIXEL_EPSILON poly = Rectangle( (x - r / 2, y - r / 2), width=r, height=r, angle=self.geom.pix_rotation.to_value(u.deg), fill=True, ) patches.append(poly) self.pixels = PatchCollection(patches, cmap=cmap, linewidth=0) self.axes.add_collection(self.pixels) self.pixel_highlighting = copy.copy(self.pixels) self.pixel_highlighting.set_facecolor("none") self.pixel_highlighting.set_linewidth(0) self.axes.add_collection(self.pixel_highlighting) # Set up some nice plot defaults self.axes.set_aspect("equal", "datalim") self.axes.set_title(title) self.axes.set_xlabel(f"X position ({self.geom.pix_x.unit})") self.axes.set_ylabel(f"Y position ({self.geom.pix_y.unit})") self.axes.autoscale_view() # set up a patch to display when a pixel is clicked (and # pixel_picker is enabled): self._active_pixel = copy.copy(patches[0]) self._active_pixel.set_facecolor("r") self._active_pixel.set_alpha(0.5) self._active_pixel.set_linewidth(2.0) self._active_pixel.set_visible(False) self.axes.add_patch(self._active_pixel) self._active_pixel_label = self.axes.text( self._active_pixel.xy[0], self._active_pixel.xy[1], "0", horizontalalignment="center", verticalalignment="center", ) self._active_pixel_label.set_visible(False) # enable ability to click on pixel and do something (can be # enabled on-the-fly later as well: if allow_pick: self.enable_pixel_picker() if image is not None: self.image = image else: self.image = np.zeros_like(self.geom.pix_id, dtype=np.float) self.norm = norm def highlight_pixels(self, pixels, color="g", linewidth=1, alpha=0.75): """ Highlight the given pixels with a colored line around them Parameters ---------- pixels : index-like The pixels to highlight. Can either be a list or array of integers or a boolean mask of length number of pixels color: a matplotlib conform color the color for the pixel highlighting linewidth: float linewidth of the highlighting in points alpha: 0 <= alpha <= 1 The transparency """ l = np.zeros_like(self.image) l[pixels] = linewidth self.pixel_highlighting.set_linewidth(l) self.pixel_highlighting.set_alpha(alpha) self.pixel_highlighting.set_edgecolor(color) self._update() def enable_pixel_picker(self): """ enable ability to click on pixels """ self.pixels.set_picker(True) # enable click self.pixels.set_pickradius( sqrt(u.Quantity(self.geom.pix_area[0]).value) / np.pi) self.pixels.set_snap(True) # snap cursor to pixel center self.axes.figure.canvas.mpl_connect("pick_event", self._on_pick) def set_limits_minmax(self, zmin, zmax): """ set the color scale limits from min to max """ self.pixels.set_clim(zmin, zmax) self.autoscale = False self._update() def set_limits_percent(self, percent=95): """ auto-scale the color range to percent of maximum """ zmin = np.nanmin(self.pixels.get_array()) zmax = np.nanmax(self.pixels.get_array()) dz = zmax - zmin frac = percent / 100.0 self.autoscale = False self.set_limits_minmax(zmin, zmax - (1.0 - frac) * dz) @property def norm(self): """ The norm instance of the Display Possible values: - "lin": linear scale - "log": log scale (cannot have negative values) - "symlog": symmetric log scale (negative values are ok) - any matplotlib.colors.Normalize instance, e. g. PowerNorm(gamma=-2) """ return self.pixels.norm @norm.setter def norm(self, norm): if norm == "lin": self.pixels.norm = Normalize() elif norm == "log": self.pixels.norm = LogNorm() self.pixels.autoscale() # this is to handle matplotlib bug #5424 elif norm == "symlog": self.pixels.norm = SymLogNorm(linthresh=1.0) self.pixels.autoscale() elif isinstance(norm, Normalize): self.pixels.norm = norm else: raise ValueError( "Unsupported norm: '{}', options are 'lin'," "'log','symlog', or a matplotlib Normalize object".format( norm)) self.update(force=True) self.pixels.autoscale() @property def cmap(self): """ Color map to use. Either a name or `matplotlib.colors.ColorMap` instance, e.g. from `matplotlib.pyplot.cm` """ return self.pixels.get_cmap() @cmap.setter def cmap(self, cmap): self.pixels.set_cmap(cmap) self._update() @property def image(self): """The image displayed on the camera (1D array of pixel values)""" return self.pixels.get_array() @image.setter def image(self, image): """ Change the image displayed on the Camera. Parameters ---------- image: array_like array of values corresponding to the pixels in the CameraGeometry. """ image = np.asanyarray(image) if image.shape != self.geom.pix_x.shape: raise ValueError( ("Image has a different shape {} than the " "given CameraGeometry {}").format(image.shape, self.geom.pix_x.shape)) self.pixels.set_array(np.ma.masked_invalid(image[self.mask])) self.pixels.changed() if self.autoscale: self.pixels.autoscale() self._update() def _update(self, force=False): """ signal a redraw if autoupdate is turned on """ if self.autoupdate: self.update(force) def update(self, force=False): """ redraw the display now """ self.axes.figure.canvas.draw() if self.colorbar is not None: if force is True: self.colorbar.update_bruteforce(self.pixels) else: self.colorbar.update_normal(self.pixels) self.colorbar.draw_all() def add_colorbar(self, **kwargs): """ add a colorbar to the camera plot kwargs are passed to `figure.colorbar(self.pixels, **kwargs)` See matplotlib documentation for the supported kwargs: http://matplotlib.org/api/figure_api.html#matplotlib.figure.Figure.colorbar """ if self.colorbar is not None: raise ValueError( "There is already a colorbar attached to this CameraDisplay") else: if "ax" not in kwargs: kwargs["ax"] = self.axes self.colorbar = self.axes.figure.colorbar(self.pixels, **kwargs) self.update() def add_ellipse(self, centroid, length, width, angle, asymmetry=0.0, **kwargs): """ plot an ellipse on top of the camera Parameters ---------- centroid: (float, float) position of centroid length: float major axis width: float minor axis angle: float rotation angle wrt x-axis about the centroid, anticlockwise, in radians asymmetry: float 3rd-order moment for directionality if known kwargs: any MatPlotLib style arguments to pass to the Ellipse patch """ ellipse = Ellipse( xy=centroid, width=length, height=width, angle=np.degrees(angle), fill=False, **kwargs, ) self.axes.add_patch(ellipse) self.update() return ellipse def overlay_moments(self, hillas_parameters, with_label=True, keep_old=False, **kwargs): """helper to overlay ellipse from a `HillasParametersContainer` structure Parameters ---------- hillas_parameters: `HillasParametersContainer` structuring containing Hillas-style parameterization with_label: bool If True, show coordinates of centroid and width and length keep_old: bool If True, to not remove old overlays kwargs: key=value any style keywords to pass to matplotlib (e.g. color='red' or linewidth=6) """ if not keep_old: self.clear_overlays() # strip off any units cen_x = u.Quantity(hillas_parameters.x).value cen_y = u.Quantity(hillas_parameters.y).value length = u.Quantity(hillas_parameters.length).value width = u.Quantity(hillas_parameters.width).value el = self.add_ellipse( centroid=(cen_x, cen_y), length=length * 2, width=width * 2, angle=hillas_parameters.psi.rad, **kwargs, ) self._axes_overlays.append(el) if with_label: text = self.axes.text( cen_x, cen_y, "({:.02f},{:.02f})\n[w={:.02f},l={:.02f}]".format( hillas_parameters.x, hillas_parameters.y, hillas_parameters.width, hillas_parameters.length, ), color=el.get_edgecolor(), ) self._axes_overlays.append(text) def clear_overlays(self): """ Remove added overlays from the axes """ while self._axes_overlays: overlay = self._axes_overlays.pop() overlay.remove() def _on_pick(self, event): """ handler for when a pixel is clicked """ pix_id = event.ind[-1] xx, yy, aa = ( u.Quantity(self.geom.pix_x[pix_id]).value, u.Quantity(self.geom.pix_y[pix_id]).value, u.Quantity(np.array(self.geom.pix_area)[pix_id]), ) if self.geom.pix_type.startswith("hex"): self._active_pixel.xy = (xx, yy) else: rr = sqrt(aa) self._active_pixel.xy = (xx - rr / 2.0, yy - rr / 2.0) self._active_pixel.set_visible(True) self._active_pixel_label.set_x(xx) self._active_pixel_label.set_y(yy) self._active_pixel_label.set_text(f"{pix_id:003d}") self._active_pixel_label.set_visible(True) self._update() self.on_pixel_clicked(pix_id) # call user-function def on_pixel_clicked(self, pix_id): """virtual function to overide in sub-classes to do something special when a pixel is clicked """ print(f"Clicked pixel_id {pix_id}") def show(self): self.axes.figure.show()
class CameraDisplay: """ Camera Display using matplotlib. Parameters ---------- geometry : `~ctapipe.instrument.CameraGeometry` Definition of the Camera/Image image: array_like array of values corresponding to the pixels in the CameraGeometry. ax : `matplotlib.axes.Axes` A matplotlib axes object to plot on, or None to create a new one title : str (default "Camera") Title to put on camera plot norm : str or `matplotlib.color.Normalize` instance (default 'lin') Normalization for the color scale. Supported str arguments are - 'lin': linear scale - 'log': logarithmic scale (base 10) cmap : str or `matplotlib.colors.Colormap` (default 'hot') Color map to use (see `matplotlib.cm`) allow_pick : bool (default False) if True, allow user to click and select a pixel autoupdate : bool (default True) redraw automatically (otherwise need to call plt.draw()) autoscale : bool (default True) rescale the vmin/vmax values when the image changes. This is set to False if `set_limits_*` is called to explicity set data limits. antialiased : bool (default True) whether to draw in antialiased mode or not. Notes ----- Speed: CameraDisplay is not intended to be very fast (matplotlib is not a very speed performant graphics library, it is intended for nice output plots). However, most of the slowness of CameraDisplay is in the constructor. Once one is displayed, changing the image that is displayed is relatively fast and efficient. Therefore it is best to initialize an instance, and change the data, rather than generating new CameraDisplays. Pixel Implementation: Pixels are rendered as a `matplotlib.collections.PatchCollection` of Polygons (either 6 or 4 sided). You can access the PatchCollection directly (to e.g. change low-level style parameters) via `CameraDisplay.pixels` Output: Since CameraDisplay uses matplotlib, any display can be saved to any output file supported via plt.savefig(filename). This includes `.pdf` and `.png`. """ def __init__( self, geometry, image=None, ax=None, title=None, norm="lin", cmap=None, allow_pick=False, autoupdate=True, autoscale=True, antialiased=True, ): self.axes = ax if ax is not None else plt.gca() self.geom = geometry self.pixels = None self.colorbar = None self.autoupdate = autoupdate self.autoscale = autoscale self._active_pixel = None self._active_pixel_label = None if title is None: title = geometry.cam_id # initialize the plot and generate the pixels as a # RegularPolyCollection patches = [] if not hasattr(self.geom, "mask"): self.geom.mask = np.ones_like(self.geom.pix_x.value, dtype=bool) for xx, yy, aa in zip( u.Quantity(self.geom.pix_x[self.geom.mask]).value, u.Quantity(self.geom.pix_y[self.geom.mask]).value, u.Quantity(np.array(self.geom.pix_area)[self.geom.mask]).value): if self.geom.pix_type.startswith("hex"): rr = sqrt(aa * 2 / 3 / sqrt(3)) + 2*PIXEL_EPSILON poly = RegularPolygon( (xx, yy), 6, radius=rr, orientation=self.geom.pix_rotation.rad, fill=True, ) else: rr = sqrt(aa) + PIXEL_EPSILON poly = Rectangle( (xx-rr/2., yy-rr/2.), width=rr, height=rr, angle=self.geom.pix_rotation.deg, fill=True, ) patches.append(poly) self.pixels = PatchCollection(patches, cmap=cmap, linewidth=0) self.axes.add_collection(self.pixels) self.pixel_highlighting = copy.copy(self.pixels) self.pixel_highlighting.set_facecolor('none') self.pixel_highlighting.set_linewidth(0) self.axes.add_collection(self.pixel_highlighting) # Set up some nice plot defaults self.axes.set_aspect('equal', 'datalim') self.axes.set_title(title) self.axes.set_xlabel("X position ({})".format(self.geom.pix_x.unit)) self.axes.set_ylabel("Y position ({})".format(self.geom.pix_y.unit)) self.axes.autoscale_view() # set up a patch to display when a pixel is clicked (and # pixel_picker is enabled): self._active_pixel = copy.copy(patches[0]) self._active_pixel.set_facecolor('r') self._active_pixel.set_alpha(0.5) self._active_pixel.set_linewidth(2.0) self._active_pixel.set_visible(False) self.axes.add_patch(self._active_pixel) self._active_pixel_label = self.axes.text(self._active_pixel.xy[0], self._active_pixel.xy[1], "0", horizontalalignment='center', verticalalignment='center') self._active_pixel_label.set_visible(False) # enable ability to click on pixel and do something (can be # enabled on-the-fly later as well: if allow_pick: self.enable_pixel_picker() if image is not None: self.image = image else: self.image = np.zeros_like(self.geom.pix_id, dtype=np.float) self.norm = norm def highlight_pixels(self, pixels, color='g', linewidth=1, alpha=0.75): ''' Highlight the given pixels with a colored line around them Parameters ---------- pixels : index-like The pixels to highlight. Can either be a list or array of integers or a boolean mask of length number of pixels color: a matplotlib conform color the color for the pixel highlighting linewidth: float linewidth of the highlighting in points alpha: 0 <= alpha <= 1 The transparency ''' l = np.zeros_like(self.image) l[pixels] = linewidth self.pixel_highlighting.set_linewidth(l) self.pixel_highlighting.set_alpha(alpha) self.pixel_highlighting.set_edgecolor(color) self._update() def enable_pixel_picker(self): """ enable ability to click on pixels """ self.pixels.set_picker(True) # enable click self.pixels.set_pickradius(sqrt(u.Quantity(self.geom.pix_area[0]) .value) / np.pi) self.pixels.set_snap(True) # snap cursor to pixel center self.axes.figure.canvas.mpl_connect('pick_event', self._on_pick) def set_limits_minmax(self, zmin, zmax): """ set the color scale limits from min to max """ self.pixels.set_clim(zmin, zmax) self.autoscale = False self._update() def set_limits_percent(self, percent=95): """ auto-scale the color range to percent of maximum """ zmin = self.pixels.get_array().min() zmax = self.pixels.get_array().max() dz = zmax - zmin frac = percent / 100.0 self.autoscale = False self.set_limits_minmax(zmin, zmax - (1.0 - frac) * dz) @property def norm(self): ''' The norm instance of the Display Possible values: - "lin": linear scale - "log": log scale (cannot have negative values) - "symlog": symmetric log scale (negative values are ok) - any matplotlib.colors.Normalize instance, e. g. PowerNorm(gamma=-2) ''' return self.pixels.norm @norm.setter def norm(self, norm): if norm == 'lin': self.pixels.norm = Normalize() elif norm == 'log': self.pixels.norm = LogNorm() self.pixels.autoscale() # this is to handle matplotlib bug #5424 elif norm == 'symlog': self.pixels.norm = SymLogNorm(linthresh=1.0) self.pixels.autoscale() elif isinstance(norm, Normalize): self.pixels.norm = norm else: raise ValueError("Unsupported norm: '{}', options are 'lin'," "'log','symlog', or a matplotlib Normalize object" .format(norm)) self.update(force=True) self.pixels.autoscale() @property def cmap(self): """ Color map to use. Either a name or `matplotlib.colors.ColorMap` instance, e.g. from `matplotlib.pyplot.cm` """ return self.pixels.get_cmap() @cmap.setter def cmap(self, cmap): self.pixels.set_cmap(cmap) self._update() @property def image(self): """The image displayed on the camera (1D array of pixel values)""" return self.pixels.get_array() @image.setter def image(self, image): """ Change the image displayed on the Camera. Parameters ---------- image: array_like array of values corresponding to the pixels in the CameraGeometry. """ image = np.asanyarray(image) if image.shape != self.geom.pix_x.shape: raise ValueError( "Image has a different shape {} than the " "given CameraGeometry {}" .format(image.shape, self.geom.pix_x.shape) ) self.pixels.set_array(image[self.geom.mask]) self.pixels.changed() if self.autoscale: self.pixels.autoscale() self._update() def _update(self, force=False): """ signal a redraw if autoupdate is turned on """ if self.autoupdate: self.update(force) def update(self, force=False): """ redraw the display now """ self.axes.figure.canvas.draw() if self.colorbar is not None: if force is True: self.colorbar.update_bruteforce(self.pixels) else: self.colorbar.update_normal(self.pixels) self.colorbar.draw_all() def add_colorbar(self, **kwargs): """ add a colobar to the camera plot kwargs are passed to `figure.colorbar(self.pixels, **kwargs)` See matplotlib documentation for the supported kwargs: http://matplotlib.org/api/figure_api.html#matplotlib.figure.Figure.colorbar """ if self.colorbar is not None: raise ValueError( 'There is already a colorbar attached to this CameraDisplay' ) else: self.colorbar = self.axes.figure.colorbar(self.pixels, **kwargs) self.update() def add_ellipse(self, centroid, length, width, angle, asymmetry=0.0, **kwargs): """ plot an ellipse on top of the camera Parameters ---------- centroid: (float, float) position of centroid length: float major axis width: float minor axis angle: float rotation angle wrt x-axis about the centroid, anticlockwise, in radians asymmetry: float 3rd-order moment for directionality if known kwargs: any MatPlotLib style arguments to pass to the Ellipse patch """ ellipse = Ellipse(xy=centroid, width=length, height=width, angle=np.degrees(angle), fill=False, **kwargs) self.axes.add_patch(ellipse) self.update() return ellipse def overlay_moments(self, momparams, with_label=True, **kwargs): """helper to overlay ellipse from a `reco.MomentParameters` structure Parameters ---------- momparams: `reco.MomentParameters` structuring containing Hillas-style parameterization kwargs: key=value any style keywords to pass to matplotlib (e.g. color='red' or linewidth=6) """ # strip off any units cen_x = u.Quantity(momparams.cen_x).value cen_y = u.Quantity(momparams.cen_y).value length = u.Quantity(momparams.length).value width = u.Quantity(momparams.width).value el = self.add_ellipse(centroid=(cen_x, cen_y), length=length*2, width=width*2, angle=momparams.psi.rad, **kwargs) if with_label: self.axes.text(cen_x, cen_y, ("({:.02f},{:.02f})\n" "[w={:.02f},l={:.02f}]") .format(momparams.cen_x, momparams.cen_y, momparams.width, momparams.length), color=el.get_edgecolor()) def _on_pick(self, event): """ handler for when a pixel is clicked """ pix_id = event.ind[-1] xx, yy, aa = u.Quantity(self.geom.pix_x[pix_id]).value, \ u.Quantity(self.geom.pix_y[pix_id]).value, \ u.Quantity(np.array(self.geom.pix_area)[pix_id]) if self.geom.pix_type.startswith("hex"): self._active_pixel.xy = (xx, yy) else: rr = sqrt(aa) self._active_pixel.xy = (xx - rr / 2., yy - rr / 2.) self._active_pixel.set_visible(True) self._active_pixel_label.set_x(xx) self._active_pixel_label.set_y(yy) self._active_pixel_label.set_text("{:003d}".format(pix_id)) self._active_pixel_label.set_visible(True) self._update() self.on_pixel_clicked(pix_id) # call user-function def on_pixel_clicked(self, pix_id): """virtual function to overide in sub-classes to do something special when a pixel is clicked """ print("Clicked pixel_id {}".format(pix_id)) def show(self): self.axes.figure.show()
class CameraPlot(object): '''A Class for a camera pixel''' def __init__( self, telescope, ax, data=None, cmap='gray', vmin=None, vmax=None, ): ''' :telescope: the telescope class for the pixel :data: array-like with one value for each pixel :cmap: a matpixellib colormap string or instance :vmin: minimum value of the colormap :vmax: maximum value of the colormap ''' self.telescope = telescope if data is None: data = np.zeros(telescope.n_pixel) patches = [] if telescope.pixel_shape == 'hexagon': for xy in zip(telescope.pixel_x, telescope.pixel_y): patches.append( RegularPolygon( xy=xy, numVertices=6, radius=telescope.pixel_size, orientation=telescope.pixel_orientation, ) ) self.pixel = PatchCollection(patches) self.pixel.set_linewidth(0) self.pixel.set_cmap(cmap) self.pixel.set_array(data) self.pixel.set_clim(vmin, vmax) self.vmin = vmin self.vmax = vmax self.ax = ax self.ax.add_collection(self.pixel) self.ax.set_xlim( self.telescope.pixel_x.min() - 2 * self.telescope.pixel_size, self.telescope.pixel_x.max() + 2 * self.telescope.pixel_size, ) self.ax.set_ylim( self.telescope.pixel_y.min() - 2 * self.telescope.pixel_size, self.telescope.pixel_y.max() + 2 * self.telescope.pixel_size, ) @property def data(self): return self.pixel.get_array() @data.setter def data(self, data): self.pixel.set_array(data) if not self.vmin or not self.vmax: self.pixel.autoscale() self.pixel.changed()
class CameraImage(Plotter): def __init__(self, xpix, ypix, size, cmap=None, **kwargs): """ Create a camera-image plot Parameters ---------- xpix : ndarray The X positions of the pixels/superpixels/TMs ypix : ndarray The Y positions of the pixels/superpixels/TMs size : float The size of the pixels/superpixels/TMs kwargs Arguments passed to `CHECLabPy.plottong.setup.Plotter` """ super().__init__(**kwargs) rc = { "xtick.direction": 'out', "ytick.direction": 'out', } mpl.rcParams.update(rc) self._image = None self._mapping = None self.colorbar = None self.autoscale = True self.xpix = xpix self.ypix = ypix assert self.xpix.size == self.ypix.size self.n_pixels = self.xpix.size patches = [] for xx, yy in zip(self.xpix, self.ypix): rr = size + 0.0001 # extra size to pixels to avoid aliasing poly = Rectangle( (xx - rr / 2., yy - rr / 2.), width=rr, height=rr, fill=True, ) patches.append(poly) self.pixels = PatchCollection(patches, linewidth=0, cmap=cmap) self.ax.add_collection(self.pixels) self.pixels.set_array(np.zeros(self.n_pixels)) self.ax.set_aspect('equal', 'datalim') self.ax.set_xlabel("X position (m)") self.ax.set_ylabel("Y position (m)") self.ax.autoscale_view() self.ax.axis('off') @property def image(self): return self._image @image.setter def image(self, val): assert val.size == self.n_pixels self._image = val self.pixels.set_array(np.ma.masked_invalid(val)) self.pixels.changed() if self.autoscale: self.pixels.autoscale() # Updates the colorbar self.ax.figure.canvas.draw() def save(self, output_path, **kwargs): super().save(output_path, **kwargs) if output_path.endswith('.pdf'): try: self.crop(output_path) except ModuleNotFoundError: pass @staticmethod def crop(path): from PyPDF2 import PdfFileWriter, PdfFileReader with open(path, "rb") as in_f: input1 = PdfFileReader(in_f) output = PdfFileWriter() num_pages = input1.getNumPages() for i in range(num_pages): page = input1.getPage(i) page.cropBox.lowerLeft = (100, 20) page.cropBox.upperRight = (340, 220) output.addPage(page) pdf_path = os.path.splitext(path)[0] + "_cropped.pdf" with open(pdf_path, "wb") as out_f: output.write(out_f) print("Cropped figure saved to: {}".format(pdf_path)) def set_cmap(self, cmap="viridis"): self.pixels.set_cmap(cmap) def add_colorbar(self, label='', pad=-0.2, ax=None, **kwargs): if ax is None: ax = self.ax self.colorbar = self.ax.figure.colorbar(self.pixels, label=label, pad=pad, ax=ax, **kwargs) def set_limits_minmax(self, zmin, zmax): """ Set the color scale limits from min to max """ self.pixels.set_clim(zmin, zmax) self.autoscale = False def set_z_log(self): self.pixels.norm = LogNorm() self.pixels.autoscale() def reset_limits(self): """ Reset to auto color scale limits """ self.autoscale = True self.pixels.autoscale() def annotate_on_telescope_up(self): """ Add an arrow indicating where "ON-Telescope-UP" is """ if self._mapping is not None: axl = self._mapping.metadata['fOTUpX_l'] ayl = self._mapping.metadata['fOTUpY_l'] adx = self._mapping.metadata['fOTUpX_u'] - axl ady = self._mapping.metadata['fOTUpY_u'] - ayl text = "ON-Telescope UP" self.ax.arrow(axl, ayl, adx, ady, head_width=0.01, head_length=0.01, fc='r', ec='r') self.ax.text(axl, ayl, text, fontsize=4, color='r', ha='center', va='bottom') else: print("Cannot annotate, no mapping attached to class") def add_text_to_pixel(self, pixel, value, size=3, color='w', **kwargs): """ Add a text label to a single pixel Parameters ---------- pixel : int value : str or float size : int Font size color : str Color of the text kwargs Named arguments to pass to matplotlib.pyplot.text """ pos_x = self.xpix[pixel] pos_y = self.ypix[pixel] self.ax.text(pos_x, pos_y, value, fontsize=size, color=color, ha='center', **kwargs) def add_pixel_text(self, values, size=3, color='w', **kwargs): """ Add a text label to each pixel Parameters ---------- values : ndarray size : int Font size color : str Color of the text kwargs Named arguments to pass to matplotlib.pyplot.text """ assert values.size == self.n_pixels for pixel in range(self.n_pixels): self.add_text_to_pixel(pixel, values[pixel], size, color, **kwargs) def highlight_pixels(self, pixels, color='g', linewidth=0.5, alpha=0.75): """ Highlight the given pixels with a colored line around them Parameters ---------- pixels : index-like The pixels to highlight. Can either be a list or array of integers or a boolean mask of length number of pixels color: a matplotlib conform color the color for the pixel highlighting linewidth: float linewidth of the highlighting in points alpha: 0 <= alpha <= 1 The transparency """ lw_array = np.zeros_like(self.image) lw_array[pixels] = linewidth pixel_highlighting = copy(self.pixels) pixel_highlighting.set_facecolor('none') pixel_highlighting.set_linewidth(lw_array) pixel_highlighting.set_alpha(alpha) pixel_highlighting.set_edgecolor(color) self.ax.add_collection(pixel_highlighting) return pixel_highlighting def annotate_tm_edge_label(self): """ Annotate each of the TMs on the top and bottom of the camera """ if self._mapping is not None: kw = dict(fontsize=6, color='black', ha='center') m = self._mapping pix_size = self._mapping.metadata['size'] f_tm_top = lambda g: m.ix[m.ix[g.index]['row'].idxmax(), 'slot'] f_tm_bottom = lambda g: m.ix[m.ix[g.index]['row'].idxmin(), 'slot'] tm_top = np.unique(m.groupby('col').agg(f_tm_top)['slot']) tm_bottom = np.unique(m.groupby('col').agg(f_tm_bottom)['slot']) for tm in tm_top: df = m.loc[m['slot'] == tm] ypix = df['ypix'].max() + pix_size * 0.7 xpix = df['xpix'].mean() tm_txt = "TM{:02d}".format(tm) self.ax.text(xpix, ypix, tm_txt, va='bottom', **kw) for tm in tm_bottom: df = m.loc[m['slot'] == tm] ypix = df['ypix'].min() - pix_size * 0.7 xpix = df['xpix'].mean() tm_txt = "TM{:02d}".format(tm) self.ax.text(xpix, ypix, tm_txt, va='top', **kw) else: print("Cannot annotate, no mapping attached to class") def annotate_led_flasher(self): """ Annotate each of the LED flashers in the four corners of the camera """ if self._mapping is not None: pix_size = self._mapping.metadata['size'] axl = self._mapping.metadata['fOTUpX_l'] ayl = self._mapping.metadata['fOTUpY_l'] + 2 * pix_size dxl = [1, -1, 1, -1] dyl = [1, 1, -1, -1] for i, (dx, dy) in enumerate(zip(dxl, dyl)): x = axl * dx y = ayl * dy self.ax.add_patch(Circle((x, y), radius=0.01, color='red')) self.ax.text(x, y, f"{i}", fontsize=7, color='white', ha='center', va='center') else: print("Cannot annotate, no mapping attached to class") @classmethod def from_mapping(cls, mapping, **kwargs): """ Generate the class from a CHECLabPy mapping dataframe Parameters ---------- mapping : `pandas.DataFrame` The mapping for the pixels stored in a pandas DataFrame. Can be obtained from either of these options: CHECLabPy.io.TIOReader.mapping CHECLabPy.io.ReaderR0.mapping CHECLabPy.io.ReaderR1.mapping CHECLabPy.io.DL1Reader.mapping CHECLabPy.utils.mapping.get_clp_mapping_from_tc_mapping kwargs Arguments passed to `CHECLabPy.plottong.setup.Plotter` Returns ------- `CameraImage` """ xpix = mapping['xpix'].values ypix = mapping['ypix'].values size = mapping.metadata['size'] image = cls(xpix, ypix, size, **kwargs) image._mapping = mapping return image @classmethod def from_tc_mapping(cls, tc_mapping, **kwargs): """ Generate the class using the TargetCalib Mapping Class Parameters ---------- tc_mapping : `target_calib.Mapping` kwargs Arguments passed to `CHECLabPy.plottong.setup.Plotter` Returns ------- `CameraImage` """ mapping = get_clp_mapping_from_tc_mapping(tc_mapping) return cls.from_mapping(mapping, **kwargs) @classmethod def from_camera_version(cls, camera_version, single=False, **kwargs): """ Generate the class using the camera version (required TargetCalib) Parameters ---------- camera_version : str Version of the camera (e.g. "1.0.1" corresponds to CHEC-S) single : bool Designate if it is just a single module you wish to plot kwargs Arguments passed to `CHECLabPy.plottong.setup.Plotter` Returns ------- `CameraImage` """ from target_calib import CameraConfiguration config = CameraConfiguration(camera_version) tc_mapping = config.GetMapping(single) return cls.from_tc_mapping(tc_mapping, **kwargs)
class CameraImage(Plotter): def __init__(self, xpix, ypix, size, **kwargs): """ Create a camera-image plot Parameters ---------- xpix : ndarray The X positions of the pixels/superpixels/TMs ypix : ndarray The Y positions of the pixels/superpixels/TMs size : float The size of the pixels/superpixels/TMs kwargs Arguments passed to `CHECLabPy.plottong.setup.Plotter` """ super().__init__(**kwargs) self._image = None self._mapping = None self.colorbar = None self.autoscale = True self.xpix = xpix self.ypix = ypix assert self.xpix.size == self.ypix.size self.n_pixels = self.xpix.size patches = [] for xx, yy in zip(self.xpix, self.ypix): rr = size + 0.0001 # extra size to pixels to avoid aliasing poly = Rectangle( (xx - rr / 2., yy - rr / 2.), width=rr, height=rr, fill=True, ) patches.append(poly) self.pixels = PatchCollection(patches, linewidth=0) self.ax.add_collection(self.pixels) self.pixels.set_array(np.zeros(self.n_pixels)) self.ax.set_aspect('equal', 'datalim') self.ax.set_xlabel("X position (m)") self.ax.set_ylabel("Y position (m)") self.ax.autoscale_view() self.ax.axis('off') @staticmethod def figsize(scale=1.5): super(CameraPlotter, CameraPlotter).figsize(scale) @property def image(self): return self._image @image.setter def image(self, val): assert val.size == self.n_pixels self.pixels.set_array(val) self.pixels.changed() if self.autoscale: self.pixels.autoscale() # Updates the colorbar self.ax.figure.canvas.draw() def add_colorbar(self, label=''): self.colorbar = self.ax.figure.colorbar(self.pixels, label=label) def set_limits_minmax(self, zmin, zmax): """ Set the color scale limits from min to max """ self.pixels.set_clim(zmin, zmax) self.autoscale = False def reset_limits(self): """ Reset to auto color scale limits """ self.autoscale = True self.pixels.autoscale() def annotate_on_telescope_up(self): """ Add an arrow indicating where "ON-Telescope-UP" is """ if self._mapping is not None: axl = self._mapping.metadata['fOTUpX_l'] ayl = self._mapping.metadata['fOTUpY_l'] adx = self._mapping.metadata['fOTUpX_u'] - axl ady = self._mapping.metadata['fOTUpY_u'] - ayl text = "ON-Telescope UP" self.ax.arrow(axl, ayl, adx, ady, head_width=0.01, head_length=0.01, fc='r', ec='r') self.ax.text(axl, ayl, text, fontsize=4, color='r', ha='center', va='bottom') else: print("Cannot annotate, no mapping attached to class") def add_text_to_pixel(self, pixel, value, fmt=None, size=3): """ Add a text label to a single pixel Parameters ---------- pixel : int value : str or float fmt : str String/float formatting expression size : int Font size """ pos_x = self.xpix[pixel] pos_y = self.ypix[pixel] if fmt: val = fmt.format(value) self.ax.text(pos_x, pos_y, value, fontsize=size, color='w', ha='center') def add_pixel_text(self, values, fmt=None, size=3): """ Add a text label to each pixel Parameters ---------- values : ndarray fmt : str String/float formatting expression size : int Font size """ assert values.size == self.n_pixels for pixel in range(self.n_pixels): self.add_text_to_pixel(pixel, values[pixel], fmt, size) def annotate_tm_edge_label(self): """ Annotate each of the TMs on the top and bottom of the camera """ if self._mapping is not None: kw = dict(fontsize=6, color='black', ha='center') m = self._mapping pix_size = self._mapping.metadata['size'] f_tm_top = lambda g: m.ix[m.ix[g.index]['row'].idxmax(), 'slot'] f_tm_bottom = lambda g: m.ix[m.ix[g.index]['row'].idxmin(), 'slot'] tm_top = np.unique(m.groupby('col').agg(f_tm_top)['slot']) tm_bottom = np.unique(m.groupby('col').agg(f_tm_bottom)['slot']) for tm in tm_top: df = m.loc[m['slot'] == tm] ypix = df['ypix'].max() + pix_size * 0.7 xpix = df['xpix'].mean() tm_txt = "TM{:02d}".format(tm) self.ax.text(xpix, ypix, tm_txt, va='bottom', **kw) for tm in tm_bottom: df = m.loc[m['slot'] == tm] ypix = df['ypix'].min() - pix_size * 0.7 xpix = df['xpix'].mean() tm_txt = "TM{:02d}".format(tm) self.ax.text(xpix, ypix, tm_txt, va='top', **kw) else: print("Cannot annotate, no mapping attached to class") @classmethod def from_mapping(cls, mapping, **kwargs): """ Generate the class from a CHECLabPy mapping dataframe Parameters ---------- mapping : `pandas.DataFrame` The mapping for the pixels stored in a pandas DataFrame. Can be obtained from either of these options: CHECLabPy.io.TIOReader.mapping CHECLabPy.io.ReaderR0.mapping CHECLabPy.io.ReaderR1.mapping CHECLabPy.io.DL1Reader.mapping CHECLabPy.utils.mapping.get_clp_mapping_from_tc_mapping kwargs Arguments passed to `CHECLabPy.plottong.setup.Plotter` Returns ------- `CameraImage` """ xpix = mapping['xpix'].values ypix = mapping['ypix'].values size = mapping.metadata['size'] image = cls(xpix, ypix, size, **kwargs) image._mapping = mapping return image @classmethod def from_tc_mapping(cls, tc_mapping, **kwargs): """ Generate the class using the TargetCalib Mapping Class Parameters ---------- tc_mapping : `target_calib.Mapping` kwargs Arguments passed to `CHECLabPy.plottong.setup.Plotter` Returns ------- `CameraImage` """ mapping = get_clp_mapping_from_tc_mapping(tc_mapping) return cls.from_mapping(mapping, **kwargs) @classmethod def from_camera_version(cls, camera_version, single=False, **kwargs): """ Generate the class using the camera version (required TargetCalib) Parameters ---------- camera_version : str Version of the camera (e.g. "1.0.1" corresponds to CHEC-S) single : bool Designate if it is just a single module you wish to plot kwargs Arguments passed to `CHECLabPy.plottong.setup.Plotter` Returns ------- `CameraImage` """ from target_calib import CameraConfiguration config = CameraConfiguration(camera_version) tc_mapping = config.GetMapping(single) return cls.from_tc_mapping(tc_mapping, **kwargs)