GoCV でヒストグラムを描かせてみた

2022/10/04に公開

こんにちは、わたる です。

はじめに

画像認識のお勉強をしていた時のテキストに「ヒストグラムを描かせてみる」というタイトルの記事があったのですが、プログラミング言語が Python で書かれてあったので、それを Go へ移植出来ないか?ということが出発点となりトライしてみました。

考え方

ヒストグラムの作成

生成は単純に GoCV(OpenCV) にある CalcHist を使うだけです。
関数仕様は下記の通り。

func CalcHist(src []Mat, 
              channels []int,
              mask Mat, 
              hist *Mat,
              size []int,
              ranges []float64,
              acc bool)

参考:
GoCVの関数仕様
OpenCVの関数仕様

引数 src の画を入力、hist を出力にして、

mask := gocv.NewMat()
histSize := []int { math.MaxUint8 + 1 }
ranges := []float64 { 0, math.MaxUint8 + 1 }
for idx := 0; idx < 3; idx++ {
	channels := []int { idx }
	gocv.CalcHist( src, channels, mask, &hist[idx], histSize, ranges, false )
}

…こんな感じでコールすると生成できました。

グラフのプロットについて

グラフのプロットは gonum/plot を使用しました。
導入には以下のコマンドが必要です。

$ go get gonum.org/v1/plot
$ go get gonum.org/v1/plot/plotter
$ go get gonum.org/v1/plot/vg

しかしながら、plot のサンプルはほとんどすべて、と言っていいほどファイルへ落としていました。

自分としては結果を画面への表示だけがしたかったので、一度テンポラリファイルへ落としてそれを GoCV の IMRead を使って gocv.Mat オブジェクトへロードさせてからファイルを消すようにしてロードした gocv.Mat を IMShow 関数で表示させるようにしました。

実装イメージは下記になります。

var captionText [3]string = [3]string {"blue", "green", "red"}
var colorIdx [3]color.RGBA = [3]color.RGBA {color.RGBA{  0,   0, 255, 255},
                                            color.RGBA{  0, 255,   0, 255},
                                            color.RGBA{255,   0,   0, 255}}
// プロットの作成
myPlot := plot.New()
myPlot.Title.Text = "histogram"
myPlot.X.Label.Text = "X"
myPlot.Y.Label.Text = "Y"

// RGB ごとにプロット
for idx := 0; idx < 3; idx++ {
    var line plotter.XYer
    plots := make(plotter.XYs, math.MaxUint8 + 1)
    line = plots
    for i := 0; i < math.MaxUint8 + 1; i++ {
        plots[i].X = float64(i)
        plots[i].Y = float64(hist[idx].GetFloatAt(0, i))
    }
    graph, _, _ := plotter.NewLinePoints(line)
    graph.Color = colorIdx[idx]
    myPlot.Add(graph)
    myPlot.Legend.Add(captionText[idx], graph)
}

// 中間ファイルに落とす
var filename string = "/tmp/temp.png"
myPlot.Save(5*vg.Inch, 5*vg.Inch, filename)
// gocv.Mat へロードして
img := gocv.IMRead(filename, gocv.IMReadColor)
// 中間ファイルを消す
os.Remove(filename)

window := gocv.NewWindow("Histogram")
window.IMShow(img)

参考:
Go言語(Golang)で散布図・ヒストグラム作成【Gota & Gonum Plot】

Go のソース

以上を踏まえたヒストグラムを描かせるソースは下記になりました。

histogram.go
package main

import (
    "fmt"
    "os"
    "math"
    "image/color"
    "gocv.io/x/gocv"
    "gonum.org/v1/plot"
    "gonum.org/v1/plot/plotter"
    "gonum.org/v1/plot/vg"
)

