OpenCV, GoCV, Go言語における画像処理のパフォーマンスの比較

f:id:ikeponsu:20190711174141j:plain Gopher's design for Ryuta Tezuka(@Tzone99

こんにちは、ZOZOテクノロジーズ開発部の池田(@ikeponsu)です。 本記事では、 Go言語における画像処理の可能性を、ベンチマークを通して探ってみたいと思います。

はじめに

業務内でGo言語での画像処理を行う機会があり、Goの標準パッケージやGoCVについて調べていました。 ただ、画像処理に関する記述はまだまだ少なく、実装している人自体も少ないのかなという印象でした。

今回行った「Go言語での画像処理の速度はどの程度か」のベンチマークが、これからGo言語で画像処理の実装を行おうとしている方の参考になればと思います。

ベンチマークの内容

比較対象

  • C++のOpenCV内のバイリニア補間
  • GoCV内のバイリニア補間
  • Go言語とimageパッケージを使って実装したバイリニア補間

処理内容

  • 画像入出力
  • バイリニア補間で画像サイズを1/2に縮小

処理枚数

以下サイトで入手した人物画像10000枚。

Labeled Faces in the Wild: http://vis-www.cs.umass.edu/lfw/

マシンスペック

Machine spec

検証の内容

C++のOpenCV内のバイリニア補間

使用したライブラリ

opencvhttps://github.com/opencv/opencv/releases

ソースコード

#include <opencv2/opencv.hpp>
#include <sys/types.h>
#include <dirent.h>
#include <string>
#include <iomanip>
#include <sstream>
#include <sys/stat.h>
#include <sys/types.h>

// 読み込む画像枚数を多めに定義
#define MAX_IMAGESIZE 10000

int main(int argc, char *argv[])
{
    std::cout<<"start"<<std::endl;

    cv::Mat search_img[MAX_IMAGESIZE];

    time_t t = time(nullptr);
 
    // 形式を変換する    
    const tm* lt = localtime(&t);
 
    // ディレクトリ名を作成
    std::stringstream s;
    s<<"20";
    s<<lt->tm_year-100;
    s<<lt->tm_mon+1;
    s<<lt->tm_mday;
    s<<"-";
    s<<lt->tm_hour;
    s<<lt->tm_min;
    s<<lt->tm_sec;

    std::string outputPath = s.str();
    outputPath = "../result/" + outputPath;

    mkdir(outputPath.c_str(), 0755);

    cv::Size size = cv::Size{0,0};
    cv::Mat output;
    
    std::chrono::system_clock::time_point  start;
    std::chrono::system_clock::time_point end;

    // 計測開始時間
    start = std::chrono::system_clock::now();

    for (int i = 0; i < MAX_IMAGESIZE; i++ ){
        
        // 画像のディレクトリ、ファイル名、拡張子を指定
        search_img[i] = cv::imread("../sample/" + std::to_string(i) + ".jpg", 1);
        // 全ての画像を(連番で)読み込み終えるとループを抜ける
        if(!search_img[i].data) break;
        cv::resize(search_img[i], output, size, 0.5, 0.5, cv::INTER_LINEAR);
        cv::imwrite(outputPath + "/" + std::to_string(i) + ".jpg", output);
    }

    // 計測終了時間
    end = std::chrono::system_clock::now();
    // 処理に要した時間をミリ秒に変換
    double elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(end-start).count();

    std::cout<<elapsed / 1000 << "s" <<std::endl;
    
    return 0;
}

検証結果

1回目計測:11.383s

2回目計測:11.303s

3回目計測:11.541s

GoCV内のバイリニア補間

使用したライブラリ

gocvhttps://github.com/hybridgroup/gocv

ソースコード

package main

import (
    "fmt"
    "gocv.io/x/gocv"
    "image"
    "image/jpeg"
    "os"
    "path/filepath"
    "strconv"
    "time"
)

// main処理
func main() {

    count := 0

    datapath := filepath.Join("入力先のパス", "*.jpg")

    file, _ := filepath.Glob(datapath)
    var d float64 = 1/2
    size := image.Point{0,0}
    timeLayout := time.Now()
    timeString := timeLayout.Format("20060102150405")
    d_path := filepath.Join("出力先のパス", timeString)
    if err := os.Mkdir(d_path, 0777); err != nil {
        fmt.Println(err)
    }

    start := time.Now()

    for _, item := range file{
        exc(item, d, size, timeString, count)
        count++
    }
    end := time.Now()
    fmt.Println("A.Total time: ", end.Sub(start))

}

