見出し画像

画像解析を高速化させるためのTipsわかりやすくまとめてみた(1)

こんにちは、こんばんは、はじめまして。株式会社アダコテックの伊部です。弊社では、製造業に向けた異常検知のAIソフトウェアを開発・提供しています。私はそこで、画像処理・機械学習のソフトウェアを開発するエンジニアをしています。

画像解析で検査を自動化する上で、画像認識の処理速度がミリセックオーダーで検査を実行するのが必須で、高速に画像処理・学習・解析をおこなうため、アダコテックでは長年にかけてそこのチューニングをしてきたので、そこらへんのエッセンスをお伝えしようと思います。

想定読者:画像処理を勉強されている方で処理時間にイライラ、高速化させたいと思っている人とか。

整数型での四捨五入について

画像フィルタ処理では、整数型の割り算がでてきます。例えば、単純平滑化(3x3)では、次のようなカーネルを用いて計算します。

画像1

フィルタ適用後の値は、3x3ピクセルの輝度値を合計して9で割った値です。ここで、9で割った値の小数点以下を切り捨ててしまうと、下図のようにちょっと暗めになってしまいます。そのため、小数点以下を切り捨てるのではなく、四捨五入するようにしましょう。

画像2

正攻法でやる場合、round関数などを使うでしょう。

int x;  // 割られる数
int y;  // 割る数

int ans = (int)std::round((double)x / (double)y);

しかし、画像フィルタのように何百回も計算する場合、double型にキャストして計算すると、時間がかかってしまいそうです。そこで、int型のみで、割り算の計算結果の四捨五入の値を計算してみます。

int型とint型の演算結果はint型になり、例えば以下のようになります。

画像4

これは、小数点以下を切り捨てた値ですね。int型の割り算の結果は、小数点以下は切り捨てられます。また、double型をint型にキャストした場合も、小数点以下は切り捨てられます。

四捨五入するには、割った値に0.5を足してから切り捨てることで計算できます。いったんdouble型に変換するならば、

int x;  // 割られる数
int y;  // 割る数

int ans = (int)((double)x / (double)y + 0.5);

となりますが、これをint型だけでやるなら、割る前に (int)(0.5 * y) すなわち y / 2 すなわち y >> 1 を足してあげればよいです。

int x;  // 割られる数
int y;  // 割る数

int ans = (x + (y >> 1)) / y;

ただし、以下のような場合は、この方法は使用できませんので、用法用量を守って正しくお使いください。
・ x + (y >> 1) がオーバーフローする可能性がある場合は使用できません。
・ x < 0 の場合は、round関数とは挙動が異なります。

ベンチマーク

こんなC++のコードで実行時間を計測してみました。cppファイルを分割しているのは、インライン展開の最適化を避けるためです。

//! @file: div_round.cpp

// double型版
int div_round_double(int x, int y)
{
  return (int)((double)x / (double)y + 0.5);
}

// int型版
int div_round_int(int x, int y)
{
  return (x + (y >> 1)) / y;
}
//! @file: benchmark.cpp

#include <iostream>

#include <chrono>

// ベンチマーク
void benchmark(int (*func)(int, int), int n, const int* x, const int* y, int* ans)
{
  auto start = std::chrono::system_clock::now();

  for (int i = 0; i < n; ++i) {
      ans[i] = func(x[i], y[i]);
  }

  auto end = std::chrono::system_clock::now();
  auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(end - start).count();
  std::cout << elapsed << std::endl;
}
//! @file: main.cpp

#include <random>

#include <algorithm>

int div_round_double(int x, int y);
int div_round_int(int x, int y);
void benchmark(int (*func)(int, int), int n, const int* x, const int* y, int* ans);

int main()
{
  static const int N = 10000000;

  std::random_device rd;
  std::mt19937 mt(rd());
  std::uniform_int_distribution<> rand256(1, 255);

  std::vector<int> x(N);
  std::vector<int> y(N);
  std::vector<int> ans(N);
  std::generate(x.begin(), x.end(), [&](){ return rand256(mt); });
  std::generate(y.begin(), y.end(), [&](){ return rand256(mt); });

  benchmark(div_round_double, N, x.data(), y.data(), ans.data());
  benchmark(div_round_int, N, x.data(), y.data(), ans.data();

  return 0;
}

実行環境:
・CPU: 8th Generation Core i7
・OS: Ubuntu (WSL2)
・Compiler1: gcc version 7.5.0 (Ubuntu 7.5.0-3ubuntu1~18.04)
・Compiler2: clang version 6.0.0-1ubuntu2
・Compile Options: -O[03] -mno-mmx -mno-sse -mno-sse2 -mno-sse3
※SIMDを使用しないオプションを追加しているのは、double型の演算でSIMD命令を使用する最適化をおこなうようになり、double型の演算のほうが速くなったりする場合もあるためです。(<del>じゃあこの記事意味ないじゃん…?</del>)

ベンチマーク結果(5試行平均):

画像4

1.1倍~1.5倍速くなりました。ちょっと微妙ですね。

次回は、これを更に高速化(SIMDで実装)してみたいと思います。

メンバー募集中です
アダコテックは上記のような画像処理技術を使って、大手メーカーの検査ラインを自動化するソフトウェアを開発している会社です。
機械学習や画像処理の内部ロジックに興味がある方、ご連絡下さい!
我々と一緒にモノづくりに革新を起こしましょう!


この記事が気に入ったら、サポートをしてみませんか?
気軽にクリエイターの支援と、記事のオススメができます!
株式会社アダコテック(https://adacotech.co.jp/)でエンジニアをしています。