Repository: sunpy/sunpy

Issue:
Mask out occulting disk in coronagraph images
<!-- This comments are hidden when you submit the issue so you do not need to remove them!
Please be sure to check out our contributing guidelines: https://github.com/sunpy/sunpy/blob/master/CONTRIBUTING.rst
Please be sure to check out our code of conduct:
https://github.com/sunpy/sunpy/blob/master/CODE_OF_CONDUCT.rst -->

<!-- Please have a search on our GitHub repository to see if a similar issue has already been posted.
If a similar issue is closed, have a quick look to see if you are satisfied by the resolution.
If not please go ahead and open an issue! -->

### Description
<!-- Provide a general description of the feature you would like. -->
<!-- "If you want to, you can suggest a draft design or API. -->
<!-- This way we have a deeper discussion on the feature. -->
It could be useful to provide the size of the occulting disk in coronagraph imagery and mask out the pixels that are inside that disk since they contain no useful data.

### Additional context
<!-- Add any other context or screenshots about the feature request here. -->
<!-- This part is optional. -->
This would enable checks of wether a pixel is actual measured coronal emission or not.



Discussion:
*points at maputils*
Where would you want to store the coronagraph occulter radius? Add another
property to the map via the LASCO source file?

On Thu, May 9, 2019 at 10:00 AM Stuart Mumford <notifications@github.com>
wrote:

> *points at maputils*
>
> —
> You are receiving this because you are subscribed to this thread.
> Reply to this email directly, view it on GitHub
> <https://github.com/sunpy/sunpy/issues/3098#issuecomment-490917184>, or mute
> the thread
> <https://github.com/notifications/unsubscribe-auth/AAHQEFYEP7HWF3G6FZR5GODPUQU77ANCNFSM4HL2IXXQ>
> .
>

We could add the radius information to the meta data on creation but really I was thinking that we would hand off a masked array.
If we only we had a CoronagraphMap class...
no, bad Steve.
The LASCO mask should also include the pylon. Having that mask in the LASCO map object would be very useful.

Sent from my iPhone

> On May 10, 2019, at 7:12 AM, Stuart Mumford <notifications@github.com> wrote:
> 
> no, bad Steve.
> 
> —
> You are receiving this because you commented.
> Reply to this email directly, view it on GitHub, or mute the thread.

The LASCO mask is already in SSWIDL. We use it in helioviewer. So we could keep it in SunPy. Or we could try to parameterize it and generate it instead of storing it.

Sent from my iPhone

> On May 10, 2019, at 8:34 AM, jackireland@gmail.com wrote:
> 
> The LASCO mask should also include the pylon. Having that mask in the LASCO map object would be very useful.
> 
> Sent from my iPhone
> 
>> On May 10, 2019, at 7:12 AM, Stuart Mumford <notifications@github.com> wrote:
>> 
>> no, bad Steve.
>> 
>> —
>> You are receiving this because you commented.
>> Reply to this email directly, view it on GitHub, or mute the thread.

Sounds like an excellent use for #1939. I would say that this function should be in `sunpy.instr.lasco` tbh.
On the topic of #1939 could you elaborate on where this mask is, how we can download it and what we should do with it? So that @vn-ki can consider doing it as part of his GSOC project?
The masks are here

https://hesperia.gsfc.nasa.gov/ssw/soho/lasco/idl/data/calib/

The mask you want is probably this one

c3clearmask2a.fts
<https://hesperia.gsfc.nasa.gov/ssw/soho/lasco/idl/data/calib/c3clearmask2a.fts>

I say probably because the mask processing algorithm is rather opaque.
At least this is a good place to start.

Jack






On Fri, May 10, 2019 at 9:12 AM Stuart Mumford <notifications@github.com>
wrote:

