Metadata-Version: 2.4
Name: viewgraphforge
Version: 1.0.0
Summary: Generator, loaders, baseline solvers and benchmark tools for ViewGraphBench, synthetic view graphs for global Structure-from-Motion
Author: Aizierjiang Aiersilan
License-Expression: MIT
Project-URL: Homepage, https://github.com/Ezharjan/ViewGraphForge
Project-URL: Repository, https://github.com/Ezharjan/ViewGraphForge
Project-URL: Issues, https://github.com/Ezharjan/ViewGraphForge/issues
Project-URL: Changelog, https://github.com/Ezharjan/ViewGraphForge/blob/main/CHANGELOG.md
Project-URL: Dataset, https://huggingface.co/datasets/ezharjan/ViewGraphBench
Keywords: structure-from-motion,global sfm,view graph,pose graph,rotation averaging,translation averaging,pose-graph optimization,outlier detection,synchronization,benchmark,synthetic dataset,g2o
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Science/Research
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3 :: Only
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Classifier: Topic :: Scientific/Engineering
Classifier: Topic :: Scientific/Engineering :: Image Recognition
Classifier: Topic :: Scientific/Engineering :: Mathematics
Requires-Python: >=3.10
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: numpy>=1.23
Requires-Dist: scipy>=1.9
Requires-Dist: pandas>=1.5
Requires-Dist: matplotlib>=3.6
Requires-Dist: pyyaml>=6.0
Provides-Extra: hub
Requires-Dist: huggingface_hub>=1.0; extra == "hub"
Provides-Extra: dev
Requires-Dist: pytest>=7; extra == "dev"
Dynamic: license-file

# ViewGraphForge

