Преобразование равноугольной панорамы 2:1 в кубическую карту

В настоящее время я работаю над простой программой просмотра 3D-панорам для веб-сайта. Из соображений производительности мобильных устройств я использую three.js рендерер CSS3. Для этого требуется кубическая карта, разделенная на 6 отдельных изображений.

Я записываю изображения на iPhone с помощью приложения Google Photosphere или аналогичных приложений, которые создают равноугольные панорамы 2:1. Затем я изменяю их размер и конвертирую в кубическую карту с помощью этого веб-сайта: http://gonchar.me/panorama/ (Flash)

Предпочтительно, чтобы я сделал преобразование самостоятельно либо на лету в three.js, если это возможно, либо в Photoshop. Я нашел экшены Photoshop Эндрю Хазелдена, и они кажутся похожими, но прямое преобразование недоступно. Есть ли математический способ конвертировать их или какой-то скрипт, который это делает? Я бы хотел по возможности не использовать 3D-приложение, такое как Blender.

Может быть, это далеко не так, но я подумал, что спрошу. У меня неплохой опыт работы с javascript, но я новичок в three.js. Я также не решаюсь полагаться на функциональность WebGL, поскольку на мобильных устройствах она кажется либо медленной, либо глючной. Поддержка также все еще неравномерна.


person oelna    schedule 16.04.2015    source источник
comment
Есть способы сделать это в javascript, используя CSS или холст. Однако я не уверен, что он совместим с three.js. stackoverflow.com/questions/8912917 /   -  person Salix alba    schedule 16.04.2015
comment
Для этого я создал скрипт на Python github.com/seiferteric/cubemap.   -  person Eric Seifert    schedule 03.03.2017


Ответы (12)


Если вы хотите сделать это на стороне сервера, есть много вариантов. http://www.imagemagick.org/ содержит множество инструментов командной строки, которые могут разрезать ваше изображение на части. . Вы можете поместить команду для этого в сценарий и просто запускать его каждый раз, когда у вас появляется новое изображение.

Трудно сказать, какой алгоритм используется в программе. Мы можем попытаться реконструировать то, что происходит, загрузив в программу квадратную сетку. Я использовал сетку из Википедии

сетка 64 на 64

Что дает проецируемую сеткуЭто дает нам представление о том, как устроена коробка.

Сфера изображения с линиями широты и долготы на ней и окружающий ее куб. Теперь проецирование из точки в центре сферы создаст искаженную сетку на кубе.

Математически возьмем полярные координаты r, θ, ø для сферы r=1, 0 ‹ θ ‹ π, -π/4 ‹ ø ‹ 7π/4

  • x= r sin θ cos ø
  • y= r sin θ sin ø
  • г = г, потому что θ

центрально спроецируйте их на куб. Сначала разделим на четыре области по широте -π/4 ‹ ø ‹ π/4, π/4 ‹ ø ‹ 3π/4, 3π/4 ‹ ø ‹ 5π/4, 5π/4 ‹ ø ‹ 7π/4. Они будут либо проецироваться на одну из четырех сторон сверху или снизу.

Предположим, что мы находимся на первой стороне -π/4 ‹ ø ‹ π/4. Центральная проекция (sin θ cos ø, sin θ sin ø, cos θ) будет (sin θ cos ø, sin θ sin ø, cos θ), которая попадает в плоскость x=1, когда

  • sin θ cos ø = 1

so

  • а = 1 / (sin θ cos ø)

и спроецированная точка

  • (1, tan ø, кроватка θ / cos ø)

Если | раскладушка θ / cos ø | ‹ 1 это будет на лицевой стороне. В противном случае он будет проецироваться сверху или снизу, и для этого вам понадобится другая проекция. Лучший тест для вершины использует тот факт, что минимальное значение cos ø будет cos π/4 = 1/√2, поэтому спроецированная точка всегда будет на вершине, если ctg θ / (1/√2) > 1 или тангенс θ ‹ 1/√2. Это работает как θ ‹ 35º или 0,615 радиан.

Соедините это в питоне

import sys
from PIL import Image
from math import pi,sin,cos,tan

def cot(angle):
    return 1/tan(angle)

# Project polar coordinates onto a surrounding cube
# assume ranges theta is [0,pi] with 0 the north poll, pi south poll
# phi is in range [0,2pi] 
def projection(theta,phi): 
        if theta<0.615:
            return projectTop(theta,phi)
        elif theta>2.527:
            return projectBottom(theta,phi)
        elif phi <= pi/4 or phi > 7*pi/4:
            return projectLeft(theta,phi)
        elif phi > pi/4 and phi <= 3*pi/4:
            return projectFront(theta,phi)
        elif phi > 3*pi/4 and phi <= 5*pi/4:
            return projectRight(theta,phi)
        elif phi > 5*pi/4 and phi <= 7*pi/4:
            return projectBack(theta,phi)