func exc(item string, d float64, size image.Point, timeString string, count int) {
    img := gocv.IMRead(item, gocv.IMReadColor)

    rowSize := int(float64(img.Rows()) * d)
    colSize := int(float64(img.Cols()) * d)
    outputImg := gocv.NewMatWithSize(rowSize, colSize, gocv.IMReadGrayScale)
    gocv.Resize(img, &outputImg, size, 0.5, 0.5, gocv.InterpolationLinear)
    image, _ := outputImg.ToImage()
    path := filepath.Join("出力先のパス", timeString, strconv.Itoa(count) + ".jpg")

    qt := jpeg.Options{
        Quality:60,
    }
    file, _ := os.Create(path)
    jpeg.Encode(file, image, &qt)
}

検証結果

1回目計測:14.980s

2回目計測:14.841s

3回目計測:14.823s

Go言語とimageパッケージを使って実装したバイリニア補間

使用したライブラリ

imagehttps://golang.org/pkg/image/

ソースコード

  • 画像入力
func Input (filePath string) image.Image {

    file, err := os.Open(filePath)
    if err != nil {
        log.Fatal(err)
    }

    pngImage, _, err := image.Decode(file)
    
    return pngImage
}
  • バイリニア補間
func Bilinear(inputImage image.Image, f float64) image.Image {
    // 重み値を定義
    var x float64
    var y float64

    // リサイズ後
    size := inputImage.Bounds()
    size.Max.X = int(float64(inputImage.Bounds().Max.X) * f)
    size.Max.Y = int(float64(inputImage.Bounds().Max.Y) * f)

    // 逆数
    reciprocalScalingRows := 1 / f
    reciprocalScalingCols := 1 / f

    // アウトプット画像を定義
    outputImage := image.NewRGBA(size)

    var outputColor color.RGBA64

    // 画像の左上から順に画素を読み込む
    for imgRows := 0; imgRows < size.Max.Y; imgRows++ {
        for imgCols := 0; imgCols < size.Max.X; imgCols++ {

            // 双一次補完式

            // 元画像の座標定義
            // 元画像の縦の座標
            inputRows := int(float64(imgRows) * reciprocalScalingRows)
            // 元画像の横の座標
            inputCols := int(float64(imgCols) * reciprocalScalingCols)

            // 補完式で使う元画像のpixel
            // point(0, 0)
            src00 := inputImage.At(inputCols, inputRows)
            // point(0, 1)
            src01 := inputImage.At(inputCols + 1, inputRows)
            // point(1, 0)
            src10 := inputImage.At(inputCols, inputRows + 1)
            // point(1, 1)
            src11 := inputImage.At(inputCols + 1, inputRows + 1)

            // 重み値を算出
            x = float64(imgCols) * reciprocalScalingCols
            y = float64(imgRows) * reciprocalScalingRows
            // 小数点以下を抽出
            x = x - float64(int(x))
            y = y - float64(int(y))

            r00, g00, b00, a00 := src00.RGBA()
            r01, g01, b01, _ := src01.RGBA()
            r10, g10, b10, _ := src10.RGBA()
            r11, g11, b11, _ := src11.RGBA()

            // 拡大後の画素を算出
            outputColor.R = uint16((1 - x) * (1 - y) * float64(r00))
            outputColor.G = uint16((1 - x) * (1 - y) * float64(g00))
            outputColor.B = uint16((1 - x) * (1 - y) * float64(b00))


            outputColor.R += uint16(x * (1 - y) * float64(r01))
            outputColor.G += uint16(x * (1 - y) * float64(g01))
            outputColor.B += uint16(x * (1 - y) * float64(b01))

            outputColor.R += uint16((1 - x) * y * float64(r10))
            outputColor.G += uint16((1 - x) * y * float64(g10))
            outputColor.B += uint16((1 - x) * y * float64(b10))

            outputColor.R += uint16(x * y * float64(r11))
            outputColor.G += uint16(x * y * float64(g11))
            outputColor.B += uint16(x * y * float64(b11))

            outputColor.A = uint16(a00)


            outputImage.Set(imgCols, imgRows, outputColor)
        }
    }

    return outputImage
}
  • main
package main

import (
    "fmt"
    "image/jpeg"
    "img-test/ioimg"
    "img-test/procimg"
    "os"
    "path/filepath"
    "strconv"
    "sync"
    "time"
)

