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/ マシンスペック 検証の内容 C++のOpenCV内のバイリニア補間 使用したライブラリ opencv : https://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(), 0 755 ); 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内のバイリニア補間 使用したライブラリ gocv : https://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パッケージを使って実装したバイリニア補間 使用したライブラリ image : https://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