Read-only mirror of https://github.com/swisstopo/swissgeol-boreholes-photo-processor — Bundesamt für Landestopografie. Issues & pull requests at the source.
  • Python 99.5%
  • Shell 0.5%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
Stijn Vermeeren 60bd37b505
Merge pull request #105 from swisstopo/output-filename-with-depths
Output filename with depths + reduce number of cuttings per composite
2026-09-17 10:23:40 +02:00
.github/workflows fix ocr version + upscale to have better res 2026-07-20 11:07:00 +02:00
.vscode feat(run): use click for the CLI with cores/cuttings subcommands 2026-08-11 17:38:05 +02:00
examples test(segment): add end-to-end example test with real core/tray/ruler image 2026-07-15 16:47:01 +02:00
scripts fix(cuttings): increase output resolution and restore web downscale factor 2026-09-10 17:45:48 +02:00
src fix types in docstrings 2026-09-16 17:37:22 +02:00
tests Include depths in output filename 2026-09-16 16:48:11 +02:00
.env.template fix(config): correct env template variable names and quoting 2026-06-08 15:10:06 +02:00
.gitignore fix(run): log run output and evaluation summary to mlflow instead of disk 2026-07-29 16:19:05 +02:00
.pre-commit-config.yaml ci: install project deps before running pre-commit in CI 2026-06-09 17:09:04 +02:00
.python-version fix: address PR review findings 2026-06-04 14:51:11 +02:00
ARCHITECTURE.md feat: allow manually cropped override images for cores and cuttings 2026-09-10 16:45:36 +02:00
config.yaml Reduce default rows for cuttings composites from 15 to 10 2026-09-16 16:48:31 +02:00
pyproject.toml build: bump pillow, mlflow and pytest to patch known vulnerabilities 2026-09-15 10:10:59 +02:00
README.md feat: allow manually cropped override images for cores and cuttings 2026-09-10 16:45:36 +02:00
uv.lock build: bump pillow, mlflow and pytest to patch known vulnerabilities 2026-09-15 10:10:59 +02:00

Boreholes Photo Processor

Boreholes Photo Processor is a pipeline that generates composite images from photos of borehole cores and cuttings. This project was initiated by the Swiss Federal Office of Topography swisstopo, and is developed with support from Visium.

Installation

In a first step you need to clone the repository:

#using https
git clone https://github.com/swisstopo/swissgeol-boreholes-photo-processor.git

OCR

The pipeline uses OCR to read the printed tick numbers on the depth ruler in each photo.

# Install Tesseract OCR engine (system-level)
# On Ubuntu/Debian:
sudo apt-get install -y tesseract-ocr=5.3.4*

# On macOS:
brew install tesseract

# On Windows: download installer from
# https://github.com/UB-Mannheim/tesseract/wiki

Python

We use uv to manage package dependencies. Install uv first if you haven't already:

curl -LsSf https://astral.sh/uv/install.sh | sh

The below commands will install the package for you:

uv sync --all-extras

Then activate your environment:

source .venv/bin/activate

Adding packages can be done by editing the pyproject.toml of the project and adding the required package, then running uv sync to update the lock file.

Running Tests

uv run pytest tests/

To also generate a coverage report:

uv run pytest --cov=src tests/

Pipeline

Download the borehole profiles, optional

You can download the data using the AWS CLI. First, you need to make sure that the CLI is installed:

brew install awscli

Configure your AWS credentials using aws configure, which will prompt you for:

AWS Access Key ID [None]: <your key id>
AWS Secret Access Key [None]: <your key>
Default region name [None]: eu-central-1
Default output format [None]: json

Now you can download the cores from the bucket:

aws s3 sync s3://stijnvermeeren-corephotos-cuttings/cores ./data/cores

And the cuttings from:

aws s3 sync s3://stijnvermeeren-corephotos-cuttings/cuttings ./data/cuttings

To sync only a single borehole (useful for local testing), specify the full prefix:

aws s3 sync s3://stijnvermeeren-corephotos-cuttings/cores/GBC/GBC-CB50 ./data/cores/GBC/GBC-CB50

Expected Data Format (Cores)