def projectLeft(theta,phi):
        x = 1
        y = tan(phi)
        z = cot(theta) / cos(phi)
        if z < -1:
            return projectBottom(theta,phi)
        if z > 1:
            return projectTop(theta,phi)
        return ("Left",x,y,z)

def projectFront(theta,phi):
        x = tan(phi-pi/2)
        y = 1
        z = cot(theta) / cos(phi-pi/2)
        if z < -1:
            return projectBottom(theta,phi)
        if z > 1:
            return projectTop(theta,phi)
        return ("Front",x,y,z)

def projectRight(theta,phi):
        x = -1
        y = tan(phi)
        z = -cot(theta) / cos(phi)
        if z < -1:
            return projectBottom(theta,phi)
        if z > 1:
            return projectTop(theta,phi)
        return ("Right",x,-y,z)

def projectBack(theta,phi):
        x = tan(phi-3*pi/2)
        y = -1
        z = cot(theta) / cos(phi-3*pi/2)
        if z < -1:
            return projectBottom(theta,phi)
        if z > 1:
            return projectTop(theta,phi)
        return ("Back",-x,y,z)

def projectTop(theta,phi):
        # (a sin θ cos ø, a sin θ sin ø, a cos θ) = (x,y,1)
        a = 1 / cos(theta)
        x = tan(theta) * cos(phi)
        y = tan(theta) * sin(phi)
        z = 1
        return ("Top",x,y,z)

def projectBottom(theta,phi):
        # (a sin θ cos ø, a sin θ sin ø, a cos θ) = (x,y,-1)
        a = -1 / cos(theta)
        x = -tan(theta) * cos(phi)
        y = -tan(theta) * sin(phi)
        z = -1
        return ("Bottom",x,y,z)

# Convert coords in cube to image coords 
# coords is a tuple with the side and x,y,z coords
# edge is the length of an edge of the cube in pixels
def cubeToImg(coords,edge):
    if coords[0]=="Left":
        (x,y) = (int(edge*(coords[2]+1)/2), int(edge*(3-coords[3])/2) )
    elif coords[0]=="Front":
        (x,y) = (int(edge*(coords[1]+3)/2), int(edge*(3-coords[3])/2) )
    elif coords[0]=="Right":
        (x,y) = (int(edge*(5-coords[2])/2), int(edge*(3-coords[3])/2) )
    elif coords[0]=="Back":
        (x,y) = (int(edge*(7-coords[1])/2), int(edge*(3-coords[3])/2) )
    elif coords[0]=="Top":
        (x,y) = (int(edge*(3-coords[1])/2), int(edge*(1+coords[2])/2) )
    elif coords[0]=="Bottom":
        (x,y) = (int(edge*(3-coords[1])/2), int(edge*(5-coords[2])/2) )
    return (x,y)

# convert the in image to out image
def convert(imgIn,imgOut):
    inSize = imgIn.size
    outSize = imgOut.size
    inPix = imgIn.load()
    outPix = imgOut.load()
    edge = inSize[0]/4   # the length of each edge in pixels
    for i in xrange(inSize[0]):
        for j in xrange(inSize[1]):
            pixel = inPix[i,j]
            phi = i * 2 * pi / inSize[0]
            theta = j * pi / inSize[1]
            res = projection(theta,phi)
            (x,y) = cubeToImg(res,edge)
            #if i % 100 == 0 and j % 100 == 0:
            #   print i,j,phi,theta,res,x,y
            if x >= outSize[0]:
                #print "x out of range ",x,res
                x=outSize[0]-1
            if y >= outSize[1]:
                #print "y out of range ",y,res
                y=outSize[1]-1
            outPix[x,y] = pixel

imgIn = Image.open(sys.argv[1])
inSize = imgIn.size
imgOut = Image.new("RGB",(inSize[0],inSize[0]*3/4),"black")
convert(imgIn,imgOut)
imgOut.show()

Функция projection принимает значения theta и phi и возвращает координаты в кубе от -1 до 1 в каждом направлении. cubeToImg берет координаты (x,y,z) и переводит их в координаты выходного изображения.

Приведенный выше алгоритм, похоже, правильно использует геометрию, используя изображение Букингемского дворца, мы получаем < img src="https://i.stack.imgur.com/rfgE8.png" alt="кубическая карта Букингемского дворца"> Это, кажется, дает правильное большинство линий мощения.

Мы получаем несколько артефактов изображения. Это связано с отсутствием карты пикселей 1 к 1. Что нам нужно сделать, так это использовать обратное преобразование. Вместо того, чтобы перебирать каждый пиксель в источнике и находить соответствующий пиксель в цели, мы перебираем целевые изображения и находим ближайший соответствующий исходный пиксель.

import sys
from PIL import Image
from math import pi,sin,cos,tan,atan2,hypot,floor
from numpy import clip

