Télécharager et placer le fichier CSV dans un dossier de votre choix.
Dans ce même dossier, copier le fichier .blend du cours 1 (pour avoir le workspace adéquat) et nommez-le arbresYUL.blend
Ouvrez le fichier Blender arbresYUL.blend en double-cliquant dessus (sinon le cwd sera celui du l’executable Blender)
Assurez-vous d’ouvrir également la console (Window → Toggle System Console)
Code, itération 1 - Lecture du fichier CSV
import osfrom datetime import datetimeimport csvimport collectionsimport pprintimport math# Montreal Trees CSV fileTREES_CSV = os.path.join(os.path.abspath(os.getcwd()), "arbres-publics.csv")# CSV data set columnsCOL = { 'INV_TYPE': 0, 'EMP_NO': 1, 'ARROND': 2, 'ARROND_NOM': 3, 'Rue': 4, 'Rue_cote': 5, 'No_civique': 6, 'Emplacement': 7, 'Sigle': 8, 'Essence_latin': 9, 'Essence_fr': 10, 'Essence_ang': 11, 'DHP,Date_Releve': 12, 'Date_Plantation': 13, 'LOCALISATION': 14, 'Localisation_code': 15, 'CODE_PARC': 16, 'NOM_PARC': 17, 'Rue_de': 18, 'Rue_a': 19, 'Distance_pave': 20, 'Distance_ligne_rue': 21, 'Stationnement_jour': 22, 'Stationnement_heure': 23, 'District': 24, 'Arbre_remarquable': 25, 'Code_secteur': 26, 'Nom_secteur': 27, 'Coord_X': 28, 'Coord_Y': 29, 'Longitude': 30, 'Latitude:': 31,}## Lecture du CSV#pprint.pprint("Lecture de la base de données d'arbres")trees_csv = ()with open(TREES_CSV, mode='r') as file: # Create a CSV reader object trees_csv = list(csv.reader(file)) # Remove first element, it's the header trees_csv.pop(0)pprint.pprint(trees_csv[:10])
Code, itération 2 - Triage et regroupement des données
import osfrom datetime import datetimeimport csvimport collectionsimport pprintimport math# Montreal Trees CSV fileTREES_CSV = os.path.join(os.path.abspath(os.getcwd()), "arbres-publics.csv")# CSV data set columnsCOL = { 'INV_TYPE': 0, 'EMP_NO': 1, 'ARROND': 2, 'ARROND_NOM': 3, 'Rue': 4, 'Rue_cote': 5, 'No_civique': 6, 'Emplacement': 7, 'Sigle': 8, 'Essence_latin': 9, 'Essence_fr': 10, 'Essence_ang': 11, 'DHP,Date_Releve': 12, 'Date_Plantation': 13, 'LOCALISATION': 14, 'Localisation_code': 15, 'CODE_PARC': 16, 'NOM_PARC': 17, 'Rue_de': 18, 'Rue_a': 19, 'Distance_pave': 20, 'Distance_ligne_rue': 21, 'Stationnement_jour': 22, 'Stationnement_heure': 23, 'District': 24, 'Arbre_remarquable': 25, 'Code_secteur': 26, 'Nom_secteur': 27, 'Coord_X': 28, 'Coord_Y': 29, 'Longitude': 30, 'Latitude:': 31,}## For debugging and tracing purposes#def output(txt): print("\n*** " + datetime.now().isoformat() + " ***") pprint.pprint(txt)## Lecture du CSV#output("Lecture de la base de données d'arbres")trees_csv = ()with open(TREES_CSV, mode='r') as file: # Create a CSV reader object trees_csv = list(csv.reader(file)) # Remove first element, it's the header trees_csv.pop(0)#output(trees_csv[:10])# The names of the dictionary kesTOTAL = 'Total'ESSENCE = 'Essence'SIGLE = 'Sigle'# The maximum number of trees to displayMAX_TREES = 15## Grouping by Tree type (Sigle)## A dictionary of dictionaries. Key of outer dict is the type (Sigle), inner dict is made of the# name of the tree (Essence) and the total number in Montreal for that particular treetrees_by_type = {}for row in trees_csv: # Let's get the type (Sigle) of the tree in the this row sigle = row[COL[SIGLE]] # If we already have this type of tree in our dict, incretment the total number if sigle in trees_by_type: trees_by_type[sigle][TOTAL] += 1 # otherwise create that key, and get the name of the tree (Essence) and set the total to 1 else: trees_by_type[sigle] = {TOTAL: 1, ESSENCE: row[COL['Essence_fr']]}# Sort by 'total' in descending ordersorted_trees = sorted(trees_by_type.values(), key=lambda x: x[TOTAL], reverse=True)[:MAX_TREES]output(sorted_trees)
Code, itération 3 - Affichage des barres
import bpyimport osfrom datetime import datetimeimport csvimport collectionsimport pprintimport math# Montreal Trees CSV fileTREES_CSV = os.path.join(os.path.abspath(os.getcwd()), "arbres-publics.csv")# CSV data set columnsCOL = { 'INV_TYPE': 0, 'EMP_NO': 1, 'ARROND': 2, 'ARROND_NOM': 3, 'Rue': 4, 'Rue_cote': 5, 'No_civique': 6, 'Emplacement': 7, 'Sigle': 8, 'Essence_latin': 9, 'Essence_fr': 10, 'Essence_ang': 11, 'DHP,Date_Releve': 12, 'Date_Plantation': 13, 'LOCALISATION': 14, 'Localisation_code': 15, 'CODE_PARC': 16, 'NOM_PARC': 17, 'Rue_de': 18, 'Rue_a': 19, 'Distance_pave': 20, 'Distance_ligne_rue': 21, 'Stationnement_jour': 22, 'Stationnement_heure': 23, 'District': 24, 'Arbre_remarquable': 25, 'Code_secteur': 26, 'Nom_secteur': 27, 'Coord_X': 28, 'Coord_Y': 29, 'Longitude': 30, 'Latitude:': 31,}## For debugging and tracing purposes#def output(txt): print("\n*** " + datetime.now().isoformat() + " ***") pprint.pprint(txt)## Lecture du CSV#output("Lecture de la base de données d'arbres")trees_csv = ()with open(TREES_CSV, mode='r') as file: # Create a CSV reader object trees_csv = list(csv.reader(file)) # Remove first element, it's the header trees_csv.pop(0)#output(trees_csv)# The names of the dictionary kesTOTAL = 'Total'ESSENCE = 'Essence'SIGLE = 'Sigle'# The maximum number of trees to displayMAX_TREES = 15## Grouping by Tree type (Sigle)## A dictionary of dictionaries. Key of outer dict is the type (Sigle), inner dict is made of the# name of the tree (Essence) and the total number in Montreal for that particular treetrees_by_type = {}for row in trees_csv: # Let's get the type (Sigle) of the tree in the this row sigle = row[COL[SIGLE]] # If we already have this type of tree in our dict, incretment the total number if sigle in trees_by_type: trees_by_type[sigle][TOTAL] += 1 # otherwise create that key, and get the name of the tree (Essence) and set the total to 1 else: trees_by_type[sigle] = {TOTAL: 1, ESSENCE: row[COL['Essence_fr']]}# Sort by 'total' in descending ordersorted_trees = sorted(trees_by_type.values(), key=lambda x: x[TOTAL], reverse=True)[:MAX_TREES]output(sorted_trees)## Time to generate the scene# # Select all objects in the scenebpy.ops.object.select_all(action='SELECT')output("Cleaning up the scene")# Delete all selected objectsbpy.ops.object.delete()output("Purging unused data if any")# Clear out any meshes, materials, etc. that might still be in memorybpy.ops.outliner.orphans_purge(do_local_ids=True, do_linked_ids=True, do_recursive=True)# Bar propertiesbar_height = 1.0 # Fixed height (Y axis)bar_depth = 0.5 # Fixed depth (Z axis)spacing = 2.2 # Space between bars on the Y axisscale=10# Find max and min TOTAL to normalize the datamax_total = max(item[TOTAL] for item in sorted_trees)min_total = 0# Normalize TOTAL values (scaling between 0 and 1)for item in sorted_trees: item['TOTAL_NORM'] = (item[TOTAL] - min_total) / (max_total - min_total) if max_total > min_total else 1# Loop through data and create barsfor i, entry in enumerate(sorted_trees): total = entry[TOTAL] total_norm = entry['TOTAL_NORM'] # Use normalized value for the length of the bars essence = entry[ESSENCE] # Create the bar (starting at X = 0, so no need to move it along the X axis) bpy.ops.mesh.primitive_cube_add(location=(0, -i * spacing, 0)) # Adjust location so the bar starts at X=0 bar = bpy.context.object bar.scale = (total_norm * scale, bar_height, bar_depth) # Multiply by a scaling factor to visualize better bar.location.x = total_norm * scale
Code, itération 4 - Ajout du texte et des textures
import bpyimport osfrom datetime import datetimeimport csvimport collectionsimport pprintimport math# Montreal Trees CSV fileTREES_CSV = os.path.join(os.path.abspath(os.getcwd()), "arbres-publics.csv")# CSV data set columnsCOL = { 'INV_TYPE': 0, 'EMP_NO': 1, 'ARROND': 2, 'ARROND_NOM': 3, 'Rue': 4, 'Rue_cote': 5, 'No_civique': 6, 'Emplacement': 7, 'Sigle': 8, 'Essence_latin': 9, 'Essence_fr': 10, 'Essence_ang': 11, 'DHP,Date_Releve': 12, 'Date_Plantation': 13, 'LOCALISATION': 14, 'Localisation_code': 15, 'CODE_PARC': 16, 'NOM_PARC': 17, 'Rue_de': 18, 'Rue_a': 19, 'Distance_pave': 20, 'Distance_ligne_rue': 21, 'Stationnement_jour': 22, 'Stationnement_heure': 23, 'District': 24, 'Arbre_remarquable': 25, 'Code_secteur': 26, 'Nom_secteur': 27, 'Coord_X': 28, 'Coord_Y': 29, 'Longitude': 30, 'Latitude:': 31,}## For debugging and tracing purposes#def output(txt): print("\n*** " + datetime.now().isoformat() + " ***") pprint.pprint(txt)## Lecture du CSV#output("Lecture de la base de données d'arbres")trees_csv = ()with open(TREES_CSV, mode='r') as file: # Create a CSV reader object trees_csv = list(csv.reader(file)) # Remove first element, it's the header trees_csv.pop(0)#output(trees_csv)# The names of the dictionary kesTOTAL = 'Total'ESSENCE = 'Essence'SIGLE = 'Sigle'# The maximum number of trees to displayMAX_TREES = 15## Grouping by Tree type (Sigle)## A dictionary of dictionaries. Key of outer dict is the type (Sigle), inner dict is made of the# name of the tree (Essence) and the total number in Montreal for that particular treetrees_by_type = {}for row in trees_csv: # Let's get the type (Sigle) of the tree in the this row sigle = row[COL[SIGLE]] # If we already have this type of tree in our dict, incretment the total number if sigle in trees_by_type: trees_by_type[sigle][TOTAL] += 1 # otherwise create that key, and get the name of the tree (Essence) and set the total to 1 else: trees_by_type[sigle] = {TOTAL: 1, ESSENCE: row[COL['Essence_fr']]}# Sort by 'total' in descending ordersorted_trees = sorted(trees_by_type.values(), key=lambda x: x[TOTAL], reverse=True)[:MAX_TREES]output(sorted_trees)## Time to generate the scene# # Select all objects in the scenebpy.ops.object.select_all(action='SELECT')output("Cleaning up the scene")# Delete all selected objectsbpy.ops.object.delete()output("Purging unused data if any")# Clear out any meshes, materials, etc. that might still be in memorybpy.ops.outliner.orphans_purge(do_local_ids=True, do_linked_ids=True, do_recursive=True)# Bar propertiesbar_height = 1.0 # Fixed height (Y axis)bar_depth = 0.5 # Fixed depth (Z axis)spacing = 2.2 # Space between bars on the Y axisscale=10# Find max and min TOTAL to normalize the datamax_total = max(item[TOTAL] for item in sorted_trees)min_total = 0# Normalize TOTAL values (scaling between 0 and 1)for item in sorted_trees: item['TOTAL_NORM'] = (item[TOTAL] - min_total) / (max_total - min_total) if max_total > min_total else 1# Loop through data and create barsfor i, entry in enumerate(sorted_trees): total = entry[TOTAL] total_norm = entry['TOTAL_NORM'] # Use normalized value for the length of the bars essence = entry[ESSENCE] # Create the bar (starting at X = 0, so no need to move it along the X axis) bpy.ops.mesh.primitive_cube_add(location=(0, -i * spacing, 0)) # Adjust location so the bar starts at X=0 bar = bpy.context.object bar.scale = (total_norm * scale, bar_height, bar_depth) # Multiply by a scaling factor to visualize better bar.location.x = total_norm * scale # Create a material for the bar based on the TOTAL value (color from red to green) mat = bpy.data.materials.new(name=f"BarMaterial_{i}") color_intensity = total_norm # Use normalized TOTAL for color intensity mat.diffuse_color = (1 - color_intensity, color_intensity, 0, 1) # Transition from red to green bar.data.materials.append(mat) # Create text for essence bpy.ops.object.text_add(location=(-0.5, -i * spacing, 0)) # Place the text to the left of the bar text_obj = bpy.context.object text_obj.data.body = essence text_obj.data.align_x = 'RIGHT' # Create text for the total label bpy.ops.object.text_add(location=(0.5, -i * spacing - spacing / 4, bar_height/2)) # Place the text to the right of the bar text_obj = bpy.context.object text_obj.data.body = str(total) text_obj.data.align_x = 'LEFT' text_obj.scale = (0.5, 0.5, 0.5) bpy.ops.transform.translate(value=(total_norm * scale * 2, 0, 0), orient_type='GLOBAL', orient_matrix=((1, 0, 0), (0, 1, 0), (0, 0, 1)), orient_matrix_type='GLOBAL', constraint_axis=(True, False, False), mirror=False, use_proportional_edit=False, proportional_edit_falloff='SMOOTH', proportional_size=1, use_proportional_connected=False, use_proportional_projected=False, snap=False, snap_elements={'INCREMENT'}, use_snap_project=False, snap_target='CLOSEST', use_snap_self=True, use_snap_edit=True, use_snap_nonedit=True, use_snap_selectable=False)
Code, itération 5 - Animation
import bpyimport osfrom datetime import datetimeimport csvimport collectionsimport pprintimport math# Montreal Trees CSV fileTREES_CSV = os.path.join(os.path.abspath(os.getcwd()), "arbres-publics.csv")# CSV data set columnsCOL = { 'INV_TYPE': 0, 'EMP_NO': 1, 'ARROND': 2, 'ARROND_NOM': 3, 'Rue': 4, 'Rue_cote': 5, 'No_civique': 6, 'Emplacement': 7, 'Sigle': 8, 'Essence_latin': 9, 'Essence_fr': 10, 'Essence_ang': 11, 'DHP,Date_Releve': 12, 'Date_Plantation': 13, 'LOCALISATION': 14, 'Localisation_code': 15, 'CODE_PARC': 16, 'NOM_PARC': 17, 'Rue_de': 18, 'Rue_a': 19, 'Distance_pave': 20, 'Distance_ligne_rue': 21, 'Stationnement_jour': 22, 'Stationnement_heure': 23, 'District': 24, 'Arbre_remarquable': 25, 'Code_secteur': 26, 'Nom_secteur': 27, 'Coord_X': 28, 'Coord_Y': 29, 'Longitude': 30, 'Latitude:': 31,}## For debugging and tracing purposes#def output(txt): print("\n*** " + datetime.now().isoformat() + " ***") pprint.pprint(txt)## Lecture du CSV#output("Lecture de la base de données d'arbres")trees_csv = ()with open(TREES_CSV, mode='r') as file: # Create a CSV reader object trees_csv = list(csv.reader(file)) # Remove first element, it's the header trees_csv.pop(0)#output(trees_csv)# The names of the dictionary keysTOTAL = 'Total'ESSENCE = 'Essence'SIGLE = 'Sigle'# The maximum number of trees to displayMAX_TREES = 15## Grouping by Tree type (Sigle)## A dictionary of dictionaries. Key of outer dict is the type (Sigle), inner dict is made of the# name of the tree (Essence) and the total number in Montreal for that particular treetrees_by_type = {}for row in trees_csv: # Let's get the type (Sigle) of the tree in the this row sigle = row[COL[SIGLE]] # If we already have this type of tree in our dict, incretment the total number if sigle in trees_by_type: trees_by_type[sigle][TOTAL] += 1 # otherwise create that key, and get the name of the tree (Essence) and set the total to 1 else: trees_by_type[sigle] = {TOTAL: 1, ESSENCE: row[COL['Essence_fr']]}# Sort by 'total' in descending ordersorted_trees = sorted(trees_by_type.values(), key=lambda x: x[TOTAL], reverse=True)[:MAX_TREES]output(sorted_trees)## Time to generate the scene# # Select all objects in the scenebpy.ops.object.select_all(action='SELECT')output("Cleaning up the scene")# Delete all selected objectsbpy.ops.object.delete()output("Purging unused data if any")# Clear out any meshes, materials, etc. that might still be in memorybpy.ops.outliner.orphans_purge(do_local_ids=True, do_linked_ids=True, do_recursive=True)# Bar propertiesbar_height = 1.0 # Fixed height (Y axis)bar_depth = 0.5 # Fixed depth (Z axis)spacing = 2.2 # Space between bars on the Y axisscale=10# Find max and min TOTAL to normalize the datamax_total = max(item[TOTAL] for item in sorted_trees)min_total = 0# Normalize TOTAL values (scaling between 0 and 1)for item in sorted_trees: item['TOTAL_NORM'] = (item[TOTAL] - min_total) / (max_total - min_total) if max_total > min_total else 1# Create an empty, we will use it for some animationbpy.ops.object.empty_add(type='PLAIN_AXES', align='WORLD', location=(0, 0, 0), scale=(1, 1, 1))empty = bpy.context.object# Loop through data and create barsfor i, entry in enumerate(sorted_trees): total = entry[TOTAL] total_norm = entry['TOTAL_NORM'] # Use normalized value for the length of the bars essence = entry[ESSENCE] # Create the bar (starting at X = 0, so no need to move it along the X axis) bpy.ops.mesh.primitive_cube_add(location=(0, -i * spacing, 0)) # Adjust location so the bar starts at X=0 bar = bpy.context.object bar.scale = (total_norm * scale, bar_height, bar_depth) # Multiply by a scaling factor to visualize better bar.location.x = total_norm * scale # Make the bar the child of emtpy bar.parent = empty # Create a material for the bar based on the TOTAL value (color from red to green) mat = bpy.data.materials.new(name=f"BarMaterial_{i}") color_intensity = total_norm # Use normalized TOTAL for color intensity mat.diffuse_color = (1 - color_intensity, color_intensity, 0, 1) # Transition from red to green bar.data.materials.append(mat) # Create text for essence bpy.ops.object.text_add(location=(-0.5, -i * spacing, 0)) # Place the text to the left of the bar text_obj = bpy.context.object text_obj.data.body = essence text_obj.data.align_x = 'RIGHT' # Make the text the child of emtpy text_obj.parent = empty # Create text for the total label bpy.ops.object.text_add(location=(0.5, -i * spacing - spacing / 4, bar_height/2)) # Place the text to the right of the bar text_obj = bpy.context.object text_obj.data.body = str(total) text_obj.data.align_x = 'LEFT' text_obj.scale = (0.5, 0.5, 0.5) bpy.ops.transform.translate(value=(total_norm * scale * 2, 0, 0), orient_type='GLOBAL', orient_matrix=((1, 0, 0), (0, 1, 0), (0, 0, 1)), orient_matrix_type='GLOBAL', constraint_axis=(True, False, False), mirror=False, use_proportional_edit=False, proportional_edit_falloff='SMOOTH', proportional_size=1, use_proportional_connected=False, use_proportional_projected=False, snap=False, snap_elements={'INCREMENT'}, use_snap_project=False, snap_target='CLOSEST', use_snap_self=True, use_snap_edit=True, use_snap_nonedit=True, use_snap_selectable=False) # Make the label the child of emtpy text_obj.parent = empty# Set up the cameracam_data = bpy.data.cameras.new(name="Camera")cam_obj = bpy.data.objects.new("Camera", cam_data)bpy.context.collection.objects.link(cam_obj)# Set camera rotation and lensecam_obj.rotation_euler = (0, 12 * math.pi/180, 0) # Rotate to face the graph (along -Z axis)cam_obj.data.lens = 35cam_obj.location = (scale + MAX_TREES, -MAX_TREES, MAX_TREES * scale/1.5) # Adjust camera position based on the largest bar# Make the camera activebpy.context.scene.camera = cam_obj# Set up lightinglight_data = bpy.data.lights.new(name="Light", type='AREA')light = bpy.data.objects.new(name="Light", object_data=light_data)light.data.energy = 500light.location = (10, -MAX_TREES, 10)bpy.context.collection.objects.link(light)## Time for some animation!#start_frame = 1end_frame = 120min_rotation = -30max_rotation = 30bpy.context.scene.frame_start = start_framebpy.context.scene.frame_end = end_framedef setKeyframe(object, frame, rotation): bpy.context.scene.frame_set(frame) # Set current frame object.rotation_euler[1] = math.radians(rotation) # Rotate along Y-axis object.keyframe_insert(data_path="rotation_euler", index=1) # Insert keyframe for Y-axissetKeyframe(empty, start_frame, min_rotation)setKeyframe(empty, int(round(end_frame - start_frame)/2), max_rotation)setKeyframe(empty, end_frame, min_rotation)# Set interpolation to linearfor fcurve in empty.animation_data.action.fcurves: for keyframe in fcurve.keyframe_points: keyframe.interpolation = 'BEZIER' # Set interpolation to linearbpy.context.scene.frame_set(1)# Play the animationbpy.ops.screen.animation_play()