Skip to main content
Version: 3.4 (unreleased)

Structure-from-Motion (SfM) & Dense MVS

VRGS can rebuild a 3D scene from a set of overlapping photographs. The pipeline runs as a chain of stages — the first always runs, the rest are optional follow-ons you trigger when you want them:

  1. Sparse SfM — detects features in each photo, matches them across overlapping pairs, and solves for every camera's position/orientation and a sparse coloured point cloud (the matched tie points). This stage always runs.
  2. Dense MVS (optional) — takes the solved cameras and estimates a depth map per view, then fuses them into a dense coloured point cloud of the surface.
  3. Create Mesh (optional) — turns the dense cloud into a triangulated surface (Poisson or Greedy Projection), carrying the cloud's per-vertex colour.
  4. Texture Mesh from Photos (optional) — projects the solved photographs back onto the mesh and bakes a high-detail photographic texture.

Typical inputs are drone/UAV imagery or hand-held photos of an outcrop. Typical outputs are a georeferenced camera set (with image thumbnails in the 3D view), a sparse point cloud, and—if enabled—a dense point cloud, a surface mesh, and a photo-textured mesh you can interpret.

Workflow in one line

New an SfM workflow → Add Photos… → set parameters → Run Reconstruction → (optional) Create Mesh → (optional) Texture Mesh from Photos.


Running a reconstruction

All actions live on the SFM branch of the Data Tree.

StepActionWhere
1. Create a workflowRight-click the SFM GroupNewCreates Model 1, Model 2, …
2. Add imagesRight-click the workflow → Add Photos…Browse to your JPEGs
3. ConfigureSelect the workflow; pick a Quality preset, or edit the ParametersSee Parameters
4. Control (optional)Right-click the workflow → Import Ground Control Points (CSV)…See Ground control points
5. ReconstructRight-click the workflow → Run ReconstructionRuns on a background thread, with a progress bar
6. Check qualityRight-click the workflow → Show Quality ReportSee Quality report
7. Review imagesRight-click the workflow → Open Photo BrowserPer-photo keypoint/track stats; also where you mark control points
8. Mesh (optional)Right-click the workflow → Create Mesh (Poisson)… or Create Mesh (Greedy Projection)…Needs a dense cloud; see Building a mesh
9. Texture (optional)Right-click the workflow → Texture Mesh from Photos…Needs a mesh; see Texturing the mesh

Other commands on the workflow's right-click menu:

CommandWhat it does
Use Project Ground Control PointsAdopts ground control already in the project into this workflow
Scale Bars (Known Distances)…Gives a model real size without GPS or control — see Scale bars
Seed Mesh Tie Points from Ground ControlTurns measured control into the mesh's own tie points, so the mesh ribbon's Corrections → GCP can pull the mesh onto the survey
Open Photos in Annotation ViewOpens the mesh's photos in the annotation view for SAM2/UNet interpretation
Show Camera GPS as Point CloudA QC cloud of the raw EXIF GPS positions, to compare against the solved cameras
Add Satellite Imagery BasemapDrapes Bing aerial imagery under the model

As soon as you Add Photos, any photo with EXIF GPS gets a camera marker in the 3D view at its GPS position — before you run anything. These are drawn without a thumbnail to mark them as provisional: they are where the receiver said the camera was, not a solved pose. After a reconstruction the registered cameras are redrawn with thumbnails, in their solved positions.

