Synthetic Extremity X-rays

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"""Synthetic Extremity X-ray Image Generation Recipe
10
11Generate synthetic extremity X-ray style images with controlled variation over
12anatomical region, view, imaging context, technical quality, and musculoskeletal
13findings.
14
15Medical disclaimer:
16 These generated images are synthetic and intended only for AI research,
17 education, data-pipeline prototyping, and evaluation workflows. They are not
18 real medical images and must not be used for diagnosis, treatment planning,
19 clinical decision-making, or as a substitute for real clinical validation.
20
21Prerequisites:
22 - An image-generation provider key for the selected model. The defaults use
23 OpenRouter, so set OPENROUTER_API_KEY before running.
24
25Run:
26 uv run medical_extremity_xrays.py --num-records 5
27"""
28
29from __future__ import annotations
30
31import argparse
32from pathlib import Path
33
34import data_designer.config as dd
35from data_designer.interface import DataDesigner, DatasetCreationResults
36
37DEFAULT_MODEL_PROVIDER = "openrouter"
38DEFAULT_MODEL_ID = "google/gemini-3.1-flash-image-preview"
39DEFAULT_MODEL_ALIAS = "medical-image-model"
40
41
42def build_model_configs(
43 *,
44 model_provider: str,
45 model_id: str,
46 model_alias: str,
47 image_size: str,
48 aspect_ratio: str,
49 max_parallel_requests: int,
50) -> list[dd.ModelConfig]:
51 """Build a provider-agnostic image-generation model config."""
52 return [
53 dd.ModelConfig(
54 alias=model_alias,
55 model=model_id,
56 provider=model_provider,
57 inference_parameters=dd.ImageInferenceParams(
58 extra_body={
59 "modalities": ["image", "text"],
60 "image_config": {
61 "aspect_ratio": aspect_ratio,
62 "image_size": image_size,
63 },
64 },
65 max_parallel_requests=max_parallel_requests,
66 ),
67 skip_health_check=True,
68 )
69 ]
70
71
72def add_category(config_builder: dd.DataDesignerConfigBuilder, name: str, values: list[str]) -> None:
73 """Add a categorical sampler column."""
74 config_builder.add_column(
75 dd.SamplerColumnConfig(
76 name=name,
77 sampler_type=dd.SamplerType.CATEGORY,
78 params=dd.CategorySamplerParams(values=values),
79 )
80 )
81
82
83def add_visual_variation_id(config_builder: dd.DataDesignerConfigBuilder) -> None:
84 """Add a unique row-level key that discourages duplicate image generations."""
85 config_builder.add_column(
86 dd.SamplerColumnConfig(
87 name="visual_variation_id",
88 sampler_type=dd.SamplerType.UUID,
89 params=dd.UUIDSamplerParams(prefix="xray-", short_form=True),
90 )
91 )
92
93
94def build_config(
95 *,
96 model_provider: str = DEFAULT_MODEL_PROVIDER,
97 model_id: str = DEFAULT_MODEL_ID,
98 model_alias: str = DEFAULT_MODEL_ALIAS,
99 image_size: str = "1K",
100 aspect_ratio: str = "1:1",
101 max_parallel_requests: int = 10,
102) -> dd.DataDesignerConfigBuilder:
103 """Build a synthetic extremity X-ray image-generation pipeline."""
104 model_configs = build_model_configs(
105 model_provider=model_provider,
106 model_id=model_id,
107 model_alias=model_alias,
108 image_size=image_size,
109 aspect_ratio=aspect_ratio,
110 max_parallel_requests=max_parallel_requests,
111 )
112 config_builder = dd.DataDesignerConfigBuilder(model_configs=model_configs)
113 add_visual_variation_id(config_builder)
114
115 add_category(
116 config_builder,
117 "patient_age_group",
118 [
119 "young adult",
120 "adult",
121 "middle-aged adult",
122 "older adult",
123 "geriatric adult",
124 ],
125 )
126
127 add_category(
128 config_builder,
129 "patient_sex",
130 [
131 "female",
132 "male",
133 ],
134 )
135
136 add_category(
137 config_builder,
138 "body_habitus",
139 [
140 "thin build",
141 "athletic build",
142 "average build",
143 "overweight build",
144 "obese build",
145 ],
146 )
147
148 add_category(
149 config_builder,
150 "anatomical_region",
151 [
152 "right shoulder",
153 "left shoulder",
154 "right humerus",
155 "left humerus",
156 "right elbow",
157 "left elbow",
158 "right forearm with radius and ulna",
159 "left forearm with radius and ulna",
160 "right wrist",
161 "left wrist",
162 "right hand and fingers",
163 "left hand and fingers",
164 "right hip",
165 "left hip",
166 "right femur",
167 "left femur",
168 "right knee",
169 "left knee",
170 "right tibia and fibula",
171 "left tibia and fibula",
172 "right ankle",
173 "left ankle",
174 "right foot and toes",
175 "left foot and toes",
176 ],
177 )
178
179 add_category(
180 config_builder,
181 "equipment_type",
182 [
183 "fixed radiography unit",
184 "portable X-ray machine",
185 "digital radiography system",
186 "computed radiography system",
187 ],
188 )
189
190 add_category(
191 config_builder,
192 "imaging_context",
193 [
194 "emergency department acute trauma",
195 "emergency department fall injury",
196 "emergency department sports injury",
197 "orthopedic clinic routine follow-up",
198 "post-operative hardware check",
199 "pre-operative planning",
200 "urgent care pain evaluation",
201 ],
202 )
203
204 add_category(
205 config_builder,
206 "xray_view",
207 [
208 "anteroposterior (AP)",
209 "lateral",
210 "oblique internal rotation",
211 "oblique external rotation",
212 "weight-bearing AP",
213 "stress view",
214 ],
215 )
216
217 add_category(
218 config_builder,
219 "exposure_quality",
220 [
221 "underexposed with cortical margins poorly defined",
222 "optimal exposure with clear cortical and trabecular detail",
223 "overexposed with washed out bone detail",
224 "low kVp technique with high bone contrast",
225 "high kVp technique with better soft tissue visualization",
226 ],
227 )
228
229 add_category(
230 config_builder,
231 "positioning",
232 [
233 "well-positioned true AP or lateral",
234 "slightly rotated",
235 "oblique positioning",
236 "splint or cast in place",
237 "traction device visible",
238 "suboptimal because the patient could not cooperate due to pain",
239 ],
240 )
241
242 add_category(
243 config_builder,
244 "primary_finding",
245 [
246 "normal with no acute osseous abnormality",
247 "nondisplaced fracture through the imaged bone",
248 "displaced fracture through the imaged bone",
249 "comminuted fracture involving the imaged bone",
250 "stress fracture line in the imaged bone",
251 "joint dislocation or subluxation in the imaged region",
252 "degenerative osteoarthritis in the imaged joint",
253 "suspected osteomyelitis with focal cortical destruction",
254 "soft tissue swelling with no acute fracture identified",
255 ],
256 )
257
258 add_category(
259 config_builder,
260 "secondary_findings",
261 [
262 "none",
263 "osteopenia",
264 "degenerative joint changes at adjacent joints",
265 "old healed fracture with callus formation",
266 "orthopedic plate and screws",
267 "intramedullary nail",
268 "joint effusion",
269 "soft tissue calcifications",
270 "vascular calcifications",
271 ],
272 )
273
274 add_category(
275 config_builder,
276 "image_quality",
277 [
278 "excellent sharp cortical margins and clear trabecular pattern",
279 "good adequate visualization of all bony structures",
280 "fair with mild motion artifact",
281 "fair with mild noise or graininess",
282 "fair with cast or splint partially obscuring detail",
283 "limited portable technique with technical limitations",
284 "limited by patient body habitus",
285 ],
286 )
287
288 config_builder.add_column(
289 dd.ImageColumnConfig(
290 name="extremity_xray",
291 prompt=EXTREMITY_XRAY_PROMPT,
292 model_alias=model_alias,
293 )
294 )
295
296 return config_builder
297
298
299def create_dataset(
300 config_builder: dd.DataDesignerConfigBuilder,
301 *,
302 num_records: int,
303 dataset_name: str,
304 artifact_path: Path | str | None = None,
305) -> DatasetCreationResults:
306 data_designer = DataDesigner(artifact_path=artifact_path)
307 data_designer.validate(config_builder)
308 return data_designer.create(config_builder, num_records=num_records, dataset_name=dataset_name)
309
310
311EXTREMITY_XRAY_PROMPT = """\
312Create a synthetic research-only grayscale X-ray style radiograph of the
313{{ anatomical_region }}, {{ xray_view }} view.
314
315Patient and acquisition context:
316- Visual variation ID, for internal diversity only: {{ visual_variation_id }}
317- Patient age group: {{ patient_age_group }}
318- Patient sex: {{ patient_sex }}
319- Body habitus: {{ body_habitus }}
320- Equipment: {{ equipment_type }}
321- Context: {{ imaging_context }}
322- Technical quality: {{ exposure_quality }}
323- Positioning: {{ positioning }}
324- Image quality: {{ image_quality }}
325
326Findings to depict:
327- Primary finding: {{ primary_finding }}
328- Secondary findings: {{ secondary_findings }}
329
330Use a realistic educational radiograph style with visible bones, joints, cortex,
331trabecular pattern, and soft-tissue silhouette. Include standard left/right
332markers where appropriate. Make the image look synthetic but useful for AI
333research and data-pipeline prototyping. Do not include real patient names, real
334medical record numbers, hospital logos, or any real protected health information.
335Generate exactly one final radiograph for this row. Do not return alternate
336versions, a two-view panel, a grid, a before/after image, duplicated views, or
337multiple image candidates. Use the visual variation ID only as an internal
338diversity key for anatomy framing, rotation, exposure texture, and soft-tissue
339background; never render it as text. Do not add diagnostic captions or
340explanatory text overlays.
341"""
342
343
344def parse_args() -> argparse.Namespace:
345 parser = argparse.ArgumentParser(description="Generate synthetic extremity X-ray style images.")
346 parser.add_argument("--num-records", type=int, default=5, help="Number of synthetic X-ray images to generate.")
347 parser.add_argument("--dataset-name", default="synthetic-extremity-xrays", help="Output dataset name.")
348 parser.add_argument("--artifact-path", type=Path, default=None, help="Optional Data Designer artifact directory.")
349 parser.add_argument("--model-provider", default=DEFAULT_MODEL_PROVIDER, help="Image model provider name.")
350 parser.add_argument("--model-id", default=DEFAULT_MODEL_ID, help="Provider model ID.")
351 parser.add_argument("--model-alias", default=DEFAULT_MODEL_ALIAS, help="Alias used by image columns.")
352 parser.add_argument("--image-size", default="1K", help="OpenRouter image size tier, such as 1K, 2K, or 4K.")
353 parser.add_argument("--aspect-ratio", default="1:1", help="Provider-specific aspect ratio value.")
354 parser.add_argument("--max-parallel-requests", type=int, default=10, help="Maximum parallel image requests.")
355 return parser.parse_args()
356
357
358def main() -> None:
359 args = parse_args()
360 config_builder = build_config(
361 model_provider=args.model_provider,
362 model_id=args.model_id,
363 model_alias=args.model_alias,
364 image_size=args.image_size,
365 aspect_ratio=args.aspect_ratio,
366 max_parallel_requests=args.max_parallel_requests,
367 )
368 results = create_dataset(
369 config_builder,
370 num_records=args.num_records,
371 dataset_name=args.dataset_name,
372 artifact_path=args.artifact_path,
373 )
374 dataset = results.load_dataset()
375 print(f"Generated {len(dataset)} synthetic extremity X-ray rows.")
376 print(f"Dataset artifacts: {results.artifact_storage.base_dataset_path}")
377
378
379if __name__ == "__main__":
380 main()