11from __future__ import annotations
22
33import base64
4+ import io
45
56from celery import shared_task
67from django .conf import settings
8+ from django .contrib .gis .geos import Polygon
79from django_large_image import utilities
810import large_image
11+ import numpy as np
12+ from PIL import Image
13+ from rasterio .features import rasterize
14+ from rasterio .transform import from_bounds
15+ from shapely import wkt
916
10- from uvdat .core .models import RasterData , TaskResult
17+ from uvdat .core .models import RasterData , Region , TaskResult
1118
1219from .analysis_type import AnalysisInputError , AnalysisTask , AnalysisType
1320
1421MODEL_CARD_URL = "https://huggingface.co/unsloth/Qwen3.5-9B-GGUF"
1522SYSTEM_PROMPT = (
1623 "You are an urban planning and geospatial analysis expert specializing in "
1724 "land use patterns, hydrology, transportation networks, and municipal policy. "
25+ "Masked aerial imagery will be provided; ignore transparent areas of the image. "
1826 "Analyze the provided imagery to answer the user's question. In your answer, "
1927 "assume that the user is also a geospatial analyst with the same expertise."
2028)
@@ -40,6 +48,7 @@ def __init__(self):
4048 "imagery" : "RasterData" ,
4149 "text_prompt" : "string" ,
4250 "max_tokens" : "number" ,
51+ "region" : "Region" ,
4352 }
4453 self .output_types = {
4554 "response" : "markdown" ,
@@ -60,6 +69,7 @@ def get_input_options(self):
6069 "imagery" : RasterData .objects .filter (dataset__category = "imagery" ),
6170 "text_prompt" : [],
6271 "max_tokens" : [TOKEN_RANGE ],
72+ "region" : Region .objects .all (),
6373 }
6474
6575 def validate_inputs (self , inputs ):
@@ -80,6 +90,11 @@ def validate_inputs(self, inputs):
8090 if max_tokens < TOKEN_RANGE ["min" ] or max_tokens > TOKEN_RANGE ["max" ]:
8191 err_msg = f"max_tokens must be between { TOKEN_RANGE ['min' ]} and { TOKEN_RANGE ['max' ]} ."
8292 raise AnalysisInputError (err_msg )
93+ try :
94+ Region .objects .get (id = inputs .get ("region" ))
95+ except Region .DoesNotExist as e :
96+ err_msg = "Region does not exist."
97+ raise AnalysisInputError (err_msg ) from e
8398
8499 def run_task (self , * , project , ** inputs ):
85100 text_prompt = inputs .get ("text_prompt" )
@@ -106,15 +121,66 @@ def imagery_ask_qwen(result_id):
106121 )
107122
108123 result = TaskResult .objects .get (id = result_id )
124+ if any (
125+ setting == "changeme"
126+ for setting in [
127+ settings .UVDAT_HF_ENDPOINT_NAMES ,
128+ settings .UVDAT_HF_NAMESPACE ,
129+ settings .UVDAT_HF_TOKEN ,
130+ ]
131+ ):
132+ result .write_outputs ({"response" : "Huggingface configuration not set; not running task." })
133+ return
134+
109135 imagery = RasterData .objects .get (id = result .inputs .get ("imagery" ))
110136 text_prompt = result .inputs .get ("text_prompt" )
111137 max_tokens = int (result .inputs .get ("max_tokens" ))
138+ region = Region .objects .get (id = result .inputs .get ("region" ))
139+ (xmin , ymin , xmax , ymax ) = region .boundary .extent
112140
113- result .write_status ("Encoding imagery..." )
141+ result .write_status ("Cropping and encoding imagery..." )
114142 imagery_path = utilities .field_file_to_local_path (imagery .cloud_optimized_geotiff )
115143 src = large_image .open (imagery_path )
116- thumbnail_bytes , _ = src .getThumbnail (THUMBNAIL_SIZE , THUMBNAIL_SIZE , encoding = "PNG" )
117- thumbnail_b64 = base64 .b64encode (thumbnail_bytes ).decode ("utf-8" )
144+ src_bounds = src .getBounds ()
145+ if not region .boundary .intersects (
146+ Polygon .from_bbox (
147+ (
148+ src_bounds .get ("xmin" ),
149+ src_bounds .get ("ymin" ),
150+ src_bounds .get ("xmax" ),
151+ src_bounds .get ("ymax" ),
152+ )
153+ )
154+ ):
155+ result .write_outputs (
156+ {"response" : "Selected region does not intersect imagery; not running task." }
157+ )
158+ return
159+
160+ (xmin , ymin , xmax , ymax ) = (
161+ max (xmin , src_bounds .get ("xmin" )),
162+ max (ymin , src_bounds .get ("ymin" )),
163+ min (xmax , src_bounds .get ("xmax" )),
164+ min (ymax , src_bounds .get ("ymax" )),
165+ )
166+ thumbnail , _ = src .getRegion (
167+ region = {"left" : xmin , "right" : xmax , "top" : ymax , "bottom" : ymin , "units" : "EPSG:4326" },
168+ output = {"maxWidth" : THUMBNAIL_SIZE , "maxHeight" : THUMBNAIL_SIZE },
169+ format = "numpy" ,
170+ )
171+ height , width , _ = thumbnail .shape
172+ mask = rasterize (
173+ [wkt .loads (region .boundary .wkt )],
174+ out_shape = (height , width ),
175+ transform = from_bounds (xmin , ymin , xmax , ymax , width , height ),
176+ fill = 0 ,
177+ default_value = 1 ,
178+ dtype = "uint8" ,
179+ ).astype (bool )
180+ masked = np .where (mask [:, :, np .newaxis ], thumbnail , 0 )
181+ byte_stream = io .BytesIO ()
182+ Image .fromarray (masked ).save (byte_stream , format = "PNG" )
183+ thumbnail_b64 = base64 .b64encode (byte_stream .getvalue ()).decode ("utf-8" )
118184 thumbnail_uri = f"data:image/jpeg;base64,{ thumbnail_b64 } "
119185
120186 result .write_status ("Starting inference endpoint..." )
0 commit comments