こんにちは、とうとうです。この記事はXR好き開発合宿ブログリレー2日目の記事です。2泊3日の合宿中に開発した内容を紹介していきます。
概要
VRSNSで撮った大量の写真の中からいい感じに写真を選んでTwitterに投稿するのって大変ですよね...それを自動で(ランダムに選ぶよりかは)いい感じに選んで投稿するシステムを作ってみました!
仕組みとしては、写真を保存したフォルダを読み込む、できるだけ最近撮影した画像を選ぶ、dHashで画像距離計算をして似ている画像は選ばない、API経由でTwitterに投稿する、定期実行にはWindows標準アプリのタスクスケジューラーを利用するといったものです。
VRChatで毎日増える大量の写真の中からいい感じに写真を選んで毎日投稿するのは手間なんですが、このシステムを使えば楽ができます。
実は、とうとうさんの「最近の!」ツイートは2021年4月頃から自動化されていて、最近の写真をランダムに選ぶという手法をとっていたのですが、それだと同時に似た画像を選んでしまいbot感が出てしまっていたので、今回の合宿でdHashを用いた画像距離計算をし、似た画像を選ばないアルゴリズムを追加したという経緯です。
また、GitHub Actionsを利用してリリースページに成果物を置けるようにしました。テストを書いていなかったり、まだまだ機能追加できる部分があったりするのですが、少しづつ良くしていきたいです。リポジトリはこちらになります。
紹介スライド
誰向け
- PickPicTweetがどんな実装か知りたい
- フォルダやファイル移動周りの処理について
- dHashについて
- Twitter APIの簡単な使い方について
- VRSNSで撮った写真を自動で投稿できるようにしたい
- 今回の記事の場合、自力でTwitter開発者申請してRead/Write用のトークンを取得できること
- GitHub Actionsを利用して、.NETプロジェクトをWindows環境でビルドし、リリースページのアセットに特定のファイルを置くyamlファイルを参考にしたい
- Windows標準アプリのタスクスケジューラーの使い方を知りたい
機能
- 写真フォルダ配下を月別・日付別に割り振り直す
- デフォルトでは機能OFF
- 写真フォルダの構成を変えてしまうので利用は慎重に
- いい感じに写真を選ぶ
- できるだけ最近撮影した写真を選びます
- dHashを用いた画像類似度を計算し似ている写真は投稿写真から除かれます
- 過去(100枚)に投稿した写真は取り除かれます
- secretフォルダ直下の写真は選ばれない
- 手動でsecretフォルダを作成し、その直下に選びたくない写真を予め入れておく必要があります
- 画像付きツイート
実行時オプション
-
--full-path "画像が含まれるフォルダのフルパス"
- デフォルトはVRChatの画像フォルダへのパス
-
--every-dir n
- 0:デフォルト値、何もしない
- 1:画像を月別のフォルダに振り分ける。一度実行するともとに戻せないので注意が必要。
- 2:画像を月別フォルダ配下の日付別フォルダに振り分ける。一度実行するともとに戻せないので注意が必要。
-
--pic-pool-count n
- n枚の写真から4枚が選ばれる。デフォルトは100
-
--no-tweet n
- 0:ツイートする、デフォルト値
- 1:ツイートしない
使い方
アプリケーションを使う場合は自己責任でお願いします。
リリースページから実行ファイルをダウンロード
-
Twitter開発者プラットフォーム で開発者用トークンを申請して取得する。
- Read/Write権限が必要です。
- 参考記事:【2022年度最新版】Twitter API 申請方法(Twitter v1.1 v2対応)
-
Releases · tou-tou/PickPicTweet から
PickPicTweet.exe
とappsettings.json
をダウンロード -
appsettings.json
に取得したトークンを設定し、実行ファイルと同じディレクトリに配置 - Windowsタスクスケジューラーに日時や実行ファイル、引数を指定する
Visual Studio2022で実行ファイルをビルド
-
Twitter開発者プラットフォームで開発者用トークンを申請して取得する
- Read/Write権限が必要です。
- 参考記事:【2022年度最新版】Twitter API 申請方法(Twitter v1.1 v2対応)
-
appsettings_sample.json
を参考にappsettings.json
を作成し取得したトークンを設定する - Visual Studio2022でソリューションをビルドし、実行ファイルを生成する
- .NET6.0のビルド環境です
- 参考
- Windowsタスクスケジューラーに日時や実行ファイルを指定する
実装について
以下のような実装となっています。
- VRChatの画像フォルダ配下を日別フォルダに振り分ける
- いい感じに写真選ぶアルゴリズム
- 直近100枚の画像を時系列降順に並び替えたリストを作成
- リストから過去に投稿した画像を取り除く
- リストの前半から1枚(pic1)をランダムに選び、リストから除く
- pic1との画像距離が一定以下(画像類似度が一定以上)の画像をリストから除く
- リストの前半からランダムに1枚(pic2)を選ぶ
- 2-4を繰り返し同様にpic3,pic4を選ぶ。リストの要素数が0になるかpic4を選ぶかで終了
- 最大4枚の選ばれた画像パスをローカルテキストに保存
- TwitterAPIを利用して選ばれた画像をツイート
- 定期実行はWindows標準アプリのタスクスケジューラーで投稿間隔と実行ファイルを登録
画像を日別フォルダに振り分ける
画像作成時刻で月別または月別/日付別にフォルダを作成しそのフォルダ配下に画像を移動します。実装は以下です。
いい感じに写真を選ぶアルゴリズム
(ランダムに選ぶよりかは)いい感じに写真を選ぶアルゴリズムですが、以下のような感じになっています。
- 直近100枚ぐらいの画像のリスト
_orderedSet
を時系列降順に並び替え -
_orderedSet
の前半から1枚の画像(pic1)をランダムに選び、_orderedSet
から除く - pic1との画像距離が一定以下(画像類似度が一定より大きい)の画像を
_orderdSet
から除く -
_orderedSet
の前半からランダムに1枚(pic2)を選ぶ - 3-4を繰り返し同様にpic3,pic4を選ぶ。
_orderedSet
の要素数が0になるかpic4を選ぶかで終了
実装は以下になっています。
dHashを用いた画像距離計算について
dHashを用いた画像距離計算は以下のようなシンプルなアルゴリズムとなっています。
ハッシュ値の比較計算
dHashの良さは画像を64bitのハッシュ値に変換することができ、2つの画像を表すハッシュ値の比較がXOR
、POPCNT
といった(おそらく)ハードウェア命令のレベルで実装されている関数を利用して計算するので高速なはずです。(そもそも高速にする必要があるのかみたいな話がありますが、それは置いてもらって...)
そして、XOR
とPOPCNT
を用いたハッシュ値の比較計算は以下のようになります。
実際の実装は以下の実装でシンプルにかけて嬉しい気持ちになれます。
/// <summary>
/// ulong(uint64)のハミング距離[0,64]を計算
/// 二つの画像のハッシュ(uint64)をXORしてPopCount(いくつのビットが1かをカウントする)
/// XORとPopCountはどちらもプリミティブな命令なので高速に計算できる
/// ref:https://ja.wikipedia.org/wiki/%E3%83%8F%E3%83%9F%E3%83%B3%E3%82%B0%E8%B7%9D%E9%9B%A2
/// </summary>
/// <param name="img1"></param>
/// <param name="img2"></param>
/// <returns></returns>
public int ComputeHammingDistance(Bitmap img1,Bitmap img2)
{
// 画像サイズを小さくして、グレースケールに変換しハッシュ化
var hash1 = ComputeAdjacentPixelDiff(ConvertGray(ReduceSize(img1)));
var hash2 = ComputeAdjacentPixelDiff(ConvertGray(ReduceSize(img2)));
// ハミング距離の計算
return BitOperations.PopCount(hash1^hash2);
}
TwitterAPIを使用して画像ツイート
Twitter開発者プラットフォームで開発者用トークンを申請して取得
こちらの記事【2022年度最新版】Twitter API 申請方法(Twitter v1.1 v2対応)などを参考にしながら開発者用トークンを申請して取得してください。Read/Write権限が必要なのでご注意ください。
トークンをappsettings.json
に保存
取得したトークンをプロジェクト内のPickPicTweet/appsettings.json
に設定します。
{
"ConsumerKey": "",
"ConsumerKeySecret": "",
"AccessToken": "",
"AccessTokenSecret": ""
}
PickPickTweet.csproj
ファイルにappsettings.json
がBuildに含まれるように下記を追加
PickPicTweet/PickPickTweet.csproj
ファイルにappsettings.json
がBuildに含まれるように下記を追加
<ItemGroup>
<None Update="appsettings.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
</ItemGroup>
画像付きツイート
画像をアップロードしてメディアidを取得し、画像付きツイートをします。
/// <summary>
/// 画像をアップロードしてメディアidを取得
/// </summary>
/// <param name="path"></param>
/// <returns></returns>
private long UploadImage(string path)
{
MediaUploadResult result = Token.Media.Upload(media: new FileInfo(path));
return result.MediaId;
}
/// <summary>
/// 最大4枚の画像付きツイート
/// 画像がない場合はテキストのみツイート
/// </summary>
/// <param name="text"></param>
/// <param name="paths"></param>
public void ImageTweet(string text, List<string> paths)
{
// 画像をアップロードして返ってきたmedia_idを追加する
List<long> mediaIds = paths.Select(UploadImage).ToList();
Token.Statuses.Update(new
{
status = text,
media_ids = mediaIds
});
}
タスクスケジューラで定期実行
実行ファイルの生成
Visual Studio 2022
やRider
でプロジェクトのビルドを行い、実行ファイルを生成します。ビルド環境が.NET6.0
なので注意が必要です。
リポジトリからcloneまたはダウンロードしてきたら、Visual Studio 2022
でそのディレクトリを選んで開きます。ソリューションとして認識されない場合は、フォルダービューからPickPicTweet.sln
をクリックするとソリューションとして認識されるはずです。そのあと、appsettings.json
にトークンを設定し、下図のようにビルドすれば実行ファイルを生成できます。
または、Releases · tou-tou/PickPicTweetから拾ってくる方法もありますが、この場合はTwitter用のトークンを設定したappsettings.json
と実行ファイルを同じディレクトリに配置してください。
タスクスケジューラーに実行ファイルを登録
Windowsの検索窓から「タスクスケジューラ」で検索し起動
実行ファイルを登録し(必要であれば)引数を指定します。下図では--every-dir 2
を指定しますが、このオプションは画像フォルダ配下を月別/日付別フォルダに変えてしまい、一度実行すると元に戻せないので注意が必要です。
GihHub Actionsでのリリース
mainブランチにpushしたら、.NETプロジェクトをWindows環境上でビルドして、実行ファイルや設定ファイルのテンプレートをリリースページのAssetとしておくようにしました。
yamlファイルはこんな感じです。
今後の課題とか
- 単体テストを書く
- VRSNSのログとかを使ってアバターやワールドの種類を画像選択に組み込む
- 撮影した写真の中で投稿したくない画像(ネタバレ系とか取り扱い注意な画像)を投稿しないようにする
- Twitter連携できるようにして誰でも使えるようにする
まとめ
正直なところ、テストを書いていなかったり、Twtter開発者申請が必要だったり、明らかに撮影ミスした写真が選ばれたりするなど、使い勝手がよくない状態でリリースしたり記事を書いたりするのには抵抗感があったのですが、いつまでたっても公開しないのもよくないので、とりあえず公開してみることにしました。この記事がだれかのお役にたてば幸いです。
謝辞
また、最後になりますが、今回の開発合宿では多くの支援者の皆様にサポートしていただきました1。楽しく開発できたり、メンバーとの交流ができたりでとても楽しかったです。本当にありがとうございました。