LoginSignup
19
7

【MATLAB】プロットにサクッと拡大図を追加する ZoomPlot

Last updated at Posted at 2021-12-25

はじめに

拡大図をちゃちゃっと挿入することが可能になる ZoomPlot 関数(by Kepeng Qiu さん)の紹介です。

image_0.png

使い方はベースのプロットを描いた後に

Code(Display)
zp = BaseZoom();
zp.plot;

の 2 行!

個人的に今年一番感動した File Exchange 関数だったので、これがどうやって実装されているのか、内部の処理を簡単に紹介します。

現バージョンでは zp.plot でなく zp.run; で起動するよう変更されていますので要注意(2024/2/26 追記)

インストール方法:アドオンの入手

MATLAB の File Exchange で 公開されている(ZoomPlot)のでそちらからコードをダウンロードするのもよいですが、おススメは「アドオンの入手」からのインストール。

image_1.png

「アドオンの入手」をクリックすると「アドオンエクスプローラー」が開きますので、「zoomPlot」で検索して「追加」で OK です!

パスの設定などもよしなにやってくれるのでお手軽です。

image_2.png

必要な環境

バージョンは R2014b 以降であれば OK。

拡大領域を選択する UI のために Image Processing Toolbox の drawrectangle 関数(R2018a 以前だと imrect 関数)を使っているので、Image Processing Toolbox が必要な点にご注意くださいませ。

コード解説

コードを開くと BaseZoom クラスが 500 行程度で作られていることが分かります。R2018a 以前のバージョン対応のために、バージョンによって drawrectangle 関数か imrect 関数を使うかに分けていて、丁寧に作り込まれています。

Code
version_ = version('-release')
Output
version_ = '2021b'

こんな感じで version 関数で使われている環境(バージョン)が確認されています。

拡大図はどうやって挿入されている?

一番重要な部分がこれ。ですが、大変シンプルです。

  1. 新しい axes オブジェクトを作成
  2. 元の axes オブジェクトから要素をコピー
  3. 拡大したい部分だけ表示
  4. 拡大した部分に長方形を表示

の流れです。元の axes オブジェクトの Children をマルっとコピーしちゃっています。

オブジェクト操作に慣れている方であればイメージがわくかもしれませんが、一部要素だけ拡大したい場合はここの検索条件を Tag などを使って指定すればいいですね。

拡大図作成操作部分をマネするとこんな感じ。

Code
figure
fplot(@sin); % 例えばこんなプロット

% 元 axes のオブジェクトハンドル
h_original = gca; 
% 新しい axes 作成 [x,y,width,height]
h_zoom = axes('Position',[0.3,0.6,0.2,0.2]);
% 元 axes からプロットコピー
copyobj(get(h_original, 'children'), h_zoom);
% 拡大したい部分を指定
xlim(h_zoom,[-2,-1]);
ylim(h_zoom,[-1,-0.8]);
% 拡大した部分に長方形表示 [x,y,width,height]
rectangle(h_original,'Position',[-2,-1,1,0.2],...
    'EdgeColor','red',...
    'LineWidth',2);

figure_0.png

後は新しい軸(拡大図)と拡大した部分(赤枠)の頂点を線で結べば完了です。とはいえ、実際実装しようとするとヤヤコシイですね。位置関係によってどの2つの頂点をそれぞれから選ぶかが問題。この辺は connectAxesAndBox メソッドで定義されていますが、そこで呼ばれている getLineDirection メソッドで長方形との位置関係によって丁寧に(地道に)条件分岐しています。

マニュアルで指定する機能が欲しい・・

この例では拡大箇所・拡大図を置く場所をマニュアルで指定しています。以前(v1.1)ではできましたが、現時点(v1.2.1)の ZoomPlot ではマニュアル指定ができません。

そんな時は File Exchange のページの「View Version History」をクリックして古いバージョンを使うのも手です。マニュアル指定・マウスでの指定、両方使えるのが一番ですけど。

image_3.png

こんな感じで使えます(使えました)

Code(Display)
parameters = struct('axesPosition', [0.6, 0.1, 0.2, 0.4],...
                    'zoomZone', [1.5, 2.5; 0.6, 1.3],...
                    'lineDirection', [1, 2; 4, 3]);

