Skip to content

Repository files navigation

Seblimo — Monocular Depth Estimation

CIL 2026 depth estimation project.
Training infrastructure: Lightning Fabric + Hydra + WandB.

RGB | SAMv2 segments | MAD plane-fitted depth (3D)

Left to right: original RGB · SAMv2 segmentation overlay · raw DINOv2 depth prediction rendered as a spinning 3D point cloud.
Plane fitting enforces geometric consistency within each SAMv2 segment via robust least-squares (MAD outlier rejection), improving scale-invariant SILog on the test set.

Authors: Harvi Seitaj, Cyril Moser, David Blickenstorfer, Yu-chuan Liao — Department of Computer Science, ETH Zurich


Table of Contents


Setup

The cluster provides a shared conda environment with most dependencies pre-installed. Activate it first, then install this project and any missing packages on top:

# 1. Activate the shared conda env
conda activate monocular-depth-estimation

# 2. Clone and enter the repo
git clone https://github.com/CyrilFMoser/Seblimo
cd Seblimo

# 3. Install the project in editable mode.
#    pip skips packages already satisfied by the conda env and only
#    pulls in what is genuinely missing (e.g. hydra-core).
pip install --user -e .

If you hit version conflicts, install only the project itself and then add the missing packages manually:

pip install --user --no-deps -e .
pip install --user hydra-core   # add others as needed

The data lives at /cluster/courses/cil/monocular-depth-estimation/ and is already mounted on the cluster — you don't need to download anything.


Project structure

Seblimo/
├── configs/               # All experiment configuration (Hydra)
│   ├── config.yaml        # Root config — edit defaults here
│   ├── model/             # One .yaml per model architecture
│   │   ├── dinov2_dpt.yaml    # DINOv2-ViT-S + DPT decoder (default)
│   │   └── unet.yaml          # UNet baseline
│   ├── data/              # Dataset paths, batch size, split settings
│   ├── training/          # Epochs, validation, logging, hardware
│   ├── optimizer/         # adam.yaml, adamw.yaml
│   ├── scheduler/         # cosine.yaml, none.yaml
│   └── loss/              # silog.yaml, combined.yaml
├── src/seblimo/
│   ├── data/dataset.py         # DepthDataset, DepthTestDataset, make_splits()
│   ├── data/mask_generator.py  # SAMv2 automatic mask generation + caching
│   ├── models/
│   │   ├── base.py             # DepthModel ABC — all models must subclass this
│   │   ├── dinov2_dpt.py       # DINOv2-ViT-S encoder + DPT decoder (default model)
│   │   ├── unet.py             # UNet with batch norm baseline
│   │   └── dummy.py            # Trivial model for smoke-testing
│   ├── losses/
│   │   ├── silog.py       # Scale-invariant log RMSE loss
│   │   └── combined.py    # WeightedCombinedLoss
│   ├── metrics/depth.py   # SILogRMSEMeter for evaluation
│   ├── training/trainer.py # Fabric-based Trainer class
│   └── utils/
│       ├── visualization.py    # depth → colourmap, composite preview panels
│       └── postprocess.py      # segment-aware plane fitting (SAMv2 masks)
├── train.py               # Entry point: python train.py [overrides]
├── eval.py                # Entry point: python eval.py checkpoint=...
└── predict.py             # Entry point: python predict.py checkpoint=...

Running training

# Basic run with all defaults (dummy model, SILog loss, AdamW, cosine LR)
python train.py

# Quick smoke-test on CPU to check everything connects
python train.py training.max_epochs=2 data.batch_size=2 training.accelerator=cpu

# Resume from a checkpoint
python train.py training.resume_from=outputs/2025-01-01/12-00-00/checkpoints/last.pt

Hydra creates a timestamped output directory for each run:

outputs/
└── 2025-01-01/
    └── 12-00-00/
        ├── .hydra/config.yaml   ← full config snapshot
        └── checkpoints/
            ├── config.yaml      ← copy of the config (self-contained)
            ├── epoch_0001.pt
            ├── best.pt          ← best validation SILog RMSE
            └── last.pt

