Python
画像処理
Huawei
DepthMap
DOF

Huawei Mate 10 liteで撮った画像から深度情報(Depth of Field・Depth Map)を取得する方法

概要

最近、化石のようなHTC J ISW13HT(Android 4.0)から、Huawei Mate 10 liteに買い換えたところ、面白い機能があったので、それを使って遊んでいました。

ワイドアパチャー
https://dc.watch.impress.co.jp/topics/huawei1706/

なる機能がついていまして、ビルトインのカメラアプリの左から2番目のシャッターのアイコンを押すと、青色になり有効になります。

image.png

どのような機能かというと、このような1枚の画像から

image.png

後ろの壁に焦点のあった画像や

image.png

前のハルヒに焦点のあった画像を生成したりできる

image.png

そういう機能でした。

これって画像内に深度情報が入っていないと不可能だよね?
深度情報を取りたい!!

と思って調べてみました。

深度画像を抽出する

とりあえずネットで調べてみると、過去に発売されたHuawei P9にもデュアルカメラが搭載されており、その深度画像を取りたい旨の投稿がありました。

Extracting Both Images from P9 Dual Camera

その結果、1つのjpeg画像に普通のカラー画像と深度画像が入っていることがわかりました。
そこから、pythonでカラー画像と深度画像を抽出するコードが公開されていました。

https://github.com/jpbarraca/dual-camera-edof

結果からいうとこのスクリプトでは動きませんでした。

そういうことでてろてろてろーと改造しました。

myextract_edof.py
#!/usr/bin/python3
#encoding=utf-8

import sys
import os
import binascii
from PIL import Image

# Configuration defaults
show_edof = False
save_edof = False
save_original = False
save_processed = False
delete_file = False

def extract_edof(data, idx, fname):
    if data[idx + 4:idx + 8] != b'edof':
        print("ERROR: Frame is not EDOF")
        return False

    idx += 8
    columns = int.from_bytes(data[idx + 16: idx + 18], byteorder='little')
    rows = int.from_bytes(data[idx + 18: idx + 20], byteorder='little')
    print("\t* found EDOF at %d with geometry=%dx%d" % (idx, columns, rows))

    orientation = data[idx + 7]

    idx += 68
    img = Image.frombuffer('L', (columns, rows), data[idx:], 'raw', 'L', 0, 0)
    if orientation == 0x10:
        img = img.transpose(Image.FLIP_TOP_BOTTOM)

    if show_edof:
        img.show()

    if save_edof:
        outfname = (''.join(fname.split('.')[:-1])) + '-EDOF.png'
        print("\t  * saving to %s" % outfname)
        img.save(outfname)

    return True

def print_usage():
    print("Usage: %s [options] img1 img2 img3... " % sys.argv[0])
    print("Options: ")
    print("\t-p: Save the originaly processed image to the same directory")
    print("\t-o: Save the originaly unprocessed image to the same directory")
    print("\t-e: Save the EDOF as an image to the same directory")
    print("\t-v: View the EDOF image")
    print("\t-d: Delete file and only keep extracted (will enforce -o -e)")


def main(fname):
    print("Processing: %s" % fname)
    fin = None

    try:
        fin = open(fname, "rb")
    except FileNotFoundError:
        print("ERROR: Could not open %s" % fname)
        return False

    data = fin.read()

    if data[:3] != b'\xff\xd8\xff':
        print("No JPEG header found")
        return False

    print ("\t* scanning file")

    if  data.find(bytes([0x00, 0x65, 0x64, 0x6f, 0x66, 0x00])) < 0:
        print("No EDOF header found")
        return False

    idx = 0
    while idx < len(data):
        if data[idx + 4:idx + 8] == b'edof':
            print("call")
            extract_edof(data,idx,fname)
        idx += 1

if __name__ == "__main__":
    print("Huawei Dual Camera EDOF Extractor\n")

    if sys.version_info[0] < 3:
        print("This script requires Python 3")
        sys.exit(-1)

    if len(sys.argv) < 2:
        print_usage()
        sys.exit(-1)
    else:
        for o in sys.argv[1:]:
            if o.startswith("-"):
                for c in o[1:]:
                    if c == 'e':
                        save_edof = True
                    elif c == 'o':
                        save_original = True
                    elif c == 'p':
                        save_processed = True
                    elif c == 'v':
                        show_edof = True
                    elif c == 'd':
                        save_original = True
                        delete_file = True
                    else:
                        print("Unknown Option: %s" % c)
                        print_usage()
                        sys.exit(-1)

        for p in sys.argv[1:]:
            if p[0] != "-":
                if not os.path.exists(p):
                    print("File not found: %s" % p)
                    continue

                r = main(p)

                if r and delete_file:
                    print("\t* Deleting file: %s" % p)
                    os.unlink(p)

