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()