(O+P)ut

習うより慣れろ、Practice makes perfect。この言葉をモットーに、Slerで働く若手インフラエンジニアが、学んだ知識を【 (O+P)ut = OutPut 】していく場です。

【Java】複数画像を元にGIF画像生成(おまけ:AKB総選挙)

Twitterの話題ワードがどのように遷移しているかをGIFで可視化するという記事ですが、
【R言語】Twitterより取得した話題ワードの遷移可視化 - (O+P)ut
上の記事は文字列から一意の座標を生成するところにフォーカスをあてていました。

今回は、Javaを用いて大量のJPEG画像からGIF画像を生成する手法について紹介します。
というのも、実は海外の方*1が書いたソースコードがネットに転がっていました。
http://elliot.kroo.net/software/java/GifSequenceWriter/GifSequenceWriter.java
特別なライブラリも必要なく、コメントもしっかりと記述されているのでとても参考になります。

ソースコードを見てみると、

ImageOutputStream output = 
new FileImageOutputStream(new File(args[args.length - 1]));

となっています。つまり、引数argsの末端が出力GIFのパス、それ以外は入力画像のパスという仕組みです。
今回はディレクトリ直下の画像のパスを取得し、pathlistという新たな配列を用意してargs代わりに使いました。*2

String path = "hoge"; //画像が置いてあるディレクトリ
File dir = new File(path);
File[] files = dir.listFiles();
String[] pathlist = new String[files.length];
for (int i = 0; i < files.length; i++) {
 pathlist[i] = files[i].toString();
}

あとは、args部分をpathlistに置き換えて実行すれば問題なく稼働します。

timeBetweenFramesMS

という変数で、フレームの切り替わりの時間を管理しているので、そこは適宜変更してみてください。ループするか否か等も変更可能です。

ちょうど昨日、AKBの総選挙で 結婚発表? があったようでTwitterが大荒れしていました。f:id:mtiit:20170618111701p:plain

せっかくなので、
【R言語】Twitterより取得した話題ワードの遷移可視化 - (O+P)utと同じように
6/17 19:30- 6/18 6:30 の間の話題ワードの遷移を可視化したものが以下になります。

私がGIFを作る時は、"サブリーン"というウェブサイトの"パラパラマンガ"機能をよく使いますが、
プログラムとしてGIFが生成できるというのも、自動化等を考えた際には有効だと思います。
ご参考ください。

*1:Elliot Kroo氏作

*2:dir.listは、取得順序が不明なようです。本来は、きちんと名前順でソート等も必要ですが今回は目視して名前順になっていたのでソートしてないです。

【R言語】Twitterより取得した話題ワードの遷移可視化

Twitterのホットトピック取得については、こちらで紹介しました。
その日、「最も」話題となったキーワードを調べてみる - (O+P)ut

その中で、2016/4/23のTwitterのデータを取得したので、可視化してみました。
話題ワードTop20から、24時間分のTop10を10分毎に表示しています。
大きさは、ランキング上位なものほど文字のサイズを大きくしています。
f:id:mtiit:20170521004737g:plain

この画像を作成するにあたって行った、
文字列 → 数値 への変換について当記事では紹介します。

同じワードは同じ位置に表示させたいため、同じ文字列から同じx座標、y座標を生成する必要があります。
今回は、R言語にて Message Digest 5 を用いました。
パッケージをインストールすると、

> library(openssl)
> md5("hoge")
[1] "ea703e7aa1efda0064eaa507d9e8ab7e"

のように 文字列"hoge"から"ea703e7aa1efda0064eaa507d9e8ab7e"といったハッシュ値が生成されます。

文字列"hoge"からx座標とy座標を生成するために、md5で生成された文字列から[a-z]を抜いて、整数に変換しています。
y座標は、文字列を反転させて(hogeならegoh)同じ処理をしています。

> as.integer(substr(gsub("[a-z]","",md5("hoge")),0,5))
[1] 70371
> as.integer(substr(paste(rev(strsplit(gsub("[a-z]","",md5("hoge")),NULL)[[1]]),collapse=""),0,5))
[1] 78970

あとは、上で取得した座標位置に text 関数 で 文字を貼付けてplot画像を生成しました。
文字の色も、md5の値を使って文字列から一意になるようにしています。*1

一応、ソースコード全文は以下に掲載しておきます。
ひらがなをplotに図示するために par(family="HiraKakuProN-W3") という設定を入れています。

