Examples#
Working scripts in examples/.
basic_depth.py#
Basic depth prediction and visualization.
#!/usr/bin/env python3
"""
Basic depth prediction example.
This demonstrates the simplest usage of panodac: predicting depth from a single image.
"""
import panodac
import numpy as np
from PIL import Image
import matplotlib.pyplot as plt
def main():
# Predict depth - it's this simple!
depth = panodac.predict("path/to/your/image.jpg")
# depth is a numpy array (H, W) with metric depth in meters
print(f"Depth shape: {depth.shape}")
print(f"Depth range: {depth.min():.2f}m - {depth.max():.2f}m")
# Visualize the depth map
plt.figure(figsize=(12, 5))
plt.subplot(1, 2, 1)
plt.title("Input Image")
plt.imshow(Image.open("path/to/your/image.jpg"))
plt.axis("off")
plt.subplot(1, 2, 2)
plt.title("Predicted Depth")
plt.imshow(depth, cmap="turbo")
plt.colorbar(label="Depth (m)")
plt.axis("off")
plt.tight_layout()
plt.savefig("depth_result.jpg")
print("Saved visualization to depth_result.jpg")
def example_with_options():
"""Example showing all available options."""
# List available models
print("Available models:", panodac.list_models())
# Output: ['outdoor-resnet101', 'outdoor-swinl', 'indoor-resnet101', 'indoor-swinl']
# Check current device
print("Using device:", panodac.get_device())
# Output: cpu, cuda, or mps (depending on your system)
# Predict with a specific model
depth = panodac.predict(
"photo.jpg",
model="outdoor-swinl", # Higher quality, slower
device="mps", # Force specific device
)
# The predict function accepts multiple input types:
# 1. File path (string or Path)
depth = panodac.predict("photo.jpg")
# 2. PIL Image
from PIL import Image
img = Image.open("photo.jpg")
depth = panodac.predict(img)
# 3. NumPy array (H, W, 3) RGB
img_np = np.array(Image.open("photo.jpg"))
depth = panodac.predict(img_np)
if __name__ == "__main__":
main()
panorama_depth.py#
360° panorama depth prediction with point cloud export.
#!/usr/bin/env python3
"""
Panorama depth prediction with point cloud export.
This example shows how to process 360° equirectangular panoramas
and export the result as a 3D point cloud.
"""
import numpy as np
import panodac
from PIL import Image
def main():
# Path to your panorama image (should have 2:1 aspect ratio)
pano_path = "../assets/test-pano.jpg"
# Predict depth
print("Predicting depth...")
depth = panodac.predict(pano_path, model="outdoor-resnet101")
print(f"Depth shape: {depth.shape}")
print(f"Depth range: {depth.min():.2f}m - {depth.max():.2f}m")
# Save depth visualization
save_depth_visualization(depth, "panorama_depth.jpg")
# Export as point cloud
print("Generating point cloud...")
pano_img = np.array(Image.open(pano_path))
points, colors = erp_to_pointcloud(pano_img, depth)
save_ply("panorama_pointcloud.ply", points, colors)
print(f"Saved point cloud with {len(points)} points")
def save_depth_visualization(depth: np.ndarray, output_path: str):
"""Save a colorized depth visualization."""
import cv2
# Normalize depth to 0-255 for visualization
depth_norm = (depth - depth.min()) / (depth.max() - depth.min())
depth_vis = (depth_norm * 255).astype(np.uint8)
# Apply colormap
depth_colored = cv2.applyColorMap(depth_vis, cv2.COLORMAP_TURBO)
depth_colored = cv2.cvtColor(depth_colored, cv2.COLOR_BGR2RGB)
Image.fromarray(depth_colored).save(output_path)
print(f"Saved depth visualization to {output_path}")
def erp_to_pointcloud(
image: np.ndarray,
depth: np.ndarray,
max_points: int = 500000,
min_depth: float = 0.1,
max_depth: float = 100.0,
) -> tuple[np.ndarray, np.ndarray]:
"""Convert equirectangular panorama with depth to 3D point cloud.
Args:
image: RGB image (H, W, 3)
depth: Depth map (H, W) in meters
max_points: Maximum number of points to generate
min_depth: Minimum depth threshold in meters
max_depth: Maximum depth threshold in meters
Returns:
points: (N, 3) xyz coordinates
colors: (N, 3) RGB colors (0-255)
"""
H, W = depth.shape
# Create coordinate grids
u = np.linspace(0, 1, W, dtype=np.float32)
v = np.linspace(0, 1, H, dtype=np.float32)
u, v = np.meshgrid(u, v)
# Convert to spherical coordinates
# u: 0->1 maps to longitude -π->π
# v: 0->1 maps to latitude π/2->-π/2
longitude = (u - 0.5) * 2 * np.pi
latitude = (0.5 - v) * np.pi
# Convert to Cartesian coordinates (Y-up convention)
# x: right, y: up, z: forward
x = depth * np.cos(latitude) * np.sin(longitude)
y = depth * np.sin(latitude)
z = depth * np.cos(latitude) * np.cos(longitude)
# Flatten all arrays
points = np.stack([x.ravel(), y.ravel(), z.ravel()], axis=1)
colors = image.reshape(-1, 3)
depth_flat = depth.ravel()
# Filter by actual depth (radial distance), not Cartesian z-coordinate
# This is critical for 360° panoramas where z can be negative (behind viewer)
valid = (depth_flat > min_depth) & (depth_flat < max_depth)
points = points[valid]
colors = colors[valid]
# Subsample if too many points (after filtering to preserve valid point ratio)
if len(points) > max_points:
idx = np.random.choice(len(points), max_points, replace=False)
points = points[idx]
colors = colors[idx]
return points, colors
def save_ply(filename: str, points: np.ndarray, colors: np.ndarray):
"""Save point cloud to PLY file."""
with open(filename, "w") as f:
f.write("ply\n")
f.write("format ascii 1.0\n")
f.write(f"element vertex {len(points)}\n")
f.write("property float x\n")
f.write("property float y\n")
f.write("property float z\n")
f.write("property uchar red\n")
f.write("property uchar green\n")
f.write("property uchar blue\n")
f.write("end_header\n")
for (x, y, z), (r, g, b) in zip(points, colors):
f.write(f"{x:.4f} {y:.4f} {z:.4f} {int(r)} {int(g)} {int(b)}\n")
if __name__ == "__main__":
main()
panorama_depth_compare_blending.py#
Compare ERP panorama depth output with and without Poisson seam blending.
#!/usr/bin/env python3
"""
Panorama depth prediction: compare seam correction on/off.
This example runs the model once (without seam correction), then applies the
Poisson seam blender as a post-process to produce a blended result. It saves:
- panorama_depth_no_blend.jpg
- panorama_depth_blend.jpg
and prints simple seam metrics.
"""
import numpy as np
import panodac
from PIL import Image
def main():
# Path to your panorama image (should have 2:1 aspect ratio)
pano_path = "../assets/test-pano.jpg"
print("Predicting raw depth (no blending)...")
depth_raw = panodac.predict(
pano_path,
model="outdoor-resnet101",
fix_panorama_seam=False,
)
print(f"Raw depth shape: {depth_raw.shape}")
print(f"Raw depth range: {depth_raw.min():.2f}m - {depth_raw.max():.2f}m")
print("Applying Poisson seam blending...")
from panodac.seam_blending import fix_panorama_seam, validate_seam_quality
H, W = depth_raw.shape
blend_width = max(8, W // 32)
depth_blend = fix_panorama_seam(depth_raw, blend_width=blend_width)
metrics = validate_seam_quality(depth_raw, depth_blend)
print(
"Seam discontinuity (mean |col0-colLast|): "
f"{metrics['seam_diff_before']:.4f} -> {metrics['seam_diff_after']:.4f} "
f"({metrics['improvement_pct']:.1f}%)"
)
save_depth_visualization(depth_raw, "panorama_depth_no_blend.jpg")
save_depth_visualization(depth_blend, "panorama_depth_blend.jpg")
def save_depth_visualization(depth: np.ndarray, output_path: str):
"""Save a colorized depth visualization."""
import cv2
depth = depth.astype(np.float32, copy=False)
dmin, dmax = float(depth.min()), float(depth.max())
denom = max(1e-6, dmax - dmin)
# Normalize depth to 0-255 for visualization
depth_norm = (depth - dmin) / denom
depth_vis = (depth_norm * 255).astype(np.uint8)
# Apply colormap
depth_colored = cv2.applyColorMap(depth_vis, cv2.COLORMAP_TURBO)
depth_colored = cv2.cvtColor(depth_colored, cv2.COLOR_BGR2RGB)
Image.fromarray(depth_colored).save(output_path)
print(f"Saved depth visualization to {output_path}")
if __name__ == "__main__":
main()
batch_inference.py#
Batch process multiple images from the command line.
#!/usr/bin/env python3
"""
Batch processing example.
Process multiple images from a directory and save depth maps.
"""
import argparse
from pathlib import Path
import cv2
import numpy as np
from PIL import Image
from tqdm import tqdm
import panodac
def main():
parser = argparse.ArgumentParser(description="Batch depth prediction")
parser.add_argument("input_dir", type=str, help="Directory with input images")
parser.add_argument("output_dir", type=str, help="Directory for output depth maps")
parser.add_argument(
"--model", type=str, default="outdoor-resnet101",
choices=panodac.list_models(),
help="Model to use"
)
parser.add_argument(
"--format", type=str, default="png",
choices=["png", "npy", "exr"],
help="Output format for depth maps"
)
args = parser.parse_args()
# Create output directory
input_dir = Path(args.input_dir)
output_dir = Path(args.output_dir)
output_dir.mkdir(parents=True, exist_ok=True)
# Find all images
extensions = {".jpg", ".jpeg", ".png", ".webp", ".bmp"}
image_files = [f for f in input_dir.iterdir() if f.suffix.lower() in extensions]
if not image_files:
print(f"No images found in {input_dir}")
return
print(f"Found {len(image_files)} images")
print(f"Using model: {args.model}")
print(f"Using device: {panodac.get_device()}")
# Process images
for img_path in tqdm(image_files, desc="Processing"):
try:
# Predict depth
depth = panodac.predict(str(img_path), model=args.model)
# Save output
output_name = img_path.stem + "_depth"
if args.format == "png":
# Save as 16-bit PNG (depth in mm)
depth_mm = (depth * 1000).astype(np.uint16)
output_path = output_dir / f"{output_name}.png"
cv2.imwrite(str(output_path), depth_mm)
elif args.format == "npy":
# Save as numpy array (original float values in meters)
output_path = output_dir / f"{output_name}.npy"
np.save(output_path, depth)
elif args.format == "exr":
# Save as OpenEXR (requires opencv-python-headless[contrib])
output_path = output_dir / f"{output_name}.exr"
cv2.imwrite(str(output_path), depth.astype(np.float32))
# Also save a visualization
save_visualization(depth, output_dir / f"{output_name}_vis.jpg")
except Exception as e:
print(f"\nError processing {img_path.name}: {e}")
print(f"\nResults saved to {output_dir}")
def save_visualization(depth: np.ndarray, output_path: Path):
"""Save a colorized depth visualization."""
# Normalize depth
depth_norm = (depth - depth.min()) / (depth.max() - depth.min() + 1e-8)
depth_vis = (depth_norm * 255).astype(np.uint8)
# Apply colormap
depth_colored = cv2.applyColorMap(depth_vis, cv2.COLORMAP_TURBO)
cv2.imwrite(str(output_path), depth_colored)
if __name__ == "__main__":
main()