この記事は
「クソアプリ Advent Calendar 2019」の20日目の記事となります。
過去の記憶
あなたがはじめてパソコンを触らせてもらったときのこと、覚えていますか?
私はよく覚えていません。
画面の左上のリンゴのマークを見て、「かっこいい!」と思ったような気もしますし、机の上でマウスを動かすと画面上のカーソルが動くことに驚いたような気もします。
でもよくよく記憶を辿っていくと、どの体験も何となくおぼろげで、
後から作られた記憶のような、実は誰か他の人の記憶なんじゃないかというような、
そんな不安に駆られるのです。
何かに感動したことだけは覚えているのに、どの記憶も確かじゃない。
そんな不安定な遠い過去の記憶の中で、はっきりと覚えていることが一つだけあります。
シャットダウン。
パソコンを終了させるときに行う、あの行為です。
これまで無数に開いていたウィンドウがトリガーを一つ引いただけで次々と閉じていく。
そしてすべてのウィンドウが閉じると次の瞬間にはパソコンの画面そのものが暗転する。
幼い頃の私は、そこになにか底知れぬ不気味さと怖さを感じました。
「無への回帰」とまでは思わないまでも、この世界もパソコンのようにいつか突然シャットダウンしてしまうのではないか。
そんなぼんやりとした恐怖感を抱いて、これまで生きてきたような気がします。
というわけでですね。今日はさっそく世界をシャットダウンしていきたいと思います。
今回作る物
世界をシャットダウンしていくアプリです。
右から左に流れるようにさくさく終わらせていきましょう。
I/Oを以下のように定義します。
Input: 今の世界
Output: 終わりゆく世界
今回は今の世界を表現するものとして「現実の写真」を使うことにします。
次に「終わりゆく世界」を表現したいので、Mask R-CNNという技術を使って物体認識をすることにします。
これは画像内から物体を検出してマスクすることができる技術で、ある特定の物体に対して画像処理をしたいようなときに有用です。
※画像は https://github.com/matterport/Mask_RCNN より
イメージとしては
画像 → 物体に分解 → 物体が消えていく → 世界の終わり
という感じ。
同級会には行けません。
もう、そのお店は消えるから。
なんでじゃんけんに負けたか明日までに考えなくても大丈夫。
もう、明日はやってこないから。
アドベントカレンダーも残念ですが今日で終わりです。
できたもの
(以前はこの行にURLがありましたがサーバーを止めたため動いてません)
操作手順
- あるTwitterアカウントにメンションで画像を送ります。
- しばらく待ちます
(サーバーを止めたため動いてません)
期待結果
機嫌が良ければ世界が終わります。
どうやって作ったか
分析部
Twitterから画像を取ってきたら、Mask R-CNNで分析します。
以下のリポジトリを見ながら作りました。
matterport/Mask_RCNN
Sampleの通り、MS COCOの学習済みモデルをそのまま利用しています。
# コード中のsunnydayというのはこのアプリのコードネーム
class ImageExecutor:
def __init__(self):
self.model = None
def load_model(self):
from sunnyday_bot.mask_rcnn_lib.executor import get_model
self.model = get_model()
def analyze_image(self, sunnyday_object):
from sunnyday_bot.mask_rcnn_lib.executor import detect_image
if not self.model:
self.load_model()
return detect_image(sunnyday_object['image_url'], self.model)
結果は以下のような形でParseして、Json形式でストレージに保存しておきます。
def parse_result(self, result):
parse = {}
output_array = []
for v in result['masks']['verts']:
new_array = []
for d in v:
for pt in d:
new_array.append(pt)
output_array.append(new_array)
parse['points'] = json.dumps(result['result']['rois'], cls=NumpyEncoder)
parse['verts'] = json.dumps(output_array, cls=NumpyEncoder)
return parse
parse後の「verts」が物体の形を捉えているマスクの部分、
「points」が物体を囲む長方形の部分になります。
APIサーバーはこの情報をフロントに返せばいいだけです。
フロント
見るべきところはあまりありません。
Canvasにガリガリ書いて、タイマーで動かすゴリ押し仕様となっております。
タイマー時刻をランダムで設定しておくことで「徐々にものが消えていく」様子を表現しようとしています。
const rects = res['points'].map((e, index) => {
timers.push(100 + this.getRandomInt(100))
return {
x: e[1] * scale,
y: e[0] * scale,
height: (e[2] - e[0]) * scale,
width: (e[3] - e[1]) * scale,
}
});
let blackRects = [];
const halfTime = maxTimer / 2;
this.generateBlackRects(halfTime, halfTime - 80, 50, res, scale, blackRects);
this.generateBlackRects(halfTime - 80, halfTime - 120, 20, res, scale, blackRects);
this.generateBlackRects(halfTime - 120, halfTime - 140, 20, res, scale, blackRects);
this.generateBlackRects(halfTime - 140, halfTime - 150, 100, res, scale, blackRects);
const verts = res['verts'].map((e, index) => {
return e.map((e2) => {
return e2 * scale;
});
});
流れは以下の通りです。
起動
↓
APIから画像と座標を得る
↓
タイマースタート
↓
物体をノイズで覆う
↓
物体を黒塗りする
↓
ノイズを消す(黒塗りしたのが出てくる)
↓
四角い黒い物体をベタベタ貼る(尺余りを誤魔化す)
↓
時間になったらエラーメッセージを表示
ノイズはReact KonvaというライブラリにNoiseフィルターがあるのでこれを使いました。
<Rect
filters={[Konva.Filters.Noise]}
noise={1}
x={this.props.x}
y={this.props.y}
opacity={this.state.noiseOpacity}
width={this.props.width}
height={this.props.height}
fill={this.state.color}
ref={node => {
this.rect = node;
}}
/>
終了時に表示されるファミコン風のダイアログは、NES.cssを利用しています。
世界の未来
技術と科学によって、私達は無事世界を終わらせることに成功しました。
ですが、一つの世界が終わった後、こっそりF5キーを押してみたり、
ブラウザーの再読み込みボタンを押してみると……どうでしょう。
また新しい世界が始まり、再び少しずつ時間をかけて終わっていくのが見えるはずです。
画面の中の世界はずっとそれを繰り返し、恐らくはパソコンの寿命かAWSに登録してるクレジットカードの期限が切れるまで
延々と同じ処理を繰り返していきます。
その一方で、現実の世界と時間とアドベントカレンダーは未来に向けて一方向に進んでいきます。
明日、21日の「クソアプリ Advent Calendar 2019」は @ampersand さんです!