Open In Colab

ImageStacking šŸ“šĀ¶

This notebook aims to explore the purpose of Image Stacking and observe how capturing multiple short exposures is advantegeous than one single exposure for Astrophotography.

Author: Timothy Do

References¶

[1] PhotographingSpace.com. (2023, December 22). Homework: Download and Stack my Data! [Online]. Available: https://www.photographingspace.com/homework-download-stack-data/
[2] Ethan Rublee, Vincent Rabaud, Kurt Konolige, and Gary Bradski. ORB: an efficient alternative to SIFT or SURF. In 2011 IEEE International Conference on Computer Vision (ICCV), 1 pages 2564–2571. IEEE, 2011.
[3] AstroBackyard.com. (2024). Deep Sky Stacker Tutorial Practice Files. [Online]. Available: https://astrobackyard.com/dss-practice-files/

Dependencies¶

InĀ [1]:
IN_COLAB = True
try: 
    import google.colab
except: 
    IN_COLAB = False
print(f'In CoLab: {IN_COLAB}')
In CoLab: False
InĀ [2]:
if(IN_COLAB):
    !curl https://raw.githubusercontent.com/dotimothy/astronomy/main/requirements.txt -o ./requirements.txt
    !pip install -r requirements.txt
else:
    !pip install -r ../requirements.txt
