Drone Aerial Inspection

View as Markdown
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
11Generate synthetic low-altitude drone inspection images with controlled
12variation over site type, inspection target, altitude, camera angle, defect or
13event, severity, occlusion, lighting, and surface condition.
14
15The recipe is intended for infrastructure inspection, property review,
16construction monitoring, disaster-response review, visual QA, reviewer
17calibration, and VLM evaluation. It avoids surveillance, military targeting,
18evasion, or sensitive-facility prompts.
19
20Prerequisites:
21 - An image-generation provider key for the selected model. The defaults use
22 OpenRouter, so set OPENROUTER_API_KEY before running.
23
24Run:
25 uv run drone_aerial_inspection.py --num-records 10
26"""
27
28from __future__ import annotations
29
30import argparse
31from pathlib import Path
32
33import data_designer.config as dd
34from data_designer.interface import DataDesigner, DatasetCreationResults
35
36DEFAULT_MODEL_PROVIDER = "openrouter"
37DEFAULT_MODEL_ID = "google/gemini-3.1-flash-image-preview"
38DEFAULT_MODEL_ALIAS = "drone-aerial-inspection-model"
39
40
41def 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
70def 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
80def 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
91def 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
240def 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
252DRONE_AERIAL_INSPECTION_PROMPT = """\
253Create a realistic low-altitude drone inspection image.
254
255Image 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
266Render the image as if captured by a civilian inspection drone, not a satellite
267or normal ground camera. Make the inspection target and requested defect,
268event, baseline condition, or confounder visible enough for visual QA,
269reviewer calibration, or VLM evaluation. Show realistic materials, shadows,
270scale, surfaces, construction context, vegetation, drainage, roof texture,
271panels, tracks, roads, or structural elements when requested.
272
273Render this as a clean raw drone camera frame. Do not include surveillance UI,
274inspection report graphics, HUD elements, map overlays, crosshairs, targeting
275reticles, bounding boxes, segmentation masks, heatmap colors, arrows, callouts,
276measurement graphics, labels, timestamps, coordinates, real place names,
277readable license plates, identifiable people, faces, watermarks, or any text
278overlay. Do not frame it as a military, police, or sensitive-facility image.
279Generate exactly one final drone inspection image for this row. Do not return
280alternate versions, a grid, a pair of examples, before/after panels, or multiple
281frames. Use the visual variation ID only as an internal diversity key; never
282render it as text.
283"""
284
285
286def 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
300def 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
321if __name__ == "__main__":
322 main()