次ログ

次ログ

ゆるりと働いているSREの技術ブログのような何か。趣味の話も書く

Go言語 学習メモ03 - 総当り組み合わせ二次元配列の作成

前書き

どうもです、次郎です。

有給とってGo言語の学習してました。

以前Pythonの記事で紹介したツールの移植がほぼ完了して、今はドキュメントの整備と設 定ファイル編集ツールの作成をしています。

実際ツールを作成するとなったら、いろんな仕様を駆使しないといけないので、 ツール作成を学習に利用するという試みは結構成功だったなぁと。 ある程度使えるようになったらEffective GOとかも買わないとか考えてます。

ということで、今回はそんなツールの中で実装した、総当り組み合わせの実装につ いてメモします。

注意
二次元配列、と書いてますけれど実際はスライスです。
二次元スライスという読みが奇妙なので、ここでは二次元配列と呼びます。

目次

目的

ゲーム用のキャライラストを作成するときに、イラストの差分を用意することがあると思います。

差分の用意は人によってまちまちで、いろんなタイプがあると思います。 表情のみ変える方もいれば、ポーズごと変える方もいると思います。

主な差分は目、眉、口とかでしょうか。
あるいは、それらを組み合わせた顔をまるごと置き換える感じでしょうか。

たとえば以下のような感じの二次元配列です。

[
   ["eye1", "mosue1", "eyebrows1"]
  ,["eye1", "mosue1", "eyebrows2"]
  ,["eye1", "mosue1", "eyebrows3"]
  ,["eye1", "mosue2", "eyebrows1"]
  ,["eye1", "mosue2", "eyebrows2"]
  ,["eye1", "mosue2", "eyebrows3"]
  ,["eye1", "mosue3", "eyebrows1"]
  ,["eye1", "mosue3", "eyebrows2"]
  ,["eye1", "mosue3", "eyebrows3"]
]

これらのうち、あとから口の差分が増えたり、オプションとしてメガネとか猫耳とか尻尾とか
組み合わせの要素が増えたときに、差分画像が一気に増えます。

また、差分の修正が発生したときに組み合わせ直す作業が面倒です。
配布するためにZip圧縮し直す必要もあります。

僕の場合は表情のみ変えるタイプの人なのですが、その表情の差分用意が非常にめんどくさかったので、 それの自動化を行う上で、総当り組み合わせを適用した二次元配列を作成する必要がありました。

今回のコードではそれを実装したものになります。

コード

// 画像組み合わせを保持するための二次元配列patternListに対して、
// 総当り組み合わせの配列を追加する。
//     usage:
//     SetImagePatten(&patternList, nil, images, 0)
func SetImagePatten(patternList *[][]image.Image, pattern []image.Image, imgArr [][]image.Image, index int) {
    if len(imgArr) <= index {
        p := make([]image.Image, len(pattern))
        // リターン先で値が更新されてしまうので、
        // コピーをスライスに追加する
        copy(p, pattern)
        *patternList = append(*patternList, p)
        return
    }

    if index == 0 {
        pattern = make([]image.Image, 0)
    }

    for _, v := range imgArr[index] {
        pattern = append(pattern, v)
        SetImagePatten(patternList, pattern, imgArr, index+1)
        pattern = pattern[:len(pattern)-1]
    }
}

この再起関数を利用しているCLIツールのコード全体は下記。
https://github.com/jiro4989/igen

実装面での工夫点

今回の総当り組み合わせで使ったテクニックは下記のとおりです。

  • 再起関数
  • スライス
  • ポインタ
  • rangeループ

再起関数

今回、画像の総当り組み合わせを実装する上で、forループによる愚直な実装は絶対に嫌 でした。

というのも、柔軟性が失われる上に要素が増えたときに対応できないからです。

下記はPythonで実装したときに、画像の総当り組み合わせを実装したコードです。

