Cet article décrit comment créer un script Python permettant de transformer un rapport CIPM extrait de l'outil Progress Monitoring en :
- fichiers BCF à utiliser avec un modèle Revit ou tout autre outil
- Annotations mises en forme dans un projet Cintoo, liées aux éléments de modèle problématiques en fonction du paramètre Coverage
Autorisations : les utilisateurs dont le rôle inclut l'autorisation Gérer les annotations peuvent créer des annotations à l'aide de ce script.
Un exemple de script est fourni en pièce jointe. Tous les scripts complémentaires sont également joints.
TABLE DES MATIÈRES
- Prérequis
- Informations à saisir
- Autorisation
- Limites
- Résultats
- Explication du script CreateBCF.py
- Explication du script CreateAnnotation.py
Prérequis
Installez Python en suivant les instructions du chatbot IA de votre choix. Demandez comment exécuter des scripts Python depuis l'invite de commandes si aucun environnement dédié n'est installé.
Ce script Python utilise les bibliothèques suivantes pour exécuter le flux de travail.
Installez-les avant d'utiliser le script :
- png - exécutez la commande suivante pour connecter la bibliothèque
pip install pypng- requests - exécutez la commande suivante pour connecter la bibliothèque
pip install requestsInformations à saisir
Exécutez le script createBCF.py dans l'invite de commandes. Tous les autres scripts sont des scripts complémentaires.
python createBCF.py
Une fois exécuté, le script invite l'utilisateur à fournir les informations suivantes :
- Insert the path to the CIPM .csv report - chemin complet du rapport CIPM au format CSV, avec l'extension
Exemple : C:\Projects\ProgressMonitoring\Walls_0.0500(Meters).csv - Insert the path where BCF files should be created - chemin complet où créer et stocker localement les fichiers BCF
Exemple : C:\Projects\ProgressMonitoring\BCF - Insert the minimum coverage - limite définie par l'utilisateur, en pourcentage : toutes les valeurs de Coverage strictement inférieures à cette limite sont prises en compte.
Exemple : pour créer des BCF/annotations pour les éléments de modèle dont la couverture de scan est inférieure à 25 %, saisissez la valeur 25 - Insert the path to the workzone where to create annotations - adresse URL complète de la zone de travail dans le projet Cintoo.
Remarque : il est fortement recommandé de créer une zone de travail dédiée aux annotations créées par ce script, ce qui permet de mieux contrôler le résultat et de faciliter le nettoyage en cas de besoin.
Exemple : https://aec.cintoo.com/accounts/0a000a00-000a-000a-a0000aa0a00000a0/
projects/a0a0000a-aaa0000a-000a-aaaa00aaa000/workzones/aaaaaaaaaa0aaaaaaaa/data
Copiez cette URL directement depuis le navigateur.
Autorisation
Une fois tous les paramètres saisis, l'utilisateur est invité à autoriser l'utilisation d'un compte Cintoo dans une page de navigateur qui s'ouvre en fenêtre contextuelle. Cliquez sur Allow pour continuer.
Une fois l'autorisation accordée, le message suivant s'affiche pour signaler que l'authentification a réussi. Cet onglet du navigateur peut être fermé en toute sécurité.

Cintoo-tokens.json - fichier JSON d'authentification créé par le script pour établir la connexion. Il est recommandé de le supprimer chaque fois qu'un utilisateur a terminé son travail.
Remarque : le temps nécessaire pour générer les fichiers BCF et les annotations dépend de la valeur du niveau de couverture et du nombre d'éléments de modèle.
Limites
Avertissement : veuillez noter que le script ne peut créer, via les API, qu'un maximum de 3000 annotations.
Résultats
- Fichiers BCF enregistrés localement à l'emplacement défini par l'utilisateur, utilisables dans Revit (par exemple) pour détecter les éléments de modèle problématiques et les corriger

