やる気パルスが回復するアニソン

40代のおっさんなので、新しいのは知りません。リンクは歌詞。

機動戦士ガンダム0083-STARDUST MEMORY-

THE WINNER
THE WINNER

銀河鉄道999

THE GALAXY EXPRESS 999
THE GALAXY EXPRESS 999

キン肉マン II世

愛のマッスル
愛のマッスル

真(チェンジ!!)ゲッターロボ 世界最後の日

今がその時だ
今がその時だ

六神合体ゴットマーズ

愛の金字塔
愛の金字塔


う。面倒になってきた。アニソンのリスト作るサービスくらいあってもよさそうなんだが…。

Behat + Minkで作ったテストをAndroid+Chromeでも動くようにした

状況

  • PHPで作ったアプリのE2EテストをBehat+Minkで実装している
  • アプリがモバイルでも動くかどうか確認する必要が出てきた
  • Behat+Minkで作ったE2Eテストを、Android上のChromeを使って動かして、動作確認をしたい

大まかな仕組み

以下のようになる。 すでにMacbook上でChrome Driverを使ってE2Eテストを動かしているので、adb -> Android -> Chromeのところを準備すればOK。

Behat -> Mink -> Selenium -> Chrome Driver -> adb -> Android -> Chrome

動かすためにやったこと

  • AndroidWifi経由で操作できるようにする
  • behat.ymlにAndroid用のプロファイルを追加
  • Androidからアクセスできるよう、BeforeScenarioでMinkとアプリの設定を切り替え
  • Chromeでのみ発生する要素をクリックできない問題に対処

以下、それぞれの説明

AndroidWifi経由で操作できるようにする

Android 11以降は、Wifi経由で操作ができるようになる。それ以前だとUSB接続のみ。
USBは面倒なのでWifiで接続できるようにした。

Android Debug Bridge(adb)  |  Android デベロッパー  |  Android Developers

ハマった点

  • Android Studioを最新にアップデートする
    • 苦労した覚えはあるんだけどなんだったか忘れた
  • ADBの接続コマンド
    • 公式ドキュメントを見てやれば大丈夫

behat.ymlにAndroid用のプロファイルを追加

extra_capabilitiesにgoog:chromeOptionsのandroidPackageを入れるのがポイント。 base_urlについては後述。

android:
  extensions:
    Behat\MinkExtension:
      base_url: http://#localhostIP#:8090
      selenium2:
        browser: chrome
        capabilities:
          extra_capabilities:
            goog:chromeOptions:
              androidPackage: com.android.chrome

Androidからアクセスできるよう、BeforeScenarioでアプリの設定を切り替え

通常、Macbook上ではlocalhostを使って接続しているので、URLは常に http://localhost で良いのであるが、AndroidからWifi経由で接続する場合は、IPアドレスを使って接続する必要がある。で、MacbookIPアドレスは常に変わる。(固定しろよって話もあるが...)

ちょっと面倒だけど、毎回実行時にIPアドレスを取得して、Minkとアプリの設定を書き換えることにした。

RawMinkContextを継承しているクラスで以下のコードを、@BeforeScenario で実行すればOK。

        $baseUrl = $this->getMinkParameter('base_url');

        if (strpos($baseUrl, "#localhostIP#") > 0) {
            // IPアドレスを取得
            $ipAddressesRC = preg_grep("/^192\./", gethostbynamel(gethostname()));
            $ipAdderss = array_shift($ipAddressesRC);

            // URLを書き換え
            $newBaseUrl = str_replace("#localhostIP#", $ipAdderss, $baseUrl);

            // 全てのContextに設定を反映
            $environment = $scope->getEnvironment();
            foreach ($environment->getContexts() as $context) {
                if ($context instanceof \Behat\MinkExtension\Context\RawMinkContext) {
                    $context->setMinkParameter('base_url', $newBaseUrl);
                }
            }
        }

ポイントは $environment = $scope->getEnvironment(); 以降のコード。
利用している全てのContextに対して、設定を変更する必要がある。

上記で変更したURLを使って、アプリの設定を書き換えればOK。コードは省略。

