写真から3Dモデルを作る方法を思いついた その02画像読み込みと頂点の描画

どーもKsukeです。　
写真から3Dモデルを作る方法を思いついたその02で、画像読み込みと頂点の描画をやっていきます。
その1はこちらhttps://qiita.com/Ksuke/items/7b3df37399cecd34d036

<b>※注意※</b>
この記事は思いついて試した事の末路を載せているだけなので、唐突なネタやBad Endで終わる可能性があります。
##やってみる
<b>手順</b>
1.画像の読み込み
2.その3に向けた前処理
3.頂点データの作成
4.頂点の表示


ところどころにあるコードは、最後にまとめたものを載せてあります。

###1.画像の読み込み
 何かしらのオブジェクトを前面、側面、上面の写真を撮ります。
 そして背景を真っ白に塗りつぶして、縦横の寸法を同じにします。(背景分離等の後の処理をやりやすくするためです。ほんとはここもプログラムで処理したいけど今回の本題ではないので手抜き。)
 自前のコップでやったところこんな感じになりました。これ以降はこの画像でいろいろやっていきます。

<table>
  <tr>
    <th style="text-align:center;">正面画像</th>
    <th style="text-align:center;">側面画像</th>
    <th style="text-align:center;">上面画像</th>
  </tr>
  <tr>
    <td style="text-align:center;">
      <img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/699258/850449a2-5140-499d-d0d7-4acaac6fa0f5.jpeg">
    </td>
    <td style="text-align:center;">
      <img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/699258/4624d7ca-7442-93f2-3ca9-70f0b63f7bb2.jpeg">
    </td>
    <td style="text-align:center;">
      <img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/699258/14695dea-fcb1-262b-4bef-a9f8c530374d.jpeg">
    </td>
  </tr>
</table>

用意した画像をどこかのフォルダにおいて、画像を扱うサイズを決めて、blenderで次のコードを実行して読み込みます。

```python:画像読み込み
#3Dモデルの粗さ(画像サイズ)を指定
imgSize = 100
   
#画像があるフォルダの絶対パス
basePath = "任意！"

#画像があるフォルダからの相対パス
imgPaths = ["/front.jpg","/side.jpg","/top.jpg"]

#各方向からの画像読み込み,imgSizeに縮小拡大
imgs = [cv2.resize(cv2.imread(basePath+imgPath),(imgSize,imgSize)) for imgPath in imgPaths]
```



###2.その3に向けた前処理

画像から背景を分離する関数を用意します。今回は白とそれ以外で分離しますが、必要に応じて書き換えてください。

```python:背景分離
#背景分離用のメソッド
#これが本題ではないのでざっくりと、画像は白背景ってことにしといて、白いかどうかで判断
#各画素のRGBを平均して、230以上なら背景ってことにする。
def sepBack(img):

    #白い画素を0、白く無い画素を1に置き換えて、背景が0の二値画像にする。
    sepBackImg = np.where(230<=img.mean(axis=2),0,1)
    
    #二値画像を返す
    return sepBackImg
    

#読み込んだ画像から、背景と被写体を分離した画像を作る
sepBackImgs = [sepBack(img) for img in imgs]


```



###3.頂点データの作成

つづいて、二値の配列から、値が1の個所の座標を返す関数を用意します。

```python:座標取り出し
#二値配列から座標を計算するメソッド
#二値配列の、値が1の個所の座標を求めて返す。
def binary2coords(binary):
    #値が1の座標を求める
    coords = np.where(binary==1)
    
    #値がdarrayになっておらず使いにくいので整形
    coords = np.stack([*coords],axis=1)
    
    #座標のdarrayを返す
    return coords
    

#背景を分離した画像から、被写体の位置の座標のlistを作る
#3D表示するときに座標が2次元だと困るので、画像に次元を1つ増やしてから座標をとる
imgCoords = [binary2coords(sepBackImg[:,:,None]) for sepBackImg in sepBackImgs]

```

###4.頂点の表示

最後に、頂点をオブジェクトとして表示する関数を用意します。
今後辺や面の情報も追加できるように引数を用意しておきます。

