はじめに
この記事は「R Advent Calendar 2022」の10日目の記事です。
rtweet
パッケージを利用して収集したツイート画像をmagick
パッケージを利用してアニメーションにします。
【目次】
したいこと
2022年12月10日(この記事の投稿日)現在、モーニング娘。'22には13名のメンバーが在籍しています。その中の1人に、加賀楓さんという方がいます。出身は東京都ですが名前が同じということをきっかけに色々な活動を経て、石川県加賀市の加賀温泉郷の観光大使をされています。
詳しくはこちらを覗いてみてください。
例年秋冬頃に、加賀温泉郷のプロモーション用のポスター広告が首都圏のJRの駅に1週間ほど掲載されます。百数十枚のポスターが百二十駅ほどに掲載されます。
加賀楓オタクの方々は期間中に全てのポスターを見付けようと奮闘します。ポスターを発見すると、ポスターの写真と駅名にハッシュタグ「貼ろうプロジェクト」を付けてツイートします。
東京駅#貼ろうプロジェクト #加賀は引力 pic.twitter.com/1D82eUUaDI
— 大久保浩秀|OKB (@HirohideOkubo) 2022年10月19日
掲載駅や掲載数をスプレッドシートにまとめていき、みんなでコンプリートを目指します。
ちなみに、このツイートの方は加賀温泉郷のプロモーションをされている方で、掲載期間の後半になると掲載数などの情報を教えてくれます。
私はというと、関西在住なので探しに行くことはできず、土地勘もなくポスター探しには協力できません。しかし私はR使いなので、Rを使って、ツイートされたポスター写真を収集して動画にまとめることにしました。
過去3年分の動画がこちらです。
・2022年10月
掲載期間中にツイートされたポスター写真のうち(半)自動で収集できた412枚をまとめて動画にしました。
— しょこ📚 (@anemptyarchive) 2022年11月8日
改めましてコンプリートおめでとうございます!西の方から楽しませていただきました🍁♨️#貼ろうプロジェクト #加賀は引力 #加賀楓温泉郷 #加賀温泉郷 #加賀楓 pic.twitter.com/pSibYUheGf
・2021年8月
自動収集できた600枚ほどのポスター写真をアニメーションにしました🍁(動画は140秒までみたいなので高速です)
— しょこ📚 (@anemptyarchive) 2021年9月22日
皆さまお疲れ様でしたーコンプリートおめでとうございます❗️関西から楽しませていただきました❗️ありがとうございます#加賀は引力 #貼ろうプロジェクト pic.twitter.com/AS7mlJgyDT
・2020年3月
ポスターコンプリートおめでとうございます!!皆様が見付けたかえでぃー達に集合していただきました。お納めください🍁♨️(つづく)#加賀は引力 #貼ろうプロジェクト pic.twitter.com/YNZypTBICs
— しょこ📚 (@anemptyarchive) 2020年3月23日
毎回試行錯誤して少しずつレベルアップしました。
以上が企画説明です。言うなればhello! pRojectです。はい。
さてこの記事では、ツイート収集と動画作成の方法を解説します。
ツイート画像のアニメーションの作成
rtweet
パッケージを利用して特定の単語やハッシュタグを含むツイートを収集し、ツイートに含まれる画像データをダウンロードします。集めた画像にggtext
パッケージを利用してラベル付けして(文字列を書き込んで)、magick
パッケージを利用してアニメーションを作成します。
利用するパッケージを読み込みます。
# 利用パッケージ library(tidyverse) library(rtweet) library(magick) library(ggtext)
この記事では、パッケージ名::関数名()
の記法を使うので、パッケージを読み込む必要はありません。
また、ネイティブパイプ演算子|>
を使っています。magrittr
パッケージのパイプ演算子%>%
に置き換えても処理できますが、その場合はmagrittr
を読み込む必要があります。
ツイート画像の収集
まずは、指定したハッシュタグを含むツイートを収集して、ツイートに含まれる画像を収集します。ツイートに含まれる画像のスクレイピングについては「Rでツイート画像を取得する - からっぽのしょこ」を参照してください。
ハッシュタグや単語を指定してツイートを収集します。
# ツイートを収集 tw_origin_df <- rtweet::search_tweets("#貼ろうプロジェクト", n = 10000, include_rts = FALSE)
search_tweets()
で指定した文字列を含むツイートを収集できます。
指定した日付のツイートを抽出します。
# 日付を指定 date_str <- "20221024" # 1日分のツイートを抽出 tw_jst_df <- tw_origin_df |> dplyr::mutate( created_at = lubridate::as_datetime(created_at, tz = "Asia/Tokyo") ) |> # 日本標準時に変換 dplyr::filter( created_at >= lubridate::as_datetime(date_str, tz = "Asia/Tokyo"), # から created_at < (lubridate::as_datetime(date_str, tz = "Asia/Tokyo") + lubridate::days(1)) # まで ) # 期間内のデータを抽出 tw_jst_df[1:5, c("created_at", "hashtags")]; nrow(tw_jst_df)
## created_at hashtags ## 1 2022-10-24 20:15:21 貼ろうプロジェクト ## 2 2022-10-24 19:27:31 貼ろうプロジェクト ## 3 2022-10-24 19:58:19 貼ろうプロジェクト ## 4 2022-10-24 19:13:43 貼ろうプロジェクト ## 5 2022-10-24 21:34:37 貼ろうプロジェクト ## [1] 141
収集したツイートの投稿日時は協定世界時(UTC)なので、日本標準時(JST)に変換する必要があります。
手打ちで用意した駅名一覧を読み込みます。
# 駅名リストを読み込み ekimei_df <- readr::read_csv( file = ".ekimei.csv", locale = readr::locale(encoding = "utf8"), col_types = readr::cols(駅名 = "c", えきめい = "c", ekimei = "c") ) ekimei_df
## # A tibble: 113 × 3 ## 駅名 えきめい ekimei ## <chr> <chr> <chr> ## 1 秋葉原 あきはばら akihabara ## 2 阿佐ヶ谷 あさがや <NA> ## 3 浅草橋 あさくさばし asakusa ## 4 熱海 あたみ atami ## 5 我孫子 あびこ abiko ## 6 飯田橋 いいだばし <NA> ## 7 池袋 いけぶくろ ikebukuro ## 8 板橋 いたばし <NA> ## 9 市ヶ谷 いちがや ichigaya ## 10 市川 いちかわ <NA> ## # … with 103 more rows
(アルファベット表記が欠損しているのは途中で使わなくなって打ち込むのを止めたからです。)
画像付きのツイートを抽出します。
# 画像付きツイートを抽出 tw_station_df <- tw_jst_df |> dplyr::select(created_at, text, ext_media_url, status_id, screen_name, user_id) |> dplyr::arrange(created_at) |> # 投稿が早い順に並べ替え tidyr::unnest(ext_media_url) |> # 複数画像ツイートのネストを展開 dplyr::filter(!is.na(ext_media_url)) # 画像付きツイートを抽出 tw_station_df[, c("created_at", "ext_media_url")]
## # A tibble: 41 × 2 ## created_at ext_media_url ## <dttm> <chr> ## 1 2022-10-24 02:00:15 http://pbs.twimg.com/media/FfxPKOUacAADmDt.jpg ## 2 2022-10-24 02:00:15 http://pbs.twimg.com/media/FfxPKOVagAQYDBS.jpg ## 3 2022-10-24 02:00:15 http://pbs.twimg.com/media/FfxPKOYaMAAQVbF.jpg ## 4 2022-10-24 04:42:27 http://pbs.twimg.com/ext_tw_video_thumb/1584268488918454… ## 5 2022-10-24 04:52:31 http://pbs.twimg.com/ext_tw_video_thumb/1584271144772722… ## 6 2022-10-24 10:13:54 http://pbs.twimg.com/media/FfzAGviaMAEdvPM.jpg ## 7 2022-10-24 10:47:28 http://pbs.twimg.com/media/FfzH0GxaEAA6CPn.jpg ## 8 2022-10-24 10:47:28 http://pbs.twimg.com/media/FfzH0HFacAAgBS5.jpg ## 9 2022-10-24 13:02:35 http://pbs.twimg.com/media/FfzmwbCUcAAvpSd.jpg ## 10 2022-10-24 13:02:35 http://pbs.twimg.com/media/FfzmwbKUoAAj5Yk.jpg ## # … with 31 more rows
画像urlはext_media_url
列に(複数画像ツイートの場合は)ネストされた状態で格納されているので、unnest()
で展開します。
リストの駅名を含むツイートテキストを検索します。
# 駅名を含むツイートを検索 for(i in 1:nrow(ekimei_df)) { # 駅名を取得 station_name <- ekimei_df[["駅名"]][i] # 駅名列を追加 tw_station_df <- tw_station_df |> dplyr::mutate(!!station_name := stringr::str_detect(text, pattern = station_name)) } tw_station_df[, c(1, 7, 8, 9, 10)]
## # A tibble: 41 × 5 ## created_at 秋葉原 阿佐ヶ谷 浅草橋 熱海 ## <dttm> <lgl> <lgl> <lgl> <lgl> ## 1 2022-10-24 02:00:15 FALSE FALSE FALSE FALSE ## 2 2022-10-24 02:00:15 FALSE FALSE FALSE FALSE ## 3 2022-10-24 02:00:15 FALSE FALSE FALSE FALSE ## 4 2022-10-24 04:42:27 FALSE FALSE FALSE FALSE ## 5 2022-10-24 04:52:31 FALSE FALSE FALSE FALSE ## 6 2022-10-24 10:13:54 FALSE FALSE FALSE FALSE ## 7 2022-10-24 10:47:28 FALSE FALSE FALSE FALSE ## 8 2022-10-24 10:47:28 FALSE FALSE FALSE FALSE ## 9 2022-10-24 13:02:35 FALSE TRUE FALSE FALSE ## 10 2022-10-24 13:02:35 FALSE TRUE FALSE FALSE ## # … with 31 more rows
セイウチ演算子:=
を使って各駅名の列を作成します。ツイートテキスト(text
列)にその駅名(station_name
)を含む場合はTRUE
、含まない場合はFALSE
になります。
駅名列を1つの列にまとめて、TRUE
の行を抽出します。
# 駅名を含むツイートを抽出 tw_photo_df <- tw_station_df |> tidyr::pivot_longer( cols = !c(created_at, text, ext_media_url, status_id, screen_name, user_id), names_to = "station", values_to = "flag" ) |> # 駅名列をまとめる dplyr::filter(flag == TRUE) |> # 駅名を含むツイートを抽出 dplyr::arrange(created_at) |> # 投稿が早い順に並べ替え dplyr::group_by(status_id) |> # 番号付け用にグループ化 dplyr::mutate(num = dplyr::row_number()) |> # 同一ツイートを番号付け dplyr::ungroup() # グループ化を解除 tw_photo_df[, c("created_at", "station", "flag")]
## # A tibble: 43 × 3 ## created_at station flag ## <dttm> <chr> <lgl> ## 1 2022-10-24 10:13:54 町田 TRUE ## 2 2022-10-24 10:47:28 町田 TRUE ## 3 2022-10-24 10:47:28 町田 TRUE ## 4 2022-10-24 13:02:35 阿佐ヶ谷 TRUE ## 5 2022-10-24 13:02:35 高円寺 TRUE ## 6 2022-10-24 13:02:35 中野 TRUE ## 7 2022-10-24 13:02:35 阿佐ヶ谷 TRUE ## 8 2022-10-24 13:02:35 高円寺 TRUE ## 9 2022-10-24 13:02:35 中野 TRUE ## 10 2022-10-24 13:02:35 阿佐ヶ谷 TRUE ## # … with 33 more rows
ここまでで、駅名と画像を含むツイートを取り出せました。
ただし現状だと、例えば「東中野」に「中野」も引っかかってしまいます。また、複数の駅名が含まれる場合も行(データ)が重複します。
画像をダウンロードして、駅名ラベルを付けて保存します。
# 保存先フォルダパスを指定 dir_path <- "tmp_folder" # 画像サイズの上限値を指定 max_w <- 800 max_h <- 600 # 画像数を取得 photo_num <- nrow(tw_photo_df) # ダミーのデータフレームを作成 dummy_df <- tidyr::tibble(v = 0) # 画像ごとに処理 for(i in 1:photo_num) { # 情報を取得 photo_url <- tw_photo_df[["ext_media_url"]][i] # 画像url photo_n <- tw_photo_df[["num"]][i] |> stringr::str_pad(width = 2, pad = "0") # 画像番号 tw_date <- tw_photo_df[["created_at"]][i] |> lubridate::date() |> stringr::str_remove_all(pattern = "-") # ツイートの日付 tw_datetime <- tw_photo_df[["created_at"]][i] |> stringr::str_replace(pattern = " ", replacement = "_") |> stringr::str_remove_all(pattern = "-|:") # ツイートの日時 tw_id <- tw_photo_df[["status_id"]][i] # ツイートのユニークID tw_user <- tw_photo_df[["screen_name"]][i] # ツイートのユーザー名 station_name <- tw_photo_df[["station"]][i] |> (\(.) {paste0(., "駅")})() # 駅名 # ファイル名を作成 file_name <- paste0(tw_datetime, "_", tw_id, "_", photo_n, ".png") # 画像をダウンロード photo_data <- magick::image_read(path = photo_url) # 画像サイズを整形 resize_data <- magick::image_scale(image = photo_data, geometry = paste0(max_w, "x", max_h)) # 画像サイズを取得 resize_info_df <- magick::image_info(image = resize_data) resize_w <- resize_info_df[["width"]] resize_h <- resize_info_df[["height"]] # 駅名ラベルを書き込み g <- ggplot2::ggplot(data = dummy_df) + # ダミーデータを設定 ggplot2::annotation_raster(raster = resize_data, xmin = -Inf, xmax = Inf, ymin = -Inf, ymax = Inf) + # 画像 ggtext::geom_textbox(label = station_name, x = 0, y = 0, hjust = 0, vjust = 0, text.color = "red", box.color = "red", fill = "white", alpha = 0.8, size = 10, box.size = 1, width = ggplot2::unit(3, units = "inches"), box.padding = ggplot2::unit(c(10.5, 5.5, 10.5, 10.5), units = "pt"), box.r = ggplot2::unit(1, units = "lines")) + # 駅名ラベル ggplot2::lims(x = c(0, 1), y = c(0, 1)) + # ラベル位置を調整 ggplot2::theme_void() # 軸を非表示 # ラベル付き画像を書き出し ggplot2::ggsave( file = paste0(dir_path, "/", file_name), plot = g, width = resize_w/100, height = resize_h/100, dpi = 100 ) # おまじない Sys.sleep(2) }
画像サイズは様々だったりオリジナルサイズだと重くなったりするので、image_scale()
である程度整えておきます。サイズ整形については「magick::image_scale関数による画像変形 - からっぽのしょこ」を参照してください。
geom_textbox()
を使って画像に駅名ラベルを書き込みます。ラベル付けについては「ggtext::geom_textbox関数によるラベル付け - からっぽのしょこ」を参照してください。
ラベル付けしたポスター写真をggsave()
で保存します。
以上で、アニメーションに利用するツイート画像を収集できました。ただし、駅名が重複した場合は、ポスター写真かどうかを目grepする必要があります。
動画作成
次は、集めた画像のサイズを統一するように加工して、アニメーションを作成します。詳しくは「magickパッケージによるアニメーションの作成 - からっぽのしょこ」を参照してください。
各ラベル付き写真を背景用の画像に重ねることで、画像サイズを統一します。
# ファイル名を取得 file_name_vec <- list.files(dir_path) # ファイルパスを作成 file_path_vec <- paste0(dir_path, "/", file_name_vec) # サイズ統一用の背景画像を読み込み background_data <- magick::image_read(path = "background.png") |> magick::image_scale(geometry = paste0(max_w, "x", max_h, "!")) # 画像ごとに背景を挿入 for(i in seq_along(file_path_vec)) { # i番目のファイルパスを抽出 file_path <- file_path_vec[i] # 画像を読み込み pic_data <- magick::image_read(path = file_path) if(i == 1) { # 背景画像に重ねる pic_data_vec <- magick::image_mosaic(image = c(background_data, pic_data)) } else { # 背景画像に重ねる pic_back_data <- magick::image_mosaic(image = c(background_data, pic_data)) # ベクトルに格納 pic_data_vec <- c(pic_data_vec, pic_back_data) } }
背景用の画像を用意しておき、image_mosaic()
で写真データと重ねて、pic_data_vec
に格納していきます。
アニメーション(gifファイルまたはmp4ファイル)を作成します。
# gif画像を保存 magick::image_write_gif(image = pic_data_vec, path = "kaedy.gif", delay = 0.1) # mp4動画を保存 magick::image_write_video(image = pic_data_vec, path = "kaedy.mp4", framerate = 1)
image_write_gif()
でgifファイル、image_write_video()
でmp4ファイルに変換できます。delay
引数は1フレーム当たりの表示秒数、framerate
引数は1秒当たりのフレーム数を指定できます。1秒当たり10フレームとするのであれば、delay
引数に1/10
、framerate
引数に10
を指定します。
以上で、ツイート画像のアニメーションを作成できました。
Enjoy!
参考
おわりに
書き残さなくてはと思ってからはや幾年、何度も継ぎ接ぎして使ってきたスクリプトをようやくまとめられました。これで何度ポスター企画があっても大丈夫なので、いつでもどうぞ!
解説を書いていたらどうにも話がとっ散らかったので、処理の目的ごとに別の記事にしたので、詳細が気になった方はリンクしてある記事も読んでみてください。
そして、2022年12月10日はモーニング娘。'22の加賀楓さんの卒業の日です。
かえでぃーがリーダーでエースなモーニング娘。を見たかったという思いはまだあるけど、自己プロデュースするかえでぃーを見てみたい。卒業おめでとうございます!