Estimating Surface Orientation and 3D from One Image with MoGe-2 on Apple Silicon
Hello, everyone.
Estimating distance from one image is useful, but knowing surface orientation as well—whether a road faces upward or a wall faces sideways—provides richer information for 3D conversion and relighting.
Today, I ran MoGe-2 ViT-S Normal on Apple Silicon and tested its surface-normal output, CPU and MPS speed, and consistency with the 3D information produced by the same inference.
To give the result first, median inference time on an Apple M1 Max was 212.13 ms with MPS and 1,259.34 ms with the CPU, making MPS 5.94 times faster. Surface orientation and object boundaries were visible across a street, tabletop objects, and a crowd. I also turned single images of a street, a cat, and a girl into 2.5D animations with sideways camera movement.
What is MoGe-2?
MoGe-2 is a monocular geometry estimation model released by researchers at Microsoft Research on June 10, 2025. Monocular means that it uses a single RGB image rather than a stereo camera pair. Its paper was submitted to arXiv on July 3 of the same year.
While the original MoGe focused on relative 3D shape within an image, MoGe-2 adds metric scale, sharper details, and normal estimation in a unified model.
A surface normal is a three-dimensional vector that tells us which way the surface at each pixel is facing. In the colorful normal maps in this article, the x, y, and z directions are mapped to RGB. The colors represent orientation, not object categories.
What we are testing
I used Ruicheng/moge-2-vits-normal, the smallest official model with normal output, to check:
- Whether it runs on an Apple M1 Max CPU and MPS without patches
- How much faster MPS is than the CPU with the same FP32 model
- Whether normals are visually readable for a street, tabletop objects, and a crowd
- How closely the directly predicted normals agree with normals calculated from the 3D point map
- Whether the point map can be converted into a textured 3D mesh
MPS is the PyTorch backend that uses the GPU in a Mac. The target lab is available here:
kiarina/labs/2026/07/16/moge2-surface-normal-apple-silicon
Reproducing the environment
You need an Apple Silicon Mac, mise, uv, and an internet connection for the initial model and shared-image downloads.
git clone --depth 1 --filter=blob:none --sparse \
https://github.com/kiarina/labs.git
cd labs
git sparse-checkout set .gitignore .mise/tasks Makefile mise.toml 2026/07/16/moge2-surface-normal-apple-silicon
mise -C 2026/07/16/moge2-surface-normal-apple-silicon run
The first run downloads a checkpoint at a fixed revision and verifies its SHA-256 hash. To also create a GLB and a four-second Blender video from the street image, install Blender 5.1.2 and FFmpeg, then run:
mise -C 2026/07/16/moge2-surface-normal-apple-silicon run render-video
Model, mechanism, and licenses
This test uses one pretrained model. It is not a pipeline that calls multiple AI models in sequence. Inside one checkpoint, an encoder reads image features and several heads produce task-specific outputs.
RGB image
-> DINOv2 ViT-S encoder: extract global and local image features
-> shared neck: expand them into multi-resolution features
-> points head: 3D coordinate at each pixel
normal head: surface orientation at each pixel
mask head: reliable pixels
scale head: metric scale in meters
-> metric point map / depth / normal / mask / camera intrinsics
-> optional: point map + source-image texture -> GLB mesh
A head is a small output component that turns shared features into values for one task. A point map assigns a 3D camera-space point to every pixel. Camera intrinsics describe internal camera properties related to focal length and field of view.
| Item | Pinned value |
|---|---|
| checkpoint | Ruicheng/moge-2-vits-normal/model.pt |
| parameters | 35,103,656 |
| file size | 140,550,416 bytes |
| SHA-256 | 79a16621928c2bf0ed04659218c55c01075e950507f40bb3332fb4c873d3e1dc |
| Hugging Face revision | 679230677b4d282c6f304189a93e98e14f085902 |
| MoGe repository commit | 07444410f1e33f402353b99d6ccd26bd31e469e8 |
The licenses are:
- MoGe code: MIT License
- DINOv2 code under
moge/model/dinov2: Apache License 2.0 - The Hugging Face checkpoint used here: labeled MIT
The lab does not commit the checkpoint. It downloads the pinned revision at runtime. Check the current official terms again before redistributing the model or embedding it in a product.
Method
I resized each input to a short side of 384 pixels and fixed the settings to resolution_level=5, FP32, and batch size 1. For the street-image benchmark, I measured 10 runs after three warm-up runs. Model loading, image loading, preprocessing, and saving were excluded. Warm-up reduces the effect of one-time setup work on the timing.
The comparison target for the predicted normals is not ground truth. I calculated a second normal from neighboring 3D points in the point map produced by the same inference, then measured the per-pixel angular difference. This tests internal consistency between two outputs.
Before and after
The input is on the left, and the directly predicted MoGe-2 normal is on the right.
Street
The road, buildings on both sides, cars, and trees have distinct orientations. The sky is black because it was marked invalid.
Tabletop objects
Large surfaces on the desk and books have consistent colors, while the colors vary smoothly around the curved cup and bottle.
Crowd
Rounded faces and shoulders remain visible, as do boundaries between overlapping people. However, the distant background and small people also show unstable bands.
Results
CPU and MPS speed
| backend | mean | median | min | max | std dev |
|---|---|---|---|---|---|
| PyTorch CPU | 1,272.74 ms | 1,259.34 ms | 1,244.37 ms | 1,340.16 ms | 28.24 ms |
| PyTorch MPS | 211.58 ms | 212.13 ms | 208.75 ms | 214.68 ms | 1.88 ms |
By median time, MPS was 5.94 times faster than the CPU. About 212 ms corresponds to roughly 4.71 FPS for model inference alone. The normal difference between CPU and MPS on the same input had a mean of 0.0063 degrees, a median of 0 degrees, and a maximum of 0.0396 degrees.
I ran the same command again while writing this article. Median time was 1,299.96 ms on the CPU and 175.88 ms on MPS, making MPS 7.39 times faster in that run. The model hash, normal differences, and three-image consistency data matched the original measurement. Inference time varies with runtime conditions, so these two runs should be read as a measured range rather than one fixed speed.
Consistency between two normal representations
| image | valid pixels | mean | median | p90 |
|---|---|---|---|---|
| street | 90.80% | 30.75° | 18.69° | 78.55° |
| objects | 100.00% | 14.41° | 7.05° | 35.86° |
| crowd | 98.63% | 37.94° | 33.04° | 73.47° |
The two representations agreed most closely on the broad surfaces in the tabletop image. Differences increased at object boundaries, thin structures, trees, crowds, and distant details. Normals derived from a point map use differences between neighboring pixels, so they become unstable when those neighbors cross a sudden depth discontinuity. These angular differences should not be interpreted as the accuracy of the directly predicted normals.
Converting three images into 3D animations
I used the official CLI to turn each point map into triangles and create GLBs textured with the source images. I then moved a Blender camera sideways from the original viewpoint and rendered four-second animations that show the resulting parallax.
The APNGs below are reduced to 512x288 at 10 FPS for article display. The lab produces the source MP4 files at 1280x720 and 30 FPS.
Street with cars
Parallax appears between the foreground cars, road, buildings on both sides, and people. Even a small sideways camera movement makes it clear that the single image has been converted into surfaces with depth.
vertices: 233,120
triangles: 449,292
dimensions: approximately 30.75 x 114.21 x 21.15 m
estimated horizontal FoV: 60.52°
GLB size: 13,342,320 bytes
Cat character
The pink cat is placed in front of the trees, bench, and flower bed, producing clear parallax. Its head has gentle curvature, but the ears, hands, bow tie, and body are thin shapes centered on surfaces visible from the source camera.
Girl character
The girl, bench, streetlight, sign, and background trees are separated into different depths. However, the hair, arms, and legs are not complete independent volumes; they appear as thin surfaces following the visible outlines.
The measured mesh data for all three scenes is below.
| image | vertices | triangles | FoV x / y | GLB size |
|---|---|---|---|---|
| street | 233,120 | 449,292 | 60.52° / 32.53° | 13,342,320 bytes |
| cat | 312,105 | 599,514 | 54.27° / 42.05° | 17,893,416 bytes |
| girl | 325,649 | 621,216 | 60.30° / 47.08° | 18,731,304 bytes |
I moved the street camera by ±0.8 m. The cat and girl were estimated closer to the camera, so I reduced their movement to ±0.5 m. These results are 2.5D: they look three-dimensional near the original viewpoint but do not reconstruct the hidden backs of objects.
All three examples support small viewpoint changes, but black holes appear where triangles were removed around the sky, leaves, hair, and object boundaries. Stylized images can therefore be animated as well as realistic ones, but thin outlines and stylized shapes remain difficult to interpret as 3D geometry.
What I learned during the test
My first attempt calculated normals only from horizontal and vertical differences in the depth image. The median angular difference was an invalid 113–143 degrees because that method ignored perspective projection and camera intrinsics. Switching to neighboring 3D points in the metric point map produced comparable orientations.
The official implementation also emitted a warning because FP32 autocast is unsupported on both CPU and MPS. Autocast was disabled automatically, and inference completed without a patch.
The verification environment was:
machine: MacBook Pro (Apple M1 Max, 64 GB, arm64)
OS: macOS 26.5.2
Python: 3.12.10
PyTorch: 2.13.0
OpenCV: 5.0.0
NumPy: 2.5.1
Blender: 5.1.2
FFmpeg: 8.1.2
Interpretation
The detailed data is above, but the simpler reading has three main points.
- The Mac GPU reduced the wait to a practical range
MPS took about 176–212 ms per image. That is still too slow for real-time video, but practical for image-editing or 3D preprocessing tasks.
- One inference produced several kinds of 3D information
Metric point maps, depth, normals, masks, and camera intrinsics were produced together. No separate models were needed, making the outputs easy to combine.
- Boundaries and hidden surfaces remain difficult
Normals represented broad planes and curved objects, but thin structures and distant details were unstable. A mesh made from one image is also not a complete 3D space with reconstructed backsides.
This test was limited to three generated images without ground-truth normals or depth. Timing used only one street image, a 384-pixel short side, FP32, and one M1 Max. I did not test official benchmarks, real photos, ViT-B/L, FP16, Core ML, the Apple Neural Engine, temporal video consistency, power consumption, or accuracy against ground truth.
Thoughts after verification
Getting road orientation, object curvature, and a metric point map from one image in one pass was more convenient than I expected. In particular, the dedicated normal head produced smoother boundaries than the normals calculated naively from the point map, which made the value of direct normal prediction easy to see.
The 3D mesh is interesting for small viewpoint changes, but it is not a space that can be explored freely.
This looks like a fun way to add a little camera movement to a single image.






Top comments (0)