Instagramを着る

皆さんはinstagramに写真を投稿するとき，どのような写真を投稿しますか？[^1]
他人に見せるためのSNS，つい無意識に**充実している自分を演出してしまいがち**かと思います。
それってひょっとすると**自身のSNSの過去の投稿に自身の行動が縛られて**いるのではありませんか？

そんな状況を表現したいと思います。

> 本記事は慶應SFC徳井研 (Computational Creativity Lab.) 
> 夏合宿2019ハッカソンでの作品解説も兼ねています

## MemoryBody

![memory_body.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/198787/58fab805-bcc2-ff0c-fa3c-0629ab91e82f.png)

作品名は思い出を着ている状態：そのまま「**MemoryBody**」としました。
自身のinstagramの画像を着用するモデルとしては、渡辺直美さんのinstagramアカウント[^4]を拝借しました。
渡辺直美さんで試したら**結構かっこよくなってしまった**気もします。

また、本作品はSFC徳井研夏合宿ハッカソンでのチーム(私 @jg43yr, @santa_sukitoku, [花形慎さん](https://hana-folio.tumblr.com/?fbclid=IwAR2Aq5YE_ENOYUi1o4FxrcLdeYSvQ-iP-fJWONDj7eURWNulw0aVBqVlkRs))との共同作品です（実装を主に担当しました）

上のような作品を，**クラウド上**で実装します。

## Methods: 技術について

それでは技術面のお話をします。

> やさしいマサカリをお待ちしています[^3]

### 1.DensePoseによる人体表面の3D座標推定

論文: [DensePose: Dense Human Pose Estimation in the Wild](https://ieeexplore.ieee.org/document/8578860)

[DensePose](https://github.com/facebookresearch/DensePose)は、Facebook Researchから2018年のCVPRで発表されたCNNベースの二次元画像からの人体のセグメンテーションをし、且つ体表のUV座標を推定する深層モデルです。実装と学習済モデルが公開されているので、コイツを使います。

<img width="1408" alt="スクリーンショット 0001-12-07 12.15.42.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/198787/1dd21efb-5fc6-29a1-8bda-f9c696c69447.png"> [^2]

ただし、公式の実装が**Python2系&NVIDIA GPUを積んだLinux上でしか動かない**[^6]という制約があるので自身の実機ではなく、Google Colaboratory上で実装します。

### 2.人体表面のテクスチャ張替え

DensePoseではテクスチャ用画像を適切に用意することで，推定体表のテクスチャを張り替えられます。

<img width="100%" alt="スクリーンショット 0001-12-10 15.53.59.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/198787/cb5c34fa-d736-8e88-7872-386012e4f9e3.png">

Webカメラで撮影した集合写真は以下のようになります。

<img width="75%" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/198787/c83aa734-9f18-df59-ee62-e764d4de8cc4.png">

<details><summary>テクスチャ変換のスクリプトセル</summary><div>


```python
import os
import requests
import cv2
import numpy as np
from IPython.display import display, Javascript
from google.colab.output import eval_js
from base64 import b64decode
from IPython.display import Image


def texture_transfer(photo_name="image.jpg",
    texture_img_name="DensePose/DensePoseData/demo_data/texture_from_SURREAL.png"):
    print("Executing pose detection...")

    os.system(
"""
cd {} && python2 tools/infer_simple.py 
--cfg configs/DensePose_ResNet101_FPN_s1x-e2e.yaml
--output-dir DensePoseData/infer_out/
--image-ext jpg
--wts https://dl.fbaipublicfiles.com/densepose/DensePose_ResNet101_FPN_s1x-e2e.pkl ../{}
""".format(project_name, photo_name))

    im  = cv2.imread(photo_name)
    IUV = cv2.imread(join(project_name, 'DensePoseData/infer_out/{}_IUV.png'.format(photo_name[:-4])))
    INDS = cv2.imread(join(project_name, 'DensePoseData/infer_out/{}_INDS.png'.format(photo_name[:-4])),  0)

    fig = plt.figure(figsize=[8,8])
    Tex_Atlas = cv2.imread(texture_img_name)[:,:,::-1]/255.
    # plt.imshow(Tex_Atlas.transpose([1,0,2]));plt.axis('off'); plt.show()

    TextureIm  = np.zeros([24,200,200,3]);

    print("Synthesis...")
    for i in range(4):
        for j in range(6):
            TextureIm[(6*i+j) , :,:,:] = Tex_Atlas[ (200*j):(200*j+200)  , (200*i):(200*i+200) ,: ]

    fig = plt.figure(figsize=[20, 20])
    plt.imshow( im[:,:,::-1] )
    plt.contour( IUV[:,:,1]/256.,10, linewidths = 1 )
    plt.contour( IUV[:,:,2]/256.,10, linewidths = 1 )
    plt.axis('off')
    plt.show()

    print("Transfering...")
    image = TransferTexture(TextureIm,np.zeros(IUV.shape),IUV)
    fig = plt.figure(figsize=[14,14])
    image = TransferTexture(TextureIm,im,IUV)
    fig = plt.figure(figsize=[14,14])
    plt.imshow(image[:,:,::-1])
    plt.axis('off')
    plt.show()

    if "/" in texture_img_name:
        texture_img_name = texture_img_name.split("/")[-1]

    output_file = "texture_transfer_" + photo_name[:-4] + "_" + texture_img_name[:-4] + ".png"
    plt.savefig("/content/" + output_file)
    return output_file
```
</div></details>


### 3.Seleniumでのinstagram画像の取得

instagramはreact製でjsでレンダリングされるため，普通にhtmlのスクレイピングが出来ません。よって`Selenium`でDOMをレンダリングしてから画像を取得します。その後，OpenCVで画像の読み込みをし、テクスチャ画像の形にして保存します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/198787/54e86521-71fe-40e8-6908-21bcf859e1ab.png)[^7]

> 追記: 現在Colab上でのChrome Driverを用いたスクレイプがうまく行かないことがあるので，instagramからの画像取得はローカルで行う方が良いかもしれません。その場合はgithubにあるget_instagram_images.pyを実行してください。

```shell
$ python get_instagram_images.py
put your instagram user name:watanabenaomi703
Now downloading picture 0...
Now downloading picture 1...
Now downloading picture 2...
```

<details><summary>instagram画像のURL取得とテクスチャ用画像生成</summary><div>


```python
import requests
import cv2
from bs4 import BeautifulSoup
from selenium import webdriver
from selenium.webdriver.chrome.options import Options


def url_to_image(url):
    # download the image, convert it to a NumPy array, and then read
    # it into OpenCV format
    resp = requests.get(url)
    image = np.asarray(bytearray(resp.content), dtype="uint8")
    image = cv2.imdecode(image, cv2.IMREAD_COLOR)
    # return the image
    return image


def get_instagram(user_id="atsuya_jg43yr"):

    url = "https://www.instagram.com/{}/".format(user_id)

    options = Options()
    options.set_headless(True)
    driver = webdriver.Chrome(chrome_options=options, executable_path='./chromedriver')
    driver.get(url)
    html = driver.page_source.encode('utf-8')
    soup = BeautifulSoup(html, "lxml")
    res = requests.get(url)

    img_size = 200
    tile = np.zeros([200*4, 200*6, 3])  # 4 x 6

    for i in range((4*6)-2):  # get 22 (2x2) shape images
        row_idx = i // 6
        col_idx = i % 6
        img = cv2.resize(url_to_image(pathes[i]), (200, 200))
        print("Now downloading picture {}...".format(i))
        tile[200*row_idx:200*(row_idx+1),
               200*col_idx:200*(col_idx+1), :] = img

    tile = tile.transpose(1, 0, 2)
    fname = "sample_texture.png"
    cv2.imwrite(fname, tile)
    print("Saved your img file for Texture: {}".format(fname))
    return fname
```

</div></details>

### 4.Colab上での写真撮影

Colab上にDOMを生成することでWebカメラから写真撮影をします。


<details><summary>Colab上でのWebカメラ利用</summary><div>

```python
def take_photo(filename='image.jpg', quality=0.9):
    js = Javascript(
'''
async function takePhoto(quality) {
    const div = document.createElement('div');
    const capture = document.createElement('button');
    capture.textContent = '写真をとるよ！';
    div.appendChild(capture);
    const video = document.createElement('video');
    video.style.display = 'block';
    const stream = await navigator.mediaDevices.getUserMedia({video: true});
    document.body.appendChild(div);
    div.appendChild(video);
    video.srcObject = stream;
    await video.play();
    /*
    Resize the output to fit the video 
    element.google.colab.output.setIframeHeight(document.documentElement.scrollHeight, true);
    Wait for Capture to be 
      clicked.await new Promise((resolve) => capture.onclick = resolve);
      const canvas = document.createElement('canvas');
      canvas.width = video.videoWidth;
      canvas.height = video.videoHeight;
      canvas.getContext('2d').drawImage(video, 0, 0);
      stream.getVideoTracks()[0].stop();
      div.remove();
      return canvas.toDataURL('image/jpeg', quality);}
    */
  ''')
    display(js)
    data = eval_js('takePhoto({})'.format(quality))
    binary = b64decode(data.split(',')[1])
    with open(filename, 'wb') as f:
        f.write(binary)
    return filename
```

</div></details>

### 5.開発環境 on Google Colab

[![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/drive/1qPx3D2dXl_cpTk7Zr5AxyZkcMbeJxFEw)

以上で紹介したスクリプト（セル）は，下記のセルを事前に実行して環境を建てることで実行できます。

本作品は、すべてGoogle Colaboratory[^5]で作られているので、誰でも試せます！是非、ご自身のPCでも遊んでみてください。

また私(@jg43yr)のGitHub上でソースコードを公開しています（[GitHub: atsukoba/MemoryBody](https://github.com/atsukoba/MemoryBody)）ので、バグ報告や追加機能から感想まで、なんでもissueお待ちしています。

`!`マジックコマンドを駆使しconda2環境から建てていきます。ちなみに，Colabは20年1月でPython2のカーネルのサポート終了とのことです。

<details><summary>conda2環境のインストール用スクリプトセル</summary><div>

```python
import os
from os.path import exists, join, basename, splitext

# install Anaconda Python 2.7 to control the dependencies
# see for more info: 
if not exists('anaconda2'):
  !wget -q https://repo.anaconda.com/archive/Anaconda2-2019.03-Linux-x86_64.sh
  !chmod +x Anaconda2-2019.03-Linux-x86_64.sh
  !bash ./Anaconda2-2019.03-Linux-x86_64.sh -b -f -p /content/anaconda2
  # set PATH environment variable
  os.environ['PATH'] = "/content/anaconda2/bin:" + os.environ['PATH']
  # install PyTorch
  !conda install -y pyyaml=3.12
  !conda install -y mkl-include
  !conda install -y pytorch=1.0.1 torchvision cudatoolkit=10.0 -c pytorch
  !ln -s /content/anaconda2/lib/python2.7/site-packages/torch/lib/ /content/anaconda2/lib/python2.7/site-packages/
  # install GCC 4.9
  !conda install -y -c serge-sans-paille gcc_49
  !ln -fs /content/anaconda2/lib/libmpfr.so /content/anaconda2/lib/libmpfr.so.4
  os.environ['CC'] = '/content/anaconda2/bin/gcc-4.9'
  os.environ['CXX'] = '/content/anaconda2/bin/g++-4.9'
  # protobuf 3.5
  #!apt-get -qq remove -y protobuf-compiler
  !conda install -y protobuf=3.5
  # pycocotools
  !conda install -y -c conda-forge pycocotools

!conda install -y -c conda-forge selenium
!conda install -y -c conda-forge bs4
  
# we need some headers from the pytorch source
if not exists('pytorch'):
  !git clone -q --depth 1 --recursive -b v1.0.1 https://github.com/pytorch/pytorch

git_repo_url = 'https://github.com/facebookresearch/DensePose.git'
project_name = splitext(basename(git_repo_url))[0]
if not exists(project_name):
  # clone project
  !git clone -q --depth 1 $git_repo_url
  # install dependencies
  !cd $project_name && pip install -q -r requirements.txt
  # update CMakeLists.txt
  cmakelists_txt_content = """
cmake_minimum_required(VERSION 2.8.12 FATAL_ERROR)
set(Caffe2_DIR "/content/anaconda2/lib/python2.7/site-packages/torch/share/cmake/Caffe2/")
find_package(Caffe2 REQUIRED)
include_directories("/content/anaconda2/lib/python2.7/site-packages/torch/lib/include")
include_directories("/content/anaconda2/include")
include_directories("/content/pytorch")
add_library(libprotobuf STATIC IMPORTED)
set(PROTOBUF_LIB "/content/anaconda2/lib/libprotobuf.a")
set_property(TARGET libprotobuf PROPERTY IMPORTED_LOCATION "${PROTOBUF_LIB}")

if (${CAFFE2_VERSION} VERSION_LESS 0.8.2)
  # Pre-0.8.2 caffe2 does not have proper interface libraries set up, so we
  # will rely on the old path.
  message(WARNING
      "You are using an older version of Caffe2 (version " ${CAFFE2_VERSION}
      "). Please consider moving to a newer version.")
  include(cmake/legacy/legacymake.cmake)
  return()
endif()

# Add compiler flags.
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -std=c11")
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=c++11 -O2 -fPIC -Wno-narrowing")
# Print configuration summary.
include(cmake/Summary.cmake)
detectron_print_config_summary()
# Collect custom ops sources.
file(GLOB CUSTOM_OPS_CPU_SRCS ${CMAKE_CURRENT_SOURCE_DIR}/detectron/ops/*.cc)
file(GLOB CUSTOM_OPS_GPU_SRCS ${CMAKE_CURRENT_SOURCE_DIR}/detectron/ops/*.cu)
# Install custom CPU ops lib.
add_library(
     caffe2_detectron_custom_ops SHARED
     ${CUSTOM_OPS_CPU_SRCS})
target_link_libraries(caffe2_detectron_custom_ops caffe2_library libprotobuf)
install(TARGETS caffe2_detectron_custom_ops DESTINATION lib)
# Install custom GPU ops lib, if gpu is present.
if (CAFFE2_USE_CUDA OR CAFFE2_FOUND_CUDA)
  # Additional -I prefix is required for CMake versions before commit (< 3.7):
  # https://github.com/Kitware/CMake/commit/7ded655f7ba82ea72a82d0555449f2df5ef38594
  list(APPEND CUDA_INCLUDE_DIRS -I${CAFFE2_INCLUDE_DIRS})
  CUDA_ADD_LIBRARY(
      caffe2_detectron_custom_ops_gpu SHARED
      ${CUSTOM_OPS_CPU_SRCS}
      ${CUSTOM_OPS_GPU_SRCS})
  target_link_libraries(caffe2_detectron_custom_ops_gpu caffe2_gpu_library libprotobuf)
  install(TARGETS caffe2_detectron_custom_ops_gpu DESTINATION lib)
endif()
"""
  open(join(project_name, 'CMakeLists.txt'), 'w').write(cmakelists_txt_content)
  # build
  !cd $project_name && make
  !cd $project_name && make ops
  # download dense pose data
  !cd $project_name/DensePoseData && bash get_densepose_uv.sh
 
!python2 $project_name/detectron/tests/test_spatial_narrow_as_op.py
!python2 $project_name/detectron/tests/test_zero_even_op.py
!wget https://chromedriver.storage.googleapis.com/77.0.3865.10/chromedriver_linux64.zip
!unzip chromedriver_linux64.zip
!apt install chromium-chromedriver
!cp /usr/lib/chromium-browser/chromedriver /usr/bin
```

</div><details>

## おわりに

みなさんも、このようにPythonとかを書いて自信が面白いと思えるものをつくっていけたら良いのではないでしょうか。SFC生のみなさん，何か一緒に作りましょう！是非連絡お待ちしています！

[^1]: 私は主にコインランドリーの写真しか投稿していません。ぜひ探してみてください
[^2]: GitHub READMEより画像引用
[^3]: ハッカソンの1日で実装したのでお許しを
[^4]: https://www.instagram.com/watanabenaomi703/
[^5]: Googleのjupyter notebook型クラウド開発環境です。
[^6]: びっくりした
[^7]: 2019年8月時点の@watanabenaomi703のinstagram新着画像をテクスチャの形に並び替えたもの