The reconstruction runs in the background; progress appears on the main progress bar and a summary in the log (and the workflow's Status property). When it finishes, the results are added to the project automatically:

  • <name> sparse — the sparse tie-point cloud (e.g. Model 1 sparse), carrying Observations and Reprojection error attributes you can colour by — see Inspecting the sparse cloud.
  • <name> dense — the dense MVS cloud, only if Dense MVS was enabled.
  • <name> mesh — the surface mesh, once you run Create Mesh (it later carries the baked texture and the photographs too).
  • Camera poses — each registered photo, with its image thumbnail.
  • Ground control points — any control you imported, as project objects under Ground Control Points in the Data Tree, showing how far the model landed from each one.

The mesh and texturing stages also run on background threads; the mesh links back to its workflow, so Texture Mesh from Photos always finds the mesh that workflow created.

Re-running is cheap — the solve is reused

A completed solve is kept in the workflow's workspace, and Run Reconstruction reuses it whenever nothing that shapes the solve has changed — same photos, same reconstruction parameters, same control, tie points and scale bars. The re-run then skips feature extraction, matching and registration entirely and goes straight to whatever is new.

The everyday case: you run a reconstruction, only then remember Dense MVS was left off. Enable it and run again — the second run starts at the dense stage within seconds instead of re-registering every photo for half an hour. The same applies to tuning any Dense MVS parameter: the dense settings are deliberately not part of the reuse check, so dense experiments never pay the sparse cost. The log states plainly when a run reused the solve and when it did a full one.

Change any reconstruction parameter — or the photo list, ground control, tie points or scale bars — and the next run does an honest full solve. To force one without changing anything, delete sparse_solution.bin from the workflow's workspace folder.

Coordinate frame

The model is placed by whichever of these you give it, in this order of priority:

  1. Ground control points — 3 or more marked control points fit the model directly to your survey (centimetre accuracy). See Ground control points.
  2. Camera GPS — with Use GPS prior on and valid EXIF GPS, the model is fitted to the camera positions in the project's coordinate system, so the output lands alongside the rest of your project data (metre accuracy).
  3. Nothing — the model is reconstructed in an arbitrary local frame with no scale and no orientation. A scale bar can still give it real size, but it will not be georeferenced.

If the photos carry GPS and the project has no coordinate system yet, VRGS opens the Coordinate System dialog with the UTM zone covering your photos already selected — accept it or search for a different CRS.

Whatever placed the model, the quality report always states which data did it and how well it fitted.


Parameter reference

Parameters are grouped below the way they appear in the Properties panel. Every value has a sensible default—you can run a first reconstruction without changing anything and tune afterwards.

Quality preset

Start here. The preset moves the handful of settings that genuinely trade speed against accuracy, so you rarely need to touch the rest.

  • Options: Fast, Balanced (the defaults), Accurate, Custom.
  • What it does:
    • Fast — half resolution, fewer features, retrieval matching. Loses fine detail and some weak-overlap pairs. Use it same-day, in the field, to find out whether the survey actually worked.
    • Balanced — the shipped defaults. What you want almost always.
    • Accurate — full resolution, dense features, exhaustive matching, tighter gates. Markedly slower (on a large set the exhaustive matcher is the cost), but leaves nothing on the table.
    • Custom — a readout, not a choice: it means the settings below no longer match any preset. Selecting it does nothing.
  • What it will not touch: your GPS, altitude-datum and Dense MVS settings. A preset never silently undoes a decision you made for a different reason.

Feature extraction & matching

These control how features are found in each image and matched across images. They have the biggest effect on how many photos register and how complete the sparse cloud is.

Detector

  • What it is: the feature detector/descriptor algorithm.
  • Options / default: SIFT (default), ORB, AKAZE.
  • What it does: SIFT gives the most robust, repeatable matches on natural rock texture and is the best default for quality. ORB is a fast binary detector—much quicker and lighter on memory, but produces fewer reliable matches. AKAZE sits in between.
  • Example: keep SIFT for final outcrop reconstructions. Switch to ORB for a fast sanity check on a very large image set, or when you only need a rough camera layout quickly.

Alignment image scale

  • Setting key: align_image_scale
  • Options / default: Full, Half (1/2) (default), Quarter (1/4), Eighth (1/8), Sixteenth (1/16).
  • What it does: the working resolution for feature detection, relative to each photo rather than an absolute pixel cap. Smaller is faster and uses less memory but finds fewer fine features.
  • Why relative: an absolute cap makes a preset mean different things on different cameras. At a 3200 px cap, a 45 MP frame and a 12 MP frame both land at 3200 px, so the larger sensor buys nothing. A relative ladder preserves each camera's angular resolution, so a preset means the same thing everywhere.
  • Example: Half for normal work, Quarter for quick tests, Full to squeeze maximum detail from a small, high-resolution set.

This matches Metashape's alignment accuracy ladder: their High = Full, Medium = Half, Low = Quarter. The presets set it for you — Fast = Quarter, Balanced = Half, Accurate = Full.

Replaces "Max image dimension"

Older projects stored an absolute max_image_dim in pixels. It is read and converted to the nearest rung when such a project is opened; the setting itself is gone.

Features per megapixel

  • Setting key: features_per_mpx
  • Default: 2000. 0 ignores density and uses the cap as a flat count.
  • What it does: keypoint density on the working (downsampled) image. The budget for a photo is this times its working megapixels, then capped by Max features per image. Expressing it as a density rather than a count keeps detection even across mixed-resolution sets.
  • Example: leave at 2000. Raise it on low-texture scenes where pairs are hard to verify. Ignored by the AKAZE detector.

Max features per image

  • Default: 8000.
  • What it does: the upper bound on keypoints kept per image. More features give more matches and a denser sparse cloud at the cost of speed and memory.
  • Example: raise to 1200020000 on low-texture scenes (smooth, uniform rock) where pairs are hard to verify; drop to 4000 for speed.

Lowe ratio test

  • Default: 0.8 (typical range 0.70.8).
  • What it does: a match is accepted only if the best descriptor match is clearly better than the second-best by this ratio. Lower is stricter (fewer but cleaner matches); higher keeps more matches but admits more noise.
  • Example: lower to 0.75 on repetitive texture (e.g. bedded sandstone, brickwork) to suppress ambiguous matches.

Min geometric inliers

  • Default: 20.
  • What it does: the minimum number of geometrically-verified inlier matches for an image pair to be trusted. It is also the floor used when choosing the initial pair and registering later views. Higher = stricter (cleaner pose graph, but a thin-overlap dataset may fragment); lower admits weaker pairs.
  • Example: raise to 3040 on clean, high-overlap datasets for robustness; only lower below 20 (cautiously) when overlap is genuinely sparse.

Match strategy

  • Options / default: Auto (default), Exhaustive, Vocabulary tree.
  • What it does: decides which image pairs are matched at all. Exhaustive tries every pair — the most thorough, but the work grows with the square of the image count and becomes impractical past a couple of thousand photos. Vocabulary tree matches each image only against its most visually similar neighbours, which collapses the cost and tends to improve robustness by never attempting pairs that cannot overlap. Auto is exhaustive up to the threshold below, then switches.
  • Example: leave on Auto. Force Exhaustive on a small, awkward set where every possible pair is worth trying.

Auto retrieval threshold (images)

  • Default: 600.
  • What it does: the image count above which Auto switches to vocabulary-tree retrieval. Ignored unless Match strategy is Auto.

Retrieval neighbours

  • Default: 40.
  • What it does: how many similar images each image is matched against under retrieval. Higher finds more overlap at proportionally more matching time.
  • Example: raise to 60+ if a large set is registering poorly; drop to 20 for speed.

Retrieval sequential overlap

  • Default: 0 (retrieved candidates only).
  • What it does: additionally matches each photo against this many neighbours either side of it in capture order, whatever the similarity search made of them.
  • Example: set to 25 on ordered flight lines. Retrieval ranks images by visual similarity, so a frame over featureless sand, water or snow can score poorly against its true neighbours and never be tried — this pairs it with them anyway, on the strength of capture order alone.

Epipolar RANSAC (px)

  • Default: 2.0, in detection-scale pixels.
  • What it does: the inlier threshold used to verify a pair's geometry.
  • Example: raise it if pairs that clearly overlap are being rejected on noisy or motion-blurred imagery.

Registration tolerance (px)

  • Default: 4.0.
  • What it does: the base reprojection tolerance for the gates that decide whether a photo registers at all.
  • Example: raise to 68 when photos that obviously overlap still fail to register; lower to 3 for a stricter, cleaner model.

Min triangulation angle (deg)

  • Default: 2.0.
  • What it does: points whose viewing rays converge at a shallower angle than this are rejected — their depth is too uncertain to trust.
  • Example: raise to 35 for a cleaner cloud; lower only for narrow-baseline sets where you are losing too many points.

Seed pair min median angle (deg)

  • Default: 8.0.
  • What it does: the geometry demanded of the first pair. A weak seed settles the solve into a poor basin and everything registered afterwards inherits it. If no pair qualifies, VRGS relaxes to Min triangulation angle and retries, logging a warning.

Cameras & calibration

Share intrinsics per camera

  • Default: on.
  • What it does: groups photos by camera make/model/focal length so they share one set of intrinsics (focal length, principal point, distortion). Fewer unknowns means a more stable, faster bundle adjustment.
  • Turn off when: every image may have different intrinsics—mixed cameras, or a zoom lens used at varying focal lengths. Per-image intrinsics need more overlap to solve reliably.
  • Example: leave on for a single drone camera at a fixed focal length; consider off for an ad-hoc mix of phone and drone photos.

Where the starting focal length comes from

Bundle adjustment needs a focal length to start from, and it is taken from one of three sources, in this order:

  1. A calibration already attached to the photograph — including one written by an earlier reconstruction of the same photos. This is the most accurate source, and it is why re-running a workflow often starts from better intrinsics than the first run did.
  2. EXIF FocalLengthIn35mmFilm (tag 41989). It is normalised to a 36 mm frame, so fx = equivalent × width / 36 without needing the sensor size. The tag holds a rounded whole number of millimetres, which is close enough to start from.
  3. A 50 mm assumption, if neither of the above is available.

The third is a guess, and a poor one for the imagery this tool usually sees: drone and phone cameras sit nearer 20 mm equivalent, so 50 mm begins the solve with a focal length roughly 2.5× too long. That is enough to stop photographs registering at all — and the symptom, a reconstruction that fails to register or comes out badly scaled, looks like a matching problem rather than a missing EXIF tag. The run now states it plainly instead of proceeding in silence:

[WARNING] SfM intrinsics: 12 of 255 photo(s) carry no usable focal length in
EXIF (FocalLengthIn35mmFilm, tag 41989) and were seeded from a 50 mm
ASSUMPTION. Drone and phone imagery is nearer 20 mm equivalent, so this seeds
fx roughly 2.5x too long.

If you see this line and registration is poor, attach a camera calibration to those photographs.

EXIF is read at import, not at solve time

Focal length, GPS and camera identity are read when photographs are imported and stored with the workflow. Photographs imported by an earlier version keep whatever was read then, so re-import them if you want a later version's reading.

Cameras that correct their own lens

Some cameras remove lens distortion in firmware, before the JPEG is written — every DJI drone does this, and so do most phones. The file you get is already rectilinear even though it is the original, straight off the card.

VRGS reads the camera make and model from EXIF and recognises these bodies automatically. When it does, it holds the distortion coefficients at zero through bundle adjustment instead of solving for them, and says so in the log:

[INFO] SfM intrinsics: camera 0 'DJI FC330' 4000x3000 seed fx=2222.22
-- de-warped in firmware: distortion FIXED at zero

A camera with a genuinely raw lens is reported the other way and solved normally:

[INFO] SfM intrinsics: camera 0 'SONY NEX-5R' 3568x2368 -- distortion will be
refined by BA

This matters more than it looks. If distortion is left free on an already-corrected camera, bundle adjustment will still find coefficients for it — a near-nadir survey makes distortion, focal length and depth strongly correlated, so the solver can lower reprojection error by absorbing feature-matching noise into the radial terms while the poses and points quietly compensate. The result reports an excellent reprojection error and is geometrically wrong, and the damage only becomes visible later, when the model is used to project photographs onto real geometry: each photo lands slightly off, by an amount that grows toward its frame corners, and the texture bake blends the disagreement into smeared bands across the model.

A low reprojection error does not prove the camera model is right

Bundle adjustment fits image features, and those features carry whatever bias the images have. The honest test is photometric — do overlapping photographs agree when sampled at real geometry? That is exactly what the texture bake's distortion self-check measures.

GPS & georeferencing

These only matter when your photos carry EXIF GPS (most drone imagery does).

Use GPS prior

  • Default: on.
  • What it does: uses EXIF GPS to (1) optionally pre-filter image pairs that are too far apart to overlap, and (2) after a purely visual reconstruction, align the whole model to a metric East-North-Up frame and refine it with GPS as a soft constraint. The result has real-world scale and georeferencing.
  • Turn off when: photos have no/poor GPS, or you deliberately want a scale-free visual reconstruction.
  • Example: on for drone surveys; off for ground-based photos with no GPS.

GPS altitude is MSL

  • Default: off (heights are treated as ellipsoidal).
  • What it does: tells VRGS how to read the EXIF altitude. Off means ellipsoidal height — what most drones actually write, despite the EXIF specification saying otherwise — and VRGS converts it into the project's vertical frame if a vertical datum is configured. On means the altitude is already MSL/orthometric and is passed straight through without a second geoid correction.
  • Why it matters: getting this wrong shifts the whole model vertically by the local geoid separation, which can be tens of metres. It is nothing to do with horizontal accuracy, and it will not look obviously wrong — it just sits at the wrong height.
  • Example: leave off for DJI and most consumer drones. Turn on only if you know your camera or flight software wrote true MSL altitudes.

GPS sigma XY (m)

  • Default: 5.0.
  • What it does: the assumed horizontal uncertainty of the GPS positions— how much to trust GPS versus the visual geometry. Smaller pulls cameras harder onto their GPS coordinates; larger lets the photo geometry dominate.
  • Example: 12 m for RTK/PPK drones; 510 m for consumer/phone EXIF.

GPS sigma Z (m)

  • Default: 10.0.
  • What it does: the assumed vertical uncertainty. GPS altitude is usually much less accurate than horizontal position, so this is normally larger than GPS sigma XY.
  • Example: 1020 m for consumer EXIF altitude; smaller for survey-grade vertical control.

GPS pair max distance (m)

  • Default: 100.0. 0 disables it.
  • What it does: when greater than 0 and both photos in a pair have GPS, matching is skipped if the cameras are farther apart than this. On large flights most pairs cannot overlap, so this is a big speed-up. Pairs where either photo lacks GPS are always tried, so projects without GPS are unaffected.
  • Example: set to roughly 2–3× your photo spacing/footprint (e.g. 3050 m) on a big survey. Raise it (or set 0) if photos that genuinely overlap are failing to register — for example a convergent set shot from far apart, or imagery whose EXIF GPS is poor enough to push a real pair over the limit.
caution

This is on by default at 100 m. If your survey has overlapping photos taken more than 100 m apart — long-range oblique or telephoto shots, for instance — those pairs are dropped before matching is even attempted. Raise the value or set it to 0.

Bundle adjustment

BA max iterations (global)

  • Default: 100.
  • What it does: the maximum optimiser iterations per global bundle-adjustment pass. The solver usually converges well before the cap; more iterations can refine a difficult solve at the cost of time.
  • Example: leave at 100; drop to 3050 for fast previews.

Huber threshold (px)

  • Default: 2.0, in original-image pixels.
  • What it does: sets how much reprojection error bundle adjustment will believe before it decides an observation is probably wrong.

Every tie point contributes a residual: the gap, in pixels, between where the solved model says the point should appear in a photo and where the feature matcher actually found it. Bundle adjustment moves the cameras and points to make those gaps small. The question this setting answers is what to do with the gaps that stay large.

Below the threshold, a residual is treated as ordinary measurement noise and counts at full strength. Above it, the observation still counts — but its influence is capped rather than growing with the square of the error. So a tie point that lands 40 px out is heard, not obeyed.

That cap is what stops one bad match from wrecking a model. Without it, error grows quadratically without limit, so a single gross mismatch — a repeated texture, a moving object, a reflection matched across two photos — can outweigh hundreds of good observations and visibly bend the cameras around it. The wreckage usually shows up far from the actual mistake, which makes it painful to diagnose.

  • Lower (1.01.5): stricter. Reasonable for sharp, well-textured imagery where a large residual really does mean a bad match.
  • Higher (3.04.0): more forgiving. Reasonable for soft, noisy, or motion-blurred imagery, where honest observations routinely land a few pixels out and a tight threshold would quietly discount most of your data.
These are not the same pixels as Epipolar RANSAC

Epipolar RANSAC (px) is in detection-scale pixels — measured on the downsampled working image and scaled up internally. Huber threshold (px) is in original-image pixels. The same number means different things in the two boxes, and the gap is large: on a 45 MP frame at Alignment image scale Quarter, the working image is about 2.9× smaller, so 2.0 detection px is roughly 5.8 original px.

The practical consequence is that this threshold has to track Max image dimension. Downsample harder and feature positions get scaled up further, so their noise in original pixels grows; a threshold left where it was would sit below the noise floor and start treating routine noise as suspect. The fit would not break — it would lean toward a median rather than a mean — but you would lose precision you had paid for.

The quality presets do this for you: 4.0 for Fast (working resolution 1600), 2.0 for Balanced (3200), 1.5 for Accurate (no downsampling). You only need to move it yourself if you set Alignment image scale by hand, in which case scale it inversely — halve the resolution, roughly double this.

note

This applies to image observations only, across every bundle-adjustment pass (local, global, and the ground-control refinement). GPS and ground-control priors are robustified separately at a fixed 2 sigma, in units of their own quoted accuracy, and are unaffected by this setting — tune those with GPS sigma XY / Z and the per-point accuracies in your control CSV instead.

Dense MVS

Dense MVS runs after sparse SfM and produces the dense surface cloud. It is off by default.

Dense MVS (learned)

  • Default: off.
  • What it does: the master on/off switch for the dense stage. When on, a dense coloured point cloud (<name> dense) is produced in addition to the sparse one.
  • Example: turn on whenever you want a dense surface to mesh or interpret.

Dense MVS backend

  • Setting key: dense_mvs_backend
  • Options / default: PLANE_SWEEP (default), PLANE_SWEEP_GPU, CASMVSNET.
  • What it does: chooses the depth estimator.
    • PLANE_SWEEP is the built-in CPU plane-sweep (normalised cross-correlation). It needs no model file and runs anywhere. Roughly 22 seconds per view.
    • PLANE_SWEEP_GPU is the same algorithm as Vulkan compute shaders, on any Vulkan 1.1 GPU. Roughly 0.2 seconds per view — a 370-view survey sweeps in about a minute instead of two and a quarter hours. It needs no model file and no 3D view open, and it falls back to PLANE_SWEEP for the whole run if no suitable device is available, reporting why in the log.
    • CASMVSNET is a learned neural-network estimator that requires a casmvsnet.onnx model in the project MODELS folder and a CUDA-capable GPU.
  • Do the two plane sweeps agree? They are held to the same result by differential tests: on a synthetic scene the GPU chain reproduces the CPU sweep's winning depth bin on 196,606 of 196,608 pixels, with identical validity. They share one code path for the depth bracket and for every post-filter, so they cannot drift on those at all.
  • Example: use PLANE_SWEEP_GPU if your machine has a Vulkan GPU — it is the same result, far faster, and it is what makes a raised depth hypotheses count affordable. Choose CASMVSNET only if you have the model file and a GPU and want to compare learned depth.

Depth map quality

  • Setting key: depth_map_quality
  • Options / default: Full, Half (1/2), Quarter (1/4) (default), Eighth (1/8), Sixteenth (1/16) — the same relative ladder as Alignment image scale, applied to dense matching.
  • What it does: the working resolution for dense depth estimation, relative to each photo. Cost and memory grow with the square of this, so one rung up is roughly four times the work.
  • Memory: the plane-sweep grid is trimmed if it will not fit its memory budget, and the log reports both the grid actually used and the budgets it was measured against. On the PLANE_SWEEP_GPU backend the cost volumes live in graphics memory, so only fusion is priced against system RAM.
  • CasMVSNet: its ONNX input is locked at 1152×864, so for that backend Half and Full buy detail by tiling instead, at roughly (T+1)² inferences per view.
  • Example: Quarter is the default and is a good balance. Matches Metashape's depth map quality ladder: their Ultra High = Full, High = Half, Medium = Quarter.

Dense MVS neighbor views

  • Setting key: dense_mvs_num_views
  • Default: 5.
  • What it does: how many neighbouring source views are used to estimate depth for each reference view. More views give more robust depth (slower); fewer are faster but noisier.
  • Example: 5 is a good default; try 7 for wide-baseline captures, 3 for speed. (The CASMVSNET backend uses the view count baked into its model.)

Dense MVS depth hypotheses

  • Setting key: dense_mvs_num_depths
  • Default: 128. Allowed: a multiple of 32 from 32 to 1024.
  • What it does: how many candidate depths the sweep tests per pixel. The sweep spaces them evenly in inverse depth, which is the right choice for perspective imagery but means the hypotheses are fine near the camera and coarse far away.
  • Example: leave at 128 for close-range or flat-on captures. Raise to 256 or 512 on the GPU backend when the log shows wide depth brackets, or when you see a plume of points projecting away from the camera below the outcrop. If a value is too large for your GPU the run reports the device limit and falls back to the CPU sweep rather than producing a wrong cloud. The CASMVSNET backend ignores this setting; its hypotheses are baked into the exported model.

The depth resolution at the far end of a view's range is (far / near - 1) / (hypotheses - 1), expressed as a fraction of the depth itself. A view whose visible scene spans 2 m to 42 m — a 20:1 range, ordinary when looking along a cliff — therefore resolves its far end in steps of about 15%, or 5.5 metres at 42 m, when 128 hypotheses are used.

That shows up in the cloud as points landing on discrete shells metres apart: a plume projecting away from the camera, typically below and behind the outcrop. It is a quantisation artefact rather than a matching failure, so no confidence, consistency or outlier filter can remove it — a measurement whose quantum is 5 m cannot be repaired downstream. The cure is more hypotheses, or a narrower depth range.

To keep far-end resolution under about 4%:

Depth range (far / near)Hypotheses needed
5:1128 — the default is fine
10:1256
20:1512

Your own ranges are in the log. Each view reports a line like MVS depth diag view 370: bracket 2.10643-42.4069 m over 128 bins (bin width 0.0158811 m near / 5.55205 m far). If that far bin width is a significant fraction of the detail you care about, raise the setting.

Time and memory both scale linearly with the count. On PLANE_SWEEP_GPU that is about 0.08 s per view at 128 and 0.33 s at 512, which is what makes the setting practical. On PLANE_SWEEP the same change takes a view from roughly 22 s to 88 s and shrinks the working image: the CPU backend holds its cost volumes in system RAM, so the memory budget trades image area for hypotheses, and 128 → 512 costs about three quarters of the grid. The GPU backend keeps those volumes in graphics memory and checks the graphics heap instead, so raising the count there costs time only.

Dense MVS confidence cutoff

  • Default: 0.75 (used by the PLANE_SWEEP backend).
  • What it does: per-pixel confidence threshold on the plane-sweep result, mapped as (NCC + 1) / 2, so 0.75 corresponds to NCC >= 0.5 (a well-matched, textured pixel). Pixels below the cutoff are discarded before fusion. Raise for sparser/cleaner output; lower for denser/noisier.
  • Example: 0.75 default; 0.6 (NCC >= 0.2) to densify a sparse cloud once depth is trustworthy; 0.85 for a cleaner cloud.

Dense MVS CasMVSNet confidence cutoff

  • Default: 0.3 (used only when backend is CASMVSNET).
  • What it does: the equivalent cutoff for the learned backend. Its confidence is a network probability on a different scale (it peaks much lower than the plane-sweep value), so this default is far below 0.75. Applying the plane-sweep cutoff to it would reject almost every point.
  • Example: 0.3 default; raise to 0.40.5 for a cleaner learned cloud.

Dense MVS consistent views

  • Default: 2.
  • What it does: a fused point is kept only if at least this many other reference views agree on its depth (so 2 means a 3-view consensus including the source view). Higher = cleaner/sparser; lower = denser/noisier.
  • Example: 2 default; 1 for maximum density (noisier); 3 for a clean cloud on high-overlap data.

Dense MVS depth tolerance

  • Default: 0.025 (= 2.5%).
  • What it does: the relative depth-agreement tolerance used by the cross-view consistency check above. Loosen it to keep more points; tighten it for a cleaner cloud.
  • Example: raise to 0.04 if the cloud is too sparse once per-view depth is good; drop to 0.015 for a tighter, cleaner surface.

Dense MVS sky filter

  • Default: on.
  • What it does: runs the bundled SkyWater SegFormer model over each undistorted dense-MVS image and removes the predicted sky before depth matching and fusion. Water and person predictions are not removed. VRGS keeps sky predictions with probability at least 0.5 only when they belong to a component connected to an image border; this avoids cutting isolated pale or blue areas out of the rock. It then expands the mask by 3 pixels at dense-MVS resolution to cover the mixed sky/foreground pixels that create white or blue silhouette halos.
  • Effect on reconstruction: masked pixels are excluded from plane-sweep and CasMVSNet matching. Any remaining masked depth and confidence values are also cleared before cross-view fusion, so sky cannot re-enter the dense cloud from a later stage.
  • Example: leave this on for outcrops, cliffs and landscapes photographed against the sky. Turn it off only when the sky itself must remain eligible for reconstruction or while comparing diagnostic runs. The fusion log reports a sky-masked count showing how many depth pixels the active sky filter removed.

VRGS ships FP16 and FP32 ONNX versions of the model. It tries FP16 on CUDA first, then FP32 on CUDA, and finally FP32 on the CPU, so the filter still works on a machine without a compatible NVIDIA GPU. A project-specific model with the same filename in the project's MODELS folder takes precedence over the installed copy. If no model can be loaded, or inference fails for one photograph, VRGS logs the reason and uses the older colour-and-confidence sky test for the affected view instead of failing the dense reconstruction.

Semantic probability maps are cached as PNG files in <workspace>/sky_masks/. Re-running Dense MVS normally reuses them; changing the source image, image geometry, model file or preprocessing version invalidates the corresponding cache entry automatically.

To check the segmentation visually, open the Photo Browser, right-click the image and choose Show Sky Mask (magenta). VRGS overlays the exact sky mask used by the most recent successful Dense MVS run on its undistorted MVS image. The command is disabled until a successful run has produced a preview for that photo. Mask-preview mode is read-only and temporarily hides tie-point, keypoint and ground-control-point markers because those markers use the original photo's distorted pixel coordinates. Choose the command again to return to the original photo.

Dense MVS fusion voxel

  • Setting key: dense_mvs_fuse_voxel_size
  • Default: 0 — size it from the imagery. Otherwise a voxel edge in metres.
  • What it does: fusion bins every surviving depth observation into a regular 3D grid and emits one point per occupied cell, at the confidence-weighted centroid of the observations in it. That is what stops two views of the same rock face producing a pair of slightly offset parallel surfaces. It also means the voxel size is the output resolution, and it is by far the largest influence on final point count, which scales as 1 / voxel².
  • Example: leave at 0. Set a larger value (say 0.02) when you want a lighter cloud for interpretation or export; there is little point setting it smaller than auto, because that subdivides a single pixel's footprint and only splits noise.

Auto uses one ground sample distancedepth / focal length in MVS pixels, the spacing of two adjacent depth-map pixels where they land on the rock. That averages across views, which is the noise reduction the voxel grid exists for, without averaging across surface, which is detail the sweep measured. It therefore follows the thing that actually determines resolution: how far away the subject is and what resolution you swept it at.

The fusion log names the choice and the reason:

Fusion voxel 0.00372 m (auto: one ground sample, i.e. depth / focal in mvs pixels) over 446948470 observations
A gridded-looking cloud means the voxel is too coarse

If the points look laid out on a regular lattice rather than scattered over the surface, the voxel is much larger than a ground sample. Each emitted point is then the mean of many observations spread through its cell, and the mean of many samples in a cube converges on the cube's centre — so every point sits near a grid node.

Divide voxels by the consistency>=2 count in the fusion funnel to see how many observations are being averaged into each point. A handful is healthy; hundreds means the voxel is far coarser than the imagery and detail is being averaged away. Setting 0 restores the automatic size.

Dense MVS min component size

  • Default: 200 points (0 disables).
  • What it does: after fusion and outlier removal, the cloud is split into spatially connected components and any isolated clump with fewer than this many points is deleted (the largest component is always kept, so the filter can never remove the model itself). This is the filter that kills the small floating patches of noise that can appear below the model over low-texture or repetitive ground: those patches are locally dense, so the statistical outlier stage cannot tell them apart from real surface — only their isolation gives them away.
  • Example: if a few larger islands survive, raise it (5001000); if a genuinely separate part of the scene (a detached block, a second outcrop face) is being removed, lower it or set 0. The fusion log reports how many islands and points were dropped.

Texturing

These control the photo-texture bake, not the reconstruction. All of them are diagnosable from the mesh attributes the bake writes.

Texture multi-photo blend

  • Setting key: texture_blend_enabled
  • Default: on.
  • What it does: blends every photograph that can see a texel, weighted by how well each sees that point, instead of taking one photo per triangle.
  • Example: leave on. Turn it off to see where the photo boundaries actually are — they become hard-edged, which is useful for diagnosing stripes.

Texture blend sharpness

  • Setting key: texture_blend_sharpness
  • Default: 4.0.
  • What it does: the seam-versus-sharpness dial for the blend. High confines mixing to a narrow band — and a narrow band between two photos that disagree shows as a stripe. Low blends broadly, which dissolves stripes but softens detail.
  • Example: try 1.0 if you see stripes.

Texture lens distortion

  • Setting key: texture_distortion
  • Options / default: Auto (default), On, Off.
  • What it does: whether to apply the solved lens distortion when sampling photographs. Auto measures it: each bake compares, over points two photos both see, how well those photos agree with the model applied and without, and uses the winner when the margin is decisive (>5%).
  • When to override: Off is right for any camera that de-warps in firmware — all DJI drones and most phones — where the JPEG is already corrected even though it is the original file. On is right for a genuinely raw lens with a well-conditioned solve. Both are obeyed exactly: the measurement still runs and is reported, but will not override you.
  • Getting it wrong displaces samples radially from each photo's centre, and the blend turns that into smeared bands.

Texture max UV stretch

  • Setting key: texture_max_uv_anisotropy
  • Default: 8.0. 0 turns the check off.
  • What it does: refuses to photo-texture a triangle whose UV mapping is stretched more than this ratio (1 = undistorted, over 4 is visibly smeared, over 16 is a streak). Rejected triangles take vertex colour instead.
  • What it fixes: ribbons of repeating "ladder" texture that survive every calibration change. The photograph is innocent — the unwrap is at fault: a sliver triangle squeezed to near-zero width in UV space replicates one row of source pixels along its whole length. Because the bake writes through the same UVs the renderer reads, the smear is baked in and model-locked, so it does not move with the camera. Usually a fraction of a percent of the mesh, but each one is long, so they dominate visually.

Texture max view angle (deg)

  • Setting key: texture_max_view_angle_deg
  • Default: 75.
  • What it does: rejects a photograph whose view of a surface is more grazing than this, in degrees from the surface normal. A source pixel seen at angle θ is smeared across 1/cos θ of surface, so 75° is a 4× smear and 84° is 10× — which is what the thin banded "comb" patches on steep faces and in recesses are.
  • Example: lower it if you see banding; raise it if you would rather have a smeared texture than a flat facet. Square-on views already win on score, so this only decides what happens where no good view exists. Colour the mesh by the Texture view angle (deg) attribute the bake writes to see which areas are affected.

Texture rescue occluded triangles

  • Setting key: texture_rescue_occluded
  • Default: off.
  • What it does: textures a triangle from its best-scoring photograph even when every photograph considers it occluded.
  • Example: turn it on when genuinely visible surface is being left untextured by an over-eager occlusion test; leave it off otherwise, since the rescued colour comes from a photo that may be looking at something in front.

Diagnostics & output

Write undistorted previews

  • Default: on.
  • What it does: saves an undistorted JPEG per registered view to <workspace>/undistorted/. Useful for quality control and for downstream tools that expect undistorted images.
  • Example: turn off to save disk space and a little time once you no longer need the previews.

Verbose logging

  • Default: off.
  • What it does: emits extra per-photo, per-pair, and per-cache diagnostic messages to the log.
  • Example: turn on while tuning parameters or troubleshooting a run; turn off for clean logs.

Ground control points

Ground control points (GCPs) are physical targets whose coordinates you surveyed. Marking them in the photographs lets VRGS fit the model to your survey instead of to consumer GPS — centimetres instead of metres — and, just as importantly, lets you measure how accurate the result actually is.

1. Import the survey

Right-click the workflow → Import Ground Control Points (CSV)….

The importer is deliberately tolerant of what survey instruments actually export: byte-order marks, Windows or Unix line endings, quoted fields, comment lines, comma or semicolon or tab or space delimiters (auto-detected), and decimal commas (547871,5). Pick the file and it will guess your column mapping from the header; confirm or correct it, and check the preview grid before importing.

Two things it will not do quietly:

  • Ambiguous numbers are refused, not guessed. 1.234,5 could be 1234.5 or 1.234, so it fails loudly rather than silently importing a wrong coordinate.
  • Every dropped row is reported, with the line number as it appears in your file, in the messages panel.

Coordinates are in — either the project's own coordinate system (the usual case: your surveyor delivered grid coordinates) or WGS84 longitude/latitude, which VRGS reprojects on import. Everything is stored in the project CRS, so mixed frames never reach the solver.

Imported points appear immediately in the 3D view and under Ground Control Points in the Data Tree — before you spend an hour marking them. This is the fastest way to catch a wrong CRS or a swapped easting/northing: if the control lands in the wrong county, stop now.

2. Mark them in the photographs

Open the Photo Browser, right-click → Mark Ground Control Point and choose a point from the list (it shows how many photos each one is marked in). Click the target in the image. Click again to correct the position; Shift+click removes a mark; Escape or Stop Marking leaves the mode.

GCP marks are drawn as magenta boxed crosses, deliberately unlike the tie-point circles — a tie point says "these clicks are the same thing", a GCP says "this click is that surveyed coordinate".

Each point needs two photos

A control point cannot be located in 3D until it is marked in at least two registered photographs. Three or more marked points are needed before they can drive the georeferencing.

3. Re-run

With 3+ usable control points, GCPs take over the georeferencing from GPS, which drops back to a soft constraint on cameras far from any control. The log says which drove the fit — it never happens silently.

Control points vs check points

Turn Use as control off on a point (in its properties) to make it a check point: it is measured but never fitted.

This matters more than it sounds. A control point's residual only tells you how well the fit absorbed it — fit enough points and you can drive that number to almost zero while the model is still wrong elsewhere. A check point is held back from the fit, so its residual is the only honest estimate of the model's real accuracy. The quality report RMSs the two separately and tells you when you have no check points at all.

Spread your control

Control pegged out along a single line — down one road or one bench — cannot fix rotation about that line. VRGS refuses such a fit rather than emit a model rolled by whatever the noise favoured, and says so in the log. Spread the points across the site, and in height where you can.

Using control elsewhere in the project

Ground control points are ordinary project objects, not an SfM-only feature:

  • Use Project Ground Control Points adopts control that already exists in the project (surveyed for something else, or imported before this workflow existed) into the workflow.
  • Seed Mesh Tie Points from Ground Control turns measured control into the mesh's own tie points, so the mesh ribbon's Corrections → GCP can pull an existing mesh onto the survey. Most useful when a model was GPS-aligned and the GCPs were check points: the residuals are real, and this is the one-click way to consume them.

Scale bars

A photograph set can never recover its own scale: a model of a quarry and a model of a sandpit look identical to the solver. GPS and ground control both give scale as a side effect of giving position. When you have neither — hand-held photos of an outcrop with no GPS and no survey — a single measured distance is enough.

Right-click the workflow → Scale Bars (Known Distances)…. Pick the two tie points at either end of something you measured (a ranging rod, a tape, a known feature), type the distance in metres, and Add. Re-run the reconstruction.

Both ends must be tie points linked across at least two photographs, so mark them first in the Photo Browser (Ctrl+click a point to start a link, then click the same feature in another photo). Only links that qualify are offered.

Scale is not georeferencing

A scale bar gives the model real size. It does not give it a position or an orientation on the Earth. A scale-bar-only model is metric — you can measure distances on it — but it is not georeferenced and must not be measured against other project data. The quality report says this explicitly.

If GPS or ground control already scaled the model, scale bars are not applied — re-scaling would pull the model off the coordinates that placed it. They are still measured, which makes them a free independent check of the GPS/GCP scale.

Several bars are combined by the median, so one mistyped distance (a 1 m rod entered as 10) cannot drag the whole model's scale.


Quality report

Right-click the workflow → Show Quality Report. It goes to the messages panel and to quality_report.txt in the workflow's workspace folder, so it can be handed over with a deliverable.

It answers, in one place:

  • Registration — how many photos registered, and which ones did not.
  • Reprojection error — RMS plus median, 90th and 99th percentiles. If the 99th exceeds 5× the median the report says so: a handful of outliers is dominating the RMS, the bulk of the model is better than that number suggests, and the fix is to find the outliers rather than to distrust the whole thing.
  • Georeferencing — what placed the model (GCPs, GPS, or nothing) and how well it fitted.
  • Ground control accuracy — per-point dX/dY/dZ and 3D error, with control and check points RMS'd separately. A point that was never measured is reported as such, not as a zero error.
  • Scale bars — expected vs. actual, and whether they were applied or are acting as a check.
  • Worst photographs — the ten with the highest reprojection error, which is usually where a bad solve is hiding.

Inspecting the sparse cloud

The sparse cloud carries two attributes you can colour by, filter on, and histogram like any other VRGS attribute:

  • Observations — how many cameras actually see each point. Thin coverage is where a reconstruction is weakest. Colour by this before launching a dense MVS run that will spend hours on geometry the sparse solve never really pinned down.
  • Reprojection error — the per-point residual in pixels. A healthy RMS can hide a bad tail; filtering to the worst points shows you exactly where the model is unhappy, rather than just telling you that it is.

Building a mesh

Once you have a dense cloud (<name> dense), turn it into a triangulated surface. Both methods run on a background thread, copy the cloud's per-vertex colour onto the mesh, and add <name> mesh to the project. Right-click the workflow and pick one:

Needs a dense cloud

Create Mesh works on the dense MVS cloud, so enable Dense MVS (learned) and run a reconstruction first. If the dense cloud is missing or still loading, the action tells you so.

Create Mesh (Poisson)

Poisson reconstruction fits a single watertight surface through the points — it fills small gaps and gives clean, closed geometry. Best default for outcrops.

Octree depth

  • Default: 0 = auto — matched to the measured point spacing.
  • What it does: the resolution budget of the whole reconstruction. Poisson builds the surface on a grid of 2^depth cells across the model, and nothing smaller than one cell can appear in the mesh, however dense the cloud is. At depth 8 a 300 m outcrop gets ~1.3 m cells — a 5-million-point cloud with centimetre spacing comes out looking melted, which is the classic "dense cloud looks great, mesh looks poor" complaint. Auto sizes the cell to ~2× the cloud's point spacing (capped at depth 13), and the log prints the chosen depth, cell size and spacing so the budget is visible. Each +1 of depth halves the cell and roughly quadruples time and memory.
  • Example: leave at 0. Set an explicit 910 only when you want a lighter, generalised mesh — for draping or as a fast preview.

Samples per node

  • Default: 4.
  • What it does: how much noise averaging Poisson applies: how many input points fall in a leaf cell before it is fully trusted. 1 interpolates every point exactly — right for clean laser scans, wrong for photogrammetry, whose dense clouds carry centimetre-scale noise that would be reconstructed as surface bumps at high depth. 48 averages that noise away while the octree depth carries the real detail.
  • Example: 4 for drone photogrammetry; 1 for TLS/LiDAR input; raise toward 8 if a noisy cloud still meshes bumpy.

Trim factor (× point spacing)

  • Default: 6. 0 keeps the full watertight surface.
  • What it does: Poisson extrapolates a "bubble" beyond the real data; this trims away triangles whose vertices sit farther than factor × the local point spacing from any input point. Lower trims more aggressively (tighter to the data, but can punch holes); higher keeps more surface; 0 disables trimming.
  • Example: 6 removes the balloon-like overhang around the edges while keeping the surface intact. Lower to 34 if the bubble is still obvious; set 0 if you specifically want a closed watertight mesh.

Create Mesh (Greedy Projection)

Greedy projection triangulates the points directly — it follows the cloud closely and is fast, but it does not fill gaps (sparse areas stay holed). Use it when you want the raw measured surface rather than an interpolated one.

FieldDefaultWhat it does
Search radius0.5Maximum edge length / connection distance, in model units. The single most important value — set it to a few times the point spacing.
Max edge multiplier (μ)2.5Caps an edge at μ × the local point distance, so dense areas use short edges and sparse areas longer ones.
Max nearest neighbours100How many neighbours each point may connect to. Higher closes more triangles (slower).
Max surface angle45°Don't connect points across a normal change larger than this — preserves sharp edges.
Min triangle angle10°Lower bound on triangle angles (avoids slivers).
Max triangle angle120°Upper bound on triangle angles.
Poisson vs Greedy

Use Poisson for a clean, closed surface you'll texture or interpret. Use Greedy for a fast, faithful triangulation of exactly the measured points when you don't want gaps filled in.


Texturing the mesh

Texture Mesh from Photos… projects the solved photographs back onto the mesh and bakes a photographic texture — far more detail than the per-vertex colour the mesh inherits from the cloud. It needs a mesh (run Create Mesh first) and the workflow's photos. The bake runs on a background thread and the texture is saved with the project.

Let VRGS choose the budget

You do not have to answer any of these questions. The first prompt shows a budget worked out from your own data and you can simply accept it:

Recommended: 2 page(s) of 8192 x 8192 (536 MB).

The photographs resolve about 0.043 units per pixel at this range, so the survey
holds roughly that much real detail — more texture than this stores blur, not
information.
That gives ~336 texels per triangle over 1639133 triangles.

YES - use the recommendation
NO - set the resolution manually (advanced)
CANCEL - abort

Answer Yes and the bake starts. Everything below this point is the advanced path, reached with No.

The recommendation is arithmetic, not a house default. A photograph resolves the ground at range ÷ focal length in pixels, so the whole survey contains about mesh surface area ÷ gsd² genuinely distinct texels — and asking for more texture than that stores interpolation, at full cost in memory and bake time. VRGS sums the real triangle areas, takes the median ground sample distance across the posed photographs (a median so one distant or badly-posed camera cannot skew it), then picks the smallest page size that covers the requirement in four pages or fewer.

If the mesh is far denser than the photographs can resolve, the prompt says so and suggests decimating first rather than silently baking a starved texture.

This is the setting people get most wrong

Texture resolution is easy to over-buy. A survey whose imagery supports four pages of real detail will happily accept a request for 128 — and spend gigabytes of disk and hours of bake time storing blur. The recommendation exists so that the number of pages is tied to what the photographs actually contain rather than to a guess.

Resolution mode (advanced)

If you answered No, the next prompt picks the mode:

  • Target pixel size (answer Yes) — you specify the real-world size each texel should cover (the ground sample distance). VRGS packs the atlas at exactly that density: a target of 0.005 m means each texel is 5 mm on the outcrop (200 texels per metre). This is the most intuitive control — ask for the detail you need and let the page count follow.
  • Page budget (answer No) — you instead fix the number of texture pages and VRGS spreads the available texels evenly over the surface. Use this when you want a hard limit on texture memory rather than a guaranteed detail.

Page size (advanced)

  • Default: 4096 texels per side.

  • Choices: 4096, 8192 or 16384. Anything you type is rounded to a power of two and clamped to 16384, with a note in the messages panel if it was.

  • What it does: the dimensions of each atlas page. Larger pages hold more detail per page (so fewer pages) but cost size × size × 4 bytes each:

    Page sizeMemory per page
    409664 MB
    8192256 MB
    163841 GB
  • Why 16384 is the ceiling: graphics hardware will not create a 2D texture larger than this — 16384 is the limit on AMD and Intel GPUs, and the Vulkan standard only guarantees 4096. Some high-end NVIDIA cards manage 32768, but a page that size is 4 GB and sits on the single-allocation limit most drivers report, so it would work on one machine and fail on the next.

  • Example: 4096 suits most meshes; 8192 where the recommendation asks for more than about four pages at 4096.

Bigger pages are not automatically cheaper

Nine pages at 4096 is 576 MB; one page at 16384 is 1 GB for the same texel count. Fewer, larger pages pack slightly better and cost fewer draw calls, but they are not the smaller option. The recommendation weighs this for you.

Target pixel size (pixel-size mode)

  • Default: 0.01 m/texel.
  • What it does: the real-world size of one texel. Smaller = sharper texture but more pages and more memory. There is a safety cap of 8 pages — if your pixel size would need more, VRGS coarsens the density automatically and notes it in the log rather than exhausting memory.
  • Example: 0.005 for close-range outcrop detail; 0.02 for a quick, light texture of a large area.

Number of pages (page-budget mode)

  • Default: calculated from the mesh and pre-filled, not a fixed number. VRGS aims for about 64 texels per triangle, allowing for the ~30% of each page that goes on the padding every atlas chart needs:

    pages = triangles × 64 ÷ (page size² × 0.7)
  • What it does: how many pages to fill. More pages = finer texture over the same surface. The prompt shows the suggestion, your triangle count and the cost per page.

  • Example: accept the suggestion unless you have a reason; drop to 1 for a light preview texture.

How a triangle is textured

VRGS ranks the photographs that can see each triangle — most head-on and highest-resolution wins — rejecting back-facing and very oblique views and checking occlusion, so a photo can never texture a surface hidden behind the mesh. Triangles no photo can see fall back to the mesh's per-vertex colour, so there are no black holes.

The occlusion test is a ray cast, and on a rough surface it is deliberately forgiving: its tolerance scales with the size of the triangle being tested, so a ray that clips a neighbouring bump a few centimetres early is not mistaken for a real obstruction. Without that, whether a triangle textured at all came down to whether its ray happened to graze a nearby lump, which showed up as flat facets scattered through otherwise good texture.

If a triangle loses every candidate to the occlusion test, VRGS textures it from its best-scoring photograph anyway rather than leaving it flat. Photo content that may be slightly bled from an occluder reads as texture; a flat facet reads as a hole. Only triangles that no photograph framed at all fall back to vertex colour. The log separates the two, because they need opposite fixes:

[INFO] Texture bake: occlusion outcome: 21313 triangle(s) lost every candidate to
the occlusion test, of which 21313 were textured anyway from their
best-scoring photo instead of going flat; 3017 were framed by NO photo at
all and are the only genuine vertex-colour fills

A large second number means a coverage problem — those parts of the outcrop were never photographed from a usable angle.

Every texel is then blended from the photos that see it, weighted by how well each one sees that texel — how square-on, how close, and how far from the edge of its frame. Because those weights depend on the point in space rather than on which triangle it belongs to, the mix changes smoothly across the whole surface, and the exposure/white-balance seams you would otherwise get between neighbouring triangles that chose different photos are dissolved.

Patches showing flat colour instead of photographic detail are triangles no photo could see (outside every image, or framed only at an impossibly grazing angle).

Lens distortion

Texture apply lens distortion (SfM workflow properties, default on) controls whether the solved lens model is applied when sampling a photograph.

Leave it off for any camera that de-warps in firmware — all DJI drones, most phones — because the image is already rectilinear even though it is the original file. Turn it on only for a genuinely raw lens with a solved model.

You do not have to work this out. Every bake measures it. Over roughly 4,000 triangles that two photographs both see, VRGS compares the colour each photo reports for the same point in space, with the model applied and without. The correct setting is the one where overlapping photographs agree:

[INFO] Texture bake: distortion self-check over 3866 twice-seen triangles --
cross-photo colour disagreement with distortion ON 0.0779, OFF 0.0779
=> OFF agrees better by 0%

If the setting you used is more than 5% worse than the alternative, VRGS says so in the messages panel and tells you which way to change it.

Once the camera model is right the two readings become identical, as above, and the setting stops mattering at all — which is the real sign that the intrinsics are honest rather than that the toggle happens to be in the lucky position.

Getting this wrong looks like banding, not blur

Applying a distortion model that does not belong to the image displaces every sample radially from the centre of that photograph — nothing at the middle, a few texels at the corners. Each texel is blended from several photographs that see it at different radii, so they disagree by different amounts, and the blend turns that into smeared, evenly spaced bands across the model. It looks like a texturing bug and is actually a camera-model one.

Texture resolution attributes

Each bake writes two per-triangle attribute layers, so you can see how well resolved any part of the model is instead of guessing:

  • Texture GSD (units/texel) — the real ground sample distance, in project units. It is the worse of two independent limits: what the atlas can store (set by page size and count) and what the imagery carries (range ÷ focal length, set by flying height and lens).
  • Texture resolution 0-1 — the same information normalised, 1 = finest. Use this one on an unscaled mesh, where an absolute distance is meaningless. It is mapped logarithmically between the 5th and 95th percentiles: logarithmic because GSD spans orders of magnitude, and percentile-clipped so a handful of sliver triangles cannot flatten everything else onto one colour.

Colour the mesh by either layer from the Attributes node. The log also reports which limit dominates:

[INFO] Texture bake: GSD per triangle (project units/texel) p5 0.021, median
0.043, p95 0.11 -- 62% of triangles are SOURCE-limited (photo resolution),
the rest TEXTURE-limited (add pages or decimate)

That split is the actionable part. Source-limited means the photographs are the constraint — more pages will not help, you would need to fly lower or use a longer lens. Texture-limited means the atlas is the constraint, and more pages (or a coarser mesh) will sharpen it.

If the texture looks soft

Blending is weighted heavily toward the single best view, so it should stay sharp. If a bake looks blurred or ghosted, the usual cause is a small pose error letting two views disagree. Textures are baked with blending on, up to 3 photos per texel, at a sharpness of 4 — higher biases harder toward the one best photo, lower spreads the blend wider.

How long the bake takes, and how to stop it

A single progress bar tracks the whole bake and names the stage it is in: UV unwrap, building occlusion tree, decoding photos, scoring photos per triangle, rasterizing, finalizing. The ✕ on the progress bar cancels the bake, including part-way through the unwrap; a cancelled bake leaves the mesh's existing texture untouched.

UV unwrap is the slow stage. VRGS splits the mesh into chunks of at most 100,000 triangles and unwraps them in parallel across your CPU cores — the log records the split:

[INFO] Texture bake: 6279507 triangles in 63 unwrap chunk(s)

Without that split a large mesh is unwrapped as a single unit on one core, and the cost grows far faster than the triangle count — a multi-million triangle mesh would appear to hang for hours. Photo decoding, per-triangle photo scoring and rasterisation all run in parallel too.

Decimate before texturing a very dense mesh

The texture, not the geometry, carries the visible detail — so a mesh far denser than the atlas can resolve costs time and memory for nothing. At the end of a bake the log reports the budget:

[INFO] Texture bake: 21.4 texels per atlased triangle

Below about 16 texels per triangle VRGS also warns in the messages panel. A mesh of a few hundred thousand triangles textured at the same page budget gives a sharper result than a multi-million triangle one, and bakes far faster. Decimate the mesh first, then texture.


Reading the run log

Ask Athos to read it for you

The Athos AI assistant can interpret all of this for you. Right-click the SfM workflow in the project tree and choose Ask Athos about this Model, or ask something like "why does my dense cloud have holes?" in the chat. Athos reads the same settings, run statistics, and per-view diagnostics described below and answers with your actual numbers — including which photos failed, whether gaps are a coverage or a pose problem, and which parameters to change. It can also apply a change or start a re-run for you, always after showing you what it will do.

Two summary lines tell you most of what you need to tune a run.

Sparse summary

SFM done: 20/21 views, 22196 points (reproj px: med=0.34, p90=0.81, p99=2.10, rms=0.55)
  • 20/21 views — registered vs. total photos. If many photos fail to register, the cause is usually too little overlap, too few features, or too strict matching thresholds.
  • 22196 points — size of the sparse cloud.
  • reproj px — per-point reprojection error percentiles. A sub-pixel median is healthy. A small median with a large p99 means a few outliers; a large median everywhere means a weak solve.

Dense fusion funnel

Fusion: 5836800 -> 4443855 depth>0 -> 3841509 conf>=0.75 -> 2241200 consistency>=2 -> 12844 conflict-cut -> 31840 sky-masked -> 706202 voxels -> 698061 after SOR -> 697620 after component filter(min=200 pts: dropped 28 islands / 441 pts)

Each arrow shows how many pixels/points survive a stage, so you can see where points are lost and which knob to turn:

  • big drop at conf>= → lower Dense MVS confidence cutoff;
  • big drop at consistency>= → loosen Dense MVS depth tolerance or lower Dense MVS consistent views;
  • conflict-cut counts consistency survivors that were vetoed because more neighbouring views measured a clearly different surface along the same ray than agreed with the point — the signature of a correlated mismatch buried below (or floating in front of) the real surface. A very large number here means the per-view depth maps disagree badly: check overlap and texture before loosening anything;
  • with the sky filter on, an extra sky-masked term shows how many semantic sky depth pixels were removed (or colour/confidence fallback pixels if the model was unavailable);
  • the final component filter term reports the isolated islands removed — see Dense MVS min component size.
Tune from the funnel, not by guesswork

Run once with defaults, read the funnel, then change the one stage that is cutting too much (or too little). Re-run and compare.

Confidence profile

Fusion confidence profile over 67332814 px with depth (cutoff 0.75): ... | 48.3%
below cutoff, of which 12.7% of all depth px would be admitted by relaxing the
cutoff to 0.6

The funnel tells you the confidence gate cut half your pixels; this tells you whether that was worth doing. The histogram is collected before the gate, so the buckets below the cutoff are populated, and the closing figure answers the only question that matters in practice: how much would come back if you lowered the cutoff.

Mass piled just under the cutoff means real surface is being rejected on close calls, and lowering Dense MVS confidence cutoff will recover geometry. Mass piled far below it is noise, and lowering the cutoff will only admit rubbish.

Per-reference yield

Fusion per-ref yield: median 119255 pts/ref, range 0-280048; 44 of 252 ref(s)
yielded <=10% of median (partners p=nominal/e=effective) (weakest views:
115=0(p8/e0) 144=0(p6/e1) 146=0(p7/e0) ...)

This separates a hole from thinning. If every view sits near the median, the cloud is uniformly sparse and the funnel tells you which gate to relax. If a handful of views yielded almost nothing, the loss is concentrated in whatever those cameras were looking at — and since the numbers are view IDs, you can find that stretch of the flight path and check its overlap, lighting and angle.

Each weak view carries (p<nominal>/e<effective>), which splits a near-zero yield into its two possible causes:

  • p (nominal) — how many overlapping views the matcher assigned to this reference.
  • e (effective) — how many of those actually confirmed its depth at one or more pixels.

p1/e0 or p0/e0 is a coverage gap — nothing overlaps that viewpoint, and the only fix is more photographs from that angle. p8/e0 is a pose fault — eight views should see that surface and none agree, so the camera (or its neighbours) was placed wrong in the reconstruction, and no amount of extra imagery or dense-setting tuning will fill it until the pose is corrected. This matters because a mis-placed camera can still have a low reprojection error, so the pose looks fine by the usual measure; the effective-partner count is the check that catches it.

Per-view depth diagnostic

One line per reference view describes how the plane sweep behaved:

MVS depth diag view 37: bracket 34.2-106.8 m over 64 bins (bin width 0.371 m
near / 3.52 m far); winners 41% edge-far, 2.1% edge-near, 3.4% flat-cost, 0.6%
non-parabolic, 52.9% refined (18.9% by <0.05 bin) => 66.0% on a bin centre
<-- RANGE-LIMITED: surface outside the depth bracket; more bins will NOT help

The bracket is the depth range searched for that view, derived from the sparse points it can see. Depth hypotheses are spaced evenly in inverse depth, so the metric step grows with the square of distance — hence a bin 0.37 m thick near the camera and 3.5 m thick at the far end of the same view. When the far figure exceeds the Dense MVS depth tolerance applied at that range, distant surface in that view cannot pass the cross-view consistency check however well it was photographed.

The percentages say where each best match landed, and the trailing verdict names the dominant cause:

  • RANGE-LIMITED — most matches sat against a wall of the bracket, so the true surface lies outside the range searched. Usually the view is dominated by sky or distant background, or the sparse cloud within it is too thin to bracket it properly. Better sparse coverage helps; the dense settings cannot.
  • TEXTURE-LIMITED — the correlation curve is flat, so no depth is distinguishable from any other. The surface carries no usable texture at the scale the matcher works at. Nothing in the settings panel fixes this; it needs closer or better-lit photographs.
  • OVER-QUANTISED — the matches landed cleanly inside the volume, but the far bin is coarser than the depth tolerance that has to confirm it, so distant surface in this view cannot be verified by any other view. This is the one a finer depth search improves, and it is invisible to the other counters — a view can refine every winner cleanly and still emit nothing.
  • TERRACED — depths are quantised onto discrete shells. Also answered by a finer depth search.
Two of these are not settings problems

Read this line before spending a run turning knobs. If your views report RANGE-LIMITED or TEXTURE-LIMITED, lowering the confidence cutoff or loosening the depth tolerance will add noise without filling the gaps, because the depth was never measured in the first place. OVER-QUANTISED and TERRACED are the opposite — the depth was measured but sampled too coarsely, and a finer sweep (handled internally) recovers it.

Why the dense cloud has gaps where the sparse cloud has points

This is expected, not a fault — the two clouds are not built to comparable standards:

  • a sparse point needs one distinctive feature matched in two photographs, at full image resolution;
  • a dense point needs an entire correlation patch to match in three or more views, all agreeing on depth within the tolerance, at the reduced resolution the dense stage works at.

So the sparse cloud thins gracefully as conditions worsen while the dense cloud tends toward all-or-nothing: full density where matching succeeds, empty where it fails. Shadowed recesses, smooth or uniform rock, and surfaces seen only at a grazing angle are the usual empty areas. The last one catches people out — a well-textured face photographed obliquely correlates just as badly as a featureless one photographed straight on, because the correlation patch is foreshortened differently in each view.

The dense stage also runs at a reduced image resolution, which puts a floor under point spacing no matter how good the photographs are. If the dense cloud is uniformly coarser than you expected rather than patchy, that floor is what you are seeing, and no setting will move it.


Worked examples

Drone survey with GPS (nadir)

SIFT, Alignment image scale Half, Share intrinsics per camera on, Use GPS prior on, GPS sigma XY 25, GPS pair max distance set to a few times the photo spacing. Enable Dense MVS with the PLANE_SWEEP backend.

Convergent outcrop (oblique, high overlap)

SIFT, Max features 8000+, Use GPS prior on if the photos are tagged. Enable Dense MVS and leave the default Sky filter on when the outcrop is shot against the sky.

Densest possible dense cloud

Dense MVS confidence cutoff 0.6, Dense MVS consistent views 1, Dense MVS depth tolerance 0.04. (Expect more noise—clean up afterwards with point-cloud filters.)

Cleanest dense cloud

Dense MVS confidence cutoff 0.85, Dense MVS consistent views 3, Dense MVS depth tolerance 0.015.

Fast preview

ORB, Alignment image scale Quarter, Max features 4000, BA max iterations 40, Dense MVS off (or on with neighbor views 3).

Textured outcrop mesh

Run the reconstruction with Dense MVS on, then Create Mesh (Poisson) with Octree depth 9, Trim factor 6. Finally Texture Mesh from Photos and accept the recommended budget — it is derived from what your photographs can actually resolve, which is the ceiling on useful detail whatever you ask for.

Only override it if you have a specific reason: Target pixel size mode at 0.005 m with Page size 4096 forces a fixed 5 mm texel, which is worth doing when you need a guaranteed density for measurement rather than the best texture the imagery supports.


Troubleshooting

SymptomLikely cause / fix
Few photos register (5/20 views)Too little overlap, too few features, or matching too strict. Raise Max features and/or Alignment image scale; lower Lowe ratio test slightly; only lower Min geometric inliers as a last resort. Check the photos aren't blurry.
Dense cloud nearly empty / very sparseCheck the per-view depth diagnostic first — if it reports RANGE-LIMITED or TEXTURE-LIMITED, the depth was never measured and relaxing the gates only adds noise. Otherwise read the fusion funnel and relax the stage that cuts most: lower confidence cutoff, loosen depth tolerance, or lower consistent views.
Dense cloud has gaps where the sparse cloud has pointsNormal, and explained here — dense matching needs three views and a whole patch to correlate, sparse needs two views and one feature. Use the per-reference yield line to tell a localised hole from uniform thinning, and the confidence profile to see whether lowering the cutoff would actually recover anything.
A few specific views contribute no dense pointsRead their (p/e) partner counts in the per-reference yield line. p0/e0 or p1/e0 is a coverage gap (add photos from that angle); p8/e0 means overlapping views were found but disagree on the depth, which points at a bad camera pose rather than the imagery — re-check that stretch of the flight path in the sparse solution.
seeded from a 50 mm ASSUMPTION warningThose photographs carry no EXIF FocalLengthIn35mmFilm, so the solve started from a guess about 2.5× too long for typical drone or phone imagery. See where the starting focal length comes from; attach a camera calibration if registration is poor.
Dense cloud is noisyTighten the same three: raise confidence cutoff, raise consistent views, tighten depth tolerance.
Points look laid out on a regular gridThe Dense MVS fusion voxel is much coarser than a ground sample, so each point is the mean of many observations and that mean lands near its cell centre. Set the voxel to 0 for the automatic size. Divide voxels by consistency>=2 in the fusion funnel: a handful of observations per point is healthy, hundreds is not.
Points project away from the camera below the outcropDepth quantisation on a wide depth range, not a matching failure. Check the bin width ... near / ... far figures in the per-view depth diagnostic and raise Dense MVS depth hypotheses — see that setting for the resolution table.
Small floating patches of noise below the modelRemoved by default by the component filter; raise Dense MVS min component size if some survive. If a genuinely separate structure disappears instead, lower it or set 0.
Blue/white halo around the outcropLeave the Dense MVS sky filter on and re-run Dense MVS. Check the log for SkyWater semantic masks enabled and a non-zero sky-masked count. If VRGS reports that the model is unavailable, verify that the installed VRGS_ml folder contains skywater_segformer_b2_fp16.onnx and skywater_segformer_b2_fp32.onnx; the colour/confidence fallback is less reliable around white cloud and pale sky.
Sky filter is slow on the first runWithout compatible CUDA, VRGS deliberately falls back to the FP32 CPU model. Let the first run finish: masks are cached under <workspace>/sky_masks/, so unchanged photographs are substantially cheaper on later dense runs.
Sky filter removes part of the subjectThe semantic filter removes only border-connected sky, but an object touching the image edge can still be misclassified. Turn the filter off for that workflow and compare the result; avoid lowering dense confidence or consistency thresholds, because those do not correct a segmentation error.
GPS-tagged run looks worse than expectedCheck the EXIF GPS is valid; raise GPS sigma XY/Z (trust GPS less), or turn Use GPS prior off to reconstruct in a purely visual frame.
CASMVSNET backend does nothingIt needs casmvsnet.onnx in the project MODELS folder and a CUDA GPU; otherwise use PLANE_SWEEP.
"No mesh to texture"Run Create Mesh on the workflow first — texturing needs a <name> mesh.
Texture looks coarse / blurryColour the mesh by Texture resolution 0-1 and check the log's GSD line first. If most triangles are source-limited the photographs are the ceiling and more pages will not help. If texture-limited, use more pages or a larger Page size — or accept the recommended budget, which sizes this for you. Also check for a "density coarsened" note; you may have hit the 8-page safety cap.
Mesh has holesPoisson Trim factor too aggressive — raise it or set 0 for a watertight surface; or you used Greedy, which leaves gaps in sparse areas (switch to Poisson).
Patches show flat colour, not photo detailThose triangles were seen by no photograph (occluded or outside every image), so they fall back to per-vertex colour. Add photos covering that area or re-shoot it.
Faint seams across the textureEvery texel is blended from up to three photographs weighted by how well each sees that point, so exposure seams should dissolve. If they persist, lower Texture blend sharpness (4 → 1) to spread the blend wider.
Smeared or ghosted bands across the textureA lens-distortion mismatch, not a texturing fault. Check the bake's distortion self-check line — if it says the other setting agrees better, change Texture apply lens distortion and re-bake. The lasting fix is to re-run the reconstruction so the camera model is solved correctly for a firmware-corrected camera.
Flat single-colour triangles among good textureCheck the occlusion outcome line. Triangles "framed by NO photo at all" are a coverage gap — that part of the outcrop was never shot from a usable angle.
Texture bake sits a long time in UV unwrapNormal on a large mesh — it is the slow stage. Check the log for the chunk split (N unwrap chunk(s)); the bar advances as chunks finish. Cancel with the ✕ on the progress bar if you would rather decimate the mesh first.
Log warns about texels per triangleThe mesh is much denser than the atlas can resolve. Decimate the mesh before texturing, or raise the Page size / page count.