Language: 🇬🇧 English · 🇫🇷 Français
This repository is a minimal, Docker-first example showing how to use roofer with IGNF datasets (BD TOPO® and LiDAR HD) to produce 3D LOD2.2 buildings. This is a starting point for experimenting.
roofer is the 3DBAG reconstruction tool that turns building footprints and point clouds into 3D building models. The wider 3dbag-pipeline project shows how these tools are used in larger production workflows. This repository focuses on a much smaller example: starting from a Lambert-93 bounding box, downloading the required IGNF data from its Géoplateforme, and preparing the inputs needed to run roofer and produce 3D buildings.
The workflow of this project is:
- Start from a bounding box in Lambert-93 (
EPSG:2154) - Download
BDTOPO_V3:batimentbuildings from the IGN WFS with pagination support - Compute the real extent of the downloaded buildings
- Define the LiDAR extraction bbox by adding a configurable buffer around that extent
- Query the
IGNF_NUAGES-DE-POINTS-LIDAR-HD:dalletile index from the IGN WFS with pagination support - Build a PDAL pipeline that streams exactly the LiDAR covering the extraction bbox, cropped from the intersecting COPC tiles
- Remap LiDAR HD classification
67 -> 6, becauserooferfollows the ASPRS LAS standard and only treats class6as building, whereas IGN LiDAR HD also places building points in its non-standard class67(Divers - bâtis, i.e. miscellaneous built structures). Without this remap those points would be invisible torooferand lost for roof reconstruction - Clean and complete the building ground and roof elevation attributes, which
rooferfalls back on when a footprint has too few ground points (for the floor elevation) or roof points (for the roof height) - Run
rooferon the resulting LAZ file and the prepared building GeoPackage - Convert each native CityJSONSeq result to a matching CityJSON file
The goal is to keep the code and user setup as simple as possible. The host only needs Docker.
- Linux or macOS host (the workflow itself always runs inside a Linux container)
- Docker only
- Input bbox must be in
EPSG:2154for now - One bbox at a time
- No native local installation path
- Docker installed and available in
PATH(Docker Engine on Linux, Docker Desktop on macOS) - A POSIX
bashto runrun.sh(macOS ships bash 3.2, which is sufficient) - Network access to:
https://data.geopf.fr- the COPC storage URLs returned by the LiDAR HD tiles WFS
- Docker Hub to pull
3dgi/3dbag-pipeline-tools:2026.07.29
Example running the workflow with a Lambert-93 bounding box centered on Les Espaces d'Abraxas in Noisy-le-Grand:
./run.sh --bbox 666201 6859851 666701 6860351With a custom buffer (default is 10 meters) and output root directory (default is ./output):
./run.sh --bbox 666201 6859851 666701 6860351 --buffer 15 --out ./example-outputThe generated CityJSON result files in output/run-*/roofer_output/ or example-output/run-*/roofer_output/ can be opened directly in ninja.cityjson.org.
Open or drag and drop the generated CityJSON output directly in ninja.cityjson.org.
Inspect the reconstructed buildings interactively in the viewer.
Corporate proxy support
For most users, there is nothing to configure.
If you run this workflow from the IGNF network or other networks behind corporate proxy, export your proxy variables in the shell before calling run.sh if they are not already defined. The run.sh script forwards them to Docker.
Example:
export HTTPS_PROXY=http://proxy.example.com:8080
export HTTP_PROXY=http://proxy.example.com:8080
export NO_PROXY=localhost,127.0.0.1
./run.sh --bbox 666201 6859851 666701 6860351The workflow writes all intermediate artifacts in a dedicated run directory under the output root (--out) so the process stays easy to inspect and debug. Each run directory is named run-YYYYMMDD-HHMMSS. Existing run directories with previous artifacts are refused by default; pass --clean to clear marked run directories before running.
Expected files inside each run directory:
buildings.gpkg: building footprints downloaded fromBDTOPO_V3:batimentinEPSG:2154and normalized toMULTIPOLYGONbuildings_extent.json: the real building extent computed from the downloaded building layerlidar_extraction_bbox.json: the buffered building extent used to query and crop the LiDAR datalidar_tile_index.gpkg: the local LiDAR tile index containing the footprints, identifiers, names, and COPC URLs returned byIGNF_NUAGES-DE-POINTS-LIDAR-HD:dallepdal_pipeline.json: the generated PDAL pipelinelidar_subset.laz: the cropped LiDAR subset written by PDAL for the LiDAR extraction bbox, with class67remapped to6buildings_prepared.gpkg: building footprints after attribute cleaning and completion, used as the polygon source forrooferroofer_output/: the native CityJSONSeq files produced byrooferand their matching converted CityJSON files.roofer-run-output: marker used byrun.shto identify run directories it is allowed to clean with--clean
Host-side entrypoint that:
- validates the CLI arguments
- creates the output root and per-run output directory on the host
- marks run directories with
.roofer-run-output - refuses non-empty unmarked run directories, even when
--cleanis passed - refuses marked run directories with existing run artifacts unless
--cleanis passed - passes proxy-related environment variables to Docker
- launches the container workflow
CLI:
./run.sh --bbox xmin ymin xmax ymax [--buffer meters] [--out path] [--jobs n] [--clean] [--verbose]
./run.sh --clean [--out path]Arguments:
--bbox xmin ymin xmax ymaxinput extent inEPSG:2154, required when running the workflow--bufferoptional, between0and500meters, defaults to10meters--outoptional, defaults to./output; this is the output root that contains run directories--jobsoptional, forwarded toroofer -j, defaults tonproc. For values greater than1, roofer reserves one job for the pipeline and uses the others for the reconstructor pool--cleanoptional, clears marked run directories under--out. Without--bbox, it cleans and exits, while with--bboxit cleans before running--verboseoptional, enables detailed output from the pipeline generator, PDAL, building preparation, roofer, andcjio
Container-side workflow that:
- checks that
ogr2ogr,ogrinfo,pdal,roofer,cjio,python3,awk, andsedare present in the runtime image - downloads building footprints from
BDTOPO_V3:batiment - computes the real building extent
- prepares the LiDAR extraction bbox by buffering that extent
- downloads the LiDAR tile index from
IGNF_NUAGES-DE-POINTS-LIDAR-HD:dalle - reads COPC URLs from the tile
urlattribute - generates
pdal_pipeline.jsonto extract the required LiDAR subset - extracts the LiDAR subset with
pdal pipeline - prepares the building footprints with
set_building_attributes.sh(requiressqlite3) - runs
roofer - converts each CityJSONSeq output to a matching CityJSON file with
cjio - prints a per-step and total timing summary when the workflow completes
- enables additional diagnostics for supported tools when
--verboseis passed
Small Python helper that:
- reads the local LiDAR tile index with
ogrinfo -json - reads COPC URLs from the schema-defined
urlproperty - generates a PDAL pipeline with one
readers.copcper tile
CLI:
python3 scripts/build_pdal_pipeline.py \
--tiles lidar_tile_index.gpkg \
--layer lidar_tiles \
--bbox xmin ymin xmax ymax \
--output-pipeline pdal_pipeline.json \
--laz-output lidar_subset.lazArguments:
--tiles: path to the local LiDAR tile index, typically the generatedlidar_tile_index.gpkg--layer: name of the LiDAR tile index layer to read inside--tiles(e.g.lidar_tiles)--bbox: LiDAR extraction bbox inEPSG:2154, used as the PDALboundson eachreaders.copc--output-pipeline: path of the generatedpdal_pipeline.json--laz-output: path of the cropped LAZ file written by the generated PDAL pipeline--verbose: print a compact summary of the generated pipeline
Post-processes a building GeoPackage to clean and complete the ground and roof elevation attributes that roofer falls back on when a footprint has too few ground points (for the floor elevation) or roof points (for the roof height).
The script:
- removes features with NULL geometries
- fills missing minimum ground elevation from maximum ground elevation
- fills missing maximum ground elevation from minimum ground elevation
- fills missing minimum roof elevation from maximum roof elevation
- fills missing maximum roof elevation from minimum roof elevation
- computes missing building height using:
maximum roof elevation - minimum ground elevation - reconstructs missing roof elevations using:
ground elevation + building height - reconstructs missing ground elevations using:
roof elevation - building height
CLI:
bash scripts/set_building_attributes.sh \
--input buildings.gpkg \
--output buildings_prepared.gpkg \
--layer buildings \
--ground-min-field altitude_minimale_sol \
--ground-max-field altitude_maximale_sol \
--roof-min-field altitude_minimale_toit \
--roof-max-field altitude_maximale_toit \
--height-field hauteur \
--verbose 1Arguments:
--input: input building GeoPackage (read-only)--output: output GeoPackage created by the script--layer: building layer name inside the GeoPackage (default:buildings)--ground-min-field: field name forminimal ground altitude(default:altitude_minimale_sol)--ground-max-field: field name formaximal ground altitude(default:altitude_maximale_sol)--roof-min-field: field name forminimal roof altitude(default:altitude_minimale_toit)--roof-max-field: field name formaximal roof altitude(default:altitude_maximale_toit)--height-field: field name forbuilding height(default:hauteur)--verbose: verbosity level:0: quiet mode1: main processing steps and summary2: detailed SQL diagnostics and per-step statistics
- The runtime image is
3dgi/3dbag-pipeline-tools:2026.07.29. - The tool binaries in that image live under
/opt/3dbag-pipeline/tools/bin, so the workflow exports that path explicitly before running GDAL, PDAL, and roofer. - The building download uses the GDAL WFS driver through
ogr2ogr. - The implementation relies on GDAL paging support and does not implement any custom WFS paging code.
roofertreats the input polygons purely as 2D footprints (roofprints) and ignores anyZpresent in their geometry. All elevations are derived from the LiDAR point cloud, with thealtitude_*attributes used only as fallbacks (see step 8). The building download therefore flattens geometries to 2D (ogr2ogr -dim 2), which is lossless for this workflow since the polygonZwould be discarded byrooferanyway.- The LiDAR extraction keeps the streamed crop on each
readers.copcentry. It does not crop full tiles after download. - The only LiDAR-specific transformation in this example is the class remapping
67 -> 6, which aligns IGN's bâtis divers class with the ASPRS class6thatrooferexpects for buildings (see step 7). - The workflow retains the native
CityJSONSeqoutput fromrooferand writes a matchingCityJSONfile beside it. - Bounding box size directly drives runtime and reliability. A larger bbox means more buildings and more LiDAR tiles, all fetched through paged WFS requests: every extra page is another network round-trip that can time out or be cut short server-side, so very large areas are both slower and more likely to fail mid-download. The bbox is intentionally not capped in code, since the right size depends on your machine, network, and patience. For large areas, prefer splitting the work into several smaller runs rather than issuing a single very large request. The
--bufferis a secondary expansion applied automatically around the building extent, so it is capped at500meters to guard against accidental runaway downloads.
- IGN LiDAR HD product page: https://cartes.gouv.fr/rechercher-une-donnee/dataset/IGNF_NUAGES-DE-POINTS-LIDAR-HD
- IGN LiDAR HD content descriptor (classification nomenclature, incl. class
67): https://geoservices.ign.fr/sites/default/files/2024-09/DC_LiDAR_HD_1-0.pdf - IGN BDTOPO product page: https://cartes.gouv.fr/rechercher-une-donnee/dataset/IGNF_BD-TOPO
- IGN BDTOPO content descriptor: https://data.geopf.fr/annexes/ressources/documentation/DC_BDTOPO_3-5.pdf
- IGN WFS service: https://cartes.gouv.fr/aide/fr/guides-utilisateur/utiliser-les-services-de-la-geoplateforme/diffusion/wfs/
- Roofer getting started: https://innovation.3dbag.nl/roofer/getting_started.html
- Roofer CLI docs: https://innovation.3dbag.nl/roofer/cli_application.html
- Roofer input requirements: https://innovation.3dbag.nl/roofer/data_requirements.html
- PDAL
readers.copc: https://pdal.io/en/2.8.4/stages/readers.copc.html - PDAL
filters.assign: https://pdal.io/en/2.8.4/stages/filters.assign.html


