Este artículo describe cómo crear un script de Python que permite transformar un reporte de CIPM extraído de la herramienta Progress Monitoring en:
- Archivos BCF para usar con un modelo Revit o cualquier otra herramienta
- formateadas Annotations dentro de un proyecto Cintoo ligado a elementos problemáticos del modelo basados en el parámetro Coverage
Permisos: usuarios con roles que incluyen Gestionar Anotaciones pueden crear anotaciones usando este script.
Se proporciona un ejemplo de script en el archivo adjunto. También se adjuntan todos los scripts complementarios.
ÍNDICE DE CONTENIDOS
- Requisitos previos
- Información de entrada
- Autorización
- Limitaciones
- Resultados
- CreateBCF.py Script Explicado
- CreateAnnotation.py Script Explicado
Requisitos previos
Instalar Python usando las instrucciones del chatbot de IA de su elección. Pida una instrucción para ejecutar scripts de Python desde el símbolo del sistema si no hay un entorno dedicado instalado.
En este script de Python se utilizan las siguientes bibliotecas para ejecutar el flujo de trabajo.
Instálelas antes de usar el script:
- png - ejecute el siguiente comando para conectar la biblioteca
pip install pypng- requests - ejecute el siguiente comando para conectar la biblioteca
pip install requestsInformación de entrada
Ejecute el script createBCF.py en el símbolo del sistema. Todos los demás scripts son complementarios.
python createBCF.py
Una vez ejecutado, el script solicitará al usuario que proporcione la siguiente información:
- Ingrese la ruta al reporte CIPM .csv - ruta completa al reporte CIPM en formato CSV con extensión
Ejemplo: C:\Projects\ProgressMonitoring\Walls_0.0500(Meters).csv - Ingrese la ruta donde se deben crear los archivos BCF - ruta completa para crear y almacenar archivos BCF localmente
Ejemplo: C:\Projects\ProgressMonitoring\BCF - Ingrese la cobertura mínima - límite definido por el usuario para tomar todos los valores de Coverage estrictamente por debajo de él en porcentaje.
Ejemplo: para crear BCF/Anotaciones para elementos del modelo con menos del 25% de cobertura de escaneo, inserte el valor 25 - Ingrese la ruta a la workzone donde crear anotaciones - dirección URL completa a la work zone en el proyecto Cintoo.
Nota: se recomienda encarecidamente crear una zona de trabajo dedicada para las anotaciones creadas por este script, ya que esto permitiría un mayor control sobre el resultado y una limpieza más fácil en caso necesario.
Ejemplo: https://aec.cintoo.com/accounts/0a000a00-000a-000a-a0000aa0a00000a0/
projects/a0a0000a-aaa0000a-000a-aaaa00aaa000/workzones/aaaaaaaaaa0aaaaaaaa/data
Copie esta URL directamente desde el navegador.
Autorización
Una vez que se insertan todos los parámetros, se solicita al usuario que autorice el uso de una cuenta Cintoo en una página del navegador emergente. Haga clic en Permitir para continuar.
Una vez autorizado, aparecerá el siguiente mensaje señalando una autenticación exitosa. Esta pestaña del navegador podríacerrarse de manera segura.

Cintoo-tokens.json - json de autenticación creado por el script para establecer la conexión. Se recomienda eliminarlo cada vez que un usuario finaliza el trabajo.
Nota: el tiempo dedicado a generar BCF y anotaciones se basa en el valor del nivel de cobertura y la cantidad de elementos del modelo.
Limitaciones
Advertencia: tenga en cuenta que se puede crear un máximo de 3000 anotaciones con el script usando APIs.
Resultados
- Archivos BCF guardados localmente en la ruta definida por el usuario, que se pueden usar en Revit (por ejemplo) para detectar elementos problemáticos del modelo y corregirlos

- Cintoo Anotaciones en el proyecto, vinculadas visualmente a elementos del modelo con todos los datos necesarios en la descripción (editable).

