Preserve native orthophoto detail for training
This commit is contained in:
@@ -1357,6 +1357,10 @@ Settings: `ORTHOPHOTO_ENABLED`, `ORTHOPHOTO_WMS_URL`,
|
||||
`ORTHOPHOTO_TIMEOUT_SECONDS`, `ORTHOPHOTO_MAX_RESPONSE_MB` and
|
||||
`ORTHOPHOTO_CACHE_TTL_HOURS`. Keep the official HTTPS URL and 1 m profile
|
||||
unless a separately verified deployment/model profile requires a change.
|
||||
An explicit bounded request may provide `resolution_m` down to the governed
|
||||
product's native resolution. This is intended for reviewed training corpora;
|
||||
the service rejects source oversampling and records rolling-latest observation
|
||||
time as unknown per pixel rather than equating it with download time.
|
||||
|
||||
Before a future `most_recent` source release is allowed into a governed pixel
|
||||
stage, run the metadata-only preflight for the exact intended rectangle:
|
||||
|
||||
@@ -2,7 +2,7 @@ from __future__ import annotations
|
||||
|
||||
from uuid import UUID
|
||||
|
||||
from pydantic import BaseModel
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from .operations import VectorSelectionBBox
|
||||
|
||||
@@ -12,6 +12,7 @@ class OrthophotoAcquireRequest(BaseModel):
|
||||
area_id: UUID | None = None
|
||||
product_key: str = "most_recent"
|
||||
force_refresh: bool = False
|
||||
resolution_m: float | None = Field(default=None, ge=0.1, le=2.0)
|
||||
|
||||
|
||||
class OrthophotoProductRead(BaseModel):
|
||||
|
||||
@@ -262,8 +262,16 @@ class OrthophotoAcquisitionService:
|
||||
status_code=422,
|
||||
)
|
||||
|
||||
width = max(1, math.ceil(width_m / settings.orthophoto_resolution_m))
|
||||
height = max(1, math.ceil(height_m / settings.orthophoto_resolution_m))
|
||||
resolution_m = float(payload.resolution_m or settings.orthophoto_resolution_m)
|
||||
if resolution_m < product.native_resolution_m:
|
||||
raise AppError(
|
||||
code="ORTHOPHOTO_RESOLUTION_EXCEEDS_SOURCE",
|
||||
message="Requested sampling cannot be finer than the governed source resolution",
|
||||
details={"requested_resolution_m": resolution_m, "native_resolution_m": product.native_resolution_m},
|
||||
status_code=422,
|
||||
)
|
||||
width = max(1, math.ceil(width_m / resolution_m))
|
||||
height = max(1, math.ceil(height_m / resolution_m))
|
||||
bbox_4326 = [min_x, min_y, max_x, max_y]
|
||||
bbox_31370 = [float(value) for value in lambert_bounds]
|
||||
request_identity = {
|
||||
@@ -275,14 +283,14 @@ class OrthophotoAcquisitionService:
|
||||
"bbox_epsg31370": [round(value, 3) for value in bbox_31370],
|
||||
"width": width,
|
||||
"height": height,
|
||||
"resolution_m": settings.orthophoto_resolution_m,
|
||||
"resolution_m": resolution_m,
|
||||
}
|
||||
request_hash = hashlib.sha256(json.dumps(request_identity, sort_keys=True).encode("utf-8")).hexdigest()
|
||||
spatial_identity = {
|
||||
"bbox_epsg4326": request_identity["bbox_epsg4326"],
|
||||
"width": width,
|
||||
"height": height,
|
||||
"resolution_m": settings.orthophoto_resolution_m,
|
||||
"resolution_m": resolution_m,
|
||||
}
|
||||
spatial_hash = hashlib.sha256(json.dumps(spatial_identity, sort_keys=True).encode("utf-8")).hexdigest()
|
||||
params = {
|
||||
@@ -522,7 +530,7 @@ class OrthophotoAcquisitionService:
|
||||
layer=product.layer,
|
||||
width=prepared["width"],
|
||||
height=prepared["height"],
|
||||
resolution_m=resolved_settings.orthophoto_resolution_m,
|
||||
resolution_m=float(prepared["resolution_m"]),
|
||||
bbox_epsg4326=prepared["bbox_epsg4326"],
|
||||
bbox_epsg31370=prepared["bbox_epsg31370"],
|
||||
attribution=product.attribution,
|
||||
@@ -561,7 +569,10 @@ class OrthophotoAcquisitionService:
|
||||
"observation_label": product.observation_label,
|
||||
"observation_date_precision": product.temporal_granularity,
|
||||
"native_resolution_m": product.native_resolution_m,
|
||||
"requested_resolution_m": resolved_settings.orthophoto_resolution_m,
|
||||
"requested_resolution_m": float(prepared["resolution_m"]),
|
||||
"observation_time_precision": (
|
||||
"unknown_per_pixel" if product.key == "most_recent" or product.key.endswith("_latest") else "product_period"
|
||||
),
|
||||
"color_mode": product.color_mode,
|
||||
"supports_detection": product.supports_detection,
|
||||
"layer": product.layer,
|
||||
@@ -581,7 +592,7 @@ class OrthophotoAcquisitionService:
|
||||
"bbox_epsg31370": prepared["bbox_epsg31370"],
|
||||
"width": prepared["width"],
|
||||
"height": prepared["height"],
|
||||
"resolution_m": resolved_settings.orthophoto_resolution_m,
|
||||
"resolution_m": float(prepared["resolution_m"]),
|
||||
"limitation_message": product.limitation_message,
|
||||
},
|
||||
)
|
||||
@@ -597,7 +608,7 @@ class OrthophotoAcquisitionService:
|
||||
layer=product.layer,
|
||||
width=prepared["width"],
|
||||
height=prepared["height"],
|
||||
resolution_m=resolved_settings.orthophoto_resolution_m,
|
||||
resolution_m=float(prepared["resolution_m"]),
|
||||
bbox_epsg4326=prepared["bbox_epsg4326"],
|
||||
bbox_epsg31370=prepared["bbox_epsg31370"],
|
||||
attribution=product.attribution,
|
||||
|
||||
@@ -92,6 +92,7 @@ def _selection_payload(
|
||||
force_refresh: bool = True,
|
||||
area_id=None,
|
||||
product_key: str = "most_recent",
|
||||
resolution_m: float | None = None,
|
||||
) -> OrthophotoAcquireRequest:
|
||||
west, south = 199_000.0, 210_000.0
|
||||
transformer = Transformer.from_crs("EPSG:31370", "EPSG:4326", always_xy=True)
|
||||
@@ -108,6 +109,7 @@ def _selection_payload(
|
||||
area_id=area_id,
|
||||
product_key=product_key,
|
||||
force_refresh=force_refresh,
|
||||
resolution_m=resolution_m,
|
||||
)
|
||||
|
||||
|
||||
@@ -143,6 +145,21 @@ def test_orthophoto_request_is_bounded_and_uses_official_wms_contract() -> None:
|
||||
assert len(prepared["request_hash"]) == 64
|
||||
|
||||
|
||||
def test_training_request_can_use_native_resolution_but_not_oversample_source() -> None:
|
||||
settings = Settings(_env_file=None)
|
||||
prepared = OrthophotoAcquisitionService._prepared_request(
|
||||
_selection_payload(product_key="wallonia_latest", resolution_m=0.25), settings
|
||||
)
|
||||
assert 2_000 <= prepared["width"] <= 2_120
|
||||
assert prepared["resolution_m"] == 0.25
|
||||
|
||||
with pytest.raises(AppError) as exc_info:
|
||||
OrthophotoAcquisitionService._prepared_request(
|
||||
_selection_payload(product_key="wallonia_latest", resolution_m=0.1), settings
|
||||
)
|
||||
assert exc_info.value.code == "ORTHOPHOTO_RESOLUTION_EXCEEDS_SOURCE"
|
||||
|
||||
|
||||
def test_orthophoto_product_registry_exposes_only_governed_official_layers() -> None:
|
||||
settings = Settings(_env_file=None)
|
||||
products = OrthophotoAcquisitionService.list_products(settings)
|
||||
|
||||
Reference in New Issue
Block a user