# get x,y,z coords from out image pixels coords
# i,j are pixel coords
# face is face number
# edge is edge length
def outImgToXYZ(i,j,face,edge):
    a = 2.0*float(i)/edge
    b = 2.0*float(j)/edge
    if face==0: # back
        (x,y,z) = (-1.0, 1.0-a, 3.0 - b)
    elif face==1: # left
        (x,y,z) = (a-3.0, -1.0, 3.0 - b)
    elif face==2: # front
        (x,y,z) = (1.0, a - 5.0, 3.0 - b)
    elif face==3: # right
        (x,y,z) = (7.0-a, 1.0, 3.0 - b)
    elif face==4: # top
        (x,y,z) = (b-1.0, a -5.0, 1.0)
    elif face==5: # bottom
        (x,y,z) = (5.0-b, a-5.0, -1.0)
    return (x,y,z)

# convert using an inverse transformation
def convertBack(imgIn,imgOut):
    inSize = imgIn.size
    outSize = imgOut.size
    inPix = imgIn.load()
    outPix = imgOut.load()
    edge = inSize[0]/4   # the length of each edge in pixels
    for i in xrange(outSize[0]):
        face = int(i/edge) # 0 - back, 1 - left 2 - front, 3 - right
        if face==2:
            rng = xrange(0,edge*3)
        else:
            rng = xrange(edge,edge*2)

        for j in rng:
            if j<edge:
                face2 = 4 # top
            elif j>=2*edge:
                face2 = 5 # bottom
            else:
                face2 = face

            (x,y,z) = outImgToXYZ(i,j,face2,edge)
            theta = atan2(y,x) # range -pi to pi
            r = hypot(x,y)
            phi = atan2(z,r) # range -pi/2 to pi/2
            # source img coords
            uf = ( 2.0*edge*(theta + pi)/pi )
            vf = ( 2.0*edge * (pi/2 - phi)/pi)
            # Use bilinear interpolation between the four surrounding pixels
            ui = floor(uf)  # coord of pixel to bottom left
            vi = floor(vf)
            u2 = ui+1       # coords of pixel to top right
            v2 = vi+1
            mu = uf-ui      # fraction of way across pixel
            nu = vf-vi
            # Pixel values of four corners
            A = inPix[ui % inSize[0],clip(vi,0,inSize[1]-1)]
            B = inPix[u2 % inSize[0],clip(vi,0,inSize[1]-1)]
            C = inPix[ui % inSize[0],clip(v2,0,inSize[1]-1)]
            D = inPix[u2 % inSize[0],clip(v2,0,inSize[1]-1)]
            # interpolate
            (r,g,b) = (
              A[0]*(1-mu)*(1-nu) + B[0]*(mu)*(1-nu) + C[0]*(1-mu)*nu+D[0]*mu*nu,
              A[1]*(1-mu)*(1-nu) + B[1]*(mu)*(1-nu) + C[1]*(1-mu)*nu+D[1]*mu*nu,
              A[2]*(1-mu)*(1-nu) + B[2]*(mu)*(1-nu) + C[2]*(1-mu)*nu+D[2]*mu*nu )

            outPix[i,j] = (int(round(r)),int(round(g)),int(round(b)))

imgIn = Image.open(sys.argv[1])
inSize = imgIn.size
imgOut = Image.new("RGB",(inSize[0],inSize[0]*3/4),"black")
convertBack(imgIn,imgOut)
imgOut.save(sys.argv[1].split('.')[0]+"Out2.png")
imgOut.show()

Результатом этого являются Использование обратного преобразования

person Salix alba    schedule 16.04.2015
comment
Да и нарезка не проблема. Сложная часть — это перекос и деформация проекции, поэтому изображение соответствует кубической карте. - person oelna; 16.04.2015
comment
Спасибо за исчерпывающий ответ. Я должен признать, что это немного выше моего понимания, но похоже, что это может сработать. Я немного подумаю об этом, и если у меня получится, я опубликую обновление своего вопроса с подробным описанием результатов. А пока я приму ваш ответ. Спасибо еще раз! - person oelna; 20.04.2015
comment
@Salixalba Как получить тета и фи для пикселя? - person Brans Ds; 16.06.2015
comment
@oelna Ты проверял это? - person Brans Ds; 16.06.2015
comment
@BransDs К сожалению, нет. Как я уже сказал, это сложнее, чем я могу сейчас понять. Я думаю, что ответ все же может помочь другим. Если кто-то действительно использует это и заставляет его работать, я хотел бы услышать отзывы. - person oelna; 16.06.2015
comment
Тета и фи – это в основном широта и долгота. θ=0 — северный полюс, θ=π/2 — экватор и θ=π — южный полюс. Итак, возьмите координату y из входного изображения, разделите на высоту и умножьте на π. Для ø возьмите координату x, разделите на ширину, умножьте на 2π и вычтите на π/4. Гораздо проще было бы просто взять полоску из середины изображения и разделить ее на четыре равные части. Это дало бы четыре стены. Это n=может быть искажено, но приемлемо. - person Salix alba; 16.06.2015
comment
@Salixalba не знает только, что такое функция клипа? это Clip(x, lo, hi) if (x ‹ lo) return lo; если (x › привет) вернуть привет; вернуть х; - person Brans Ds; 17.06.2015
comment
да. Это функция numpy.clip(). Вам не нужна проверка нижней границы. - person Salix alba; 17.06.2015
comment
Вы должны упаковать это и выложить на github. - person freakTheMighty; 29.12.2015
comment
Я заменил numpy.clip чистым python и улучшил (от 130 до 50 секунд) производительность (numpy.clip для списка чисел), например: A = inPix[int(ui% inSize[0]), sorted([0 , vi, inSize[1]-1])[1]] - person Pooya Mobasher Behrooz; 16.02.2017
comment
Во время работы над проектом я реализовал этот алгоритм как отдельное приложение на C++ =› github.com/denivip/panorama< /а> - person Denis Bulichenko; 13.03.2017