library(openssl)
tweet_mat <- matrix(scan("上位20位がカンマ区切りで記載されているファイルパス",what=character(),sep=","),nrow=20)
par(family="HiraKakuProN-W3")
for (i in 1:144)
  {
  plot(-10,10,xlab="",ylab="",xlim=c(-2.4,2.4),ylim=c(-1.2,1.2))
  for (j in 1:10)
  {
    tweet_x <- 2-4*as.integer(substr(gsub("[a-z]","",md5(tweet_mat[j,i])),0,5))/100000
    tweet_y <- 1-2*as.integer(substr(paste(rev(strsplit(gsub("[a-z]","",md5(tweet_mat[j,i])),NULL)[[1]]),collapse=""),0,5))/100000
    tweet_col <- as.integer(substr(gsub("[a-z]","",md5(tweet_mat[j,i])),0,5))%%8 + 1
    text(x=tweet_x,y=tweet_y,labels=tweet_mat[j,i],cex=(10-j)/5+0.5,col=tweet_col)
  }
  filename <- paste("./hoge/file",i,".png",sep="_")
  dev.copy(png,file=filename)
  dev.off()
}

以上です。

*1:また、気が向いたら md5 での 文字列→数字 の生成部分が乱数のように機能しているのか検証してみたいです。

その日、「最も」話題となったキーワードを調べてみる

Twitterからの情報を取得しながら何かをしようとすると、Twitterと連携するためのOAuth認証を行わなければなりません。
Twitter4J - A Java library for the Twitter APIなどを使えば、もちろん認証周りをスムーズにはしてくれるのですが、やはり認証用アカウントの取得は必須です。

一方で、このようなアカウントが不要でTwitterの情報が使用できるサービスに
Yahoo!検索があり、ここでは最新の話題ワードTop20が表示されています。*1

ここのhtmlを取得すれば簡単に世間の関心等をフォローすることができますので、紹介ついでに少し解析して遊んでみたいと思います。

過去の記事同様 【Linux】wgetで桂離宮の参観可能日を取得する - (O+P)ut
wgetで取得してもいいのですが、今回は jsoup( Java で HTML の解析・編集を行うためのライブラリ)を使ってhtmlを取得しました。

ライブラリを取得し、org.jsoup...をインポートさえ行えば

String url = "https://search.yahoo.co.jp/realtime"
Document document = Jsoup.connect(url).get();
String str = document.html();

たったこれだけの記述でstrにhtmlが入っています。

あとは、htmlの構造を見ながら必要な箇所のみを出力して以下のようにTop20のキーワードを吐くようにしました。*2

java -jar getHotTag.jar 
パラド,グラファイト,ジュリオ,17号,永夢,エグゼイド,飛彩,マーダッコ,リオ,パラドクス,リプログラミング,アンラッキー,界王様,いちか,スティンガー,サンデーモーニング,プリキュア,ポッピーピポパポ,キュウレンジャー,リュウテイオー

これを10分毎に起動して`date +%Y%m%d`.csvファイルに出力し、「その日、最も話題となったキーワード」を調べてみます。
2017/4/23 0:00~24:00 での計144回の検索結果を以下のワンライナーでソートしました。

cat 20170423.csv | awk 'BEGIN{RS=",";ORS="\n"}{print $0}' | awk '{count[$0]++}END{for(i in count)print i","count[i]}' > result.txt

RS=",";ORS="\n"によってカンマ区切りを全て改行にし、count[$0]とすることで連想配列としてカウントしています。

ちなみに、上の出力結果を wc で確認すると

$ wc result.txt 
     265     305    4577 result.txt

つまり、4/23 に「最新の話題ワードTop20」に出てきたワードは265種類だということになります。

この265種類のワードをcountの値でソートして出力した結果はこちらです。

f:id:mtiit:20170423235920p:plain

ぶっちぎりの一位が確認できます。
さて、上位3位はこのようになっています。

1. ナオトインティライミ (count:93)
2. エキシビション (count:43)
2. 不審物 (count:43)

一位に関しては、
f:id:mtiit:20170424000129p:plain
とのことでTwitter上で賑わっているようです(笑)

以上です。

*1:非公開に設定されていない日本語のツイートを主な対象として表示している模様

*2:2017/4/23/10:00あたり