ざっくりというと、元スクリプトでは「カラー画像」と「深度画像」のデータの境目を認識し、それを元にカラー画像と深度画像の分離を図っているようなのですが、P9とは仕様が異なるようです。そこで、フォーラムにも書いてあるのですが、深度画像のはじめの方には"edof"という文字列がバイナリで格納されており、そこを起点に処理すると深度画像が取れるようです。そこで、バイナリを全舐めして、"edof"が入っているあたりを無理やりバイナリをパースしてぶっこぬきました。使い方は、

$ python3 myextract_edof.py -e IMG_20171230_143648.jpg 
Huawei Dual Camera EDOF Extractor

Processing: IMG_20171230_143648.jpg
    * scanning file
call
    * found EDOF at 5559822 with geometry=816x612
      * saving to IMG_20171230_143648-EDOF.png

DOF画像が入っていると、IMG_20171230_143648-EDOF.pngという画像が出力されます。
元々のプログラムを適当にしか改造していないので、-eオプションぐらいしか作用しないです。

実際の例だと、このような画像を食わせると、

image.png

こうなります!

image.png

元の画像の解像度が、3264×2448に対し、DOFは816x612になっています。1/4の解像度ですね。

深度画像を見てわかったHuawei Mate 10 liteの画像処理のあれそれ

これをみると色々と合点がいくところがあって、例えば長門のバニー耳のぼかしがおかしいなぁ。みたいなことを思っていました。

image.png

小泉方向には黒のぼかしが強く出ているのですが、バニーの耳の内側方向にはあんまりぼかしがないなぁ。みたいなことを思っていました。

image.png

これをみるとはっきりわかって、小泉の頭の部分はよく深度が推定されていて、頭と背景の分離がうまくできています。しかし、その一方で、耳の内側方向は背景分離がうまくできていなくて、ぼかし効果が小さい感じになっています。

これが一番わかりやすいかもしれませんが、ハルヒの股下の画像処理がおかしいです。

image.png

中央から少し左をみると、背景の壁部分は強いぼかしが入っています。しかし、ハルヒの股下から見る壁部分は、ぼかしが入っておらず、エッジが立っています。これをDOFで見ると、

image.png

中央部の柱状の黒がハルヒです。ここを見ると股下部分の深度推定がうまくいっていないです。そのため、背景部分なのにエッジが立っている。というロジックになっているようです。

もう少し詳しく解析したい。と思いまして、さっきの画像をガンマ値をいじってみました。

image.png

そうすると足の部分や、フィギュアの支柱のポールが見えました。これを見ると、ある程度、股下の部分も深度推定ができているようです。しかし、足の形が不明瞭であったり、台座とテーブル部分が分離されていない。みたいなことも確認できます。

なぜ股下の深度推定はうまくいかないか?

Mate 10 liteがどのように深度推定を行なっているか?という仕組みを考えたほうがよさそうだと思いました。
自分が知っている深度推定方法は、

  1. 赤外線パターン照射型
  2. TOF方式

の2つです。これらをできるだけ検証してみました。

暗所実験

まず初めに、「暗い部分ではうまく動かないらしい。」という報告があった。

https://forum.xda-developers.com/showpost.php?p=72225398&postcount=19

一般的なパターン照射型やTOFのようなシステムであれば環境光に依存しないため、深度画像が取れても不思議ではない。しかし、無理なようだ。

そこで暗所で自分で写真を撮ってみた。

image.png

そうすると当たり前だが真っ暗な写真が撮れた。これから、DOFを抜いてみると、

image.png

このような画像が得られた。何か少し映ってるらしい。
先ほどのカラー画像の方を目一杯ガンマ値をいじるとこうなる。

image.png

台座の上にコーヒーカップを置いて、写真を撮っていた。もしかすると、このプリンターの勾配が撮れていたのではないか?実際に画面半分ぐらいまでが台座で、その奥が壁であったので、DOF画像でも似たような場所で勾配が途切れているのも納得がいく。しかし、なぜか、コーヒーカップが映っていない。
この辺りが謎である。

赤外線実験

赤外線パターン照射であれば、カメラを通して、そのパターンが見えるはずである。
iPhoneではフロントカメラであれば赤外線が見えるそうだ。そこで、手近にあったエアコンの赤外線を見てみる。

image.png

このように紫色に光り、赤外線が照射されていることがわかる。では、mate 10 liteの場合はどうだろうか?

image.png

