C++ ベースの OpenCV プロジェクト戦闘 - 部品の自動光学検査
1. 背景
最初のタスクの背景は AOI (自動光学検査)
最も重要な目的は、前景とオブジェクトをセグメント化して分類することです。
シナリオ図:
なお、ナッツのベルトコンベア上にはフロントライトとバックライトが必要で、対象物に光が当たって初めて鮮明な画像が撮影できます。
2. 基礎知識
まず、以下の手順に分かれます。
1. ノイズ抑制(前処理)
2. 背景除去(セグメンテーション)
3. 2値化
4. 連結領域と輪郭探索アルゴリズム
- ノイズ低減アルゴリズム:
最初にメディアン フィルターを使用して塩胡椒ノイズをフィルターし、次にガウス フィルターを使用してオブジェクトのエッジをぼかします。
-
背景の除去
まず、背景の除去には減算と除算という 2 つの方式があります。
-
接続グラフ検出数
まず、接続されたドメインのタイプは 4 方向接続と 8 方向接続に分けられます。
接続グラフ検出アルゴリズムを使用すると、接続されていない各オブジェクトを異なる色に分けることができます。
3. コードの実装
1. マルチウィンドウ表示を実現
1つのウィンドウ内に複数の画像を表示したい、つまり画像のつなぎ合わせの操作を実現したい場合は、Pythonコードで実装した方が便利かもしれませんが、C++コードではクラスを定義する必要があり、実際の記述は面倒です;
class Display {
private:
int cols, rows, width, height;
String title;
vector<String> win_names;
vector<Mat> images;
Mat canvas;
public:
Display(String t, int c, int r, int flags) :title(t), cols(c), rows(r) {
height = 1080;
width = 1920;
namedWindow(title, flags);
canvas = Mat(height, width, CV_8UC3);
imshow(title, canvas);
}
int add_window(String win_name, Mat image, bool flag = true) {
win_names.push_back(win_name);
images.push_back(image);
if (flag) {
draw();
}
return win_names.size();
}
// 实现删除窗口
int delete_window(String win_name) {
int index = 0;
for (const auto& it : win_names) {
if (it == win_name) break;
index++;
}
win_names.erase(win_names.begin() + index);
images.erase(images.begin() + index);
return win_names.size();
}
void draw() {
canvas.setTo(Scalar(20, 20, 20));
int single_width = width / cols;
int single_height = height / rows;
int max_win = win_names.size() > cols * rows ? cols * rows : win_names.size();
int i = 0;
auto iw = win_names.begin();
for (auto it = images.begin(); it != images.end()&&i<max_win; it++,i++,iw++) {
String win_name = *iw;
Mat img = *it;
int x = (single_width) * (i % cols);
int y = (single_height)*floor(i * 1.0 / cols);
Rect mask(x, y, single_width, single_height);
rectangle(canvas, mask, Scalar(255, 255, 255), 9);
Mat resized_img;
resize(img, resized_img, Size(single_width, single_height));
Mat sub_canvas(canvas, mask);
if (resized_img.channels() == 1) {
cvtColor(resized_img, resized_img, COLOR_GRAY2BGR);
}
resized_img.copyTo(sub_canvas);
putText(sub_canvas, win_name, Point(50, 50), FONT_HERSHEY_COMPLEX, 2, Scalar(0, 0, 255), 3, LINE_AA);
}
imshow(title, canvas);
}
};
// 使用智能指针
shared_ptr<Display> multi_window;
int main(int argc, char** argv)
{
// 实现多窗口
String total_path = "imgpath";
String background_path = "imgpath";
Mat abc = imread(total_path, 0);
multi_window = make_shared<Display>("Review for all", 3, 2, WINDOW_NORMAL);
multi_window->add_window("ABC", abc);
multi_window->add_window("ABCC", abc);
multi_window->delete_window("ABC"); // 也支持删除窗口
multi_window->draw();
waitKey(0);
return 0;
}
2. ノイズリダクション処理
メディアンフィルター + ガウスフィルターを使用したノイズ低減方法:
Mat get_background(const Mat& bg){
Mat img;
medianBlur(bg,img,3);
GaussianBlur(bg,img,Size(3,3),0);
return img;
}
Mat smoothen_img(const Mat& noise_img){
Mat img;
medianBlur(noise_img,img,5);
GaussianBlur(img,img,Size(3,3),0);
return img;
}
3. 背景の除去
引き算と割り算の 2 つの方法に分かれています。
Mat remove_background_divide(Mat image, Mat background) {
Mat tmp;
Mat fg, bg;
image.convertTo(fg, CV_32F);
background.convertTo(bg, CV_32F);
tmp = 1 - (fg / bg);
tmp.convertTo(tmp, CV_8U, 255);
return tmp;
}
Mat remove_background_minus(Mat image, Mat background) {
return background - image;
}
結果のグラフから、分割方法は白い部分の情報をよりよく保持できるため、分割方法が選択されます。
4. 連結グラフの実現
二値化された画像は接続されたドメインに分割され、ランダムな色でマスク マップ上に描画されます。
void connection_check(Mat image) {
Mat labels;
int num = connectedComponents(image, labels);
if (num <= 1) {
cout << "No stuff detect!!" << endl;
return;
}
else
{
cout << num << " objects detected!!" << endl;
}
Mat display = Mat::zeros(image.rows, image.cols, CV_8UC3);
for (int i = 1; i < num; i++) {
Mat mask = (labels == i);
display.setTo(random_color_generator(seed), mask);
}
multi_window->add_window("Segment", display);
}
5. 接続ドメイン面積の計算
OpenCV には面積の計算も含まれています。
void connection_heavy_check(Mat image) {
Mat labels, stats, centroids;
int num =connectedComponentsWithStats(image, labels, stats, centroids);
if (num <= 1) {
cout << "No stuff detect!!" << endl;
return;
}
else
{
cout << num << " objects detected!!" << endl;
}
Mat display = Mat::zeros(image.rows, image.cols, CV_8UC3);
for (int i = 1; i < num; i++) {
// 得到连通域的质心点
Point2i pt(centroids.at<double>(i, 0), centroids.at<double>(i, 1));
// 打印标签和连通域坐标和面积
cout << "Stuff #" << i << ", Position: " << pt << " ,Area: " << stats.at<int>(i, CC_STAT_AREA) << endl;
Mat mask = (labels == i);
display.setTo(random_color_generator(seed), mask);
stringstream ss;
ss << stats.at<int>(i, CC_STAT_AREA);
putText(display, ss.str(), pt, FONT_HERSHEY_SIMPLEX, 0.5, Scalar(255, 255, 255), 2);
}
multi_window->add_window("Segment more", display);
}
6. 輪郭検出
物体の輪郭の検出を実現します。
void get_contour(Mat image) {
vector<vector<Point>> contours;
findContours(image, contours, RETR_EXTERNAL, CHAIN_APPROX_SIMPLE);
Mat display = Mat::zeros(image.rows, image.cols, CV_8UC3);
if (contours.size() == 0) {
cout << "No contour detect!!" << endl;
return;
}
else
{
cout << contours.size() << " contour detected!!" << endl;
}
for (int i = 0; i < contours.size(); i++) {
drawContours(display, contours, i, random_color_generator(seed), 2);
}
multi_window->add_window("CONTOURS", display);
}
上の結果図からわかるように、検出された輪郭には内部輪郭は含まれていません。すべての輪郭を検出したい場合は、findContours 関数の type パラメーターを RETR_LIST に変更する必要があります。
4. まとめ
このプロジェクトに関わる技術的なポイントは次のとおりです。
- マルチウィンドウ表示
- 背景の除去
- Connected Graphの実現
- 輪郭エッジ検出
また、実際の C++ コードでは、スマート ポインターなどの高度な知識も必要になります。
ビジュアル分野の比較的成熟した着陸プロジェクトとして、工業用品質検査プロジェクトは主にディープラーニングに基づいて実装されていますが、いくつかの OpenCV メソッドを習得できれば、プロジェクトの最適化にも役立ちます。