From 05ebd720fdaf0e19f001c7ffda060a9aca28835c Mon Sep 17 00:00:00 2001 From: ofekp Date: Thu, 8 Dec 2022 03:24:55 +0200 Subject: [PATCH] GeoCode initial commit --- .gitignore | 43 ++ __init__.py | 0 common/__init__.py | 0 common/bpy_util.py | 165 +++++++ common/domain.py | 9 + common/file_util.py | 50 ++ common/input_param_map.py | 170 +++++++ common/intersection_util.py | 161 +++++++ common/param_descriptors.py | 230 +++++++++ common/point_cloud_util.py | 16 + common/sampling_util.py | 75 +++ config/neptune_config_example.yml | 3 + data/__init__.py | 0 data/data_processing.py | 95 ++++ data/dataset_pc.py | 123 +++++ data/dataset_sketch.py | 119 +++++ data/dataset_util.py | 21 + dataset_generator/__init__.py | 0 dataset_generator/dataset_generator.py | 439 ++++++++++++++++++ .../recipe_files/recipe_chair.yml | 318 +++++++++++++ .../recipe_files/recipe_table.yml | 198 ++++++++ .../recipe_files/recipe_vase.yml | 208 +++++++++ .../shape_validators/__init__.py | 0 .../shape_validators/chair_validator.py | 41 ++ .../shape_validators/common_validations.py | 58 +++ .../shape_validator_factory.py | 18 + .../shape_validator_interface.py | 4 + .../shape_validators/table_validator.py | 18 + .../shape_validators/vase_validator.py | 20 + dataset_generator/sketch_generator.py | 164 +++++++ dataset_processing/__init__.py | 0 dataset_processing/prepare_coseg.py | 80 ++++ dataset_processing/save_obj.py | 77 +++ dataset_processing/simplified_mesh_dataset.py | 83 ++++ environment.yml | 29 ++ geocode/__init__.py | 0 geocode/barplot_util.py | 68 +++ geocode/calculator_accuracy.py | 52 +++ geocode/calculator_loss.py | 58 +++ geocode/calculator_util.py | 27 ++ geocode/geocode.py | 62 +++ geocode/geocode_model.py | 339 ++++++++++++++ geocode/geocode_test.py | 262 +++++++++++ geocode/geocode_train.py | 150 ++++++ geocode/geocode_util.py | 35 ++ resources/teaser.png | Bin 0 -> 131519 bytes scripts/__init__.py | 0 scripts/download_ds.py | 125 +++++ setup.py | 2 + stability_metric/__init__.py | 0 stability_metric/stability.py | 167 +++++++ stability_metric/stability_parallel.py | 102 ++++ stability_metric/stability_simulation.blend | Bin 0 -> 789216 bytes visualize_results/visualize.py | 183 ++++++++ 54 files changed, 4667 insertions(+) create mode 100644 .gitignore create mode 100644 __init__.py create mode 100644 common/__init__.py create mode 100644 common/bpy_util.py create mode 100644 common/domain.py create mode 100644 common/file_util.py create mode 100644 common/input_param_map.py create mode 100644 common/intersection_util.py create mode 100644 common/param_descriptors.py create mode 100644 common/point_cloud_util.py create mode 100644 common/sampling_util.py create mode 100644 config/neptune_config_example.yml create mode 100644 data/__init__.py create mode 100644 data/data_processing.py create mode 100644 data/dataset_pc.py create mode 100644 data/dataset_sketch.py create mode 100644 data/dataset_util.py create mode 100644 dataset_generator/__init__.py create mode 100644 dataset_generator/dataset_generator.py create mode 100644 dataset_generator/recipe_files/recipe_chair.yml create mode 100644 dataset_generator/recipe_files/recipe_table.yml create mode 100644 dataset_generator/recipe_files/recipe_vase.yml create mode 100644 dataset_generator/shape_validators/__init__.py create mode 100644 dataset_generator/shape_validators/chair_validator.py create mode 100644 dataset_generator/shape_validators/common_validations.py create mode 100644 dataset_generator/shape_validators/shape_validator_factory.py create mode 100644 dataset_generator/shape_validators/shape_validator_interface.py create mode 100644 dataset_generator/shape_validators/table_validator.py create mode 100644 dataset_generator/shape_validators/vase_validator.py create mode 100644 dataset_generator/sketch_generator.py create mode 100644 dataset_processing/__init__.py create mode 100644 dataset_processing/prepare_coseg.py create mode 100644 dataset_processing/save_obj.py create mode 100644 dataset_processing/simplified_mesh_dataset.py create mode 100644 environment.yml create mode 100644 geocode/__init__.py create mode 100644 geocode/barplot_util.py create mode 100644 geocode/calculator_accuracy.py create mode 100644 geocode/calculator_loss.py create mode 100644 geocode/calculator_util.py create mode 100644 geocode/geocode.py create mode 100644 geocode/geocode_model.py create mode 100644 geocode/geocode_test.py create mode 100644 geocode/geocode_train.py create mode 100644 geocode/geocode_util.py create mode 100644 resources/teaser.png create mode 100644 scripts/__init__.py create mode 100644 scripts/download_ds.py create mode 100644 setup.py create mode 100644 stability_metric/__init__.py create mode 100644 stability_metric/stability.py create mode 100644 stability_metric/stability_parallel.py create mode 100644 stability_metric/stability_simulation.blend create mode 100644 visualize_results/visualize.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3d1c133 --- /dev/null +++ b/.gitignore @@ -0,0 +1,43 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + + +.DS_Store +datasets/ +*.zip. +.idea/ +Models/ +Logs/ +cls/ +slurm-*.out +node_modules/ +neptune_config.yml +neptune_session.json +.neptune/ +.vscode/ +*.egg_inf +lightning_logs/ +stability_results.json +dataset_processing/model-converter-python/ diff --git a/__init__.py b/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/common/__init__.py b/common/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/common/bpy_util.py b/common/bpy_util.py new file mode 100644 index 0000000..c67e0ef --- /dev/null +++ b/common/bpy_util.py @@ -0,0 +1,165 @@ +import bpy +import math +from mathutils import Vector +from typing import Union +from pathlib import Path + + +def save_obj(target_obj_file_path: Union[Path, str], additional_objs_to_save=None, simplification_ratio=None): + """ + save the object and returns a mesh duplicate version of it + """ + obj = select_shape() + refresh_obj_in_viewport(obj) + dup_obj = copy(obj) + # set active + bpy.ops.object.select_all(action='DESELECT') + dup_obj.select_set(True) + bpy.context.view_layer.objects.active = dup_obj + # apply the modifier to turn the geometry node to a mesh + bpy.ops.object.modifier_apply(modifier="GeometryNodes") + if simplification_ratio and simplification_ratio < 1.0: + bpy.ops.object.modifier_add(type='DECIMATE') + dup_obj.modifiers["Decimate"].decimate_type = 'COLLAPSE' + dup_obj.modifiers["Decimate"].ratio = simplification_ratio + bpy.ops.object.modifier_apply(modifier="Decimate") + assert dup_obj.type == 'MESH' + bpy.ops.object.transform_apply(location=True, rotation=True, scale=True) + # set origin to center of bounding box + bpy.ops.object.origin_set(type='ORIGIN_GEOMETRY', center='BOUNDS') + dup_obj.location.x = dup_obj.location.y = dup_obj.location.z = 0 + normalize_scale(dup_obj) + if additional_objs_to_save: + for additional_obj in additional_objs_to_save: + additional_obj.select_set(True) + # save + bpy.ops.export_scene.obj(filepath=str(target_obj_file_path), use_selection=True, use_materials=False, use_triangles=True) + return dup_obj + + +def get_geometric_nodes_modifier(obj): + # loop through all modifiers of the given object + gnodes_mod = None + for modifier in obj.modifiers: + # check if current modifier is the geometry nodes modifier + if modifier.type == "NODES": + gnodes_mod = modifier + break + return gnodes_mod + + +def normalize_scale(obj): + obj.select_set(True) + bpy.context.view_layer.objects.active = obj + bpy.ops.object.transform_apply(location=True, rotation=True, scale=True) + # set origin to the center of the bounding box + bpy.ops.object.origin_set(type='ORIGIN_GEOMETRY', center='BOUNDS') + + obj.location.x = 0 + obj.location.y = 0 + obj.location.z = 0 + + bpy.ops.object.transform_apply(location=True, rotation=True, scale=True) + max_vert_dist = math.sqrt(max([v.co.dot(v.co) for v in obj.data.vertices])) + + for v in obj.data.vertices: + v.co /= max_vert_dist + + bpy.ops.object.transform_apply(location=True, rotation=True, scale=True) + + # verify that the shape is normalized + # max_vert_dist = math.sqrt(max([v.co.dot(v.co) for v in obj.data.vertices])) + # assert abs(max_vert_dist - 1.0) < 0.01 + + +def setup_lights(): + """ + setup lights for rendering + used for visualization of 3D objects as images + """ + scene = bpy.context.scene + # light 1 + light_data_1 = bpy.data.lights.new(name="light_data_1", type='POINT') + light_data_1.energy = 300 + light_object_1 = bpy.data.objects.new(name="Light_1", object_data=light_data_1) + light_object_1.location = Vector((10, -10, 10)) + scene.collection.objects.link(light_object_1) + # light 2 + light_data_2 = bpy.data.lights.new(name="light_data_2", type='POINT') + light_data_2.energy = 300 + light_object_2 = bpy.data.objects.new(name="Light_2", object_data=light_data_2) + light_object_2.location = Vector((-10, -10, 10)) + scene.collection.objects.link(light_object_2) + # light 3 + light_data_3 = bpy.data.lights.new(name="light_data_3", type='POINT') + light_data_3.energy = 300 + light_object_3 = bpy.data.objects.new(name="Light_3", object_data=light_data_3) + light_object_3.location = Vector((10, 0, 10)) + scene.collection.objects.link(light_object_3) + + +def look_at(obj_camera, point): + """ + orient the given camera with a fixed position to loot at a given point in space + """ + loc_camera = obj_camera.matrix_world.to_translation() + direction = point - loc_camera + # point the cameras '-Z' and use its 'Y' as up + rot_quat = direction.to_track_quat('-Z', 'Y') + obj_camera.rotation_euler = rot_quat.to_euler() + + +def clean_scene(start_with_strings=["Camera", "procedural", "Light"]): + """ + delete all object of which the name's prefix is matching any of the given strings + """ + scene = bpy.context.scene + bpy.ops.object.select_all(action='DESELECT') + for obj in scene.objects: + if any([obj.name.startswith(starts_with_string) for starts_with_string in start_with_strings]): + # select the object + if obj.visible_get(): + obj.select_set(True) + bpy.ops.object.delete() + + +def del_obj(obj): + bpy.ops.object.select_all(action='DESELECT') + obj.select_set(True) + bpy.ops.object.delete() + + +def refresh_obj_in_viewport(obj): + # the following two line cause the object to update according to the new geometric nodes input + obj.show_bounds = not obj.show_bounds + obj.show_bounds = not obj.show_bounds + + +def select_objs(*objs): + bpy.ops.object.select_all(action='DESELECT') + for i, obj in enumerate(objs): + if i == 0: + bpy.context.view_layer.objects.active = obj + obj.select_set(True) + + +def select_obj(obj): + select_objs(obj) + + +def select_shape(): + """ + select the procedural shape in the blend file + note that in all our domains, the procedural shape is named "procedural shape" within the blend file + """ + obj = bpy.data.objects["procedural shape"] + select_obj(obj) + return obj + + +def copy(obj): + dup_obj = obj.copy() + dup_obj.data = obj.data.copy() + dup_obj.animation_data_clear() + bpy.context.collection.objects.link(dup_obj) + return dup_obj diff --git a/common/domain.py b/common/domain.py new file mode 100644 index 0000000..ec2dc22 --- /dev/null +++ b/common/domain.py @@ -0,0 +1,9 @@ +from enum import Enum + +class Domain(Enum): + chair = 'chair' + vase = 'vase' + table = 'table' + + def __str__(self): + return self.value diff --git a/common/file_util.py b/common/file_util.py new file mode 100644 index 0000000..73a3d67 --- /dev/null +++ b/common/file_util.py @@ -0,0 +1,50 @@ +import yaml +import hashlib +import numpy as np +from pathlib import Path +from typing import Union + + +def save_yml(yml_obj, target_yml_file_path): + with open(target_yml_file_path, 'w') as target_yml_file: + yaml.dump(yml_obj, target_yml_file, sort_keys=False, width=1000) + + +def get_source_recipe_file_path(domain): + """ + get the path to the recipe file path that is found in the source code under the directory "recipe_files" + """ + return Path(__file__).parent.joinpath('..', 'dataset_generator', 'recipe_files', f'recipe_{domain}.yml').resolve() + + +def hash_file_name(file_name): + return int(hashlib.sha1(file_name.encode("utf-8")).hexdigest(), 16) % (10 ** 8) + + +def get_recipe_yml_obj(recipe_file_path: Union[str, Path]): + with open(recipe_file_path, 'r') as recipe_file: + recipe_yml_obj = yaml.load(recipe_file, Loader=yaml.FullLoader) + return recipe_yml_obj + + +def load_obj(file: str): + vs, faces = [], [] + f = open(file) + for line in f: + line = line.strip() + split_line = line.split() + if not split_line: + continue + elif split_line[0] == 'v': + vs.append([float(v) for v in split_line[1:4]]) + elif split_line[0] == 'f': + face_vertex_ids = [int(c.split('/')[0]) for c in split_line[1:]] + assert len(face_vertex_ids) == 3 + face_vertex_ids = [(ind - 1) if (ind >= 0) else (len(vs) + ind) + for ind in face_vertex_ids] + faces.append(face_vertex_ids) + f.close() + vs = np.asarray(vs) + faces = np.asarray(faces, dtype=np.int64) + assert np.logical_and(faces >= 0, faces < len(vs)).all() + return vs, faces diff --git a/common/input_param_map.py b/common/input_param_map.py new file mode 100644 index 0000000..1673b49 --- /dev/null +++ b/common/input_param_map.py @@ -0,0 +1,170 @@ +import yaml +import random +import traceback +import numpy as np +from typing import List, Optional +from dataclasses import dataclass +from bpy.types import NodeInputs, Modifier +from common.bpy_util import select_shape, refresh_obj_in_viewport, get_geometric_nodes_modifier + + +@dataclass +class InputParam: + gnodes_mod: Modifier + input: NodeInputs + axis: Optional[str] # None indicates that this is not a vector + possible_values: List + + def assign_random_value(self): + self.assign_value(random.choice(self.possible_values)) + + def assign_value(self, val): + assert val in self.possible_values + input_type = self.input.bl_label + identifier = self.input.identifier + if input_type == "Float": + self.gnodes_mod[identifier] = val + + if input_type == "Integer": + self.gnodes_mod[identifier] = int(val) + + if input_type == "Boolean": + self.gnodes_mod[identifier] = int(val) + + if input_type == "Vector": + axis_idx = ['x', 'y', 'z'].index(self.axis) + self.gnodes_mod[identifier][axis_idx] = val + + def get_value(self): + identifier = self.input.identifier + if self.axis: + axis_idx = ['x', 'y', 'z'].index(self.axis) + return self.gnodes_mod[identifier][axis_idx] + return self.gnodes_mod[identifier] + + def get_name_for_file(self): + res = str(self.input.name) + ("" if not self.axis else "_" + self.axis) + return res.replace(" ", "_") + + +def get_input_values(input, yml_gen_rule): + min_value = None + max_value = None + if input.bl_label != 'Boolean': + min_value = input.min_value + max_value = input.max_value + # override min and max with requested values from recipe yml file + if 'min' in yml_gen_rule: + requested_min_value = yml_gen_rule['min'] + if min_value and requested_min_value < min_value: + if abs(min_value - requested_min_value) > 1e-6: + raise Exception( + f'Requested a min value of [{requested_min_value}] for parameter [{input.name}], but min allowed is [{min_value}]') + # otherwise min_value should remain input.min_value + else: + min_value = requested_min_value + if 'max' in yml_gen_rule: + requested_max_value = yml_gen_rule['max'] + if max_value and requested_max_value > max_value: + if abs(max_value - requested_max_value) > 1e-6: + raise Exception( + f'Requested a max value of [{requested_max_value}] for parameter [{input.name}], but max allowed is [{max_value}]') + # otherwise max_value should remain input.max_value + max_value = requested_max_value + step = 1 if 'samples' not in yml_gen_rule else calculate_step(min_value, max_value, yml_gen_rule['samples']) + res = np.arange(min_value, max_value + 1e-6, step) + + # convert to integers if needed + if input.bl_label in ['Boolean', 'Integer']: + res = list(res.astype(int)) + else: + res = [round(x, 4) for x in list(res)] + + return res + + +def calculate_step(min_value, max_value, samples): + return (max_value - min_value) / (samples - 1) + + +def get_input_param_map(gnodes_mod, yml): + input_params_map = {} + # loops through all the inputs in the geometric node group + for param_name in yml['dataset_generation']: + if param_name not in gnodes_mod.node_group.inputs: + raise Exception(f"Parameter named [{param_name}] was not found in geometry nodes input group.") + for input in gnodes_mod.node_group.inputs: + param_name = str(input.name) + + # we only change inputs that are explicitly noted in the yaml object + if param_name in yml['dataset_generation']: + param_gen_rule = yml['dataset_generation'][param_name] + if 'x' in param_gen_rule or 'y' in param_gen_rule or 'z' in param_gen_rule: + # vector handling + for idx, axis in enumerate(['x', 'y', 'z']): + if not axis in param_gen_rule: + continue + curr_param_values = get_input_values(input, param_gen_rule[axis]) + input_params_map[f"{param_name} {axis}"] = InputParam(gnodes_mod, input, axis, curr_param_values) + else: + curr_param_values = get_input_values(input, param_gen_rule) + input_params_map[param_name] = InputParam(gnodes_mod, input, None, curr_param_values) + return input_params_map + + +def yml_to_shape(shape_yml_obj, input_params_map, ignore_sanity_check=False): + try: + # select the object in blender + obj = select_shape() + # get the geometric nodes modifier fo the object + gnodes_mod = get_geometric_nodes_modifier(obj) + + # loops through all the inputs in the geometric node group + for input in gnodes_mod.node_group.inputs: + param_name = str(input.name) + if param_name not in shape_yml_obj: + continue + param_val = shape_yml_obj[param_name] + if hasattr(param_val, '__iter__'): + # vector handling + for axis_idx, axis in enumerate(['x', 'y', 'z']): + val = param_val[axis] + val = round(val, 4) + param_name_with_axis = f'{param_name} {axis}' + gnodes_mod[input.identifier][axis_idx] = val if abs(val + 1.0) > 0.1 else input_params_map[param_name_with_axis].possible_values[0].item() + assert gnodes_mod[input.identifier][axis_idx] >= 0.0 + else: + param_val = round(param_val, 4) + if not ignore_sanity_check: + err_msg = f'param_name [{param_name}] param_val [{param_val}] possible_values {input_params_map[param_name].possible_values}' + assert param_val == -1 or (param_val in input_params_map[param_name].possible_values), err_msg + gnodes_mod[input.identifier] = param_val if (abs(param_val + 1.0) > 0.1) else (input_params_map[param_name].possible_values[0].item()) + # we assume that all input values are non-negative + assert gnodes_mod[input.identifier] >= 0.0 + + refresh_obj_in_viewport(obj) + except Exception as e: + print(repr(e)) + print(traceback.format_exc()) + + +def load_shape_from_yml(yml_file_path, input_params_map, ignore_sanity_check=False): + with open(yml_file_path, 'r') as f: + yml_obj = yaml.load(f, Loader=yaml.FullLoader) + yml_to_shape(yml_obj, input_params_map, ignore_sanity_check=ignore_sanity_check) + + +def load_base_shape_from_yml(recipe_file_path, input_params_map): + print(f'Loading the base shape from the YML file [{recipe_file_path}]') + + with open(recipe_file_path, 'r') as f: + yml_obj = yaml.load(f, Loader=yaml.FullLoader) + + yml_to_shape(yml_obj['base'], input_params_map) + + +def randomize_all_params(input_params_map): + param_values_map = {} + for param_name, input_param in input_params_map.items(): + param_values_map[param_name] = random.choice(input_param.possible_values) + return param_values_map diff --git a/common/intersection_util.py b/common/intersection_util.py new file mode 100644 index 0000000..bee5045 --- /dev/null +++ b/common/intersection_util.py @@ -0,0 +1,161 @@ +import bpy +import array +import mathutils +from object_print3d_utils import mesh_helpers +from common.bpy_util import select_objs, select_shape, refresh_obj_in_viewport + + +def isolate_node_as_final_geometry(obj, node_label): + gm = obj.modifiers.get("GeometryNodes") + group_output_node = None + node_to_isolate = None + for n in gm.node_group.nodes: + # print(n.name), print(n.type), print(dir(n)) + if n.type == 'GROUP_OUTPUT': + group_output_node = n + elif n.label == node_label: + node_to_isolate = n + if not node_to_isolate: + raise Exception(f"Did not find any node with the label [{node_label}]") + + realize_instances_node = group_output_node.inputs[0].links[0].from_node + third_to_last_node = realize_instances_node.inputs[0].links[0].from_node + third_to_last_node_socket = None + # to later revert this operation, we need to find the socket which is currently connected + # this happens since the SWITCH node has multiple options, and each option translates to + # a different output socket in the node (so there isn't just one socket as you would think) + for i, socket in enumerate(third_to_last_node.outputs): + if socket.is_linked: + third_to_last_node_socket = i + break + node_group = next(m for m in obj.modifiers if m.type == 'NODES').node_group + # find the output socket that actually is connected to something, + # we do this since some nodes have multiple output sockets + out_socket_idx = 0 + for out_socket_idx, out_socket in enumerate(node_to_isolate.outputs): + if out_socket.is_linked: + break + node_group.links.new(node_to_isolate.outputs[out_socket_idx], realize_instances_node.inputs[0]) + def revert(): + node_group.links.new(third_to_last_node.outputs[third_to_last_node_socket], realize_instances_node.inputs[0]) + refresh_obj_in_viewport(obj) + return revert + + +def detect_self_intersection(obj): + """ + refer to: + https://blenderartists.org/t/self-intersection-detection/671080 + documentation of the intersection detection method + https://docs.blender.org/api/current/mathutils.bvhtree.html + """ + if not obj.data.polygons: + return array.array('i', ()) + + bm = mesh_helpers.bmesh_copy_from_object(obj, transform=False, triangulate=False) + tree = mathutils.bvhtree.BVHTree.FromBMesh(bm, epsilon=0.00001) + + overlap = tree.overlap(tree) + faces_error = {i for i_pair in overlap for i in i_pair} + return array.array('i', faces_error) + + +def find_self_intersections(node_label): + # intersection detection + chair = select_shape() + revert_isolation = isolate_node_as_final_geometry(chair, node_label) + + dup_obj = chair.copy() + dup_obj.data = chair.data.copy() + dup_obj.animation_data_clear() + bpy.context.collection.objects.link(dup_obj) + # move for clarity + dup_obj.location.x += 2.0 + + # set active + bpy.ops.object.select_all(action='DESELECT') + dup_obj.select_set(True) + bpy.context.view_layer.objects.active = dup_obj + # apply the modifier to turn the geometry node to a mesh + bpy.ops.object.modifier_apply(modifier="GeometryNodes") + assert dup_obj.type == 'MESH' + + intersections = detect_self_intersection(dup_obj) + + # delete the duplicate + bpy.ops.object.delete() + + revert_isolation() + + # reselect the original object + select_shape() + + return len(intersections) + + +def detect_cross_intersection(obj1, obj2): + if not obj1.data.polygons or not obj2.data.polygons: + return array.array('i', ()) + + bm1 = mesh_helpers.bmesh_copy_from_object(obj1, transform=False, triangulate=False) + tree1 = mathutils.bvhtree.BVHTree.FromBMesh(bm1, epsilon=0.00001) + bm2 = mesh_helpers.bmesh_copy_from_object(obj2, transform=False, triangulate=False) + tree2 = mathutils.bvhtree.BVHTree.FromBMesh(bm2, epsilon=0.00001) + + overlap = tree1.overlap(tree2) + faces_error = {i for i_pair in overlap for i in i_pair} + return array.array('i', faces_error) + + +def find_cross_intersections(node_label1, node_label2): + # intersection detection + chair = select_shape() + revert_isolation = isolate_node_as_final_geometry(chair, node_label1) + + dup_obj1 = chair.copy() + dup_obj1.data = chair.data.copy() + dup_obj1.animation_data_clear() + bpy.context.collection.objects.link(dup_obj1) + # move for clarity + dup_obj1.location.x += 2.0 + # set active + bpy.ops.object.select_all(action='DESELECT') + dup_obj1.select_set(True) + bpy.context.view_layer.objects.active = dup_obj1 + # apply the modifier to turn the geometry node to a mesh + bpy.ops.object.modifier_apply(modifier="GeometryNodes") + # export the object + assert dup_obj1.type == 'MESH' + + revert_isolation() + + chair = select_shape() + revert_isolation = isolate_node_as_final_geometry(chair, node_label2) + + dup_obj2 = chair.copy() + dup_obj2.data = chair.data.copy() + dup_obj2.animation_data_clear() + bpy.context.collection.objects.link(dup_obj2) + # move for clarity + dup_obj2.location.x += 2.0 + # set active + bpy.ops.object.select_all(action='DESELECT') + dup_obj2.select_set(True) + bpy.context.view_layer.objects.active = dup_obj2 + # apply the modifier to turn the geometry node to a mesh + bpy.ops.object.modifier_apply(modifier="GeometryNodes") + # export the object + assert dup_obj2.type == 'MESH' + + revert_isolation() + + intersections = detect_cross_intersection(dup_obj1, dup_obj2) + + # delete the duplicate + select_objs(dup_obj1, dup_obj2) + bpy.ops.object.delete() + + # reselect the original object + select_shape() + + return len(intersections) diff --git a/common/param_descriptors.py b/common/param_descriptors.py new file mode 100644 index 0000000..229a7ae --- /dev/null +++ b/common/param_descriptors.py @@ -0,0 +1,230 @@ +import numpy as np +from dataclasses import dataclass +from collections import OrderedDict +from typing import List, Optional, Dict + + +arithmetic_symbols = ['and', 'or', 'not', '(', ')', '<', '<=' , '>', '>=', '==', '-', '+', '/', '*'] + +def isfloat(num): + try: + float(num) + return True + except ValueError: + return False + + +@dataclass +class ParamDescriptor: + input_type: str + num_classes: int + step: float + classes: np.ndarray + normalized_classes: np.ndarray + min_val: float + max_val: float + visibility_condition: str + is_regression: bool + normalized_acc_threshold: float + + def is_visible(self, param_values_map): + assert param_values_map + if not self.visibility_condition: + return True + is_visible_cond = " ".join([(word if (word in arithmetic_symbols or isfloat(word) or word.isnumeric()) else (f"param_values_map[\"{word}\"] == 1" if 'is_' in word else f"param_values_map[\"{word}\"]")) for word in self.visibility_condition.split(" ")]) + return eval(is_visible_cond) + + +class ParamDescriptors: + def __init__(self, recipe_yml_obj, inputs_to_eval, use_regression=False, train_with_visibility_label=True): + self.epsilon = 1e-6 + self.recipe_yml_obj = recipe_yml_obj + self.inputs_to_eval = inputs_to_eval + self.use_regression = use_regression + self.train_with_visibility_label = train_with_visibility_label + self.__overall_num_of_classes_without_visibility_label = 0 + self.param_descriptors_map: Optional[Dict[str, ParamDescriptor]] = None + self.__constraints: Optional[List[str]] = None + + def check_constraints(self, param_values_map): + assert param_values_map + for constraint in self.get_constraints(): + is_fulfilled = " ".join([(word if (word in arithmetic_symbols or isfloat(word) or word.isnumeric()) else (f"param_values_map[\"{word}\"] == 1" if 'is_' in word else f"param_values_map[\"{word}\"]")) for word in constraint.split(" ")]) + if not eval(is_fulfilled): + return False + return True + + def get_constraints(self): + if self.__constraints: + return self.__constraints + self.__constraints = [] + if 'constraints' in self.recipe_yml_obj: + for constraint_name, constraint in self.recipe_yml_obj['constraints'].items(): + self.__constraints.append(constraint) + return self.__constraints + + def get_param_descriptors_map(self): + if self.param_descriptors_map: + return self.param_descriptors_map + recipe_yml_obj = self.recipe_yml_obj # for readability + param_descriptors_map = OrderedDict() + visibility_conditions = {} + if 'visibility_conditions' in recipe_yml_obj: + visibility_conditions = recipe_yml_obj['visibility_conditions'] + + for i, param_name in enumerate(self.inputs_to_eval): + is_regression = False + normalized_acc_threshold = None + if " x" in param_name or " y" in param_name or " z" in param_name: + input_type = recipe_yml_obj['data_types'][param_name[:-2]]['type'] + else: + input_type = recipe_yml_obj['data_types'][param_name]['type'] + if input_type == 'Integer' or input_type == 'Boolean': + max_val = recipe_yml_obj['dataset_generation'][param_name]['max'] + min_val = recipe_yml_obj['dataset_generation'][param_name]['min'] + step = 1 + num_classes = max_val - min_val + step + self.__overall_num_of_classes_without_visibility_label += num_classes + classes = np.arange(min_val, max_val + self.epsilon, step) + normalized_classes = classes - min_val + # visibility label adjustment + if self.train_with_visibility_label: + for vis_cond_name, vis_cond in visibility_conditions.items(): + if vis_cond_name in param_name: + num_classes += 1 + classes = np.concatenate((np.array([-1.0]), classes)) + normalized_classes = np.concatenate((np.array([-1.0]), normalized_classes)) + break + elif input_type == 'Float': + max_val = recipe_yml_obj['dataset_generation'][param_name]['max'] + min_val = recipe_yml_obj['dataset_generation'][param_name]['min'] + samples = recipe_yml_obj['dataset_generation'][param_name]['samples'] + step, num_classes, classes, normalized_classes, is_regression, normalized_acc_threshold \ + = self._handle_float(param_name, samples, min_val, max_val, visibility_conditions) + elif input_type == 'Vector': + axis = param_name[-1] + param_name_no_axis = param_name[:-2] + max_val = recipe_yml_obj['dataset_generation'][param_name_no_axis][axis]['max'] + min_val = recipe_yml_obj['dataset_generation'][param_name_no_axis][axis]['min'] + samples = recipe_yml_obj['dataset_generation'][param_name_no_axis][axis]['samples'] + step, num_classes, classes, normalized_classes, is_regression, normalized_acc_threshold \ + = self._handle_float(param_name_no_axis, samples, min_val, max_val, visibility_conditions) + else: + raise Exception(f'Input type [{input_type}] is not supported yet') + + visibility_condition = None + for vis_cond_name, vis_cond in visibility_conditions.items(): + if vis_cond_name in param_name: + visibility_condition = vis_cond + break + param_descriptors_map[param_name] = ParamDescriptor(input_type, num_classes, step, classes, + normalized_classes, min_val, max_val, + visibility_condition, is_regression, + normalized_acc_threshold) + self.param_descriptors_map = param_descriptors_map + return self.param_descriptors_map + + def _handle_float(self, param_name, samples, min_val, max_val, visibility_conditions): + """ + :param param_name: the parameter name, if the parameter is a vector, the axis should be omitted + :param samples: the number of samples requested in the recipe file + :param min_val: the min value allowed in the recipe file + :param max_val: the max value allowed in the recipe file + :param visibility_conditions: visibility conditions from the recipe file + :return: step, num_classes, classes, normalized_classes, is_regression, normalized_acc_threshold + """ + is_regression = False + normalized_acc_threshold = None + if not self.use_regression: + step = (max_val - min_val) / (samples - 1) + classes = np.arange(min_val, max_val + self.epsilon, step) + classes = classes.astype(np.float64) + normalized_classes = (classes - min_val) / (max_val - min_val) + normalized_classes = normalized_classes.astype(np.float64) + num_classes = classes.shape[0] + self.__overall_num_of_classes_without_visibility_label += num_classes + # visibility label adjustment + if self.train_with_visibility_label: + for vis_cond_name, vis_cond in visibility_conditions.items(): + if vis_cond_name in param_name: + num_classes += 1 + classes = np.concatenate((np.array([-1.0]), classes)) + normalized_classes = np.concatenate((np.array([-1.0]), normalized_classes)) + break + else: + step = 0 + num_classes = 2 # one for prediction and one for visibility label + classes = None + normalized_classes = None + is_regression = True + normalized_acc_threshold = 1 / (2 * (samples - 1)) + return step, num_classes, classes, normalized_classes, is_regression, normalized_acc_threshold + + + def convert_prediction_vector_to_map(self, pred_vector, use_regression=False): + """ + :param pred_vector: predicted vector from the network + :param use_regression: whether we use regression for float values + :return: map object representing the shape + """ + pred_vector = pred_vector.squeeze() + assert len(pred_vector.shape) == 1 + shape_map = {} + idx = 0 + param_descriptors_map = self.get_param_descriptors_map() + for param_name in self.inputs_to_eval: + param_descriptor = param_descriptors_map[param_name] + input_type = param_descriptor.input_type + classes = param_descriptor.classes + num_classes = param_descriptor.num_classes + if input_type == 'Float' or input_type == 'Vector': + if not use_regression: + normalized_pred_class = int(np.argmax(pred_vector[idx:idx + num_classes])) + pred_val = float(classes[normalized_pred_class]) + else: + min_val = param_descriptor.min_val + max_val = param_descriptor.max_val + pred_val = -1.0 + if float(pred_vector[idx + 1]) < 0.5: # visibility class + pred_val = (float(pred_vector[idx]) * (max_val - min_val)) + min_val + else: + # Integer or Boolean + normalized_pred_class = int(np.argmax(pred_vector[idx:idx + num_classes])) + pred_val = int(classes[normalized_pred_class]) + if input_type == 'Vector': + if param_name[:-2] not in shape_map: + shape_map[param_name[:-2]] = {} + shape_map[param_name[:-2]][param_name[-1]] = pred_val + else: + shape_map[param_name] = pred_val + idx += num_classes + return shape_map + + def get_overall_num_of_classes_without_visibility_label(self): + self.get_param_descriptors_map() + return self.__overall_num_of_classes_without_visibility_label + + def expand_target_vector(self, targets): + """ + :param targets: 1-dim target vector which includes a single normalized value for each parameter + :return: 1-dim vector where each parameter prediction is in one-hot representation + """ + targets = targets.squeeze() + assert len(targets.shape) == 1 + res_vector = np.array([]) + param_descriptors = self.get_param_descriptors_map() + for i, param_name in enumerate(self.inputs_to_eval): + param_descriptor = param_descriptors[param_name] + num_classes = param_descriptor.num_classes + if param_descriptor.is_regression: + val = targets[i].reshape(1).item() + if val == -1.0: + res_vector = np.concatenate((res_vector, np.array([0.0, 1.0]))) + else: + res_vector = np.concatenate((res_vector, np.array([val, 0.0]))) + else: + normalized_classes = param_descriptor.normalized_classes + normalized_gt_class_idx = int(np.where(abs(normalized_classes - targets[i].item()) < 1e-3)[0].item()) + one_hot = np.eye(num_classes)[normalized_gt_class_idx] + res_vector = np.concatenate((res_vector, one_hot)) + return res_vector diff --git a/common/point_cloud_util.py b/common/point_cloud_util.py new file mode 100644 index 0000000..1a48244 --- /dev/null +++ b/common/point_cloud_util.py @@ -0,0 +1,16 @@ +import torch + + +def normalize_point_cloud(point_cloud, use_center_of_bounding_box=True): + min_x, max_x = torch.min(point_cloud[:, 0]), torch.max(point_cloud[:, 0]) + min_y, max_y = torch.min(point_cloud[:, 1]), torch.max(point_cloud[:, 1]) + min_z, max_z = torch.min(point_cloud[:, 2]), torch.max(point_cloud[:, 2]) + # center the point cloud + if use_center_of_bounding_box: + center = torch.tensor([(min_x + max_x) / 2, (min_y + max_y) / 2, (min_z + max_z) / 2]) + else: + center = torch.mean(point_cloud, dim=0) + point_cloud = point_cloud - center + dist = torch.max(torch.sqrt(torch.sum((point_cloud ** 2), dim=1))) + point_cloud = point_cloud / dist # scale the point cloud + return point_cloud diff --git a/common/sampling_util.py b/common/sampling_util.py new file mode 100644 index 0000000..bab9b5a --- /dev/null +++ b/common/sampling_util.py @@ -0,0 +1,75 @@ +import torch +from dgl.geometry import farthest_point_sampler + + +def farthest_point_sampling(faces, vertices, num_points=1000): + random_sampling = sample_surface(faces, vertices, num_points=30000) + point_cloud_indices = farthest_point_sampler(random_sampling.unsqueeze(0), npoints=num_points) + point_cloud = random_sampling[point_cloud_indices[0]] + return point_cloud + + +def face_areas_normals(faces, vs): + face_normals = torch.cross(vs[:, faces[:, 1], :] - vs[:, faces[:, 0], :], + vs[:, faces[:, 2], :] - vs[:, faces[:, 1], :], dim=2) + face_areas = torch.norm(face_normals, dim=2) + face_normals = face_normals / face_areas[:, :, None] + face_areas = 0.5 * face_areas + return face_areas, face_normals + + +def sample_surface(faces, vertices, num_points=1000): + """ + sample mesh surface + sample method: + http://mathworld.wolfram.com/TrianglePointPicking.html + Args + --------- + vertices: vertices + faces: triangle faces (torch.long) + num_points: number of samples in the final point cloud + Return + --------- + samples: (count, 3) points in space on the surface of mesh + normals: (count, 3) corresponding face normals for points + """ + bsize, nvs, _ = vertices.shape + weights, normal = face_areas_normals(faces, vertices) + weights_sum = torch.sum(weights, dim=1) + dist = torch.distributions.categorical.Categorical(probs=weights / weights_sum[:, None]) + face_index = dist.sample((num_points,)) + + # pull triangles into the form of an origin + 2 vectors + tri_origins = vertices[:, faces[:, 0], :] + tri_vectors = vertices[:, faces[:, 1:], :].clone() + tri_vectors -= tri_origins.repeat(1, 1, 2).reshape((bsize, len(faces), 2, 3)) + + # pull the vectors for the faces we are going to sample from + face_index = face_index.transpose(0, 1) + face_index = face_index[:, :, None].expand((bsize, num_points, 3)) + tri_origins = torch.gather(tri_origins, dim=1, index=face_index) + face_index2 = face_index[:, :, None, :].expand((bsize, num_points, 2, 3)) + tri_vectors = torch.gather(tri_vectors, dim=1, index=face_index2) + + # randomly generate two 0-1 scalar components to multiply edge vectors by + random_lengths = torch.rand(num_points, 2, 1, device=vertices.device, dtype=tri_vectors.dtype) + + # points will be distributed on a quadrilateral if we use 2x [0-1] samples + # if the two scalar components sum less than 1.0 the point will be + # inside the triangle, so we find vectors longer than 1.0 and + # transform them to be inside the triangle + random_test = random_lengths.sum(dim=1).reshape(-1) > 1.0 + random_lengths[random_test] -= 1.0 + random_lengths = torch.abs(random_lengths) + + # multiply triangle edge vectors by the random lengths and sum + sample_vector = (tri_vectors * random_lengths[None, :]).sum(dim=2) + + # finally, offset by the origin to generate + # (n,3) points in space on the triangle + samples = sample_vector + tri_origins + + # normals = torch.gather(normal, dim=1, index=face_index) + + # return samples, normals + return samples[0] diff --git a/config/neptune_config_example.yml b/config/neptune_config_example.yml new file mode 100644 index 0000000..1b5dfee --- /dev/null +++ b/config/neptune_config_example.yml @@ -0,0 +1,3 @@ +neptune: + api_token = "" + project = "project_dir/project_name" diff --git a/data/__init__.py b/data/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/data/data_processing.py b/data/data_processing.py new file mode 100644 index 0000000..0031e8e --- /dev/null +++ b/data/data_processing.py @@ -0,0 +1,95 @@ +import yaml +import numpy as np +from pathlib import Path +from tqdm import tqdm +import traceback +import torch +from common.file_util import load_obj +from common.point_cloud_util import normalize_point_cloud + + +def generate_point_clouds(data_dir, phase, num_points, num_point_clouds_per_combination, + processed_dataset_dir_name, sampling_method, + gaussian=0.0, apply_point_cloud_normalization=False): + """ + samples point cloud from mesh + """ + dataset_dir = Path(data_dir, phase) + processed_dataset_dir = dataset_dir.joinpath(processed_dataset_dir_name) + processed_dataset_dir.mkdir(exist_ok=True) + files = sorted(dataset_dir.glob('*.obj')) + for file in tqdm(files): + faces = None + vertices = None + should_load_obj = True # this is done to only load the obj if it is actually required + for point_cloud_idx in range(num_point_clouds_per_combination): + new_file_name = Path(processed_dataset_dir, file.with_suffix('.npy').name.replace(".npy", f"_{point_cloud_idx}.npy")) + if new_file_name.is_file(): + continue + # only load the obj (once per all the instances) if it is actually needed + if should_load_obj: + vertices, faces = load_obj(file) + vertices = vertices.reshape(1, vertices.shape[0], vertices.shape[1]) + vertices = torch.from_numpy(vertices) + faces = torch.from_numpy(faces) + should_load_obj = False + + try: + point_cloud = sampling_method(faces, vertices, num_points=num_points) + except Exception as e: + print(traceback.format_exc()) + print(repr(e)) + print(file) + continue + if apply_point_cloud_normalization: + # normalize the point cloud and use center of bounding box + point_cloud = normalize_point_cloud(point_cloud) + if gaussian and gaussian > 0.0: + point_cloud += np.random.normal(0, gaussian, point_cloud.shape) + np.save(str(new_file_name), point_cloud) + + +def normalize_labels(data_dir, phase, processed_dataset_dir_name, params_descriptors, train_with_visibility_label): + dataset_dir = Path(data_dir, phase) + processed_dataset_dir = dataset_dir.joinpath(processed_dataset_dir_name) + processed_dataset_dir.mkdir(exist_ok=True) + + files = sorted(dataset_dir.glob('*.yml')) + for file in files: + if not file.is_file(): + # it is only allowed to not have a gt yml file when we are in test phase + assert phase == "test" + continue + save_path = Path(processed_dataset_dir, file.name) + if save_path.is_file(): + # this will skip normalization if the file exists, but if the recipe file changes, then normalization needs to be performed again + # in that case, disable this if statement to regenerate the normalized labels + continue + with open(file, 'r') as f: + yml_obj = yaml.load(f, Loader=yaml.FullLoader) + normalized_yml_obj = yml_obj.copy() + + # only apply the normalization to the inputs that were changed in this dataset + for param_name, param_descriptor in params_descriptors.items(): + param_input_type = param_descriptor.input_types + min_val = param_descriptor.min_val + max_val = param_descriptor.max_val + if param_input_type == 'Integer': + normalized_yml_obj[param_name] -= min_val + elif param_input_type == 'Float': + value = yml_obj[param_name] + normalized_yml_obj[param_name] = (value - min_val) / (max_val - min_val) + elif param_input_type == 'Boolean': + pass + elif param_input_type == 'Vector': + param_name_no_axis = param_name[:-2] + for axis in ['x', 'y', 'z']: + if param_name[-2:] != f" {axis}": + continue + value = yml_obj[param_name_no_axis][axis] + normalized_yml_obj[param_name_no_axis][axis] = (value - min_val) / (max_val - min_val) + if train_with_visibility_label and not params_descriptors[param_name].is_visible(yml_obj): + normalized_yml_obj[param_name] = -1 + + with open(save_path, 'w') as out_file: + yaml.dump(normalized_yml_obj, out_file) diff --git a/data/dataset_pc.py b/data/dataset_pc.py new file mode 100644 index 0000000..b36d572 --- /dev/null +++ b/data/dataset_pc.py @@ -0,0 +1,123 @@ +import numpy as np +import torch.utils.data as data +import torch +import yaml +from pathlib import Path +from .data_processing import generate_point_clouds, normalize_labels +from common.sampling_util import sample_surface, farthest_point_sampling +from .dataset_util import assemble_targets + + +class DatasetPC(data.Dataset): + def __init__(self, + inputs_to_eval, + dataset_processing_preferred_device, + params_descriptors, + data_dir, + phase, + num_points=1500, + num_point_clouds_per_combination=1, + random_pc=None, + gaussian=0.0, + apply_point_cloud_normalization=False, + scanobjectnn=False, + augment_with_random_points=True, + train_with_visibility_label=True): + self.inputs_to_eval = inputs_to_eval + self.data_dir = data_dir + self.phase = phase + self.random_pc = random_pc + self.gaussian = gaussian + self.apply_point_cloud_normalization = apply_point_cloud_normalization + self.dataset_processing_preferred_device = dataset_processing_preferred_device + self.train_with_visibility_label = train_with_visibility_label + self.yml_gt_normalized_dir_name = 'yml_gt_normalized' + self.point_cloud_fps_dir_name = 'point_cloud_fps' + self.point_cloud_random_dir_name = 'point_cloud_random' + self.num_point_clouds_per_combination = num_point_clouds_per_combination + self.augment_with_random_points = augment_with_random_points + self.ds_path = Path(data_dir, phase) + if not self.ds_path.is_dir(): + raise Exception(f"Could not find a dataset in path [{self.ds_path}]") + + if scanobjectnn: + random_pc_dir = self.ds_path.joinpath(self.point_cloud_random_dir_name) + # [:-2] removes the _0 so that when it is added later it will match the file name + self.file_names = [f.stem[:-2] for f in random_pc_dir.glob("*.npy")] + self.num_files = len(self.file_names) + self.size = self.num_files * self.num_point_clouds_per_combination + return + print(f"Processing dataset [{phase}] with farthest point sampling...") + if not self.random_pc: + generate_point_clouds(data_dir, phase, num_points, self.num_point_clouds_per_combination, + self.point_cloud_fps_dir_name, sampling_method=farthest_point_sampling, gaussian=self.gaussian, + apply_point_cloud_normalization=self.apply_point_cloud_normalization) + else: + num_points = self.random_pc + print(f"Using uniform sampling only with [{num_points}] samples") + normalize_labels(data_dir, phase, self.yml_gt_normalized_dir_name, params_descriptors, self.train_with_visibility_label) + print(f"Processing dataset [{phase}] with uniform sampling (augmentation)...") + generate_point_clouds(data_dir, phase, num_points, self.num_point_clouds_per_combination, + self.point_cloud_random_dir_name, sampling_method=sample_surface, gaussian=self.gaussian, + apply_point_cloud_normalization=self.apply_point_cloud_normalization) + + obj_gt_dir = self.ds_path.joinpath('obj_gt') + self.file_names = [f.stem for f in obj_gt_dir.glob("*.obj")] + + self.num_files = len(self.file_names) + self.size = self.num_files * self.num_point_clouds_per_combination + + + def __getitem__(self, _index): + file_idx = _index // self.num_point_clouds_per_combination + sample_idx = _index % self.num_point_clouds_per_combination + file_name = self.file_names[file_idx] + + pc = [] + random_pc_path = self.ds_path.joinpath(self.point_cloud_random_dir_name, f"{file_name}_{sample_idx}.npy") + fps_pc_path = self.ds_path.joinpath(self.point_cloud_fps_dir_name, f"{file_name}_{sample_idx}.npy") + if self.random_pc: + pc = np.load(str(random_pc_path)) + pc = torch.from_numpy(pc).float() + assert len(pc) == self.random_pc + else: + if fps_pc_path.is_file(): + pc = np.load(str(fps_pc_path)) + pc = torch.from_numpy(pc).float() + + # augment the farthest point sampled point cloud with points from a randomly sampled point cloud + # note that in some tests we did not apply the augmentation + if self.augment_with_random_points: + pc_aug = np.load(str(random_pc_path)) + pc_aug = torch.from_numpy(pc_aug).float() + pc_aug = pc_aug[np.random.choice(pc_aug.shape[0], replace=False, size=800)] + pc = torch.cat((pc, pc_aug), dim=0) + else: + assert self.phase == "real" + + # assert that the point cloud is normalized + max_diff = 0.05 + if self.random_pc: + max_diff = 0.3 + if not self.gaussian or self.gaussian == 0.0: + max_dist_from_center = abs(1.0 - torch.max(torch.sqrt(torch.sum((pc ** 2), dim=1)))) + assert max_dist_from_center < max_diff, f"Point cloud is not normalized [{max_dist_from_center} > {max_diff}] for sample [{file_name}]. If this is an external ds, please consider using prepare_coseg.py script first." + + # load target vectors, for test phase, some examples may not have a yml file attached to the + yml_path = self.ds_path.joinpath(self.yml_gt_normalized_dir_name, f"{file_name}.yml") + yml_obj = None + if yml_path.is_file(): + with open(yml_path, 'r') as f: + yml_obj = yaml.load(f, Loader=yaml.FullLoader) + else: + # for training and validation we must have a yml file for each sample, for certain phases, yml file is not mandatory + assert self.phase == "coseg" or self.phase == "real" + + # assemble the vectors in the requested order of parameters + targets = assemble_targets(yml_obj, self.inputs_to_eval) + + # dataloaders are not allowed to return None, anything empty is converted to [] + return file_name, pc, targets, yml_obj if yml_obj else [] + + def __len__(self): + return self.size diff --git a/data/dataset_sketch.py b/data/dataset_sketch.py new file mode 100644 index 0000000..3469303 --- /dev/null +++ b/data/dataset_sketch.py @@ -0,0 +1,119 @@ +import numpy as np +import torch.utils.data as data +import yaml +from pathlib import Path +from .data_processing import normalize_labels +from torchvision import transforms +from PIL import Image +from skimage.morphology import erosion, dilation +import random +from .dataset_util import assemble_targets + + +class DatasetSketch(data.Dataset): + def __init__(self, + inputs_to_eval, + params_descriptors, + camera_angles_to_process, + pretrained_vgg, + data_dir, + phase, + train_with_visibility_label=True): + self.inputs_to_eval = inputs_to_eval + self.data_dir = data_dir + self.phase = phase + self.pretrained_vgg = pretrained_vgg + self.train_with_visibility_label = train_with_visibility_label + self.camera_angles_to_process = camera_angles_to_process + self.num_sketches_camera_angles = len(self.camera_angles_to_process) + self.yml_gt_normalized_dir_name = 'yml_gt_normalized' + self.ds_path = Path(data_dir, phase) + if not self.ds_path.is_dir(): + raise Exception(f"Could not find a dataset in path [{self.ds_path}]") + self.sketches_path = self.ds_path.joinpath("sketches") + if not self.sketches_path.is_dir(): + raise Exception(f"Could not find a sketches in path [{self.sketches_path}]") + self.sketch_transforms = transforms.Compose([ + transforms.RandomHorizontalFlip(), + transforms.ToTensor(), + transforms.Normalize((0.48145466, 0.4578275, 0.40821073), (0.26862954, 0.26130258, 0.27577711)), + ]) + normalize_labels(data_dir, phase, self.yml_gt_normalized_dir_name, params_descriptors, self.train_with_visibility_label) + + obj_gt_dir = self.ds_path.joinpath('obj_gt') + self.file_names = [f.stem for f in obj_gt_dir.glob("*.obj")] + if self.phase == "real" or self.phase == "clipasso" or self.phase == "traced": + self.file_names = [f.stem for f in self.sketches_path.glob("*.png")] + + num_files = len(self.file_names) + if self.phase == "real" or self.phase == "clipasso" or self.phase == "traced": + self.size = num_files + else: + self.size = num_files * self.num_sketches_camera_angles + + def __getitem__(self, _index): + if self.phase == "real" or self.phase == "clipasso" or self.phase == "traced": + file_idx = _index + sketch_idx = 0 + else: + file_idx = _index // self.num_sketches_camera_angles + sketch_idx = _index % self.num_sketches_camera_angles + file_name = self.file_names[file_idx] + + # load target vectors, for test phase, some examples may not have a yml file attached to them + yml_path = self.ds_path.joinpath(self.yml_gt_normalized_dir_name, f"{file_name}.yml") + yml_obj = None + if yml_path.is_file(): + with open(yml_path, 'r') as f: + yml_obj = yaml.load(f, Loader=yaml.FullLoader) + else: + # for training and validation we must have a yml file for each sample, for certain phases, yml file is not mandatory + assert self.phase == "test" or self.phase == "coseg" or self.phase == "real" or self.phase == "clipasso" or self.phase == "traced" + + # assemble the vectors in the requested order of parameters + targets = assemble_targets(yml_obj, self.inputs_to_eval) + + sketch_files = sorted(self.sketches_path.glob(f"{file_name}_*.png")) + if self.phase == "real" or self.phase == "clipasso" or self.phase == "traced": + sketch_files = sorted(self.sketches_path.glob(f"{file_name}.png")) + # filter out sketches from camera angles that are excluded + if self.phase != "real" and self.phase != "clipasso" and self.phase != "traced": + sketch_files = [f for f in sketch_files if any( camera_angle in f.name for camera_angle in self.camera_angles_to_process )] + if len(sketch_files) != len(self.camera_angles_to_process): + raise Exception(f"Object [{file_name}] is missing sketch files") + sketch_file = sketch_files[sketch_idx] + sketch = Image.open(sketch_file).convert("RGB") + if sketch.size[0] != sketch.size[0]: + raise Exception(f"Images should be square, got [{sketch.size}] instead.") + if sketch.size[0] != 224: + sketch = sketch.resize((224, 224), Image.BILINEAR) + # augmentation for the sketches + if self.phase == "train": + # three augmentation options: 1) original 2) erosion 3) erosion then dilation + aug_idx = random.randint(0, 2) + if aug_idx == 1: + sketch = np.array(sketch) + sketch = erosion(sketch) + sketch = Image.fromarray(sketch) + if aug_idx == 2: + sketch = np.array(sketch) + eroded = erosion(sketch) + sketch = dilation(eroded) + sketch = Image.fromarray(sketch) + sketch = self.sketch_transforms(sketch) + if not self.pretrained_vgg: + sketch = sketch[0].unsqueeze(0) # sketch.shape = [1, 224, 224] + + curr_file_camera_angle = 'angle_na' + for camera_angle in self.camera_angles_to_process: + if camera_angle in str(sketch_file): + curr_file_camera_angle = camera_angle + break + if self.phase != "real" and self.phase != "clipasso" and self.phase != "traced": + assert curr_file_camera_angle != 'angle_na' + + # dataloaders are not allowed to return None, anything empty is converted to [] + return file_name, curr_file_camera_angle, sketch, targets, yml_obj if yml_obj else [] + + def __len__(self): + return self.size diff --git a/data/dataset_util.py b/data/dataset_util.py new file mode 100644 index 0000000..e26da24 --- /dev/null +++ b/data/dataset_util.py @@ -0,0 +1,21 @@ +import torch +import numpy as np +from typing import Dict, List + + +def assemble_targets(yml_obj: Dict, inputs_to_eval: List[str]): + targets = [] + if yml_obj: + for param_name in inputs_to_eval: + if param_name[-2:] == ' x': + targets.append(yml_obj[param_name[:-2]]['x']) + elif param_name[-2:] == ' y': + targets.append(yml_obj[param_name[:-2]]['y']) + elif param_name[-2:] == ' z': + targets.append(yml_obj[param_name[:-2]]['z']) + else: + targets.append(yml_obj[param_name]) + + # convert from list to numpy array and then to torch tensor + targets = torch.from_numpy(np.asarray(targets)) + return targets diff --git a/dataset_generator/__init__.py b/dataset_generator/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/dataset_generator/dataset_generator.py b/dataset_generator/dataset_generator.py new file mode 100644 index 0000000..702c4ee --- /dev/null +++ b/dataset_generator/dataset_generator.py @@ -0,0 +1,439 @@ +#!/usr/bin/env python3 + +import sys +import bpy +import time +import yaml +import json +import hashlib +import argparse +import traceback +import subprocess +from pathlib import Path +import importlib + +def import_parents(level=1): + global __package__ + file = Path(__file__).resolve() + parent, top = file.parent, file.parents[level] + + sys.path.append(str(top)) + try: + sys.path.remove(str(parent)) + except ValueError: + # already removed + pass + + __package__ = '.'.join(parent.parts[len(top.parts):]) + importlib.import_module(__package__) + +if __name__ == '__main__' and __package__ is None: + import_parents(level=1) + +from common.param_descriptors import ParamDescriptors +from common.file_util import get_recipe_yml_obj, hash_file_name, get_source_recipe_file_path, save_yml +from common.bpy_util import refresh_obj_in_viewport, select_objs, select_shape, get_geometric_nodes_modifier, save_obj +from common.domain import Domain +from common.input_param_map import get_input_param_map, randomize_all_params, yml_to_shape +from dataset_generator.shape_validators.common_validations import object_sanity_check +from dataset_generator.shape_validators.shape_validator_factory import ShapeValidatorFactory + + +def shape_to_yml(gnodes_mod): + shape_yml_obj = {} + for input in gnodes_mod.node_group.inputs: + param_name = str(input.name) + param_val = gnodes_mod[input.identifier] + if input.bl_label == "Vector": + shape_yml_obj[param_name] = {} + shape_yml_obj[param_name]['x'] = param_val[0] + shape_yml_obj[param_name]['y'] = param_val[1] + shape_yml_obj[param_name]['z'] = param_val[2] + else: + shape_yml_obj[param_name] = param_val + return shape_yml_obj + + +def save_obj_label(gnodes_mod, targe_yml_file_path: Path): + """ + convert the object to parameter space and save it as a yaml file + """ + shape_yml_obj = shape_to_yml(gnodes_mod) + save_yml(shape_yml_obj, targe_yml_file_path) + + +def update_base_shape_in_yml(gnodes_mod, recipe_file_path: Path): + """ + This will completely overwrite the base shape in the given yml file while keeping other + fields in the yaml untouched. + If the yml file does not exist, it will generate a new base shape as yaml in the given file. + """ + print(f'Updating the base shape in the YML file [{recipe_file_path}]') + # init an empty object in case the file does not exist + recipe_yml_obj = {} + if recipe_file_path.is_file(): + recipe_yml_obj = get_recipe_yml_obj(recipe_file_path) + base_yml_obj = shape_to_yml(gnodes_mod) + # completely overwrite 'base' in the final yml object + recipe_yml_obj['base'] = base_yml_obj + # save the object as YML file + save_yml(recipe_yml_obj, recipe_file_path) + return recipe_yml_obj + + +def json_hash(json_obj): + return hashlib.md5(json.dumps(json_obj).encode("utf-8")).hexdigest().strip() + + +def update_recipe_yml_obj_with_metadata(recipe_yml_obj, gnodes_mod): + # loops through all the inputs in the geometric node group + data_types = {} + for input in gnodes_mod.node_group.inputs: + param_name = str(input.name) + data_types[param_name] = {} + data_types[param_name]['type'] = input.bl_label + if input.bl_label != 'Boolean': + data_types[param_name]['min'] = input.min_value + data_types[param_name]['max'] = input.max_value + recipe_yml_obj['data_types'] = data_types + + +def generate_dataset(domain, dataset_dir: Path, phase, random_shapes_per_value, parallel=1, mod=None): + """ + Params: + random_shapes_per_value - number of random shapes we will generate per parameter value + mod - allows running this in parallel + """ + try: + # all other processes must wait for the folder to be created before continuing + phase_dir = dataset_dir.joinpath(phase) + yml_gt_dir = phase_dir.joinpath('yml_gt') + obj_gt_dir = phase_dir.joinpath('obj_gt') + if parallel > 1 and mod != 0: + while not (dataset_dir.is_dir() and phase_dir.is_dir() and yml_gt_dir.is_dir() and obj_gt_dir.is_dir()): + time.sleep(2) + + dataset_dir.mkdir(exist_ok=True) + phase_dir.mkdir(exist_ok=True) + + obj = select_shape() + # get the geometric nodes modifier for the object + gnodes_mod = get_geometric_nodes_modifier(obj) + recipe_file_path = get_source_recipe_file_path(domain) + if parallel <= 1: + update_base_shape_in_yml(gnodes_mod, recipe_file_path) + + # load recipe file and add some required metadata to it + recipe_yml_obj = get_recipe_yml_obj(recipe_file_path) + base_shape_yml = recipe_yml_obj['base'].copy() # used to return the viewport shape to this base shape at the end of the dataset generation + update_recipe_yml_obj_with_metadata(recipe_yml_obj, gnodes_mod) + + if parallel <= 1: + # save the recipe object as yml file in the dataset main dir (since it now also contains additional required metadata) + target_recipe_file_path = dataset_dir.joinpath('recipe.yml') + save_yml(recipe_yml_obj, target_recipe_file_path) + + yml_gt_dir.mkdir(exist_ok=True) + obj_gt_dir.mkdir(exist_ok=True) + + input_params_map = get_input_param_map(gnodes_mod, recipe_yml_obj) + inputs_to_eval = list(input_params_map.keys()) + param_descriptors = ParamDescriptors(recipe_yml_obj, inputs_to_eval) + param_descriptors_map = param_descriptors.get_param_descriptors_map() + dup_hashes_attempts = [] + + shape_validator = ShapeValidatorFactory.create_validator(domain) + + existing_samples = {} + for curr_param_name, curr_input_param in input_params_map.items(): + for value_idx, curr_param_value in enumerate(curr_input_param.possible_values): + shape_idx = 0 + while shape_idx < random_shapes_per_value: + if parallel > 1: + if hash_file_name(f'{curr_param_name}_{value_idx}_{shape_idx}') % parallel != mod: + shape_idx += 1 + continue + + curr_param_value_str_for_file = f"{curr_param_value:.4f}".replace('.', '_') + file_name = f"{domain}_{curr_input_param.get_name_for_file()}_{curr_param_value_str_for_file}_{shape_idx:04d}" + obj_file = obj_gt_dir.joinpath(f"{file_name}.obj") + yml_file = yml_gt_dir.joinpath(f"{file_name}.yml") + if obj_file.is_file() and yml_file.is_file() and object_sanity_check(obj_file): + with open(yml_file, 'r') as file: + param_values_map_from_yml = yaml.load(file, Loader=yaml.FullLoader) + param_values_map = {} + for param_name, _ in input_params_map.items(): + if param_name[-2:] in [' x', ' y', ' z']: + param_values_map[param_name] = param_values_map_from_yml[param_name[:-2]][param_name[-1:]] + else: + param_values_map[param_name] = param_values_map_from_yml[param_name] + + shape_yml = {} + for param_name, param_value in param_values_map.items(): + if not param_descriptors_map[param_name].is_visible(param_values_map): + shape_yml[param_name] = -1 + else: + shape_yml[param_name] = round(param_value, 4) + sample_hash = json_hash(shape_yml) + if sample_hash in existing_samples: + raise Exception("Found a duplicate within a single process") + existing_samples[sample_hash] = file_name + shape_idx += 1 + continue + + param_values_map = randomize_all_params(input_params_map) + param_values_map[curr_param_name] = curr_param_value + + if not param_descriptors.check_constraints(param_values_map): + with open(f'./retry_{mod}.log', 'a') as f: + f.write(f'constraints {file_name}\n') + continue + + if not param_descriptors_map[curr_param_name].is_visible(param_values_map): + with open(f'./retry_{mod}.log', 'a') as f: + f.write(f'visibility conditions {file_name} [{param_descriptors_map[curr_param_name].visibility_condition}] \n') + continue + + for param_name, param_value in param_values_map.items(): + input_params_map[param_name].assign_value(param_value) + + # sanity check to make sure we did not override what we are trying to generate + assert abs(input_params_map[curr_param_name].get_value() - curr_param_value) < 1e-6 + # must refresh the shape at this point + refresh_obj_in_viewport(obj) + + # shape-specific validation + is_valid, msg = shape_validator.validate_shape(input_params_map) + if not is_valid: + with open(f'./retry_{mod}.log', 'a') as f: + f.write(f'Shape invalid with message [{msg}] for file {file_name}\n') + continue + + # make sure this is a completely new shape + shape_yml = {} + for param_name, param_value in param_values_map.items(): + if not param_descriptors_map[param_name].is_visible(param_values_map): + shape_yml[param_name] = -1 + else: + shape_yml[param_name] = round(input_params_map[param_name].get_value(), 4) + sample_hash = json_hash(shape_yml) + if sample_hash in existing_samples: + dup_hashes_attempts.append(sample_hash) + with open(f'./retry_{mod}.log', 'a') as f: + f.write(f'already exists {sample_hash} {file_name}\n') + continue + existing_samples[sample_hash] = file_name + + targe_yml_file_path = yml_gt_dir.joinpath(f"{file_name}.yml") + save_obj_label(gnodes_mod, targe_yml_file_path) + targe_obj_file_path = obj_gt_dir.joinpath(f"{file_name}.obj") + dup_obj = save_obj(targe_obj_file_path) + # delete the duplicate object + select_objs(dup_obj) + bpy.ops.object.delete() + shape_idx += 1 + + # return the shape in Blender viewport to its original state + yml_to_shape(base_shape_yml, input_params_map, ignore_sanity_check=True) + dup_hashes_attempts_file_path = dataset_dir.joinpath(f"dup_hashes_attempts_{mod}.txt") + with open(dup_hashes_attempts_file_path, 'a') as dup_hashes_attempts_file: + dup_hashes_attempts_file.writelines([f"{h}\n" for h in dup_hashes_attempts]) + dup_hashes_attempts_file.write('---\n') + + return existing_samples + + except Exception as e: + with open(f'./err_{mod}.log', 'a') as f: + f.write(repr(e)) + f.write('\n') + f.write(traceback.format_exc()) + f.write('\n\n') + + +def main_generate_dataset_single_proc(args, blender_exe, blend_file): + assert blender_exe + assert blend_file + # show the main collections (if it is already shown, there is no effect) + bpy.context.scene.view_layers['View Layer'].layer_collection.children['Main'].hide_viewport = False + bpy.context.scene.view_layers['View Layer'].layer_collection.children['Main'].exclude = False + + try: + dataset_dir = Path(args.dataset_dir).expanduser() + existing_samples = generate_dataset(args.domain, dataset_dir, args.phase, + args.num_variations, parallel=args.parallel, mod=args.mod) + samples_hashes_file_path = dataset_dir.joinpath(f"sample_hashes_{args.mod}.json") + with open(samples_hashes_file_path, 'w') as samples_hashes_file: + json.dump(existing_samples, samples_hashes_file) + print(f"Process [{args.mod}] done") + except Exception as e: + print(repr(e)) + print(traceback.format_exc()) + + +def main_generate_dataset_parallel(args, blender_exe, blend_file): + dataset_dir = Path(args.dataset_dir).expanduser() + dataset_dir.mkdir(exist_ok=True) + + phase_dir = dataset_dir.joinpath(args.phase) + phase_dir.mkdir(exist_ok=True) + + try: + for existing_shapes_json_file_path in dataset_dir.glob("sample_hashes_*.json"): + existing_shapes_json_file_path.unlink() + + # select the procedural shape in blender + obj = select_shape() + # get the geometric nodes modifier fo the object + gnodes_mod = get_geometric_nodes_modifier(obj) + recipe_file_path = get_source_recipe_file_path(args.domain) + recipe_yml_obj = get_recipe_yml_obj(str(recipe_file_path)) + update_base_shape_in_yml(gnodes_mod, recipe_file_path) + update_recipe_yml_obj_with_metadata(recipe_yml_obj, gnodes_mod) + # save the recipe.yml file in the dataset's main dir (it now also contains required metadata) + target_recipe_file_path = dataset_dir.joinpath('recipe.yml') + save_yml(recipe_yml_obj, target_recipe_file_path) + input_params_map = get_input_param_map(gnodes_mod, recipe_yml_obj) + # loops through all the inputs in the geometric node group + data_types = {} + for input in gnodes_mod.node_group.inputs: + param_name = str(input.name) + data_types[param_name] = {} + data_types[param_name]['type'] = input.bl_label + if input.bl_label != 'Boolean': + data_types[param_name]['min'] = input.min_value + data_types[param_name]['max'] = input.max_value + recipe_yml_obj['data_types'] = data_types + inputs_to_eval = list(input_params_map.keys()) + param_descriptors = ParamDescriptors(recipe_yml_obj, inputs_to_eval) + + expected_number_of_samples = param_descriptors.get_overall_num_of_classes_without_visibility_label() * args.num_variations + print(f"Overall expected number of objects to generate is [{expected_number_of_samples}]") + + iteration_count = 0 + while True: + iteration_count += 1 + duplicates = [] + + # load any current saved state (this is to generate val, test, and train, since we cannot do this simultaneously) + # please note that all the sample_hashes files are common to all phases of the dataset (val, test, and train) so we + # will avoid creating the same sample across all of these phases + + existing_samples = {} + # add all the samples hashes from any other phase to avoid duplicates with other phases + sample_hashes_json_file_path = dataset_dir.joinpath("sample_hashes.json") + if sample_hashes_json_file_path.is_file(): + with open(sample_hashes_json_file_path, 'r') as existing_samples_file: + existing_samples = json.load(existing_samples_file) + # clear any current phase sample hashes as they are added by the processes + existing_samples[args.phase] = {} + + # every process generates a 'sample_hashes_.json' file containing hash -> file_name map + for existing_samples_json_file_path in dataset_dir.glob("sample_hashes_*.json"): + with open(existing_samples_json_file_path, 'r') as existing_samples_json_file: + existing_samples_json = json.load(existing_samples_json_file) + print(existing_samples_json_file_path) + print(existing_samples_json) + for hash, file_name in existing_samples_json.items(): + is_dup = False + for phase, sample_hashes in existing_samples.items(): + if hash in sample_hashes: + duplicates.append(file_name) + is_dup = True + break + if not is_dup: + existing_samples[args.phase][hash] = file_name + + # delete the duplicated files so they will be regenerated + if duplicates: + print(f"Found [{len(duplicates)}] duplicates that will be regenerated") + print("\n\t".join(duplicates)) + for file_name in duplicates: + obj_file = phase_dir.joinpath(file_name + ".obj") + yml_file = phase_dir.joinpath(file_name + ".yml") + obj_file.unlink() + yml_file.unlink() + + # backup the current sample_hashes + with open(f"{dataset_dir}/sample_hashes.json", 'w') as sample_hashes_file: + json.dump(existing_samples, sample_hashes_file) + + if len(existing_samples[args.phase]) > expected_number_of_samples: + raise Exception("Something went wrong, make sure you know how to count") + print(len(existing_samples[args.phase])) + print(expected_number_of_samples) + if len(existing_samples[args.phase]) == expected_number_of_samples: + print('Done creating the requested dataset') + break + + # since we need another iteration, we remove all the sample_hashes files + for existing_samples_json_file_path in dataset_dir.glob("sample_hashes_*.json"): + existing_samples_json_file_path.unlink() + + processes = [] + dataset_generator_path = Path(__file__).parent.joinpath('dataset_generator.py').resolve() + for mod in range(args.parallel): + try: + cmd = [str(blender_exe), str(blend_file), '-b', '--python', str(dataset_generator_path), '--', + 'generate-dataset-single-process', + '--dataset-dir', str(dataset_dir), + '--domain', str(args.domain), + '--phase', args.phase, + '--num-variations', str(args.num_variations), + '--parallel', str(args.parallel), + '--mod', str(mod)] + print(f'Mod {mod}:') + print(" ".join(cmd)) + process = subprocess.Popen(cmd, stdout=subprocess.DEVNULL) # DEVNULL is to avoid processes getting stuck + processes.append(process) + except Exception as e: + print(repr(e)) + print(traceback.format_exc()) + for process in processes: + process.wait() + + print(f"Dataset generation iteration [{iteration_count}] is done") + except Exception as e: + print(repr(e)) + print(traceback.format_exc()) + + +def main(): + if '--' in sys.argv: + # refer to https://b3d.interplanety.org/en/how-to-pass-command-line-arguments-to-a-blender-python-script-or-add-on/ + argv = sys.argv[sys.argv.index('--') + 1:] + else: + raise Exception("Expected \'--\' followed by arguments to the script") + + parser = argparse.ArgumentParser(prog='dataset_generator') + common_parser = argparse.ArgumentParser(add_help=False) + common_parser.add_argument('--dataset-dir', type=str, required=True, help='Path to dataset directory') + + sp = parser.add_subparsers() + sp_gen_single_proc = sp.add_parser('generate-dataset-single-process', parents=[common_parser]) + sp_gen_parallel = sp.add_parser('generate-dataset', parents=[common_parser]) + + sp_gen_single_proc.set_defaults(func=main_generate_dataset_single_proc) + sp_gen_single_proc.add_argument('--domain', type=Domain, choices=list(Domain), required=True, help='The domain name to generate the dataset for.') + sp_gen_single_proc.add_argument('--phase', type=str, required=True, help='E.g. train, val, or test') + sp_gen_single_proc.add_argument('--num-variations', type=int, default=3) + sp_gen_single_proc.add_argument('--parallel', type=int, default=1, help='Number of processes that are running the script in parallel') + sp_gen_single_proc.add_argument('--mod', type=int, default=None, help='The modulo for this process to match files\' hash') + + sp_gen_parallel.set_defaults(func=main_generate_dataset_parallel) + sp_gen_parallel.add_argument('--domain', type=Domain, choices=list(Domain), required=True, help='The domain name to generate the dataset for.') + sp_gen_parallel.add_argument('--phase', type=str, required=True, help='E.g. train, val, or test') + sp_gen_parallel.add_argument('--num-variations', type=int, default=3, help='The number of random shapes to generate for each parameter value') + sp_gen_parallel.add_argument('--parallel', type=int, default=1, help='Number of processes that will run the script') + + blender_exe_path = Path(sys.argv[0]).resolve() + blend_file_path = Path(sys.argv[1]).resolve() + try: + args = parser.parse_known_args(argv)[0] + args.func(args, blender_exe_path, blend_file_path) + except Exception as e: + print(repr(e)) + print(traceback.format_exc()) + + +if __name__ == "__main__": + main() diff --git a/dataset_generator/recipe_files/recipe_chair.yml b/dataset_generator/recipe_files/recipe_chair.yml new file mode 100644 index 0000000..df981b2 --- /dev/null +++ b/dataset_generator/recipe_files/recipe_chair.yml @@ -0,0 +1,318 @@ +base: + scale: + x: 1.0 + y: 1.0 + z: 1.0 + bevel_rails: 0.0 + pillow_state: 1 + pillow_fill_edge: 1 + seat_shape: 0.423308789730072 + seat_pos: 0.5646687746047974 + cr_count: 5 + cr_scale_y: 0.7333 + cr_scale_z: 0.98 + cr_offset_bottom: 0.4071 + cr_offset_top: 0.7786 + cr_shape_1: 0.0 + curvature: 0.25 + is_top_rail: 1 + tr_fill_edge: 1 + tr_scale_y: 0.6800000071525574 + tr_scale_z: 1.3600000143051147 + tr_shape_1: 0.5525987148284912 + is_vertical_rail: 1 + vr_count: 4 + vr_scale_x: 0.7799999713897705 + vr_scale_y: 0.3499999940395355 + vr_shape_1: 0.0 + is_back_rest: 0 + legs_shape_1: 1.0 + legs_shape_2: 1.0 + legs_bevel: 0.5 + is_monoleg: 0 + is_monoleg_tent: 0 + monoleg_tent_pct: 0.4 + monoleg_tent_count: 4 + monoleg_bezier_start_x_offset: 0.0 + monoleg_bezier_start_handle_x_offset: 0.0 + monoleg_bezier_start_handle_z_pct: 0.4 + monoleg_bezier_end_x_offset: 0.8199999332427979 + monoleg_bezier_end_handle_x_offset: 0.4 + monoleg_bezier_end_handle_z_pct: 1.0 + back_frame_top_y_offset_pct: 0.0 + back_frame_mid_y_offset_pct: 0.0 + back_leg_bottom_y_offset_pct: 0.0 + back_leg_mid_y_offset_pct: 0.0 + handles_state: 0 + is_handles_support: 1 + is_handles_cusion: 1 + handles_base_pos_z_pct: 0.15 + handles_mid_pos_x_pct: 0.0 + handles_mid_pos_y_pct: 0.42329999804496765 + handles_mid_pos_z_pct: 0.5 + handles_edge_pos_x_pct: 0.0 + handles_bottom_pos_along_seat_pct: 0.4 + handles_profile_width: 0.9 + handles_profile_height: 0.9 + handles_support_mid_x: 0.5800000429153442 + handles_support_mid_y: 0.0 + handles_support_top_pos: 0.5 + handles_support_thickness: 0.8167 + handles_cusion_cover_pct: 1.0 +dataset_generation: + scale: + x: + min: 0.5 + max: 2.0 + samples: 10 + y: + min: 0.5 + max: 2.0 + samples: 10 + z: + min: 0.5 + max: 2.0 + samples: 10 + bevel_rails: + min: 0.0 + max: 1.0 + samples: 3 + pillow_state: + min: 0 + max: 2 + pillow_fill_edge: + min: 0 + max: 1 + seat_shape: + min: 0.0 + max: 1.0 + samples: 5 + seat_pos: + min: 0.2 + max: 1.0 + samples: 9 + cr_count: + min: 3 + max: 8 + cr_scale_y: + min: 0.5 + max: 1.2 + samples: 4 + cr_scale_z: + min: 0.3 + max: 2.0 + samples: 6 + cr_offset_bottom: + min: 0.15 + max: 0.75 + samples: 8 + cr_offset_top: + min: 0.35 + max: 0.95 + samples: 8 + cr_shape_1: + min: 0.0 + max: 2.0 + samples: 7 + curvature: + min: 0.0 + max: 1.0 + samples: 5 + is_top_rail: + min: 0 + max: 1 + tr_fill_edge: + min: 0 + max: 1 + tr_scale_y: + min: 0.5 + max: 1.2 + samples: 5 + tr_scale_z: + min: 0.5 + max: 1.5 + samples: 5 + tr_shape_1: + min: 0.0 + max: 1.0 + samples: 5 + is_vertical_rail: + min: 0 + max: 1 + vr_count: + min: 3 + max: 8 + vr_scale_x: + min: 0.3 + max: 2.0 + samples: 6 + vr_scale_y: + min: 0.2 + max: 1.0 + samples: 4 + vr_shape_1: + min: 0.0 + max: 0.4 + samples: 5 + is_back_rest: + min: 0 + max: 1 + legs_shape_1: + min: 0.0 + max: 1.0 + samples: 3 + legs_shape_2: + min: 0.0 + max: 1.0 + samples: 3 + legs_bevel: + min: 0.0 + max: 1.0 + samples: 3 + is_monoleg: + min: 0 + max: 1 + is_monoleg_tent: + min: 0 + max: 1 + monoleg_tent_pct: + min: 0.2 + max: 0.8 + samples: 7 + monoleg_tent_count: + min: 3 + max: 8 + monoleg_bezier_start_x_offset: + min: 0.0 + max: 1.0 + samples: 6 + monoleg_bezier_start_handle_x_offset: + min: 0.0 + max: 1.0 + samples: 6 + monoleg_bezier_start_handle_z_pct: + min: 0.0 + max: 1.0 + samples: 6 + monoleg_bezier_end_x_offset: + min: 0.0 + max: 1.0 + samples: 6 + monoleg_bezier_end_handle_x_offset: + min: 0.0 + max: 1.0 + samples: 6 + monoleg_bezier_end_handle_z_pct: + min: 0.0 + max: 1.0 + samples: 6 + back_frame_top_y_offset_pct: + min: 0.0 + max: 1.0 + samples: 6 + back_frame_mid_y_offset_pct: + min: 0.0 + max: 1.0 + samples: 6 + back_leg_bottom_y_offset_pct: + min: 0.0 + max: 1.0 + samples: 6 + back_leg_mid_y_offset_pct: + min: 0.0 + max: 1.0 + samples: 6 + handles_state: + min: 0 + max: 2 + is_handles_support: + min: 0 + max: 1 + is_handles_cusion: + min: 0 + max: 1 + handles_profile_width: + min: 0.5 + max: 1.0 + samples: 6 + handles_profile_height: + min: 0.5 + max: 1.0 + samples: 6 + handles_base_pos_z_pct: + min: 0.15 + max: 0.8 + samples: 8 + handles_mid_pos_x_pct: + min: 0.0 + max: 1.0 + samples: 7 + handles_mid_pos_y_pct: + min: 0.1 + max: 0.9 + samples: 7 + handles_mid_pos_z_pct: + min: 0.0 + max: 1.0 + samples: 7 + handles_edge_pos_x_pct: + min: 0.0 + max: 1.0 + samples: 6 + handles_bottom_pos_along_seat_pct: + min: 0.3 + max: 0.9 + samples: 7 + handles_support_mid_x: + min: 0.0 + max: 1.0 + samples: 6 + handles_support_mid_y: + min: 0.0 + max: 1.0 + samples: 6 + handles_support_top_pos: + min: 0.3 + max: 0.9 + samples: 7 + handles_support_thickness: + min: 0.4 + max: 0.9 + samples: 7 + handles_cusion_cover_pct: + min: 0.0 + max: 1.0 + samples: 6 +constraints: + rule1: cr_offset_top - cr_offset_bottom >= 0.1 + rule2: monoleg_bezier_end_x_offset >= monoleg_bezier_start_x_offset +visibility_conditions: + bevel_rails: ( not is_back_rest ) + cr_: ( not is_back_rest ) and ( not is_top_rail or not is_vertical_rail ) + vr_: ( not is_back_rest ) and is_top_rail and is_vertical_rail + tr_: is_top_rail + is_vertical_rail: ( not is_back_rest ) and ( is_top_rail ) + legs_shape_: ( is_monoleg and is_monoleg_tent ) or ( not is_monoleg ) + is_monoleg_tent: is_monoleg + monoleg_tent_pct: is_monoleg and is_monoleg_tent + monoleg_tent_count: is_monoleg and is_monoleg_tent + monoleg_bezier: is_monoleg + pillow_fill_edge: pillow_state > 0 + back_leg_: ( not is_monoleg ) + is_handles_support: handles_state == 1 + handles_support_: ( handles_state == 1 ) and ( is_handles_support ) + is_handles_cusion: handles_state > 0 + handles_cusion_cover_pct: ( handles_state > 0 ) and ( is_handles_cusion ) + handles_profile_: handles_state > 0 + handles_base_: handles_state > 0 + handles_mid_: handles_state > 0 + handles_edge_: handles_state == 1 + handles_bottom_pos_along_seat_pct: ( handles_state == 2 ) or ( handles_state == 1 and is_handles_support == 1 ) +camera_angles_train: +- - -30.0 + - 35.0 +- - -30.0 + - 55.0 +camera_angles_test: +- - -30.0 + - 15.0 diff --git a/dataset_generator/recipe_files/recipe_table.yml b/dataset_generator/recipe_files/recipe_table.yml new file mode 100644 index 0000000..9d11255 --- /dev/null +++ b/dataset_generator/recipe_files/recipe_table.yml @@ -0,0 +1,198 @@ +base: + table_top_scale_x: 2.5999999046325684 + table_top_scale_y: 2.5999999046325684 + table_top_height: 0.0 + table_top_shape: 1.0 + table_top_thickness: 0.0 + table_top_profile_state: 2 + table_top_profile_strength: 0.0 + legs_shape_1: 1.0 + legs_shape_2: 1.0 + legs_bevel: 0.0 + std_legs_bottom_offset_y: 1.0 + std_legs_mid_offset_y: 0.0 + std_legs_top_offset_x: 0.26999998092651367 + std_legs_top_offset_y: 0.0 + std_legs_rotation: 0.0 + is_std_legs_support_x: 1 + std_legs_support_x_height: 0.7200000286102295 + std_legs_support_x_curvature: 0.0 + std_legs_support_x_profile_width: 1.0 + std_legs_support_x_profile_height: 0.27000004053115845 + is_std_legs_support_y: 1 + std_legs_support_y_height: 0.36000001430511475 + std_legs_support_y_curvature: 0.0 + std_legs_support_y_profile_width: 1.0 + std_legs_support_y_profile_height: 1.0 + is_monoleg: 0 + is_monoleg_tent: 1 + monoleg_tent_pct: 0.6649518609046936 + monoleg_tent_base_radius: 0.0 + monoleg_tent_count: 5 + monoleg_bezier_start_x_offset: 0.5273312330245972 + monoleg_bezier_start_handle_x_offset: 1.0 + monoleg_bezier_start_handle_z_pct: 0.46000000834465027 + monoleg_bezier_end_x_offset: 0.2499999850988388 + monoleg_bezier_end_handle_x_offset: 0.10999999940395355 + monoleg_bezier_end_handle_z_pct: 0.20999999344348907 +dataset_generation: + table_top_scale_x: + min: 0.6 + max: 2.6 + samples: 12 + table_top_scale_y: + min: 0.6 + max: 2.6 + samples: 12 + table_top_height: + min: 0.0 + max: 1.0 + samples: 8 + table_top_shape: + min: 0.0 + max: 1.0 + samples: 11 + table_top_thickness: + min: 0.0 + max: 1.0 + samples: 6 + table_top_profile_state: + min: 0 + max: 3 + table_top_profile_strength: + min: 0.0 + max: 1.0 + samples: 6 + legs_shape_1: + min: 0.0 + max: 1.0 + samples: 3 + legs_shape_2: + min: 0.0 + max: 1.0 + samples: 3 + legs_bevel: + min: 0.0 + max: 1.0 + samples: 3 + std_legs_bottom_offset_y: + min: 0.0 + max: 1.0 + samples: 6 + std_legs_mid_offset_y: + min: 0.0 + max: 1.0 + samples: 6 + std_legs_top_offset_x: + min: 0.0 + max: 1.0 + samples: 6 + std_legs_top_offset_y: + min: 0.0 + max: 1.0 + samples: 6 + std_legs_rotation: + min: 0.0 + max: 1.0 + samples: 6 + is_std_legs_support_x: + min: 0 + max: 1 + std_legs_support_x_height: + min: 0.0 + max: 1.0 + samples: 6 + std_legs_support_x_curvature: + min: 0.0 + max: 1.0 + samples: 6 + std_legs_support_x_profile_width: + min: 0.0 + max: 1.0 + samples: 5 + std_legs_support_x_profile_height: + min: 0.0 + max: 1.0 + samples: 5 + is_std_legs_support_y: + min: 0 + max: 1 + std_legs_support_y_height: + min: 0.0 + max: 1.0 + samples: 6 + std_legs_support_y_curvature: + min: 0.0 + max: 1.0 + samples: 6 + std_legs_support_y_profile_width: + min: 0.0 + max: 1.0 + samples: 5 + std_legs_support_y_profile_height: + min: 0.0 + max: 1.0 + samples: 5 + is_monoleg: + min: 0 + max: 1 + is_monoleg_tent: + min: 0 + max: 1 + monoleg_tent_pct: + min: 0.2 + max: 0.8 + samples: 7 + monoleg_tent_base_radius: + min: 0.0 + max: 1.0 + samples: 11 + monoleg_tent_count: + min: 3 + max: 8 + monoleg_bezier_start_x_offset: + min: 0.0 + max: 1.0 + samples: 6 + monoleg_bezier_start_handle_x_offset: + min: 0.0 + max: 1.0 + samples: 6 + monoleg_bezier_start_handle_z_pct: + min: 0.0 + max: 1.0 + samples: 6 + monoleg_bezier_end_x_offset: + min: 0.2 + max: 1.0 + samples: 5 + monoleg_bezier_end_handle_x_offset: + min: 0.0 + max: 1.0 + samples: 6 + monoleg_bezier_end_handle_z_pct: + min: 0.0 + max: 1.0 + samples: 6 +constraints: + rule1: monoleg_bezier_end_x_offset >= monoleg_bezier_start_x_offset +visibility_conditions: + legs_shape_: ( is_monoleg and is_monoleg_tent ) or ( not is_monoleg ) + legs_bevel: ( is_monoleg and is_monoleg_tent ) or ( not is_monoleg ) + is_monoleg_tent: is_monoleg + monoleg_tent_pct: is_monoleg and is_monoleg_tent + monoleg_tent_count: is_monoleg and is_monoleg_tent + monoleg_tent_base_radius: is_monoleg and is_monoleg_tent + monoleg_bezier: is_monoleg and ( not is_monoleg_tent or monoleg_tent_pct < 0.4 ) + std_legs_support_x_: ( not is_monoleg ) and is_std_legs_support_x + std_legs_support_y_: ( not is_monoleg ) and is_std_legs_support_y + std_legs: ( not is_monoleg ) + table_top_profile_strength: table_top_profile_state > 0 +camera_angles_train: +- - -30.0 + - 35.0 +- - -30.0 + - 55.0 +camera_angles_test: +- - -30.0 + - 15.0 diff --git a/dataset_generator/recipe_files/recipe_vase.yml b/dataset_generator/recipe_files/recipe_vase.yml new file mode 100644 index 0000000..22475cc --- /dev/null +++ b/dataset_generator/recipe_files/recipe_vase.yml @@ -0,0 +1,208 @@ +base: + body_height: 1.2143 + body_width: 0.2944 + body_bottom_curve_width: 0.0 + body_bottom_curve_height: 0.6 + body_mouth_width: 0.6 + body_top_curve_width: 0.2 + body_top_curve_height: 0.9 + body_profile_blend: 0.8 + has_body_thickness: 0 + body_thickness_val: 0.05999999865889549 + handle_count: 5 + hndl_type: 2 + hndl_profile_width: 1.0 + hndl_profile_height: 0.2 + hndl_profile_blend: 0.8 + hndl_base_z: 0.4699999988079071 + hndl_base_bezier_handle_angle: 0.4 + hndl_base_bezier_handle_length: 0.2 + hndl_radius_along_path: 0.0 + hndl1_top_z: 0.5 + hndl1_end_bezier_handle_angle: 0.2 + hndl1_end_bezier_handle_length: 0.6 + hndl2_end_x: 0.0 + hndl2_end_z: 0.4099999964237213 + hndl2_end_bezier_handle_x: 0.0 + hndl2_end_bezier_handle_z: 0.2 + has_neck: 1 + neck_end_x: 0.8 + neck_end_z: 0.8 + neck_end_bezier_handle_x: 0.8 + neck_end_bezier_handle_z: 0.0 + has_base: 1 + base_start_x: 0.7 + base_start_z: 0.6 + base_mid_x: 0.2 + base_mid_z: 0.8 + has_lid: 0 + has_lid_handle: 1 + lid_handle_radius: 0.02 +dataset_generation: + body_height: + min: 0.5 + max: 1.5 + samples: 15 + body_width: + min: 0.1 + max: 0.45 + samples: 10 + body_bottom_curve_width: + min: 0.0 + max: 1.0 + samples: 11 + body_bottom_curve_height: + min: 0.1 + max: 0.9 + samples: 9 + body_mouth_width: + min: 0.0 + max: 0.7 + samples: 8 + body_top_curve_width: + min: 0.0 + max: 0.9 + samples: 10 + body_top_curve_height: + min: 0.1 + max: 0.9 + samples: 9 + body_profile_blend: + min: 0.0 + max: 1.0 + samples: 11 + has_body_thickness: + min: 0 + max: 1 + body_thickness_val: + min: 0.01 + max: 0.07 + samples: 7 + handle_count: + min: 0 + max: 6 + hndl_type: + min: 1 + max: 2 + hndl_profile_width: + min: 0.0 + max: 1.0 + samples: 6 + hndl_profile_height: + min: 0.0 + max: 1.0 + samples: 6 + hndl_profile_blend: + min: 0.0 + max: 1.0 + samples: 6 + hndl_base_z: + min: 0.1 + max: 0.6 + samples: 6 + hndl_base_bezier_handle_angle: + min: 0.0 + max: 1.0 + samples: 11 + hndl_base_bezier_handle_length: + min: 0.0 + max: 1.0 + samples: 6 + hndl_radius_along_path: + min: 0.0 + max: 1.0 + samples: 11 + hndl1_top_z: + min: 0.2 + max: 0.8 + samples: 7 + hndl1_end_bezier_handle_angle: + min: 0.0 + max: 1.0 + samples: 11 + hndl1_end_bezier_handle_length: + min: 0.0 + max: 1.0 + samples: 6 + hndl2_end_x: + min: 0.0 + max: 1.0 + samples: 11 + hndl2_end_z: + min: 0.0 + max: 1.0 + samples: 11 + hndl2_end_bezier_handle_x: + min: 0.0 + max: 1.0 + samples: 11 + hndl2_end_bezier_handle_z: + min: 0.1 + max: 1.0 + samples: 10 + has_neck: + min: 0 + max: 1 + neck_end_x: + min: 0.1 + max: 1.0 + samples: 10 + neck_end_z: + min: 0.0 + max: 1.0 + samples: 11 + neck_end_bezier_handle_x: + min: 0.0 + max: 1.0 + samples: 11 + neck_end_bezier_handle_z: + min: 0.0 + max: 1.0 + samples: 11 + has_base: + min: 0 + max: 1 + base_start_x: + min: 0.0 + max: 1.0 + samples: 11 + base_start_z: + min: 0.0 + max: 1.0 + samples: 6 + base_mid_x: + min: 0.0 + max: 1.0 + samples: 11 + base_mid_z: + min: 0.0 + max: 1.0 + samples: 6 + has_lid: + min: 0 + max: 1 + has_lid_handle: + min: 0 + max: 1 + lid_handle_radius: + min: 0.02 + max: 0.07 + samples: 6 +visibility_conditions: + body_thickness_val: has_body_thickness and not has_lid + hndl_: handle_count > 0 + hndl1_: handle_count > 0 and hndl_type == 1 + hndl2_: handle_count > 0 and hndl_type == 2 + neck_: has_neck + base_start_: has_base + base_mid_: has_base + has_lid_handle: has_lid + lid_handle_radius: has_lid and has_lid_handle +camera_angles_train: +- - -30.0 + - 35.0 +- - -30.0 + - 55.0 +camera_angles_test: +- - -30.0 + - 15.0 diff --git a/dataset_generator/shape_validators/__init__.py b/dataset_generator/shape_validators/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/dataset_generator/shape_validators/chair_validator.py b/dataset_generator/shape_validators/chair_validator.py new file mode 100644 index 0000000..501e85c --- /dev/null +++ b/dataset_generator/shape_validators/chair_validator.py @@ -0,0 +1,41 @@ +from dataset_generator.shape_validators.shape_validator_interface import ShapeValidatorInterface +from dataset_generator.shape_validators.common_validations import validate_monoleg +from common.intersection_util import find_self_intersections, find_cross_intersections + + +class ChairValidator(ShapeValidatorInterface): + def validate_shape(self, input_params_map) -> (bool, str): + if not self.is_valid_chair(input_params_map): + return False, "Collision" + return True, "Valid" + + def is_valid_chair(self, input_params_map): + if input_params_map['is_back_rest'].get_value() == 0 \ + and input_params_map['is_top_rail'].get_value() == 1 \ + and input_params_map['is_vertical_rail'].get_value() == 1: + # reaching here means the vertical rails are visible + # also note the assumption that the min vertical rails count is 3 + if find_self_intersections('vertical_rails_out') > 0: + # try again, as we have vertical rails intersecting each other + # print("vr, found collisions") + return False + if input_params_map['is_back_rest'].get_value() == 0 \ + and (input_params_map['is_top_rail'].get_value() == 0 + or input_params_map['is_vertical_rail'].get_value() == 0): + # reaching here means the cross rails are visible + # also note the assumption that the min cross rails count is 3 + if find_self_intersections('cross_rails_and_top_rail_out') > 0: + # try again, as we have cross rails intersecting each other or the top rail + # print("cr, found collisions...") + return False + if input_params_map['handles_state'].get_value() == 1 and input_params_map['is_handles_support'].get_value(): + if find_self_intersections('handles_support_and_back_frame') > 0: + return False + if input_params_map['handles_state'].get_value() > 0: + if find_cross_intersections('handles_left_side', 'handles_right_side') > 0: + # the handles in both sides of the chair should never intersect + return False + if input_params_map['is_monoleg'].get_value() > 0 and input_params_map['is_monoleg_tent'].get_value() == 0: + if not validate_monoleg('monoleg'): + return False + return True diff --git a/dataset_generator/shape_validators/common_validations.py b/dataset_generator/shape_validators/common_validations.py new file mode 100644 index 0000000..8168e70 --- /dev/null +++ b/dataset_generator/shape_validators/common_validations.py @@ -0,0 +1,58 @@ +import bpy +import numpy as np +from common.file_util import load_obj +from common.bpy_util import select_shape +from common.intersection_util import isolate_node_as_final_geometry + + +def triangle_area(x): + a = x[:, 0, :] - x[:, 1, :] + b = x[:, 0, :] - x[:, 2, :] + cross = np.cross(a, b) + area = 0.5 * np.norm(cross, dim=1) + return area + + +def object_sanity_check(obj_file): + try: + vertices, faces = load_obj(obj_file) + vertices = vertices.reshape(1, vertices.shape[0], vertices.shape[1]) + faces = vertices.squeeze()[faces] + triangle_area(faces) + except Exception: + print('Invalid sample') + return False + return True + + +def validate_monoleg(node_label, factor=0.08): + chair = select_shape() + revert_isolation = isolate_node_as_final_geometry(chair, node_label) + + dup_obj = chair.copy() + dup_obj.data = chair.data.copy() + dup_obj.animation_data_clear() + bpy.context.collection.objects.link(dup_obj) + # move for clarity + dup_obj.location.x += 2.0 + # set active + bpy.ops.object.select_all(action='DESELECT') + dup_obj.select_set(True) + bpy.context.view_layer.objects.active = dup_obj + # apply the modifier to turn the geometry node to a mesh + bpy.ops.object.modifier_apply(modifier="GeometryNodes") + # export the object + assert dup_obj.type == 'MESH' + + revert_isolation() + + bpy.ops.object.origin_set(type='ORIGIN_GEOMETRY', center='BOUNDS') + center_of_volume = dup_obj.location[2] + # another option is to use (type='ORIGIN_CENTER_OF_MASS', center='MEDIAN') as the center of mass + bpy.ops.object.origin_set(type='ORIGIN_CENTER_OF_VOLUME', center='MEDIAN') + center_of_mass = dup_obj.location[2] + height = dup_obj.dimensions[2] + + if center_of_volume - center_of_mass > factor * height: + return True + return False diff --git a/dataset_generator/shape_validators/shape_validator_factory.py b/dataset_generator/shape_validators/shape_validator_factory.py new file mode 100644 index 0000000..4204e69 --- /dev/null +++ b/dataset_generator/shape_validators/shape_validator_factory.py @@ -0,0 +1,18 @@ +from common.domain import Domain +from dataset_generator.shape_validators.shape_validator_interface import ShapeValidatorInterface +from dataset_generator.shape_validators.chair_validator import ChairValidator +from dataset_generator.shape_validators.vase_validator import VaseValidator +from dataset_generator.shape_validators.table_validator import TableValidator + + +class ShapeValidatorFactory: + @staticmethod + def create_validator(domain) -> ShapeValidatorInterface: + if domain == Domain.chair: + return ChairValidator() + elif domain == Domain.vase: + return VaseValidator() + elif domain == Domain.table: + return TableValidator() + else: + raise Exception(f"Domain [{domain}] is not recognized.") diff --git a/dataset_generator/shape_validators/shape_validator_interface.py b/dataset_generator/shape_validators/shape_validator_interface.py new file mode 100644 index 0000000..e99f54f --- /dev/null +++ b/dataset_generator/shape_validators/shape_validator_interface.py @@ -0,0 +1,4 @@ +class ShapeValidatorInterface: + def validate_shape(self, input_params_map) -> (bool, str): + """validate the shape and return True if valid""" + pass diff --git a/dataset_generator/shape_validators/table_validator.py b/dataset_generator/shape_validators/table_validator.py new file mode 100644 index 0000000..e6659f0 --- /dev/null +++ b/dataset_generator/shape_validators/table_validator.py @@ -0,0 +1,18 @@ +from dataset_generator.shape_validators.shape_validator_interface import ShapeValidatorInterface +from dataset_generator.shape_validators.common_validations import validate_monoleg +from common.intersection_util import find_self_intersections + + +class TableValidator(ShapeValidatorInterface): + def validate_shape(self, input_params_map) -> (bool, str): + table_top_and_legs_support_intersections = find_self_intersections('table_top_and_legs_support') + if table_top_and_legs_support_intersections > 0: + return False, "Table top intersects with the legs supports" + floor_and_legs_support_intersections = find_self_intersections('floor_and_legs_support') + if floor_and_legs_support_intersections > 0: + return False, "Legs supports intersect with the floor" + if input_params_map['is_monoleg'].get_value() > 0 and input_params_map['is_monoleg_tent'].get_value() == 0: + if not validate_monoleg('monoleg', factor=0.16): + # the factor is more restricting since the tables can be much wider than chairs + return False, "Invalid monoleg" + return True, "Valid" diff --git a/dataset_generator/shape_validators/vase_validator.py b/dataset_generator/shape_validators/vase_validator.py new file mode 100644 index 0000000..6828d69 --- /dev/null +++ b/dataset_generator/shape_validators/vase_validator.py @@ -0,0 +1,20 @@ +from dataset_generator.shape_validators.shape_validator_interface import ShapeValidatorInterface +from common.intersection_util import find_self_intersections, find_cross_intersections + + +class VaseValidator(ShapeValidatorInterface): + def validate_shape(self, input_params_map) -> (bool, str): + body_self_intersections = find_self_intersections('Body Self Intersections') + if body_self_intersections > 0: + return False, "Self intersection in the body" + if input_params_map['handle_count'].get_value() > 0: + handle_self_intersections = find_self_intersections('Handle Self Intersections') + if handle_self_intersections > 0: + return False, "self intersection in the handle" + base_handle_intersections = find_self_intersections('Base and Handle Intersections') + if base_handle_intersections > 0: + return False, "Base intersects with handles" + floor_handle_intersections = find_self_intersections('Floor and Handle Intersections') + if floor_handle_intersections > 0: + return False, "Floor intersects with handles" + return True, "Valid" diff --git a/dataset_generator/sketch_generator.py b/dataset_generator/sketch_generator.py new file mode 100644 index 0000000..754af49 --- /dev/null +++ b/dataset_generator/sketch_generator.py @@ -0,0 +1,164 @@ +#!/usr/bin/env python3 + +import sys +import traceback +import bpy +import time +from mathutils import Vector +import math +import mathutils +import random +import argparse +from tqdm import tqdm +from pathlib import Path +import importlib + +def import_parents(level=1): + global __package__ + file = Path(__file__).resolve() + parent, top = file.parent, file.parents[level] + + sys.path.append(str(top)) + try: + sys.path.remove(str(parent)) + except ValueError: + # already removed + pass + + __package__ = '.'.join(parent.parts[len(top.parts):]) + importlib.import_module(__package__) + +if __name__ == '__main__' and __package__ is None: + import_parents(level=1) + +from common.bpy_util import normalize_scale, look_at, del_obj, clean_scene +from common.file_util import get_recipe_yml_obj, hash_file_name + + +""" +Shader references: + pencil shader - https://www.youtube.com/watch?v=71KGlu_Yxtg + white background (compositing) - https://www.youtube.com/watch?v=aegiN7XeLow + creating transparent object - https://www.katsbits.com/codex/transparency-cycles/ +""" + + +def main(dataset_dir: Path, phase, parallel, mod): + try: + clean_scene() + + # setup to avoid rendering surfaces and only render the freestyle curves + bpy.context.scene.view_layers["View Layer"].use_pass_z = False + bpy.context.scene.view_layers["View Layer"].use_pass_combined = False + bpy.context.scene.view_layers["View Layer"].use_sky = False + bpy.context.scene.view_layers["View Layer"].use_solid = False + bpy.context.scene.view_layers["View Layer"].use_volumes = False + bpy.context.scene.view_layers["View Layer"].use_strand = True # freestyle curves + + recipe_file_path = dataset_dir.joinpath('recipe.yml') + recipe_yml_obj = get_recipe_yml_obj(recipe_file_path) + camera_angles = recipe_yml_obj['camera_angles_train'] + recipe_yml_obj['camera_angles_test'] + # euler setting + radius = 2 + eulers = [mathutils.Euler((math.radians(camera_angle[0]), 0.0, math.radians(camera_angle[1])), 'XYZ') for camera_angle in camera_angles] + + obj_gt_dir = dataset_dir.joinpath(phase, 'obj_gt') + path_to_sketches = dataset_dir.joinpath(phase, 'sketches') # output folder + if (parallel == 1 or mod == 0) and not path_to_sketches.is_dir(): + path_to_sketches.mkdir() + + if parallel == 1 and mod != 0: + while not path_to_sketches.is_dir(): + time.sleep(2) + + obj_files = sorted(obj_gt_dir.glob('*.obj')) + # filter out files that were already processed + obj_files = [file for file in obj_files if + not all( + list(path_to_sketches.glob(f'{file.stem}_{camera_angle[0]}_{camera_angle[1]}.png')) + for camera_angle in camera_angles)] + # remove any file that is not handled in this job + if parallel > 1: + obj_files = [file for file in obj_files if hash_file_name(file.name) % parallel == mod] + + for obj_file in tqdm(obj_files): + file_name = obj_file.name + + filepath = obj_gt_dir.joinpath(file_name) + bpy.ops.import_scene.obj(filepath=str(filepath), axis_forward='-Z', axis_up='Y', filter_glob="*.obj;*.mtl") + obj = bpy.context.selected_objects[0] + + # normalize the object + normalize_scale(obj) + + for i, eul in enumerate(eulers): + filename_no_ext = obj_file.stem + target_file_name = f"{filename_no_ext}_{camera_angles[i][0]:.1f}_{camera_angles[i][1]:.1f}.png" + target_file = path_to_sketches.joinpath(target_file_name) + if target_file.is_file(): + continue + + # camera setting + cam_pos = mathutils.Vector((0.0, -radius, 0.0)) + cam_pos.rotate(eul) + if i < 4: + # camera position perturbation + rand_x = random.uniform(-2.0, 2.0) + rand_z = random.uniform(-3.0, 3.0) + eul_perturb = mathutils.Euler((math.radians(rand_x), 0.0, math.radians(rand_z)), 'XYZ') + cam_pos.rotate(eul_perturb) + + scene = bpy.context.scene + bpy.ops.object.camera_add(enter_editmode=False, location=cam_pos) + new_camera = bpy.context.active_object + new_camera.name = "camera_tmp" + new_camera.data.name = "camera_tmp" + new_camera.data.lens_unit = 'FOV' + new_camera.data.angle = math.radians(60) + look_at(new_camera, Vector((0.0, 0.0, 0.0))) + + # render + scene.camera = new_camera + scene.render.filepath = str(target_file) + scene.render.resolution_x = 224 + scene.render.resolution_y = 224 + bpy.context.scene.cycles.samples = 10 + bpy.ops.render.render(write_still=True) + + # prepare for the next camera + del_obj(new_camera) + + # delete the obj to prepare for the next one + del_obj(obj) + + # clean the scene + clean_scene() + except Exception as e: + print(repr(e)) + print(traceback.format_exc()) + + +if __name__ == "__main__": + argv = sys.argv + if '--' in sys.argv: + # refer to https://b3d.interplanety.org/en/how-to-pass-command-line-arguments-to-a-blender-python-script-or-add-on/ + argv = sys.argv[sys.argv.index('--') + 1:] + else: + raise Exception("Expected \'--\' followed by arguments to the script") + + parser = argparse.ArgumentParser() + parser.add_argument('--dataset-dir', type=str, required=True, help='Path to dataset directory') + parser.add_argument('--parallel', type=int, default=1, help='Number of processes that will run the script') + parser.add_argument('--mod', type=int, default=0, help='The modulo for this process to match files\' hash') + parser.add_argument('--phases', type=str, required=True, nargs='+', help='List of phases to generate the sketches for') + + args = parser.parse_args(argv) + + # hide the main collections (if it is already hidden, there is no effect) + bpy.context.scene.view_layers['View Layer'].layer_collection.children['Main'].hide_viewport = True + bpy.context.scene.view_layers['View Layer'].layer_collection.children['Main'].exclude = True + + dataset_dir = Path(args.dataset_dir).expanduser() + phases = args.phases + for phase in phases: + main(dataset_dir, phase, args.parallel, args.mod) diff --git a/dataset_processing/__init__.py b/dataset_processing/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/dataset_processing/prepare_coseg.py b/dataset_processing/prepare_coseg.py new file mode 100644 index 0000000..fd9426c --- /dev/null +++ b/dataset_processing/prepare_coseg.py @@ -0,0 +1,80 @@ +#!/usr/bin/env python3 + +import bpy +import sys +from tqdm import tqdm +from subprocess import Popen, PIPE +import argparse +from pathlib import Path +import importlib +import io +from contextlib import redirect_stdout + +def import_parents(level=1): + global __package__ + file = Path(__file__).resolve() + parent, top = file.parent, file.parents[level] + + sys.path.append(str(top)) + try: + sys.path.remove(str(parent)) + except ValueError: + # already removed + pass + + __package__ = '.'.join(parent.parts[len(top.parts):]) + importlib.import_module(__package__) + +if __name__ == '__main__' and __package__ is None: + import_parents(level=1) + +from common.bpy_util import normalize_scale + + +def main(): + if '--' in sys.argv: + # refer to https://b3d.interplanety.org/en/how-to-pass-command-line-arguments-to-a-blender-python-script-or-add-on/ + argv = sys.argv[sys.argv.index('--') + 1:] + else: + raise Exception("Expected \'--\' followed by arguments to the script") + + parser = argparse.ArgumentParser("prepare_coseg") + parser.add_argument('--shapes-dir', type=str, required=True, help='Path to COSEG raw shapes directory which contains the .off files') + parser.add_argument('--target-dataset-dir', type=str, required=True, help='Path to dataset directory where the normalized COSEG. obj files will be stored') + parser.add_argument('--target-phase', type=str, required=True, help='The name of the phase will hold the .obj files, e.g. \"coseg\"') + args = parser.parse_args(argv) + + shapes_dir = Path(args.shapes_dir) + target_dataset_dir = Path(args.target_dataset_dir).expanduser() + target_phase_dir = target_dataset_dir.joinpath(args.target_phase) + target_phase_dir.mkdir(exist_ok=True) + target_obj_gt_dir = target_phase_dir.joinpath('obj_gt') + target_obj_gt_dir.mkdir(exist_ok=True) + + print("Converting .off files to obj files...") + for off_file in tqdm(list(shapes_dir.glob('*.off'))): + obj_file = target_obj_gt_dir.joinpath(f'{off_file.stem}.obj') + if obj_file.is_file(): + continue + path_to_converter = Path(__file__).parent.joinpath('model-converter-python', 'convert.py').resolve() + cmd = [str(path_to_converter), '-i', str(off_file), '-o', str(obj_file)] + print(" ".join(cmd)) + process = Popen(cmd, stdout=PIPE) + process.wait() + + print("Normalizing obj files...") + for obj_file in tqdm(list(target_obj_gt_dir.glob("*.obj"))): + with redirect_stdout(io.StringIO()): + bpy.ops.import_scene.obj(filepath=str(obj_file)) + imported_object = bpy.context.selected_objects[0] + imported_object.data.materials.clear() + normalize_scale(imported_object) + bpy.ops.object.select_all(action='DESELECT') + bpy.context.view_layer.objects.active = imported_object + imported_object.select_set(True) + bpy.ops.export_scene.obj(filepath=str(obj_file), use_selection=True, use_materials=False, use_triangles=True) + bpy.ops.object.delete() + + +if __name__ == "__main__": + main() diff --git a/dataset_processing/save_obj.py b/dataset_processing/save_obj.py new file mode 100644 index 0000000..c55bfa1 --- /dev/null +++ b/dataset_processing/save_obj.py @@ -0,0 +1,77 @@ +#!/usr/bin/env python3 + +import sys +import bpy +import argparse +import traceback +from pathlib import Path +import importlib + +def import_parents(level=1): + global __package__ + file = Path(__file__).resolve() + parent, top = file.parent, file.parents[level] + sys.path.append(str(top)) + try: + sys.path.remove(str(parent)) + except ValueError: + # already removed + pass + + __package__ = '.'.join(parent.parts[len(top.parts):]) + importlib.import_module(__package__) + +if __name__ == '__main__' and __package__ is None: + import_parents(level=1) + +from common.file_util import get_recipe_yml_obj +from common.input_param_map import get_input_param_map, load_shape_from_yml +from common.bpy_util import clean_scene, select_shape, select_objs, get_geometric_nodes_modifier, save_obj + + +def save_obj_from_yml(args): + if args.simplification_ratio: + assert 0.0 <= args.simplification_ratio <= 1.0 + target_obj_file_path = Path(args.target_obj_file_path) + assert target_obj_file_path.suffix == ".obj" + clean_scene(start_with_strings=["Camera", "Light"]) + bpy.context.scene.view_layers['View Layer'].layer_collection.children['Main'].hide_viewport = False + bpy.context.scene.view_layers['View Layer'].layer_collection.children['Main'].exclude = False + obj = select_shape() + gnodes_mod = get_geometric_nodes_modifier(obj) + recipe_yml = get_recipe_yml_obj(args.recipe_file_path) + input_params_map = get_input_param_map(gnodes_mod, recipe_yml) + load_shape_from_yml(args.yml_file_path, input_params_map, ignore_sanity_check=args.ignore_sanity_check) + dup_obj = save_obj(target_obj_file_path, simplification_ratio=args.simplification_ratio) + bpy.data.collections["Main"].hide_render = False + chair_obj = select_shape() + dup_obj.hide_render = False + chair_obj.hide_render = True + dup_obj.data.materials.clear() + select_objs(dup_obj) + bpy.ops.object.delete() + + +def main(): + if '--' in sys.argv: + # refer to https://b3d.interplanety.org/en/how-to-pass-command-line-arguments-to-a-blender-python-script-or-add-on/ + argv = sys.argv[sys.argv.index('--') + 1:] + else: + raise Exception("Expected \'--\' followed by arguments to the script") + + parser = argparse.ArgumentParser(prog='save_obj') + parser.add_argument('--recipe-file-path', type=str, required=True, help='Path to recipe.yml file') + parser.add_argument('--yml-file-path', type=str, required=True, help='Path to yaml file to convert to object') + parser.add_argument('--target-obj-file-path', type=str, required=True, help='Path the obj file that will be created') + parser.add_argument('--simplification-ratio', type=float, default=None, help='Simplification ratio to decimate the mesh') + parser.add_argument('--ignore-sanity-check', action='store_true', default=False, help='Do not check the shape\'s parameters') + + try: + args = parser.parse_known_args(argv)[0] + save_obj_from_yml(args) + except Exception as e: + print(repr(e)) + print(traceback.format_exc()) + +if __name__ == '__main__': + main() diff --git a/dataset_processing/simplified_mesh_dataset.py b/dataset_processing/simplified_mesh_dataset.py new file mode 100644 index 0000000..ac7f510 --- /dev/null +++ b/dataset_processing/simplified_mesh_dataset.py @@ -0,0 +1,83 @@ +#!/usr/bin/env python3 + +import shutil +import argparse +import multiprocessing +from pathlib import Path +from functools import partial +from subprocess import Popen, PIPE + + +def simplify_and_save_obj_process(yml_file_path: Path, dst_phase_dir: Path, simplification_ratio: float, recipe_file_path, + blender_exe: Path, blend_file: Path): + assert 0.0 <= simplification_ratio <= 1.0 + simplification_ratio_str = f'{simplification_ratio:.3f}'.replace('.', '_') + out_file_name_no_ext = f'{yml_file_path.stem}_simplification_ratio_{simplification_ratio_str}' + print(f"Converting [{yml_file_path}] to obj file [{out_file_name_no_ext}]") + new_yml_path = dst_phase_dir.joinpath('yml_gt', f'{out_file_name_no_ext}.yml') + new_obj_path = dst_phase_dir.joinpath('obj_gt', f'{out_file_name_no_ext}.obj') + shutil.copy(yml_file_path, new_yml_path) + save_obj_script_path = Path(__file__).parent.joinpath('save_obj.py').resolve() + cmd = [str(blender_exe.expanduser()), str(blend_file.expanduser()), '-b', '--python', + str(save_obj_script_path), '--', + '--recipe-file-path', str(recipe_file_path), + '--yml-file-path', str(yml_file_path), + '--target-obj-file-path', str(new_obj_path), + '--simplification-ratio', str(simplification_ratio)] + print(" ".join(cmd)) + process = Popen(cmd, stdout=PIPE) + process.wait() + + +def main(): + parser = argparse.ArgumentParser("simplified_mesh_dataset") + parser.add_argument('--dataset-dir', type=str, required=True, help='Path to dataset directory') + parser.add_argument('--src-phase', type=str, required=True, help='Directory name of the source dataset phase') + parser.add_argument('--dst-phase', type=str, default='simplified', help='Directory name of the destination dataset phase') + parser.add_argument('--blender-exe', type=str, required=True, help='Path to blender executable') + parser.add_argument('--blend-file', type=str, required=True, help='Path to blend file') + parser.add_argument('--simplification-ratios', type=float, required=True, nargs='+', help='List of simplification ratios to generate datasets for,' + 'note that 1.0 means the original shape is used with no simplification') + args = parser.parse_args() + + assert all([0.0 <= simplification_ratio <= 1.0 for simplification_ratio in args.simplification_ratios]) + + ds_dir = Path(args.dataset_dir).expanduser() + src_phase_dir = ds_dir.joinpath(args.src_phase) + dst_phase_dir = ds_dir.joinpath(args.dst_phase) + if dst_phase_dir.is_dir(): + raise Exception(f'Simplified mesh dataset target directory [{dst_phase_dir}] already exists') + dst_phase_dir.mkdir() + + dest_yml_gt_path = dst_phase_dir.joinpath('yml_gt') + dest_yml_gt_path.mkdir(exist_ok=True) + dest_obj_gt_path = dst_phase_dir.joinpath('obj_gt') + dest_obj_gt_path.mkdir(exist_ok=True) + + blender_exe = Path(args.blender_exe) + blend_file = Path(args.blend_file) + + # create all the obj from the prediction yaml files + # note that for point cloud we have one yml and for sketch we have multiple yml files (one for each camera angle) + cpu_count = multiprocessing.cpu_count() + print(f"Generating simplified-mesh dataset in [{src_phase_dir}] with [{cpu_count}] processes") + recipe_file_path = Path(args.dataset_dir, 'recipe.yml') + src_yml_gt_path = src_phase_dir.joinpath('yml_gt') + yml_file_paths = [yml_file_path for yml_file_path in src_yml_gt_path.glob("*.yml")] + # note that 1.0 means no simplification takes place + for simplification_ratio in args.simplification_ratios: + print(f"Started generating simplified-mesh dataset with ratio [{simplification_ratio}]") + simplify_and_save_obj_process_partial = partial(simplify_and_save_obj_process, + dst_phase_dir=dst_phase_dir, + simplification_ratio=simplification_ratio, + recipe_file_path=recipe_file_path, + blender_exe=blender_exe, + blend_file=blend_file) + p = multiprocessing.Pool(cpu_count) + p.map(simplify_and_save_obj_process_partial, yml_file_paths) + p.close() + p.join() + + +if __name__ == "__main__": + main() diff --git a/environment.yml b/environment.yml new file mode 100644 index 0000000..293dced --- /dev/null +++ b/environment.yml @@ -0,0 +1,29 @@ +name: geocode +channels: + - pytorch + - defaults + - dglteam + - bottler + - conda-forge + - fvcore + - iopath + - pytorch3d +dependencies: + - python=3.8 + - pytorch=1.10.2 + - torchvision + - numpy=1.21.2 + - matplotlib=3.5.1 + - pytorch-lightning=1.5.10 + - neptune-client=0.16.4 + - nvidiacub=1.10.0 + - dgl=0.9.1 + - scikit-image=0.19.2 + - iopath=0.1.9 + - fvcore=0.1.5.post20210915 + - tqdm>=4.64.0 + - gdown>=4.5.4 + - pytorch3d + - pip + - pip: + - git+https://github.com/otaheri/chamfer_distance diff --git a/geocode/__init__.py b/geocode/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/geocode/barplot_util.py b/geocode/barplot_util.py new file mode 100644 index 0000000..c455d63 --- /dev/null +++ b/geocode/barplot_util.py @@ -0,0 +1,68 @@ +import json +import numpy as np + + +def gen_and_save_barplot(barplot_json_path, title, barplot_target_image_path=None): + from matplotlib import pyplot as plt + with open(barplot_json_path, 'r') as barplot_json_file: + data = json.load(barplot_json_file) + + inputs_to_eval = data['inputs_to_eval'] + correct_arr_pc = data['correct_arr_pc'] + correct_arr_sketch = data['correct_arr_sketch'] + total_pc = data['total_pc'] + total_sketch = data['total_sketch'] + + correct = [a + b for a, b in zip(correct_arr_pc, correct_arr_sketch)] + accuracy_avg = [a / (total_pc + total_sketch) for a in correct] + accuracy_pc = [a / total_pc for a in correct_arr_pc] + accuracy_sketch = [a / total_sketch for a in correct_arr_sketch] + + overall_acc_avg = (sum(correct_arr_pc) + sum(correct_arr_sketch)) / ( + len(inputs_to_eval) * (total_pc + total_sketch)) + overall_acc_pc = sum(correct_arr_pc) / (len(inputs_to_eval) * total_pc) + overall_acc_sketch = sum(correct_arr_sketch) / (len(inputs_to_eval) * total_sketch) + + is_only_sketches = False + is_only_pcs = False + if all([param_acc == 0 for param_acc in accuracy_pc]): + # only sketches + overall_acc_avg = overall_acc_sketch + is_only_sketches = True + if all([param_acc == 0 for param_acc in accuracy_sketch]): + # only pcs + overall_acc_avg = overall_acc_pc + is_only_pcs = True + + # sort by average accuracy + inputs_to_eval, accuracy_avg, accuracy_pc, accuracy_sketch = zip( + *sorted(zip(inputs_to_eval, accuracy_avg, accuracy_pc, accuracy_sketch), key=lambda x: x[1])) + + inputs_to_eval += ("Overall",) + accuracy_avg += (overall_acc_avg,) + accuracy_pc += (overall_acc_pc,) + accuracy_sketch += (overall_acc_sketch,) + + fig, ax = plt.subplots(figsize=(16, 14)) + X_axis = np.arange(len(inputs_to_eval)) * 2.6 + if not is_only_pcs and not is_only_sketches: + pps = ax.barh(X_axis + 0.7, accuracy_avg, 0.7, color='steelblue') + ax.barh(X_axis - 0.0, accuracy_pc, 0.7, color='lightsteelblue') + ax.barh(X_axis - 0.7, accuracy_sketch, 0.7, color='wheat') + ax.legend(labels=['Average', 'Point Clouds', 'Sketches']) + elif is_only_pcs: + pps = ax.barh(X_axis - 0.0, accuracy_pc, 0.7, color='lightsteelblue') + ax.legend(labels=['Point Clouds']) + elif is_only_sketches: + pps = ax.barh(X_axis - 0.7, accuracy_sketch, 0.7, color='wheat') + ax.legend(labels=['Sketches']) + else: + raise Exception("Either point cloud or sketch input should be processed") + + ax.bar_label(pps, fmt='%.2f', label_type='center', fontsize=8) + ax.set_yticks(X_axis, inputs_to_eval) + ax.set_title(title) + + if barplot_target_image_path: + plt.savefig(barplot_target_image_path) + return fig diff --git a/geocode/calculator_accuracy.py b/geocode/calculator_accuracy.py new file mode 100644 index 0000000..a04e28a --- /dev/null +++ b/geocode/calculator_accuracy.py @@ -0,0 +1,52 @@ +import torch +from calculator_util import eval_metadata + + +class AccuracyCalculator(): + def __init__(self, inputs_to_eval, param_descriptors): + self.inputs_to_eval = inputs_to_eval + self.normalized_classes_all, self.num_classes_all_shifted_cumulated, self.num_classes_all, self.regression_params_indices \ + = eval_metadata(inputs_to_eval, param_descriptors) + self.param_descriptors = param_descriptors + + def eval(self, pred, targets, top_k_acc): + batch_size = pred.shape[0] + device = targets.device + normalized_classes_all = self.normalized_classes_all.to(device) + num_classes_all_shifted_cumulated = self.num_classes_all_shifted_cumulated.to(device) + num_classes_all = self.num_classes_all.to(device) + correct = [[0] * len(self.inputs_to_eval) for _ in range(top_k_acc)] + targets_interleaved = torch.repeat_interleave(targets, num_classes_all.view(-1), dim=1) + normalized_classes_all_repeated = normalized_classes_all.repeat(batch_size, 1).to(device) + target_class = torch.abs(normalized_classes_all_repeated - targets_interleaved) + target_class = torch.where(target_class < 1e-3)[1].view(batch_size, -1) # take the indices along dim=1 since target is of size [1, param_count] + if len(self.regression_params_indices) > 0: + regression_params_indices_repeated = self.regression_params_indices.repeat(batch_size, 1).to(device) + target_class = torch.cat((target_class, regression_params_indices_repeated), dim=1) + target_class, _ = torch.sort(target_class, dim=1) + assert target_class.shape[1] == len(self.inputs_to_eval) + target_class = target_class - num_classes_all_shifted_cumulated + pred_split = torch.split(pred, list(num_classes_all), dim=1) + class_indices_diff = [(torch.argmax(p, dim=1) - t if p.shape[1] > 1 else None) for p, t in zip( pred_split, target_class.T )] + + l1_distance = [None] * targets.shape[1] + if len(self.regression_params_indices) > 0: + for param_idx, (p, t) in enumerate(zip(pred_split, targets.T)): + if self.param_descriptors[self.inputs_to_eval[param_idx]].is_regression: + adjusted_pred = p[:, 0].clone() + adjusted_pred[p[:, 1] >= 0.5] = -1.0 + l1_distance[param_idx] = torch.abs(adjusted_pred.squeeze() - t) + + for i, param_name in enumerate(self.inputs_to_eval): + if self.param_descriptors[param_name].is_regression: + # regression parameter + normalized_acc_threshold = self.param_descriptors[param_name].normalized_acc_threshold + for j in range(top_k_acc): + assert len(l1_distance[i]) == batch_size + correct[j][i] += torch.sum((l1_distance[i] < normalized_acc_threshold * (j + 1)).int()).item() + else: + cid = class_indices_diff[i] + assert len(cid) == batch_size + for j in range(top_k_acc): + correct[j][i] += len(cid[(cid <= j) & (cid >= -j)]) + return correct diff --git a/geocode/calculator_loss.py b/geocode/calculator_loss.py new file mode 100644 index 0000000..2800e12 --- /dev/null +++ b/geocode/calculator_loss.py @@ -0,0 +1,58 @@ +import torch +from calculator_util import eval_metadata + + +MSE = torch.nn.MSELoss() +CElossSum = torch.nn.CrossEntropyLoss(reduction='sum') + + +class LossCalculator(): + def __init__(self, inputs_to_eval, param_descriptors): + self.inputs_to_eval = inputs_to_eval + self.normalized_classes_all, self.num_classes_all_shifted_cumulated, self.num_classes_all, self.regression_params_indices \ + = eval_metadata(inputs_to_eval, param_descriptors) + self.param_descriptors = param_descriptors + + def loss(self, pred, targets): + """ + _pred: (B, TARGET_VEC_LEN) + """ + batch_size = pred.shape[0] + device = targets.device + normalized_classes_all = self.normalized_classes_all.to(device) + num_classes_all_shifted_cumulated = self.num_classes_all_shifted_cumulated.to(device) + num_classes_all = self.num_classes_all.to(device) + targets_interleaved = torch.repeat_interleave(targets, num_classes_all.view(-1), dim=1) + normalized_classes_all_repeated = normalized_classes_all.repeat(batch_size, 1).to(device) + target_class = torch.abs(normalized_classes_all_repeated - targets_interleaved) + target_class = torch.where(target_class < 1e-3)[1].view(batch_size, -1) # take the indices along dim=1 + if len(self.regression_params_indices) > 0: + regression_params_indices_repeated = self.regression_params_indices.repeat(batch_size, 1).to(device) + target_class = torch.cat((target_class, regression_params_indices_repeated), dim=1) + target_class, _ = torch.sort(target_class, dim=1) + assert target_class.shape[1] == len(self.inputs_to_eval) + target_class = target_class - num_classes_all_shifted_cumulated + # target_class = target_class.to(_pred.get_device()) + pred_split = torch.split(pred, list(num_classes_all), dim=1) + detailed_ce_loss = [(CElossSum(p, t) if p.shape[1] > 1 else None) for p, t in zip( pred_split, target_class.T )] + + detailed_mse_loss = [None] * targets.shape[1] + if len(self.regression_params_indices) > 0: + for param_idx, (p, t) in enumerate(zip(pred_split, targets.T)): + if self.param_descriptors[self.inputs_to_eval[param_idx]].is_regression: + t_visibility = torch.zeros(t.shape[0]) + t_visibility[t >= 0.0] = 0.0 + t_visibility[t == -1.0] = 1.0 + t_visibility = t_visibility.to(device) + t_clone = t.clone() + t_clone = t_clone.float() + t_clone[t_clone == -1] = p[t_clone == -1,0] + t_adjusted = torch.concat((t_clone.unsqueeze(1), t_visibility.unsqueeze(1)), dim=1) + detailed_mse_loss[param_idx] = MSE(p, t_adjusted) + detailed_mse_loss_no_none = [e for e in detailed_mse_loss if e] + detailed_ce_loss_no_none = [e for e in detailed_ce_loss if e] + mse_loss_range = 1.0 if not detailed_mse_loss_no_none else (max(detailed_mse_loss_no_none).item() - min(detailed_mse_loss_no_none).item()) + ce_loss_range = max(detailed_ce_loss_no_none).item() - min(detailed_ce_loss_no_none).item() + detailed_loss = [(ce_loss / ce_loss_range) if not mse_loss else (mse_loss / mse_loss_range) for ce_loss, mse_loss in zip(detailed_ce_loss, detailed_mse_loss)] + + return sum(detailed_loss), detailed_loss diff --git a/geocode/calculator_util.py b/geocode/calculator_util.py new file mode 100644 index 0000000..3f50427 --- /dev/null +++ b/geocode/calculator_util.py @@ -0,0 +1,27 @@ +import torch +from typing import Dict, List +from common.param_descriptors import ParamDescriptor + + +def eval_metadata(inputs_to_eval: List[str], param_descriptors_map: Dict[str, ParamDescriptor]): + num_classes_all = torch.empty(0) + normalized_classes_all = torch.empty(0) + for i, param_name in enumerate(inputs_to_eval): + param_descriptor = param_descriptors_map[param_name] + num_classes = param_descriptor.num_classes # Including the visibility label. If using regression then num_classes=2. + num_classes_all = torch.cat((num_classes_all, torch. tensor([num_classes]))).long() + if param_descriptor.normalized_classes is not None: + normalized_classes = torch.from_numpy(param_descriptor.normalized_classes) + else: + # high values so that eval and loss methods will work when using regression + normalized_classes = torch.tensor([100000.0, 100000.0]) + normalized_classes_all = torch.cat((normalized_classes_all, normalized_classes.view(-1))) + num_classes_all_shifted = torch.cat((torch.tensor([0]), num_classes_all))[0:-1] # shift right + drop right-most element + num_classes_all_shifted_cumulated = torch.cumsum(num_classes_all_shifted, dim=0).view(1, -1) + + # get the indices of all the regression params, then shift them to match the expanded vector + regression_params = torch.tensor([param_descriptors_map[param_name].is_regression for param_name in inputs_to_eval], dtype=torch.int) + regression_params_indices = torch.where(regression_params)[0] + regression_params_indices = torch.tensor([num_classes_all_shifted_cumulated[0, idx] for idx in regression_params_indices]) + + return normalized_classes_all, num_classes_all_shifted_cumulated, num_classes_all, regression_params_indices diff --git a/geocode/geocode.py b/geocode/geocode.py new file mode 100644 index 0000000..a58f827 --- /dev/null +++ b/geocode/geocode.py @@ -0,0 +1,62 @@ +#!/usr/bin/env python3 + +import argparse +from geocode_util import InputType +from geocode_train import train +from geocode_test import test + + +def str2bool(v): + if isinstance(v, bool): + return v + if v.lower() in ('yes', 'true', 't', 'y', '1'): + return True + elif v.lower() in ('no', 'false', 'f', 'n', '0'): + return False + else: + raise argparse.ArgumentTypeError('Boolean value expected but got [{}].'.format(v)) + + +def main(): + parser = argparse.ArgumentParser(prog='ShapeEditing') + + common_parser = argparse.ArgumentParser(add_help=False) + common_parser.add_argument('--dataset-dir', type=str, required=True, help='Path to dataset directory') + common_parser.add_argument('--models-dir', type=str, required=True, help='Directory where experiments will be saved') + common_parser.add_argument('--exp-name', type=str, required=True, help='Experiment directory within the models directory, where checkpoints will be saved') + common_parser.add_argument('--input-type', type=InputType, nargs='+', default='pc sketch', help='Either \"pc\", \"sketch\" or \"pc sketch\"') + common_parser.add_argument('--increase-network-size', action='store_true', default=False, help='Use larger encoders networks sizes') + common_parser.add_argument('--normalize-embeddings', action='store_true', default=False, help='Normalize embeddings before using the decoders') + common_parser.add_argument('--pretrained-vgg', action='store_true', default=False, help='Use a pretrained VGG network') + common_parser.add_argument('--use-regression', action='store_true', default=False, help='Use regression instead of classification for continuous parameters') + + sp = parser.add_subparsers() + sp_train = sp.add_parser('train', parents=[common_parser]) + sp_test = sp.add_parser('test', parents=[common_parser]) + + sp_train.set_defaults(func=train) + sp_test.set_defaults(func=test) + + sp_train.add_argument('--batch_size', type=int, required=True, help='Batch size') + sp_train.add_argument('--nepoch', type=int, required=True, help='Number of epochs to train') + + sp_test.add_argument('--phase', type=str, default='test') + sp_test.add_argument('--blender-exe', type=str, required=True, help='Path to blender executable') + sp_test.add_argument('--blend-file', type=str, required=True, help='Path to blend file') + sp_test.add_argument('--random-pc', type=int, default=None, help='Use only random point cloud sampling with specified number of points') + sp_test.add_argument('--gaussian', type=float, default=0.0, help='Add Gaussian noise to the point cloud with the specified STD') + sp_test.add_argument('--normalize-pc', action='store_true', default='False', help='Automatically normalize the input point clouds') + sp_test.add_argument('--scanobjectnn', action='store_true', default='False', help='ScanObjectNN dataset which has only point clouds input') + # we augment in phases "train", "val", "test" and experiments "coseg", "simplify_mesh", and "gaussian" + # use `--augment-with-random-points false` to disable + sp_test.add_argument('--augment-with-random-points', type=str2bool, default='True', help='Augment FPS point cloud with randomly sampled points') + + args = parser.parse_args() + args.func(args) + + # either pc or sketch, or both must be trained + assert InputType.pc in args.input_type or InputType.sketch in args.input_type + + +if __name__ == "__main__": + main() diff --git a/geocode/geocode_model.py b/geocode/geocode_model.py new file mode 100644 index 0000000..9e339bd --- /dev/null +++ b/geocode/geocode_model.py @@ -0,0 +1,339 @@ +import yaml +import json +import shutil +import torch +import torch.optim as optim +from torch.optim.lr_scheduler import StepLR +import pytorch_lightning as pl +from barplot_util import gen_and_save_barplot +from neptune.new.types import File +from models.dgcnn import DGCNN +from models.vgg import vgg11_bn +from models.decoder import DecodersNet +from calculator_accuracy import AccuracyCalculator +from calculator_loss import LossCalculator +from common.param_descriptors import ParamDescriptors +from pathlib import Path +from geocode_util import InputType + + +class Model(pl.LightningModule): + def __init__(self, top_k_acc, batch_size, detailed_vec_size, increase_network_size, normalize_embeddings, + pretrained_vgg, input_type, inputs_to_eval, lr, sched_step_size, sched_gamma, + exp_name=None, trainer=None, param_descriptors: ParamDescriptors = None, models_dir: Path = None, + results_dir: Path = None, test_dir: Path = None, test_dataloaders_types=None, test_input_type=None, + use_regression=False): + super().__init__() + # saved hyper parameters + self.input_type = input_type + self.inputs_to_eval = inputs_to_eval + self.batch_size = batch_size + self.lr = lr + self.sched_step_size = sched_step_size + self.sched_gamma = sched_gamma + self.top_k_acc = top_k_acc + + # non-saved parameters + self.trainer = trainer + self.param_descriptors = param_descriptors + self.param_descriptors_map = self.param_descriptors.get_param_descriptors_map() + self.results_dir = results_dir + self.test_dir = test_dir + self.exp_name = exp_name + self.models_dir = models_dir + self.test_dataloaders_types = test_dataloaders_types + self.test_type = test_input_type + self.use_regression = use_regression + + regression_params = None + if self.use_regression: + regression_params = [param_descriptor.input_type == "Float" or param_descriptor.input_type == "Vector" for + param_name, param_descriptor in self.param_descriptors_map.items()] + self.acc_calc = AccuracyCalculator(self.inputs_to_eval, self.param_descriptors_map) + self.loss_calc = LossCalculator(self.inputs_to_eval, self.param_descriptors_map) + self.decoders_net = DecodersNet(output_channels=detailed_vec_size, increase_network_size=increase_network_size, + regression_params=regression_params) + self.dgcnn = None + self.vgg = None + if InputType.pc in self.input_type: + self.dgcnn = DGCNN(increase_network_size=increase_network_size, normalize_embeddings=normalize_embeddings) + if InputType.sketch in self.input_type: + self.vgg = vgg11_bn(pretrained=pretrained_vgg, progress=True, encoder_only=True, + increase_network_size=increase_network_size, normalize_embeddings=normalize_embeddings) + + self.save_hyperparameters( + ignore=["trainer", "param_descriptors", "models_dir", "results_dir", "test_dir", "test_input_type", + "use_regression", "exp_name", "test_dataloaders_types"]) + + def configure_optimizers(self): + params = list(self.decoders_net.parameters()) + if InputType.pc in self.input_type: + params += list(self.dgcnn.parameters()) + if InputType.sketch in self.input_type: + params += list(self.vgg.parameters()) + optimizer = optim.Adam(params, lr=self.lr) + lr_scheduler = StepLR(optimizer, step_size=self.sched_step_size, gamma=self.sched_gamma) + return [optimizer], [lr_scheduler] + + def log_accuracy(self, phase, correct_arr, metric_type): + """ + :param phase: e.g. "train" or "val" + :param correct_arr: two dim array, rows are k in top-k accuracy, cols are the parameters + the value is the number of correct predictions for a parameter when considering + top-k accuracy (top-k with k=1 is equivalent to argmax) + :param metric_type: either "pc" or "sketch" + """ + for i, param_name in enumerate(self.inputs_to_eval): + for j in range(self.top_k_acc): + acc_metric = correct_arr[j][i] / self.batch_size + self.log(f"{phase}/acc_top{j + 1}/{metric_type}/{param_name}", acc_metric, on_step=False, on_epoch=True, + logger=True, batch_size=self.batch_size) + for j in range(self.top_k_acc): + acc_avg_metric = sum(correct_arr[j]) / (self.batch_size * len(self.inputs_to_eval)) + self.log(f"{phase}/acc_top{j + 1}/{metric_type}/avg", acc_avg_metric, on_step=False, on_epoch=True, + logger=True, batch_size=self.batch_size) + + def get_decoder_loss(self, pc_emb, sketch_emb, targets_pcs, targets_sketches): + if InputType.pc in self.input_type: + batch_size = pc_emb.shape[0] + else: + batch_size = sketch_emb.shape[0] + pred_pc = None + pred_sketch = None + if InputType.pc in self.input_type and InputType.sketch in self.input_type: + targets_both = torch.cat((targets_pcs, targets_sketches), dim=0) + pred_both = self.decoders_net.decode(torch.cat((pc_emb, sketch_emb), dim=0)) + pred_pc = pred_both[:batch_size, :] + pred_sketch = pred_both[batch_size:, :] + decoders_loss, detailed_decoder_loss = self.loss_calc.loss(pred_both, targets_both) + elif InputType.pc in self.input_type: + pred_pc = self.decoders_net.decode(pc_emb) + decoders_loss, detailed_decoder_loss = self.loss_calc.loss(pred_pc, targets_pcs) + elif InputType.sketch in self.input_type: + pred_sketch = self.decoders_net.decode(sketch_emb) + decoders_loss, detailed_decoder_loss = self.loss_calc.loss(pred_sketch, targets_sketches) + else: + raise Exception("Illegal input type") + return decoders_loss, detailed_decoder_loss, pred_pc, pred_sketch + + def training_step(self, train_batch, batch_idx): + targets_pcs = None + targets_sketches = None + pc_emb = None + sketch_emb = None + if InputType.pc in self.input_type: + _, pcs, targets_pcs, _ = train_batch["pc"] + pcs = pcs.transpose(2, 1) + pc_emb = self.dgcnn(pcs) + if InputType.sketch in self.input_type: + _, _, sketches, targets_sketches, _ = train_batch["sketch"] + sketch_emb = self.vgg(sketches) + + decoders_loss, detailed_decoder_loss, pred_pc, pred_sketch = self.get_decoder_loss(pc_emb, sketch_emb, + targets_pcs, + targets_sketches) + # log detailed decoding loss + for i, param_name in enumerate(self.inputs_to_eval): + self.log(f"train/loss/{param_name}", detailed_decoder_loss[i], on_step=False, on_epoch=True, logger=True, + batch_size=self.batch_size) + + train_loss = decoders_loss + self.log("train/loss/total", train_loss, on_step=False, on_epoch=True, logger=True, batch_size=self.batch_size) + + # compute and log accuracy for point cloud and sketch + if InputType.pc in self.input_type: + correct_arr_pc = self.acc_calc.eval(pred_pc, targets_pcs, self.top_k_acc) + self.log_accuracy("train", correct_arr_pc, "pc") + if InputType.sketch in self.input_type: + correct_arr_sketch = self.acc_calc.eval(pred_sketch, targets_sketches, self.top_k_acc) + self.log_accuracy("train", correct_arr_sketch, "sketch") + + return train_loss + + def validation_step(self, val_batch, batch_idx): + targets_pcs = None + targets_sketches = None + pc_emb = None + sketch_emb = None + if InputType.pc in self.input_type: + _, pcs, targets_pcs, _ = val_batch["pc"] + pcs = pcs.transpose(2, 1) + pc_emb = self.dgcnn(pcs) + if InputType.sketch in self.input_type: + _, _, sketches, targets_sketches, _ = val_batch["sketch"] + sketch_emb = self.vgg(sketches) + + decoders_loss, detailed_decoder_loss, pred_pc, pred_sketch = self.get_decoder_loss(pc_emb, sketch_emb, + targets_pcs, + targets_sketches) + # log detailed decoding loss + for i, param_name in enumerate(self.inputs_to_eval): + self.log(f"val/loss/{param_name}", detailed_decoder_loss[i], on_step=False, on_epoch=True, logger=True, + batch_size=self.batch_size) + val_loss = decoders_loss + self.log("val/loss/total", val_loss, on_step=False, on_epoch=True, logger=True, batch_size=self.batch_size) + + # compute and log accuracy for point cloud and sketch + correct_arr_pc = None + if InputType.pc in self.input_type: + correct_arr_pc = self.acc_calc.eval(pred_pc, targets_pcs, self.top_k_acc) + self.log_accuracy("val", correct_arr_pc, "pc") + correct_arr_sketch = None + if InputType.sketch in self.input_type: + correct_arr_sketch = self.acc_calc.eval(pred_sketch, targets_sketches, self.top_k_acc) + self.log_accuracy("val", correct_arr_sketch, "sketch") + + # log average validation accuracy + pc_acc_avg = [] + if InputType.pc in self.input_type: + for j in range(self.top_k_acc): + curr_avg = sum(correct_arr_pc[j]) / (self.batch_size * len(self.inputs_to_eval)) + pc_acc_avg.append(curr_avg) + sketch_acc_avg = [] + if InputType.sketch in self.input_type: + for j in range(self.top_k_acc): + curr_avg = sum(correct_arr_sketch[j]) / (self.batch_size * len(self.inputs_to_eval)) + sketch_acc_avg.append(curr_avg) + avg_acc = [] + for j in range(self.top_k_acc): + if InputType.pc in self.input_type and InputType.sketch in self.input_type: + curr_avg = (pc_acc_avg[j] + sketch_acc_avg[j]) / 2 + elif InputType.pc in self.input_type: + curr_avg = pc_acc_avg[j] + elif InputType.sketch in self.input_type: + curr_avg = sketch_acc_avg[j] + else: + raise Exception("Illegal input type") + avg_acc.append(curr_avg) + self.log(f"val/acc_top{j + 1}/avg", curr_avg, on_step=False, on_epoch=True, logger=True, + batch_size=self.batch_size) + return avg_acc, correct_arr_pc, correct_arr_sketch + + def validation_epoch_end(self, validation_step_outputs): + num_batches = len(validation_step_outputs) + num_samples = num_batches * self.batch_size + for j in range(self.top_k_acc): + avg_acc = 0 + correct_arr_pc = [0] * len(self.inputs_to_eval) + correct_arr_sketch = [0] * len(self.inputs_to_eval) + for avg_acc_batch, correct_arr_pc_batch, correct_arr_sketch_batch in validation_step_outputs: + avg_acc += avg_acc_batch[j] + for i in range(len(self.inputs_to_eval)): + if correct_arr_pc_batch: + correct_arr_pc[i] += correct_arr_pc_batch[j][i] + if correct_arr_sketch_batch: + correct_arr_sketch[i] += correct_arr_sketch_batch[j][i] + avg_acc /= len(validation_step_outputs) + + if f'val/acc_top{j + 1}/best_avg' in self.trainer.callback_metrics: + best_avg_val_acc = max(self.trainer.callback_metrics[f'val/acc_top{j + 1}/best_avg'], avg_acc) + else: + best_avg_val_acc = avg_acc + self.log(f"val/acc_top{j + 1}/best_avg", best_avg_val_acc, on_step=False, on_epoch=True, logger=True, + batch_size=self.batch_size) + if avg_acc == best_avg_val_acc: + barplot_data = { + "inputs_to_eval": self.inputs_to_eval, + "correct_arr_pc": correct_arr_pc, + "total_pc": num_samples, + "correct_arr_sketch": correct_arr_sketch, + "total_sketch": num_samples, + "accuracy_top_k": j + 1, + } + barplot_data_file_path = f'{self.models_dir}/{self.exp_name}/val_barplot_top_{j + 1}.json' + with open(barplot_data_file_path, 'w') as barplot_data_file: + json.dump(barplot_data, barplot_data_file) + # log the bar plot as image + fig = gen_and_save_barplot(barplot_data_file_path, + title=f"Validation Accuracy Top {j + 1} @ Epoch {self.trainer.current_epoch}") + if self.logger: + self.logger.run["barplot"].log(File.as_image(fig)) + + def test_step(self, test_batch, batch_idx, dataloader_idx=0): + assert self.batch_size == 1 + + pc, sketch = None, None + if self.test_dataloaders_types[dataloader_idx] == 'pc': + file_name, pc, targets, shape = test_batch + elif self.test_dataloaders_types[dataloader_idx] == 'sketch': + file_name, sketch_camera_angle, sketch, targets, shape = test_batch + else: + raise Exception(f"Unrecognized dataloader type [{self.test_dataloaders_types[dataloader_idx]}]") + + if pc is not None: + pcs = pc.transpose(2, 1) + pred_pc = self.decoders_net.decode(self.dgcnn(pcs)) + pred_map_pc = self.param_descriptors.convert_prediction_vector_to_map(pred_pc.cpu(), use_regression=self.use_regression) + pc_pred_yaml_file_path = self.results_dir.joinpath('yml_predictions_pc', f'{file_name[0]}_pred_pc.yml') + with open(pc_pred_yaml_file_path, 'w') as yaml_file: + yaml.dump(pred_map_pc, yaml_file) + elif sketch is not None: + pred_sketch = self.decoders_net.decode(self.vgg(sketch)) + pred_map_sketch = self.param_descriptors.convert_prediction_vector_to_map(pred_sketch.cpu(), use_regression=self.use_regression) + sketch_pred_yaml_file_path = self.results_dir.joinpath('yml_predictions_sketch', + f'{file_name[0]}_{sketch_camera_angle[0]}_pred_sketch.yml') + with open(sketch_pred_yaml_file_path, 'w') as yaml_file: + yaml.dump(pred_map_sketch, yaml_file) + else: + raise Exception("No pc and no sketch input") + + # compute accuracy for point cloud and sketch + correct_arr_pc = None + correct_arr_sketch = None + if shape: + # this means we had a target vector to compare against + if pc is not None: + correct_arr_pc = self.acc_calc.eval(pred_pc, targets, self.top_k_acc) + if sketch is not None: + correct_arr_sketch = self.acc_calc.eval(pred_sketch, targets, self.top_k_acc) + # save ground truth yaml + expanded_targets_vector = self.param_descriptors.expand_target_vector(targets.cpu()) + gt_map = self.param_descriptors.convert_prediction_vector_to_map(expanded_targets_vector, use_regression=self.use_regression) + gt_yaml_file_path = self.results_dir.joinpath('yml_gt', f'{file_name[0]}_gt.yml') + with open(gt_yaml_file_path, 'w') as yaml_file: + yaml.dump(gt_map, yaml_file) + + # save the gt sketches, note that obj gt are saved during the visualization step + sketches_dir = self.test_dir.joinpath("sketches") + if sketches_dir.is_dir(): + sketch_files = sketches_dir.glob(f'{file_name[0]}*') + for sketch_file in sketch_files: + shutil.copy(sketch_file, self.results_dir.joinpath('sketch_gt', sketch_file.name)) + + return correct_arr_pc, correct_arr_sketch + + def test_epoch_end(self, test_step_outputs): + assert self.batch_size == 1 + assert self.test_type + if InputType.pc in self.test_type and InputType.sketch in self.test_type: + test_step_outputs_pc_and_sketch = test_step_outputs[0] + test_step_outputs[1] + else: + test_step_outputs_pc_and_sketch = test_step_outputs + for top_k in range(self.top_k_acc): + total_pcs = 0 + total_sketches = 0 + correct_arr_pc = [0] * len(self.inputs_to_eval) + correct_arr_sketch = [0] * len(self.inputs_to_eval) + for correct_arr_pc_batch, correct_arr_sketch_batch in test_step_outputs_pc_and_sketch: + if correct_arr_pc_batch is not None: + total_pcs += 1 + for i in range(len(self.inputs_to_eval)): + correct_arr_pc[i] += correct_arr_pc_batch[top_k][i] + if correct_arr_sketch_batch is not None: + total_sketches += 1 + for i in range(len(self.inputs_to_eval)): + correct_arr_sketch[i] += correct_arr_sketch_batch[top_k][i] + barplot_data = { + "inputs_to_eval": self.inputs_to_eval, + "correct_arr_pc": correct_arr_pc, + "total_pc": total_pcs, + "correct_arr_sketch": correct_arr_sketch, + "total_sketch": total_sketches, + "accuracy_top_k": top_k + 1, + } + if total_pcs > 0 and total_sketches > 0: + print( + f'Saving test barplot for [{total_pcs}] point cloud sampels and [{total_sketches}] sketch samples') + barplot_data_file_path = f'{self.models_dir}/{self.exp_name}/test_barplot_top_{top_k + 1}.json' + with open(barplot_data_file_path, 'w') as barplot_data_file: + json.dump(barplot_data, barplot_data_file) diff --git a/geocode/geocode_test.py b/geocode/geocode_test.py new file mode 100644 index 0000000..0977ec3 --- /dev/null +++ b/geocode/geocode_test.py @@ -0,0 +1,262 @@ +import json +import torch +import shutil +import traceback +import numpy as np +import multiprocessing +from pathlib import Path +from functools import partial +import pytorch_lightning as pl +from subprocess import Popen, PIPE +from data.dataset_pc import DatasetPC +from data.dataset_sketch import DatasetSketch +from barplot_util import gen_and_save_barplot +from common.param_descriptors import ParamDescriptors +from geocode_util import InputType, get_inputs_to_eval, calc_prediction_vector_size +from geocode_model import Model +from torch.utils.data import DataLoader +from chamfer_distance import ChamferDistance as chamfer_dist +from common.sampling_util import sample_surface +from common.file_util import load_obj, get_recipe_yml_obj +from common.point_cloud_util import normalize_point_cloud + + +def sample_pc_random(obj_path, num_points=10_000, apply_point_cloud_normalization=False): + """ + Chamfer evaluation + """ + vertices, faces = load_obj(obj_path) + vertices = vertices.reshape(1, vertices.shape[0], vertices.shape[1]) + vertices = torch.from_numpy(vertices) + faces = torch.from_numpy(faces) + point_cloud = sample_surface(faces, vertices, num_points=num_points) + if apply_point_cloud_normalization: + point_cloud = normalize_point_cloud(point_cloud) + # assert that the point cloud is normalized + max_dist_in_pc = torch.max(torch.sqrt(torch.sum((point_cloud ** 2), dim=1))) + threshold = 0.1 + assert abs(1.0 - max_dist_in_pc) <= threshold, f"PC of obj [{obj_path}] is not normalized, max distance in PC was [{abs(1.0 - max_dist_in_pc)}] but required to be <= [{threshold}]." + return point_cloud + + +def get_chamfer_distance(target_pc, gt_pc, device, num_points_in_pc, check_rot=False): + """ + num_points_in_pc - for sanity check + check_rot - was done for the tables since they are symmetric, and sketches are randomly flipped + it is ok to leave it on for the vase and chair, just makes it slower + """ + gt_pc = gt_pc.reshape(1, gt_pc.shape[0], gt_pc.shape[1]) # add batch dim + target_pc = target_pc.reshape(1, target_pc.shape[0], target_pc.shape[1]) # add batch dim + assert gt_pc.shape[1] == target_pc.shape[1] == num_points_in_pc + dist1, dist2, idx1, idx2 = chamfer_dist(target_pc.float().to(device), gt_pc.float().to(device)) + chamfer_distance = (torch.sum(dist1) + torch.sum(dist2)) / (gt_pc.shape[1] * 2) + if check_rot: + rot_mat = torch.tensor([[0.0, 0.0, -1.0], [0.0, 1.0, 0.0], [1.0, 0.0, 0.0]], dtype=torch.float64) + target_pc_rot = torch.matmul(rot_mat, target_pc.squeeze().t()).t().unsqueeze(0) + dist1, dist2, idx1, idx2 = chamfer_dist(target_pc_rot.float().to(device), gt_pc.float().to(device)) + chamfer_distance_rot = (torch.sum(dist1) + torch.sum(dist2)) / (gt_pc.shape[1] * 2) + return torch.min(chamfer_distance, chamfer_distance_rot) + return chamfer_distance + + +def save_as_obj_proc(pred_yml_file_path: Path, recipe_file_path: Path, results_dir: Path, out_dir: str, blender_exe: str, blend_file: str): + target_obj_file_path = results_dir.joinpath(out_dir, f'{pred_yml_file_path.stem}.obj') + print(f"Converting [{pred_yml_file_path}] to obj file [{target_obj_file_path}]") + save_obj_script_path = Path(__file__).parent.joinpath('..', 'dataset_processing', 'save_obj.py').resolve() + cmd = [f'{str(Path(blender_exe).expanduser())}', f'{str(Path(blend_file).expanduser())}', '-b', '--python', + f"{str(save_obj_script_path)}", '--', + '--recipe-file-path', str(recipe_file_path), + '--yml-file-path', str(pred_yml_file_path), + '--target-obj-file-path', str(target_obj_file_path), + '--ignore-sanity-check'] + print(" ".join(cmd)) + process = Popen(cmd, stdout=PIPE) + process.wait() + + +def test(opt): + recipe_file_path = Path(opt.dataset_dir, 'recipe.yml') + print(recipe_file_path) + if not recipe_file_path.is_file(): + raise Exception(f'No \'recipe.yml\' file found in path [{recipe_file_path}]') + recipe_yml_obj = get_recipe_yml_obj(str(recipe_file_path)) + + inputs_to_eval = get_inputs_to_eval(recipe_yml_obj) + + device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu") + camera_angles_to_process = recipe_yml_obj['camera_angles_train'] + recipe_yml_obj['camera_angles_test'] + camera_angles_to_process = [f'{a}_{b}' for a, b in camera_angles_to_process] + + param_descriptors = ParamDescriptors(recipe_yml_obj, inputs_to_eval, opt.use_regression) + param_descriptors_map = param_descriptors.get_param_descriptors_map() + detailed_vec_size = calc_prediction_vector_size(param_descriptors_map) + print(f"Prediction vector length is set to [{sum(detailed_vec_size)}]") + + # setup required dirs + required_dirs = ['barplot', + 'yml_gt', 'yml_predictions_pc', 'yml_predictions_sketch', + 'obj_gt', 'obj_predictions_pc', 'obj_predictions_sketch', + 'render_gt', 'render_predictions_pc', 'render_predictions_sketch', + 'sketch_gt'] + test_dir = Path(opt.dataset_dir, opt.phase) + test_dir_obj_gt = test_dir.joinpath('obj_gt') + results_dir = test_dir.joinpath(f'results_{opt.exp_name}') + results_dir.mkdir(exist_ok=True) + for dir in required_dirs: + results_dir.joinpath(dir).mkdir(exist_ok=True) + + # save the recipe to the results directory + shutil.copy(recipe_file_path, results_dir.joinpath('recipe.yml')) + + # find the best checkpoint (the one with the highest epoch number out of the saved checkpoints) + exp_dir = Path(opt.models_dir, opt.exp_name) + best_model_and_highest_epoch = None + highest_epoch = 0 + for ckpt_file in exp_dir.glob("*.ckpt"): + file_name = ckpt_file.name + if 'epoch' not in file_name: + continue + epoch_start_idx = file_name.find('epoch') + len('epoch') + epoch = int(file_name[epoch_start_idx:epoch_start_idx + 3]) + if epoch > highest_epoch: + best_model_and_highest_epoch = ckpt_file + highest_epoch = epoch + print(f'Best model with highest epoch is [{best_model_and_highest_epoch}]') + + batch_size = 1 + test_dataloaders = [] + test_dataloaders_types = [] + # pc + if InputType.pc in opt.input_type: + test_dataset_pc = DatasetPC(inputs_to_eval, device, param_descriptors_map, + opt.dataset_dir, opt.phase, random_pc=opt.random_pc, + gaussian=opt.gaussian, apply_point_cloud_normalization=opt.normalize_pc, + scanobjectnn=opt.scanobjectnn, augment_with_random_points=opt.augment_with_random_points) + test_dataloader_pc = DataLoader(test_dataset_pc, batch_size=batch_size, shuffle=False, + num_workers=2, prefetch_factor=2) + test_dataloaders.append(test_dataloader_pc) + test_dataloaders_types.append('pc') + # sketch + if InputType.sketch in opt.input_type: + test_dataset_sketch = DatasetSketch(inputs_to_eval, param_descriptors_map, + camera_angles_to_process, opt.pretrained_vgg, + opt.dataset_dir, opt.phase) + test_dataloader_sketch = DataLoader(test_dataset_sketch, batch_size=batch_size, shuffle=False, + num_workers=2, prefetch_factor=2) + test_dataloaders.append(test_dataloader_sketch) + test_dataloaders_types.append('sketch') + + pl_model = Model.load_from_checkpoint(str(best_model_and_highest_epoch), batch_size=1, + param_descriptors=param_descriptors, results_dir=results_dir, + test_dir=test_dir, models_dir=opt.models_dir, + test_dataloaders_types=test_dataloaders_types, test_input_type=opt.input_type, + exp_name=opt.exp_name) + + if True: + trainer = pl.Trainer(gpus=1) + trainer.test(model=pl_model, dataloaders=test_dataloaders, ckpt_path=best_model_and_highest_epoch) + + # save the validation and test bar-plots as image + barplot_target_dir = results_dir.joinpath('barplot') + for barplot_type in ['val', 'test']: + barplot_json_path = Path(opt.models_dir, opt.exp_name, f'{barplot_type}_barplot_top_1.json') + if not barplot_json_path.is_file(): + print(f"Could not find barplot [{barplot_json_path}] skipping copy") + continue + barplot_target_image_path = barplot_target_dir.joinpath(f'{barplot_type}_barplot.png') + title = "Validation Accuracy" if barplot_type == 'val' else "Test Accuracy" + gen_and_save_barplot(barplot_json_path, title, barplot_target_image_path=barplot_target_image_path) + shutil.copy(barplot_json_path, barplot_target_dir.joinpath(barplot_json_path.name)) + + gt_dir = results_dir.joinpath('yml_gt') + model_predictions_pc_dir = results_dir.joinpath('yml_predictions_pc') + model_predictions_sketch_dir = results_dir.joinpath('yml_predictions_sketch') + file_names = sorted([f.stem for f in test_dir_obj_gt.glob("*.obj")]) + + random_pc_dir = test_dir.joinpath("point_cloud_random") + if opt.scanobjectnn: + # [:-2] removed the _0 suffix + file_names = [str(f.stem)[:-2] for f in random_pc_dir.glob("*.npy")] + + # create all the obj from the prediction yaml files + # note that for pc we have one yml and for sketch we have multiple yml files (one for each camera angle) + if True: + cpu_count = multiprocessing.cpu_count() + print(f"Converting yml files to obj files using [{cpu_count}] processes") + for yml_dir, out_dir in [(gt_dir, 'obj_gt'), (model_predictions_pc_dir, 'obj_predictions_pc'), (model_predictions_sketch_dir, 'obj_predictions_sketch')]: + try: + # for each gt obj file we might have multiple yml files as predictions, like for the sketches + yml_files = sorted(yml_dir.glob("*.yml")) + # filter out existing + yml_files_filtered = [yml_file for yml_file in yml_files if not results_dir.joinpath(out_dir, f'{yml_file.stem}.obj').is_file()] + if out_dir == 'obj_gt' and not yml_files: + # COSEG (or any external ds for which we do not have ground truth yml files) + for obj_file in test_dir_obj_gt.glob("*.obj"): + print(f"shutil [{obj_file}]") + shutil.copy(obj_file, str(Path(results_dir, out_dir, f"{obj_file.stem}_gt.obj"))) + continue + save_as_obj_proc_partial = partial(save_as_obj_proc, + recipe_file_path=recipe_file_path, + results_dir=results_dir, + out_dir=out_dir, + blender_exe=opt.blender_exe, + blend_file=opt.blend_file) + p = multiprocessing.Pool(cpu_count) + p.map(save_as_obj_proc_partial, yml_files_filtered) + p.close() + p.join() + except Exception as e: + print(traceback.format_exc()) + print(repr(e)) + print("Done converting yml files to obj files") + + if True: + num_points_in_pc_for_chamfer = 10000 + chamfer_json = {'pc': {}, 'sketch': {}} + chamfer_summary_json = {'pc': {'chamfer_sum': 0.0, 'num_samples': 0}, 'sketch': {'chamfer_sum': 0.0, 'num_samples': 0}} + + for file_idx, file_name in enumerate(file_names): # for each unique test object + # get ground truth point cloud (uniform) + gt_file_name = file_name + if "_decimate_ratio_0" in file_name: + # edge case for comparing on the decimated ds + gt_file_name = gt_file_name.replace("_decimate_ratio_0_100", "_decimate_ratio_1_000") + gt_file_name = gt_file_name.replace("_decimate_ratio_0_010", "_decimate_ratio_1_000") + gt_file_name = gt_file_name.replace("_decimate_ratio_0_005", "_decimate_ratio_1_000") + assert "_decimate_ratio_0_100" not in gt_file_name + assert "_decimate_ratio_0_010" not in gt_file_name + assert "_decimate_ratio_0_005" not in gt_file_name + + if opt.scanobjectnn: + random_pc_path = random_pc_dir.joinpath(f"{file_name}_0.npy") + gt_pc = np.load(str(random_pc_path)) + gt_pc = torch.from_numpy(gt_pc).float() + num_points_in_pc_for_chamfer = 2048 + else: + gt_pc = sample_pc_random(results_dir.joinpath('obj_gt',f'{file_name}_gt.obj'), + num_points=num_points_in_pc_for_chamfer, + apply_point_cloud_normalization=opt.normalize_pc) + + for input_type, model_prediction_dir in [('pc', model_predictions_pc_dir), ('sketch', model_predictions_sketch_dir)]: + yml_files = sorted(model_prediction_dir.glob(f"{file_name}_*.yml")) + for yml_file in yml_files: + yml_file_base_name_no_ext = yml_file.stem + target_pc = sample_pc_random(results_dir.joinpath(f'obj_predictions_{input_type}', f'{yml_file_base_name_no_ext}.obj'), + num_points=num_points_in_pc_for_chamfer, + apply_point_cloud_normalization=opt.normalize_pc) + chamf_distance = get_chamfer_distance(target_pc, gt_pc, device, num_points_in_pc_for_chamfer, check_rot=(input_type == 'sketch')) + chamfer_summary_json[input_type]['chamfer_sum'] += chamf_distance.item() + chamfer_summary_json[input_type]['num_samples'] += 1 + chamfer_json[input_type][yml_file_base_name_no_ext] = chamf_distance.item() + + # compute overall average + if chamfer_summary_json['pc']['num_samples'] > 0: + chamfer_json['pc']['avg'] = chamfer_summary_json['pc']['chamfer_sum'] / chamfer_summary_json['pc']['num_samples'] + if chamfer_summary_json['sketch']['num_samples'] > 0: + chamfer_json['sketch']['avg'] = chamfer_summary_json['sketch']['chamfer_sum'] / chamfer_summary_json['sketch']['num_samples'] + + # save chamfer json to the results dir + with open(results_dir.joinpath("chamfer.json"), "w") as outfile: + json.dump(chamfer_json, outfile) + + print("Done calculating Chamfer distances") diff --git a/geocode/geocode_train.py b/geocode/geocode_train.py new file mode 100644 index 0000000..762206a --- /dev/null +++ b/geocode/geocode_train.py @@ -0,0 +1,150 @@ +import yaml +import json +from pathlib import Path +import torch +from torch.utils.data import DataLoader +import pytorch_lightning as pl +from pytorch_lightning.callbacks import ModelCheckpoint +from pytorch_lightning.loggers import NeptuneLogger +import neptune.new as neptune +from data.dataset_pc import DatasetPC +from data.dataset_sketch import DatasetSketch +from common.param_descriptors import ParamDescriptors +from common.file_util import get_recipe_yml_obj +from geocode_model import Model +from pytorch_lightning.trainer.supporters import CombinedLoader +from geocode_util import InputType, get_inputs_to_eval, calc_prediction_vector_size + + +def train(opt): + torch.set_printoptions(precision=4) + torch.multiprocessing.set_sharing_strategy('file_system') # to prevent "received 0 items of data" errors + recipe_file_path = Path(opt.dataset_dir, 'recipe.yml') + if not recipe_file_path.is_file(): + raise Exception(f'No \'recipe.yml\' file found in path [{recipe_file_path}]') + recipe_yml_obj = get_recipe_yml_obj(str(recipe_file_path)) + + inputs_to_eval = get_inputs_to_eval(recipe_yml_obj) + + top_k_acc = 2 + camera_angles_to_process = [f'{a}_{b}' for a, b in recipe_yml_obj['camera_angles_train']] + param_descriptors = ParamDescriptors(recipe_yml_obj, inputs_to_eval, use_regression=opt.use_regression) + param_descriptors_map = param_descriptors.get_param_descriptors_map() + detailed_vec_size = calc_prediction_vector_size(param_descriptors_map) + print(f"Prediction vector length is set to [{sum(detailed_vec_size)}]") + + # create datasets + device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu") + train_loaders_map = {} + val_loaders_map = {} + + # pc + if InputType.pc in opt.input_type: + train_dataset_pc = DatasetPC(inputs_to_eval, device, param_descriptors_map, + opt.dataset_dir, "train", augment_with_random_points=True) + train_dataloader_pc = DataLoader(train_dataset_pc, batch_size=opt.batch_size, shuffle=True, num_workers=5, prefetch_factor=5) + val_dataset_pc = DatasetPC(inputs_to_eval, device, param_descriptors_map, + opt.dataset_dir, "val", augment_with_random_points=True) + val_dataloader_pc = DataLoader(val_dataset_pc, batch_size=opt.batch_size, shuffle=False, num_workers=5, prefetch_factor=5) + train_loaders_map['pc'] = train_dataloader_pc + val_loaders_map['pc'] = val_dataloader_pc + print(f"Point cloud train dataset size [{len(train_dataset_pc)}] val dataset size [{len(val_dataset_pc)}]") + + # sketch + if InputType.sketch in opt.input_type: + train_dataset_sketch = DatasetSketch(inputs_to_eval, param_descriptors_map, camera_angles_to_process, opt.pretrained_vgg, + opt.dataset_dir, "train") + train_dataloader_sketch = DataLoader(train_dataset_sketch, batch_size=opt.batch_size, shuffle=True, num_workers=5, prefetch_factor=5) + val_dataset_sketch = DatasetSketch(inputs_to_eval, param_descriptors_map, camera_angles_to_process, opt.pretrained_vgg, + opt.dataset_dir, "val") + val_dataloader_sketch = DataLoader(val_dataset_sketch, batch_size=opt.batch_size, shuffle=False, num_workers=5, prefetch_factor=5) + train_loaders_map['sketch'] = train_dataloader_sketch + val_loaders_map['sketch'] = val_dataloader_sketch + print(f"Sketch train dataset size [{len(train_dataset_sketch)}] val dataset size [{len(val_dataset_sketch)}]") + + combined_train_dataloader = CombinedLoader(train_loaders_map, mode="max_size_cycle") + combined_val_dataloader = CombinedLoader(val_loaders_map, mode="max_size_cycle") + + if InputType.pc in opt.input_type and InputType.sketch in opt.input_type: + assert ( len(camera_angles_to_process) * len(train_dataset_pc) ) == len(train_dataset_sketch) + assert ( len(camera_angles_to_process) * len(val_dataset_pc) ) == len(val_dataset_sketch) + + print(f"Experiment name [{opt.exp_name}]") + + exp_dir = Path(opt.models_dir, opt.exp_name) + exp_dir.mkdir(exist_ok=True) + + neptune_short_id = None + neptune_short_id_file_path = exp_dir.joinpath('neptune_session.json') + if neptune_short_id_file_path.is_file(): + with open(neptune_short_id_file_path, 'r') as neptune_short_id_file: + try: + neptune_session_json = json.load(neptune_short_id_file) + if 'short_id' in neptune_session_json: + neptune_short_id = neptune_session_json['short_id'] + print(f'Continuing Neptune run [{neptune_short_id}]') + except: + print("Could not resume neptune session") + + # create/load NeptuneLogger + neptune_logger = None + neptune_config_file_path = Path(__file__).parent.joinpath('..', 'config', 'neptune_config.yml').resolve() + if neptune_config_file_path.is_file(): + print(f"Found neptune config file [{neptune_config_file_path}]") + with open(neptune_config_file_path) as neptune_config_file: + config = yaml.safe_load(neptune_config_file) + api_token = config['neptune']['api_token'] + project = config['neptune']['project'] + tags = ["train"] + if neptune_short_id: + neptune_logger = NeptuneLogger( run=neptune.init(run=neptune_short_id, project=project, api_token=api_token, tags=tags), log_model_checkpoints=False ) + else: + # log_model_checkpoints=False avoids saving the models to Neptune + neptune_logger = NeptuneLogger(api_key=api_token, project=project, tags=tags, log_model_checkpoints=False) + if neptune_short_id is None: + # new experiment + neptune_short_id = neptune_logger.run.fetch()['sys']['id'] # e.g. IN-105 (-) + with open(neptune_short_id_file_path, 'w') as neptune_short_id_file: + json.dump({'short_id': neptune_short_id}, neptune_short_id_file) + print(f'Started a new Neptune.ai run with id [{neptune_short_id}]') + + # log parameters to Neptune + params = { + "exp_name": opt.exp_name, + "lr": 1e-2, + "bs": opt.batch_size, + "n_parameters": len(inputs_to_eval), + "sched_step_size": 20, + "sched_gamma": 0.9, + "normalize_embeddings": opt.normalize_embeddings, + "increase_net_size": opt.increase_network_size, + "pretrained_vgg": opt.pretrained_vgg, + "use_regression": opt.use_regression, + } + if neptune_logger: + neptune_logger.run['parameters'] = params + + checkpoint_callback = ModelCheckpoint( + dirpath=exp_dir, + filename='ise-epoch{epoch:03d}-val_loss{val/loss/total:.2f}-val_acc{val/acc_top1/avg:.2f}', + auto_insert_metric_name=False, + save_last=True, + monitor="val/acc_top1/avg", + mode="max", + save_top_k=3) + + trainer = pl.Trainer(gpus=1, max_epochs=opt.nepoch, logger=neptune_logger, callbacks=[checkpoint_callback]) + last_ckpt_file_name = f"{checkpoint_callback.CHECKPOINT_NAME_LAST}{checkpoint_callback.FILE_EXTENSION}" # "last.ckpt" by default + last_checkpoint_file_path = exp_dir.joinpath(last_ckpt_file_name) + if last_checkpoint_file_path.is_file(): + print(f"Loading checkpoint file [{last_checkpoint_file_path}]...") + pl_model = Model.load_from_checkpoint(str(last_checkpoint_file_path), + param_descriptors=param_descriptors, + trainer=trainer, + models_dir=opt.models_dir, + exp_name=opt.exp_name) + else: + pl_model = Model(top_k_acc, opt.batch_size, detailed_vec_size, opt.increase_network_size, opt.normalize_embeddings, opt.pretrained_vgg, + opt.input_type, inputs_to_eval, params['lr'], params['sched_step_size'], params['sched_gamma'], opt.exp_name, + trainer=trainer, param_descriptors=param_descriptors, models_dir=opt.models_dir, use_regression=opt.use_regression) + trainer.fit(pl_model, train_dataloaders=combined_train_dataloader, val_dataloaders=combined_val_dataloader, ckpt_path=None) diff --git a/geocode/geocode_util.py b/geocode/geocode_util.py new file mode 100644 index 0000000..9309002 --- /dev/null +++ b/geocode/geocode_util.py @@ -0,0 +1,35 @@ +from enum import Enum +from typing import Dict +from common.param_descriptors import ParamDescriptor + + +class InputType(Enum): + sketch = 'sketch' + pc = 'pc' + + def __str__(self): + return self.value + + def __eq__(self, other): + return self.value == other.value + + +def get_inputs_to_eval(recipe_yml_obj): + inputs_to_eval = [] + for param_name, param_dict in recipe_yml_obj['dataset_generation'].items(): + is_vector = False + for axis in ['x', 'y', 'z']: + if axis in param_dict: + inputs_to_eval.append(f'{param_name} {axis}') + is_vector = True + if not is_vector: + inputs_to_eval.append(param_name) + print("Inputs that will be evaluated:") + print("\t" + "\n\t".join(inputs_to_eval)) + return inputs_to_eval + + +def calc_prediction_vector_size(param_descriptors_map: Dict[str, ParamDescriptor]): + detailed_vec_size = [param_descriptor.num_classes for param_name, param_descriptor in param_descriptors_map.items()] + print(f"Found [{len(detailed_vec_size)}] parameters with a combined number of classes of [{sum(detailed_vec_size)}]") + return detailed_vec_size diff --git a/resources/teaser.png b/resources/teaser.png new file mode 100644 index 0000000000000000000000000000000000000000..2651a6a66eb2d3c2f0e9f221a2f07fc85ecc51b9 GIT binary patch literal 131519 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

