見出し画像

【技術編】 ぽこピーサムネイルモザイクアート選手権 / [Tech part] Pokopea thumbnail mosaic art battle!

※ I will update this blog post to add English part later. Sorry for inconveninece at this moment

こちらの記事は 「ぽこピー(pokopea) 🍃 🥜 Advent Calendar 2021」 3日目の記事(の技術編)です!

元の記事はこちら↓


こちらの記事でも @ikura18 のサポート役のikura18_jpが書き進めます :)

なおこちらは技術パートのため、本当に興味ある人以外は全くおもしろくないと思います!
そんなときはぽこぴーの動画をみよう!

Let's cook!

さて、早速モザイクアートの作りかたを説明していきます!(現在2021/12/3 02:38AM)

コードだけみたい~という方は githubにrepoを上げているので そちらをどうぞ! コードやデータが汚い(そしてテストがない)のには目をつむってください!

Step1: 全動画のリストを取得 via Youtube Data API

まずは素材となるサムネイルを手元に用意したいのですが、そのためには動画の一覧を用意する必要があります。
そこで使うのが Youtube Data API です。

YouTube上のチャンネル/動画/コメント/Playlistなどといったデータをプログラム越しにみたり操作したりすることのできるYouTubeオフィシャルのサービスです。
二日目のcontributorだった naname さんもこちらを使われていましたね。


こちらのAPIを使うと、動画の一覧のデータを取得する際に、各動画に使われているサムネイルの画像urlを取得できることができます。
その動画urlがまずここで欲しい。

さて、では早速APIのドキュメントを読んでみます...
なるほど、さっぱりわかりませんが、次のようにすると欲しい物を得ることができそうです。

  1. Google API tokenを作る

  2. そのチャンネルに紐づく公開動画すべてを収容する「uploads」というplaylistを `Channels: list` APIで見つける

  3. そのplaylistに対して `PlaylistItems: list` APIを適用して、全動画の情報を取得する

工程1-1: Google API Keyを作る

今回はコマンドラインからapiをコールするので、取得の簡単なAPI keyの方だけ入手しましょう.
本題ではないので詳細はカット! 「youtube data api api key 取得」 とか検索して API Keyをいい感じに手に入れましょう

工程1-2: 「uploads」というplaylistを `Channels: list` APIで見つける

API keyを取得したら早速 uploads を見つけ出します

YOUTUBE_API_KEY='{put_your_special_api_key}'
PART="id,snippet,contentDetails" # 適当です
CHANNEL_ID="UCmgWMQkenFc72QnYkdxdoKA" # ピーナッツくんチャンネルのurlから推測
curl "https://www.googleapis.com/youtube/v3/channels?part=${PART}&id=${CHANNEL_ID}&key=${YOUTUBE_API_KEY}" | jq .

