webエンジニア

ニコニコ超会議

Vtuber

サポートした話

@hey_cube

I am へいきゅーぶ  

  • @hey_cube
  • オプトテクノロジーズ
  • webエンジニア
    • TypeScript + React
    • Ruby on Rails
  • ボカロクラスタ

今日話すこと

  • 茨ひよりと一緒に写真が撮れるアプリを作った
    • アプリの概要
    • 利用技術
  • ニコニコ超会議のブースの一部として出展した
    • ブースの成果

茨ひよりとは

  • 茨城県公認のVtuber
  • いばキラTVのアナウンサー
  • 愛称は「ひよりん」
  • かわいい

ニコニコ超会議とは

  • ニコニコ超会議実行委員会が主催するイベント
  • 過去数年に渡り開催されている
  • ニコニコ超会議2019ではひよりんもブースを構えた

アプリの概要

「ひよりんと一緒」とは

  • 「ひよりんと一緒に茨城県を観光した時の記念撮影」っぽい画像を吐き出すwebアプリ
  • やってることはシンプルな画像合成
  • 合成するひよりんと茨城県の画像はランダム

デモ

開発の経緯

  • 社内ハッカソン開催にあたり、エンジニア以外からもアイデアを募った
  • ひよりんのサポートをしている部署から提案があった
  • エンジニア数人がハッカソンでベースを作った
  • マネージャー陣が調整した結果、業務として開発を継続できた

利用技術

ざっくり

  • Ruby on Rails
  • AWS
  • サーバーサイドレンダリング
  • 写真撮影はJavaScript

もうちょっと細かく

  • フロントエンド
    • Spectre.css
    • WebcamJS
  • バックエンド
    • remove.bg
    • MiniMagick
    • CarrierWave / Fog::Aws
    • RQRCode / ChunkyPNG

Spectre.css

  • 軽量・レスポンシブ・モダンなCSSフレームワーク
  • 欲しいUIコンポーネントが大体そろっている(個人の感想)

WebcamJS

  • カメラからの情報をブラウザ上で良い感じにするライブラリ
  • HTML5上で動作する
  • 2017年2月以降アクティブな開発はしてないっぽい

WebcamJS

  <div id="web-camera-container">
    <div id="web-camera"></div>
  </div>
#web-camera {
  margin: auto;
}

#result-image {
  margin-top: 20px;
}

#web-camera-container {
  position: relative;
  img {
    transform: scale(-1, 1);
  }
}

WebcamJS

    // 初期設定
    const params = new URLSearchParams(location.search);
    const webCamera = document.getElementById('web-camera');
    Webcam.set({
      width: params.get('width') || 1200,
      height: params.get('height') || 900,
      image_format: 'png',
      flip_horiz: true,
    });
    Webcam.attach('#web-camera');

    // 写真の撮影
    const takeSnapshotButton = document.getElementById('take-snapshot-button');
    takeSnapshotButton.onclick = () => {
      Webcam.snap((dataUri) => {
        webCamera.innerHTML = '<img id="image" src="' + dataUri + '"/>';

        const nextButton = document.getElementById('next-button');
        nextButton.classList.remove('disabled');
        nextButton.onclick = () => {
          const blob = dataURItoBlob(dataUri);
          const form = new FormData(document.forms[0]);
          form.append("photo[image]", blob);

          const request = new XMLHttpRequest();
          request.onreadystatechange = () => {
            if (request.readyState === XMLHttpRequest.DONE) {
              const url = JSON.parse(request.responseText).url;
              window.location = url;
            }
          }
          request.open("POST", "/photos");
          request.send(form);
        }
      });
    }

remove.bg

  • 人物を撮影した画像の背景を切り取ってくれるAPI
  • グリーンバックじゃなくても大丈夫
  • 無料でも使える

