diff --git a/agrigee_lite/api/_models.py b/agrigee_lite/api/_models.py index f66aa61..d1ed570 100644 --- a/agrigee_lite/api/_models.py +++ b/agrigee_lite/api/_models.py @@ -105,6 +105,8 @@ class ImagesRequest(BaseModel): "max_parallel_downloads": ASYNC_MAX_PARALLEL_DOWNLOADS, "force_redownload": False, "image_indices": [0], + "scale": None, + "dimensions": None, } } ) @@ -117,6 +119,22 @@ class ImagesRequest(BaseModel): max_parallel_downloads: int = Field(ASYNC_MAX_PARALLEL_DOWNLOADS, ge=1) force_redownload: bool = False image_indices: list[int] | None = None + scale: float | None = Field( + None, + gt=0, + description=( + "Resolution in meters/pixel passed to getDownloadURL. " + "When set, Earth Engine resamples to this scale before download. " + "Mutually exclusive with dimensions; scale takes precedence when both are provided." + ), + ) + dimensions: int | str | None = Field( + None, + description=( + "Target output size passed to getDownloadURL, e.g. 512 or '512x512'. " + "Ignored when scale is also provided." + ), + ) class ImagesResult(BaseModel): diff --git a/agrigee_lite/api/routes/images.py b/agrigee_lite/api/routes/images.py index 971400f..883d5c0 100644 --- a/agrigee_lite/api/routes/images.py +++ b/agrigee_lite/api/routes/images.py @@ -26,6 +26,8 @@ async def _run_images_job(job_id: str, request: ImagesRequest) -> None: max_parallel_downloads=request.max_parallel_downloads, force_redownload=request.force_redownload, image_indices=request.image_indices, + scale=request.scale, + dimensions=request.dimensions, ) from agrigee_lite.config import ASYNC_MAX_RETRIES_PER_CHUNK @@ -39,6 +41,8 @@ async def _run_images_job(job_id: str, request: ImagesRequest) -> None: image_indices=request.image_indices, max_retries_per_chunk=ASYNC_MAX_RETRIES_PER_CHUNK, crs=None, + scale=request.scale, + dimensions=request.dimensions, )) job = job_store.get(job_id) if job is not None: @@ -63,6 +67,8 @@ def _images_job_hash(request: ImagesRequest) -> str: image_indices=request.image_indices, max_retries_per_chunk=ASYNC_MAX_RETRIES_PER_CHUNK, crs=None, + scale=request.scale, + dimensions=request.dimensions, ).name diff --git a/agrigee_lite/get/image.py b/agrigee_lite/get/image.py index 3cca11b..2792597 100644 --- a/agrigee_lite/get/image.py +++ b/agrigee_lite/get/image.py @@ -33,20 +33,25 @@ def _compute_images_cache_dir( image_indices: list[int] | None, max_retries_per_chunk: int, crs: str | None, + scale: float | None = None, + dimensions: int | str | None = None, ) -> pathlib.Path: """Compute the deterministic cache directory for a set of image download params. Reproduces the dict that ``log_dict_function_call_summary`` would capture when called from inside ``download_multiple_images_async``, so hashes stay stable. """ - metadata_dict: dict[str, Any] = { - "download_multiple_images_async": { - "invalid_images_threshold": str(invalid_images_threshold), - "image_indices": str(image_indices), - "max_retries_per_chunk": str(max_retries_per_chunk), - "crs": str(crs), - } + inner: dict[str, str] = { + "invalid_images_threshold": str(invalid_images_threshold), + "image_indices": str(image_indices), + "max_retries_per_chunk": str(max_retries_per_chunk), + "crs": str(crs), } + if scale is not None: + inner["scale"] = str(float(scale)) + if dimensions is not None: + inner["dimensions"] = str(dimensions) + metadata_dict: dict[str, Any] = {"download_multiple_images_async": inner} metadata_dict |= satellite.log_dict() metadata_dict["start_date"] = start_date metadata_dict["end_date"] = end_date @@ -70,6 +75,8 @@ def download_multiple_images( image_indices: list[int] | None = None, max_retries_per_chunk: int = ASYNC_MAX_RETRIES_PER_CHUNK, crs: str | None = None, + scale: float | None = None, + dimensions: int | str | None = None, ) -> list[str]: """Download raw satellite images (as GeoTIFF ZIPs) for a geometry and date range. @@ -103,6 +110,15 @@ def download_multiple_images( without downloading everything. max_retries_per_chunk : int, default 5 Maximum retry attempts per image download. + scale : float or None, optional + Resolution in meters/pixel passed to ``getDownloadURL``. When set, + Earth Engine resamples the output to this scale before download. + Mutually exclusive with ``dimensions``; if both are provided, + ``scale`` takes precedence. Defaults to ``None`` (native scale). + dimensions : int or str or None, optional + Target output size passed to ``getDownloadURL``, e.g. ``512`` or + ``"512x512"``. Ignored when ``scale`` is also provided. Defaults + to ``None`` (native scale). Returns ------- @@ -129,6 +145,8 @@ def download_multiple_images( image_indices=image_indices, max_retries_per_chunk=max_retries_per_chunk, crs=crs, + scale=scale, + dimensions=dimensions, ) ) @@ -221,6 +239,8 @@ async def _fetch_and_download_image( output_dir: pathlib.Path, semaphore: asyncio.Semaphore, max_retries_per_chunk: int, + scale: float | None = None, + dimensions: int | str | None = None, ) -> tuple[int, bool]: """Resolve a single GEE download URL and save its ZIP payload to disk.""" async with semaphore: @@ -233,10 +253,13 @@ async def _fetch_and_download_image( img = ee.Image( ee_expression.filter(ee.Filter.eq("system:index", image_indexes[chunk_index])).first() ) + params: dict[str, Any] = {"name": image_names[chunk_index], "region": ee_geometry} + if scale is not None: + params["scale"] = scale + elif dimensions is not None: + params["dimensions"] = dimensions url = await asyncio.wait_for( - asyncio.to_thread( - img.getDownloadURL, {"name": image_names[chunk_index], "region": ee_geometry} - ), + asyncio.to_thread(img.getDownloadURL, params), timeout=180, ) file_path = output_dir / f"{image_names[chunk_index]}.zip" @@ -307,6 +330,8 @@ async def download_multiple_images_async( image_indices: list[int] | None = None, max_retries_per_chunk: int = ASYNC_MAX_RETRIES_PER_CHUNK, crs: str | None = None, + scale: float | None = None, + dimensions: int | str | None = None, ) -> list[str]: """Async version of :func:`download_multiple_images`. @@ -335,6 +360,15 @@ async def download_multiple_images_async( Restrict to specific collection positions. max_retries_per_chunk : int, default 5 Maximum retry attempts per image download. + scale : float or None, optional + Resolution in meters/pixel passed to ``getDownloadURL``. When set, + Earth Engine resamples the output to this scale before download. + Mutually exclusive with ``dimensions``; if both are provided, + ``scale`` takes precedence. Defaults to ``None`` (native scale). + dimensions : int or str or None, optional + Target output size passed to ``getDownloadURL``, e.g. ``512`` or + ``"512x512"``. Ignored when ``scale`` is also provided. Defaults + to ``None`` (native scale). Returns ------- @@ -358,6 +392,8 @@ async def download_multiple_images_async( image_indices=image_indices, max_retries_per_chunk=max_retries_per_chunk, crs=crs, + scale=scale, + dimensions=dimensions, ) return await _download_single_image_zip_async( satellite=satellite, @@ -380,6 +416,8 @@ async def download_multiple_images_async( image_indices=image_indices, max_retries_per_chunk=max_retries_per_chunk, crs=crs, + scale=scale, + dimensions=dimensions, ) collection_size = await asyncio.to_thread(ee_expression.size().getInfo) @@ -424,6 +462,8 @@ async def download_multiple_images_async( output_dir=output_path, semaphore=semaphore, max_retries_per_chunk=max_retries_per_chunk, + scale=scale, + dimensions=dimensions, ) ) for i in pending_chunks