# Blender et Python 2

Python et les arbres de Montréal

Ces deux pages ouèbe vont nous être très utiles, bookmarquez-les

Téléchargement d’un CSV - les arbres de Montréal

  • Le fichier CSV que l’on va analyser se trouve ici: https://drive.google.com/file/d/1-Z_wJQGVzgibD86AsNV1e6ao1yEw249w/view?usp=sharing
  • 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 os
from datetime import datetime
import csv
import collections
import pprint
import math
 
 
 
# Montreal Trees CSV file
TREES_CSV = os.path.join(os.path.abspath(os.getcwd()), "arbres-publics.csv")
 
# CSV data set columns
COL = {
    '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 os
from datetime import datetime
import csv
import collections
import pprint
import math
 
 
 
# Montreal Trees CSV file
TREES_CSV = os.path.join(os.path.abspath(os.getcwd()), "arbres-publics.csv")
 
# CSV data set columns
COL = {
    '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 kes
TOTAL = 'Total'
ESSENCE = 'Essence'
SIGLE = 'Sigle'
 
# The maximum number of trees to display
MAX_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 tree
trees_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 order
sorted_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 bpy
import os
from datetime import datetime
import csv
import collections
import pprint
import math
 
 
 
# Montreal Trees CSV file
TREES_CSV = os.path.join(os.path.abspath(os.getcwd()), "arbres-publics.csv")
 
# CSV data set columns
COL = {
    '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 kes
TOTAL = 'Total'
ESSENCE = 'Essence'
SIGLE = 'Sigle'
 
# The maximum number of trees to display
MAX_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 tree
trees_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 order
sorted_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 scene
bpy.ops.object.select_all(action='SELECT')
 
output("Cleaning up the scene")
 
# Delete all selected objects
bpy.ops.object.delete()
 
output("Purging unused data if any")
 
# Clear out any meshes, materials, etc. that might still be in memory
bpy.ops.outliner.orphans_purge(do_local_ids=True, do_linked_ids=True, do_recursive=True)
 
 
# Bar properties
bar_height = 1.0  # Fixed height (Y axis)
bar_depth = 0.5   # Fixed depth (Z axis)
spacing = 2.2     # Space between bars on the Y axis
scale=10
 
# Find max and min TOTAL to normalize the data
max_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 bars
for 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 bpy
import os
from datetime import datetime
import csv
import collections
import pprint
import math
 
 
 
# Montreal Trees CSV file
TREES_CSV = os.path.join(os.path.abspath(os.getcwd()), "arbres-publics.csv")
 
# CSV data set columns
COL = {
    '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 kes
TOTAL = 'Total'
ESSENCE = 'Essence'
SIGLE = 'Sigle'
 
# The maximum number of trees to display
MAX_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 tree
trees_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 order
sorted_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 scene
bpy.ops.object.select_all(action='SELECT')
 
output("Cleaning up the scene")
 
# Delete all selected objects
bpy.ops.object.delete()
 
output("Purging unused data if any")
 
# Clear out any meshes, materials, etc. that might still be in memory
bpy.ops.outliner.orphans_purge(do_local_ids=True, do_linked_ids=True, do_recursive=True)
 
 
# Bar properties
bar_height = 1.0  # Fixed height (Y axis)
bar_depth = 0.5   # Fixed depth (Z axis)
spacing = 2.2     # Space between bars on the Y axis
scale=10
 
# Find max and min TOTAL to normalize the data
max_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 bars
for 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 bpy
import os
from datetime import datetime
import csv
import collections
import pprint
import math
 
 
 
# Montreal Trees CSV file
TREES_CSV = os.path.join(os.path.abspath(os.getcwd()), "arbres-publics.csv")
 
# CSV data set columns
COL = {
    '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 keys
TOTAL = 'Total'
ESSENCE = 'Essence'
SIGLE = 'Sigle'
 
# The maximum number of trees to display
MAX_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 tree
trees_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 order
sorted_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 scene
bpy.ops.object.select_all(action='SELECT')
 
output("Cleaning up the scene")
 
# Delete all selected objects
bpy.ops.object.delete()
 
output("Purging unused data if any")
 
# Clear out any meshes, materials, etc. that might still be in memory
bpy.ops.outliner.orphans_purge(do_local_ids=True, do_linked_ids=True, do_recursive=True)
 
 
# Bar properties
bar_height = 1.0  # Fixed height (Y axis)
bar_depth = 0.5   # Fixed depth (Z axis)
spacing = 2.2     # Space between bars on the Y axis
scale=10
 
# Find max and min TOTAL to normalize the data
max_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 animation
bpy.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 bars
for 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 camera
cam_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 lense
cam_obj.rotation_euler = (0, 12 * math.pi/180, 0)  # Rotate to face the graph (along -Z axis)
cam_obj.data.lens = 35
cam_obj.location = (scale + MAX_TREES, -MAX_TREES, MAX_TREES * scale/1.5)  # Adjust camera position based on the largest bar
 
 
# Make the camera active
bpy.context.scene.camera = cam_obj
 
 
# Set up lighting
light_data = bpy.data.lights.new(name="Light", type='AREA')
light = bpy.data.objects.new(name="Light", object_data=light_data)
light.data.energy = 500
light.location = (10, -MAX_TREES, 10)
bpy.context.collection.objects.link(light)
 
 
 
 
 
#
# Time for some animation!
#
start_frame = 1
end_frame = 120
min_rotation = -30
max_rotation = 30
 
bpy.context.scene.frame_start = start_frame
bpy.context.scene.frame_end = end_frame
 
 
def 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-axis
 
 
setKeyframe(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 linear
for fcurve in empty.animation_data.action.fcurves:
    for keyframe in fcurve.keyframe_points:
        keyframe.interpolation = 'BEZIER'  # Set interpolation to linear
 
 
bpy.context.scene.frame_set(1)
 
# Play the animation
bpy.ops.screen.animation_play()

Resultat