Repository: sunpy/sunpy

Issue:
Helioviewer SOHO/LASCO coronagraph images: CROTA2 header correct?
<!--
We know asking good questions takes effort, and we appreciate your time.
Thank you.

Please be aware that everyone has to follow our code of conduct:
https://github.com/sunpy/sunpy/blob/master/CODE_OF_CONDUCT.rst

These comments are hidden when you submit this github issue.

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

When looking at the FITS headers of the JPEG2000 files supplied by the Helioviewer API for SOHO/LASCO C3 and C2, I noticed that the CROTA2 header is set to non-zero values. If I understood correctly, this header contains the rotation of the image in degrees relative to solar north.

SunPy [uses this information to assign a rotation matrix to the Map](https://github.com/sunpy/sunpy/blob/master/sunpy/map/mapbase.py#L977-L989), so it is important that this is correct when e.g. plotting other features into these images.

But when looking at the Helioviewer images themselves, it looks like they are already rotated to the correct orientation, as e.g. the occulter arm in C3 is not always on the same side of the image (in contrast to unprocessed images from VSO, which have the same value in their CROTA2 header).

So, could it be that SunPy actually needs to ignore the CROTA2 header for Helioviewer images and set the rotation to zero? Or is it an issue on the Helioviewer side that this header is set in the first place?

### Steps to Reproduce
<!--
Please include code that reproduces the issue whenever possible.
The best reproductions are self-contained scripts with minimal dependencies.
-->

```python
from sunpy.io import read_file
from sunpy.map import Map
from sunpy.net import helioviewer
from sunpy.net import Fido
from sunpy.net import attrs as a
import datetime as dt
import matplotlib.pyplot as plt

date = dt.datetime(2013, 5, 13, 16, 54)

fig = plt.figure()

# Helioviewer image
hv = helioviewer.HelioviewerClient()
file = hv.download_jp2(date, observatory='SOHO', instrument='LASCO', detector='C3')
data, header = read_file(file)[0]

print(header['CROTA2'])  # -> -173.555

map = Map(data, header)

ax1 = fig.add_subplot(1, 2, 1, projection=map)
map.plot(axes=ax1)

# VSO image
result = Fido.search(a.Time(date, date + dt.timedelta(minutes=10)), a.Instrument.lasco, a.Detector.c3)
downloaded_files = Fido.fetch(result)
 
data, header = read_file(downloaded_files[0])[0]

print(header['CROTA2'])  # -> -173.554

map2 = sunpy.map.Map(data, header)
ax2 = fig.add_subplot(1, 2, 2, projection=map2)
map2.plot(axes=ax2)

plt.show()
```

![Figure_3](https://user-images.githubusercontent.com/5310424/95572737-af3f5680-0a2a-11eb-8362-d8df2187250c.png)

The two images shown here are from the same date, time and instrument. In the plot, they appear to be in the same coordinate system, but they are obviously rotated differently.

### System Details
<!--
We need to know the the package version you are using.
We provide a short function in sunpy (``sunpy.util.system_info()``) that will provide most of the below information.
-->
- SunPy Version: 2.0.3
- Astropy Version: 4.0.1.post1
- Python Version: 3.7.7
- OS information: Windows 10



Discussion:
The SOHO spacecraft rolls every 3 months in order to keep its high gain antenna visible by the Earth.  The orientation of SOHO should be recorded in the LASCO FITS header.

Helioviewer JPEG2000 images were originally designed to enable the browsing and visualization of solar image data online.  LASCO JPEG2000 images are created using an adapted version of the LASCO data processing pipeline to save a JPEG2000 file from LASCO quicklook data.  That image processing pipeline applies a rotation so that solar north is consistent over the mission (i.e. solar north is "up").  A rotated JPEG2000 image is stored because rotating images on-the-fly for visualization is relatively expensive computationally - it is simpler to rotate the image once and make that one available.  The JPEG2000-derived image above has that rotation applied.  The FITS-derived image above does not.

The JPEG2000 format allows for metadata to be stored inside an XML file.  Helioviewer JPEG2000 files use this capability to store metadata.  At the time that we were defining the Helioviewer JPEG2000 metadata information we thought that users would be interested in seeing the original FITS header information, so we attempted to pass that on via the JPEG2000 metadata.  Even so, I wouldn't be surprised that a comparison of the original FITS header information and what is stored in the JPEG2000 FITS metadata tag would show some differences.  Boiling this down, I would say that the fundamental issue is that use of JPEG2000 images outside of Helioviewer clients was not considered at the time.  We built for the needs of Helioviewer at the time.

A couple of notes about about the LASCO JPEG2000 images. The JPEG2000 LASCO images are made using quicklook data.  This necessarily means we use an estimated background subtraction model, i.e., the best background subtraction model that is available when the quicklook data is available.  Better backgrounds become available later.  So if you want the best quality LASCO images, you have to make them when the best background data is available.  Also, the default color table used in SunPy is not the same as the one used by the LASCO team.  The one used by the LASCO team is highly compressed compared to the one used by SunPy.  Helioviewer.org uses the colour table provided by the LASCO team.  This was done because the images of the data provided by the LASCO team are what the scientific community is now used to.

Finally, we (Helioviewer) have an outstanding task to go back and re-process the entire LASCO JPEG2000 archive to provide better quality images.  It's on the to-do list.  Doing this presents a good opportunity to take a look at what metadata should be in a newer Helioviewer JPEG2000 file.  What applications could we support going forward, and what are their metadata requirements?  Perhaps we should be treating the Helioviewer JPEG2000 files more like science data and less like simple images of data.
> At the time that we were defining the Helioviewer JPEG2000 metadata information we thought that users would be interested in seeing the original FITS header information, so we attempted to pass that on via the JPEG2000 metadata.

The problem for SunPy, of course, is that a user calling `.rotate()` on such a Map will be making use of metadata that no longer applies.  In fact, I did exactly that in [this post](https://github.com/sunpy/sunpy/pull/4003#issuecomment-611033912), which is why my Maps are wonkily rotated.

The fix in SunPy should be that our `LASCOMap` class performs some cleanup on the keywords if the data/header comes from a Helioviewer JP2.  Something like (with logging a la sunpy/sunpy-SEP#50):
```python
if self.meta['helioviewer']:
    log.debug("LASCOMap: Cleaning up CROTAn keywords because the map has already been rotated by Helioviewer")
    self.meta.pop('crota')
    self.meta.pop('crota1')
    self.meta['crota2'] = 0
```
@wafels Thank you for the detailed explanation! 

@ayshih Indeed, that looks like the simplest fix for now, until the metadata provided by Helioviewer is adjusted with new JPEG2000 files. I can take a look at that and create a PR (over the weekend or maybe early next week).

Resolving patch:
diff --git a/sunpy/map/sources/soho.py b/sunpy/map/sources/soho.py
--- a/sunpy/map/sources/soho.py
+++ b/sunpy/map/sources/soho.py
@@ -8,6 +8,7 @@
 from astropy.visualization import PowerStretch
 from astropy.visualization.mpl_normalize import ImageNormalize
 
+from sunpy import log
 from sunpy.map import GenericMap
 from sunpy.map.sources.source_type import source_stretch
 from sunpy.time import parse_time
@@ -123,6 +124,17 @@ def __init__(self, data, header, **kwargs):
         self.plot_settings['norm'] = ImageNormalize(
             stretch=source_stretch(self.meta, PowerStretch(0.5)), clip=False)
 
+        # For Helioviewer images, clear rotation metadata, as these have already been rotated.
+        # Also check that all CROTAn keywords exist to make sure that it's an untouched
+        # Helioviewer file.
+        if ('helioviewer' in self.meta
+                and 'crota' in self.meta and 'crota1' in self.meta and 'crota2' in self.meta):
+            log.debug("LASCOMap: Cleaning up CROTAn keywords "
+                      "because the map has already been rotated by Helioviewer")
+            self.meta.pop('crota')
+            self.meta.pop('crota1')
+            self.meta['crota2'] = 0
+
     @property
     def measurement(self):
         """


Test patch:
diff --git a/sunpy/data/test/2013_05_13__16_54_06_137__SOHO_LASCO_C3_white-light.jp2 b/sunpy/data/test/2013_05_13__16_54_06_137__SOHO_LASCO_C3_white-light.jp2
new file mode 100644
Binary files /dev/null and b/sunpy/data/test/2013_05_13__16_54_06_137__SOHO_LASCO_C3_white-light.jp2 differ
diff --git a/sunpy/map/sources/tests/test_lasco_source.py b/sunpy/map/sources/tests/test_lasco_source.py
--- a/sunpy/map/sources/tests/test_lasco_source.py
+++ b/sunpy/map/sources/tests/test_lasco_source.py
@@ -6,39 +6,64 @@
 import os
 import glob
 
+import numpy as np
+import pytest
+
 import sunpy.data.test
 from sunpy.map import Map
 from sunpy.map.sources.soho import LASCOMap
+from sunpy.tests.helpers import skip_glymur
 
 path = sunpy.data.test.rootdir
-fitspath = glob.glob(os.path.join(path, "lasco_c2_25299383_s.fts"))
-lasco = Map(fitspath)
+
+
+@pytest.fixture
+def lasco():
+    fitspath = glob.glob(os.path.join(path, "lasco_c2_25299383_s.fts"))
+    return Map(fitspath)
+
+
+@pytest.fixture
+def lasco_helioviewer():
+    jp2path = glob.glob(os.path.join(
+        path, "2013_05_13__16_54_06_137__SOHO_LASCO_C3_white-light.jp2"))
+    return Map(jp2path)
+
 
 # LASCO Tests
 
 
-def test_fitstoLASCO():
+def test_fitstoLASCO(lasco):
     """Tests the creation of LASCOMap using FITS."""
     assert isinstance(lasco, LASCOMap)
 
 
-def test_is_datasource_for():
+def test_is_datasource_for(lasco):
     """Test the is_datasource_for method of LASCOMap.
     Note that header data to be provided as an argument
     can be a MetaDict object."""
     assert lasco.is_datasource_for(lasco.data, lasco.meta)
 
 
-def test_measurement():
+def test_measurement(lasco):
     """Tests the measurement property of the LASCOMap object."""
     assert lasco.measurement == "white-light"
 
 
-def test_observatory():
+def test_observatory(lasco):
     """Tests the observatory property of the LASCOMap object."""
     assert lasco.observatory == "SOHO"
 
 
-def test_norm_clip():
+def test_norm_clip(lasco):
     # Tests that the default normalizer has clipping disabled
     assert not lasco.plot_settings['norm'].clip
+
+
+@skip_glymur
+def test_helioviewer_rotation(lasco, lasco_helioviewer):
+    """Tests that rotation metadata is correctly removed
+    for JPEG2000 images provided by Helioviewer.org."""
+    np.testing.assert_allclose(lasco.rotation_matrix,
+                               [[0.999966, -0.008296], [0.008296, 0.999966]], rtol=1e-6)
+    np.testing.assert_array_equal(lasco_helioviewer.rotation_matrix, [[1., 0.], [0., 1.]])