OpenCV-pythonをdispatchから読み解く

はじめに
=
 - 本記事は[OpenCV Advent Calendar 2022](https://qiita.com/advent-calendar/2022/opencv) の15日目の記事です。
    - [14日目の記事](https://qiita.com/osata/items/7b4ac200309482fbf326)は[@osata](https://qiita.com/osata)さんによる[OpenCVで面積を計算してみた on iOS](https://qiita.com/osata/items/7b4ac200309482fbf326)です。是非お読み下さい。
    - 周囲長のピクセル数だけ面積の誤差が発生するから、2.7%の誤差はまぁそんなところではないか、あとは画素数増やすのが良いのではという気がしますが、iPhoneだからカメラは変わらないんだよな
 - その他の記事は[目次](https://qiita.com/advent-calendar/2022/opencv)をご覧ください。
 - アドベントカレンダーが空いていたので、しょうもない記事で埋めます。

よみとく
=
 - 今年は業務でPythonを~~使わざるを得ず~~使い、`opencv-python`も`pip`でインストールしました。~~Python大嫌いなのに~~
 - インストールされたパッケージがどんなものか、ソースコードを読まずに紐解いてみましょう。


```
> pytyon
Python 3.6.5 (v3.6.5:f59c0932b4, Mar 28 2018, 17:00:18) [MSC v.1900 64 bit (AMD64)] on win32
Type "help", "copyright", "credits" or "license" for more information.
```

 - 環境は以下のとおりです
     - Windows 11
     - Pythonは3.6.5
     - OpenCVは4.6.0
     - pipのパッケージはopencv-python 4.6.0.66
 - さて、OpenCV内部の情報を見るためには`OPENCV_DUMP_CONFIG`変数を設定します。`bash`とかなら簡単ですが、コマンドプロンプトでも可能です。

```
>set OPENCV_DUMP_CONFIG=1

>echo %OPENCV_DUMP_CONFIG%
1
```

 - `OPENCV_DUMP_CONFIG`変数の中身は空っぽでなければ何でも大丈夫です。詳しくは[何年か前の記事](https://qiita.com/tomoaki_teshima/items/a395703284d30fa6e660#opencv_dump_config)で書きました
 - さて、ここのあとPythonから`import cv2`をやってみましょう。


```
>python
Python 3.6.5 (v3.6.5:f59c0932b4, Mar 28 2018, 17:00:18) [MSC v.1900 64 bit (AMD64)] on win32
Type "help", "copyright", "credits" or "license" for more information.
>>> import cv2

OpenCV build configuration is:

General configuration for OpenCV 4.6.0 =====================================
  Version control:               4.6.0

  Platform:
    Timestamp:                   2022-06-07T10:17:42Z
    Host:                        Windows 10.0.17763 AMD64
    CMake:                       3.22.5
    CMake generator:             Visual Studio 14 2015
    CMake build tool:            MSBuild.exe
    MSVC:                        1900
    Configuration:               Debug Release

  CPU/HW features:
    Baseline:                    SSE SSE2 SSE3
      requested:                 SSE3
    Dispatched code generation:  SSE4_1 SSE4_2 FP16 AVX AVX2
      requested:                 SSE4_1 SSE4_2 AVX FP16 AVX2 AVX512_SKX
      SSE4_1 (16 files):         + SSSE3 SSE4_1
      SSE4_2 (1 files):          + SSSE3 SSE4_1 POPCNT SSE4_2
      FP16 (0 files):            + SSSE3 SSE4_1 POPCNT SSE4_2 FP16 AVX
      AVX (4 files):             + SSSE3 SSE4_1 POPCNT SSE4_2 AVX
      AVX2 (31 files):           + SSSE3 SSE4_1 POPCNT SSE4_2 FP16 FMA3 AVX AVX2

  C/C++:
    Built as dynamic libs?:      NO
    C++ standard:                11
    C++ Compiler:                C:/Program Files (x86)/Microsoft Visual Studio 14.0/VC/bin/x86_amd64/cl.exe  (ver 19.0.24245.0)
    C++ flags (Release):         /DWIN32 /D_WINDOWS /W4 /GR  /D _CRT_SECURE_NO_DEPRECATE /D _CRT_NONSTDC_NO_DEPRECATE /D _SCL_SECURE_NO_WARNINGS /Gy /bigobj /Oi  /fp:precise     /EHa /wd4127 /wd4251 /wd4324 /wd4275 /wd4512 /wd4589 /MP  /MT /O2 /Ob2 /DNDEBUG
    C++ flags (Debug):           /DWIN32 /D_WINDOWS /W4 /GR  /D _CRT_SECURE_NO_DEPRECATE /D _CRT_NONSTDC_NO_DEPRECATE /D _SCL_SECURE_NO_WARNINGS /Gy /bigobj /Oi  /fp:precise     /EHa /wd4127 /wd4251 /wd4324 /wd4275 /wd4512 /wd4589 /MP  /MTd /Zi /Ob0 /Od /RTC1
    C Compiler:                  C:/Program Files (x86)/Microsoft Visual Studio 14.0/VC/bin/x86_amd64/cl.exe
    C flags (Release):           /DWIN32 /D_WINDOWS /W3  /D _CRT_SECURE_NO_DEPRECATE /D _CRT_NONSTDC_NO_DEPRECATE /D _SCL_SECURE_NO_WARNINGS /Gy /bigobj /Oi  /fp:precise     /MP   /MT /O2 /Ob2 /DNDEBUG
    C flags (Debug):             /DWIN32 /D_WINDOWS /W3  /D _CRT_SECURE_NO_DEPRECATE /D _CRT_NONSTDC_NO_DEPRECATE /D _SCL_SECURE_NO_WARNINGS /Gy /bigobj /Oi  /fp:precise     /MP /MTd /Zi /Ob0 /Od /RTC1
    Linker flags (Release):      /machine:x64  /NODEFAULTLIB:atlthunk.lib /INCREMENTAL:NO  /NODEFAULTLIB:libcmtd.lib /NODEFAULTLIB:libcpmtd.lib /NODEFAULTLIB:msvcrtd.lib
    Linker flags (Debug):        /machine:x64  /NODEFAULTLIB:atlthunk.lib /debug /INCREMENTAL  /NODEFAULTLIB:libcmt.lib /NODEFAULTLIB:libcpmt.lib /NODEFAULTLIB:msvcrt.lib
    ccache:                      NO
    Precompiled headers:         YES
    Extra dependencies:          wsock32 comctl32 gdi32 ole32 setupapi ws2_32
    3rdparty dependencies:       libprotobuf ade ittnotify libjpeg-turbo libwebp libpng libtiff libopenjp2 IlmImf zlib quirc ippiw ippicv

  OpenCV modules:
    To be built:                 calib3d core dnn features2d flann gapi highgui imgcodecs imgproc ml objdetect photo python3 stitching video videoio
    Disabled:                    world
    Disabled by dependency:      -
    Unavailable:                 java python2 ts
    Applications:                -
    Documentation:               NO
    Non-free algorithms:         NO

  Windows RT support:            NO

  GUI:                           WIN32UI
    Win32 UI:                    YES
    VTK support:                 NO

  Media I/O:
    ZLib:                        build (ver 1.2.12)
    JPEG:                        build-libjpeg-turbo (ver 2.1.2-62)
    WEBP:                        build (ver encoder: 0x020f)
    PNG:                         build (ver 1.6.37)
    TIFF:                        build (ver 42 - 4.2.0)
    JPEG 2000:                   build (ver 2.4.0)
    OpenEXR:                     build (ver 2.3.0)
    HDR:                         YES
    SUNRASTER:                   YES
    PXM:                         YES
    PFM:                         YES

  Video I/O:
    DC1394:                      NO
    FFMPEG:                      YES (prebuilt binaries)
      avcodec:                   YES (58.134.100)
      avformat:                  YES (58.76.100)
      avutil:                    YES (56.70.100)
      swscale:                   YES (5.9.100)
      avresample:                YES (4.0.0)
    GStreamer:                   NO
    DirectShow:                  YES
    Media Foundation:            YES
      DXVA:                      YES

  Parallel framework:            Concurrency

  Trace:                         YES (with Intel ITT)

  Other third-party libraries:
    Intel IPP:                   2020.0.0 Gold [2020.0.0]
           at:                   D:/a/opencv-python/opencv-python/_skbuild/win-amd64-3.6/cmake-build/3rdparty/ippicv/ippicv_win/icv
    Intel IPP IW:                sources (2020.0.0)
              at:                D:/a/opencv-python/opencv-python/_skbuild/win-amd64-3.6/cmake-build/3rdparty/ippicv/ippicv_win/iw
    Lapack:                      NO
    Eigen:                       NO
    Custom HAL:                  NO
    Protobuf:                    build (3.19.1)

  OpenCL:                        YES (NVD3D11)
    Include path:                D:/a/opencv-python/opencv-python/opencv/3rdparty/include/opencl/1.2
    Link libraries:              Dynamic load

  Python 3:
    Interpreter:                 C:/hostedtoolcache/windows/Python/3.6.8/x64/python.exe (ver 3.6.8)
    Libraries:                   C:/hostedtoolcache/windows/Python/3.6.8/x64/libs/python36.lib (ver 3.6.8)
    numpy:                       C:/hostedtoolcache/windows/Python/3.6.8/x64/lib/site-packages/numpy/core/include (ver 1.13.3)
    install path:                python/cv2/python-3

  Python (for build):            C:/hostedtoolcache/windows/Python/2.7.18/x64/python.exe

  Java:
    ant:                         NO
    JNI:                         C:/hostedtoolcache/windows/Java_Temurin-Hotspot_jdk/8.0.332-9/x64/include C:/hostedtoolcache/windows/Java_Temurin-Hotspot_jdk/8.0.332-9/x64/include/win32 C:/hostedtoolcache/windows/Java_Temurin-Hotspot_jdk/8.0.332-9/x64/include
    Java wrappers:               NO
    Java tests:                  NO

  Install to:                    D:/a/opencv-python/opencv-python/_skbuild/win-amd64-3.6/cmake-install
-----------------------------------------------------------------

>>>
```

 - すごーく長いログが表示されました。これはOpenCVが初期化時に環境変数`OPENCV_DUMP_CONFIG`をチェックし、空でなければビルド時の情報を表示するように設定されています。
    - `cv2.getBuildInformation()` とやれば環境変数を設定しなくても情報が取れます
 - さて、肝心のdispatchは中段ぐらいに書いてあります。


```
  CPU/HW features:
    Baseline:                    SSE SSE2 SSE3
      requested:                 SSE3
    Dispatched code generation:  SSE4_1 SSE4_2 FP16 AVX AVX2
      requested:                 SSE4_1 SSE4_2 AVX FP16 AVX2 AVX512_SKX
      SSE4_1 (16 files):         + SSSE3 SSE4_1
      SSE4_2 (1 files):          + SSSE3 SSE4_1 POPCNT SSE4_2
      FP16 (0 files):            + SSSE3 SSE4_1 POPCNT SSE4_2 FP16 AVX
      AVX (4 files):             + SSSE3 SSE4_1 POPCNT SSE4_2 AVX
      AVX2 (31 files):           + SSSE3 SSE4_1 POPCNT SSE4_2 FP16 FMA3 AVX AVX2
```

 - `Baseline` : ここまでの情報はCPUでサポートされてる前提としています。
    - Intel製のCPUであれば、最近のハイエンドモデルであればだいたいAVX2までサポートしているのですが、それ以外のモデルですと一部SSE3止まりのものもあり、OpenCVのデフォルトのコンフィグではSSE3までがBaselineに設定されます。
    - `opencv-python`はどうもデフォルトの設定でビルドしているようです。
 - `requested`: このラインに書かれてるのがコンフィグ時に渡されたbaselineです。
    - `SSE`のSIMD命令は`SSE`→`SSE2`→`SSE3`と制定されましたので、`SSE3`が使えるHWの場合`SSE`と`SSE2`もサポートされるという前提のもと、前述の`Baseline`の情報が構成されます。
 - `Dispatched code generation` : ここに`dispatch`される命令の種類が書かれます。SSE4.1、SSE4.2、AVX、AVX2、およびFP16ですね。
    - なお、こいつら命令は一部包含関係になっており、それが続く行に書かれています。
 - `requested`: には`SSE4_1`、`SSE4_2`、` AVX`、`FP16`、`AVX2`、`AVX512_SKX`の5つが書かれています。
    - `AVX512_SKX`はコンフィグで指定しましたが、おそらくコンパイラが対応していないため除外されました。
 - 注目はFP16とAVXの行です。

```
FP16 (0 files):            + SSSE3 SSE4_1 POPCNT SSE4_2 FP16 AVX
AVX (4 files):             + SSSE3 SSE4_1 POPCNT SSE4_2 AVX
AVX2 (31 files):           + SSSE3 SSE4_1 POPCNT SSE4_2 FP16 FMA3 AVX AVX2
```

 - この行は左側に命令セット、右側にはその際に前提として有効化される命令セットがあります。
 - FP16を有効にした場合、AVXも有効化されますが、AVXだけを有効化した場合はFP16は有効になりません。
 - 同様にAVX2を有効にした場合はFP16、AVXに加えFMA3も有効になります。
 - 不思議なのは`FP16 (0 files)`と、0になってるのは不思議です。ここはいくつのファイルでdispatchが有効になったか、ファイル数が表示されるのですが、なんで0なのか今度詳しく追ってみましょう。
    - (追記): 追ってみたので、後述します

```
To be built:                 calib3d core dnn features2d flann gapi highgui imgcodecs imgproc ml objdetect photo python3 stitching video videoio
```

 - この行に書かれてるのはビルドされたモジュール群です。CUDA関連のモジュールも入ってないですし、そもそもcontribモジュールも入っていません。
 - contribに含まれるモジュール(CUDA関連、aruco、freetypeやsfmなど)をpythonから使いたい場合はビルドする必要があります。
   - なお、[コメントで指摘](https://qiita.com/tomoaki_teshima/items/148f4adc763fcc42abe3#comment-ba35363c53c158015e71)されたように、opencv-contrib-pythonパッケージを使うことでビルドを迂回できる場合があります。

> contribモジュールを使いたい場合はopencv-contrib-pythonパッケージを使えば良い場合もあると思います。

追記:) FP16のファイルが0個なのを深追いしてみたらやっぱり使われてなかった
==

 - 前述の通り、dispatchの項目の`FP16`には `(0 files)`と書かれています。
 - このファイルのカウントはCMakeLists.txt にかかれている拡張命令を集計したものになります

```CMakeLists.txt
ocv_add_dispatched_file(arithm SSE2 SSE4_1 AVX2 VSX3)
ocv_add_dispatched_file(convert SSE2 AVX2 VSX3)
ocv_add_dispatched_file(convert_scale SSE2 AVX2)
ocv_add_dispatched_file(count_non_zero SSE2 AVX2)
```

 - これはcoreモジュールに記載されているオプションですが、たしかにFP16というキーワードはどこにも現れていません。
 - 本来なら、`convert.dispatch.cpp`内で`FP32(float)`←→`FP16(half)`の変換が行われるため、ここにFP16と書いてあるべきですが、書いてありません。
 - 一方で、`convert.simd.hpp` を見てみると、他の型の変換もこのファイル内で行われていることがわかります。

```convert.simd.hpp
BinaryFunc getConvertFunc(int sdepth, int ddepth)
{
    static BinaryFunc cvtTab[][8] =
    {
        {
            (cvt8u), (cvt8s8u), (cvt16u8u),
            (cvt16s8u), (cvt32s8u), (cvt32f8u),
            (cvt64f8u), (cvt16f8u)
        },
        {
            :
        },
        {
            (cvt8u16f), (cvt8s16f), (cvt16u16f), (cvt16s16f),
            (cvt32s16f), (cvt32f16f), (cvt64f16f), (cvt16u)
        }
    };
```

 - この`getConvertFunc`には各型から各型への変換関数が並べられています。
    - 例えば`cvt16u8u`は、`CV_16U`を`CV_8U`へ、つまり`unsigned short`を`unsigned char`に変換します
    - そして他の変換には`AVX2`命令が使われています
 - さて、ここで不思議なことが浮かび上がります「何故FP16が書いておらず、AVX2が指定されているのか？」
    - こちらは実装の大部分が重複するためだと推測されます
    - FP16以外の変換関数はSSE2で書かれているため、FP16を指定すると、その他の関数はSSE2での実装が採用されます
    - このとき、FP16の変換関数は2つだけ、その他の変換は7種類の2乗で49種類あります。
    - 49種類の実装は単純にバイナリのサイズを肥大化させるだけですので、おそらくFP16フラグの場合の実装をスキップしてAVX2版だけにしたのだと思います
    - 一応デメリットとしてAVX2命令非対応でAVX命令とFP16命令だけに対応しているCPUでは、FP16を有効にできない[^MBP] 
 - というわけで`(0 Files)`って表記が気になったので追ってみたら、思いの外入り組んだ実装になってました、という取止めのない話
 - 追記ここまで

[^MBP]: 筆者の先代の私物ラップトップがまさにそうでした。そしてそのラップトップでこの`convertFP16`関数のPRを書いたのに巡り巡って、、、、

おわりに
=

 - というわけで、突発的にopencv-pythonのビルド情報を読み解く記事を書いて投稿しました。
 - 明日は[@okm_6880](https://qiita.com/okm_6880)さんの記事でタイトルは[上下の有るサンプルにおける回転角の取得について](https://qiita.com/okm_6880/items/655f52d56514f1e75fc9)です。お楽しみに！