Chromeでのみ発生する要素をクリックできない問題に対処

ここまでで実行するだけならできる...んだけど、テストが通るようにするために、Chrome特有の問題に対応する必要があった。

問題とは以下のもの。画面外にある要素をクリックしようとするとエラーになる。

seleniumにてButtonがクリックできない時の対処法 - Qiita

解決方法は難しくない。Clickの前に上記のエントリにあるようにスクロールをするか、フォーカスしてあげればOK。
カスタムのContextを使っている場合は、コードにスクロールなり、フォーカスするコードを追加すればよい。

私は、カスタムのContextに加えて、Behat\MinkExtension\Context\MinkContext も使っていたので、メソッドをオーバーライドしてフォーカスするコードを追加した。以下のような感じ。クラスの名前がやっつけすぎるな...。

class MyMinkContext extends MinkContext implements TranslatableContext
{
    public function clickLink($link)
    {
        $link = $this->fixStepArgument($link);
        $elem = $this->getSession()->getPage()->findLink($link);
        $elem->focus();
        $elem->click();
    }

BlenderでコレクションごとにSTLファイルを出力するスクリプト

フルアーマーガンダムMk-IIのアーマーを自作するために3DプリンタELEGOO Mars 2を購入した。 Blenderを使って3Dモデルを作成し、CHITUBOXでスライス、印刷をしている。

f:id:couger:20210506212539p:plain

CHITUBOXで読み込むために、肩、腰、胸などのパーツごとにSTLをエクスポートしているんだけども、オブジェクトを選択、エクスポート処理を呼び出し、ファイル名を入力するという操作を何回も何回も何回も繰り返すのが嫌になってきたので、簡単なスクリプトを書いた。

  • targetsに指定したコレクションをSTLファイルに出力
  • 出力先は、"プロジェクトファイルのあるフォルダ"/stl。なければ勝手に作成
  • ファイル名は "full-armor-gundam-mk2_" + targetsで指定したsuffix
import bpy
import os
import shutil

bpy.ops.object.mode_set(mode='OBJECT')

targets = []
targets.append({"collections":["Shoulder"],"suffix":"shoulder"})
targets.append({"collections":["Chest-Side", "Chest-Center"],"suffix":"chest"})

out_dir_path = bpy.path.abspath("//stl")

if os.path.exists(out_dir_path):
  shutil.rmtree(out_dir_path)
os.mkdir(out_dir_path)

out_file_path_base = bpy.path.abspath("//stl/full-armor-gundam-mk2")

for target in targets:
  #print(target["suffix"])
  bpy.ops.object.select_all(action="DESELECT")
  for collection in target["collections"]:
      #print(collection)
      for obj in bpy.data.collections[collection].all_objects: 
          obj.select_set(True)
      
