mpetroff / pannellum

Pannellum is a lightweight, free, and open source panorama viewer for the web.
https://pannellum.org/
MIT License
4.21k stars 717 forks source link

Feature Request - multiview from cubic #76

Closed trumpton closed 8 years ago

trumpton commented 9 years ago

Is it possible to use generate.py in such a way that it can create multiview images from a series of 6 cubic images. This would enable the tripod to be photoshopped out, for example in just one place. Ta, Steve C

mpetroff commented 9 years ago

The generate.py script only takes equirectangular images as input. All the panorama stitching software I'm familiar with support this as an output option.

trumpton commented 9 years ago

Thanks for a quick reply, and thanks also for producing an excellent application!!!!!

I started out using SaladoPlayer, but found it was restrictive due to being based on old technology - the one feature I did find useful though was the fact that SaladoConverter could do the following translations:

It was the step via the cubic to deep-zoom (their equivalent of multiview) that I found useful when photoshopping out the tripod.

I guess that this could also be the reason others have been asking about overlays to hide the tripod, and importing deep-zoom cubics from Salado.

Again, thanks for a great application!!!

Steve C

On Monday 01 June 2015 17:16:37 Matthew Petroff wrote:

The generate.py script only takes equirectangular images as input. All the panorama stitching software I'm familiar with support this as an output option.


Reply to this email directly or view it on GitHub: https://github.com/mpetroff/pannellum/issues/76#issuecomment-107751457

mpetroff commented 9 years ago

Personally, I stitch a rectilinear view of the nadir, remove the tripod from it, and add it to the stitching project before stitching the final equirectangular image. Obviously, everyone's workflow is different. While I could add cubic input support, I'm not sure I see a whole lot of reason to, when other tools easily convert a cubic panorama to an equirectangular one, such as Bruno Postle's excellent Panotools-Script with its cubic2erect script. However, it's something I'll probably add if I ever write a GUI for the multires conversion.

trumpton commented 9 years ago

Thanks for this. I'll look into the script you mentioned an change the way i work. Best regards Steve.

Sent from my Samsung device

