Help us understand the problem. What is going on with this article?

jpegのcolor modelにCMYKを使ったやつの色が変だったのでGoで機械学習して直してみた

More than 1 year has passed since last update.

この記事はLivesense - 自 Advent Calendar 2017の記事です.

私はエモい内容を書けないので普通に作った内容にします。

皆さんjpegの画像ファイルには色の保持方法が複数あることをご存知でしょうか?
一般的によく使われてるjpegファイルはYCbCrというカラーモデルが使われています。
YCbCrの説明を始めるとjpegの圧縮方式の話になるので割愛しますが気になる人はこの記事でも読んでください。
JPEG の YCbCr について

jpegはYCbCrの他にCMYKというカラーモデルで色を保持することができます。
CMYKってのは印刷とかで色を表現するときに使うやつですね。
プリンタとかのインクもCMYKになってたりします。

カラーモデルが複数あるとどういう問題が起こるかというと、表示のときに人間に見える色がずれて見えたりします。
例えばディスプレイに表示する際ですがCMYK->RGBに変換したりYCbCr->RGBに変換しています。
この時きれいに変換出来ないらしく同じ画像でもカラーモデルによって見え方が変わります。

他にもCMYK -> RGB -> YCbCrの用に変換しようとすると色が変わっちゃったりします。

ここで突然ですがGo言語の話をしましょう。
Go言語に置いてYCbCrとCMYKのカラーモデルは標準パッケージの image/color によってサポートされています。
CMYKカラーからYCbCrに変換しようとすると次のような実装になります。

func CMYKtoYCbCr(cmyk color.CMYK) color.YCbCr {
    r, g, b, _ := cmyk.RGBA()
    y, cb, cr := color.RGBToYCbCr(uint8(r >> 8), uint8(g >> 8), uint8(b >> 8))
    return color.YCbCr{
        y,
        cb,
        cr,
    }
}

CMYK構造体の RGBA() 関数は計算することによってRGBの値を求めてます。
計算してるとは言えCMYK <-> RGBの完全な変換は不可能らしいので色合いがずれてしまうようです。
func (c CMYK) RGBA() (uint32, uint32, uint32, uint32)

func (c CMYK) RGBA() (uint32, uint32, uint32, uint32) {
    w := 0xffff - uint32(c.K)*0x101
    r := (0xffff - uint32(c.C)*0x101) * w / 0xffff
    g := (0xffff - uint32(c.M)*0x101) * w / 0xffff
    b := (0xffff - uint32(c.Y)*0x101) * w / 0xffff
    return r, g, b, 0xffff
}

ちなみにこれはGo言語に限った話ではなくwebブラウザにも同じことがいえます。
FireFoxやGoogle Chromeで開くとこのように色がかわってしまいます。
ただしChrome Canaryは例外でCMYKだろうがYCbCrだろうが正常な色で表示してくれます。

左: CMYK
右: YCbCr
スクリーンショット 2017-12-15 0.09.58.png

風景画像とかだとそこまで致命的にならないですがご飯の画像だと不味そうな画像になって致命的です。
なので違和感の無い変換をしたいと思います。

方針

ニューラルネットワークを使ってCMYKの画像を違和感ない感じのRGBの色味に変換してjpegを作る。

Go言語でニューラルネットワークを扱う

gobrain をベースに使いました。
readmeに従って何も考えずに動かしたら動いたのでパッケージの使い方の説明は割愛します。
実際はそのまま使うとerror周りの戻り値が気に食わなかったので拡張して使っています。
sse命令をつかって学習をもっと高速化しようと思ってましたがデバッグしきれなかったので今回はなしです。

やったことはそこまで難しいことではなく単純な多層ニューラルネットワークです。
構造は次のような感じです。
ニューラルネットワークは入力層がC, M, Y, Kの四要素。
隠れ層が5層。
出力層がR, G, Bの三要素になっています。

また入力も出力も -1.0以上かつ1.0以下の値を取るので255で割った値を使っています。

実装はこちらのリポジトリにおいています。
ieee0824/libcmyk

学習データの用意

適当なjpeg画像を20枚くらい用意してPhotoshopのバッチモードでカラーモードをRGB(保存時にYCbCrになる)とCMYKにして保存した。
PhotoshopがなければImageMagickとかで作ってください。
人物認識の学習機を作るわけじゃないので画像を反転したりなどの水増しは必要ない。

学習は cmd/cmyk-train/train.go を使うと行えます。

$ go run cmd/cmyk-train/train.go -rgb "rgbの画像が入ったディレクトリ" -cmyk "cmykの画像が入ったディレクトリ"

cmykからYCbCrへの変換は次のようにできます。

$ go run cmd/convert/convert.go -src "変換したい画像ファイル"

実行結果

左端: CMYK
真ん中: Photoshopが作ったYCbCr
右端: 今回作ったやつが生成したYCbCr
スクリーンショット 2017-11-19 11.15.24.png

おまけ

人間遅いプログラムがあったら高速化したくなるものです。
ニューラルネットワークはfloat64型のsliceを多様しています。
なんかsse命令使ったら早くなりそうじゃないですか?
なので少しだけsse使ったコードを書いてみました。
sseの部分はアセンブラを書く時間がなかったので mengzhuo/intrinsic/sse2を利用しています。
まだデバッグできてないのでちゃんと動きません。
何故か色合いが茶色くなる。
ベンチマークもしてないので本当に早くなってるかもわからないのでそのうち調べようと思います。
本来はちゃんとベンチマークして遅いところから書き換えるべきですが今回は簡単に書き換えられそうなところから手をつけました。
ココらへんになると完全に自己満足の領域になるのでインテルの最適化マニュアルでも読みながらゆっくり直していきたいと思っています。

シグモイド関数の実装は普通に書くとこうなります。

func sigmoid(x float64) float64 {
    return 1 / (1 + math.Exp(-x))
}

これをsse化すると次のようになります。
長さ4のarrayにしているのは入力層が必ず4で決まっているためです。

func msigmoid(x [4]float64) [4]float64 {
    ret := [4]float64{1,1}
    xb := []float64{math.Exp(-x[0]), math.Exp(-x[1]), math.Exp(-x[2]), math.Exp(-x[3])}
    sse2.ADDPDm128float64(xb, []float64{1,1})
    sse2.DIVPDm128float64(ret[:], xb)
    sse2.ADDPDm128float64(xb[2:], []float64{1,1})
    sse2.DIVPDm128float64(ret[2:], xb)
    return ret
}

最後に

機械学習といえばPythonだろとかツッコミがあるとは思いますがGoでやったのは単に自分がやりたかっただけです。

Why do not you register as a user and use Qiita more conveniently?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away