【Java】簡単モザイク生成

これはなんのキャラクターでしょうか。
f:id:mtiit:20170314232445j:plain
どことなく、国民的アニメの主人公に色合いが似てる気がしませんか?

少し前に、モザイクをかけても人間はアニメキャラクターを見破ってしまう というのが話題になりました。
私も同じように、画像を配列と見た時に各行の平均の色を出力することで簡易モザイクを作成する遊びをしてみたのでここに共有します。

Javaでの画像取得は以下のように行います。BurreredImageのメソッドを用いて縦横のサイズも取得可能。

File f = new File("[入力画像のフルパス]");
BufferedImage read=ImageIO.read(f);
int w = read.getWidth();
int h = read.getHeight();

さて、画像の各画素を取り出して平均をとりたいんですが、
BufferedImageでは画素を取得するメソッドgetRGBを用いると、 R G B を8bitずつで表現する以下のような形が返り値となっています。
00000000 00000000 00000000 00000000
なので、R G B をそれぞれ取り出すために

public class Img{
 public static int r(int c){
  return c>>16&0xff;
 }
 public static int g(int c){
  return c>>8&0xff;
 }
 public static int b(int c){
  return c&0xff;
 }
 public static int rgb(int r,int g,int b){
  return 0xff000000 | r <<16 | g <<8 | b;
 }
}

といったクラスを用意しておくと便利ですね。
rgbは、各r,g,bを基に元の値を構成するメソッドです。

画像の各列の画素の平均値の画像を出力するコードは以下になります。

public class Main {
 public static void main(String[] args) throws IOException {
  File f = new File("[入力画像のフルパス]");
  BufferedImage read=ImageIO.read(f);
  int w = read.getWidth();
  int h = read.getHeight();
  BufferedImage write = new BufferedImage(w, h, BufferedImage.TYPE_INT_RGB);
  for(int y=0;y<h;y++){
   int r = 0;
   int g = 0;
   int b = 0;
   for(int x=0;x<w;x++){
    int c = read.getRGB(x,y);
    r = r + Img.r(c);
    g = g + Img.g(c);
    b = b + Img.b(c);
   }
   int n_r = r/h;
   int n_g = g/h;
   int n_b = b/h;
   for(int xx=0;xx<w;xx++){
    int rgb = Img.rgb(n_r, n_g, n_b);
    write.setRGB(xx, y, rgb);
   }
  }
  File f2 = new File("[出力先のフルパス]");
  ImageIO.write(write, "jpg", f2); //jpgで出力
 }
}

最初に見せたモザイク画像の元画像はこちらです。
f:id:mtiit:20170314234708p:plain
両側が黒くなっているのは、透明化している部分の画素が 0/0/0 で 黒と同じだからみたいです。
この辺は、改良の余地ありですね。以上です。

【Linux】wgetで桂離宮の参観可能日を取得する

f:id:mtiit:20170213141405j:plain
この写真は、約1年前に桂離宮で私が撮ったものです。

日本には、桂離宮修学院離宮の二つの現存する離宮があります。*1
これらの参観は無料ですが、事前に予約を行うのが一般的です。詳しくは宮内庁のHP参照。
予約はWebからもでき、例えば桂離宮について見てみますと以下のようにWebで空き人数付参観可能日が確認できます。
f:id:mtiit:20170213142210p:plain
これらの情報を、Webサーバーからファイルをダウンロードするためのコマンドwgetを用いて取得してみたいと思います。

2017年2月現在、上のページのurlは

"https://sankan.kunaicho.go.jp/register/frame/4201?ym=[西暦4桁][月二桁]"

という命名規則となっています。

標準出力に出すためのオプション -O - を使いまして来月(201703)でwgetをしますと以下のようにエラーとなります。※環境によっては証明書の発行者が不明というエラーが先に出る場合があるかとは思いますが。*2

wget -O - https://sankan.kunaicho.go.jp/register/frame/4201?ym=201703<html><head><title>Request Rejected</title></head><body>The requested URL was rejected. Please consult with your administrator.<br><br>Your support ID is

The requested URL was rejected に関してですが、調べてみますと
"ユーザエージェントWgetを排除している可能性がある"
ということが分かりました。宮内庁のHPに関しては、他のページも同じエラーが起きました。悪戯対策なんですかね?
オプション -U "" を用いて明示的にユーザエージェントを空欄に変更してみると、

