はじめに
この記事ではリアルタイム・ビジュアルフィードバック技術で制御するロボットシステムの「目」となるカメラ周辺の設定について、前回記事の補足の2つめとして書きます。
画像認識ではツールやワークを識別するために、カメラで取得した画像の中から対象物を色で検出することがあります。検出する対象物の情報を中心に検出上限、検出下限の値を決めますが、この値を適切に決めることで対象物の正しい選択や安定した認識が行えます。
ここではチトセロボティクス社「クルーボ」のチュートリアルに含まれる内容を題材に、カラーピッカーとして使用するソフトウェアを作成し、設定の手順を紹介します。
(例)次の画像では、検出対象となっているピクセルに「緑」を描画しています。
・対象としてコンベア上のワークを選択、検出範囲が適切に調整された状態
・対象としてロボットアームのツールを選択、検出範囲が適切に調整されていない状態(これを、目標物のみ検出して緑色がつくように調整します。)
動作環境
環境はチトセロボティクス社の「クルーボ (Crewbo)」です。
Ubuntu22上でロボット制御用ライブラリを含むCPPの開発環境が用意されており、OpenCVを使って画像を取り扱っています。
Crewbo環境以外でも同様のことは可能ですが、ここでは「クルーボ」のTutorialを進める上でのWebカメラの調整としてCrewbo環境での内容を記述します。
クルーボではIntel社のDepthカメラで深度画像を扱うRealSenseクラスも用意されていますが、ここではUVCでWebカメラを扱うUsbCameraクラスを使用しています。
クルーボについて
チトセロボティクス社の製品「クルーボ」は、産業用として利用されている6軸アームを動かすロボットのビジュアルフィードバック制御のコントローラです。製品の詳細は下記のチトセロボティクス社のサイトで見ることができます。
■内容物の写真やTutorialで提供されているソフトウェアの掲載、内容の改変、改変したソフトウェアの掲載はチトセロボティクス社から了解を得ています。
関連記事
この記事の元になった前回の記事はこちらです。2台のWebカメラを使用してキャリブレーションや位置決めなしでロボットアーム先端のピンをワークのパイプにインサートする作業です。
第一回の記事はこちらです。ロボットアームにマーカーを取り付け、カメラで検出したマーカーの位置をカメラ画像上でクリックした位置に動かしました。全体の構成、ロボットやカメラの接続、クルーボを使った基本的な開発の進め方についてはこちらの記事を参照してください。
画像認識に使用するカメラの機種によってコントロールできる項目が異なるため、カメラごとに設定可能な項目を調べてトラックバーの内容を入れ替え、調整ができるように変更する手順を紹介しました。
今回作成するソフトウェア
今回作成するソフトウェアは、カメラ画像をもとに対象物を識別するためのデータを取得するツールです。
具体的には、画像内で対象箇所をいくつかクリックしてHSV値を取得し、それらの情報を基にトラックバーを操作して識別範囲を調整します。この調整によって、HSV値の上限値と下限値を簡単に設定することができます。
-
画像内のクリック操作
画像の任意の場所を複数クリックすることで、その位置のHSV平均値が計算されます。クリックしたポイントは逐次記録され、BackSpaceキーで測定点をひとつずつ削除することも可能です。 -
トラックバーでの調整
別のコントロールウインドウに表示されるトラックバーを操作して、H(色相)、S(彩度)、V(明度)のそれぞれに対して最小値(min)と最大値(max)を設定します。トラックバーの調整により、検出範囲をリアルタイムで視覚的に確認しながら範囲を絞り込むことが可能です。 -
データの利用
最終的に設定したHSV値の上限値と下限値を、実際の作業プログラムの画像認識モジュールに適用することで、対象物を正確に検出することができます。
このソフトウェアは、対象物の検出条件を直感的に調整できるため、画像認識モジュールで認識する対象の設定や対象物の安定した検出が可能かどうかを検証するのに役立ちます。
・記事の末尾にあるソースコードをチュートリアルの事例にならってCrewboのプロジェクト内のsrc/main.cppに貼り付けます。
カメラの調整
カラーピッカーを使用する前に、Webカメラの機種ごとの調整を行います。前回記事を参考に、使用するカメラで操作できる調整項目と設定値を取得してください。
https://qiita.com/mshioji/items/4b8419f14b9ece495408
対象のカメラを選択します。UsbCameraのカメラIDで切り替えますので、調整するカメラを有効にして、それ以外をコメントアウトします。
・cameraAutoAdjustment()でカメラの自動調整機能のON/OFFを設定します。
・cameraPropertySetting()で該当のカメラで有効な調整値を設定します。
カラーピッカーの起動~検出範囲の取得~設定までの流れ
コードの準備ができたらビルドして実行します。
VS Codeの「ターミナル」メニューから「ビルド タスクの実行」を選択するか、Ctrl+Shift+Bを押して、表示されるメニューから「このソースファイルをビルドして実行する」を選択します。起動してカメラが接続されると、カメラ画像のウインドウとコントロールウインドウが開きます。
検出範囲の取得
1. オレンジ色の「ワーク」の検出範囲を取得
マウスでカメラ画像の中のオレンジ色の「ワーク」をクリックすると、その測定ポイントのHSV値を取得して、上下にトレランスを持たせて下限の設定値(Lower Bound HSV)と上限の設定値(Upper Bound HSV)をセットし、その条件で識別対象となるピクセルの位置を緑色で表示します。
この状態で対象物が上手く検出できていれば、「ワーク」側の下限の設定値(Lower Bound HSV)と上限の設定値(Upper Bound HSV)を控えておきます。
2. ブルーの「ツール」の検出範囲を取得
次に、カメラ画像の中のブルーの「ツール」の設定値を取得します。
・先ほどオレンジのワークをクリックして測定したので、測定ポイントが残っています。キーボードのBackSpaceキーを押して測定ポイントを削除します。緑色の検出範囲が消えた状態になります。
この状態でこちらも対象物が上手く検出できていれば、「ツール」側の下限の設定値(Lower Bound HSV)と上限の設定値(Upper Bound HSV)を控えておきます。
3. 作業プログラムへの設定
チュートリアルの中で実際に作業を行う「インサートタスク」のソフトウェアに、ワークの検出範囲とツールの検出範囲をそれぞれ設定します。
従来と同様にcrewbo createコマンドで新規プロジェクトを作成し、examples/tutorial/melfa
フォルダの中からmelfa_5_2_vf_for_insertion_task.cpp
の内容をコピーしてsrc/main.cpp
にペーストします。
main()の中に、alighTwoPointsPosition()
やalighTwoPointsRotationB()
、alighTwoPointsRotationA()
、そしてalighTwoPointsPositionInsertion()
の各ファンクションがあり、ロボットの動作はこの順にアームを動かしていきます。
このTutorialのコードでは、これらの各ファンクションの中にそれぞれワークの検出範囲を設定する部分があります。それぞれのファンクション(4種類)に先ほど取得した上限値と下限値を設定していきます。
上記の中で、maybe_hole_work
にオレンジの「ワーク」のHSV上限値と下限値、maybe_peg_work
にはブルーの「ツール」のHSV上限値と下限値をそれぞれ設定するように変更します。
detectworkDirectedlineSegment()
の第2引数にHSV下限値、第3引数にHSV上限値をセットします。
この変更により、実際にロボットを設置した場所で実際に使用するカメラを使用して取得した検出上限値と検出下限値で画像を認識し、その認識結果に基づいてロボットを動作させることができるようになります。
カラーピッカーの操作の詳細
大まかな流れは前述のとおりですが、カラーピッカーを使って上限値と下限値を取得する際の細かい操作を紹介します。
先の大まかな流れの解説の中ではマウスクリックだけで設定値を取得していましたが、マウスクリックした後に画像を見ながらControl Windowのスライドバーを使ってリアルタイムにHSVの検出範囲を変更して検出状態を確認することができます。
測定ポイントの追加と削除
マウスで複数ポイントをクリックしたときは、各ポイントのHSV平均値を表示して、それを中心にトレランスを持って上限と下限を自動設定します。
クリックのたびに測定ポイントを追加しますが、不要なポイントを読み込んだ場合はBackspaceキーでポイントを削除することができます。
検出範囲の変更
Control WindowにH(色相),S(彩度),V(明度)それぞれの最小値/最大値をセットするスライドバーが表示されています。先の操作で測定ポイントをセットすると一旦検出範囲は自動設定されますが、このスライドバーで検出範囲を変更することで自由に設定値を変更して検出範囲を確認することができます。
最小値/最大値をひとつのスライドバーで範囲指定できればよいのですが、今回使用したスライドバーでは範囲指定ができないので2つ使用しています。
2本のスライドバーの設定で対象の検出範囲が存在しない場合は検出できませんのでご注意ください。
検出範囲設定の事例
スライドバーの設定によって、色々な検出の組み合わせができますので試してみてください。
- オレンジのワークを検出
- ブルーのツールを検出
- 検出範囲を拡大
- 更に検出範囲を拡大して画像全域を検出対象とした場合
- 色相を全域として彩度の高いものだけを検出対象とした場合
(オレンジとブルーの両方を検出)
動画
ロボット動作中のカメラ画像(PC画面)
コード
#include <iostream>
#include <opencv2/opencv.hpp>
#include "crewbo/crewbo.h"
struct MouseParameter {
int u;
int v;
bool click = false;
};
void mouseCallback(int event, int x, int y, int flags, void* user_data) {
(void)flags;
if (event == cv::EVENT_LBUTTONDOWN) {
MouseParameter* mouse_parameter = static_cast<MouseParameter*>(user_data);
mouse_parameter->u = x;
mouse_parameter->v = y;
mouse_parameter->click = true;
}
}
class Trackbar {
public:
Trackbar(
const std::string& trackbar_name,
const std::string& window_name,
const int upper_limit_position,
const int initial_position)
: trackbar_name_(trackbar_name), window_name_(window_name), position_(initial_position) {
cv::createTrackbar(trackbar_name, window_name, &(this->position_), upper_limit_position);
}
int getPosition() const { return position_; }
private:
std::string trackbar_name_;
std::string window_name_;
int position_;
};
std::vector<std::shared_ptr<Trackbar>> createTrackbars(const cv::String& window_name) {
std::vector<std::shared_ptr<Trackbar>> trackbars;
trackbars.push_back(std::make_shared<Trackbar>("H-min", window_name, 179, 79));
trackbars.push_back(std::make_shared<Trackbar>("H-max", window_name, 179, 99));
trackbars.push_back(std::make_shared<Trackbar>("S-min", window_name, 255, 107));
trackbars.push_back(std::make_shared<Trackbar>("S-max", window_name, 255, 147));
trackbars.push_back(std::make_shared<Trackbar>("V-min", window_name, 255, 107));
trackbars.push_back(std::make_shared<Trackbar>("V-max", window_name, 255, 147));
return trackbars;
}
void updateTrackbarsWithAvg(cv::Scalar hsv_avg, int h_tolerance, int s_tolerance, int v_tolerance) {
int h_min = std::max(0, static_cast<int>(hsv_avg[0] - h_tolerance));
int h_max = std::min(179, static_cast<int>(hsv_avg[0] + h_tolerance));
int s_min = std::max(0, static_cast<int>(hsv_avg[1] - s_tolerance));
int s_max = std::min(255, static_cast<int>(hsv_avg[1] + s_tolerance));
int v_min = std::max(0, static_cast<int>(hsv_avg[2] - v_tolerance));
int v_max = std::min(255, static_cast<int>(hsv_avg[2] + v_tolerance));
// トラックバーに値をセット
cv::setTrackbarPos("H-min", "WindowName", h_min);
cv::setTrackbarPos("H-max", "WindowName", h_max);
cv::setTrackbarPos("S-min", "WindowName", s_min);
cv::setTrackbarPos("S-max", "WindowName", s_max);
cv::setTrackbarPos("V-min", "WindowName", v_min);
cv::setTrackbarPos("V-max", "WindowName", v_max);
}
// Elecom Webcam [UCAM-C520FEBK]
void cameraPropertySetting(crewbo::camera::UsbCamera& camera) { // カメラの調整値
camera.setCameraProperty_(cv::CAP_PROP_BRIGHTNESS, 10);
camera.setCameraProperty_(cv::CAP_PROP_CONTRAST, 20);
camera.setCameraProperty_(cv::CAP_PROP_SATURATION, 36);
camera.setCameraProperty_(cv::CAP_PROP_HUE, 0);
camera.setCameraProperty_(cv::CAP_PROP_GAMMA, 80);
}
// Elecom Webcam [UCAM-C520FEBK]
void cameraAutoAdjustment(crewbo::camera::UsbCamera& camera) {
// camera.disableAutoExposure_();
// camera.disableAutoFocus_();
// camera.disableAutoWhiteBalance_();
}
cv::Scalar calculateAverageHSV(const std::vector<cv::Scalar>& hsv_values) {
double sum_h = 0, sum_s = 0, sum_v = 0;
for (const auto& hsv : hsv_values) {
sum_h += hsv[0];
sum_s += hsv[1];
sum_v += hsv[2];
}
int count = hsv_values.size();
return cv::Scalar(
static_cast<int>(sum_h / count), static_cast<int>(sum_s / count), static_cast<int>(sum_v / count));
}
// HSVの範囲に基づいてピクセルを描画
void drawPixelsInRange(cv::Mat& image, cv::Scalar hsv_avg, cv::Scalar lower_bound, cv::Scalar upper_bound) {
cv::Mat hsv_image;
cv::cvtColor(image, hsv_image, cv::COLOR_BGR2HSV);
for (int y = 0; y < image.rows; y++) {
for (int x = 0; x < image.cols; x++) {
cv::Vec3b hsv_pixel = hsv_image.at<cv::Vec3b>(y, x);
// ピクセルのHSVが範囲内に収まるかチェック
if (hsv_pixel[0] >= lower_bound[0] && hsv_pixel[0] <= upper_bound[0] && hsv_pixel[1] >= lower_bound[1] &&
hsv_pixel[1] <= upper_bound[1] && hsv_pixel[2] >= lower_bound[2] && hsv_pixel[2] <= upper_bound[2]) {
// 範囲内のピクセルに緑色を描画
image.at<cv::Vec3b>(y, x) = cv::Vec3b(0, 255, 0); // BGR形式で緑
}
}
}
}
void updateControlWindow(
const std::string& control_window_name,
const cv::Scalar& hsv_avg,
const cv::Scalar& lower_bound,
const cv::Scalar& upper_bound) {
// 描画用マット
cv::Mat controlImage(200, 400, CV_8UC3, cv::Scalar(0, 0, 0));
// フォント設定
double font_scale = 0.5; // フォントの大きさ
int thickness = 1; // 線の太さ
int line_type = cv::LINE_AA; // アンチエイリアスを適用
// HSV Avg 表示
cv::putText(
controlImage,
"HSV Avg: " + std::to_string(static_cast<int>(hsv_avg[0])) + "," +
std::to_string(static_cast<int>(hsv_avg[1])) + "," + std::to_string(static_cast<int>(hsv_avg[2])),
cv::Point(10, 30),
cv::FONT_HERSHEY_SIMPLEX,
font_scale,
cv::Scalar(255, 255, 255),
thickness,
line_type);
// Lower Bound 表示
cv::putText(
controlImage,
"Lower Bound HSV: " + std::to_string(static_cast<int>(lower_bound[0])) + "," +
std::to_string(static_cast<int>(lower_bound[1])) + "," +
std::to_string(static_cast<int>(lower_bound[2])),
cv::Point(10, 60),
cv::FONT_HERSHEY_SIMPLEX,
font_scale,
cv::Scalar(255, 255, 255),
thickness,
line_type);
// Upper Bound 表示
cv::putText(
controlImage,
"Upper Bound HSV: " + std::to_string(static_cast<int>(upper_bound[0])) + "," +
std::to_string(static_cast<int>(upper_bound[1])) + "," +
std::to_string(static_cast<int>(upper_bound[2])),
cv::Point(10, 90),
cv::FONT_HERSHEY_SIMPLEX,
font_scale,
cv::Scalar(255, 255, 255),
thickness,
line_type);
cv::imshow(control_window_name, controlImage);
}
int main(void) {
// カメラオブジェクトを生成する。
const int image_width = 1280;
const int image_height = 720;
crewbo::camera::UsbCamera camera(0, image_width, image_height); // カメラ1の選択(どちらか1台づつ確認)
// crewbo::camera::UsbCamera camera(2, image_width, image_height); // カメラ2の選択(どちらか1台づつ確認)
cameraAutoAdjustment(camera); // カメラの自動調整機能のON/OFFを設定する
cameraPropertySetting(camera); // 先に取得しておいたカメラの調整値をセットする
cv::String pic_window_name = "color picker";
cv::namedWindow(pic_window_name);
cv::String ctl_window_name = "Control Window";
int trackbar_height = 10; // トラックバーの高さ
int num_trackbars = 6; // トラックバーの数
int window_height = num_trackbars * trackbar_height + 20;
cv::namedWindow(ctl_window_name);
cv::resizeWindow(ctl_window_name, 400, window_height);
// トラックバーをコントロールウィンドウに作成
std::vector<std::shared_ptr<Trackbar>> control_window = createTrackbars(ctl_window_name);
std::cout << "マウスでターゲットを選択し、クリックのたびにHSV平均を更新します。" << std::endl;
std::cout << "[Backspace]でクリックで追加した情報をひとつ消去します。" << std::endl;
MouseParameter mouse;
mouse.u = 0;
mouse.v = 0;
cv::setMouseCallback(pic_window_name, mouseCallback, &mouse);
std::vector<cv::Scalar> hsv_values; // HSV値を保持するリスト
cv::Scalar hsv_avg(0, 0, 0); // 平均HSV値
// クリック時のhsvトレランス設定
int h_tolerance = 10;
int s_tolerance = 20;
int v_tolerance = 20;
cv::Scalar lower_bound(0, 0, 0), upper_bound(0, 0, 0); // 判定上限、判定下限
std::cout << "q キー押下で終了します。 " << std::endl;
bool kPressed = false;
while (true) {
int key = cv::waitKey(1);
if (key == 'q') break; // 'q'が押されたら終了
cv::Mat image = camera.fetchSingleFrame_();
cv::Mat original_image = image.clone(); // データ取得用にクローンを保存
// トラックバーの現在の値を取得
int h_min = control_window[0]->getPosition();
int h_max = control_window[1]->getPosition();
int s_min = control_window[2]->getPosition();
int s_max = control_window[3]->getPosition();
int v_min = control_window[4]->getPosition();
int v_max = control_window[5]->getPosition();
// HSVの範囲をトラックバーの値で設定
lower_bound = cv::Scalar(h_min, s_min, v_min);
upper_bound = cv::Scalar(h_max, s_max, v_max);
// マウスクリックされた場所に赤のドットを描画
if (mouse.u + mouse.v > 0) {
cv::circle(image, {mouse.u, mouse.v}, 3, {0, 0, 255}, -1);
}
// HSV範囲内のピクセルに対して緑を描画
if (!hsv_values.empty()) {
drawPixelsInRange(image, hsv_avg, lower_bound, upper_bound);
}
cv::imshow(pic_window_name, image);
if (key == 'p' && !kPressed) { // キーを押されたときに情報の表示
kPressed = true;
std::cout << "Boundary settings: " << std::endl;
std::cout << "tolerance hsv: (" << h_tolerance << ", " << s_tolerance << ", " << v_tolerance << ")"
<< std::endl;
std::cout << "HSV_avg: (" << hsv_avg[0] << ", " << hsv_avg[1] << ", " << hsv_avg[2] << ")" << std::endl;
std::cout << "lower_bound hsv: (" << lower_bound[0] << ", " << lower_bound[1] << ", " << lower_bound[2]
<< ")" << std::endl;
std::cout << "upper_bound hsv: (" << upper_bound[0] << ", " << upper_bound[1] << ", " << upper_bound[2]
<< ")" << std::endl;
}
// BackSpaceキーでクリックしたデータを1つ減らす
else if (key == 8 && !kPressed && !hsv_values.empty()) { // 8はBackSpaceのキーコード
kPressed = true;
mouse.u = 0; // 緑ドットが消えるよう0,0をセット
mouse.v = 0;
hsv_values.pop_back(); // 最後のデータを削除
if (!hsv_values.empty()) {
hsv_avg = calculateAverageHSV(hsv_values); // 平均値を再計算
std::cout << "Last clicked HSV value removed." << std::endl;
std::cout << "HSV_avg: (" << hsv_avg[0] << ", " << hsv_avg[1] << ", " << hsv_avg[2] << ")" << std::endl;
} else {
std::cout << "No HSV values left after removal." << std::endl;
hsv_avg = (0, 0, 0);
}
} else if (key == -1 && kPressed) {
kPressed = false;
}
// マウスクリックでの処理
if (mouse.click) {
std::cout << "clicked" << std::endl;
mouse.click = false;
cv::Vec3b rgb_pixel = original_image.at<cv::Vec3b>(mouse.v, mouse.u);
int r = rgb_pixel[2];
int g = rgb_pixel[1];
int b = rgb_pixel[0];
std::cout << "RGB: (" << r << ", " << g << ", " << b << ")" << std::endl;
// RGBをHSVに変換
cv::Mat rgb_mat(1, 1, CV_8UC3, cv::Scalar(b, g, r));
cv::Mat hsv_mat;
cv::cvtColor(rgb_mat, hsv_mat, cv::COLOR_BGR2HSV);
cv::Vec3b hsv_pixel = hsv_mat.at<cv::Vec3b>(0, 0);
int h = hsv_pixel[0];
int s = hsv_pixel[1];
int v = hsv_pixel[2];
// HSV値をリストに追加
hsv_values.push_back(cv::Scalar(h, s, v));
std::cout << "HSV: (" << h << ", " << s << ", " << v << ")" << std::endl;
// HSV平均値を計算
hsv_avg = calculateAverageHSV(hsv_values);
std::cout << "HSV_avg: (" << hsv_avg[0] << ", " << hsv_avg[1] << ", " << hsv_avg[2] << ")" << std::endl;
// 更新したHSV平均値を中心に検出ウインドウを設定
int h_min = std::max(0, static_cast<int>(hsv_avg[0] - h_tolerance));
int h_max = std::min(179, static_cast<int>(hsv_avg[0] + h_tolerance));
int s_min = std::max(0, static_cast<int>(hsv_avg[1] - s_tolerance));
int s_max = std::min(255, static_cast<int>(hsv_avg[1] + s_tolerance));
int v_min = std::max(0, static_cast<int>(hsv_avg[2] - v_tolerance));
int v_max = std::min(255, static_cast<int>(hsv_avg[2] + v_tolerance));
// トラックバーに値をセット
cv::setTrackbarPos("H-min", ctl_window_name, h_min);
cv::setTrackbarPos("H-max", ctl_window_name, h_max);
cv::setTrackbarPos("S-min", ctl_window_name, s_min);
cv::setTrackbarPos("S-max", ctl_window_name, s_max);
cv::setTrackbarPos("V-min", ctl_window_name, v_min);
cv::setTrackbarPos("V-max", ctl_window_name, v_max);
}
updateControlWindow(ctl_window_name, hsv_avg, lower_bound, upper_bound);
}
return 0;
}
さいごに
このようにカラーピッカーを作って画像からターゲットの色情報を元に検出するためのツールのようなスタイルで作ってみましたが、そもそも、この設定値を取得する動機になったのがCrewboのキットに付属していたワークのパイプが、私の実験室の環境ではどうにも上手く安定して検出できなかった事によります。
実際に付属していたパイプはエンジ色に近い感じで、カラーピッカーを使って検出した場合でも背景の物体も一緒に検出してしまい、上手く分離ができませんでした。
今回のカラーピッカーでは、こういう状況も視覚的に確認ができますので改善が進むかと思います。実際の環境では、このパイプを3Dプリンターで彩度の高いオレンジ色で作り直した上で、照明にも工夫をすることで安定した検出ができるようになりました。
ロボットを導入する環境にもいろいろと異なる条件がありますので、それが問題となることも多くあります。このように状況を視覚化することで問題を絞り込んで対策につなげることができました。この事例が何かの参考になれば嬉しいです。