Requirement already satisfied: opencv-python in /Users/tim/env/lib/python3.9/site-packages (from -r ../requirements.txt (line 2)) (4.10.0.84)
Requirement already satisfied: pillow in /Users/tim/env/lib/python3.9/site-packages (from -r ../requirements.txt (line 3)) (11.0.0)
Requirement already satisfied: exifread in /Users/tim/env/lib/python3.9/site-packages (from -r ../requirements.txt (line 4)) (3.0.0)
Requirement already satisfied: matplotlib in /Users/tim/env/lib/python3.9/site-packages (from -r ../requirements.txt (line 5)) (3.9.2)
Requirement already satisfied: numpy in /Users/tim/env/lib/python3.9/site-packages (from -r ../requirements.txt (line 6)) (1.26.4)
Requirement already satisfied: rawpy in /Users/tim/env/lib/python3.9/site-packages (from -r ../requirements.txt (line 7)) (0.24.0)
Requirement already satisfied: tqdm in /Users/tim/env/lib/python3.9/site-packages (from -r ../requirements.txt (line 8)) (4.67.0)
Requirement already satisfied: jupyter in /Users/tim/env/lib/python3.9/site-packages (from -r ../requirements.txt (line 9)) (1.1.1)
Requirement already satisfied: torch in /Users/tim/env/lib/python3.9/site-packages (from -r ../requirements.txt (line 10)) (2.5.1)
Requirement already satisfied: h5py in /Users/tim/env/lib/python3.9/site-packages (from -r ../requirements.txt (line 11)) (3.12.1)
Requirement already satisfied: pandas in /Users/tim/env/lib/python3.9/site-packages (from -r ../requirements.txt (line 12)) (2.2.3)
Requirement already satisfied: xarray in /Users/tim/env/lib/python3.9/site-packages (from -r ../requirements.txt (line 13)) (2024.7.0)
Requirement already satisfied: earthaccess in /Users/tim/env/lib/python3.9/site-packages (from -r ../requirements.txt (line 14)) (0.11.0)
Requirement already satisfied: gdown in /Users/tim/env/lib/python3.9/site-packages (from -r ../requirements.txt (line 15)) (5.2.0)
Requirement already satisfied: contourpy>=1.0.1 in /Users/tim/env/lib/python3.9/site-packages (from matplotlib->-r ../requirements.txt (line 5)) (1.3.0)
Requirement already satisfied: cycler>=0.10 in /Users/tim/env/lib/python3.9/site-packages (from matplotlib->-r ../requirements.txt (line 5)) (0.12.1)
Requirement already satisfied: fonttools>=4.22.0 in /Users/tim/env/lib/python3.9/site-packages (from matplotlib->-r ../requirements.txt (line 5)) (4.55.0)
Requirement already satisfied: kiwisolver>=1.3.1 in /Users/tim/env/lib/python3.9/site-packages (from matplotlib->-r ../requirements.txt (line 5)) (1.4.7)
Requirement already satisfied: packaging>=20.0 in /Users/tim/env/lib/python3.9/site-packages (from matplotlib->-r ../requirements.txt (line 5)) (24.2)
Requirement already satisfied: pyparsing>=2.3.1 in /Users/tim/env/lib/python3.9/site-packages (from matplotlib->-r ../requirements.txt (line 5)) (3.2.0)
Requirement already satisfied: python-dateutil>=2.7 in /Users/tim/env/lib/python3.9/site-packages (from matplotlib->-r ../requirements.txt (line 5)) (2.9.0.post0)
Requirement already satisfied: importlib-resources>=3.2.0 in /Users/tim/env/lib/python3.9/site-packages (from matplotlib->-r ../requirements.txt (line 5)) (6.4.5)
Requirement already satisfied: notebook in /Users/tim/env/lib/python3.9/site-packages (from jupyter->-r ../requirements.txt (line 9)) (7.2.2)
Requirement already satisfied: jupyter-console in /Users/tim/env/lib/python3.9/site-packages (from jupyter->-r ../requirements.txt (line 9)) (6.6.3)
Requirement already satisfied: nbconvert in /Users/tim/env/lib/python3.9/site-packages (from jupyter->-r ../requirements.txt (line 9)) (7.16.4)
Requirement already satisfied: ipykernel in /Users/tim/env/lib/python3.9/site-packages (from jupyter->-r ../requirements.txt (line 9)) (6.29.5)
Requirement already satisfied: ipywidgets in /Users/tim/env/lib/python3.9/site-packages (from jupyter->-r ../requirements.txt (line 9)) (8.1.5)
Requirement already satisfied: jupyterlab in /Users/tim/env/lib/python3.9/site-packages (from jupyter->-r ../requirements.txt (line 9)) (4.2.6)
Requirement already satisfied: filelock in /Users/tim/env/lib/python3.9/site-packages (from torch->-r ../requirements.txt (line 10)) (3.16.1)
Requirement already satisfied: typing-extensions>=4.8.0 in /Users/tim/env/lib/python3.9/site-packages (from torch->-r ../requirements.txt (line 10)) (4.12.2)
Requirement already satisfied: networkx in /Users/tim/env/lib/python3.9/site-packages (from torch->-r ../requirements.txt (line 10)) (3.2.1)
Requirement already satisfied: jinja2 in /Users/tim/env/lib/python3.9/site-packages (from torch->-r ../requirements.txt (line 10)) (3.1.4)
Requirement already satisfied: fsspec in /Users/tim/env/lib/python3.9/site-packages (from torch->-r ../requirements.txt (line 10)) (2024.12.0)
Requirement already satisfied: sympy==1.13.1 in /Users/tim/env/lib/python3.9/site-packages (from torch->-r ../requirements.txt (line 10)) (1.13.1)
Requirement already satisfied: mpmath<1.4,>=1.1.0 in /Users/tim/env/lib/python3.9/site-packages (from sympy==1.13.1->torch->-r ../requirements.txt (line 10)) (1.3.0)
Requirement already satisfied: pytz>=2020.1 in /Users/tim/env/lib/python3.9/site-packages (from pandas->-r ../requirements.txt (line 12)) (2024.2)
Requirement already satisfied: tzdata>=2022.7 in /Users/tim/env/lib/python3.9/site-packages (from pandas->-r ../requirements.txt (line 12)) (2024.2)
Requirement already satisfied: multimethod>=1.8 in /Users/tim/env/lib/python3.9/site-packages (from earthaccess->-r ../requirements.txt (line 14)) (1.12)
Requirement already satisfied: pqdm>=0.1 in /Users/tim/env/lib/python3.9/site-packages (from earthaccess->-r ../requirements.txt (line 14)) (0.2.0)
Requirement already satisfied: python-cmr>=0.10.0 in /Users/tim/env/lib/python3.9/site-packages (from earthaccess->-r ../requirements.txt (line 14)) (0.13.0)
Requirement already satisfied: requests>=2.26 in /Users/tim/env/lib/python3.9/site-packages (from earthaccess->-r ../requirements.txt (line 14)) (2.32.3)
Requirement already satisfied: s3fs>=2022.11 in /Users/tim/env/lib/python3.9/site-packages (from earthaccess->-r ../requirements.txt (line 14)) (2024.12.0)
Requirement already satisfied: tinynetrc>=1.3.1 in /Users/tim/env/lib/python3.9/site-packages (from earthaccess->-r ../requirements.txt (line 14)) (1.3.1)
Requirement already satisfied: beautifulsoup4 in /Users/tim/env/lib/python3.9/site-packages (from gdown->-r ../requirements.txt (line 15)) (4.12.3)
Requirement already satisfied: zipp>=3.1.0 in /Users/tim/env/lib/python3.9/site-packages (from importlib-resources>=3.2.0->matplotlib->-r ../requirements.txt (line 5)) (3.21.0)
Requirement already satisfied: bounded-pool-executor in /Users/tim/env/lib/python3.9/site-packages (from pqdm>=0.1->earthaccess->-r ../requirements.txt (line 14)) (0.0.3)
Requirement already satisfied: six>=1.5 in /Users/tim/env/lib/python3.9/site-packages (from python-dateutil>=2.7->matplotlib->-r ../requirements.txt (line 5)) (1.16.0)
Requirement already satisfied: charset-normalizer<4,>=2 in /Users/tim/env/lib/python3.9/site-packages (from requests>=2.26->earthaccess->-r ../requirements.txt (line 14)) (3.4.0)
Requirement already satisfied: idna<4,>=2.5 in /Users/tim/env/lib/python3.9/site-packages (from requests>=2.26->earthaccess->-r ../requirements.txt (line 14)) (3.7)
Requirement already satisfied: urllib3<3,>=1.21.1 in /Users/tim/env/lib/python3.9/site-packages (from requests>=2.26->earthaccess->-r ../requirements.txt (line 14)) (1.26.20)
Requirement already satisfied: certifi>=2017.4.17 in /Users/tim/env/lib/python3.9/site-packages (from requests>=2.26->earthaccess->-r ../requirements.txt (line 14)) (2024.8.30)
Requirement already satisfied: aiobotocore<3.0.0,>=2.5.4 in /Users/tim/env/lib/python3.9/site-packages (from s3fs>=2022.11->earthaccess->-r ../requirements.txt (line 14)) (2.16.0)
Requirement already satisfied: aiohttp!=4.0.0a0,!=4.0.0a1 in /Users/tim/env/lib/python3.9/site-packages (from s3fs>=2022.11->earthaccess->-r ../requirements.txt (line 14)) (3.11.11)
Requirement already satisfied: soupsieve>1.2 in /Users/tim/env/lib/python3.9/site-packages (from beautifulsoup4->gdown->-r ../requirements.txt (line 15)) (2.6)
Requirement already satisfied: appnope in /Users/tim/env/lib/python3.9/site-packages (from ipykernel->jupyter->-r ../requirements.txt (line 9)) (0.1.4)
Requirement already satisfied: comm>=0.1.1 in /Users/tim/env/lib/python3.9/site-packages (from ipykernel->jupyter->-r ../requirements.txt (line 9)) (0.2.2)
Requirement already satisfied: debugpy>=1.6.5 in /Users/tim/env/lib/python3.9/site-packages (from ipykernel->jupyter->-r ../requirements.txt (line 9)) (1.8.8)
Requirement already satisfied: ipython>=7.23.1 in /Users/tim/env/lib/python3.9/site-packages (from ipykernel->jupyter->-r ../requirements.txt (line 9)) (8.18.1)
Requirement already satisfied: jupyter-client>=6.1.12 in /Users/tim/env/lib/python3.9/site-packages (from ipykernel->jupyter->-r ../requirements.txt (line 9)) (8.6.3)
Requirement already satisfied: jupyter-core!=5.0.*,>=4.12 in /Users/tim/env/lib/python3.9/site-packages (from ipykernel->jupyter->-r ../requirements.txt (line 9)) (5.7.2)
Requirement already satisfied: matplotlib-inline>=0.1 in /Users/tim/env/lib/python3.9/site-packages (from ipykernel->jupyter->-r ../requirements.txt (line 9)) (0.1.7)
Requirement already satisfied: nest-asyncio in /Users/tim/env/lib/python3.9/site-packages (from ipykernel->jupyter->-r ../requirements.txt (line 9)) (1.6.0)
Requirement already satisfied: psutil in /Users/tim/env/lib/python3.9/site-packages (from ipykernel->jupyter->-r ../requirements.txt (line 9)) (6.1.0)
Requirement already satisfied: pyzmq>=24 in /Users/tim/env/lib/python3.9/site-packages (from ipykernel->jupyter->-r ../requirements.txt (line 9)) (26.2.0)
Requirement already satisfied: tornado>=6.1 in /Users/tim/env/lib/python3.9/site-packages (from ipykernel->jupyter->-r ../requirements.txt (line 9)) (6.4.1)
Requirement already satisfied: traitlets>=5.4.0 in /Users/tim/env/lib/python3.9/site-packages (from ipykernel->jupyter->-r ../requirements.txt (line 9)) (5.14.3)
Requirement already satisfied: widgetsnbextension~=4.0.12 in /Users/tim/env/lib/python3.9/site-packages (from ipywidgets->jupyter->-r ../requirements.txt (line 9)) (4.0.13)
Requirement already satisfied: jupyterlab-widgets~=3.0.12 in /Users/tim/env/lib/python3.9/site-packages (from ipywidgets->jupyter->-r ../requirements.txt (line 9)) (3.0.13)
Requirement already satisfied: MarkupSafe>=2.0 in /Users/tim/env/lib/python3.9/site-packages (from jinja2->torch->-r ../requirements.txt (line 10)) (2.1.5)
Requirement already satisfied: prompt-toolkit>=3.0.30 in /Users/tim/env/lib/python3.9/site-packages (from jupyter-console->jupyter->-r ../requirements.txt (line 9)) (3.0.48)
Requirement already satisfied: pygments in /Users/tim/env/lib/python3.9/site-packages (from jupyter-console->jupyter->-r ../requirements.txt (line 9)) (2.18.0)
Requirement already satisfied: async-lru>=1.0.0 in /Users/tim/env/lib/python3.9/site-packages (from jupyterlab->jupyter->-r ../requirements.txt (line 9)) (2.0.4)
Requirement already satisfied: httpx>=0.25.0 in /Users/tim/env/lib/python3.9/site-packages (from jupyterlab->jupyter->-r ../requirements.txt (line 9)) (0.27.2)
Requirement already satisfied: importlib-metadata>=4.8.3 in /Users/tim/env/lib/python3.9/site-packages (from jupyterlab->jupyter->-r ../requirements.txt (line 9)) (8.5.0)
Requirement already satisfied: jupyter-lsp>=2.0.0 in /Users/tim/env/lib/python3.9/site-packages (from jupyterlab->jupyter->-r ../requirements.txt (line 9)) (2.2.5)
Requirement already satisfied: jupyter-server<3,>=2.4.0 in /Users/tim/env/lib/python3.9/site-packages (from jupyterlab->jupyter->-r ../requirements.txt (line 9)) (2.14.2)
Requirement already satisfied: jupyterlab-server<3,>=2.27.1 in /Users/tim/env/lib/python3.9/site-packages (from jupyterlab->jupyter->-r ../requirements.txt (line 9)) (2.27.3)
Requirement already satisfied: notebook-shim>=0.2 in /Users/tim/env/lib/python3.9/site-packages (from jupyterlab->jupyter->-r ../requirements.txt (line 9)) (0.2.4)
Requirement already satisfied: setuptools>=40.1.0 in /Users/tim/env/lib/python3.9/site-packages (from jupyterlab->jupyter->-r ../requirements.txt (line 9)) (74.1.2)
Requirement already satisfied: tomli>=1.2.2 in /Users/tim/env/lib/python3.9/site-packages (from jupyterlab->jupyter->-r ../requirements.txt (line 9)) (2.1.0)
Requirement already satisfied: bleach!=5.0.0 in /Users/tim/env/lib/python3.9/site-packages (from nbconvert->jupyter->-r ../requirements.txt (line 9)) (6.2.0)
Requirement already satisfied: defusedxml in /Users/tim/env/lib/python3.9/site-packages (from nbconvert->jupyter->-r ../requirements.txt (line 9)) (0.7.1)
Requirement already satisfied: jupyterlab-pygments in /Users/tim/env/lib/python3.9/site-packages (from nbconvert->jupyter->-r ../requirements.txt (line 9)) (0.3.0)
Requirement already satisfied: mistune<4,>=2.0.3 in /Users/tim/env/lib/python3.9/site-packages (from nbconvert->jupyter->-r ../requirements.txt (line 9)) (3.0.2)
Requirement already satisfied: nbclient>=0.5.0 in /Users/tim/env/lib/python3.9/site-packages (from nbconvert->jupyter->-r ../requirements.txt (line 9)) (0.10.0)
Requirement already satisfied: nbformat>=5.7 in /Users/tim/env/lib/python3.9/site-packages (from nbconvert->jupyter->-r ../requirements.txt (line 9)) (5.10.4)
Requirement already satisfied: pandocfilters>=1.4.1 in /Users/tim/env/lib/python3.9/site-packages (from nbconvert->jupyter->-r ../requirements.txt (line 9)) (1.5.1)
Requirement already satisfied: tinycss2 in /Users/tim/env/lib/python3.9/site-packages (from nbconvert->jupyter->-r ../requirements.txt (line 9)) (1.4.0)
Requirement already satisfied: PySocks!=1.5.7,>=1.5.6 in /Users/tim/env/lib/python3.9/site-packages (from requests[socks]->gdown->-r ../requirements.txt (line 15)) (1.7.1)
Requirement already satisfied: botocore<1.35.82,>=1.35.74 in /Users/tim/env/lib/python3.9/site-packages (from aiobotocore<3.0.0,>=2.5.4->s3fs>=2022.11->earthaccess->-r ../requirements.txt (line 14)) (1.35.81)
Requirement already satisfied: wrapt<2.0.0,>=1.10.10 in /Users/tim/env/lib/python3.9/site-packages (from aiobotocore<3.0.0,>=2.5.4->s3fs>=2022.11->earthaccess->-r ../requirements.txt (line 14)) (1.17.0)
Requirement already satisfied: aioitertools<1.0.0,>=0.5.1 in /Users/tim/env/lib/python3.9/site-packages (from aiobotocore<3.0.0,>=2.5.4->s3fs>=2022.11->earthaccess->-r ../requirements.txt (line 14)) (0.12.0)
Requirement already satisfied: aiohappyeyeballs>=2.3.0 in /Users/tim/env/lib/python3.9/site-packages (from aiohttp!=4.0.0a0,!=4.0.0a1->s3fs>=2022.11->earthaccess->-r ../requirements.txt (line 14)) (2.4.4)
Requirement already satisfied: aiosignal>=1.1.2 in /Users/tim/env/lib/python3.9/site-packages (from aiohttp!=4.0.0a0,!=4.0.0a1->s3fs>=2022.11->earthaccess->-r ../requirements.txt (line 14)) (1.3.2)
Requirement already satisfied: async-timeout<6.0,>=4.0 in /Users/tim/env/lib/python3.9/site-packages (from aiohttp!=4.0.0a0,!=4.0.0a1->s3fs>=2022.11->earthaccess->-r ../requirements.txt (line 14)) (5.0.1)
Requirement already satisfied: attrs>=17.3.0 in /Users/tim/env/lib/python3.9/site-packages (from aiohttp!=4.0.0a0,!=4.0.0a1->s3fs>=2022.11->earthaccess->-r ../requirements.txt (line 14)) (24.2.0)
Requirement already satisfied: frozenlist>=1.1.1 in /Users/tim/env/lib/python3.9/site-packages (from aiohttp!=4.0.0a0,!=4.0.0a1->s3fs>=2022.11->earthaccess->-r ../requirements.txt (line 14)) (1.5.0)
Requirement already satisfied: multidict<7.0,>=4.5 in /Users/tim/env/lib/python3.9/site-packages (from aiohttp!=4.0.0a0,!=4.0.0a1->s3fs>=2022.11->earthaccess->-r ../requirements.txt (line 14)) (6.1.0)
Requirement already satisfied: propcache>=0.2.0 in /Users/tim/env/lib/python3.9/site-packages (from aiohttp!=4.0.0a0,!=4.0.0a1->s3fs>=2022.11->earthaccess->-r ../requirements.txt (line 14)) (0.2.1)
Requirement already satisfied: yarl<2.0,>=1.17.0 in /Users/tim/env/lib/python3.9/site-packages (from aiohttp!=4.0.0a0,!=4.0.0a1->s3fs>=2022.11->earthaccess->-r ../requirements.txt (line 14)) (1.18.3)
Requirement already satisfied: webencodings in /Users/tim/env/lib/python3.9/site-packages (from bleach!=5.0.0->nbconvert->jupyter->-r ../requirements.txt (line 9)) (0.5.1)
Requirement already satisfied: anyio in /Users/tim/env/lib/python3.9/site-packages (from httpx>=0.25.0->jupyterlab->jupyter->-r ../requirements.txt (line 9)) (4.6.2.post1)
Requirement already satisfied: httpcore==1.* in /Users/tim/env/lib/python3.9/site-packages (from httpx>=0.25.0->jupyterlab->jupyter->-r ../requirements.txt (line 9)) (1.0.7)
Requirement already satisfied: sniffio in /Users/tim/env/lib/python3.9/site-packages (from httpx>=0.25.0->jupyterlab->jupyter->-r ../requirements.txt (line 9)) (1.3.1)
Requirement already satisfied: h11<0.15,>=0.13 in /Users/tim/env/lib/python3.9/site-packages (from httpcore==1.*->httpx>=0.25.0->jupyterlab->jupyter->-r ../requirements.txt (line 9)) (0.14.0)
Requirement already satisfied: decorator in /Users/tim/env/lib/python3.9/site-packages (from ipython>=7.23.1->ipykernel->jupyter->-r ../requirements.txt (line 9)) (5.1.1)
Requirement already satisfied: jedi>=0.16 in /Users/tim/env/lib/python3.9/site-packages (from ipython>=7.23.1->ipykernel->jupyter->-r ../requirements.txt (line 9)) (0.19.2)
Requirement already satisfied: stack-data in /Users/tim/env/lib/python3.9/site-packages (from ipython>=7.23.1->ipykernel->jupyter->-r ../requirements.txt (line 9)) (0.6.3)
Requirement already satisfied: exceptiongroup in /Users/tim/env/lib/python3.9/site-packages (from ipython>=7.23.1->ipykernel->jupyter->-r ../requirements.txt (line 9)) (1.2.2)
Requirement already satisfied: pexpect>4.3 in /Users/tim/env/lib/python3.9/site-packages (from ipython>=7.23.1->ipykernel->jupyter->-r ../requirements.txt (line 9)) (4.9.0)
Requirement already satisfied: platformdirs>=2.5 in /Users/tim/env/lib/python3.9/site-packages (from jupyter-core!=5.0.*,>=4.12->ipykernel->jupyter->-r ../requirements.txt (line 9)) (4.3.6)
Requirement already satisfied: argon2-cffi>=21.1 in /Users/tim/env/lib/python3.9/site-packages (from jupyter-server<3,>=2.4.0->jupyterlab->jupyter->-r ../requirements.txt (line 9)) (23.1.0)
Requirement already satisfied: jupyter-events>=0.9.0 in /Users/tim/env/lib/python3.9/site-packages (from jupyter-server<3,>=2.4.0->jupyterlab->jupyter->-r ../requirements.txt (line 9)) (0.10.0)
Requirement already satisfied: jupyter-server-terminals>=0.4.4 in /Users/tim/env/lib/python3.9/site-packages (from jupyter-server<3,>=2.4.0->jupyterlab->jupyter->-r ../requirements.txt (line 9)) (0.5.3)
Requirement already satisfied: overrides>=5.0 in /Users/tim/env/lib/python3.9/site-packages (from jupyter-server<3,>=2.4.0->jupyterlab->jupyter->-r ../requirements.txt (line 9)) (7.7.0)
Requirement already satisfied: prometheus-client>=0.9 in /Users/tim/env/lib/python3.9/site-packages (from jupyter-server<3,>=2.4.0->jupyterlab->jupyter->-r ../requirements.txt (line 9)) (0.21.0)
Requirement already satisfied: send2trash>=1.8.2 in /Users/tim/env/lib/python3.9/site-packages (from jupyter-server<3,>=2.4.0->jupyterlab->jupyter->-r ../requirements.txt (line 9)) (1.8.3)
Requirement already satisfied: terminado>=0.8.3 in /Users/tim/env/lib/python3.9/site-packages (from jupyter-server<3,>=2.4.0->jupyterlab->jupyter->-r ../requirements.txt (line 9)) (0.18.1)
Requirement already satisfied: websocket-client>=1.7 in /Users/tim/env/lib/python3.9/site-packages (from jupyter-server<3,>=2.4.0->jupyterlab->jupyter->-r ../requirements.txt (line 9)) (1.8.0)
Requirement already satisfied: babel>=2.10 in /Users/tim/env/lib/python3.9/site-packages (from jupyterlab-server<3,>=2.27.1->jupyterlab->jupyter->-r ../requirements.txt (line 9)) (2.16.0)
Requirement already satisfied: json5>=0.9.0 in /Users/tim/env/lib/python3.9/site-packages (from jupyterlab-server<3,>=2.27.1->jupyterlab->jupyter->-r ../requirements.txt (line 9)) (0.9.28)
Requirement already satisfied: jsonschema>=4.18.0 in /Users/tim/env/lib/python3.9/site-packages (from jupyterlab-server<3,>=2.27.1->jupyterlab->jupyter->-r ../requirements.txt (line 9)) (4.23.0)
Requirement already satisfied: fastjsonschema>=2.15 in /Users/tim/env/lib/python3.9/site-packages (from nbformat>=5.7->nbconvert->jupyter->-r ../requirements.txt (line 9)) (2.20.0)
Requirement already satisfied: wcwidth in /Users/tim/env/lib/python3.9/site-packages (from prompt-toolkit>=3.0.30->jupyter-console->jupyter->-r ../requirements.txt (line 9)) (0.2.13)
Requirement already satisfied: argon2-cffi-bindings in /Users/tim/env/lib/python3.9/site-packages (from argon2-cffi>=21.1->jupyter-server<3,>=2.4.0->jupyterlab->jupyter->-r ../requirements.txt (line 9)) (21.2.0)
Requirement already satisfied: jmespath<2.0.0,>=0.7.1 in /Users/tim/env/lib/python3.9/site-packages (from botocore<1.35.82,>=1.35.74->aiobotocore<3.0.0,>=2.5.4->s3fs>=2022.11->earthaccess->-r ../requirements.txt (line 14)) (1.0.1)
Requirement already satisfied: parso<0.9.0,>=0.8.4 in /Users/tim/env/lib/python3.9/site-packages (from jedi>=0.16->ipython>=7.23.1->ipykernel->jupyter->-r ../requirements.txt (line 9)) (0.8.4)
Requirement already satisfied: jsonschema-specifications>=2023.03.6 in /Users/tim/env/lib/python3.9/site-packages (from jsonschema>=4.18.0->jupyterlab-server<3,>=2.27.1->jupyterlab->jupyter->-r ../requirements.txt (line 9)) (2024.10.1)
Requirement already satisfied: referencing>=0.28.4 in /Users/tim/env/lib/python3.9/site-packages (from jsonschema>=4.18.0->jupyterlab-server<3,>=2.27.1->jupyterlab->jupyter->-r ../requirements.txt (line 9)) (0.35.1)
Requirement already satisfied: rpds-py>=0.7.1 in /Users/tim/env/lib/python3.9/site-packages (from jsonschema>=4.18.0->jupyterlab-server<3,>=2.27.1->jupyterlab->jupyter->-r ../requirements.txt (line 9)) (0.21.0)
Requirement already satisfied: python-json-logger>=2.0.4 in /Users/tim/env/lib/python3.9/site-packages (from jupyter-events>=0.9.0->jupyter-server<3,>=2.4.0->jupyterlab->jupyter->-r ../requirements.txt (line 9)) (2.0.7)
Requirement already satisfied: pyyaml>=5.3 in /Users/tim/env/lib/python3.9/site-packages (from jupyter-events>=0.9.0->jupyter-server<3,>=2.4.0->jupyterlab->jupyter->-r ../requirements.txt (line 9)) (6.0.2)
Requirement already satisfied: rfc3339-validator in /Users/tim/env/lib/python3.9/site-packages (from jupyter-events>=0.9.0->jupyter-server<3,>=2.4.0->jupyterlab->jupyter->-r ../requirements.txt (line 9)) (0.1.4)
Requirement already satisfied: rfc3986-validator>=0.1.1 in /Users/tim/env/lib/python3.9/site-packages (from jupyter-events>=0.9.0->jupyter-server<3,>=2.4.0->jupyterlab->jupyter->-r ../requirements.txt (line 9)) (0.1.1)
Requirement already satisfied: ptyprocess>=0.5 in /Users/tim/env/lib/python3.9/site-packages (from pexpect>4.3->ipython>=7.23.1->ipykernel->jupyter->-r ../requirements.txt (line 9)) (0.7.0)
Requirement already satisfied: executing>=1.2.0 in /Users/tim/env/lib/python3.9/site-packages (from stack-data->ipython>=7.23.1->ipykernel->jupyter->-r ../requirements.txt (line 9)) (2.1.0)
Requirement already satisfied: asttokens>=2.1.0 in /Users/tim/env/lib/python3.9/site-packages (from stack-data->ipython>=7.23.1->ipykernel->jupyter->-r ../requirements.txt (line 9)) (2.4.1)
Requirement already satisfied: pure-eval in /Users/tim/env/lib/python3.9/site-packages (from stack-data->ipython>=7.23.1->ipykernel->jupyter->-r ../requirements.txt (line 9)) (0.2.3)
Requirement already satisfied: fqdn in /Users/tim/env/lib/python3.9/site-packages (from jsonschema[format-nongpl]>=4.18.0->jupyter-events>=0.9.0->jupyter-server<3,>=2.4.0->jupyterlab->jupyter->-r ../requirements.txt (line 9)) (1.5.1)
Requirement already satisfied: isoduration in /Users/tim/env/lib/python3.9/site-packages (from jsonschema[format-nongpl]>=4.18.0->jupyter-events>=0.9.0->jupyter-server<3,>=2.4.0->jupyterlab->jupyter->-r ../requirements.txt (line 9)) (20.11.0)
Requirement already satisfied: jsonpointer>1.13 in /Users/tim/env/lib/python3.9/site-packages (from jsonschema[format-nongpl]>=4.18.0->jupyter-events>=0.9.0->jupyter-server<3,>=2.4.0->jupyterlab->jupyter->-r ../requirements.txt (line 9)) (3.0.0)
Requirement already satisfied: uri-template in /Users/tim/env/lib/python3.9/site-packages (from jsonschema[format-nongpl]>=4.18.0->jupyter-events>=0.9.0->jupyter-server<3,>=2.4.0->jupyterlab->jupyter->-r ../requirements.txt (line 9)) (1.3.0)
Requirement already satisfied: webcolors>=24.6.0 in /Users/tim/env/lib/python3.9/site-packages (from jsonschema[format-nongpl]>=4.18.0->jupyter-events>=0.9.0->jupyter-server<3,>=2.4.0->jupyterlab->jupyter->-r ../requirements.txt (line 9)) (24.11.1)
Requirement already satisfied: cffi>=1.0.1 in /Users/tim/env/lib/python3.9/site-packages (from argon2-cffi-bindings->argon2-cffi>=21.1->jupyter-server<3,>=2.4.0->jupyterlab->jupyter->-r ../requirements.txt (line 9)) (1.17.1)
Requirement already satisfied: pycparser in /Users/tim/env/lib/python3.9/site-packages (from cffi>=1.0.1->argon2-cffi-bindings->argon2-cffi>=21.1->jupyter-server<3,>=2.4.0->jupyterlab->jupyter->-r ../requirements.txt (line 9)) (2.22)
Requirement already satisfied: arrow>=0.15.0 in /Users/tim/env/lib/python3.9/site-packages (from isoduration->jsonschema[format-nongpl]>=4.18.0->jupyter-events>=0.9.0->jupyter-server<3,>=2.4.0->jupyterlab->jupyter->-r ../requirements.txt (line 9)) (1.3.0)
Requirement already satisfied: types-python-dateutil>=2.8.10 in /Users/tim/env/lib/python3.9/site-packages (from arrow>=0.15.0->isoduration->jsonschema[format-nongpl]>=4.18.0->jupyter-events>=0.9.0->jupyter-server<3,>=2.4.0->jupyterlab->jupyter->-r ../requirements.txt (line 9)) (2.9.0.20241003)
InĀ [3]:
# Dependencies
import os
import gc
import gdown
import numpy as np
import rawpy
import cv2 as cv
import matplotlib.pyplot as plt 


