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的問題,目前還沒找到解決方法,不過就從斷點繼續轉就好,不太礙事。