1#I;|M_=6BaJSpo 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 {max_diff}] for sample [{self.obj_file_path.name}]. If this is an external dataset, please consider adding --apply-normalization flag." + # position the object at drop height + obj.location = Vector((0, 0, obj.location.z - min(vertices[:, 2]) + self.drop_height)) + height_before_drop = max(vertices[:, 2]) - min(vertices[:, 2]) + + # apply rigid body simulation + bpy.ops.rigidbody.object_add() + frame_end = self.duration_sec * 25 + area = [a for a in bpy.context.screen.areas if a.type == "VIEW_3D"][0] + with bpy.context.temp_override(area=area): + select_obj(obj) + bpy.ops.rigidbody.bake_to_keyframes(frame_start=1, frame_end=frame_end, step=1) + bpy.context.scene.frame_current = frame_end + obj.data.update() + bpy.context.view_layer.update() + print("Simulation completed") + self.eval(obj, height_before_drop) + + def eval(self, obj, height_before_drop): + vertices = np.array([(obj.matrix_world @ v.co) for v in obj.data.vertices]) + height_after_drop = max(vertices[:, 2]) - min(vertices[:, 2]) + score = min(height_after_drop, height_before_drop) / max(height_after_drop, height_before_drop) + score = 1.0 if score > 1.0 else score + print(f"Height before simulation [{height_before_drop:.5f}]") + print(f"Height after simulation [{height_after_drop:.5f}]") + print(f"Score [{score:.5f}]") + print(f"is_stable (score > 0.98) [{score > 0.98}]") + self.reset_simulation(obj) + # structural evaluation + if self.skip_components_check: + print("is_structurally_valid (shape is connected) [True] (check skipped)") + else: + print("Checking structural validity...") + obj_is_valid = self.is_structurally_connected() + print(f"is_structurally_valid (shape is connected) [{obj_is_valid}]") + + def is_structurally_connected(self): + """ + return True if all the parts that make the shape are reachable from any other part + two parts are connected if they are intersecting or there is a path from one part + to the other that passes only through intersecting parts + """ + bpy.ops.import_scene.obj(filepath=str(self.obj_file_path), use_split_objects=False) + obj = bpy.context.selected_objects[0] + select_obj(obj) + bpy.ops.object.mode_set(mode='EDIT') + bpy.ops.mesh.separate(type='LOOSE') + bpy.ops.object.mode_set(mode='OBJECT') + parts = bpy.context.selected_objects + # in the beginning, each part is put into a separate set + components = [] + for part in parts: + components.append({part}) + + idx_a = 0 + while idx_a + 1 < len(components): + component_a = components[idx_a] + found_intersection = False + for idx_b in range(idx_a + 1, len(components)): + component_b = components[idx_b] + for part_a in component_a: + for part_b in component_b: + assert part_a.name != part_b.name + if len(detect_cross_intersection(part_a, part_b)) > 0: + components.remove(component_a) + components.remove(component_b) + components.append(component_a.union(component_b)) + found_intersection = True + break + if found_intersection: + break + if found_intersection: + break + if not found_intersection: + idx_a += 1 + # note that we can 'break' here and return False if we are only looking to have a single connected component + bpy.ops.object.delete() + return len(components) <= 1 + + @staticmethod + def reset_simulation(obj): + bpy.context.scene.frame_current = 0 + obj.data.update() + bpy.context.view_layer.update() + bpy.ops.object.delete() + + +def main(): + if '--' in sys.argv: + # refer to https://b3d.interplanety.org/en/how-to-pass-command-line-arguments-to-a-blender-python-script-or-add-on/ + argv = sys.argv[sys.argv.index('--') + 1:] + else: + raise Exception("Expected \'--\' followed by arguments to the script") + + parser = argparse.ArgumentParser(prog='stability') + parser.add_argument('--obj-path', type=str, required=True, help='Path to the object to test') + parser.add_argument('--apply-normalization', action='store_true', default=False, help='Apply normalization on the object upon importing') + parser.add_argument('--skip-components-check', action='store_true', default=False, help='Do not check that the shape is structurally valid') + + try: + args = parser.parse_known_args(argv)[0] + drop_simulator = DropSimulator(args) + drop_simulator.simulate() + except Exception as e: + print(repr(e)) + print(traceback.format_exc()) + + +if __name__ == '__main__': + main() diff --git a/stability_metric/stability_parallel.py b/stability_metric/stability_parallel.py new file mode 100644 index 0000000..203bd7e --- /dev/null +++ b/stability_metric/stability_parallel.py @@ -0,0 +1,102 @@ +#!/usr/bin/env python3 + +import json +import random +import argparse +import traceback +import multiprocessing +import subprocess +from subprocess import Popen +from functools import partial +from pathlib import Path + + +def calculate_stability_proc(pred_obj_file_path, apply_normalization, skip_components_check, blender_exe: Path): + print(f"Calculating stability for object [{pred_obj_file_path}]") + simulation_blend_file_path = Path(__file__).parent.joinpath('stability_simulation.blend').resolve() + stability_script_path = Path(__file__).parent.joinpath('stability.py').resolve() + cmd = [str(blender_exe.expanduser()), + str(simulation_blend_file_path), + '-b', '--python', str(stability_script_path), '--', + 'sim-obj', '--obj-path', str(pred_obj_file_path)] + if apply_normalization: + cmd.append('--apply-normalization') + if skip_components_check: + cmd.append('--skip-components-check') + print(" ".join(cmd)) + process = Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, universal_newlines=True) + out, err = process.communicate() + result = out.splitlines() + score_str_list = [line for line in result if 'Score [' in line] + structurally_valid_str_list = [line for line in result if 'is_structurally_valid' in line] + assert score_str_list, out + score = float(score_str_list[0][-8:-1]) + assert structurally_valid_str_list, out + is_structurally_valid = True if 'True' in structurally_valid_str_list[0] else False + assert score > 0.1 + return score, is_structurally_valid + + +def sim_dir_parallel(args): + cpu_count = multiprocessing.cpu_count() + stability_json = {} + count_stable = 0 + count_structurally_valid = 0 + count_good = 0 + dir_path = Path(args.dir_path).resolve() + print(f"Calculating stability for dir [{dir_path}] with [{cpu_count}] processes") + try: + obj_files = sorted(dir_path.glob("*.obj")) + print(len(obj_files)) + if args.limit and args.limit < len(obj_files): + obj_files = random.sample(obj_files, args.limit) + blender_exe = Path(args.blender_exe).resolve() + calculate_stability_proc_partial = partial(calculate_stability_proc, + apply_normalization=args.apply_normalization, + skip_components_check=args.skip_components_check, + blender_exe=blender_exe) + p = multiprocessing.Pool(cpu_count) + stability_results = p.map(calculate_stability_proc_partial, obj_files) + p.close() + p.join() + for obj_file_idx, obj_file in enumerate(obj_files): + stability_json[str(obj_file)] = stability_results[obj_file_idx] + score = stability_results[obj_file_idx][0] + is_structurally_valid = stability_results[obj_file_idx][1] + count_stable += 1 if score > 0.98 else 0 + count_structurally_valid += 1 if is_structurally_valid else 0 + count_good += 1 if (score > 0.98 and is_structurally_valid) else 0 + except Exception as e: + print(traceback.format_exc()) + print(repr(e)) + sample_count = len(stability_json) + print(f"# stable samples [{count_stable}] out of total [{sample_count}]") + print(f"# structurally valid samples [{count_structurally_valid}] out of total [{sample_count}]") + print(f"# good samples [{count_good}] out of total [{sample_count}] = [{(count_good/sample_count) * 100}%]") + # save the detailed results to a json file + stability_json['execution-details'] = {} + stability_json['execution-details']['dir-path'] = str(dir_path) + json_result_file_path = Path(__file__).parent.joinpath('stability_results.json').resolve() + with open(json_result_file_path, 'w') as json_result_file: + json.dump(stability_json, json_result_file) + print(f"Results per .obj file were saved to [{json_result_file_path}]") + + +def main(): + parser = argparse.ArgumentParser(prog='stability_parallel') + parser.add_argument('--dir-path', type=str, required=True, help='Path to the dir to test with the \'stability metric\'') + parser.add_argument('--blender-exe', type=str, required=True, help='Path to Blender executable') + parser.add_argument('--skip-components-check', action='store_true', default=False, help='Skip checking if the shape is structurally valid') + parser.add_argument('--apply-normalization', action='store_true', default=False, help='Apply normalization on the imported objects') + parser.add_argument('--limit', type=int, default=None, help='Limit the number of shapes that will be evaluated, randomly selected shapes will be tested') + + try: + args = parser.parse_args() + sim_dir_parallel(args) + except Exception as e: + print(repr(e)) + print(traceback.format_exc()) + + +if __name__ == '__main__': + main() diff --git a/stability_metric/stability_simulation.blend b/stability_metric/stability_simulation.blend new file mode 100644 index 0000000000000000000000000000000000000000..42b32d8d11576e66de76a0607c09ab038d0c22e7 GIT binary patch literal 789216 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�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̖jo`2aAK}2oRnJ@PR@0QQ;2se>752n z2WNmYbIZe7{GJWY0bc=Y2zM?x51bD!!0m*%%yMQ_bqU3?&NS??v!wS z?$q$@+-c!ExP2FVkNj>>{>-C6lt*)HU9Qaf{6@?-f$!Tm2;pYTw}4y04|1o6+j3`w zALh;sx983ZcaZ0ur1vAz`Z4$k=DWb%guMXX;2zqwj$KR@{N=s)--qA*_-Rc4l=QDm za>f(?EVqJMLF=R6gZz$VRFvcT5OE&H{s?%GIK!Zza(@)}E3v0@#BBl`S3XIPvp*KzE*}ObLa5O`jFF3Duk!_^UK^C+@KA~x%}M(zw<2KuPFCc zv}+kVtfxui88D39m1G^yn)zfiKWFAsNHsv;1H5A(Ex7jN0@AAt&*v`0k39h4ULegE z`F$yO5$TFMlbh(p#e`M+tc|~C;hVNWKDMCGw>QC)>=u>&>y+y)@HTh{tR?Mt`F#)knQ&tX^%v&TUvpQ)^u?Gins|2CpE zqaIt5$5vo4_N~DM$?;(u^1CPZtyuSK!?tDDh3(3&58D%N2l5(f?e^_h_x1Iw`XL`A z{o9&&Lx{H{d8=QBmVGDIePq}P|Bq;sVT9Qk>;iTL!*Ta9qH+A)7@oZlX^g;ccd!TH z_T*RTkF>PaKl$*bvhOjcy>0d~f91=XSat(Eu=m$S(5k(NH-q0RDAU$4zSnJCY}@tK zZDbgorK@qq-ix%xg1v#(xqbNE7mNe@f&IY&#Bn-yV{F@8IFNh~!u=y;5-QWdmhMgL zjMH+4@MYZ8ZsM7TlFoQA0qj>MUYd25ww;K*hHyJu-+Z6BsrWZTdGvcrm{fLam|XUQ zFs1CaFtx0Im{#_~a0qb@1=ERN%kN=jw}%;Jcf`C`gu1d7;cze$)Pq@IcG;a_4&mp5 zdBiya97(=MfhUoc99<@9iSqRL$CP!1W6OSmdp-JsA6qwI8>yfHmM; za2}AXZ7lhT7K$c{9^Q~V6zRjm;e5;Ek!aU^l?><6nC8fE0W{@8(z%E{FDC!Bm@fgB zV!w>v%fVN{SR_>5CL_bwuq*r(;7ZJ*`7H6_Dlp8(++%UfJ&rU}byNR@@O5xCah|5e z-{AKe@J-wlU$1pv3q*&m!~J^jZQ5FG`5nsjUF_cjHiYe_5`0t znyxb4Z1sPVdaE422)C3y6>cT`55R5UhlIZ!Xl~vC?gT#qKL$SmcY(WsXx%;h-V5%- zd_VXp>HiEo03HMnfro*{?HbDc2<3j1a34bhv@-o1zkx{cHn*~@pPoEMpFB?3C%}_7 zubr;FPddMd>HGZG`1loRrfr-HPm!l&Ex#n*uPE!&;2H2N@t&hB&zIGQUz7d|*k8o{ z62C8l3$5Rt&eD1Xw^xbx8}J(D*THYW@BR;Iss3-2J(H!iwUs3weouaXAn!l^f74*! zN|-mTE|L{J8)^7+;jOag!`q}YlspE);rW=<8tn4jcPQhXKxNRl(s+88wB9RwAzKIj z9R2^o?^qL7*+PNoh^Mt*uUeiFgMfZ zqbRfT{+RIp07J`OiQ|>hBRiMsryG_1lcU`aIGE_0cfOI2K8= zkK>Wy3*<2nYz_v2Ex?wPXDcumY)!c?piz-^Y6XA#6|jJAf|&@m@pne+)Zf9-99X<z)56$Oy zVjFqCc>5qfrW^xv??P8~KK5Q5v-85p{JY^x^u=D_gWR7p+PD$zGm8A)CBM;>eGGVm zb{dP@-e4c>AJJC(=Kn$oR8FTy<0#X9_>0!;&+h^Gzh-n_Jc;Jnd!(tkQk?#QmjC;d zF5PS9!miNa;ozV+1`g);%V0ctPXN_mBB&wHN#r#dOaW8DG;jzwG{0Avo=^9!+JlPs zPWPeeyAMJwc@N5cNSl1fNcxaA@_olJeh<$54R@Uu{H=$3GyMghL*5E^A+513r*AVT zPaQZM=*(^={`HvOVEoR?SB2Sm$*G2hIr*VsZvO9KUcMt70glA|sQky_XmAYnWAh6` zL;fG(IO^L7CQz3q&`f$Qpf&$b^3R3&_-PC*V2muv+HT9RF#oU6hM(j=#}j@LSPYg> zmZgN9&HQiYw*#C2I<1X930?V5!m|9Qp_?|zA-!9k|94oC|17M;zAE28tOh57lfcQ~ z6mTjyjkLutpPqL)*%{a+t2vW0YcGEmzmkug&F?uta8EQkUq_nP zgKy&|8S8iW{Vwj3N8ZnP`yTciz&hM+1fL>{yoow~AKVOX$^R$Zn*TifAb)qbE&p)% zA#S(l{~PYe|1aE0xiUH~tGm%z*574Rzf4R{T_4t@)M2j0l* zj4PM?9`hgQH}Nii#QZ1lCi!1L{odkNdz80vdj~Tb8A&<4^Dg%Hz@I7mF!~BT0@?-| znJcH4ZVP{*ou`$3jy#HWmbBl`S0o?gE0YgNL+5dSBi={E|2y`N`TYm@C-@im1bhnq z4L$?^0iP58zl8lCzoDExGRT#)&Vgg-nLNMc<@uz71+Nl(g|bu;rXSb{Y+TO%7;FkQ z1O34O@C7gsY+hcK3?keXV9WCIWUKPD4tIyam%xNift1>2GS_T~MozB`mZOx+&} zU&L()Ww?OatM7Lt&7oi?!VlwjXRr(AUHKgjcEdaZ><;!I?w(*I_!4R8%wR9fqwpII z#(=T7@6GQ%VBd1l$XpnQ+kRkw+zuf8f&3l>4#xatAbG`j>=QsW<(-JzV%%!5PXd#{ z6fhM`1Bc*$D8JLoH%bb$UGzy~(a&0H@jndA0CnJSFcZ{MC#_$SmCnLG8_WT7!8~vT z{=?|^BV*aqvzmeI06(^KFU>WbBW#>(%35_)`NrXB%5w}j7Bqn4KqF`Z&7cLeQvZRZ zJHLFBMA|{x^DMx9A!q}~gGFF5Wmv-RQqT@0kLlp|1kee(z%tMcmXr4fxlI%4kgOoy zO0Wv7#_z;f5AhWzk@rWO3~y$q!<&gfKnEJa$%I?Wm_LQzQ^9HAbZ`bZlXT81-yxh` z-ak2~d_YoRC&~k2T=sl~S!3yPK24je&CbPs9ylNDl>0)mC2kjB7Vmo@zY6mn)8Zn` z7lXBw@e*(;d0mG6a`08~HK4XiIQzZ=^OfML@`1_M%QsK1F0TvUC?Avz;B@;M++QK@ z8tCiZ(5i1*ojKnly=y7ob@*Qoz74(uz6-twZifH5p?r&EwxzLgvW`Bvk?`98KSNvI zMEvh#zZu*DZUsNEv~$cIbTYsXal0M2JHVYlGMXRZ_G9o9a2L26_Y1)V;2xm5-%I}Y z;deg}|MyeuKLZbx4+;;K_YV&d?qTdYH+zJ99tA%Kj}iWHexJbqN$?Bs6!<0h72%)8 z?-}qc=I03eJnp~7`~r9p_m{xSm|p>}g5QAG2=hAlE%+V&Z-C!}KM?A$?4gZIYyTOTca{fuYf8jS4Ed@@dlDEsYWgP2w>*N#4^C^B8 zB-9~?J(~61% zLM3<$KB1}tx{Ui%lyV>%5s;U#5otXc4hS1p92hpK_#*Q{eV;?G4w_9m+R%uey`LMx z3h()BhI@Z7fbd_a*fVhe4n#B!E zu^Z(HVMpACf}Qk>U2A~k8^b7z&cAl1JiAnkNOT4=Fy_B2e!~erkhAsOD&~d}q_um+ z?v!%xHcjPs8aM>#Oz%*_O$W8$Ffaqu zfx|1lWbsu0RbVD=^V4!!7-HaSkM5Ds~E-j z9G)~*3{RTq`?mZ}Npr=pq@`l#q_tv~WPZi2$pZR$A?dV%<4J!JSPTY%C15FN2OZ!9 z!gP{H7v^Q48!QLXxmxk?3?UOT1xLh{2u}j6ZR4CDEK*e3_One6W~ek3+zvUUxHtOr@=GeS@0Zq zo-n@#FJOKVyoC8>eqRBvg5QAG!0X_*;CJ8+@O$tF@JH|`@FsYRxNn1Zz`Ni*-2M#y zg88rDeeglWxa33dH|q2eet!oaSL_FMhwsI0AX-{^>L=dipV-y52j{Ab&T>V&H%k5m z9s8uBKQf;Kl24&`ALI^9{!RMgH9k|?Mi&_)(A7^(uj8QPKaBs+3H#rQvC01`wn(-~ z&={)h9}PWAR(?6zyi)vJ|1hZX?yyDW_@uFHhp;8#x2oJb8Cdhs!_I`?rLsENwQ^!Iys{?Q4fhdXcd!T86O0630@6d; zi{DXTG}s*b80s<>>!B+|U zH5=dX9JpTz-h;on3iq#rt1Cr^CMDmfoXlEsNU{wl9a9qNZf_p0soXq#6MTzs*MjT7 z_2Ap2`yKFI@I7z?SO+8rxRKwR!1uw;;1+NzdHw+028Na$8pruG)-`Ea_SLS_;RV*` z2R$a8CpjJ1V!dGwNp2_KJHVabN8rccC&bepL4A7{^Y(7~=N@ox<w(I{lLtvx^?r!ohruJbKMIDjfB89SKZf~n@C1HOR?Z~-diW+U z!;_4sUsTRwU$VZssvfhGr-&=Q|CgjEKJi!d!PA)4t^-N0&a_O|mxki`JROKe>Qj|U zb1td*7{3tB)PnhlME4?}fkr+{`p=Qi^OZ*?+ikFXsh*Oz^{>h61@K}l>r0eTbm?V& zUjZ7g(usT3#^h1SZz#)amGhFs?vdV`q% z1l|O1k=NV&4umqjLm%#&58++x?^SLT{!AJE0{#l#2OkjjL;Q5tKynQA+uv{-jN3Ae_-$tVHbWOU+rUlz`59}< ze=1uj=lc4?!%A1~bISBz>hwSI3RR+Utw~bF+6Br$zG{9_UR9G+fXb=`NmUj4+h8NG zG1#PPA>*kn>%W0$1Mj%rd=ChlR!I+c5pBLW*{n)yp86r}KX?Vq{j1O$tb$HZsW0$5 z5Nr+xfi1w6K;us578;9NRdM48Y>oRiRYxb=R<%J(mcRqC=fHjYs?Ea=;ER}tfE`JH zD8D-qUU~z=s+Mk`t@aH&6LuG{tA%%ckm1;O!#)D+4)y?hf|1}$U@tHVj0R(9r?LFL zP`-q5)lRu2*WWnoO?&SH_66g>ex#}W-~NO>032A=LFv}lhwaHhq$}N&g9)p8e7Wib zXx0g|+6jyY>78~a z;4r63JlSf-tJ=(I;ER-dF8=et&9tp_Wz@z;U_X*F9Yq@)O`bO}J~d8{!Tr@rjmr}^ zu-=@s0l$-NeqV%Ed<9^Y$9{5BQ(6U?+sO_lG~~Hooa0?9%&$2^U|K*-1g?D zM{@|rRh^NU8>`M_zrVir>mQnE^JdUOUaeq0SO6AM&tc&#`u1%4?rics+sfF6-|n0a zX?@$1b~>K&Eu#L5fzG>@@VgYW6RxA`oMaQ)^aR34zS2p%b%AA|8!QJa@Dne)k~pg{ ze}uOGK)`O!^4pmk0INyoL~s%~nQ*5N?^JLaI31h;&ID(Hv%xvwE0*pWW(|8n%;$ph z!1>?;a3Qz|NcZ+)!mlOWORCPLyzn--Uj{A*Uj<(SSAZ+QRp9I3YQlem-wU~cz;Y0- z!Te3|Ept0R8N-c*Ypd2I*MaN7x50P7cft3-4PYI(5!?j64{ipxfLp;2z-{1%;C65a zWxJEKe?-1Nro2DFd>6PI+ym|fJJbI6;eS8opMsx(2f%~iA@DGG1Uw3U4ju!KgD1d~ z;1}R2@JsM3(s>#@gZU$<&mXCy>h~;o4m=Nj4b+w|0M-9R@Dg|#yaHYYzX7j-*THYW z@4y@2_uvoUkKj+>P4HILdCA-Sz60I`?}0yqzkt7j_rV9?L-0575%@d!82khL6Z{K& z0zL)*2A_fdfX~5y!T$gU@ByL{a-a<4K{==Zm7ogr0~>*j!6sl+uo>tN27oVsfnakm z2y6ki1Y3c@U~8}q*cNODwg)?aFM=UpM=%uZ1crf~!7gA|FdXa#Mu6SH9$-%}5_}2l z1xA6LvUx1?`{%oB%pO7gz?m!E&$ytOTpTYH%Vr37ia00XpwGmEY6A>EH}- zCO8Y64bA~y0c*gy;5=|XxBy%TE&>;WwcrwPDYy(=4!#P$2Ce{Cf~&yS!PVd!;2Q8v z@GWpHxDH$oz74(uz6-twZUF1Rjo>EmeQ-0l1>D;2LU>p9JD4>eK87ARJ-*0j=JUCW zyx-v!AJ!R_Daz+EZUjIWNWSQgeHp@~WH%=PqNNyA1cs z%>4=6wZ=W!kG)F2%Zc;7w{5Z!E1N}Z{Z!hhY#=(J^>3?G~E%R84l(iAOvDSOzfqukA+)o zuH%3MoZtdCemuB&!3UvWf#1Oz2+Vj8pX&sW5E5~p7%Cz&3E{I-c9S9_86@XEg~O|* zgj5g)sUaNFKw3x#>G78V{~0m6ljcmw&CGokh=8n+O=LT!>N#-B$+dhF=EBSkH~7BF zgP**R5As6+D2QHPKq0~u#$OTaib63c4ke%@l)|kvl!3BP4$2d*0^urhT?s#xp$b&R zt{PN_8c-8z;kPz!b>K_x>tg=M*i3(@NBFOxKK2cup`(`82(vLX!M-UpgXYizT0$%2 zwZ^>-a@s;WXb)dQ2j~c$;2ZpQ1{ojz7V|skLfEd*4fpS%J92tp-xGR4Z}`D6P4DBF zq4#yn)cZMR>HQtk`OcZcSebn1c(osSCIes~^oS*MMe>f&4@2Hy!ViI=Fbsyn2p9>Y zU^I+@3i@1qtfQnp&f(O?JLc&V95N0%pLcXVuk3u@+xhxLhfkY?Op(3BSU`_{0d5PF z+hoUleF|$K z6?_9zPvdVE`7#^kz+9LI^I-w;EDY$e2!CPBtu03HC8U3;V-b0?lDJnQbET?pmN{1G zq3p<{=H`AiaZsNlX9dXE+e*w;uo~9jwidGm{qJ>LuZIl|S;w|U-{@FN8rC9bt&%Hy zHh#k0Vs~c@(11VMCUncqH%kM3J^9DF1MVYX3&_}+ee$H*M$0c_m=V!3T;OJWtemM8?T=sCCi=Cx29P9PH~}8l&n15OKJ{xy@pCLH9z#3M^$9o` zh-)t9JQxNe;S`*9L=v}GI|FCo9Gr(=L3Fs_*rNYNIlJiCs$W9a%RIL$j?L(`jqe=l zZe(18>u>}2FmsN6(@|Xe-LYN2h2KYf+ula!J5+*qalZ%m9XrU&9q7IT-R1m%oq7ne z9^mgG{J}f*r(+j(_Bg{M?9%akA3J16E$d}*e+tjwImr6=7x+04Yqx4gUSjtOUc(!B z3$jn<9e(VxzK2$YI-NhaVoCgr`=Gz9bN`6>2^fyhU#?137QjCI%ig5@?6lrb`2E~FwKz_vR-Cx)IdXdr zM)qhwA)g$`bVkW$jG1dUc);sq3q8M~^^bt0$%HJzuG-&$PP zcAio1PaP*GIN`T0cJ<&i<@77e`XJ*e4KN!bTgvTH+GN?Uy23n*9Oj#Gn{1w=%%3AG z&yjz2c@n=;z8WL93HjdC*+6gRJkL9KPQPsI)|(Tqh4WWr$bB8HrSn3x>)!(Yg8TO` zB3Jxfin4E|(t26ND{I(WBTv?4x4~=+avnlE%=W||#>omX*>V20^NQX9IzlJ-20A;t zGBV=OzI9&3&sEZJmAF2S1Aj+6S+r|BgKM~5`xiILCGnont~0-P9Y5Fq=0`?*WE~&l zC+O1^xF z!7v1d!Y~*PvKM>=W*p3sgdGK=2|GIEhCaqAYZ`CTpS_9RH~&R%`eDTXZ+pGQB13eu z_j*<1eX{dx9C1mTGTtd;mA~`6>~ni2IIEEh2WYpY{~`P2Wskg5o9N7_O;YuS{r*oz zuG9fjU@A;Qo{Xn{pxjxkV4u$Wc#Ci|FlGLBrt=nY+(Q3b=r3nK+~)o^_qVy1Jy(&0 zv->==cz>5Vq>sYB5T4oWfUcR5KZkso3-ch1_c8;1=feV6=#)9#?fN3;9X%T-4AA38 zw$wFuSoh`C786F=^(E-L6qdnqo~^tCD{zy&2P?5#1*>5V$egXLOtO?I zgr8s&Y=%hK0$X7lY=<4N6L!JRup8uj3^^NT4{`2=edw^CG#-G1=za(eJMS`9C;1^~ zi16*^q#Y+OAF=oODDKCc_w?h0_41CNR^y3`VAB7Py?Zhzb^@8_$-k4tcglI6XNnHA z8{wQn{G1d25Y9;qzd9f47vMMIyXbt2f6i8cOXznQuE15eM*MQd z&UM1wfSd5U^AB=N+Ol}sE$(kS|J3imUAX6rr`>lx(o1lP17BN#XCrH(<+<49@d0vW zP4+`%{{er(BX|rWd7e)QUyHC$dEU?9xl`7ZvjzeCm!#tryoNXM7VQ1swC7yQ8gW@4 zJ{%>`9py^wg`L|K>_eg}`gw@Y>+)(o7vFzixty9G;<{u#{S)#~?4B~VDkf_d z$j5ldh|hHbNXT_F%7o1IB;r0XB!Q%^XFMl4J>wZNqsa^8E)}sCm)54YiNG%QSY4Mj1(u0J{;CfD3431U2 zpzr>l#?PgnD*Fg(=^4>CQy^c0^IAuFOqr42Kz~jiz9in4q~#@Pd8TJ^N&iOn7)YFU z`!>Y!in>ou26=@adyTTVMYvv5ufE3qHTF`{gKY}qfymEBycgL&ot^6(AoWL1^vR{- zU`-m=d0cPk{>iF=H^_+4^5U-ub#6Yw+WU)bnfdXXfW%vzO2SHM=EkIMvh$??c~;OB z4Ev6G1-a&ZR_SK%3T9y?H$p3dSrl2tlsx()m?dzRIZ@dQG)sR^d64k*5qO_U;#Mjk zr!;06ko`nuaW4nup@Qp!Y6B!sWWK1N>m%)~$Y4AHDuL|rl==4}=BFq(@n0EzWM7`F z8)08CwTc~1o_9s1vmVWk{*SUVu(Qj7JX3qkvdOafU|3sbYVxay8Da!=sfw<$?^fc) zp8jnN!>z*D=?@`4rGG4KcYK~zf`DJqPtxpQUrIFb)g=71@)l; zG=xU#nMph6WD999Ut~Rr>o55v?_gu4zuQQt{7HQ5_aS^!?3+PzXaOyu6|{ynAbHZ3 zxY|K`_!>GuN9Y9KKxg?s}x zQjU5M{t`R4SY5@p5Z~cEM=86#uq>6S!)`u)EKV@W0*qh=bVwP9 z$FAEYf28g+$iK7rSU{MCDy;NtyxJntz1Wq)sD-bRvQ_{&OR!stj<%kB%aI>)PQo(m zmct5I39FE^TItKqA=0rHyLGT0yAkw~H@Mhf?+;U8&H&GwlKA!J5e^ z%gN9`8QP^VwxG*a(z*?{BVWqe4%e4jI`p&qdtrg}r8aiD>~$W+m`C1)&Of`tjopL^ z<7-I9$F5Ny?Fq=3PyBm9#vJxx?uP@o9fU*Zd6@8aUZgR8LAN8=A9bZQj`0kR!wK$B zVxEH2a0br8In}nOqfE2zL!~Ep{%rby9q&AOXVN9mI*h;{#}`Q zmqeCb$6jz{!9GI!%@u)J5Hl-gBxW|u8kpHJ8)4?4wz1#ui>{o!fA)EGvVUV7{liP- z{bk6-$x`y2-ir51zAxol^9nj##m&-k~=E`MUhZ}Gcx8LEGD-U&>eDBk5<^B%b zg?p~NN}jlhTp2^M&(sik`TiHT{OHTNTjG7-Dq#4~JvgrRBV(x#c|L!DoSXV5<|E?D zMm|2qe8T-|GV&>5+weS}@ob*E3K~+kzQFw@Oe0;g4*wO`=jr>)86>aqe~A;ZWbEP% z_HU6R_PID6OZJ({I)ZoDzjqZ@_aC^Hv7(fmk|phN4YfZexF@D4X=5J46{>$Ce#z%$ zw6AO^Cf&s-)9eFr^F+8eLH4Gei6efQ_vL=EUR>!L!%ei$`4Y&dK_~_=J_bO+XkqDJLOjB9OXp zCVp8HiX5kw*zMGkxXaQeGsei98HOqOBrYOz&#S%1d~X|F?Bw8LB>?=4PE3*MJjvg=04ERD=EFpYT2y6Z*x zD~DZa_g7K=%5z@eZ_zLQC-vAmyBWMgw+|8A|TF7haZlSK5aovN{;pF`k`{p2hnLAiWU;07J zR^)}u%-a2x3%IqwPfK@e($1P|EjOg0V-H|xtP=PL}y0Xi`_Te%RV>ymxSx!?#!Da<-n_TBupFV zgg)P3TKKKXsdbWnow@(kEi2w+t(LBR$M%3O*sUlg)!j|mb#s4@$(WtH zyV9w5teCa8C{RpB6Bu zqf;>4jA++0aTj|zeNU$*)3t$wuY@_sJzM$f>UNQ)Im%yG_uPOv54T`lF<;p=j4dbi zxwK4yc%+O~(B$F_^Ml=scwcBI8MBcwj2e`oA(Ww^Fbsyf7aAjAB#h#IG>mcA zp^38lRb4R0Qntpq<7(q!0`ex}XA(??DKM3Kb{gd>%xus6O^6x`nU4Jom$#=hCjEf9?skmTF^5S!^I-x07Gf^KTntONUW&O4b2;V;cYA#$<|s0x-7|RHLV=zaF-3HucFLN7b9c?4+))vE0>0&m~&F+89G@Nu8Lcht)9Dtn5vJyLs zSzM_HHW79+M8X!>3fo{i>>%AcVHf-iyRqAYxtC|Sk9g&bZ5NXFquT)}&HX|A9OC*g z{KEASI10zOm*;bw>l1JiPGNT%&cIpZo`dt){|Xo2H@Jw~C7#!1xB^$<8eE4Pa1(xq zTkb~MZTC{NVSbP>64zbA-*YF{yHbl+BMRapkse| zK0zMF5;PCvhhRWf{FxrKh8Wz(#EeC!BR0eV2RN~JVYLo8nXq0H zDtML{6+KIhN}lDkFLqwb_eCCBjTo@sSi#d+K@+ipx+LhHRF~FV)=X4Jeiib_;#5yg zLei=tvzliaVZ?o`Dwa>Db=f9ojR};5dP#b!w_e7I;ptTMjDy{?Lf^+8;J1@%2u^#=HB==qL4@(!($$DuWbCeRd`dDhZq%UC?miZmra z!zH9d-qq%wbt=8ejdIxny<0+Zb3OJES}V^6BOU(Mk&ms3cO<8hN}s+B@wSC_o}ZA) zc>-L24IK#E5jw#)%Ks+%ahr&36X}p~2J%3+>sc8$6n#2-HX~y*el`dEaH+iNoi$u?s!a))^2 zYz#RgL;Omg-5w{9aj~I<+syqi%$c+;!!bu7b0o4yaXlKwpsVa#9Lx1M7!UDTM>GL* zB20qGFa>1ZPUiEZPdJtPX|Nl)a(>Emu4lkZn1%e=FbC$sJeUs)U?F^-ceLjg7h$&; zmcUY22FuY?<|Mno3a(cocNMzIezxfIldJK&2G+tlSPvUuBm4xLU^B=*>`2Tluobp} zoYTJ@**lP%nE9ohT<^kuA>WEWWA27MuovW9|9zNpc1!Ram;KxyAk0C+%NqVe+#iNt z;0PRrV{jZ!z)3g-r{N5og>!HoeuWG08}cr~B@gWdT*2-tT=P`du6yJR9j|r+^QI?4 z`yFn1WSn2l*mYS&?qfb6{4Vr*d@{P9465PI87#Jlx`g)GKqi zmokxndpXlOA!Z^-3`rm1-noGL9ROgG29MId}GPG4r&MQcg;R@k?OHqh34 z#Aru(+hQCslBv_Dj}Z4h+TQ&}d)%w*v{A_Dpycp;X;C|pu1>h6r7ryjdBMJ9XRg16 z@1P5Gg>Lx!9{&wu%UNu)Z;-mrd)VmV{e^sSYCXM2$z$4Y{Pp%8GkySS9P;3p%7Z@G z_YK6|55HA8`8p=B@>%6!OWr+cpZXJr#fjJ=t2F7fI1&3t?@1#ZlN}pM)-hxb2*?_U zZg$?Y)*U-Z-+jjF=X#Gm$lmaNS`x z%;A16%!BzVjy=W#Z(@C+_q4Idd&XGoJ!>rSo+EE$ES-3%zm}5^vVU-_%8%z-%BXf% zzVYq)Y$>vr!E!HWoxv>hUCI3_SWR4OU?6(^HNCV|UOD%Tb5)=ob@e)Q`kGR=9{=(U zxPdSmx&8@e8|RE+u}+cp^Ncf|=XspxdD#6q`JVje`QN`97Je@CdjYrk7TARB%@B#K z`;2c&nU}LS(*Y7X$J|Pdi@e@5oqMZ2G7&u49kKif)(wMSf5h+G%9&1v%42 z*^|_4L`MJ}3_g~8JXWi|0Gx!b$%kVuUNu2h& zr$Us&OMKS^&n%Dh-e(N=?>Yt3V!u<;$M~IRBy%b9Zr$SgHr(+(V7^Aee$Las*B4S3 z$U7@pG)5qwb z?+pFX$3_g_lR&*-p9vn*_iyR7-_KZti%s}A;P5dnhv@=0$eHvW;_>>P@}58YM>+VM zcXnNx7Ck$lpVSRL!pXCh-yg7e+v5N$ng2`M@+aoUU^euvN-l3x{$Nr5j`twTGd+H?HemVNdyOs=j$&n%Lj4dlD;{)%x_?7p9 z@yvk#6!=d`N>f2f!lmNgJ|Ft;;XelQ!wxU`A@7aMxrQlSJ_YjQ@A5>ipbnZDlYVS! z$qz^)rgAH(r}gQSX2UF?nPxg)h?z+BO=6hoeKE`oKChOMGLs21Ll%g@E-Pg7X=ZlJ z9FP-oL2h46GY{m2eB9^vmDCIPVwnXgUthpse&M_j@fAjPdi4t#G5Dnn&JyrBwW8S7 z(qxT&ajh7Bi~Hi3C439@lD;{5DfE^7JEe)g4A^tR?03T7cKSGPdH2goU^XimpX~yQ4M%XzV zu%quxnmUqqos8)SSUyl5db(S?Q^noeB4?uciK-Pxc=Lzlki< z!>^A}qLA-5M=-9=OHSKuj}b|F<#%JG9!X36b%J+7&WN!mO9?XsUl#Vunw^4*vcImZdVfe`(CNmbnam0hrJCE=2h z$Fe3yes`CBy2LSnXH=FCy2FH(cXOaGIewCpPs!DDNJE%G_!$gg$`50Pl$zK)?;+R? zCEthnQkuhwt24T!rLStIKNb4&jT5D>-;7JXGrEmjS+kkK{1zF#L*%z?Q<)?2C%>05 zO3AUy-w>YkXyTB5>=?`(xRu_*tsfP(7&WG zEzdM9VbT&NC{Nbm7MUPZ7v{R7IM7^7W@3>l0aJIx7(H?cNzJy+?S4$ zBIDySewrTH@@qHgksW+~tiz6xfi$N#S5WuM*zrojtwPpnNbbzY^A$JSevK~^Wg_U8 z?^&MHTKvoSm(1O-#NRq!W);s0?AL?*W>j8c9Z73o%d-1r8+;ph;*4A3XA9qiKcVv! zJ%aZ)0^boT&o+^^%@9d`J>na*8v4oe*g}}CQ99at7xJ6i3|SW{I&8yzJIGwg4oun8 zAo1)3d5;z{jw5r9ySV=ucEcXn3;SR{9Dsvx2o9seFTOd92+ii)<*eoqPge5?<>ja^ zQac7W8J|4v%dDM%lW+=7!x=aW=iog23K!rvxCocX^UG9^a@O+|;<*af;5yuZo1{g) zy?*EV7RXqmoPBpQsJg?n%x9>7EF|9}-3NTGD0ToGD8+*L_k)M@yTr1Wyd`SW=`VBg?($pNP&HJ3hm~gr~g&AD#@Es3)gW)a64t#znZ zYr}n8Xop>U_&RhhD?+85%kL@4Zxq$gIuO1iej4*Wb>jLP=nUWDU(N~2L;9q?{tmk? z&=tDD_kOM@;l7EF)H5~sbC!^`x%*EBEG)R5BkGo?0&=?00Y6^>uA^64fMjAi~}A; z4>>1oP-u4QFWJ-ic~9tI;u;c~gSMLo4!S1iAZ6BuhUQfD)UeP(wAw)(WW62xli?u@ zCqDc4p=FeSMJMSat>bztvEPOL&!P5OTG`LFJG204rY^_Lj=Laju>2ZY!9d)3&AqtoBdwf)N?dll;emA) z`?)_5`h^*fd_PDWhme05^A|V*N8uP8hZDFRrFA{YbzVxwDZ-wHGjJBSS$c?ej_dPW z{|XmE3zAiz+`4LyOU$DMsED<6g>*Ghv{)oMHg<79 z+C7IwTLms~g9p6egHW)*4{;$L#D@fs5IJ`J#tMI~6GIZ~6l;r5YDq2jFr!Oy7(*W8 zL>qCMZvt_XI@Z=P1#+VQ#DY29GGZki-RZB|^;mAn?<;m}~2$Od?9q#EN17x%s zus1hYAF=|L`4BxytLGMheHj1cn;|RmvLPcovRZl3xNWkH7NxqyK=6 z0{D?L<3*pt{7Rq)ST%;bJX7Xb$-6JCa%Lgq6h;qO2PNl-y~aa+d>-ar!Y&S}h_1mh z!S}jVgzrdut-w9@5*D>0wPNTbYjTTQ70u7Tx4~~3l9wf+6uQW}mBRZGe|A~gMY)lB zs5I_n0(mWSsWN|97JX*XcPock9x6aZs05YqJ3_BSJfBEMgjU6h(5fOs%4aa1YWS%R zGM-k0Fg2kT)P_3nCDes_@DKwjHruoF6dgHAH9)S3MF7QTZn*gaKqqE*bUR#nPDRl-&!tbDt2E*8M%Zdm@yfm_`KFXMen#x-yyMl(%k1p< z!K%TVA=k17Ox)%9)ubMZZby5M?7qa$S*ij55|;KK`dhXC;or6w|8@AD`Kv73ExODv zNvrsgvCHUk`%VUc{b0DCV$D_!NhUN+w>oD zYO1?)ee(lpP7q*>za}8JJy+fc}FfJ1CFXN!% zCiy->@AzNbJ4MAmQt9(e)cq)R-#O~u*6-V>`{U~U_%0yd&X3XPFjDk_u`tf+5~X)= zyhO&ZV$urP<(0Lr+y~EAk-aG6kud=#s`$G_#m5Et@O_j#`?*kNRQ^;kvqZH)|7_P} zUUU*NC!>?puTwCmg6KO9f@Nen*E6u6Nxd`+X2Tqq3-e$;ZVO-`{uXgB>%W4(^&@8q zEXHmLEQMvT99H0er6uc&j}G?c1+gfZy=v|2K?5DjNQxG{JXJh z!kBZhkb?Ri>iUb$-lU;7G4&=5KbU*5%g?vKKC2J?#y;5h!Cua5D5~!d$X>))mFyWl z5GAWGxyjlu^sJ!wLta0^^;3BweMZTXFn)E`1o`z@yZu(j!_fY1=e+9eSZ!Bhy7svM zs=d$-S!MM1q4N6z5?3GdF!KBIeczA#8bDwF7veuc`ebdwk1^!8j*@FfEw6se>T4!q z0o8G8~dTs%20FQJ4X&XS=WV~PcCj&{Z{F?5-Kzaw!rD3GVa$b`L*pq=5=d; zdBYlH-o*Uf8WNcMknub_9pRX_@PC_fb_ec){0{V1%G@@%$NhbH0Bz}yKSYN=;7`Jg zpv*k7WQ}>G_89jk@D%%J)=(r+X1N!+FW?rO<(a(1d}R&en_Ipq=`$ls=2+ig{}$fC zd-wn!;S0)5!((=)7v&&OVzdcSF3o|yv z0S7qo=R$@XJmB>YHGTeJW~hGzby#rCmHnQ{?%YJw*%tBo3CDTdT3mlyR`GJCf}b<1 zsE5Y7N0^bZMwlZp<-149vg|F0?~kh`@SCJn&f}+#Px#a#&mW;BMqUy~id`~D4vQI= zNP%5SbQBqV`DW`EYb0eveg%1?DkG`m|xZ^PKY*MW9O5c!&_X%A?tw{i;LY;n@*o&4&3LR z56Wo4?@Y!uCZg{ovjlNGr=BQ@o~58Pl<`ldzaisM>?6ay9F&Jzd{fru8>%uUdu4d9 z7)yjylyaFrX&yS&Oc15N)ZubIj$MyeK(6$KVEyj#)~k|7%m|Q<8Q9NI_L5G?7x~@V zaO#L^l$YxMHCiIRclZSd#-nVVWc@}BbgU_9hT2dEzC 1: + file_names = [file for file in file_names if hash_file_name(file) % args.parallel == args.mod] + for file_name in file_names: + original_obj_file_path = obj_dir.joinpath(f'{file_name}.obj') + bpy.ops.import_scene.obj(filepath=str(original_obj_file_path)) + imported_object = bpy.context.selected_objects[0] + imported_object.hide_render = False + imported_object.data.materials.clear() + normalize_scale(imported_object) + title_obj = add_3d_text(imported_object, title) + render_images(render_dir, file_name) + select_objs(title_obj, imported_object) + bpy.ops.object.delete() + except Exception as e: + print(repr(e)) + print(traceback.format_exc()) + + +def render_images(target_dir: Path, file_name, suffix=None): + # euler setting + camera_angles = [ + [-30.0, -35.0] + ] + radius = 2 + eulers = [mathutils.Euler((math.radians(camera_angle[0]), 0.0, math.radians(camera_angle[1])), 'XYZ') for + camera_angle in camera_angles] + + for i, eul in enumerate(eulers): + target_file_name = f"{file_name}{(f'_{suffix}' if suffix else '')}_at_{camera_angles[i][0]:.1f}_{camera_angles[i][1]:.1f}.png" + target_file = target_dir.joinpath(target_file_name) + + # camera setting + cam_pos = mathutils.Vector((0.0, -radius, 0.0)) + cam_pos.rotate(eul) + if i < 4: + rand_x = random.uniform(-2.0, 2.0) + rand_z = random.uniform(-5.0, 5.0) + eul_perturb = mathutils.Euler((math.radians(rand_x), 0.0, math.radians(rand_z)), 'XYZ') + cam_pos.rotate(eul_perturb) + + scene = bpy.context.scene + bpy.ops.object.camera_add(enter_editmode=False, location=cam_pos) + new_camera = bpy.context.active_object + new_camera.name = "camera_tmp" + new_camera.data.name = "camera_tmp" + new_camera.data.lens_unit = 'FOV' + new_camera.data.angle = math.radians(60) + look_at(new_camera, Vector((0.0, 0.0, 0.0))) + + # render + # scene.render.engine = 'BLENDER_EEVEE' # don't need anything fancy here + scene.camera = new_camera + scene.render.filepath = str(target_file) + scene.render.resolution_x = 224 + scene.render.resolution_y = 224 + bpy.context.scene.cycles.samples = 5 + # bpy.context.scene.eevee.taa_render_samples = 32 + # disable the sketch shader + bpy.context.scene.render.use_freestyle = False + bpy.ops.render.render(write_still=True) + + # prepare for the next camera + del_obj(new_camera) + + +def main(): + if '--' in sys.argv: + # refer to https://b3d.interplanety.org/en/how-to-pass-command-line-arguments-to-a-blender-python-script-or-add-on/ + argv = sys.argv[sys.argv.index('--') + 1:] + else: + raise Exception("Expected \'--\' followed by arguments to the script") + + parser = argparse.ArgumentParser(prog='dataset_generator') + parser.add_argument('--dataset-dir', type=str, required=True, help='Path to dataset directory') + parser.add_argument('--phase', type=str, required=True, help='E.g. train, test or val') + parser.add_argument('--exp-name', type=str, required=True) + parser.add_argument('--parallel', type=int, default=1) + parser.add_argument('--mod', type=int, default=None) + + try: + args = parser.parse_known_args(argv)[0] + visualize_results(args) + except Exception as e: + print(repr(e)) + print(traceback.format_exc()) + + +if __name__ == '__main__': + main()