# Enable EXR
os.environ["OPENCV_IO_ENABLE_OPENEXR"] = "1"
InĀ [4]:
# Global Helper Functions
# Crudely Normalize an Image (float) from 0-1
def normalize(img):
    return (img - img.min())/(img.max()-img.min())

# Contrast Stretches an Image (float)
def contrast_stretch(img,min=0,max=1):
    return np.clip(normalize(img) * (max-min) + min,a_min=0,a_max=1)

# Convert a uint8/uint16 image to a float image
def int_to_float(img):
    return normalize(img.astype(np.float))

# Convert a float image to a uint8 image
def float_to_int(img):
    return ((2**8-1)*normalize(img)).astype(np.uint8)

# Convert a float image to a uint16 image
def float_to_int16(img):
    return ((2**16-1)*normalize(img)).astype(np.uint16)

# Converts CR2 to Numpy Array
def cr2_to_numpy(cr2Path,uint=False,gamma=(1,1),half_size=True,use_camera_wb=False,no_auto_bright=False):
    with rawpy.imread(cr2Path) as raw:
        if(uint):
            out = raw.postprocess() # uint8
        else:
            out = raw.postprocess(gamma=gamma,use_camera_wb=use_camera_wb,use_auto_wb=True,no_auto_bright=no_auto_bright,half_size=half_size,output_bps=16,demosaic_algorithm=rawpy.DemosaicAlgorithm.VNG,output_color=rawpy.ColorSpace.sRGB)
            out = out.astype(np.float32)/ 2**16
        if(out.shape[2] > out.shape[1]): # Vertical, must rotate
            out = np.rot90(out,k=3)
    return out

