はじめに
第1章 画像処理で遊ぼう!
第2章 Goでグラフを描写しよう
第3章 GoでCLIを作ろう!
第4章 TinyGo で WebAssembly
第5章 実録!Goのクリーンアーキテクチャ
Women Who Go Tokyoは、Goを楽しむためのコンテンツをオムニバス形式で用意したものです。コンテンツを楽しむためは、いくつかの準備(Goやサードパーティパッケージのインストール)が必要になります。
この本が読者のみなさまにとって、Goをさらに愛するきっかけとなりますように。
Women Who Goは、2014年にサンフランシスコで、女性・ジェンダーマイノリティの方々が集まるGoのグローバルコミュニティとして、設立されました。
活動の目的は、多くの女性やジェンダーマイノリティの方々が、Goのコミュニティに安心して参加できるようにすることです。
技術者のコミュニティは、最初は一人だと参加しづらいという声がしばしば聞かれます。Women Who Goは、仲間をみつけることができる機会を提供したいと考えています。また、一人だとどうすればよいかわからないことを質問し合うなど、協力し合いながら、楽しくGoについて学んでいきます。
Women Who Goとして実施する活動の内容は各国自由で、ベルリンでは「Go In Action」の著者を招いてワークショップを行ったり、シカゴではGoの新バージョンがリリースされた際に、リリースパーティを行ったりしています。そして、そのイベントの多くは無料で開催されています。
他国の活動については、Women Who GoのTwitterアカウント1から確認できます。
Women Who Go Tokyoは、2016年6月、日本ではじめてのWomen Who Goのコミュニティとして設立されました。
2016年6月に初のイベントを実施して以降、毎月1回、都内の会場に10人前後で集まり、Goについて学んでいました。2020年からは、オンラインでの開催に移行しています。
主な内容は、プログラミング初学者向けのワークショップや読書会で、自分で手を動かしながら進めていきます。堅苦しくなく、できるだけゆるく和やかに気軽に参加できるような雰囲気にしたいと思い、運営しています。そして、イベントは無料で開催しています。
本書に関するお問い合わせ:
・https://web.womenwhogo.tokyo2
・admins@womenwhogo.tokyo3
本書に記載された内容は、情報の提供のみを目的としています。したがって、本書を用いた開発、製作、運用は、必ずご自身の責任と判断によって行ってください。
これらの情報による開発、製作、運用の結果について、著者はいかなる責任も負いません。
この章は、@saki_engineerが担当しています。
また、ソースコードの挙動はすべて、"go version go1.15.5 darwin/amd64"で確認したものです。
画像処理をGoでやってみたい!となったときに、真っ先に見にいくのは、標準imageパッケージでしょう。新しいことを始めるワクワク感を胸いっぱいに抱いて、意気揚々とGoDocのページを開いたときの感想は───
少なくとも私はそう思いました。なに「アルファ」って?RGBAとかNRGBAとかYCbCrとかいろいろあるけど、結局どれ使えばいいの??構造体とかメソッドの定義だけじゃなくて、具体的にやりそうな処理の実装例とか載っけといてよーーーー!と。
この章では、「Goで画像をいじってみたいけど、そもそもの用語類が全然わからない」という方々向けに、imageパッケージでできることやその詳細、理解のために必要な用語類1を解説します。
画像の加工をGoで行うためにはどうしたらいいのか、まずはグレースケール変換を例にとって、全体図を見てみましょう。2
import (
_ "image/jpeg"
_ "image/png"
// (略)
)
file, _ := os.Open("pic.png")
defer file.Close()
img, _, _ := image.Decode(file)
bound := img.Bounds()
NewImg := image.NewGray(bound)
for y := bound.Min.Y; y < bound.Max.Y; y++ {
for x := bound.Min.X; x < bound.Max.X; x++ {
NewImg.Set(x, y, img.At(x, y))
}
}
newfile, _ := os.Create("new.png")
defer newfile.Close()
png.Encode(newfile, NewImg)
次に、このコード全体で何をやっているのか?というところを詳しく見ていきましょう。
読み込んだファイルをGoで画像として扱えるようにするためには、Fileオブジェクトからimage.Imageインターフェースに変換してあげる必要があります。このos.Open()で得たFile型を、image.Imageインターフェースにする作業をデコードといいます。
import (
_ "image/jpeg"
_ "image/png"
// (略)
)
file, _ := os.Open("pic.png")
defer file.Close()
img, _, _ := image.Decode(file)
ここでは、imageパッケージ内に存在するimage.Decode()関数を用いています。このimage.Decode()関数を用いる場合は、使いたいファイル拡張子に応じたパッケージのインポート宣言をする必要があります。今回の場合、image/jpegとimage/pngをインポートしています。よって、jpegファイルとpngファイルのデコードができるようになっています。
image.Decode()ではなく、image/pngやimage/jpegパッケージ内に存在する、個別のデコード関数を直接用いることも、一応できます。
import (
"image/png"
// (略)
)
file, _ := os.Open("pic.png")
defer file.Close()
img, _ := png.Decode(file)
しかし、読み込むファイルがpngではなくなった場合、コードの書き換えが必要です。image.Decode()を使用すれば、画像ファイルの拡張子が変わったり、不確定である場合にも、インポート宣言を増やすだけで対応可能です。よって、特別な状況でない限り、image.Decode()の使用が望ましいでしょう。
image.Decode()の実行時に、読み込みたい拡張子のパッケージのインポート宣言が行われていないと、エラーが出ます。
img, _, err := image.Decode(file)
fmt.Println(err)
// image: unknown format
ここでいう"image: unknown format"とは、どういうことなのでしょうか。image.Decode()の説明文を見てみます。3
Decode decodes an image that has been encoded in a registered format.Format registration is typically done by an init function in the codec- specific package.
訳: Decodeでは、登録されたフォーマット形式(registered format)の画像をデコードします。 formatへの登録は、特定のパッケージのinitによって行われます。
エラー文の"unknown format"というのは、この「登録されたフォーマット形式」にデコードを試みた、ファイル拡張子のものがないということです。そして、その登録は「パッケージのインポート時のinit関数の実行」で行われます。
func init() {
image.RegisterFormat("jpeg", "\xff\xd8", Decode, DecodeConfig)
}
これは、image/jpegをインポートした際に実行されるinit関数です。4ここでは、image.RegisterFormat関数を使って、jpegをデコードするための関数jpeg.Decode(第三引数)を登録しています。
さて、今まで出てきた「フォーマット」というのは、一体何者なのでしょうか。一言で説明するならば、「画像バイナリファイルのプレフィックスパターンと、デコード設定・関数を対応付けたもの」です。
具体例で理解するために、ここでもう一度image.RegisterFormat関数を使って、jpegフォーマットを登録している様子を見返してみましょう。あれは、「画像ファイルのプレフィックスが\xff\xd8(第二引数)で始まるものをjpeg(第一引数)として認識して、それがimage.Decodeやimage.DecodeConfig関数の引数として渡された場合には、jpegパッケージ内のDecode関数とDecodeConfig関数を用いる」という対応付けを登録しているのです。
各パッケージのinit()を用いて登録されたフォーマットは、imageパッケージで定義されているグローバル変数atomicFormatsに保存されます。そして、image.Decode()実行時に、渡されたファイルのプレフィックスと同じものが、atomicFormatsに登録されているフォーマットに含まれているか、探す処理が行われます。5
func Decode(r io.Reader) (Image, string, error) {
rr := asReader(r)
f := sniff(rr) // どの登録フォーマットと一致するか探す処理
if f.decode == nil {
return nil, "", ErrFormat // 見つからなかったらerr
}
m, err := f.decode(rr) // デコード実行
return m, f.name, err
}
// どの登録フォーマットと一致するか探す処理
func sniff(r reader) format {
// atomicFormatsの中身を取得
formats, _ := atomicFormats.Load().([]format)
for _, f := range formats {
// デコードしたいファイルのプレフィックスを取得
b, err := r.Peek(len(f.magic))
// 登録フォーマットのプレフィックスと比較
if err == nil && match(f.magic, b) {
return f // 一致していれば、その拡張子と判断
}
}
return format{}
}
この実装を見ればわかるとおり、実はimageパッケージのDecode関数は「フォーマット登録された関数の中から、どれを使うのが適切なのか?」という判断しか行っていないのです。つまり、実際にファイルをデコードする処理自体は、image/pngやimage/jpegといった個別のパッケージ上で実装されており、大元のimageパッケージではそれを受け取って使う仕組み(image.RegisterFormat関数)を整えているだけなのです。
このように、実際のデコード処理を別パッケージに移譲する仕組みをとった理由として考えられるのは、先ほど述べたとおり「拡張子不明の状態でもDecodeできるようなコードを書けるようにする」というだけではなく、フォーマットの統一性を保つという側面もあると推測されます。これは、image.Decode()やimage.DecodeConfig()の返り値をそれぞれimage.Imageや image.Configという型で指定することで、個別パッケージで実装されるDecode関数・DecodeConfig関数も同様のフォーマットで作られることになります。実際に、標準パッケージではない"golang.org/x/image/bmp"6では、拡張子BMPの画像をDecodeする関数を提供していますが、このDecode関数も、他同様の返り値を持ちます。こうすることで、「どんな拡張子であっても同じように扱える」という、プログラマーにとって優しい世界が構築されます。Goの標準パッケージというのは、ただ単によく使われる、もしくは基本的な機能を提供するというだけではなく、Goコミュニティの中でのものの扱い方に関する指針を固め、示す役割もあるということが、この例からよくわかりますね。
image.Imageインターフェースに元画像を変換したあとにやることは、大きく分けてふたつです。
1.新画像用のimage.Imageを用意する
2.元画像のデータを使いながら、新画像のピクセルを埋める
たとえば、冒頭に挙げたグレースケール変換の例の場合は、次のような処理になっています。
img := getImage() // 元画像のimageを用意
// 新画像のimageを用意する。
bound := img.Bounds()
NewImg := image.NewGray(bound)
// 新画像のimageを1ピクセルずつ埋める
for y := bound.Min.Y; y < bound.Max.Y; y++ {
for x := bound.Min.X; x < bound.Max.X; x++ {
NewImg.Set(x, y, img.At(x, y))
}
}
まずは、image.NewGrayでグレースケールのimageを用意します。その際に、作る画像の大きさを指定する必要があるので、元画像の大きさをBoundメソッドで取得して、それをimage.NewGrayの引数として渡しています。
その後、forループを回して、新しい画像の各ピクセルを何色にするのかをひとつひとつ指定していきます。具体的には、グレースケールimageのSetメソッドを使うことで、元画像のピクセルデータをグレースケールの色に直しています。
新しい画像のimageを作り終わったら、それをファイルに出力します。この作業のことをエンコードといいます。
デコードの際は、image.Decodeを使用することで、どの画像拡張子であったとしても一律に読み込むことができていました。ですが、エンコードにはそのような「どのフォーマットでもOK」という関数は存在しません。pngとして出力したいのならばimage/pngのpng.Encodeを、jpegとして出力したいのならばimage/jpegのjpeg.Encodeを使用します。
newfile, _ := os.Create("new.png")
defer newfile.Close()
png.Encode(newfile, NewImg)
newfile, _ := os.Create("new.jpeg")
defer newfile.Close()
jpeg.Encode(newfile, NewImg, &jpeg.Options{Quality: 75})
表1.1: jpegとpngの比較
|
jpeg | png |
色空間 | RGB, CMYK, YCbCr | RGBのみ |
圧縮方法 | 非可逆 | 可逆 |
背景 | 透明不可 | 透明可 |
jpegの特徴は、多彩な色空間と圧縮方法です。圧縮の仕方は「数ピクセルをブロックとして扱い、一番近い色ひとつに置き換える」というやり方なので、圧縮後に元に戻すことはできません。また、圧縮処理の特性上、「色の境界部分がぼやける」ような結果になります。そのため、色変化がはっきり・くっきりしている人工物の描画には不向きです。逆に、グラデーションのような色変化は扱いやすく、写真の画像はこのjpeg形式で保存されます。
pngの特徴は、透過処理と可逆圧縮が可能なことです。そのため、何回も加工するような画像や、イラスト・ロゴの保存に適しています。ただ、多彩な色を表現しようとすると、ファイルサイズが大きくなるという欠点があります。
グレースケールにした結果は次の画像のとおりです。