zp = BaseZoom();
zp.plot(parameters)

これで、

  1. 新しい axes オブジェクトを作成
  2. 元の axes オブジェクトから要素をコピー
  3. 拡大したい部分だけ表示
  4. 拡大した部分に長方形を表示

の 2 と 3 ができました。

領域選択はどうやって?

領域選択は drawrectangle 関数か imrect 関数ですが、ここでは R2018b 以降推奨の drawrectangle 関数で。Image Processing Toolbox の関数であるので、本来は画像の ROI(Region of Interest)を選択する機能です。

Code
I = imread('cameraman.tif');
figure
imshow(I); 
roi = drawrectangle('Color','r')

figure_1.png

Output
roi = 
  Rectangle のプロパティ:

         Position: [115 51 59 54]
    RotationAngle: 0
      AspectRatio: 0.9153
            Label: ''
            Color: [1 0 0]
           Parent: [1x1 Axes]
          Visible: on
         Selected: 0

  すべてのプロパティ を表示

こんな感じで Position として選択した領域の情報が取れます。

この情報をもとに

  1. 新しい axes オブジェクトを作成
  2. 元の axes オブジェクトから要素をコピー
  3. 拡大したい部分だけ表示
  4. 拡大した部分に長方形を表示

の 1 と 4 を実施します。

領域選択中の動作はどうする?

この動画の後半部分、拡大部分を選択しているときに拡大図が動的に変化していることが分かります。

image_0.png

これは ROI 移動イベントのリスナーを設定することで実現します。イベントを検知すると指定されたコールバック関数を実行します。BaseZoom.m の中を見ると

Code(Display)
addlistener(obj.roi, 'MovingROI', @obj.allEventsForRectangleNew);
addlistener(obj.roi, 'ROIMoved', @obj.allEventsForRectangleNew);

の設定があります。使用可能なイベントのリストは Rectangle のイベント にありますが以下の通り。

  • DeletingROI:ROI が対話的に削除されようとしている。
  • DrawingStarted:ROI が対話的に描画されようとしている。
  • DrawingFinished:ROI が対話的に描画された。
  • MovingROI:ROI の形状または位置が対話的に変更中である。
  • ROIMoved:ROI の形状または位置が対話的に変更された。
  • ROIClicked:ROI がクリックされた。

ここでは MovingROIROIMoved が設定されており、ROI の形状の変更中・そして変更後にコールバック関数(allEventsForRectangleNew)が呼ばれる仕様であることが分かります。

試しにこちらサンプルのコードを動かすとこんな感じです。

image_5.png

ROI を変更中の位置と変更後の位置の表示が確認できますね。リスナーの設定は

Code
addlistener(roi,'MovingROI',@allevents);
addlistener(roi,'ROIMoved',@allevents);

で、コールバック関数 allevents の定義は以下の通り。

Code(Display)
function allevents(src,evt)
    evname = evt.EventName;
    switch(evname)
        case{'MovingROI'}
            disp(['ROI moving previous position: ' mat2str(evt.PreviousPosition)]);
            disp(['ROI moving current position: ' mat2str(evt.CurrentPosition)]);
        case{'ROIMoved'}
            disp(['ROI moved previous position: ' mat2str(evt.PreviousPosition)]);
            disp(['ROI moved current position: ' mat2str(evt.CurrentPosition)]);
    end
end

関数化しちゃう

他のデータで同じ位置に拡大図をつける・・そんなケースは少ないかもしれませんが、もしあるなら

image_6.png

これですね。

幸い拡大図も含んだ関数が自動生成されてますので、その関数を使えば同じプロットが再現できます。ただ今回のケースですと下のように表示範囲を指定するコードがコメントアウトされているので、ここはコメント解除する必要はありますので要注意。

いろんなデータで同じ拡大図を付けたい時は便利になるかと。

Code
% Axes の X 軸の範囲を保持するために以下のラインのコメントを解除
% xlim(axes1,[0 12.5663706143592]);
% Axes の Y 軸の範囲を保持するために以下のラインのコメントを解除
% ylim(axes1,[0 300]);

さいごに

ZoomPlot 関数は自分が作った関数でもなんでもないんですが、実装方法がエレガントだなぁ・・と感動したので勝手にざっくり解説しました。

19
7
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
19
7