[![PyPI](https://img.shields.io/pypi/v/viewgraphforge)](https://pypi.org/project/viewgraphforge/)
[![Python](https://img.shields.io/pypi/pyversions/viewgraphforge)](https://pypi.org/project/viewgraphforge/)
[![License: MIT](https://img.shields.io/badge/license-MIT-blue)](https://github.com/Ezharjan/ViewGraphForge/blob/main/LICENSE)
[![Dataset: ViewGraphBench](https://img.shields.io/badge/dataset-ViewGraphBench-yellow)](https://huggingface.co/datasets/ezharjan/ViewGraphBench)

ViewGraphForge generates view graphs for the global stage of Structure-from-Motion (SfM) and evaluates solvers
on them: rotation averaging, translation averaging, SE(3) pose-graph optimisation and the detection of outlier
edges. It is the generator of [ViewGraphBench](https://huggingface.co/datasets/ezharjan/ViewGraphBench), a
dataset of 45,495 view graphs with 12 to 100,000 cameras each.

A graph is made by simulating the front end of a global-SfM pipeline on a procedural 3-D scene. Cameras
following one of seven capture patterns observe the scene points; edges come from co-visibility and an
image-retrieval step; match counts and measurement errors follow from the viewing geometry; and repeated
structure in the scene produces wrong relative poses that agree with each other around cycles. Every graph
stores the ground truth of each camera and of each measurement, including the type of every outlier.

![One graph of each of the seven camera-network families](https://raw.githubusercontent.com/Ezharjan/ViewGraphForge/main/docs/figures/families.png)

*The seven camera-network families (top views of `bench_topology` graphs): cameras in blue, inlier edges in
grey, random outliers in red, pink and purple, gross errors of the noise model in yellow.*

The package provides

* the generator: YAML configurations, parallel and resumable generation with a memory-aware scheduler;
* the `.npz` graph format with loaders, and g2o export and import;
* baseline solvers for the four tasks, evaluation metrics and a benchmark runner with leaderboards;
* validation, statistics, figures, and the tools that prepare a dataset for the Hugging Face Hub.

## Contents

1. [Installation](#1-installation)
2. [Quick start](#2-quick-start)
3. [The ViewGraphBench dataset](#3-the-viewgraphbench-dataset)
4. [How a graph is generated](#4-how-a-graph-is-generated)
5. [What a graph contains](#5-what-a-graph-contains)
6. [Generating datasets](#6-generating-datasets)
7. [Benchmark](#7-benchmark)
8. [Figures](#8-figures)
9. [Command reference](#9-command-reference)
10. [Parameter reference](#10-parameter-reference)
11. [Examples](#11-examples)
12. [Tests and reproducibility](#12-tests-and-reproducibility)
13. [Troubleshooting](#13-troubleshooting)
14. [Citation](#14-citation)
15. [References](#15-references)
16. [License](#16-license)

## 1. Installation

```
pip install viewgraphforge
```

ViewGraphForge needs Python 3.10 or newer and depends on numpy, scipy, pandas, matplotlib and PyYAML. It is
pure Python and has been used on Linux and Windows. `pip install "viewgraphforge[hub]"` also installs
`huggingface_hub` 1.0 or newer, whose `hf` command downloads the dataset. For development:

```
git clone https://github.com/Ezharjan/ViewGraphForge.git
cd ViewGraphForge
pip install -e ".[dev]"
python -m pytest
```

## 2. Quick start

Generate a graph, run two baselines and save it:

```python
import viewgraphforge as vgf
from viewgraphforge.benchmark import metrics, rotation_averaging, translation_averaging

g = vgf.generate_graph({"family": "crowd", "n_nodes": 200, "outlier_ratio": 0.2}, seed=1)
print(g.n_nodes, g.n_edges, g.is_outlier.mean())         # cameras, edges, fraction of outlier edges

R = rotation_averaging.l1_irls(g)                         # global rotations from the relative ones
print(metrics.rotation_metrics(R, g.R_wc)["rot_median_deg"])
C = translation_averaging.lud(g, R)                       # camera centres up to a similarity
print(metrics.position_metrics(C, g.centers)["pos_median_rel"])

vgf.save_graph(g, "crowd.npz")                            # also writes crowd.json with the metadata
vgf.write_g2o(g, "crowd.g2o")
```

On the command line, generate a small dataset (53 graphs in 11 splits) and run the tools on it:

```
vgf generate --config smoke --out data_smoke --workers 2
vgf validate --data data_smoke
vgf stats --data data_smoke
vgf benchmark --data data_smoke --workers 2
vgf visualize --data data_smoke
```

`vgf --help` lists all commands and `vgf <command> --help` explains the options of one.

## 3. The ViewGraphBench dataset

[ViewGraphBench](https://huggingface.co/datasets/ezharjan/ViewGraphBench) was generated with the `full`
configuration of this package (`vgf config full` prints it). It holds 45,495 graphs with about 15.2 million
cameras and 291 million relative-pose measurements in 16 splits:

| split | graphs | cameras requested | what varies |
|---|---:|---|---|
| `train`, `val`, `test_wild` | 40,000, 2,000, 2,000 | 30 to 800 | random draws of family, size, pixel noise, outlier ratio and type, retrieval depth, detection rate, noise model, heavy tails, scale noise and repeated structure |
| `test_wild_large` | 200 | 800 to 5,000 | the same with larger graphs (analytic noise model only) |
| `bench_scale` | 231 | 50 to 100,000 | number of cameras |
| `bench_noise` | 147 | 300 | pixel noise, 0.25 to 16 px |
| `bench_outliers` | 168 | 300 | fraction of random outliers, 0 to 0.6 |
| `bench_outlier_mode` | 63 | 300 | what a random outlier replaces: rotation, direction or both |
| `bench_symmetry` | 126 | 300 | confusability of repeated structure, 0 to 0.75 |
| `bench_density` | 126 | 300 | retrieval depth, 3 to 80 views kept per camera |
| `bench_heavytail` | 84 | 300 | Gaussian noise and Student-t noise with 5, 3 and 2 degrees of freedom |
| `bench_matches` | 84 | 300 | minimum number of matches per edge, 10 to 80 |
| `bench_topology` | 70 | 300 | camera-network family, 10 repetitions |
| `bench_twoview` | 63 | 100, 300, 1,000 | number of cameras, with the simulated two-view estimator |
| `bench_twoview_symmetry` | 28 | 300 | repeated structure, with the simulated two-view estimator |
| `bench_pgo` | 105 | 500 | log-normal noise of the metric baseline lengths, 0.01 to 0.4 |

The benchmark splits vary one factor over the seven camera families (or over seven cases with repeated
structure), mostly with three repetitions. All levels of a factor use the same seed in a repetition, so the
scene, the cameras and the random numbers are the same up to the step where the factor acts. Download the
parts you need with the `hf` command (`*` also matches `/`):

```
pip install "viewgraphforge[hub]"
hf download ezharjan/ViewGraphBench --repo-type dataset --local-dir ViewGraphBench --include "splits/bench_*" --include "*.csv" --include "*.json" --include "manifest_sha256.txt"
```

```python
import viewgraphforge as vgf

index = vgf.read_index("ViewGraphBench")                  # one row per graph: split, family, size, settings
rows = index[(index.split == "bench_outliers") & (index.outlier_ratio_target == 0.3)]
graphs = [vgf.load_graph(f"ViewGraphBench/{p}") for p in rows.npz_path]
```

The [dataset card](https://huggingface.co/datasets/ezharjan/ViewGraphBench) describes the files, the complete
array layout, the statistics of every split and the baseline results.

## 4. How a graph is generated

Global SfM back ends are usually evaluated on view graphs of real photo collections such as those of 1DSfM [17],
whose reference poses are themselves reconstructions, or on synthetic graphs with random topology, isotropic
noise and uniformly random outliers. ViewGraphForge simulates the front end instead: edges, match counts and
errors follow from a simulated scene and camera network, and the ground truth is exact. Each graph is generated
from its own seed:

1. Scene: a procedural point cloud with surface normals (object, facade, corridor, urban block, room, terrain
   or plaza) whose extent grows with the number of cameras for the corridor, urban, terrain and facade scenes.
   Scenes with repeated structure contain identical copies of a module: the sectors of a symmetric object,
   facade columns, corridor segments, twin buildings, the faces of a tower or a monument.
2. Cameras: exactly `n` pinhole cameras (1600 x 1200 px) of one family: `orbit` (a ring around an object),
   `dome` (several rings on a hemisphere), `corridor` (forward motion along a street), `lawnmower` (an aerial
   survey), `crowd` (unordered photos of a landmark with varying focal lengths), `walk` (hand-held motion in a
   room) or `manhattan` (a vehicle on a street grid, with loop closures).
3. Visibility: a point is observed when it is within the depth range, projects into the image, faces the camera,
   passes a z-buffer occlusion test and is detected; at most 3,000 points per image are kept.
4. Edges: in graphs of up to 400 cameras every camera pair is a candidate; larger graphs take the nearest
   neighbours in camera position and viewing direction, the three previous and next cameras in acquisition
   order and, for repeated structure, random pairs of cameras that see copies of the same module. Match counts
   are binomial in the number of co-visible points with a probability that falls with the angle between the
   views and the scale change. Repeated structure adds competing hypotheses, and the reported relative pose is
   drawn among those with enough matches, favouring the one with the most support. Each camera keeps its
   `retrieval_k` candidates with the most co-visible points and up to `cand_sym_k` identical-looking views, a
   candidate becomes an edge when its reported pose has at least `min_matches` matches, and the largest
   connected component is kept.
5. Measurements: the reported relative pose with noise, from a closed-form model (`analytic`) or from a
   simulated two-view estimator (`twoview`: the normalised 8-point algorithm [1] on noisy correspondences,
   Levenberg-Marquardt refinements of the Sampson error [10] from its solution and from the reported pose, of
   which the one with the lower error is kept, and covariances from the Fisher information). The analytic model
   is calibrated against the estimator: its errors scale with the pixel noise over the focal length and with
   1/sqrt(matches), depend on the baseline-to-depth ratio and carry a log-normal factor per edge, with optional
   Student-t tails (analytic model only).
6. Outliers: a fraction of the edges, chosen with probability inversely proportional to their number of matches,
   receives random rotations and/or directions; the symmetry-induced outliers come from step 4. A metric
   translation is added for pose-graph optimisation, and every error is recorded.

## 5. What a graph contains

A graph is one compressed numpy archive (`.npz`) with `n` cameras and `m` edges, every camera pair stored once
with `i < j`. The main arrays:

| arrays | content |
|---|---|
| `node_q_wc`, `node_t_wc`, `node_center`, `node_K` | ground-truth world-to-camera rotation (quaternion) and translation, camera centre, intrinsics |
| `edge_i`, `edge_j` | the two cameras of an edge |
| `edge_q_meas`, `edge_tdir_meas`, `edge_t_meas` | measured relative rotation, unit translation direction and metric translation |
| `edge_q_true`, `edge_tdir_true`, `edge_t_true` | the same quantities from the ground-truth poses |
| `edge_sigma_rot`, `edge_sigma_dir`, `edge_sigma_trans` | noise levels reported for the measurement: standard deviations, or the Student-t scale with heavy tails |
| `edge_n_matches`, `edge_n_covis`, `edge_baseline`, `edge_median_depth`, `edge_view_angle_deg`, `edge_overlap_i`, `edge_overlap_j` | covariates of the edge |
| `edge_err_rot_deg`, `edge_err_dir_deg`, `edge_err_trans` | true errors of the measurement |
| `edge_outlier_type`, `edge_is_outlier`, `edge_symmetry_id` | outlier labels (table below) and the symmetry behind a symmetry-induced outlier |
| `points_*`, `obs_*`, `sym_*` | scene points, image observations of the points and the symmetry transforms behind the symmetry-induced outliers |
| `meta_json` | generation parameters, seed and statistics |

| `edge_outlier_type` | the measurement is |
|---:|---|
| 0 `none` | the true relative pose with noise (inlier) |
| 1 `random_both` | a random rotation and a random direction |
| 2 `random_rotation` | a random rotation with the noisy true direction |
| 3 `random_translation` | a random direction with the noisy true rotation |
| 4 `symmetry` | the relative pose, with noise, to a virtual camera: the true camera moved by a symmetry of the scene (for the translational symmetries of periodic facades and corridors, only the direction is wrong) |
| 5 `gross_error` | not injected: a measurement of the noise model with an error above 10 deg in rotation or 20 deg in direction |

Conventions: metres, world frame with +z up, OpenCV camera frame (x right, y down, z forward), world-to-camera
poses `x_cam = R_i X + t_i`, relative poses `R_ij = R_j R_i^T` and `t_ij = t_j - R_ij t_i`, quaternions
`(qx, qy, qz, qw)` with `qw >= 0`. The `.g2o` files hold camera-to-world vertices `(R_i^T, C_i)` initialised
from a spanning tree of the measurements and `EDGE_SE3:QUAT` measurements `Z_ij = (R_ij^T, -R_ij^T t_ij)` with
their information matrices, whose rotation block refers to the rotation vector. GTSAM [15] reads them as they
are; for SE-Sync [16], remove the first line, a comment. g2o [14] reads them too, but its `EdgeSE3` applies the
rotation block to the vector part of the quaternion, about half the rotation vector, which weights rotations four
times less (multiply the block by 4 to match). `vgf.load_graph` returns a `PoseGraph` with the arrays and
convenience properties (`R_wc`, `centers`, `edges`, `R_meas`, `tdir_meas`, `t_meas`, `is_outlier`, `meta`).

![Overview of one graph](https://raw.githubusercontent.com/Ezharjan/ViewGraphForge/main/docs/figures/graph_overview.png)

*One graph in detail (`vgf visualize`): top and 3-D view, adjacency in acquisition order, the errors of each
edge class and the direction error against the baseline-to-depth ratio.*

Repeated structure is what makes the symmetry-induced outliers (type 4) hard: every edge corrupted by the same
symmetry agrees with the same moved camera, so the corrupted edges close their own cycles.

![Symmetry-induced outliers in the seven cases with repeated structure](https://raw.githubusercontent.com/Ezharjan/ViewGraphForge/main/docs/figures/symmetry.png)

*The seven cases with repeated structure of `bench_symmetry` at confusability 0.5; the orange edges are
symmetry-induced outliers. The twin buildings of the urban case produce few of them.*

## 6. Generating datasets

A dataset is described by a YAML configuration. Two are shipped: `smoke` (53 graphs, for trying things out)
and `full` (the published dataset). `vgf config` lists them and `vgf config full` prints one.

```
vgf generate --config full --out data_full --workers 16
```

Each graph is written to `<out>/splits/<split>/<graph_id>.npz` together with a `.json` copy of its metadata and,
for graphs of up to 20,000 cameras (`write_g2o_max_nodes`), a `.g2o` file; splits of more than 3,000 graphs are
divided into folders of 1,000 graphs (`splits/train/01000-01999/`). At the end the command writes `index.csv`,
`manifest_sha256.txt` and `dataset_info.json`, and the log goes to `<out>/logs/`.

* **Resuming.** Running the same command again continues an interrupted run: finished graphs are kept, partial
  files are removed and missing `.json` or `.g2o` files are exported again from the `.npz`. All files are
  written atomically. A lock file keeps a second generator away from the same folder.
* **Memory.** Graphs run in `--workers` processes, largest first. A graph is started only when its estimated
  peak memory fits into the budget (`--max-memory-gb`, 70 % of the RAM by default) next to the memory the
  workers actually use, which the scheduler measures. The estimates are corrected by the peaks measured during
  the run, and a graph that still fails with `MemoryError` is retried. Workers are replaced after 50 graphs or
  when they hold more than 1 GB, and the BLAS libraries run single-threaded inside them (through threadpoolctl
  when it is installed; without it, thread counts already set in the environment are kept).
* **Time and disk.** The published dataset took about 13 hours with 16 workers on a laptop with 16 logical CPUs
  and 32 GB of RAM, and takes 139 GB with the `.json` and `.g2o` files. Most graphs of up to 1,000 cameras take
  seconds (the `twoview` graphs up to a few minutes); the 100,000-camera graphs of `bench_scale` take from 3
  minutes to 2.7 hours each and up to 4.3 GB of memory. The generator uses the CPU only.
* **Subsets and previews.** `--splits train val` generates some splits, `--limit N` the next N graphs, and
  `--dry-run` (with `--list` for every graph) prints the plan without writing anything.

A configuration lists splits of three kinds. `sweep` varies one factor over a list of values, `grid` repeats
fixed settings and `wild` draws the settings at random:

```yaml
dataset: {name: MyViewGraphs, version: "1.0.0", seed: 7}
generation: {workers: 8, write_g2o: true, write_json: true}
defaults: {store_points: true}                 # applied to every graph
splits:
  outliers:
    kind: sweep
    factor: outlier_ratio
    values: [0.0, 0.2, 0.4]
    families: all                              # or a list, e.g. [orbit, crowd]
    seeds: 5
    params: {n_nodes: 500}
  corridors:
    kind: grid
    families: [corridor]
    seeds: 20
    params: {n_nodes: 1000, noise_model: twoview}
  training:
    kind: wild
    count: 5000
    sampler: {n_nodes: {min: 50, max: 2000, log: true, int: true}}
```

Every graph parameter of [section 10](#10-parameter-reference) can appear in `defaults`, `params` or as the
`factor` of a sweep. In a `wild` split the values drawn by the sampler take precedence over `defaults` and
`params`; their ranges are set in `sampler` (the keys of `DEFAULT_WILD_SAMPLER` in `viewgraphforge/splits.py`).
`vgf config smoke > my.yaml` writes a complete example to start from.

## 7. Benchmark

```
vgf benchmark --data ViewGraphBench --splits "bench_*" "test_*" --workers 16
```

runs the baselines on the graphs of the selected splits and writes `benchmarks/results.csv` (one row per graph
and method), `leaderboard.csv` and `leaderboard.md` (means per split and method), `sweeps.csv` (metrics against
the swept factor) and `run.json` (settings and software versions). Results are cached, so an interrupted run
continues where it stopped. By default graphs with more than 3,000 cameras are skipped (`--max-nodes`), and
pose-graph optimisation runs on graphs of up to 1,500 cameras (`--max-nodes-pgo`).

| task | baselines | metrics |
|---|---|---|
| rotation averaging | spanning tree; chordal L2 [2]; chordal L2 with Cauchy IRLS; L1 averaging followed by IRLS [4, 5, 6], with and without information weights | rotation errors after robust alignment of the global rotation: median, mean, RMS, 90th percentile, fraction below 1, 3, 5 and 10 deg |
| translation averaging | linear cross-product method [3]; least unsquared deviations, LUD [7]; each with ground-truth and with L1-IRLS rotations | position errors after a similarity alignment [11], absolute and relative to the extent of the cameras |
| pose-graph optimisation | spanning-tree initialisation; Levenberg-Marquardt with exact SE(3) Jacobians [12, 13] and L2, Huber and Cauchy losses; Cauchy from a rotation-averaging initialisation [9] | position and rotation errors after a rigid alignment, iterations |
| outlier detection | triangle cycle consistency [8]; rotation residuals after L1-IRLS; direction residuals after LUD | precision, recall, F1 and average precision against the edges with a rotation error above 5 deg (direction error above 10 deg); graphs without such edges are left out of the means |

<p>
<img src="https://raw.githubusercontent.com/Ezharjan/ViewGraphForge/main/docs/figures/sweep_outliers_rotation.png" alt="Rotation averaging against the fraction of random outliers" width="49%">
<img src="https://raw.githubusercontent.com/Ezharjan/ViewGraphForge/main/docs/figures/sweep_symmetry_outlier.png" alt="Outlier detection against the confusability of repeated structure" width="49%">
</p>

*Left: median rotation error against the fraction of random outliers (`bench_outliers`). Right: F1 score of
outlier detection against the confusability of repeated structure (`bench_symmetry`); cycle consistency misses
almost all symmetry-induced outliers. Each point is the mean over the 21 graphs of one level (for F1, over those
with labelled outliers).*

A method of your own is a function of the graph; registered in the method table, it is evaluated with the same
protocol by the serial runner (the worker processes of `vgf benchmark` know only the built-in methods):

```python
from viewgraphforge.benchmark import rotation_averaging, runner

rotation_averaging.ROTATION_METHODS["rot/my_method"] = my_solver    # graph -> (n,3,3) world-to-camera rotations
results = runner.run_dataset("ViewGraphBench", splits=["bench_outliers"], tasks=["rotation"])
```

`examples/09_benchmark_and_leaderboard.py` does this and builds the leaderboard and the sweep plots.

## 8. Figures

`vgf visualize --data <folder>` renders into `<folder>/figures/`: the family and symmetry overviews above,
per-graph overviews, a gallery of every split, the dataset summary, the sweep curves and leaderboard bars of the
benchmark, and solver results on a few graphs such as the one below. The plotting functions are available in
`viewgraphforge.viz` (see `examples/10_visualize_dataset.py`).

![Rotation and translation averaging on a dome graph](https://raw.githubusercontent.com/Ezharjan/ViewGraphForge/main/docs/figures/solver_result.png)

*A dome graph with 300 cameras and 10 % outliers: per-camera rotation errors of four rotation-averaging methods,
the errors of L1-IRLS on the cameras, and the camera centres from LUD after a similarity alignment.*

## 9. Command reference

| command | purpose |
|---|---|
| `vgf generate --config <name or file> --out <folder>` | generate a dataset; parallel, resumable, memory-aware |
| `vgf validate --data <folder>` | check every graph, its `.g2o` file, `index.csv` and the SHA-256 manifest |
| `vgf stats --data <folder>` | statistics per graph and per split in `stats/` |
| `vgf benchmark --data <folder>` | run the baselines and write the leaderboards in `benchmarks/` |
| `vgf visualize --data <folder>` | render the figures in `figures/` |
| `vgf export --data <folder> --out <folder>` | assemble the files published on the Hugging Face Hub (hard links, sub-folders for large splits) |
| `vgf card --data <folder>` | write the dataset card (`README.md`) from the files of a folder |
| `vgf config [name]` | list the shipped configurations or print one |
| `vgf calibrate` | fit the analytic noise model to the simulated two-view estimator |
| `vgf clean` | remove caches, build files and the leftovers of interrupted runs; `--data <folder> --wipe` deletes a dataset |

`validate`, `stats` and `benchmark` accept `--workers N`. The `--splits` option of `generate`, `validate`,
`benchmark` and `visualize` takes split names or shell-style patterns such as `"bench_*"`, as does `--g2o` of
`export`; `stats` reads every split. `python -m viewgraphforge` is the same as `vgf`.

## 10. Parameter reference

Graph parameters with their defaults (`viewgraphforge/config.py`):

| parameters | defaults | meaning |
|---|---|---|
| `family`, `scene` | `orbit`, family default | camera family (`orbit`, `dome`, `corridor`, `lawnmower`, `crowd`, `walk`, `manhattan`) and scene (`object`, `facade`, `corridor`, `urban`, `room`, `terrain`, `plaza`) |
| `n_nodes` | 200 | number of cameras; the largest connected component of the view graph is kept |
| `n_points`, `scene_params` | scale-aware | scene points and options of the scene builder, e.g. `{symmetry_fold: 4}`, `{periodic: true}`, `{tower_fold: 4}`, `{twin_buildings: 4}`, `{monument_fold: 2}` |
| `camera_params`, `image_size`, `hfov_deg` | family defaults, 1600 x 1200 | options of the camera family and intrinsics |
| `detection_prob`, `max_depth_factor`, `max_depth_abs`, `max_incidence_deg`, `zbuffer_tol`, `max_visible_per_camera` | 0.8, 6, 400 m, 80 deg, 0.08, 3000 | visibility model |
| `retrieval_k`, `min_matches`, `p_match_max`, `theta0_deg`, `s0`, `hypothesis_sharpness` | 20, 20, 0.7, 40 deg, 0.7, 4 | retrieval depth, minimum matches of an edge, match probability model and selection among competing hypotheses |
| `symmetry_confusability`, `cand_sym_k` | 0, 4 | factor on the match probability of correspondences between two copies of repeated structure (0: never confused); retrieval budget per camera for identical-looking views |
| `noise_model`, `pixel_noise`, `heavy_tail_df`, `scale_noise` | `analytic`, 1 px, 0 (Gaussian), 0.05 | measurement noise; `twoview` runs the simulated two-view estimator |
| `outlier_ratio`, `outlier_mode`, `outlier_weighting` | 0.1, `mixed`, `inverse_matches` | random outliers: fraction, type (`rotation`, `translation`, `both`, or `mixed`: both, translation and rotation with probabilities 0.4, 0.4 and 0.2) and choice of the edges |
| `gross_rot_deg`, `gross_dir_deg` | 10 deg, 20 deg | thresholds of the `gross_error` label |
| `store_points`, `max_points_stored`, `store_observations`, `store_observations_max_nodes`, `max_observations_per_camera` | true, 20000, true, 2000, 300 | stored scene points and image observations |

The constants of the analytic noise model (`viewgraphforge/noise.py`) were fitted with
`vgf calibrate --n-nodes 100 --seeds 3` on a development version of the generator (28,715 inlier edges;
`viewgraphforge/configs/noise_calibration.json` holds the fit). The released version gives similar but not
identical constants (about 29,800 edges, `a_rot` 35.4 instead of 36.7); the shipped constants are kept so that
ViewGraphBench can be regenerated.

## 11. Examples

The [examples](https://github.com/Ezharjan/ViewGraphForge/tree/main/examples) run on the smoke dataset
(`vgf generate --config smoke --out data_smoke --workers 2`) or on a downloaded copy of ViewGraphBench:

| example | shows |
|---|---|
| `01_load_and_inspect.py` | the arrays of a graph and a check of the pose, relative-pose and epipolar conventions |
| `02_rotation_averaging.py` | rotation averaging by spanning tree, chordal L2, chordal L2 with Huber IRLS and L1-IRLS, and the error distributions |
| `03_translation_averaging.py` | world-frame baseline directions, the linear method and LUD with true and estimated rotations |
| `04_pose_graph_optimization_g2o.py` | robust SE(3) pose-graph optimisation from a `.g2o` file, with an optional GTSAM cross-check |
| `05_outlier_filtering.py` | cycle-consistency and residual filters, and why symmetry-induced outliers defeat cycle consistency |
| `06_learning_edge_classifier.py` | per-edge features, conversion to PyTorch Geometric and a learned outlier classifier on the wild splits |
| `07_point_based_global_positioning.py` | camera positions from the image observations, the idea behind the global positioning of GLOMAP [18], where camera-only translation averaging fails |
| `08_custom_generation.py` | generating graphs from Python with repeated structure, two-view noise and heavy tails |
| `09_benchmark_and_leaderboard.py` | the benchmark runner with a method of your own, a leaderboard and a sweep plot |
| `10_visualize_dataset.py` | the plotting functions of `viewgraphforge.viz` |

## 12. Tests and reproducibility

`python -m pytest` runs the test suite: the exponential and logarithm maps of SO(3) and SE(3), the SE(3)
Jacobians against finite differences, the pose, quaternion and projection conventions, the invariants of
generated graphs of every family, the exact consistency of noise-free graphs, the virtual-camera relation of
symmetry-induced outliers, the two-view estimator, the npz and g2o round trips, determinism, the solvers and
metrics on clean and corrupted graphs, and the commands (parallel generation, resume, lock, scheduler,
validation, export, dataset card, clean).

Every graph has a 32-bit seed, `SeedSequence(dataset_seed, spawn_key=key).generate_state(1, uint32)[0]` with
`key = (crc32(s), k)` for graph `k` of a `wild` split `s` and `key = (crc32(s), c, r)` for case `c` and
repetition `r` of a `sweep` or `grid` split (column `seed` of `index.csv`), whatever the number of workers and
the order of execution. The graph is generated with `numpy.random.default_rng(seed)`; the settings of a `wild`
graph are drawn before, with `default_rng(SeedSequence(seed, spawn_key=(7,)))`. On the same platform and library
versions a configuration therefore reproduces every array of every graph exactly; only the generation times
stored in each file differ, so the file hashes of two runs differ. Elsewhere the last bits of
floating-point results can differ, which can change a visibility or matching decision and then the rest of that
graph. Regenerated on Linux with numpy 2.5.3 and scipy 1.18.1, 61 benchmark graphs of the published copy (made on
Windows) kept their edges, match counts and labels, while 3 of 7 training graphs (two with the `twoview`
estimator, one with more than 400 cameras) did not; with numpy 2.4.4 and scipy 1.17.1, 26 of the 61 benchmark
graphs differed as well. Datasets from different machines are therefore compared with `vgf validate` and
`vgf stats` rather than by file hash.

## 13. Troubleshooting

* *`MemoryError` in the log.* The machine ran out of memory, often because other programs use a large share of
  it. Lower the budget (`--max-memory-gb`) or the number of workers and run the same command again; failed
  graphs are generated on the next run.
* *Generation is slow and the CPU is mostly idle.* The machine is swapping. Check the memory figures in the log
  lines and lower `--max-memory-gb`, `--workers` or `--recycle-gb`. High CPU use with slow progress points to
  `OMP_NUM_THREADS` or `MKL_NUM_THREADS` set above 1 in the environment, which the workers keep when
  threadpoolctl is not installed.
* *`another vgf generate (pid ...) is writing to ...`.* A second `vgf generate` on the same folder was refused.
  A lock left by a crashed run is removed automatically; `--force` overrides the check.
* *A graph has fewer cameras than its id says.* Only the largest connected component is kept; the id records
  the requested number, `index.csv` and the metadata the actual one.
* *Rows of two tables do not match.* A graph id is unique within its split but can occur in several splits, so
  tables are joined on (`split`, `graph_id`).
* *Translation averaging fails on `corridor` and `manhattan` graphs.* The directions between cameras on a
  straight path do not determine their spacing. The image observations do
  (`examples/07_point_based_global_positioning.py`).

## 14. Citation

If you use the dataset or the software, please cite the dataset:

```bibtex
@misc{viewgraphbench2026,
  title        = {ViewGraphBench: Physically-Grounded Synthetic View Graphs for Global Structure-from-Motion Calibration},
  author       = {Aiersilan, Aizierjiang},
  year         = {2026},
  note         = {Dataset version 1.0.0, generated with ViewGraphForge},
  howpublished = {Hugging Face Hub},
  url          = {https://huggingface.co/datasets/ezharjan/ViewGraphBench}
}
```

To refer to the software itself:

```bibtex
@software{viewgraphforge2026,
  title        = {ViewGraphForge},
  author       = {Aiersilan, Aizierjiang},
  year         = {2026},
  url          = {https://github.com/Ezharjan/ViewGraphForge}
}
```

[`CITATION.cff`](https://github.com/Ezharjan/ViewGraphForge/blob/main/CITATION.cff) holds the same information in
the Citation File Format.

## 15. References

1. R. Hartley. In defense of the eight-point algorithm. *IEEE TPAMI* 19(6):580-593, 1997.
2. D. Martinec and T. Pajdla. Robust rotation and translation estimation in multiview reconstruction. *CVPR* 2007.
3. V. M. Govindu. Combining two-view constraints for motion estimation. *CVPR* 2001.
4. V. M. Govindu. Lie-algebraic averaging for globally consistent motion estimation. *CVPR* 2004.
5. A. Chatterjee and V. M. Govindu. Efficient and robust large-scale rotation averaging. *ICCV* 2013.
6. A. Chatterjee and V. M. Govindu. Robust relative rotation averaging. *IEEE TPAMI* 40(4):958-972, 2018.
7. O. Özyeşil and A. Singer. Robust camera location estimation by convex programming. *CVPR* 2015.
8. C. Zach, M. Klopschitz and M. Pollefeys. Disambiguating visual relations using loop constraints. *CVPR* 2010.
9. L. Carlone, R. Tron, K. Daniilidis and F. Dellaert. Initialization techniques for 3D SLAM: a survey on rotation
   estimation and its use in pose graph optimization. *ICRA* 2015.
10. P. D. Sampson. Fitting conic sections to "very scattered" data: an iterative refinement of the Bookstein
    algorithm. *Computer Graphics and Image Processing* 18(1):97-108, 1982.
11. S. Umeyama. Least-squares estimation of transformation parameters between two point patterns. *IEEE TPAMI*
    13(4):376-380, 1991.
12. T. D. Barfoot. *State Estimation for Robotics.* Cambridge University Press, 2017.
13. J. Solà, J. Deray and D. Atchuthan. A micro Lie theory for state estimation in robotics. arXiv:1812.01537, 2018.
14. R. Kümmerle, G. Grisetti, H. Strasdat, K. Konolige and W. Burgard. g2o: a general framework for graph
    optimization. *ICRA* 2011.
15. F. Dellaert. Factor graphs and GTSAM: a hands-on introduction. Technical report GT-RIM-CP&R-2012-002,
    Georgia Institute of Technology, 2012.
16. D. M. Rosen, L. Carlone, A. S. Bandeira and J. J. Leonard. SE-Sync: a certifiably correct algorithm for
    synchronization over the special Euclidean group. *International Journal of Robotics Research*
    38(2-3):95-125, 2019.
17. K. Wilson and N. Snavely. Robust global translations with 1DSfM. *ECCV* 2014.
18. L. Pan, D. Baráth, M. Pollefeys and J. L. Schönberger. Global structure-from-motion revisited. *ECCV* 2024.

## 16. License

MIT License, Copyright (c) 2026 Aizierjiang Aiersilan. See [`LICENSE`](https://github.com/Ezharjan/ViewGraphForge/blob/main/LICENSE).
