ctapipe is not stable yet, so expect large and rapid changes to structure and functionality as we explore various design choices before the 1.0 release.

Displaying Camera Images#

 7 import astropy.coordinates as c
 8 import astropy.units as u
 9 import matplotlib.pyplot as plt
10 import numpy as np
11 from matplotlib.animation import FuncAnimation
12 from matplotlib.colors import PowerNorm
13
14 from ctapipe.coordinates import CameraFrame, EngineeringCameraFrame, TelescopeFrame
15 from ctapipe.image import hillas_parameters, tailcuts_clean, toymodel
16 from ctapipe.instrument import SubarrayDescription
17 from ctapipe.io import EventSource
18 from ctapipe.visualization import CameraDisplay

First, let’s create a fake Cherenkov image from a given CameraGeometry and fill it with some data that we can draw later.

25 # load an example camera geometry from a simulation file
26 subarray = SubarrayDescription.read("dataset://gamma_prod5.simtel.zst")
27 geom = subarray.tel[100].camera.geometry
28
29 # create a fake camera image to display:
30 model = toymodel.Gaussian(
31     x=0.2 * u.m,
32     y=0.0 * u.m,
33     width=0.05 * u.m,
34     length=0.15 * u.m,
35     psi="35d",
36 )
37
38 image, sig, bg = model.generate_image(geom, intensity=1500, nsb_level_pe=10)
39 mask = tailcuts_clean(geom, image, picture_thresh=15, boundary_thresh=5)
42 geom
CameraGeometry(name='NectarCam', pix_type=PixelShape.HEXAGON, npix=1855, cam_rot=0.000 deg, pix_rot=40.893 deg, frame=<CameraFrame Frame (focal_length=16.445049285888672 m, rotation=0.0 rad, telescope_pointing=None, obstime=None, location=None)>)

Displaying Images#

The simplest plot is just to generate a CameraDisplay with an image in its constructor. A figure and axis will be created automatically

53 CameraDisplay(geom)
NectarCam
<ctapipe.visualization.mpl_camera.CameraDisplay object at 0x7f746aaf8280>

You can also specify the initial image, cmap and norm (colomap and normalization, see below), title to use. You can specify ax if you want to draw the camera on an existing matplotlib Axes object (otherwise one is created).

To change other options, or to change options dynamically, you can call the relevant functions of the CameraDisplay object that is returned. For example to add a color bar, call add_colorbar(), or to change the color scale, modify the cmap or norm properties directly.

Choosing a coordinate frame#

The CameraGeometry object contains a ctapipe.coordinates.Frame used by CameraDisplay to draw the camera in the correct orientation and distance units. The default frame is the CameraFrame, which will display the camera in units of meters and with an orientation that the top of the camera (when parked) is aligned to the X-axis. To show the camera in another orientation, it’s useful to apply a coordinate transform to the CameraGeometry before passing it to the CameraDisplay. The following Frames are supported:

  • CameraFrame: The frame used by SimTelArray, with the top of the camera on the x-axis

  • EngineeringCameraFrame: similar to CameraFrame, but with the top of the camera aligned to the Y axis

  • TelescopeFrame: In degrees (on the sky) coordinates relative to the telescope Alt/Az pointing position, with the Alt axis pointing upward.

Note the the name of the Frame appears in the lower-right corner

93 fig, ax = plt.subplots(1, 3, figsize=(15, 4))
94 CameraDisplay(geom, image=image, ax=ax[0])
95 CameraDisplay(geom.transform_to(EngineeringCameraFrame()), image=image, ax=ax[1])
96 CameraDisplay(geom.transform_to(TelescopeFrame()), image=image, ax=ax[2])
NectarCam, NectarCam, NectarCam
<ctapipe.visualization.mpl_camera.CameraDisplay object at 0x7f747a5acb80>

For the rest of this demo, let’s use the TelescopeFrame

103 geom_camframe = geom
104 geom = geom_camframe.transform_to(EngineeringCameraFrame())

Changing the color map and scale#

CameraDisplay supports any matplotlib color map It is highly recommended to use a perceptually uniform map, unless you have a good reason not to.

