Drone Aerial Inspection
Download Recipe
1 # SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. 2 # SPDX-License-Identifier: Apache-2.0 3 # /// script 4 # requires-python = ">=3.10" 5 # dependencies = [ 6 # "data-designer", 7 # ] 8 # /// 9 """Drone Aerial Inspection Image Generation Recipe 10 11 Generate synthetic low-altitude drone inspection images with controlled 12 variation over site type, inspection target, altitude, camera angle, defect or 13 event, severity, occlusion, lighting, and surface condition. 14 15 The recipe is intended for infrastructure inspection, property review, 16 construction monitoring, disaster-response review, visual QA, reviewer 17 calibration, and VLM evaluation. It avoids surveillance, military targeting, 18 evasion, or sensitive-facility prompts. 19 20 Prerequisites: 21 - An image-generation provider key for the selected model. The defaults use 22 OpenRouter, so set OPENROUTER_API_KEY before running. 23 24 Run: 25 uv run drone_aerial_inspection.py --num-records 10 26 """ 27 28 from __future__ import annotations 29 30 import argparse 31 from pathlib import Path 32 33 import data_designer.config as dd 34 from data_designer.interface import DataDesigner, DatasetCreationResults 35 36 DEFAULT_MODEL_PROVIDER = "openrouter" 37 DEFAULT_MODEL_ID = "google/gemini-3.1-flash-image-preview" 38 DEFAULT_MODEL_ALIAS = "drone-aerial-inspection-model" 39 40 41 def build_model_configs( 42 *, 43 model_provider: str, 44 model_id: str, 45 model_alias: str, 46 image_size: str, 47 aspect_ratio: str, 48 max_parallel_requests: int, 49 ) -> list[dd.ModelConfig]: 50 return [ 51 dd.ModelConfig( 52 alias=model_alias, 53 model=model_id, 54 provider=model_provider, 55 inference_parameters=dd.ImageInferenceParams( 56 extra_body={ 57 "modalities": ["image", "text"], 58 "image_config": { 59 "aspect_ratio": aspect_ratio, 60 "image_size": image_size, 61 }, 62 }, 63 max_parallel_requests=max_parallel_requests, 64 ), 65 skip_health_check=True, 66 ) 67 ] 68 69 70 def add_category(config_builder: dd.DataDesignerConfigBuilder, name: str, values: list[str]) -> None: 71 config_builder.add_column( 72 dd.SamplerColumnConfig( 73 name=name, 74 sampler_type=dd.SamplerType.CATEGORY, 75 params=dd.CategorySamplerParams(values=values), 76 ) 77 ) 78 79 80 def add_visual_variation_id(config_builder: dd.DataDesignerConfigBuilder) -> None: 81 """Add a unique row-level key that discourages duplicate image generations.""" 82 config_builder.add_column( 83 dd.SamplerColumnConfig( 84 name="visual_variation_id", 85 sampler_type=dd.SamplerType.UUID, 86 params=dd.UUIDSamplerParams(prefix="drone-", short_form=True), 87 ) 88 ) 89 90 91 def build_config( 92 *, 93 model_provider: str = DEFAULT_MODEL_PROVIDER, 94 model_id: str = DEFAULT_MODEL_ID, 95 model_alias: str = DEFAULT_MODEL_ALIAS, 96 image_size: str = "1K", 97 aspect_ratio: str = "16:9", 98 max_parallel_requests: int = 10, 99 ) -> dd.DataDesignerConfigBuilder: 100 model_configs = build_model_configs( 101 model_provider=model_provider, 102 model_id=model_id, 103 model_alias=model_alias, 104 image_size=image_size, 105 aspect_ratio=aspect_ratio, 106 max_parallel_requests=max_parallel_requests, 107 ) 108 config_builder = dd.DataDesignerConfigBuilder(model_configs=model_configs) 109 add_visual_variation_id(config_builder) 110 111 add_category( 112 config_builder, 113 "site_type", 114 [ 115 "residential roof and yard", 116 "commercial flat roof", 117 "bridge deck and support structure", 118 "rail corridor and track bed", 119 "solar farm rows", 120 "wind turbine tower and blades", 121 "construction site", 122 "roadway and drainage culvert", 123 "utility pipeline corridor", 124 "storm-affected neighborhood street", 125 ], 126 ) 127 add_category( 128 config_builder, 129 "inspection_target", 130 [ 131 "roof covering condition", 132 "surface cracking", 133 "standing water", 134 "vegetation encroachment", 135 "panel or blade damage", 136 "debris blocking access", 137 "material staging progress", 138 "erosion around infrastructure", 139 "storm or hail damage", 140 "construction progress milestone", 141 ], 142 ) 143 add_category( 144 config_builder, 145 "altitude", 146 [ 147 "very low drone pass, about 10 meters above the target", 148 "low drone pass, about 25 meters above the target", 149 "medium drone pass, about 60 meters above the target", 150 "higher overview pass, about 100 meters above the target", 151 ], 152 ) 153 add_category( 154 config_builder, 155 "camera_angle", 156 [ 157 "straight-down nadir view", 158 "oblique 45-degree inspection angle", 159 "shallow side-looking pass", 160 "close detail view with wide-angle lens", 161 "overview frame with the target centered", 162 ], 163 ) 164 add_category( 165 config_builder, 166 "defect_or_event", 167 [ 168 "no visible issue, normal baseline condition", 169 "small crack or seam separation", 170 "moderate staining or water pooling", 171 "missing roof shingles or damaged surface panels", 172 "debris scattered across the inspection area", 173 "vegetation growth obscuring part of the asset", 174 "erosion or washout near an edge", 175 "construction material staged in the wrong zone", 176 "storm damage with displaced objects", 177 "surface discoloration that may be benign", 178 ], 179 ) 180 defect_severity_values = [ 181 "minor and easy to miss", 182 "moderate and localized", 183 "severe and clearly visible", 184 ] 185 config_builder.add_column( 186 dd.SamplerColumnConfig( 187 name="severity", 188 sampler_type=dd.SamplerType.SUBCATEGORY, 189 params=dd.SubcategorySamplerParams( 190 category="defect_or_event", 191 values={ 192 "no visible issue, normal baseline condition": ["none"], 193 "small crack or seam separation": defect_severity_values, 194 "moderate staining or water pooling": defect_severity_values, 195 "missing roof shingles or damaged surface panels": defect_severity_values, 196 "debris scattered across the inspection area": defect_severity_values, 197 "vegetation growth obscuring part of the asset": defect_severity_values, 198 "erosion or washout near an edge": defect_severity_values, 199 "construction material staged in the wrong zone": defect_severity_values, 200 "storm damage with displaced objects": defect_severity_values, 201 "surface discoloration that may be benign": ["none - benign confounder"], 202 }, 203 ), 204 ) 205 ) 206 add_category( 207 config_builder, 208 "occlusion", 209 [ 210 "clear unobstructed view", 211 "partially occluded by tree branches", 212 "partially occluded by shadows", 213 "partially occluded by temporary equipment", 214 "motion blur from drone movement", 215 ], 216 ) 217 add_category( 218 config_builder, 219 "weather_lighting", 220 [ 221 "bright midday sun", 222 "soft overcast light", 223 "golden hour light with long shadows", 224 "after-rain wet surfaces", 225 "hazy light with reduced contrast", 226 ], 227 ) 228 229 config_builder.add_column( 230 dd.ImageColumnConfig( 231 name="drone_inspection_image", 232 prompt=DRONE_AERIAL_INSPECTION_PROMPT, 233 model_alias=model_alias, 234 ) 235 ) 236 237 return config_builder 238 239 240 def create_dataset( 241 config_builder: dd.DataDesignerConfigBuilder, 242 *, 243 num_records: int, 244 dataset_name: str, 245 artifact_path: Path | str | None = None, 246 ) -> DatasetCreationResults: 247 data_designer = DataDesigner(artifact_path=artifact_path) 248 data_designer.validate(config_builder) 249 return data_designer.create(config_builder, num_records=num_records, dataset_name=dataset_name) 250 251 252 DRONE_AERIAL_INSPECTION_PROMPT = """\ 253 Create a realistic low-altitude drone inspection image. 254 255 Image requirements: 256 - Visual variation ID, for internal diversity only: {{ visual_variation_id }} 257 - Site type: {{ site_type }} 258 - Inspection target: {{ inspection_target }} 259 - Altitude: {{ altitude }} 260 - Camera angle: {{ camera_angle }} 261 - Defect or event: {{ defect_or_event }} 262 - Severity: {{ severity }} 263 - Occlusion: {{ occlusion }} 264 - Weather and lighting: {{ weather_lighting }} 265 266 Render the image as if captured by a civilian inspection drone, not a satellite 267 or normal ground camera. Make the inspection target and requested defect, 268 event, baseline condition, or confounder visible enough for visual QA, 269 reviewer calibration, or VLM evaluation. Show realistic materials, shadows, 270 scale, surfaces, construction context, vegetation, drainage, roof texture, 271 panels, tracks, roads, or structural elements when requested. 272 273 Render this as a clean raw drone camera frame. Do not include surveillance UI, 274 inspection report graphics, HUD elements, map overlays, crosshairs, targeting 275 reticles, bounding boxes, segmentation masks, heatmap colors, arrows, callouts, 276 measurement graphics, labels, timestamps, coordinates, real place names, 277 readable license plates, identifiable people, faces, watermarks, or any text 278 overlay. Do not frame it as a military, police, or sensitive-facility image. 279 Generate exactly one final drone inspection image for this row. Do not return 280 alternate versions, a grid, a pair of examples, before/after panels, or multiple 281 frames. Use the visual variation ID only as an internal diversity key; never 282 render it as text. 283 """ 284 285 286 def parse_args() -> argparse.Namespace: 287 parser = argparse.ArgumentParser(description="Generate synthetic drone aerial inspection imagery.") 288 parser.add_argument("--num-records", type=int, default=10, help="Number of drone inspection images to generate.") 289 parser.add_argument("--dataset-name", default="drone-aerial-inspection", help="Output dataset name.") 290 parser.add_argument("--artifact-path", type=Path, default=None, help="Optional Data Designer artifact directory.") 291 parser.add_argument("--model-provider", default=DEFAULT_MODEL_PROVIDER, help="Image model provider name.") 292 parser.add_argument("--model-id", default=DEFAULT_MODEL_ID, help="Provider model ID.") 293 parser.add_argument("--model-alias", default=DEFAULT_MODEL_ALIAS, help="Alias used by image columns.") 294 parser.add_argument("--image-size", default="1K", help="OpenRouter image size tier, such as 1K, 2K, or 4K.") 295 parser.add_argument("--aspect-ratio", default="16:9", help="Provider-specific aspect ratio value.") 296 parser.add_argument("--max-parallel-requests", type=int, default=10, help="Maximum parallel image requests.") 297 return parser.parse_args() 298 299 300 def main() -> None: 301 args = parse_args() 302 config_builder = build_config( 303 model_provider=args.model_provider, 304 model_id=args.model_id, 305 model_alias=args.model_alias, 306 image_size=args.image_size, 307 aspect_ratio=args.aspect_ratio, 308 max_parallel_requests=args.max_parallel_requests, 309 ) 310 results = create_dataset( 311 config_builder, 312 num_records=args.num_records, 313 dataset_name=args.dataset_name, 314 artifact_path=args.artifact_path, 315 ) 316 dataset = results.load_dataset() 317 print(f"Generated {len(dataset)} drone aerial inspection image rows.") 318 print(f"Dataset artifacts: {results.artifact_storage.base_dataset_path}") 319 320 321 if __name__ == "__main__": 322 main()