Учитывая отличный принятый ответ, я хотел добавить свою соответствующую реализацию C++ на основе OpenCV.

Для тех, кто не знаком с OpenCV, подумайте о Mat как об изображении. Сначала мы создаем две карты, которые преобразуют равнопрямоугольное изображение в нашу соответствующую грань кубической карты. Затем мы делаем тяжелую работу (т.е. переназначаем с интерполяцией) с помощью OpenCV.

Код можно сделать более компактным, если читабельность не имеет значения.

// Define our six cube faces. 
// 0 - 3 are side faces, clockwise order
// 4 and 5 are top and bottom, respectively
float faceTransform[6][2] = 
{ 
    {0, 0},
    {M_PI / 2, 0},
    {M_PI, 0},
    {-M_PI / 2, 0},
    {0, -M_PI / 2},
    {0, M_PI / 2}
};

// Map a part of the equirectangular panorama (in) to a cube face
// (face). The ID of the face is given by faceId. The desired
// width and height are given by width and height. 
inline void createCubeMapFace(const Mat &in, Mat &face, 
        int faceId = 0, const int width = -1, 
        const int height = -1) {

    float inWidth = in.cols;
    float inHeight = in.rows;

    // Allocate map
    Mat mapx(height, width, CV_32F);
    Mat mapy(height, width, CV_32F);

    // Calculate adjacent (ak) and opposite (an) of the
    // triangle that is spanned from the sphere center 
    //to our cube face.
    const float an = sin(M_PI / 4);
    const float ak = cos(M_PI / 4);

    const float ftu = faceTransform[faceId][0];
    const float ftv = faceTransform[faceId][1];

    // For each point in the target image, 
    // calculate the corresponding source coordinates. 
    for(int y = 0; y < height; y++) {
        for(int x = 0; x < width; x++) {

            // Map face pixel coordinates to [-1, 1] on plane
            float nx = (float)y / (float)height - 0.5f;
            float ny = (float)x / (float)width - 0.5f;

            nx *= 2;
            ny *= 2;

            // Map [-1, 1] plane coords to [-an, an]
            // thats the coordinates in respect to a unit sphere 
            // that contains our box. 
            nx *= an; 
            ny *= an; 

            float u, v;

            // Project from plane to sphere surface.
            if(ftv == 0) {
                // Center faces
                u = atan2(nx, ak);
                v = atan2(ny * cos(u), ak);
                u += ftu; 
            } else if(ftv > 0) { 
                // Bottom face 
                float d = sqrt(nx * nx + ny * ny);
                v = M_PI / 2 - atan2(d, ak);
                u = atan2(ny, nx);
            } else {
                // Top face
                float d = sqrt(nx * nx + ny * ny);
                v = -M_PI / 2 + atan2(d, ak);
                u = atan2(-ny, nx);
            }

            // Map from angular coordinates to [-1, 1], respectively.
            u = u / (M_PI); 
            v = v / (M_PI / 2);

            // Warp around, if our coordinates are out of bounds. 
            while (v < -1) {
                v += 2;
                u += 1;
            } 
            while (v > 1) {
                v -= 2;
                u += 1;
            } 

            while(u < -1) {
                u += 2;
            }
            while(u > 1) {
                u -= 2;
            }

            // Map from [-1, 1] to in texture space
            u = u / 2.0f + 0.5f;
            v = v / 2.0f + 0.5f;

            u = u * (inWidth - 1);
            v = v * (inHeight - 1);

            // Save the result for this pixel in map
            mapx.at<float>(x, y) = u;
            mapy.at<float>(x, y) = v; 
        }
    }

    // Recreate output image if it has wrong size or type. 
    if(face.cols != width || face.rows != height || 
        face.type() != in.type()) {
        face = Mat(width, height, in.type());
    }

    // Do actual resampling using OpenCV's remap
    remap(in, face, mapx, mapy, 
         CV_INTER_LINEAR, BORDER_CONSTANT, Scalar(0, 0, 0));
}

