panorama-tiler 0.1.2

A tiling and configuration generator to convert equirectangular or cylindrical panoramas into multi-resolution cubemaps.
Documentation

panorama-tiler

Tiling and configuration generator for creating multi-resolution cubemap pyramids from equirectangular or cylindrical panoramas, designed for use with the Pannellum web viewer.

Crates.io Documentation

panorama-tiler transforms flat equirectangular or cylindrical panorama images into partitioned, multi-resolution zoom levels and outputs a companion config.json configuration file. It supports both complete 360-degree spheres and cropped/partial panoramas.

Features

  • Projection Conversion: Converts equirectangular and cylindrical source projections into 6-faced cubemaps (f, b, u, d, l, r).
  • Angle and Crop Extraction: Parses Google Photo Sphere (GPano) XMP tags and EXIF data to determine field of view ( HAOV/VAOV) and vertical alignment.
  • High resolution: Pannellum multi-res supports high resolution panoramas by splitting the image into tiles, this crate does the tiling.
  • Pyramid Downscaling: Generates multi-level zoom crops of the panorama that Pannellum loads dynamically.
  • Fast: Over 10x faster than Pannellum's generate.py for (almost) the same functionality.
  • No Hugin/nona dependency: This is a pure rust implementation, no external dependencies needed.

What's this?

This crate acts as a pre-processing pipeline for Pannellum's Multi-resolution (multires) panorama type. Rather than loading a single high-resolution image, which can degrade web client performance or hit browser texture limits, this crate segments the panorama into smaller tiles (e.g., 512x512 pixels) across different resolution tiers. The tile logic is similar to how map tiles work, given that it generates tiles for different zoom levels until the full resolution of the panorama is reached.

What it can do:

  • Generate directory hierarchies containing cropped tiles for each zoom tier (/%l/%s%y_%x).
  • Generate flat, low-resolution fallback cubemaps for fallback paths (/fallback/%s).
  • Compute and output structural constraints to config.json, including:
    • haov, minYaw, maxYaw
    • vaov, minPitch, maxPitch, vOffset
    • northOffset
    • multiRes object parameters (tileResolution, maxLevel, cubeResolution, missingTiles, path, fallbackPath)
  • Compress missing tile grids for partial views using Pannellum's Base83 character set specification.

What it cannot do:

  • It does not generate a perceptual hash, like generate.py does. This hash can be used to show a representation of the panorama while the web browser is loading it.
  • It does not stitch overlapping source images together. The input image must already be a fully stitched panorama.

Concepts

  • Equirectangular vs. Cylindrical: Equirectangular projection maps coordinates linearly across latitude and longitude. Cylindrical projection maps coordinates onto a cylinder, requiring trigonometric scaling of vertical coordinates based on focal length. An equirectangular panorama anyone might be familiar with is Google StreetView.
  • Direct vs. Recursive Downscaling:
    • Direct: Each pyramid tier is scaled down directly from the full-resolution cubemap face. This preserves image sharpness but takes longer.
    • Recursive: Tiers are recursively generated from the level immediately above them. This is faster but can introduce minor interpolation errors over deep zoom pyramids.
  • Base83 Missing Tiles String: For partial panoramas, tiles that contain purely background color are discarded during processing. To prevent Pannellum from throwing 404 HTTP errors trying to fetch these non-existent tiles, their absence is encoded into a Base83 string stored in config.json.

Usage

Minimal config tile generation

If your input image contains Exif/XMP metadata for panoramas, you can run the auto-detect pipeline. The crate will look in the provided image file for panorama related Exif/XMP tags, and use it to automatically set the right configuration. If it can't find the tags it will do a best guess estimation to find the proper configuration.

use panorama_tiler::{OutputConfig, OutputFormat, tile_panorama_with_guessed_angles};
use std::path::Path;

fn main() -> Result<(), panorama_tiler::TilerError> {
    let input_path = Path::new("img/sphere/input_photosphere.jpg");
    let output_dir = Path::new("tiles_output");

    let output_config = OutputConfig {
        format: OutputFormat::Webp, // Supports Jpeg, Png, Webp
        quality: 85,
        ..Default::default()
    };

    tile_panorama_with_guessed_angles(
        input_path,
        output_dir,
        Some(output_config),
    )?;

    Ok(())
}

Manual Configuration

When metadata tags are absent or you need to supply manual field-of-view angles, you can define them explicitly.

use panorama_tiler::{
    TilerConfig, PanoAngles, OutputConfig, Projection, InterpolationMode,
    DownscalingMethod, tile_panorama, OutputFormat
};
use std::path::Path;

fn main() -> Result<(), panorama_tiler::TilerError> {
    let config = TilerConfig {
        angles: PanoAngles {
            haov: 180.0,             // Horizontal Angle of View (degrees)
            vaov: 90.0,              // Vertical Angle of View (degrees)
            v_offset: 5.0,           // Vertical center pitch offset
            projection: Projection::Cylindrical,
            ..Default::default()
        },
        output: OutputConfig {
            tile_size: 512,
            fallback_size: 1024,
            format: OutputFormat::Jpeg,
            quality: 80,
            interpolation_mode: InterpolationMode::Bicubic,
            downscaling_method: DownscalingMethod::Direct,
            ..Default::default()
        },
    };

    tile_panorama(
        Path::new("img/cylinder/input_pano.jpg"),
        Path::new("target/tiles_output"),
        &config,
    )?;

    Ok(())
}

Minimal Frontend Example

Once the tile pyramid is generated, host the target directory containing your tiles and the config.json file. You can then load it into Pannellum with the following HTML.

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Multires Panorama</title>
    <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/pannellum@2.5.7/build/pannellum.css">
    <script src="https://cdn.jsdelivr.net/npm/pannellum@2.5.7/build/pannellum.js"></script>
    <style>
        body {
            margin: 0;
        }

        #panorama {
            width: 100vw;
            height: 100vh;
        }
    </style>
</head>

<body>

<div id="panorama"></div>

<script>
    // Point `BASE_PATH` to the output folder generated
    const BASE_PATH = 'tiles_output'
    fetch(`${BASE_PATH}/config.json`)
            .then(response => response.json())
            .then(config => {
                config.multiRes.basePath = BASE_PATH
                config.autoLoad = true
                pannellum.viewer('panorama', config);
            });
</script>

</body>

</html>

Cargo Features

Feature Default Description Dependencies
metadata Yes Enables metadata reading, parsing GPano and EXIF fields. exif, xmpkit
webp Yes Enables saving processed tiles in WebP format. webp

Performance Measurements

Below is an indicative execution time comparison between the Pannellum's provided generate.py and this Rust implementation on the same equirectangular source image:

Metric Python Script (generate.py) Rust Implementation
Mean Execution 10.380 seconds 1.031 seconds
Median Execution 10.097 seconds 1.028 seconds
Standard Deviation 1.121 seconds 0.008 seconds