Go言語初学者です。普段の生息界隈はJS/TS+Javaですが、サーバーサイド言語の処理をもっと得意になりたいという理由から、今年からGo言語に挑戦しています。挑戦理由は「モダン, マルチスレッド, コンテナ関連での活躍」が挙げられます。
やはり、実際に何かを作って習得していくスタイルが自分には合っているので、今回は「画像処理」をテーマに、CLIとしてもAPIとしても使えるツールyellow-high5/pictarを作ってみました。画像処理といってもCNNを使って画像認識させるような高度なものではないです。
Go標準のimageパッケージ
まずはGo標準のimageパッケージがどんな仕組みになっているか簡単に確認してみました。
Point, Rectangle
座標点PointはintのXとYで構成されていて、長方形領域RectangleはPointのMinとMaxで構成されています。
Color
- Alpha...透明度
- CMYK...シアン、マゼンタ、イエロー、ブラック(Key Plate)で示した色の表現方法
- RGB...レッド、グリーン、ブルーで示した色の表現方法
- Gray..グレースケールはRGBだと3つの値全てが同じ値になります。
image.NRGBA
この構造体では画像をPix
でRed,Green,Blue,Alphaを繰り返す1次元配列として扱います。Stride
とは画像の横一列分のサイズを表しています。
imagingライブラリを読んでみる
画像処理が専門領域ではない人間には、難しい計算式を理解できないのでライブラリを使用します。今回はdisintegration/imagingを利用します。ソース自体も簡潔にまとまっていて、とても読みやすいです。機能としては以下のようなことができます。
画像処理
- adjust(調整)...グレースケール、反転、コントラスト、サチュレーション
- convolution(畳み込み)...3x3圧縮、5x5圧縮
- effects...ぼかし、シャープ
- histogram(正規化されたヒストグラム)...256(16x16)の配列に明るさを表現する
- resize...サイズ変更、切り抜き、スケール変更(fit)、サムネイル化
- transform...反転(Flip)、転置(Transpose)、回転(Rotate)
補助ユーティリティ
- io...画像読み込み、画像を開く、画像書き込み、画像保存、画像の向き(EXIFフラグ)の読み取り、変換、修正
- scanner...長方形で指定された領域を読み込む
- tools...新規画像作成、コピー、ペースト、透過
- utils...parallel(並列処理)、その他ユーティリティ
番外編:画像処理の用語集
ツールを作るには、画像処理の用語をある程度理解しておく必要があると感じたので、知識を整理しておきます
- HSV色空間...色相(Hue)、彩度(Saturation)、明度(Value)の3つからなる成分空間。原色の加減法混合で決まるRGB空間より直感的でわかりやすいらしいです。
- HLS色空間...色相(Hue)、輝度(Lightness)、彩度(Saturation)の3つからなる成分空間。HSV空間と似ています。
- 彩度...高いと画像の色が濃くなり、低いと画像の色が薄くなるイメージ。
- コントラスト...高いと画像の明暗差がはっきりし、低いとぼんやりするイメージ。
- 明度...高いと白っぽくなり、低いと黒っぽくなるイメージ。
- ガンマ補正...素直な比例関係ではなく、人間の視覚特性に合わせたRGBの補正方法。
- ガウシアンぼかし...シグマ値がぼかす量にあたる。
- シグモイド関数...DeepLearningでは、「この画像に書かれている数字は1である確率」を算出するのにお馴染みの関数でしたが、画像処理ではコントラストを調整するのに使われているようです。
並行処理
disintegration/imagingでは、画像を処理する際に並行処理を利用しています。これにより比較的早い処理ができるようです。画像処理の速度について計測している方がいらっしゃったので、詳しく知りたい人はこちらを参考にすると良いかと思います。
OpenCV, GoCV, Go言語における画像処理のパフォーマンスの比較 - ZOZO Technologies TECH BLOG
では、このライブラリではgoroutineでどのような並行処理が施されているのでしょうか。ヒントは以下のparallel関数にありました。
// parallel processes the data in separate goroutines.
func parallel(start, stop int, fn func(<-chan int)) {
count := stop - start
if count < 1 {
return
}
procs := runtime.GOMAXPROCS(0)
limit := int(atomic.LoadInt64(&maxProcs))
if procs > limit && limit > 0 {
procs = limit
}
if procs > count {
procs = count
}
c := make(chan int, count)
for i := start; i < stop; i++ {
c <- i
}
close(c)
var wg sync.WaitGroup
for i := 0; i < procs; i++ {
wg.Add(1)
go func() {
defer wg.Done()
fn(c)
}()
}
wg.Wait()
}
countは、実行する必要のある処理の数です。画像処理の例だと画素数や画像横一列分などがこれにあたります。
procsは、同時実行させる処理の数です。(デフォルトはCPUの数になっています。limitで同時処理するgoroutineの数を指定した場合はlimitが採用されます。)
チャネルにcount数の入力値を送信しておき、procs数のgoroutineがチャネルから入力値を受信し、渡された関数で並列的に処理させます。
以下にimaging.FlipH(画像の左右を反転させる処理)がどのような過程で並行処理して出力するのかを表した絵を描いてみました。
func FlipH(img image.Image) *image.NRGBA {
src := newScanner(img)
dstW := src.w
dstH := src.h
rowSize := dstW * 4
dst := image.NewNRGBA(image.Rect(0, 0, dstW, dstH))
// 画像の高さの数だけ並行処理の仕事をさせる
parallel(0, dstH, func(ys <-chan int) {
// 1行分の画素群を反転させる
for dstY := range ys {
// 反転時の場所に各画素を配置
i := dstY * dst.Stride
srcY := dstY
src.scan(0, srcY, src.w, srcY+1, dst.Pix[i:i+rowSize])
reverse(dst.Pix[i : i+rowSize])
}
})
return dst
}
cobraでCLIツール
これらのライブラリをラップしてCLIをcobraを使って作成していきます。
まずはCLIをデザインする場合はそのコマンドで実行するサブコマンドとオプションを洗い出しておきましょう。さらに、オプションはグローバルで適用できるようにするのかも考慮しておくと良いです。
基本的にデザインパターンでいうところのビルダーパターンで作成しています。デザインの参考としてはcobraの実績でも取り扱われていますが、Hugoを模してみました。
Hugoではサブコマンドを定義するときは以下のように定義します。
package commands
//構造体でオプションを定義している
type hogeCmd struct {
*baseBuilderCmd
/*オプションを書き込んでいる*/
...
}
// コマンドビルダーでオプションを返している
func (b *commandsBuilder) newHogeCmd() *hogeCmd {
// オプションの空インタフェースを定義
cc := &hogeCmd{}
// cobraでコマンド定義
cmd := &cobra.Command{
Use: "hoge",
Short: "Short Description",
Long: `Long Description`,
RunE: func(cmd *cobra.Command, args []string) error {...},
}
// サブサブコマンドがあれば、定義
cmd.AddCommand(...)
// オプションをフラグとして定義
cmd.Flags().StringVarp(...)
// ビルダーを登録しておく
cc.baseBuilderCmd = b.newBuilderBasicCmd(cmd)
return cc
}
コマンドビルダーに必要なサブコマンドを追加していけば完成していきます。コマンドビルダーの設計についてはこのファイルを見るとよく理解できます。
Ginで画像処理サーバー
画像をhttpでPOSTすると、画像を加工してオブジェクトストレージ(今回はS3)に保存するという機能を作成しました。加工内容は設定ファイルから読み込む仕組みです。アプリでプロフィール画像の登録やサムネイル作成などに使える機能を想定して実装しました。
Ginのロジック
Go製のWebフレームワークであるGinのGoDocを読んで大雑把に絵にしました。かなり端折ってしまっていますが、簡略化するとこんな感じかと。処理をHandlerFunc(ミドルウェア)でつないで、HTTPリクエストからHTTPレスポンスを返すロジックを作成しているようです。
-
Engine...フレームワークのインスタンス(
gin.Default()
あるいはgin.New()
で生成される) - Context...最も重要な部分。受け取ったリクエストからレスポンスを返すまでの情報を保持する。
- HandlerFunc...処理ロジック(Dファイル処理からロギングまで多様な処理を記述できる)
- HandlersChain...処理ロジックを集めた配列。一連の処理の流れを作るチェーン。
- RouterGroup...処理ロジックとURIのエンドポイントを繋ぐ
HTTP処理をミドルウェアチェーンで表現するあたりは、Node.jsのExpressとよく似てます。
S3へのファイルアップロード
脱線しましたが、本題のファイルアップロードの処理に話を戻します。処理の流れは大まかに3ステップ。
- HTTPリクエストボディで指定されたクライアントからの画像を読み込みサーバーのファイルシイステムへ保存。
- 読み込んだファイルをimagingライブラリで加工処理する。
- 加工処理を施した画像ファイルをS3へアップロードして、成功ステータスを返す。
Goへのファイルアップロードについては以下を参考にしました。詳細な設定を行う場合はAmazon Web Services - Go SDKを参考にしてください。
viperを使えば、オブジェクトストレージの接続設定や画像の保存名など詳細設定をの設定ファイル(config.jsonなど)に外出しすることも可能です。
まとめ
調べていくうちにGinの実績でも紹介されているGo製の画像処理サーバーが見つかりました。
こちらの方が私のような初心者なんちゃってツールより流石によく設計されています。サービス実装で作るときは遠慮なくこちらを使おうと思いました。自分で作ろうとしていたものの解が見つかると学習がより深まります。
とりあえずは、Goの標準パッケージや並行処理を使いこなせるようになるのが自分の課題だと感じました。Goのパッケージ類は見通しがよく、あまり疲れないのでしばらく鍛錬できそうです。