{
  "kind": "youtube#channelListResponse",
  "etag": "J-EDK4OI1YEA5dzGn3IkrRLZUvU",
  "pageInfo": {
    "totalResults": 1,
    "resultsPerPage": 5
  },
  "items": [
    {
      "kind": "youtube#channel",
      "etag": "RDrvKo-mj8hJ1CNm9r61QPPDb9E",
      "id": "UCmgWMQkenFc72QnYkdxdoKA",
      "snippet": {
        "title": "ピーナッツくん!オシャレになりたい!",
        "description": "オシャレになりたいピーナッツくんがいろんなオシャレ人と出会う大冒険ショートアニメ!!\n\n毎週金曜がアニメ更新!!\n毎週火曜は実況、企画、生放送、おやすみしたり!!\n\nグッズは不定期で期間限定販売してます!!買えたらラッキー!!\n\nお仕事関連の連絡先はこちら!\nmode.aimail@gmail.com\n\n◎LINEスタンプ発売中!!\n→https://store.line.me/stickershop/aut...\n\n◎チャンネル登録よろしくナッツ〜〜〜!!\nhttps://www.youtube.com/channel/UCmgW...\n\n◎オシャレになりたい!ピーナッツくん\nTwitter→https://twitter.com/osyarenuts/\nInstagram→https://www.instagram.com/osyarenuts/",
        "publishedAt": "2017-06-27T08:49:00Z",
        "thumbnails": {
          "default": {
            "url": "https://yt3.ggpht.com/ytc/AKedOLTK4uV3--sjYmABzqHCv_yH6frOG__Pt8-B9AvT=s88-c-k-c0x00ffffff-no-rj",
            "width": 88,
            "height": 88
          },
          "medium": {
            "url": "https://yt3.ggpht.com/ytc/AKedOLTK4uV3--sjYmABzqHCv_yH6frOG__Pt8-B9AvT=s240-c-k-c0x00ffffff-no-rj",
            "width": 240,
            "height": 240
          },
          "high": {
            "url": "https://yt3.ggpht.com/ytc/AKedOLTK4uV3--sjYmABzqHCv_yH6frOG__Pt8-B9AvT=s800-c-k-c0x00ffffff-no-rj",
            "width": 800,
            "height": 800
          }
        },
        "localized": {
          "title": "ピーナッツくん!オシャレになりたい!",
          "description": "オシャレになりたいピーナッツくんがいろんなオシャレ人と出会う大冒険ショートアニメ!!\n\n毎週金曜がアニメ更新!!\n毎週火曜は実況、企画、生放送、おやすみしたり!!\n\nグッズは不定期で期間限定販売してます!!買えたらラッキー!!\n\nお仕事関連の連絡先はこちら!\nmode.aimail@gmail.com\n\n◎LINEスタンプ発売中!!\n→https://store.line.me/stickershop/aut...\n\n◎チャンネル登録よろしくナッツ〜〜〜!!\nhttps://www.youtube.com/channel/UCmgW...\n\n◎オシャレになりたい!ピーナッツくん\nTwitter→https://twitter.com/osyarenuts/\nInstagram→https://www.instagram.com/osyarenuts/"
        },
        "country": "JP"
      },
      "contentDetails": {
        "relatedPlaylists": {
          "likes": "",
          "uploads": "UUmgWMQkenFc72QnYkdxdoKA"
        }
      }
    }
  ]
}

お、いい感じに結果が帰ってきましたね!
中には `relatedPlaylists.uploads` というそれっぽいkeyも存在します

curl "https://www.googleapis.com/youtube/v3/channels?part=${PART}&id=${CHANNEL_ID}&key=${YOUTUBE_API_KEY}" | jq -r ".items[0].contentDetails.relatedPlaylists.uploads"

UUmgWMQkenFc72QnYkdxdoKA

これがピーナッツくんのチャンネルで公開されている動画すべてを収めたplaylistのIDでしょう。

同様にぽんぽこのも取得します

YOUTUBE_API_KEY='{put_your_special_api_key}'
PART="id,snippet,contentDetails"
CHANNEL_ID="UC1EB8moGYdkoZQfWHjh7Ivw" # ぽんぽこチャンネルのurlから推測
curl "https://www.googleapis.com/youtube/v3/channels?part=${PART}&id=${CHANNEL_ID}&key=${YOUTUBE_API_KEY}" | jq -r ".items[0].contentDetails.relatedPlaylists.uploads"

UU1EB8moGYdkoZQfWHjh7Ivw

工程1-3: そのplaylistに対して `PlaylistItems: list` APIを適用して、全動画の情報を取得する

では取得したplaylist idを使ってその中にある動画を見てみましょう

YOUTUBE_API_KEY='{put_your_special_api_key}'
PART="id,contentDetails,snippet,status"
PLAYLIST_ID="UUmgWMQkenFc72QnYkdxdoKA"
# endpointが先程までと変わっています
curl "https://www.googleapis.com/youtube/v3/playlistItems?part=${PART}&playlistId=${PLAYLIST_ID}&key=${YOUTUBE_API_KEY}" | jq .

