Synthetic Extremity X-rays
Synthetic Extremity X-rays
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 """Synthetic Extremity X-ray Image Generation Recipe 10 11 Generate synthetic extremity X-ray style images with controlled variation over 12 anatomical region, view, imaging context, technical quality, and musculoskeletal 13 findings. 14 15 Medical 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 21 Prerequisites: 22 - An image-generation provider key for the selected model. The defaults use 23 OpenRouter, so set OPENROUTER_API_KEY before running. 24 25 Run: 26 uv run medical_extremity_xrays.py --num-records 5 27 """ 28 29 from __future__ import annotations 30 31 import argparse 32 from pathlib import Path 33 34 import data_designer.config as dd 35 from data_designer.interface import DataDesigner, DatasetCreationResults 36 37 DEFAULT_MODEL_PROVIDER = "openrouter" 38 DEFAULT_MODEL_ID = "google/gemini-3.1-flash-image-preview" 39 DEFAULT_MODEL_ALIAS = "medical-image-model" 40 41 42 def 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 72 def 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 83 def 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 94 def 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 299 def 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 311 EXTREMITY_XRAY_PROMPT = """\ 312 Create a synthetic research-only grayscale X-ray style radiograph of the 313 {{ anatomical_region }}, {{ xray_view }} view. 314 315 Patient 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 326 Findings to depict: 327 - Primary finding: {{ primary_finding }} 328 - Secondary findings: {{ secondary_findings }} 329 330 Use a realistic educational radiograph style with visible bones, joints, cortex, 331 trabecular pattern, and soft-tissue silhouette. Include standard left/right 332 markers where appropriate. Make the image look synthetic but useful for AI 333 research and data-pipeline prototyping. Do not include real patient names, real 334 medical record numbers, hospital logos, or any real protected health information. 335 Generate exactly one final radiograph for this row. Do not return alternate 336 versions, a two-view panel, a grid, a before/after image, duplicated views, or 337 multiple image candidates. Use the visual variation ID only as an internal 338 diversity key for anatomy framing, rotation, exposure texture, and soft-tissue 339 background; never render it as text. Do not add diagnostic captions or 340 explanatory text overlays. 341 """ 342 343 344 def 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 358 def 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 379 if __name__ == "__main__": 380 main()