# Convert Directory of CR2s into a Tensor
def cr2Dir_to_tensor(stackPath,gamma=(1,1),use_camera_wb=False,half_size=True,no_auto_bright=False,exclude=[]):
    stackFiles = sorted([file for file in os.listdir(stackPath) if (file not in exclude) and (file.endswith('.CR2') or file.endswith('.CR3'))])
    print(f'Stack Files: {stackFiles}')
    print('Stacking Raw Images into a Tensor')
    stackedNumpy = np.stack([cr2_to_numpy(f'{stackPath}/{stackFile}',gamma=gamma,use_camera_wb=use_camera_wb,half_size=half_size,no_auto_bright=no_auto_bright) for stackFile in tqdm(stackFiles)])
    print(f'Stacked Numpy Shape: {stackedNumpy.shape}')
    print(f'Stacked Numpy Data Type: {stackedNumpy.dtype}')
    gc.collect()
    return stackedNumpy 

Downloading the Data¶

For exploring the process of Image Stacking, we will be downloading a "Homework" example from Cory Schmitz's blog in the PhotographSpace site! He takes a really nice shot of the milky way that we can work with. [1]

Here are some logisitcs of the stack, taken on a static tripod:

  • Camera: Canon 5D Mark III
  • ISO: 6400
  • Exposure Length: 15 seconds
  • White Balance: 4250K