{
  "kind": "youtube#playlistItemListResponse",
  "etag": "6k0vTb-S2v53UZJe_7PUzecbeOw",
  "nextPageToken": "EAAaBlBUOkNBVQ",
  "items": [
    {
      "kind": "youtube#playlistItem",
      "etag": "OAm8bim5-sHd769vGmigkSACSgk",
      "id": "VVVtZ1dNUWtlbkZjNzJRbllrZHhkb0tBLjF3U2pPWEJVankw",
      "snippet": {
        "publishedAt": "2021-11-29T09:48:59Z",
        "channelId": "UCmgWMQkenFc72QnYkdxdoKA",
        "title": "【#Vtuberバトルロワイアル】やめろ!ここから脱出する方法をぼくは知ってる・・・!",
        "description": "▼主催会場▼\nhttps://youtu.be/mb8Voo597zI\n@バーチャル債務者youtuber天開司 \n\nフィールド:バーチャル空間上のとある島(664㎡)\n制限時間 :5日間(ゲーム内時間)\nエリア  :島内は16のエリアに分かれており、2日目から1日あたり4エリアのペースで侵入禁止エリアに指定される。侵入禁止エリアに入ると首の爆弾が爆発する。\n支給品  :島の至る所に特殊チェストがあり、武器、弾薬、食料等ゲームに役立つアイテムが手に入る。\r\n\n◎チャンネル登録よろしくナッツ〜〜〜!!\r\nhttps://www.youtube.com/channel/UCmgWMQkenFc72QnYkdxdoKA\r\nTwitter→https://twitter.com/osyarenuts/\r\n\r\n◎LINEスタンプ・着せ替え発売中!!\r\nスタンプ→https://store.line.me/stickershop/author/195259\r\n着せ替え→https://store.line.me/themeshop/author/172254/ja\r\n\r\n\r\n◎その他のチャンネル\r\nぽんぽこちゃんねる(ぽんぽこがバーチャルYouTuberとして活動してるナッツ!よくお邪魔してるナッツ!)\r\n→https://www.youtube.com/channel/UC1EB8moGYdkoZQfWHjh7Ivw\r\n\r\nレオブタとヤギハイ(レオブタの音楽専用チャンネルナッツ!)\r\n→https://www.youtube.com/channel/UCPQMAXEjEqp3D5wepIGr50w\r\n\r\nチャンチョのゲーム部屋\r\n→https://www.youtube.com/channel/UC5fwtXKwDpgboOud4DbjQTg",
        "thumbnails": {
          "default": {
            "url": "https://i.ytimg.com/vi/1wSjOXBUjy0/default.jpg",
            "width": 120,
            "height": 90
          },
          "medium": {
            "url": "https://i.ytimg.com/vi/1wSjOXBUjy0/mqdefault.jpg",
            "width": 320,
            "height": 180
          },
          "high": {
            "url": "https://i.ytimg.com/vi/1wSjOXBUjy0/hqdefault.jpg",
            "width": 480,
            "height": 360
          },
          "standard": {
            "url": "https://i.ytimg.com/vi/1wSjOXBUjy0/sddefault.jpg",
            "width": 640,
            "height": 480
          }
        },
        "channelTitle": "ピーナッツくん!オシャレになりたい!",
        "playlistId": "UUmgWMQkenFc72QnYkdxdoKA",
        "position": 0,
        "resourceId": {
          "kind": "youtube#video",
          "videoId": "1wSjOXBUjy0"
        },
        "videoOwnerChannelTitle": "ピーナッツくん!オシャレになりたい!",
        "videoOwnerChannelId": "UCmgWMQkenFc72QnYkdxdoKA"
      },
      "contentDetails": {
        "videoId": "1wSjOXBUjy0",
        "videoPublishedAt": "2021-11-29T13:17:36Z"
      },
      "status": {
        "privacyStatus": "public"
      }
    },
	....
  ],
  "pageInfo": {
    "totalResults": 280,
    "resultsPerPage": 5
  }
}

