Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
555fd6b
docs: Updated README + chore: Updated dependencies
MiguelPadillaR Dec 4, 2025
86d9576
Merge branch 'main' of https://github.com/MiguelPadillaR/sen2sr-tools…
MiguelPadillaR Dec 4, 2025
adfc1a0
feat: Added `example.polygon` for demo
MiguelPadillaR Dec 4, 2025
8cbd801
fix: missing return for parser + overall bug fixing
MiguelPadillaR Dec 4, 2025
93d77a5
docs: Updated README
MiguelPadillaR Dec 4, 2025
f3a56f3
fix: Fixed issue removing HEAD commit
MiguelPadillaR Dec 4, 2025
2fd8ec6
Merge pull request #3 from MiguelPadillaR/fix/terminal-commands-fix
MiguelPadillaR Dec 4, 2025
a819832
fix: Fixed minimum size issue + Updated function to be more modular
MiguelPadillaR Dec 5, 2025
cbf4c29
docs: Updated README (Download Cubo section + Minor fixes)
MiguelPadillaR Dec 5, 2025
d1cd070
update: Uploaded SEN2SR pre-trained model
MiguelPadillaR Dec 5, 2025
bcbe657
feat: Added missing dependecy + Added paths to new models + Fixed rel…
MiguelPadillaR Dec 10, 2025
9db34c8
feat: Added RGBN only and all bands upscaling models
MiguelPadillaR Dec 10, 2025
6d8a596
feat: Uses correct model for specific use case (RGB or 10+ bands) + U…
MiguelPadillaR Dec 10, 2025
c752470
Merge pull request #1 from KhaosResearch/feat/sr-10-bands
MiguelPadillaR Dec 10, 2025
0c5e6f7
refactor: Minor refactoring
MiguelPadillaR Dec 10, 2025
be3351d
fix: Made `crop_parcel_from_tif` method and returns data instead of a…
MiguelPadillaR Dec 11, 2025
4367391
docs: Updated README
MiguelPadillaR Dec 11, 2025
4bf94f7
chore: Minor refactoring + typo
MiguelPadillaR Dec 15, 2025
42e5f64
Merge pull request #2 from KhaosResearch/fixes/general-fixes
MiguelPadillaR Dec 15, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 2 additions & 5 deletions .github/workflows/update_pr_description.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,16 +17,13 @@ jobs:
uses: actions/checkout@v4
# We need to fetch the history up to the base branch
with:
fetch-depth: 0
ref: ${{ github.event.pull_request.head.sha }}
fetch-depth: 0

- name: βš™οΈ Get Commit List and Format
id: commit_list
run: |
BASE_BRANCH="origin/${{ github.event.pull_request.base.ref }}"
COMMIT_LIST=$(git log --pretty=format:"* %s (%an, %ad)" ${{ github.event.pull_request.base.sha }}..HEAD)

COMMIT_LIST=$(git log --pretty=format:"* %s (%an, %ad)" $BASE_BRANCH..HEAD)

FORMATTED_LIST="## πŸ“¦ Commits Included in this PR:\n\n${COMMIT_LIST}"

echo "FORMATTED_LIST<<EOF" >> $GITHUB_ENV
Expand Down
63 changes: 48 additions & 15 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,10 @@
# SEN2SR Tools

