Initial GeoIntel V1 foundation
This commit is contained in:
@@ -0,0 +1,73 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from pyproj import Transformer
|
||||
from shapely.geometry import Polygon
|
||||
|
||||
from app.core.errors import AppError
|
||||
|
||||
|
||||
def pixel_bbox_to_epsg4326_polygon(bbox: list[float], tile: dict[str, Any], crs: str | None = None) -> Polygon:
|
||||
if len(bbox) != 4:
|
||||
raise AppError(code="DETECTION_INVALID_BBOX", message="YOLO detection bbox must contain four pixel coordinates", status_code=422)
|
||||
|
||||
x_min, y_min, x_max, y_max = [float(value) for value in bbox]
|
||||
if x_max <= x_min or y_max <= y_min:
|
||||
raise AppError(code="DETECTION_INVALID_BBOX", message="YOLO detection bbox must have positive width and height", status_code=422)
|
||||
|
||||
transform = tile.get("transform")
|
||||
if isinstance(transform, list) and len(transform) >= 6:
|
||||
corners = [
|
||||
_apply_gdal_transform(transform, x_min, y_min),
|
||||
_apply_gdal_transform(transform, x_max, y_min),
|
||||
_apply_gdal_transform(transform, x_max, y_max),
|
||||
_apply_gdal_transform(transform, x_min, y_max),
|
||||
_apply_gdal_transform(transform, x_min, y_min),
|
||||
]
|
||||
else:
|
||||
corners = _corners_from_bounds(bbox=[x_min, y_min, x_max, y_max], tile=tile)
|
||||
|
||||
source_crs = crs or tile.get("crs") or tile.get("source_crs") or "EPSG:4326"
|
||||
if str(source_crs).upper() not in {"EPSG:4326", "4326"}:
|
||||
transformer = Transformer.from_crs(source_crs, "EPSG:4326", always_xy=True)
|
||||
corners = [transformer.transform(x, y) for x, y in corners]
|
||||
|
||||
polygon = Polygon(corners)
|
||||
if polygon.is_empty or not polygon.is_valid:
|
||||
raise AppError(code="DETECTION_INVALID_GEOMETRY", message="Georeferenced detection geometry is invalid", status_code=422)
|
||||
return polygon
|
||||
|
||||
|
||||
def _apply_gdal_transform(transform: list[float], x: float, y: float) -> tuple[float, float]:
|
||||
c, a, b, f, d, e = [float(value) for value in transform[:6]]
|
||||
return (a * x + b * y + c, d * x + e * y + f)
|
||||
|
||||
|
||||
def _corners_from_bounds(bbox: list[float], tile: dict[str, Any]) -> list[tuple[float, float]]:
|
||||
bounds = tile.get("bounds")
|
||||
pixel_window = tile.get("pixel_window")
|
||||
if not (isinstance(bounds, list) and len(bounds) == 4 and isinstance(pixel_window, list) and len(pixel_window) == 4):
|
||||
raise AppError(
|
||||
code="DETECTION_TILE_MANIFEST_INVALID",
|
||||
message="Tile manifest entries require transform or bounds plus pixel_window for georeferencing",
|
||||
status_code=422,
|
||||
)
|
||||
x_min, y_min, x_max, y_max = bbox
|
||||
left, bottom, right, top = [float(value) for value in bounds]
|
||||
_, _, width, height = [float(value) for value in pixel_window]
|
||||
if width <= 0 or height <= 0:
|
||||
raise AppError(code="DETECTION_TILE_MANIFEST_INVALID", message="Tile pixel_window must have positive size", status_code=422)
|
||||
|
||||
def project(px: float, py: float) -> tuple[float, float]:
|
||||
x = left + (px / width) * (right - left)
|
||||
y = top - (py / height) * (top - bottom)
|
||||
return (x, y)
|
||||
|
||||
return [
|
||||
project(x_min, y_min),
|
||||
project(x_max, y_min),
|
||||
project(x_max, y_max),
|
||||
project(x_min, y_max),
|
||||
project(x_min, y_min),
|
||||
]
|
||||
Reference in New Issue
Block a user