MATLABを使ってみませんか?

第2回:カメラからの画像取得と画像解析

設備・情報技術室 AI・メカトロニクス班
木庭 洋介

はじめに

皆様はMATLABというソフトウェアをご存知でしょうか。MATLABはアルゴリズム開発、データ解析、可視化、数値計算のための統合開発環境です。様々な分野に対応したToolbox(アドオン製品)があり、これらを利用することで、複雑な処理を簡潔なコードで実現できます。

> MathWorks社 MATLAB紹介ページ

九州大学情報統括本部は、「MATLAB Campus-Wide License(包括ライセンス契約)」の運用を2023年1月4日から開始しました。これにより、MATLAB/Simulinkおよび100以上のToolbox(アドオン製品)の他オンラインツールやサービスを、教職員は有償ですが、学生は無償(!!)で利用できるようになりました。ここでは、「MATLABでこういう事ができます」ということをご紹介していきたいと思います。

第2回のテーマは「カメラからの画像取得と画像解析」です。PCに接続したカメラをキャリブレーションし、それを用いて10円玉の直径を測定します。カメラは、手元にあったオートフォーカスWEBカメラ(SANWA SUPPLY製 CMS-V45S)を使用することにします。この例では、WEBカメラとの接続にMATLAB Support Package for USB Webcams、画像の取得とその解析にImage Processing ToolboxImage Acquisition ToolboxComputer Vision Toolboxが必要です。MATLABはWEBカメラ以外にも様々なカメラとの接続に対応しています。以下のリンク先にはImage Acquisition Toolboxがサポートしているハードウェアが紹介されていますので、ご参考にしてください。

> Hardware Support from Image Acquisition Toolbox

ところで、できるだけ正確に測定を行うためには、キャリブレーションを行う面と測定する10円玉を置く面はほぼ同じにしないといけません。そこで、以下のようなカメラ固定台を作製しました。カメラから200mm離れた白い板上でキャリブレーションと10円玉の測定を行います。

カメラから画像を取得する方法

まず、webcamlist関数でPCに接続しているカメラのリストを取得します。今回使用するWEBカメラ(SANWA SUPPLY製 CMS-V45S)は”FULL HD webcam”という名前で登録されているようです。もし、複数台のカメラが接続されているのであれば、それぞれの名前を値に持つcell配列が出力されます。

camlist = webcamlist
camlist =

  1×1 の cell 配列

    {'FULL HD webcam'}

次に、webcam関数で使用したいカメラのオブジェクトを作成します。入力引数に先ほど取得したカメラ名’FULL HD webcam’を設定します。オブジェクトにはカメラ名、設定可能な解像度、現在の解像度など、様々なパラメータが含まれています。これらのパラメータはカメラによって異なります。

cam = webcam('FULL HD webcam')
cam = 

  webcam のプロパティ:

                     Name: 'FULL HD webcam'
     AvailableResolutions: {'640x480'  '320x240'  '1280x720'  '1920x1080'  '1280x960'  '800x600'}
               Resolution: '640x480'
               Saturation: 40
                 Contrast: 35
                    Focus: 0
             WhiteBalance: 4600
                    Gamma: 140
             ExposureMode: 'auto'
    BacklightCompensation: 65
                 Exposure: -4
                     Iris: 0
                FocusMode: 'auto'
               Brightness: 136
         WhiteBalanceMode: 'auto'
                Sharpness: 5
                      Hue: -600

パラメータを変更したい場合は、オブジェクトの内容を上書きします。例えば、解像度を’1280×960’、フォーカス値を’70’に変更したい場合は以下のようにします。

cam.Resolution = '1280×960'
cam.FocusMode = 'manual'
cam.Focus = 70

それでは、snapshot関数で画像を取得してみます。入力引数に与えたカメラオブジェクトから画像を取得し、960×1280×3のuint8型の配列imgとして出力します。

img = snapshot(cam);

imwrite関数で画像ファイルとして保存できます。

 imwrite(img,"image.png")

カメラキャリブレーション

