├── pyrsgis ├── tests │ ├── __init__.py │ ├── data │ │ ├── raster_multiband.tif │ │ ├── raster_singleband_discrete.tif │ │ └── raster_singleband_continuous.tif │ └── test_raster.py ├── __init__.py ├── utils │ └── __init__.py ├── ml │ └── __init__.py ├── convert │ └── __init__.py └── raster │ └── __init__.py ├── media └── northing_easting.png ├── requirements.txt ├── docs ├── generated │ ├── pyrsgis.raster.clip.rst │ ├── pyrsgis.raster.read.rst │ ├── pyrsgis.raster.trim.rst │ ├── pyrsgis.raster.shift.rst │ ├── pyrsgis.raster.easting.rst │ ├── pyrsgis.raster.export.rst │ ├── pyrsgis.raster.northing.rst │ ├── pyrsgis.raster.clip_file.rst │ ├── pyrsgis.raster.trim_file.rst │ ├── pyrsgis.raster.north_east.rst │ ├── pyrsgis.raster.shift_file.rst │ ├── pyrsgis.raster.trim_array.rst │ ├── pyrsgis.ml.array_to_chips.rst │ ├── pyrsgis.ml.raster_to_chips.rst │ ├── pyrsgis.ml.array2d_to_chips.rst │ ├── pyrsgis.convert.csv_to_raster.rst │ ├── pyrsgis.convert.raster_to_csv.rst │ ├── pyrsgis.convert.array_to_table.rst │ ├── pyrsgis.convert.table_to_array.rst │ └── pyrsgis.raster.north_east_coordinates.rst ├── builddocs.sh ├── requirements.txt ├── rtd_env.yml ├── make.bat ├── Makefile ├── index.rst ├── README.md ├── conf.py ├── api.rst └── installation.rst ├── .gitignore ├── environment.yml ├── .readthedocs.yaml ├── generate_requirements.sh ├── CITATION.cff ├── LICENSE ├── pyproject.toml ├── .github └── workflows │ └── publishpypi.yml └── README.md /pyrsgis/tests/__init__.py: -------------------------------------------------------------------------------- 1 | #Test -------------------------------------------------------------------------------- /media/northing_easting.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PratyushTripathy/pyrsgis/HEAD/media/northing_easting.png -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | gdal==3.11.0 2 | numpy==2.2.6 3 | pandas==2.2.3 4 | pytest==8.3.5 5 | scikit_learn==1.6.1 6 | -------------------------------------------------------------------------------- /pyrsgis/tests/data/raster_multiband.tif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PratyushTripathy/pyrsgis/HEAD/pyrsgis/tests/data/raster_multiband.tif -------------------------------------------------------------------------------- /docs/generated/pyrsgis.raster.clip.rst: -------------------------------------------------------------------------------- 1 | pyrsgis.raster.clip 2 | =================== 3 | 4 | .. currentmodule:: pyrsgis.raster 5 | 6 | .. autofunction:: clip -------------------------------------------------------------------------------- /docs/generated/pyrsgis.raster.read.rst: -------------------------------------------------------------------------------- 1 | pyrsgis.raster.read 2 | =================== 3 | 4 | .. currentmodule:: pyrsgis.raster 5 | 6 | .. autofunction:: read -------------------------------------------------------------------------------- /docs/generated/pyrsgis.raster.trim.rst: -------------------------------------------------------------------------------- 1 | pyrsgis.raster.trim 2 | =================== 3 | 4 | .. currentmodule:: pyrsgis.raster 5 | 6 | .. autofunction:: trim -------------------------------------------------------------------------------- /pyrsgis/tests/data/raster_singleband_discrete.tif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PratyushTripathy/pyrsgis/HEAD/pyrsgis/tests/data/raster_singleband_discrete.tif -------------------------------------------------------------------------------- /docs/generated/pyrsgis.raster.shift.rst: -------------------------------------------------------------------------------- 1 | pyrsgis.raster.shift 2 | ==================== 3 | 4 | .. currentmodule:: pyrsgis.raster 5 | 6 | .. autofunction:: shift -------------------------------------------------------------------------------- /pyrsgis/tests/data/raster_singleband_continuous.tif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PratyushTripathy/pyrsgis/HEAD/pyrsgis/tests/data/raster_singleband_continuous.tif -------------------------------------------------------------------------------- /docs/generated/pyrsgis.raster.easting.rst: -------------------------------------------------------------------------------- 1 | pyrsgis.raster.easting 2 | ====================== 3 | 4 | .. currentmodule:: pyrsgis.raster 5 | 6 | .. autofunction:: easting -------------------------------------------------------------------------------- /docs/generated/pyrsgis.raster.export.rst: -------------------------------------------------------------------------------- 1 | pyrsgis.raster.export 2 | ===================== 3 | 4 | .. currentmodule:: pyrsgis.raster 5 | 6 | .. autofunction:: export -------------------------------------------------------------------------------- /docs/generated/pyrsgis.raster.northing.rst: -------------------------------------------------------------------------------- 1 | pyrsgis.raster.northing 2 | ======================= 3 | 4 | .. currentmodule:: pyrsgis.raster 5 | 6 | .. autofunction:: northing -------------------------------------------------------------------------------- /docs/generated/pyrsgis.raster.clip_file.rst: -------------------------------------------------------------------------------- 1 | pyrsgis.raster.clip\_file 2 | ========================= 3 | 4 | .. currentmodule:: pyrsgis.raster 5 | 6 | .. autofunction:: clip_file -------------------------------------------------------------------------------- /docs/generated/pyrsgis.raster.trim_file.rst: -------------------------------------------------------------------------------- 1 | pyrsgis.raster.trim\_file 2 | ========================= 3 | 4 | .. currentmodule:: pyrsgis.raster 5 | 6 | .. autofunction:: trim_file -------------------------------------------------------------------------------- /docs/generated/pyrsgis.raster.north_east.rst: -------------------------------------------------------------------------------- 1 | pyrsgis.raster.north\_east 2 | ========================== 3 | 4 | .. currentmodule:: pyrsgis.raster 5 | 6 | .. autofunction:: north_east -------------------------------------------------------------------------------- /docs/generated/pyrsgis.raster.shift_file.rst: -------------------------------------------------------------------------------- 1 | pyrsgis.raster.shift\_file 2 | ========================== 3 | 4 | .. currentmodule:: pyrsgis.raster 5 | 6 | .. autofunction:: shift_file -------------------------------------------------------------------------------- /docs/generated/pyrsgis.raster.trim_array.rst: -------------------------------------------------------------------------------- 1 | pyrsgis.raster.trim\_array 2 | ========================== 3 | 4 | .. currentmodule:: pyrsgis.raster 5 | 6 | .. autofunction:: trim_array -------------------------------------------------------------------------------- /docs/generated/pyrsgis.ml.array_to_chips.rst: -------------------------------------------------------------------------------- 1 | pyrsgis.ml.array\_to\_chips 2 | =========================== 3 | 4 | .. currentmodule:: pyrsgis.ml 5 | 6 | .. autofunction:: array_to_chips -------------------------------------------------------------------------------- /docs/generated/pyrsgis.ml.raster_to_chips.rst: -------------------------------------------------------------------------------- 1 | pyrsgis.ml.raster\_to\_chips 2 | ============================ 3 | 4 | .. currentmodule:: pyrsgis.ml 5 | 6 | .. autofunction:: raster_to_chips -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | word.txt 2 | *word.txt 3 | .idea/ 4 | *.xml 5 | *.pyc 6 | .DS_Store 7 | .iml 8 | docs/generated/ 9 | docs/_build/ 10 | *.pyc 11 | dist/ 12 | build/ 13 | *.egg-info/ 14 | -------------------------------------------------------------------------------- /docs/generated/pyrsgis.ml.array2d_to_chips.rst: -------------------------------------------------------------------------------- 1 | pyrsgis.ml.array2d\_to\_chips 2 | ============================= 3 | 4 | .. currentmodule:: pyrsgis.ml 5 | 6 | .. autofunction:: array2d_to_chips -------------------------------------------------------------------------------- /environment.yml: -------------------------------------------------------------------------------- 1 | name: pyrsgis 2 | channels: 3 | - conda-forge 4 | dependencies: 5 | - python 6 | - gdal 7 | - numpy 8 | - pandas 9 | - pytest 10 | - scikit-learn 11 | -------------------------------------------------------------------------------- /docs/generated/pyrsgis.convert.csv_to_raster.rst: -------------------------------------------------------------------------------- 1 | pyrsgis.convert.csv\_to\_raster 2 | =============================== 3 | 4 | .. currentmodule:: pyrsgis.convert 5 | 6 | .. autofunction:: csv_to_raster -------------------------------------------------------------------------------- /docs/generated/pyrsgis.convert.raster_to_csv.rst: -------------------------------------------------------------------------------- 1 | pyrsgis.convert.raster\_to\_csv 2 | =============================== 3 | 4 | .. currentmodule:: pyrsgis.convert 5 | 6 | .. autofunction:: raster_to_csv -------------------------------------------------------------------------------- /docs/builddocs.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | make apidoc 3 | rm -f generated/pyrsgis.rst generated/pyrsgis.raster.rst generated/pyrsgis.convert.rst generated/pyrsgis.ml.rst generated/pyrsgis.utils.rst 4 | make html 5 | -------------------------------------------------------------------------------- /docs/generated/pyrsgis.convert.array_to_table.rst: -------------------------------------------------------------------------------- 1 | pyrsgis.convert.array\_to\_table 2 | ================================ 3 | 4 | .. currentmodule:: pyrsgis.convert 5 | 6 | .. autofunction:: array_to_table -------------------------------------------------------------------------------- /docs/generated/pyrsgis.convert.table_to_array.rst: -------------------------------------------------------------------------------- 1 | pyrsgis.convert.table\_to\_array 2 | ================================ 3 | 4 | .. currentmodule:: pyrsgis.convert 5 | 6 | .. autofunction:: table_to_array -------------------------------------------------------------------------------- /docs/generated/pyrsgis.raster.north_east_coordinates.rst: -------------------------------------------------------------------------------- 1 | pyrsgis.raster.north\_east\_coordinates 2 | ======================================= 3 | 4 | .. currentmodule:: pyrsgis.raster 5 | 6 | .. autofunction:: north_east_coordinates -------------------------------------------------------------------------------- /docs/requirements.txt: -------------------------------------------------------------------------------- 1 | sphinx>=7 2 | myst-parser>=2 3 | pydata-sphinx-theme>=0.16.1 4 | numpydoc>=1.8.0 5 | sphinx-copybutton 6 | readthedocs-sphinx-search>=0.3.2 7 | nbsphinx 8 | jupyter-sphinx 9 | ipython 10 | matplotlib 11 | -------------------------------------------------------------------------------- /.readthedocs.yaml: -------------------------------------------------------------------------------- 1 | version: 2 2 | 3 | build: 4 | os: ubuntu-24.04 5 | 6 | conda: 7 | environment: docs/rtd_env.yml 8 | 9 | sphinx: 10 | configuration: docs/conf.py 11 | 12 | python: 13 | install: 14 | - method: pip 15 | path: . 16 | -------------------------------------------------------------------------------- /pyrsgis/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | pyrsgis - Python for Remote Sensing and GIS 3 | author: pratkrtgmail.com 4 | Compatible with Python versions 3+ 5 | """ 6 | 7 | name = 'pyrsgis' 8 | __version__ = "0.4.2b1" 9 | doc_address = r'https://pyrsgis.readthedocs.io/en/master/' 10 | 11 | -------------------------------------------------------------------------------- /generate_requirements.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | PACKAGE_DIR="./pyrsgis" 5 | OUTPUT_FILE="$(pwd)/requirements.txt" 6 | 7 | echo "Running pipreqs on $PACKAGE_DIR..." 8 | pipreqs "$PACKAGE_DIR" --force --savepath "$OUTPUT_FILE" 9 | 10 | echo "✅ Saved minimal requirements to $OUTPUT_FILE" 11 | -------------------------------------------------------------------------------- /CITATION.cff: -------------------------------------------------------------------------------- 1 | cff-version: 0.4.2b1.0 2 | message: "If you use this software, please cite it as below." 3 | authors: 4 | - family-names: "Tripathy" 5 | given-names: "Pratyush" 6 | orcid: "https://orcid.org/0000-0002-9278-8995" 7 | 8 | title: "PyRSGIS: A Python package for remote sensing and GIS applications" 9 | version: 0.4.2b1 10 | date-released: 2025-07-14 11 | doi: 10.5281/zenodo.2552933 12 | url: "https://github.com/PratyushTripathy/PyRSGIS" 13 | -------------------------------------------------------------------------------- /docs/rtd_env.yml: -------------------------------------------------------------------------------- 1 | name: pyrsgis-docs 2 | channels: 3 | - conda-forge 4 | dependencies: 5 | - python=3.11 # matches your requires-python 6 | - gdal=3.10.* # GDAL & its native libs 7 | - numpy # common dep for your package 8 | - matplotlib # you enable matplotlib.sphinxext.plot_directive 9 | - ipython # you enable IPython console highlighting 10 | # ---- Sphinx stack ---- 11 | - sphinx>=7 12 | - pydata-sphinx-theme>=0.16.1 13 | - numpydoc>=1.8.0 14 | - myst-parser>=2 15 | - nbsphinx 16 | - jupyter-sphinx 17 | - sphinx-copybutton 18 | - readthedocs-sphinx-search>=0.3.2 19 | - pip 20 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | pushd %~dp0 4 | 5 | REM Command file for Sphinx documentation 6 | 7 | if "%SPHINXBUILD%" == "" ( 8 | set SPHINXBUILD=sphinx-build 9 | ) 10 | set SOURCEDIR=. 11 | set BUILDDIR=_build 12 | 13 | if "%1" == "" goto help 14 | 15 | %SPHINXBUILD% >NUL 2>NUL 16 | if errorlevel 9009 ( 17 | echo. 18 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 19 | echo.installed, then set the SPHINXBUILD environment variable to point 20 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 21 | echo.may add the Sphinx directory to PATH. 22 | echo. 23 | echo.If you don't have Sphinx installed, grab it from 24 | echo.http://sphinx-doc.org/ 25 | exit /b 1 26 | ) 27 | 28 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 29 | goto end 30 | 31 | :help 32 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 33 | 34 | :end 35 | popd 36 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | 3 | # These variables can be set from the command line or environment 4 | SPHINXOPTS ?= 5 | SPHINXBUILD ?= sphinx-build 6 | SOURCEDIR = . 7 | BUILDDIR = _build 8 | 9 | # Default target: show help 10 | help: 11 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 12 | 13 | # Main Sphinx build targets (html, latexpdf, etc.) 14 | %: Makefile 15 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 16 | 17 | # Custom target to generate API .rst files automatically 18 | .PHONY: apidoc 19 | apidoc: 20 | @echo "Current directory is: $$(pwd)" 21 | @ls generated/pyrsgis* 22 | @rm -v generated/pyrsgis.rst generated/pyrsgis.raster.rst generated/pyrsgis.convert.rst generated/pyrsgis.ml.rst generated/pyrsgis.utils.rst || true 23 | @sphinx-apidoc -o generated ../pyrsgis --module-first --separate --no-toc ../pyrsgis/tests/* 24 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | 2 | pyrsgis: A Python package to read, process and export GeoTIFFs. 3 | =============================================================== 4 | 5 | pyrsgis enables the user to read, process and export GeoTIFFs. 6 | The module is built on the GDAL library, but is much more convenient 7 | when it comes to reading and exporting GeoTIFs. There are several 8 | other functions available in this package that ease raster pre-processing, 9 | some focused on machine learning tasks. 10 | 11 | Feedback and bug reports are most welcome. Since this is an open-source 12 | project, I always look forward to contributors. 13 | 14 | GitHub repository : https://github.com/PratyushTripathy/pyrsgis 15 | 16 | pyrsgis is available on both, PyPI and Anaconda. Please submit your query as 17 | a pull request on the GitHub repo. For more information, write to: pratkrt@gmail.com 18 | 19 | .. toctree:: 20 | :maxdepth: 2 21 | :caption: Contents: 22 | 23 | Installation 24 | API reference 25 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Copyright (c) Pratyush Tripathy 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 13 | all 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 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /pyrsgis/utils/__init__.py: -------------------------------------------------------------------------------- 1 | # pyrsgis/utils 2 | 3 | # safely import gdal (support old version) 4 | try: 5 | import gdal 6 | except: 7 | from osgeo import gdal 8 | 9 | """ 10 | Create data type dictionaries to call later 11 | """ 12 | datatype_dict_str_num = { 13 | 'byte': gdal.GDT_Byte, 14 | 'cfloat32': gdal.GDT_CFloat32, 15 | 'cfloat64': gdal.GDT_CFloat64, 16 | 'cint16': gdal.GDT_CInt16, 17 | 'cint32': gdal.GDT_CInt32, 18 | 'float': gdal.GDT_Float32, 19 | 'float32': gdal.GDT_Float32, 20 | 'float64': gdal.GDT_Float64, 21 | 'int': gdal.GDT_Int16, 22 | 'int16': gdal.GDT_Int16, 23 | 'int32': gdal.GDT_Int32, 24 | 'uint8': gdal.GDT_Byte, 25 | 'uint16': gdal.GDT_UInt16, 26 | 'uint32': gdal.GDT_UInt32 27 | } 28 | 29 | datatype_dict_num_str = { 30 | gdal.GDT_Byte: 'byte', 31 | gdal.GDT_CFloat32: 'cfloat32', 32 | gdal.GDT_CFloat64: 'cfloat64', 33 | gdal.GDT_CInt16: 'cint16', 34 | gdal.GDT_CInt32: 'cint32', 35 | gdal.GDT_Float32: 'float', 36 | gdal.GDT_Float64: 'float64', 37 | gdal.GDT_Int16: 'int', 38 | gdal.GDT_Int32: 'int32', 39 | gdal.GDT_UInt16: 'uint16', 40 | gdal.GDT_UInt32: 'uint32' 41 | } 42 | 43 | 44 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "pyrsgis" 3 | version = "0.4.2b1" 4 | description = "A Python package for remote sensing and GIS applications" 5 | readme = "README.md" 6 | license = {text="MIT"} 7 | authors = [ 8 | { name = "Pratyush Tripathy", email = "pratkrt@gmail.com" } 9 | ] 10 | requires-python = ">=3.11" 11 | dependencies = [ 12 | "numpy", 13 | "matplotlib", 14 | "gdal>=3.0.0" 15 | ] 16 | classifiers = [ 17 | "Development Status :: 4 - Beta", 18 | "Intended Audience :: Science/Research", 19 | "Topic :: Scientific/Engineering :: GIS", 20 | "Programming Language :: Python :: 3", 21 | "Programming Language :: Python :: 3.11", 22 | "Operating System :: OS Independent" 23 | ] 24 | 25 | [tool.setuptools.packages.find] 26 | where = ["."] 27 | include = ["pyrsgis*"] 28 | exclude = ["media*", "docs*", "dist*"] 29 | 30 | [tool.setuptools.package-data] 31 | "pyrsgis" = ["pyrsgis/tests/data*"] 32 | 33 | [project.urls] 34 | Homepage = "https://github.com/PratyushTripathy/pyrsgis" 35 | Issues = "https://github.com/PratyushTripathy/pyrsgis/issues" 36 | 37 | [build-system] 38 | requires = ["setuptools>=77.0.0", "wheel"] 39 | build-backend = "setuptools.build_meta" 40 | 41 | [tool.setuptools] 42 | license-files = [] -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | # pyrsgis Documentation 2 | 3 | This folder contains the documentation source for the pyrsgis Python package. 4 | 5 | ## Local Build Instructions 6 | 7 | To build the documentation locally: 8 | 9 | 1. **From the project root**, activate your environment and navigate to this folder: 10 | ```bash 11 | cd docs 12 | ``` 13 | 14 | 2. **Run the build script** to generate clean HTML docs: 15 | ```bash 16 | ./builddocs.sh 17 | ``` 18 | 19 | This script: 20 | - Runs `sphinx-apidoc` to generate API reference files 21 | - Deletes summary `.rst` files that cause duplicate warnings 22 | - Builds the HTML documentation 23 | 24 | 3. **Open the docs**: 25 | - Open `_build/html/index.html` in your browser 26 | 27 | ## Troubleshooting 28 | 29 | - If you see duplicate warnings, ensure you always use `./builddocs.sh` (not `make html` directly). 30 | - If the script fails with permissions, run `chmod +x builddocs.sh`. 31 | 32 | ## Dependencies 33 | 34 | - All dependencies should be listed in `rtd_env.yml` 35 | - For doc theme customization, see `conf.py` 36 | 37 | ## Publishing 38 | 39 | - To deploy on ReadTheDocs, just push to the repository—RTD uses the same steps. 40 | - For other questions, check the main project `README.md`. 41 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | from importlib.metadata import version as pkg_version, PackageNotFoundError 2 | from pathlib import Path 3 | import sys 4 | 5 | ROOT = Path(__file__).resolve().parents[1] 6 | SRC = ROOT / "src" 7 | sys.path.insert(0, str(SRC if SRC.exists() else ROOT)) 8 | 9 | project = "pyrsgis" 10 | author = "Pratyush Tripathy" 11 | 12 | try: 13 | release = pkg_version("pyrsgis") 14 | except PackageNotFoundError: 15 | release = "0.0.0" 16 | version = ".".join(release.split(".")[:2]) 17 | 18 | extensions = [ 19 | "myst_parser", 20 | "sphinx.ext.autodoc", 21 | "sphinx.ext.autosummary", 22 | "sphinx.ext.napoleon", 23 | "sphinx.ext.intersphinx", 24 | "sphinx.ext.viewcode", 25 | "sphinx.ext.doctest", 26 | "IPython.sphinxext.ipython_console_highlighting", 27 | "matplotlib.sphinxext.plot_directive", 28 | "numpydoc", 29 | ] 30 | 31 | # Don't mock GDAL now; it's installed by conda 32 | autodoc_mock_imports = [ 33 | "sklearn", "sklearn.feature_extraction", "sklearn.ensemble", 34 | "skimage", "skimage.filters", 35 | "rasterio", "xarray", # mock if not needed in docs 36 | ] 37 | 38 | intersphinx_mapping = {"python": ("https://docs.python.org/3", None)} 39 | html_theme = "pydata_sphinx_theme" 40 | html_title = f"{project} v{release}" 41 | -------------------------------------------------------------------------------- /.github/workflows/publishpypi.yml: -------------------------------------------------------------------------------- 1 | name: Upload Python Package 2 | 3 | on: 4 | release: 5 | types: [published] 6 | workflow_dispatch: 7 | 8 | concurrency: 9 | group: ${{ github.workflow }}-${{ github.ref }} 10 | cancel-in-progress: true 11 | 12 | jobs: 13 | deploy: 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@v4 17 | 18 | - uses: actions/setup-python@v5 19 | with: 20 | python-version: "3.11" 21 | cache: "pip" 22 | 23 | - name: Upgrade build tools 24 | run: python -m pip install --upgrade pip build twine 25 | 26 | - name: Clean build artifacts 27 | run: rm -rf build dist *.egg-info 28 | 29 | - name: Build package (sdist + wheel) 30 | run: python -m build 31 | 32 | - name: Twine check 33 | run: twine check dist/* 34 | 35 | - name: Verify version matches tag 36 | if: startsWith(github.ref, 'refs/tags/') 37 | shell: bash 38 | run: | 39 | set -euo pipefail 40 | TAG="${GITHUB_REF_NAME#v}" 41 | PYV=$(python -c 'import tomllib; print(tomllib.load(open("pyproject.toml","rb"))["project"]["version"])') 42 | echo "Tag: $TAG PyProject: $PYV" 43 | test "$TAG" = "$PYV" 44 | 45 | - name: Publish to PyPI 46 | env: 47 | TWINE_USERNAME: __token__ 48 | TWINE_PASSWORD: ${{ secrets.PYPI_API_TOKEN }} 49 | run: twine upload dist/* 50 | -------------------------------------------------------------------------------- /docs/api.rst: -------------------------------------------------------------------------------- 1 | .. _api_ref: 2 | 3 | pyrsgis API reference 4 | ====================== 5 | 6 | Reading and exporting GeoTIFFs 7 | ------------------------------ 8 | 9 | .. autosummary:: 10 | :toctree: generated/ 11 | 12 | pyrsgis.raster.read 13 | pyrsgis.raster.export 14 | 15 | Clipping a raster 16 | ----------------- 17 | 18 | .. autosummary:: 19 | :toctree: generated/ 20 | 21 | pyrsgis.raster.clip 22 | pyrsgis.raster.clip_file 23 | pyrsgis.raster.trim 24 | pyrsgis.raster.trim_array 25 | pyrsgis.raster.trim_file 26 | 27 | Shifting a raster 28 | ----------------- 29 | 30 | .. autosummary:: 31 | :toctree: generated/ 32 | 33 | pyrsgis.raster.shift 34 | pyrsgis.raster.shift_file 35 | 36 | Reshaping GeoTIFF array for statistical analysis 37 | ------------------------------------------------ 38 | 39 | .. autosummary:: 40 | :toctree: generated/ 41 | 42 | pyrsgis.convert.array_to_table 43 | pyrsgis.convert.table_to_array 44 | pyrsgis.convert.raster_to_csv 45 | pyrsgis.convert.csv_to_raster 46 | 47 | Creating image chips for deep learning 48 | -------------------------------------- 49 | 50 | .. autosummary:: 51 | :toctree: generated/ 52 | 53 | pyrsgis.ml.array_to_chips 54 | pyrsgis.ml.array2d_to_chips 55 | pyrsgis.ml.raster_to_chips 56 | 57 | Generating northing and easting raster 58 | -------------------------------------- 59 | 60 | .. autosummary:: 61 | :toctree: generated/ 62 | 63 | pyrsgis.raster.north_east 64 | pyrsgis.raster.north_east_coordinates 65 | pyrsgis.raster.northing 66 | pyrsgis.raster.easting 67 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Python-for-Remote-Sensing-and-GIS 2 | [![PyPI version](https://badge.fury.io/py/pyrsgis.svg)](https://pypi.org/project/pyrsgis/) 3 | [![Anaconda-Server Badge](https://anaconda.org/pratyusht/pyrsgis/badges/version.svg)](https://anaconda.org/pratyusht/pyrsgis) 4 | [![Anaconda-Server Badge](https://anaconda.org/pratyusht/pyrsgis/badges/latest_release_date.svg)](https://github.com/PratyushTripathy/pyrsgis) 5 | [![Documentation Status](https://readthedocs.org/projects/pyrsgis/badge/?version=master)](https://pyrsgis.readthedocs.io/en/master/?badge=master) 6 | [![Downloads](https://pepy.tech/badge/pyrsgis)](https://pepy.tech/project/pyrsgis) 7 | 8 | *pyrsgis* enables the user to read, process and export GeoTIFFs. The module is built on the GDAL library but is much more convenient when it comes to reading and exporting GeoTIFs. There are several other functions available in this package that ease raster pre-processing.
9 | 10 | A list of available functions and sample code are available on the [documentation page](https://pyrsgis.readthedocs.io/en/master/api.html).
11 | 12 | Feedback and bug reports are most welcome. Since this is an open-source project, I always look forward to contributors. You can write to me at [pratkrt@gmail.com](mailto:pratkrt@gmail.com). 13 | 14 | pyrsgis is available on both, PyPI and Anaconda. Please check the [installation page](https://pyrsgis.readthedocs.io/en/master/installation.html) for details. 15 | 16 | **Recommended citation:**
17 | Tripathy, P. pyrsgis: A Python package for remote sensing and GIS data processing. V0.4. Available at https://github.com/PratyushTripathy/pyrsgis. 18 | -------------------------------------------------------------------------------- /pyrsgis/tests/test_raster.py: -------------------------------------------------------------------------------- 1 | 2 | # -*- coding: utf-8 -*- 3 | """ 4 | Tests for pyrsgis package 5 | """ 6 | import numpy as np 7 | 8 | ''' Tests for pyrsgis.raster 9 | 10 | Naming rules: 11 | 1. class: Test{filename}{Class}{method} with appropriate camel case 12 | 2. function: test_{method}_t{test_id} 13 | Notes on how to test: 14 | 0. Make sure [pytest](https://docs.pytest.org) has been installed: `pip install pytest` 15 | 1. execute `pytest {directory_path}` in terminal to perform all tests in all testing files inside the specified directory 16 | 2. execute `pytest {file_path}` in terminal to perform all tests in the specified file 17 | 3. execute `pytest {file_path}::{TestClass}::{test_method}` in terminal to perform a specific test class/method inside the specified file 18 | 4. after `pip install pytest-xdist`, one may execute "pytest -n 4" to test in parallel with number of workers specified by `-n` 19 | 5. for more details, see https://docs.pytest.org/en/stable/usage.html 20 | ''' 21 | 22 | #import pytest 23 | import os 24 | from pyrsgis import raster 25 | import numpy as np 26 | import pytest 27 | 28 | # define all the file paths to run the test on 29 | DATA_DIR = 'data/' 30 | DATA_DIR = os.path.join(os.path.dirname(__file__), 'data') 31 | MULTIBAND_FILEPATH = f'{DATA_DIR}/raster_multiband.tif' 32 | SINGLEBAND_DISCRETE_FILEPATH = f'{DATA_DIR}/raster_singleband_discrete.tif' 33 | SINGLEBAND_CONTINUOUS_FILEPATH = f'{DATA_DIR}/raster_singleband_continuous.tif' 34 | 35 | class TestPyrsgisRaster: 36 | ''' Test for raster.read instantiation ''' 37 | 38 | def test_init_t0(self): 39 | ds_singleband_continuous, arr_singleband_continuous = raster.read(SINGLEBAND_CONTINUOUS_FILEPATH) 40 | ds_singleband_discrete, arr_singleband_discrete = raster.read(SINGLEBAND_DISCRETE_FILEPATH) 41 | ds_multiband, arr_multiband = raster.read(MULTIBAND_FILEPATH) 42 | 43 | for ds in [ds_singleband_continuous, ds_singleband_discrete, ds_multiband]: 44 | assert type(ds.GeoTransform) == type(tuple()) 45 | assert type(ds.Projection) == type(str()) 46 | assert type(ds.RasterCount) == type(int()) 47 | assert type(ds.RasterXSize) == type(int()) 48 | assert type(ds.RasterYSize) == type(int()) 49 | 50 | print('pyrsgis.raster tests ran successfully!') 51 | 52 | 53 | @pytest.mark.xfail 54 | def test_init_t1(self): 55 | ds_singleband_continuous, arr_singleband_continuous = raster.read(SINGLEBAND_CONTINUOUS_FILEPATH) 56 | assert type(arr_singleband_continuous) == type(np.array()) 57 | 58 | # call the modules in the class and run the tests 59 | TestPyrsgisRaster().test_init_t0() -------------------------------------------------------------------------------- /docs/installation.rst: -------------------------------------------------------------------------------- 1 | .. Install 2 | 3 | Installation 4 | ============ 5 | 6 | Recommended: Conda/Mamba Installation 7 | ------------------------------------- 8 | 9 | The easiest way to get started with `pyrsgis` is to use the provided `environment.yml` file, which sets up a compatible environment with all necessary dependencies. 10 | This method works with both `conda` and `mamba`: 11 | 12 | .. code-block:: bash 13 | 14 | # Using mamba (recommended for speed) 15 | mamba env create -f environment.yml 16 | 17 | # Or with conda 18 | conda env create -f environment.yml 19 | 20 | Then activate the environment: 21 | 22 | .. code-block:: bash 23 | 24 | mamba activate pyrsgis 25 | # or 26 | conda activate pyrsgis 27 | 28 | This ensures all required libraries are installed in versions known to work with `pyrsgis`. 29 | 30 | Alternative: Installation with requirements.txt 31 | ----------------------------------------------- 32 | 33 | You can also use the `requirements.txt` file to install dependencies in your existing Python environment: 34 | 35 | .. code-block:: bash 36 | 37 | pip install -r requirements.txt 38 | 39 | PyPI Installation (pip) 40 | ----------------------- 41 | 42 | `pyrsgis` is available on the `Python Package Index`_. 43 | To install the latest **stable** version with pip: 44 | 45 | .. code-block:: bash 46 | 47 | pip install pyrsgis 48 | 49 | To install a specific version, specify it explicitly (see `release history`_ for available versions): 50 | 51 | .. code-block:: bash 52 | 53 | pip install pyrsgis==0.4.2b1 54 | 55 | **Note:** The latest stable and pre-release versions may differ. 56 | 57 | Pre-release Installation (Beta Version) 58 | --------------------------------------- 59 | 60 | The current beta release is **0.4.2b1**. 61 | To install this pre-release version from PyPI, use: 62 | 63 | .. code-block:: bash 64 | 65 | pip install --pre pyrsgis 66 | 67 | The `--pre` flag allows pip to install pre-release (beta, alpha, or release candidate) versions. 68 | 69 | Conda/Anaconda Installation 70 | --------------------------- 71 | 72 | `pyrsgis` is also available on Anaconda Cloud (community channel): 73 | 74 | .. code-block:: bash 75 | 76 | conda install -c pratyusht pyrsgis 77 | 78 | **Note:** The conda version may lag behind PyPI. The recommended installation is via the provided `environment.yml` if you use conda or mamba. 79 | 80 | Supported Python Versions 81 | ------------------------- 82 | 83 | `pyrsgis` supports Python **3.7 and above**. 84 | Older versions (3.5–3.8) are still supported in legacy releases, but for best results, use a modern Python 3 environment. 85 | 86 | Contributing to pyrsgis 87 | ----------------------- 88 | 89 | `pyrsgis` is an open-source, non-profit project—contributions are welcome! 90 | To contribute: 91 | 92 | 1. Fork the `GitHub repo`_. 93 | 2. Clone your fork locally. 94 | 3. Create a new branch and make your changes. 95 | 4. Open a pull request for review. 96 | 97 | Suggestions, issues, and feature requests are also appreciated. 98 | 99 | .. _Python Package Index: https://pypi.org/project/pyrsgis/ 100 | .. _release history: https://pypi.org/project/pyrsgis/#history 101 | .. _Anaconda: https://anaconda.org/pratyusht/pyrsgis 102 | .. _GitHub repo: https://github.com/PratyushTripathy/pyrsgis 103 | -------------------------------------------------------------------------------- /pyrsgis/ml/__init__.py: -------------------------------------------------------------------------------- 1 | #pyrsgis/ml 2 | 3 | from copy import deepcopy 4 | import numpy as np 5 | from ..raster import read 6 | from sklearn.feature_extraction import image 7 | 8 | # define a function to create image chips from single band array 9 | def array2d_to_chips(data_arr, y_size=5, x_size=5): 10 | """ 11 | Image chips from 2D array 12 | 13 | This function generates images chips from single band arrays. The image chips can 14 | be used as a direct input to deep learning models (eg. Convolutional Neural Network). 15 | 16 | Parameters 17 | ---------- 18 | data_arr : array 19 | A 2D array from which image chips will be created. 20 | 21 | y_size : integer 22 | The height of the image chips. Ideally an odd number. 23 | 24 | x_size : integer 25 | The width of the image chips. Ideally an odd number. 26 | 27 | Returns 28 | ------- 29 | image_chips : array 30 | A 3D array containing stacked image chips. The first index 31 | represents each image chip and the size is equal to total number 32 | of cells in the input array. The 2nd and 3rd index represent the 33 | height and the width of the image chips. 34 | 35 | Examples 36 | -------- 37 | >>> from pyrsgis import raster, ml 38 | >>> infile = r'E:/path_to_your_file/your_file.tif' 39 | >>> ds, data_arr = raster.read(infile) 40 | >>> image_chips = ml.array2d_to_chips(data_arr, y_size=5, x_size=5) 41 | >>> print('Shape of input array:', data_arr.shape) 42 | >>> print('Shape of generated image chips:', image_chips.shape) 43 | Shape of input array: (2054, 2044) 44 | Shape of generated image chips: (4198376, 5, 5) 45 | 46 | """ 47 | image_chips = deepcopy(data_arr) 48 | image_chips = np.pad(image_chips, (int(y_size/2),int(x_size/2)), 'reflect') 49 | image_chips = image.extract_patches_2d(image_chips, (y_size, x_size)) 50 | 51 | return(image_chips) 52 | 53 | def imageChipsFromSingleBandArray(data_arr, y_size=5, x_size=5): 54 | image_chips = deepcopy(data_arr) 55 | image_chips = np.pad(image_chips, (int(y_size/2),int(x_size/2)), 'reflect') 56 | image_chips = image.extract_patches_2d(image_chips, (y_size, x_size)) 57 | 58 | return(image_chips) 59 | 60 | # define a function to create image chips from array 61 | def array_to_chips(data_arr, y_size=5, x_size=5): 62 | """ 63 | Image chips from raster array 64 | 65 | This function generates images chips from single or multi band raster arrays. The image 66 | chips can be used as a direct input to deep learning models (eg. Convolutional Neural Network). 67 | 68 | Parameters 69 | ---------- 70 | data_arr : array 71 | A 2D or 3D raster array from which image chips will be created. This 72 | should be similar as the one generated by ``pyrsgis.raster.read`` function. 73 | 74 | y_size : integer 75 | The height of the image chips. Ideally an odd number. 76 | 77 | x_size : integer 78 | The width of the image chips. Ideally an odd number. 79 | 80 | Returns 81 | ------- 82 | image_chips : array 83 | A 3D or 4D array containing stacked image chips. The first index 84 | represents each image chip and the size is equal to total number 85 | of cells in the input array. The 2nd and 3rd index represent the 86 | height and the width of the image chips. If the input array is a 87 | 3D array, then image_clips will be 4D where the 4th index will 88 | represent the number of bands. 89 | 90 | Examples 91 | -------- 92 | >>> from pyrsgis import raster, ml 93 | >>> infile = r'E:/path_to_your_file/your_file.tif' 94 | >>> ds, data_arr = raster.read(infile) 95 | >>> image_chips = ml.array_to_chips(data_arr, y_size=7, x_size=7) 96 | >>> print('Shape of input array:', data_arr.shape) 97 | >>> print('Shape of generated image chips:', image_chips.shape) 98 | Shape of input array: (6, 2054, 2044) 99 | Shape of generated image chips: (4198376, 7, 7, 6) 100 | 101 | """ 102 | 103 | # if array is a single band image 104 | if len(data_arr.shape) == 2: 105 | return(array2d_to_chips(data_arr, y_size=y_size, x_size=x_size)) 106 | 107 | # if array is a multi band image 108 | elif len(data_arr.shape) > 2: 109 | data_arr = deepcopy(data_arr) 110 | 111 | for band in range(data_arr.shape[0]): 112 | temp_array = array2d_to_chips(data_arr[band, :, :], y_size=y_size, x_size=x_size) 113 | 114 | if band == 0: 115 | out_array = np.expand_dims(temp_array, axis=3) 116 | else: 117 | out_array = np.concatenate((out_array, np.expand_dims(temp_array, axis=3)), axis=3) 118 | 119 | return(out_array) 120 | 121 | # if shape of the image is less than two dimensions, raise error 122 | else: 123 | raise Exception("Sorry, only two or three dimensional arrays allowed.") 124 | 125 | 126 | def imageChipsFromArray(data_array, x_size=5, y_size=5): 127 | 128 | # if array is a single band image 129 | if len(data_array.shape) == 2: 130 | return(imageChipsFromSingleBandArray(data_array, x_size=x_size, y_size=y_size)) 131 | 132 | # if array is a multi band image 133 | elif len(data_array.shape) > 2: 134 | data_array = deepcopy(data_array) 135 | data_array = np.rollaxis(data_array, 0, 3) 136 | 137 | for band in range(data_array.shape[2]): 138 | temp_array = imageChipsFromSingleBandArray(data_array[:, :, band], x_size=x_size, y_size=y_size) 139 | 140 | if band == 0: 141 | out_array = np.expand_dims(temp_array, axis=3) 142 | else: 143 | out_array = np.concatenate((out_array, np.expand_dims(temp_array, axis=3)), axis=3) 144 | 145 | return(out_array) 146 | 147 | # if shape of the image is less than two dimensions, raise error 148 | else: 149 | raise Exception("Sorry, only two or three dimensional arrays allowed.") 150 | 151 | # define a function to create image chips from TIF file 152 | def raster_to_chips(file, y_size=5, x_size=5): 153 | """ 154 | Image chips from raster file 155 | 156 | This function generates images chips from single or multi band GeoTIFF file. The image 157 | chips can be used as a direct input to deep learning models (eg. Convolutional Neural Network). 158 | 159 | This is built on the ``pyrsgis.ml.array_to_chips`` function. 160 | 161 | Parameters 162 | ---------- 163 | file : string 164 | Name or path of the GeoTIFF file from which image chips will be created. 165 | 166 | y_size : integer 167 | The height of the image chips. Ideally an odd number. 168 | 169 | x_size : integer 170 | The width of the image chips. Ideally an odd number. 171 | 172 | Returns 173 | ------- 174 | image_chips : array 175 | A 3D or 4D array containing stacked image chips. The first index 176 | represents each image chip and the size is equal to total number 177 | of cells in the input array. The 2nd and 3rd index represent the 178 | height and the width of the image chips. If the input file is a 179 | multiband raster, then image_clips will be 4D where the 4th index will 180 | represent the number of bands. 181 | 182 | Examples 183 | -------- 184 | >>> from pyrsgis import raster, ml 185 | >>> infile_2d = r'E:/path_to_your_file/your_2d_file.tif' 186 | >>> image_chips = ml.raster_to_chips(infile_2d, y_size=7, x_size=7) 187 | >>> print('Shape of single band generated image chips:', image_chips.shape) 188 | Shape of single bandgenerated image chips: (4198376, 7, 7) 189 | 190 | Not that here the shape of the input raster file is 2054 rows by 2044 columns. 191 | If the raster file is multiband: 192 | 193 | >>> infile_3d = r'E:/path_to_your_file/your_3d_file.tif' 194 | >>> image_chips = ml.raster_to_chips(infile_3d, y_size=7, x_size=7) 195 | >>> print('Shape of multiband generated image chips:', image_chips.shape) 196 | Shape of multiband generated image chips: (4198376, 7, 7, 6) 197 | 198 | """ 199 | 200 | ds, data_arr = read(file) 201 | 202 | return(array_to_chips(data_arr, y_size=y_size, x_size=x_size)) 203 | 204 | def imageChipsFromFile(infile, y_size=5, x_size=5): 205 | ds, data_arr = read(infile) 206 | 207 | return(imageChipsFromArray(data_arr, y_size=y_size, x_size=x_size)) 208 | -------------------------------------------------------------------------------- /pyrsgis/convert/__init__.py: -------------------------------------------------------------------------------- 1 | #pyrsgis/convert 2 | 3 | import os, glob 4 | import numpy as np 5 | import pandas as pd 6 | import csv 7 | from ..raster import read 8 | from ..raster import export 9 | from .. import doc_address 10 | 11 | 12 | def changeDimension(arr): 13 | """ 14 | The pyrsgis.convert.changeDimension() module has moved to 15 | pyrsgis.convert.array_to_table. Please check the documentation. 16 | """ 17 | print('The "changeDimension()" function has moved to "array_to_table()" and will be deprecated in future versions. ' + 18 | 'Please check the pyrsgis documentation at %s for more details.' % (doc_address)) 19 | 20 | if len(arr.shape) == 3: 21 | layer, row, col = arr.shape 22 | temparr = np.random.randint(1, size=(row*col, layer)) 23 | for n in range(0, layer): 24 | temparr[:,n] = np.reshape(arr[n,:,:], (row*col,)) 25 | return(temparr) 26 | if len(arr.shape) == 2: 27 | row, col = arr.shape 28 | temparr = np.reshape(arr, (row*col,)) 29 | return(temparr) 30 | else: 31 | print("Inconsistent shape of input array.\n2-d or 3-d array expected.") 32 | 33 | def array_to_table(arr): 34 | """ 35 | Convert 2D or 3D array to table 36 | 37 | The function converts single band or multiband raster array to a table where 38 | columns represents the input bands and each row represents a cell. 39 | 40 | Parameters 41 | ---------- 42 | arr : numpy array 43 | A single band (2D) or multiband (3D) raster array. Please note that for 44 | multiband raster arrays, the band index should be in the beginning, similar 45 | to the one generated by the pyrsgis.raster.read function. 46 | 47 | Examples 48 | -------- 49 | >>> from pyrsgis import raster, convert 50 | >>> input_file = r'E:/path_to_your_file/raster_file.tif' 51 | >>> ds, data_arr = raster.read(input_file) 52 | >>> data_table = convert.array_to_table(data_arr) 53 | 54 | Now check the shape of the input and reshaped arrays. 55 | 56 | >>> print('Shape of the input array', data_arr.shape) 57 | >>> print('Shape of the reshaped array:', data_table.shape) 58 | Shape of the input array: (6, 800, 400) 59 | Shape of the reshaped array: (320000, 6) 60 | 61 | Here, the input was a six band multispectral raster image. 62 | Same method applies for single band rasters also. 63 | 64 | """ 65 | 66 | if len(arr.shape) == 3: 67 | layer, row, col = arr.shape 68 | temparr = np.random.randint(1, size=(row*col, layer)) 69 | for n in range(0, layer): 70 | temparr[:,n] = np.reshape(arr[n,:,:], (row*col,)) 71 | return(temparr) 72 | if len(arr.shape) == 2: 73 | row, col = arr.shape 74 | temparr = np.reshape(arr, (row*col,)) 75 | return(temparr) 76 | else: 77 | print("Inconsistent shape of input array.\n2-d or 3-d array expected.") 78 | 79 | 80 | def table_to_array(table, n_rows=None, n_cols=None): 81 | """ 82 | Convert tablar array to 2D or 3D array 83 | 84 | The function converts a table where columns represents the input bands 85 | and each row represents a cell to a single band or multiband raster array. 86 | 87 | Parameters 88 | ---------- 89 | table : numpy array 90 | A 2D array where rows represent cells of to be generated 91 | raster array and each column represents band. This is similar 92 | to the one generated by the pyrsgis.convert.array_to_table function. 93 | 94 | Examples 95 | -------- 96 | >>> from pyrsgis import raster, convert 97 | >>> input_file = r'E:/path_to_your_file/raster_file.tif' 98 | >>> ds, data_arr = raster.read(input_file) 99 | >>> data_table = convert.array_to_table(data_arr) 100 | >>> print('Shape of the input array:', data_arr.shape) 101 | >>> print('Shape of the reshaped array:', data_table.shape) 102 | Shape of the input array: (6, 800, 400) 103 | Shape of the reshaped array: (320000, 6) 104 | 105 | ...some analysis/processing that you may want to do and generate more columns, 106 | say two more columns. Then: 107 | 108 | >>> new_data_arr = convert.table_to_array(data_table, n_rows=ds.RasterYSize, n_cols=ds.RasterXSize) 109 | >>> print('Shape of the array with newly added bands:', new_data_arr.shape) 110 | Shape of the array with newly added bands: (8, 800, 400) 111 | 112 | If you want to reshape only the new band(s), then: 113 | 114 | >>> new_data_arr = convert.table_to_array(data_table[:, -2:], n_rows=ds.RasterYSize, n_cols=ds.RasterXSize) 115 | >>> print('Shape of the array with newly added bands:', new_data_arr.shape) 116 | Shape of the array with newly added bands: (2, 800, 400) 117 | 118 | """ 119 | 120 | if len(table.shape) > 2: 121 | print('A three dimensional array was provided. Please provied a 1D or 2D array. ' + 122 | 'Please check the pyrsgis documentation at %s' % (doc_address)) 123 | return None 124 | 125 | elif len(table.shape) > 1: 126 | n_bands = table.shape[1] 127 | 128 | if n_bands > 1: 129 | out_arr = np.zeros((n_bands, n_rows, n_cols)) 130 | 131 | for n in range(0, n_bands): 132 | out_arr[n, :, :] = np.reshape(table[:, n], (n_rows, n_cols)) 133 | else: 134 | out_arr = np.reshape(table, (n_rows, n_cols)) 135 | 136 | return out_arr 137 | 138 | 139 | def raster_to_csv(path, filename='pyrsgis_rastertocsv.csv', negative=True, remove=[], badrows=True): 140 | """ 141 | Convert raster to a tabular CSV file 142 | 143 | This function converts a single or multiband raster or rasters present in a 144 | given directory to a CSV file. Each row in the output CSV file represents a 145 | cell and columns represent band(s) of the input raster(s). 146 | 147 | Parameters 148 | ---------- 149 | path : string 150 | Path to a file or a directory containing raster file(s). 151 | 152 | filename : string 153 | Output CSV file name, with or without path. 154 | 155 | negative : boolean 156 | Whether to retain negative values or not. If False, all negative 157 | values will be forced to zero in the output CSV. This maybe useful in 158 | some cases as NoData cells in raster files are often negative. 159 | 160 | remove : list 161 | A list of values that you want to remove from the exported table. If a list 162 | is passed, all the values of the list will be converted to zero in the raster 163 | before transforming to table. Please note that in the backend, this step 164 | happens before bad rows removal. 165 | 166 | badrows : True 167 | Whether to retain rows in the CSV where all cells have zero value. 168 | This can be helpful since raster layers masked using a non-rectangular 169 | polygon may have unnecessary NoData cells. In such cases, if all the bands 170 | have a cell value of zero and are not relevant, this parameter can help in 171 | reducing the size of the data. Please note that cells converted to zero by 172 | passing the 'negative' and 'remove' arguments will also be considered as bad cells. 173 | 174 | Examples 175 | -------- 176 | >>> from pyrsgis import convert 177 | 178 | If you want to convert a single raster file (single or multiple bands): 179 | 180 | >>> input_file = r'E:/path_to_your_file/raster_file.tif' 181 | >>> output_file = r'E:/path_to_your_file/tabular_file.csv' 182 | >>> convert.raster_to_csv(input_file, filename=output_file) 183 | 184 | If you want to convert all files in a directory, please ensure that all 185 | rasters in the directory have the same extent, cell size and geometry. 186 | The files in the directory can be a mix of single and multiband rasters. 187 | 188 | >>> input_dir = r'E:/path_to_your_file/' 189 | >>> output_file = r'E:/path_to_your_file/tabular_file.csv' 190 | >>> convert.raster_to_csv(input_dir, filename=output_file) 191 | 192 | If you want to remove negative values, simply pass the 'negative' argument to False: 193 | 194 | >>> convert.raster_to_csv(input_dir, filename=output_file, negative=False) 195 | 196 | If you want to remove specific values, use this: 197 | 198 | >>> convert.raster_to_csv(input_dir, filename=output_file, remove=[10, 54, 127]) 199 | 200 | If you want to remove bad rows, use the following line: 201 | 202 | >>> convert.raster_to_csv(input_dir, filename=output_file, badrows=False) 203 | 204 | """ 205 | 206 | data_df = pd.DataFrame() 207 | names = [] 208 | 209 | # If an input file is provided 210 | if os.path.splitext(path)[-1].lower()[-3:] == 'tif': 211 | ds, arr = read(path) 212 | header = os.path.splitext(os.path.basename(path))[0] 213 | 214 | if ds.RasterCount > 1: 215 | for n in range(0, ds.RasterCount): 216 | data_df['%s@%d' % (header, n+1)] = np.ravel(arr[n, :, :]) 217 | else: 218 | data_df['%s@%d' % (header, 1)] = np.ravel(arr) 219 | 220 | # If a directory is provided 221 | else: 222 | os.chdir(path) 223 | 224 | for file in glob.glob("*.tif"): 225 | print('Converting %s..' % (file)) 226 | header = os.path.basename(file) 227 | 228 | ds, arr = read(file) 229 | n_bands = ds.RasterCount 230 | 231 | if n_bands > 1: 232 | for n in range(0, n_bands): 233 | data_df['%s@%d' % (header, n+1)] = np.ravel(arr[n, :, :]) 234 | else: 235 | data_df['%s@%d' % (header, 1)] = np.ravel(arr) 236 | 237 | # Based on passed arguments, check for negatives and values to be removed 238 | if negative==False: 239 | data_df[data_df < 0] = 0 240 | 241 | for value in range(0, len(remove)): 242 | data_df[data_df == remove[value]] = 0 243 | 244 | 245 | if badrows == False: 246 | data_df = data_df[(data_df.T != 0).any()] 247 | 248 | # export the file 249 | data_df.to_csv(filename, index=False) 250 | 251 | 252 | def csv_to_raster(csvfile, ref_raster, cols=[], stacked=True, filename=None, 253 | dtype='default', compress=None, nodata=-9999): 254 | """ 255 | Convert a CSV file to raster 256 | 257 | Parameters 258 | ---------- 259 | csvfile : string 260 | CSV file name. Please provide full path if file is not located 261 | in the working directory. 262 | 263 | ref_raster : string 264 | A reference raster file for target cell size, extent, projection, etc. 265 | 266 | cols : list 267 | The list of column names of the CSV files that should be exported. Passing 268 | a blank list will export all the columns. 269 | 270 | stacked : boolean 271 | Whether to stack all bands in one file or export them as separate files. 272 | 273 | filename : string 274 | The name of the output GeoTIFF file. Please note that if the 'stacked' 275 | argument is set to negative, the column name will be added towards the 276 | end of the output file name. 277 | 278 | dtype : string 279 | The data type of the output raster. This is same as the options in the 280 | pyrsgis.raster.export module. Options are: 'byte', 'cfloat32', 281 | 'cfloat64', 'cint16', 'cint32', 'float', 'float32', 'float64', 'int', 282 | 'int16', 'int32', 'uint8', 'uint16', 'uint32'. 283 | 284 | compress : string 285 | Compression type of the raster. This is same as the pyrsgis.raster.export 286 | function. Options are 'LZW', 'DEFLATE' and other options that GDAL offers. 287 | 288 | nodata : signed number 289 | Value to treat as NoData in the out out raster. 290 | 291 | Examples 292 | -------- 293 | Let's assume that you convert a GeoTIFF file to CSV and perform some statistical analysis. 294 | 295 | >>> from pyrsgis import convert 296 | >>> input_file = r'E:/path_to_your_file/raster_file.tif' 297 | >>> out_csvfile = input_file.replace('.tif', '.csv') 298 | >>> convert.raster_to_csv(input_file, filename=out_csvfile, negative=False) 299 | 300 | ...create new column(s) (eg. clustering classes, predictions from a stats/ML model). And then 301 | convert the CSV to TIF file. 302 | 303 | >>> new_csvfile = r'E:/path_to_your_file/predicted_file.tif' 304 | >>> out_tiffile = new_csvfile.replace('.csv', '.tif') 305 | >>> convert.csv_to_raster(new_csvfile, ref_raster=input_file, filename=out_tiffile, compress='DEFLATE') 306 | 307 | This will export a GeoTIFF file. If there are multiple columns in the CSV file, the arrays 308 | will be stacked and exported as multispectral file. One can explicitly selct the columns to 309 | be exported but you should know the name of the columns beforehand. 310 | 311 | >>> convert.csv_to_raster(new_csvfile, ref_raster=input_file, filename=out_tiffile, 312 | cols=['Blue', 'Green', 'KMeans', 'RF_Class'], compress='DEFLATE') 313 | 314 | If you want to export each of the columns as separate bands, set the ``stacked`` parameter to 315 | ``False``. 316 | 317 | >>> convert.csv_to_raster(new_csvfile, ref_raster=input_file, filename=out_tiffile, 318 | cols=['Blue', 'Green', 'KMeans', 'RF_Class'], stacked=False, compress='DEFLATE') 319 | 320 | """ 321 | 322 | if filename == None: 323 | filename = csvfile.replace('.csv', '.tif') 324 | 325 | ds, _ = read(ref_raster, bands=1) 326 | _ = None 327 | x_size, y_size = ds.RasterYSize, ds.RasterXSize 328 | 329 | data_df = pd.read_csv(csvfile) 330 | n_cols = data_df.columns 331 | 332 | if len(cols) == 0: 333 | cols = data_df.columns 334 | 335 | out_arr = np.zeros((len(cols), x_size, y_size)) 336 | for n, col in enumerate(cols): 337 | out_arr[n, :, :] = np.reshape(data_df[col].values, (x_size, y_size)) 338 | data_df = None 339 | 340 | # add extension in the filename if missing 341 | if filename.endswith('.tif') == False: 342 | filename = filename + '.tif' 343 | 344 | if stacked == True: 345 | export(out_arr, ds, filename, dtype=dtype, compress=compress, nodata=nodata) 346 | else: 347 | for n, col in enumerate(cols): 348 | export(out_arr[n,:,:], ds, filename.replace('.tif', '_%s.tif'%(col)), 349 | dtype=dtype, compress=compress, nodata=nodata) 350 | 351 | """ 352 | def pandas_to_raster(data_df, x_col, y_col, ref_raster, filename='pyrsgis_pandastoraster.tif', 353 | columns=None, x_range=None, y_range=None, dtype='int', compress='default', 354 | nodata=-9999): 355 | # get minimum and maximum value for x 356 | if x_range == None: 357 | x_min = data_df[x_col].min() 358 | x_max = data_df[x_col].max() 359 | else: 360 | try: 361 | x_min, x_max = x_range 362 | except: 363 | print('Please provide a list containing range for "x_range" parameter.') 364 | 365 | if y_range == None: 366 | y_min = data_df[y_col].min() 367 | y_max = data_df[y_col].max() 368 | else: 369 | try: 370 | y_min, y_max = y_range 371 | except: 372 | print('Please provide a list containing range for "y_range" parameter.') 373 | 374 | # normalise and scale the x and y columns 375 | data_df[x_col] = data_df[x_col] - x_min 376 | data_df[y_col] = data_df[y_col] - y_min 377 | 378 | # generate raster to export 379 | ds, _ = raster.read(ref_raster) 380 | _ = None 381 | data_arr = np.zeros((data_df.shape[1] - 2, ds.RasterXSize, ds.RasterYSize)) 382 | 383 | if columns == None: 384 | columns = list(df.keys()) 385 | for col in [x_col, y_col]: 386 | columns.remove(col) 387 | 388 | for x_idx in data_df[x_col].values: 389 | for y_idx in data_df[y_col].values: 390 | for n, item in enumerate(columns): 391 | data_arr[n, x_id, y_idx] = data_df[item] 392 | 393 | # export the raster 394 | raster.export(data_arr, ds, filename, dtype=dtype, compress=compress, nodata=nodata) 395 | 396 | """ 397 | -------------------------------------------------------------------------------- /pyrsgis/raster/__init__.py: -------------------------------------------------------------------------------- 1 | #pyrsgis/raster 2 | 3 | import os, math 4 | import numpy as np 5 | from .. import doc_address 6 | from copy import deepcopy 7 | from .. import utils 8 | 9 | # safely import gdal (support old version) 10 | try: 11 | from osgeo import gdal 12 | except: 13 | import gdal 14 | 15 | 16 | class _create_ds(): 17 | 18 | def __init__(self, ds): 19 | self.Projection = ds.GetProjection() 20 | self.GeoTransform = ds.GetGeoTransform() 21 | self.RasterCount = ds.RasterCount 22 | self.RasterXSize = ds.RasterXSize 23 | self.RasterYSize = ds.RasterYSize 24 | self.DataType = ds.GetRasterBand(1).DataType 25 | self.update_bbox() 26 | self.dtypes = [ 27 | utils.datatype_dict_num_str[ds.GetRasterBand(i + 1).DataType] 28 | for i in range(ds.RasterCount) 29 | ] 30 | 31 | def GetProjection(self): 32 | return(self.Projection) 33 | 34 | def GetGeoTransform(self): 35 | return(self.GeoTransform) 36 | 37 | def RasterCount(self): 38 | return(self.RasterCount) 39 | 40 | def RasterXSize(self): 41 | return(self.RasterXSize) 42 | 43 | def RasterYSize(self): 44 | return(self.RasterYSize) 45 | 46 | def DataType(self): 47 | return(self.DataType) 48 | 49 | def update_bbox(self): 50 | self.ul_lon, self.cell_xsize, _, self.ul_lat, _, self.cell_ysize = self.GeoTransform 51 | self.lr_lon = self.ul_lon + (self.RasterXSize * self.cell_xsize) 52 | self.lr_lat = self.ul_lat + (self.RasterYSize * self.cell_ysize) 53 | 54 | self.bbox = tuple([[self.ul_lon, self.lr_lat], [self.lr_lon, self.ul_lat]]) 55 | 56 | def _extract_bands(ds, bands): 57 | if ds.RasterCount > 1: 58 | layer, row, col = ds.RasterCount, ds.RasterYSize, ds.RasterXSize 59 | if (type(bands) == type(list())) or (type(bands) == type(tuple())): 60 | array = np.random.randint(1, size=(len(bands), row, col)) 61 | for n, index in enumerate(bands): 62 | tempArray = ds.GetRasterBand(index) 63 | array[n, :, :] = tempArray.ReadAsArray() 64 | if array.shape[0] == 1: 65 | array = np.reshape(array, (row, col)) 66 | elif type(bands) == type(1): 67 | array = ds.GetRasterBand(bands).ReadAsArray() 68 | else: 69 | array = ds.ReadAsArray() 70 | return(array) 71 | 72 | def read(file, bands='all'): 73 | """ 74 | Read raster file 75 | 76 | The function reads the raster file and generates two object. The first one is a 77 | datasource object and the second is a numpy array that contains cell values of the 78 | bands. 79 | 80 | Parameters 81 | ---------- 82 | file : datasource object 83 | Path to the input file. 84 | 85 | bands : integer, tuple, list or 'all' 86 | Bands to read. This can either be a specific band number you wish 87 | to read or a list or tuple of band numbers or 'all'. 88 | 89 | Returns 90 | ------- 91 | datasource : datasource object 92 | A data source object that contains the metadata of the raster file. 93 | Information such as the number of rows and columns, number of bands, 94 | data type of raster, projection, cellsize, geographic extent etc. are 95 | stored in this datasource object. 96 | 97 | data_array : numpy array 98 | A 2D or 3D numpy array that contains the DN values of the raster file. 99 | If the input file is a single band raster, the function returns a 2D array. 100 | Whereas, if the input file is a multiband raster, this function returns a 101 | 3D array. 102 | 103 | Attributes 104 | ---------- 105 | RasterCount : integer 106 | The total number of bands in the input file. 107 | 108 | RasterXSize : integer 109 | Height of the raster represented as the number of rows. 110 | 111 | RasterYSize : integer 112 | Width of the raster represented as the number of columns. 113 | 114 | Projection : projection object 115 | The projection of the input file. 116 | 117 | GeoTransform : geotransform object 118 | This is the Geotransform tuple of the input file. The tuple 119 | can be converted to list and updated to change various properties 120 | of the raster file. 121 | 122 | Examples 123 | -------- 124 | >>> from pyrsgis import raster 125 | >>> input_file = r'E:/path_to_your_file/raster_file.tif' 126 | >>> ds, data_arr = raster.read(input_file) 127 | 128 | The 'data_arr' is a numpy array. The 'ds' is the datasource object that 129 | contains details about the raster file. 130 | 131 | >>> print(ds.RasterXSize, ds.RasterYSize) 132 | 133 | The output from the above line will be equal to the followng line: 134 | 135 | >>> print(arr.shape) 136 | 137 | Please note that if the input file is a multispectral raster, the 'arr.shape' 138 | command will result a tuple of size three, where the number of bands will be at 139 | the first index. For multispectral input files, the 'arr.shape' will be equal 140 | to the following line: 141 | 142 | >>> print(ds.RasterCount, ds.RasterXSize, ds.RasterYSize) 143 | 144 | """ 145 | 146 | ds = gdal.Open(file) 147 | if type(bands) == type('all'): 148 | if bands.lower()=='all': 149 | array = ds.ReadAsArray() 150 | ds = _create_ds(ds) 151 | return(ds, array) 152 | elif type(bands) == type(list()) or\ 153 | type(bands) == type(tuple()) or\ 154 | type(bands) == type(1): 155 | array = _extract_bands(ds, bands) 156 | ds = _create_ds(ds) 157 | return(ds, array) 158 | else: 159 | print("Inappropriate bands selection. Please use the following arguments:\n1) bands = 'all'\n2) bands = [2, 3, 4]\n3) bands = 2") 160 | return(None, None) 161 | 162 | raster_dtype = {'byte': gdal.GDT_Byte, 163 | 'cfloat32': gdal.GDT_CFloat32, 164 | 'cfloat64': gdal.GDT_CFloat64, 165 | 'cint16': gdal.GDT_CInt16, 166 | 'cint32': gdal.GDT_CInt32, 167 | 'float': gdal.GDT_Float32, 168 | 'float32': gdal.GDT_Float32, 169 | 'float64': gdal.GDT_Float64, 170 | 'int': gdal.GDT_Int16, 171 | 'int16': gdal.GDT_Int16, 172 | 'int32': gdal.GDT_Int32, 173 | 'uint8': gdal.GDT_Byte, 174 | 'uint16': gdal.GDT_UInt16, 175 | 'uint32': gdal.GDT_UInt32, 176 | } 177 | 178 | def export(arr, ds, filename='pyrsgis_outFile.tif', dtype='default', bands='all', nodata=-9999, compress=None): 179 | """ 180 | Export GeoTIFF file 181 | 182 | This function exports the GeoTIF file using a data source object 183 | and an array containing cell values. 184 | 185 | Parameters 186 | ---------- 187 | arr : array 188 | A numpy array containing the cell values of 189 | to-be exported raster. This can either be a 2D 190 | or a 3D array. Please note that the channel 191 | index should be in the beginning. 192 | 193 | ds : datasource object 194 | The datasource object of a target reference raster. 195 | 196 | filename : string 197 | Output file name ending with '.tif'. This can include 198 | relative path or full path of the file. 199 | 200 | dtype : string 201 | The data type of the raster to be exported. It can take 202 | one of these values, 'byte', 'cfloat32', 'cfloat64', 203 | 'cint16', 'cint32', 'float', 'float32', 'float64', 204 | 'int', 'int16', 'int32', 'uint8', 'uint16', 'uint32' 205 | or 'default'. The 'default' type value will take the 206 | data type from the given datasource object. It is advised 207 | to use a lower depth data value, this helps in reducing the 208 | file size on the disk. 209 | 210 | bands : integer, list, tuple or 'all' 211 | The band(s) you want to export. Please note that the band 212 | number here is actual position on band instead of Python's default 213 | index number. That is, the first band should be referred as 1. 214 | The 'bands' parameter defaults to 1 and should only be tweaked when 215 | the data array to be exported is a 3D array. If not specified, only the 216 | first band of the 3D array will be exported. 217 | 218 | nodata : signed integer 219 | The value that you want to tret as NULL or NoData in you output raster. 220 | 221 | compress : string 222 | Compression type of your output raster. Options are 'LZW', 'DEFLATE' 223 | and other methods that GDAL offers. Compressing the data can save a lot 224 | of disk space without losing data. Some methods, for instance 'LZW' 225 | can reduce the size of the raster from more than a GB to less than 20 MB. 226 | 227 | Examples 228 | -------- 229 | >>> from pyrsgis import raster 230 | >>> input_file = r'E:/path_to_your_file/landsat8_multispectral.tif' 231 | >>> ds, data_arr = raster.read(input_file) 232 | >>> red_arr = data_arr[3, :, :] 233 | >>> nir_arr = data_arr[4, :, :] 234 | >>> ndvi_arr = (nir_arr - red_arr) / (nir_arr + red_arr) 235 | 236 | Or directly in one go: 237 | 238 | >>> ndvi_arr = (data_arr[4, :, :] - data_arr[3, :, :]) / (data_arr[4, :, :] + data_arr[3, :, :]) 239 | 240 | And then export: 241 | 242 | >>> output_file = r'E:/path_to_your_file/landsat8_ndvi.tif' 243 | >>> raster.export(ndvi_arr, ds, output_file, dtype='float32') 244 | 245 | Note that the 'dtype' parameter here is explicitly defined as 'float32' 246 | since NDVI is a continuous data. 247 | 248 | """ 249 | 250 | # if given file name does not end with .tif, add it 251 | if os.path.splitext(filename)[-1].lower() != '.tif': 252 | filename = filename + '.tif' 253 | 254 | #If dtype is default and matches with ds, use int16. 255 | #If dtype is default and disagrees with ds, use ds datatype. 256 | #If a dtype is specified, use that. 257 | 258 | if (dtype == 'default') and (ds.DataType == raster_dtype['int']): 259 | dtype = 'int' 260 | elif (dtype == 'default') and (ds.DataType != raster_dtype['int']): 261 | dtype = list(raster_dtype.keys())[list(raster_dtype.values()).index(ds.DataType)] 262 | else: 263 | pass 264 | 265 | if len(arr.shape) == 3: 266 | layers, row, col = arr.shape 267 | elif len(arr.shape) == 2: 268 | row, col = arr.shape 269 | layers = 1 270 | 271 | if type(bands) == type('all'): 272 | if bands.lower() == 'all': 273 | nBands = layers 274 | if nBands > 1: 275 | outBands = list(np.arange(1, layers+1)) 276 | elif nBands == 1: 277 | outBands = [1] 278 | arr = np.reshape(arr, (1, row, col)) 279 | if type(bands) == type(1): 280 | nBands = 1 281 | elif type(bands) == type(list()) or\ 282 | type(bands) == type(tuple()): 283 | nBands = len(bands) 284 | outBands = bands 285 | driver = gdal.GetDriverByName("GTiff") 286 | 287 | if compress == None: 288 | outdata = driver.Create(filename, col, row, nBands, raster_dtype[dtype.lower()]) 289 | else: 290 | outdata = driver.Create(filename, col, row, nBands, raster_dtype[dtype.lower()], options=['COMPRESS=%s'%(compress)]) 291 | outdata.SetGeoTransform(ds.GetGeoTransform()) 292 | outdata.SetProjection(ds.GetProjection()) 293 | if type(bands) == type(1): 294 | if layers > 1: 295 | outdata.GetRasterBand(nBands).WriteArray(arr[bands-1, :, :]) 296 | outdata.GetRasterBand(nBands).SetNoDataValue(nodata) 297 | else: 298 | outdata.GetRasterBand(nBands).WriteArray(arr) 299 | outdata.GetRasterBand(nBands).SetNoDataValue(nodata) 300 | elif (type(bands) == type('all') and bands.lower()=='all') or\ 301 | type(bands) == type([1, 2, 3])or\ 302 | type(bands) == type(tuple()): 303 | for n, bandNumber in enumerate(outBands): 304 | outdata.GetRasterBand(n+1).WriteArray(arr[bandNumber-1,:,:]) 305 | outdata.GetRasterBand(n+1).SetNoDataValue(nodata) 306 | outdata.FlushCache() 307 | outdata = None 308 | 309 | def north_east(arr, layer='both', flip_north=False, flip_east=False): 310 | """ 311 | Generate row and column number arrays 312 | 313 | This function can generate 2D arrays containing row or column number 314 | of each cell, which are referred to as northing and easting arrays here. 315 | 316 | Parameters 317 | ---------- 318 | arr : array 319 | A 2D or 3D numpy array. 320 | 321 | layer : string 322 | Either of these options: 'both', 'north', 'east' 323 | 324 | flip_north : boolean 325 | Whether to flip the northing array. If True, the 326 | array will be flipped such that the values increase 327 | from bottom to top instead of top to bottom, which 328 | is the default way. 329 | 330 | flip_east : boolean 331 | Whether to flip the easting array. If True, the 332 | array will be flipped such that the values increase 333 | from right to left instead of left to right, which 334 | is the default way. 335 | 336 | Returns 337 | ------- 338 | array(s) : 2D numpy array(s) 339 | If ``layers`` argument was left default or set to 'both', 340 | then the function returns a tuple of both the northing and 341 | easting arrays. Otherwise, either northing or easting array will 342 | be returned depending on the ``layers`` argument. 343 | 344 | Examples 345 | -------- 346 | >>> from pyrsgis import raster 347 | >>> input_file = r'E:/path_to_your_file/your_file.tif' 348 | >>> ds, data_arr = raster.read(your_file) 349 | >>> north_arr, east_arr = raster.north_east(data_arr) 350 | >>> print(north_arr.shape, east_arr.shape) 351 | 352 | The above line will generate both the arrays of same size as the input array. 353 | If you want either of north or east rasters, you can do so by specifying the 354 | ``layer`` parameter: 355 | 356 | >>> north_arr = raster.north_east(data_arr, layer='north') 357 | 358 | To check, you can display them one by one using matplotlib: 359 | 360 | >>> from matplotlib import pyplot as plt 361 | >>> plt.imshow(north_arr) 362 | >>> plt.show() 363 | >>> plt.imshow(east_arr) 364 | >>> plt.show() 365 | 366 | The arrays will be displayed as image one by one. You can hover over the 367 | displayed image to check the cell values. If you wish to flip the northing 368 | array upside down or easting array left to right, you can do so by specifying 369 | the ``flip_north`` and/or ``flip_east`` to ``True``. 370 | 371 | >>> north_arr, east_arr = raster.north_east(data_arr, flip_north=True, flip_east=True) 372 | 373 | You can again display these new arrays and hover the mouse to check values, 374 | which will now be different from the default ones. 375 | """ 376 | 377 | if len(arr.shape) > 2 : _, row, col = arr.shape 378 | if len(arr.shape) == 2 : row, col = arr.shape 379 | 380 | north = np.linspace(1, row, row) 381 | east = np.linspace(1, col, col) 382 | east, north = np.meshgrid(east, north) 383 | 384 | if flip_north == True : north = np.flip(north, axis=0) 385 | if flip_east == True : east = np.flip(east, axis=1) 386 | 387 | if layer=='both': 388 | return(north, east) 389 | elif layer=='north': 390 | return(north) 391 | elif layer=='east': 392 | return(east) 393 | 394 | def north_east_coordinates(ds, arr, layer='both'): 395 | """ 396 | Generate arrays with cell latitude and longitude value. 397 | 398 | This function can generate arrays that contain the centroid latitude 399 | and longitude values of each cell. 400 | 401 | Parameters 402 | ---------- 403 | ds : datasource object 404 | A datasource object of a raster, typically generated using the 405 | ``pyrsgis.raster.read`` function. 406 | 407 | arr : array 408 | A 2D or 3D array of the raster corresponding to the daasource object. 409 | 410 | layer : string 411 | Either of these options: 'both', 'north', 'east' 412 | 413 | Returns 414 | ------- 415 | array(s) : 2D numpy array(s) 416 | If ``layers`` argument was left default or set to 'both', 417 | then the function returns a tuple of both the northing and 418 | easting arrays. Otherwise, either northing or easting array will 419 | be returned depending on the ``layers`` argument. 420 | 421 | Examples 422 | -------- 423 | >>> from pyrsgis import raster 424 | >>> input_file = r'E:/path_to_your_file/your_file.tif' 425 | >>> ds, data_arr = raster.read(your_file) 426 | >>> north_arr, east_arr = raster.north_east(data_arr) 427 | >>> print(north_arr.shape, east_arr.shape) 428 | 429 | The above line will generate both the arrays of same size as the input array. 430 | If you want either of north or east rasters, you can do so by specifying the 431 | ``layer`` parameter: 432 | 433 | >>> north_arr = raster.north_east(data_arr, layer='north') 434 | 435 | To check, you can display them one by one using matplotlib: 436 | 437 | >>> from matplotlib import pyplot as plt 438 | >>> plt.imshow(north_arr) 439 | >>> plt.show() 440 | >>> plt.imshow(east_arr) 441 | >>> plt.show() 442 | 443 | The arrays will be displayed as image one by one. You can hover over the 444 | displayed image to check the cell values. 445 | 446 | The generated latitude and longitude arrays can be exported using the following: 447 | 448 | >>> raster.export(north_arr, ds, r'E:/path_to_your_file/northing.tif', dtype='float32') 449 | >>> raster.export(east_arr, ds, r'E:/path_to_your_file/easting.tif', dtype='float32') 450 | 451 | """ 452 | 453 | if layer.lower() == 'north': north = north_east(arr, layer='north') 454 | if layer.lower() == 'east': east = north_east(arr, layer='east') 455 | if layer.lower() == 'both': north, east = north_east(arr, layer='both') 456 | 457 | if 'north' in locals().keys(): north = list(ds.GeoTransform)[3] + (north * list(ds.GeoTransform)[-1] - list(ds.GeoTransform)[-1]/2) 458 | if 'east' in locals().keys(): east = list(ds.GeoTransform)[0] + (east * list(ds.GeoTransform)[1] - list(ds.GeoTransform)[1]/2) 459 | 460 | if layer=='both': 461 | return(north, east) 462 | elif layer=='north': 463 | return(north) 464 | elif layer=='east': 465 | return(east) 466 | 467 | def northing(reference_file, outFile='pyrsgis_northing.tif', value='number', flip=True, dtype='int16', compress=None): 468 | """ 469 | Generate northing raster using a reference .tif file 470 | 471 | This function generates northing raster from a given raster file. 472 | 473 | Parameters 474 | ---------- 475 | reference_file : string 476 | Path to a reference raster file for which the northing file 477 | will be generated. 478 | 479 | outfile : string 480 | Path to the output northing file, ideally with '.tif' extension. 481 | 482 | value : string 483 | The desired value in the output raster. Available options are 484 | 'number', 'normalised' and 'coordinates'. 'number' will result in 485 | the row number of the cells, 'normalised' will scale the row number 486 | values such that the output raster ranges from 0 to 1. 'coordinates' 487 | will result in raster that contain the latitude of centroid of each cell. 488 | 489 | flip : boolean 490 | Whether to flip the resulting raster or not. If ``True``, the 491 | values in output northing raster will be flipped upside down. 492 | Please note that this option is only viable when the ``value`` 493 | parameter is set to number or normalised. Flipping will not 494 | work when the ``value`` parameter is set to coordinates. 495 | 496 | dtype : string 497 | The data type of the output raster. 498 | 499 | compress : string 500 | Compression type of your output raster. Options are 'LZW', 'DEFLATE' 501 | and other methods that GDAL offers. This is same as the ``pyrsgis.raster.export`` 502 | function. 503 | 504 | Examples 505 | -------- 506 | >>> from pyrsgis import raster 507 | >>> reference_file = r'E:/path_to_your_file/your_file.tif' 508 | >>> raster.northing(file1, r'E:/path_to_your_file/northing_number.tif', flip=False, value='number') 509 | 510 | This will save the file where cells calue represents the row number. Please note that the 511 | ``flip`` parameter defaults to ``True`` to replicate the way latitudes increase, that is, 512 | from bottom to top. 513 | 514 | If you want to normalise the output raster, switch the ``value`` parameter to 'normalised'. 515 | You can switch the ``flip`` parameter as per your requirement. 516 | 517 | >>> raster.northing(file1, r'E:/path_to_your_file/northing_normalised.tif', value='normalised') 518 | 519 | If you want the output file to have the latitude of cell centre, you can change the ``value`` 520 | switch. Please note that the ``flip`` switch will be disabled when exporting as coordinates. 521 | 522 | >>> raster.northing(file1, r'E:/path_to_your_file/northing_coordinates.tif', value='coordinates') 523 | 524 | It has been found that significant reduction is disk space usage can be achieved by passing a 525 | compression type of the output raster, therefore, doing so is highly recommended. An example of 526 | using the ``compres`` parameter. 527 | 528 | >>> raster.northing(file1, r'E:/path_to_your_file/northing_number_compressed.tif', compress='DEFLATE') 529 | 530 | """ 531 | 532 | ds, data_arr = read(reference_file, bands=1) 533 | north = north_east(data_arr, layer='north') 534 | 535 | if value.lower() == 'coordinates': 536 | flip = False 537 | north = list(ds.GeoTransform)[3] + (north * list(ds.GeoTransform)[-1] - list(ds.GeoTransform)[-1]/2) 538 | dtype = 'float32' 539 | 540 | elif value.lower() == 'normalised': 541 | north += 1 542 | north /= north.max() 543 | dtype = 'float32' 544 | 545 | if flip == True : north = np.flip(north, axis=0) 546 | 547 | export(north, ds, filename=outFile, dtype=dtype, compress=compress) 548 | 549 | def easting(reference_file, outfile='pyrsgis_easting.tif', value='number', flip=False, dtype='int16', compress=None): 550 | """ 551 | Generate easting raster using a reference .tif file 552 | 553 | This function generates northing raster from a given raster file. 554 | 555 | Parameters 556 | ---------- 557 | reference_file : string 558 | Path to a reference raster file for which the easting file 559 | will be generated. 560 | 561 | outfile : string 562 | Path to the output easting file, ideally with '.tif' extension. 563 | 564 | value : string 565 | The desired value in the output raster. Available options are 566 | 'number', 'normalised' and 'coordinates'. 'number' will result in 567 | the column number of the cells, 'normalised' will scale the column number 568 | values such that the output raster ranges from 0 to 1. 'coordinates' 569 | will result in raster that contain the longitude of centroid of each cell. 570 | 571 | flip : boolean 572 | Whether to flip the resulting raster or not. If ``True``, the 573 | values in output northing raster will be flipped upside down. 574 | Please note that this option is only viable when the ``value`` 575 | parameter is set to number or normalised. Flipping will not 576 | work when the ``value`` parameter is set to coordinates. 577 | 578 | dtype : string 579 | The data type of the output raster. 580 | 581 | compress : string 582 | Compression type of your output raster. Options are 'LZW', 'DEFLATE' 583 | and other methods that GDAL offers. This is same as the ``pyrsgis.raster.export`` 584 | function. 585 | 586 | Examples 587 | -------- 588 | >>> from pyrsgis import raster 589 | >>> reference_file = r'E:/path_to_your_file/your_file.tif' 590 | >>> raster.easting(file1, r'E:/path_to_your_file/easting_number.tif', flip=False, value='number') 591 | 592 | This will save the file where cells calue represents the column number. Please note that the 593 | ``flip`` parameter defaults to ``False`` to replicate the way longitudes increase, that is, 594 | from left to right. 595 | 596 | If you want to normalise the output raster, switch the ``value`` parameter to 'normalised'. 597 | You can switch the ``flip`` parameter as per your requirement. 598 | 599 | >>> raster.easting(file1, r'E:/path_to_your_file/easting_normalised.tif', value='normalised') 600 | 601 | If you want the output file to have the longitude of cell centre, you can change the ``value`` 602 | switch. Please note that the ``flip`` switch will be disabled when exporting as coordinates. 603 | 604 | >>> raster.easting(file1, r'E:/path_to_your_file/easting_coordinates.tif', value='coordinates') 605 | 606 | It has been found that significant reduction is disk space usage can be achieved by passing a 607 | compression type of the output raster, therefore, doing so is highly recommended. An example of 608 | using the ``compres`` parameter. 609 | 610 | >>> raster.easting(file1, r'E:/path_to_your_file/easting_number_compressed.tif', compress='DEFLATE') 611 | 612 | """ 613 | 614 | ds, data_arr = read(reference_file, bands=1) 615 | east = north_east(data_arr, layer='east') 616 | 617 | if value.lower() == 'coordinates': 618 | flip = False 619 | east = list(ds.GeoTransform)[0] + (east * list(ds.GeoTransform)[1] - list(ds.GeoTransform)[1]/2) 620 | dtype = 'float32' 621 | 622 | elif value.lower() == 'normalised': 623 | east += 1 624 | east /= east.max() 625 | dtype = 'float32' 626 | 627 | if flip==True: east = np.flip(east, axis=1) 628 | 629 | export(east, ds, filename=outfile, dtype=dtype, compress=compress) 630 | 631 | def radiometric_correction(arr, pixel_depth=8, return_minmax=False, min_val=None, max_val=None): 632 | if len(arr.shape) == 3: 633 | if min_val == None: min_val = list() 634 | if max_val == None: max_val = list() 635 | 636 | for bandNumber in range(arr.shape[0]): 637 | min_val.append(arr[bandNumber, :, :].min()) 638 | max_val.append(arr[bandNumber, :, :].max()) 639 | 640 | arr[bandNumber, :, :] = 2**pixel_depth*((arr[bandNumber, :, :] - min_val[-1]) / (max_val[-1] - min_val[-1])) 641 | else: 642 | if min_val == None: min_val = arr.min() 643 | if max_val == None: max_val = arr.max() 644 | 645 | arr = (arr - min_val) / (max_val - min_val) 646 | 647 | if return_minmax == True: 648 | return arr, min_val, max_val 649 | else: 650 | return arr 651 | 652 | def shift(ds, x=0, y=0, shift_type='coordinate'): 653 | """ 654 | Shift the datasource of a raster file 655 | 656 | This function can modify the geographic extent in the datasource object of the 657 | raster file. When the modified datasource object is used, the exported raster 658 | file will be shifted towards a given direction. 659 | 660 | Parameters 661 | ---------- 662 | ds : datasource object 663 | A datasource object of the raster. Ideally, the datasource should be the 664 | same as one generated by the ``pyrsgis.raster.read`` function. 665 | 666 | shift_type : string 667 | Available options are 'coordinates' and 'cell', which correspond to 668 | shifting the datasource either by the projection units of the raster 669 | or by number of cells respectively. 670 | 671 | x : number 672 | The amount of shift required in x direction (longitude). Please note that 673 | this can not be float value if the ``shift_type`` parameter is set to 'cell'. 674 | 675 | y : number 676 | The amount of shift required in y direction (latitude). Please note that 677 | this can not be float value if the ``shift_type`` parameter is set to 'cell'. 678 | 679 | Returns 680 | ------- 681 | new_ds : datasource object 682 | A new modified datasource object which when used to export a raster will 683 | result in a shifted raster file. 684 | 685 | Examples 686 | -------- 687 | >>> from pyrsgis import raster 688 | >>> infile = r'E:/path_to_your_file/your_file.tif' 689 | >>> ds, data_arr = raster.read(infile) 690 | >>> new_ds = raster.shift(ds, x=10, y=10) 691 | >>> print('Original bounding box:', ds.bbox) 692 | >>> print('Modified bounding box:', new_ds.bbox) 693 | Original geo transform: ([752895.0, 1405185.0], [814215.0, 1466805.0]) 694 | Modified geo transform: ([752905.0, 1405195.0], [814225.0, 1466815.0]) 695 | 696 | Notice that the bounding box values in the ``ds`` object have both shifted 697 | by 10 units. Negative values can be given to shift the ``ds`` onject in the opposite 698 | direction. 699 | 700 | The ``ds`` object can also be shifted by number of cells by switching the ``shift_type`` 701 | parameter. 702 | 703 | >>> new_ds = raster.shift(ds, x=10, y=10, shift_type='cell') 704 | >>> print('Modified bounding box:', new_ds.GeoTransform) 705 | Modified geo transform: ([753195.0, 1405485.0], [814515.0, 1467105.0]) 706 | 707 | Notice that the modified ``ds`` object is now shifted by 10*cell size (30 - a Landsat 708 | data used for demonstration). 709 | 710 | The modified ``ds`` object can be used to export raste file. 711 | 712 | >>> raster.export(data_arr, new_ds, r'E:/path_to_your_file/shifted_file.tif') 713 | 714 | """ 715 | 716 | if shift_type.lower() in ['coordinate', 'cell']: 717 | 718 | new_ds = deepcopy(ds) 719 | out_transform = list(new_ds.GeoTransform) 720 | 721 | if shift_type.lower() == 'coordinate': 722 | delta_x = x 723 | delta_y = y 724 | elif shift_type.lower() == 'cell': 725 | delta_x = x * out_transform[1] 726 | delta_y = -y * out_transform[-1] 727 | 728 | out_transform[0] += delta_x 729 | out_transform[3] += delta_y 730 | new_ds.GeoTransform = tuple(out_transform) 731 | 732 | new_ds.update_bbox() 733 | 734 | return(new_ds) 735 | else: 736 | print("Invalid shift_type. Acceptable options are " + \ 737 | "'coordinate' and 'cell'. Please see the documentation at %s" % (doc_address)) 738 | 739 | def shift_file(file, shift_type='coordinate', x=0, y=0, outfile=None, dtype='default'): 740 | """ 741 | Shift and export raster file in one go 742 | 743 | This function can export a shifted version of the input raster file. 744 | 745 | Parameters 746 | ---------- 747 | file : string 748 | Name or path of the file to be shifted. 749 | 750 | shift_type : string 751 | Available options are 'coordinates' and 'cell', which correspond to 752 | shifting the raster either by the projection units of the raster 753 | or by number of cells respectively. 754 | 755 | x : number 756 | The amount of shift required in x direction (longitude). Please note that 757 | this can not be float value if the ``shift_type`` parameter is set to 'cell'. 758 | 759 | y : number 760 | The amount of shift required in y direction (latitude). Please note that 761 | this can not be float value if the ``shift_type`` parameter is set to 'cell'. 762 | 763 | outfile : string 764 | Outpt raster file name or path, ideally with a '.tif' extension. 765 | 766 | dtype : string 767 | The data type of the output raster. If nothing is passed, the data type is 768 | picked from the ``ds`` object. 769 | 770 | Examples 771 | -------- 772 | >>> from pyrsgis import raster 773 | >>> infile = r'E:/path_to_your_file/your_file.tif' 774 | >>> outfile = r'E:/path_to_your_file/shifted_file.tif' 775 | >>> raster.shift_file(infile, x=10, y=10, outfile=outfile) 776 | 777 | The exported file will be shifted by 10 units in the x and y directions. 778 | You may pass negative values to shift the raster in the opposite direction. 779 | 780 | The raster file can also be shifted by number of cells by switching the ``shift_type`` 781 | parameter. 782 | 783 | >>> raster.shift_file(infile, x=10, y=10, outfile=outfile, shift_type='cell') 784 | 785 | """ 786 | 787 | ds, arr = read(file) 788 | out_ds = shift(ds, x, y, shift_type) 789 | 790 | if outfile == None: 791 | outfile = '%s_shifted.tif' % (os.path.splitext(file)[0]) 792 | 793 | export(arr, out_ds, filename=outfile, dtype=dtype) 794 | 795 | def clip(ds, data_arr, x_min, x_max, y_min, y_max): 796 | """ 797 | ds : datasource object 798 | A datasource object of the raster. Ideally, the datasource should be the 799 | same as one generated by the ``pyrsgis.raster.read`` function. 800 | 801 | data_arr : array 802 | A 2D or 3D array to clip. 803 | 804 | x_min : integer or float 805 | The lower longitude value. 806 | 807 | x_max : integer or float 808 | The upper longitude value. 809 | 810 | y_min : integer or float 811 | The lower latitude value. 812 | 813 | y_max : integer or float 814 | The upper latitude value. 815 | 816 | 817 | Returns 818 | ------- 819 | ds : datasource object 820 | A modified ``ds`` object. 821 | 822 | clipped_array : array 823 | A 2D or 3D array. This is the clipped version of the input array. 824 | 825 | Examples 826 | -------- 827 | >>> from pyrsgis import raster 828 | >>> infile = r'E:/path_to_your_file/your_file.tif' 829 | >>> ds, data_arr = raster.read(infile) 830 | >>> print('Original bounding box:', ds.bbox) 831 | >>> print('Original shape of raster:', data_arr.shape) 832 | Original bounding box: ([752893.9818, 1405185.0], [814213.9818, 1466805.0]) 833 | Original shape of raster: (2054, 2044) 834 | 835 | This is the original raster data. Now clip the raster using a bounding box of 836 | your choice. 837 | 838 | >>> new_ds, clipped_arr = raster.clip(ds, data_arr, x_min=770000, x_max=790000, y_min=1420000, y_max=1440000) 839 | >>> print('Clipped bounding box:', ds.bbox) 840 | >>> print('Clipped shape of raster:', data_arr.shape) 841 | Clipped bounding box: ([770023.9818, 1420005.0], [789973.9818, 1439985.0]) 842 | Clipped shape of raster: (666, 665) 843 | 844 | Note that the raster extent and the data array has now been clipped using the 845 | given bounding box. The raster can now easily be exported. 846 | 847 | >>> raster.export(clipped_arr, new_ds, r'E:/path_to_your_file/clipped_file.tif') 848 | 849 | """ 850 | 851 | # if array is multiband, take the first band 852 | temp_array = data_arr[0, :, :] if len(data_arr.shape) == 3 else data_arr 853 | 854 | north, east = north_east_coordinates(ds, temp_array, layer='both') 855 | 856 | # make values beyond the lat long zero 857 | temp_array[north < (y_min - ds.cell_ysize/2)] = 0 858 | temp_array[north > (y_max + ds.cell_ysize/2)] = 0 859 | temp_array[east < (x_min + ds.cell_xsize/2)] = 0 860 | temp_array[east > (x_max - ds.cell_xsize/2)] = 0 861 | 862 | # get the bouding box and clip the array 863 | non_zero_index = np.nonzero(temp_array) 864 | row_min, row_max = non_zero_index[0].min(), non_zero_index[0].max()+1 865 | col_min, col_max = non_zero_index[1].min(), non_zero_index[1].max()+1 866 | 867 | # modify the metadata 868 | north = north[row_min:row_max, col_min:col_max] 869 | east = east[row_min:row_max, col_min:col_max] 870 | #print(north.shape, east.shape) 871 | #print(north.min(), east.max()) 872 | 873 | geo_transform = list(ds.GeoTransform) 874 | geo_transform[3] = north.max() - (ds.GeoTransform[-1] / 2) 875 | geo_transform[0] = east.min() - (ds.GeoTransform[1] / 2) 876 | out_ds = deepcopy(ds) 877 | out_ds.GeoTransform = tuple(geo_transform) 878 | out_ds.RasterYSize, out_ds.RasterXSize = north.shape 879 | out_ds.update_bbox() 880 | 881 | if len(data_arr.shape) == 3: 882 | return(out_ds, data_arr[:, row_min:row_max, col_min:col_max]) 883 | else: 884 | return(out_ds, data_arr[row_min:row_max, col_min:col_max]) 885 | 886 | def clip_file(file, x_min, x_max, y_min, y_max, outfile=None): 887 | """ 888 | Clip and export raster file in one go 889 | 890 | This function can clip a raster file by using minimum and maximum latitude 891 | and longitude values. This function builds on the ``pyrsgis.raster.clip`` 892 | function. 893 | 894 | Parameters 895 | ---------- 896 | file : string 897 | Input file name or path to clip. 898 | 899 | x_min : integer or float 900 | The lower longitude value. 901 | 902 | x_max : integer or float 903 | The upper longitude value. 904 | 905 | y_min : integer or float 906 | The lower latitude value. 907 | 908 | y_max : integer or float 909 | The upper latitude value. 910 | 911 | outfile : string 912 | Name or path to store the clipped raster file. 913 | 914 | Examples 915 | -------- 916 | >>> from pyrsgis import raster 917 | >>> infile = r'E:/path_to_your_file/your_file.tif' 918 | >>> outfile = r'E:/path_to_your_file/clipped_file.tif' 919 | >>> raster.clip_file(infile, x_min=770000, x_max=790000, y_min=1420000, y_max=1440000, outfile=outfile) 920 | 921 | The catch here is that the user should be aware of and has to assign the minimum 922 | and maximum latitude and longitude values. If you are not sure of the extent to 923 | clip, you may want to first read the raster file, plot and find your area of interest 924 | and check the bounding box of the input raster. To do so, please check the 925 | ``pyrsgis.raster.clip`` function. 926 | 927 | """ 928 | 929 | ds, array = read(file) 930 | 931 | ds, array = clip(ds, array, x_min, x_max, y_min, y_max) 932 | 933 | if outfile == None: 934 | outfile = '%s_clipped.tif' % (os.path.splitext(file)[0]) 935 | 936 | export(array, ds, filename=outfile, bands='all') 937 | 938 | def trim_array(data_arr, remove='negative', return_clip_index=False): 939 | """ 940 | Trim raster array to remove NoData value at the edge 941 | 942 | This function trims an array by removing the given unwanted value. Note that the function 943 | will trim array for the smallest possible bounding box outside which all the cells in the 944 | input array have the value equal to the value passed using ``remove`` parameter. 945 | 946 | Parameters 947 | ---------- 948 | data_arr : array 949 | A 2D or 3D array to clip. Ideally this should be a raster array that has 950 | unimportant value at the edges (NoData cells in most cases). 951 | 952 | remove : integer or float or string 953 | The value to be considered as irrelevant at the edges. It can be a 954 | integer or a float number. An additional option 'negative' is also 955 | available that will treat negative values of the array as unnecessary. 956 | 957 | return_clip_index: boolean 958 | Whether to return the index used for clipping. If ``True``, the function 959 | will return both, the clipped array and a list cointaining [x_min, y_min] 960 | and [x_max, y_max] representing column and row indexes. 961 | 962 | Returns 963 | ------- 964 | trimmed_arr : array 965 | A trimmed array. 966 | 967 | Examples 968 | -------- 969 | >>> from pyrsgis import raster 970 | >>> infile = r'E:/path_to_your_file/your_file.tif' 971 | >>> ds, data_arr = raster.read(infile) 972 | >>> new_arr = raster.trim_array(data_arr, remove=-9999) 973 | >>> print('Shape of the input array:', data_arr.shape) 974 | >>> print('Shape of the trimmed array:', new_arr.shape) 975 | Shape of the input array: (15000, 15900) 976 | Shape of the trimmed array: (7059, 7685) 977 | 978 | In the above example a raster file which was masked using a polygon but the option 'match extent 979 | of the raster with extent of the input polygon' was disabled has been used for demonstration. 980 | Although this is a classic example of observing unnecessary padding at the edges of a raster file, 981 | there can be many more reason for the same. In this particular case, useful values were only towards 982 | a corner of the raster surround by NoData cells. Hence, the actual extent of the file was much larger 983 | (and unnecessary). 984 | 985 | In a similar fashion, any other value can be used to trim the raster array. Using the 'negative' 986 | option for the ``remove`` parameter will treat negative values as unnecessary. It should be noted 987 | that cells within the 'meaningful' region of the raster that have value same as the ``remove`` value 988 | will remain unaffected by the trimming process. 989 | 990 | """ 991 | 992 | out_array = deepcopy(data_arr) 993 | 994 | # remove undesired values 995 | if type(remove) == type('negative'): 996 | if remove.lower() == 'negative': out_array[out_array < 0] = 0 997 | else: 998 | out_array[out_array == remove] = 0 999 | 1000 | # get the bouding box and clip the array 1001 | non_zero_index = np.nonzero(out_array) 1002 | row_min, row_max = non_zero_index[0].min(), non_zero_index[0].max()+1 1003 | col_min, col_max = non_zero_index[1].min(), non_zero_index[1].max()+1 1004 | 1005 | if return_clip_index: 1006 | return data_arr[row_min:row_max, col_min:col_max], [[col_min, row_min], [col_max, row_max]] 1007 | else: 1008 | return data_arr[row_min:row_max, col_min:col_max] 1009 | 1010 | 1011 | def trim(ds, data_arr, remove): 1012 | """ 1013 | Trim raster array and modify ds 1014 | 1015 | This function trims the raster array by removing the given unwanted value and update datasource 1016 | object accordingly. Note that the function will trim array for the smallest possible bounding 1017 | box outside which all the cells in the input array have the value equal to the value passed 1018 | using ``remove`` parameter. 1019 | 1020 | Parameters 1021 | ---------- 1022 | ds : datasource object 1023 | A datasource object of the raster. Ideally, the datasource should be the 1024 | same as one generated by the ``pyrsgis.raster.read`` function. 1025 | 1026 | data_arr : array 1027 | A 2D or 3D array to clip. Ideally this should be a raster array that has 1028 | unimportant value at the edges (NoData cells in most cases). 1029 | 1030 | remove : integer or float or string 1031 | The value to be considered as irrelevant at the edges. It can be a 1032 | integer or a float number. An additional option 'negative' is also 1033 | available that will treat negative values of the array as unnecessary. 1034 | 1035 | Returns 1036 | ------- 1037 | new_ds : datasource object 1038 | The modified ``ds`` object that can be used to export the trimmed raster. 1039 | 1040 | trimmed_arr : array 1041 | A trimmed array. 1042 | 1043 | Examples 1044 | -------- 1045 | >>> from pyrsgis import raster 1046 | >>> infile = r'E:/path_to_your_file/your_file.tif' 1047 | >>> ds, data_arr = raster.read(infile) 1048 | >>> new_ds, new_arr = raster.trim(ds, data_arr, remove=-9999) 1049 | >>> print('Shape of the input array:', data_arr.shape) 1050 | >>> print('Shape of the trimmed array:', new_arr.shape) 1051 | Shape of the input array: (15000, 15900) 1052 | Shape of the trimmed array: (7059, 7685) 1053 | 1054 | In the above example a raster file which was masked using a polygon but the option 'match extent 1055 | of the raster with extent of the input polygon' was disabled has been used for demonstration. 1056 | Although this is a classic example of observing unnecessary padding at the edges of a raster file, 1057 | there can be many more reason for the same. In this particular case, useful values were only towards 1058 | a corner of the raster surround by NoData cells. Hence, the actual extent of the file was much larger 1059 | (and unnecessary). 1060 | 1061 | In a similar fashion, any other value can be used to trim the raster array. Using the 'negative' 1062 | option for the ``remove`` parameter will treat negative values as unnecessary. It should be noted 1063 | that cells within the 'meaningful' region of the raster that have value same as the ``remove`` value 1064 | will remain unaffected by the trimming process. 1065 | 1066 | The trimmed raster can be exported with the following line: 1067 | 1068 | >>> raster.export(new_arr, new_ds, r'E:/path_to_your_file/trimmed_file.tif') 1069 | 1070 | """ 1071 | 1072 | if len(data_arr.shape) == 2: 1073 | temp_arr = deepcopy(data_arr) 1074 | elif len(data_arr.shape) > 2: 1075 | temp_arr = deepcopy(data_arr[0, :, :]) 1076 | else: 1077 | print('Inconsistent shape of array received. Please check!') 1078 | 1079 | # trim the array using trim function, get the bbox 1080 | _, bbox = trim_array(temp_arr, remove=remove, return_clip_index=True) 1081 | [col_min, row_min], [col_max, row_max] = bbox 1082 | 1083 | # update the dds object by generating north and east rasters 1084 | north, east = north_east_coordinates(ds, temp_arr, layer='both') 1085 | 1086 | north = north[row_min:row_max, col_min:col_max] 1087 | east = east[row_min:row_max, col_min:col_max] 1088 | 1089 | geo_transform = list(ds.GeoTransform) 1090 | geo_transform[3] = north.max() - (ds.GeoTransform[-1] / 2) 1091 | geo_transform[0] = east.min() - (ds.GeoTransform[1] / 2) 1092 | out_ds = deepcopy(ds) 1093 | out_ds.GeoTransform = tuple(geo_transform) 1094 | out_ds.RasterYSize, out_ds.RasterXSize = north.shape 1095 | out_ds.update_bbox() 1096 | 1097 | if len(data_arr.shape) == 2: 1098 | return out_ds, data_arr[row_min:row_max, col_min:col_max] 1099 | elif len(data_arr.shape) > 2: 1100 | return out_ds, data_arr[:, row_min:row_max, col_min:col_max] 1101 | 1102 | def trim_file(filename, remove, outfile): 1103 | """ 1104 | Trim and export raster file 1105 | 1106 | This function trims and exports the raster array by removing the given unwanted value 1107 | Note that the function will trim raster file for the smallest possible bounding box outside 1108 | which all the cells in the raster have the value equal to the value passed 1109 | using ``remove`` parameter. This function is built on the ``pyrsgis.raster.trim`` 1110 | function. 1111 | 1112 | Parameters 1113 | ---------- 1114 | file : string 1115 | Input file name or path to trim. 1116 | 1117 | remove : integer or float or string 1118 | The value to be considered as irrelevant at the edges. It can be a 1119 | integer or a float number. An additional option 'negative' is also 1120 | available that will treat negative values of the array as unnecessary. 1121 | 1122 | outfile : string 1123 | Name or path to store the trimemd raster file. 1124 | 1125 | Examples 1126 | -------- 1127 | >>> from pyrsgis import raster 1128 | >>> infile = r'E:/path_to_your_file/your_file.tif' 1129 | >>> outfile = r'E:/path_to_your_file/trimmed_file.tif' 1130 | >>> raster.trim_file(infile, -9999, outfile) 1131 | 1132 | In the above example a raster file which was masked using a polygon but the option 'match extent 1133 | of the raster with extent of the input polygon' was disabled has been used for demonstration. 1134 | Although this is a classic example of observing unnecessary padding at the edges of a raster file, 1135 | there can be many more reason for the same. In this particular case, useful values were only towards 1136 | a corner of the raster surround by NoData cells. Hence, the actual extent of the file was much larger 1137 | (and unnecessary). 1138 | 1139 | In a similar fashion, any other value can be used to trim the raster array. Using the 'negative' 1140 | option for the ``remove`` parameter will treat negative values as unnecessary. It should be noted 1141 | that cells within the 'meaningful' region of the raster that have value same as the ``remove`` value 1142 | will remain unaffected by the trimming process. 1143 | 1144 | """ 1145 | 1146 | # read the file 1147 | ds, data_arr = read(filename) 1148 | 1149 | # handle non-georeferenced raster (make y cell size negative) 1150 | if not ds.GeoTransform[-1] < 0: 1151 | corrected_geotransform = list(ds.GeoTransform) 1152 | corrected_geotransform[-1] *= -1 1153 | ds.GeoTransform = tuple(corrected_geotransform) 1154 | 1155 | # call trim function 1156 | new_ds, new_arr = trim(ds, data_arr, remove=remove) 1157 | 1158 | # export the outfile 1159 | export(new_arr, new_ds, outfile) 1160 | 1161 | # define a function to update datasource object 1162 | def update_ds(ds_object, new_lon, new_lat): 1163 | """ 1164 | This function updates the latitude and longitude values in the datasource object. 1165 | 1166 | Parameters 1167 | ---------- 1168 | ds_object : datasource object 1169 | The input datasource object to use as template. This is typically generated using the raster.read() function. 1170 | 1171 | new_lon : int or float 1172 | The new longitude value to feed in the datasource object. 1173 | 1174 | new_lat : int or float 1175 | The new latitude value to feed in the datasource object. 1176 | 1177 | Returns 1178 | ------- 1179 | new_ds : datasource object 1180 | The updated datasource object. 1181 | 1182 | Examples 1183 | -------- 1184 | >>> from pyrsgis import raster 1185 | >>> infile = r'E:/path_to_your_file/your_file.tif' 1186 | >>> ds, arr = raster.read(infile) 1187 | >>> print('Geo transform in original DS:', ds.GeoTransform) 1188 | Geo transform in original DS: (250087.5326, 0.8, 0.0, 2820013.2987, 0.0, -0.8) 1189 | 1190 | >>> new_ds = raster.update_ds(ds, new_lon=250200, new_lat=2820400) 1191 | >>> print('Geo transform in new DS:', new_ds.GeoTransform) 1192 | Geo transform in new DS: (250200, 0.8, 0.0, 2820400, 0.0, -0.8) 1193 | 1194 | """ 1195 | 1196 | temp_geoTransform = list(ds_object.GeoTransform) 1197 | temp_geoTransform[0] = new_lon 1198 | temp_geoTransform[3] = new_lat 1199 | new_ds = deepcopy(ds_object) 1200 | new_ds.GeoTransform = tuple(temp_geoTransform) 1201 | 1202 | return new_ds 1203 | 1204 | def fragment_raster(filename, nrows, ncols, outdir=None, prefix=None): 1205 | """ 1206 | This function clips the raster into smaller rasters using a nxm grid. 1207 | 1208 | Parameters 1209 | ---------- 1210 | filename : string 1211 | Path to the input raster file. 1212 | 1213 | nrows : integer 1214 | No. of rows of the expected grid. 1215 | 1216 | ncols : string 1217 | No. of cols of the expected grid. 1218 | 1219 | outdir : string, optional 1220 | Output directory where files will be stored. If not specified, the clipped rasters will be 1221 | exported in the same directory as the input file. Note that the output directory should exist, 1222 | this function will not generate the output directory. 1223 | 1224 | prefix : string, optional 1225 | If specified, the prefix will be used for clipped rasters files. Otherwise, the input file name will be used. 1226 | 1227 | Returns 1228 | ------- 1229 | exported_files_list : list 1230 | A list containing path to all the exported files. 1231 | 1232 | Examples 1233 | -------- 1234 | .. code-block:: python 1235 | 1236 | from pyrsgis import raster 1237 | infile = r'E:/path_to_your_file/your_file.tif' 1238 | outdir = r'E:/path_to_output_directory/clipped_images' 1239 | exported_files = raster.fragment_raster(infile, nrows=3, ncols=5, outdir=outdir, prefix='ClippedSample') 1240 | print('Following files were exported:') 1241 | print('\\n'.join(exported_files)) 1242 | 1243 | Example output: 1244 | 1245 | .. code-block:: text 1246 | 1247 | Following files were exported: 1248 | E:/path_to_output_directory/clipped_images/ClippedSample_1_1.tif 1249 | E:/path_to_output_directory/clipped_images/ClippedSample_2_1.tif 1250 | ... 1251 | E:/path_to_output_directory/clipped_images/ClippedSample_5_3.tif 1252 | """ 1253 | 1254 | 1255 | # resolve the output file name 1256 | if prefix == None: 1257 | outfile = filename 1258 | else: 1259 | outfile = os.path.join(os.path.split(filename)[0], f'{prefix}.tif') 1260 | 1261 | if outdir != None: 1262 | outfile = os.path.join(outdir, os.path.split(outfile)[-1]) 1263 | 1264 | # read the raster file 1265 | ds, arr = read(filename) 1266 | 1267 | # fragment the main data array 1268 | arr_fragmented = [np.split(item, ncols, axis=-1) for item in np.array_split(arr, nrows, axis=-2)] 1269 | arr_fragmented = [col_item for row_item in arr_fragmented for col_item in row_item] 1270 | 1271 | # generate latitude and longitude arrays 1272 | latitude_arr, longitude_arr = north_east_coordinates(ds, arr, layer='both') 1273 | arr = None # arr variable no longer required 1274 | 1275 | # transform the values from cell center to cell upper left corner 1276 | latitude_arr -= list(ds.GeoTransform)[-1] / 2 1277 | longitude_arr -= list(ds.GeoTransform)[1] / 2 1278 | 1279 | ## since north and east arrays have redundant cols and rows respectively, slice them 1280 | latitude_arr = latitude_arr[:, 0] 1281 | longitude_arr = longitude_arr[0, :] 1282 | 1283 | #_fragment latitude and longitude arrays 1284 | latitude_arr_fragmented = [item[0] for item in np.array_split(latitude_arr, nrows)] 1285 | longitude_arr_fragmented = [item[0] for item in np.array_split(longitude_arr, ncols)] 1286 | 1287 | # combine latitudes and longitudes to easily loop over them later 1288 | coordinates_fragmented = [(lat, lon) for lat in latitude_arr_fragmented for lon in longitude_arr_fragmented] 1289 | 1290 | # create x and 7 numbers to use as suffix during file export 1291 | x_list = np.arange(1, ncols + 1) 1292 | y_list = np.arange(1, nrows + 1) 1293 | xy_list = [(y, x) for y in y_list for x in x_list] 1294 | 1295 | # loop through the fragmented arrays and export the raster files 1296 | exported_files_list = [] 1297 | for xy, location, data_arr in zip(xy_list, coordinates_fragmented, arr_fragmented): 1298 | new_ds = update_ds(ds, location[1], location[0]) 1299 | 1300 | export_file = outfile.replace('.tif', f'_{xy[1]}_{xy[0]}.tif') 1301 | export(data_arr, new_ds,filename = export_file) 1302 | exported_files_list.append(export_file) 1303 | 1304 | return exported_files_list 1305 | 1306 | --------------------------------------------------------------------------------