  bpy.ops.export_mesh.stl(filepath=out_file_path_base + "-" + target["suffix"] + ".stl", use_selection=True)

「でもやるんだよ」のツラみを好きなもので緩和しているお話

アジャイルに限らず、継続することって大事なんだけど大変ですよね。私は飽きっぽいので続けるツラみがハンパなくて困ってます。どうにかしてほしい。これ。

とはいえ、続けないと先はないので、飽きっぽい自分に「でもやるんだよ」と言い聞かせながら進んでいるわけですが、そもそもツラいことなので、真面目にやっていると息が詰まる。

息が詰まると続けられなくなる、続けられないと先がなくなる。とはいえ、ここから引き返して他の道を探るのはかなりツラそう。仕方がないので続けるか…というのが現在のポジション。

まぁ、そんな経緯はともかく、息が詰まらないように続けないと先がなくなっちゃうので、そのためにどうすればいいのか? と日々悩んでおり、その結果「自分の好き」から「でもやるんだよ」成分を摂取して、真面目で埋め尽くされないようにするという戦法に行きつきました。

そんなわけで、今日は、私のでもやるんだよを支えてくれているあれこれを紹介しようと思います。

水曜どうでしょう

水曜どうでしょう 番組スタッフからのメッセージ

そして、今後は、我々のわがままではありますが、
一生続けられるようなペースで、
「水曜どうでしょう」を作っていけたら、と思っています。

一生どうでしょうします
個人(代々受け継ぐやつを除くって意味)で続けられるレベルではこれ以上のものはないんではないかと。肩の力を抜いた持続可能なペースでありつつ、しっかりお金も稼いでいる彼ら。いや、ほんと、見習いたい。

みうらじゅん

「でもやるんだよ」の扉を叩いたのは、みうらじゅん氏の『「ない仕事」の作り方』がきっかけだったと思います。 ツラくなった時は「それがいいんじゃない!」「不安タスティック」などと叫んだりすると良いみたいですよ。

それがいいんじゃない! みうらじゅん 人生が楽になる魔法の呪文

『つまらないな』と思ったら『つま・・・』のあたりで、『そこがいいんじゃない!』って言うと、
つまらないことがいいことになるってことだから。
2倍戻ってくるんですよね。
ええ。『俺、つまらないところが好きだったんだ!』って思うと、
『そうだよな』みたいな、得した気持ちになりますよね?ええ。

不安タスティック 21人の「Life is」- ほぼ日刊イトイ新聞

人生はつねに不安なものである、しかし、
「不安」に「タスティック」をつけることによって
毎日をたのしんでいこうじゃないか──ということです。

角 征典氏

アジャイル界隈のでもやるんだよと言えば、角さんと角谷さん。で、私は角さん推しなんです。

でもやるんだよにあふれた?角さんのTwitterアカウントは是非フォローしてほしいです。

この話は「落語とは、人間の業の肯定である」という話につながっていくみたいです。いろいろと深そうな世界…。

もけんちゅTV

最近みつけたでもやるんだよ。

プラモアイドルの香坂きの氏のYoutubeチャンネルですね。最近再開したプラモデル作りはこの方の動画がきっかけ。

で、どこが「でもやるんだよ」かと申しますと、以下の動画。

www.youtube.com

10年続けているってところも驚いたんですが、5分くらいからの「やってみたいと思っている人の背中を押すのがプラモアイドルのお仕事」「ずっと同じことやってるじゃんって言われそうだけど、それがずっと初心者さんの近くにいるってことじゃないのかな」この辺のくだりがもうもうもう。ぜひ、動画をみてください。(こんなのバッカ)

マッキーめぐみのガンプラch

同じくガンプラつながりで。 初代ガンプラ王 鋭之介初代日野さんのお話が聞けるのであります。

この方のお話は、なんというか、私にとってのUncle Bobみがあるんですよ(分からない方は、Clean Agileを読んでみてください)。

一例をあげると…。

www.youtube.com

あなたは性格的に性急的なんでな、
ちょっと落ち着いて、少しずつ段階的にを身に着けませんと。
これは模型だけじゃありませんよ、人生全てにおいて段階的に落ち着いて…
とりあえず思いついたらパッと動く前に3秒考えて…
最も失敗しない、論理的科学的考え方はどれかって言うことがまず大事。
失敗の大半は「それは失敗しますよ」という方法を選んでいることが多いんで

持続可能のためにゆるみは必要だと思うんですが、だるんだるんになるまでゆるみすぎると良くない気もしており。たまに動画を見返すと背筋が伸びるんですよね。シャキッとするというか。

ちなみにこのチャンネル、見てる人はおじさんが多いそうで…。なるほど、納得。

倉持由香

グラビアアイドルの仕事論 打算と反骨のSNSプロデュース術」がスゴいんです。

グラビアアイドル史上初のビジネス書発売! 
「尻職人」倉持由香が伝授する最新の自己プロデュース術にして仕事論!

倉持由香「登れる山がないなら、自分で新しく山を作る」/ はたらく気分を転換させる|女性の深呼吸マガジン「りっすん」

ちょうど「グラドル自画撮り部」を始める少し前に、
「その大きな尻を活かした方がいいよ! 活かさなかったらただの無駄尻だよ!」と
知り合いのカメラマンさんに言われたんですよ。
自分ではヒップをコンプレックスだと感じていたんですが、
その言葉をきっかけに「売りにしていくぞ!」と決めて、タイムラインを尻で埋め尽くしてやろうと
5分に1回のペースで載せていきました。飯テロならぬ尻テロです(笑)。
私はグラドルとしては胸も小さい方ですし、
既にあるグラドルの山に登ろうとしても、他のグラドルには勝てません。
だったらもう、自分で山を作るしかない。王道のグラドルたちが登る富士山は諦めて、
その近くに尻の山を作りました。小さい山でも頂上に立つことが大事なんですよね。

アジャイル界隈もスゴい人ばっかりで、どの山も満員御礼なんですよ。となると、自分で山を作るしかないんですが、私に売りなんてあるわけないじゃん!!! と逆ギレしそうになるところを、ぐっと我慢。倉持氏のようにコンプレックスを逆手にとって山を作ればもしかすると…。

だからと言って、ハトの山を作っても仕方がないことはよくわかりました。はい。

高橋優 プライド

社内のコミュニティでとある方から教えてもらった曲。

www.youtube.com

君ではダメだと言われてしまったか?
君じゃない人の方がいいと諦められたか?

初手からツラみがスゴいんですが、聞いた後は仕方ない、じゃ、やるかって感じになったり、ならなかったり。

いろんなあれこれを集めてみ..たい??

いろんな人の「でもやるんだよ」を支えているあれこれを集めてみると面白かったりするのかしら。博覧会っぽく。

路地裏アジャイル

路地裏アジャイルとは何か?

何でそんなことを考えたのか?

私が「アジャイル向いてない人」だから。

2人でなら話せるけど、3人になると途端に話せなくなる。雑談が超苦手。天気の話から先がつながらない。人への興味が少ないんだと思う。個人と対話大事なのに。
自分中心なので、お客さんのためにってのが正直ピンとこないところがある。顧客満足は最優先なんだけども。
飽きっぽくてモクモクやるのが苦手。ふりかえりとかだんだん億劫になってくる。面倒なタスクは後に回しガチで、泣きながら追い込むことは多い。持続可能なペースはどこいった。

動くソフトウェアは好き。働くソフトウェアかと言われると微妙...。品質もまぁ動けばいいってくらいかな。品質は制御変数じゃないのに!!!

なので、まぁデフォルトとしてはアジャイル向いてない人なのだ。

何でそんな人がアジャイルをやろうと思うのか?

ロマンだから。憧れだから。そこに尽きる。自分にないものに憧れるってありませんかね。
漫画だと炎尾燃。映画だとLotRのフロドやサムとか。熱血、誠実。なんかそういうの。

じゃ、黙ってやってればいいではないのか?

いや、ホントその通り。それができたらいいんだけども...。

アジャイル向いてないから「しつけ」が必要になる。とはいえ、50年弱に渡って叩き込んだ習慣を直すのはとっても大変。
というか、直らない。調子の良い時にちらっと良い習慣が顔を出すことはあっても、疲れるとすぐ素の自分に戻る。
ずっとそれの繰り返し。このままずっとそうなんじゃないかと思う。というか、多分ずっとそうなんだ。

そういう状況だから、

「一生逃げるのもなんだけど、一生進むってのも……こわい」(逆境ナインより)

って感じで行ったり来たりしているワケ。
もちろん黙ってるやれるほどの根性はないから、こうやってブツブツ言いながらウロウロしているのである。

岡崎体育氏のSnackの歌詞がぴったりだ。

ズルしてるライフか無理してるライフ
どっちか選べばって殺生じゃない
ちょっとズルして少し無理したい

そう殺生なのだ。ちょっと弱音吐いて、少し修行したい。いや、だいぶ弱音吐いて、少し修行するくらいにしたい。

面倒なやつだ。結局、どうしたいの?

多分、私は私で勝手にダラダラやっていくんだろう。でも、せっかくだからもうちょっと面白くできないかとは思わないではない。
ツラみを面白くする、笑うって話だと、べてるの家と、水曜どうでしょうを思い出す。(理由はちょっと置いておく)
彼らのツラみに比べると、自分のツラみは小さく、取るに足らないものだろうけど、それでも自分のツラみを解消するために参考になることは多そうだ。

以下はべてるの家で活動している方のエントリ。

「笑う力~ユーモアの大切さ」べてるの家の笑いと当事者研究

病気についてだけではなく、誰もが持っている苦労や生活の工夫を仲間と一緒に話し合うことで、
「弱さ」を自分の中から取り出して、みんなでそれを眺めることができ、
弱さと上手に付き合う新しい工夫が生まれます。
精神障害の幻覚妄想の体験は、時に深刻な話題になりがちです。
しかし当事者研究という場には、いつもユーモアと笑いが絶えません。
ユーモアの定義の一つに、「にもかかわらず笑うこと」があります。
「ユーモア」は、究極の生きる勇気だとも言われています。

弱さと上手に付き合う、にもかかわらず笑う。すごい。
ただ、すごいってことは私にとっては憧れで、アジャイルと一緒ってことだ。だからやっぱり到達できないところにあるんだろう。

でも、まぁいいんだ。弱音吐きながらちょっとずつやるから。

路地裏アジャイルとは何か?

路地裏は、メインストリートに繋がってはいるんだけど、ちょっと薄暗い感じ。
それ、いいよね、わかる、わかるんだけど、ツラいよねぇ。それ。ってやつ。
だから、路地裏アジャイルは、アジャイル、いいよね、わかる、わかるんだけど、
修行ツラいよねぇってことになる。

メインストリート=修行を続ける道に繋がっているんだけど、流れがゆったりしているところ、または止まっているところ。 そこでは修行にちょっと疲れた人が一休みしながら、お互いのツラみやたわいもない話をしている...のかも。
というのが、今のところの認識。今のところだから、これから変わるかもしれないし、変わらないかもしれない。

場所にアジャイルってつけるのは変な気もするけれど、そこはえーと、怒られるだけ怒られておきます。 いい名前が思いついたらそっちに乗り換える...ことは多分しないだろうけども。

今後は、私の行ったり来たりをしているところを見せていくって話になるのかな。正直面倒だけど、まぁいいや。ぼちぼちやろう。

目指せ!? 副業プラモデラー!!!

副業でプラモデラー

副業ということは、お金がもらえるお仕事。軽く調べてみたところ、プラモデル作ってお金をもらう方法はいくつかあるっぽい。 模型雑誌のようなお仕事は私にできるわけがないので、それを除くと、完成品をヤフオクに出す、制作代行をする、くらいになる。

以下は、リスペクトしているモデラーさん達。

gumpla-auction.com

値段はほんと様々。もしかすると稼ぐというよりは、次のプラモを買う資金稼ぎくらいでやってるのかもしれない。 食い扶持にならないとは言え、人様に買ってもらえるくらいのプラモデルを作るというのは、なんかこうちょっと憧れるものがある。

最大の敵は自分

では、私の現在のポジションはと言うと、作ったのが2体、作り中が1体。初心者も良いところである。はるか遠くに見える副業の門を見つつ、一歩を踏み出したという感じ。
更に私は飽きっぽい。継続、コツコツが苦手である。なので、最大の敵は自分の飽きっぽさ。こいつを上手く騙しながら、少しずつ門ににじり寄ってく必要があるのだ。超面倒くさい。でもプラモデラーは小学生の頃からの憧れなのでちょっとだけ頑張ってみる。

自分を騙す3つの方法

そんなわけで、憧れのプラモデラーを目指すべく、現時点での自分の飽きっぽさを上手く騙すために心がけていることをここに書く。誰得?とか思わないわけではないが、書かないとアドベントカレンダーのネタが1個足りなくなるので書く。

心がけているのは主に以下の3つである。

  • 失敗してもガックリが少ないものから始める
  • へたっぴを受け入れる
  • やってる様子をつぷやく

それぞれ説明してみる。

失敗してもガックリが少ないものから始める

これはそのまんま。高いガンプラで失敗するとガックリするので、安いものから始める。
積みプラもあったんだけど、HGUCとMGなので、最初のプラモデルとしてはちとお高い。 そこで選んだのはFG。お値段300円。お安い。パーツも少なく組み上げるのが簡単なので、ちょうど良かった。

FG-01 ガンダムと FG-02 シャアザク

ただ、3体目に選んだ旧キットは失敗だったかもしれない。パーツの精度が最近のキットと比べて低くいらしく、段差が大きく、合わせ目消すのが大変なのである。加工の練習にはいいんだけども…。

旧キット ジムコマンド(宇宙用)

へたっぴを受け入れる

こればっかりは仕方がないというか。上手くなるには、今下手なことは受け入れるしかない。 いつも、下手なのを見るのが嫌で辞めちゃうけど、辞めると上手くならないし。

とは言え、下手ばかり見るのもツラい。が、そこは神様も多少配慮してくれてるようで、素人でも、クリティカルヒットが出る=意外に上手くいくことが稀にあるのだ。

なので、下手なところは仕方ないと割り切りつつ、クリティカルヒットが出た(と思われる)ところを見て、悦に入るようにした…と思う。

合わせ目が消えているのを悦に入る写真。あちこちアラがあるけどキニシナイ

やってる様子をつぶやく

狙ってやってたわけではない。Twitterのつぶやきネタに使えるのでポツリポツリと制作の様子をつぶやいてた。

そしたら、アドバイスをくれたり、いいねをくれたりがあった。もちろん数は少ない、少ないけど、反応があったのが嬉しかった。

あと、写真とか撮ってつぶやいてると、何となく憧れのモデラーさんたちと同じことをやってるんだと嬉しくなったりもする。惚れ惚れとする写真をみて、どうやれば撮れるんだろうと、あれこれブログを漁ってみたり。
良い感じに興味がいろいろ出てき始めて、飽きずに済んでいるような気がする。が、興味なくなるとどうなるんだろ…。

そんなこんなで、今のところは3台、5ヶ月くらいは飽きずにボチボチ続けられている。

まとめ?

最大の敵である自分の飽きっぽさへの対処をまとめてみた。

今は新しいとこばかりで、興味によって飽きを遠ざけてるところも多いので、この先、5体、10体続けていると、似たようなことも増えて飽きがぐっと近寄ってきそうな気もする。ウェザリングとか、改造とか、ちょいちょい新しいものを取り込んでいくのかな?

ちなみに、このエントリを書くのも、飽き対策になってる気がする。ガンプラ作りを、ガンプラが上手に作れるようになるためだけの修行にしてしまうとツラみが増えるので、こんな感じでエントリを書くネタにも使えるよ、と自分を洗脳しているんだと思う。

そうそう、これは自分洗脳の一環なんだな。自分洗脳*1が完了したら、副業にできるかどうかは分からないけど、コツコツ作り続けられるようになってるんだろう。

下地用に買った塗料が到着したから、明日ちょっと吹いてみようっと。

*1:文字通り自分で自分を洗脳する。みうらじゅん氏の『「ない仕事」の作り方』に出てくる言葉

ハトマスクステッカーアプリを作ったよ

ハトマスクステッカーアプリって?

顔や全身が写っている写真をURL、または、ファイルで指定すると、適当な感じでステッカーと合成するアプリ。 Agile Japan 2020 ハトマスクステッカー(未認可・非公式)おみまいするぞ で遊ぶことができる。 Agile Japan 2020というイベントのちょっとしたお遊びとして作ろうと思ったのがきっかけ。

f:id:couger:20201123213035p:plain
ハトマスクステッカー「に」合成

f:id:couger:20201123213023p:plain
ハトマスクステッカー「を」合成

どう作った?

コマンドラインバージョン

最初はコマンドラインで、「ハトマスクステッカー合成」する機能を作ろうとしていた。

f:id:couger:20201123213035p:plain

最初の一歩として、写真の背景を切り抜く処理を探す。なるべく自動化したいので、APIが提供されているものを探して、見つけたのは以下の2つ。

残念だけど両方ともお財布に合わなかったので、別のものを探すことに。でもなかなか見つからない...。自分の財力ではムリがある。 GitHubにないかなぁと "github remove background" でググったら、トピックのリストが見つかった!!!

github.com

なんとなく使ってる人の多そうな danielgatis/rembg: Rembg is a tool to remove images background. を使うことにした。 GitHubスゴい。OSSスゴい。

説明を見ながらCLIで実行。キレイに切り抜きができた!!! スゴい。 更に、ステッカーに合成する簡単なプログラムを見よう見まねでPythonで作り実行。上手くいった!
※ RembgがPythonだったのでそれに合わせた。

その時、作ったコードが以下である。

curl -s $1 | rembg > work/target.png
python hatomask-gosei.py work/target.png
import sys
from PIL import Image

def resize_image(original, width):
  return original.resize((width, (int)(width * original.size[1] / original.size[0])))

hatomask = Image.open('hatomask-aj2020.png')

pathToTarget = sys.argv[1]

target = resize_image(Image.open(pathToTarget), 300)

hatomask.paste(target, (1024-300, 600), target)
hatomask.save('omimaisuruzo.png')

これで、合成したい写真のURLを指定すれば、ハトマスクステッカーに合成ができるようになった。ここで辞めておけばよかったのに、Webアプリにしたい欲求が湧いてきてしまう。
そういえば、最近プログラム全くしてなかったし。Azureも久しぶりに使ってみようかと軽い気持ちで始めたら、結局土日2日を潰すことになった。思いつきはだいたい高くつく...。

Webアプリにする

まずは軽いWebアプリフレームワークと、CSSフレームワークを探す。選んだのは以下の2つ。

初めて触るフレームワークだったので、以下のような感じで1ステップずつ進めていった。都度、App Serviceにデプロイして動作確認をしていたと思う。

  • 画面を表示する
  • 画像のURLが指定できるようにする
  • 指定したURLの画像を表示する

この後、Rembgを使った「指定したURLの画像の背景を切り抜く」処理を追加、App Serviceにデプロイしようとするとエラーが出た。 数回試したけどダメ。エラーメッセージにはメモリがないとかディスクのスペースがないとか出てくる。仕方がないので、プランをFreeから、B1にグレードアップ。やっぱり失敗する。

泣く泣くP1v2にして、デプロイが成功。ただし、デプロイに10分くらいかかる。ツラい。1ヶ月使うと1万円かかる。ツラい。
※ S1にしなかった理由は、P1v2と比べて1000円くらいしか違わないから。なんとなく性能の問題のような気もしたし。

とはいえ、のんびり調査している暇もないので、そのまま開発を続けて最初のバージョンをリリースした。 リリースした後「ハトマスクステッカーを合成」する方がニーズあるんじゃないかな? と思い、急遽機能を追加、次の日に再度リリース。

最終的なコードは以下(アプリ部分のみ)。

import sys
import io
import base64
import urllib.request
import hashlib
from PIL import Image
from flask import Flask, render_template, request
from rembg.bg import remove
app = Flask(__name__)

app.config['MAX_CONTENT_LENGTH'] = 10 * 1024 * 1024

def resize_image(original, width):
  return original.resize((width, (int)(width * original.size[1] / original.size[0])))

def crop_image(original):
  # https://stackoverflow.com/questions/14211340/automatically-cropping-an-image-with-python-pil/51703287
  imageSize = original.size
  imageBox = original.getbbox()

  imageComponents = original.split()

  rgbImage = Image.new("RGB", imageSize, (0,0,0))
  rgbImage.paste(original, mask=imageComponents[3])
  croppedBox = rgbImage.getbbox()

  return original.crop(croppedBox)

def convert_image_to_base64(image):
  image_buffered = io.BytesIO()
  image.save(image_buffered, format="PNG")
  return base64.b64encode(image_buffered.getvalue()).decode("utf-8")

def remove_background(imageStream):
  r = lambda i: i.buffer.read() if hasattr(i, "buffer") else i.read()
  w = lambda o, data: o.buffer.write(data) if hasattr(o, "buffer") else o.write(data)

  result = io.BytesIO()

  w(
    result,
    remove(
      r(imageStream),
      model_name="u2net",
      alpha_matting=False,
      alpha_matting_foreground_threshold=240,
      alpha_matting_background_threshold=10,
      alpha_matting_erode_structure_size=10,
    ),
  )

  return Image.open(result)

def hatomask_omimaisuruzo(target):
  result = Image.open('static/images/hatomask-aj2020.png')
  target_resized = resize_image(target, 300)
  result.paste(target_resized, (1024-300, 600), target_resized)
  return result

def sticker_ga_ikuzo(target):
  sticker = Image.open('static/images/hatomask-aj2020-rembg.png')
  sticker_resized = resize_image(sticker, (int)(target.size[0] / 3))