InĀ [5]:
# Creating Directory to Store the Data
dataDir1 = '../data/imagestacking/photographspace'
os.makedirs(dataDir1,exist_ok=True)
InĀ [6]:
# Downloading the Single Raw Image & Full Stack
site = "https://www.photographingspace.com/downloads/"
singleRawFile = "CSM30803.CR2"
stackFile = "11x_ISO6400_f2.8_15s_5DMkIII_raw.zip"
for file in [singleRawFile,stackFile]: 
    filePath = f'{dataDir1}/{file}'
    if(not(os.path.exists(filePath))):
        fileLink = f'{site}/{file}'
        os.system(f'curl {fileLink} -o {filePath}')
    if(file.endswith('.zip')):
        zipDir = filePath.split('.zip')[0]
        if(not(os.path.exists(zipDir))):
            os.system(f'unzip {filePath} -d {zipDir}')

Visualizing the Data¶

InĀ [7]:
# Extracting Single Raw Image & Viewing
singleRawPath = f'{dataDir1}/{singleRawFile}'
singleRaw = cr2_to_numpy(singleRawPath,use_camera_wb=True)
print(f'Single Raw Numpy Shape: {singleRaw.shape}')
print(f'Single Raw Numpy Data Type: {singleRaw.dtype}')
cv.imwrite(f"{dataDir1}/{singleRawFile.split('.CR2')[0]}.tiff",float_to_int16(singleRaw)[...,::-1])

# Plotting
plt.figure(figsize=(15,10))
plt.title(f'Single Raw: {singleRawFile}')
plt.imshow(contrast_stretch(singleRaw))
plt.axis('off')
plt.show()
Single Raw Numpy Shape: (1935, 2898, 3)
Single Raw Numpy Data Type: float64
No description has been provided for this image
InĀ [8]:
# Read the Stacks into Memory
stackPath = f'{dataDir1}/11x_ISO6400_f2.8_15s_5DMkIII_raw/raw'
stackFiles = sorted([file for file in os.listdir(stackPath) if file.endswith('.CR2')])
stackedNumpy = cr2Dir_to_tensor(stackPath,use_camera_wb=True)
Stack Files: ['CSM30799.CR2', 'CSM30800.CR2', 'CSM30801.CR2', 'CSM30802.CR2', 'CSM30803.CR2', 'CSM30804.CR2', 'CSM30805.CR2', 'CSM30806.CR2', 'CSM30807.CR2', 'CSM30808.CR2', 'CSM30809.CR2']
Stacking Raw Images into a Tensor
100%|ā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆ| 11/11 [00:04<00:00,  2.66it/s]
Stacked Numpy Shape: (11, 1935, 2898, 3)
Stacked Numpy Data Type: float64

