はじめに
セマンティックセグメンテーション (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
MinGW-w64のインストール
https://qiita.com/yomei_o/items/2e706a5fa3ac5ffc3a50
WindowsでOpenCVをMinGW-w64でビルド
https://qiita.com/yomei_o/items/4d794536e24ffc12e29d
Mac
macでも以下のような感じでインストールできます。
https://qiita.com/kai_kou/items/df335eb7ee78229ee46f
セグメンテーションの学習済みONNXモデルの取得
今回はPytorchのセマンティックセグメンテーションのサイトからセマンティックセグメンテーションのモデルデータを取得します。
https://pytorch.org/hub/pytorch_vision_deeplabv3_resnet101/
Pytorchのモデルデータは、簡単にONNX形式に変換することができます。
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
#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})
画像は次のものを使用しました。
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
結果
今回は、何が物体を検出したエリアを緑色で塗りつぶすようにしてみました。
大体あっているようですが、よく見ると羊の脚の部分や犬など醜い部分が見分けられてないですね。
OpenCVを使ったセマンティックセグメンテーションのやり方でした。