func main() {
    if len(os.Args) < 2 {
        fmt.Println("How to run:\n\thistogram [imgfile]")
        return
    }

    // 元画像の取得
    filename := os.Args[1]
    img := gocv.IMRead(filename, gocv.IMReadColor)
    if img.Empty() {
        fmt.Printf("Error reading image from: %v\n", filename)
        return
    }
    // main が終わると解放するが念のため
    defer img.Close()

    // ヒストグラム生成
    var hist [3]gocv.Mat = [3]gocv.Mat { gocv.NewMat(), gocv.NewMat(), gocv.NewMat() }
    // main が終わると以下略
    defer hist[0].Close()
    defer hist[1].Close()
    defer hist[2].Close()
    createHist_RGB(img, &hist)

    // プロット
    myPlot := HistToPlot(hist)

    // 画面表示を行うため GoCV.Mat へ変換
    img_hist := PlotToImage(myPlot)
    // main が以下略
    defer img_hist.Close()

    // 画面表示
    window_before := gocv.NewWindow("original")
    window_histogram := gocv.NewWindow("histogram")
    window_before.IMShow(img)
    for {
        window_histogram.IMShow(img_hist)
        if window_histogram.WaitKey(1) >= 0 {
            window_before.Close()
            window_histogram.Close()
            break
        }
    }
}

// ヒストグラム生成関数
func createHist_RGB(img gocv.Mat, hist *[3]gocv.Mat) {
    // 元画像の GoCV.Mat 生成
    src := []gocv.Mat { img }
    // マスクはここでは未使用
    mask := gocv.NewMat()
    // defer しないとリークする?
    defer mask.Close()
    // 取りうるヒストグラムの数字は画素要素である 0 - 255 の間
    histSize := []int { math.MaxUint8 + 1 }
    ranges := []float64 { 0, math.MaxUint8 + 1 }
    // RGB の 3 プレーンでヒストグラム生成
    for idx := 0; idx < 3; idx++ {
        channels := []int { idx }
        gocv.CalcHist( src, channels, mask, &hist[idx], histSize, ranges, false )
    }
}

func HistToPlot(hist [3]gocv.Mat) *plot.Plot {
    var captionText [3]string = [3]string {"blue", "green", "red"}
    var colorIdx [3]color.RGBA = [3]color.RGBA {color.RGBA{  0,   0, 255, 255},
                                                color.RGBA{  0, 255,   0, 255},
                                                color.RGBA{255,   0,   0, 255}}
    // プロット初期化
    p := plot.New()
    p.Title.Text = "histogram"
    p.X.Label.Text = "X"
    p.Y.Label.Text = "Y"

    // RGB の各プレーンでプロット処理
    for idx := 0; idx < 3; idx++ {
        var line plotter.XYer
        plots := make(plotter.XYs, math.MaxUint8 + 1)
        line = plots
        for i := 0; i < math.MaxUint8 + 1; i++ {
            plots[i].X = float64(i)
            plots[i].Y = float64(hist[idx].GetFloatAt(0, i))
        }
        graph, _, _ := plotter.NewLinePoints(line)
        graph.Color = colorIdx[idx]
        p.Add(graph)
        p.Legend.Add(captionText[idx], graph)
    }

    return p
}

// プロットしたものを gocv.Mat へ変換する関数
func PlotToImage(myPlot *plot.Plot) gocv.Mat {
    // 中間ファイルへ保存
    var filename string = "/tmp/temp.png"
    myPlot.Save(5*vg.Inch, 5*vg.Inch, filename)
    // gocv.Mat へロード
    img := gocv.IMRead(filename, gocv.IMReadColor)
    // ロードすればファイルは要らないので随意のタイミングで消去
    defer os.Remove(filename)
    return img
}

実行させてみた結果のスクリーンショットです。

まとめ

透過色が入った画像のヒストグラムはうまく出来ない課題はあるようですが、ヒストグラムのサンプルを書いてみました。
Go 言語はそれなりに実行速度の速い言語であると認識しています。
サンプルのソースはまだ筆者の長年の癖もあってか C/C++ チックな記載になっているかもしれませんが、得た知見をどんどん記事にしたいと思います。

Discussion