Учитывая следующий ввод:

введите описание изображения здесь

Создаются следующие лица:

введите описание изображения здесь

Изображение предоставлено Optonaut.

person Emiswelt    schedule 11.01.2016
comment
Привет! Ваша программа верна, но в OpenCV x и y в .at(x,y) меняются местами, нет? Должно быть mapx.at<float>(y, x) = u; mapy.at<float>(y, x) = v; - person Raph Schim; 13.02.2018
comment
это не проблема, если ширина = высота, но это может привести к ошибкам ... Но если вы замените x на y, ваше изображение будет повернуто :) Тем не менее, это работает очень хорошо, спасибо! - person Raph Schim; 13.02.2018
comment
это цилиндрическая -> кубическая карта, а не равнопрямоугольная.. поэтому на верхней и нижней полюсах кубической карты есть черные кружки.. потому что цилиндрическая проекция не имеет верхних и нижних данных - person David Jeske; 01.07.2020
comment
@DavidJeske Это определенно равнопрямоугольный. На входной панораме (из моей коллекции) просто отсутствуют верхняя и нижняя части. Если запустить код с полной панорамой, дырок не будет. - person Emiswelt; 05.07.2020

ОБНОВЛЕНИЕ 2: похоже, что кто-то другой уже создал гораздо более совершенную веб-приложение, чем мое собственное. Их преобразование выполняется на стороне клиента, поэтому не нужно беспокоиться о загрузке/выгрузке.

Я полагаю, если вы по какой-то причине ненавидите JS или пытаетесь сделать это на своем мобильном телефоне, то мое веб-приложение ниже подойдет.

ОБНОВЛЕНИЕ: я опубликовал простое веб-приложение, в котором может загрузить панораму и вернуть 6 изображений скайбокса в zip-архиве.

Исходный код представляет собой переработанную реализацию того, что показано ниже, и доступен на Github.

В настоящее время приложение работает на одном динамометрическом стенде Heroku бесплатного уровня, не пытайтесь использовать его в качестве API. Если вам нужна автоматизация, сделайте собственное развертывание; доступно развертывание в Heroku одним щелчком мыши.

ОРИГИНАЛ: вот (наивно) модифицированная версия Абсолютно фантастический ответ Salix Alba, который преобразует одно лицо за раз, создает шесть разных изображений и сохраняет исходный тип файла изображения.

Помимо того факта, что в большинстве вариантов использования, вероятно, предполагается шесть отдельных изображений, основным преимуществом преобразования одного лица за раз является то, что при работе с огромными изображениями требуется гораздо меньше памяти.

#!/usr/bin/env python
import sys
from PIL import Image
from math import pi, sin, cos, tan, atan2, hypot, floor
from numpy import clip

# get x,y,z coords from out image pixels coords
# i,j are pixel coords
# faceIdx is face number
# faceSize is edge length
def outImgToXYZ(i, j, faceIdx, faceSize):
    a = 2.0 * float(i) / faceSize
    b = 2.0 * float(j) / faceSize

    if faceIdx == 0: # back
        (x,y,z) = (-1.0, 1.0 - a, 1.0 - b)
    elif faceIdx == 1: # left
        (x,y,z) = (a - 1.0, -1.0, 1.0 - b)
    elif faceIdx == 2: # front
        (x,y,z) = (1.0, a - 1.0, 1.0 - b)
    elif faceIdx == 3: # right
        (x,y,z) = (1.0 - a, 1.0, 1.0 - b)
    elif faceIdx == 4: # top
        (x,y,z) = (b - 1.0, a - 1.0, 1.0)
    elif faceIdx == 5: # bottom
        (x,y,z) = (1.0 - b, a - 1.0, -1.0)

    return (x, y, z)

