Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: added orthophoto tile endpoints for COG image serving #312

Merged
merged 17 commits into from
Nov 4, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
28 changes: 26 additions & 2 deletions src/backend/app/projects/image_processing.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
from psycopg import Connection
from asgiref.sync import async_to_sync
from app.config import settings
import zipfile


class DroneImageProcessor:
Expand Down Expand Up @@ -101,10 +102,8 @@ def process_new_task(
:return: The created task object.
"""
opts = self.options_list_to_dict(options)

# FIXME: take this from the function above
opts = {"dsm": True}

task = self.node.create_task(
images, opts, name, progress_callback, webhook=webhook
)
Expand Down Expand Up @@ -235,6 +234,31 @@ async def download_and_upload_assets_from_odm_to_s3(

log.info(f"Assets for task {task_id} successfully uploaded to S3.")

# Extract the zip file to find the orthophoto
with zipfile.ZipFile(assets_path, "r") as zip_ref:
zip_ref.extractall(output_file_path)

# Locate the orthophoto (odm_orthophoto.tif)
orthophoto_path = os.path.join(
output_file_path, "odm_orthophoto", "odm_orthophoto.tif"
)
if not os.path.exists(orthophoto_path):
log.error(f"Orthophoto file not found at {orthophoto_path}")
raise FileNotFoundError(f"Orthophoto not found in {output_file_path}")

log.info(f"Orthophoto found at {orthophoto_path}")

# Upload the orthophoto to S3
s3_ortho_path = (
f"projects/{dtm_project_id}/{dtm_task_id}/orthophoto/odm_orthophoto.tif"
)
log.info(f"Uploading orthophoto to S3 path: {s3_ortho_path}")
add_file_to_bucket(settings.S3_BUCKET_NAME, orthophoto_path, s3_ortho_path)

log.info(
f"Orthophoto for task {task_id} successfully uploaded to S3 at {s3_ortho_path}"
)

# Update background task status to COMPLETED
pool = await database.get_db_connection_pool()

Expand Down
1 change: 1 addition & 0 deletions src/backend/app/projects/project_logic.py
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,7 @@ def process_drone_images(
options = [
{"name": "dsm", "value": True},
{"name": "orthophoto-resolution", "value": 5},
{"name": "cog", "value": True},
]

webhook_url = f"{settings.BACKEND_URL}/api/projects/odm/webhook/{user_id}/{project_id}/{task_id}/"
Expand Down
44 changes: 43 additions & 1 deletion src/backend/app/projects/project_routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,13 +27,15 @@
from app.projects import project_schemas, project_deps, project_logic, image_processing
from app.db import database
from app.models.enums import HTTPStatus, State
from app.s3 import s3_client
from app.s3 import get_cog_path, s3_client
from app.config import settings
from app.users.user_deps import login_required
from app.users.user_schemas import AuthUser
from app.tasks import task_schemas
from app.utils import geojson_to_kml, timestamp
from app.users import user_schemas
from rio_tiler.io import Reader
from rio_tiler.errors import TileOutsideBounds
from minio.deleteobjects import DeleteObject


Expand Down Expand Up @@ -578,3 +580,43 @@ async def odm_webhook(
log.info(f"Task ID: {task_id}, Status: Webhook received")

return {"message": "Webhook received", "task_id": task_id}


@router.get(
"/orthophoto/{z}/{x}/{y}.png",
tags=["Image Processing"],
)
async def get_orthophoto_tile(
# user_data: Annotated[AuthUser, Depends(login_required)],
project_id: str,
task_id: str,
z: int,
x: int,
y: int,
):
"""
Endpoint to serve COG tiles as PNG images.

:param project_id: ID of the project.
:param task_id: ID of the task.
:param z: Zoom level.
:param x: Tile X coordinate.
:param y: Tile Y coordinate.
:return: PNG image tile.
"""
try:
cog_path = get_cog_path("dtm-data", project_id, task_id)
with Reader(cog_path) as tiff:
try:
img = tiff.tile(int(x), int(y), int(z), tilesize=256, expression=None)
tile = img.render()
return Response(content=tile, media_type="image/png")

except TileOutsideBounds:
return []
raise HTTPException(
status_code=200, detail="Tile is outside the bounds of the image."
)

except Exception as e:
raise HTTPException(status_code=500, detail=f"Error generating tile: {str(e)}")
18 changes: 18 additions & 0 deletions src/backend/app/s3.py
Original file line number Diff line number Diff line change
Expand Up @@ -213,3 +213,21 @@ def get_object_metadata(bucket_name: str, object_name: str):
"""
client = s3_client()
return client.stat_object(bucket_name, object_name)


def get_cog_path(bucket_name: str, project_id: str, task_id: str):
"""Generate the presigned URL for a COG file in an S3 bucket.

Args:
bucket_name (str): The name of the S3 bucket.
project_id (str): The unique project identifier.
orthophoto_name (str): The name of the COG file.

Returns:
str: The presigned URL to access the COG file.
"""
# Construct the S3 path for the COG file
s3_path = f"projects/{project_id}/{task_id}/orthophoto/odm_orthophoto.tif"

# Get the presigned URL
return get_presigned_url(bucket_name, s3_path)
21 changes: 21 additions & 0 deletions src/backend/app/tasks/task_routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,26 @@ async def read_task(
"""
SELECT
ST_Area(ST_Transform(tasks.outline, 3857)) / 1000000 AS task_area,

-- Construct the outline as a GeoJSON Feature
jsonb_build_object(
'type', 'Feature',
'geometry', jsonb_build_object(
'type', ST_GeometryType(tasks.outline)::text, -- Get the type of the geometry (e.g., Polygon, MultiPolygon)
'coordinates', ST_AsGeoJSON(tasks.outline, 8)::jsonb->'coordinates' -- Get the geometry coordinates
),
'properties', jsonb_build_object(
'id', tasks.id,
'bbox', jsonb_build_array( -- Build the bounding box
ST_XMin(ST_Envelope(tasks.outline)),
ST_YMin(ST_Envelope(tasks.outline)),
ST_XMax(ST_Envelope(tasks.outline)),
ST_YMax(ST_Envelope(tasks.outline))
)
),
'id', tasks.id
) AS outline,

te.created_at,
te.updated_at,
te.state,
Expand All @@ -43,6 +63,7 @@ async def read_task(
projects.side_overlap AS side_overlap,
projects.gsd_cm_px AS gsd_cm_px,
projects.gimble_angles_degrees AS gimble_angles_degrees

FROM (
SELECT DISTINCT ON (te.task_id)
te.task_id,
Expand Down
Loading