DEV Community

Nariaki Wada
Nariaki Wada

Posted on

Estimating Surface Orientation and 3D from One Image with MoGe-2 on Apple Silicon

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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:

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

Before and after comparison of a street image and its MoGe-2 surface normals

The road, buildings on both sides, cars, and trees have distinct orientations. The sky is black because it was marked invalid.

Tabletop objects

Before and after comparison of tabletop objects and their MoGe-2 surface normals

Large surfaces on the desk and books have consistent colors, while the colors vary smoothly around the curved cup and bottle.

Crowd

Before and after comparison of a crowd image and its MoGe-2 surface normals

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

Camera moving sideways through a 2.5D street created from one image with MoGe-2

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
Enter fullscreen mode Exit fullscreen mode

Cat character

Camera moving sideways through a 2.5D cat-character scene created with MoGe-2

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

Camera moving sideways through a 2.5D girl-character scene created with MoGe-2

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
Enter fullscreen mode Exit fullscreen mode

Interpretation

The detailed data is above, but the simpler reading has three main points.

  1. 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.

  1. 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.

  1. 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)