カメラで取得した画像を用いてなにかを測定する場合、そのカメラのレンズ歪みや2次元イメージ座標と実空間座標の関係を求める必要があります。それらのパラメーターを推定する行為をキャリブレーションと呼びます。

> カメラキャリブレーションとは

MATLABにはカメラキャリブレーションのための関数やアプリが用意されています。今回は関数を用いてカメラパラメーターを推定します。アプリについては以下に紹介されていますので、ご参考にしてください。

> 単一カメラ キャリブレーター アプリの使用

カメラキャリブレーションは一般的に、白黒の正方形が並んだ非対称のチェッカーボードの角度を変えながら撮影した画像を使用します。正確なキャリブレーションを行うには10~20枚の画像を使用することが推奨されています。

キャリブレーション画像の準備

snapshot関数とimwrite関数で、チェッカーボードの角度を変えながら撮影し、image1.png、image2.png、…として保存します。下図はキャリブレーション画像の1例です。

次に、キャリブレーションに使用する画像ファイルのフルパスを代入したcell配列を作ります。

numImages = 20; % キャリブレーション画像の枚数
files = cell(1, numImages);
for i = 1:numImages
    % ファイルのフルパスをcell配列に代入
    files{i} = fullfile(pwd, sprintf('image%d.png', i));
end

カメラパラメータの推定

detectCheckerboardPoints関数で各画像のチェッカーボードを検出します。検出されたチェッカーボードの正方形の各頂点の2次元イメージ座標(imgagePoints)と正方形の行数、列数(boradSize)が出力されます。

[imagePoints, boardSize] = detectCheckerboardPoints(files);

次に、generateCheckerboardPoints関数にboardSizeと正方形の1辺の長さ(squareSize)を与え、正方形の各頂点の座標を作成します。

squareSize = 10.0; % 正方形の1辺の長さ(mm)
worldPoints = generateCheckerboardPoints(boardSize, squareSize);

それでは、estimateCameraParameters関数でカメラパラメータを推定してみましょう。入力引数として、先ほど作った座標のimagePoints、worldPointsと画像サイズ(imageSize)を与えます。他にも設定できるオプションがあるのですが、それらは規定値のままとします。出力のcameraParamsはカメラのパラメーター、レンズ歪みパラメーター、推定精度などが含まれるオブジェクトです。

imageSize = [size(I, 1), size(I, 2)];
cameraParams = estimateCameraParameters(imagePoints, worldPoints, ...
                                     ImageSize = imageSize);

推定されたパラメーターの精度を評価する関数も用意されており、これらを用いてキャリブレーションがうまくできたかどうかが評価ができます。望ましい精度が得られていない場合は、パラメーター推定時のオプションの変更やキャリブレーション画像の追加、精度を悪くしている原因の画像の削除などを試しながらパラメーター推定を繰り返します。

> 単一カメラのキャリブレーションの精度の評価

10円玉の検出と直径の測定

求めたカメラパラメータを用いて、画像の中の10円玉の直径を測定します。今回使用する画像は以下のものです。

画像の歪みの補正

まず、読み込んだ画像のレンズ歪みを補正します。undistortImage関数に、補正したい画像imOrigとカメラパラメータオブジェクトcameraParamsを与えます。OutputViewは出力イメージのサイズを指定するオプションです。補正後の画像(im)とその原点(newOrigin)が出力されます。

imOrig = imread(fullfile(pwd, "coins.png")); % 画像の読み込み
[im, newOrigin] = undistortImage(imOrig, cameraParams, OutputView = "same");

画像の2値化

次に、10円玉を検出するために画像を2値化するのですが、読み込んだ画像(RGBイメージ)をそのままグレースケール化して2値化処理を行うと、フレームなどの暗い部分も拾ってしまいそうです。なので、色相(H)、彩度(S)、明度(V)イメージに変換し、彩度の値を用いて2値化処理を行います。

imHSV = rgb2hsv(im); % hsvイメージに変換
saturation = imHSV(:, :, 2); % 彩度の値だけ取り出す