> On the topic of #1939 <https://github.com/sunpy/sunpy/issues/1939> could
> you elaborate on where this mask is, how we can download it and what we
> should do with it? So that @vn-ki <https://github.com/vn-ki> can consider
> doing it as part of his GSOC project?
>
> —
> You are receiving this because you commented.
> Reply to this email directly, view it on GitHub
> <https://github.com/sunpy/sunpy/issues/3098#issuecomment-491283764>, or mute
> the thread
> <https://github.com/notifications/unsubscribe-auth/AAHQEF62Y7LLJQWEQHTLZG3PUVYEHANCNFSM4HL2IXXQ>
> .
>

The center of the occulting disk of a coronagraph is provided (or can be calculated) from the information available in the fits header (at least for STEREO COR and SoHO LASCO [e.g. see here the CRPIXi keyword](https://hesperia.gsfc.nasa.gov/ssw/stereo/secchi/doc/FITS_keywords.pdf)). Therefore, having the center and also the radius of the occulter (which can be determined manually or from the [calibration data](https://hesperia.gsfc.nasa.gov/ssw/soho/lasco/idl/data/calib/) ) a mask can be produced for each case. I attach a sample script:
 
[occulters_test.zip](https://github.com/sunpy/sunpy/files/6994251/occulters_test.zip)

From a communication with @dstansby:

Q1. Is it not possible to assume that the center of the occulter is always the center of the image (ie. the pixel coordinate of the center is is m.data.shape / 2)?
A1: The center of the occulter is where the CRPIXi points at. If the image has been recentered with respect to this point then this can be the case, else it is not possible to assume this.

Q2. Are the width of the occulters (in pixels) always constant?
A2: Yes, I think the radius of the occulters is always constant (Lets think of it a bit more).


Resolving patch:
diff --git a/examples/map/lasco_mask.py b/examples/map/lasco_mask.py
new file mode 100644
--- /dev/null
+++ b/examples/map/lasco_mask.py
@@ -0,0 +1,70 @@
+"""
+=================================
+Creating a mask for LASCO C2 data
+=================================
+
+In this example, we will manually create a mask to block the occulter in an
+unprocessed LASCO C2 coronagraph image.
+"""
+import matplotlib.pyplot as plt
+import numpy as np
+
+import astropy.units as u
+from astropy.coordinates import SkyCoord
+
+from sunpy.map import Map
+from sunpy.map.maputils import all_coordinates_from_map
+from sunpy.net import Fido
+from sunpy.net import attrs as a
+
+###############################################################################
+# First, download some unprocessed LASCO C2 data with `~sunpy.net.Fido`.
+
+result = Fido.search(a.Time('2011/06/07 06:30', '2011/06/07 06:36'),
+                     a.Instrument.lasco,
+                     a.Detector.c2)
+lasco_file = Fido.fetch(result)
+lasco_map = Map(lasco_file)
+
+###############################################################################
+# The LASCO C2 coronagraph has a field of view extending from 2-6 solar
+# radii. So, our mask will have two parts: an inner component which masks the
+# occulter and an outer component which masks data outside the field of view.
+#
+# We will follow a process similar to
+# :ref:`sphx_glr_generated_gallery_computer_vision_techniques_finding_masking_bright_pixels.py`
+# to express the coordinates relative to the occulter center.
+
+pixel_coords = all_coordinates_from_map(lasco_map)
+solar_center = SkyCoord(0*u.deg, 0*u.deg, frame=lasco_map.coordinate_frame)
+pixel_radii = np.sqrt((pixel_coords.Tx-solar_center.Tx)**2 +
+                      (pixel_coords.Ty-solar_center.Ty)**2)
+# Note that the inner mask extends just beyond 2 solar radii to mask the
+# Fresnel diffraction caused by the occulter edge.
+mask_inner = pixel_radii < lasco_map.rsun_obs*2.4
+mask_outer = pixel_radii > lasco_map.rsun_obs*6
+final_mask = mask_inner + mask_outer
+
+###############################################################################
+# To apply the final mask, we must create a new map.
+
+masked_lasco = Map(lasco_map.data, lasco_map.meta, mask=final_mask)
+
+# Before plotting the map, we need to create a new colormap to ensure we mask
+# the bad values correctly.
+occult_colormap = lasco_map.cmap.copy()
+occult_colormap.set_bad('black')
+
+fig = plt.figure()
+ax1 = fig.add_subplot(1, 2, 1, projection=lasco_map)
+ax2 = fig.add_subplot(1, 2, 2, projection=masked_lasco)
+
+lasco_map.plot(clip_interval=(2, 98)*u.percent, cmap=occult_colormap, axes=ax1)
+lasco_map.draw_limb()
+ax1.set_title("Level 1 LASCO C2")
+
+masked_lasco.plot(clip_interval=(2, 98)*u.percent, cmap=occult_colormap, axes=ax2)
+masked_lasco.draw_limb()
+ax2.set_title("Masked LASCO C2")
+
+plt.show()
diff --git a/examples/plotting/lasco_overlay.py b/examples/plotting/lasco_overlay.py
new file mode 100644
--- /dev/null
+++ b/examples/plotting/lasco_overlay.py
@@ -0,0 +1,69 @@
+"""
+==============================================
+Overlay an AIA image on a LASCO C2 coronagraph
+==============================================
+
+This example shows the steps needed to overlay the disc and off-limb
+components of an AIA image within the masked occulter of a LASCO C2 image.
+"""
+from datetime import datetime
+
+import hvpy
+import matplotlib.pyplot as plt
+from hvpy.datasource import DataSource
+
+import astropy.units as u
+from astropy.coordinates import SkyCoord
+
+import sunpy.data.sample
+from sunpy.coordinates import Helioprojective
+from sunpy.map import Map
+from sunpy.util.config import get_and_create_download_dir
+
+###############################################################################
+# First, we will acquire a calibrated LASCO C2 image from Helioviewer and
+# create a map. hvpy uses the standard datetime instead of astropy.time.
+
+lasco_jp2_file = hvpy.save_file(hvpy.getJP2Image(datetime(2011, 6, 7, 6, 34),
+                                                 DataSource.LASCO_C2.value),
+                                filename=get_and_create_download_dir() + "/LASCO_C2.jp2", overwrite=True)
+lasco_map = Map(lasco_jp2_file)
+aia_map = Map(sunpy.data.sample.AIA_171_IMAGE)
+
+###############################################################################
+# In order to plot off-limb features of the AIA image.
+#
+# Note that off-disk AIA data are not retained by default because an
+# additional assumption is required to define the location of the AIA
+# emission in 3D space. We can use
+# :meth:`~sunpy.coordinates.Helioprojective.assume_spherical_screen` to
+# retain the off-disk AIA data. See
+# :ref:`sphx_glr_generated_gallery_map_transformations_reprojection_spherical_screen.py`
+# for more reference.
+
+projected_coord = SkyCoord(0*u.arcsec, 0*u.arcsec,
+                           obstime=lasco_map.observer_coordinate.obstime,
+                           frame='helioprojective',
+                           observer=lasco_map.observer_coordinate,
+                           rsun=aia_map.coordinate_frame.rsun)
+projected_header = sunpy.map.make_fitswcs_header(aia_map.data.shape,
+                                                 projected_coord,
+                                                 scale=u.Quantity(aia_map.scale),
+                                                 instrument=aia_map.instrument,
+                                                 wavelength=aia_map.wavelength)
+# We use `assume_spherical_screen` to ensure that the off limb AIA pixels are reprojected
+# otherwise it will only be the on disk pixels that are reprojected.
+with Helioprojective.assume_spherical_screen(aia_map.observer_coordinate):
+    aia_reprojected = aia_map.reproject_to(projected_header)
+
+###############################################################################
+# Finally, we plot the images by layering the AIA image on top of the LASCO C2
+# image.
+
+fig = plt.figure()
+ax = fig.add_subplot(projection=lasco_map)
+lasco_map.plot(axes=ax)
+aia_reprojected.plot(axes=ax, clip_interval=(1, 99.9)*u.percent, autoalign=True)
+ax.set_title("AIA and LASCO C2 Overlay")
+
+plt.show()


Test patch: