東北ずん子のアニメデータ(原画+中割14000枚)で何ができるか考えてみた

概要

  • 3万円でアニメの原画+中割14000枚+着色後中割14000枚が6月12日までの支援で入手できる
  • 着色、中割の作画、エフェクトについて自動化できないかという期待があるらしい。まあまあできそうな気はする
  • 圧倒的ボリュームのデータを使っての画像処理やら機械学習やら深層学習やらで色んなことができるね、たーのしー、になりたい

東北ずん子のアニメとは

東北ずん子とは

  • 東北復興を応援する2次元キャラクター。 東北ずん子 公式HP
  • f:id:esuji5:20170609002643p:plain
  • 東北ずん子は東北企業であればイラストを無償で商用利用出来るキャラクターです。 を始めとした様々な利用ガイドラインがある。

ずんだホライずんとは

よくわかるアニメ業界の現状とこのデータセットにかける関係者の思い

が、こちらのTogetterにまとまっていますのでご一読ください。

アニメ技術開発の研究材料に。東北ずん子がデータ28000枚提供 - Togetterまとめ

支援するためのクラウドファンディングのページはこちら

取得したデータでどんなことができそうか

取得できるデータ

  • 原画
  • 中割絵(いわいる動画)の未着色のもの:14000枚
  • 中割絵(いわいる動画)の着色後のもの(仕上げ処理まで?):14000枚
  • 容量については100GBほど、何らかのサーバーからダウンロードする形式になるらしい

やってみたい

  • 着色の自動化:カット毎の1枚目の着色後中割を基点にしてその後の未着色中割に適切な色を塗る
    • 画像の輪郭群を取得して、重心毎に色を記憶しておき、次のフレーム画像に対して近い位置にある重心に同じ色を塗っていくような画像処理の範疇で実装できるのでは
  • 中割の自動化:原画と原画のモーフィングのような手段しかアイデアがない。既存手法も似たようなものではなかろうか。CNNで上手いことやればどうにかできるのかもしれないが、私には知見がない

完成動画から抜き出したデータを含めてどんなことができそうか

完成後の動画を使ったアレコレも許されるのであれば、以下のようなデータを生成して追加で試してみたい。

生成

  • 完成動画(撮影処理後)のコマ毎の画像に切り出す
  • 画像を線画化したものを作成する

やってみたい

  • エフェクトの自動化?:着色後中割とコマ毎の画像の差分でどんな撮影処理が入っているかを見る
    • GohhoChainerのような画風変換処理でエフェクトの感じを抜き出せないだろうか
  • 以下、制作側を助けるものではなく、ファン側が悦に入るための仕組みについて
    • カット数を数える:場面の移り変わりを検知してカット数を数える。作品の予算感が分かる部分である
    • 動画枚数を数える:単純に差分のある枚数だけ数えても、炎のCGエフェクト等で毎フレーム差分がある箇所があったりする。そういうのは除外して作画してある動画枚数を数えたい。カット数と同じく、作品の予算感が分かる。
      • 正解として14000枚という目安があるのはとてもありがたい
    • OP・EDに出てくるスタッフ一覧をOCRでデータ化する:あの原画の人がここでもーみたいなのを色んなアニメ作品を横断して知りたい
      • 作品によってフォーマットが違いすぎるので、公式にどこかに出してもらうようにするほうが100倍くらい建設的ではある

アニメ業界を救えるの?

6月7日のクローズアップ現代でもアニメーターの貧困が取り上げられて、一般的なアニメファンの間でも業界をどうにかしたほうが良いのではという機運が高まっているのを感じます。 ただ、門外漢からすべての状況が見えるほど単純な問題ではなさそうなので、「実際にある具体的な問題を提示してもらって、技術者として解を見つけられそうなら協力する」くらいのスタンスでいるのがいいのかなと思います。今回で言えば「出)原画、中割、着色後中割 求)着色の自動化、中割の自動化、エフェクトの自動化」くらいの対応であるべきかと。

「技術でこんなことができそうだなー」と門外漢で盛り上がるのは勝手だが、「アニメ業界はこんなことをするべきだ!」と押し付けるのはちょっと違うかなと。

まとめ

はやくクラウドファンディングに支援して画像処理やら機械学習やら深層学習やらでこんなことができそうだねたーのしー、になりましょう。 (この記事を書くのが遅すぎてあと少ししか期間がない。6月12日までです) 僕が考えたもの以外でもやってみたいこと・やれそうなことでアイデアを募りたい。

せっかくデータを色んな人が入手しそうなので「東北ずん子のことをやるもくもく会」とかやっていきたい。あわよくばアニメ関係者にも参加してもらって知見・アイデアをいただきたい。

支援するためのクラウドファンディングのページはこちら

  • 「ずんだホライずんのデータをゲットできる特別コース」(30000円)と合わせて、「アニメを絶対見るぞコース」(5000円)を申し込むのが色々ゲット出来る組み合わせっぽいです。

PyQについて私的なおすすめポイントを紹介します

株式会社ビープラウドが「PyQ」というオンラインでプログロミングが学べるサービスを4/12にリリースしました。