wget -O - -U "" https://sankan.kunaicho.go.jp/register/frame/4201?ym=201703<html>
  <head>
    <META http-equiv="Content-Type" content="text/html; charset=UTF-8">
    <META http-equiv="Content-Style-Type" content="text/css">
...以下略

といった形で正規のhtmlを取得できました。

あとは、
日付情報↓

<TD width="20" align="center"><B>01</B></td>

参観可能人数情報↓

<img src="/images/false.gif">10:00(0人)

といったような構造になっておりましたので、以下のワンライナーで結果を取り出してみました。
読み方は、3月1日の15:30から2人空いている、といった感じです。

$ wget -O - -U "" https://sankan.kunaicho.go.jp/register/frame/4201?ym=201703 2>/dev/null | grep -B 10 '[1-9]人' | grep -e '[1-9]人' -e '<B>[0-9][0-9]</B>' | awk 'BEGIN{FS="[><]"}{print $5}'01
15:30(2)
02
10:00(1)
15:30(2)
03
09:00(4)
13:30(1)

ワンライナーに関して簡単に説明します。
大部分は 0人 ですので 0人以外のところを grep -B 10 '[1-9]人' で拾います。-Bオプションをつけることで、一致した行から前10行を同時に表示し、日付情報を拾い出しています。htmlを見ますと、日付~参加可能人数が10行毎に繰り返していたので今回はこのようにしています。そして、改めて日付と参加可能人数の情報をgrepで拾い出し、awkで欲しい箇所を抽出しています。今回は、< または > で区切るとどちらも5列目でしたので上のようにしました。

ちなみに、5月だと1日も空いてませんでした。。。
京都に立ち寄る予定がある方は、ぜひ事前に調べてみてください。

*1:修学院離宮にも行きました。どちらもぜひ一度は行ってみる価値ありです!

*2:接続先が信用できる場合では --no-check-certificate を追記ですれば簡易的に対応可能です。

【Linux】とりあえず2つだけ覚える、変数のパターンマッチ

bashでは「%」や「#」を用いた変数のパターンマッチがありますが、最近使うことが増えてきたのでメモとして書いておきます。

紹介するのは、とりあえず2種類です。