Configuring experiments

Every value in configs/ can be overridden from the command line using Hydra's dot-notation syntax. You can also swap entire sub-configs by name.

# Use a different optimiser
python train.py optimizer=adam

# Change learning rate
python train.py optimizer.lr=3e-4

# Train for more epochs with a larger batch
python train.py training.max_epochs=200 data.batch_size=16

# Use 20% of training data as validation
python train.py data.val_fraction=0.2

# Use combined loss (SILog + gradient matching) instead of SILog only
python train.py loss=combined

# Disable validation entirely
python train.py data.val_fraction=0.0

# Mixed precision on GPU
python train.py training.precision=bf16-mixed

# Disable LR scheduling
python train.py scheduler=none

SAMv2 segmentation masks

The pipeline can optionally generate and load per-image segment label maps using SAMv2 (Segment Anything Model v2 from Meta). These are stored alongside the RGB/depth data and made available as a mask key in every batch, intended for future mask-aware losses such as depth-consistency regularization within segments.

What is the model?

facebook/sam2.1-hiera-large (default) — SAMv2.1 with a Hiera-Large hierarchical vision encoder. ~224 M parameters, ~2.4 GB download from HuggingFace (cached automatically on first use in ~/.cache/huggingface/).

Lighter alternatives if VRAM is tight (set data.masks.model=...):

Model ID Size VRAM (approx.)
facebook/sam2.1-hiera-large 2.4 GB ~8 GB
facebook/sam2.1-hiera-base-plus 800 MB ~4 GB
facebook/sam2.1-hiera-small 185 MB ~3 GB
facebook/sam2.1-hiera-tiny 155 MB ~2.5 GB

Hardware requirements

Mask generation runs the model over a grid of 32×32 point prompts per image (1024 forward passes). A CUDA GPU with at least 8 GB VRAM is required. CPU inference is not practical — it takes several minutes per image.

Mask generation only runs once per dataset; after that, every epoch just reads the cached PNGs from disk. Prefer a 2080 Ti over a 1080 Ti — the older card takes roughly twice as long (~25 h for the full set).

Cache layout

masks/
  train/   ← masks for the training set  (used by train.py)
  test/    ← masks for the test set      (used by predict.py)

Both directories are tracked with Git LFS (see .gitattributes).

Installation

The sam2 package is an optional dependency (not installed by default):

pip install --user -e ".[masks]"
# or simply:
pip install --user sam2

Getting pre-generated masks via Git LFS

If the masks have already been generated and pushed, teammates can pull them without ever touching a GPU:

# First-time setup (once per machine)
git lfs install

# Pull all mask files
git lfs pull

Generating masks yourself (SLURM)

The script writes directly to masks/train/ and masks/test/ regardless of where it is submitted from. The paths are anchored to the script's own location in the repo.

# Quick smoke-test (20 images per split, ~1 min)
# Leave N_IMAGES=20 in the script, then:
sbatch scripts/generate_masks.sh

# Full generation (all images, ~12–25 h depending on GPU)
# Set N_IMAGES=0 in the script, then:
sbatch scripts/generate_masks.sh

After generation, push to share with teammates:

git add masks/train masks/test
git commit -m "Add SAMv2 masks"
git push   # LFS pointers are pushed automatically

Enabling masks during training / prediction

# Training — masks are read from masks/train/ automatically
python train.py data.masks.enabled=true

# Prediction — masks are read from masks/test/ automatically
python predict.py checkpoint=.../best.pt data.masks.enabled=true

# Use a lighter model to save VRAM
python train.py data.masks.enabled=true data.masks.model=facebook/sam2.1-hiera-base-plus

On the first run (if masks are missing), training/prediction will pause to generate them before starting. Subsequent runs skip this entirely.

What is cached?

