用 HTC Vive 看 Cardboard Camera 的照片

Google 在 Android/iOS 雙平台推出的 Cardboard Camera,算是目前看過效果最好的360立體照片軟體之一,而且拍攝極其方便簡單,就可以產生出約10000像素寬的高解析立體照片。
問題是,這照片只能在手機自己的APP看,從手機的相簿或者傳到電腦上,會發現只有一張照片。

這問題在 Vector Cult VR 的文章 有很詳盡的解釋。

圖檔結構跟解出原始檔

原來右眼的照片跟錄製時的環境音檔都存在這張Jpg的中繼資料裡面,XMP竟然可以保存這麼多格式。
知道原理後,便著手開始寫轉換的script,取得jpg中所需的資料,包括右眼圖檔、上下的裁高,把上下不足的地方補回去打糊,就像手機APP觀看的方式一樣。

Vector Cult 在文章所使用提取Jpg中繼資料的方法是用 Python XMP Toolkit,但是這模組在windows無法編譯,所以轉個彎採exiftool用subprocess方式去接收資料。

因為想以後可以直接拖曳想要轉檔的jpg到程式就可以直接轉,所以這邊寫了一個bat,來跟py做連結,畢竟沒辦法直接把檔案拖曳到py上。

bat這邊就很簡單的寫上一行:

python "%~dp0cb.py" %~dp0 %*

來把bat所在路徑跟拖曳檔案的資訊傳到py裡。

取得所有圖檔所在後便開始批次處理,首先用exiftool取得拍攝的細節資訊,了解到上下被裁切了多少,這些資訊等等圖像處理時會用到。
另外也用exiftool -b的方式把圖檔的右眼照片提取出來,提出來會是binary資料,用stringIO暫存下來給pillow用,就不用還另外存一個jpg檔。
值得注意的是,提出的binary資料用base64解碼預設可能會有padding的問題,所以另外弄了一個function補齊padding。

import subprocess, os, json, itertools, sys
from base64 import b64decode
from cStringIO import StringIO
from PIL import Image, ImageFilter, ImageDraw


def decode_base64(data):
    missing_padding = len(data) % 4
    if missing_padding != 0:
        data += b'='* (4 - missing_padding)
    return b64decode(data)

def exiftool(cmd):
    process = subprocess.Popen("exiftool "+cmd, stdout=subprocess.PIPE, shell=True)
    process_data = process.stdout.read()
    process.kill()
    return process_data.strip()

output_dir = sys.argv[1] + "output"

if not os.path.exists(output_dir):
    os.makedirs(output_dir)

jpg_list = sys.argv[2:]

for idx, oi in enumerate(jpg_list):

    print "====== Start Processing {0} ({1}/{2}) ======".format(oi, idx+1, len(jpg_list))


    #Get Image Information
    print "Fetch Image Information ... "
    meta = json.loads(exiftool("-G -j -sort {0}".format(oi)).decode("utf-8").rstrip("\r\n"))[0]
    i_w = int(meta[u"XMP:FullPanoWidthPixels"])
    i_h = int(i_w/2)
    c_h = int(meta[u"XMP:CroppedAreaImageHeightPixels"])
    c_t = int(meta[u"XMP:CroppedAreaTopPixels"])
    c_b = i_h-c_h-c_t


    #Extract Right Eye Image
    print "Extract Right Eye Image ... "
    r_data = exiftool("{0} -XMP-GImage:Data -b".format(oi))
    ri = StringIO(decode_base64(r_data))


    #Image Setting
    lm = Image.open(oi)
    rm = Image.open(ri)
    main = Image.new("RGB", (i_w, i_h*2))

    im_list = []

圖像處理

接下來就是pillow的圖像處理部分,這邊是根據裁切的上下高度,去依比例將原有圖像分割兩塊,並垂直翻轉後放在原始圖像上下兩端延伸,再上一層模糊濾鏡。
在此之前先做了一個mask,稍微羽化邊緣,去當作前段所述的合成圖像跟原始圖像疊加的遮罩。
左右眼批次做完上述動作後合在一起,便完成所有步驟。

#Create Alpha Mask for Image Overlay    
    mask = Image.new("L", (i_w, i_h))
    mask_draw = ImageDraw.Draw(mask)
    mask_draw.rectangle([0, 0, i_w, c_t], 255)
    mask_draw.rectangle([0, c_t+c_h, i_w, i_h], 255)
    del mask_draw
    mask = mask.filter(ImageFilter.GaussianBlur(50))


    #Image Process    
    for pic, eye in itertools.izip([lm, rm], ["Left", "Right"]):
        print "Post-Processing {0} Eye Image ... ".format(eye)
        pic_t = pic.copy().crop((0, 0, i_w, c_t/float(c_t+c_b)*c_h)).transpose(Image.FLIP_TOP_BOTTOM).resize((i_w, c_t))
        pic_d = pic.copy().crop((0, c_t/float(c_t+c_b)*c_h, i_w, c_h)).transpose(Image.FLIP_TOP_BOTTOM).resize((i_w, c_b))
        pic_canvas = Image.new("RGB", (i_w, i_h))
        pic_canvas.paste(pic_t, (0, 0))
        pic_canvas.paste(pic, (0, c_t))
        pic_canvas.paste(pic_d, (0, c_t+c_h))  
        pic_overlay = pic_canvas.copy().filter(ImageFilter.GaussianBlur(100))
        pic_canvas.paste(pic_overlay, (0, 0), mask)
        im_list.append(pic_canvas)
        for im in [pic, pic_t, pic_d, pic_overlay]:
            im.close()


    #Composite and Output
    print "Finalize Composition ... "
    main.paste(im_list[0], (0, 0))
    main.paste(im_list[1], (0, i_h))
    main.save(output_dir + "/" + os.path.basename(oi))

    for im in ([mask, main, lm, rm] + im_list):
        im.close()
    ri.truncate(0)


print "Finish!!"

接下來就可以戴上 HTC Vive 或 Oculus Rift 使用程式觀看(上面影片所使用的是Virtual Desktop)。

要更進階的話,其實可以針對所有圖檔以及左右眼的圖像處理做threading,加快處理速度(PIL真的很慢)。另外exiftool在處理十張照片左右偶爾會出現memory leak的問題,目前還沒找到解決方法,不過就從斷點繼續轉就好,不太礙事。

附上原始檔連結