120 fig, ax = plt.subplots(1, 3, figsize=(15, 4))
121 for ii, cmap in enumerate(["PuOr_r", "rainbow", "twilight"]):
122     disp = CameraDisplay(geom, image=image, ax=ax[ii], title=cmap)
123     disp.add_colorbar()
124     disp.cmap = cmap
PuOr_r, rainbow, twilight

By default the minimum and maximum of the color bar are set automatically by the data in the image. To choose fixed limits, use:`

132 fig, ax = plt.subplots(1, 3, figsize=(15, 4))
133 for ii, minmax in enumerate([(10, 50), (-10, 10), (1, 100)]):
134     disp = CameraDisplay(geom, image=image, ax=ax[ii], title=minmax)
135     disp.add_colorbar()
136     disp.set_limits_minmax(minmax[0], minmax[1])
(10, 50), (-10, 10), (1, 100)

Or you can set the maximum limit by percentile of the charge distribution:

144 fig, ax = plt.subplots(1, 3, figsize=(15, 4))
145 for ii, pct in enumerate([30, 50, 90]):
146     disp = CameraDisplay(geom, image=image, ax=ax[ii], title=f"{pct} %")
147     disp.add_colorbar()
148     disp.set_limits_percent(pct)
30 %, 50 %, 90 %

Using different normalizations#

You can choose from several preset normalizations (lin, log, symlog) and also provide a custom normalization, for example a PowerNorm:

163 fig, axes = plt.subplots(2, 2, figsize=(14, 10))
164 norms = ["lin", "log", "symlog", PowerNorm(0.5)]
165
166 for norm, ax in zip(norms, axes.flatten()):
167     disp = CameraDisplay(geom, image=image, ax=ax)
168     disp.norm = norm
169     disp.add_colorbar()
170     ax.set_title(str(norm))
171
172 axes[1, 1].set_title("PowerNorm(0.5)")
173 plt.show()
lin, log, symlog, PowerNorm(0.5)

Overlays#

Marking pixels#

here we will mark pixels in the image mask. That will change their outline color

190 fig, ax = plt.subplots(1, 2, figsize=(10, 4))
191 disp = CameraDisplay(
192     geom, image=image, cmap="gray", ax=ax[0], title="Image mask in green"
193 )
194 disp.highlight_pixels(mask, alpha=0.8, linewidth=2, color="green")
195
196 disp = CameraDisplay(
197     geom, image=image, cmap="gray", ax=ax[1], title="Image mask in green (zoom)"
198 )
199 disp.highlight_pixels(mask, alpha=1, linewidth=3, color="green")
200
201 ax[1].set_ylim(-0.5, 0.5)
202 ax[1].set_xlim(-0.5, 0.5)
Image mask in green, Image mask in green (zoom)
(-0.5, 0.5)

Drawing a Hillas-parameter ellipse#

For this, we will first compute some Hillas Parameters in the current frame:

216 clean_image = image.copy()
217 clean_image[~mask] = 0
218 hillas = hillas_parameters(geom, clean_image)
219
220 plt.figure(figsize=(6, 6))
221 disp = CameraDisplay(geom, image=image, cmap="gray_r")
222 disp.highlight_pixels(mask, alpha=0.5, color="dodgerblue")
223 disp.overlay_moments(hillas, color="red", linewidth=3, with_label=False)
NectarCam

Drawing a marker at a coordinate#

This depends on the coordinate frame of the CameraGeometry. Here we will specify the coordinate the EngineerngCameraFrame, but if you have enough information to do the coordinate transform, you could use ICRS coordinates and overlay star positions. CameraDisplay will convert the coordinate you pass in to the Frame of the display automatically (if sufficient frame attributes are set).

Note that the parameter keep_old is False by default, meaning adding a new point will clear the previous ones (useful for animations, but perhaps unexpected for a static plot). Set it to True to plot multiple markers.

243 plt.figure(figsize=(6, 6))
244 disp = CameraDisplay(geom, image=image, cmap="gray_r")
245
246 coord = c.SkyCoord(x=0.5 * u.m, y=0.7 * u.m, frame=geom.frame)
247 coord_in_another_frame = c.SkyCoord(x=0.5 * u.m, y=0.7 * u.m, frame=CameraFrame())
248 disp.overlay_coordinate(coord, markersize=20, marker="*")
249 disp.overlay_coordinate(
250     coord_in_another_frame, markersize=20, marker="*", keep_old=True
251 )
NectarCam

Generating an animation#

Here we will make an animation of fake events by re-using a single display (much faster than generating a new one each time)

263 subarray = SubarrayDescription.read("dataset://gamma_prod5.simtel.zst")
264 geom = subarray.tel[1].camera.geometry
265
266 fov = 1.0
267 maxwid = 0.05
268 maxlen = 0.1
269
270 fig, ax = plt.subplots(1, 1, figsize=(8, 6))
271 disp = CameraDisplay(geom, ax=ax)  # we only need one display (it can be re-used)
272 disp.cmap = "inferno"
273 disp.add_colorbar(ax=ax)
274
275
276 def update(frame):
277     """this function will be called for each frame of the animation"""
278     x, y = np.random.uniform(-fov, fov, size=2)
279     width = np.random.uniform(0.01, maxwid)
280     length = np.random.uniform(width, maxlen)
281     angle = np.random.uniform(0, 180)
282     intens = width * length * (5e4 + 1e5 * np.random.exponential(2))
283
284     model = toymodel.Gaussian(
285         x=x * u.m,
286         y=y * u.m,
287         width=width * u.m,
288         length=length * u.m,
289         psi=angle * u.deg,
290     )
291     image, _, _ = model.generate_image(
292         geom,
293         intensity=intens,
294         nsb_level_pe=5,
295     )
296     disp.image = image
297
298
299 # Create the animation and convert to a displayable video:
300 anim = FuncAnimation(fig, func=update, frames=10, interval=200)
301 plt.show()

Using CameraDisplays interactively#

CameraDisplays can be used interactively when displayed in a window, and also when using Jupyter notebooks/lab with appropriate backends.

When this is the case, the same CameraDisplay object can be re-used. We can’t show this here in the documentation, but creating an animation when in a matplotlib window is quite easy! Try this in an interactive ipython session:

Running interactive displays in a matplotlib window#

ipython -i –maplotlib=auto

That will open an ipython session with matplotlib graphics in a separate thread, meaning that you can type code and interact with plots simultaneneously.

In the ipython session try running the following code and you will see an animation (here in the documentation, it will of course be static)

First we load some real data so we have a nice image to view:

337 DATA = "dataset://gamma_20deg_0deg_run1___cta-prod5-lapalma_desert-2158m-LaPalma-dark_100evts.simtel.zst"
338
339 with EventSource(
340     DATA,
341     max_events=1,
342     focal_length_choice="EQUIVALENT",
343 ) as source:
344     event = next(iter(source))
345
346 tel_id = list(event.r0.tel.keys())[0]
347 geom = source.subarray.tel[tel_id].camera.geometry
348 waveform = event.r0.tel[tel_id].waveform
349 n_chan, n_pix, n_samp = waveform.shape

Running the following the will bring up a window and animate the shower image as a function of time.

357 disp = CameraDisplay(geom)
358
359 for ii in range(n_samp):
360     disp.image = waveform[0, :, ii]
361     plt.pause(0.1)  # this lets matplotlib re-draw the scene
NectarCam

The output will be similar to the static animation created as follows:

368 fig, ax = plt.subplots(1, 1)
369 disp = CameraDisplay(geom, ax=ax)
370 disp.add_colorbar()
371 disp.autoscale = False
372
373
374 def draw_sample(frame):
375     ax.set_title(f"sample: {frame}")
376     disp.set_limits_minmax(200, 400)
377     disp.image = waveform[0, :, frame]
378
379
380 anim = FuncAnimation(fig, func=draw_sample, frames=n_samp, interval=100)
381 plt.show()

Making it clickable#

Also when running in a window, you can enable the disp.enable_pixel_picker() option. This will then allow the user to click a pixel and a function will run. By default the function simply prints the pixel and value to stdout, however you can override the function on_pixel_clicked(pix_id) to do anything you want by making a subclass

396 class MyCameraDisplay(CameraDisplay):
397     def on_pixel_clicked(self, pix_id):
398         print(f"{pix_id=} has value {self.image[pix_id]:.2f}")
399
400
401 disp = MyCameraDisplay(geom, image=image)
402 disp.enable_pixel_picker()
NectarCam

then, when a user clicks a pixel it would print:

pixel 5 has value 2.44

Total running time of the script: (1 minutes 15.828 seconds)

Gallery generated by Sphinx-Gallery