![Python Version from PEP 621 TOML](https://img.shields.io/python/required-version-toml?tomlFilePath=https%3A%2F%2Fraw.githubusercontent.com%2FMiguelPadillaR%2Fsen2sr-tools%2Fmain%2Fpyproject.toml)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
![Status: Active](https://img.shields.io/badge/Status-Active-00aa00.svg)
![Code style: Ruff](https://img.shields.io/badge/code%20style-autopep8-aa0000.svg)

This repository provides a list of comprehensive tools and utilities for the usage of the [SEN2SR](https://github.com/ESAOpenSR/SEN2SR.git) neural network super-resolution model, developed by the [ESAOpenSR](https://opensr.eu/) team. This model specializes in upscaling Sentinel-2's 10m/px images up to a x4 times improvement of 2.5m/px.

This package implements a feature to crop a polygon directly from the SR image, useful for close-up satellite observation of agricultural states.
Expand All @@ -15,38 +21,49 @@ This package implements a feature to crop a polygon directly from the SR image,
source venv/bin/activate # On Windows use `venv\Scripts\activate`
pip install -e .
```

## Quickstart example
The overall workflow can be run directly, since it implements a simple example in it:
```shell
cd sen2sr-tools
python -m sen2sr_tools.get_sr_image
```
The example can also be run using commands:
```shell
python -m sen2sr_tools get-sr-image --latitude 42.465226 --longitude -2.292699 --start-date 2025-11-01 --end-date 2025-11-15 --geojson-path ./example.geojson
```
Output will be at `sen2sr_out`, where the SR and original images can be seen.
## Usage in Python
All relevant methods are stored in the `get_sr_image` module.

### Get a SR image

It runs the full workflow, from date and location data to super resolved image. Default `size` is 128 px, and it must be a multiple of 32. Default `bands` are Near-Infrared, Red, Blue, Green and the Scene Classification Layer, that contains cloud density information among others. Cropping a polygon from the image is optional, provided the GeoJSON filepath.
Using `get_sr_image` runs the full workflow, from date and location data to super resolved image. Default `size` is the minimal requested 128px. Default `bands` are Near-Infrared, Red, Blue, Green and the Scene Classification Layer, that contains cloud density information among others. Cropping a polygon from the image is optional, provided the GeoJSON filepath. The final image outputs are in TIF and PNG formats, for usage and visualization respectively.

```python
from sen2sr_tools.get_sr_image import get_sr_image

lat = 37.265840
lng = -4.593406
start_date = "2025-11-1"
start_date = "2025-11-01"
end_date = "2025-11-15"
# bands = ["B08", "B02", "B03", "B04", "SCL"] # Default bands
# size = 128 # Default size
# geojson_path = None # Default value
# geometry = None # Default value

sr_filepath = get_sr_image(lat, lng, bands, start_date, end_date)

print(f"SR image successfully downloaded and saved at: {sr_filepath}")
```
### Download CUBO

It serves the requested bands' data directly. It uses the date range to get cloudless (<=1%) images. The `crs` can be provided (i.e, "EPSG:32630"), but it can also be automatically calculated from the `lat` and `lon` arguments. If no requested data was found within the date range, it retries with by expanding it backwards in time.
Running `download_sentinel_cubo` serves the requested bands' data directly. It uses the date range to get cloudless (<=1%) images. The `crs` can be provided (i.e, "EPSG:32630"), but it can also be automatically calculated from the `lat` and `lon` arguments. If no requested data is found within the date range, it retries with by expanding it backwards in time. The CUBO data is provided as a `Dask.array`.

```python
from sen2sr_tools.get_sr_image import download_sentinel_cubo

lat = 37.265840
lng = -4.593406
start_date = "2025-11-1"
start_date = "2025-11-01"
end_date = "2025-11-15"
# crs = None # Default CRS
# bands = ["B08", "B02", "B03", "B04", "SCL"] # Default bands
Expand All @@ -55,22 +72,38 @@ end_date = "2025-11-15"
# max_retries = 3 # Default retries
# retry_days_shift = 15 # Default shift

cloudless_image_data_array, sample_date = download_sentinel_cubo(lat, lon, start_date, end_date):
cloudless_cubo_data_array, sample_date = download_sentinel_cubo(lat, lon, start_date, end_date)

print(f"CUBO data summary:\n\n{cloudless_cubo_data_array.coords}")

# If you need the array as NumPy
# cubo_np_array = (cloudless_cubo_data_array.astype("float32") / 10_000).compute()
```

### Crop parcel from image

It crops the polygon from the given TIF file and returns the cropped image as PNG.
The `crop_parcel_from_tif` method crops the polygon from the given TIF file and returns the cropped image output and metadata.

```python
from sen2sr_tools.get_sr_image import crop_png_from_tif
import json
import geopandas as gpd
from sen2sr_tools.get_sr_image import crop_parcel_from_tif

raster_path = "path/to/file.tif"
geojson_path = "path/to/polygon.geojson"
date = "2025-11-1"
gdf = gpd.read_file(geometry)

out_png_path = crop_png_from_tif(raster_path, geojson_path, date)

print(f"Polygon successfully cropped and saved at: {out_png_path}")
raster_path = "path/to/file.tif"
geometry = {
"type": "Polygon",
"coordinates": [[
[-2.29544172799985, 42.46242962302221],[-2.292531924994703, 42.46538599420777],
[-2.292180580738519, 42.463584097818824], [-2.286733836169323, 42.464620125420105],
[-2.29544172799985, 42.46242962302221]
]],
"CRS": "epsg:4258"}

out_image, out_meta = crop_parcel_from_tif(raster_path, geojson_path)

print(f"Polygon successfully cropped!")
```

## Attribution and License
Expand Down
1 change: 1 addition & 0 deletions example.geojson

Large diffs are not rendered by default.

17 changes: 15 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,16 @@ authors = [
license = "MIT"
readme = "README.md"
requires-python = ">=3.10"
classifiers = [
"Programming Language :: Python :: 3 :: Only",
"Programming Language :: Python :: 3.10",
"Development Status :: 1 - Planning",
"License :: OSI Approved :: MIT License",
"Topic :: Scientific/Engineering",
"Topic :: Scientific/Engineering :: GIS",
"Topic :: Scientific/Engineering :: SR",
"Topic :: Utilities"
]
dependencies = [
"cubo",
"mlstac",
Expand All @@ -19,14 +29,17 @@ dependencies = [
"structlog",
"xarray",
"pillow",
"matplotlib",
"opencv-python (>=4.11, <4.12)",
"numpy (>=1.9, <2.0.0)"
]

[project.urls]
"Homepage" = "https://github.com/KhaosResearch/sigpac-tools"
"Bug Tracker" = "https://github.com/KhaosResearch/sigpac-tools/issues"
"Homepage" = "https://github.com/MiguelPadillaR/sen2sr-tools"
"Bug Tracker" = "https://github.com/MiguelPadillaR/sen2sr-tools/issues"

[project.optional-dependencies]
dev = ["autopep8", "pytest"]

[build-system]
requires = ["poetry-core>=2.0.0,<3.0.0"]
Expand Down
64 changes: 39 additions & 25 deletions sen2sr_tools/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,11 @@

logger = structlog.get_logger()

class VerboseArgumentParser(argparse.ArgumentParser):
def error(self, message):
raise RuntimeError(f"Argument error: {message}")


# --- Helper to load dynamic logic and run command ---

def run_command(command_name: str, args: argparse.Namespace):
Expand All @@ -18,27 +23,29 @@ def run_command(command_name: str, args: argparse.Namespace):
crop_png_from_tif,
)
except ImportError:
logger.error(f"Could not import logic from sen2sr_tools.get_sr_image. Ensure the package is installed in editable mode.")
logger.error(f"Could not import logic from sen2sr_tools.get-sr-image. Ensure the package is installed in editable mode.")
sys.exit(1)

# Dispatch logic based on command
if command_name == "get_sr_image":
if command_name == "get-sr-image":
logger.info("Starting Super Resolution task",
latitude=args.latitude, longitude=args.longitude,
start_date=args.start_date, end_date=args.end_date)

start_date=args.start_date, end_date=args.end_date,
geometry=args.geometry
)
result = get_sr_image(
lat=args.latitude,
lon=args.longitude,
bands=args.bands,
start_date=args.start_date,
end_date=args.end_date,
size=args.size
size=args.size,
geometry=args.geometry
)
logger.info(f"βœ… Get SR Image task completed. Result (filepath):\n{result}")
return result

elif command_name == "download_cubo":
elif command_name == "download-cubo":
logger.info("Starting Sentinel-2 Cubo download", start_date=args.start_date, end_date=args.end_date)

# Note: The download function returns two values, we'll only print the path/date for CLI feedback
Expand All @@ -53,9 +60,9 @@ def run_command(command_name: str, args: argparse.Namespace):
cloud_threshold=args.cloud_threshold
)
logger.info(f"βœ… Download successful for acquisition date: {date}")
return date
return data, date

elif command_name == "crop_parcel":
elif command_name == "crop-polygon":
logger.info("Starting parcel cropping.", tif_path=args.raster_path)

# Note: The crop function needs a `date` string as the second argument,
Expand All @@ -65,52 +72,56 @@ def run_command(command_name: str, args: argparse.Namespace):

out_path = crop_png_from_tif(
raster_path=args.raster_path,
geometry=args.geometry,
date=args.acquisition_date
)
logger.info(f"βœ… Cropping successful. Output path: {out_path}")
return out_path



# --- Argument Parser Setup ---

def get_parser():
parser = argparse.ArgumentParser(description="SEN2SR Tools: Command Line Interface for Sentinel-2 Super Resolution.")
parser = VerboseArgumentParser(description="SEN2SR Tools: Command Line Interface for Sentinel-2 Super Resolution.")

subparsers = parser.add_subparsers(
dest="command", help="Available commands")
subparsers.required = True

# =========================================================================
# 1. get_sr_image (Main Function)
# 1. get-sr-image (Main Function)
# =========================================================================
get_sr_image_parser = subparsers.add_parser(
"get_sr_image", help="Get SR image from downloaded Sentinel's imagery data and load up SEN2SR model to Super-Resolve it."
"get-sr-image", help="Get SR image from downloaded Sentinel's imagery data and load up SEN2SR model to Super-Resolve it."
)
get_sr_image_parser.add_argument(
"--latitude", type=float, help="Latitude coordinate component (e.g., 37.265840).", required=True, metavar="FLOAT",
)
get_sr_image_parser.add_argument(
"--longitude", type=float, help="Longitude coordinate component (e.g., -4.593406).", required=True, metavar="FLOAT",
)
get_sr_image_parser.add_argument(
"--bands", nargs='+', default=["B08", "B02", "B03", "B04", "SCL"],
help="Image bands to retrieve (space-separated, e.g., B08 B02 B03 B04 SCL).", required=False, metavar="STR [...]",
)
get_sr_image_parser.add_argument(
"--start-date", type=str, help="Initial date in search range in ISO format (yyyy-mm-dd).", required=True, metavar="STR",
)
get_sr_image_parser.add_argument(
"--end-date", type=str, help="Final date in search range in ISO format (yyyy-mm-dd).", required=True, metavar="STR",
)
get_sr_image_parser.add_argument(
"--bands", nargs='+', default=["B08", "B02", "B03", "B04", "SCL"],
help="Image bands to retrieve (space-separated, e.g., B08 B02 B03 B04 SCL).", required=False,
)
get_sr_image_parser.add_argument(
"--size", type=int, default=128, help="Size of the image tile in pixels (e.g., 128 for 128x128).", required=False, metavar="INT",
)
get_sr_image_parser.add_argument(
"--geojson-path", type=str, default="", help="GeoJSON to crop SR image filepath. If none is provided, it downloads the image in sizexsize", metavar="STR",
)


# =========================================================================
# 2. download_cubo (Helper Function)
# =========================================================================
download_cubo_parser = subparsers.add_parser(
"download_cubo", help="Download raw Sentinel-2 imagery data using Cubo and filter for cloudless data."
"download-cubo", help="Download raw Sentinel-2 imagery data using Cubo and filter for cloudless data."
)
# Re-use common arguments
download_cubo_parser.add_argument(
Expand All @@ -119,15 +130,15 @@ def get_parser():
download_cubo_parser.add_argument(
"--longitude", type=float, help="Longitude coordinate component.", required=True, metavar="FLOAT",
)
download_cubo_parser.add_argument(
"--bands", nargs='+', default=["B08", "B02", "B03", "B04", "SCL"], help="Image bands to retrieve.", metavar="STR [...]",
)
download_cubo_parser.add_argument(
"--start-date", type=str, help="Initial date in search range (yyyy-mm-dd).", required=True, metavar="STR",
)
download_cubo_parser.add_argument(
"--end-date", type=str, help="Final date in search range (yyyy-mm-dd).", required=True, metavar="STR",
)
download_cubo_parser.add_argument(
"--bands", nargs='+', default=["B08", "B02", "B03", "B04", "SCL"], help="Image bands to retrieve.",
)
download_cubo_parser.add_argument(
"--size", type=int, default=128, help="Size of the image tile in pixels.", metavar="INT",
)
Expand All @@ -139,15 +150,15 @@ def get_parser():
"--cloud-threshold", type=float, default=0.01, help="Max percentage of cloud density tolerated (0.0 to 1.0).", metavar="FLOAT",
)
download_cubo_parser.add_argument(
"--polygon-path", type=str, default="", help="GeoJSON to crop SR image filepath. If none is provided, it downloads the image in sizexsize", metavar="STR",
"--geojson-path", type=str, default="", help="GeoJSON to crop SR image filepath. If none is provided, it downloads the image in sizexsize", metavar="STR",
)


# =========================================================================
# 3. crop_parcel (Helper Function)
# =========================================================================
crop_parcel_parser = subparsers.add_parser(
"crop_parcel", help="Crops a parcel from a Super-Resolved TIF using a local GeoJSON file."
"crop-polygon", help="Crops a parcel from a Super-Resolved TIF using a local GeoJSON file."
)
crop_parcel_parser.add_argument(
"--raster-path", type=Path, help="Path to the uncropped SR TIF image.", required=True, metavar="PATH",
Expand All @@ -161,13 +172,16 @@ def get_parser():
"--acquisition-date", type=str, help="Acquisition date (yyyy-mm-dd) used to name the output file.", required=True, metavar="STR",
)

return parser

def main():
args = get_parser().parse_args()

# Run the command
run_command(args.command, args)
result = run_command(args.command, args)

logger.info(f"RESULT:")
logger.info(f"{result}")

if __name__ == "__main__":
main()
main()
4 changes: 3 additions & 1 deletion sen2sr_tools/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@

CURR_SCRIPT_DIR = Path.Path(os.path.dirname(os.path.abspath(__file__)))

MODEL_DIR = str(CURR_SCRIPT_DIR / "model")
MODEL_DIR = CURR_SCRIPT_DIR / "model"
RGBN_MODEL_DIR = str(MODEL_DIR / "SEN2SRLite_RGBN")
MAIN_MODEL_DIR = str(MODEL_DIR / "SEN2SRLite")
SEN2SR_SR_DIR = CURR_SCRIPT_DIR / "sen2sr_out"
PNG_DIR = SEN2SR_SR_DIR / "png"
TIF_DIR = SEN2SR_SR_DIR / "tif"
Expand Down
Loading