「三次元には興味がない…」と食わず嫌いして視野を狭めるのもなんなので、「鳴かぬなら鳴かせて見せようホトトギス」の精神で 写真をアニメっぽく変換するプログラムを書いてみた。
使った言語はGo言語。今回は勉強がてら気の利いたライブラリを使わず地道に1ドットずつ変換してるので、アルゴリズムを流用すれば他の言語版も簡単に作れると思う。
仕組み
こんな感じのアルゴリズムで画像を変換している。
- あらかじめ使える色を決める(今回は基本16色から灰色系を除いた14色)
- 変換元となる画像を1ドットずつ色を調べる
- 使える色の中から一番近い色に変換する
- 完成!
ようは画像を劣化させている。
色を絞ればグラディエーションが平坦になってアニメっぽくなるという算段。
一番近い色とは?
「限りなく黒に近いグレー」や「限りなく透明に近いブルー」など、世の中たくさんの色で溢れている。このような中途半端な色を白黒はっきりさせてカテゴライズするにはどうすればいいだろうか。
R(レッド),G(グリーン),B(ブルー)の三原色のレベルをX,Y,Z座標に置き換えて、3次元空間上の二点間の距離で色の近さを判断する。例えば赤色は(255, 0, 0)なので、X=255,Y=0,Z=0の座標になる。
中学校で習った平面上の二点間の距離の公式はこれ。これは自分も覚えてる。
平面上の二点間の距離=√((X1-Y1)^2+(X2-Y2)^2)
たぶん習ったと思うんだけどググるまで忘れてた三次元空間上の二点間の距離の公式はこれ。
三次元上の二点間の距離=√((X1-X2)^2+(Y1-Y2))^2+(Z1-Z2)^2)
あらかじめ用意した代表色との距離を求め、もっとも距離が近い代表色に置き換える。
今回は実装しなかったけど、「限りなく透明に近いブルー」のような透明度のある画像を考慮するにはalpha値の座標を加えて四次元空間の距離を求める必要がある。四次元空間の距離・・・なんだか急にオカルトっぽくなってきた。
ソース
具体的なソースはこちら。ジャスト100行。意外と短い。
package main
import (
"fmt"
"image"
"image/color"
_ "image/jpeg"
"image/png"
"os"
"strconv"
"time"
)
var COLOR_SET = [...]string{
"000000", "FFFFFF", //"808080", "c0c0c0",
"800000", "FF0000", "808000", "FFFF00",
"008000", "00FF00", "008080", "00FFFF",
"000080", "0000FF", "800080", "FF00FF",
}
func main() {
path := ""
start := time.Now()
if len(os.Args) >= 2 {
path = os.Args[1]
} else {
fmt.Println("コマンドライン引数に画像ファイルを指定してください")
return
}
img := getIMG(path)
newImg := convertColor(img)
saveImage(newImg)
end := time.Now()
fmt.Printf("complete! (%fs)\n", (end.Sub(start)).Seconds())
}
//画像を読み込む
func getIMG(path string) image.Image {
file, err := os.Open(path)
defer file.Close()
if err != nil {
fmt.Println(err)
os.Exit(1)
}
img, _, err := image.Decode(file)
if err != nil {
fmt.Println(err)
os.Exit(1)
}
return img
}
//画像を保存する
func saveImage(img image.Image) {
out, err := os.Create("output.png")
defer out.Close()
if err != nil {
fmt.Println(err)
os.Exit(1)
}
err = png.Encode(out, img)
if err != nil {
fmt.Println(err)
os.Exit(1)
}
}
//画像を変換する
func convertColor(img image.Image) image.Image {
rect := img.Bounds()
rgba := image.NewRGBA(rect)
conv_colors := make([][3]uint8, len(COLOR_SET))
for i := 0; i < len(COLOR_SET); i++ {
r, _ := (strconv.ParseUint(COLOR_SET[i][0:2], 16, 0))
g, _ := (strconv.ParseUint(COLOR_SET[i][2:4], 16, 0))
b, _ := (strconv.ParseUint(COLOR_SET[i][4:6], 16, 0))
conv_colors[i] = [3]uint8{uint8(r), uint8(g), uint8(b)}
}
for y := 0; y < rect.Size().Y; y++ {
for x := 0; x < rect.Size().X; x++ {
r0, g0, b0, _ := img.At(x, y).RGBA()
r, g, b := uint8(r0), uint8(g0), uint8(b0)
r, g, b = nearColor(r, g, b, conv_colors)
rgba.Set(x, y, color.RGBA{r, g, b, 255})
}
}
return rgba
}
//近い色を返す
func nearColor(r0, g0, b0 uint8, conv_colors [][3]uint8) (uint8, uint8, uint8) {
sel_r, sel_g, sel_b := uint8(0), uint8(0), uint8(0)
sel_d := 999999.0
for i := 0; i < len(conv_colors); i++ {
rx := conv_colors[i][0]
gx := conv_colors[i][1]
bx := conv_colors[i][2]
rd := (float64(r0) - float64(rx)) * (float64(r0) - float64(rx))
gd := (float64(g0) - float64(gx)) * (float64(g0) - float64(gx))
bd := (float64(b0) - float64(bx)) * (float64(b0) - float64(bx))
d := (rd + gd + bd)
if d <= sel_d {
sel_r, sel_g, sel_b,sel_d = rx, gx, bx,d
}
}
return sel_r, sel_g, sel_b
}
Githubにもあげた
https://github.com/kurehajime/imgTest/tree/qiita
応用
おまけ。
複雑な色を単純な色に変換する
という処理でアニメっぽくなることはわかった。
では逆に単純な色を複雑な色に変換する
としてみたらどうだろう。
つまりはこう。
- 画像を複数用意する(変換元画像、テクスチャ画像)
- あらかじめ使える色を決める(今回は基本16色から灰色系を除いた14色)
- 変換元となる画像を1ドットずつ色を調べる
- 使える色の中から一番近い色に変換する
- 特定の色のある座標に、テクスチャ画像から色を移す