# convert using an inverse transformation
def convertFace(imgIn, imgOut, faceIdx):
    inSize = imgIn.size
    outSize = imgOut.size
    inPix = imgIn.load()
    outPix = imgOut.load()
    faceSize = outSize[0]

    for xOut in xrange(faceSize):
        for yOut in xrange(faceSize):
            (x,y,z) = outImgToXYZ(xOut, yOut, faceIdx, faceSize)
            theta = atan2(y,x) # range -pi to pi
            r = hypot(x,y)
            phi = atan2(z,r) # range -pi/2 to pi/2

            # source img coords
            uf = 0.5 * inSize[0] * (theta + pi) / pi
            vf = 0.5 * inSize[0] * (pi/2 - phi) / pi

            # Use bilinear interpolation between the four surrounding pixels
            ui = floor(uf)  # coord of pixel to bottom left
            vi = floor(vf)
            u2 = ui+1       # coords of pixel to top right
            v2 = vi+1
            mu = uf-ui      # fraction of way across pixel
            nu = vf-vi

            # Pixel values of four corners
            A = inPix[ui % inSize[0], clip(vi, 0, inSize[1]-1)]
            B = inPix[u2 % inSize[0], clip(vi, 0, inSize[1]-1)]
            C = inPix[ui % inSize[0], clip(v2, 0, inSize[1]-1)]
            D = inPix[u2 % inSize[0], clip(v2, 0, inSize[1]-1)]

            # interpolate
            (r,g,b) = (
              A[0]*(1-mu)*(1-nu) + B[0]*(mu)*(1-nu) + C[0]*(1-mu)*nu+D[0]*mu*nu,
              A[1]*(1-mu)*(1-nu) + B[1]*(mu)*(1-nu) + C[1]*(1-mu)*nu+D[1]*mu*nu,
              A[2]*(1-mu)*(1-nu) + B[2]*(mu)*(1-nu) + C[2]*(1-mu)*nu+D[2]*mu*nu )

            outPix[xOut, yOut] = (int(round(r)), int(round(g)), int(round(b)))

imgIn = Image.open(sys.argv[1])
inSize = imgIn.size
faceSize = inSize[0] / 4
components = sys.argv[1].rsplit('.', 2)

FACE_NAMES = {
  0: 'back',
  1: 'left',
  2: 'front',
  3: 'right',
  4: 'top',
  5: 'bottom'
}

for face in xrange(6):
  imgOut = Image.new("RGB", (faceSize, faceSize), "black")
  convertFace(imgIn, imgOut, face)
  imgOut.save(components[0] + "_" + FACE_NAMES[face] + "." + components[1])
person Benjamin Dobell    schedule 02.05.2016

Я написал скрипт для разрезания сгенерированной кубической карты на отдельные файлы (posx.png, negx.png, posy.png, negy.png, posz.png и negz.png). Он также упакует 6 файлов в файл .zip.

Источник находится здесь: https://github.com/dankex/compv/blob/master/3d-graphics/skybox/cubemap-cut.py

Вы можете изменить массив, чтобы установить файлы изображений:

name_map = [ \
 ["", "", "posy", ""],
 ["negz", "negx", "posz", "posx"],
 ["", "", "negy", ""]]

Преобразованные файлы:

введите описание изображения здесь введите здесь описание изображения < img src="https://i.stack.imgur.com/I3qMC.png" alt="введите здесь описание изображения"> введите здесь описание изображения введите здесь описание изображения введите здесь описание изображения

person Danke Xie    schedule 22.08.2015

Нашел этот вопрос, и хотя ответы хорошие, я думаю, что еще есть некоторая информация, так что вот мои два цента.

Во-первых: если вам действительно не нужно конвертировать изображения самостоятельно (например, из-за каких-то конкретных требований к программному обеспечению), не делайте этого.

Причина в том, что, несмотря на то, что существует очень простое сопоставление между равнопрямоугольной проекцией и кубической проекцией, сопоставление между областями не является простым: когда вы устанавливаете соответствие между определенной точкой вашего целевого изображения и точку в исходнике с помощью элементарного вычисления, как только вы конвертируете обе точки в пиксели путем округления, вы делаете очень грубое приближение, которое не учитывает размер пикселей, и качество изображения будет низким.

Во-вторых: даже если вам нужно выполнить преобразование во время выполнения, уверены ли вы, что вам вообще нужно выполнять преобразование? Если нет какой-то очень серьезной проблемы с производительностью, если вам просто нужен скайбокс, создайте очень большую сферу, нашейте на нее равнопрямоугольную текстуру и вперед. Три JS уже обеспечивают сферу, насколько я помню ;-)

Третье: НАСА предоставляет инструмент для преобразования между всеми мыслимыми проекциями (я только что узнал, протестировал его, и он отлично работает). Вы можете найти это здесь:

G.Projector — проектор глобальных карт

и я считаю разумным думать, что ребята знают, что делают ;-)

Надеюсь это поможет

ОБНОВЛЕНИЕ: оказывается, что "ребята" до определенного момента знают, что они делают: сгенерированная кубическая карта имеет отвратительную границу, что делает преобразование не таким уж простым...

ОБНОВЛЕНИЕ 2: найден окончательный инструмент для преобразования равнопрямоугольной карты в кубическую, и он называется erect2cubic.

Это небольшая утилита, которая генерирует сценарий для Hugin следующим образом:

$ erect2cubic --erect=input.png --ptofile=cube.pto
$ nona -o cube_prefix cube.pto 