func main() {

    count := 0

    datapath := filepath.Join("入力先のパス", "*.jpg")

    file, _ := filepath.Glob(datapath)

    timeLayout := time.Now()
    timeString := timeLayout.Format("20060102150405")
    d_path := filepath.Join("data", "result", timeString)
    if err := os.Mkdir(d_path, 0777); err != nil {
        fmt.Println(err)
    }

    start := time.Now()

    for _, item := range file{
        exc(item, timeString, count)
        count++
    }

    end := time.Now()
    fmt.Println("B.Total time: ", end.Sub(start))

}

func exc(item string, timeString string, count int) {

    img := ioimg.Input(item)

    rimg := procimg.Bilinear(img, 0.5)

    path := filepath.Join("出力先のパス", timeString, strconv.Itoa(count) + ".jpg")

    qt := jpeg.Options{
        Quality:60,
    }
    file, _ := os.Create(path)
    jpeg.Encode(file, rimg, &qt)
}

検証結果

1回目計測:35.220s

2回目計測:35.162s

3回目計測:35.238s

goroutineで実装した場合

先程比較したGo言語のソースコードの処理をgoroutineで書いた場合、実装前に比べどの様な差があるか気になったので、追加で検証してみました。

GoCV内のバイリニア補間

ソースコード

  • main
package main

import (
    "fmt"
    "gocv.io/x/gocv"
    "image"
    "image/jpeg"
    "os"
    "path/filepath"
    "strconv"
    "sync"
    "time"
)

func main() {

    count := 0

    datapath := "Data/sample/*.jpg"

    file, _ := filepath.Glob(datapath)
    var d float64 = 1/2
    size := image.Point{0,0}
    timeLayout := time.Now()
    timeString := timeLayout.Format("20060102150405")
    d_path := filepath.Join("Data", "result", timeString)
    if err := os.Mkdir(d_path, 0777); err != nil {
        fmt.Println(err)
    }

    start := time.Now()
    var wg sync.WaitGroup

    for _, item := range file{
        wg.Add(1)

        go func(item string, d float64, size image.Point, timeString string, count int) {
            defer wg.Done()
            exc(item, d, size, timeString, count)
        }(item, d, size, timeString, count)
        count++
    }

    wg.Wait()
    end := time.Now()

    fmt.Println("A.Total time: ", end.Sub(start))

}

検証結果

1回目計測:3.253s

2回目計測:3.299s

3回目計測:2.975s

Go言語とimageパッケージを使って実装したバイリニア補間

ソースコード

  • main
package main

import (
    "fmt"
    "image/jpeg"
    "img-test/ioimg"
    "img-test/procimg"
    "os"
    "path/filepath"
    "strconv"
    "sync"
    "time"
)

func main() {

    count := 0

    datapath := "Data/sample/*.jpg"

    file, _ := filepath.Glob(datapath)

    timeLayout := time.Now()
    timeString := timeLayout.Format("20060102150405")
    d_path := filepath.Join("data", "result", timeString)
    if err := os.Mkdir(d_path, 0777); err != nil {
        fmt.Println(err)
    }

    start := time.Now()
    var wg sync.WaitGroup

    for _, item := range file{

        wg.Add(1)
        go func(item string, timeString string, count int) {
            defer wg.Done()
            exc(item, timeString, count)

        }(item, timeString, count)
        count++
    }

    wg.Wait()

    end := time.Now()
    fmt.Println("B.Total time: ", end.Sub(start))
}

検証結果

1回目計測:8.989s

2回目計測:9.118s

3回目計測:9.192s

まとめ

今回、比較対象とした3つのソースコードでの処理速度差ですが、多少の処理内容の差や自作コードのチューニング不足によるところもあると思います。 ただ、ベンチマークを行うことで新たな発見もありましたので、あくまで参考の一部としてご確認いただければと思います。

処理 1回目(s) 2回目(s) 3回目(s)
C++のOpenCV内のバイリニア補間 11.383 11.303 11.541
GoCV内のバイリニア補間 14.980 14.841 14.823
Go言語とimageパッケージを使って実装したバイリニア補間 35.220 35.162 35.238
【goroutine】GoCV内のバイリニア補間 3.253 3.299 2.975
【goroutine】Go言語とimageパッケージを使って実装したバイリニア補間 8.989 9.118 9.192

「Go言語とimageパッケージを使って実装したバイリニア補間」のソースコードは以下にアップしていますので、良ければご覧ください。

github.com

また、Imageパッケージを使って実装した画像処理のその他のアルゴリズムも、これからアップしていく予定です。

最後に

ZOZOテクノロジーズでは、一緒にサービスを作り上げてくれる方を募集中です。 ご興味のある方は、以下のリンクからぜひご応募ください。

tech.zozo.com

カテゴリー