Depth Anything V2: Predicting Depth from a Single Image with Python
Hey folks, in this tutorial I want to take you along to play around with one of the computer vision models that I find most exciting in the last few years. Its name is Depth Anything, specifically the second version, Depth Anything V2. This model has one task that sounds simple but is actually really hard: guessing how far every pixel in a photo is from the camera, from just ONE image. Yep, one ordinary image, not a stereo photo, not LiDAR sensor data, not a special depth camera. Just hand it a regular RGB photo and it will give you a depth map, a map that shows what is near and what is far.
In this tutorial I will explain from scratch what monocular depth estimation is, why it is super useful for 3D, robotics, all the way to portrait-mode photo effects on your phone, then move on to hands-on practice using HuggingFace transformers. We will run Depth Anything through the super easy pipeline("depth-estimation"), then go deeper into using the model repo manually so you understand what actually happens behind the scenes. We will also cover how to normalize and visualize the depth map, choose model sizes (Small, Base, Large), the difference between metric depth and relative depth, and how to run this model on video frame by frame. Grab a coffee, we will start slow.
Introduction
Before we jump into the code, I want you to understand the big picture first. This is important so that when you see the output later, you know what those numbers actually mean.
What Is Monocular Depth Estimation
Monocular depth estimation is the task of estimating depth (distance from the camera) for every pixel in an image, using only a single image. The word "monocular" means one eye, one point of view. This is different from classic depth estimation which usually needs two cameras (stereo) or active sensors like LiDAR that shoot out beams to measure distance.
Why is this hard? Because mathematically, a single 2D image is ambiguous. Imagine you photograph a small ball that is close next to a large ball that is far away. In a 2D image, both could look the same size. The human brain can guess which one is near and which is far because we have a lifetime of experience about object sizes, perspective, shadows, texture, and context. Well, deep learning models like Depth Anything basically learn that same "intuition" from millions of images.
Depth Anything V2 is trained using a combination of labeled and unlabeled data in massive quantities (millions of images), plus synthetic data techniques and a teacher-student model approach. As a result, this model generalizes really well to images it has never seen, from indoor photos, outdoor scenes, human faces, to even AI-generated images.
What Does a Depth Map Look Like
The output of this model is called a depth map. It is a grayscale image (single channel) with the same size as the input image. Every pixel in the depth map has a value that indicates relative or absolute depth, depending on the model type.
Something you need to note carefully: Depth Anything (the relative version) outputs something called inverse depth or disparity. This means a LARGE value actually means CLOSE to the camera, and a SMALL value means FAR. This often confuses beginners, so please remember it. Later during visualization, near objects will usually appear bright and far objects will appear dark.
Real World Use Cases
Let me give you a few examples so you get why this is worth learning:
For photo effects, this is the easiest to appreciate. Portrait mode on your phone that blurs the background (bokeh) needs depth information to know which is the subject and which is the background. With a depth map you can create blur effects, replace the background, or add realistic fog effects.
For robotics and drones, a robot needs to know the distance to objects so it does not crash. With monocular depth, a cheap robot with just one camera can get distance estimates without needing expensive sensors.
For 3D and AR, a depth map can be used for 3D reconstruction, building point clouds, or placing virtual objects at the right position in augmented reality.
For autonomous vehicles, depth estimation helps the vehicle understand its surroundings, distance to other cars, pedestrians, and obstacles.
For creative and visual effects, many digital artists use depth maps to create parallax effects, 2.5D animations, or relighting photos.
Okay, that is enough theory. Let us move to installation.
Installation
Depth Anything V2 is neatly integrated into HuggingFace transformers, so we do not need to clone any weird repo or install complicated dependencies. Just use the standard libraries of the HuggingFace ecosystem.
First, make sure you have Python 3.8 or above. I recommend creating a virtual environment first so things do not get messy.
python -m venv venv
source venv/bin/activate # Linux / Mac
venv\Scripts\activate # Windows
Then install the packages we need:
pip install "transformers>=4.45.0" torch pillow numpy matplotlib opencv-python
A quick explanation of each package:
transformers is the main library from HuggingFace that already has the Depth Anything architecture and pipeline. Make sure the version is recent enough (4.45 and above) because Depth Anything V2 support only landed in the latest versions.
torch (PyTorch) is the deep learning backend used to run the model. If you have an NVIDIA GPU, install the CUDA version so it runs faster, but CPU works too, just slower.
pillow for reading and processing images, numpy for array manipulation, matplotlib for visualizing the depth map with nice colormaps, and opencv-python for video processing later.
To check whether your PyTorch can use the GPU, run this:
import torch
print("CUDA available:", torch.cuda.isavailable())
print("Device:", "cuda" if torch.cuda.isavailable() else "cpu")
If it prints True, awesome, your GPU is ready. If it prints False, no worries, we can still run on CPU for ordinary images.
Basic Usage
Now for the most anticipated part. The easiest way to run Depth Anything is using pipeline from transformers. It only takes a few lines of code.
Using pipeline("depth-estimation")
The pipeline is a high-level abstraction from HuggingFace that handles all the preprocessing, inference, and postprocessing for us. So we just hand it an image and get back a depth map.
from transformers import pipeline
from PIL import Image
Create a depth estimation pipeline with Depth Anything V2 Small
pipe = pipeline(
task="depth-estimation",
model="depth-anything/Depth-Anything-V2-Small-hf"
)
Open the image we want to process
image = Image.open("photo.jpg")
Run the prediction
result = pipe(image)
result is a dict containing 'predicteddepth' (tensor) and 'depth' (PIL image)
print(result.keys())
depthimage = result["depth"] # this is already a grayscale PIL Image
depthimage.save("depthoutput.png")
print("Depth map saved!")
Super easy, right? The result object is a dictionary containing two important things. The first is predicteddepth, a raw PyTorch tensor containing the actual depth values. The second is depth, which is already a PIL Image ready to save or display, already normalized to the 0-255 range for us.
If you do not have an image yet, you can pull an image straight from the internet to try:
from transformers import pipeline
from PIL import Image
import requests
url = "http://images.cocodataset.org/val2017/000000039769.jpg"
image = Image.open(requests.get(url, stream=True).raw)
pipe = pipeline("depth-estimation", model="depth-anything/Depth-Anything-V2-Small-hf")
result = pipe(image)
result["depth"].save("depthcats.png")
That sample image from COCO is a photo of two cats lounging on a couch. After processing, you will see the near cat appear bright and the wall behind appear dark.
Choosing Model Size: Small, Base, Large
Depth Anything V2 comes in several sizes, and you just swap the model name in the pipeline. Here are the options:
# Small: fastest, lightest (~25 million parameters), good for real-time / CPU
"depth-anything/Depth-Anything-V2-Small-hf"
Base: balance of speed and accuracy (~98 million parameters)
"depth-anything/Depth-Anything-V2-Base-hf"
Large: most accurate, most detailed (~335 million parameters), needs GPU
"depth-anything/Depth-Anything-V2-Large-hf"
How to choose? If you need speed, for example for processing video or running on CPU/a regular laptop, use Small. The results are already really good for most cases. If you need maximum detail for large prints or precise 3D reconstruction, and you have a decent GPU, use Large. Base sits in the middle for those who want balance.
Personally I usually start from Small for prototyping because it is fast, then upgrade to Large only if I really need the quality. The inference time difference is quite significant, Small can be several times faster than Large.
Visualization with a Nice Colormap
A grayscale depth map is a bit boring to look at. To make it more informative and pleasant, we can apply a colormap. I like using the "inferno" or "magma" colormap from matplotlib.
import numpy as np
import matplotlib.pyplot as plt
from transformers import pipeline
from PIL import Image
pipe = pipeline("depth-estimation", model="depth-anything/Depth-Anything-V2-Small-hf")
image = Image.open("photo.jpg")
result = pipe(image)
Get the raw depth as a numpy array
depth = np.array(result["depth"])
Show side by side: original image and colored depth map
fig, axes = plt.subplots(1, 2, figsize=(14, 6))
axes[0].imshow(image)
axes[0].settitle("Original Image")
axes[0].axis("off")
im = axes[1].imshow(depth, cmap="inferno")
axes[1].settitle("Depth Map")
axes[1].axis("off")
fig.colorbar(im, ax=axes[1], fraction=0.046)
plt.tightlayout()
plt.savefig("depthcomparison.png", dpi=150)
plt.show()
With the inferno colormap, near areas (high values) will appear bright yellow, and far areas (low values) will appear dark purple. Much nicer to look at than plain grayscale.
Advanced Usage
Okay now we level up. The pipeline is great for quick work, but sometimes we need more control. In this section we will load the model manually, understand the raw depth process, discuss metric vs relative depth, and process video.
Loading the Model Manually
If you want full control, for example to set the input size, do batch processing, or grab the raw tensor for further computation, load the model directly using AutoImageProcessor and AutoModelForDepthEstimation.
import torch
import numpy as np
from PIL import Image
from transformers import AutoImageProcessor, AutoModelForDepthEstimation
device = "cuda" if torch.cuda.isavailable() else "cpu"
Load the processor and model
modelname = "depth-anything/Depth-Anything-V2-Small-hf"
processor = AutoImageProcessor.frompretrained(modelname)
model = AutoModelForDepthEstimation.frompretrained(modelname).to(device)
model.eval()
Prepare the image
image = Image.open("photo.jpg").convert("RGB")
Preprocess: resize, normalize, convert to tensor
inputs = processor(images=image, returntensors="pt").to(device)
Inference without computing gradients to save memory
with torch.nograd():
outputs = model(*inputs)
predicteddepth = outputs.predicteddepth
print("Raw depth shape:", predicteddepth.shape)
Notice that the predicteddepth that comes out is sized according to the model input, not the original image size. The model usually resizes the input to a certain resolution. That is why we need to interpolate it back to the original size.
Interpolating to Original Size and Normalizing
This is an important step that people often forget. We need to resize the depth map back to the original image dimensions, then normalize it for visualization.
import torch.nn.functional as F
Interpolate depth to the original image size (height, width)
prediction = F.interpolate(
predicteddepth.unsqueeze(1), # add a channel dimension
size=image.size[::-1], # PIL size = (w, h), we need (h, w)
mode="bicubic",
aligncorners=False,
).squeeze()
Move to CPU and convert to numpy
depth = prediction.cpu().numpy()
Normalize to 0-255 range for visualization
depthmin = depth.min()
depthmax = depth.max()
depthnormalized = (depth - depthmin) / (depthmax - depthmin) # becomes 0-1
depthuint8 = (depthnormalized 255).astype(np.uint8)
Save as a grayscale image
Image.fromarray(depthuint8).save("depthmanual.png")
print("Raw depth range:", depthmin, "to", depthmax)
The min-max normalization formula above is very standard. We shift the smallest value to 0 and scale the largest value to 1, then multiply by 255 to make it 8-bit. Remember, since this is inverse depth, bright = near.
Metric Depth vs Relative Depth
This is an important concept you must understand before using depth maps for serious applications.
Relative depth means the model only tells you the ordering or comparison of depth, but not the actual distance in meters. So you know that object A is closer than object B, but you do not know exactly whether A is 2 meters or 5 meters from the camera. The default Depth Anything V2 (which we used above) outputs relative depth. This is enough for visual effects, foreground-background segmentation, and most creative applications.
Metric depth means the model gives you absolute distance numbers in meters. This is what you need if you want real 3D reconstruction, measuring distance for a robot, or navigation. Depth Anything V2 has special metric depth variants fine-tuned for indoor (Hypersim) and outdoor (Virtual KITTI) datasets.
To use metric depth, you use the metric variant model:
import torch
from PIL import Image
from transformers import AutoImageProcessor, AutoModelForDepthEstimation
device = "cuda" if torch.cuda.isavailable() else "cpu"
Metric depth model for indoor
modelname = "depth-anything/Depth-Anything-V2-Metric-Indoor-Small-hf"
processor = AutoImageProcessor.frompretrained(modelname)
model = AutoModelForDepthEstimation.frompretrained(modelname).to(device)
model.eval()
image = Image.open("room.jpg").convert("RGB")
inputs = processor(images=image, returntensors="pt").to(device)
with torch.nograd():
outputs = model(*inputs)
predicteddepth = outputs.predicteddepth
For a metric model, the depth values ARE the distance in meters
depthmeters = predicteddepth.squeeze().cpu().numpy()
print("Minimum distance:", depthmeters.min(), "meters")
print("Maximum distance:", depthmeters.max(), "meters")
Unlike relative depth, here a LARGER value actually means FARTHER (real distance in meters), the opposite of inverse depth. So please be careful to distinguish, with a relative model large value = near, with a metric model large value = far. Always check the documentation of the model you are using.
For outdoor, just change the model to the outdoor variant:
# For outdoor scenes (roads, vehicles, etc)
modelname = "depth-anything/Depth-Anything-V2-Metric-Outdoor-Small-hf"
Creating a Bokeh Effect (Background Blur)
Now we build something cool using the depth map, a portrait-mode effect like on your phone. The idea: the near subject stays sharp, and the far background gets blurred.
import cv2
import numpy as np
from PIL import Image
from transformers import pipeline
pipe = pipeline("depth-estimation", model="depth-anything/Depth-Anything-V2-Small-hf")
image = Image.open("person.jpg").convert("RGB")
result = pipe(image)
Depth is already 0-255, near subject = high value
depth = np.array(result["depth"]).astype(np.float32) / 255.0
img = np.array(image)
Create a blurred version of the image
blurred = cv2.GaussianBlur(img, (0, 0), sigmaX=15)
Create a mask: pixels with high depth (near) = sharp, low (far) = blur
depth becomes a per-pixel weight, expand to 3 channels
mask = depth[:, :, np.newaxis]
Blend the sharp and blurred images using the mask as weight
output = (img mask + blurred (1 - mask)).astype(np.uint8)
Image.fromarray(output).save("bokehoutput.jpg")
print("Bokeh effect done!")
In this code we use the depth map as a blending weight. Near pixels (high depth) take more from the sharp image, far pixels take more from the blurred image. The result is a smooth bokeh effect. You can play with the threshold to make the separation between subject and background sharper.
Processing Video Frame by Frame
Depth Anything can also be used for video, just process each frame. For video, I recommend using the Small model so it does not take too long. We use OpenCV to read and write the video.
import cv2
import numpy as np
import torch
from PIL import Image
from transformers import pipeline
device = 0 if torch.cuda.isavailable() else -1
pipe = pipeline(
"depth-estimation",
model="depth-anything/Depth-Anything-V2-Small-hf",
device=device,
)
Open the input video
cap = cv2.VideoCapture("input.mp4")
fps = cap.get(cv2.CAPPROPFPS)
width = int(cap.get(cv2.CAPPROPFRAMEWIDTH))
height = int(cap.get(cv2.CAPPROPFRAMEHEIGHT))
Prepare the output writer
fourcc = cv2.VideoWriterfourcc("mp4v")
out = cv2.VideoWriter("outputdepth.mp4", fourcc, fps, (width, height))
framecount = 0
while cap.isOpened():
ret, frame = cap.read()
if not ret:
break
# OpenCV uses BGR, convert to RGB for PIL
rgb = cv2.cvtColor(frame, cv2.COLORBGR2RGB)
pilimage = Image.fromarray(rgb)
# Predict depth
result = pipe(pilimage)
depth = np.array(result["depth"])
# Apply a colormap to add color (OpenCV colormap)
depthcolor = cv2.applyColorMap(depth, cv2.COLORMAPINFERNO)
# Make sure the size matches the output
depthcolor = cv2.resize(depthcolor, (width, height))
out.write(depthcolor)
framecount += 1
if framecount % 30 == 0:
print(f"Processed {framecount} frames")
cap.release()
out.release()
print("Depth video done:", framecount, "frames")
Note that video processing is heavy because each frame is inferred one by one. A 10 second video at 30 fps is already 300 frames. If you have a GPU, it is much faster. To speed things up, you can lower the frame resolution before inference, or skip some frames and then interpolate.
One important note about video: because each frame is processed independently, sometimes there is flickering between frames because the depth values are not consistent. For serious applications, there are temporal smoothing techniques or special video variants of Depth Anything, but for most cases the frame-by-frame result is good enough.
Batch Processing Many Images
If you have many images to process, do not process them one at a time reloading the model. Load the model once, then loop.
import os
import numpy as np
from PIL import Image
from transformers import pipeline
pipe = pipeline("depth-estimation", model="depth-anything/Depth-Anything-V2-Base-hf")
inputdir = "inputimages"
outputdir = "depthoutput"
os.makedirs(outputdir, existok=True)
for filename in os.listdir(inputdir):
if not filename.lower().endswith((".jpg", ".png", ".jpeg")):
continue
path = os.path.join(inputdir, filename)
image = Image.open(path).convert("RGB")
result = pipe(image)
outname = os.path.splitext(filename)[0] + "depth.png"
result["depth"].save(os.path.join(outputdir, outname))
print("Done:", filename)
print("All images processed!")
Best Practices
After playing with this model for quite a while, there are a few tips I want to share so you do not fall into the same holes I did.
First about model selection, always start from Small for experiments. The difference from Large in many cases is not as big as you might think, but the speed difference is huge. Only upgrade to Base or Large if you truly see that the Small result lacks detail for your needs.
Second about inverse depth, always remember that the relative depth model outputs inverse depth where high value = near. If you want more intuitive numbers (far = high value), just flip it with depthinvert = depth.max() - depth. But be careful, this is only for visualization, not real metric distance.
Third about normalization, if you compare depth maps across different images, do not normalize each one independently if you want a fair comparison, because each image has its own min-max range. For absolute comparison, use the metric depth model.
Fourth about memory, the Large model is heavy, especially if your image is high resolution. If you hit out of memory on the GPU, try lowering the input resolution first, or switch to CPU, or use a smaller model. Always wrap inference with torch.nograd() so it does not store unnecessary gradients.
# Example of a clean, reusable helper function
import torch
import numpy as np
import torch.nn.functional as F
from PIL import Image
from transformers import AutoImageProcessor, AutoModelForDepthEstimation
class DepthEstimator:
def init(self, modelname="depth-anything/Depth-Anything-V2-Small-hf"):
self.device = "cuda" if torch.cuda.isavailable() else "cpu"
self.processor = AutoImageProcessor.frompretrained(modelname)
self.model = AutoModelForDepthEstimation.frompretrained(modelname).to(self.device)
self.model.eval()
def predict(self, imagepath):
image = Image.open(imagepath).convert("RGB")
inputs = self.processor(images=image, returntensors="pt").to(self.device)
with torch.nograd():
outputs = self.model(*inputs)
prediction = F.interpolate(
outputs.predicteddepth.unsqueeze(1),
size=image.size[::-1],
mode="bicubic",
aligncorners=False,
).squeeze()
return prediction.cpu().numpy()
@staticmethod
def normalize(depth):
d = (depth - depth.min()) / (depth.max() - depth.min())
return (d 255).astype(np.uint8)
Now usage is super clean
estimator = DepthEstimator()
depth = estimator.predict("photo.jpg")
Image.fromarray(DepthEstimator.normalize(depth)).save("result.png")
Fifth about input quality, this model is quite robust, but still a clear, bright, and focused image will give a better depth map than a blurry or dark one. Images with objects that have clear boundaries will get a sharper depth map.
Sixth about preprocessing, do not forget to always convert the image to RGB using .convert("RGB"). Sometimes there are PNG images with an alpha channel or grayscale images that will cause an error if not converted first.
Seventh about GPU vs CPU, for production that needs high throughput, a GPU is a must. But for lightweight applications or small batch processing in the background, CPU with the Small model is still very viable. Measure your needs first before immediately renting an expensive GPU.
Conclusion
Okay folks, we have reached the end of the tutorial. We have learned a lot about Depth Anything V2, from the basic concept of monocular depth estimation, why it is useful, all the way to hands-on practice from the simple pipeline to loading the model manually.
The key points I hope you take home: monocular depth estimation is guessing depth from just one image, and Depth Anything V2 is one of the best models for that right now. The easiest way to use it is through pipeline("depth-estimation"), just a few lines of code. There are three model sizes, Small for speed, Large for accuracy, Base in the middle. Remember well the difference between relative depth (comparison, inverse, high value = near) and metric depth (real distance in meters, high value = far). For visualization, we do min-max normalization then apply a colormap. And we have also seen how to create a bokeh effect and process video frame by frame.
What I love most about this model is that it makes technology that used to need expensive hardware (LiDAR, stereo cameras) accessible to anyone with just one photo and a bit of Python code. This opens the door to so many creative ideas and practical applications.
My advice, do not just read, practice right away. Take a random photo from your gallery, run the code above, and see the depth map for yourself. Then try to create a bokeh effect or process a short video. From there you will get a feel for how this model works and where its limitations are. Once you are comfortable, try integrating it into your own project, whether that is a photo app, 3D tools, or robotics.
Happy tinkering folks, I hope this tutorial is useful. See you in the next tutorial!