remove.bg

    def remove
      api_key = ENV['REMOVE_BG_KEY']
      conn = Faraday.new('https://api.remove.bg', headers: {
          "X-Api-Key" => api_key
      }) do |f|
        f.request :multipart
        f.request :url_encoded
        f.adapter :net_http
      end

      response = conn.post('/v1.0/removebg',
                           image_file: Faraday::UploadIO.new(current_path, 'image/png'),
                           size: 'auto'
      )

      if response.success?
        File.binwrite(current_path, response.body)
      else
        puts "Error: #{response.status} #{response.body}"
      end
    end

MiniMagick

  • 画像を加工してくれるgem
  • 実体はImageMagickのRubyラッパー

MiniMagick

    def compose
      manipulate! do |person|
        ibaraki = MiniMagick::Image.open("#{Rails.root}/public#{model.ibaraki_path}")
        hiyorin = MiniMagick::Image.open("#{Rails.root}/public#{model.hiyorin_path}")
        hiyorin.geometry "80%"
        image = ibaraki.composite(hiyorin) do |c|
          c.channel "RGBA"
          c.compose "Over"
          c.geometry "-200-0"
          c.gravity "Southwest"
        end
        person.format "png"
        image = image.composite(person) do |c|
          c.compose "Over"
          c.geometry "-0-0"
          c.gravity "Southeast"
        end
        image
      end
    end

CarrierWave / Fog::Aws

  • CarrierWave
    • 画像などを指定した場所にアップロードしてくれるgem
  • Fog::Aws
    • CarrierWaveと併用することでS3に画像をアップロードできる

CarrierWave / Fog::Aws

class ImageUploader < CarrierWave::Uploader::Base
  include CarrierWave::MiniMagick

  storage Rails.env.production? ? :fog : :file

  def prefix
    Rails.env.production? ? '' : 'uploads/'
  end

  def store_dir
    "#{prefix}p/#{model.digest}"
  end

  def full_filename(for_file)
    "original.png"
  end

  version :removed do
    process :remove
    def remove
      # 人物画像の背景切り取り
    end

    def full_filename(for_file)
      'removed.png'
    end
  end

  version :composed, from_version: :removed do
    process resize_to_fit: [1600, 900]
    process :compose
    def compose
      # 画像合成
    end

    def full_filename(for_file)
      "hiyorin.png"
    end
  end
end

RQRCode / ChunkyPNG

  • RQRCode
    • QRコードを生成してくれるgem
  • ChunkyPNG
    • pngファイルを読み書きするためのgem
    • これを使うことで画像を保存せずに表示させることができる

RQRCode / ChunkyPNG

  def gen_qrcode(text, options = {})
    qr = RQRCode::QRCode.new(text, options)
    return ChunkyPNG::Image.from_datastream(qr.as_png.resize(500,500).to_datastream).to_data_url
  end
        <div class="content">
          <%= image_tag gen_qrcode @photo.image.composed.url %>
        </div>

ブースの成果

ひよりんのブース全体

  • 延べ1000人以上がコンテンツを体験したりツイートしたりしてくれました
  • 会場の様子はハッシュタグ「#超・茨ひより」で確認できます

「ひよりんと一緒」

  • 約150人が「ひよりんと一緒」を体験してくれました🎉
  • 画像をツイートしてくれた人も

まとめ

感想とか

  • 普段開発しているtoBのアプリとは毛色が違って面白かった
  • 開催前日の超会議会場に潜り込めたのはかなりテンションが上がった
  • ひよりんかわいい

かかったコストとか

  • 10営業日ちょいでできた
    • 約2人で制作
    • 4時間/日・人くらい
  • 普通のwebエンジニアが作った

僕が伝えられること

  • xR関連の技術を直接使わなくてもできることはある
  • プロダクトを立ち上げる時に考えることは色々ある
    • 誰が、どれくらいの期間で、どんなツールを使って開発をするか
    • 実現したいことは何か

おしまい  

webエンジニアがニコニコ超会議でVtuberをサポートした話

By hey_cube

webエンジニアがニコニコ超会議でVtuberをサポートした話

茨城県公認 Vtuber の茨ひよりと一緒に写真を撮れるアプリケーションを作ったのでそれの解説 / https://vrtokyo.connpass.com/event/137007/

  • 3,110