```python:オブジェクト生成

#頂点をオブジェクトとして登録する
def addObj(coords=[],edges=[],faces=[],offset=[],name=""):
    
    #offsetが指定されていた場合はoffsetを加える
    if len(offset)!=0:coords = coords+offset
        
    #新規メッシュを作成
    me = bpy.data.meshes.new("{}Mesh".format(name))

    # メッシュでオブジェクトを作成
    ob = bpy.data.objects.new("{}Object".format(name), me)
    
    # オブジェクトの配置位置を3Dカーソルの位置に合わせる
    ob.location = bpy.context.scene.cursor.location
    
    # オブジェクトをシーンにリンク
    bpy.context.scene.collection.objects.link(ob)
    
    # メッシュの頂点、辺、面を埋める
    me.from_pydata(coords,edges,faces)
    
    # 変更オブジェクトをアクティブに変更する
    bpy.context.view_layer.objects.active = ob
        
    # 新たなデータでメッシュを更新
    me.update(calc_edges=True)
    
    #作成したメッシュを返す
    return me,ob


#各画像の頂点のずらし幅
offsets = [[-50,-150,0],[-50,-50,0],[-50,50,0]]

#各画像の頂点をオブジェクトとして登録するときの名前
names = ['frontImg','sideImg','topImg']

#頂点を描画
[addObj(coords=imgCoord,name = name,offset=offset) for imgCoord,name,offset in zip(imgCoords,names,offsets)]

```

##動作確認
最後にコードが問題なく動くか確認。

###1.実行
下にまとめてあるコードを前回までのコードの下に加えてblenderで実行。
こんな感じのオブジェクト(現状では点群)が3つ出れば成功。
![キャプチャ.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/699258/20703ef8-70be-ca47-05ba-d22b676f3010.png)


##次は？
画像を3次元の配列に移し替えて、厚みのあるオブジェクト(まだ点群)を表示するつもりです。

2020/9/18追記
その3を公開しました。
https://qiita.com/Ksuke/items/8b7f2dc840126753b4e9


##コードまとめ
前回のコードの後ろに追加すれば動くはずです。(ファイルパスは要修正)

###関数編
```python:コードまとめ(関数編)


#背景分離用のメソッド
#これが本題ではないのでざっくりと、ここでは画像は白背景ってことにしといて、白いかどうかで判断
#各画素のRGBを平均して、230以上なら背景ってことにする。
def sepBack(img):

    #白い画素を0、白く無い画素を1に置き換えて、背景が0の二値画像にする。
    sepBackImg = np.where(230<=img.mean(axis=2),0,1)
    
    #二値画像を返す
    return sepBackImg
    

#二値配列から座標を計算するメソッド
#二値配列の、値が1の個所の座標を求めて返す。
def binary2coords(binary):
    #値が1の座標を求める
    coords = np.where(binary==1)
    
    #値がdarrayになっておらず使いにくいので整形
    coords = np.stack([*coords],axis=1)
    
    #座標のlistを返す
    return coords
    

#頂点をオブジェクトとして登録する
def addObj(coords=[],edges=[],faces=[],offset=[],name=""):
    
    #offsetが指定されていた場合はoffsetを加える
    if len(offset)!=0:coords = coords+offset
        
    #新規メッシュを作成
    me = bpy.data.meshes.new("{}Mesh".format(name))

    # メッシュでオブジェクトを作成
    ob = bpy.data.objects.new("{}Object".format(name), me)
    
    # オブジェクトの配置位置を3Dカーソルの位置に合わせる
    ob.location = bpy.context.scene.cursor.location
    
    # オブジェクトをシーンにリンク
    bpy.context.scene.collection.objects.link(ob)
    
    # メッシュの頂点、辺、面を埋める
    me.from_pydata(coords,edges,faces)
    
    # 変更オブジェクトをアクティブに変更する
    bpy.context.view_layer.objects.active = ob
        
    # 新たなデータでメッシュを更新
    me.update(calc_edges=True)
    
    #作成したメッシュを返す
    return me,ob
```

###実行コード編
```python:コードまとめ(実行コード編)

#すでにある全オブジェクトを削除
for item in bpy.data.objects:
   bpy.data.objects.remove(item)

#3Dモデルの粗さ(画像サイズ)を指定
imgSize = 100

#画像があるフォルダの絶対パス
basePath = "任意！"

#画像があるフォルダからの相対パス
imgPaths = ["/front.jpg","/side.jpg","/top.jpg"]

#各方向からの画像読み込み,imgSizeに縮小拡大
imgs = [cv2.resize(cv2.imread(basePath+imgPath),(imgSize,imgSize)) for imgPath in imgPaths]
    
#読み込んだ画像から、背景と被写体を分離した画像を作る
sepBackImgs = [sepBack(img) for img in imgs]

print("step02:image read and preprocessing is success.\n")



#以下確認表示用(メインの流れと関係ないので、次の回では多分消えてる)

#背景を分離した画像から、被写体の位置の座標のlistを作る
imgCoords = [binary2coords(sepBackImg[:,:,None]) for sepBackImg in sepBackImgs]

#各画像の頂点のずらし幅
offsets = [[-50,-150,0],[-50,-50,0],[-50,50,0]]

#各画像の頂点をオブジェクトとして登録するときの名前
names = ['frontImg','sideImg','topImg']

#頂点を描画
[addObj(coords=imgCoord,name = name,offset=offset) for imgCoord,name,offset in zip(imgCoords,names,offsets)]
```