CoolFace
Apppublic

AnonymousECCV15285/MMIB_Counterfactual_image_generation_tool

sourceHugging Facemitupdated 7mo agoView on Hugging Face
0likes
render.py1205 linesDownload Raw Back to scripts
1from __future__ import print_function
2import math, sys, random, argparse, json, os, tempfile
3from datetime import datetime as dt
4from collections import Counter
5try:
6    from PIL import Image, ImageFilter
7except ImportError:
8    Image = None
9    ImageFilter = None
10
11INSIDE_BLENDER = True
12try:
13  import bpy, bpy_extras
14  from mathutils import Vector
15except ImportError as e:
16  INSIDE_BLENDER = False
17
18def extract_args(input_argv=None):
19  if input_argv is None:
20    input_argv = sys.argv
21  output_argv = []
22  if '--' in input_argv:
23    idx = input_argv.index('--')
24    output_argv = input_argv[(idx + 1):]
25  return output_argv
26
27def parse_args(parser, argv=None):
28  return parser.parse_args(extract_args(argv))
29
30def delete_object(obj):
31  if not INSIDE_BLENDER:
32    return
33  bpy.ops.object.select_all(action='DESELECT')
34  obj.select_set(True)
35  bpy.context.view_layer.objects.active = obj
36  bpy.ops.object.delete()
37
38def get_camera_coords(cam, pos):
39  if not INSIDE_BLENDER:
40    return (0, 0, 0)
41  scene = bpy.context.scene
42  x, y, z = bpy_extras.object_utils.world_to_camera_view(scene, cam, pos)
43  scale = scene.render.resolution_percentage / 100.0
44  w = int(scale * scene.render.resolution_x)
45  h = int(scale * scene.render.resolution_y)
46  px = int(round(x * w))
47  py = int(round(h - y * h))
48  return (px, py, z)
49
50def set_layer(obj, layer_idx):
51  if not INSIDE_BLENDER:
52    return
53  obj.layers[layer_idx] = True
54  for i in range(len(obj.layers)):
55    obj.layers[i] = (i == layer_idx)
56
57def add_object(object_dir, name, scale, loc, theta=0):
58  if not INSIDE_BLENDER:
59    return
60  object_dir = os.path.abspath(os.path.normpath(object_dir))
61  if not os.path.exists(object_dir):
62    print(f"ERROR: Object directory does not exist: {object_dir}")
63    return
64  count = 0
65  for obj in bpy.data.objects:
66    if obj.name.startswith(name):
67      count += 1
68
69  blend_file = os.path.join(object_dir, '%s.blend' % name)
70  blend_file = os.path.abspath(blend_file).replace('\\', '/')
71  if not os.path.exists(blend_file):
72    print(f"ERROR: Blend file does not exist: {blend_file}")
73    return
74  directory = blend_file + '/Object/'
75  try:
76    bpy.ops.wm.append(
77      directory=directory,
78      filename=name,
79      filter_blender=True
80    )
81  except Exception as e:
82    error_msg = str(e)
83    print(f"ERROR: Failed to load object {name} from {directory}: {error_msg}")
84    print(f"  Error type: {type(e).__name__}")
85    raise
86
87  new_name = '%s_%d' % (name, count)
88  bpy.data.objects[name].name = new_name
89
90  x, y = loc
91  bpy.context.view_layer.objects.active = bpy.data.objects[new_name]
92  bpy.context.object.rotation_euler[2] = theta
93  bpy.ops.transform.resize(value=(scale, scale, scale))
94  bpy.ops.transform.translate(value=(x, y, scale))
95
96def load_materials(material_dir):
97  if not INSIDE_BLENDER:
98    return
99  material_dir = os.path.abspath(os.path.normpath(material_dir))
100  if not os.path.exists(material_dir):
101    print(f"ERROR: Material directory does not exist: {material_dir}")
102    return
103  for fn in os.listdir(material_dir):
104    if not fn.endswith('.blend'): continue
105    name = os.path.splitext(fn)[0]
106    blend_file = os.path.join(material_dir, fn)
107    blend_file = os.path.abspath(blend_file).replace('\\', '/')
108    if not os.path.exists(blend_file):
109      print(f"ERROR: Blend file does not exist: {blend_file}")
110      continue
111    directory = blend_file + '/NodeTree/'
112    try:
113      bpy.ops.wm.append(
114        directory=directory,
115        filename=name,
116        filter_blender=True
117      )
118    except Exception as e:
119      error_msg = str(e)
120      print(f"ERROR: Failed to load material {name} from {directory}: {error_msg}")
121      print(f"  Error type: {type(e).__name__}")
122      raise
123
124def apply_filter_to_image(image_path, filter_type, filter_strength):
125  image_path = os.path.abspath(image_path)
126  if Image is None:
127    print(f"ERROR: PIL/Image not available, cannot apply filter {filter_type}")
128    return
129  if not os.path.exists(image_path):
130    print(f"ERROR: Image file does not exist: {image_path}")
131    return
132  
133  try:
134    img = Image.open(image_path)
135    
136    if filter_type == 'blur':
137      radius = max(1, int(filter_strength))
138      img = img.filter(ImageFilter.GaussianBlur(radius=radius))
139    
140    elif filter_type == 'vignette':
141      width, height = img.size
142      center_x, center_y = width // 2, height // 2
143      max_dist = math.sqrt(center_x**2 + center_y**2)
144      
145      img = img.convert('RGB')
146      pixels = img.load()
147      for y in range(height):
148        for x in range(width):
149          dist = math.sqrt((x - center_x)**2 + (y - center_y)**2)
150          factor = 1.0 - (dist / max_dist) * (filter_strength / 5.0)
151          factor = max(0.0, min(1.0, factor))
152          
153          r, g, b = pixels[x, y]
154          pixels[x, y] = (int(r * factor), int(g * factor), int(b * factor))
155    
156    elif filter_type == 'fisheye':
157      width, height = img.size
158      center_x, center_y = width / 2.0, height / 2.0
159      max_radius = min(center_x, center_y)
160      
161      img = img.convert('RGB')
162      output = Image.new('RGB', (width, height))
163      out_pixels = output.load()
164      in_pixels = img.load()
165      
166      for y in range(height):
167        for x in range(width):
168          dx = (x - center_x) / max_radius
169          dy = (y - center_y) / max_radius
170          distance = math.sqrt(dx*dx + dy*dy)
171          
172          if distance > 1.0:
173            out_pixels[x, y] = (0, 0, 0)
174          else:
175            theta = math.atan2(dy, dx)
176            r_normalized = distance
177            r_distorted = r_normalized * (1.0 + filter_strength * (1.0 - r_normalized))
178            r_distorted = min(1.0, r_distorted)
179            
180            src_x = int(center_x + r_distorted * max_radius * math.cos(theta))
181            src_y = int(center_y + r_distorted * max_radius * math.sin(theta))
182            
183            if 0 <= src_x < width and 0 <= src_y < height:
184              out_pixels[x, y] = in_pixels[src_x, src_y]
185            else:
186              out_pixels[x, y] = (0, 0, 0)
187      
188      img = output
189    
190    img.save(image_path)
191    print(f"[OK] Applied {filter_type} filter (strength: {filter_strength:.2f})")
192  except Exception as e:
193    import traceback
194    print(f"ERROR applying filter {filter_type}: {e}")
195    traceback.print_exc()
196    raise
197
198def add_material(name, **properties):
199  if not INSIDE_BLENDER:
200    return
201  mat_count = len(bpy.data.materials)
202  bpy.ops.material.new()
203  mat = bpy.data.materials['Material']
204  mat.name = 'Material_%d' % mat_count
205  obj = bpy.context.active_object
206  assert len(obj.data.materials) == 0
207  obj.data.materials.append(mat)
208
209  output_node = None
210  for n in mat.node_tree.nodes:
211    if n.name == 'Material Output':
212      output_node = n
213      break
214
215  group_node = mat.node_tree.nodes.new('ShaderNodeGroup')
216  group_node.node_tree = bpy.data.node_groups[name]
217
218  for inp in group_node.inputs:
219    if inp.name in properties:
220      inp.default_value = properties[inp.name]
221
222  mat.node_tree.links.new(
223      group_node.outputs['Shader'],
224      output_node.inputs['Surface'],
225  )
226
227parser = argparse.ArgumentParser()
228parser.add_argument('--scene_file', default=None,
229    help="Optional JSON file to load scene from. If provided, renders from JSON instead of generating random scenes.")
230parser.add_argument('--base_scene_blendfile', default='data/base_scene.blend',
231    help="Base blender file on which all scenes are based; includes " +
232          "ground plane, lights, and camera.")
233parser.add_argument('--properties_json', default='data/properties.json',
234    help="JSON file defining objects, materials, sizes, and colors. " +
235         "The \"colors\" field maps from CLEVR color names to RGB values; " +
236         "The \"sizes\" field maps from CLEVR size names to scalars used to " +
237         "rescale object models; the \"materials\" and \"shapes\" fields map " +
238         "from CLEVR material and shape names to .blend files in the " +
239         "--object_material_dir and --shape_dir directories respectively.")
240parser.add_argument('--shape_dir', default='data/shapes',
241    help="Directory where .blend files for object models are stored")
242parser.add_argument('--material_dir', default='data/materials',
243    help="Directory where .blend files for materials are stored")
244parser.add_argument('--shape_color_combos_json', default=None,
245    help="Optional path to a JSON file mapping shape names to a list of " +
246         "allowed color names for that shape. This allows rendering images " +
247         "for CLEVR-CoGenT.")
248
249parser.add_argument('--min_objects', default=3, type=int,
250    help="The minimum number of objects to place in each scene")
251parser.add_argument('--max_objects', default=10, type=int,
252    help="The maximum number of objects to place in each scene")
253parser.add_argument('--min_dist', default=0.15, type=float,
254    help="The minimum allowed distance between object centers")
255parser.add_argument('--margin', default=0.2, type=float,
256    help="Along all cardinal directions (left, right, front, back), all " +
257         "objects will be at least this distance apart. This makes resolving " +
258         "spatial relationships slightly less ambiguous.")
259parser.add_argument('--min_pixels_per_object', default=50, type=int,
260    help="All objects will have at least this many visible pixels in the " +
261         "final rendered images; this ensures that no objects are fully " +
262         "occluded by other objects.")
263parser.add_argument('--max_retries', default=100, type=int,
264    help="The number of times to try placing an object before giving up and " +
265         "re-placing all objects in the scene.")
266
267parser.add_argument('--start_idx', default=0, type=int,
268    help="The index at which to start for numbering rendered images. Setting " +
269         "this to non-zero values allows you to distribute rendering across " +
270         "multiple machines and recombine the results later.")
271parser.add_argument('--num_images', default=5, type=int,
272    help="The number of images to render")
273parser.add_argument('--filename_prefix', default='CLEVR',
274    help="This prefix will be prepended to the rendered images and JSON scenes")
275parser.add_argument('--split', default='new',
276    help="Name of the split for which we are rendering. This will be added to " +
277         "the names of rendered images, and will also be stored in the JSON " +
278         "scene structure for each image.")
279parser.add_argument('--output_image_dir', default='../output/images/',
280    help="The directory where output images will be stored. It will be " +
281         "created if it does not exist.")
282parser.add_argument('--output_scene_dir', default='../output/scenes/',
283    help="The directory where output JSON scene structures will be stored. " +
284         "It will be created if it does not exist.")
285parser.add_argument('--output_scene_file', default='../output/CLEVR_scenes.json',
286    help="Path to write a single JSON file containing all scene information")
287parser.add_argument('--output_blend_dir', default='output/blendfiles',
288    help="The directory where blender scene files will be stored, if the " +
289         "user requested that these files be saved using the " +
290         "--save_blendfiles flag; in this case it will be created if it does " +
291         "not already exist.")
292parser.add_argument('--save_blendfiles', type=int, default=0,
293    help="Setting --save_blendfiles 1 will cause the blender scene file for " +
294         "each generated image to be stored in the directory specified by " +
295         "the --output_blend_dir flag. These files are not saved by default " +
296         "because they take up ~5-10MB each.")
297parser.add_argument('--version', default='1.0',
298    help="String to store in the \"version\" field of the generated JSON file")
299parser.add_argument('--license',
300    default="Creative Commons Attribution (CC-BY 4.0)",
301    help="String to store in the \"license\" field of the generated JSON file")
302parser.add_argument('--date', default=dt.today().strftime("%m/%d/%Y"),
303    help="String to store in the \"date\" field of the generated JSON file; " +
304         "defaults to today's date")
305
306parser.add_argument('--use_gpu', default=0, type=int,
307    help="Setting --use_gpu 1 enables GPU-accelerated rendering using CUDA. " +
308         "You must have an NVIDIA GPU with the CUDA toolkit installed for " +
309         "to work.")
310parser.add_argument('--width', default=320, type=int,
311    help="The width (in pixels) for the rendered images")
312parser.add_argument('--height', default=240, type=int,
313    help="The height (in pixels) for the rendered images")
314parser.add_argument('--key_light_jitter', default=1.0, type=float,
315    help="The magnitude of random jitter to add to the key light position.")
316parser.add_argument('--fill_light_jitter', default=1.0, type=float,
317    help="The magnitude of random jitter to add to the fill light position.")
318parser.add_argument('--back_light_jitter', default=1.0, type=float,
319    help="The magnitude of random jitter to add to the back light position.")
320parser.add_argument('--camera_jitter', default=0.5, type=float,
321    help="The magnitude of random jitter to add to the camera position")
322parser.add_argument('--render_num_samples', default=512, type=int,
323    help="The number of samples to use when rendering. Larger values will " +
324         "result in nicer images but will cause rendering to take longer.")
325parser.add_argument('--render_min_bounces', default=8, type=int,
326    help="The minimum number of bounces to use for rendering.")
327parser.add_argument('--render_max_bounces', default=8, type=int,
328    help="The maximum number of bounces to use for rendering.")
329parser.add_argument('--render_tile_size', default=256, type=int,
330    help="The tile size to use for rendering. This should not affect the " +
331         "quality of the rendered image but may affect the speed; CPU-based " +
332         "rendering may achieve better performance using smaller tile sizes " +
333         "while larger tile sizes may be optimal for GPU-based rendering.")
334parser.add_argument('--output_image', default=None,
335    help="Output image path (used when rendering from JSON)")
336
337MIN_VISIBLE_FRACTION = 0.001
338MIN_VISIBLE_FRACTION_PARTIAL_OCCLUSION = 0.0005
339MIN_PIXELS_FLOOR = 50
340
341BASE_MIN_VISIBILITY_FRACTION = 0.9
342CF_OCCLUSION_MIN_VISIBILITY_FRACTION = 0.3
343CF_OCCLUSION_MAX_VISIBILITY_FRACTION = 0.5
344CF_OCCLUSION_HARD_MIN_FRACTION = 0.2
345
346
347def min_visible_pixels(width, height, fraction=MIN_VISIBLE_FRACTION, floor=MIN_PIXELS_FLOOR):
348  return max(floor, int(width * height * fraction))
349
350
351BACKGROUND_COLORS = {
352    'default': None,
353    'gray': (0.5, 0.5, 0.5),
354    'blue': (0.2, 0.4, 0.8),
355    'green': (0.2, 0.6, 0.3),
356    'brown': (0.4, 0.3, 0.2),
357    'purple': (0.5, 0.3, 0.6),
358    'orange': (0.8, 0.5, 0.2),
359    'white': (0.9, 0.9, 0.9),
360    'dark_gray': (0.2, 0.2, 0.2),
361    'red': (0.7, 0.2, 0.2),
362    'yellow': (0.8, 0.8, 0.3),
363    'cyan': (0.3, 0.7, 0.8),
364}
365
366LIGHTING_PRESETS = {
367    'default': {'key': 1.0, 'fill': 0.5, 'back': 0.3},
368    'bright': {'key': 12.0, 'fill': 6.0, 'back': 4.0},
369    'dim': {'key': 0.008, 'fill': 0.004, 'back': 0.002},
370    'warm': {'key': 5.0, 'fill': 0.8, 'back': 0.3, 'color': (1.0, 0.5, 0.2)},
371    'cool': {'key': 4.0, 'fill': 2.0, 'back': 1.5, 'color': (0.2, 0.5, 1.0)},
372    'dramatic': {'key': 15.0, 'fill': 0.005, 'back': 0.002},
373}
374
375def set_background_color(color_name):
376  """Set the world background color"""
377  if not INSIDE_BLENDER:
378    return
379  if color_name not in BACKGROUND_COLORS or BACKGROUND_COLORS[color_name] is None:
380    return
381  
382  rgb = BACKGROUND_COLORS[color_name]
383  
384  world = bpy.context.scene.world
385  if world is None:
386    world = bpy.data.worlds.new("World")
387    bpy.context.scene.world = world
388  
389  world.use_nodes = True
390  nodes = world.node_tree.nodes
391  
392  bg_node = None
393  for node in nodes:
394    if node.type == 'BACKGROUND':
395      bg_node = node
396      break
397  
398  if bg_node is None:
399    bg_node = nodes.new(type='ShaderNodeBackground')
400  
401  bg_node.inputs['Color'].default_value = (rgb[0], rgb[1], rgb[2], 1.0)
402  bg_node.inputs['Strength'].default_value = 1.0
403  
404  print(f"Set background color to {color_name}: RGB{rgb}")
405
406def set_ground_color(color_name):
407  if not INSIDE_BLENDER:
408    return
409  if color_name not in BACKGROUND_COLORS or BACKGROUND_COLORS[color_name] is None:
410    return
411  
412  rgb = BACKGROUND_COLORS[color_name]
413  
414  ground = None
415  for obj in bpy.data.objects:
416    if 'ground' in obj.name.lower() or 'plane' in obj.name.lower():
417      ground = obj
418      break
419  
420  if ground is None:
421    return
422  
423  if len(ground.data.materials) == 0:
424    mat = bpy.data.materials.new(name="Ground_Material")
425    ground.data.materials.append(mat)
426  else:
427    mat = ground.data.materials[0]
428  
429  mat.use_nodes = True
430  nodes = mat.node_tree.nodes
431  
432  bsdf = None
433  for node in nodes:
434    if node.type == 'BSDF_PRINCIPLED':
435      bsdf = node
436      break
437  
438  if bsdf:
439    bsdf.inputs['Base Color'].default_value = (rgb[0], rgb[1], rgb[2], 1.0)
440    print(f"Set ground color to {color_name}")
441
442def set_lighting(lighting_name):
443  """Set lighting conditions"""
444  if not INSIDE_BLENDER:
445    return
446  if lighting_name not in LIGHTING_PRESETS:
447    return
448  
449  preset = LIGHTING_PRESETS[lighting_name]
450  
451  lamp_names = ['Lamp_Key', 'Lamp_Fill', 'Lamp_Back']
452  intensity_keys = ['key', 'fill', 'back']
453  
454  for lamp_name, int_key in zip(lamp_names, intensity_keys):
455    if lamp_name in bpy.data.objects:
456      lamp_obj = bpy.data.objects[lamp_name]
457      if lamp_obj.data and hasattr(lamp_obj.data, 'energy'):
458        base_energy = lamp_obj.data.energy
459        lamp_obj.data.energy = base_energy * preset.get(int_key, 1.0)
460        
461        if 'color' in preset and hasattr(lamp_obj.data, 'color'):
462          lamp_obj.data.color = preset['color']
463  
464  print(f"Set lighting to {lighting_name}")
465
466def render_from_json(args):
467  if not INSIDE_BLENDER:
468    print("ERROR: render_from_json must be run inside Blender")
469    return
470  
471  output_dir = os.path.dirname(args.output_image) if args.output_image else '.'
472  if output_dir and not os.path.exists(output_dir):
473    os.makedirs(output_dir)
474  
475  with open(args.scene_file, 'r') as f:
476    scene_struct = json.load(f)
477  
478  num_objects = len(scene_struct.get('objects', []))
479  print(f"Scene has {num_objects} objects")
480  
481  base_scene_path = os.path.abspath(args.base_scene_blendfile)
482  bpy.ops.wm.open_mainfile(filepath=base_scene_path)
483  
484  try:
485    load_materials(args.material_dir)
486  except Exception as e:
487    print(f"Warning: Could not load materials: {e}")
488
489  background_color = scene_struct.get('background_color', None)
490  if background_color:
491    set_background_color(background_color)
492    set_ground_color(background_color)
493  
494  lighting = scene_struct.get('lighting', None)
495  if lighting:
496    set_lighting(lighting)
497  
498  render_args = bpy.context.scene.render
499  render_args.engine = "CYCLES"
500  render_args.filepath = args.output_image
501  render_args.resolution_x = args.width
502  render_args.resolution_y = args.height
503  render_args.resolution_percentage = 100
504  
505  if args.use_gpu == 1:
506    try:
507      bpy.context.preferences.addons['cycles'].preferences.compute_device_type = 'CUDA'
508      bpy.context.scene.cycles.device = 'GPU'
509      print("[OK] GPU rendering enabled")
510    except Exception as e:
511      print(f"Warning: Could not enable GPU: {e}")
512  
513  bpy.context.scene.cycles.samples = args.render_num_samples
514  
515  filter_type = scene_struct.get('filter_type')
516  filter_strength = scene_struct.get('filter_strength', 1.0)
517  
518  if filter_type == 'fisheye':
519    camera = bpy.data.objects.get('Camera')
520    if camera and camera.data:
521      cam_data = camera.data
522      if cam_data.type == 'PERSP':
523        cam_data.lens = cam_data.lens * 0.7
524        print(f"[OK] Zoomed out camera for fisheye: lens={cam_data.lens:.1f}mm")
525  
526  with open(args.properties_json, 'r') as f:
527    properties = json.load(f)
528    color_name_to_rgba = {}
529    for name, rgb in properties['colors'].items():
530      rgba = [float(c) / 255.0 for c in rgb] + [1.0]
531      color_name_to_rgba[name] = rgba
532    size_mapping = properties['sizes']
533  
534  shape_semantic_to_file = properties['shapes']
535  material_semantic_to_file = properties['materials']
536  
537  blender_objects = []
538  print("Adding objects to scene...")
539  for i, obj_info in enumerate(scene_struct.get('objects', [])):
540    x, y, z = obj_info['3d_coords']
541    r = size_mapping[obj_info['size']]
542    semantic_shape = obj_info['shape']
543    
544    if semantic_shape == 'cube':
545      r /= math.sqrt(2)
546    
547    if semantic_shape not in shape_semantic_to_file:
548      print(f"ERROR: Shape '{semantic_shape}' not found")
549      continue
550    
551    shape_file_name = shape_semantic_to_file[semantic_shape]
552    
553    try:
554      add_object(args.shape_dir, shape_file_name, r, (x, y), theta=obj_info['rotation'])
555    except Exception as e:
556      print(f"Error adding object {i}: {e}")
557      continue
558    if INSIDE_BLENDER and bpy.context.object:
559      blender_objects.append(bpy.context.object)
560    
561    rgba = color_name_to_rgba[obj_info['color']]
562    semantic_material = obj_info['material']
563    
564    if semantic_material not in material_semantic_to_file:
565      print(f"ERROR: Material '{semantic_material}' not found")
566      continue
567    
568    mat_file_name = material_semantic_to_file[semantic_material]
569    
570    try:
571      add_material(mat_file_name, Color=rgba)
572    except Exception as e:
573      print(f"Warning: Could not add material: {e}")
574  
575  if blender_objects:
576    cf_meta = scene_struct.get('cf_metadata') or {}
577    cf_type = cf_meta.get('cf_type', '')
578
579    visibility_info = None
580    if INSIDE_BLENDER and Image is not None:
581      try:
582        visibility_info = compute_visibility_fractions(blender_objects)
583      except Exception as e:
584        print(f"Warning: compute_visibility_fractions failed during render: {e}")
585        visibility_info = None
586
587    all_visible = True
588    fail_reason = 'unknown visibility failure'
589
590    if visibility_info is not None:
591      ratios, scene_counts, full_counts = visibility_info
592
593      if cf_type == 'occlusion_change':
594        too_hidden = [
595          (i, r) for i, r in enumerate(ratios)
596          if full_counts[i] > 0 and r < CF_OCCLUSION_HARD_MIN_FRACTION
597        ]
598        band_objects = [
599          (i, r) for i, r in enumerate(ratios)
600          if full_counts[i] > 0 and CF_OCCLUSION_MIN_VISIBILITY_FRACTION <= r <= CF_OCCLUSION_MAX_VISIBILITY_FRACTION
601        ]
602
603        if too_hidden:
604          all_visible = False
605          min_r = min(r for (_, r) in too_hidden)
606          fail_reason = (f'at least one object is too occluded in occlusion_change CF; '
607                         f'min visibility fraction={min_r:.3f} '
608                         f'(required >= {CF_OCCLUSION_HARD_MIN_FRACTION})')
609        elif not band_objects:
610          all_visible = False
611          fail_reason = (f'no object falls into required occlusion band '
612                         f'[{CF_OCCLUSION_MIN_VISIBILITY_FRACTION}, '
613                         f'{CF_OCCLUSION_MAX_VISIBILITY_FRACTION}]')
614        else:
615          all_visible = True
616
617      else:
618        too_occluded = [
619          (i, r) for i, r in enumerate(ratios)
620          if full_counts[i] > 0 and r < BASE_MIN_VISIBILITY_FRACTION
621        ]
622        if too_occluded:
623          all_visible = False
624          min_r = min(r for (_, r) in too_occluded)
625          fail_reason = (f'at least one object is too occluded in base scene; '
626                         f'min visibility fraction={min_r:.3f} '
627                         f'(required >= {BASE_MIN_VISIBILITY_FRACTION})')
628        else:
629          all_visible = True
630
631    else:
632      # Fallback to legacy absolute pixel-based visibility when we cannot
633      # compute per-object relative visibility (e.g., PIL not available).
634      w = getattr(args, 'width', 320)
635      h = getattr(args, 'height', 240)
636      if cf_type == 'occlusion_change':
637        min_pixels = min_visible_pixels(w, h, MIN_VISIBLE_FRACTION_PARTIAL_OCCLUSION, MIN_PIXELS_FLOOR)
638      else:
639        base = min_visible_pixels(w, h, MIN_VISIBLE_FRACTION, MIN_PIXELS_FLOOR)
640        min_pixels = max(getattr(args, 'min_pixels_per_object', MIN_PIXELS_FLOOR), base)
641      all_visible = check_visibility(blender_objects, min_pixels)
642      if not all_visible:
643        fail_reason = 'at least one object has too few visible pixels'
644
645    if not all_visible:
646      print(f'Visibility check failed: {fail_reason}')
647      for obj in blender_objects:
648        try:
649          delete_object(obj)
650        except Exception:
651          pass
652      sys.exit(1)
653  
654  filter_type = scene_struct.get('filter_type')
655  filter_strength = scene_struct.get('filter_strength', 1.0)
656  
657  print(f"Rendering to {args.output_image}...")
658  
659  try:
660    bpy.ops.render.render(write_still=True)
661    print("[OK] Rendering complete!")
662  except Exception as e:
663    print(f"Error during rendering: {e}")
664    sys.exit(1)
665  
666  post_filter_type = scene_struct.get('filter_type')
667  if post_filter_type and post_filter_type != 'fisheye':
668    if Image is None:
669      print(f"Warning: PIL not available, cannot apply post-filter {post_filter_type}")
670    elif not os.path.exists(args.output_image):
671      print(f"Warning: Output image does not exist: {args.output_image}")
672    else:
673      try:
674        post_filter_strength = scene_struct.get('filter_strength', 1.0)
675        apply_filter_to_image(args.output_image, post_filter_type, post_filter_strength)
676      except Exception as e:
677        import traceback
678        print(f"Warning: Failed to apply post-filter {post_filter_type}: {e}")
679        traceback.print_exc()
680
681def main(args):
682  if args.scene_file:
683    render_from_json(args)
684    return
685  
686  num_digits = 6
687  prefix = '%s_%s_' % (args.filename_prefix, args.split)
688  img_template = '%s%%0%dd.png' % (prefix, num_digits)
689  scene_template = '%s%%0%dd.json' % (prefix, num_digits)
690  blend_template = '%s%%0%dd.blend' % (prefix, num_digits)
691  img_template = os.path.join(args.output_image_dir, img_template)
692  scene_template = os.path.join(args.output_scene_dir, scene_template)
693  blend_template = os.path.join(args.output_blend_dir, blend_template)
694
695  if not os.path.isdir(args.output_image_dir):
696    os.makedirs(args.output_image_dir)
697  if not os.path.isdir(args.output_scene_dir):
698    os.makedirs(args.output_scene_dir)
699  if args.save_blendfiles == 1 and not os.path.isdir(args.output_blend_dir):
700    os.makedirs(args.output_blend_dir)
701  
702  all_scene_paths = []
703  for i in range(args.num_images):
704    img_path = img_template % (i + args.start_idx)
705    scene_path = scene_template % (i + args.start_idx)
706    all_scene_paths.append(scene_path)
707    blend_path = None
708    if args.save_blendfiles == 1:
709      blend_path = blend_template % (i + args.start_idx)
710    num_objects = random.randint(args.min_objects, args.max_objects)
711    render_scene(args,
712      num_objects=num_objects,
713      output_index=(i + args.start_idx),
714      output_split=args.split,
715      output_image=img_path,
716      output_scene=scene_path,
717      output_blendfile=blend_path,
718    )
719
720  all_scenes = []
721  for scene_path in all_scene_paths:
722    with open(scene_path, 'r') as f:
723      all_scenes.append(json.load(f))
724  output = {
725    'info': {
726      'date': args.date,
727      'version': args.version,
728      'split': args.split,
729      'license': args.license,
730    },
731    'scenes': all_scenes
732  }
733  if args.output_scene_file:
734    output_dir = os.path.dirname(os.path.abspath(args.output_scene_file))
735    if output_dir:
736      os.makedirs(output_dir, exist_ok=True)
737    with open(args.output_scene_file, 'w') as f:
738      json.dump(output, f)
739
740
741
742def render_scene(args,
743    num_objects=5,
744    output_index=0,
745    output_split='none',
746    output_image='render.png',
747    output_scene='render_json',
748    output_blendfile=None,
749  ):
750
751  base_scene_path = os.path.abspath(args.base_scene_blendfile)
752  bpy.ops.wm.open_mainfile(filepath=base_scene_path)
753  load_materials(args.material_dir)
754
755  render_args = bpy.context.scene.render
756  render_args.engine = "CYCLES"
757  render_args.filepath = output_image
758  render_args.resolution_x = args.width
759  render_args.resolution_y = args.height
760  render_args.resolution_percentage = 100
761  if args.use_gpu == 1:
762    bpy.context.preferences.addons['cycles'].preferences.compute_device_type = 'CUDA'
763    bpy.context.preferences.addons['cycles'].preferences.get_devices()
764    for device in bpy.context.preferences.addons['cycles'].preferences.devices:
765      device.use = True
766
767  bpy.data.worlds['World'].cycles.sample_as_light = True
768  bpy.context.scene.cycles.blur_glossy = 2.0
769  bpy.context.scene.cycles.samples = args.render_num_samples
770  bpy.context.scene.cycles.transparent_min_bounces = args.render_min_bounces
771  bpy.context.scene.cycles.transparent_max_bounces = args.render_max_bounces
772  if args.use_gpu == 1:
773    bpy.context.scene.cycles.device = 'GPU'
774
775  scene_struct = {
776      'split': output_split,
777      'image_index': output_index,
778      'image_filename': os.path.basename(output_image),
779      'objects': [],
780      'directions': {},
781  }
782
783  bpy.ops.mesh.primitive_plane_add(size=10, location=(0, 0, 0))
784  plane = bpy.context.object
785
786  def rand(L):
787    return 2.0 * L * (random.random() - 0.5)
788
789  if args.camera_jitter > 0:
790    for i in range(3):
791      bpy.data.objects['Camera'].location[i] += rand(args.camera_jitter)
792
793  camera = bpy.data.objects['Camera']
794  plane_normal = plane.data.vertices[0].normal
795  cam_behind = camera.matrix_world.to_quaternion() @ Vector((0, 0, -1))
796  cam_left = camera.matrix_world.to_quaternion() @ Vector((-1, 0, 0))
797  cam_up = camera.matrix_world.to_quaternion() @ Vector((0, 1, 0))
798  plane_behind = (cam_behind - cam_behind.project(plane_normal)).normalized()
799  plane_left = (cam_left - cam_left.project(plane_normal)).normalized()
800  plane_up = cam_up.project(plane_normal).normalized()
801
802  delete_object(plane)
803
804  scene_struct['directions']['behind'] = tuple(plane_behind)
805  scene_struct['directions']['front'] = tuple(-plane_behind)
806  scene_struct['directions']['left'] = tuple(plane_left)
807  scene_struct['directions']['right'] = tuple(-plane_left)
808  scene_struct['directions']['above'] = tuple(plane_up)
809  scene_struct['directions']['below'] = tuple(-plane_up)
810
811  if args.key_light_jitter > 0:
812    for i in range(3):
813      bpy.data.objects['Lamp_Key'].location[i] += rand(args.key_light_jitter)
814  if args.back_light_jitter > 0:
815    for i in range(3):
816      bpy.data.objects['Lamp_Back'].location[i] += rand(args.back_light_jitter)
817  if args.fill_light_jitter > 0:
818    for i in range(3):
819      bpy.data.objects['Lamp_Fill'].location[i] += rand(args.fill_light_jitter)
820
821  objects, blender_objects = add_random_objects(scene_struct, num_objects, args, camera)
822  scene_struct['objects'] = objects
823  scene_struct['relationships'] = compute_all_relationships(scene_struct)
824  while True:
825    try:
826      bpy.ops.render.render(write_still=True)
827      break
828    except Exception as e:
829      print(e)
830
831  with open(output_scene, 'w') as f:
832    json.dump(scene_struct, f, indent=2)
833
834  if output_blendfile is not None:
835    bpy.ops.wm.save_as_mainfile(filepath=output_blendfile)
836
837
838def add_random_objects(scene_struct, num_objects, args, camera, max_scene_attempts=10):
839  scene_attempt = 0
840  while scene_attempt < max_scene_attempts:
841    scene_attempt += 1
842
843    with open(args.properties_json, 'r') as f:
844      properties = json.load(f)
845      color_name_to_rgba = {}
846      for name, rgb in properties['colors'].items():
847        rgba = [float(c) / 255.0 for c in rgb] + [1.0]
848        color_name_to_rgba[name] = rgba
849      material_mapping = [(v, k) for k, v in properties['materials'].items()]
850      object_mapping = [(v, k) for k, v in properties['shapes'].items()]
851      size_mapping = list(properties['sizes'].items())
852
853    shape_color_combos = None
854    if args.shape_color_combos_json is not None:
855      with open(args.shape_color_combos_json, 'r') as f:
856        shape_color_combos = list(json.load(f).items())
857
858    positions = []
859    objects = []
860    blender_objects = []
861    for i in range(num_objects):
862      size_name, r = random.choice(size_mapping)
863
864      num_tries = 0
865      while True:
866        num_tries += 1
867        if num_tries > args.max_retries:
868          for obj in blender_objects:
869            delete_object(obj)
870          break
871        x = random.uniform(-3, 3)
872        y = random.uniform(-3, 3)
873        dists_good = True
874        margins_good = True
875        for (xx, yy, rr) in positions:
876          dx, dy = x - xx, y - yy
877          dist = math.sqrt(dx * dx + dy * dy)
878          if dist - r - rr < args.min_dist:
879            dists_good = False
880            break
881          for direction_name in ['left', 'right', 'front', 'behind']:
882            direction_vec = scene_struct['directions'][direction_name]
883            assert direction_vec[2] == 0
884            margin = dx * direction_vec[0] + dy * direction_vec[1]
885            if 0 < margin < args.margin:
886              print(margin, args.margin, direction_name)
887              print('BROKEN MARGIN!')
888              margins_good = False
889              break
890          if not margins_good:
891            break
892
893        if dists_good and margins_good:
894          break
895      
896      if num_tries > args.max_retries:
897        break
898
899      if shape_color_combos is None:
900        obj_name, obj_name_out = random.choice(object_mapping)
901        color_name, rgba = random.choice(list(color_name_to_rgba.items()))
902      else:
903        obj_name_out, color_choices = random.choice(shape_color_combos)
904        color_name = random.choice(color_choices)
905        obj_name = [k for k, v in object_mapping if v == obj_name_out][0]
906        rgba = color_name_to_rgba[color_name]
907
908      if obj_name == 'Cube':
909        r /= math.sqrt(2)
910
911      theta = 360.0 * random.random()
912      add_object(args.shape_dir, obj_name, r, (x, y), theta=theta)
913      obj = bpy.context.object
914      blender_objects.append(obj)
915      positions.append((x, y, r))
916
917      mat_name, mat_name_out = random.choice(material_mapping)
918      add_material(mat_name, Color=rgba)
919
920      pixel_coords = get_camera_coords(camera, obj.location)
921      objects.append({
922        'shape': obj_name_out,
923        'size': size_name,
924        'material': mat_name_out,
925        '3d_coords': tuple(obj.location),
926        'rotation': theta,
927        'pixel_coords': pixel_coords,
928        'color': color_name,
929      })
930
931    if len(objects) < num_objects:
932      continue
933
934    visibility_info = None
935    if INSIDE_BLENDER and Image is not None:
936      try:
937        visibility_info = compute_visibility_fractions(blender_objects)
938      except Exception as e:
939        print(f"Warning: compute_visibility_fractions failed during scene generation: {e}")
940        visibility_info = None
941
942    all_visible = True
943    if visibility_info is not None:
944      ratios, scene_counts, full_counts = visibility_info
945      min_ratio = min((r for r in ratios if full_counts[ratios.index(r)] > 0), default=1.0)
946      all_visible = all(
947        (full_counts[i] == 0) or (ratios[i] >= BASE_MIN_VISIBILITY_FRACTION)
948        for i in range(len(ratios))
949      )
950      if not all_visible:
951        print(f'Some objects are too occluded in generated scene; '
952              f'min visibility fraction={min_ratio:.3f} (required >= {BASE_MIN_VISIBILITY_FRACTION})')
953    else:
954      # Fallback to legacy absolute pixel-based visibility when PIL or Blender context is unavailable.
955      min_pixels = max(args.min_pixels_per_object, min_visible_pixels(args.width, args.height))
956      all_visible = check_visibility(blender_objects, min_pixels)
957
958    if not all_visible:
959      print('Some objects are occluded; replacing objects')
960      for obj in blender_objects:
961        delete_object(obj)
962      continue
963
964    return objects, blender_objects
965
966  raise RuntimeError(f"Failed to generate a valid scene after {max_scene_attempts} attempts")
967
968
969def compute_all_relationships(scene_struct, eps=0.2):
970  """
971  Computes relationships between all pairs of objects in the scene.
972  
973  Returns a dictionary mapping string relationship names to lists of lists of
974  integers, where output[rel][i] gives a list of object indices that have the
975  relationship rel with object i. For example if j is in output['left'][i] then
976  object j is left of object j.
977  """
978  all_relationships = {}
979  for name, direction_vec in scene_struct['directions'].items():
980    if name == 'above' or name == 'below': continue
981    all_relationships[name] = []
982    for i, obj1 in enumerate(scene_struct['objects']):
983      coords1 = obj1['3d_coords']
984      related = set()
985      for j, obj2 in enumerate(scene_struct['objects']):
986        if obj1 == obj2: continue
987        coords2 = obj2['3d_coords']
988        diff = [coords2[k] - coords1[k] for k in [0, 1, 2]]
989        dot = sum(diff[k] * direction_vec[k] for k in [0, 1, 2])
990        if dot > eps:
991          related.add(j)
992      all_relationships[name].append(sorted(list(related)))
993  return all_relationships
994
995
996def compute_visibility_fractions(blender_objects):
997  if not INSIDE_BLENDER or not blender_objects:
998    return None
999  if Image is None:
1000    return None
1001
1002  # First pass: all objects together (occluded counts).
1003  fd, path = tempfile.mkstemp(suffix='.png')
1004  os.close(fd)
1005  try:
1006    colors_list = render_shadeless(blender_objects, path, use_distinct_colors=True)
1007    img = Image.open(path).convert('RGB')
1008    w, h = img.size
1009    pix = img.load()
1010    color_to_idx = {}
1011    for i, (r, g, b) in enumerate(colors_list):
1012      key = (round(r * 255), round(g * 255), round(b * 255))
1013      color_to_idx[key] = i
1014    scene_counts = [0] * len(blender_objects)
1015    for y in range(h):
1016      for x in range(w):
1017        key = (pix[x, y][0], pix[x, y][1], pix[x, y][2])
1018        if key in color_to_idx:
1019          scene_counts[color_to_idx[key]] += 1
1020  finally:
1021    try:
1022      os.remove(path)
1023    except Exception:
1024      pass
1025
1026  # Second pass: per-object "full area" with other objects hidden.
1027  full_counts = []
1028  original_hide_render = [obj.hide_render for obj in blender_objects]
1029  try:
1030    for idx, obj in enumerate(blender_objects):
1031      # Hide all other objects, ensure this one is visible.
1032      for j, other in enumerate(blender_objects):
1033        if j == idx:
1034          other.hide_render = False
1035        else:
1036          other.hide_render = True
1037
1038      fd_i, path_i = tempfile.mkstemp(suffix='.png')
1039      os.close(fd_i)
1040      try:
1041        colors_list = render_shadeless([obj], path_i, use_distinct_colors=True)
1042        img = Image.open(path_i).convert('RGB')
1043        w, h = img.size
1044        pix = img.load()
1045        color_to_idx = {}
1046        for i, (r, g, b) in enumerate(colors_list):
1047          key = (round(r * 255), round(g * 255), round(b * 255))
1048          color_to_idx[key] = i
1049        count = 0
1050        for y in range(h):
1051          for x in range(w):
1052            key = (pix[x, y][0], pix[x, y][1], pix[x, y][2])
1053            if key in color_to_idx:
1054              count += 1
1055        full_counts.append(count)
1056      finally:
1057        try:
1058          os.remove(path_i)
1059        except Exception:
1060          pass
1061  finally:
1062    # Restore previous hide_render flags.
1063    for obj, prev in zip(blender_objects, original_hide_render):
1064      obj.hide_render = prev
1065
1066  visibility = []
1067  for scene_c, full_c in zip(scene_counts, full_counts):
1068    if full_c <= 0:
1069      visibility.append(0.0)
1070    else:
1071      visibility.append(float(scene_c) / float(full_c))
1072
1073  return visibility, scene_counts, full_counts
1074
1075
1076def check_visibility(blender_objects, min_pixels_per_object):
1077  """
1078  Legacy absolute pixel-count visibility check, kept as a fallback when
1079  relative per-object visibility cannot be computed.
1080  """
1081  if not INSIDE_BLENDER or not blender_objects:
1082    return True
1083  if Image is None:
1084    return True
1085  fd, path = tempfile.mkstemp(suffix='.png')
1086  os.close(fd)
1087  try:
1088    colors_list = render_shadeless(blender_objects, path, use_distinct_colors=True)
1089    img = Image.open(path).convert('RGB')
1090    w, h = img.size
1091    pix = img.load()
1092    color_to_idx = {}
1093    for i, (r, g, b) in enumerate(colors_list):
1094      key = (round(r * 255), round(g * 255), round(b * 255))
1095      color_to_idx[key] = i
1096    counts = [0] * len(blender_objects)
1097    for y in range(h):
1098      for x in range(w):
1099        key = (pix[x, y][0], pix[x, y][1], pix[x, y][2])
1100        if key in color_to_idx:
1101          counts[color_to_idx[key]] += 1
1102    all_visible = all(c >= min_pixels_per_object for c in counts)
1103    return all_visible
1104  finally:
1105    try:
1106      os.remove(path)
1107    except Exception:
1108      pass
1109
1110
1111def render_shadeless(blender_objects, path='flat.png', use_distinct_colors=False):
1112  """
1113  Render a version of the scene with shading disabled and unique materials
1114  assigned to all objects. The image itself is written to path. This is used to ensure
1115  that all objects will be visible in the final rendered scene (when check_visibility is enabled).
1116  Returns a list of (r,g,b) colors in object order (for visibility counting when use_distinct_colors=True).
1117  """
1118  render_args = bpy.context.scene.render
1119
1120  old_filepath = render_args.filepath
1121  old_engine = render_args.engine
1122
1123  render_args.filepath = path
1124  render_args.engine = 'BLENDER_EEVEE_NEXT'
1125  
1126  view_layer = bpy.context.scene.view_layers[0]
1127  old_use_pass_combined = view_layer.use_pass_combined
1128  
1129  for obj_name in ['Lamp_Key', 'Lamp_Fill', 'Lamp_Back', 'Ground']:
1130    if obj_name in bpy.data.objects:
1131      obj = bpy.data.objects[obj_name]
1132      obj.hide_render = True
1133
1134  n = len(blender_objects)
1135  object_colors = [] if use_distinct_colors else set()
1136  old_materials = []
1137  for i, obj in enumerate(blender_objects):
1138    if len(obj.data.materials) > 0:
1139      old_materials.append(obj.data.materials[0])
1140    else:
1141      old_materials.append(None)
1142    
1143    mat = bpy.data.materials.new(name='Material_%d' % i)
1144    mat.use_nodes = True
1145    nodes = mat.node_tree.nodes
1146    nodes.clear()
1147    
1148    node_emission = nodes.new(type='ShaderNodeEmission')
1149    node_output = nodes.new(type='ShaderNodeOutputMaterial')
1150    
1151    if use_distinct_colors:
1152      r = (i + 1) / (n + 1)
1153      g, b = 0.5, 0.5
1154      object_colors.append((r, g, b))
1155    else:
1156      while True:
1157        r, g, b = [random.random() for _ in range(3)]
1158        if (r, g, b) not in object_colors:
1159          break
1160      object_colors.add((r, g, b))
1161    
1162    node_emission.inputs['Color'].default_value = (r, g, b, 1.0)
1163    mat.node_tree.links.new(node_emission.outputs['Emission'], node_output.inputs['Surface'])
1164    
1165    if len(obj.data.materials) > 0:
1166      obj.data.materials[0] = mat
1167    else:
1168      obj.data.materials.append(mat)
1169
1170  bpy.ops.render.render(write_still=True)
1171
1172  for mat, obj in zip(old_materials, blender_objects):
1173    if mat is not None:
1174      obj.data.materials[0] = mat
1175    elif len(obj.data.materials) > 0:
1176      obj.data.materials.clear()
1177
1178  for obj_name in ['Lamp_Key', 'Lamp_Fill', 'Lamp_Back', 'Ground']:
1179    if obj_name in bpy.data.objects:
1180      obj = bpy.data.objects[obj_name]
1181      obj.hide_render = False
1182
1183  render_args.filepath = old_filepath
1184  render_args.engine = old_engine
1185
1186  return object_colors
1187
1188
1189if __name__ == '__main__':
1190  if INSIDE_BLENDER:
1191    argv = extract_args()
1192    args = parser.parse_args(argv)
1193    main(args)
1194  elif '--help' in sys.argv or '-h' in sys.argv:
1195    parser.print_help()
1196  else:
1197    print('This script is intended to be called from blender like this:')
1198    print()
1199    print('blender --background --python render_images.py -- [args]')
1200    print()

Showing the first 1,200 of 1205 lines. Download the file for the rest.