├── __init__.py ├── tests ├── __init__.py └── test_synthplane_gen.py ├── utils ├── __init__.py ├── make_dir.py ├── mpl_utils.py └── raster_io.py ├── requirements.txt ├── data ├── diff_ingram_test_deramped.jpeg ├── diff_ingram_test_deramped_fft.jpeg ├── diff_ingram_test_spectrum_fft.jpeg ├── diff_ingram_test_MAError_grid_search.jpeg ├── diff_ingram_test_input_field_vs_phase_ramp.jpeg └── diff_ingram_test_input_field_vs_phase_ramp_fft.jpeg ├── environment.yml ├── LICENSE ├── Dockerfile ├── .github └── workflows │ ├── python_test.yaml │ └── python_test_conda.yaml ├── .gitignore ├── README.md ├── remove_phase_ramp_fft.py └── remove_phase_ramp.py /__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /utils/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | tqdm 2 | numpy 3 | matplotlib 4 | rasterio 5 | pytest 6 | pytest-cov 7 | -------------------------------------------------------------------------------- /data/diff_ingram_test_deramped.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eciraci/InSAR_remove_phase_ramp/HEAD/data/diff_ingram_test_deramped.jpeg -------------------------------------------------------------------------------- /data/diff_ingram_test_deramped_fft.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eciraci/InSAR_remove_phase_ramp/HEAD/data/diff_ingram_test_deramped_fft.jpeg -------------------------------------------------------------------------------- /data/diff_ingram_test_spectrum_fft.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eciraci/InSAR_remove_phase_ramp/HEAD/data/diff_ingram_test_spectrum_fft.jpeg -------------------------------------------------------------------------------- /data/diff_ingram_test_MAError_grid_search.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eciraci/InSAR_remove_phase_ramp/HEAD/data/diff_ingram_test_MAError_grid_search.jpeg -------------------------------------------------------------------------------- /data/diff_ingram_test_input_field_vs_phase_ramp.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eciraci/InSAR_remove_phase_ramp/HEAD/data/diff_ingram_test_input_field_vs_phase_ramp.jpeg -------------------------------------------------------------------------------- /data/diff_ingram_test_input_field_vs_phase_ramp_fft.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eciraci/InSAR_remove_phase_ramp/HEAD/data/diff_ingram_test_input_field_vs_phase_ramp_fft.jpeg -------------------------------------------------------------------------------- /environment.yml: -------------------------------------------------------------------------------- 1 | name: InSAR_remove_phase_ramp 2 | channels: 3 | - conda-forge 4 | dependencies: 5 | - pip 6 | - tqdm 7 | - numpy 8 | - rasterio 9 | - matplotlib 10 | - pytest 11 | - pytest-cov 12 | -------------------------------------------------------------------------------- /utils/make_dir.py: -------------------------------------------------------------------------------- 1 | """ 2 | Enrico Ciraci 02/2022 3 | Create Directory at the selected location. 4 | """ 5 | import os 6 | 7 | 8 | def make_dir(abs_path: str, dir_name: str) -> str: 9 | """ 10 | Create directory 11 | :param abs_path: absolute path to the output directory 12 | :param dir_name: new directory name 13 | :return: absolute path to the new directory 14 | """ 15 | dir_to_create = os.path.join(abs_path, dir_name) 16 | if not os.path.exists(dir_to_create): 17 | os.mkdir(dir_to_create) 18 | return dir_to_create 19 | -------------------------------------------------------------------------------- /utils/mpl_utils.py: -------------------------------------------------------------------------------- 1 | """ 2 | Enrico Ciraci 03/2022 3 | Set of utility functions that can be used to generate figures with matplotlib. 4 | """ 5 | import matplotlib.pyplot as plt 6 | from mpl_toolkits.axes_grid1 import make_axes_locatable 7 | 8 | 9 | def add_colorbar(fig: plt.figure, ax: plt.Axes, 10 | im: plt.pcolormesh) -> plt.colorbar: 11 | """ 12 | Add colorbar to the selected plt.Axes. 13 | :param fig: plt.figure object 14 | :param ax: plt.Axes object. 15 | :param im: plt.pcolormesh object. 16 | :return: plt.colorbar 17 | """ 18 | divider = make_axes_locatable(ax) 19 | cax = divider.new_vertical(size='5%', pad=0.6, pack_start=True) 20 | fig.add_axes(cax) 21 | cb = fig.colorbar(im, cax=cax, orientation='horizontal') 22 | return cb 23 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Enrico Ciracì 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /tests/test_synthplane_gen.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import sys 3 | import numpy as np 4 | import pytest 5 | # add parent directory to system path 6 | sys.path.append('..') 7 | from remove_phase_ramp import synth_plane, estimate_phase_ramp 8 | 9 | # - Test Parameters 10 | slope_c = 1 11 | slope_r = -1 12 | n_cycle_c = 3.9 13 | n_cycle_r = 7.7 14 | n_columns = n_rows = 20 15 | xx_m, yy_m = np.meshgrid(np.arange(n_columns), np.arange(n_rows)) 16 | dd_phase_complex = synth_plane(slope_c, slope_r, n_columns, n_rows, 17 | n_cycle_c, n_cycle_r, xx_m, yy_m) 18 | 19 | 20 | def test_data_type(): 21 | # - Parameters 22 | with pytest.raises(TypeError): 23 | synth_complex = synth_plane(slope_c, slope_r, 7.6, n_rows, n_cycle_c, 24 | n_cycle_r, xx_m, yy_m) 25 | 26 | with pytest.raises(ValueError): 27 | synth_complex = synth_plane(slope_c, slope_r, n_columns, n_rows, 28 | n_cycle_c, n_cycle_r, 29 | xx_m, np.arange(n_columns)) 30 | 31 | 32 | def test_estimate_phase_ramp(): 33 | with pytest.raises(ValueError): 34 | estimate_phase_ramp(dd_phase_complex, n_cycle_r, n_cycle_c, slope_r=1, 35 | slope_c=1, s_radius=2, s_step=-0.1) 36 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Define the specific image the docker image is going to rely on 2 | FROM python:3.8-slim-buster 3 | 4 | # Label Docker file 5 | LABEL Enrico-Ciraci "eciraci@uci.edu" 6 | 7 | USER root 8 | RUN apt-get update && \ 9 | apt-get install -y \ 10 | build-essential \ 11 | python-all-dev \ 12 | libpq-dev \ 13 | libgeos-dev \ 14 | wget \ 15 | curl \ 16 | sqlite3 \ 17 | cmake \ 18 | libtiff-dev \ 19 | libsqlite3-dev \ 20 | libcurl4-openssl-dev \ 21 | pkg-config 22 | 23 | 24 | # Installing PROJ from source 25 | RUN curl https://download.osgeo.org/proj/proj-8.2.1.tar.gz | tar -xz &&\ 26 | cd proj-8.2.1 &&\ 27 | mkdir build &&\ 28 | cd build && \ 29 | cmake .. &&\ 30 | make && \ 31 | make install 32 | 33 | # Installing GDAL from source 34 | RUN wget http://download.osgeo.org/gdal/3.4.0/gdal-3.4.0.tar.gz 35 | RUN tar xvfz gdal-3.4.0.tar.gz 36 | WORKDIR ./gdal-3.4.0 37 | RUN ./configure --with-python --with-pg --with-geos &&\ 38 | make && \ 39 | make install && \ 40 | ldconfig 41 | 42 | 43 | # set the working directory 44 | WORKDIR /app 45 | 46 | # install dependencies 47 | COPY ./requirements.txt /app 48 | RUN pip install --no-cache-dir --upgrade -r requirements.txt 49 | 50 | # copy the scripts to the folder 51 | COPY . /app 52 | 53 | # run test with pytset 54 | CMD ["python", "-m", "pytest", "--import-mode=append", "tests/"] -------------------------------------------------------------------------------- /.github/workflows/python_test.yaml: -------------------------------------------------------------------------------- 1 | name: preliminary-ci-test-with-pip 2 | 3 | on: 4 | push: 5 | branches: [ dev ] 6 | 7 | jobs: 8 | build: 9 | runs-on: ${{ matrix.os }} 10 | strategy: 11 | matrix: 12 | os: [ ubuntu-latest, macos-latest, windows-latest] 13 | python-version: ['3.8', '3.9', '3.10'] 14 | steps: 15 | - uses: actions/checkout@v2 16 | - name: Set up Python 17 | uses: actions/setup-python@v2 18 | with: 19 | python-version: ${{ matrix.python-version }} 20 | 21 | - uses: actions/cache@v2 22 | with: 23 | path: ~/.cache/pip 24 | key: ${{ hashFiles('setup.py') }}-.-${{ hashFiles('requirements.txt') }} 25 | 26 | - name: Install dependencies for the considered OS 27 | run: | 28 | python -m pip install --upgrade pip 29 | pip install -r requirements.txt 30 | pip install flake8 pytest pytest-cov 31 | 32 | - name: Display Python version 33 | run: python --version 34 | 35 | - name: Lint with flake8 36 | run: | 37 | # stop the build if there are Python syntax errors or undefined names 38 | flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics 39 | # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide flake 40 | 41 | - name: Run Unit test with pytest 42 | run: | 43 | python -m pytest --import-mode=append tests/ -------------------------------------------------------------------------------- /.github/workflows/python_test_conda.yaml: -------------------------------------------------------------------------------- 1 | name: preliminary-ci-test-with-mambaforge 2 | 3 | on: 4 | push: 5 | branches: [ dev ] 6 | 7 | env: 8 | CACHE_NUMBER: 1 # increase to reset cache manually 9 | 10 | jobs: 11 | build-linux: 12 | strategy: 13 | matrix: 14 | include: 15 | - os: ubuntu-latest 16 | label: linux-64 17 | prefix: /usr/share/miniconda3/envs/my-env 18 | python-version: 3.7 19 | 20 | - os: ubuntu-latest 21 | label: linux-64b 22 | prefix: /usr/share/miniconda3/envs/my-env 23 | python-version: 3.8 24 | 25 | - os: ubuntu-latest 26 | label: linux-64b 27 | prefix: /usr/share/miniconda3/envs/my-env 28 | python-version: 3.9 29 | 30 | name: ${{ matrix.label }} 31 | runs-on: ${{ matrix.os }} 32 | steps: 33 | - uses: actions/checkout@v2 34 | - name: Setup Mambaforge 35 | uses: conda-incubator/setup-miniconda@v2 36 | with: 37 | miniforge-variant: Mambaforge 38 | miniforge-version: latest 39 | activate-environment: my-env 40 | use-mamba: true 41 | python-version: ${{ matrix.python-version }} 42 | 43 | - name: Set cache date 44 | run: echo "DATE=$(date +'%Y%m%d')" >> $GITHUB_ENV 45 | 46 | - uses: actions/cache@v2 47 | with: 48 | path: ${{ matrix.prefix }} 49 | key: ${{ matrix.label }}-conda-${{ hashFiles('environment.yml') }}-${{ env.DATE }}-${{ env.CACHE_NUMBER }} 50 | id: cache 51 | 52 | - name: Update environment 53 | run: mamba env update -n my-env -f environment.yml 54 | if: steps.cache.outputs.cache-hit != 'true' 55 | 56 | - name: Install Other Dependencies with PIP 57 | run: | 58 | python -m pip install --upgrade pip 59 | pip install flake8 60 | - name: Lint with flake8 61 | run: | 62 | # stop the build if there are Python syntax errors or undefined names 63 | flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics 64 | # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide 65 | flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics 66 | 67 | - name: Run Unit test with pytest 68 | shell: bash -l {0} 69 | run: | 70 | python -m pytest --import-mode=append tests/ -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # osx .DS_Store 2 | .DS_Store 3 | data/.DS_Store 4 | # pycharm settings files 5 | .idea/ 6 | 7 | # icloud files 8 | *.icloud 9 | 10 | # geotiff 11 | *.tiff 12 | 13 | # Qgis 14 | *.aux.xml 15 | 16 | # Byte-compiled / optimized / DLL files 17 | __pycache__/ 18 | *.py[cod] 19 | *$py.class 20 | 21 | # C extensions 22 | *.so 23 | 24 | # Distribution / packaging 25 | .Python 26 | build/ 27 | develop-eggs/ 28 | dist/ 29 | downloads/ 30 | eggs/ 31 | .eggs/ 32 | lib/ 33 | lib64/ 34 | parts/ 35 | sdist/ 36 | var/ 37 | wheels/ 38 | pip-wheel-metadata/ 39 | share/python-wheels/ 40 | *.egg-info/ 41 | .installed.cfg 42 | *.egg 43 | MANIFEST 44 | 45 | # PyInstaller 46 | # Usually these files are written by a python script from a template 47 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 48 | *.manifest 49 | *.spec 50 | 51 | # Installer logs 52 | pip-log.txt 53 | pip-delete-this-directory.txt 54 | 55 | # Unit test / coverage reports 56 | htmlcov/ 57 | .tox/ 58 | .nox/ 59 | .coverage 60 | .coverage.* 61 | .cache 62 | nosetests.xml 63 | coverage.xml 64 | *.cover 65 | *.py,cover 66 | .hypothesis/ 67 | .pytest_cache/ 68 | 69 | # Translations 70 | *.mo 71 | *.pot 72 | 73 | # Django stuff: 74 | *.log 75 | local_settings.py 76 | db.sqlite3 77 | db.sqlite3-journal 78 | 79 | # Flask stuff: 80 | instance/ 81 | .webassets-cache 82 | 83 | # Scrapy stuff: 84 | .scrapy 85 | 86 | # Sphinx documentation 87 | docs/_build/ 88 | 89 | # PyBuilder 90 | target/ 91 | 92 | # Jupyter Notebook 93 | .ipynb_checkpoints 94 | 95 | # IPython 96 | profile_default/ 97 | ipython_config.py 98 | 99 | # pyenv 100 | .python-version 101 | 102 | # pipenv 103 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 104 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 105 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 106 | # install all needed dependencies. 107 | #Pipfile.lock 108 | 109 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 110 | __pypackages__/ 111 | 112 | # Celery stuff 113 | celerybeat-schedule 114 | celerybeat.pid 115 | 116 | # SageMath parsed files 117 | *.sage.py 118 | 119 | # Environments 120 | .env 121 | .venv 122 | env/ 123 | venv/ 124 | ENV/ 125 | env.bak/ 126 | venv.bak/ 127 | 128 | # Spyder project settings 129 | .spyderproject 130 | .spyproject 131 | 132 | # Rope project settings 133 | .ropeproject 134 | 135 | # mkdocs documentation 136 | /site 137 | 138 | # mypy 139 | .mypy_cache/ 140 | .dmypy.json 141 | dmypy.json 142 | 143 | # Pyre type checker 144 | .pyre/ 145 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Estimate and Remove a Linear Phase Ramp from Differential Interferograms. 2 | 3 | [![Language][]][1] 4 | [![License][]][2] 5 | 6 | Estimate and remove the contribution of a *Linear Ramp* to the **Wrapped 7 | Phase** of the considered Differential InSAR Interferograms. 8 | 9 | image 10 | 11 | 12 | 13 | ## **Scripts**: 14 | 15 | 1. **remove_phase_ramp.py** - Find the Phase Ramp optimal parameters 16 | employing a Grid Search approach. A first guess of the ramp 17 | parameters - e.g. number of cycles along columns and rows - must be 18 | provided by the user. 19 | 20 | 2. **remove_phase_ramp_fft.py** - Estimate the Linear Phase Ramp in the 21 | Frequency Domain as the maximum value of the Power Spectrum of the 22 | Signal. 23 | 24 | ## **Scripts**: 25 | 26 | ### **Installation**: 27 | 28 | **Install Python Dependencies with Miniconda**: 29 | 30 | 1. Setup minimal **conda** installation using [Miniconda][5] 31 | 32 | 2. Create Python Virtual Environment 33 | - Creating an environment with commands ([Link][3]); 34 | - Creating an environment from an environment.yml file ([Link][4]); 35 | 36 | **Install Python Dependencies with pip**: 37 | 38 | > pip install -r requirements.txt 39 | 40 | **PYTHON DEPENDENCIES**: 41 | - [numpy: The fundamental package for scientific computing with Python.][] 42 | - [rasterio: access to geospatial raster data.][] 43 | - [matplotlib: Library for creating static, animated, and interactive visualizations in Python.][] 44 | - [tqdm: A Fast, Extensible Progress Bar for Python and CLI.][] 45 | 46 | # License 47 | 48 | The content of this project is licensed under the [Creative Commons 49 | Attribution 4.0 Attribution license][] and the source code is licensed 50 | under the [MIT license][]. 51 | 52 | [Language]: https://img.shields.io/badge/python%20-3.7%2B-brightgreen 53 | [1]: ..%20image::%20https://www.python.org/ 54 | [License]: https://img.shields.io/badge/license-MIT-green.svg 55 | [2]: https://github.com/eciraci/ee_insar_test/blob/main/LICENSE 56 | [3]: https://docs.conda.io/projects/conda/en/latest/user-guide/tasks/manage-environments.html#creating-an-environment-with-commands 57 | [4]: https://docs.conda.io/projects/conda/en/latest/user-guide/tasks/manage-environments.html#creating-an-environment-from-an-environment-yml-file 58 | [5]: https://docs.conda.io/en/latest/miniconda.html 59 | [numpy: The fundamental package for scientific computing with Python.]: 60 | https://numpy.org 61 | [rasterio: access to geospatial raster data.]: https://rasterio.readthedocs.io 62 | [matplotlib: Library for creating static, animated, and interactive visualizations in Python.]: 63 | https://matplotlib.org 64 | [tqdm: A Fast, Extensible Progress Bar for Python and CLI.]: https://github.com/tqdm/tqdm 65 | [Creative Commons Attribution 4.0 Attribution license]: https://creativecommons.org/licenses/by/4.0/ 66 | [MIT license]: LICENSE -------------------------------------------------------------------------------- /utils/raster_io.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """ 3 | Enrico Ciraci 02/2022 4 | Set of utility functions used to read/write raster data using Rasterio. 5 | Rasterio is and alternative GDAL’s Python bindings using more idiomatic Python 6 | types and protocols compared to the standard GDAL library. 7 | 8 | For more info about the Rasterio project see: 9 | https://rasterio.readthedocs.io/en/latest/ 10 | 11 | For more info about GDAL/OGR in Python see: 12 | https://gdal.org/api/python.html 13 | """ 14 | import rasterio 15 | from rasterio.transform import Affine 16 | from rasterio.enums import Resampling 17 | from rasterio import shutil as rio_shutil 18 | from rasterio.vrt import WarpedVRT 19 | import rasterio.mask 20 | import fiona 21 | import affine 22 | from pyproj import CRS 23 | import numpy as np 24 | from typing import Union 25 | 26 | 27 | def load_raster(in_path: str) -> dict: 28 | """ 29 | # - Load raster saved in GeoTiff format 30 | :param in_path: absolute path to input file 31 | :return: dictionary containing the input raster + ancillary info. 32 | """ 33 | with rasterio.open(in_path, mode='r+') as src: 34 | # - read band #1 35 | raster_input = src.read(1).astype(src.dtypes[0]) 36 | # - raster upper-left and lower-right corners 37 | ul_corner = src.transform * (0, 0) 38 | lr_corner = src.transform * (src.width, src.height) 39 | grid_res = src.res 40 | # - compute x- and y-axis coordinates 41 | x_coords = np.arange(ul_corner[0], lr_corner[0], grid_res[0]) 42 | y_coords = np.arange(lr_corner[1], ul_corner[1], grid_res[1]) 43 | # - compute raster extent - (left, right, bottom, top) 44 | extent = [ul_corner[0], lr_corner[0], lr_corner[1], ul_corner[1]] 45 | # - compute cell centroids 46 | x_centroids = x_coords + (grid_res[0]/2.) 47 | y_centroids = y_coords + (grid_res[1]/2.) 48 | # - rotate the output numpy array in such a way that 49 | # - the lower-left corner of the raster is considered 50 | # - the origin of the reference system. 51 | if src.transform.e < 0: 52 | raster_input = np.flipud(raster_input) 53 | # - Compute New Affine Transform 54 | transform = (Affine.translation(x_coords[0], y_coords[0]) 55 | * Affine.scale(src.res[0], src.res[1])) 56 | 57 | return{'data': raster_input, 'crs': src.crs, 'res': src.res, 58 | 'y_coords': y_coords, 'x_coords': x_coords, 59 | 'y_centroids': y_centroids, 'x_centroids': x_centroids, 60 | 'transform': transform, 'src_transform': src.transform, 61 | 'width': src.width, 'height': src.height, 'extent': extent, 62 | 'ul_corner': ul_corner, 'lr_corner': lr_corner, 63 | 'nodata': src.nodata, 'dtype': src.dtypes[0]} 64 | 65 | 66 | def save_raster(raster: np.ndarray, res: int, x: np.ndarray, 67 | y: np.ndarray, out_path: str, crs: int) -> None: 68 | """ 69 | Save the Provided Raster in GeoTiff format 70 | :param raster: input raster - np.ndarray 71 | :param res: raster resolution - integer 72 | :param x: x-axis - np.ndarray 73 | :param y: y-axis - np.ndarray 74 | :param crs: - coordinates reference system 75 | :param out_path: absolute path to output file 76 | :return: None 77 | """ 78 | # - Calculate Affine Transformation of the output raster 79 | if y[1] > y[0]: 80 | y = np.flipud(y) 81 | raster = np.flipud(raster) 82 | transform = (Affine.translation(x[0], y[0]) 83 | * Affine.scale(res, -res)) 84 | with rasterio.open(out_path, 'w', driver='GTiff', 85 | height=raster.shape[0], 86 | width=raster.shape[1], count=1, 87 | dtype=raster.dtype, crs=crs, 88 | transform=transform, 89 | nodata=-9999.) as dst: 90 | dst.write(raster, 1) 91 | 92 | 93 | def vrt_param(crs, res: int, bounds: list, 94 | resampling_alg: str, dtype: str) -> dict: 95 | """ 96 | Virtual Warp Parameters 97 | :param crs: destination coordinate reference system 98 | :param res: output x/y-resolution 99 | :param bounds: Interpolation Domain Boundaries 100 | :param resampling_alg: Interpolation Algorithm 101 | :param dtype: the working data type for warp operation and output. 102 | :return: dictionary containing vrt options. 103 | """ 104 | # - Re-projection Parameters 105 | dst_crs = CRS.from_epsg(crs) # - Destination CRS 106 | # - Output image transform 107 | xres = yres = res 108 | left, bottom, right, top = bounds 109 | dst_width = (right - left) / xres 110 | dst_height = (top - bottom) / yres 111 | # - Affine transformation matrix 112 | dst_transform = affine.Affine(xres, 0.0, left, 113 | 0.0, -yres, top) 114 | # - Virtual Warping Options 115 | vrt_options = { 116 | 'resampling': Resampling[resampling_alg], 117 | 'crs': dst_crs, 118 | 'transform': dst_transform, 119 | 'height': dst_height, 120 | 'width': dst_width, 121 | 'src_nodata': -9999, 122 | 'nodata': -9999, 123 | 'dtype': dtype, 124 | } 125 | return vrt_options 126 | 127 | 128 | def virtual_warp_rio(src_file: str, out_file: str, res: int = 250, 129 | crs: int = 3413, method: str = 'med', 130 | dtype=None) -> None: 131 | """ 132 | Rasterio Virtual Warp 133 | :param src_file: absolute path to source file 134 | :param out_file: absolute path to output file 135 | :param res: output resolution 136 | :param crs: output coordinate reference system 137 | :param method: resampling method 138 | :param dtype: output data type 139 | :return: None 140 | """ 141 | # - Define output grid - with regular step equal to the 142 | # - selected resolution 143 | dem_src = load_raster(src_file) 144 | # - raster upper - left and lower - right corners 145 | ul_corner_1 = dem_src['ul_corner'] 146 | lr_corner_1 = dem_src['lr_corner'] 147 | minx = int((ul_corner_1[0] // res) * res) - res 148 | miny = int((lr_corner_1[1] // res) * res) - res 149 | maxx = int((lr_corner_1[0] // res) * res) + res 150 | maxy = int((ul_corner_1[1] // res) * res) + res 151 | output_bounds = [minx, miny, maxx, maxy] 152 | 153 | with rasterio.open(src_file) as src: 154 | # - virtual Warp Parameters 155 | if dtype is None: 156 | # - if not selected, source data type 157 | dtype = src.dtypes[0] 158 | vrt_options = vrt_param(crs, res, 159 | output_bounds, method, dtype) 160 | with WarpedVRT(src, **vrt_options) as vrt: 161 | # Read all data into memory. 162 | data = vrt.read() 163 | # - Process the dataset in chunks. 164 | # - See Rasterio Documentation for more details. 165 | # - https://rasterio.readthedocs.io/en/latest 166 | # - /topics/virtual-warping.html 167 | for _, window in vrt.block_windows(): 168 | data = vrt.read(window=window) 169 | 170 | # - Save Reprojected Data 171 | rio_shutil.copy(vrt, out_file, driver='GTiff') 172 | 173 | 174 | def clip_raster(src_file: str, ref_shp: str, out_file: str) -> Union[str, None]: 175 | """ 176 | Clip Input Raster Using Rasterio. Find more info here: 177 | https://rasterio.readthedocs.io/en/latest/topics/masking-by-shapefile.html 178 | :param src_file: absolute path to input raster file 179 | :param ref_shp: absolute path to reference shapefile 180 | :param out_file: absolute path to output raster file 181 | :return: None 182 | """ 183 | # - Open Reference shapefile 184 | with fiona.open(ref_shp, 'r') as shapefile: 185 | shapes = [feature['geometry'] for feature in shapefile] 186 | 187 | # - Open Input Raster 188 | with rasterio.open(src_file) as src: 189 | out_raster, out_transform = rasterio.mask.mask(src, shapes, crop=True) 190 | out_meta = src.meta 191 | 192 | # - Define Output raster metadata 193 | out_meta.update({'driver': 'GTiff', 194 | 'height': out_raster.shape[1], 195 | 'width': out_raster.shape[2], 196 | 'transform': out_transform}) 197 | out_raster[out_raster == src.nodata] = np.nan 198 | 199 | if out_raster[np.isfinite(out_raster)].shape[0] == 0: 200 | return None 201 | # - Save clipped raster 202 | # - [only if valid data are found within the clipped area] 203 | out_raster[np.isnan(out_raster)] = -9999. 204 | with rasterio.open(out_file, 'w', **out_meta) as dest: 205 | dest.write(out_raster) 206 | -------------------------------------------------------------------------------- /remove_phase_ramp_fft.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | u""" 3 | remove_phase_ramp_fft.py 4 | Written by Enrico Ciraci' (03/2022) 5 | 6 | Estimate and Remove the contribution of a "Linear Ramp" to the Wrapped Phase 7 | of a Differential InSAR Interferogram. 8 | 9 | Estimate the Linear Phase Ramp in the Frequency Domain as the maximum value 10 | of the Power Spectrum of the Signal. 11 | 12 | COMMAND LINE OPTIONS: 13 | usage: remove_phase_ramp_fft.py [-h] path_to_intf 14 | 15 | Estimate and Remove Linear Phase Ramp characterizing the considered 16 | Differential Interferogram - Fast Fourier Based Approach. 17 | 18 | positional arguments: 19 | path_to_intf Absolute path to input interferogram. 20 | 21 | optional arguments: 22 | -h, --help show this help message and exit 23 | 24 | PYTHON DEPENDENCIES: 25 | argparse: Parser for command-line options, arguments and sub-commands 26 | https://docs.python.org/3/library/argparse.html 27 | numpy: The fundamental package for scientific computing with Python 28 | https://numpy.org/ 29 | matplotlib: library for creating static, animated, and interactive 30 | visualizations in Python. 31 | https://matplotlib.org 32 | rasterio: Access to geospatial raster data 33 | https://rasterio.readthedocs.io 34 | datetime: Basic date and time types 35 | https://docs.python.org/3/library/datetime.html#module-datetime 36 | 37 | UPDATE HISTORY: 38 | """ 39 | # - python dependencies 40 | from __future__ import print_function 41 | import argparse 42 | import numpy as np 43 | import rasterio 44 | import rasterio.mask 45 | import datetime 46 | import matplotlib.pyplot as plt 47 | # - program dependencies 48 | from utils.mpl_utils import add_colorbar 49 | 50 | 51 | def estimate_phase_ramp(igram_cpx: np.ndarray, 52 | row_pad: int = 0, col_pad: int = 0) -> dict: 53 | """ 54 | Estimate Phase Ramp as the maximum value of signal Power Spectrum 55 | :param igram_cpx: Interferogram as a complex array 56 | :param row_pad: padding rows - int 57 | :param col_pad: padding columns - int 58 | :return: dict 59 | """ 60 | # - Use Zero-Padding to increase resolution in the frequency domain 61 | igram_cpx = np.pad(igram_cpx, ((int(row_pad), int(row_pad)), 62 | (int(col_pad), int(col_pad))), 63 | constant_values=((0, 0), (0, 0))) 64 | # - Compute 2-D Fast Fourier Transform 65 | igram_fft = np.fft.fft2(igram_cpx) 66 | # - Compute Power Spectrum 67 | igram_pwr = np.abs(igram_fft) 68 | 69 | # - Find Power Spectrum Maximum Value 70 | igram_pwr[:, 0] = 0 71 | igram_pwr[0, :] = 0 72 | igram_pwr_nx = igram_pwr[:, :] 73 | index_t = np.where(igram_pwr_nx == np.max(igram_pwr_nx)) 74 | index_r = index_t[0][0] 75 | index_c = index_t[1][0] 76 | # - Generate Synthetic Phase Ramp 77 | est_synth = np.zeros(igram_pwr.shape, dtype=complex) 78 | est_synth[index_r, index_c] = igram_fft[index_r, index_c] 79 | phase_ramp = np.fft.ifft2(est_synth) 80 | 81 | # - Remove padding from the estimated phase ramp 82 | if row_pad == 0 and col_pad == 0: 83 | phase_ramp = phase_ramp[:, :] 84 | elif row_pad == 0 and col_pad != 0: 85 | phase_ramp = phase_ramp[:, col_pad:-col_pad] 86 | elif row_pad != 0 and col_pad == 0: 87 | phase_ramp = phase_ramp[row_pad:-row_pad, :] 88 | else: 89 | phase_ramp = phase_ramp[row_pad:-row_pad, col_pad:-col_pad] 90 | 91 | return{'phase_ramp': phase_ramp} 92 | 93 | 94 | def remove_phase_ramp(path_to_intf: str, row_pad: int = 0, 95 | col_pad: int = 0) -> dict: 96 | """ 97 | Estimate and Remove a phase ramp from the provided input interferogram 98 | :param path_to_intf: absolute path to input interferogram 99 | :param row_pad: add zero padding rows 100 | :param col_pad: add zero padding columns 101 | :return: Python dictionary containing estimated phase rampy and de-ramped 102 | interferogram. 103 | """ 104 | fig_format = 'jpeg' # - output figure format 105 | 106 | # - Read Input Raster 107 | with rasterio.open(path_to_intf, mode="r+") as dataset_c: 108 | # - Read Input Raster and Binary Mask 109 | intf_phase = np.array(dataset_c.read(1), 110 | dtype=dataset_c.dtypes[0]) 111 | # - Define Valid data mask 112 | raster_mask = np.array(dataset_c.read_masks(1), 113 | dtype=dataset_c.dtypes[0]) 114 | raster_mask[raster_mask == 255] = 1. 115 | raster_mask[raster_mask == 0] = np.nan 116 | 117 | # - Transform the Input Phase Field into a complex array 118 | # - Create unit-magnitude interferogram. 119 | dd_phase_complex = np.exp(1j * intf_phase).astype(np.complex64) 120 | # - Estimate Phase Ramp in the Frequency domain 121 | rmp = estimate_phase_ramp(dd_phase_complex, 122 | row_pad=row_pad, col_pad=col_pad) 123 | phase_ramp = rmp['phase_ramp'] 124 | # - Extract wrapped phase 125 | ingram_ramp = np.angle(phase_ramp) 126 | 127 | # - Remove the estimated phase ramp from the input phase field by 128 | # - computing the complex conjugate product between the input phase 129 | # - field and the estimated ramp. 130 | dd_phase_complex_corrected = np.angle(dd_phase_complex 131 | * np.conjugate(phase_ramp)) 132 | fig_1 = plt.figure(figsize=(8, 8)) 133 | ax_1 = fig_1.add_subplot(121) 134 | ax_1.set_title('Input Interferogram', weight='bold') 135 | im_1 = ax_1.pcolormesh(intf_phase * raster_mask, 136 | vmin=-np.pi, vmax=np.pi, 137 | cmap=plt.cm.get_cmap('jet')) 138 | cb_1 = add_colorbar(fig_1, ax_1, im_1) 139 | cb_1.set_label(label='Rad', weight='bold') 140 | cb_1.ax.set_xticks([-np.pi, 0, np.pi]) 141 | cb_1.ax.set_xticklabels([r'-$\pi$', '0', r'$\pi$']) 142 | ax_1.grid(color='m', linestyle='dotted', alpha=0.3) 143 | 144 | ax_2 = fig_1.add_subplot(122) 145 | ax_2.set_title('Estimated Phase Ramp', weight='bold') 146 | im_2 = ax_2.pcolormesh(ingram_ramp * raster_mask, 147 | cmap=plt.cm.get_cmap('jet')) 148 | cb_2 = add_colorbar(fig_1, ax_2, im_2) 149 | cb_2.set_label(label='Rad', weight='bold') 150 | cb_2.ax.set_xticks([-np.pi, 0, np.pi]) 151 | cb_2.ax.set_xticklabels([r'-$\pi$', '0', r'$\pi$']) 152 | ax_2.grid(color='m', linestyle='dotted', alpha=0.3) 153 | plt.tight_layout() 154 | # - save output figure 155 | out_intf = path_to_intf.replace('.tiff', 156 | '_input_field_vs_phase_ramp_fft.' 157 | + fig_format) 158 | plt.savefig(out_intf, dpi=200, format=fig_format) 159 | plt.close() 160 | 161 | # - Compare Input with corrected Interferogram 162 | fig_3 = plt.figure(figsize=(8, 8)) 163 | ax_3 = fig_3.add_subplot(121) 164 | ax_3.set_title('Input Interferogram', weight='bold') 165 | im_3a = ax_3.pcolormesh(intf_phase * raster_mask, 166 | vmin=-np.pi, vmax=np.pi, 167 | cmap=plt.cm.get_cmap('jet')) 168 | cb_3a = add_colorbar(fig_3, ax_3, im_3a) 169 | cb_3a.set_label(label='Rad', weight='bold') 170 | cb_3a.ax.set_xticks([-np.pi, 0, np.pi]) 171 | cb_3a.ax.set_xticklabels([r'-$\pi$', '0', r'$\pi$']) 172 | ax_3.grid(color='m', linestyle='dotted', alpha=0.3) 173 | 174 | ax_3 = fig_3.add_subplot(122) 175 | ax_3.set_title('Input Phase Field - Phase Ramp', weight='bold') 176 | im_3b = ax_3.pcolormesh(dd_phase_complex_corrected * raster_mask, 177 | cmap=plt.cm.get_cmap('jet')) 178 | cb_3b = add_colorbar(fig_3, ax_3, im_3b) 179 | cb_3b.set_label(label='Rad', weight='bold') 180 | cb_3b.ax.set_xticks([-np.pi, 0, np.pi]) 181 | cb_3b.ax.set_xticklabels([r'-$\pi$', '0', r'$\pi$']) 182 | ax_3.grid(color='m', linestyle='dotted', alpha=0.3) 183 | plt.tight_layout() 184 | # - save output figure 185 | out_fig_3 = path_to_intf.replace('.tiff', '_deramped_fft.' + fig_format) 186 | plt.savefig(out_fig_3, dpi=200, format=fig_format) 187 | plt.close() 188 | 189 | # - Compare input and output interferogram spectrum 190 | igram_fft = np.fft.fft2(dd_phase_complex) 191 | est_corr_fft = np.fft.fft2(dd_phase_complex_corrected) 192 | plt.figure(figsize=(8, 6)) 193 | plt.subplot(1, 2, 1) 194 | plt.title('Input Interf Power Spectrum\nmagnitude - log', size=9) 195 | plt.imshow(np.log(np.abs(igram_fft)), cmap=plt.get_cmap('jet')) 196 | plt.subplot(1, 2, 2) 197 | plt.title('Ourput Interf Power Spectrumm\nmagnitude - log', size=9) 198 | plt.imshow(np.log(np.abs(est_corr_fft)), cmap=plt.get_cmap('jet')) 199 | plt.tight_layout() 200 | # - save output figure 201 | out_fig_4 = path_to_intf.replace('.tiff', '_spectrum_fft.' + fig_format) 202 | plt.savefig(out_fig_4, dpi=200, format=fig_format) 203 | plt.close() 204 | 205 | # - Save the de-ramped interferogram in Geotiff format 206 | dd_phase_complex_corrected[np.isnan(raster_mask)] = -9999 207 | with rasterio.open(path_to_intf, mode='r+') as dataset_c: 208 | o_transform = dataset_c.transform 209 | o_crs = dataset_c.crs 210 | 211 | dd_phase_complex_corrected = np.array(dd_phase_complex_corrected, 212 | dtype=dataset_c.dtypes[0]) 213 | out_f_name = path_to_intf.replace('.tiff', '_deramped_fft.tiff') 214 | with rasterio.open(out_f_name, 'w', driver='GTiff', 215 | height=dd_phase_complex_corrected.shape[0], 216 | width=dd_phase_complex_corrected.shape[1], 217 | count=1, dtype=rasterio.float32, 218 | crs=o_crs, transform=o_transform, 219 | nodata=-9999) as dst: 220 | dst.write(dd_phase_complex_corrected, 1) 221 | 222 | # - 223 | return{'synth_phase_ramp': phase_ramp, 224 | 'dd_phase': dd_phase_complex_corrected} 225 | 226 | 227 | def main(): 228 | """ 229 | Main: Estimate and Remove Linear Phase Ramp from input Differential 230 | Interferogram - Fast Fourier Based Approach. 231 | """ 232 | # - Read the system arguments listed after the program 233 | parser = argparse.ArgumentParser( 234 | description="""Estimate and Remove Linear Phase Ramp characterizing 235 | the considered Differential Interferogram - 236 | Fast Fourier Based Approach. 237 | """ 238 | ) 239 | # - Positional Arguments 240 | parser.add_argument('path_to_intf', type=str, 241 | help='Absolute path to input interferogram.') 242 | # - Zero Padding 243 | parser.add_argument('--row_pad', '-R', 244 | type=int, default=0, 245 | help='Zero Padding - Rows.') 246 | parser.add_argument('--col_pad', '-C', 247 | type=int, default=0, 248 | help='Zero Padding - Columns.') 249 | 250 | args = parser.parse_args() 251 | 252 | # - Processing Parameters 253 | path_to_intf = args.path_to_intf 254 | remove_phase_ramp(path_to_intf, row_pad=args.row_pad, col_pad=args.col_pad) 255 | 256 | 257 | if __name__ == '__main__': 258 | start_time = datetime.datetime.now() 259 | main() 260 | end_time = datetime.datetime.now() 261 | print(f'# - Computation Time: {end_time - start_time}') 262 | -------------------------------------------------------------------------------- /remove_phase_ramp.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | u""" 3 | remove_phase_ramp.py 4 | Written by Enrico Ciraci' (03/2022) 5 | 6 | Estimate and Remove the contribution of a "Linear Ramp" to the Wrapped Phase 7 | of a Differential InSAR Interferogram. 8 | 9 | NOTE: In this implementation of the algorithm, a first guess or preliminary 10 | estimate of the parameters defining the ramp must be provided by the user. 11 | These parameters include the number of phase cycles characterizing the ramp in 12 | the X and Y (columns and rows) directions of the input raster. 13 | 14 | A GRID SEARCH around the user-defined first guess is performed to obtain the 15 | best estimate of the ramp parameters. 16 | 17 | NOTE: To perform the GRID SEARCH optimization, set the search radius 18 | parameter to a value bigger than zero. See: --s_radius, -R S_RADIUS 19 | 20 | 21 | COMMAND LINE OPTIONS: 22 | usage: remove_phase_ramp.py [-h] [--slope {-1,1}] [--s_radius S_RADIUS] 23 | [--s_step S_STEP] path_to_intf cycle_r cycle_c 24 | 25 | Estimate and Remove Linear Phase Ramp characterizing the 26 | considered Differential Interferogram. 27 | 28 | Positional arguments: 29 | path_to_intf Absolute path to input interferogram. 30 | cycle_r Number of Cycles -> Rows-axis. 31 | cycle_c Number of Cycles -> Columns-axis. 32 | 33 | Optional arguments: 34 | -h, --help show this help message and exit 35 | --slope {-1,1}, -S {-1,1} Ramp Slope. 36 | --slope_r {-1,1} Phase Ramp Slope -> Rows-axis. 37 | --slope_c {-1,1} Phase Ramp Slope -> Columns-axis. 38 | 39 | --s_radius S_RADIUS, -R S_RADIUS 40 | Grid Search Radius around the provided reference 41 | -> Default 0. 42 | 43 | --s_step S_STEP, -T S_STEP 44 | Grid Search Step 45 | 46 | PYTHON DEPENDENCIES: 47 | argparse: Parser for command-line options, arguments and sub-commands 48 | https://docs.python.org/3/library/argparse.html 49 | numpy: The fundamental package for scientific computing with Python 50 | https://numpy.org/ 51 | matplotlib: library for creating static, animated, and interactive 52 | visualizations in Python. 53 | https://matplotlib.org 54 | rasterio: Access to geospatial raster data 55 | https://rasterio.readthedocs.io 56 | datetime: Basic date and time types 57 | https://docs.python.org/3/library/datetime.html#module-datetime 58 | tqdm: Progress Bar in Python. 59 | https://tqdm.github.io/ 60 | 61 | UPDATE HISTORY: 62 | """ 63 | # - - python dependencies 64 | from __future__ import print_function 65 | import argparse 66 | import numpy as np 67 | import rasterio 68 | import rasterio.mask 69 | import datetime 70 | import matplotlib as mpl 71 | import matplotlib.pyplot as plt 72 | from tqdm import tqdm 73 | # - program dependencies 74 | from utils.mpl_utils import add_colorbar 75 | 76 | 77 | def synth_plane(slope_c: int, slope_r: float, n_columns: int, n_rows: int, 78 | n_cycle_c: float, n_cycle_r: float, xx_m: np.ndarray, 79 | yy_m: np.ndarray) -> np.ndarray: 80 | """ 81 | Return phase ramp generated with the selected parameters 82 | :param slope_c: phase slope, columns - x-axis [1, -1] 83 | :param slope_r: phase slope, rows - y-axis [1, -1] 84 | :param n_columns: phase plan number of columns 85 | :param n_rows: phase plane number of rows 86 | :param n_cycle_c: number of cycles, columns - x-axis 87 | :param n_cycle_r: number of cycles, rows - y-axis 88 | :param xx_m: domain x-grid 89 | :param yy_m: domain y-grid 90 | :return: synthetic phase plan 91 | """ 92 | if not isinstance(n_columns, int) or not isinstance(n_rows, int): 93 | raise TypeError("n_columns and n_rows must be integers") 94 | if xx_m.shape != yy_m.shape: 95 | raise ValueError("xx_m and yy_m must have the same shape") 96 | synth_real = slope_c * (2 * np.pi / n_columns) * n_cycle_c * xx_m 97 | synth_imag = slope_r * (2 * np.pi / n_rows) * n_cycle_r * yy_m 98 | synth_phase_plane = synth_real + synth_imag 99 | synth_complex = np.exp(1j * synth_phase_plane) 100 | 101 | return synth_complex 102 | 103 | 104 | def estimate_phase_ramp(dd_phase_complex: np.ndarray, 105 | cycle_r: float, cycle_c: float, 106 | slope_r: int = 1, slope_c: int = 1, 107 | s_radius: float = 2, s_step: float = 0.1) -> dict: 108 | """ 109 | Estimate a phase ramp from the provided input interferogram 110 | :param dd_phase_complex: interferogram phase expressed as complex array 111 | :param cycle_r: phase ramp number of cycles along rows 112 | :param cycle_c: phase ramp number of cycles along columns 113 | :param slope_r: phase ramp slope sign - rows axis 114 | :param slope_c: phase ramp slope sign - columns axis 115 | :param s_radius: grid search domain radius 116 | :param s_step: grid search step 117 | :return: Python dictionary containing the results of the grid search 118 | """ 119 | if s_radius <= 0: 120 | raise ValueError("Search Radius [s_radius] must be a positive value.") 121 | if s_step <= 0: 122 | raise ValueError("Search Step [s_step] must be a positive value.") 123 | # - Generate synthetic field domain 124 | array_dim = dd_phase_complex.shape 125 | n_rows = array_dim[0] 126 | n_columns = array_dim[1] 127 | raster_mask = np.ones(array_dim) 128 | raster_mask[np.isnan(dd_phase_complex)] = 0 129 | 130 | # - Integration Domain used to define the phase ramp 131 | xx_m, yy_m = np.meshgrid(np.arange(n_columns), np.arange(n_rows)) 132 | 133 | if cycle_r - s_radius <= 0: 134 | n_cycle_r_vect_f = np.arange(s_step, cycle_r + s_radius + s_step, 135 | s_step) 136 | else: 137 | n_cycle_r_vect_f = np.arange(cycle_r - s_radius, 138 | cycle_r + s_radius + s_step, 139 | s_step) 140 | if cycle_c - s_radius <= 0: 141 | n_cycle_c_vect_f = np.arange(s_step, cycle_c + s_radius + s_step, 142 | s_step) 143 | else: 144 | n_cycle_c_vect_f = np.arange(cycle_c - s_radius, 145 | cycle_c + s_radius + s_step, 146 | s_step) 147 | 148 | # - Create Grid Search Domain 149 | n_cycle_c_vect_f_xx, n_cycle_r_vect_f_yy \ 150 | = np.meshgrid(n_cycle_c_vect_f, n_cycle_r_vect_f) 151 | error_array_f = np.zeros([len(n_cycle_r_vect_f), len(n_cycle_c_vect_f)]) 152 | 153 | for r_count, n_cycle_r in tqdm(enumerate(list(n_cycle_r_vect_f)), 154 | total=len(n_cycle_r_vect_f), ncols=60): 155 | for c_count, n_cycle_c in enumerate(list(n_cycle_c_vect_f)): 156 | synth_complex = synth_plane(slope_c, slope_r, n_columns, n_rows, 157 | n_cycle_c, n_cycle_r, xx_m, yy_m) 158 | 159 | # - Compute Complex Conjugate product between the synthetic phase 160 | # - ramp and the input interferogram. 161 | dd_phase_complex_corrected \ 162 | = np.angle(dd_phase_complex * np.conj(synth_complex)) 163 | # - Compute the Mean Absolute value of the phase residuals 164 | # - > Mean Absolute Error 165 | error = np.abs(dd_phase_complex_corrected) 166 | mae = np.nansum(error) / np.nansum(raster_mask) 167 | error_array_f[r_count, c_count] = mae 168 | 169 | return{'error_array_f': error_array_f, 170 | 'xx_m': xx_m, 'yy_m': yy_m, 171 | 'n_cycle_c_vect_f': n_cycle_c_vect_f, 172 | 'n_cycle_c_vect_f_xx': n_cycle_c_vect_f_xx, 173 | 'n_cycle_r_vect_f': n_cycle_r_vect_f, 174 | 'n_cycle_r_vect_f_yy': n_cycle_r_vect_f_yy 175 | } 176 | 177 | 178 | def remove_phase_ramp(path_to_intf: str, cycle_r: int, cycle_c: int, 179 | slope_r: int = 1, slope_c: int = 1, 180 | s_radius: float = 2, s_step: float = 0.1) -> dict: 181 | """ 182 | Estimate and Remove a phase ramp from the provided input interferogram 183 | :param path_to_intf: absolute path to input interferogram 184 | :param cycle_r: phase ramp number of cycles along rows 185 | :param cycle_c: phase ramp number of cycles columns 186 | :param slope_r: phase ramp slope sign - rows axis 187 | :param slope_c: phase ramp slope sign - columns axis 188 | :param s_radius: grid search domain radius 189 | :param s_step: grid search step 190 | :return: Python dictionary containing estimated phase rampy 191 | and de-ramped interferogram. 192 | """ 193 | print('# - Provided First Guess:') 194 | print(f'# - Num. Cycles -> Rows : {cycle_r}') 195 | print(f'# - Num. Cycles -> Columns : {cycle_c}\n') 196 | 197 | # - Figure Parameters - Not Editable 198 | fig_size1 = (10, 8) 199 | fig_size2 = (8, 8) 200 | fig_format = 'jpeg' 201 | 202 | # - Read Input Raster 203 | with rasterio.open(path_to_intf, mode="r+") as dataset_c: 204 | # - Read Input Raster and Binary Mask 205 | clipped_raster = np.array(dataset_c.read(1), 206 | dtype=dataset_c.dtypes[0]) 207 | # - Define Valid data mask 208 | raster_mask = np.array(dataset_c.read_masks(1), 209 | dtype=dataset_c.dtypes[0]) 210 | raster_mask[raster_mask == 255] = 1. 211 | raster_mask[raster_mask == 0] = np.nan 212 | 213 | # - Transform the Input Phase Field into a complex array 214 | dd_phase_complex = np.exp(1j * clipped_raster) 215 | 216 | # - Generate synthetic field domain 217 | array_dim = dd_phase_complex.shape 218 | n_rows = array_dim[0] 219 | n_columns = array_dim[1] 220 | # - Initialize Number of Cycles Minimum 221 | n_cycle_r_min = cycle_r 222 | n_cycle_c_min = cycle_c 223 | 224 | if s_radius > 0: 225 | print('# - Running Grid Search.') 226 | e_ramp = estimate_phase_ramp(dd_phase_complex, cycle_r, cycle_c, 227 | slope_r=slope_r, slope_c=slope_c, 228 | s_radius=s_radius, s_step=s_step) 229 | xx_m = e_ramp['xx_m'] # - error domain x-grid 230 | yy_m = e_ramp['yy_m'] # - error domain y-grid 231 | # - error domain N. cycles X direction 232 | n_cycle_c_vect_f_xx = e_ramp['n_cycle_c_vect_f_xx'] 233 | n_cycle_c_vect_f = e_ramp['n_cycle_c_vect_f'] 234 | # - error domain N. cycles Y direction 235 | n_cycle_r_vect_f_yy = e_ramp['n_cycle_r_vect_f_yy'] 236 | n_cycle_r_vect_f = e_ramp['n_cycle_r_vect_f'] 237 | # - Mean Absolute Error Grid 238 | error_array_f = e_ramp['error_array_f'] 239 | 240 | # - Find location of the Minimum Absolute Error Value 241 | ind_min = np.where(error_array_f == np.nanmin(error_array_f)) 242 | n_cycle_c_min = np.round(n_cycle_c_vect_f[ind_min[1]][0], decimals=3) 243 | n_cycle_r_min = np.round(n_cycle_r_vect_f[ind_min[0]][0], decimals=3) 244 | 245 | print('# - Minimum Found at:') 246 | print(f'# - Num. Cycles -> Rows : {n_cycle_r_min}') 247 | print(f'# - Num. Cycles -> Columns : {n_cycle_c_min}') 248 | 249 | # - Show Grid Search Error Array 250 | fig_0 = plt.figure(figsize=fig_size1) 251 | ax_0 = fig_0.add_subplot(111) 252 | ax_0.set_title(r'Mean Absolute Error - ' 253 | rf'$\Delta Num. Cycles$ = {s_step}', weight='bold') 254 | ax_0.set_xlabel(r'$X - Num. Cycles$') 255 | ax_0.set_ylabel(r'$Y - Num. Cycles$') 256 | ag_0 = ax_0.pcolormesh(n_cycle_c_vect_f_xx, n_cycle_r_vect_f_yy, 257 | error_array_f, cmap=mpl.colormaps['jet']) 258 | ax_0.scatter(n_cycle_c_vect_f_xx[ind_min], 259 | n_cycle_r_vect_f_yy[ind_min], 260 | marker='X', color='m', s=180, label='Minimum MAE') 261 | cb_0 = plt.colorbar(ag_0) 262 | cb_0.set_label(label='Rad', weight='bold') 263 | ax_0.legend(loc='best', prop={'size': 16}) 264 | ax_0.grid(color='m', linestyle='dotted', alpha=0.3) 265 | txt = f'Minimum Error found at: (X={n_cycle_c_min}, Y={n_cycle_r_min})' 266 | ax_0.annotate(txt, xy=(0.03, 0.03), xycoords="axes fraction", 267 | size=12, zorder=100, 268 | bbox=dict(boxstyle="square", fc="w")) 269 | plt.tight_layout() 270 | # - Save Error Map 271 | out_intf \ 272 | = path_to_intf.replace('.tiff', '_MAError_grid_search.' + fig_format) 273 | plt.savefig(out_intf, dpi=200, format=fig_format) 274 | plt.close() 275 | 276 | # - Compare Estimated Phase Ramp with input interferogram. 277 | n_cycle_r = n_cycle_r_vect_f[ind_min[0]] 278 | n_cycle_c = n_cycle_c_vect_f[ind_min[1]] 279 | else: 280 | # - Integration Domain used to define the phase ramp 281 | xx_m, yy_m = np.meshgrid(np.arange(n_columns), np.arange(n_rows)) 282 | n_cycle_c = cycle_c 283 | n_cycle_r = cycle_r 284 | 285 | # - Generate synthetic phase ramp 286 | synth_complex = synth_plane(slope_c, slope_r, n_columns, n_rows, 287 | n_cycle_c, n_cycle_r, xx_m, yy_m) 288 | synth_wrapped = np.angle(synth_complex) # - Wrap phase ramp 289 | 290 | fig_1 = plt.figure(figsize=fig_size2) 291 | ax_1 = fig_1.add_subplot(121) 292 | ax_1.set_title('Input Interferogram', weight='bold') 293 | im_1 = ax_1.pcolormesh(clipped_raster * raster_mask, 294 | vmin=-np.pi, vmax=np.pi, 295 | cmap=mpl.colormaps['jet']) 296 | cb_1 = add_colorbar(fig_1, ax_1, im_1) 297 | cb_1.set_label(label='Rad', weight='bold') 298 | cb_1.ax.set_xticks([-np.pi, 0, np.pi]) 299 | cb_1.ax.set_xticklabels([r'-$\pi$', '0', r'$\pi$']) 300 | ax_1.grid(color='m', linestyle='dotted', alpha=0.3) 301 | 302 | ax_2 = fig_1.add_subplot(122) 303 | ax_2.set_title('Estimated Phase Ramp', weight='bold') 304 | im_2 = ax_2.pcolormesh(synth_wrapped * raster_mask, 305 | cmap=mpl.colormaps['jet']) 306 | cb_2 = add_colorbar(fig_1, ax_2, im_2) 307 | cb_2.set_label(label='Rad', weight='bold') 308 | cb_2.ax.set_xticks([-np.pi, 0, np.pi]) 309 | cb_2.ax.set_xticklabels([r'-$\pi$', '0', r'$\pi$']) 310 | ax_2.grid(color='m', linestyle='dotted', alpha=0.3) 311 | if s_radius: 312 | txt = f'Number of Cycles: \n(X={n_cycle_c_min}, Y={n_cycle_r_min}) ' \ 313 | f'\n Slope R. {slope_r}, Slope C. {slope_c}' 314 | else: 315 | txt = f'Number of Cycles: \n(X={n_cycle_c}, Y={n_cycle_r}) ' \ 316 | f'\n Slope R. {slope_r}, Slope C. {slope_c}' 317 | 318 | ax_2.annotate(txt, xy=(0.03, 0.03), xycoords="axes fraction", 319 | size=12, zorder=100, 320 | bbox=dict(boxstyle="square", fc="w")) 321 | plt.tight_layout() 322 | # - save output figure 323 | out_intf = path_to_intf.replace('.tiff', 324 | '_input_field_vs_phase_ramp.' + fig_format) 325 | plt.savefig(out_intf, dpi=200, format=fig_format) 326 | plt.close() 327 | 328 | # - Remove the estimated phase ramp from the input phase field. 329 | # - Compute the complex conjugate product between the input phase 330 | # - field and the estimated ramp. 331 | dd_phase_complex_corrected \ 332 | = np.angle(dd_phase_complex * np.conj(synth_complex)) 333 | 334 | fig_3 = plt.figure(figsize=fig_size2) 335 | ax_3 = fig_3.add_subplot(121) 336 | ax_3.set_title('Input Interferogram', weight='bold') 337 | im_3a = ax_3.pcolormesh(clipped_raster * raster_mask, 338 | vmin=-np.pi, vmax=np.pi, 339 | cmap=mpl.colormaps['jet']) 340 | cb_3a = add_colorbar(fig_3, ax_3, im_3a) 341 | cb_3a.set_label(label='Rad', weight='bold') 342 | cb_3a.ax.set_xticks([-np.pi, 0, np.pi]) 343 | cb_3a.ax.set_xticklabels([r'-$\pi$', '0', r'$\pi$']) 344 | ax_3.grid(color='m', linestyle='dotted', alpha=0.3) 345 | 346 | ax_3 = fig_3.add_subplot(122) 347 | ax_3.set_title('Input Phase Field - Phase Ramp', weight='bold') 348 | im_3b = ax_3.pcolormesh(dd_phase_complex_corrected * raster_mask, 349 | cmap=mpl.colormaps['jet']) 350 | cb_3b = add_colorbar(fig_3, ax_3, im_3b) 351 | cb_3b.set_label(label='Rad', weight='bold') 352 | cb_3b.ax.set_xticks([-np.pi, 0, np.pi]) 353 | cb_3b.ax.set_xticklabels([r'-$\pi$', '0', r'$\pi$']) 354 | ax_3.grid(color='m', linestyle='dotted', alpha=0.3) 355 | plt.tight_layout() 356 | # - save output figure 357 | out_fig_3 = path_to_intf.replace('.tiff', '_deramped.' + fig_format) 358 | plt.savefig(out_fig_3, dpi=200, format=fig_format) 359 | plt.close() 360 | 361 | # - Save the de-ramped interferogram in Geotiff format 362 | dd_phase_complex_corrected[np.isnan(raster_mask)] = -9999 363 | with rasterio.open(path_to_intf, mode="r+") as dataset_c: 364 | o_transform = dataset_c.transform 365 | o_crs = dataset_c.crs 366 | 367 | dd_phase_complex_corrected = np.array(dd_phase_complex_corrected, 368 | dtype=dataset_c.dtypes[0]) 369 | out_f_name = path_to_intf.replace('.tiff', '_deramped.tiff') 370 | with rasterio.open(out_f_name, 'w', driver='GTiff', 371 | height=dd_phase_complex_corrected.shape[0], 372 | width=dd_phase_complex_corrected.shape[1], 373 | count=1, dtype=rasterio.float32, 374 | crs=o_crs, transform=o_transform, compress='lzw', 375 | nodata=-9999) as dst: 376 | dst.write(dd_phase_complex_corrected, 1) 377 | 378 | # - 379 | return{'synth_phase_ramp': synth_complex, 380 | 'dd_phase': dd_phase_complex_corrected} 381 | 382 | 383 | def main(): 384 | """ 385 | Main: Estimate and Remove Linear Phase Ramp from input Differential 386 | Interferogram. 387 | """ 388 | # - Read the system arguments listed after the program 389 | parser = argparse.ArgumentParser( 390 | description="""Estimate and Remove Linear Phase Ramp characterizing 391 | the considered Differential Interferogram. 392 | """ 393 | ) 394 | # - Positional Arguments 395 | parser.add_argument('path_to_intf', type=str, 396 | help='Absolute path to input interferogram.') 397 | 398 | # - Ramp Frequency - Rows-axis - FIRST GUESS 399 | parser.add_argument('cycle_r', type=float, default=None, 400 | help='Number of Cycles -> Rows-axis.') 401 | 402 | # - Ramp Frequency - Columns-axis - FIRST GUESS 403 | parser.add_argument('cycle_c', type=float, default=None, 404 | help='Number of Cycles -> Columns-axis.') 405 | 406 | # - Phase Ramp Slope: [-1, 1] 407 | parser.add_argument('--slope_r', 408 | type=float, default=1, choices=[-1, 1], 409 | help='Phase Ramp Slope -> Rows-axis.') 410 | parser.add_argument('--slope_c', 411 | type=float, default=1, choices=[-1, 1], 412 | help='Phase Ramp Slope -> Columns-axis.') 413 | 414 | # - Grid Search Domain Radius 415 | parser.add_argument('--s_radius', '-R', type=float, default=0, 416 | help='Grid Search Radius around the provided ' 417 | 'reference - Default 0.') 418 | # - Grid Search Step 419 | parser.add_argument('--s_step', '-T', type=float, default=0.1, 420 | help='Grid Search Step.') 421 | 422 | args = parser.parse_args() 423 | 424 | # - Estimate and remove phase ramp from input interferogram 425 | remove_phase_ramp(args. path_to_intf, args.cycle_r, args.cycle_c, 426 | slope_r=args.slope_r, slope_c=args.slope_c, 427 | s_radius=args.s_radius, 428 | s_step=args.s_step) 429 | 430 | 431 | if __name__ == '__main__': 432 | start_time = datetime.datetime.now() 433 | main() 434 | end_time = datetime.datetime.now() 435 | print(f"# - Computation Time: {end_time - start_time}") 436 | --------------------------------------------------------------------------------