InĀ [9]:
# Viewing the Stack
rows = 5
cols = 2
fig, ax = plt.subplots(nrows=rows,ncols=cols,figsize=(15,30))
plt.suptitle('Raw Image Stack')
for i in range(rows):
    for j in range(cols):
        ax[i][j].axis('off')
        ax[i][j].set_title(stackFiles[(i*cols)+j])
        ax[i][j].imshow(contrast_stretch(stackedNumpy[(i*cols)+j]))
plt.tight_layout()
plt.show()
No description has been provided for this image

Stacking Methods¶

We will explore stacking methods in order to reduce noise and have the deep sky objects really pop out! We first start off with Naive methods, which work by avergaing the images to filter noise. The noise is coming from the shot, is due to photons, sensor readout, thermal, etc. The noise can modeled as additive as shown below: $$I(x,y) = f(x,y) + n(x,y)$$ $$n(x,y) \sim \mathcal{N}(\mu,\sigma^2)$$, where it is assumed zero-mean and unit variance (i.e., $\mu = 0, \sigma = 1$).

Sampling a large number of images estimates the distribution of the noise to filter it out. Averaging works in the ideal setting (i.e., when there is no parallax between scenes), but not in the real world because of star trails. Star trails are caused when the stars rotate with the Earth's rotation in perspective of the shot, so we need to perform image registartion before stacking the images!

Simulation¶

We first provide an ideal scene, where a test scene is generated with a simulated deep sky object.

InĀ [10]:
# Helper Functions to Simulate Noise
# Creating Test Scene 
def createTestScene(width=32,height=32,radius=2):
    # Image dimensions
    # Create an empty image (black background)
    image = np.zeros((height, width,3))

    # Define circle parameters
    center_x = width // 2
    center_y = height // 2

    # Create a meshgrid
    y, x = np.ogrid[-center_y:height-center_y, -center_x:width-center_x]

    # Calculate distance from center
    dist_from_center = np.sqrt(x**2 + y**2)

    # Set pixels within the circle to white
    image[dist_from_center <= radius] = 1
    return image

# Genereates Noise with Zero Mean and Unit Variance 
def createNoise(width=32,height=32,dist='gauss'):
    if(dist == 'uniform'):
        noise = np.random.rand(height,width,3)
    elif(dist == 'gauss'):
        noise = np.random.randn(height,width,3)
    return normalize(noise)
InĀ [11]:
testScene = createTestScene()
print(f'Test Scene Shape: {testScene.shape}')
print(f'Test Scene Data Type: {testScene.dtype}')
plt.figure(figsize=(15,10))
plt.imshow(testScene)
plt.title('Test Scene')
plt.axis('off')
plt.show()
Test Scene Shape: (32, 32, 3)
Test Scene Data Type: float64
No description has been provided for this image
InĀ [12]:
testNoise = createNoise(dist='gauss')
print(f'Test Noise Shape: {testNoise.shape}')
print(f'Test Noise Data Type: {testNoise.dtype}')
print(f'Test Noise Min: {testNoise.min()}')
print(f'Test Noise Max: {testNoise.max()}')
plt.figure(figsize=(15,10))
plt.imshow(testNoise)
plt.title('Test Noise')
plt.axis('off')
plt.show()
Test Noise Shape: (32, 32, 3)
Test Noise Data Type: float64
Test Noise Min: 0.0
Test Noise Max: 1.0
No description has been provided for this image
InĀ [13]:
# Generate the Noisy Stack 
scale = 1
numStack = 10000
print('Creating Test Stack...')
testStack = np.stack([normalize(testScene + scale*createNoise(dist='uniform')) for _ in tqdm(range(numStack))])
print(f'Test Stack Shape: {testStack.shape}')
Creating Test Stack...
100%|ā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆ| 10000/10000 [00:00<00:00, 48349.10it/s]
Test Stack Shape: (10000, 32, 32, 3)

InĀ [14]:
plt.figure(figsize=(15,10))
plt.title('Test Stack with Added Noise')
plt.axis('off')
plt.imshow(contrast_stretch(testStack[0]))
plt.show()
No description has been provided for this image
InĀ [15]:
# Viewing the Average Raw Image (Naive Stacking)
testMean = np.mean(testStack,axis=0)
# Plotting the Average
plt.figure(figsize=(15,10))
plt.title(f'Averaged Post-Processed Test Stack')
plt.imshow(contrast_stretch(testMean))
plt.axis('off')
plt.show()
No description has been provided for this image
InĀ [16]:
# Viewing the Median Raw Image (Naive Stacking)
testMedian = np.median(testStack,axis=0)
# Plotting the Average
plt.figure(figsize=(15,10))
plt.title(f'Median Post-Processed Test Stack')
plt.imshow(contrast_stretch(testMedian))
plt.axis('off')
plt.show()
No description has been provided for this image

Real Images¶

As seen above, averaging the noisy image stack elimiantes the zero mean noise, that's because as many samples, it models the distribution of the noise, which is $\sigma(0,1)$. However, the assumption is the scene is static, which is not the case for the Earth's constant rotation.

Every day, the Earth approximately rotates by 360 degrees, translating to 0.25 degrees a minute. Naively stacking all the images leads to translational error, visually seen in the form of star trails as shown below.

InĀ [17]:
# Viewing the Average Raw Image (Naive Stacking)
stackedMean = np.mean(stackedNumpy,axis=0)
# Plotting the Average
plt.figure(figsize=(15,10))
plt.title(f'Naive Stacking by Mean')
plt.imshow(contrast_stretch(stackedMean))
plt.axis('off')
plt.show()
No description has been provided for this image
InĀ [18]:
# Viewing the Median Raw Image (Naive Stacking)
stackedMedian = np.median(stackedNumpy,axis=0)
# Plotting the Median
plt.figure(figsize=(15,10))
plt.title(f'Naive Stacking by Median')
plt.imshow(contrast_stretch(stackedMedian))
plt.axis('off')
plt.show()
No description has been provided for this image

Image Registration¶

In order to correctly stack many astrophotography long exposures, a preprocessing operation of image registration needs to occur to align the images. ORB Image registration finds common keypoints between an image and a reference, estimates the transformation matrix, and maps the new pixels with respect to the reference to correct rotation [2]. This will mitigate the prescence of star trails and allow us to accurately stack our images for a nice celestial object!

InĀ [19]:
# Registers a Float Img (img2) with Respect to a Reference (img1)
def register_image(img1,img2,numFeatures=5000,match=0.9,refill=True):
    # Convert to uint8 grayscale.
    gray1 = cv.cvtColor(float_to_int(img1), cv.COLOR_BGR2GRAY)
    gray2 = cv.cvtColor(float_to_int(img2), cv.COLOR_BGR2GRAY)
    height, width = gray1.shape
    
    # Create ORB detector
    orb_detector = cv.ORB_create(numFeatures)
    
    # Find keypoints and descriptors.
    # The first arg is the image, second arg is the mask
    #  (which is not required in this case).
    kp1, d1 = orb_detector.detectAndCompute(gray1, None)
    kp2, d2 = orb_detector.detectAndCompute(gray2, None)
    
    # Match features between the two images.
    # We create a Brute Force matcher with 
    # Hamming distance as measurement mode.
    matcher = cv.BFMatcher(cv.NORM_HAMMING, crossCheck = True)
    
    # Match the two sets of descriptors.
    matches = list(matcher.match(d1, d2))

    
    # Sort matches on the basis of their Hamming distance.
    matches.sort(key = lambda x: x.distance)
    
    # Take the top matches forward.
    matches = matches[:int(len(matches)*match)]
    no_of_matches = len(matches)
    
    # Define empty matrices of shape no_of_matches * 2.
    p1 = np.zeros((no_of_matches, 2))
    p2 = np.zeros((no_of_matches, 2))
    
    for i in range(len(matches)):
      p1[i, :] = kp1[matches[i].queryIdx].pt
      p2[i, :] = kp2[matches[i].trainIdx].pt
    
    # Find the homography matrix.
    homography, mask = cv.findHomography(p1, p2, cv.RANSAC)
    
    # Use this matrix to transform the
    # colored image wrt the reference image.
    transformed_img = cv.warpPerspective(img1,
                        homography, (width, height))

    if(refill): 
        transformed_img = np.where(transformed_img != 0, transformed_img, img2) 
    
    return transformed_img