む、またもやいい感じです! `.items[*].snippet.thumbnails` の中にサムネイルのurlと思われるものが格納されています。
ただ `pageInfo.resultsPerPage` をみるに一度のリクエストで帰ってくるのは5件のみのようです。
API docをみてもresultsPerPageを変更することはできなさそうなので、loopを回す必要がありそうです。

というわけで適当なPython scriptをこしらえます。
(実際のコードは最後にgithubにあげてありますので、もし興味あればそちらをどうぞ🙏)

これをぽんぽこ/ピーナッツくん両名のチャンネルに走らせ、次のようなjsonとして手元に保存します

[
  {
    "title": "【#Vtuberバトルロワイアル】令和のタヌキは狩る側って知ってた?",
    "videoID": "tE2UtpCeHVY",
    "position": 0,
    "publishedAt": "2021-11-29T13:14:46Z",
    "thumbnailHigh": {
      "url": "https://i.ytimg.com/vi/tE2UtpCeHVY/hqdefault.jpg",
      "width": 480,
      "height": 360
    },
    "thumbnailMedium": {
      "url": "https://i.ytimg.com/vi/tE2UtpCeHVY/mqdefault.jpg",
      "width": 320,
      "height": 180
    },
    "thumbnailDefault": {
      "url": "https://i.ytimg.com/vi/tE2UtpCeHVY/default.jpg",
      "width": 120,
      "height": 90
    }
  },
  ...
]

次はこのurlから実際に画像をとってきます!

Step2: 全サムネイルの取得

ここは簡単! 各urlを順繰りにみて画像データをダウンロードするのみ!
コードは get_image_list.py にあります。

Step3: 全サムネイルに対し、その代表色を決める

ここが今回の山場といえます。

一つの画像から、そのサムネを代表する色 = 代表色を出す方法はいくつか考えられます。

  1. その画像に登場する色の中間色 -> 平均値(mean)

  2. その画像の中でもっとも占める割合の多い色 -> 最頻値(mode)

  3. もう少しテクい代表値 -> k-means

それぞれの手法の評価方法も丁寧に考えたかったのですが(例えば最小二乗法)、検討と実装の時間がなかったため、ぽんぽこ/ピーナッツくんそれぞれのチャンネルの最初の10数枚のサムネイルに対し、どれだけうまく代表色を選択できているかを主観で選ぶことにします。

具体的なコードは analyze_images.py にあります。

では早速どういった出力がでるかをみてみます。

上から次の順で並べてあります。

  • 対象の画像

  • meanによる代表色

  • modeによる代表色

  • k-means(n_clusters=2)による代表色

ピーナッツくんその1
ピーナッツくんその2
ぽんぽこその2
ぽんぽこその2


主観的な評価にとどまりますが、

  • modeは大抵の場合目立つ色を選べているものの、たまに大きく外す色を選択している

  • meanは安定してそれっぽい色を選べるものの、中間色になるので、例えば二色で二分される構図の場合、そこには存在しない中間色を代表色としてしまう

  • modeのような精度とmeanのような安定感を併せ持つ"印象がある"

といったところでしょうか。

ここでは主観を信じてk-meansを採用することとします。

なおこのパートの実装には 画像の代表的な色を抽出する - アールテクニカ地下ガレージ を大変参考にさせていただきました。

各サムネイルに対し、代表色を選択できたらあとは実際にモザイク画にするサムネイルの各pixelを、最もその色に代表色が近いサムネイルで差し替えていく段階です!

Step3+α: 少し寄り道して、パレットを作ってみる

せっかく全サムネイルに代表色を割り当てたので、それを並べてカラーパレットにして遊んでみましょう。
それが こちら

赤と黄色系統のサムネイルが多いことがわかります。

