Paper · 2021-04-01 · cs.CV, cs.RO
NeuralRecon: Real-Time Coherent 3D Reconstruction from Monocular Video
NeuralRecon predicts a sparse TSDF volume for each fragment of 9 key frames in one network pass. It unprojects image features into 3D, decodes them coarse-to-fine with sparse 3D convolutions, and fuses them with earlier fragments via a ConvGRU. Author-reported: F-score 0.562 on ScanNet (Atlas split) at 30 ms per key frame on an RTX 2080 Ti.
Multi-view reconstructionNeural implicit surfacesMonocular depth estimationRecurrent neural network (RNN)3d-reconstructionmonocular-videotsdfreal-time
arXivPDFCodeMarkdown14 min read
AI-compiled by claude-haiku-5-5 on 2026-10-31, not yet reviewed by a human. Every claim cites its source; check it when a detail matters.
In plain words
NeuralRecon builds a 3D model of a room from an ordinary video taken with a camera that moves through it. The camera's path is assumed to be known already, from a separate tracking system. The video is cut into short groups of consecutive key frames, called fragments.
Older real-time methods work frame by frame. They guess a depth map for each frame and then merge those guesses. Because each guess is made alone, the scale can drift between frames and the same wall gets estimated several times. NeuralRecon instead predicts the 3D surface for a whole fragment at once, as a grid of small cubes that store the distance to the surface. A learned memory then carries what was seen in earlier fragments into the next one, so the pieces fit together.
The point of the design is speed without giving up coherence. The authors report that this runs at real-time rates and is more accurate than the depth-merging methods they compare with.
TL;DR
NeuralRecon is a real-time monocular reconstruction network that predicts a sparse signed-distance (TSDF) volume per fragment of 9 key frames, rather than per-frame depth maps. Image features are back-projected into a 3D grid, decoded coarse-to-fine with sparse 3D convolutions, and fused with earlier fragments by a 3D ConvGRU. Author-reported: 30 ms per key frame (33 key frames per second) on an RTX 2080 Ti, and F-score 0.562 on ScanNet (Atlas split, single-layer mesh) tab. 1. The official code is Apache-2.0, and its training configs differ from the paper in several details (see Paper vs code).
Problem
Real-time monocular reconstruction usually estimates a depth map on each key frame and fuses the maps into a TSDF volume afterwards §1. The authors identify two drawbacks. First, each key-frame depth is estimated from scratch, so scale can change between views and the surface can come out layered or scattered §1. Second, the same surface is estimated repeatedly in overlapping key frames, which wastes computation §1. The offline volumetric method Atlas is the main accuracy comparison, and the authors' method is about 10× faster than it §1 §4.2. The method assumes camera poses come from an external SLAM system §3.
Method
Overview. Key frames are chosen from the incoming stream. Each window of N key frames is one local fragment, and the network predicts the TSDF of that fragment directly in a 3D volume §3.1 §3.2. The fragment is then merged into a global volume and meshed with Marching Cubes at every time step §3.2.
Key-frame selection and fragments §3.1. A frame becomes a key frame when its relative translation and relative rotation both exceed thresholds. A window of N key frames forms a fragment. A cubic fragment bounding volume (FBV) encloses the view frusta of its key frames, with each frustum cut at depth d_max §3.1. Only voxels in the FBV are handled during reconstruction §3.1.
Feature volume §3.2. Each key frame goes through an image backbone that yields multi-level features. These are back-projected along rays into the fragment volume. Each voxel gets the average of the features from the views that see it, weighted by visibility §3.2. The unprojection is illustrated in fig. 3.
Coarse-to-fine decoding §3.2. The network works in 3 levels §3.2 §3.3. At each level it applies sparse 3D convolutions to the feature volume. Two MLP heads predict an SDF value and an occupancy score per voxel §3.2. Voxels whose occupancy is below θ = 0.5 are treated as empty and removed §3.2 §3.3. The survivors are upsampled by 2x with nearest-neighbour interpolation and concatenated with the next level's features §3.2 §3.3.
GRU fusion §3.2 GRU Fusion. The fragment's geometric features G_t are combined with a hidden state H_{t-1} that is read from the global hidden state inside the FBV. A ConvGRU, written with sparse convolutions, produces the updated hidden state H_t. The four GRU equations in this section are unnumbered in the paper. The updated hidden state is also written back to the global hidden state. The paper calls this a learned replacement for running-average TSDF fusion §3.2.
Integration §3.2. The last level's sparse TSDF is transformed to global coordinates and replaces the corresponding global voxels, because the fusion already happened inside the GRU §3.2. Marching Cubes is then run on the global volume at each time step §3.2.
Supervision §3.2 Supervision. Occupancy is trained with binary cross-entropy against ground truth. SDF is trained with an L1 loss on log-transformed values. Both losses are applied at every coarse-to-fine level §3.2.
# Fragment reconstruction, one window of N key frames [§3] [§3.2]
# Input: images (N, 3, H, W); poses (N, 4, 4); global hidden state per level
# Output: sparse TSDF of the fragment, written into the global volume
def reconstruct_fragment(images, poses, global_state):
# Extract 2D feature maps for every key frame [§3.3]
feats = [backbone(image) for image in images]
# Back-project features, average over the visible views [§3.2]
volume, visible = unproject_average(feats, poses) # (V, C), (V,)
# Start from the coarsest grid over the fragment bounding volume [§3.2]
coords = dense_grid(fragment_dims, interval=4) # 16 cm (inferred)
feat = gather(volume, coords)
for level in [0, 1, 2]:
# Sparse 3D convolution on the unprojected features [§3.2]
geo = sparse_conv_level(feat, coords, level)
# Fuse with the hidden state from earlier fragments [§3.2 GRU]
hidden = gru_fuse(geo, global_state[level], coords)
# Predict a truncated SDF and an occupancy logit per voxel [§3.2]
sdf = mlp_sdf(hidden)
occ = mlp_occ(hidden)
# Drop voxels with occupancy <= 0.5, i.e. logit <= 0 [§3.3]
coords, feat = sparsify(coords, hidden, sdf, occ)
# Upsample 2x with nearest neighbour, except after the last level
coords, feat = upsample(coords, feat)
# Replace the matching voxels of the global TSDF [§3.2]
write_global(global_state, coords, sdf)
# Mesh the global volume with Marching Cubes [§3.2]
return marching_cubes(global_state)# Key-frame test for one incoming frame [§3.1] [§3.3]
# Input: pose of the incoming frame; pose of the last key frame
def is_keyframe(pose, last_pose):
# Translation larger than t_max = 0.1 m [§3.3]
moved = distance(pose.center, last_pose.center) > 0.1
# Rotation angle larger than R_max = 15 degrees [§3.3]
turned = rotation_angle(pose, last_pose) > 15.0
# Paper wording: both conditions [§3.1]; code uses "or" (see Paper vs code)
return moved and turned# Supervision at one level [§3.2 Supervision]
# Input: predicted sdf, occupancy logit; ground-truth sdf, occupancy (0/1)
def level_loss(sdf, occ_logit, sdf_gt, occ_gt):
# Balance free and occupied voxels [code: models/neucon_network.py#L244]
# POS_WEIGHT = 1.5 in train.yaml
pos_weight = n_free / n_occupied * POS_WEIGHT
# Occupancy: binary cross-entropy on logits [§3.2]
occ_loss = bce_with_logits(occ_logit, occ_gt, pos_weight)
# SDF: L1 after signed log transform, on occupied voxels [§3.2]
occupied = occ_gt == 1
sdf_loss = l1(signed_log(sdf[occupied]), signed_log(sdf_gt[occupied]))
return occ_loss + sdf_loss# Total training loss [§3.2 Supervision] [code: config/train.yaml#L38]
# Level weights LW = [1.0, 0.8, 0.64]; the coarsest level has weight 1.0
def total_loss(levels):
# Weighted sum of the per-level losses (not stated in the paper)
return sum(LW[i] * level_loss(*levels[i]) for i in range(3))Sparse convolutions are implemented with torchsparse §3.3. The equations for the hidden state are unnumbered; cite them as §3.2 GRU Fusion.
Hyperparameters
Values marked "paper" come from the text or tables. Values marked "code" are from the official repository at code_commit; the paper does not state them.
| Name | Value | Where | Notes |
|---|---|---|---|
| Key frames per fragment N | 9 | §4.3 tab. 5 train.yaml | Paper compares 5, 7, 9, 11 and uses 9 elsewhere. Code default N_VIEWS is 5 default.py. |
| Translation threshold t_max | 0.1 m | §3.3 generate_gt.py | Combined with rotation by "and" in the paper, by "or" in code |
| Rotation threshold R_max | 15° | §3.3 generate_gt.py | Code measures the angle between optical axes generate_gt.py |
| Max depth d_max | 3 m | §3.1 §3.3 scannet.py | Depths above 3 m are set to 0 |
| Finest voxel size | 4 cm | §3.3 train.yaml | VOXEL_SIZE: 0.04 |
| Coarse voxel sizes | 16 cm, 8 cm (inferred) | neucon_network.py | Interval 2^(n−i) per level; not stated in the paper |
| Number of levels | 3 | §3.2 train.yaml | N_LAYER: 3 |
| TSDF truncation λ | 12 cm | §3.3 generate_gt.py | Margin of 3 voxels at the finest level; coarser levels get 24 and 48 cm in code generate_gt.py |
| Occupancy threshold θ | 0.5 | §3.3 default.py | Applied to logits at 0, equivalent to probability 0.5 |
| Upsampling | nearest neighbour, 2x | §3.3 neucon_network.py | Each voxel is copied to its 8 children |
| Backbone | MnasNet variant, ImageNet-pretrained, with FPN | §3.3 train.yaml | Width factor 1 (fpn-mnas-1); the paper does not state the width |
| Image size | 640 × 480 | main.py | Not stated in the paper |
| Feature channels per level | 96, 48, 24 | neucon_network.py | Sparse conv stem widths 32, 64, 128, 96, 96 scaled by 1/2^level modules.py |
| Training samples cap per level | 4096, 16384, 65536 per batch item | default.py neucon_network.py | Random subsampling of occupied voxels during training only; not stated in the paper |
| Level loss weights | 1.0, 0.8, 0.64 | train.yaml | Not stated in the paper |
| Occupancy pos_weight | 1.5 | train.yaml | Default in code is 1.0 default.py |
| Optimizer | Adam, β = (0.9, 0.999) | main.py | Not stated in the paper |
| Weight decay | 0 | default.py | Not stated in the paper |
| Learning rate | 1e-3 | train.yaml | Not stated in the paper |
| LR schedule | ×0.5 at epochs 12, 24, 48 | train.yaml main.py | Default config uses 12, 24, 36 |
| Epochs | 991 | train.yaml | Not stated in the paper; the README describes a 50-epoch two-phase schedule |
| Batch size | 1 per GPU, 2 GPUs | train.yaml train.sh | Not stated in the paper |
| Random rotation / translation | on, about the z-axis | main.py transforms.py | Augmentation of the fragment volume; not stated in the paper |
| Padding of the bounding volume | 0.1 m XY, 0.025 m Z | default.py | Not stated in the paper |
| Timing | 30 ms per key frame | §4.2 tab. 4 | Measured on an NVIDIA RTX 2080 Ti |
Results
Author-reported. Setup: ScanNet (V2) with two train/validation splits, one from Atlas (top block) and one from BA-Net (bottom block) §4.1 tab. 1 tab. 2. The 7-Scenes results use the model trained on ScanNet without further training §4.1 tab. 3. Depth-based baselines are fused with point-cloud or TSDF fusion, and the Atlas comparison uses double-layer meshes §4.1. Timing is per key frame on an RTX 2080 Ti §4.2.
3D geometry on ScanNet, Atlas split (top) and BA-Net split (bottom) tab. 1
| Method | Layer | Comp ↓ | Acc ↓ | Recall ↑ | Prec ↑ | F-score ↑ | Time (ms) ↓ |
|---|---|---|---|---|---|---|---|
| MVDepthNet | single | 0.040 | 0.240 | 0.831 | 0.208 | 0.329 | 48 |
| GPMVS | single | 0.031 | 0.879 | 0.871 | 0.188 | 0.304 | 51 |
| DPSNet | single | 0.045 | 0.284 | 0.793 | 0.223 | 0.344 | 322 |
| COLMAP | single | 0.069 | 0.135 | 0.634 | 0.505 | 0.558 | 2076 |
| This paper | single | 0.128 | 0.054 | 0.479 | 0.684 | 0.562 | 30 |
| Atlas | double | 0.062 | 0.128 | 0.732 | 0.382 | 0.499 | 292 |
| This paper | double | 0.106 | 0.073 | 0.609 | 0.450 | 0.516 | 30 |
| DeepV2D (BA-Net split) | single | 0.057 | 0.239 | 0.646 | 0.329 | 0.431 | 347 |
| Consistent Depth (BA-Net split) | single | 0.091 | 0.344 | 0.461 | 0.266 | 0.331 | 2321 |
| This paper (BA-Net split) | single | 0.120 | 0.062 | 0.428 | 0.592 | 0.494 | 30 |
2D depth on ScanNet, Atlas split tab. 2
| Method | Abs Rel ↓ | Abs Diff ↓ | Sq Rel ↓ | RMSE ↓ | δ<1.25 ↑ | Comp ↑ |
|---|---|---|---|---|---|---|
| COLMAP | 0.137 | 0.264 | 0.138 | 0.502 | 83.4 | 0.871 |
| MVDepthNet | 0.098 | 0.191 | 0.061 | 0.293 | 89.6 | 0.928 |
| GPMVS | 0.130 | 0.239 | 0.339 | 0.472 | 90.6 | 0.928 |
| DPSNet | 0.087 | 0.158 | 0.035 | 0.232 | 92.5 | 0.928 |
| Atlas | 0.065 | 0.123 | 0.045 | 0.251 | 93.6 | 0.999 |
| This paper | 0.065 | 0.106 | 0.031 | 0.195 | 94.8 | 0.909 |
2D depth on ScanNet, BA-Net split tab. 2
| Method | Abs Rel ↓ | Sq Rel ↓ | RMSE ↓ | RMSE log ↓ | Sc Inv ↓ |
|---|---|---|---|---|---|
| DeMoN | 0.231 | 0.520 | 0.761 | 0.289 | 0.284 |
| BA-Net | 0.161 | 0.092 | 0.346 | 0.214 | 0.184 |
| DeepV2D | 0.057 | 0.010 | 0.168 | 0.080 | 0.077 |
| Consistent Depth | 0.073 | 0.037 | 0.217 | 0.105 | 0.103 |
| This paper | 0.047 | 0.024 | 0.164 | 0.093 | 0.092 |
7-Scenes, 3D tab. 3, top
| Method | Comp ↓ | Acc ↓ | Recall ↑ | Prec ↑ | F-score ↑ |
|---|---|---|---|---|---|
| DeepV2D | 0.180 | 0.518 | 0.175 | 0.087 | 0.115 |
| CNMNet | 0.150 | 0.398 | 0.246 | 0.111 | 0.149 |
| This paper | 0.228 | 0.100 | 0.227 | 0.389 | 0.282 |
7-Scenes, 2D tab. 3, bottom
| Method | δ<1.25 ↑ | Abs Rel ↓ | Sq Rel ↓ | RMSE ↓ | Time (ms) ↓ |
|---|---|---|---|---|---|
| DeMoN | 31.88 | 0.3888 | 0.4198 | 0.8549 | 110 |
| MVSNet | 64.09 | 0.2339 | 0.1904 | 0.5078 | 1050 |
| N-RGBD | 69.26 | 0.1758 | 0.1123 | 0.4408 | 202 |
| MVDNet | 71.79 | 0.1925 | 0.2350 | 0.4585 | 48 |
| DPSNet | 70.96 | 0.1991 | 0.1420 | 0.4382 | 322 |
| DeepV2D | 42.80 | 0.4370 | 0.5530 | 0.8690 | 347 |
| CNMNet | 76.64 | 0.1612 | 0.0832 | 0.3614 | 80 |
| This paper | 82.00 | 0.1550 | 0.1040 | 0.3470 | 30 |
Timing per key frame, averaged over fragments tab. 4
| Stage | Unprojection | Sparse conv | GRU | Total |
|---|---|---|---|---|
| Image encoder | 4.03 ms | |||
| Level 1 | 1.27 ms | 3.70 ms | 2.18 ms | |
| Level 2 | 1.21 ms | 3.84 ms | 2.24 ms | |
| Level 3 | 2.18 ms | 5.11 ms | 3.80 ms | |
| Sum | 29.56 ms |
Ablation on ScanNet, 3D metrics tab. 5
| Row | Views | Area | Fusion | Recall ↑ | Prec ↑ | F-score ↑ |
|---|---|---|---|---|---|---|
| i | 5 | OCC | Linear (running average) | 0.576 | 0.386 | 0.462 |
| ii | 5 | OCC | Average of features | 0.535 | 0.432 | 0.478 |
| iii | 5 | OCC | GRU | 0.572 | 0.426 | 0.488 |
| iv | 5 | FBV | GRU | 0.613 | 0.421 | 0.494 |
| – | 7 | FBV | GRU | 0.607 | 0.435 | 0.507 |
| v | 9 | FBV | GRU | 0.609 | 0.450 | 0.516 |
| – | 11 | FBV | GRU | 0.593 | 0.398 | 0.474 |
Efficiency and ablation text: §4.2 §4.3. The paper reports about 10× speed-up over Atlas and about 77× over Consistent Depth §4.2. The 2D and 3D metric definitions are in a supplementary document that is not in the source bundle we read §4.1.
Paper vs code
Code checked: official repository zju3dv/NeuralRecon at code_commit, Apache-2.0. The abstract links the project page at zju3dv.github.io/neuralrecon [abstract], and that page links the repository. The paper text itself does not name a code URL.
- Key-frame rule. Paper: a frame is a key frame when translation and rotation exceed the thresholds §3.1. Code: a frame is a key frame when the translation or the angle exceeds them generate_gt.py. The angle is computed from the optical axes generate_gt.py.
- Fragment windows. The paper does not say whether fragments overlap. Code builds consecutive non-overlapping windows of
window_sizekey frames generate_gt.py. - GRU operator. Paper: the four equations use only sparse convolutions §3.2 GRU Fusion. Code: each sparse convolution also adds a pointwise linear term of its input modules.py modules.py.
- Feature volume. Code concatenates one extra channel, the normalised mean depth of the voxel over its views, to the averaged image features back_project.py neucon_network.py. The paper does not mention it.
- Feature averaging. Paper: average weighted by visibility §3.2. Code: plain mean over the views where the voxel projects inside the image back_project.py. Voxels seen by at most one view are masked out of the loss and of the occupancy output unless
FUSION.FULLis set neucon_network.py neucon_network.py. - Loss. Paper: BCE for occupancy, L1 on log-transformed SDF, at all levels §3.2 Supervision. Code: BCE with a pos_weight of (free/occupied) × POS_WEIGHT neucon_network.py; L1 only on occupied voxels neucon_network.py; level weights 1.0, 0.8, 0.64 neuralrecon.py.
- Truncation. Paper: λ = 12 cm §3.3. Code: margin 3 voxels at each level, so 12 cm at the finest level and 24 and 48 cm at coarser levels generate_gt.py fusion.py.
- Augmentation. Code applies a random rotation about z and a random translation of the cropped volume during training main.py transforms.py. Not in the paper.
- Training schedule. The README describes two phases. Phase 1 (epochs 0–20) trains single fragments without GRU fusion. Phase 2 (epochs 21–50) enables GRU fusion. The switch is made by hand README.md. The shipped
train.yamlsets 991 epochs, andFUSION_ONandFULLare True in it train.yaml train.yaml. The paper describes neither schedule. - Fusion region. Paper: fusion inside the FBV, called FBV in the ablation tab. 5. Code:
FULL: Truekeeps all valid FBV voxels as the updated set gru_fusion.py. MappingFULLto the paper's FBV is our inference from the code. - Config defaults.
config/default.pyuses N_VOX [128, 224, 192], N_VIEWS 5, POS_WEIGHT 1.0 and LR milestones 12, 24, 36 default.py default.py default.py default.py. The shippedtrain.yamluses [96, 96, 96], 9 views, 1.5 and 12, 24, 48 train.yaml train.yaml train.yaml train.yaml. - Dates. The README dates the code and pretrained models to 2021-05-02 README.md. The arXiv record is v1 of 2021-04-01 [raw/arxiv]. The code therefore post-dates this version, but no commit history was checked against the paper.
The values stated in the text (voxel size, λ, θ, d_max, t_max, R_max) match the shipped code, with the caveats above.
Pitfalls
- Equations are unnumbered. The GRU update is written in an unnumbered
align*environment §3.2 GRU Fusion. Cite it as a section, not as an equation number. - Coarse levels are underspecified. The paper gives only the finest voxel size (4 cm) and that there are 3 levels §3.2 §3.3. The coarse voxel sizes (16 and 8 cm) and the channel widths come from the code neucon_network.py modules.py.
- Sampling caps change the loss. During training, occupied voxels beyond 4096, 16384 and 65536 per batch item are randomly dropped at the three levels neucon_network.py. This changes which voxels get loss and is not in the paper.
- Hidden-state size is unused.
FUSION.HIDDEN_DIMis defined but never read in the Python code. Hidden channel counts come from the per-level feature widths default.py neucon_network.py. - Default config fails to build. The default
BACKBONE2D.ARCisfpn-mnas. The model parses the part after the last hyphen as a float, which raises onmnas. Usefpn-mnas-1fromtrain.yamldefault.py neuralrecon.py. - Occupancy threshold on logits. Code thresholds the raw logit at 0, not the probability neucon_network.py. This is equivalent to θ = 0.5 on the sigmoid output, but a reimplementation that thresholds the probability must do the same.
- Dataset preparation is required. Training reads precomputed fragment pickles and TSDF files produced by
generate_gt.py. Its defaults (window 9, margin 3 voxels at 4 cm, min angle 15°, min distance 0.1 m) match the values stated in the paper generate_gt.py. - Timing scope. The 30 ms figure is the fragment reconstruction time divided by the number of key frames. Depth-based methods are timed without their TSDF fusion §4.2.
- Single- vs double-layer meshes. The Atlas comparison uses double-layer meshes §4.1. On the Atlas split, this method's double-layer F-score (0.516) is lower than its single-layer F-score (0.562) tab. 1.
- Images. Frames are resized to 640 × 480 and ScanNet images are padded vertically by 4 pixels main.py transforms.py. The paper states neither.
- Model weights. Pretrained weights are linked from the README and are not hosted in the repository README.md. This card does not use them.
Lineage
- Builds on: GRU as the recurrent cell §3.2 GRU Fusion; the volumetric feature back-projection of prior learned reconstruction work §3.2; sparse 3D convolution through torchsparse §3.3; key-frame and fragment selection following GPMVS §3.1.
- Replaces: the running-average linear TSDF fusion of KinectFusion, which the GRU takes the place of §3.2 §4.3.
- Compared against: real-time multi-view depth methods (MVDepthNet, GPMVS, CNMNet, N-RGBD), multi-view stereo methods (COLMAP, DPSNet, MVSNet, Atlas, Consistent Depth), and learning-based SLAM methods (DeMoN, BA-Net, DeepV2D) §4.1 §4.2 tab. 1 tab. 3.
- Superseded by: not stated in the sources of this card.
Source and license
Adapted from “NeuralRecon: Real-Time Coherent 3D Reconstruction from Monocular Video” by Jiaming Sun, Yiming Xie, Linghao Chen, Xiaowei Zhou, Hujun Bao (arXiv:2104.00681), used under CC BY 4.0. Changes: condensed and restructured for implementation, with tables, equations and algorithms taken from the paper and its code, each cited to its source. Not affiliated with or endorsed by the authors. Code citations refer to https://github.com/zju3dv/NeuralRecon at commit 0fd2df30d1b2 (Apache-2.0). Any quoted code is shown under that license.