# Stacks Images with Automatic Alignment
def stack_images(imgStack,method='mean',align=True,refIndex=0.5,refill=True):
    if align:
        refIndex = len(imgStack) // 2 if refIndex == 0.5 else refIndex
        newImgs = []
        for i in tqdm(range(imgStack.shape[0])): 
            newImgs.append(normalize(imgStack[i]) if i == refIndex else register_image(imgStack[i],imgStack[refIndex],refill=refill))
        imgStack = np.stack(newImgs)
    stackedImg = np.median(imgStack,axis=0) if method == 'med' else np.mean(imgStack,axis=0)
    return stackedImg

# Stack Images with Automatic Alignment (Memory Saver with Mean Method)
def stack_images_opt(stackFiles,gamma=(1,1),use_camera_wb=False,half_size=True,no_auto_bright=False,align=True,refIndex=0.5,refill=True,stackedDark=None):
    stackedImg = 0
    rawSettings = (False,gamma,half_size,use_camera_wb,no_auto_bright)
    if align:
        refIndex = len(stackFiles) // 2 if refIndex == 0.5 else refIndex
        refImg = np.clip(cr2_to_numpy(stackFiles[refIndex],*rawSettings) - (stackedDark if stackedDark is not None else 0),a_min=0,a_max=1)
        for i in tqdm(range(len(stackFiles))):
            if i == refIndex:
                stackedImg += refImg/len(stackFiles) 
            else: 
                capImg = np.clip(cr2_to_numpy(stackFiles[i],*rawSettings) - (stackedDark if stackedDark is not None else 0),a_min=0,a_max=1)
                stackedImg += (register_image(capImg,refImg,refill=refill) if align else capImg)/len(stackFiles)
        gc.collect()
    return stackedImg
InĀ [20]:
# Stacking the Astro Image
stackedFiles = sorted([f'{stackPath}/{file}' for file in os.listdir(stackPath) if file.endswith('.CR2')])
stackedImg = stack_images_opt(stackedFiles,use_camera_wb=True)
100%|ā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆ| 11/11 [00:06<00:00,  1.65it/s]
InĀ [21]:
# Plotting the Stacked Image
plt.figure(figsize=(15,10))
plt.title(f'Mean Stacking with ORB Alignment')
plt.imshow(contrast_stretch(stackedImg))
plt.axis('off')
plt.show()
No description has been provided for this image
InĀ [22]:
# Writing the Final Result
cv.imwrite(f'{dataDir1}/stackedImg.tiff',stackedImg[...,::-1].astype(np.float32),[cv.IMWRITE_TIFF_COMPRESSION, 0])
cv.imwrite(f'{dataDir1}/stackedImg16.tiff',float_to_int16(stackedImg)[...,::-1])
cv.imwrite(f'{dataDir1}/stackedImg.jpg',float_to_int(stackedImg)[...,::-1])
print('Saving the Final Milky Way Result')
Saving the Final Milky Way Result

Now with Image Registration, the Astrophoto stack was succesfully merged using Python! Feel free to apply any postprocessing tactics to make the images look nice!

InĀ [23]:
# Freeing Up Old Tensors for the Next Experiment
del singleRaw,testScene,testNoise,testStack,testMean,testMedian,stackedNumpy,stackedImg
gc.collect()
print('Freeing up Memory for the next Experiment')
Freeing up Memory for the next Experiment

Another Example: Andromeda Galaxy (M31)¶

We will implement the full image stacking pipeline on Andromeda Galaxy (M31) data! This data has been provided by Jerry Lodriguss from AstroPix.

InĀ [24]:
# Creating Directory to Store the Data
dataDir2 = f'../data/imagestacking/astropix'
os.makedirs(dataDir2,exist_ok=True)
InĀ [25]:
# Downloading M31 Zip 
m31Link = 'https://drive.google.com/uc?id=19MZpu3mdGXuPvOXg_SIX-Ff57My3Wx8n'
m31Zip = f'{dataDir2}/M31.zip'
m31Dir = f'{dataDir2}/M31'
if(not(os.path.exists(m31Zip))):
    gdown.download(m31Link,output=m31Zip)
if(not(os.path.exists(m31Dir))):
    os.system(f'unzip {m31Zip} -d {m31Dir}')
InĀ [26]:
# Extracting Single Raw Image & Viewing
singleRawFile = 'IMG_0702.CR2'
m31StackDir = f'{m31Dir}/M31 RAW 48F ISO800 8min'
singleRawPath = f'{m31StackDir}/{singleRawFile}'
singleRaw = cr2_to_numpy(singleRawPath,use_camera_wb=False,gamma=(3,3),no_auto_bright=True)
print(f'Single Raw Numpy Shape: {singleRaw.shape}')
print(f'Single Raw Numpy Data Type: {singleRaw.dtype}')
cv.imwrite(f"{dataDir2}/{singleRawFile.split('.CR2')[0]}.tiff",float_to_int16(singleRaw)[...,::-1])

# Plotting
plt.figure(figsize=(15,10))
plt.title(f'M31 Single Raw: {singleRawFile}')
plt.imshow(contrast_stretch(singleRaw))
plt.axis('off')
plt.show()
Single Raw Numpy Shape: (1738, 2604, 3)
Single Raw Numpy Data Type: float64
No description has been provided for this image
InĀ [27]:
# Getting the Stacked Image
m31StackFiles = sorted([f'{m31StackDir}/{file}' for file in os.listdir(m31StackDir) if file.endswith('.CR2')])
stackedImg = stack_images_opt(m31StackFiles,gamma=(3,3),use_camera_wb=False,no_auto_bright=True)
100%|ā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆ| 8/8 [00:03<00:00,  2.48it/s]
InĀ [28]:
# Plotting the Stacked Image
plt.figure(figsize=(15,10))
plt.title(f'M31 Stacked Image')
plt.imshow(contrast_stretch(stackedImg))
plt.axis('off')
plt.show()
No description has been provided for this image
InĀ [31]:
# Writing the Final Result
cv.imwrite(f'{dataDir2}/stackedImg.tiff',stackedImg[...,::-1].astype(np.float32),[cv.IMWRITE_TIFF_COMPRESSION,0])
cv.imwrite(f'{dataDir2}/stackedImg16.tiff',float_to_int16(stackedImg)[...,::-1])
cv.imwrite(f'{dataDir2}/stackedImg.png',float_to_int(stackedImg)[...,::-1])
print('Saving the Final M31 Result')
Saving the Final M31 Result

Incorporating Dark Calibration¶

In advanced astrophotography, an astronmer would usually take a set of calibration "dark" images. Dark images are defined as frames that are captured with the lens cap on to only capture the noise with the same camera settings (i.e., same ISO, apeture, shutter speed, etc.). Since the scene is assumed to be zero, the dark images is modeled as: $$I_{dark}(x,y) = n(x,y)$$

For this next section, we will use another dataset provided by Trevor Jones from AstroBackyard. He captures a really nice shot of the North American Nebula with Class 4 Bortle Skies with a Canon EOS Ra Sky-Walker Star Adventurer Star Tracker tracking the movement of the stars (mitigating image registration).

For ease of download, I provided the link for each raw image on Google Drive in a text file.