${変数#パターン}
${変数%パターン}

[ファイル名][数字].[拡張子]のようなファイルを一気に処理したい場合等に使えたりして便利です。

紹介のために、以下のような4つのファイルを用意しました。

$ ls
file1.txt  file2.txt  file3.txt  file4.txt

4つのファイルをワンライナーでいじろうとすると以下の書き方が使えます。
fileで始まるファイルを変数xに格納して、echoで画面に表示しています。

$ for x in file*; do echo $x; done ⏎
file1.txt
file2.txt
file3.txt
file4.txt

さて、まずは ${変数#パターン} です。
ざっくり言えば "パターン部分を除いたファイル名の後半を取り出したい時" に使います。
正確に書けば、"変数の内容について、最初の部分とパターンがマッチした場合に、もっとも短く一致する部分を取り除いた残りの部分を返す" 役割が ${変数#パターン} にはあります。

以下を見ていただければイメージが掴めるかと思います。

$ for x in file*; do echo ${x#file}; done ⏎
1.txt
2.txt
3.txt
4.txt

これを応用すれば、file[数字].txt ではなく text[数字].txt としたい時などに使えます。

$ for x in file*; do mv $x text${x#file}; done ⏎
$ ls ⏎
text1.txt  text2.txt  text3.txt  text4.txt

次に、 ${変数%パターン} です。

こちらもざっくり言えば "パターン部分を除いたファイル名の前半を取り出したい時" に使います。
正確に書けば、"変数の内容について、最後の部分とパターンがマッチした場合に、もっとも短く一致する部分を取り除いた残りの部分を返す" 役割が ${変数%パターン} にはあります。

こちらも以下を見てください。※上の実行後に行っているので、ファイル名はtextから始まっています。

$ for x in text*; do echo ${x%.txt}; done ⏎
text1
text2
text3
text4

これを応用すれば、拡張子を一気に変えたい場合なんかに使えそうですね。

他にも、「##」や「%%」などもあるんですが、とりあえずは上の2個を覚えて必要あらば詳しく調べるのが良いかなーと思います。

【Cygwin】作業ログを自動で残す

Cygwinでちょこちょこと作業をすることが多いのですが script コマンドを使えば、簡単に作業ログを残せます。

その作業ログの取得を、自動で行えるようにした際の備忘録です。

scriptコマンドの使い方は、ターミナルで script と打てば以下のようになるかと思います。

$ script
スクリプトを開始しました。ファイルは typescript です

ここから記録が ./typescript に始まり、終了する際は exit と打てば以下のように終了します。

$ exit
exit
スクリプトを終了しました。ファイルは typescript です

オプションや引数をつけることで、ファイルを上書きしたりファイル名を指定もできます。

問題は、これを起動時に自動で実行しようとする際です。
単純に .bashrc に

script

と書くとすごいことになります。以下が私の環境で実際に上記の記述を.bashrcに行い、起動した際の出力です...

スクリプトを開始しました。ファイルは typescript です
スクリプトを開始しました。ファイルは typescript です
スクリプトを開始しました。ファイルは typescript です
スクリプトを開始しました。ファイルは typescript です
スクリプトを開始しました。ファイルは typescript です
スクリプトを開始しました。ファイルは typescript です
スクリプトを開始しました。ファイルは typescript です
スクリプトを開始しました。ファイルは typescript です
スクリプトを開始しました。ファイルは typescript です
スクリプトを開始しました。ファイルは typescript です
スクリプトを開始しました。ファイルは typescript です

実はscriptコマンドは新たにシェルを立ち上げる挙動をするようですので、新たなシェルが立ち上がる→scriptコマンド実行→新たなシェルが... の無限ループになるみたいですね。

$ ps
      PID    PPID    TTY       STIME COMMAND
    13052       1    ?         01:08:49 /usr/bin/mintty
     6288    1936    pty0      01:08:56 /usr/bin/ps
     1936   13052    pty0      01:08:49 /usr/bin/bash

$ script
スクリプトを開始しました。ファイルは typescript です

$ ps
      PID    PPID   TTY      STIME COMMAND
     1288    5712  pty0      01:09:01 /usr/bin/script
    11180    4176  pty1      01:09:04 /usr/bin/ps
     5712    1936  pty0      01:09:01 /usr/bin/script
     4176    1288  pty1      01:09:02 /usr/bin/bash
    13052       1  ?         01:08:49 /usr/bin/mintty
     1936   13052  pty0      01:08:49 /usr/bin/bash

上がすごい分かりやすいと思います。pty1が立ち上がってカレントシェルが変更になります。

なので、今回は簡易的に現在のシェルの親プロセスが /usr/bin/mintty な場合にのみ script を動かすようにします。

autoscript.shというスクリプトを記述しました。*1

#!/bin/sh

ppcheck=`ps | grep $PPID | grep -v pty | awk '{print $8}'`
ttynum=`echo \`tty\` | awk '{print substr($0,(length($0)+1)-1,1)}'`
nowtime=`date +"%Y%m%d%H%M%S"`
filename="${nowtime}_${ttynum}"

if [[ ${ppcheck} = "/usr/bin/mintty" ]]; then
        script -f script_log/$filename
fi

簡単に説明しますと、ppcheckで現在のシェルの親プロセスのIDでプロセスを絞り込み、ttyが?となっている/usr/bin/minttyをppcheckとして取り込んでいます。
それを、現在のttyの最後の数字と現在の時刻とするファイル名でログをとる、という感じですね。こうしておけば、scriptで立ち上がったシェルの親プロセスは/usr/bin/minttyではなくなるので、ループは起きないといった感じです。ファイル名は、時刻だけだと同時にCygwinの窓を複数立ち上げるとやっかいかなーと思ったのでttyから値を持ってきてます。特に深い意味はありません...

これを起動時に実行するために .bashrc に以下のように記載します。

. ./autoscript.sh

カレントシェルで実行するよう記述しているのは、親プロセスが変わってしまうのを防ぐためです。

これでCygwinを立ち上げると

スクリプトを開始しました。ファイルは script_log/20170129011831_0 です

$ ls script_log/
20170129011831_0

とまぁ、こんな感じで機能しています。

とりあえずこれで運用してみて、挙動が怪しい場合にはまたその都度修正していこうと思います。
Cygwinをお使いの方で、同じようなことに興味がある方はご参考ください。

*1:直接.bashrcに記述してもいいですが、テストしやすかったのでそのままshファイルを起動するという構成にしています