魚眼カメラの映り方 2
画像の代わりに3Dモデルを適用
ひとつ前の記事では、顔画像を平面のまま扱い、投影し、それっぽい絵を作りました。
さらなる正確性を求めて、顔の3Dモデルの利用を考えます。
まずは準備として、3Dの顔の準備に取り掛かります。
3Dモデルの取得
理想的には、顔の色、テクスチャまで指定されたリアルな人間の顔モデルが欲しいです。
今回は理想にはとどかなかったので、顔の3Dモデルを用意し、AIで生成した顔画像をそこに当て込む方式で実現することにしました。
触れていませんが、どうやらUnityなどではスマホのカメラをつかって自分の顔のモデルを作れるようです。。。それもありですね!
無料の顔の3Dモデルダウンロードとデータ取得
例えばここなどを参考に取得します。(※私が使ったのはここ以外のものです)
わかりやすいと思ったのでobjファイルを使うことにしました。
このファイル形式だと、頂点のx,y,z座標をはじめとしてテクスチャ情報などがテキスト形式で保存されています。
3Dモデルの部分だけほいし場合は、ファイルの「v 100, 30, 50」みたいな形式の行を一括でとればよいです。vは頂点の略でしょうね。
試しに点を間引いてプロットすると以下のようになりました。

z方向の値に応じて色を付ける
正面から顔を見た前提で、奥行き方向の座標をもとに色付けしてみます。単純に奥行き距離に比例して色味を変えるだけだと特徴が捉えにくいです。なぜなら、顔の3Dモデルでは目や眉間のあたりが細かくなっていることが多く、奥行きの値もそこらへんの値が密になっているから。逆におでこや耳のあたりは頂点も疎になりがちなので、たとえ奥行きの値の変化が大きくても色の変化はそこまでいりません。見ればわかるし。
ヒストグラム平均化のアプローチで色味を割り振ります。
# 顔モデルの頂点の座標たちをx,y,zごとに分けて、x_values, y_values, z_valuesとする。
# 最終的には整数で扱うので、適当な数字1000とか5000とかをかけてint化しておく
# pandasとか使うと楽かもね
def create_histgram_eq_mapper(z_values, cmap_name='viridis'):
hist, bins = np.histogram(z_values, bins=256)
cdf = np.cumsum(hist)
cdf_normalized = cdf / cdf.max()
def normalize_func(z):
return np.interp(z, bins[:-1], cdf_normalized)
cmap = plt.get_cmap(cmap_name)
def mapper(z):
normalized_val = normalize_func(z)
return cmap(normalized_val)[:3] # rgb from rgba
return mapper, normalize_func # 参考:どのようにマッピングするかの関数
# x,y,z座標から、z座標だけ抜き出したものをz_valuesとする
histgram_eq_mapper, hist_norm_func = create_histgram_eq_mapper(z_values, cmap_name='plasma')
colors_hist_eq = [histgram_eq_mapper(z) for z in z_values]
plt.scatter(x_values, y_values,c = colors_hist_eq)
plt.set_title("histgram eq")
データの保存
二種類保存しておく
- 3Dの頂点のドット
かなり点が疎だけどひとまず良い。
y_max = np.max(y_values)
model_image = np.ones([int(np.max(y_values))+1, int(np.max(x_values))+1, 3]).astype(np.uint8)
model_image = model_image * 255
colors_hist_eq_int = np.zeros([len(colors_hist_eq),3])
for row_idx in range(len(colors_hist_eq_int)):
colors_hist_eq_int[row_idx] = np.array([val * 255 for val in colors_hist_eq[row_idx]]).astype(np.uint8)
for xx, yy, zz in zip(x_values, y_values, colors_hist_eq_int):
model_image[y_max-yy,xx,:] = zz
cv2.imwrite('3d_face_dots.png', model_image)
- x,y,zの3次元座標のテーブル
今回使ったモデルは鼻頭のz座標が一番大きかったのでそこを0として、耳に行くほど値が大きくなるように「depth」を用意した。
z_depth = z_values.copy()
z_depth = np.max(z_depth) + z_depth*-1
print(f'z: {np.min(z_depth)}~{np.max(z_depth)}')
model_coords = pd.DataFrame({'x': x_values, 'y': np.max(y_values)-y_values, 'z':z_depth})
model_coords.to_csv('model_coords.csv', index=False)
- javascriptで使ういやすいように加工する
上記csvファイルの値のところだけコピーして、下記の形式にまとめる。一行一行がモデルの点のx,y,z座標に相当します。この手動操作はいけてないですが、js側で作るものが少なくなります。
なおvscodeの正規表現で「$ -> ,」に変換すると行末のカンマは楽に打てる。
export const model_coords = new Uint16Array([
236,391,245,
232,383,245,
229,385,241,
...
])
もちろん、pythonで書き出すときにそういう風に書き出してもいい。
続く
Discussion