OpenCVのDNNモジュールでセマンティックセグメンテーションをする(C++編)

# はじめに

セマンティックセグメンテーション (Semantic Segmentation)は、デジタル画像を複数の画像セグメント (画像領域または画像オブジェクトとも呼ばれる) に分割するプロセスです。
セグメンテーションの目的は、画像の表現を簡略化して、より意味があり分析しやすいものにすることです。
たとえば、自動運転車は車両、歩行者、交通標識、歩道、その他の道路の特徴を識別する必要があります。

今回はこのセマンティックセグメンテーションをOpenCVのDNNモジュールを使いやってみたいと思います。

# 準備
OpenCVを使うには事前にcmakeやC++コンパイラ、OpenCVがインストールされていないといけません。

### Linux
Linuxではapt-getでインストールできます。

```
apt-get install cmake make g++ gcc libopencv-dev
```

### Windows
Windowsでビルドするには以下のサイトをみてcmakeやC++コンパイラやOpenCVを準備をしてください。

WindowsにCmakeを導入
[https://qiita.com/matskeng/items/c466c4751e1352f97ce6](https://qiita.com/matskeng/items/c466c4751e1352f97ce6)

MinGW-w64のインストール
[https://qiita.com/yomei_o/items/2e706a5fa3ac5ffc3a50](https://qiita.com/yomei_o/items/2e706a5fa3ac5ffc3a50)

WindowsでOpenCVをMinGW-w64でビルド
[https://qiita.com/yomei_o/items/4d794536e24ffc12e29d](https://qiita.com/yomei_o/items/4d794536e24ffc12e29d)
  
### Mac
macでも以下のような感じでインストールできます。
[https://qiita.com/kai_kou/items/df335eb7ee78229ee46f](https://qiita.com/kai_kou/items/df335eb7ee78229ee46f)

# セグメンテーションの学習済みONNXモデルの取得

今回はPytorchのセマンティックセグメンテーションのサイトからセマンティックセグメンテーションのモデルデータを取得します。

[https://pytorch.org/hub/pytorch_vision_deeplabv3_resnet101/](https://pytorch.org/hub/pytorch_vision_deeplabv3_resnet101/)

Pytorchのモデルデータは、簡単にONNX形式に変換することができます。


```python
import torch.onnx 
import torch

model = torch.hub.load('pytorch/vision:v0.10.0', 'deeplabv3_resnet50', pretrained=True)
# or any of these variants
# model = torch.hub.load('pytorch/vision:v0.10.0', 'deeplabv3_resnet101', pretrained=True)
# model = torch.hub.load('pytorch/vision:v0.10.0', 'deeplabv3_mobilenet_v3_large', pretrained=True)

model.eval() 

onnx_file = "deeplabv3_resnet50.onnx"

dummy_input = torch.randn(1, 3, 512, 512)
output = model(dummy_input)

torch.onnx.export(
    model=model,  # モデルの指定
    args=(torch.randn((1, 3, 512, 512))),
    f=onnx_file,  # 出力ファイル名
    export_params=True,
    opset_version=17,  # opsetのバージョン指定
    input_names=["input"],  # 入力ノード名
    output_names=["output"],  # 出力ノード名
)
```
# プログラムの作成


適当なディレクトリに「CMakeLists.txt」と「opencv_deeplabv3.cpp」のファイルを置きます。

opencv_deeplabv3.cpp
```c++
#include<opencv2/opencv.hpp>

static int inputWidth = 512;
static int inputHeight = 512;

constexpr int colorList[8][3] = {
    {255, 0, 0},
    {0, 255, 0},
    {0, 0, 255},
    {0, 255, 255},
    {255, 0, 255},
    {255, 255, 0},
    {255, 255, 255},
    {0, 0, 0} };


void tensor_to_mat(float* p, cv::Mat& map)
{
    unsigned int count = 0;
    map = cv::Mat::zeros(inputHeight, inputWidth, CV_8U);

    for (int i = 0; i < inputHeight * inputWidth; i++)
    {
        const unsigned char val = p[i];
        const int y = count / inputWidth;
        const int x = count % inputWidth;
        map.at<unsigned char>(y, x) = val;
        count++;
    }
}


void normalize(const cv::Mat& inputImage, cv::Mat& outputImage)
{
    const cv::Scalar imgMean = cv::Scalar(123.675, 116.28, 103.53);
    const cv::Scalar imgStd = cv::Scalar(58.395, 57.12, 57.375);
    inputImage.convertTo(outputImage, CV_32F);
    float* d = (float*)outputImage.data;
    float c;
    for (int i = 0; i < outputImage.rows; i++)
    {
        for (int j = 0; j < outputImage.cols; j++)
        {
            for (int k = 0; k < outputImage.channels(); k++)
            {
                c = inputImage.at<cv::Vec3b>(i, j)[k];
                c -= (float)imgMean(k);
                c /= (float)imgStd(k);
                d[k * 512 * 512 + i * 512 + j] = c;
            }

        }
    }
}

constexpr int numClasses = 4;
constexpr int color_list[numClasses][3] = {
    {255, 0, 0},
    {0, 255, 0},
    {0, 0, 255},
    {255, 255, 0}
};

void drawSegmentationMap(const cv::Mat& input, const cv::Mat& segmentation, cv::Mat& output)
{
    cv::Mat resizedSegmentation;
    cv::resize(segmentation, resizedSegmentation, cv::Size(input.cols, input.rows), 0, 0, cv::INTER_NEAREST);

    cv::Mat overlay = cv::Mat::zeros(input.rows, input.cols, CV_8UC3);
    for (int classId = 0; classId < numClasses; classId++)
    {
        cv::Mat mask;
        cv::inRange(resizedSegmentation, classId, classId, mask);

        overlay.setTo(
            cv::Scalar(color_list[classId][0], color_list[classId][1], color_list[classId][2]), mask);
    }

    const float alpha = 0.5;
    cv::addWeighted(input, alpha, overlay, 1 - alpha, 0, output);
}


int main()
{

    
    cv::Mat image_bgr = cv::imread("deeplab1.png");
    cv::Mat image_rgb;
    cv::Mat image_n;
    cv::cvtColor(image_bgr,image_rgb, cv::COLOR_BGR2RGB);
    cv::resize(image_rgb, image_rgb, cv::Size(inputWidth, inputHeight));
    normalize(image_rgb,image_n);
    int size_2[4] = { 1,3,inputHeight,inputWidth };
    cv::Mat inpBlob(4, size_2, CV_32FC1, image_n.data);
    
    cv::dnn::Net net = cv::dnn::readNetFromONNX("deeplabv3_resnet50.onnx");
    net.setInput(inpBlob);


    cv::Mat out;
    out=net.forward();

    float* v = (float*)out.data;
    float* mm = new float[512 * 512];
    float m, aa;
    for (int y = 0; y < 512; y++)for (int x = 0; x < 512; x++) {
        m = 0;
        aa = -10000;
        for (int i = 0; i < 21; i++) {
            //printf("%d  %f\n", i, v[512 * 512 * i + 512 * y + x]);
            if (v[512 * 512 * i + 512 * y + x] > aa) {
                aa = v[512 * 512 * i + 512 * y + x];
                m = i;
            }
        }
        mm[y * 512 + x] = m ? 1 : 0;
        //if (y % 4 == 0 && x % 4 == 0)printf("%d,", (int)m);
    }

    cv::Mat output;
    tensor_to_mat(mm, output);
    delete[] mm;

    cv::Mat outputImage;
    drawSegmentationMap(image_bgr, output, outputImage);

    cv::imwrite("output.bmp", outputImage);
    return true;
}
```

CMakeLists.txt
```
cmake_minimum_required(VERSION 3.13)
project(test_cmake CXX C)

find_package(OpenCV REQUIRED)

add_executable(test opencv_deeplabv3.cpp)

target_include_directories(test PRIVATE ${OpenCV_INCLUDE_DIRS})
target_link_libraries (test PRIVATE gdi32 comdlg32 ${OpenCV_LIBS})
```
画像は次のものを使用しました。

deeplab1.png
![deeplab1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3914335/3727fb2e-8529-0f11-9512-e7d684d799d9.png)

OpenCVのDNNモジュールの使い方は解説するまでもなくとてもシンプルです。
まず、以下のようにネットワークモデルを読み込みます。
```
net=cv::dnn::readNetFromONNX("deeplabv3_resnet50.onnx");
```
次に、イメージをfloatの配列に変換します。
```
blob=cv::dnn::blobFromImage(img);
```

データをネットにセットします。
```
net.setInput(blob);
```

そして、推論の実行をします。
```
cv::Mat output = net.forward();
```

# ビルド

LinuxやMacでは先ほど使ったディレクトリでシェルから以下のコマンドを実行すればプログラムがビルドされます。

```
mkdir build
cd build
cmake ..
make
```

Windowsでビルドする場合はこんな感じでビルドします。

```
mkdir build
cd build
set OpenCV_DIR=C:\prog\opencv-4.10.0\build\install
cmake -DCMAKE_CXX_COMPILER=x86_64-w64-mingw32-g++ -DCMAKE_C_COMPILER=x86_64-w64-mingw32-gcc -G "MinGW Makefiles" ..
make
```

# 結果

今回は、何が物体を検出したエリアを緑色で塗りつぶすようにしてみました。

![deeplab1_output_opencv.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3914335/63bcf918-8219-82b2-48f3-92e04923997d.jpeg)

大体あっているようですが、よく見ると羊の脚の部分や犬など醜い部分が見分けられてないですね。
OpenCVを使ったセマンティックセグメンテーションのやり方でした。