InĀ [57]:
# Creating Directory to Store the Data
dataDir3 = '../data/imagestacking/astrobackyard'
os.makedirs(dataDir3,exist_ok=True)
InĀ [58]:
# Downloading the AstroBackyard Dataset
astroBackyardDir = f'{dataDir3}/AstroBackyardNorthAmericanNebula'
lightDir = f'{astroBackyardDir}/LIGHTS'
darkDir = f'{astroBackyardDir}/DARKS'
if(not(os.path.exists(astroBackyardDir))):
    if(IN_COLAB): # Downloading GDrive Links for Each Photo
        !curl https://raw.githubusercontent.com/dotimothy/astronomy/main/analysis/AstroBackyardNorthAmericanNebulaLightLinks.txt -o ./AstroBackyardNorthAmericanNebulaLightLinks.txt
        !curl https://raw.githubusercontent.com/dotimothy/astronomy/main/analysis/AstroBackyardNorthAmericanNebulaDarkLinks.txt -o ./AstroBackyardNorthAmericanNebulaDarkLinks.txt
    with open('./AstroBackyardNorthAmericanNebulaLightLinks.txt','r') as lightLinks: # Downloading Light Images
        os.makedirs(lightDir,exist_ok=True)
        print(f'Downloading AstroBackyard Light Images')
        for line in tqdm(lightLinks.readlines()):
            lightLink = line.strip('\n')
            gdown.download(lightLink,output=lightDir,quiet=True)
    with open('./AstroBackyardNorthAmericanNebulaDarkLinks.txt','r') as darkLinks: # Downloading Dark Images
        os.makedirs(darkDir,exist_ok=True)
        print(f'Downloading Astrobackyard Dark Images')
        for line in tqdm(darkLinks.readlines()):
            darkLink = line.strip('\n')
            gdown.download(darkLink,output=darkDir,quiet=True)
InĀ [59]:
testLight = cr2_to_numpy(f'{lightDir}/1R9A3692.CR3',use_camera_wb=False,no_auto_bright=True)
print(f'Test AstroBackyard Light Shape: {testLight.shape}')
print(f'Test AstroBackyard Data Type: {testLight.dtype}')
plt.figure(figsize=(15,10))
plt.imshow(testLight)
plt.axis('off')
plt.title('AstroBackyard North American Nebula Light')
plt.show()
Test AstroBackyard Light Shape: (2249, 3371, 3)
Test AstroBackyard Data Type: float64
No description has been provided for this image
InĀ [60]:
# Test Dark
testDark = cr2_to_numpy(f'{darkDir}/1R9A3804.CR3',use_camera_wb=False,no_auto_bright=True)
print(f'Test AstroBackyard Dark Shape: {testDark.shape}')
print(f'Test AstroBackyard Data Type: {testDark.dtype}')
plt.figure(figsize=(15,10))
plt.imshow(testDark)
plt.axis('off')
plt.title('AstroBackyard North American Nebula Dark')
plt.show()
Test AstroBackyard Dark Shape: (2249, 3371, 3)
Test AstroBackyard Data Type: float64
No description has been provided for this image
InĀ [61]:
# Getting the Tensor to Only Stack the Lights
toExclude = [f'1R9A{3740+i}.CR3' for i in range(3768-3740+1)] # Excluded Misoriented Lights
lightStackFiles = sorted([f'{lightDir}/{file}' for file in os.listdir(lightDir) if (file not in toExclude and (file.endswith('.CR2') or file.endswith('.CR3')))])
darkStackFiles = sorted([f'{darkDir}/{file}' for file in os.listdir(darkDir) if (file.endswith('.CR2') or file.endswith('.CR3'))])            
InĀ [62]:
# Getting the Stacked Light (without Dark Calibration) 
stackedLight = stack_images_opt(lightStackFiles,use_camera_wb=False,no_auto_bright=True)
100%|ā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆ| 80/80 [00:52<00:00,  1.53it/s]
InĀ [63]:
# Plotting the Stacked Image without Dark Calibration
plt.figure(figsize=(15,10))
plt.title(f'AstroBackground Mean Stacking without Dark Calibration')
plt.imshow(contrast_stretch(stackedLight))
plt.axis('off')
plt.show()
No description has been provided for this image
InĀ [64]:
# Stacking the Darks
stackedDark = stack_images_opt(darkStackFiles,use_camera_wb=False,no_auto_bright=True)
100%|ā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆ| 14/14 [00:07<00:00,  1.84it/s]
InĀ [65]:
# Stacking Image with Dark Calibration
stackedImg = stack_images_opt(lightStackFiles,use_camera_wb=False,no_auto_bright=True,stackedDark=stackedDark)
100%|ā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆ| 80/80 [00:51<00:00,  1.56it/s]
InĀ [66]:
# Plotting the Stacked Image with Dark Calibration
plt.figure(figsize=(15,10))
plt.title(f'AstroBackground Mean Stacking with Dark Calibration')
plt.imshow(contrast_stretch(stackedImg))
plt.axis('off')
plt.show()
No description has been provided for this image
InĀ [67]:
# Writing the Final Result
cv.imwrite(f'{dataDir3}/stackedImg.tiff',stackedImg[...,::-1].astype(np.float32),[cv.IMWRITE_TIFF_COMPRESSION, 0])
cv.imwrite(f'{dataDir3}/stackedImg16.tiff',float_to_int16(stackedImg)[...,::-1])
cv.imwrite(f'{dataDir3}/stackedImg.png',float_to_int(stackedImg)[...,::-1])
print('Saving the Final North American Nebula Result')
Saving the Final North American Nebula Result

Custom Shots¶

Here are some results processed on some custom images. One scene in particular is taken during the great comet C/2023 A3 peak in Mt. Pinos by my buddy Sam Wagner from UCLA.

InĀ [68]:
# Custom Data Dir
customDataDir = '../data/imagestacking/custom'
os.makedirs(customDataDir,exist_ok=True)
InĀ [69]:
# Downloading Sam's Images
mtPinosDir = f'{customDataDir}/mtPinos'
mtPinosLink = f'https://drive.google.com/drive/folders/17u_iLlmqdeTJljULIXKjFjtoGfOrzZCO'
if(not(os.path.exists(mtPinosDir))):
    gdown.download_folder(mtPinosLink,output=mtPinosDir)
InĀ [70]:
testImgPath = f'{mtPinosDir}/DSC08770.ARW'
testImg = cr2_to_numpy(testImgPath,use_camera_wb=True,no_auto_bright=False)
plt.figure(figsize=(15,10))
plt.imshow(testImg)
plt.axis('off')
plt.title('Mt. Pinos Test Scene')
plt.show()
No description has been provided for this image
InĀ [71]:
# Stacking Custom Images
stackedFiles = sorted([f'{mtPinosDir}/{file}' for file in os.listdir(mtPinosDir) if file.endswith('.ARW')])
stackedImg = stack_images_opt(stackedFiles,use_camera_wb=True,no_auto_bright=False)
100%|ā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆ| 35/35 [00:16<00:00,  2.18it/s]
InĀ [72]:
# Plotting the Stacked Image
plt.figure(figsize=(15,10))
plt.title(f'Mt. Pinos Stacked Image')
plt.imshow(contrast_stretch(stackedImg))
plt.axis('off')
plt.show()
No description has been provided for this image
InĀ [74]:
# Writing the Final Result
cv.imwrite(f'{customDataDir}/stackedImg.tiff',stackedImg[...,::-1].astype(np.float32),[cv.IMWRITE_TIFF_COMPRESSION, 0])
cv.imwrite(f'{customDataDir}/stackedImg16.tiff',float_to_int16(stackedImg)[...,::-1])
cv.imwrite(f'{customDataDir}/stackedImg.png',float_to_int(stackedImg)[...,::-1])
print('Saving the Final Milky Way Result')
Saving the Final Milky Way Result