others = pattern['others']
if others != None:
    i = 8
    for eyes in others['eye']:
        eye = eyes['name']
        for eyebrows in eyes['eyebrows']:
            for mouse in others['mouse']:
                i += 1
                baseimg  = Image.open(f'{imgdir}/{base}.{ext}').convert(colormodel)
                processing_image(i, out_formatter, outdir, imgdir, ext, opt, baseimg, eyebrows, eye, mouse)

上記のコードでは、組み合わせのリストを生成するのではなくて、もう直接画像ファイル を開いて操作しているので、見てくれは全然違いますが、やっていることは同じです。

こういうのの実装には多分再起関数が有用なのだろと考えたので、苦手な再起関数に挑戦 しました。
ちなみに、最初にPythonで再起関数を作成してからGoで書き直すって感じで実装しました 。
コードのテストをするのにもやっぱりPython楽ですからねー。

Pythonで実装したコードを貼り付けようと思ったらどうも間違えて消してしまったみたい です。もったいない... orz

スライス

JavaPythonでいうところのリストです。 下記のような感じで宣言します。

sl := make([]int, 0)
// または
var sl = []int

配列の宣言の要素数を省略するとスライスとして扱われるようです。
というか、スライス自体は配列のポインタなので、見ているものは配列と同じだそうです 。
http://golang.jp/go_spec#Slice_types

ハマったところ

makeで要素数を最初に指定すると、ゼロ初期化された値が格納された状態で初期化される 。

コード

   sl := make([]int, 5)
    fmt.Println(sl)
 
    sl = append(sl, 1)
    fmt.Println(sl)

結果

[0 0 0 0 0]
[0 0 0 0 0 1]

JavaだとListを宣言するときに、事前に要素数がわかっているときは

List<Integer> list = new ArrayList<>(5);

って宣言してからaddするほうが高速に動作するので、その癖をGoでやったらはまった...
どうようのことをGoでやる方法がわからないので、とりあえず空スライスとして宣言する ように実装しています。

それはさておいて、
Javaだとlist.add(elem)
Pythonだとlist.append(elem)
Goだとsl = append(sl, elem)で、
とごちゃごちゃになってきますね...

ポインタ

func SetImagePatten(patternList *[][]image.Image, pattern []image.Image, imgArr [][]image.Image, index int) {

patternList *[][]image.Imageがそれですね。
この再起関数自体は戻り値を持たずに、引数で渡されたポインタの参照先の要素を変更す ることで、値を変更しています。

ポインタを使うときは、関数の引数で型の前に*をつけて、呼び出し側で&を使うみたい です。

// 関数定義
func f(x *int) {
  *x = 1
}

// 関数呼び出し
x := 0
f(&x)
fmt.Println(x)

大学のC言語の勉強のときにやったような記憶があるくらいでまともに使うのは今回が初 めてです。
Javaの参照渡しと似てるけれど、具体的に何が違うんだろう...

rangeループ

for _, v := range imgArr[index] {
    pattern = append(pattern, v)
    SetImagePatten(patternList, pattern, imgArr, index+1)
    pattern = pattern[:len(pattern)-1]
}

Javaの拡張for文、Pythonのinループと同じですね。 基本的にループをintカウンタで回してインデックスで要素を取得する方法は嫌いなので 、forループはほとんどこちらを利用してます。 もとの要素を変更する必要があるときだけカウンタを使います。

今回はインデックスが不要なので一つ目の要素は_で握り潰しています。
地味にこのインデックスと一緒にループって便利なんですよね。
Javaだと拡張for文でインデックスが欲しくなったらわざわざループの外で変数を宣言し ないといけないので。

まとめ

ということで、今回再起関数を実装するに当たって、いろんなGo言語の機能を利用しまし た。

少し複雑な処理を実装するとなると、一気に勉強量が増えて大変ですが、多分自分にはこ の方法が一番身につきやすいです。モチベーション維持にも効果的ですし。