Note
Go to the end to download the full example code.
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)

<ctapipe.visualization.mpl_camera.CameraDisplay object at 0x79f08b05d390>
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])

<ctapipe.visualization.mpl_camera.CameraDisplay object at 0x79f096d89390>
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.

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])

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)

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()

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)

(-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)

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 )

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

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()