Goから始める画像処理

この記事は[tomowarkar ひとりAdvent Calendar 2019](https://qiita.com/advent-calendar/2019/tomowarkar)の25日目の記事です。
# はじめに
Goで画像処理を行い、グレースケール変換、セピア色変換、ぼかしを実装していきたいと思います。

例外処理が甘いのと、パフォーマンス無視のゴリ押し実装はご愛嬌。

コードが間違っていたり、こちらの方がパフォーマンスが良いという場合はぜひ教えていただけると嬉しく思います。

# コード
### メイン処理
```go
func main() {
	filename := "hoge.jpg"
	imgSrc := readImage(filename)

	gray := gray(imgSrc)
	toPNG("gray", gray)
	sepia := sepia(imgSrc)
	toPNG("sepia", sepia)
	mosaic := shade(21, imgSrc)
	toPNG("shade", mosaic)
}
```

コード全文は長いので折りたたんで置いておきます
👇👇👇

<details><summary>コード全文</summary><div>

```go
package main

import (
	"image"
	"image/color"
	"image/jpeg"
	"image/png"
	"log"
	"math"
	"net/http"
	"os"
)

func main() {
	filename := "hoge.jpg"
	imgSrc := readImage(filename)

	gray := gray(imgSrc)
	toPNG("gray", gray)
	sepia := sepia(imgSrc)
	toPNG("sepia", sepia)
	mosaic := shade(21, imgSrc)
	toPNG("shade", mosaic)
}

func max(a, b int) int {
	if a > b {
		return a
	}
	return b
}

func min(a, b int) int {
	if b > a {
		return a
	}
	return b
}

func toPNG(name string, imgSrc image.Image) {
	wf, _ := os.Create(name + ".png")
	defer wf.Close()
	png.Encode(wf, imgSrc)
}

func gray(imgSrc image.Image) image.Image {
	w, h := imgSrc.Bounds().Dx(), imgSrc.Bounds().Dy()
	img := image.NewRGBA(image.Rect(0, 0, w, h))
	for y := 0; y < h; y++ {
		for x := 0; x < w; x++ {
			rr, gg, bb, _ := imgSrc.At(x, y).RGBA()
			v := grayRGB(rr, gg, bb)
			img.Set(x, y, color.RGBA{v, v, v, 255})
		}
	}
	return img
}

func shade(oddnum int, imgSrc image.Image) image.Image {
	if oddnum%2 == 0 || oddnum < 1 {
		log.Fatal("1より大きい奇数を入れてね")
	}
	n := oddnum / 2
	w, h := imgSrc.Bounds().Dx(), imgSrc.Bounds().Dy()
	img := image.NewRGBA(image.Rect(0, 0, w, h))
	for y := 0; y < h; y++ {
		for x := 0; x < w; x++ {
			var rr, gg, bb uint32
			var count float64
			for i := max(0, x-n); i < min(w-1, x+n); i++ {
				for j := max(0, y-n); j < min(h-1, y+n); j++ {
					r, g, b, _ := imgSrc.At(i, j).RGBA()
					rr += r
					gg += g
					bb += b
					count++
				}
			}
			img.Set(x, y, color.RGBA{f2uint8(float64(rr) / count), f2uint8(float64(gg) / count), f2uint8(float64(bb) / count), 255})
		}
	}
	return img
}

func sepia(imgSrc image.Image) image.Image {
	w, h := imgSrc.Bounds().Dx(), imgSrc.Bounds().Dy()
	img := image.NewRGBA(image.Rect(0, 0, w, h))
	for y := 0; y < h; y++ {
		for x := 0; x < w; x++ {
			rr, gg, bb, _ := imgSrc.At(x, y).RGBA()
			r, g, b := sepiaRGB(rr, gg, bb)
			img.Set(x, y, color.RGBA{r, g, b, 255})
		}
	}
	return img
}

func grayRGB(rr, gg, bb uint32) uint8 {
	m := cieXYZ(rr, gg, bb)
	return f2uint8(m)
}

func f2uint8(m float64) uint8 {
	return uint8(uint16(m+0.5) >> 8)
}

func cieXYZ(rr, gg, bb uint32) float64 {
	r := math.Pow(float64(rr), 2.2)
	g := math.Pow(float64(gg), 2.2)
	b := math.Pow(float64(bb), 2.2)
	return math.Pow(0.2125*r+0.7154*g+0.0721*b, 1/2.2)
}

func sepiaRGB(rr, gg, bb uint32) (r, g, b uint8) {
	m := cieXYZ(rr, gg, bb)
	r = f2uint8(m * 107 / 107)
	g = f2uint8(m * 74 / 107)
	b = f2uint8(m * 43 / 107)
	return
}

func readImage(path string) (imgSrc image.Image) {
	rf, err := os.Open(path)
	if err != nil {
		log.Fatal(err)
	}
	defer rf.Close()

	buffer := make([]byte, 512)
	_, err = rf.Read(buffer)
	if err != nil {
		log.Fatal(err)
	}
	cType := http.DetectContentType(buffer)
	rf.Seek(0, 0)

	switch cType {
	case "image/png":
		imgSrc, err = png.Decode(rf)
		if err != nil {
			log.Fatal(err)
		}
	case "image/jpeg":
		imgSrc, _ = jpeg.Decode(rf)
		if err != nil {
			log.Fatal(err)
		}
	default:
		log.Fatal("not defined")
	}
	return
}
```
</div></details>
👆👆👆

# オリジナル画像
![hoge.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/235435/fd0432ad-7c16-63db-d380-ebe3a23118a7.jpeg)
(https://ja.wikipedia.org/wiki/%E9%A2%A8%E6%99%AF)
# グレースケール
![gray.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/235435/1d0ed34f-ef19-8660-1a23-aa2129913214.png)
逆ガンマ補正したRGBの値に重みをつけてガンマ変換をして実装しています。
逆ガンマ変換などの細かいロジックは以下参考をご参照ください。

[R,G,B は同じ値でも明るさが違う](https://qiita.com/yoya/items/96c36b069e74398796f3#rgb-%E3%81%AF%E5%90%8C%E3%81%98%E5%80%A4%E3%81%A7%E3%82%82%E6%98%8E%E3%82%8B%E3%81%95%E3%81%8C%E9%81%95%E3%81%86)ということは普段あまり気にしていないということもあり初めて知りました。

```go
func grayRGB(rr, gg, bb uint32) uint8 {
	m := cieXYZ(rr, gg, bb)
	return f2uint8(m)
}

func f2uint8(m float64) uint8 {
	return uint8(uint16(m+0.5) >> 8)
}

func cieXYZ(rr, gg, bb uint32) float64 {
	r := math.Pow(float64(rr), 2.2)
	g := math.Pow(float64(gg), 2.2)
	b := math.Pow(float64(bb), 2.2)
	return math.Pow(0.2125*r+0.7154*g+0.0721*b, 1/2.2)
}
```
# セピア色
![sepia.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/235435/e9dfec48-6fbe-b31a-c01d-538f84f06554.png)

セピア色は先ほどのグレースケールで得た値にそれぞれ重みをつけてあげることで実装できます。

```go
func sepiaRGB(rr, gg, bb uint32) (r, g, b uint8) {
	m := cieXYZ(rr, gg, bb)
	r = f2uint8(m * 107 / 107)
	g = f2uint8(m * 74 / 107)
	b = f2uint8(m * 43 / 107)
	return
}
```
# ぼかし
### oddnum = 21
![shade.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/235435/0ad0eccf-bb1a-015f-456a-9f38d9d28788.png)
### oddnum = 101
![shade.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/235435/049e964f-3f6a-512d-a3d2-f2f19bf9302d.png)

少しゴリ押しが過ぎるぼかし処理の実装。
周囲のNマスのRGB平均をとっています。
コードを見て想像に難くないようにパフォーマンスは最悪です。

🙇‍♂️🙇‍♂️🙇‍♂️🙇‍♂️🙇‍♂️🙇‍♂️🙇‍♂️🙇‍♂️効率のいい処理方法募集中なのです🙇‍♂️🙇‍♂️🙇‍♂️🙇‍♂️🙇‍♂️🙇‍♂️🙇‍♂️🙇‍♂️

```go
func shade(oddnum int, imgSrc image.Image) image.Image {
	if oddnum%2 == 0 || oddnum < 1 {
		log.Fatal("1より大きい奇数を入れてね")
	}
	n := oddnum / 2
	w, h := imgSrc.Bounds().Dx(), imgSrc.Bounds().Dy()
	img := image.NewRGBA(image.Rect(0, 0, w, h))
	for y := 0; y < h; y++ {
		for x := 0; x < w; x++ {
			var rr, gg, bb uint32
			var count float64
			for i := max(0, x-n); i < min(w-1, x+n); i++ {
				for j := max(0, y-n); j < min(h-1, y+n); j++ {
					r, g, b, _ := imgSrc.At(i, j).RGBA()
					rr += r
					gg += g
					bb += b
					count++
				}
			}
			img.Set(x, y, color.RGBA{f2uint8(float64(rr) / count), f2uint8(float64(gg) / count), f2uint8(float64(bb) / count), 255})
		}
	}
	return img
}
```

# 終わりに
以上25日間ありがとうございました!!
[tomowarkar ひとりAdvent Calendar Advent Calendar 2019](https://qiita.com/advent-calendar/2019/tomowarkar)

# おまけ
### 回転
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/235435/b71a5e6d-9f80-a458-7465-e2953e146f01.png" width="300">  

```go
package main

import (
	"image"

	"github.com/tomowarkar/biome"
)

func main() {
	imgSrc := biome.ReadImage("hoge.jpg")
	w, h := imgSrc.Bounds().Dx(), imgSrc.Bounds().Dy()
	img := image.NewRGBA(image.Rect(0, 0, h, w))

	for y := 0; y < w; y++ {
		for x := 0; x < h; x++ {
			img.Set(x, w-y-1, imgSrc.At(y, x))
		}
	}

	biome.ToPng("huga", img)
}
```
### 鏡
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/235435/db13ba96-3d88-f450-8a8c-dfea490819c2.png" width="600">  


```go
package main

import (
	"image"

	"github.com/tomowarkar/biome"
)

func main() {
	imgSrc := biome.ReadImage("hoge.jpg")
	w, h := imgSrc.Bounds().Dx(), imgSrc.Bounds().Dy()
	img := image.NewRGBA(image.Rect(0, 0, w, h))
	for y := 0; y < h; y++ {
		for x := 0; x < w; x++ {
			if x > w/2 {
				img.Set(x, y, imgSrc.At(w-x-1, y))
			} else {
				img.Set(x, y, imgSrc.At(x, y))
			}
		}
	}
	biome.ToPng("huga", img)
}
```
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/235435/4e4cd46d-050e-83ba-b4cb-75760e467602.png" width="600">  

```go
package main

import (
	"image"

	"github.com/tomowarkar/biome"
)

func main() {
	imgSrc := biome.ReadImage("hoge.jpg")
	w, h := imgSrc.Bounds().Dx(), imgSrc.Bounds().Dy()
	img := image.NewRGBA(image.Rect(0, 0, w, h))
	for y := 0; y < h; y++ {
		for x := 0; x < w; x++ {
			if x > w/2 && y > h/2 {
				img.Set(x, y, imgSrc.At(w-x-1, h-y-1))
			} else if x > w/2 {
				img.Set(x, y, imgSrc.At(w-x-1, y))
			} else if y > h/2 {
				img.Set(x, y, imgSrc.At(x, h-y-1))
			} else {
				img.Set(x, y, imgSrc.At(x, y))
			}
		}
	}
	biome.ToPng("huga", img)
}
```
# 参考

### 一度ファイルの読み込みをしたらファイルのポインタを元に戻してあげる必要がある
`panic: runtime error: invalid memory address or nil pointer dereference`
https://www.devdungeon.com/content/working-images-go

### グレースケールのロジック
https://www.golangprograms.com/how-to-convert-colorful-png-image-to-gray-scale.html
https://qiita.com/yoya/items/96c36b069e74398796f3

### セピア変換のロジック
http://k-ichikawa.blog.enjoy.jp/etc/HP/htm/imageSepia.html

### 本家様
https://golang.org/pkg/image/