The pipeline processes borehole core photos in TIF format (.tif/.TIF, case-insensitive). Files with other extensions are ignored.

Filename convention

Each image filename must contain a depth interval of the form _XXXX.XX-YYYY.YY, where XXXX.XX is the start depth and YYYY.YY is the end depth in metres. Files without this pattern are rejected at runtime.

The borehole identifier is derived from the filename prefix — everything before the depth range:

<borehole-id>_0015.00-0016.00_<optional-suffix>.tif
└───────────┘ └─────────────┘
 borehole_id    depth range

Folder structure

For a single borehole, the input directory should contain TIF files directly:

<any-folder>/
└── <borehole-folder>/          ← pass this as --input
    ├── <borehole-id>_0000.00-0001.00_*.tif
    ├── <borehole-id>_0001.00-0002.00_*.tif
    └── ...

For batch processing across multiple boreholes, the input directory should contain one subdirectory per borehole:

<any-folder>/                   ← pass this as --input
├── <borehole-folder-1>/
│   ├── <borehole-id-1>_0000.00-0001.00_*.tif
│   └── ...
└── <borehole-folder-2>/
    ├── <borehole-id-2>_0000.00-0001.00_*.tif
    └── ...

The pipeline detects the mode automatically: if the input directory contains subdirectories it runs in batch mode, otherwise it processes the directory as a single borehole. The input folder structure is mirrored in the output directory.

Manually cropped overrides

Prefix a filename with override_ (e.g. override_GBC-CB50_0015.00-0016.00_vd_p.tif) to supply an already-cropped image for that depth range instead of letting the pipeline segment it automatically; it wins over any other file at the same range. It must still be a .tif, since it's scaled like any other core with no detected ruler (the batch's fallback scale).

Expected Data Format (Cuttings)

The pipeline processes cuttings photos in JPG, JPEG, BMP, TIF, and TIFF format (case-insensitive). Files with other extensions are ignored.

Filename convention

New data should follow this convention:

<borehole-id>_<depth>m_<sequence>.<ext>

e.g. VINZEL-1_1234.50m_01.jpg — depth is the point depth in metres (decimal allowed), and sequence should be incremented for every extra photo taken at the same depth.

A number of legacy, borehole-specific filename conventions are also recognized for existing data (e.g. an id followed by a depth range, an id and depth separated by a space, or a camera filename with the depth appended as a trailing token) — see ImageMetadataCuttings.from_path in src/models.py for the full list. Filenames with no recoverable depth are skipped with a warning, not fatal to the whole batch. Files named 00-Vials-* are always excluded outright, since they're sample-vial photos rather than depth photos.

Only the first photo (by filename) found at a given depth is kept; any extra photos at the same depth are dropped and the count is logged (and, with --mlflow, recorded as a metric). Naming every photo at a shared depth with the convention above (unique, incrementing sequence) avoids this.

Manually cropped overrides

Prefix a filename with override_ (e.g. override_1234.50m_01.jpg) to supply an already-cropped image for that depth instead of letting the pipeline crop it automatically; it wins over any other file at the same depth.

Folder structure

Same as for cores above: a single borehole folder of cuttings photos, or one subfolder per borehole for batch processing.

Output

Each output sheet places a variable number of cores side by side, respecting a minimum separation between them, top-aligned on a black background, with a ruler (major, intermediate, and minor ticks) drawn along both the left and right edges, and the borehole ID printed in the top-left corner. Depth values are shown separately as depth_start/depth_end labels (in metres) above and below each core strip.

The depth ruler is calibrated automatically: Tesseract OCR reads the printed tick numbers on each photo's ruler to derive a pixels-per-unit scale (px_per_unit), so tick counts and labels reflect actual detected depth rather than an arbitrary scale. Each core is then resized independently using its own detected px_per_unit (falling back to the batch median if no ruler was detected for that image), and clamped if it would exceed max_core_width/max_core_height.

A core's depth labels are suffixed with * when its scale could not be trusted. Either no ruler was detected for that image (falling back to the batch median scale) or the crop had to be clamped.

Configuration

Segmentation, stitching and evaluation parameters are set via a YAML config file, not CLI flags. A default config.yaml is provided at the repository root; any omitted key falls back to its default (see src/config.py).

To speed up segmentation, images are downscaled before each detection step (tray, core-trim, and ruler OCR each have their own downscale_factor under segmentation.* in config.yaml), and resulting bounding boxes are scaled back up to the original resolution for stitching.

The pipeline groups images by their on-disk shape (height, width, channels) and, for each group with at least 10 images, derives a shared bounding box by comparing images from the (assumed static) camera position and locating the region that changes between shots (the core/tray). Groups with fewer than 10 images fall back to per-image thresholding instead. Per-image preprocessing for these shape groups (tray detection and ruler OCR) runs in parallel across segmentation.n_workers worker processes, controlling which images are sampled from each group.

To use a different config file, pass --config <path> (see below).

CLI Usage

The commands below assume the package is installed (see Installation). Without installing it, run the same commands via uv run python -m src.run cores ... / uv run python -m src.run cuttings ... instead.

Run the pipeline for cores:

Standard

uv run boreholes-photo-processor --input <input-dir> --output <output-dir>
  • --input: Path to the directory containing raw borehole photos (.tif only), or nested folders containing them
  • --output: Path to the directory where processed images will be written
  • --config: Path to the YAML config file for segmentation, stitching, and evaluation parameters (default: config.yaml)

With In-Memory processing (faster)

For faster runtime, use the --cache flag to eagerly decode and cache each core/cutting crop during the (already-parallel) segmentation step, instead of deferring that work to stitching. This requires enough available memory to hold every processed crop at once. On a standard machine (32 GB), we recommend in-memory processing only for folders of 16 GB or less to avoid out-of-memory errors.

uv run boreholes-photo-processor --input <input-dir> --output <output-dir> --cache

With MLflow tracking

uv run boreholes-photo-processor --input <input-dir> --output <output-dir> --mlflow --debug
  • --mlflow: Enable MLflow artifact logging. By default logs to ./mlruns; set MLFLOW_TRACKING_URI for a remote server.
  • --debug: Additionally log per-image and per-shape-group debug images (core/tray/ruler overlays) under a debug subfolder of each run's artifacts. Only has an effect when --mlflow is also set.

To view logged artifacts, start the MLflow UI:

uv run mlflow ui

Then open http://localhost:5000 in your browser.

Run the pipeline for cuttings

Without MLflow tracking

uv run boreholes-photo-processor-cuttings --input <input-dir> --output <output-dir> --cut-type pebble
  • same flags as for the cores
  • --cut-type: Cuttings segmentation method to use — full (default, no cropping, the entire image is used), black_circle (cuttings inside a black circular tray), pebble (cuttings next to a printed reference paper sheet) or tray (cuttings inside a metal tray). Must match the physical layout used at that borehole; there's no auto-detection.
  • --dedup-keep: first or last — which image to keep (by filename) when multiple cuttings images share the same depth. Overrides segmentation.cuttings.dedup_keep from --config for this run; defaults to that config value when omitted.

With MLflow tracking

uv run boreholes-photo-processor-cuttings --input <input-dir> --output <output-dir> --mlflow --debug
  • same flags as for the cores

Batch scripts

To (re)generate output for every borehole at once — e.g. at the end of the project:

Cores — every borehole uses the same command, so a loop is enough, no script needed:

for borehole_dir in <input-root>/*/; do
  borehole=$(basename "$borehole_dir")
  uv run boreholes-photo-processor --input "$borehole_dir" --output "<output-root>/$borehole"
done

Cuttings — each borehole needs its own --cut-type, so use scripts/run_all_cuttings.sh:

scripts/run_all_cuttings.sh <input-root> <output-root> [extra flags...]
  • <input-root>/<borehole> must contain that borehole's raw cuttings photos
  • output is written to <output-root>/<borehole>/
  • any other flags (--mlflow, --debug, --cache) are forwarded as-is to every run

The script is a plain list of commands, one per borehole, not a generic tool — each borehole's --cut-type (and, where needed, --dedup-keep) is hardcoded to match its physical setup / data. Add a line for each new borehole as it comes in.