(информация взята со страницы Vinay's Hacks)

и сгенерирует все 6 граней кубической карты. Я использую его для своего проекта, и он прекрасно работает!

Единственным недостатком такого подхода является то, что скрипта erect2cubit нет в стандартном дистрибутиве Ubuntu (который я использую), и мне пришлось прибегнуть к инструкциям по этой ссылке:

Блог, описывающий, как установить и использовать erect2cubic

чтобы узнать, как его установить.

Оно того стоит!

person Rick77    schedule 28.08.2015
comment
Я не могу поверить, насколько плохи и erect2cubic, и nona. Оба завершатся без ошибок, если не смогут прочитать ни один из исходных файлов. erect2cubic невозможно запустить в Windows, и для его запуска по-прежнему требуется руководство даже в Linux. нона ничего бы не сделала без формы, сообщения или ошибки, потому что в исходном файле был дополнительный канал. - person Alexander Gräf; 26.09.2020

cmft Studio поддерживает conversion/filtering различных HDR/LDR проекций до cubemaps.

https://github.com/dariomanesku/cmftStudio

person planetboy    schedule 28.04.2016

Вот JavaScript-версия кода Бенджамна Добелла. convertFace необходимо передать два объекта ìmageData и идентификатор лица (0-6).

Предоставленный код можно безопасно использовать в веб-воркере, поскольку он не имеет зависимостей.

        // convert using an inverse transformation
        function convertFace(imgIn, imgOut, faceIdx) {
            var inPix = shimImgData(imgIn),
                        outPix = shimImgData(imgOut),
                        faceSize = imgOut.width,
                        pi = Math.PI,
                        pi_2 = pi/2;

            for(var xOut=0;xOut<faceSize;xOut++) {
                    for(var yOut=0;yOut<faceSize;yOut++) {

                    var xyz = outImgToXYZ(xOut, yOut, faceIdx, faceSize);
                    var theta = Math.atan2(xyz.y, xyz.x); // range -pi to pi
                    var r = Math.hypot(xyz.x,xyz.y);
                    var phi = Math.atan2(xyz.z,r); // range -pi/2 to pi/2

                    // source img coords
                    var uf = 0.5 * imgIn.width * (theta + pi) / pi;
                    var vf = 0.5 * imgIn.width * (pi_2 - phi) / pi;

                    // Use bilinear interpolation between the four surrounding pixels
                    var ui = Math.floor(uf);  // coord of pixel to bottom left
                    var vi = Math.floor(vf);
                    var u2 = ui+1;       // coords of pixel to top right
                    var v2 = vi+1;
                    var mu = uf-ui;      // fraction of way across pixel
                    var nu = vf-vi;

                    // Pixel values of four corners
                    var A = inPix.getPx(ui % imgIn.width, clip(vi, 0, imgIn.height-1));
                    var B = inPix.getPx(u2 % imgIn.width, clip(vi, 0, imgIn.height-1));
                    var C = inPix.getPx(ui % imgIn.width, clip(v2, 0, imgIn.height-1));
                    var D = inPix.getPx(u2 % imgIn.width, clip(v2, 0, imgIn.height-1));

                    // interpolate
                    var rgb = {
                      r:A[0]*(1-mu)*(1-nu) + B[0]*(mu)*(1-nu) + C[0]*(1-mu)*nu+D[0]*mu*nu,
                      g:A[1]*(1-mu)*(1-nu) + B[1]*(mu)*(1-nu) + C[1]*(1-mu)*nu+D[1]*mu*nu,
                      b:A[2]*(1-mu)*(1-nu) + B[2]*(mu)*(1-nu) + C[2]*(1-mu)*nu+D[2]*mu*nu
                    };

                    rgb.r=Math.round(rgb.r);
                    rgb.g=Math.round(rgb.g);
                    rgb.b=Math.round(rgb.b);

                    outPix.setPx(xOut, yOut, rgb);

                } // for(var yOut=0;yOut<faceSize;yOut++) {...}
             } // for(var xOut=0;xOut<faceSize;xOut++) {...}
        } // function convertFace(imgIn, imgOut, faceIdx) {...}

        // get x,y,z coords from out image pixels coords
        // i,j are pixel coords
        // faceIdx is face number
        // faceSize is edge length
        function outImgToXYZ(i, j, faceIdx, faceSize) {
            var a = 2 * i / faceSize,
                    b = 2 * j / faceSize;

            switch(faceIdx) {
                case 0: // back
                return({x:-1, y:1-a, z:1-b});
            case 1: // left
                return({x:a-1, y:-1, z:1-b});
            case 2: // front
                return({x: 1, y:a-1, z:1-b});
            case 3: // right
                return({x:1-a, y:1, z:1-b});
            case 4: // top
                return({x:b-1, y:a-1, z:1});
            case 5: // bottom
                return({x:1-b, y:a-1, z:-1});

            }
        } // function outImgToXYZ(i, j, faceIdx, faceSize) {...}

        function clip(val, min, max) {
            return(val<min?min:(val>max?max:val));
        }

        function shimImgData(imgData) {
            var w=imgData.width*4,
                    d=imgData.data;

            return({
                getPx:function(x,y) {
                    x=x*4+y*w;
                    return([ d[x], d[x+1], d[x+2] ]);
                },
                setPx:function(x,y,rgb) {
                    x=x*4+y*w;
                    d[x]=rgb.r;
                    d[x+1]=rgb.g;
                    d[x+2]=rgb.b;
                    d[x+3]=255; // alpha
                }
            });
        } // function shimImgData(imgData) {...}
person knee-cola    schedule 18.04.2017

Существуют различные представления карт окружения. Вот хороший обзор.

Обзор — панорамные изображения

Если вы используете Photosphere (или любое другое приложение для создания панорам), скорее всего, у вас уже есть горизонтальная широта / представление долготы. Затем вы можете просто нарисовать текстурированный файл three.js SphereGeometry. Вот учебник о том, как визуализировать землю.

Учебник — Как сделать Землю в WebGL?

Удачи :).