Each image produces one 16-bit grayscale PNG — a uint16 array of shape (H, W) where each pixel value is a segment ID (1-indexed). Value 0 means no segment (shouldn't occur in practice). Smaller foreground objects overwrite larger background regions, so distinct objects always get distinct IDs. uint16 supports up to 65535 IDs, well above SAMv2's typical output of 50–300 masks per image.

PNG's delta filter + zlib compression reduces each mask from ~1.2 MB (raw int32) to roughly 50–200 KB, so the full training set fits comfortably in a HOME directory.

Mask format in the batch

When masks are enabled, every batch contains an additional key:

batch["seg_mask"]  # Tensor, shape (B, 1, H, W), dtype int64

Shape mirrors batch["depth"]. Existing losses are unaffected — they only read rgb and depth from the batch.


Adding a new model

  1. Create src/seblimo/models/my_model.py:
from seblimo.models.base import DepthModel
import torch.nn as nn
from torch import Tensor

class MyModel(DepthModel):
    def __init__(self, some_param: int = 64):
        super().__init__()
        # build your network here
        ...

    def forward(self, rgb: Tensor) -> Tensor:
        # rgb: (B, 3, H, W)  — ImageNet-normalised
        # return: (B, 1, H, W)  — predicted depth in metres (positive)
        ...

The only contract is: input (B, 3, H, W), output (B, 1, H, W). Outputs should be positive (depth in metres).

  1. Create configs/model/my_model.yaml:
_target_: seblimo.models.my_model.MyModel
some_param: 128
  1. Use it:
python train.py model=my_model

That's it — no changes to the training code needed.


Adding a new loss

  1. Create src/seblimo/losses/my_loss.py with a nn.Module that takes (pred, gt) and returns a scalar tensor.

  2. Create configs/loss/my_loss.yaml:

_target_: seblimo.losses.my_loss.MyLoss
some_param: 1.0
  1. Use it: python train.py loss=my_loss

To combine multiple losses, edit or copy configs/loss/combined.yaml and list your losses with weights.


Generating predictions

After training, run inference on the test set:

python predict.py checkpoint=outputs/2025-01-01/12-00-00/checkpoints/best.pt

This writes a ready-to-submit predictions/submission.csv directly — depths are encoded as float16 → zlib-compressed → base64, matching the competition format. No separate submission script needed.

Optional post-processing flags (all off by default):

# TTA + MAD plane fit (best test score)
python predict.py checkpoint=.../best.pt \
  +predict.tta=true \
  +predict.mad_plane_fit=true \
  +predict.mad_k=2.0

# TTA + soft MAD plane fit (blended variant)
python predict.py checkpoint=.../best.pt \
  +predict.tta=true \
  +predict.soft_mad_plane_fit=true \
  +predict.mad_k=2.0 \
  +predict.alpha=0.5

# Override output directory
python predict.py checkpoint=.../best.pt output_dir=my_predictions/

Evaluating post-processing

Benchmarks three modes against the validation split — all start from TTA predictions:

Mode Description
tta Horizontal-flip TTA only, no post-processing
tta_mad TTA + MAD-robust plane fit per segment
tta_soft_mad TTA + blended MAD plane fit (alpha * plane + (1-alpha) * pred)
python eval.py checkpoint=outputs/.../best.pt

# Tune MAD threshold, blend factor, or minimum segment size
python eval.py checkpoint=.../best.pt +eval.mad_k=2.0 +eval.alpha=0.5 +eval.min_pixels=300

# Run offline (no WandB account needed)
python eval.py checkpoint=.../best.pt wandb.mode=offline

Results are logged to WandB under eval/ keys alongside a depth preview gallery showing raw vs post-processed predictions and SAMv2 segment masks.


Smoke test (no GPU needed)

Before spending GPU hours, verify your installation is fully working on the login node:

python scripts/smoke_test.py

This creates a tiny synthetic dataset in /tmp, runs 2 training epochs on CPU, generates predictions, and checks that checkpoints and logs were written. Takes about 30 seconds. Expected output:

ALL CHECKS PASSED — your installation is working correctly.

WandB

Weights & Biases is the experiment tracker — it records loss curves, metrics, and depth preview images for every training run so the whole team can compare experiments in a shared dashboard.

Key concepts

Term What it means
account Your personal WandB account (free at wandb.ai). Each person needs one.
entity Either your personal username, or a team name. If you create a team on WandB and set entity to the team name, all runs from all teammates end up in one shared workspace.
project A named bucket for related runs — ours is Monocular Depth Estimation. All runs in the same project appear together in the dashboard.
run One execution of python train.py. Gets a unique name, stores all metrics and config.

First-time setup (do this once per person)

# 1. Create a free account at https://wandb.ai, then:
wandb login
# Paste your API key when prompted. It's saved to ~/.netrc — you won't need
# to do this again on this machine.

The team and project are already configured in configs/config.yaml:

wandb:
  entity: seblimo
  project: Monocular Depth Estimation

Runs will appear at: wandb.ai/seblimo/Monocular Depth Estimation

What gets logged

Key When What
train/loss every step training loss
train/lr every step current learning rate
val/loss every val epoch validation loss
val/silog_rmse every val epoch scale-invariant RMSE (competition metric)
val/depth_gallery every val epoch composite panel: RGB | pred | GT | error

N images per epoch is controlled by training.log_images_per_val (default 8).

Running without WandB (offline / no account)

# Offline: logs are saved locally, you can sync later
python train.py wandb.mode=offline

# Sync offline runs to WandB later
wandb sync outputs/<date>/<time>/wandb/

# Disable WandB entirely (nothing is logged or saved)
python train.py wandb.mode=disabled

Checkpoints

Each run saves checkpoints to its own timestamped folder:

outputs/
└── 2026-01-01/
    └── 12-00-00/
        ├── .hydra/config.yaml     ← full config (written by Hydra)
        └── checkpoints/
            ├── config.yaml        ← copy of the above (for safe-keeping)
            ├── epoch_0001.pt
            ├── epoch_0002.pt
            ├── best.pt            ← lowest val/silog_rmse so far
            └── last.pt            ← always the most recent epoch

To resume training from a checkpoint:

python train.py training.resume_from=outputs/2026-01-01/12-00-00/checkpoints/last.pt

The config.yaml copy inside the checkpoint folder means you can fully reproduce any run even if the outputs/ tree is cleaned up:

python train.py --config-path outputs/2026-01-01/12-00-00/checkpoints --config-name config

Evaluation metric

The competition uses scale-invariant RMSE (SILog RMSE):

d_i = log(pred_i) - log(gt_i)
SILog-RMSE = sqrt(mean(d²) - mean(d)²) × 100

Lower is better. Both the training loss and the val/silog_rmse metric use λ=1.0 (fully scale-invariant), matching the Kaggle evaluation exactly.

AI Usage Declaration

Tool used: Claude Sonnet 4.5 / 4.6 (Claude Code CLI)

Files affected: train.py, eval.py, predict.py, src/seblimo/training/trainer.py, src/seblimo/data/dataset.py, src/seblimo/losses/, src/seblimo/metrics/, configs/, pyproject.toml, src/seblimo/models/dinov2_dpt.py, src/seblimo/models/DINOV2_DPT.md, scripts/generate_masks.sh, README.md

Purpose: The research direction — architecture selection (DINOv2+DPT), the SAMv2 mask integration strategy, loss design, and all experiment decisions — was entirely our own. The overall project infrastructure (Lightning Fabric training loop, Hydra config system, WandB integration, entry-point scripts, some documentation) was scaffolded with Claude Code. Having built equivalent training infrastructure from scratch in prior courses, we used AI to handle this standard boilerplate so we could focus our effort on the research contributions specific to this project. The DINOv2+DPT model (dinov2_dpt.py) was implemented with Claude's help after we read the DPT paper; Claude also generated the accompanying DINOV2_DPT.md summary to help us grasp it better. generate_masks.sh (SLURM job script for SAMv2 mask generation) and parts of this README were generated directly. The training infrastructure and losses are fully understood by the team. The DPT decoder implementation is understood at the architectural level: patch tokens are reassembled into spatial feature maps at multiple scales, fused via a feature pyramid with skip connections, and upsampled to the full resolution depth map. The low-level tensor operations were AI-assisted, but the architectural choices and their rationale are fully understood.

About

Monocular Depth Estimation for CIL 2026

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages