ナミブ砂漠でシマウマに会うための技術

この記事は [Akatsuki Advent Calendar 2021](https://adventar.org/calendars/6566) 12日目の記事です。


# シマウマに会いたい

クリスマスも間近に控えた近頃です。みなさまいかがお過ごしでしょうか。
僕は今、とてもシマウマに会いたいです。

実はこんな↓ツイートを見かけまして…。

https://twitter.com/masa1xkane/status/1469698753284087812?s=20

このツイートで紹介されているのは、ナミブ砂漠の人口水場に設置された定点カメラからのライブ配信です。覗いてみると、色々な動物が結構頻繁に訪れては水を飲んでいる様子が観察できます。
そして、ツイートでは

> シマウマとか（中略）なんでもくる

と語られています。シマウマくるんですか！！　めちゃくちゃみたい！
アフリカの大地を闊歩するシマウマの姿、想像するだけで胸が躍りますね。

う〜んでも僕が配信をちょっとみていた感じだと、シマウマは姿を表しませんでした。そこはサバンナの人気者、結構レアキャラなのかもしれません。

https://www.youtube.com/watch?v=ydYDqZQpim8

だからと言ってずっと見張っているわけにもいきません。どうにかしてピンポイントでシマウマに遭遇できないものでしょうか。

# シマウマがきたら通知するシステム（没）

最初に思いついたのは、

1. YouTube Liveの配信を1フレームだけ取ってくる
2. 画像認識処理にかける
3. シマウマがいれば通知する

って感じの処理を書いて、それをCloudRunとかにデプロイしてスケジュールしといて、たまに走るようにする、という全自動プランです。めちゃくちゃ便利そうですが、しかしこれはYouTubeの利用規約的にダメそう[^1]。
ほんとはこれで記事書くつもりで、動くところまで組んじゃった後に利用規約のことに気がつきました。ちょっと悔しかったのでここで触れるだけ触れて供養とさせてください。

:::note alert
この記事はYouTube利用規約違反を推奨するものではありません。この手法を試しても、筆者は責任の一切を負いませんので悪しからず！
:::

# シマウマタイムを割り出す

リアルタイムでの監視がダメなら、過去の記録をもとにシマウマの手がかりを探りましょう。

https://namibiaweather.info/Namib-Desert-Live-Cam.php

上記のサイトで公開されている直近一週間のタイムラプス映像を解析して、シマウマの出現する時間帯の傾向を割り出し、その時間帯にライブ配信を観に行けばシマウマに会えるという寸法です。これならなんだかいけそうですし規約的にもホワイト。やってみましょう。

## タイムラプス動画を加工する

まずは動画をダウンロードしてきます。

```
$ curl -OL https://weather.namsearch.com/namibdesert/webcam/NDLWegkruip_Sunday.mp4
```

こんな調子で、一週間分全ての動画を手に入れましょう。

動画を手に入れたら、連番画像に変換します。
このタイムラプス動画は1日をおおよそ90秒に圧縮しているようです。なので、この動画をフレームレート1で連番画像にした場合、およそ16分に１回のペースで１日をチェックしていることになります。まあいい感じの頻度ではないでしょうか。
ここで使う[FFmpeg](http://www.ffmpeg.org/)は動画ファイルの操作に便利なツールです。インストールの儀は省略。

```
$ mkdir img
$ ffmpeg -i NDLWegkruip_Sunday.mp4 -vcodec png -r 1 img/%04d.png 
```

ディレクトリはこのような感じになりようにします。あとでスクリプト回して走査しやすいようにしておきましょう。

```
$ tree
.
├── sun
│   ├── NDLWegkruip_Sunday.mp4
│   └── img
│       ├── 0001.png
│       ├── 0002.png
│       ├── ...
├── mon
│   ├── NDLWegkruip_Monday.mp4
│   └── img
│       ├── 0001.png
│       ├── 0002.png
│       ├── 0003.png
│       ├── ...
```

## シマウマを検知する

連番画像が手に入ったら、それを順番に画像認識にかけましょう。
探したいのはシマウマです。

モデルを1から学習させて作るのは面倒なので、すでに学習済みのモデルを使って楽しましょう。

https://pytorch.org/vision/stable/models.html

これをありがたく使わせてもらいます。

また、ImageNetクラスのJSONファイルはこちら↓を使わせていただきました。

https://storage.googleapis.com/download.tensorflow.org/data/imagenet_class_index.json

シマウマを検知したいだけなら、無料で公開されているこれらで十分な精度の分類器を作れるでしょう。
さあそれでは、いよいよ一週間のタイムラプスを分類器にかけて、シマウマタイムを割り出してみましょう。
Pythonスクリプトはこちら

```python
import os
import json
import torch
from torchvision import models, transforms
from PIL import Image

model = models.vgg16(pretrained=True)
model.eval()
preprocess = transforms.Compose([
    transforms.Resize(224),
    transforms.CenterCrop(224),
    transforms.ToTensor(),
    transforms.Normalize(
        mean=[0.5, 0.5, 0.5],
        std=[0.2, 0.2, 0.2]
    )
])

with open('./imagenet_class_index.json') as f:
    labels = json.load(f)

def zebra_exists(file):
    img_org = Image.open(file)
    img = preprocess(img_org)
    img_batch = img[None]
    result = model(img_batch)
    idx = torch.argmax(result[0])
    result_label = labels[str(idx.item())]
    if "zebra" in result_label:
        print("detect zebra at:", file)
        return True
    return False


current = os.getcwd()
zebra_results = {}
for root, _dirs, files in os.walk(current):
    zebra_results[root] = []
    for file in files:
        _, ext = os.path.splitext(file)
        if ext != ".png":
            continue
        if zebra_exists(os.path.join(root, file)):
            zebra_results[root].append(file)

print(zebra_results)
```

さあ！　このスクリプトを実行すると、いよいよ一週間のうちにシマウマが姿を表せた時間帯が明らかになります。
気になる出力結果は！？

注：みやすさのためにちょっと改変してます。

```
detect zebra at: .../thu/img/0048.png
{
'.../thu/img': ['0048.png'],
'.../wed/img': [],
'.../sat/img': [],
'.../mon/img': [],
'.../fri/img': [],
'.../tue/img': [],
'.../sun/img': []
}
```

………。
まず簡単にスクリプトの解説をしますと、先のPythonスクリプトはフォルダ内にある連番画像の全てを走査し、分類器にかけます。その結果、 `zebra` つまりシマウマとして分類された画像のみを配列につっこむようになっています。これによって、1日ごとのシマウマ出現タイミングを抑えた画像がどれか分かるわけです。

そして出力結果を見てみると、　`.../thu/img` つまり Thursday 木曜日にひとつだけ、 `0048.png` が検出されています。
つまり、この一週間のうちシマウマは木曜日の昼ごろ（1日の48/90って大体それくらいでしょう）に一度だけ現れたってことです。

なるほど……。
これはレアキャラですね。

ちなみに該当の0048.pngはこちら。

![](https://storage.googleapis.com/zenn-user-upload/c72d68a57d1f-20211212.png)

いる〜！！　シマウマ水飲んでる！！　どうやら本当に出現するみたいですね！！！

推測の時間帯と画像の右上に表示されてる時間帯が盛大にずれていますが、調べるとこのタイムラプス画像は途中の1時間が歯抜けになっていたりして、1日をまるっと圧縮したものではないようです。ちゃんと正確に測るなら画像文字認識して時刻を読み取った方が確実でしょう。今回の場合だと正確な時刻は09:25でした。

とりあえず今回の調べでは、シマウマタイムはズバリ**木曜日の09:25頃** (現地時間)ということになります。
週にただ一回のみのチャンスです。


# 結論

まとめますと、ナミブ砂漠でシマウマに会いたければ**木曜日の09:25頃** (現地時間)に定点カメラからの生配信を覗いてみればいいかもしれない！　ということになります。

ほんとか………？

まあでも出現するにはするということがわかったので、もっと広いスパンでデータ集めれば詳細な結論を出せるかもですね。

シマウマ以外にもキリンやチーターにも会いたい。
今回はお金かけないためにImageNetの学習済みモデルを使いましたが、他の動物を分類したいなら自分でモデルを学習させるか、面倒ならそもそも分類器自体外部APIに頼ってしまうのも手かもしれません。[Google Vision API](https://cloud.google.com/vision/?hl=ja&utm_source=google&utm_medium=cpc&utm_campaign=japac-JP-all-ja-dr-bkws-all-super-trial-e-dr-1009882&utm_content=text-ad-none-none-DEV_c-CRE_314712312129-ADGP_Hybrid%20%7C%20BKWS%20-%20EXA%20%7C%20Txt%20~%20AI%20%26%20ML%20~%20Vision%20AI_Vision-google%20vision%20api-en-KWID_43700030990911736-kwd-203288731687&userloc_1009285-network_g&utm_term=KW_google%20vision%20api&gclid=Cj0KCQiA2NaNBhDvARIsAEw55hg1-BeN_AKsDuLnZ_JYhjtpGlXxmIbjbYeFzvlRJGL7z_cPHRSzrsAaAv_5EALw_wcB&gclsrc=aw.ds)とかね。

[^1]: YouTubeの[利用規約](https://www.youtube.com/static?template=terms&hl=ja&gl=JP)では、 `自動化された手段（ロボット、ボットネット、スクレーパなど）を使用して本サービスにアクセスすること` を明示的に禁止しています。バッチリ該当してますね。