- Annotations Cintoo dans le projet, liées visuellement aux éléments de modèle, avec toutes les données nécessaires dans la description (modifiable).

Explication du script CreateBCF.py
Vous trouverez ci-dessous le script, avec des commentaires sur chaque partie.
Avertissement : si des modifications du code sont nécessaires, elles sont possibles, mais Cintoo n'assume aucune responsabilité quant au fonctionnement de ce code une fois modifié.
import csv
import os
import zipfile
import png
from createAnnotation import createAnnotation
import login
from cintooUtils import parse_cintoo_workzone_context_url
import datetime
import uuid
def create_png(path):
width = 255
height = 255
img = []
for y in range(height):
row = ()
for x in range(width):
row = row + (x, max(0, 255 - x - y), y)
img.append(row)
with open(path, 'wb') as f:
w = png.Writer(width, height, greyscale=False)
w.write(f, img)Importation de toutes les bibliothèques et de tous les scripts complémentaires nécessaires au fonctionnement du script principal. Création d'un modèle png, nécessaire à la création d'un fichier BCF.
def create_bcfzip(input_csv,
output_dir,
min_coverage,
tenant=None,
account_id=None,
project_id=None,
workzone_id=None,
camera_state=None,
headers=None):
if not os.path.exists(output_dir):
os.makedirs(output_dir)
report_name = input_csv.split("\\")[-1]
with open(input_csv, 'r', encoding='utf-8') as file:
reader = csv.DictReader(file)
elements_to_be_created = [row for row in reader if float(row['Coverage by Scan Data (%)']) < min_coverage]
if len(elements_to_be_created) > 1000:
print("More than 1000 elements have a lower score than the minimum coverage score. Only the top 1000 will be created.")
elements_to_be_created = sorted(
elements_to_be_created,
key=lambda x: float(x['Coverage by Scan Data (%)']),
reverse=True
)[0:999]
for row in elements_to_be_created:
if (row.get('IfcGUID') or row.get('GlobalId')) and row.get('Coverage by Scan Data (%)'):
print(row)
try:
coverage = float(row['Coverage by Scan Data (%)'])
print(coverage)
except ValueError:
continue
guid = row.get('IfcGUID') or row.get('GlobalId')
element_name = row.get('Model Element Name', 'Unknown')
element_id = list(row.values())[0]
print(element_id)
create_bcf_file(guid, element_name, output_dir, coverage, report_name)
if all([tenant, account_id, project_id, workzone_id, camera_state]):
createAnnotation(tenant,
account_id,
project_id,
workzone_id,
f'Low Coverage Issue: {element_name}',
f"The element {element_name} has a coverage score of {coverage}, according to the report {report_name}",
{"x": 0, "y": 0, "z": 0},
camera_state,
headers,
modelElementId=element_id)Fonction qui crée des fichiers BCF liés à chaque élément retenu en fonction du paramètre Coverage défini par l'utilisateur.
Elle analyse toutes les données nécessaires à partir des paramètres fournis par l'utilisateur (dossier de sortie, couverture minimale, tenant, etc.) et des GUID existants dans les lignes du rapport CIPM, afin de les extraire en fonction de la comparaison de couverture.
Avertissement : il n'est pas recommandé de modifier la fonction create_bcfzip, sauf s'il est nécessaire de modifier le fonctionnement de la condition de sélection des données (par exemple, coverage<min_coverage). Pour modifier le texte, la description ou l'étiquetage des annotations, veuillez vous référer à la fonction create_issue_xml.
def create_bcf_file(guid, element_name, output_dir, coverage, report_name):
bcf_dir = os.path.join(output_dir, guid)
os.makedirs(bcf_dir, exist_ok=True)
viewpoint_guid = str(uuid.uuid4())
comment_guid = str(uuid.uuid4())
topic_guid = str(uuid.uuid4())
project_guid = str(uuid.uuid4())
viewpoint_filename = os.path.join(bcf_dir, 'viewpoint.bcfv')
viewpoint_xml = create_viewpoint_xml(guid, viewpoint_guid)
with open(viewpoint_filename, 'w', encoding='utf-8') as f:
f.write(viewpoint_xml)
issue_filename = os.path.join(bcf_dir, 'markup.bcf')
issue_xml = create_issue_xml(element_name, coverage, report_name, viewpoint_guid, comment_guid, topic_guid)
with open(issue_filename, 'w', encoding='utf-8') as f:
f.write(issue_xml)
project_filename = os.path.join(output_dir, 'project.bcfp')
project_xml = create_project_xml(project_guid)
with open(project_filename, 'w', encoding='utf-8') as f:
f.write(project_xml)
version_filename = os.path.join(output_dir, 'bcf.version')
with open(version_filename, 'w', encoding='utf-8') as f:
f.write(create_bcf_version())
snapshot_filename = os.path.join(output_dir, 'snapshot.png')
create_png(snapshot_filename)
zip_filename = os.path.join(output_dir, f'{guid}.bcf')
with zipfile.ZipFile(zip_filename, 'w') as zf:
zf.write(viewpoint_filename, os.path.join(topic_guid, 'viewpoint.bcfv'))
zf.write(issue_filename, os.path.join(topic_guid, 'markup.bcf'))
zf.write(snapshot_filename, os.path.join(topic_guid, 'snapshot.png'))
zf.write(project_filename, os.path.join('project.bcfp'))
zf.write(version_filename, os.path.join('bcf.version'))
# Nettoyage
os.remove(viewpoint_filename)
os.remove(issue_filename)
os.remove(project_filename)
os.remove(version_filename)
os.remove(snapshot_filename)
os.rmdir(bcf_dir)Création de fichiers BCF individuels.
def create_viewpoint_xml(guid, viewpoint_guid):
return f"""<?xml version="1.0" encoding="UTF-8"?>
<VisualizationInfo Guid="{viewpoint_guid}">
<Components>
<ViewSetupHints SpacesVisible="false" SpaceBoundariesVisible="false" OpeningsVisible="false" />
<Selection>
<Component IfcGuid="{guid}" />
</Selection>
<Visibility DefaultVisibility="true" />
</Components>
<OrthogonalCamera>
<CameraViewPoint>
<X>129.93820633961636</X>
<Y>-124.61204504554462</Y>
<Z>104.47418360973809</Z>
</CameraViewPoint>
<CameraDirection>
<X>-0.58963662529065941</X>
<Y>0.5647967039409042</Y>
<Z>-0.57735026918962584</Z>
</CameraDirection>
<CameraUpVector>
<X>-0.41693605617897656</X>
<Y>0.39937157934842415</Y>
<Z>0.81649658092772603</Z>
</CameraUpVector>
<ViewToWorldScale>44.256083397559856</ViewToWorldScale>
</OrthogonalCamera>
</VisualizationInfo>"""Définition de la position de caméra par défaut.
def create_issue_xml(element_name, coverage, report_name, viewpoint_guid, comment_guid, topic_guid):
return f"""<?xml version="1.0" encoding="UTF-8"?>
<Markup>
<Topic Guid="{topic_guid}" TopicType="Issue" TopicStatus="Active">
<Title>Low Coverage Issue: {element_name}</Title>
<Priority>Normal</Priority>
<Index>1</Index>
<CreationDate>2025-03-05T09:22:02+00:00</CreationDate>
<CreationAuthor>youremail@domain.com</CreationAuthor>
<ModifiedDate>2025-03-05T09:22:03+00:00</ModifiedDate>
<ModifiedAuthor>youremail@domain.com</ModifiedAuthor>
<DueDate>2025-03-05T09:00:00+00:00</DueDate>
<AssignedTo>youremail@domain.com</AssignedTo>
<Description>The element {element_name} has a coverage score of {coverage}, according to the report {report_name}</Description>
</Topic>
<Comment Guid="{comment_guid}">
<Date>2025-03-04T09:53:03+00:00</Date>
<Author>youremail@domain.com</Author>
<Comment>The element {element_name} has a coverage score of {coverage}, according to the report {report_name}</Comment>
<Viewpoint Guid="{viewpoint_guid}" />
<ModifiedDate>2025-03-04T09:53:03+00:00</ModifiedDate>
<ModifiedAuthor>youremail@domain.com</ModifiedAuthor>
</Comment>
<Viewpoints Guid="{viewpoint_guid}">
<Viewpoint>viewpoint.bcfv</Viewpoint>
<Snapshot>snapshot.png</Snapshot>
</Viewpoints>
</Markup>
"""Création de fichiers XML pour les métadonnées des problèmes.
Saisissez les informations nécessaires sur l'auteur dans les champs :
- CreationAuthor - ligne 154
- ModifiedAuthor - ligne 156
- AssignedTo - ligne 158
- Author - ligne 163
- ModifiedAuthor - ligne 167
def create_project_xml(projectGuid):
return f"""<?xml version="1.0" encoding="UTF-8"?>
<ProjectExtension>
<Project ProjectId="{projectGuid}" />
<ExtensionSchema></ExtensionSchema>
</ProjectExtension>"""Création d'un fichier référençant un projet.
def create_extensions_xsd():
return """<?xml version="1.0" encoding="UTF-8" standalone="no" ?>
<schema>
<redefine schemaLocation="markup.xsd">
<simpleType name="Priority">
<restriction base="Priority">
<enumeration value="Aucune"/>
<enumeration value="Faible"/>
<enumeration value="Moyenne"/>
<enumeration value="Haute"/>
<enumeration value="Critique"/>
</restriction>
</simpleType>
<simpleType name="TopicStatus">
<restriction base="TopicStatus">
<enumeration value="Assignée"/>
<enumeration value="En cours"/>
<enumeration value="Terminée"/>
<enumeration value="Bloquée"/>
<enumeration value="Validée"/>
<enumeration value="Archivée"/>
</restriction>
</simpleType>
<simpleType name="TopicLabel">
<restriction base="TopicLabel"/>
</simpleType>
</redefine>
</schema>
"""Description de la structure BCF (catégories).
def create_bcf_version():
return """<?xml version="1.0" encoding="UTF-8"?>
<Version VersionId="2.1" xsi:noNamespaceSchemaLocation="version.xsd" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<DetailedVersion>2.1 KUBUS BV</DetailedVersion>
</Version>"""Définition de la version BCF (2.1).
def main():
input_csv = input("Insert the path to the CIPM .csv report :")
output_dir = input("Insert the path where BCF files should be created :")
min_coverage = float(input("Insert the minimum coverage"))
context_url = input("Insert the path to the workzone where to create annotations (e.g: "
"https://aec.cintoo.com/accounts/0a000a00-000a-000a-a000-0aa0a00000a0/projects/a0a0000a-aaa0"
"-000a-000a-aaaa00aaa000/workzones/aaaaaaaaaa0aaaaaaaa/data ")
context_details = parse_cintoo_workzone_context_url(context_url)
tenant, account_id, project_id, workzone_id = context_details['tenant'], context_details['accountId'], \
context_details['projectId'], context_details['workzoneId']
# Authentification et obtention du jeton d'accès
login.load_tokens()
token = login.get_token(tenant)
headers = {"Authorization": f"Bearer {token}"}
camera_state = {"type": "legacy",
"camerastate": {
"position": [0, 0, 1],
"rotationQuaternion": [0, 0, 0, 1],
"fov": 1.5708,
"ortho": False,
"overviewScale": 10,
"camType": "scan"},
}
create_bcfzip(input_csv,
output_dir,
min_coverage,
tenant,
account_id,
project_id,
workzone_id,
camera_state,
headers)
main()Définition de la partie principale du script : questions de saisie posées à l'utilisateur, obtention du jeton d'accès et authentification avec le compte Cintoo conformément à Authentification Cintoo API, position de caméra par défaut, modifiée une fois importée dans Cintoo, création des fichiers BCFzip de sortie et exécution de la fonction principale.
Explication du script CreateAnnotation.py
Vous trouverez ci-dessous le script, avec des commentaires sur chaque partie.
Avertissement : si des modifications du code sont nécessaires, elles sont possibles, mais Cintoo n'assume aucune responsabilité quant au fonctionnement de ce code une fois modifié.
import login
import requests
from cintooUtils import parse_cintoo_workzone_context_url
TIMEOUT = 10Importation de toutes les bibliothèques et données nécessaires issues des scripts complémentaires pour assister le script principal. Définition d'un paramètre de délai d'attente (timeout).
def getWorkzoneGuid(tenant, account_id, project_id, workzone_idv1, headers):
url = f"{tenant}/api/2/accounts/{account_id}/projects/{project_id}/workzones"
response = requests.get(url, headers=headers, timeout=TIMEOUT)
response.raise_for_status()
return [wz for wz in response.json() if wz["api1Id"] == workzone_idv1]Extraction de l'ID de la zone de travail à partir de la saisie de l'utilisateur.
def createAnnotation(tenant,
account_id,
project_id,
workzone_id,
title,
description,
position,
saved_view,
headers,
modelElementId=None,):
"""Creates a project in the specified account."""
workzone_guid = getWorkzoneGuid(tenant, account_id, project_id, workzone_id, headers)[0]['id'].split(':')[-1]
url = f"{tenant}/api/2/accounts/{account_id}/projects/{project_id}/annotations"
print(f"modelElementId : {modelElementId}")
if modelElementId:
body = {
'workzoneId': workzone_guid,
'annotationType': "Note",
'title': title,
'description': description,
'position': position,
'normal': {"x": 0, "y": 0, "z": 1},
'savedView': {**saved_view, 'modelElementId': modelElementId},
}
else:
body = {
'workzoneId': workzone_guid,
'annotationType': "Note",
'title': title,
'description': description,
'position': position,
'normal': {"x": 0, "y": 0, "z": 1},
'savedView': saved_view,
'elementId': "Toto"
}
print(url)
print(body)
response = requests.post(url, headers=headers, json=body, timeout=TIMEOUT)
print(response.text)
response.raise_for_status()
return response.json()Analyse des informations de localisation Cintoo à partir de la saisie de l'utilisateur, afin de rattacher chaque annotation à l'élément de modèle correspondant.
def main(context_url):
context_details = parse_cintoo_workzone_context_url(context_url)
tenant, account_id, project_id, workzone_id = context_details['tenant'], context_details['accountId'], \
context_details['projectId'], context_details['workzoneId']
# Authentification et obtention du jeton d'accès
login.load_tokens()
token = login.get_token(tenant)
headers = {"Authorization": f"Bearer {token}"}
camera_state = {"type": "legacy",
"camerastate": {
"position": [0, 0, 1],
"rotationQuaternion": [0, 0, 0, 1],
"fov": 1.5708,
"ortho": False,
"overviewScale": 10,
"camType": "scan"}}
print(workzone_id)
createAnnotation(tenant, account_id, project_id, workzone_id, "Test API", "Created with API",
{"x": 0, "y": 0, "z": 0}, camera_state, headers)Fonction principale comprenant le processus d'authentification, la définition de la position de caméra et la création des annotations dans le projet Cintoo.
Cet article a-t-il été utile ?
C'est super !
Merci pour votre commentaire
Désolé ! Nous n'avons pas pu vous être utile
Merci pour votre commentaire
Commentaires envoyés
Nous apprécions vos efforts et nous allons corriger l'article