CreateBCF.py Script Explicado
A continuación, encontrará el script con comentarios en cada parte.
Advertencia: si es necesario hacer ajustes en el código, es posible, pero Cintoo no asume ninguna responsabilidad sobre la funcionalidad de este código una vez modificado.
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)Importando todas las bibliotecas necesarias y scripts complementarios para apoyar el script principal. Creando un png de plantilla, que es necesario para crear un archivo 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. Solo se crearán los primeros 1000.")
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'Problema de Baja Cobertura: {element_name}',
f"El elemento {element_name} tiene un puntaje de cobertura de {coverage}, de acuerdo con el informe {report_name}",
{"x": 0, "y": 0, "z": 0},
camera_state,
headers,
modelElementId=element_id)Función que crea archivos BCF relacionados con cada elemento que se toma en función del parámetro de Coverage definido por el usuario.
Analiza todos los datos necesarios de los parámetros proporcionados por el usuario, como el directorio de salida, la cobertura mínima, el inquilino, etc., y los GUID existentes en las líneas del informe CIPM para extraerlos en función de la comparación de cobertura.
Advertencia: no se recomienda cambiar la función create_bcfzip, solo si hay necesidad de cambiar la forma en que funciona la condición para elegir los datos (por ejemplo, coverage<min_coverage). Para cambios en el texto de la anotación, la descripción, el etiquetado, consulte la función 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'))
# Cleanup
os.remove(viewpoint_filename)
os.remove(issue_filename)
os.remove(project_filename)
os.remove(version_filename)
os.remove(snapshot_filename)
os.rmdir(bcf_dir)Creando archivos BCF individuales.
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>"""Configurando la posición predeterminada de la cámara.
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>
"""Creando XMLs para los metadatos de problemas.
Ingrese la información necesaria sobre el autor en los campos:
- CreationAuthor - línea 154
- ModifiedAuthor - línea 156
- AssignedTo - línea 158
- Author - línea 163
- ModifiedAuthor - línea 167
def create_project_xml(projectGuid):
return f"""<?xml version="1.0" encoding="UTF-8"?>
<ProjectExtension>
<Project ProjectId="{projectGuid}" />
<ExtensionSchema></ExtensionSchema>
</ProjectExtension>"""Creando un archivo que hace referencia a un proyecto.
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>
"""Describiendo la estructura BCF (categorías).
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>"""Definiendo la versión 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']
# Authenticate and get access token
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()Definición de la parte principal del script que solicita al usuario las preguntas de entrada, obtiene el token de acceso y autentica con la cuenta de Cintoo según Cintoo API Authentication, posición de cámara predeterminada que se cambia una vez importada en Cintoo, creando archivos de salida BCFzip y ejecutando la función principal.
CreateAnnotation.py Script Explicado
A continuación, encuentre el script con comentarios en cada parte.
Advertencia: si es necesario realizar ajustes en el código, es posible, pero Cintoo no asume ninguna responsabilidad sobre la funcionalidad de este código una vez modificado.
import login
import requests
from cintooUtils import parse_cintoo_workzone_context_url
TIMEOUT = 10Importando todas las bibliotecas y los datos necesarios de scripts complementarios para apoyar el script principal. Estableciendo un parámetro de tiempo de espera.
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]Extrayendo la ID de la zona de trabajo de la entrada del usuario.
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()Analizando la información de ubicación de Cintoo desde la entrada del usuario, adjuntando cada anotación a un elemento del modelo correspondiente.
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']
# Authenticate and get access token
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)Función principal con proceso de autenticación, configurando la posición de la cámara y creando anotaciones en el proyecto Cintoo.
¿Le fue útil este artículo?
¡Qué bueno!
Gracias por sus comentarios
¡Sentimos mucho no haber sido de ayuda!
Gracias por sus comentarios
Comentarios enviados
Agradecemos su iniciativa, e intentaremos corregir el artículo