ここのソートはhlsのhue(色相)の値を縦軸に、横軸にはlightnessを使いましたが、どことなくぎこちなさがありますね..
代表値選定ロジックの良し悪しがここに現れていそうです。


Step4: 戦いの舞台に立つサムネイルを選出する

続いてモザイク画にするサムネイルを選定。こちらは本編をご覧ください!


Step5: 選出したサムネイルの各pixelを素材サムネイルに差し替えて再構成

全pixelを先頭からみて、順次最も最適なサムネイルを選定していきます。
ここは愚直にpixel数=N, サムネイル数=Mとしたとき、 O(NM)で全探索します。

ここは効率化の余地が大きくあります。
が、Nがたかだか100000(横320*縦180=57600pixel), Mが10000(全部で1226の動画サムネイルが利用可能)なので
一旦目をつぶって走らせます.

# 擬似コード
## main
result = []
for pixel in image:
  thumbnail = find_closest_thumbnail(pixel)
  result.append(thumbnail.id)

result.save_as_json()

## find_closest_thumbail(pixel)
closest_thumbanil = null
min_dist = INF
for thumbnail in thumbnails:
  dist = distance(pixel, thumbanil)
  if dist < min_dist:
    min_dist = dist
    closest_thumbanil = thumbnail

具体的なコードは gen_pict_img.py をご覧ください

色空間(3次元)のsortってあるんですかね、あるんでしょうね...勉強が必要です!

Step6: 完成!

そして完成したものが こちら になります。
各ページ非常に重たいので注意!スマホからだと動かないかも!あと通信量すごい食うからPCから推奨!

おまけ1

本編のおまけ3で言及した「20の動画分のサムネイルだけ使われていません。」のリストです。

そのIDリストがこちら!

cat output/video_json/peanuts_kun/video_list.json | jq '. | map(.videoID)' > ./tmp/peanuts_kun.json
cat output/video_json/ponpoko/video_list.json | jq '. | map(.videoID)' > ./tmp/ponpoko.json

for id in s-jGOeYdKt8 _nXnvHit0R4 9VhrJCbr58A BVC-dRCz-4A b5VxEOiHvdY IgBtOvlj2X8 n8msiA8RNxg Kbfrw9yFGx0
do
	cat output/pict_img/180x320/$id/pict_img.json | jq '. | flatten | unique' > ./tmp/$id.json
done

all=`cat tmp/peanuts_kun.json tmp/ponpoko.json | jq -s add | jq '. | unique'`
used=`cat tmp/s-jGOeYdKt8.json tmp/_nXnvHit0R4.json tmp/9VhrJCbr58A.json tmp/BVC-dRCz-4A.json tmp/b5VxEOiHvdY.json tmp/IgBtOvlj2X8.json tmp/n8msiA8RNxg.json tmp/Kbfrw9yFGx0.json \
  | jq -s add \
  | jq '. | unique'`

echo -n "{\"all\": $all, \"used\": $used}" | jq '.all-.used'


[
  "5TvCvoeRVqk",
  "686jx1MepsE",
  "8Ke-sFIM9hY",
  "BbGbA3nN4_s",
  "FipiU35xpUw",
  "GSMy65lxbT0",
  "M7nCXigtMt0",
  "Mf2oPz8LUGc",
  "OIQCM4iU2dA",
  "P66Apt5-v6A",
  "STme7z6pQ1g",
  "Ywj7o_h3ddg",
  "bbSaAekGdPQ",
  "eMbUvYqzXMQ",
  "fy8zXfE26P8",
  "jUaECEAdfXU",
  "rWgJAf5WxCM",
  "sMgTf15x3Xs",
  "wODnFp8_5Yc",
  "ycrnLS5Jk0c"
]


After cooking…

ここまででモザイクアートの準備が完了しました!
だいぶ駆け足でしたが、コードを見て質問などあればぜひお寄せください!

では本編に戻るとしましょう! :)


いいなと思ったら応援しよう!