  paste_lefttop = (target.size[0] - sticker_resized.size[0], target.size[1] - sticker_resized.size[1])
  target.paste(sticker_resized, paste_lefttop, sticker_resized)
  return target


@app.after_request
def add_header(r):
    r.headers["Cache-Control"] = "no-cache, no-store, must-revalidate"
    r.headers["Pragma"] = "no-cache"
    r.headers["Expires"] = "0"
    r.headers['Cache-Control'] = 'public, max-age=0'
    return r

@app.route("/")
def hello():
  return render_template('aj2020.html')

@app.route('/sareruzo_url', methods=['POST'])
def sareruzo_url():
  imageUrl = request.form.get('imageUrl')
  f = io.BytesIO(urllib.request.urlopen(imageUrl).read())
  target_removebg = remove_background(f)
  target_cropped = crop_image(target_removebg)

  hatomasked = hatomask_omimaisuruzo(target_cropped)
  return render_template('aj2020-omimaishitazo.html', imageBase64=convert_image_to_base64(hatomasked))


@app.route('/sareruzo_upload', methods=['POST'])
def sareruzo_upload():
  if 'imageFile' not in request.files:
    return render_template('aj2020-omimaishitazo.html', imageFile="images/hatomask-aj2020.png")

  imageFile = request.files['imageFile']
  imageFileName = imageFile.filename

  if '' == imageFileName:
    return render_template('aj2020-omimaishitazo.html', imageFile="images/hatomask-aj2020.png")

  target_removebg = remove_background(imageFile.stream)
  target_cropped = crop_image(target_removebg)

  hatomasked = hatomask_omimaisuruzo(target_cropped)
  return render_template('aj2020-omimaishitazo.html', imageBase64=convert_image_to_base64(hatomasked))

@app.route('/suruzo_url', methods=['POST'])
def suruzo_url():
  imageUrl = request.form.get('imageUrl')
  imageFile = Image.open(io.BytesIO(urllib.request.urlopen(imageUrl).read()))

  hatomasked = sticker_ga_ikuzo(imageFile)
  return render_template('aj2020-omimaishitazo.html', imageBase64=convert_image_to_base64(hatomasked))


@app.route('/suruzo_upload', methods=['POST'])
def suruzo_upload():
  if 'imageFile' not in request.files:
    return render_template('aj2020-omimaishitazo.html', imageFile="images/hatomask-aj2020.png")

  imageFile = request.files['imageFile']
  imageFileName = imageFile.filename

  if '' == imageFileName:
    return render_template('aj2020-omimaishitazo.html', imageFile="images/hatomask-aj2020.png")

  hatomasked = sticker_ga_ikuzo(Image.open(io.BytesIO(imageFile.read())))
  return render_template('aj2020-omimaishitazo.html', imageBase64=convert_image_to_base64(hatomasked))

ハマったところは?

Azure App ServiceのFreeプランでは動かない

B2プランなら大丈夫。Freeプランでは、Rembgを動かすためのリソースが足りないのだろうと推測。

やりたい処理によって画像ファイルを扱うクラスを変える必要がある

コピペ優先でホイホイ作ってたから、この辺を理解しておらず、追加機能を作る際に苦労した。
ちなみに以下の3つを覚えておけば多分いいはず。(それくらい最初に調べとけってツッコミは甘んじで受けます...)

  • アップロードされたファイル: werkzeug.datastructures.FileStorage
  • 画像の透過、貼り付け: PIL.Image
  • Rembgを使った背景除去: io.BytesIO

今後は?

ステッカーの種類を増やしたり、位置や大きさを変えられるようにしようかなーと思ってたりするんだけども。さて。