Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 37 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# DeepReefMap

DeepReefMap turns reef videos into 3D reconstructions and semantic maps (for example, coral classes overlaid on geometry). It is designed so you can swap segmentation and reconstruction backends while keeping the same command-line workflow.
[DeepReefMap](https://besjournals.onlinelibrary.wiley.com/doi/full/10.1111/2041-210X.14307) is a software for rapid 3D semantic mapping of coral reefs from handheld cameras.
Repository maintained by [Hugues Sibille](https://github.com/HuguesSib) (EPFL) and [Jonathan Sauder](https://josauder.github.io/) (MIT/EPFL).

## Quick overview

Expand Down Expand Up @@ -45,15 +46,15 @@ Important performance note:

### SC-SfMLearner path (simplest)

Use `--mapping scsfmlearner`. By default, the checkpoint is downloaded from Hugging Face (`EPFL-ECEO/deepreefmap-sfm-net/scsfmlearner.pt`). You can also provide a local checkpoint path:
Use `--mapping scsfmlearner`. By default, the checkpoint is downloaded from Hugging Face (`EPFL-ECEO/deepreefmap-sfm-net/scsfmlearner.pt`).

```bash
uv run deepreefmap reconstruct \
--videos GX010001.MP4 \
--mapping scsfmlearner \
--scsfmlearner-checkpoint-path /path/to/scsfmlearner.pt \
--camera-profile gopro_hero_10 \
--out out_local_ckpt
--tsdf \
--out out_scsfm
```

### LoGeR path (higher quality, more setup)
Expand All @@ -76,6 +77,18 @@ curl -L -C - "https://huggingface.co/Junyi42/LoGeR/resolve/main/LoGeR_star/lates
-o third_party/LoGeR/ckpts/LoGeR_star/latest.pt
```

And then you can run:

```bash
uv run deepreefmap reconstruct \
--videos GX010001.MP4 \
--mapping loger_star \
--camera-profile gopro_hero_10 \
--out out_loger \
```



### DINOv3 segmentation models (access + authentication)

The DINOv3-based segmentation models are higher quality than SegFormer models, but you need access/authentication on Hugging Face.
Expand All @@ -99,7 +112,7 @@ uv run huggingface-cli login

If your footage is from a GoPro Hero 10 in Linear mode with the GoPro casing setup used by this project, use the built-in profile:

- Camera profile: `gopro_hero_10` (file: `camera_profiles/gopro_hero_10.json`)
- Camera profile: `gopro_hero_10` (bundled JSON: `deepreefmap/resources/camera_profiles/gopro_hero_10.json`). You can also override or add profiles with `./camera_profiles/<name>.json` in the current working directory.

Example:

Expand Down Expand Up @@ -229,8 +242,27 @@ Viewer highlights:
- Toggle class visibility and switch color mode (RGB vs semantic colors).
- Use `Accumulate` to overlay filtered points up to current timeline index.


## Citation

If you use this repository or build on it, please cite:

```bibtex
@article{sauder2024scalable,
title={Scalable semantic 3D mapping of coral reefs with deep learning},
author={Sauder, Jonathan and Banc-Prandi, Guilhem and Meibom, Anders and Tuia, Devis},
journal={Methods in Ecology and Evolution},
volume={15},
number={5},
pages={916--934},
year={2024},
publisher={Wiley Online Library}
}
```

## License

DeepReefMap is licensed under the [Apache License 2.0](LICENSE).

Vendored or optional third-party components (notably `third_party/LoGeR` and downloaded checkpoints) carry their own terms; see `THIRD_PARTY_NOTICES.md` before redistribution.

54 changes: 0 additions & 54 deletions camera_profiles/gopro_hero_10.json

This file was deleted.

42 changes: 38 additions & 4 deletions deepreefmap/cli/main.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from pathlib import Path
from typing import Optional
import json
import logging

import typer

Expand Down Expand Up @@ -48,7 +49,10 @@ def reconstruct(
fps: int = typer.Option(10, help="Target processing framerate."),
segmentation: str = typer.Option("coralscapes-vit-b-dpt", help="Segmentation model name."),
mapping: str = typer.Option("scsfmlearner", help="3D mapping backend name."),
camera_profile: str = typer.Option(..., help="Camera profile name (in camera_profiles)."),
camera_profile: str = typer.Option(
...,
help="Camera profile name: bundled under deepreefmap or `./camera_profiles/<name>.json` in CWD.",
),
out: Path = typer.Option(Path("out"), help="Output directory."),
begin: Optional[float] = typer.Option(None, help="Start timestamp in the concatenated stream (seconds)."),
end: Optional[float] = typer.Option(None, help="End timestamp in the concatenated stream (seconds)."),
Expand All @@ -73,6 +77,13 @@ def reconstruct(
loger_model_path: Optional[Path] = typer.Option(None, help="LoGeR checkpoint path (defaults to vendored)."),
loger_window_size: int = typer.Option(32, help="LoGeR window size."),
loger_overlap_size: int = typer.Option(3, help="LoGeR overlap size."),
refine_intrinsics_from_mapper: bool = typer.Option(
False,
help=(
"Allow mapping backend to refine camera intrinsics and override camera profile K for "
"downstream 3D reconstruction."
),
),
scsfmlearner_checkpoint_path: Optional[Path] = typer.Option(
None,
help="Optional SC-SfMLearner checkpoint path (.pt containing disp_state_dict and pose_state_dict). Defaults to EPFL-ECEO/deepreefmap-sfm-net/scsfmlearner.pt on Hugging Face Hub.",
Expand Down Expand Up @@ -169,13 +180,14 @@ def reconstruct(
processing_width=processing_width,
processing_height=processing_height,
skip_segmentation=skip_segmentation,
refine_intrinsics_from_mapper=refine_intrinsics_from_mapper,
)


@app.command("calibrate")
def calibrate(
video: Path = typer.Argument(..., exists=True),
name: str = typer.Option(..., help="Profile name for camera_profiles/<name>.json"),
name: str = typer.Option(..., help="Profile name; writes `./camera_profiles/<name>.json`."),
n_frames: int = typer.Option(100),
fps: int = typer.Option(10),
begin: Optional[float] = typer.Option(None, help="Optional begin timestamp (seconds) for calibration window."),
Expand All @@ -194,7 +206,10 @@ def calibrate(

@app.command("verify-calibration")
def verify_calibration(
name: str = typer.Argument(..., help="Camera profile name in camera_profiles."),
name: str = typer.Argument(
...,
help="Camera profile name (bundled or `./camera_profiles/<name>.json` in CWD).",
),
) -> None:
report = verify_camera_profile(name)
typer.echo(json.dumps(report, indent=2))
Expand All @@ -203,8 +218,27 @@ def verify_calibration(
@app.command("render-video")
def render_video(
run_dir: Path = typer.Option(..., exists=True, help="Run output directory from reconstruct."),
transect_length_m: Optional[float] = typer.Option(
None,
"--transect-length-m",
help="Transect length in meters; enables ortho crop (matches viser). Falls back to manifest.",
),
crop_width_m: Optional[float] = typer.Option(
None,
"--crop-width-m",
help="Crop width in meters around the transect line. Falls back to manifest.",
),
) -> None:
render_offline_video_placeholder(run_dir)
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s %(levelname)s %(name)s: %(message)s",
datefmt="%H:%M:%S",
)
render_offline_video_placeholder(
run_dir,
transect_length_m=transect_length_m,
crop_width_m=crop_width_m,
)
typer.echo(f"Offline render completed in {run_dir}")


Expand Down
52 changes: 47 additions & 5 deletions deepreefmap/io/video.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,23 +21,65 @@ def iter_video_frames(
"""
global_idx = 0
cumulative_time = 0.0
next_sample_time = _first_sample_time(begin_s, target_fps)
interval_end = float("inf") if end_s is None else max(0.0, float(end_s))

for path in video_paths:
meta = iio.immeta(path)
src_fps = float(meta.get("fps", target_fps))
stride = max(1, int(round(src_fps / max(1, target_fps))))
src_fps = src_fps if src_fps > 0 else float(max(1, target_fps))
local_count = 0

for local_idx, frame in enumerate(iio.imiter(path)):
local_count = local_idx + 1
if local_idx % stride != 0:
continue
if next_sample_time >= interval_end:
break
t = cumulative_time + local_idx / src_fps
if begin_s is not None and t < begin_s:
if t + 1e-9 < next_sample_time:
continue
if end_s is not None and t >= end_s:
if t >= interval_end:
break
yield global_idx, frame
global_idx += 1
next_sample_time += 1.0 / max(1, target_fps)
while next_sample_time <= t + 1e-9:
next_sample_time += 1.0 / max(1, target_fps)

cumulative_time += local_count / src_fps
if next_sample_time >= interval_end:
break


def selected_local_indices_for_clip(
*,
nframes: int,
src_fps: float,
target_fps: int,
cumulative_time: float,
next_sample_time: float,
end_s: float | None,
) -> tuple[list[int], float]:
"""Return local frame indices selected by the timestamp sampler for one clip."""
src_fps = src_fps if src_fps > 0 else float(max(1, target_fps))
interval_end = float("inf") if end_s is None else max(0.0, float(end_s))
selected: list[int] = []
sample_time = next_sample_time
for local_idx in range(max(0, int(nframes))):
if sample_time >= interval_end:
break
t = cumulative_time + local_idx / src_fps
if t + 1e-9 < sample_time:
continue
if t >= interval_end:
break
selected.append(local_idx)
sample_time += 1.0 / max(1, target_fps)
while sample_time <= t + 1e-9:
sample_time += 1.0 / max(1, target_fps)
return selected, sample_time


def _first_sample_time(begin_s: float | None, target_fps: int) -> float:
start = 0.0 if begin_s is None else max(0.0, float(begin_s))
fps = float(max(1, target_fps))
return float(np.ceil(start * fps - 1e-9) / fps)
9 changes: 9 additions & 0 deletions deepreefmap/mapping/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -76,3 +76,12 @@ def process_sequence(
scale_type=estimates[0].scale_type,
gravity_vectors=None if gravity_vectors is None else gravity_vectors.astype(np.float32),
)

def refine_intrinsics(self, mapping_result) -> np.ndarray | None:
"""Optionally return refined 3x3 intrinsics for this sequence.

Backends can override this hook when they can estimate intrinsics from
sequence outputs. Returning ``None`` keeps the caller-provided intrinsics.
"""
del mapping_result
return None
Loading
Loading