Read-only mirror of https://github.com/swisstopo/swissgeol-boreholes-photo-processor — Bundesamt für Landestopografie. Issues & pull requests at the source.
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
Melanie Buechler 2bd302c0b0
Merge pull request #66 from swisstopo/feat/issue-50/cropping-for-cuttings
feat(cuttings): implement cropping and segmentation for cuttings images (#50)
2026-08-18 17:40:00 +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
src refactor(cuttings): move black circle segmentation constants to config 2026-08-18 17:36:53 +02:00
tests refactor(cuttings): remove pebble and tray segmentation methods 2026-08-17 13:51:45 +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 refactor(cuttings): remove pebble and tray segmentation methods 2026-08-17 13:51:45 +02:00
config.yaml Merge branch 'develop' into feat/issue-50/cropping-for-cuttings 2026-08-17 13:12:05 +02:00
pyproject.toml feat(run): use click for the CLI with cores/cuttings subcommands 2026-08-11 17:38:05 +02:00
README.md fix: close cutting image handle in cache path and fix review-flagged docs 2026-08-14 15:15:37 +02:00
uv.lock feat(run): use click for the CLI with cores/cuttings subcommands 2026-08-11 17:38:05 +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.

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.jpgdepth 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.

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 up to num_cores_per_image cores side by side, 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>
  • same flags as for the cores

With MLflow tracking

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