-------- Original message -------- From: Matthew Petroff notifications@github.com Date: 2015/06/02 18:40 (GMT+01:00) To: mpetroff/pannellum pannellum@noreply.github.com Cc: trumpton trumpton14@trumpton.org.uk Subject: Re: [pannellum] Feature Request - multiview from cubic (#76)

Personally, I stitch a rectilinear view of the nadir, remove the tripod from it, and add it to the stitching project before stitching the final equirectangular image. Obviously, everyone's workflow is different. While I could add cubic input support, I'm not sure I see a whole lot of reason to, when other tools easily convert a cubic panorama to an equirectangular one, such as Bruno Postle's excellent Panotools-Script with its cubic2erect script. However, it's something I'll probably add if I ever write a GUI for the multires conversion.

— Reply to this email directly or view it on GitHub.

trumpton commented 9 years ago

Hi,

I've managed to achieve what I needed, and would like to share the tweaks. Essentially, I've added a couple of switches to multires, the first is --r2c, which produces the TIF files for the cubic, and stops. The second is --c2mv, which takes the TIF files, and completes the multi-view. If you use either of these options, the TIF files are not deleted (the assumption is that you would be editing them after --r2c).

I've attached the code below.

Again, thanks for a great tool!

Steve C



#!/usr/bin/env python3

# Requires Python 3.2+ (or Python 2.7)

# generate.py - A multires tile set generator for Pannellum
# Copyright (c) 2014-2015 Matthew Petroff

# 20150608 - Modified to enable 2-stage production (to cubic) and to multi

# 
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
# 
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
# 
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.

from __future__ import print_function

import argparse
from PIL import Image
import os
import sys
import math
from distutils.spawn import find_executable
import subprocess

# find external programs
nona = find_executable('nona')

# Parse input
parser = argparse.ArgumentParser(description='Generate a Pannellum multires tile set from an full equirectangular panorama.',
                                 formatter_class=argparse.ArgumentDefaultsHelpFormatter)
parser.add_argument('inputFile', metavar='INPUT',
                    help='full equirectangular panorama to be processed')
parser.add_argument('-o', '--output', dest='output', default='./output',
                    help='output directory')
parser.add_argument('-s', '--tilesize', dest='tileSize', default=512, type=int,
                    help='tile size in pixels')
parser.add_argument('--png', action='store_true',
                    help='output PNG tiles instead of JPEG tiles')
parser.add_argument('--r2c', action='store_true',
            help='perform first stage only (rectiliniar to cubic)')
parser.add_argument('--c2mr', action='store_true',
            help='perform second stage only (cubic to multires)')
parser.add_argument('-n', '--nona', default=nona, required=nona is None,
                    metavar='EXECUTABLE',
                    help='location of the nona executable to use')
args = parser.parse_args()

# Process input image information
print('Processing input image information...')
origWidth, origHeight = Image.open(args.inputFile).size
if float(origWidth) / origHeight != 2:
    print('Error: the image width is not twice the image height.')
    print('Input image must be a full, not partial, equirectangular panorama!')
    sys.exit(1)
cubeSize = 8 * int(origWidth / 3.14159265 / 8)
levels = int(math.ceil(math.log(float(cubeSize) / args.tileSize, 2))) + 1
origHeight = str(origHeight)    
origWidth = str(origWidth)
origFilename = os.path.join(os.getcwd(), args.inputFile)
extension = '.jpg'
if args.png:
    extension = '.png'
faceLetters = ['f', 'b', 'u', 'd', 'l', 'r']
faces = ['face0000.tif', 'face0001.tif', 'face0002.tif', 'face0003.tif', 'face0004.tif', 'face0005.tif']

if (args.r2c or (not args.c2mv) ):

    # Create output directory
    os.makedirs(args.output)

    # Generate PTO file for nona to generate cube faces
    # Face order: front, back, up, down, left, right
    text = []
    text.append('p E0 R0 f0 h' + str(cubeSize) + ' n"TIFF_m" u0 v90 w' + str(cubeSize))
    text.append('m g1 i0 m2 p0.00784314')
    text.append('i a0 b0 c0 d0 e0 f4 h' + origHeight + ' n"' + origFilename + '" p0 r0 v360 w' + origWidth + ' y0')
    text.append('i a0 b0 c0 d0 e0 f4 h' + origHeight + ' n"' + origFilename + '" p0 r0 v360 w' + origWidth + ' y180')
    text.append('i a0 b0 c0 d0 e0 f4 h' + origHeight + ' n"' + origFilename + '" p-90 r0 v360 w' + origWidth + ' y0')
    text.append('i a0 b0 c0 d0 e0 f4 h' + origHeight + ' n"' + origFilename + '" p90 r0 v360 w' + origWidth + ' y0')
    text.append('i a0 b0 c0 d0 e0 f4 h' + origHeight + ' n"' + origFilename + '" p0 r0 v360 w' + origWidth + ' y90')
    text.append('i a0 b0 c0 d0 e0 f4 h' + origHeight + ' n"' + origFilename + '" p0 r0 v360 w' + origWidth + ' y-90')
    text.append('v')
    text.append('*')
    text = '\n'.join(text)
    with open(os.path.join(args.output, 'cubic.pto'), 'w') as f:
        f.write(text)

    # Create cube faces
    print('Generating cube faces...')
    subprocess.check_call([args.nona, '-o', os.path.join(args.output, 'face'), os.path.join(args.output, 'cubic.pto')])

if ( (not args.r2c) or args.c2mv ):

    # Generate tiles
    print('Generating tiles...')
    for f in range(0, 6):
        size = cubeSize
        face = Image.open(os.path.join(args.output, faces[f]))
        for level in range(levels, 0, -1):
            if not os.path.exists(os.path.join(args.output, str(level))):
                os.makedirs(os.path.join(args.output, str(level)))
            tiles = int(math.ceil(float(size) / args.tileSize))
            if (level < levels):
                face = face.resize([size, size], Image.ANTIALIAS)
            for i in range(0, tiles):
                for j in range(0, tiles):
                    left = j * args.tileSize
                    upper = i * args.tileSize
                    right = min(j * args.tileSize + args.tileSize, size)
                    lower = min(i * args.tileSize + args.tileSize, size)
                    tile = face.crop([left, upper, right, lower])
                    tile.load()
                    tile.save(os.path.join(args.output, str(level), faceLetters[f] + str(i) + '_' + str(j) + extension))
            size = int(size / 2)

    # Generate fallback tiles
    print('Generating fallback tiles...')
    for f in range(0, 6):
        if not os.path.exists(os.path.join(args.output, 'fallback')):
            os.makedirs(os.path.join(args.output, 'fallback'))
        face = Image.open(os.path.join(args.output, faces[f]))
        face = face.resize([1024, 1024], Image.ANTIALIAS)
        # Create 1px border by duplicating border pixels
        out = Image.new(face.mode, (1026, 1026))
        out.paste(face, (0, 1))
        out.paste(face, (2, 1))
        out.paste(face, (1, 0))
        out.paste(face, (1, 2))
        out.putpixel((0, 0), out.getpixel((1, 0)))
        out.putpixel((1025, 0), out.getpixel((1025, 1)))
        out.putpixel((1025, 1025), out.getpixel((1024, 1025)))
        out.putpixel((0, 1025), out.getpixel((0, 1024)))
        out.paste(face, (1, 1))
        out.save(os.path.join(args.output, 'fallback', faceLetters[f] + extension))

    # Generate config file
    text = []
    text.append('{')
    text.append('    "type": "multires",')
    text.append('    ')
    text.append('    "multiRes": {')
    text.append('        "path": "./%l/%s%y_%x",')
    text.append('        "fallbackPath": "./fallback/%s",')
    text.append('        "extension": "' + extension[1:] + '",')
    text.append('        "tileResolution": ' + str(args.tileSize) + ',')
    text.append('        "maxLevel": ' + str(levels) + ',')
    text.append('        "cubeResolution": ' + str(cubeSize))
    text.append('    }')
    text.append('}')
    text = '\n'.join(text)
    with open(os.path.join(args.output, 'config.json'), 'w') as f:
        f.write(text)

if (not (args.r2c or args.c2mv)):   

    # Clean up temporary files
    os.remove(os.path.join(args.output, 'cubic.pto'))
    for face in faces:
        os.remove(os.path.join(args.output, face))