DRYな備忘録

Don't Repeat Yourself.

アメッシュをターミナルに表示して、ついでに雨降ってたらSlackでおしえてくれるところまで、Goでやったことのまとめ

このエントリはGo その2 Advent Calendar 2015 - Qiitaの8日目です。

7日目のS_Shiomtoriさんの記事コマンドラインツールの話でした。

amesh

% go get github.com/otiai10/amesh/amesh
% amesh -g

思ったことや詰まったこと書きます

Goでコマンドラインツールをつくりはじめるとき

なんでわざわざGoでコマンドラインツールつくるか

  • コンパイルが非常に早いので、ほぼスクリプト言語のようにソースファイルをWrite&Runして開発することができるから
  • ロスコンパイルが容易なので、Goのコンパイル環境を要求せず、実行可能ファイルを納品できるから
  • 並行処理を書きやすく、わりとデカめのことをさくっとやってのけたりするから

Goでコマンドラインツールつくるときいつもハマること

  • flagパッケージわりと使いにくい
  • flag.Args、flag.Arg(n)、os.Argsで迷う
  • コマンドfooのとき foo hoge -fugafoo -fuga hoge で挙動がけっこう違ったりする

今回該当するコードはこのへん

なお、Goでコマンドラインツールつくるときは@deeeetさんの記事がめっちゃ参考になります

Goで画像を扱う

Goは標準ライブラリが上から下まで充実している。ここでは、imageパッケージとimage/drawパッケージの紹介。

package main

import (
    "fmt"
    "image"
    "os"

    // ここでimportしたフォーマットのみ
    // image.Decodeでデコードが可能
    _ "image/png"
    // _ "image/gif"
    // _ "image/jpeg"
    // 複数取りうるならすべてimportしておけばよい
)

func main() {
    f, _ := os.Open("ritsu.png")

    // image.Decodeはio.Readerインターフェースを受けるので
    // http.Response.Bodyとかでもいける
    img, format, err := image.Decode(f)

    // 画像フォーマットによらず、image.Imageは
    // カラーモデル ColorModel() と
    // 大きさ Bounds() と
    // 座標における色情報 At(x, y int) を提供するインターフェースを持つ
    fmt.Println(img.Bounds(), format, err)
}

// (0,0)-(256,256) png <nil>

で、image/drawはもっとすごい

みんな大好きアメッシュは、あれ実は3枚の画像から構成されていて、これをターミナルに表示しようと思ったら画像を合体させる必要があった。該当箇所はここ。

大きさが等しい3枚の画像だったので、あまり深く考えずにdraw.Drawに2枚の画像をぶっ込んでるだけ。

Goでターミナルの大きさを取得する

画像をドット絵にしなきゃいけない限り、表示範囲が小さいと意味不明な絵になってしまう。なので、ターミナルの可視領域を取得したい。 そんなことできるのだろうか、と思ってたらあった。

上では画像処理というわりと高レベルなパッケージを提供している傍ら、こちらではsyscallというごくごく低レベルな機能も標準パッケージで提供している。Go言語しゅごい。

で、取得したターミナルのサイズで、画像を分割して、ターミナル上で表示できる限りの近似色を探して表示すればよい。

ritsu

どうせなら雨降ってたらSlackでおしえてくれや

ターミナルに表示するとかしないとかはあんまり関係ないんですが、アメッシュの画像を取得し、画像の色情報をごにょごにょするので、「雨降ってる判定」だけ追加すれば通知できるんではないか、と思ったので作った。

amesh -d

とすれば、3分おきにアメッシュ見に行って、取得できた画像を雨判定する。もし必要な環境変数がそこにあれば、SlackとかTwitterとかで通知してくれるようにした。

touch develop.env
docker-compose up

で立つはずです。

現状では、雨天判定を以下のようにしてる

// DefaultIsRainingFunc ...
// とりあえず全ピクセル舐めで
// ちょっとでも雨のピクセルが全体の30%を越えてたら雨ってことにする
func DefaultIsRainingFunc(ev Event) bool {
    max := ev.Img.Bounds().Max
    var hit, all float64 = 0, float64(max.X) * float64(max.Y)
    for y := 1; y < max.Y-1; y++ {
        for x := 1; x < max.X-1; x++ {
            r, g, b, a := ev.Img.At(x, y).RGBA()
            if r+g+b+a > 100 {
                hit++
            }
        }
    }
    var threshold float64 = 30
    if (hit*100)/all > threshold {
        return true
    }
    return false // 快晴だこれ
}

雨天判定がこれでいいのか、いい塩梅を探したいんだけど検証できてない。

ここ数日、雨がまったく降ってないので。

結論


明日はogataka50氏です