彩度の値を画像で表示したものを以下に示します。ほぼ10円玉だけが高い値を取っていることがわかります。

彩度の値から大津法を用いて2値化の閾値を求め(graythresh)、2値化画像を作成します。閾値より高い彩度をもつ要素は白色で表示されます。

t = graythresh(saturation);
imCoin = (saturation > t);

10円玉の検出と直径の測定

上の2値化画像を見ると、大きな白色の連結要素が10円玉に対応すると仮定できそうです。ブロブ解析で連結要素を検出してみましょう。まず、vision.BlobAnalysis関数でブロブ解析オブジェクトを作成します。このとき、連結要素の面積(AreaOutputPort)、連結要素の境界ボックスの座標(BoundingBoxOutputPort)を出力するように設定し、検出する連結要素の最小面積(MinimumBlobArea)は10,000ピクセルにします。作成したオブジェクトに画像を与えると、検出した連結要素の面積(areas)と連結要素を囲む境界ボックスの座標(boxes)が出力されます。

blobAnalysis = vision.BlobAnalysis(AreaOutputPort = true,...
    CentroidOutputPort = false,...
    BoundingBoxOutputPort = true,...
    MinimumBlobArea = 10000);
[areas, boxes] = blobAnalysis(imCoin);

最後に10円玉の直径を測定しますが、これは境界ボックスの1辺の長さと等しいと仮定します。連結要素の境界ボックスの左上、右上の頂点の2次元イメージ座標を実空間座標へ変換し、その距離を求めます。

NumofCoins = size(areas,1); % 検出した連結要素数をカウント

% カメラパラメータの読み込み
camIntrinsics = cameraParams.Intrinsics;
camExtrinsics = cameraParams.PatternExtrinsics(1);

imagePoints = cell(NumofCoins,1);
worldPoints = cell(NumofCoins,1);
diameterInMillimeters = cell(NumofCoins,1);

boxes = double(boxes);
for i = 1:NumofCoins
    % 境界ボックスの左上、右上の頂点の2次元イメージ座標の計算
    imagePoints{i} = [boxes(i,1:2); ...
        boxes(i,1) + boxes(i,3), boxes(i,2)];

    % 2次元イメージ座標から実空間座標へ変換
    worldPoints{i} = img2world2d(imagePoints{i}, camExtrinsics, camIntrinsics);

    % 2点間のユークリッド距離を計算
    d = worldPoints{i}(2, :) - worldPoints{i}(1, :);
    diameterInMillimeters{i} = hypot(d(1), d(2));
end

境界ボックスごとに求めた長さを表示するラベルを作成し、図示すると以下のようになりました。すべての10円玉の検出がうまくいっています。ただし、直径の実測値は23.45mmでしたので、1%ほど小さく測定されています。試しに下図を求めたときよりも閾値を下げて再計算したところ、2値化画像の白色の領域が若干大きくなり、結果10円玉の直径も大きくなりました。このことから、連結要素を求めるために使用する、2値化画像を作成するときの閾値の設定が精度に影響するということがわかります。2値化画像を作成するときの最適な閾値をどのように決定するか、また2値化に使用するデータは彩度の値でよいのかといった検討が必要です。

% 各連結要素ごとのラベルを作成
label_str = cell(NumofCoins,1);
for i = 1:NumofCoins
    label_str{i} = ['Coin: ' num2str(diameterInMillimeters{i},'%0.2f') 'mm'];
end

% 測定結果の表示
im = insertObjectAnnotation(im, "rectangle", boxes, label_str ,FontSize=36);
figure; imshow(im);
title("Detected Coins", "FontSize",30);

おわりに

今回はカメラからの画像取得と画像処理を用いた長さ測定についてご紹介しました。snapshot関数で画像を取得してそれを解析するという一連の処理を、whileループやtimer関数などを用いて連続的に行えば、リアルタイムでの動画解析もできそうです。

最後までお読みいただきありがとうございました。本記事ではご理解できない点が多々あるかと思いますので、ご遠慮なく技術部までご相談いただきますようよろしくお願い申し上げます。