person Tobias Gurdan    schedule 26.07.2015

Очень простое приложение C++, которое преобразует равнопрямоугольную панораму в кубическую карту на основе ответа Саликс Альба => https://github.com/denivip/panorama

person Denis Bulichenko    schedule 13.03.2017
comment
Возникла проблема с одним изображением лица. пожалуйста, проверьте эту проблему в git github.com/denivip/panorama/issues/6 - person gadlol; 22.12.2018
comment
@gadlol да, видел уведомление. Спасибо, что заметили! После Рождества посмотрю :-) - person Denis Bulichenko; 23.12.2018

Я создал решение этой проблемы с помощью OpenGL и сделал вокруг него инструмент командной строки. Он работает как с изображениями, так и с видео, и это самый быстрый инструмент, который я там нашел.

Convert360 — проект на GitHub.

Шейдер OpenGL — фрагментный шейдер, используемый для -проекция.

Использование так же просто, как:

$ pip install convert360
$ convert360 -i ~/Pictures/Barcelona/sagrada-familia.jpg -o example.png -s 300 300

Чтобы получить что-то вроде этого:

введите здесь описание изображения

person Mateus Zitelli    schedule 24.06.2017

Возможно, я что-то упускаю здесь. Но кажется, что большинство, если не весь представленный код преобразования, могут быть несколько неправильными. Они берут сферическую панораму (равнопрямоугольную --- 360 градусов по горизонтали и 180 градусов по вертикали) и, похоже, преобразуют ее в грани куба с помощью декартова преобразования ‹-> цилиндра. Если они не используют декартово ‹-> сферическое преобразование. См. http://mathworld.wolfram.com/SphericalCoordinates.html.

Я предполагаю, что пока они реверсируют расчет, чтобы перейти от граней куба к панораме, тогда это должно сработать. А вот изображения граней куба могут немного отличаться при использовании сферического преобразования.

Если я начну с этой равноугольной (сферической панорамы):

введите здесь описание изображения

Затем, если я использую цилиндрическое преобразование (в котором я не уверен на 100% в настоящее время), я получаю такой результат:

введите здесь описание изображения

Но если я использую сферическое преобразование, я получаю такой результат:

введите здесь описание изображения

Они не то же самое. Но мой результат сферического преобразования, кажется, соответствует результату Данке Се, но его ссылка не показывает тип преобразования, который он использует, насколько я могу его прочитать.

Значит, я неправильно понимаю код, используемый многими участниками этой темы?

person fmw42    schedule 20.08.2017
comment
У меня такая же проблема, и мне интересно, являются ли это исходными изображениями. Тем не менее, те изображения, которые имеют проблемы, правильно обрабатываются, например, сферой2cube. Вы добились дальнейшего прогресса в этом? - person Longmang; 24.02.2020
comment
@ Longmang. Нет, у меня есть скрипт, который выполняет сферическое преобразование, и, похоже, он работает. Я до сих пор не понимаю тех ссылок, которые утверждают, что используют цилиндрический и называют его сферическим. - person fmw42; 24.02.2020
comment
не могли бы вы дать мне ваш сценарий? Мои знания OpenCV и т. д. слабы, поэтому я на грани своих возможностей. - person Longmang; 25.02.2020
comment
Мой сценарий представляет собой сценарий оболочки bash Unix, работающий с ImageMagick, а не с OpenCV. Он доступен для некоммерческого использования без лицензии на странице fmwconcepts.com/imagemagick. - person fmw42; 25.02.2020

kubi может преобразовывать равнопрямоугольное изображение в грани куба. Я написал это, чтобы быть быстрым и гибким. Он предоставляет опции для выбора макета вывода (6 отдельных изображений по умолчанию) и выбора метода повторной выборки.

person indus    schedule 18.06.2021