私の所属は、勉強会等でお会いした方はご存知かもしれませんが前述のビープラウドであり、私も問題作成とマーケティングで関わっているので、これはリリース記念の記事というやつです。もちろん製品に自信があってユーザーに広めたい思いがあって書かれています。

メインで関わったメンバーが気持ちのこもった記事を書いているのでそちらも参照していただきつつ、

サービス自体の紹介はPyQ公式サイトを見ていただくとして、私からは個人的に良いと思っている部分をできるだけ客観的に伝えられたらと思います。

お品書き

  • 個人的に思うPyQの良いところ
    • 環境構築がいらない
    • Pythonがメイン
    • 色んなレベルの人に学びがある
  • まとめ

技術的な部分などは、いずれ開かれるであろうPyQ Meetupのような場で話されるのがよいかと思うのでここでは割愛します。

個人的に思うPyQの良いところ

環境構築がいらない

Pythonの環境構築自体は知っていれば簡単に済ませられるものですが、インターネットで調べると情報が錯綜していることもあり、必要のないpyenv導入、必要のないvirtualenv導入、OSで使っているPythonが上書きされて死、pyenvとpyvenvで紛らわしいところにpipenvとかも出てきて死、など色んな罠が存在します。 Python自体はインストールできてpipが動くようになっても、Windowsでは色んなライブラリのインストールに失敗し複数バージョンのVisual Studioと関連ライブラリをインストールしては入れ直しという地獄に陥ったり、仕方なくAnacondaを導入して既存の環境が壊れたりと待ち受ける障害が多いです。 Pythonを使う前から嫌いになるという話もたまに聞きますが、無理もないなと感じています。

PyQではWEBブラウザからdockerを通して立ち上がったコンテナ上で問題を解けますので、PCだけでなくiPad等のタブレットでも学習を進められます

4/26 16:45 追記

現在、iPhoneiPad等の非PC環境からだと日本語が入力できないため文字列として名前を入力する場合等、一部の問題が解けない不具合があります。必ずしも学習が進められるわけではないので訂正します。

WEBアプリを作る問題では、WEBフレームワークであるDjangoもコンテナ上で動きhtmlの描画結果も確認できます。むちゃんこ楽ですね。

以下、余談。私は仕事でPython 3.5系を使いつつもプライベートでは最新の3.6系を使いたいので、3.5系は公式バイナリ、3.6系はHomebrewで入れてます。仕事用の環境を作るときは$ python3.5 -m venv work_envのようにして3.5系の環境を用意し、プライベートではipythonやjupyter系含めて3.6系を使用するようにしています。プライベートではWEB系から画像処理、機械学習と使用するライブラリが多いので仮想環境は用意しないことが多いです。

Pythonがメイン