光っていることが確認できない。したがって、赤外線は照射していないようだ。

赤外線カメラじゃないか??

この時点で、

  • 暗所で撮れない
  • 赤外線を照射していない

ということがわかりました。そこで、これってただの赤外線カメラじゃね?という疑惑が個人的に出てきました。
そこで実験。

image.png

暗所で写真をとるのは同じなのですが、横に赤外線ストーブをおきます。赤外線ストーブは暗い中ではあんまり明るくないので、カラー画像はほぼ真っ黒です。しかし、ストーブからは赤外線が大量に出ています。そして、もしカメラが赤外線カメラであるならば、バッチリとその姿が映るはずです。

image.png

DOF画像の結果はこんな感じ。何も映らない。ということで、赤外線カメラでもない。

結局のところ・・・?

ただのカメラっぽいです。

Huaweiの公式のスペック表を見ても、特殊なカメラを使ってるようには見えないんですね。

http://consumer.huawei.com/jp/phones/mate-10-lite/specs/

ということは単純に縦方向のステレオカメラにすぎないんだなーと。で、多分内部的に垂直の画素をマッチングして移動幅から、深度を作ってるんだろうなぁ。というのが個人的な見解です。
Mate 10 proの方がモノクロカメラなんで、多分liteもモノクロカメラなんでしょう。なんで普通のカメラの方のカラー画像をモノクロ化、ダウンサンプリングして、モノクロカメラで撮ったモノクロ画像とマッチングしてるんじゃないかなーというのが思っていることです。

そう見ると、納得することもあって、例えばみくるの腰部分を見ていただきたいです。
(1枚目はカラー画像。2枚目は画像処理でモノクロにした画像。3枚目はDOF画像)

image.png

image.png

image.png

チアリーダーのスカートは青で、壁部分がベージュなので、色のコントラストがはっきりしてます。それは、モノクロ画像でもそうで、はっきりと境界がわかります。そのため、画素のマッチングもうまくいき、深度が綺麗に推定できる。と考えられます。
しかし一方で、長門の方は、バニーなので、太ももが見えています。これは色的にベージュに近く、(みくるのチアリーダーのスカートと背景の関係に比べれば)壁と見分けがつきにくいです。それは、モノクロ画像の方では、色情報が少ないので、さらに見分けにくいです。このように背景色と太ももの色の見分けがつきにくいので、画素のマッチングにミスが生じ、深度の推定に失敗しているように思えます。
ハルヒの腕から、みくるの頭付近まで変なものがあるように深度が推定されているのも、この辺りの色相の変化がビビッドではないのが原因ではないかな。と読んでいます。しかし、その一方で、イツキの制服などは背景に対してビビッドであるにも関わらず抜けていないのは、なんでなのかなー?という疑問はあります。(これはもしかしたらカメラから遠いので視差があまり発生しないから?というところは個人的には思っています。)しかし、実は製品として良い落としどころなんじゃないかな。と思いました。

高精度に深度を推定したいスマホユーザーが何人いるか?

という話です。別にイツキと長門が同じような深度に映っていても対して問題はないのでしょう。おそらく多くのユーザーが求めているのは、「いちばん手前に映っている対象と、それ以外を分離したい、それぞれにフォーカスを当てたり、ぼかしたりしたい。」ぐらいの要求なのかな。と。そうすると深度方向に対して、シビアな精度は必要なく、256階調くらいで、最悪前景だけ抽出できる程度の精度しかいらないんだろうなぁ。と思いました。

感想

最初はデプス画像だけ欲しい!という欲求だけでしたが、いざ画像を見ていると、あんまり精度良くないな。と思ったのが正直な感想でした。それで、なんでだ?と思って色々と情報と実験を繰り返していくうちに、あぁ。そういうことかぁ。と納得しました。最初から、赤外線周りの技術と当て勘でかかっていましたが、まさか画像処理的なアプローチとは思いませんでした・・・最近なくなってしまいましたが、Project Tangoの話もあり、このような距離画像センサーが乗っているのであれば、精度がいいのが取れて嬉しい!と思いましたが、そうでもなかったですね・・・
でも、やっぱり製品の落とし所としてはこの辺なんだなぁ。と改めて思いました。DeepLearningもワンチャンあるかなーとか思っています(Mate 10 ProはAIチップ搭載を謳っている)が、うーん?多分違うだろう。と思います。逆にDeepLearningでやる場合、むしろ単眼で十分なので、スマホを複眼にする意味があんまりない気がしています。
実際これは私の解析なので、違うよ!こういう情報あるよ!というのがあったら教えて欲しいです。もうちょい精度良くデプスとりたい!