プログラミング言語の選定はその人が何を実現したいかによる部分ですが、私の例だと個人で運営しているサービスとして「Twitterの情報からコミケ等のサークルチェックが簡単・便利にできるサービス」という触れ込みのsubcatalog、ライフワークとして4コマ漫画の評論をプログラムで推進することをやっています。(参考1参考2

そのため、私がプログラムで実現したいことはWEBサービス開発、画像処理、データ分析・可視化、自然言語処理機械学習・深層学習と多岐に渡ります。 これらの分野に優れたライブラリが存在すること、私の使用範囲においてはそこまで高いパフォーマンスが必要ではないということから、基幹言語はPythonですべてまかなえています。

4コマ漫画の分析・評論をプログラミングで始めたい人には是非ともPythonがおすすめです! というのは冗談にしても、だいたいのジャンルでやりたいことはカバーできるので今からプログラムを学び始める人にとっては良い選択肢のように感じられます。 Pythonの情報を探すと英語で読まざるを得ないことは多いですが、問題解決だけなら簡単な単語とコードを読めば済むのでけっこうどうにかなります。 人気の言語ランキングや言語別年収ランキングで上位に来るなんて話もありますが、実際そこまで参考にするようなものではないと思うので、用途に合わせた選択をするのが良いです。

色んなレベルの人に学びがある

Python入門者向けハンズオン等のイベントでメンターをしたり、プライベートの時間でもPython初心者向けのテキストを書いていたりするのですが、双方で期待する受講者の到達レベルの1つに自分のやりたいことがプログラムで実現できるというものがあります。

では、何を教えるべきかというと、とあるプログラミング入門者向けの本に「どのプログラミング言語でも使う8つのキーワード」というものが載っていました。(本の名前は失念しました…)

以上の8つですが、はあはあなるほどというラインナップで、クラス・インスタンス・メソッドはともかくその他の概念を理解してもらえばまずは何か作れるようになりそうという気持ちになれます。

しかしながら、「初心者」のレベルが違うために「どう教えるか」のところで分岐ができてしまいます。大きなところとしては、プログラミングそのものが初めての人と他言語経験がある人とは説明すべき内容が違ってくるというものです。

書籍のような静的コンテンツでは無理なく説明できる範囲に限界がありますし、対面型で人が人に教えるのは講師・メンター側の力量が問われ、良いものを提供しようと思ったらコストが大きくなります。

PyQのシステムであればレベルに合わせて問題を解き進められます! というただの宣伝なのですが、これを対面型の研修に導入すると、講師は最低限のフォローをするだけで受講者は独立でどんどん進めるので非常に楽になります。チーム機能を使用すればメンバーの学習状況もまとめて見られるので、色んな人が幸せになります。

中級者向けにもunittestの書き方、argparseの使い方、docstringの書き方、loggingのしかた等の1つ1つのまともな使い方を調べるだけで時間がかかるコンテンツがまとめて学べるのはいい部分かと思います。おそらく、ここまで細かい内容で問題を作っている他のオンライン学習サービスさんはないかと思っていますが、もしいらっしゃったらこっそり教えてください。

どんな問題があるのかはクエスト一覧をご覧ください。

まとめ

PyQをよろしくお願いします。

ゆゆ式 Advent Calendar 2015のオープニングと4コマ切り出しプログラム

(12/1 12:10 追記したり斜めの画像を差し替えたりしました)

オープニング的なご挨拶

お久しぶりです。私です。

去る11月26日にBD-BOXが発売し、来年3月にはイベント開催も決定し、三上小又先生のインタビュー記事( 『ゆゆ式』を作り上げた大切なキーワードとは | アニメイトTV )が出てくるなど、アニメ本放送から2年以上経って新たな盛り上がりを迎える『ゆゆ式』ですが、今年もAdvent Calendarの季節がやってまいりました。

”Advent Calendarは本来、12月1日から24日までクリスマスを待つまでに1日に1つ、穴が空けられるようになっているカレンダーです。WebでのAdvent Calendarは、その風習に習い、12月1日から25日まで1日に1つ、みんなで記事を投稿していくというイベントです。”

おそらく初めて実施したであろう去年は、多くの方に参加していただき、『ゆゆ式』に関するイラスト・漫画・3DCG・考察・その他が集うバラエティ豊かなAdvent Calendarとなり、大いに盛り上がりました。 www.adventar.org

調子に乗って今年も募集してみたところ、この記事を書き始めるちょっと前くらいに25日分埋まったようです。ありがとうございます。 www.adventar.org

去年に引き続いて参加されている方も多い&似たような日付に陣取っていることが多いように見えるのがなるほどと思います。

それでは今年もゆゆ式 Advent Calendarの行く先を見守っていただければ幸いです。

4コマ切り出しプログラム

というところでオープニングは終わりなので私の責務は果たしたような気になっていますが、 せっかくなので去年、大変に好評だったエントリから影響を受けて作ったプログラムをご紹介します。

non117.hatenablog.com

上記がそのエントリですが、技術的にも発想的にも、愛や狂気が見て取れる素晴らしいエントリに仕上がっています。 この中にある枠線の検出方法が目からウロコ&自分でもコマを切り出したいという思いから後追いで作ってみることにしました。

成果物
  • 結論から言うと、記事中のロジック実装は難しくなかったのですが、手元の自炊した画像だと微妙に線が斜めになっているものがあり、それだと切り取るべき枠線がうまく検出できないようでした。
  • 斜めを修正することも考えましたが、色んな画像を切り取ることを考えると一括で処理できそうなものが思いつかなかったので、平均的な切り取り位置を検出して、一括で切り取ることにしました。
  • その際、Paddingを足してあげて広めに切り取ることで、できるだけほしい部分が切り取れているように調整しました。綺麗にする処理を別に作ってもよいかとは思います。
  • また、扉絵があっても気にせず定位置で切り取るので、そこは扉絵とコマを別に切り抜けるようにはしたいところです。

f:id:esuji5:20151201013946j:plain

平均に近い位置で切り抜けている画像

f:id:esuji5:20151201014027j:plain

横に寄った位置で切り抜かれている画像

f:id:esuji5:20151201120914j:plain

斜めがきつくて、うまく切り抜き位置を検出できないけど平均位置切り抜きでなんとかなった画像

Require
  • Python 2.7
  • OpenCV 2.4.12
  • numpy 1.9.2
    • Python以外のバージョンは最新でよいかと思います。
    • Pythonが2.7系なのは私がGoogle App EngineやTensorFlowに縛られているからです。
    • それ以外なら3.4系以上でよいかと思います。日本語でつまづく確率がぐっと減ります。

インストール手順は、記事の反応的に必要そうだったら追記します。1つだけ先に言っておくと、Windows系はOpenCVのインストールでハマる事が多いので最悪VMLinux環境を用意したほうがよいかもしれません。もしくは画像を扱っている部分をpillowに差し替えるなどでもよいと思います。

Code

avg_cut.py

# - * - coding: utf-8 - * -
import os
import sys
import glob
import cv2

import cut
from cut import CP_NUM_X
from cut import CP_NUM_Y

AVG_COUNT = 6


if __name__ == "__main__":
    # inputから指定のディレクトリを取得
    if len(sys.argv) <= 1:
        raise IOError(u'処理対象のディレクトリパスを入力してください')
    image_dir = sys.argv[1]
    if not os.path.exists(image_dir):
        raise IOError(image_dir + u'は存在しません')

    # 書き出し用ディレクトリを作成
    output_path = os.path.join(image_dir, 'cut_images')
    if not os.path.exists(output_path):
        os.mkdir(output_path)
    print '書き出しディレクトリ:', output_path
    # ディレクトリ中の画像ファイルパスを取得
    image_path_list = glob.glob(os.path.join(image_dir, u'*.jpg'))
    image_path_list.extend(glob.glob(os.path.join(image_dir, u'*.png')))

    # 切り出し座標=カットポイント(cp)を探すためのループ
    print '切り出し座標を検出しています'
    odd_cp_list = []  # 奇数indexページのカットポイントを格納
    even_cp_list = []  # 偶数indexページのカットポイントを格納
    for index, image_path in enumerate(image_path_list):
        if len(odd_cp_list) >= AVG_COUNT and index % 2 == 1:
            continue
        if len(even_cp_list) >= AVG_COUNT and index % 2 == 0:
            continue
        img = cv2.imread(image_path)
        cp_dict = cut.search_cut_point(img)
        if len(cp_dict['x']) == CP_NUM_X and len(cp_dict['y']) == CP_NUM_Y:
            if index % 2 == 1:
                odd_cp_list.append(cp_dict)
            else:
                even_cp_list.append(cp_dict)
        if len(odd_cp_list) >= AVG_COUNT and len(even_cp_list) >= AVG_COUNT:
            break

    # 平均カットポイントを算出
    odd_page_cut_point = cut.find_average_point(odd_cp_list)
    even_page_cut_point = cut.find_average_point(even_cp_list)

    # 平均切り出し座標から画像を切り出すループ
    print '画像を切り出しています'
    for index, image_path in enumerate(image_path_list):
        img = cv2.imread(image_path)
        image_path = os.path.join(output_path, os.path.split(image_path)[-1][:-4])

        if index % 2 == 1:
            cut.cutout(img, odd_page_cut_point, image_path=image_path)
        else:
            cut.cutout(img, even_page_cut_point, image_path=image_path)

cut.py

# - * - coding: utf-8 - * -
import math
import cv2
import numpy as np

CP_NUM_X = 4
CP_NUM_Y = 8
DIFF_N = 1  # diffをとる間隔
DIFF_THRESHOLD = 60  # 枠線があるかどうかのdiff値の境界
LINE_WIDTH = 4  # 枠線の範疇と判定する太さ(ピクセル)
PAD_X = 12  # 平均の切り出し座標から余白を横方向に取る(ピクセル)
PAD_Y = 7  # 平均の切り出し座標から余白を縦方向に取る(ピクセル)


# 横方向に使う言葉: x, width
# 縦方向に使う言葉: y, height
def search_cut_point(img, image_path='', idx=''):
    def get_row_avg(x):
        return sum([img[yi - 1, x, 0] for yi in y_plot]) / height

    def get_col_avg(y):
        return sum([img[y, xi - 1, 0] for xi in x_plot]) / width

    def find_cut_point(big_diff_list):
        cp_list = []
        recent_point = 0
        # 座標位置の差分が設定した線の太さより大きいときにカットポイントを設定
        for cut_index in big_diff_list:
            # カットポイントの要素数が偶数。白から黒、最初の点
            if len(cp_list) % 2 == 0 and cut_index[1] <= 0 and cut_index[0] - recent_point >= LINE_WIDTH:
                cp_list.append(cut_index[0])
                recent_point = cut_index[0]
            # カットポイントの要素数が奇数。黒から白、最後の点
            elif len(cp_list) % 2 == 1 and cut_index[1] >= 0 and cut_index[0] - recent_point >= LINE_WIDTH:
                cp_list.append(cut_index[0])
                recent_point = cut_index[0]
        return cp_list

    def define_cut_point():
        cp_list = []
        for i in range(0, len(cp_y)):
            if i % 2 == 0:
                try:
                    cp_list.append([cp_y[i], cp_y[i + 1], cp_x[0], cp_x[1]])
                    cp_list.append([cp_y[i], cp_y[i + 1], cp_x[2], cp_x[3]])
                except IndexError:
                    pass
        return cp_list

    height, width = img.shape[0], img.shape[1]
    y_plot, x_plot = np.arange(1, height, 1), np.arange(1, width, 1)

    row_avg_list = [get_row_avg(x - 1) for x in x_plot]
    col_avg_list = [get_col_avg(y - 1) for y in y_plot]

    # avgのdiffを取り、境界値より大きな座標とそのdiff値をbig_diffに保持
    row_avg_diff = np.diff(row_avg_list, n=DIFF_N)
    col_avg_diff = np.diff(col_avg_list, n=DIFF_N)
    row_avg_big_diff = [[i, row_avg_diff[i - 1]] for i in range(1, len(row_avg_diff)) if math.fabs(row_avg_diff[i - 1]) >= DIFF_THRESHOLD]
    col_avg_big_diff = [[i, col_avg_diff[i - 1]] for i in range(1, len(col_avg_diff)) if math.fabs(col_avg_diff[i - 1]) >= DIFF_THRESHOLD]

    # カットポイントを定義
    cp_x = find_cut_point(row_avg_big_diff)
    cp_y = find_cut_point(col_avg_big_diff)

    return {'x': cp_x, 'y': cp_y}


# 渡した画像とカットポイントでimage_pathに切り出す
def cutout(img, cut_point, image_path='trim'):
    cp_x = cut_point['x']
    cp_y = cut_point['y']
    for i in range(0, len(cp_y)):
        if i % 2 == 0:
            im_trim = img[cp_y[i] - PAD_Y:cp_y[i + 1] + PAD_Y, cp_x[0] - PAD_X:cp_x[1] + PAD_X]
            cv2.imwrite(image_path + '-' + str(i / 2 + 5) + '.jpg', im_trim)
            im_trim = img[cp_y[i] - PAD_Y:cp_y[i + 1] + PAD_Y, cp_x[2] - PAD_X:cp_x[3] + PAD_X]
            cv2.imwrite(image_path + '-' + str(i / 2 + 1) + '.jpg', im_trim)


# カットポイントの平均を算出
def find_average_point(cp_list):
    average_list_x = [0 for i in range(CP_NUM_X)]
    average_list_y = [0 for i in range(CP_NUM_Y)]
    for cut_point in cp_list:
        for index, cp_value in enumerate(cut_point['x']):
            average_list_x[index] += cp_value
        for index, cp_value in enumerate(cut_point['y']):
            average_list_y[index] += cp_value
    average_cp_x = [i / len(cp_list) for i in average_list_x]
    average_cp_y = [i / len(cp_list) for i in average_list_y]
    return {'x': average_cp_x, 'y': average_cp_y}
usage

avg_cut.pyとcut.pyを同じディレクトリに置き、端末から

$ python avg_cut.py path/to/image/dir を実行します。

「path/to/image/dir」は画像があるディレクトリを指定してください。

うまくいけば、しばらくした後、path/to/image/dir/cut_imagesに切り抜かれた画像が保存されます。

このコードだと画像群がないと多分動かないので、単一の画像で試す場合は

$ python
>>> import cv2
>>> import cut
>>> img = cv2.imread('path/to/image/hoge.jpg')
>>> cp = cut.search_cut_point(img)  #cp['x']が4つ、cp['y']が8つ返ってくればOK
>>> cut.cutout(img, cp)  # 実行ディレクトリにtrim-1.jpg~trim-8.jpgが生成される

で良いかと。

これで一挙に『ゆゆ式』のコマ画像が手に入ったので、最近興味を持っている機械学習なんかの分析にも手を広げていければと思います。

日本初らしいWeb3Dハッカソンに参加して、賞も頂いてきた話

概要

Web3Dのハッカソンに参加して、jThree + milkcocoaを使って
リアルタイムに3D空間で音のセッションができるようなものを作りました。

Web3dSession


また、協賛企業であるモバイルファクトリー様から賞として、
iTunes Storeカードとモバイルファクトリー様のステッカーを頂きました。


関係者各位にはこの場を借りて感謝申し上げます。

そもそもjThreeとは


jThree - 3DCGと立体音響が数行で動くWeb3Dライブラリ
ChromeFirefox、一部のIE等のモダンなブラウザで3Dコンテンツを動かせるWebGLのラッパーであるthree.jsをさらにラップすることで、jQueryでのDOM操作くらいが分かっていればブラウザで動作する3Dコンテンツを簡単に制作できるもの。
という説明で多分合ってると思います。
簡単に制作できる代償もそれなりにあって、動作が遅い、Webで3Dをやることの本質が理解しにくくなる等々言われているようです。

本文


【3DCG・立体音響の面白さで勝負】日本初Web3Dハッカソンで年越ししよう!jThree Class主催 Web3D Hackathon Japan 2014 : ATND
日本初らしいWeb3Dハッカソン、でもまあ、主催がjThreeの松田さんなので、実質、jThreeハッカソンなんだろうなと思いつつ時間が出来たので参加してみました。

協賛企業様

朝日新聞メディアラボ様からは会場を、
モバイルファクトリー様からはハッカソンで自由に使用できる音源を提供して頂きました。
ここに改めて感謝申し上げます。

アイディア発表、チームビルディング

参加できるのが決まったのが急だったので特にアイデアもなく、一緒に組んだお二方と考えることに。
結果、せっかく3Dオーディオが簡単に使えることを活かすのと、
Javascript1行を埋め込むだけで簡単にバックエンドが用意できるmilkcocoaを使って
音楽のセッションのようなことができたら面白いのではという案に落ち着きました。

作成

イデア出し1時間、昼休憩1時間、制作時間3時間半と時間が厳しいので以下のボーダーラインを設定し、まずはそこにたどり着くことを目標としました。

  • 固定した3点を置き、1つの点から音を出す処理を行うと、同時に接続してる他の人も同期してそこから音が出る。
  • また、3Dオーディオなのでカメラの位置によって、その音が左から聞こえたり右斜め前から聞こえたりする。

結局のところ、3Dオーディオの使い方を1から知るというレベルだったり、僕がmilkcocoaとの連携でつまらないミスを連発したりで、ボーダーはなんとか超えたというところでタイムオーバーでした。

出来上がったもの

概要にも貼りましたが、以下になります。

Web3dSession
右側の再生ボタンを押すとA、B、Cの3つのオブジェクトが現れ、それぞれbeat、scratch、thunderに対応しています。
カメラ切り替えボタンやドラッグで視点を変えて音を鳴らすと、音の聞こえ方が左からだったり右斜め前からだったりと変化することがわかると思います。
また、milkcocoaで同期しているため同時に接続している人が音を鳴らすと、自分のブラウザでもその音が聞こえてきます。


左のエディタを見てもらえると分かりますが、記述するのはGOML、HTML、CSSJavascriptと4種類ありますが、それぞれ50行も書いていません。これだけの記述で3Dオブジェクトを描いて、3Dオーディオを設定して、ブラウザ間のリアルタイム通信も行えるとは、jThreeとmilkcocoaの素晴らしさに誰しもが震えると思います。それこそ西野カナばりに震えますよね? 震えたくなくても、震える!

時間があればやりたかったこと

  1. BGMを設定して、ブラウザ間で再生タイミングを同期させる → jThreeの音楽を鳴らす関数にホストの再生時間をつっこめば出来そうだが、1秒くらい遅延がおきそう。
  2. オブジェクトを固定しないで、プレイヤーとして移動できるようにする →

milkcocoaでリアルタイム対戦できるFPSを作った話 - QiitaのようにFPSっぽく設定すれば出来るかと。というかこのゲームに銃の発射音を3Dオーディオで設定して、どこから撃たれているのかわかるっていうのも面白そうだと思った。

  1. 楽器を複数種類用意して多人数でセッション → 複数用意するのは大変。あと、再生タイミングの同期が音楽レベルにすると無理そう。

総じて、最初に目指した音楽のセッションというレベルは厳しそうだが、みんなが集まって音を出してるだけでも楽しいと思うのでチャカポコチャカポコできる何かが作れればいいなと感じた。

成果発表

初音ミクなんかは簡単に踊らせられるので、音ゲーを作っていたグループ、
jThreeは一切使わずthree.jsで立体音響を体感できるものを作り、技術の難しさに感動した松田さんからjThree賞をもぎ取ったグループ、
RedMine的なタスク管理ツールでのタスク割り振りを楽しくするためにjThreeを使おうぜ! という発想にいたったグループ、
バンジージャンプをしている人が杵を持ち、地上にある臼めがけてモチをつくという説明していてよく分からないけどインパクトある画面を作ってメディアラボ賞に輝いたグループ。
そして私のグループは音源を提供していただいたモバイルファクトリー様から賞を頂きました。
ステッカーがあるだけでもかなり嬉しいですね。

以下、今回のハッカソンやjThreeの勉強会に何度か出て思うところ

この日の松田さんの話で「Webで3Dと言っても、素人が見てすごいと思えることをやろうとするとレンダリング能力やファイルの容量=ローディングの重さの問題で現実的でないことが多い」というのがあって、これはそのとおりだと思います。
「その分、アイデアで勝負」というのも当然の流れかとは思いますが、まだどういうことが面白いのかという知見は集まっていないのが現状でしょう。
ただし、「『楽しい』をキーワードにjThreeを勧める」というのは非常に納得できる部分で、本来であれば面倒なことが多いはずの3Dコンテンツ作成において、今回の参加者の方々は私も含めてみんな楽しそうに作業をされていました。
今はまだ、技術を活かす道を模索する段階だとは思いますが、「楽しさ」でこちらに来てくれる人の可能性を考えれば、3Dコンテンツ全体の発展としては良い土壌となり得るのではないでしょうか。


あとはぱっとしての見てくれがいいものがあるといいかなと思うので、例えばアニメ『翠星のガルガンティア』でWebGLを用いた3Dゲームが無料で公開されていましたが、こういったコラボ企画が作れたりすると箔がつくかなと。

翠星のガルガンティア 〜キミと届けるメッセージ〜
質の良いモデルを使って簡単にWebで3Dコンテンツを作る、そういった道もあるのではないかと思いました。

参考URL

簡単にリアルタイム通信を実現:Milkcocoa - JavaScript一行に詰め込まれたバックエンド
milkcocoaとjThreeでFPSmilkcocoaでリアルタイム対戦できるFPSを作った話 - Qiita
Webオーディオの使い方について:jQueryの記法で学ぶWeb3D勉強会@Adwaysに行ってきました | takemikami's note
少し前の動画だけど、MMDでここまで出来るという作例(単に紹介したいだけ)

.

Raspberry Piを触ったのでメモ的な何か

参考URL

NOOBSダウンロード
http://www.raspberrypi.org/downloads/

初起動時設定
RaspberryPi - Raspberry Pi + 無線LAN セットアップ - Qiita
http://qiita.com/daicham/items/e24ce76f8815d339d070

無線LAN設定(うまくいかなかった)
これから Raspberry Pi をいじる方向けの資料 20130818版
http://www.slideshare.net/tominagamichiya/raspberry-pi-20130818

GPIOで単純なLED制御
raspberrypi/Raspberry PiでIO制御 - PukiWiki
http://www.pwv.co.jp/~take/TakeWiki/index.php?raspberrypi%2FRaspberry%20Pi%E3%81%A7IO%E5%88%B6%E5%BE%A1
Raspberry PiでGPIO制御 - 人と技術のマッシュアップ
http://tomowatanabe.hatenablog.com/entry/2013/01/14/181116

目標っぽい曲げセンサ付きグローブ
工作と小物のがらくた部屋: 夏休みの工作 ・・・ センサグローブとロボットハンド、別名、パイ揉みハンド
http://junkroom2cyberrobotics.blogspot.jp/2013/08/blog-post.html

コマンド

デスクトップGUI表示: $ startx
シャットダウン: $ sudo poweroff
vimをインストール: $ sudo apt-get install vim
gitをインストール: $ sudo apt-get install git-core

RtRTはAPI廃止でダメになった→すまん、ありゃウソだった

はてなブログをなんとなく開設してみました、私です。

エイプリルフールなネタというよりは、時間が経って考えてみたり実装してみたりしたらRtRTはTwitterAPI仕様で十分いけるやんと気付いたので、こんな記事でも書いてみようかと思いました。

概要

公式RT直後の発言が分かる「RtRT」が面白い - ねとらぼ
http://nlab.itmedia.co.jp/nl/articles/1210/30/news118.html

こんな感じに

  1. 自分の発言がRTされる
  2. その後、RTした人がなにかそれについて言及しているかもしれない
  3. 気になるので拾ってみよう

という流れで、RTされた直後のつぶやきを取りに行くサービスを2012年10月からTwitter API1.0廃止まで運営してました。


この時点では

  1. 核になる3つのAPIの2つが廃止
  2. APIを叩ける回数が350回/1時間から15回/15分に変更

という2点の変更があり、今までどおりの運用は不可能と判断しサービスを停止するに至りました。
でも、最近になって、よく仕様を見てみるとなんだかできそう→できた、ということがあったので報告までしておきたいと思います。
あくまでRtRTは機能的に現在でも実装できる!というだけで、サービスは色々と面倒なので再開しません。

プログラムの流れ

  1. 自分がRTされたつぶやきの一覧を取得する(statuses/retweets_of_me)
  2. 各つぶやきをRTした人を調べる(statuses/retweets/)
  3. RTした人のTLを見にいって、自分のRT直後のつぶやきを見つける(statuses/user_timeline)
  4. データとして詰めあわせてjson形式で吐き出す

RTした人たちのユーザIDだけの取得でよければ、2はstatuses/retweeters/でも多分問題ありません。

API制限を見てみたらさらに実装可能感が増す

ここでAPI制限を確認してみますと、なんと各API毎に制限値と残数と復活までの時間が設定されたようで、現在では1と2は15回/15分、3は180回/15分となっているようです。
1はプログラム起動時に1回しか使う必要がなく、2はつぶやき毎に1消費しますが、15分に15個分を1度見られれば十分です。3はなぜか180回/15分も叩けるので以前の350回/1時間よりも効率が良くなっているという状態です。さらに、3のオプションであるcount=200とpage数に対応が取れるようになったため、つぶやき量が多い人に対してもかなり深くまでAPI回数を消費せずにリーチできるようになりました。
こういった点から、15分に一度の起動で100RT程度までの対応ならば問題なく行えるといえるでしょう。

Pythonで実装してみた

Python2.7 + tweepyでコマンドラインからjson吐き出しまでを見てみます。

CONSUMER_KEY='XXXXXXXXXX'、CONSUMER_SECRET='XXXXXXXXXX'

ACCESS_KEY='XXXXXXXXXX'、ACCESS_SECRET='XXXXXXXXXX'

には、それぞれ自分のTwitter開発者用のものを入力してください。

#!/usr/bin/env python
# -*- coding: utf-8 -*-

import tweepy
import json
import codecs

LIMIT_RT = 5
LIMIT_PAGE = 20
LIMIT_COUNT = 100

CONSUMER_KEY='XXXXXXXXXXXXXXXXX'
CONSUMER_SECRET='XXXXXXXXXXXXXXXXX'
ACCESS_KEY='XXXXXXXXXXXXXXXXX'
ACCESS_SECRET='XXXXXXXXXXXXXXXXX'

auth = tweepy.OAuthHandler(CONSUMER_KEY, CONSUMER_SECRET)
auth.set_access_token(ACCESS_KEY, ACCESS_SECRET)
api = tweepy.API(auth)


def getRtrt():
    try:
        retweets_of_me = api.retweets_of_me(count=LIMIT_RT)  #rtされた一覧
    except:
        print 'auth or api error!(retweets_of_me)'

    rtrt = []
    for rt in retweets_of_me:  #ID毎にループ
        print rt.id
        rtrts = []
        try:
            retweeter = api.retweets(id=rt.id_str)  #rtした人一覧
        except:
            print 'auth or api error!(retweets)'

        for rter in retweeter:  #人毎にループ
            search_id = rter.id_str
            print "whort:" + str(rter.user.screen_name)
            rtrt_status = {}
            b_flg = False
            for p in range(1, LIMIT_PAGE):
                if b_flg:
                    break

                try:
                    tl = api.user_timeline(id=rter.user.id_str, count=LIMIT_COUNT, page=p)
                except Exception:
                    print 'auth or api error!(user_timeline)'

                print "p:"+str(p)+", c:"+str(len(tl))

                for i, v in enumerate(tl):
                    if i == 0 or i == len(tl)-1:
                        print "id:"+v.id_str+":"+search_id
                    if v.id_str == search_id:
                        rtrt_status = {'user': {'id': tl[i-1].user.id_str,
                                                'screen_name': tl[i-1].user.screen_name,
                                                'protected': tl[i-1].user.protected,
                                                'profile_image_url': tl[i-1].user.profile_image_url},
                                        'id': tl[i-1].id_str,
                                        'text': tl[i-1].text,
                                        'created_at': str(tl[i-1].created_at)}
                        b_flg = True
                        break
                    elif v.id_str < search_id:
                        b_flg = True
                        break

            if not rtrt_status:
                rtrt_status = {'user': {'id': v.user.id_str,
                                        'screen_name': v.user.screen_name,
                                        'protected': v.user.protected,
                                        'profile_image_url': v.user.profile_image_url},
                                'id': '',
                                'text': u'too much tweet!',
                                'created_at':''}

            rtrts.append(rtrt_status)
        rted_status =  {'user': {'id': rt.user.id_str,
                                'screen_name': rt.user.screen_name,
                                'protected': rt.user.protected,
                                'profile_image_url': rt.user.profile_image_url},
                        'id': rt.id_str,
                        'text': rt.text,
                        'created_at': str(rt.created_at)}
        rtrt.append({'rted_status':rted_status, 'rtrts':rtrts})

    return rtrt

def wjson(content, filename):
    f = codecs.open(filename + ".json", "w", 'utf-8')
    json.dump(content, f, ensure_ascii=False, indent=4)
    f.close


get_rtrt()した中身をwrite_json()に入れてjson形式で出力します。

[

(中略)

    {

        "rted_status": {

            "text": "話がジェットコースターなので、よくよく考えるとなんかおかしいところもある気はするけど、作品全体でのパゥワが凄すぎる。", 

            "created_at": "2014-03-22 14:32:38", 

            "user": {

                "protected": false, 

                "id": "14382115", 

                "profile_image_url": "http://pbs.twimg.com/profile_images/378800000394120263/9fe4afc9596c0f7163fce33261bce6c2_normal.png", 

                "screen_name": "esuji"

            }, 

            "id": "447380337069277184"

        }, 

        "rtrts": [

            {

                "text": "私の好きな作品によくある事(  ´ω`  )", 

                "created_at": "2014-03-22 14:35:22", 

                "user": {

                    "protected": false, 

                    "id": "XXXXXXXXXXXXXXX", 

                    "profile_image_url": "http://pbs.twimg.com/profile_images/XXXXXXXXXXX", 

                    "screen_name": "XXXXXXXXXXX"

                }, 

                "id": "XXXXXXXXXXXXXXXXXXXX"

            }

        ]

    }, 

(中略)

]

あまりRT言及っぽいのが取れなかったですが、上が私のつぶやきで下がその直後のつぶやきです。許可をもらってないので個人情報をマスクしておきましたが、実際にはちゃんと取れています。
あとはこのjsonjavascriptなんかで整形してやれば、きちんとコンテンツになります。
ただ、そのためにサーバやらなんやらの用意をする気力が私にはないので、やる気のある方が出てくれればというところです。


ということで、RtRTがダメになったというのは事実と異なる見解でした、すみません。
というところで本題は締めたいと思います。

実装についてもうちょっと詳しく

①兎にも角にもまずはアプリ用のトークンが必要です。
(上記で自分で入力する必要がある4つの文字列)
こちらはPHPの記事ですが、トークンをゲットするまでは同じ道のりなので参考になると思います。
http://webnaut.jp/develop/633.html
Pythonとtweepyをインストールする。
特に説明するほどでもない気がするので、ぐぐっていただけると話が早いです。
私はPython2.7を使用しましたが、3系でもそこまで問題ないような気がします。
③上記のソースコードに自分のCONSUMER_KEY、CONSUMER_SECRET、
ACCESS_KEY、ACCESS_SECRETを入力して任意の名前(ここでは例としてrtrt.py)で保存。
コマンドプロンプトやらコンソールやらでPythonの対話モードシェルを起動。
>>> import rtrt
>>> json = rtrt.get_rtrt()
>>> rtrt.json_write(json, 'rtrt')
これで同じフォルダにrtrt.jsonが生成される。
これをテキストエディタで開いて眺めたり、javascriptで整形して表示したりすると幸せになれる(※個人差があります)


謝辞として、各APIの仕様と取ってくるデータを確認するのにtwitSandbox様には大変お世話になりました。
http://twitsandbox.com/
また、参考にさせていただいたサイト、ブログ様もたくさんありますが、ありすぎて一つ一つは覚えてません、すみません・・・。